From fcae82ffe4e75b2f7a35d1d8bdb51f107766a384 Mon Sep 17 00:00:00 2001 From: Philipp Jungkamp Date: Thu, 25 May 2023 13:28:26 +0200 Subject: [PATCH] node-c37.118: Add IEEE Std C37.118.2 parser This includes a simple self-contained parser for IEEE Std C37.118.2 and a corresponding unit test. The C37.118 node type is a dead stub. Signed-off-by: Philipp Jungkamp --- CMakeLists.txt | 2 + include/villas/nodes/c37_118.hpp | 48 +++ include/villas/nodes/c37_118/parser.hpp | 90 ++++ include/villas/nodes/c37_118/types.hpp | 163 ++++++++ lib/nodes/CMakeLists.txt | 4 + lib/nodes/c37_118.cpp | 3 + lib/nodes/c37_118/parser.cpp | 525 ++++++++++++++++++++++++ tests/unit/CMakeLists.txt | 3 +- tests/unit/c37_118.cpp | 106 +++++ 9 files changed, 943 insertions(+), 1 deletion(-) create mode 100644 include/villas/nodes/c37_118.hpp create mode 100644 include/villas/nodes/c37_118/parser.hpp create mode 100644 include/villas/nodes/c37_118/types.hpp create mode 100644 lib/nodes/c37_118.cpp create mode 100644 lib/nodes/c37_118/parser.cpp create mode 100644 tests/unit/c37_118.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index f2871bea6..81c9e66b1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -175,6 +175,7 @@ cmake_dependent_option(WITH_TOOLS "Build auxilary tools" cmake_dependent_option(WITH_WEB "Build with internal webserver" "${WITH_DEFAULTS}" "LIBWEBSOCKETS_FOUND" OFF) cmake_dependent_option(WITH_NODE_AMQP "Build with amqp node-type" "${WITH_DEFAULTS}" "RABBITMQ_C_FOUND" OFF) +cmake_dependent_option(WITH_NODE_C37_118 "Build with c37.118 node-type" "${WITH_DEFAULTS}" "" OFF) cmake_dependent_option(WITH_NODE_CAN "Build with can node-type" "${WITH_DEFAULTS}" "" OFF) cmake_dependent_option(WITH_NODE_COMEDI "Build with comedi node-type" "${WITH_DEFAULTS}" "COMEDILIB_FOUND" OFF) cmake_dependent_option(WITH_NODE_ETHERCAT "Build with ethercat node-type" "${WITH_DEFAULTS}" "ETHERLAB_FOUND; NOT WITHOUT_GPL" OFF) @@ -281,6 +282,7 @@ add_feature_info(TOOLS WITH_TOOLS "Build auxil add_feature_info(WEB WITH_WEB "Build with internal webserver") add_feature_info(NODE_AMQP WITH_NODE_AMQP "Build with amqp node-type") +add_feature_info(NODE_C37_118 WITH_NODE_C37_118 "Build with c37.118 node-type") add_feature_info(NODE_CAN WITH_NODE_CAN "Build with can node-type") add_feature_info(NODE_COMEDI WITH_NODE_COMEDI "Build with comedi node-type") add_feature_info(NODE_ETHERCAT WITH_NODE_ETHERCAT "Build with ethercat node-type") diff --git a/include/villas/nodes/c37_118.hpp b/include/villas/nodes/c37_118.hpp new file mode 100644 index 000000000..f8fd6be16 --- /dev/null +++ b/include/villas/nodes/c37_118.hpp @@ -0,0 +1,48 @@ +/** + * @file + * @author Philipp Jungkamp + * @copyright 2014-2022, Institute for Automation of Complex Power Systems, EONERC + * @license Apache 2.0 + *********************************************************************************/ + +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace villas { +namespace node { +namespace c37_118 { + +class C37_118 : public Node { +protected: + struct Input { + std::string address; + } input; + + virtual + int _read(struct Sample *smps[], unsigned cnt) override; + +public: + C37_118(const std::string &name = ""); + + virtual + ~C37_118() override; + + virtual + int parse(json_t *json, const uuid_t sn_uuid) override; + + virtual + int start() override; + + virtual + int stop() override; +}; + +} /* namespace c37_118 */ +} /* namespace node */ +} /* namespace villas */ diff --git a/include/villas/nodes/c37_118/parser.hpp b/include/villas/nodes/c37_118/parser.hpp new file mode 100644 index 000000000..5f1beef04 --- /dev/null +++ b/include/villas/nodes/c37_118/parser.hpp @@ -0,0 +1,90 @@ +/* Parser for C37-118. + * + * Author: Philipp Jungkamp + * SPDX-FileCopyrightText: 2014-2024 Institute for Automation of Complex Power Systems, RWTH Aachen University + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include +#include + +namespace villas::node::c37_118::parser { +using namespace villas::node::c37_118::types; + +class Parser { +public: + std::optional deserialize(const unsigned char *buffer, + std::size_t length, + const Config *config); + + std::vector serialize(const Frame &frame, + const Config *config); + +private: + template void de_copy(T *value, std::size_t count = sizeof(T)) { + if (de_cursor + count > de_end) + throw RuntimeError{"c37_118: broken frame"}; + + std::memcpy((void *)value, de_cursor, count); + de_cursor += count; + } + + template + void se_copy(const T *value, std::size_t count = sizeof(T)) { + auto index = se_buffer.size(); + se_buffer.insert(se_buffer.end(), count, 0); + std::memcpy(se_buffer.data() + index, (const void *)value, count); + } + + uint16_t deserialize_uint16_t(); + uint32_t deserialize_uint32_t(); + int16_t deserialize_int16_t(); + float deserialize_float(); + std::string deserialize_name1(); + std::complex deserialize_phasor(uint16_t format, const PhasorInfo &info); + float deserialize_freq(const PmuConfig &pmu); + float deserialize_dfreq(uint16_t format); + float deserialize_analog(uint16_t format, const AnalogInfo &aninfo); + uint16_t deserialize_digital(const DigitalInfo &dginfo); + PmuData deserialize_pmu_data(const PmuConfig &pmu_config); + PmuConfig deserialize_pmu_config_simple(); + Config deserialize_config_simple(); + Config1 deserialize_config1(); + Config2 deserialize_config2(); + Data deserialize_data(const Config &config); + Header deserialize_header(); + Command deserialize_command(); + std::optional try_deserialize_frame(const Config *config); + + void serialize_uint16_t(const uint16_t &value); + void serialize_uint32_t(const uint32_t &value); + void serialize_int16_t(const int16_t &value); + void serialize_float(const float &value); + void serialize_name1(const std::string &value); + void serialize_phasor(const std::complex &value, uint16_t format, const PhasorInfo &phinfo); + void serialize_freq(const float &value, const PmuConfig &pmu); + void serialize_dfreq(const float &value, uint16_t format); + void serialize_analog(const float &value, uint16_t format, const AnalogInfo &aninfo); + void serialize_digital(const uint16_t &value, const DigitalInfo &dginfo); + void serialize_pmu_data(const PmuData &value, const PmuConfig &pmu_config); + void serialize_pmu_config_simple(const PmuConfig &value); + void serialize_config_simple(const Config &value); + void serialize_config1(const Config1 &value); + void serialize_config2(const Config2 &value); + void serialize_data(const Data &value, const Config &config); + void serialize_header(const Header &value); + void serialize_command(const Command &value); + void serialize_frame(const Frame &value, const Config *config); + + const unsigned char *de_cursor; + const unsigned char *de_end; + std::vector se_buffer; +}; + +uint16_t calculate_crc(const unsigned char *frame, uint16_t size); + +} // namespace villas::node::c37_118::parser diff --git a/include/villas/nodes/c37_118/types.hpp b/include/villas/nodes/c37_118/types.hpp new file mode 100644 index 000000000..e46f59cb8 --- /dev/null +++ b/include/villas/nodes/c37_118/types.hpp @@ -0,0 +1,163 @@ +/* Node type: C37-118. + * + * Author: Philipp Jungkamp + * SPDX-FileCopyrightText: 2014-2024 Institute for Automation of Complex Power Systems, RWTH Aachen University + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include + +using namespace std::literals; + +namespace villas::node::c37_118::types { + +struct PmuData final { + uint16_t stat; + std::vector> phasor; + float freq; + float dfreq; + std::vector analog; + std::vector digital; +}; + +struct Data final { + std::vector pmus; +}; + +struct Header final { + std::string data; +}; + +struct PhasorInfo final { + static constexpr uint8_t UNIT_VOLT = 0; + static constexpr uint8_t UNIT_AMPERE = 1; + + std::string chnam; + uint32_t phunit; + + uint8_t unit() const noexcept { + return phunit >> 24; + } + + std::string unit_str() const noexcept { + switch (unit()) { + case UNIT_VOLT: + return "volt"s; + case UNIT_AMPERE: + return "ampere"s; + default: + return "other"s; + } + } + + float scale() const noexcept { + return static_cast(phunit & 0xFFFFFF) / 10'000; + } +}; + +struct AnalogInfo final { + static constexpr uint8_t UNIT_POINT_ON_WAVE = 0; + static constexpr uint8_t UNIT_RMS = 1; + static constexpr uint8_t UNIT_PEAK = 2; + + std::string chnam; + uint32_t anunit; + + uint8_t unit() const noexcept { + return anunit >> 24; + } + + std::string unit_str() const noexcept { + switch (unit()) { + case UNIT_POINT_ON_WAVE: + return "point-on-wave"s; + case UNIT_RMS: + return "rms"s; + case UNIT_PEAK: + return "peak"s; + default: + return "other"s; + } + } + + float scale() const noexcept { + return static_cast(anunit & 0xFFFFFF); + } +}; + +struct DigitalInfo final { + std::array chnam; + uint32_t dgunit; +}; + +struct PmuConfig final { + std::string stn; + uint16_t idcode; + uint16_t format; + std::vector phinfo; + std::vector aninfo; + std::vector dginfo; + uint16_t fnom; + uint16_t cfgcnt; +}; + +struct Config { + uint32_t time_base; + std::vector pmus; + uint16_t data_rate; +}; + +class Config1 { +private: + Config inner; + +public: + Config1() = delete; + Config1(Config config) noexcept : inner(config) {} + operator Config &() noexcept { return inner; } + operator Config const &() const noexcept { return inner; } + Config *operator->() { return &inner; } + Config const *operator->() const { return &inner; } +}; + +class Config2 { +private: + Config inner; + +public: + Config2() = delete; + Config2(Config config) noexcept : inner(config) {} + operator Config &() noexcept { return inner; } + operator Config const &() const noexcept { return inner; } + Config *operator->() { return &inner; } + Config const *operator->() const { return &inner; } +}; + +struct Command final { + uint16_t cmd; + std::vector ext; + + static constexpr uint16_t DATA_STOP = 0x1; + static constexpr uint16_t DATA_START = 0x2; + static constexpr uint16_t GET_HEADER = 0x3; + static constexpr uint16_t GET_CONFIG1 = 0x4; + static constexpr uint16_t GET_CONFIG2 = 0x5; + //static constexpr uint16_t GET_CONFIG3 = 0x6; +}; + +struct Frame final { + using Variant = std::variant; + + uint16_t version; + uint16_t framesize; + uint16_t idcode; + uint32_t soc; + uint32_t fracsec; + Variant message; +}; + +} // namespace villas::node::c37_118::types diff --git a/lib/nodes/CMakeLists.txt b/lib/nodes/CMakeLists.txt index 171cb63cd..25b9cf745 100644 --- a/lib/nodes/CMakeLists.txt +++ b/lib/nodes/CMakeLists.txt @@ -40,6 +40,10 @@ if(WITH_NODE_SOCKET) list(APPEND NODE_SRC socket.cpp) endif() +if(WITH_NODE_C37_118) + list(APPEND NODE_SRC c37_118.cpp c37_118/parser.cpp) +endif() + if(WITH_NODE_FILE) list(APPEND NODE_SRC file.cpp) endif() diff --git a/lib/nodes/c37_118.cpp b/lib/nodes/c37_118.cpp new file mode 100644 index 000000000..dc61e3e9b --- /dev/null +++ b/lib/nodes/c37_118.cpp @@ -0,0 +1,3 @@ +#include + +using namespace villas::node::c37_118; diff --git a/lib/nodes/c37_118/parser.cpp b/lib/nodes/c37_118/parser.cpp new file mode 100644 index 000000000..4ebb14703 --- /dev/null +++ b/lib/nodes/c37_118/parser.cpp @@ -0,0 +1,525 @@ +/* Parser for C37-118. + * + * Author: Philipp Jungkamp + * SPDX-FileCopyrightText: 2014-2024 Institute for Automation of Complex Power Systems, RWTH Aachen University + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include + +using namespace villas::node::c37_118; +using namespace villas::node::c37_118::parser; + +// Sample CRC routine taken from IEEE Std C37.118.2-2011 Annex B +// +// TODO: Is this code license compatible to Apache 2.0? +uint16_t parser::calculate_crc(const unsigned char *frame, uint16_t size) { + uint16_t crc = 0xFFFF; + uint16_t temp; + uint16_t quick; + + for (int i = 0; i < size; i++) { + temp = (crc >> 8) ^ (uint16_t)frame[i]; + crc <<= 8; + quick = temp ^ (temp >> 4); + crc ^= quick; + quick <<= 5; + crc ^= quick; + quick <<= 7; + crc ^= quick; + } + + return crc; +} + +std::optional Parser::deserialize(const unsigned char *buffer, + std::size_t length, + const Config *config) { + de_cursor = buffer; + de_end = buffer + length; + return try_deserialize_frame(config); +} + +std::vector Parser::serialize(const Frame &frame, + const Config *config) { + se_buffer.clear(); + serialize_frame(frame, config); + return se_buffer; +} + +uint16_t Parser::deserialize_uint16_t() { + uint16_t value; + de_copy(&value); + return ntohs(value); +} + +uint32_t Parser::deserialize_uint32_t() { + uint32_t value; + de_copy(&value); + return ntohl(value); +} + +int16_t Parser::deserialize_int16_t() { return deserialize_uint16_t(); } + +float Parser::deserialize_float() { + uint32_t raw = deserialize_uint32_t(); + return *(float *)&raw; +} + +std::string Parser::deserialize_name1() { + std::string value(16, 0); + de_copy(value.data(), 16); + return value; +} + +std::complex Parser::deserialize_phasor(uint16_t format, const PhasorInfo &phinfo) { + switch (format & 0x3) { + case 0x0: { + auto real = static_cast(deserialize_int16_t()) * phinfo.scale(); + auto imag = static_cast(deserialize_int16_t()) * phinfo.scale(); + return std::complex(real, imag); + } + + case 0x1: { + auto abs = static_cast(deserialize_uint16_t()) * phinfo.scale(); + auto arg = static_cast(deserialize_int16_t()) / 10'000; + return std::polar(abs, arg); + } + + case 0x2: { + auto real = deserialize_float(); + auto imag = deserialize_float(); + return std::complex(real, imag); + } + + case 0x3: { + auto abs = deserialize_float(); + auto arg = deserialize_float(); + return std::polar(abs, arg); + } + + default: + throw RuntimeError{"c37_118: unknown phasor format"}; + } +} + +float Parser::deserialize_freq(const PmuConfig &pmu) { + switch (pmu.format & 0x8) { + case 0x0: { + auto fnom = pmu.fnom & 1 ? 50.0 : 60.0; + return (static_cast(deserialize_int16_t()) / 1'000) + fnom; + } + + case 0x8: { + return deserialize_float(); + } + + default: + throw RuntimeError{"c37_118: unknown freq format"}; + } +} + +float Parser::deserialize_dfreq(uint16_t format) { + switch (format & 0x8) { + case 0x0: { + return static_cast(deserialize_int16_t()) / 100; + } + + case 0x8: { + return deserialize_float(); + } + + default: + throw RuntimeError{"c37_118: unknown freq format"}; + } +} + +float Parser::deserialize_analog(uint16_t format, const AnalogInfo &aninfo) { + switch (format & 0x4) { + case 0x0: { + return static_cast(deserialize_int16_t()) * aninfo.scale(); + } + + case 0x4: { + return deserialize_float(); // * aninfo.scale() ????? + } + + default: + throw RuntimeError{"c37_118: unknown analog format"}; + } +} + +uint16_t Parser::deserialize_digital(const DigitalInfo &dginfo) { + auto normal = dginfo.dgunit >> 16; + auto valid = dginfo.dgunit & 0xFF; + return (deserialize_uint16_t() ^ normal) & valid; +} + +PmuData Parser::deserialize_pmu_data(const PmuConfig &pmu_config) { + std::vector> phasor(pmu_config.phinfo.size()); + std::vector analog(pmu_config.aninfo.size()); + std::vector digital(pmu_config.dginfo.size()); + + auto stat = deserialize_uint16_t(); + for (size_t i = 0; i < phasor.size(); ++i) + phasor[i] = deserialize_phasor(pmu_config.format, pmu_config.phinfo[i]); + auto freq = deserialize_freq(pmu_config); + auto dfreq = deserialize_dfreq(pmu_config.format); + for (size_t i = 0; i < analog.size(); ++i) + analog[i] = deserialize_analog(pmu_config.format, pmu_config.aninfo[i]); + for (size_t i = 0; i < digital.size(); ++i) + digital[i] = deserialize_digital(pmu_config.dginfo[i]); + + return {stat, phasor, freq, dfreq, analog, digital}; +} + +PmuConfig Parser::deserialize_pmu_config_simple() { + auto stn = deserialize_name1(); + auto idcode = deserialize_uint16_t(); + auto format = deserialize_uint16_t(); + std::vector phinfo(deserialize_uint16_t()); + std::vector aninfo(deserialize_uint16_t()); + std::vector dginfo(deserialize_uint16_t()); + + for (auto &ph : phinfo) + ph.chnam = deserialize_name1(); + for (auto &an : aninfo) + an.chnam = deserialize_name1(); + for (auto &dg : dginfo) + for (auto &nam : dg.chnam) + nam = deserialize_name1(); + + for (auto &ph : phinfo) + ph.phunit = deserialize_uint32_t(); + for (auto &an : aninfo) + an.anunit = deserialize_uint32_t(); + for (auto &dg : dginfo) + dg.dgunit = deserialize_uint32_t(); + + auto fnom = deserialize_uint16_t(); + auto cfgcnt = deserialize_uint16_t(); + + return {stn, idcode, format, phinfo, aninfo, dginfo, fnom, cfgcnt}; +} + +Config Parser::deserialize_config_simple() { + auto time_base = deserialize_uint32_t(); + std::vector pmus(deserialize_uint16_t()); + for (auto &pmu : pmus) + pmu = deserialize_pmu_config_simple(); + auto data_rate = deserialize_uint16_t(); + + return Config{time_base, pmus, data_rate}; +} + +Config1 Parser::deserialize_config1() { return deserialize_config_simple(); } + +Config2 Parser::deserialize_config2() { return deserialize_config_simple(); } + +Data Parser::deserialize_data(const Config &config) { + std::vector pmus; + pmus.reserve(config.pmus.size()); + for (const auto &pmu_config : config.pmus) + pmus.push_back(deserialize_pmu_data(pmu_config)); + + return {pmus}; +} + +Header Parser::deserialize_header() { + auto data = std::string(de_cursor, de_end); + + return {data}; +} + +Command Parser::deserialize_command() { + auto cmd = deserialize_uint16_t(); + auto ext = std::vector(de_cursor, de_end); + + return {cmd, ext}; +} + +std::optional Parser::try_deserialize_frame(const Config *config) { + auto de_begin = de_cursor; + if (de_end - de_begin < 4) + return std::nullopt; + + auto sync = deserialize_uint16_t(); + auto framesize = deserialize_uint16_t(); + + if (de_end - de_begin < framesize) + return std::nullopt; + + de_end = de_begin + framesize - sizeof(uint16_t); + auto idcode = deserialize_uint16_t(); + auto soc = deserialize_uint32_t(); + auto fracsec = deserialize_uint32_t(); + + if ((sync & 0xFF80) != 0xAA00) + throw RuntimeError{"c37_118: invalid SYNC"}; + + uint16_t version = sync & 0xF; + Frame::Variant message; + switch (sync & 0x70) { + case 0x00: + if (config) + message = deserialize_data(*config); + break; + case 0x10: + message = deserialize_header(); + break; + case 0x20: + message = deserialize_config1(); + break; + case 0x30: + message = deserialize_config2(); + break; + case 0x40: + message = deserialize_command(); + break; + default: + throw RuntimeError{"c37_118: unsupported frame type"}; + } + + de_cursor = de_end; + de_end += sizeof(uint16_t); + auto crc = deserialize_uint16_t(); + auto expected_crc = calculate_crc(de_begin, framesize - sizeof(crc)); + if (crc != expected_crc) + throw RuntimeError{"c37_118: checksum mismatch"}; + + return {{version, framesize, idcode, soc, fracsec, message}}; +} + +void Parser::serialize_uint16_t(const uint16_t &value) { + uint16_t i = htons(value); + se_copy(&i); +} + +void Parser::serialize_uint32_t(const uint32_t &value) { + uint32_t i = htonl(value); + se_copy(&i); +} + +void Parser::serialize_int16_t(const int16_t &value) { + serialize_uint16_t(value); +} + +void Parser::serialize_float(const float &value) { + uint32_t i = *(uint32_t *)&value; + serialize_uint32_t(i); +} + +void Parser::serialize_name1(const std::string &value) { + std::string copy = value; + copy.resize(16, ' '); + se_copy(value.data(), 16); +} + +void Parser::serialize_phasor(const std::complex &value, uint16_t format, const PhasorInfo &phinfo) { + switch (format & 0x3) { + case 0x0: { + serialize_int16_t(static_cast(value.real() / phinfo.scale())); + serialize_int16_t(static_cast(value.imag() / phinfo.scale())); + return; + } + + case 0x1: { + serialize_uint16_t(static_cast(std::abs(value) / phinfo.scale())); + serialize_int16_t(static_cast(std::arg(value) * 10'000)); + return; + } + + case 0x2: { + serialize_float(value.real()); + serialize_float(value.imag()); + return; + } + + case 0x3: { + serialize_float(std::abs(value)); + serialize_float(std::arg(value)); + return; + } + + default: + throw RuntimeError{"c37_118: unknown phasor format"}; + } +} + +void Parser::serialize_freq(const float &value, const PmuConfig &pmu) { + switch (pmu.format & 0x8) { + case 0x0: { + auto fnom = pmu.fnom & 1 ? 50.0 : 60.0; + serialize_int16_t(static_cast((value - fnom) * 1'000)); + return; + } + + case 0x8: { + serialize_float(value); + return; + } + + default: + throw RuntimeError{"c37_118: unknown freq format"}; + } +} + +void Parser::serialize_dfreq(const float &value, uint16_t format) { + switch (format & 0x8) { + case 0x0: { + serialize_int16_t(static_cast(value * 100)); + return; + } + + case 0x8: { + serialize_float(value); + return; + } + + default: + throw RuntimeError{"c37_118: unknown freq format"}; + } +} + +void Parser::serialize_analog(const float &value, uint16_t format, const AnalogInfo &aninfo) { + switch (format & 0x4) { + case 0x0: { + serialize_int16_t(static_cast(value / aninfo.scale())); + return; + } + + case 0x4: { + serialize_float(value); + return; + } + + default: + throw RuntimeError{"c37_118: unknown analog format"}; + } +} + +void Parser::serialize_digital(const uint16_t &value, const DigitalInfo &dginfo) { + auto normal = dginfo.dgunit >> 16; + auto valid = dginfo.dgunit & 0xFF; + return serialize_uint16_t((value ^ normal) & valid); +} + +void Parser::serialize_pmu_data(const PmuData &value, const PmuConfig &pmu_config) { + if (value.phasor.size() != pmu_config.phinfo.size()) + throw RuntimeError{"c37_118: [phasor] expected [{}], got [{}]", + pmu_config.phinfo.size(), value.phasor.size()}; + + if (value.analog.size() != pmu_config.aninfo.size()) + throw RuntimeError{"c37_118: [analog] expected [{}], got [{}]", + pmu_config.aninfo.size(), value.analog.size()}; + + if (value.digital.size() != pmu_config.dginfo.size()) + throw RuntimeError{"c37_118: [digital] expected [{}], got [{}]", + pmu_config.dginfo.size(), value.digital.size()}; + + serialize_uint16_t(value.stat); + for (size_t i = 0; i < value.phasor.size(); ++i) + serialize_phasor(value.phasor[i], pmu_config.format, pmu_config.phinfo[i]); + serialize_freq(value.freq, pmu_config); + serialize_dfreq(value.dfreq, pmu_config.format); + for (size_t i = 0; i < value.analog.size(); ++i) + serialize_analog(value.analog[i], pmu_config.format, pmu_config.aninfo[i]); + for (size_t i = 0; i < value.digital.size(); ++i) + serialize_digital(value.digital[i], pmu_config.dginfo[i]); +} + +void Parser::serialize_pmu_config_simple(const PmuConfig &value) { + serialize_name1(value.stn); + serialize_uint16_t(value.idcode); + serialize_uint16_t(value.format); + serialize_uint16_t(value.phinfo.size()); + serialize_uint16_t(value.aninfo.size()); + serialize_uint16_t(value.dginfo.size()); + + for (auto &ph : value.phinfo) + serialize_name1(ph.chnam); + for (auto &an : value.aninfo) + serialize_name1(an.chnam); + for (auto &dg : value.dginfo) + for (auto &nam : dg.chnam) + serialize_name1(nam); + + for (auto &ph : value.phinfo) + serialize_uint32_t(ph.phunit); + for (auto &an : value.aninfo) + serialize_uint32_t(an.anunit); + for (auto &dg : value.dginfo) + serialize_uint32_t(dg.dgunit); + + serialize_uint16_t(value.fnom); + serialize_uint16_t(value.cfgcnt); +} + +void Parser::serialize_config_simple(const Config &value) { + serialize_uint32_t(value.time_base); + serialize_uint16_t(value.pmus.size()); + for (auto &pmu : value.pmus) + serialize_pmu_config_simple(pmu); + serialize_uint16_t(value.data_rate); +} + +void Parser::serialize_config1(const Config1 &value) { + serialize_config_simple(value); +} + +void Parser::serialize_config2(const Config2 &value) { + serialize_config_simple(value); +} + +void Parser::serialize_data(const Data &value, const Config &config) { + if (value.pmus.size() != config.pmus.size()) + throw RuntimeError{"c37_118: [pmus] expected {}, got {}", config.pmus.size(), + value.pmus.size()}; + + for (uint16_t i = 0; i < value.pmus.size(); i++) + serialize_pmu_data(value.pmus[i], config.pmus[i]); +} + +void Parser::serialize_header(const Header &value) { + se_copy(value.data.data(), value.data.size()); +} + +void Parser::serialize_command(const Command &value) { + serialize_uint16_t(value.cmd); + se_copy(value.ext.data(), value.ext.size()); +} + +void Parser::serialize_frame(const Frame &value, const Config *config) { + auto version = 1; + uint16_t sync = 0xAA00 | ((value.message.index()) << 4) | version; + + serialize_uint16_t(sync); + serialize_uint16_t(0); // framesize placeholder + serialize_uint16_t(value.idcode); + serialize_uint32_t(value.soc); + serialize_uint32_t(value.fracsec); + + std::visit(villas::utils::overloaded{ + [&](const Data &data) { + if (config) serialize_data(data, *config); + else throw RuntimeError{"c37_118: [frame] missing configuration for data frame"}; + }, + [&](const Header &header) { serialize_header(header); }, + [&](const Config1 &config1) { serialize_config1(config1); }, + [&](const Config2 &config2) { serialize_config2(config2); }, + [&](const Command &command) { serialize_command(command); }, + [](std::monostate) { + throw RuntimeError{"c37_118: [frame] missing message"}; + }, + }, + value.message); + + auto framesize = htons(se_buffer.size() + sizeof(uint16_t)); + std::memcpy(se_buffer.data() + sizeof(sync), &framesize, sizeof(framesize)); + auto crc = calculate_crc(se_buffer.data(), se_buffer.size()); + serialize_uint16_t(crc); +} diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index f9098e53e..be229e7cd 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -17,6 +17,7 @@ set(TEST_SRC queue_signalled.cpp queue.cpp signal.cpp + c37_118.cpp ) add_executable(unit-tests ${TEST_SRC}) @@ -28,7 +29,7 @@ target_link_libraries(unit-tests PUBLIC add_custom_target(run-unit-tests COMMAND - /bin/bash -o pipefail -c \" + /usr/bin/env bash -o pipefail -c \" $ 2>&1 | c++filt\" DEPENDS unit-tests diff --git a/tests/unit/c37_118.cpp b/tests/unit/c37_118.cpp new file mode 100644 index 000000000..0a6e10699 --- /dev/null +++ b/tests/unit/c37_118.cpp @@ -0,0 +1,106 @@ +/* Unit tests for C37.118 parser. + * + * Author: Philipp Jungkamp + * SPDX-FileCopyrightText: 2014-2023 Institute for Automation of Complex Power Systems, RWTH Aachen University + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include + +#include +#include +#include +#include + +using namespace villas::node::c37_118::parser; + +// cppcheck-suppress syntaxError +ParameterizedTestParameters(c37_118, parser) { + static criterion::parameters> params; + + params.push_back( // Config2 + {0xaa, 0x31, 0x00, 0x86, 0x00, 0xf1, 0x48, 0x93, 0x34, 0x4a, 0x00, 0x19, + 0x99, 0x9a, 0x00, 0xff, 0xff, 0xff, 0x00, 0x01, 0x42, 0x6c, 0x75, 0x65, + 0x20, 0x50, 0x4d, 0x55, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x00, 0xf1, 0x00, 0x06, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x56, 0x31, + 0x4c, 0x50, 0x4d, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x56, 0x41, 0x4c, 0x50, 0x4d, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x56, 0x42, 0x4c, 0x50, 0x4d, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x56, 0x43, + 0x4c, 0x50, 0x4d, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x59, 0x00, 0x32, + 0xc1, 0xe2}); + + params.push_back( // Data + {0xaa, 0x01, 0x00, 0x36, 0x00, 0xf1, 0x48, 0x93, 0x34, 0x4a, 0x00, + 0x1e, 0xb8, 0x52, 0x08, 0x00, 0x42, 0xf6, 0x8f, 0x24, 0xc7, 0xc3, + 0x66, 0x23, 0x43, 0x01, 0x88, 0xcb, 0xc7, 0xc3, 0x63, 0x32, 0xc7, + 0xa9, 0x56, 0x76, 0x47, 0x42, 0xfe, 0x4b, 0x47, 0xa9, 0x1c, 0xdd, + 0x47, 0x43, 0xd1, 0x44, 0x00, 0x00, 0x00, 0x00, 0x47, 0xef}); + + params.push_back( // Command + {0xaa, 0x41, 0x00, 0x12, 0x00, 0xf1, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x02, 0xa7, 0x37}); + + return params; +} + +ParameterizedTest(criterion::parameters *param, c37_118, + parser) { + auto config = Config{ + .time_base = 0xffffff, + .pmus = + { + PmuConfig{ + .stn = "Blue PMU", + .idcode = 241, + .format = 0x0006, + .phinfo = + { + PhasorInfo{ + .chnam = "V1LPM", + .phunit = 1, + }, + PhasorInfo{ + .chnam = "VALPM", + .phunit = 1, + }, + PhasorInfo{ + .chnam = "VBLPM", + .phunit = 1, + }, + PhasorInfo{ + .chnam = "VCLPM", + .phunit = 1, + }, + }, + .aninfo = {}, + .dginfo = {}, + .fnom = 1, + .cfgcnt = 89, + }, + }, + .data_rate = 50, + }; + + Parser parser{}; + + auto frame = parser.deserialize(param->data(), param->size(), &config); + cr_assert(frame.has_value()); + cr_assert(frame->framesize == param->size()); + + if (auto *c = std::get_if(&frame->message)) { + cr_assert((*c)->pmus[0].phinfo.size() == config.pmus[0].phinfo.size()); + cr_assert((*c)->pmus[0].aninfo.size() == config.pmus[0].aninfo.size()); + cr_assert((*c)->pmus[0].dginfo.size() == config.pmus[0].dginfo.size()); + } + + if (auto *d = std::get_if(&frame->message)) { + cr_assert(d->pmus[0].phasor.size() == config.pmus[0].phinfo.size()); + } + + auto buf = parser.serialize(*frame, &config); + cr_assert(std::equal(param->begin(), param->end(), buf.begin(), buf.end())); +}