From 27141a10bf8fb58995db1edbde126feb63a91384 Mon Sep 17 00:00:00 2001 From: Philipp Jungkamp Date: Thu, 25 May 2023 13:28:26 +0200 Subject: [PATCH] node-c37.118: protocol parser --- CMakeLists.txt | 2 + include/villas/nodes/c37_118.hpp | 48 +++ include/villas/nodes/c37_118/parser.hpp | 95 +++++ include/villas/nodes/c37_118/types.hpp | 185 +++++++++ lib/nodes/CMakeLists.txt | 4 + lib/nodes/c37_118.cpp | 3 + lib/nodes/c37_118/parser.cpp | 499 ++++++++++++++++++++++++ lib/nodes/c37_118/types.cpp | 93 +++++ tests/unit/CMakeLists.txt | 3 +- tests/unit/c37_118.cpp | 106 +++++ 10 files changed, 1037 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 lib/nodes/c37_118/types.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..9d9c72cae --- /dev/null +++ b/include/villas/nodes/c37_118/parser.hpp @@ -0,0 +1,95 @@ +/* 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: + Parser(Config config = {}); + void set_config(Config config); + Config const *get_config() const; + std::optional deserialize(unsigned char const *buffer, + std::size_t length); + std::vector serialize(Frame const &frame); + +private: + uint16_t config_num_pmu() const; + uint16_t config_format(uint16_t pmu) const; + uint16_t config_phnmr(uint16_t pmu) const; + uint16_t config_annmr(uint16_t pmu) const; + uint16_t config_dgnmr(uint16_t pmu) const; + + 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(T const *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, (void const *)value, count); + } + + uint16_t deserialize_uint16_t(); + uint32_t deserialize_uint32_t(); + int16_t deserialize_int16_t(); + float deserialize_float(); + std::string deserialize_name1(); + Phasor deserialize_phasor(uint16_t pmu); + Analog deserialize_analog(uint16_t pmu); + Freq deserialize_freq(uint16_t pmu); + PmuData deserialize_pmu_data(uint16_t pmu); + PmuConfig deserialize_pmu_config_simple(); + Config deserialize_config_simple(); + Config1 deserialize_config1(); + Config2 deserialize_config2(); + Data deserialize_data(); + Header deserialize_header(); + Command deserialize_command(); + std::optional try_deserialize_frame(); + + 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 Phasor &value, uint16_t pmu); + void serialize_analog(const Analog &value, uint16_t pmu); + void serialize_freq(const Freq &value, uint16_t pmu); + void serialize_pmu_data(const PmuData &value, uint16_t pmu); + 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); + void serialize_header(const Header &value); + void serialize_command(const Command &value); + void serialize_frame(const Frame &value); + + std::optional config; + + unsigned char const *de_cursor; + unsigned char const *de_end; + + std::vector se_buffer; +}; + +uint16_t calculate_crc(unsigned char const *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..b29c5011a --- /dev/null +++ b/include/villas/nodes/c37_118/types.hpp @@ -0,0 +1,185 @@ +#include +#include +#include +#include +#include +#include +#include + +namespace villas::node::c37_118::types { + +class Phasor final { +public: + enum Notation { + RectangularInt = 0, + PolarInt, + RectangularFloat, + PolarFloat, + }; + using RectangularInt_t = std::tuple; + using PolarInt_t = std::tuple; + using RectangularFloat_t = std::tuple; + using PolarFloat_t = std::tuple; + using Variant = std::variant; + + Phasor() = default; + + Notation notation() const; + std::complex to_complex() const; + static Phasor from_complex(std::complex value, Notation notation); + + template , + typename First = std::tuple_element<0, Tuple>, + typename Second = std::tuple_element<1, Tuple>> + static Phasor make(First first, Second second); + + template > + Tuple get() const; + +private: + Phasor(Variant variant) : variant{std::move(variant)} {} + Variant variant; +}; + +template +Phasor Phasor::make(First first, Second second) { + return Variant{std::in_place_index, first, second}; +} + +template Tuple Phasor::get() const { + return std::get(this->variant); +} + +class Number final { +public: + enum Notation { + Int, + Float, + }; + using Int_t = int16_t; + using Float_t = float; + using Variant = std::variant; + + Number() = default; + + Notation notation() const; + float to_float() const; + static Number from_float(float value, Notation notation); + + template > + static Number make(Value value); + + template > + Value get() const; + +private: + Number(Variant variant) : variant{std::move(variant)} {} + Variant variant; +}; + +using Analog = Number; +using Freq = Number; + +struct PmuData final { + uint16_t stat; + std::vector phasor; + Freq freq; + Freq dfreq; + std::vector analog; + std::vector digital; +}; + +template Number Number::make(Value value) { + return Variant{std::in_place_index, value}; +} + +template Value Number::get() const { + return std::get(this->variant); +} + +struct Data final { + std::vector pmus; +}; + +struct Header final { + std::string data; +}; + +struct ChannelInfo final { + std::string nam; + uint32_t unit; +}; + +struct DigitalInfo final { + std::array nam; + uint32_t unit; +}; + +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_START = 0x1; + static constexpr uint16_t DATA_STOP = 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..3ac4be317 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 c37_118/types.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..8b6365274 --- /dev/null +++ b/lib/nodes/c37_118/parser.cpp @@ -0,0 +1,499 @@ +/* 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 + +using namespace villas::node::c37_118; +using namespace villas::node::c37_118::parser; + +uint16_t parser::calculate_crc(unsigned char const *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; +} + +Parser::Parser(Config config) : config{std::move(config)} {} + +void Parser::set_config(Config config) { this->config = config; } + +std::optional Parser::deserialize(unsigned char const *buffer, + std::size_t length) { + de_cursor = buffer; + de_end = buffer + length; + return try_deserialize_frame(); +} + +std::vector Parser::serialize(Frame const &frame) { + se_buffer.clear(); + serialize_frame(frame); + return se_buffer; +} + +uint16_t Parser::config_num_pmu() const { return config.value().pmus.size(); } + +uint16_t Parser::config_format(uint16_t pmu) const { + return config.value().pmus[pmu].format; +} + +uint16_t Parser::config_phnmr(uint16_t pmu) const { + return config.value().pmus[pmu].phinfo.size(); +} + +uint16_t Parser::config_annmr(uint16_t pmu) const { + return config.value().pmus[pmu].aninfo.size(); +} + +uint16_t Parser::config_dgnmr(uint16_t pmu) const { + return config.value().pmus[pmu].dginfo.size(); +} + +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; +} + +Phasor Parser::deserialize_phasor(uint16_t pmu) { + switch (config_format(pmu) & 0x3) { + case 0x0: { + auto real = deserialize_int16_t(); + auto imag = deserialize_int16_t(); + return Phasor::make(real, imag); + } + + case 0x1: { + auto abs = deserialize_uint16_t(); + auto arg = deserialize_int16_t(); + return Phasor::make(abs, arg); + } + + case 0x2: { + auto real = deserialize_float(); + auto imag = deserialize_float(); + return Phasor::make(real, imag); + } + + case 0x3: { + auto abs = deserialize_float(); + auto arg = deserialize_float(); + return Phasor::make(abs, arg); + } + + default: + throw RuntimeError{"c37_118: unknown phasor format"}; + } +} + +Analog Parser::deserialize_analog(uint16_t pmu) { + switch (config_format(pmu) & 0x4) { + case 0x0: { + auto value = deserialize_int16_t(); + return Analog::make(value); + } + + case 0x4: { + auto value = deserialize_float(); + return Analog::make(value); + } + + default: + throw RuntimeError{"c37_118: unknown analog format"}; + } +} + +Freq Parser::deserialize_freq(uint16_t pmu) { + switch (config_format(pmu) & 0x8) { + case 0x0: { + auto value = deserialize_int16_t(); + return Freq::make(value); + } + + case 0x8: { + auto value = deserialize_float(); + return Freq::make(value); + } + + default: + throw RuntimeError{"c37_118: unknown freq format"}; + } +} + +PmuData Parser::deserialize_pmu_data(uint16_t pmu) { + std::vector phasor(config_phnmr(pmu)); + std::vector analog(config_annmr(pmu)); + std::vector digital(config_dgnmr(pmu)); + + auto stat = deserialize_uint16_t(); + for (auto &p : phasor) + p = deserialize_phasor(pmu); + auto freq = deserialize_freq(pmu); + auto dfreq = deserialize_freq(pmu); + for (auto &a : analog) + a = deserialize_analog(pmu); + for (auto &d : digital) + d = deserialize_uint16_t(); + + 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.nam = deserialize_name1(); + for (auto &an : aninfo) + an.nam = deserialize_name1(); + for (auto &dg : dginfo) + for (auto &nam : dg.nam) + nam = deserialize_name1(); + for (auto &ph : phinfo) + ph.unit = deserialize_uint32_t(); + for (auto &an : aninfo) + an.unit = deserialize_uint32_t(); + for (auto &dg : dginfo) + dg.unit = 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() { + std::vector pmus; + auto num_pmu = config_num_pmu(); + pmus.reserve(num_pmu); + + for (uint16_t i = 0; i < num_pmu; i++) + pmus.push_back(deserialize_pmu_data(i)); + + 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() { + 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.has_value()) + message = deserialize_data(); + 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 Phasor &value, uint16_t pmu) { + switch (config_format(pmu) & 0x3) { + case 0x0: { + auto [real, imag] = value.get(); + serialize_int16_t(real); + serialize_int16_t(imag); + return; + } + + case 0x1: { + auto [abs, arg] = value.get(); + serialize_uint16_t(abs); + serialize_int16_t(arg); + return; + } + + case 0x2: { + auto [real, imag] = value.get(); + serialize_float(real); + serialize_float(imag); + return; + } + + case 0x3: { + auto [abs, arg] = value.get(); + serialize_float(abs); + serialize_float(arg); + return; + } + + default: + throw RuntimeError{"c37_118: unknown phasor format"}; + } +} + +void Parser::serialize_analog(const Analog &value, uint16_t pmu) { + switch (config_format(pmu) & 0x4) { + case 0x0: { + serialize_int16_t(value.get()); + return; + } + + case 0x4: { + serialize_float(value.get()); + return; + } + + default: + throw RuntimeError{"c37_118: unknown analog format"}; + } +} + +void Parser::serialize_freq(const Freq &value, uint16_t pmu) { + switch (config_format(pmu) & 0x8) { + case 0x0: { + serialize_int16_t(value.get()); + return; + } + + case 0x8: { + serialize_float(value.get()); + return; + } + + default: + throw RuntimeError{"c37_118: unknown freq format"}; + } +} + +void Parser::serialize_pmu_data(const PmuData &value, uint16_t pmu) { + if (value.phasor.size() != config_phnmr(pmu)) + throw RuntimeError{"c37_118: [phasor] expected [{}], got [{}]", + config_phnmr(pmu), value.phasor.size()}; + + if (value.analog.size() != config_annmr(pmu)) + throw RuntimeError{"c37_118: [analog] expected [{}], got [{}]", + config_annmr(pmu), value.analog.size()}; + + if (value.digital.size() != config_dgnmr(pmu)) + throw RuntimeError{"c37_118: [digital] expected [{}], got [{}]", + config_dgnmr(pmu), value.digital.size()}; + + serialize_uint16_t(value.stat); + for (auto &ph : value.phasor) + serialize_phasor(ph, pmu); + serialize_freq(value.freq, pmu); + serialize_freq(value.dfreq, pmu); + for (auto &an : value.analog) + serialize_analog(an, pmu); + for (auto &dg : value.digital) + serialize_uint16_t(dg); +} + +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.nam); + for (auto &an : value.aninfo) + serialize_name1(an.nam); + for (auto &dg : value.dginfo) + for (auto &nam : dg.nam) + serialize_name1(nam); + for (auto &ph : value.phinfo) + serialize_uint32_t(ph.unit); + for (auto &an : value.aninfo) + serialize_uint32_t(an.unit); + for (auto &dg : value.dginfo) + serialize_uint32_t(dg.unit); + 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) { + if (value.pmus.size() != config_num_pmu()) + throw RuntimeError{"c37_118: [pmus] expected {}, got {}", config_num_pmu(), + value.pmus.size()}; + + for (uint16_t i = 0; i < value.pmus.size(); i++) + serialize_pmu_data(value.pmus[i], 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) { + uint16_t sync = 0xAA00 | ((value.message.index()) << 4) | value.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{ + [&](Data const &data) { serialize_data(data); }, + [&](Header const &header) { serialize_header(header); }, + [&](Config1 const &config1) { serialize_config1(config1); }, + [&](Config2 const &config2) { serialize_config2(config2); }, + [&](Command const &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/lib/nodes/c37_118/types.cpp b/lib/nodes/c37_118/types.cpp new file mode 100644 index 000000000..a015a22b3 --- /dev/null +++ b/lib/nodes/c37_118/types.cpp @@ -0,0 +1,93 @@ +#include +#include +#include + +using namespace villas::node::c37_118::types; + +Phasor::Notation Phasor::notation() const { + return static_cast(variant.index()); +} + +std::complex Phasor::to_complex() const { + switch (this->notation()) { + case RectangularInt: { + auto [real, imag] = this->get(); + return std::complex{float(real), float(imag)}; + } + + case PolarInt: { + auto [mag, phase] = this->get(); + return std::polar(float(mag), float(phase)); + } + + case RectangularFloat: { + auto [real, imag] = this->get(); + return std::complex{real, imag}; + } + + case PolarFloat: { + auto [mag, phase] = this->get(); + return std::polar(mag, phase); + } + + default: + throw RuntimeError{"invalid notation"}; + } +} + +Phasor Phasor::from_complex(std::complex value, Notation notation) { + switch (notation) { + case RectangularInt: { + return Phasor::make(value.real(), value.imag()); + } + + case PolarInt: { + return Phasor::make(std::abs(value), std::arg(value)); + } + + case RectangularFloat: { + return Phasor::make(value.real(), value.imag()); + } + + case PolarFloat: { + return Phasor::make(std::abs(value), std::arg(value)); + } + + default: + throw RuntimeError{"invalid notation"}; + } +} + +Number::Notation Number::notation() const { + return static_cast(variant.index()); +} + +float Number::to_float() const { + switch (this->notation()) { + case Int: { + return this->get(); + } + + case Float: { + return this->get(); + } + + default: + throw RuntimeError{"invalid notation"}; + } +} + +Number Number::from_float(float value, Notation notation) { + switch (notation) { + case Int: { + return Number::make(value); + } + + case Float: { + return Number::make(value); + } + + default: + throw RuntimeError{"invalid notation"}; + } +} 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..fa9d96713 --- /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 = + { + ChannelInfo{ + .nam = "V1LPM", + .unit = 1, + }, + ChannelInfo{ + .nam = "VALPM", + .unit = 1, + }, + ChannelInfo{ + .nam = "VBLPM", + .unit = 1, + }, + ChannelInfo{ + .nam = "VCLPM", + .unit = 1, + }, + }, + .aninfo = {}, + .dginfo = {}, + .fnom = 1, + .cfgcnt = 89, + }, + }, + .data_rate = 50, + }; + + auto parser = Parser(config); + + auto frame = parser.deserialize(param->data(), param->size()); + 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); + cr_assert(std::equal(param->begin(), param->end(), buf.begin(), buf.end())); +}