1
0
Fork 0
mirror of https://git.rwth-aachen.de/acs/public/villas/node/ synced 2025-03-09 00:00:00 +01:00

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 <philipp.jungkamp@rwth-aachen.de>
This commit is contained in:
Philipp Jungkamp 2023-05-25 13:28:26 +02:00 committed by Philipp Jungkamp
parent 28d354cb84
commit fcae82ffe4
9 changed files with 943 additions and 1 deletions

View file

@ -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")

View file

@ -0,0 +1,48 @@
/**
* @file
* @author Philipp Jungkamp <philipp.jungkamp@rwth-aachen.de>
* @copyright 2014-2022, Institute for Automation of Complex Power Systems, EONERC
* @license Apache 2.0
*********************************************************************************/
#pragma once
#include <string>
#include <optional>
#include <villas/nodes/c37_118/parser.hpp>
#include <villas/node/config.hpp>
#include <villas/node.hpp>
#include <villas/signal.hpp>
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 */

View file

@ -0,0 +1,90 @@
/* Parser for C37-118.
*
* Author: Philipp Jungkamp <philipp.jungkamp@rwth-aachen.de>
* SPDX-FileCopyrightText: 2014-2024 Institute for Automation of Complex Power Systems, RWTH Aachen University
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include <cstring>
#include <optional>
#include <string>
#include <villas/nodes/c37_118/types.hpp>
namespace villas::node::c37_118::parser {
using namespace villas::node::c37_118::types;
class Parser {
public:
std::optional<Frame> deserialize(const unsigned char *buffer,
std::size_t length,
const Config *config);
std::vector<unsigned char> serialize(const Frame &frame,
const Config *config);
private:
template <typename T> 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 <typename T>
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<float> 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<Frame> 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<float> &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<unsigned char> se_buffer;
};
uint16_t calculate_crc(const unsigned char *frame, uint16_t size);
} // namespace villas::node::c37_118::parser

View file

@ -0,0 +1,163 @@
/* Node type: C37-118.
*
* Author: Philipp Jungkamp <philipp.jungkamp@rwth-aachen.de>
* SPDX-FileCopyrightText: 2014-2024 Institute for Automation of Complex Power Systems, RWTH Aachen University
* SPDX-License-Identifier: Apache-2.0
*/
#include <array>
#include <complex>
#include <stdint.h>
#include <variant>
#include <vector>
using namespace std::literals;
namespace villas::node::c37_118::types {
struct PmuData final {
uint16_t stat;
std::vector<std::complex<float>> phasor;
float freq;
float dfreq;
std::vector<float> analog;
std::vector<uint16_t> digital;
};
struct Data final {
std::vector<PmuData> 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<float>(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<float>(anunit & 0xFFFFFF);
}
};
struct DigitalInfo final {
std::array<std::string, 16> chnam;
uint32_t dgunit;
};
struct PmuConfig final {
std::string stn;
uint16_t idcode;
uint16_t format;
std::vector<PhasorInfo> phinfo;
std::vector<AnalogInfo> aninfo;
std::vector<DigitalInfo> dginfo;
uint16_t fnom;
uint16_t cfgcnt;
};
struct Config {
uint32_t time_base;
std::vector<PmuConfig> 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<unsigned char> 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<Data, Header, Config1, Config2, Command>;
uint16_t version;
uint16_t framesize;
uint16_t idcode;
uint32_t soc;
uint32_t fracsec;
Variant message;
};
} // namespace villas::node::c37_118::types

View file

@ -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()

3
lib/nodes/c37_118.cpp Normal file
View file

@ -0,0 +1,3 @@
#include <villas/nodes/c37_118.hpp>
using namespace villas::node::c37_118;

View file

@ -0,0 +1,525 @@
/* Parser for C37-118.
*
* Author: Philipp Jungkamp <philipp.jungkamp@rwth-aachen.de>
* SPDX-FileCopyrightText: 2014-2024 Institute for Automation of Complex Power Systems, RWTH Aachen University
* SPDX-License-Identifier: Apache-2.0
*/
#include <arpa/inet.h>
#include <cmath>
#include <villas/exceptions.hpp>
#include <villas/nodes/c37_118/parser.hpp>
#include <villas/utils.hpp>
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<Frame> 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<unsigned char> 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<char>(value.data(), 16);
return value;
}
std::complex<float> Parser::deserialize_phasor(uint16_t format, const PhasorInfo &phinfo) {
switch (format & 0x3) {
case 0x0: {
auto real = static_cast<float>(deserialize_int16_t()) * phinfo.scale();
auto imag = static_cast<float>(deserialize_int16_t()) * phinfo.scale();
return std::complex(real, imag);
}
case 0x1: {
auto abs = static_cast<float>(deserialize_uint16_t()) * phinfo.scale();
auto arg = static_cast<float>(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<float>(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<float>(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<float>(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<std::complex<float>> phasor(pmu_config.phinfo.size());
std::vector<float> analog(pmu_config.aninfo.size());
std::vector<uint16_t> 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<PhasorInfo> phinfo(deserialize_uint16_t());
std::vector<AnalogInfo> aninfo(deserialize_uint16_t());
std::vector<DigitalInfo> 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<PmuConfig> 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<PmuData> 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<Frame> 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<float> &value, uint16_t format, const PhasorInfo &phinfo) {
switch (format & 0x3) {
case 0x0: {
serialize_int16_t(static_cast<int16_t>(value.real() / phinfo.scale()));
serialize_int16_t(static_cast<int16_t>(value.imag() / phinfo.scale()));
return;
}
case 0x1: {
serialize_uint16_t(static_cast<uint16_t>(std::abs(value) / phinfo.scale()));
serialize_int16_t(static_cast<uint16_t>(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<int16_t>((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<int16_t>(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<int16_t>(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);
}

View file

@ -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 \"
$<TARGET_FILE:unit-tests> 2>&1 | c++filt\"
DEPENDS
unit-tests

106
tests/unit/c37_118.cpp Normal file
View file

@ -0,0 +1,106 @@
/* Unit tests for C37.118 parser.
*
* Author: Philipp Jungkamp <philipp.jungkamp@rwth-aachen.de>
* SPDX-FileCopyrightText: 2014-2023 Institute for Automation of Complex Power Systems, RWTH Aachen University
* SPDX-License-Identifier: Apache-2.0
*/
#include <criterion/criterion.h>
#include <criterion/parameterized.h>
#include <villas/config_helper.hpp>
#include <villas/exceptions.hpp>
#include <villas/nodes/c37_118.hpp>
#include <villas/utils.hpp>
using namespace villas::node::c37_118::parser;
// cppcheck-suppress syntaxError
ParameterizedTestParameters(c37_118, parser) {
static criterion::parameters<criterion::parameters<unsigned char>> 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<unsigned char> *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<Config2>(&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<Data>(&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()));
}