From adb70138c8a659471b9dbe8d262048a7d05ef47e Mon Sep 17 00:00:00 2001
From: Philipp Jungkamp
Date: Fri, 29 Apr 2022 09:47:39 +0000
Subject: [PATCH] start iec60870 node + sample to ASDU conversion
---
include/villas/nodes/iec60870.hpp | 160 +++++++++++++++++++++++
lib/nodes/CMakeLists.txt | 6 +
lib/nodes/iec60870.cpp | 203 ++++++++++++++++++++++++++++++
3 files changed, 369 insertions(+)
create mode 100644 include/villas/nodes/iec60870.hpp
create mode 100644 lib/nodes/iec60870.cpp
diff --git a/include/villas/nodes/iec60870.hpp b/include/villas/nodes/iec60870.hpp
new file mode 100644
index 000000000..c5a23034a
--- /dev/null
+++ b/include/villas/nodes/iec60870.hpp
@@ -0,0 +1,160 @@
+/** Node type: IEC60870-5-104
+ *
+ * @file
+ * @author Steffen Vogel
+ * @copyright 2014-2022, Institute for Automation of Complex Power Systems, EONERC
+ * @license GNU General Public License (version 3)
+ *
+ * VILLASnode
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *********************************************************************************/
+
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace villas {
+namespace node {
+namespace iec60870 {
+
+// A supported CS101 information data type
+class ASDUDataType {
+public:
+ enum Type {
+ // MeasuredValueScaled
+ // 16 bit
+ SCALED = M_ME_NB_1,
+ // MeasuredValueScaled + Timestamp
+ // 16 bit
+ SCALED_WITH_TIMESTAMP = M_ME_TE_1,
+ // SinglePoint
+ // bool
+ SINGLEPOINT = M_SP_NA_1,
+ // SinglePoint + Timestamp
+ // bool + timestamp
+ SINGLEPOINT_WITH_TIMESTAMP = M_SP_TB_1,
+ // DoublePoint
+ // 2 bit enum
+ DOUBLEPOINT = M_DP_NA_1,
+ // DoublePoint + Timestamp
+ // 2 bit enum + timestamp
+ DOUBLEPOINT_WITH_TIMESTAMP = M_DP_TB_1,
+ // MeasuredValueNormalized
+ // 16 bit
+ NORMALIZED = M_ME_NA_1,
+ // MeasuredValueNormalized + Timestamp
+ // 16 bit + timestamp
+ NORMALIZED_WITH_TIMESTAMP = M_ME_TD_1,
+ // MeasuredValueShort
+ // float
+ SHORT = M_ME_NC_1,
+ // MeasuredValueShort + Timestamp
+ // float + timestamp
+ 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);
+
+ // 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;
+
+ // 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;
+
+ // basic conversions and comparisons
+ ASDUDataType(Type type);
+ operator Type() const;
+ bool operator==(ASDUDataType data_type) const;
+ bool operator!=(ASDUDataType data_type) const;
+private:
+ Type type;
+};
+
+class TcpNode : public Node {
+protected:
+ struct Server {
+ bool created = false;
+
+ std::string local_address = "0.0.0.0";
+ int local_port = 2404;
+ int low_priority_queue_size = 16;
+ int high_priority_queue_size = 16;
+
+ CS104_Slave slave;
+ } server;
+
+ struct Output {
+ bool enabled = false;
+ SignalType signal_type = SignalType::INVALID;
+ ASDUDataType asdu_data_type = ASDUDataType::SHORT_WITH_TIMESTAMP;
+ unsigned signal_cnt = 0;
+ } out;
+
+ virtual
+ int _read(struct Sample * smps[], unsigned cnt) override;
+
+ virtual
+ int _write(struct Sample * smps[], unsigned cnt) override;
+
+public:
+ TcpNode(const std::string &name = "");
+
+ virtual
+ ~TcpNode() 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;
+ */
+};
+
+} /* namespace iec60870 */
+} /* namespace node */
+} /* namespace villas */
diff --git a/lib/nodes/CMakeLists.txt b/lib/nodes/CMakeLists.txt
index 1cd61da1f..555c49f97 100644
--- a/lib/nodes/CMakeLists.txt
+++ b/lib/nodes/CMakeLists.txt
@@ -66,6 +66,12 @@ if(WITH_NODE_SHMEM)
list(APPEND NODE_SRC shmem.cpp)
endif()
+# Enable IEC60870 node-types when lib60870 is available
+if(WITH_NODE_IEC60870)
+ list(APPEND NODE_SRC iec60870.cpp)
+ list(APPEND LIBRARIES PkgConfig::LIB60870)
+endif()
+
# Enable IEC61850 node-types when libiec61850 is available
if(WITH_NODE_IEC61850)
list(APPEND NODE_SRC iec61850_sv.cpp iec61850.cpp)
diff --git a/lib/nodes/iec60870.cpp b/lib/nodes/iec60870.cpp
new file mode 100644
index 000000000..52bb59366
--- /dev/null
+++ b/lib/nodes/iec60870.cpp
@@ -0,0 +1,203 @@
+/** Node type: IEC60870-5-104
+ *
+ * @author Steffen Vogel
+ * @copyright 2014-2022, Institute for Automation of Complex Power Systems, EONERC
+ * @license GNU General Public License (version 3)
+ *
+ * VILLASnode
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *********************************************************************************/
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+using namespace villas;
+using namespace villas::node;
+using namespace villas::utils;
+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");
+ }
+}
+
+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");
+ }
+}
+
+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");
+ }
+}
+
+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));
+
+ // 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");
+ }
+ CS101_ASDU_addInformationObject(asdu, io);
+ InformationObject_destroy(io);
+ }
+}
+
+ASDUDataType::ASDUDataType(ASDUDataType::Type t) : type(t) {}
+
+ASDUDataType::operator ASDUDataType::Type() const {
+ return this->type;
+}
+
+bool ASDUDataType::operator==(ASDUDataType v) const {
+ return this->type == v.type;
+}
+
+bool ASDUDataType::operator!=(ASDUDataType v) const {
+ return !(*this == v);
+}
+
+// ------------------------------------------
+// TcpNode
+// ------------------------------------------
+
+TcpNode::TcpNode(const std::string &name) :
+ Node(name)
+{ }
+
+TcpNode::~TcpNode()
+{
+ auto &server = this->server;
+
+ if (server.created) {
+ if (CS104_Slave_isRunning(server.slave)) {
+ CS104_Slave_stop(server.slave);
+ }
+ CS104_Slave_destroy(server.slave);
+ }
+}
+
+int TcpNode::_read(struct Sample *smps[], unsigned cnt)
+{
+ return 0;
+}
+
+int TcpNode::_write(struct Sample *smps[], unsigned cnt)
+{
+ return 0;
+}
+
+// ------------------------------------------
+// Plugin
+// ------------------------------------------
+
+static char name[] = "IEC60870-5-104";
+static char description[] = "Provide monitoring values over TCP/IP";
+static NodePlugin p;