diff --git a/etc/examples/nodes/iec60870-5-104-sequence.conf b/etc/examples/nodes/iec60870-5-104-sequence.conf new file mode 100644 index 000000000..b83f925a2 --- /dev/null +++ b/etc/examples/nodes/iec60870-5-104-sequence.conf @@ -0,0 +1,46 @@ + +nodes = { + iec104 = { + type = "iec60870-5-104" + address = "0.0.0.0" + port = 2404 + ca = 1 + out = { + # create a sequence of 5 floats with identical IOAs + signals = { + asdu_type_id = "M_ME_NA_1" + ioa = 4202852 + count = 5 + } + # interpret the duplicate IOAs as a sequence by incrementing the + # IOA with each duplication. This only applies to two adjacent + # signals with the same IOA. Specifying an IOA twice with other + # IOAs inbetween is an error. + duplicate_ioa_is_sequence = true + } + } + signal = { + type = "signal.v2" + rate = 1 + in = { + signals = { + name = "sine1" + signal = "sine" + frequency = 0.1 + count = 5 + } + } + } +} + +paths = ( + { + in = "signal" + out = "iec104" + hooks = ( + { + type = "print" + } + ) + } +) diff --git a/include/villas/nodes/iec60870.hpp b/include/villas/nodes/iec60870.hpp index 98cd4c243..528a0b039 100644 --- a/include/villas/nodes/iec60870.hpp +++ b/include/villas/nodes/iec60870.hpp @@ -73,13 +73,7 @@ public: }; // parse the config json - static ASDUData parse(json_t *signal_json, std::optional last_ioa); - // lookup datatype for config key asdu_type - static std::optional lookupName(char const *name, bool with_timestamp, int ioa); - // lookup datatype for config key asdu_type_id - static std::optional lookupTypeId(char const *type_id, int ioa); - // lookup datatype for numeric type identifier - static std::optional lookupType(int type, int ioa); + static ASDUData parse(json_t *signal_json, std::optional last_data, bool duplicate_ioa_is_sequence); // does this data include a timestamp bool hasTimestamp() const; @@ -100,6 +94,8 @@ public: // every value in an ASDU has an associated "information object address" (ioa) int ioa; + // start of the ioa sequence + int ioa_sequence_start; private: struct Descriptor { ASDUData::Type type; @@ -123,10 +119,17 @@ private: ASDUData::Descriptor { Type::SHORT_FLOAT_WITH_TIMESTAMP, "short-float", "M_ME_TC_1", true, Type::SHORT_FLOAT, SignalType::FLOAT }, }; - ASDUData(ASDUData::Descriptor const &descriptor, int ioa); + ASDUData(ASDUData::Descriptor const *descriptor, int ioa, int ioa_sequence_start); + + // lookup datatype for config key asdu_type + static std::optional lookupName(char const *name, bool with_timestamp, int ioa, int ioa_sequence_start); + // lookup datatype for config key asdu_type_id + static std::optional lookupTypeId(char const *type_id, int ioa, int ioa_sequence_start); + // lookup datatype for numeric type identifier + static std::optional lookupType(int type, int ioa, int ioa_sequence_start); // descriptor within the descriptors table above - ASDUData::Descriptor const &descriptor; + ASDUData::Descriptor const *descriptor; }; class SlaveNode : public Node { diff --git a/lib/nodes/iec60870.cpp b/lib/nodes/iec60870.cpp index 8482f3ba4..3faf13c01 100644 --- a/lib/nodes/iec60870.cpp +++ b/lib/nodes/iec60870.cpp @@ -50,14 +50,15 @@ timespec cp56time2a_to_timespec(CP56Time2a cp56time2a) { return time; } -ASDUData ASDUData::parse(json_t *signal_json, std::optional last_ioa) { +ASDUData ASDUData::parse(json_t *signal_json, std::optional last_data, bool duplicate_ioa_is_sequence) { json_error_t err; char const *asdu_type_name = nullptr; int with_timestamp = -1; char const *asdu_type_id = nullptr; + std::optional ioa_sequence_start = std::nullopt; int ioa = -1; - if (json_unpack_ex(signal_json, &err, 0, "{ s?: s, s?: b, s?: s, s?: i }", + if (json_unpack_ex(signal_json, &err, 0, "{ s?: s, s?: b, s?: s, s: i }", "asdu_type", &asdu_type_name, "with_timestamp", &with_timestamp, "asdu_type_id", &asdu_type_id, @@ -67,11 +68,12 @@ ASDUData ASDUData::parse(json_t *signal_json, std::optional last_ioa) { with_timestamp = with_timestamp != -1 ? with_timestamp != 0 : false; - if (ioa == -1) { - if (last_ioa) - ioa = *last_ioa; - else - throw RuntimeError("Missing ioa for signal", ioa); + // increase the ioa if it is found twice to make it a sequence + if ( duplicate_ioa_is_sequence && + last_data && + ioa == last_data->ioa_sequence_start) { + ioa = last_data->ioa + 1; + ioa_sequence_start = last_data->ioa_sequence_start; } if ( (asdu_type_name && asdu_type_id) || @@ -79,8 +81,8 @@ ASDUData ASDUData::parse(json_t *signal_json, std::optional last_ioa) { throw RuntimeError("Please specify one of asdu_type or asdu_type_id", ioa); auto asdu_data = asdu_type_name - ? ASDUData::lookupName(asdu_type_name, with_timestamp, ioa) - : ASDUData::lookupTypeId(asdu_type_id, ioa); + ? ASDUData::lookupName(asdu_type_name, with_timestamp, ioa, ioa_sequence_start.value_or(ioa)) + : ASDUData::lookupTypeId(asdu_type_id, ioa, ioa_sequence_start.value_or(ioa)); if (!asdu_data.has_value()) throw RuntimeError("Found invalid asdu_type or asdu_type_id"); @@ -91,75 +93,75 @@ ASDUData ASDUData::parse(json_t *signal_json, std::optional last_ioa) { return *asdu_data; }; -std::optional ASDUData::lookupTypeId(char const *type_id, int ioa) +std::optional ASDUData::lookupTypeId(char const *type_id, int ioa, int ioa_sequence_start) { auto check = [type_id] (Descriptor descriptor) { return !strcmp(descriptor.type_id, type_id); }; auto descriptor = std::find_if(begin(descriptors), end(descriptors), check); if (descriptor != end(descriptors)) - return ASDUData { *descriptor, ioa }; + return ASDUData { &*descriptor, ioa, ioa_sequence_start }; else return std::nullopt; } -std::optional ASDUData::lookupName(char const *name, bool with_timestamp, int ioa) +std::optional ASDUData::lookupName(char const *name, bool with_timestamp, int ioa, int ioa_sequence_start) { 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 }; + return ASDUData { &*descriptor, ioa, ioa_sequence_start }; else return std::nullopt; } -std::optional ASDUData::lookupType(int type, int ioa) +std::optional ASDUData::lookupType(int type, int ioa, int ioa_sequence_start) { auto check = [type] (Descriptor descriptor) { return descriptor.type == type; }; auto descriptor = std::find_if(begin(descriptors), end(descriptors), check); if (descriptor != end(descriptors)) - return ASDUData { *descriptor, ioa }; + return ASDUData { &*descriptor, ioa, ioa_sequence_start }; else return std::nullopt; } bool ASDUData::hasTimestamp() const { - return descriptor.has_timestamp; + return descriptor->has_timestamp; } ASDUData::Type ASDUData::type() const { - return descriptor.type; + return descriptor->type; } char const * ASDUData::name() const { - return descriptor.name; + return descriptor->name; } ASDUData::Type ASDUData::typeWithoutTimestamp() const { - return descriptor.type_without_timestamp; + return descriptor->type_without_timestamp; } ASDUData ASDUData::withoutTimestamp() const { - return ASDUData::lookupType(typeWithoutTimestamp(), ioa).value(); + return ASDUData::lookupType(typeWithoutTimestamp(), ioa, ioa_sequence_start).value(); } SignalType ASDUData::signalType() const { - return descriptor.signal_type; + return descriptor->signal_type; } std::optional ASDUData::checkASDU(CS101_ASDU const &asdu) const { - if (CS101_ASDU_getTypeID(asdu) != static_cast (descriptor.type)) + if (CS101_ASDU_getTypeID(asdu) != static_cast (descriptor->type)) return std::nullopt; for (int i = 0; i < CS101_ASDU_getNumberOfElements(asdu); i++) { @@ -250,7 +252,7 @@ bool ASDUData::addSampleToASDU(CS101_ASDU &asdu, ASDUData::Sample sample) const : std::nullopt; InformationObject io; - switch (descriptor.type) { + switch (descriptor->type) { case ASDUData::SCALED_INT: { auto value = static_cast (sample.signal_data.i & 0xFFFF); auto scaled = MeasuredValueScaled_create(NULL, ioa, value, sample.quality); @@ -308,7 +310,7 @@ bool ASDUData::addSampleToASDU(CS101_ASDU &asdu, ASDUData::Sample sample) const return successfully_added; } -ASDUData::ASDUData(ASDUData::Descriptor const &descriptor, int ioa) : ioa(ioa), descriptor(descriptor) +ASDUData::ASDUData(ASDUData::Descriptor const *descriptor, int ioa, int ioa_sequence_start) : ioa(ioa), ioa_sequence_start(ioa_sequence_start), descriptor(descriptor) {} void SlaveNode::createSlave() noexcept @@ -606,10 +608,12 @@ int SlaveNode::parse(json_t *json, const uuid_t sn_uuid) server.local_address = address; json_t *signals_json = nullptr; + int duplicate_ioa_is_sequence = false; if (out_json) { output.enabled = true; - if(json_unpack_ex(out_json, &err, 0, "{ s: o }", - "signals", &signals_json + if(json_unpack_ex(out_json, &err, 0, "{ s: o, s?: b }", + "signals", &signals_json, + "duplicate_ioa_is_sequence", &duplicate_ioa_is_sequence )) throw ConfigError(out_json, err, "node-config-node-iec60870-5-104"); } @@ -617,11 +621,11 @@ int SlaveNode::parse(json_t *json, const uuid_t sn_uuid) if (signals_json) { json_t *signal_json; size_t i; - std::optional last_ioa = std::nullopt; + std::optional last_data = std::nullopt; json_array_foreach(signals_json, i, signal_json) { auto signal = signals ? signals->getByIndex(i) : Signal::Ptr{}; - auto asdu_data = ASDUData::parse(signal_json, last_ioa); - last_ioa = asdu_data.ioa; + auto asdu_data = ASDUData::parse(signal_json, last_data, duplicate_ioa_is_sequence); + last_data = asdu_data; SignalData initial_value; if (signal) { if (signal->type != asdu_data.signalType())