mirror of
https://git.rwth-aachen.de/acs/public/villas/node/
synced 2025-03-09 00:00:00 +01:00
initial support for iec60870-5-104-slave node
This allows sending periodic signals using the new node. The node does not yet support... ...proper logging of ASDUs. ...providing data on Global Interrogation. ...buffering setValue commands to read values which have been sent to slave. ...clockSync commands, what would the proper handling be?
This commit is contained in:
parent
adb70138c8
commit
6dc75b8408
2 changed files with 555 additions and 167 deletions
|
@ -27,6 +27,7 @@
|
|||
#include <optional>
|
||||
#include <cstdint>
|
||||
#include <ctime>
|
||||
#include <array>
|
||||
#include <villas/node/config.hpp>
|
||||
#include <villas/node.hpp>
|
||||
#include <villas/pool.hpp>
|
||||
|
@ -40,7 +41,7 @@ namespace node {
|
|||
namespace iec60870 {
|
||||
|
||||
// A supported CS101 information data type
|
||||
class ASDUDataType {
|
||||
class ASDUData {
|
||||
public:
|
||||
enum Type {
|
||||
// MeasuredValueScaled
|
||||
|
@ -75,84 +76,129 @@ public:
|
|||
SHORT_WITH_TIMESTAMP = M_ME_TF_1,
|
||||
};
|
||||
|
||||
// check if ASDU type is supported
|
||||
static std::optional<ASDUDataType> checkASDU(CS101_ASDU const &asdu);
|
||||
// infer appropriate DataType for SignalType
|
||||
static std::optional<ASDUDataType> inferForSignal(SignalType type);
|
||||
struct Sample {
|
||||
SignalData signal_data;
|
||||
QualityDescriptor quality;
|
||||
std::optional<timespec> timestamp;
|
||||
};
|
||||
|
||||
// lookup datatype for config name
|
||||
static std::optional<ASDUData> lookupName(char const* name, bool with_timestamp, int ioa);
|
||||
// lookup datatype for numeric type
|
||||
static std::optional<ASDUData> lookupType(int type, int ioa);
|
||||
|
||||
// does this data include a timestamp
|
||||
bool hasTimestamp() const;
|
||||
|
||||
// get equivalent DataType without timestamp (e.g. for general interrogation response)
|
||||
ASDUDataType withoutTimestamp() const;
|
||||
|
||||
// is DataType convertible to/from SignalType
|
||||
bool isConvertibleFromSignal(SignalType signal_type) const;
|
||||
|
||||
// the IEC104 type
|
||||
ASDUData::Type type() const;
|
||||
// the config file identifier for this type
|
||||
char const* name() const;
|
||||
// get equivalent IEC104 type without timestamp (e.g. for general interrogation response)
|
||||
ASDUData::Type typeWithoutTimestamp() const;
|
||||
// corresponding signal type
|
||||
SignalType signalType() const;
|
||||
// check if ASDU contains this data
|
||||
std::optional<ASDUData::Sample> checkASDU(CS101_ASDU const &asdu) const;
|
||||
// add SignalData to an ASDU
|
||||
void addSignalsToASDU(
|
||||
CS101_ASDU &asdu,
|
||||
int ioa,
|
||||
QualityDescriptor quality,
|
||||
SignalType signal_type,
|
||||
SignalData *signal_data,
|
||||
unsigned signal_count,
|
||||
std::optional<timespec> timestamp
|
||||
) const;
|
||||
void addSampleToASDU(CS101_ASDU &asdu, ASDUData::Sample sample) const;
|
||||
|
||||
// basic conversions and comparisons
|
||||
ASDUDataType(Type type);
|
||||
operator Type() const;
|
||||
bool operator==(ASDUDataType data_type) const;
|
||||
bool operator!=(ASDUDataType data_type) const;
|
||||
// every value in an ASDU has an associated "information object address" (ioa)
|
||||
int ioa;
|
||||
private:
|
||||
Type type;
|
||||
struct Descriptor {
|
||||
ASDUData::Type type;
|
||||
char const *name;
|
||||
bool has_timestamp;
|
||||
ASDUData::Type type_without_timestamp;
|
||||
SignalType signal_type;
|
||||
};
|
||||
|
||||
inline static std::array const descriptors {
|
||||
ASDUData::Descriptor { Type::DOUBLEPOINT, "double-point", false, Type::DOUBLEPOINT, SignalType::INTEGER },
|
||||
ASDUData::Descriptor { Type::DOUBLEPOINT_WITH_TIMESTAMP, "double-point", true, Type::DOUBLEPOINT, SignalType::INTEGER },
|
||||
ASDUData::Descriptor { Type::SINGLEPOINT, "single-point", false, Type::SINGLEPOINT, SignalType::BOOLEAN },
|
||||
ASDUData::Descriptor { Type::SINGLEPOINT_WITH_TIMESTAMP, "single-point", true, Type::SINGLEPOINT, SignalType::BOOLEAN },
|
||||
ASDUData::Descriptor { Type::SCALED, "scaled", false, Type::SCALED, SignalType::INTEGER },
|
||||
ASDUData::Descriptor { Type::SCALED_WITH_TIMESTAMP, "scaled", true, Type::SCALED, SignalType::INTEGER },
|
||||
ASDUData::Descriptor { Type::NORMALIZED, "normalized", false, Type::NORMALIZED, SignalType::INTEGER },
|
||||
ASDUData::Descriptor { Type::NORMALIZED_WITH_TIMESTAMP, "normalized", true, Type::NORMALIZED, SignalType::INTEGER },
|
||||
ASDUData::Descriptor { Type::SHORT, "short", false, Type::SHORT, SignalType::FLOAT },
|
||||
ASDUData::Descriptor { Type::SHORT_WITH_TIMESTAMP, "short", true, Type::SHORT, SignalType::FLOAT },
|
||||
};
|
||||
|
||||
ASDUData(ASDUData::Descriptor const &descriptor, int ioa);
|
||||
|
||||
// descriptor within the descriptors table above
|
||||
ASDUData::Descriptor const &descriptor;
|
||||
};
|
||||
|
||||
class TcpNode : public Node {
|
||||
class SlaveNode : public Node {
|
||||
protected:
|
||||
bool debug = true;
|
||||
|
||||
struct Server {
|
||||
// slave state
|
||||
bool created = false;
|
||||
|
||||
// config (use explicit defaults)
|
||||
std::string local_address = "0.0.0.0";
|
||||
int local_port = 2404;
|
||||
int low_priority_queue_size = 16;
|
||||
int high_priority_queue_size = 16;
|
||||
int common_address = 1;
|
||||
|
||||
// config (use lib60870 defaults if std::nullopt)
|
||||
std::optional<int> apci_t0 = std::nullopt;
|
||||
std::optional<int> apci_t1 = std::nullopt;
|
||||
std::optional<int> apci_t2 = std::nullopt;
|
||||
std::optional<int> apci_t3 = std::nullopt;
|
||||
std::optional<int> apci_k = std::nullopt;
|
||||
std::optional<int> apci_w = std::nullopt;
|
||||
|
||||
// lib60870
|
||||
CS104_Slave slave;
|
||||
CS101_AppLayerParameters asdu_app_layer_parameters;
|
||||
} server;
|
||||
|
||||
struct Output {
|
||||
// config
|
||||
bool enabled = false;
|
||||
SignalType signal_type = SignalType::INVALID;
|
||||
ASDUDataType asdu_data_type = ASDUDataType::SHORT_WITH_TIMESTAMP;
|
||||
unsigned signal_cnt = 0;
|
||||
std::vector<ASDUData> mapping = {};
|
||||
} out;
|
||||
|
||||
virtual
|
||||
int _read(struct Sample * smps[], unsigned cnt) override;
|
||||
void createSlave() noexcept;
|
||||
void destroySlave() noexcept;
|
||||
|
||||
void startSlave() noexcept(false);
|
||||
void stopSlave() noexcept;
|
||||
|
||||
void debugPrintMessage(IMasterConnection connection, uint8_t* message, int message_size, bool sent) const noexcept;
|
||||
void debugPrintConnection(IMasterConnection connection, CS104_PeerConnectionEvent event) const noexcept;
|
||||
|
||||
bool onClockSync(IMasterConnection connection, CS101_ASDU asdu, CP56Time2a new_time) const noexcept;
|
||||
bool onInterrogation(IMasterConnection connection, CS101_ASDU asdu, uint8_t _of_inter) const noexcept;
|
||||
bool onASDU(IMasterConnection connection, CS101_ASDU asdu) const noexcept;
|
||||
|
||||
virtual
|
||||
int _write(struct Sample * smps[], unsigned cnt) override;
|
||||
|
||||
public:
|
||||
TcpNode(const std::string &name = "");
|
||||
SlaveNode(const std::string &name = "");
|
||||
|
||||
virtual
|
||||
~TcpNode() override;
|
||||
/*
|
||||
~SlaveNode() override;
|
||||
|
||||
virtual
|
||||
int parse(json_t *json, const uuid_t sn_uuid) override;
|
||||
|
||||
virtual
|
||||
int start() override;
|
||||
|
||||
virtual
|
||||
int stop() override;
|
||||
|
||||
virtual
|
||||
std::string & getDetails() override;
|
||||
|
||||
virtual
|
||||
int parse(json_t *json, const uuid_t sn_uuid) override;
|
||||
*/
|
||||
// virtual
|
||||
// std::string & getDetails() override;
|
||||
};
|
||||
|
||||
} /* namespace iec60870 */
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*********************************************************************************/
|
||||
|
||||
#include <algorithm>
|
||||
#include <villas/node_compat.hpp>
|
||||
#include <villas/nodes/iec60870.hpp>
|
||||
#include <villas/utils.hpp>
|
||||
|
@ -37,167 +38,508 @@ using namespace villas::node::iec60870;
|
|||
// ASDUDataType
|
||||
// ------------------------------------------
|
||||
|
||||
bool ASDUDataType::hasTimestamp() const {
|
||||
switch (this->type) {
|
||||
case ASDUDataType::SCALED:
|
||||
case ASDUDataType::NORMALIZED:
|
||||
case ASDUDataType::SHORT:
|
||||
case ASDUDataType::SINGLEPOINT:
|
||||
case ASDUDataType::DOUBLEPOINT:
|
||||
return false;
|
||||
case ASDUDataType::SCALED_WITH_TIMESTAMP:
|
||||
case ASDUDataType::NORMALIZED_WITH_TIMESTAMP:
|
||||
case ASDUDataType::SHORT_WITH_TIMESTAMP:
|
||||
case ASDUDataType::SINGLEPOINT_WITH_TIMESTAMP:
|
||||
case ASDUDataType::DOUBLEPOINT_WITH_TIMESTAMP:
|
||||
return true;
|
||||
default: assert(!"unreachable");
|
||||
std::optional<ASDUData> ASDUData::lookupType(int type, int ioa)
|
||||
{
|
||||
auto check = [type] (Descriptor descriptor) {
|
||||
return descriptor.type == type;
|
||||
};
|
||||
auto descriptor = std::find_if(begin(descriptors), end(descriptors), check);
|
||||
if (descriptor != end(descriptors)) {
|
||||
ASDUData data { *descriptor, ioa };
|
||||
return { data };
|
||||
} else {
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
ASDUDataType ASDUDataType::withoutTimestamp() const {
|
||||
switch (this->type) {
|
||||
case ASDUDataType::SCALED:
|
||||
case ASDUDataType::SCALED_WITH_TIMESTAMP:
|
||||
return ASDUDataType::SCALED;
|
||||
|
||||
case ASDUDataType::NORMALIZED:
|
||||
case ASDUDataType::NORMALIZED_WITH_TIMESTAMP:
|
||||
return ASDUDataType::NORMALIZED;
|
||||
|
||||
case ASDUDataType::SHORT:
|
||||
case ASDUDataType::SHORT_WITH_TIMESTAMP:
|
||||
return ASDUDataType::SHORT;
|
||||
|
||||
case ASDUDataType::SINGLEPOINT:
|
||||
case ASDUDataType::SINGLEPOINT_WITH_TIMESTAMP:
|
||||
return ASDUDataType::SINGLEPOINT;
|
||||
|
||||
case ASDUDataType::DOUBLEPOINT:
|
||||
case ASDUDataType::DOUBLEPOINT_WITH_TIMESTAMP:
|
||||
return ASDUDataType::DOUBLEPOINT;
|
||||
default: assert(!"unreachable");
|
||||
std::optional<ASDUData> ASDUData::lookupName(char const *name, bool with_timestamp, int ioa)
|
||||
{
|
||||
auto check = [name,with_timestamp] (Descriptor descriptor) {
|
||||
return !strcmp(descriptor.name,name) && descriptor.has_timestamp == with_timestamp;
|
||||
};
|
||||
auto descriptor = std::find_if(begin(descriptors), end(descriptors), check);
|
||||
if (descriptor != end(descriptors)) {
|
||||
return ASDUData { *descriptor, ioa };
|
||||
} else {
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
bool ASDUDataType::isConvertibleFromSignal(SignalType signal_type) const {
|
||||
switch (this->withoutTimestamp().type) {
|
||||
case ASDUDataType::SCALED:
|
||||
case ASDUDataType::NORMALIZED:
|
||||
case ASDUDataType::DOUBLEPOINT:
|
||||
return (signal_type == SignalType::INTEGER);
|
||||
case ASDUDataType::SINGLEPOINT:
|
||||
return (signal_type == SignalType::BOOLEAN);
|
||||
case ASDUDataType::SHORT:
|
||||
return (signal_type == SignalType::FLOAT);
|
||||
default: assert(!"unreachable");
|
||||
}
|
||||
bool ASDUData::hasTimestamp() const
|
||||
{
|
||||
return this->descriptor.has_timestamp;
|
||||
}
|
||||
|
||||
void ASDUDataType::addSignalsToASDU(
|
||||
CS101_ASDU &asdu,
|
||||
int ioa,
|
||||
QualityDescriptor quality,
|
||||
SignalType signal_type,
|
||||
SignalData* signal_data,
|
||||
unsigned signal_count,
|
||||
std::optional<timespec> timestamp
|
||||
) const {
|
||||
assert(this->isConvertibleFromSignal(signal_type));
|
||||
ASDUData::Type ASDUData::type() const
|
||||
{
|
||||
return this->descriptor.type;
|
||||
}
|
||||
|
||||
// lib60870 allocates information objects internally
|
||||
// these optionals allow the allocations to be reused
|
||||
std::optional<MeasuredValueScaled> scaled;
|
||||
std::optional<MeasuredValueNormalized> normalized;
|
||||
std::optional<MeasuredValueShort> short_float;
|
||||
std::optional<SinglePointInformation> single_point;
|
||||
std::optional<DoublePointInformation> double_point;
|
||||
|
||||
// the signal types are homogenous for now
|
||||
// this may be reevaluted later
|
||||
for (unsigned index = 0; index < signal_count; index++) {
|
||||
InformationObject io;
|
||||
switch (this->type) {
|
||||
case ASDUDataType::SCALED: {
|
||||
auto value = static_cast<int16_t> (signal_data[index].i & 0xFFFF);
|
||||
scaled = MeasuredValueScaled_create(scaled ? *scaled : NULL,ioa,value,quality);
|
||||
io = reinterpret_cast<InformationObject> (*scaled);
|
||||
} break;
|
||||
case ASDUDataType::NORMALIZED: {
|
||||
auto value = static_cast<int16_t> (signal_data[index].i & 0xFFFF);
|
||||
normalized = MeasuredValueNormalized_create(normalized ? *normalized : NULL,ioa,value,quality);
|
||||
io = reinterpret_cast<InformationObject> (*normalized);
|
||||
} break;
|
||||
case ASDUDataType::DOUBLEPOINT: {
|
||||
auto value = static_cast<DoublePointValue> (signal_data[index].i & 0x3);
|
||||
double_point = DoublePointInformation_create(double_point ? *double_point : NULL,ioa,value,quality);
|
||||
io = reinterpret_cast<InformationObject> (*double_point);
|
||||
} break;
|
||||
case ASDUDataType::SINGLEPOINT: {
|
||||
auto value = signal_data[index].b;
|
||||
single_point = SinglePointInformation_create(single_point ? *single_point : NULL,ioa,value,quality);
|
||||
io = reinterpret_cast<InformationObject> (*single_point);
|
||||
} break;
|
||||
case ASDUDataType::SHORT: {
|
||||
auto value = static_cast<float> (signal_data[index].f);
|
||||
short_float = MeasuredValueShort_create(short_float ? *short_float : NULL,ioa,value,quality);
|
||||
io = reinterpret_cast<InformationObject> (*short_float);
|
||||
} break;
|
||||
default: assert(!"unreachable");
|
||||
char const * ASDUData::name() const {
|
||||
return this->descriptor.name;
|
||||
}
|
||||
|
||||
ASDUData::Type ASDUData::typeWithoutTimestamp() const
|
||||
{
|
||||
return this->descriptor.type_without_timestamp;
|
||||
}
|
||||
|
||||
SignalType ASDUData::signalType() const
|
||||
{
|
||||
return this->descriptor.signal_type;
|
||||
}
|
||||
|
||||
std::optional<ASDUData::Sample> ASDUData::checkASDU(CS101_ASDU const &asdu) const
|
||||
{
|
||||
if (CS101_ASDU_getTypeID(asdu) != static_cast<int> (this->descriptor.type)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
for (int i = 0; i < CS101_ASDU_getNumberOfElements(asdu); i++) {
|
||||
InformationObject io = CS101_ASDU_getElement(asdu, i);
|
||||
int ioa = InformationObject_getObjectAddress(io);
|
||||
|
||||
if (ioa != this->ioa) {
|
||||
InformationObject_destroy(io);
|
||||
continue;
|
||||
}
|
||||
CS101_ASDU_addInformationObject(asdu, io);
|
||||
|
||||
SignalData signal_data;
|
||||
QualityDescriptor quality;
|
||||
switch (this->typeWithoutTimestamp()) {
|
||||
case ASDUData::SCALED: {
|
||||
auto scaled = reinterpret_cast<MeasuredValueScaled> (io);
|
||||
auto value = MeasuredValueScaled_getValue(scaled);
|
||||
signal_data.i = static_cast<int64_t> (value);
|
||||
quality = MeasuredValueScaled_getQuality(scaled);
|
||||
} break;
|
||||
case ASDUData::NORMALIZED: {
|
||||
auto normalized = reinterpret_cast<MeasuredValueNormalized> (io);
|
||||
auto value = MeasuredValueNormalized_getValue(normalized);
|
||||
signal_data.i = static_cast<int64_t> (value);
|
||||
quality = MeasuredValueNormalized_getQuality(normalized);
|
||||
} break;
|
||||
case ASDUData::DOUBLEPOINT: {
|
||||
auto double_point = reinterpret_cast<DoublePointInformation> (io);
|
||||
auto value = DoublePointInformation_getValue(double_point);
|
||||
signal_data.i = static_cast<int64_t> (value);
|
||||
quality = DoublePointInformation_getQuality(double_point);
|
||||
} break;
|
||||
case ASDUData::SINGLEPOINT: {
|
||||
auto single_point = reinterpret_cast<SinglePointInformation> (io);
|
||||
auto value = SinglePointInformation_getValue(single_point);
|
||||
signal_data.b = static_cast<bool> (value);
|
||||
quality = SinglePointInformation_getQuality(single_point);
|
||||
} break;
|
||||
case ASDUData::SHORT: {
|
||||
auto short_value = reinterpret_cast<MeasuredValueShort> (io);
|
||||
auto value = MeasuredValueShort_getValue(short_value);
|
||||
signal_data.f = static_cast<double> (value);
|
||||
quality = MeasuredValueShort_getQuality(short_value);
|
||||
} break;
|
||||
default: assert(!"unreachable");
|
||||
}
|
||||
|
||||
std::optional<CP56Time2a> time_cp56;
|
||||
switch (this->type()) {
|
||||
case ASDUData::SCALED_WITH_TIMESTAMP: {
|
||||
auto scaled = reinterpret_cast<MeasuredValueScaledWithCP56Time2a> (io);
|
||||
time_cp56 = MeasuredValueScaledWithCP56Time2a_getTimestamp(scaled);
|
||||
} break;
|
||||
case ASDUData::NORMALIZED_WITH_TIMESTAMP: {
|
||||
auto normalized = reinterpret_cast<MeasuredValueNormalizedWithCP56Time2a> (io);
|
||||
time_cp56 = MeasuredValueNormalizedWithCP56Time2a_getTimestamp(normalized);
|
||||
} break;
|
||||
case ASDUData::DOUBLEPOINT_WITH_TIMESTAMP: {
|
||||
auto double_point = reinterpret_cast<DoublePointWithCP56Time2a> (io);
|
||||
time_cp56 = DoublePointWithCP56Time2a_getTimestamp(double_point);
|
||||
} break;
|
||||
case ASDUData::SINGLEPOINT_WITH_TIMESTAMP: {
|
||||
auto single_point = reinterpret_cast<SinglePointWithCP56Time2a> (io);
|
||||
time_cp56 = SinglePointWithCP56Time2a_getTimestamp(single_point);
|
||||
} break;
|
||||
case ASDUData::SHORT_WITH_TIMESTAMP: {
|
||||
auto short_value = reinterpret_cast<MeasuredValueShortWithCP56Time2a> (io);
|
||||
time_cp56 = MeasuredValueShortWithCP56Time2a_getTimestamp(short_value);
|
||||
} break;
|
||||
default: time_cp56 = std::nullopt;
|
||||
}
|
||||
|
||||
InformationObject_destroy(io);
|
||||
|
||||
std::optional<timespec> timestamp;
|
||||
if (time_cp56.has_value()) {
|
||||
auto time_ms = CP56Time2a_toMsTimestamp(*time_cp56);
|
||||
timespec time {};
|
||||
time.tv_nsec = time_ms % 1000 * 1000;
|
||||
time.tv_sec = time_ms / 1000;
|
||||
timestamp = time;
|
||||
}
|
||||
|
||||
return ASDUData::Sample { signal_data, quality, timestamp };
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
ASDUDataType::ASDUDataType(ASDUDataType::Type t) : type(t) {}
|
||||
void ASDUData::addSampleToASDU(CS101_ASDU &asdu, ASDUData::Sample sample) const
|
||||
{
|
||||
CP56Time2a timestamp;
|
||||
if (this->hasTimestamp()) {
|
||||
uint64_t orgin_time_ms =
|
||||
static_cast<uint64_t> (sample.timestamp.value().tv_sec) * 1000
|
||||
+ static_cast<uint64_t> (sample.timestamp.value().tv_nsec) / 1000000;
|
||||
timestamp = CP56Time2a_createFromMsTimestamp(NULL,orgin_time_ms);
|
||||
}
|
||||
|
||||
ASDUDataType::operator ASDUDataType::Type() const {
|
||||
return this->type;
|
||||
InformationObject io;
|
||||
switch (this->descriptor.type) {
|
||||
case ASDUData::SCALED: {
|
||||
auto value = static_cast<int16_t> (sample.signal_data.i & 0xFFFF);
|
||||
auto scaled = MeasuredValueScaled_create(NULL,this->ioa,value,sample.quality);
|
||||
io = reinterpret_cast<InformationObject> (scaled);
|
||||
} break;
|
||||
case ASDUData::NORMALIZED: {
|
||||
auto value = static_cast<int16_t> (sample.signal_data.i & 0xFFFF);
|
||||
auto normalized = MeasuredValueNormalized_create(NULL,this->ioa,value,sample.quality);
|
||||
io = reinterpret_cast<InformationObject> (normalized);
|
||||
} break;
|
||||
case ASDUData::DOUBLEPOINT: {
|
||||
auto value = static_cast<DoublePointValue> (sample.signal_data.i & 0x3);
|
||||
auto double_point = DoublePointInformation_create(NULL,this->ioa,value,sample.quality);
|
||||
io = reinterpret_cast<InformationObject> (double_point);
|
||||
} break;
|
||||
case ASDUData::SINGLEPOINT: {
|
||||
auto value = sample.signal_data.b;
|
||||
auto single_point = SinglePointInformation_create(NULL,this->ioa,value,sample.quality);
|
||||
io = reinterpret_cast<InformationObject> (single_point);
|
||||
} break;
|
||||
case ASDUData::SHORT: {
|
||||
auto value = static_cast<float> (sample.signal_data.f);
|
||||
auto short_float = MeasuredValueShort_create(NULL,this->ioa,value,sample.quality);
|
||||
io = reinterpret_cast<InformationObject> (short_float);
|
||||
} break;
|
||||
case ASDUData::SCALED_WITH_TIMESTAMP: {
|
||||
auto value = static_cast<int16_t> (sample.signal_data.i & 0xFFFF);
|
||||
auto scaled = MeasuredValueScaledWithCP56Time2a_create(NULL,this->ioa,value,sample.quality,timestamp);
|
||||
io = reinterpret_cast<InformationObject> (scaled);
|
||||
} break;
|
||||
case ASDUData::NORMALIZED_WITH_TIMESTAMP: {
|
||||
auto value = static_cast<int16_t> (sample.signal_data.i & 0xFFFF);
|
||||
auto normalized = MeasuredValueNormalizedWithCP56Time2a_create(NULL,this->ioa,value,sample.quality,timestamp);
|
||||
io = reinterpret_cast<InformationObject> (normalized);
|
||||
} break;
|
||||
case ASDUData::DOUBLEPOINT_WITH_TIMESTAMP: {
|
||||
auto value = static_cast<DoublePointValue> (sample.signal_data.i & 0x3);
|
||||
auto double_point = DoublePointWithCP56Time2a_create(NULL,this->ioa,value,sample.quality,timestamp);
|
||||
io = reinterpret_cast<InformationObject> (double_point);
|
||||
} break;
|
||||
case ASDUData::SINGLEPOINT_WITH_TIMESTAMP: {
|
||||
auto value = sample.signal_data.b;
|
||||
auto single_point = SinglePointWithCP56Time2a_create(NULL,this->ioa,value,sample.quality,timestamp);
|
||||
io = reinterpret_cast<InformationObject> (single_point);
|
||||
} break;
|
||||
case ASDUData::SHORT_WITH_TIMESTAMP: {
|
||||
auto value = static_cast<float> (sample.signal_data.f);
|
||||
auto short_float = MeasuredValueShortWithCP56Time2a_create(NULL,this->ioa,value,sample.quality,timestamp);
|
||||
io = reinterpret_cast<InformationObject> (short_float);
|
||||
} break;
|
||||
default: assert(!"unreachable");
|
||||
}
|
||||
assert(CS101_ASDU_addInformationObject(asdu, io));
|
||||
InformationObject_destroy(io);
|
||||
}
|
||||
|
||||
bool ASDUDataType::operator==(ASDUDataType v) const {
|
||||
return this->type == v.type;
|
||||
}
|
||||
|
||||
bool ASDUDataType::operator!=(ASDUDataType v) const {
|
||||
return !(*this == v);
|
||||
}
|
||||
ASDUData::ASDUData(ASDUData::Descriptor const &descriptor, int ioa) : ioa(ioa), descriptor(descriptor)
|
||||
{}
|
||||
|
||||
// ------------------------------------------
|
||||
// TcpNode
|
||||
// SlaveNode
|
||||
// ------------------------------------------
|
||||
|
||||
TcpNode::TcpNode(const std::string &name) :
|
||||
Node(name)
|
||||
{ }
|
||||
|
||||
TcpNode::~TcpNode()
|
||||
void SlaveNode::createSlave() noexcept
|
||||
{
|
||||
auto &server = this->server;
|
||||
|
||||
if (server.created) {
|
||||
if (CS104_Slave_isRunning(server.slave)) {
|
||||
CS104_Slave_stop(server.slave);
|
||||
}
|
||||
CS104_Slave_destroy(server.slave);
|
||||
// destroy slave id it was already created
|
||||
this->destroySlave();
|
||||
|
||||
// create the slave object
|
||||
server.slave = CS104_Slave_create(server.low_priority_queue_size,server.high_priority_queue_size);
|
||||
CS104_Slave_setServerMode(server.slave, CS104_MODE_SINGLE_REDUNDANCY_GROUP);
|
||||
|
||||
// configure the slave according to config
|
||||
server.asdu_app_layer_parameters = CS104_Slave_getAppLayerParameters(server.slave);
|
||||
CS104_APCIParameters apci_parameters = CS104_Slave_getConnectionParameters(server.slave);
|
||||
|
||||
if (server.apci_t0) apci_parameters->t0 = *server.apci_t0;
|
||||
if (server.apci_t1) apci_parameters->t1 = *server.apci_t1;
|
||||
if (server.apci_t2) apci_parameters->t2 = *server.apci_t2;
|
||||
if (server.apci_t3) apci_parameters->t3 = *server.apci_t3;
|
||||
if (server.apci_k) apci_parameters->k = *server.apci_k;
|
||||
if (server.apci_w) apci_parameters->w = *server.apci_w;
|
||||
|
||||
CS104_Slave_setLocalAddress(server.slave, server.local_address.c_str());
|
||||
CS104_Slave_setLocalPort(server.slave, server.local_port);
|
||||
|
||||
// setup callbacks into the class
|
||||
CS104_Slave_setClockSyncHandler(server.slave, [] (void *tcp_node, IMasterConnection connection, CS101_ASDU asdu, CP56Time2a new_time) {
|
||||
auto self = static_cast<SlaveNode const *> (tcp_node);
|
||||
return self->onClockSync(connection,asdu,new_time);
|
||||
}, this);
|
||||
|
||||
CS104_Slave_setInterrogationHandler(server.slave, [] (void *tcp_node, IMasterConnection connection, CS101_ASDU asdu, uint8_t qoi) {
|
||||
auto self = static_cast<SlaveNode const *> (tcp_node);
|
||||
return self->onInterrogation(connection,asdu,qoi);
|
||||
}, this);
|
||||
|
||||
CS104_Slave_setASDUHandler(server.slave, [] (void *tcp_node, IMasterConnection connection, CS101_ASDU asdu) {
|
||||
auto self = static_cast<SlaveNode const *> (tcp_node);
|
||||
return self->onASDU(connection,asdu);
|
||||
}, this);
|
||||
|
||||
// debug print callbacks
|
||||
if (this->debug) {
|
||||
CS104_Slave_setConnectionEventHandler(server.slave, [](void *tcp_node, IMasterConnection connection, CS104_PeerConnectionEvent event){
|
||||
auto self = static_cast<SlaveNode const *> (tcp_node);
|
||||
self->debugPrintConnection(connection,event);
|
||||
}, this);
|
||||
|
||||
CS104_Slave_setRawMessageHandler(server.slave, [](void *tcp_node, IMasterConnection connection, uint8_t *message, int message_size, bool sent){
|
||||
auto self = static_cast<SlaveNode const *> (tcp_node);
|
||||
self->debugPrintMessage(connection,message,message_size,sent);
|
||||
}, this);
|
||||
}
|
||||
|
||||
server.created = true;
|
||||
}
|
||||
|
||||
void SlaveNode::destroySlave() noexcept
|
||||
{
|
||||
auto &server = this->server;
|
||||
|
||||
if (!server.created) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (CS104_Slave_isRunning(server.slave)) {
|
||||
CS104_Slave_stop(server.slave);
|
||||
}
|
||||
|
||||
CS104_Slave_destroy(server.slave);
|
||||
|
||||
server.created = false;
|
||||
}
|
||||
|
||||
void SlaveNode::startSlave() noexcept(false)
|
||||
{
|
||||
auto &server = this->server;
|
||||
|
||||
if (!server.created) {
|
||||
this->createSlave();
|
||||
} else {
|
||||
this->stopSlave();
|
||||
}
|
||||
|
||||
CS104_Slave_start(server.slave);
|
||||
|
||||
if (!CS104_Slave_isRunning(server.slave)) {
|
||||
throw std::runtime_error{"iec60870-5-104 server could not be started"};
|
||||
}
|
||||
}
|
||||
|
||||
int TcpNode::_read(struct Sample *smps[], unsigned cnt)
|
||||
void SlaveNode::stopSlave() noexcept
|
||||
{
|
||||
auto &server = this->server;
|
||||
|
||||
if (!server.created) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (CS104_Slave_isRunning(server.slave)) {
|
||||
CS104_Slave_stop(server.slave);
|
||||
}
|
||||
}
|
||||
|
||||
void SlaveNode::debugPrintMessage(IMasterConnection connection, uint8_t* message, int message_size, bool sent) const noexcept
|
||||
{
|
||||
// ToDo: debug-print a message
|
||||
}
|
||||
|
||||
void SlaveNode::debugPrintConnection(IMasterConnection connection, CS104_PeerConnectionEvent event) const noexcept
|
||||
{
|
||||
// ToDo: debug-print a message
|
||||
}
|
||||
|
||||
bool SlaveNode::onClockSync(IMasterConnection connection, CS101_ASDU asdu, CP56Time2a new_time) const noexcept
|
||||
{
|
||||
// ignore clock sync for now
|
||||
|
||||
// ToDo: check if deviation of new_time from system clock is acceptable
|
||||
// or manage internal offset from systemtime for IEC104
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SlaveNode::onInterrogation(IMasterConnection connection, CS101_ASDU asdu, uint8_t qoi) const noexcept
|
||||
{
|
||||
// ToDo: send last/default data on interrogation?
|
||||
// this should also allow a connection mode where a client pulls data from an internal queue in villas node
|
||||
// instead of villas node writing directly using a periodic cot message to all clients
|
||||
|
||||
// send negative acknowledgement
|
||||
IMasterConnection_sendACT_CON(connection, asdu, true);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SlaveNode::onASDU(IMasterConnection connection, CS101_ASDU asdu) const noexcept
|
||||
{
|
||||
// ToDo: handle some commands, e.g. test (see flege power iec104 south bridge)
|
||||
|
||||
// ignore commands
|
||||
return true;
|
||||
}
|
||||
|
||||
int SlaveNode::_write(Sample *samples[], unsigned sample_count)
|
||||
{
|
||||
for (unsigned sample_index = 0; sample_index < sample_count; sample_index++) {
|
||||
Sample const *sample = samples[sample_index];
|
||||
|
||||
CS101_ASDU asdu = CS101_ASDU_create(
|
||||
this->server.asdu_app_layer_parameters,
|
||||
0,
|
||||
CS101_COT_PERIODIC,
|
||||
0,
|
||||
this->server.common_address,
|
||||
false,
|
||||
false
|
||||
);
|
||||
|
||||
auto &mapping = this->out.mapping;
|
||||
for (unsigned signal = 0; signal < MIN(sample->length, mapping.size()); signal++) {
|
||||
auto timestamp = (sample->flags & (int) SampleFlags::HAS_TS_ORIGIN)
|
||||
? std::optional{ sample->ts.origin }
|
||||
: std::nullopt;
|
||||
|
||||
if (mapping[signal].hasTimestamp() && !timestamp.has_value())
|
||||
throw RuntimeError("Received sample without timestamp for ASDU type with mandatory timestamp");
|
||||
|
||||
if (mapping[signal].signalType() != sample_format(sample,signal))
|
||||
throw RuntimeError("Expected signal type {}, but received {}",
|
||||
signalTypeToString(mapping[signal].signalType()),
|
||||
signalTypeToString(sample_format(sample,signal))
|
||||
);
|
||||
|
||||
mapping[signal].addSampleToASDU(
|
||||
asdu,
|
||||
ASDUData::Sample { sample->data[signal], IEC60870_QUALITY_GOOD, timestamp }
|
||||
);
|
||||
}
|
||||
|
||||
CS104_Slave_enqueueASDU(this->server.slave, asdu);
|
||||
CS101_ASDU_destroy(asdu);
|
||||
}
|
||||
return sample_count;
|
||||
}
|
||||
|
||||
SlaveNode::SlaveNode(const std::string &name) :
|
||||
Node(name)
|
||||
{
|
||||
}
|
||||
|
||||
SlaveNode::~SlaveNode()
|
||||
{
|
||||
this->destroySlave();
|
||||
}
|
||||
|
||||
int SlaveNode::parse(json_t *json, const uuid_t sn_uuid)
|
||||
{
|
||||
json_error_t err;
|
||||
|
||||
if (Node::parse(json,sn_uuid))
|
||||
throw ConfigError(json, err, "node-config-node-iec60870-5-104-slave");
|
||||
|
||||
json_t *out_json = nullptr;
|
||||
char const *address = nullptr;
|
||||
if(json_unpack_ex(json, &err, 0, "{ s?: o, s?: s, s?: i, s?: i }",
|
||||
"out", &out_json,
|
||||
"address", &address,
|
||||
"port", &this->server.local_port,
|
||||
"ca", &this->server.common_address
|
||||
))
|
||||
throw ConfigError(json, err, "node-config-node-iec60870-5-104-slave");
|
||||
|
||||
if (address)
|
||||
this->server.local_address = address;
|
||||
|
||||
json_t *signals_json = nullptr;
|
||||
if (out_json) {
|
||||
this->out.enabled = true;
|
||||
if(json_unpack_ex(out_json, &err, 0, "{ s: o }",
|
||||
"signals", &signals_json
|
||||
))
|
||||
throw ConfigError(out_json, err, "node-config-node-iec60870-5-104-slave");
|
||||
|
||||
}
|
||||
|
||||
auto parse_asdu_data = [&err](json_t *signal_json) -> ASDUData {
|
||||
char const *asdu_type_name = nullptr;
|
||||
int with_timestamp = false;
|
||||
int ioa = 0;
|
||||
if (json_unpack_ex(signal_json, &err, 0, "{ s: s, s: b, s: i }",
|
||||
"asdu_type", &asdu_type_name,
|
||||
"with_timestamp", &with_timestamp,
|
||||
"ioa", &ioa
|
||||
))
|
||||
throw ConfigError(signal_json, err, "node-config-node-iec60870-5-104-slave");
|
||||
if (ioa == 0)
|
||||
throw RuntimeError("invalid ioa {}", ioa);
|
||||
auto asdu_data = ASDUData::lookupName(asdu_type_name,with_timestamp,ioa);
|
||||
if (!asdu_data.has_value())
|
||||
throw RuntimeError("invalid asdu_type {}", asdu_type_name);
|
||||
return *asdu_data;
|
||||
};
|
||||
|
||||
auto &mapping = this->out.mapping;
|
||||
auto signals = this->getOutputSignals();
|
||||
if (signals_json) {
|
||||
json_t *signal_json;
|
||||
size_t i;
|
||||
json_array_foreach(signals_json, i, signal_json) {
|
||||
auto signal = signals ? signals->getByIndex(i) : Signal::Ptr{};
|
||||
auto asdu_data = parse_asdu_data(signal_json);
|
||||
if (signal && signal->type != asdu_data.signalType()) {
|
||||
throw RuntimeError("Type mismatch! Expected type {} for signal {}, but found {}",
|
||||
signalTypeToString(asdu_data.signalType()),
|
||||
signal->name,
|
||||
signalTypeToString(signal->type)
|
||||
);
|
||||
}
|
||||
mapping.push_back(asdu_data);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int TcpNode::_write(struct Sample *smps[], unsigned cnt)
|
||||
int SlaveNode::start()
|
||||
{
|
||||
return 0;
|
||||
this->startSlave();
|
||||
return Node::start();
|
||||
}
|
||||
|
||||
int SlaveNode::stop()
|
||||
{
|
||||
this->stopSlave();
|
||||
return Node::stop();
|
||||
}
|
||||
|
||||
// ------------------------------------------
|
||||
// Plugin
|
||||
// ------------------------------------------
|
||||
|
||||
static char name[] = "IEC60870-5-104";
|
||||
static char description[] = "Provide monitoring values over TCP/IP";
|
||||
static NodePlugin<TcpNode, name, description, (int) NodeFactory::Flags::HIDDEN> p;
|
||||
static char name[] = "iec60870-5-104-slave";
|
||||
static char description[] = "Provide values as protocol slave";
|
||||
static NodePlugin<SlaveNode, name, description, (int) NodeFactory::Flags::SUPPORTS_WRITE | (int) NodeFactory::Flags::SUPPORTS_READ> p;
|
||||
|
|
Loading…
Add table
Reference in a new issue