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;