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:
parent
28d354cb84
commit
fcae82ffe4
9 changed files with 943 additions and 1 deletions
|
@ -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")
|
||||
|
|
48
include/villas/nodes/c37_118.hpp
Normal file
48
include/villas/nodes/c37_118.hpp
Normal 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 */
|
90
include/villas/nodes/c37_118/parser.hpp
Normal file
90
include/villas/nodes/c37_118/parser.hpp
Normal 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
|
163
include/villas/nodes/c37_118/types.hpp
Normal file
163
include/villas/nodes/c37_118/types.hpp
Normal 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
|
|
@ -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
3
lib/nodes/c37_118.cpp
Normal file
|
@ -0,0 +1,3 @@
|
|||
#include <villas/nodes/c37_118.hpp>
|
||||
|
||||
using namespace villas::node::c37_118;
|
525
lib/nodes/c37_118/parser.cpp
Normal file
525
lib/nodes/c37_118/parser.cpp
Normal 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);
|
||||
}
|
|
@ -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
106
tests/unit/c37_118.cpp
Normal 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()));
|
||||
}
|
Loading…
Add table
Reference in a new issue