1
0
Fork 0
mirror of https://git.rwth-aachen.de/acs/public/villas/node/ synced 2025-03-09 00:00:00 +01:00
VILLASnode/lib/nodes/iec61850_goose.cpp
Steffen Vogel 02a2aa4f94 Apply clang-format changes
Signed-off-by: Steffen Vogel <steffen.vogel@opal-rt.com>
2023-09-08 11:37:42 +02:00

875 lines
24 KiB
C++

/* Node type: IEC 61850 - GOOSE.
*
* Author: Philipp Jungkamp <philipp.jungkamp@rwth-aachen.de>
* SPDX-FileCopyrightText: 2023 Institute for Automation of Complex Power Systems, RWTH Aachen University
* SPDX-License-Identifier: Apache-2.0
*/
#include <algorithm>
#include <chrono>
#include <villas/exceptions.hpp>
#include <villas/node_compat.hpp>
#include <villas/nodes/iec61850_goose.hpp>
#include <villas/sample.hpp>
#include <villas/super_node.hpp>
#include <villas/utils.hpp>
using namespace std::literals::chrono_literals;
using namespace std::literals::string_literals;
using namespace villas;
using namespace villas::node;
using namespace villas::utils;
using namespace villas::node::iec61850;
static std::optional<std::array<uint8_t, 6>> stringToMac(char *mac_string) {
std::array<uint8_t, 6> mac;
char *save;
char *token = strtok_r(mac_string, ":", &save);
for (auto &i : mac) {
if (!token)
return std::nullopt;
i = static_cast<uint8_t>(strtol(token, NULL, 16));
token = strtok_r(NULL, ":", &save);
}
return std::optional{mac};
}
MmsType GooseSignal::mmsType() const { return descriptor->mms_type; }
std::string const &GooseSignal::name() const { return descriptor->name; }
SignalType GooseSignal::signalType() const { return descriptor->signal_type; }
std::optional<GooseSignal> GooseSignal::fromMmsValue(MmsValue *mms_value) {
auto mms_type = MmsValue_getType(mms_value);
auto descriptor = lookupMmsType(mms_type);
SignalData data;
if (!descriptor)
return std::nullopt;
switch (mms_type) {
case MmsType::MMS_BOOLEAN:
data.b = MmsValue_getBoolean(mms_value);
break;
case MmsType::MMS_INTEGER:
data.i = MmsValue_toInt64(mms_value);
break;
case MmsType::MMS_UNSIGNED:
data.i = static_cast<int64_t>(MmsValue_toUint32(mms_value));
break;
case MmsType::MMS_BIT_STRING:
data.i = static_cast<int64_t>(MmsValue_getBitStringAsInteger(mms_value));
break;
case MmsType::MMS_FLOAT:
data.f = MmsValue_toDouble(mms_value);
break;
default:
return std::nullopt;
}
return GooseSignal{descriptor.value(), data};
}
std::optional<GooseSignal>
GooseSignal::fromNameAndValue(char const *name, SignalData value,
std::optional<Meta> meta) {
auto descriptor = lookupMmsTypeName(name);
if (!descriptor)
return std::nullopt;
return GooseSignal{descriptor.value(), value, meta};
}
MmsValue *GooseSignal::toMmsValue() const {
switch (descriptor->mms_type) {
case MmsType::MMS_BOOLEAN:
return MmsValue_newBoolean(signal_data.b);
case MmsType::MMS_INTEGER:
return newMmsInteger(signal_data.i, meta.size);
case MmsType::MMS_UNSIGNED:
return newMmsUnsigned(static_cast<uint64_t>(signal_data.i), meta.size);
case MmsType::MMS_BIT_STRING:
return newMmsBitString(static_cast<uint32_t>(signal_data.i), meta.size);
case MmsType::MMS_FLOAT:
return newMmsFloat(signal_data.f, meta.size);
default:
throw RuntimeError{"invalid mms type"};
}
}
MmsValue *GooseSignal::newMmsBitString(uint32_t i, int size) {
auto mms_bitstring = MmsValue_newBitString(size);
MmsValue_setBitStringFromInteger(mms_bitstring, i);
return mms_bitstring;
}
MmsValue *GooseSignal::newMmsInteger(int64_t i, int size) {
auto mms_integer = MmsValue_newInteger(size);
switch (size) {
case 8:
MmsValue_setInt8(mms_integer, static_cast<int8_t>(i));
return mms_integer;
case 16:
MmsValue_setInt16(mms_integer, static_cast<int16_t>(i));
return mms_integer;
case 32:
MmsValue_setInt32(mms_integer, static_cast<int32_t>(i));
return mms_integer;
case 64:
MmsValue_setInt64(mms_integer, static_cast<int64_t>(i));
return mms_integer;
default:
throw RuntimeError{"invalid mms integer size"};
}
}
MmsValue *GooseSignal::newMmsUnsigned(uint64_t u, int size) {
auto mms_unsigned = MmsValue_newUnsigned(size);
switch (size) {
case 8:
MmsValue_setUint8(mms_unsigned, static_cast<uint8_t>(u));
return mms_unsigned;
case 16:
MmsValue_setUint16(mms_unsigned, static_cast<uint16_t>(u));
return mms_unsigned;
case 32:
MmsValue_setUint32(mms_unsigned, static_cast<uint32_t>(u));
return mms_unsigned;
default:
throw RuntimeError{"invalid mms integer size"};
}
}
MmsValue *GooseSignal::newMmsFloat(double d, int size) {
switch (size) {
case 32:
return MmsValue_newFloat(static_cast<float>(d));
case 64:
return MmsValue_newDouble(d);
default:
throw RuntimeError{"invalid mms float size"};
}
}
std::optional<GooseSignal::Type> GooseSignal::lookupMmsType(int mms_type) {
auto check = [mms_type](Descriptor descriptor) {
return descriptor.mms_type == mms_type;
};
auto descriptor = std::find_if(begin(descriptors), end(descriptors), check);
if (descriptor != end(descriptors))
return &*descriptor;
else
return std::nullopt;
}
std::optional<GooseSignal::Type>
GooseSignal::lookupMmsTypeName(char const *name) {
auto check = [name](Descriptor descriptor) {
return descriptor.name == name;
};
auto descriptor = std::find_if(begin(descriptors), end(descriptors), check);
if (descriptor != end(descriptors))
return &*descriptor;
else
return std::nullopt;
}
GooseSignal::GooseSignal(GooseSignal::Descriptor const *descriptor,
SignalData data, std::optional<Meta> meta)
: signal_data(data), meta(meta.value_or(descriptor->default_meta)),
descriptor(descriptor) {}
bool iec61850::operator==(GooseSignal &lhs, GooseSignal &rhs) {
if (lhs.mmsType() != rhs.mmsType())
return false;
switch (lhs.mmsType()) {
case MmsType::MMS_INTEGER:
case MmsType::MMS_UNSIGNED:
case MmsType::MMS_BIT_STRING:
case MmsType::MMS_FLOAT:
if (lhs.meta.size != rhs.meta.size)
return false;
break;
default:
break;
}
switch (lhs.signalType()) {
case SignalType::BOOLEAN:
return lhs.signal_data.b == rhs.signal_data.b;
case SignalType::INTEGER:
return lhs.signal_data.i == rhs.signal_data.i;
case SignalType::FLOAT:
return lhs.signal_data.f == rhs.signal_data.f;
default:
return false;
}
}
bool iec61850::operator!=(GooseSignal &lhs, GooseSignal &rhs) {
return !(lhs == rhs);
}
void GooseNode::onEvent(GooseSubscriber subscriber,
GooseNode::InputEventContext &ctx) noexcept {
if (!GooseSubscriber_isValid(subscriber) ||
GooseSubscriber_needsCommission(subscriber))
return;
int last_state_num = ctx.last_state_num;
int state_num = GooseSubscriber_getStNum(subscriber);
ctx.last_state_num = state_num;
if (ctx.subscriber_config.trigger == InputTrigger::CHANGE &&
!ctx.values.empty() && state_num == last_state_num)
return;
auto mms_values = GooseSubscriber_getDataSetValues(subscriber);
if (MmsValue_getType(mms_values) != MmsType::MMS_ARRAY) {
ctx.node->logger->warn("DataSet is not an array");
return;
}
ctx.values.clear();
for (unsigned int i = 0; i < MmsValue_getArraySize(mms_values); i++) {
auto mms_value = MmsValue_getElement(mms_values, i);
auto goose_value = GooseSignal::fromMmsValue(mms_value);
ctx.values.push_back(goose_value);
}
uint64_t timestamp = GooseSubscriber_getTimestamp(subscriber);
ctx.node->pushSample(timestamp);
}
void GooseNode::pushSample(uint64_t timestamp) noexcept {
Sample *sample = sample_alloc(&input.pool);
if (!sample) {
logger->warn("Pool underrun");
return;
}
sample->length = input.mappings.size();
sample->flags = (int)SampleFlags::HAS_DATA;
sample->signals = getInputSignals(false);
if (input.with_timestamp) {
sample->flags |= (int)SampleFlags::HAS_TS_ORIGIN;
sample->ts.origin.tv_sec = timestamp / 1000;
sample->ts.origin.tv_nsec = 1000 * (timestamp % 1000);
}
for (unsigned int signal = 0; signal < sample->length; signal++) {
auto &mapping = input.mappings[signal];
auto &values = input.contexts[mapping.subscriber].values;
if (mapping.index >= values.size() || !values[mapping.index]) {
auto signal_str = sample->signals->getByIndex(signal)->toString();
logger->error("tried to access unavailable goose value for signal {}",
signal_str);
continue;
}
if (mapping.type->mms_type != values[mapping.index]->mmsType()) {
auto signal_str = sample->signals->getByIndex(signal)->toString();
logger->error("received unexpected mms_type for signal {}", signal_str);
continue;
}
sample->data[signal] = values[mapping.index]->signal_data;
}
if (queue_signalled_push(&input.queue, sample) != 1)
logger->warn("Failed to enqueue samples");
}
void GooseNode::addSubscriber(GooseNode::InputEventContext &ctx) noexcept {
GooseSubscriber subscriber;
SubscriberConfig &sc = ctx.subscriber_config;
ctx.node = this;
subscriber = GooseSubscriber_create(sc.go_cb_ref.data(), NULL);
if (sc.dst_address)
GooseSubscriber_setDstMac(subscriber, sc.dst_address->data());
if (sc.app_id)
GooseSubscriber_setAppId(subscriber, *sc.app_id);
GooseSubscriber_setListener(
subscriber,
[](GooseSubscriber goose_subscriber, void *event_context) {
auto context = static_cast<InputEventContext *>(event_context);
onEvent(goose_subscriber, *context);
},
&ctx);
GooseReceiver_addSubscriber(input.receiver, subscriber);
}
void GooseNode::createReceiver() noexcept {
destroyReceiver();
input.receiver = GooseReceiver_create();
GooseReceiver_setInterfaceId(input.receiver, input.interface_id.c_str());
for (auto &pair_key_context : input.contexts)
addSubscriber(pair_key_context.second);
input.state = Input::READY;
}
void GooseNode::destroyReceiver() noexcept {
int err __attribute__((unused));
if (input.state == Input::NONE)
return;
stopReceiver();
GooseReceiver_destroy(input.receiver);
err = queue_signalled_destroy(&input.queue);
input.state = Input::NONE;
}
void GooseNode::startReceiver() noexcept(false) {
if (input.state == Input::NONE)
createReceiver();
else
stopReceiver();
GooseReceiver_start(input.receiver);
if (!GooseReceiver_isRunning(input.receiver))
throw RuntimeError{"iec61850-GOOSE receiver could not be started"};
input.state = Input::READY;
}
void GooseNode::stopReceiver() noexcept {
if (input.state == Input::NONE)
return;
input.state = Input::STOPPED;
if (!GooseReceiver_isRunning(input.receiver))
return;
GooseReceiver_stop(input.receiver);
}
void GooseNode::createPublishers() noexcept {
destroyPublishers();
for (auto &ctx : output.contexts) {
auto dst_address = ctx.config.dst_address;
auto comm = CommParameters{/* vlanPriority */ 0,
/* vlanId */ 0,
ctx.config.app_id,
{}};
memcpy(comm.dstAddress, dst_address.data(), dst_address.size());
ctx.publisher =
GoosePublisher_createEx(&comm, output.interface_id.c_str(), false);
GoosePublisher_setGoID(ctx.publisher, ctx.config.go_id.data());
GoosePublisher_setGoCbRef(ctx.publisher, ctx.config.go_cb_ref.data());
GoosePublisher_setDataSetRef(ctx.publisher, ctx.config.data_set_ref.data());
GoosePublisher_setConfRev(ctx.publisher, ctx.config.conf_rev);
GoosePublisher_setTimeAllowedToLive(ctx.publisher,
ctx.config.time_allowed_to_live);
}
output.state = Output::READY;
}
void GooseNode::destroyPublishers() noexcept {
int err __attribute__((unused));
if (output.state == Output::NONE)
return;
stopPublishers();
for (auto &ctx : output.contexts)
GoosePublisher_destroy(ctx.publisher);
output.state = Output::NONE;
}
void GooseNode::startPublishers() noexcept(false) {
if (output.state == Output::NONE)
createPublishers();
else
stopPublishers();
output.resend_thread_stop = false;
output.resend_thread = std::thread(resend_thread, &output);
output.state = Output::READY;
}
void GooseNode::stopPublishers() noexcept {
if (output.state == Output::NONE)
return;
if (output.resend_thread) {
auto lock = std::unique_lock{output.send_mutex};
output.resend_thread_stop = true;
lock.unlock();
output.resend_thread_cv.notify_all();
output.resend_thread->join();
output.resend_thread = std::nullopt;
}
output.state = Output::STOPPED;
}
int GooseNode::_read(Sample *samples[], unsigned sample_count) {
int available_samples;
struct Sample *copies[sample_count];
if (input.state != Input::READY)
return 0;
available_samples =
queue_signalled_pull_many(&input.queue, (void **)copies, sample_count);
sample_copy_many(samples, copies, available_samples);
sample_decref_many(copies, available_samples);
return available_samples;
}
void GooseNode::publish_values(GoosePublisher publisher,
std::vector<GooseSignal> &values, bool changed,
int burst) noexcept {
auto data_set = LinkedList_create();
for (auto &value : values) {
LinkedList_add(data_set, value.toMmsValue());
}
if (changed)
GoosePublisher_increaseStNum(publisher);
do {
GoosePublisher_publish(publisher, data_set);
} while (changed && --burst > 0);
LinkedList_destroyDeep(data_set,
(LinkedListValueDeleteFunction)MmsValue_delete);
}
void GooseNode::resend_thread(GooseNode::Output *output) noexcept {
using namespace std::chrono;
auto interval = duration_cast<steady_clock::duration>(
duration<double>(output->resend_interval));
auto lock = std::unique_lock{output->send_mutex};
time_point<steady_clock> next_sample_time;
// wait for the first GooseNode::_write call
output->resend_thread_cv.wait(lock);
while (!output->resend_thread_stop) {
if (output->changed) {
output->changed = false;
next_sample_time = steady_clock::now() + interval;
}
auto status = output->resend_thread_cv.wait_until(lock, next_sample_time);
if (status == std::cv_status::no_timeout || output->changed)
continue;
for (auto &ctx : output->contexts)
publish_values(ctx.publisher, ctx.values, false);
next_sample_time = next_sample_time + interval;
}
}
int GooseNode::_write(Sample *samples[], unsigned sample_count) {
auto lock = std::unique_lock{output.send_mutex};
for (unsigned int i = 0; i < sample_count; i++) {
auto sample = samples[i];
for (auto &ctx : output.contexts) {
bool changed = false;
for (unsigned int data_index = 0; data_index < ctx.config.data.size();
data_index++) {
auto data = ctx.config.data[data_index];
auto goose_value = data.default_value;
auto signal = data.signal;
if (signal && *signal < sample->length)
goose_value.signal_data = sample->data[*signal];
if (ctx.values.size() <= data_index) {
changed = true;
ctx.values.push_back(goose_value);
} else if (ctx.values[data_index] != goose_value) {
changed = true;
ctx.values[data_index] = goose_value;
}
}
if (changed) {
output.changed = true;
publish_values(ctx.publisher, ctx.values, changed, ctx.config.burst);
}
}
}
if (output.changed) {
lock.unlock();
output.resend_thread_cv.notify_all();
}
return sample_count;
}
GooseNode::GooseNode(const uuid_t &id, const std::string &name)
: Node(id, name) {
input.state = Input::NONE;
input.contexts = {};
input.mappings = {};
input.interface_id = "lo";
input.queue_length = 1024;
output.state = Output::NONE;
output.interface_id = "lo";
output.changed = false;
output.resend_interval = 1.;
output.resend_thread = std::nullopt;
}
GooseNode::~GooseNode() {
int err __attribute__((unused));
destroyReceiver();
destroyPublishers();
err = queue_signalled_destroy(&input.queue);
err = pool_destroy(&input.pool);
}
int GooseNode::parse(json_t *json) {
int ret;
json_error_t err;
ret = Node::parse(json);
if (ret)
return ret;
json_t *json_in = nullptr;
json_t *json_out = nullptr;
ret = json_unpack_ex(json, &err, 0, "{ s: o, s: o }", "in", &json_in, "out",
&json_out);
if (ret)
throw ConfigError(json, err, "node-config-node-iec61850-8-1");
parseInput(json_in);
parseOutput(json_out);
return 0;
}
void GooseNode::parseInput(json_t *json) {
int ret;
json_error_t err;
json_t *json_subscribers = nullptr;
json_t *json_signals = nullptr;
char const *interface_id = input.interface_id.c_str();
int with_timestamp = true;
ret = json_unpack_ex(json, &err, 0, "{ s: o, s: o, s?: s, s: b }",
"subscribers", &json_subscribers, "signals",
&json_signals, "interface", &interface_id,
"with_timestamp", &with_timestamp);
if (ret)
throw ConfigError(json, err, "node-config-node-iec61850-8-1");
parseSubscribers(json_subscribers, input.contexts);
parseInputSignals(json_signals, input.mappings);
input.interface_id = interface_id;
input.with_timestamp = with_timestamp;
}
void GooseNode::parseSubscriber(json_t *json, GooseNode::SubscriberConfig &sc) {
int ret;
json_error_t err;
char *go_cb_ref = nullptr;
char *dst_address_str = nullptr;
char *trigger = nullptr;
int app_id = 0;
ret = json_unpack_ex(json, &err, 0, "{ s: s, s?: s, s?: s, s?: i }",
"go_cb_ref", &go_cb_ref, "trigger", &trigger,
"dst_address", &dst_address_str, "app_id", &app_id);
if (ret)
throw ConfigError(json, err, "node-config-node-iec61850-8-1");
sc.go_cb_ref = std::string{go_cb_ref};
if (!trigger || !strcmp(trigger, "always"))
sc.trigger = InputTrigger::ALWAYS;
else if (!strcmp(trigger, "change"))
sc.trigger = InputTrigger::CHANGE;
else
throw RuntimeError("Unknown trigger type");
if (dst_address_str) {
std::optional dst_address = stringToMac(dst_address_str);
if (!dst_address)
throw RuntimeError("Invalid dst_address");
sc.dst_address = *dst_address;
}
if (app_id != 0)
sc.app_id = static_cast<int16_t>(app_id);
}
void GooseNode::parseSubscribers(
json_t *json, std::map<std::string, InputEventContext> &ctx) {
char const *key;
json_t *json_subscriber;
if (!json_is_object(json))
throw RuntimeError("subscribers is not an object");
json_object_foreach(json, key, json_subscriber) {
SubscriberConfig sc;
parseSubscriber(json_subscriber, sc);
ctx[key] = InputEventContext{sc};
}
}
void GooseNode::parseInputSignals(
json_t *json, std::vector<GooseNode::InputMapping> &mappings) {
int ret;
json_error_t err;
int index;
json_t *value;
mappings.clear();
json_array_foreach(json, index, value) {
char *mapping_subscriber;
unsigned int mapping_index;
char *mapping_type_name;
ret = json_unpack_ex(value, &err, 0, "{ s: s, s: i, s: s }", "subscriber",
&mapping_subscriber, "index", &mapping_index,
"mms_type", &mapping_type_name);
if (ret)
throw ConfigError(json, err, "node-config-node-iec61850-8-1");
auto mapping_type =
GooseSignal::lookupMmsTypeName(mapping_type_name).value();
mappings.push_back(InputMapping{
mapping_subscriber,
mapping_index,
mapping_type,
});
}
}
void GooseNode::parseOutput(json_t *json) {
int ret;
json_error_t err;
json_t *json_publishers = nullptr;
json_t *json_signals = nullptr;
char const *interface_id = output.interface_id.c_str();
ret = json_unpack_ex(json, &err, 0, "{ s: o, s: o, s?: s, s?: f }",
"publishers", &json_publishers, "signals", &json_signals,
"interface", &interface_id, "resend_interval",
&output.resend_interval);
if (ret)
throw ConfigError(json, err, "node-config-node-iec61850-8-1");
parsePublishers(json_publishers, output.contexts);
output.interface_id = interface_id;
}
void GooseNode::parsePublisherData(json_t *json,
std::vector<OutputData> &data) {
int ret;
json_error_t err;
int index;
json_t *json_signal_or_value;
if (!json_is_array(json))
throw RuntimeError("publisher data is not an array");
json_array_foreach(json, index, json_signal_or_value) {
char const *mms_type = nullptr;
char const *signal_str = nullptr;
json_t *json_value = nullptr;
int bitstring_size = -1;
ret = json_unpack_ex(json_signal_or_value, &err, 0,
"{ s: s, s?: s, s?: o, s?: i }", "mms_type", &mms_type,
"signal", &signal_str, "value", &json_value,
"mms_bitstring_size", &bitstring_size);
if (ret)
throw ConfigError(json, err, "node-config-node-iec61850-8-1");
auto goose_type = GooseSignal::lookupMmsTypeName(mms_type).value();
std::optional<GooseSignal::Meta> meta = std::nullopt;
if (goose_type->mms_type == MmsType::MMS_BIT_STRING && bitstring_size != -1)
meta = {.size = bitstring_size};
auto signal_data = SignalData{};
if (json_value) {
ret = signal_data.parseJson(goose_type->signal_type, json_value);
if (ret)
throw ConfigError(json, err, "node-config-node-iec61850-8-1");
}
auto signal = std::optional<int>{};
if (signal_str)
signal = out.signals->getIndexByName(signal_str);
OutputData value = {.signal = signal,
.default_value =
GooseSignal{goose_type, signal_data, meta}};
data.push_back(value);
};
}
void GooseNode::parsePublisher(json_t *json, PublisherConfig &pc) {
int ret;
json_error_t err;
char *go_id = nullptr;
char *go_cb_ref = nullptr;
char *data_set_ref = nullptr;
char *dst_address_str = nullptr;
int app_id = 0;
int conf_rev = 0;
int time_allowed_to_live = 0;
int burst = 1;
json_t *json_data = nullptr;
ret = json_unpack_ex(
json, &err, 0,
"{ s: s, s: s, s: s, s: s, s: i, s: i, s: i, s?: i, s: o }", "go_id",
&go_id, "go_cb_ref", &go_cb_ref, "data_set_ref", &data_set_ref,
"dst_address", &dst_address_str, "app_id", &app_id, "conf_rev", &conf_rev,
"time_allowed_to_live", &time_allowed_to_live, "burst", &burst, "data",
&json_data);
if (ret)
throw ConfigError(json, err, "node-config-node-iec61850-8-1");
std::optional dst_address = stringToMac(dst_address_str);
if (!dst_address)
throw RuntimeError("Invalid dst_address");
pc.go_id = std::string{go_id};
pc.go_cb_ref = std::string{go_cb_ref};
pc.data_set_ref = std::string{data_set_ref};
pc.dst_address = *dst_address;
pc.app_id = app_id;
pc.conf_rev = conf_rev;
pc.time_allowed_to_live = time_allowed_to_live;
pc.burst = burst;
parsePublisherData(json_data, pc.data);
}
void GooseNode::parsePublishers(json_t *json, std::vector<OutputContext> &ctx) {
int index;
json_t *json_publisher;
json_array_foreach(json, index, json_publisher) {
PublisherConfig pc;
parsePublisher(json_publisher, pc);
ctx.push_back(OutputContext{pc});
}
}
std::vector<int> GooseNode::getPollFDs() {
return {queue_signalled_fd(&input.queue)};
}
int GooseNode::prepare() {
int ret;
ret = pool_init(&input.pool, input.queue_length,
SAMPLE_LENGTH(getInputSignals(false)->size()));
if (ret)
return ret;
ret = queue_signalled_init(&input.queue, input.queue_length);
if (ret)
return ret;
return Node::prepare();
}
int GooseNode::start() {
if (in.enabled)
startReceiver();
if (out.enabled)
startPublishers();
return Node::start();
}
int GooseNode::stop() {
int err __attribute__((unused));
stopReceiver();
stopPublishers();
err = queue_signalled_close(&input.queue);
return Node::stop();
}
// Register node
static char name[] = "iec61850-8-1";
static char description[] = "IEC 61850-8-1 (GOOSE)";
static NodePlugin<GooseNode, name, description,
(int)NodeFactory::Flags::SUPPORTS_READ |
(int)NodeFactory::Flags::SUPPORTS_WRITE |
(int)NodeFactory::Flags::SUPPORTS_POLL,
1>
p;