diff --git a/include/villas/nodes/iec60870.hpp b/include/villas/nodes/iec60870.hpp index c5a23034a..77749b421 100644 --- a/include/villas/nodes/iec60870.hpp +++ b/include/villas/nodes/iec60870.hpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -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 checkASDU(CS101_ASDU const &asdu); - // infer appropriate DataType for SignalType - static std::optional inferForSignal(SignalType type); + struct Sample { + SignalData signal_data; + QualityDescriptor quality; + std::optional timestamp; + }; + + // lookup datatype for config name + static std::optional lookupName(char const* name, bool with_timestamp, int ioa); + // lookup datatype for numeric type + static std::optional 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 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 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 apci_t0 = std::nullopt; + std::optional apci_t1 = std::nullopt; + std::optional apci_t2 = std::nullopt; + std::optional apci_t3 = std::nullopt; + std::optional apci_k = std::nullopt; + std::optional 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 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 */ diff --git a/lib/nodes/iec60870.cpp b/lib/nodes/iec60870.cpp index 52bb59366..56379b670 100644 --- a/lib/nodes/iec60870.cpp +++ b/lib/nodes/iec60870.cpp @@ -20,6 +20,7 @@ * along with this program. If not, see . *********************************************************************************/ +#include #include #include #include @@ -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::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::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 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 scaled; - std::optional normalized; - std::optional short_float; - std::optional single_point; - std::optional 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 (signal_data[index].i & 0xFFFF); - scaled = MeasuredValueScaled_create(scaled ? *scaled : NULL,ioa,value,quality); - io = reinterpret_cast (*scaled); - } break; - case ASDUDataType::NORMALIZED: { - auto value = static_cast (signal_data[index].i & 0xFFFF); - normalized = MeasuredValueNormalized_create(normalized ? *normalized : NULL,ioa,value,quality); - io = reinterpret_cast (*normalized); - } break; - case ASDUDataType::DOUBLEPOINT: { - auto value = static_cast (signal_data[index].i & 0x3); - double_point = DoublePointInformation_create(double_point ? *double_point : NULL,ioa,value,quality); - io = reinterpret_cast (*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 (*single_point); - } break; - case ASDUDataType::SHORT: { - auto value = static_cast (signal_data[index].f); - short_float = MeasuredValueShort_create(short_float ? *short_float : NULL,ioa,value,quality); - io = reinterpret_cast (*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::checkASDU(CS101_ASDU const &asdu) const +{ + if (CS101_ASDU_getTypeID(asdu) != static_cast (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 (io); + auto value = MeasuredValueScaled_getValue(scaled); + signal_data.i = static_cast (value); + quality = MeasuredValueScaled_getQuality(scaled); + } break; + case ASDUData::NORMALIZED: { + auto normalized = reinterpret_cast (io); + auto value = MeasuredValueNormalized_getValue(normalized); + signal_data.i = static_cast (value); + quality = MeasuredValueNormalized_getQuality(normalized); + } break; + case ASDUData::DOUBLEPOINT: { + auto double_point = reinterpret_cast (io); + auto value = DoublePointInformation_getValue(double_point); + signal_data.i = static_cast (value); + quality = DoublePointInformation_getQuality(double_point); + } break; + case ASDUData::SINGLEPOINT: { + auto single_point = reinterpret_cast (io); + auto value = SinglePointInformation_getValue(single_point); + signal_data.b = static_cast (value); + quality = SinglePointInformation_getQuality(single_point); + } break; + case ASDUData::SHORT: { + auto short_value = reinterpret_cast (io); + auto value = MeasuredValueShort_getValue(short_value); + signal_data.f = static_cast (value); + quality = MeasuredValueShort_getQuality(short_value); + } break; + default: assert(!"unreachable"); + } + + std::optional time_cp56; + switch (this->type()) { + case ASDUData::SCALED_WITH_TIMESTAMP: { + auto scaled = reinterpret_cast (io); + time_cp56 = MeasuredValueScaledWithCP56Time2a_getTimestamp(scaled); + } break; + case ASDUData::NORMALIZED_WITH_TIMESTAMP: { + auto normalized = reinterpret_cast (io); + time_cp56 = MeasuredValueNormalizedWithCP56Time2a_getTimestamp(normalized); + } break; + case ASDUData::DOUBLEPOINT_WITH_TIMESTAMP: { + auto double_point = reinterpret_cast (io); + time_cp56 = DoublePointWithCP56Time2a_getTimestamp(double_point); + } break; + case ASDUData::SINGLEPOINT_WITH_TIMESTAMP: { + auto single_point = reinterpret_cast (io); + time_cp56 = SinglePointWithCP56Time2a_getTimestamp(single_point); + } break; + case ASDUData::SHORT_WITH_TIMESTAMP: { + auto short_value = reinterpret_cast (io); + time_cp56 = MeasuredValueShortWithCP56Time2a_getTimestamp(short_value); + } break; + default: time_cp56 = std::nullopt; + } + InformationObject_destroy(io); + + std::optional 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 (sample.timestamp.value().tv_sec) * 1000 + + static_cast (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 (sample.signal_data.i & 0xFFFF); + auto scaled = MeasuredValueScaled_create(NULL,this->ioa,value,sample.quality); + io = reinterpret_cast (scaled); + } break; + case ASDUData::NORMALIZED: { + auto value = static_cast (sample.signal_data.i & 0xFFFF); + auto normalized = MeasuredValueNormalized_create(NULL,this->ioa,value,sample.quality); + io = reinterpret_cast (normalized); + } break; + case ASDUData::DOUBLEPOINT: { + auto value = static_cast (sample.signal_data.i & 0x3); + auto double_point = DoublePointInformation_create(NULL,this->ioa,value,sample.quality); + io = reinterpret_cast (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 (single_point); + } break; + case ASDUData::SHORT: { + auto value = static_cast (sample.signal_data.f); + auto short_float = MeasuredValueShort_create(NULL,this->ioa,value,sample.quality); + io = reinterpret_cast (short_float); + } break; + case ASDUData::SCALED_WITH_TIMESTAMP: { + auto value = static_cast (sample.signal_data.i & 0xFFFF); + auto scaled = MeasuredValueScaledWithCP56Time2a_create(NULL,this->ioa,value,sample.quality,timestamp); + io = reinterpret_cast (scaled); + } break; + case ASDUData::NORMALIZED_WITH_TIMESTAMP: { + auto value = static_cast (sample.signal_data.i & 0xFFFF); + auto normalized = MeasuredValueNormalizedWithCP56Time2a_create(NULL,this->ioa,value,sample.quality,timestamp); + io = reinterpret_cast (normalized); + } break; + case ASDUData::DOUBLEPOINT_WITH_TIMESTAMP: { + auto value = static_cast (sample.signal_data.i & 0x3); + auto double_point = DoublePointWithCP56Time2a_create(NULL,this->ioa,value,sample.quality,timestamp); + io = reinterpret_cast (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 (single_point); + } break; + case ASDUData::SHORT_WITH_TIMESTAMP: { + auto value = static_cast (sample.signal_data.f); + auto short_float = MeasuredValueShortWithCP56Time2a_create(NULL,this->ioa,value,sample.quality,timestamp); + io = reinterpret_cast (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 (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 (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 (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 (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 (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 p; +static char name[] = "iec60870-5-104-slave"; +static char description[] = "Provide values as protocol slave"; +static NodePlugin p;