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/iec60870.cpp

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

813 lines
26 KiB
C++
Raw Permalink Normal View History

/* Node type: IEC60870-5-104.
*
2022-07-27 11:31:35 +00:00
* Author: Philipp Jungkamp <philipp.jungkamp@rwth-aachen.de>
* SPDX-FileCopyrightText: 2014-2023 Institute for Automation of Complex Power Systems, RWTH Aachen University
2022-07-27 11:31:35 +00:00
* SPDX-License-Identifier: Apache-2.0
*/
2022-07-27 15:46:17 +02:00
#include <algorithm>
2022-07-27 15:46:17 +02:00
#include <villas/exceptions.hpp>
#include <villas/node_compat.hpp>
#include <villas/nodes/iec60870.hpp>
#include <villas/sample.hpp>
#include <villas/super_node.hpp>
#include <villas/utils.hpp>
using namespace villas;
using namespace villas::node;
using namespace villas::utils;
using namespace villas::node::iec60870;
using namespace std::literals::chrono_literals;
2022-07-27 10:42:49 +00:00
static CP56Time2a timespec_to_cp56time2a(timespec time) {
time_t time_ms = static_cast<time_t>(time.tv_sec) * 1000 +
static_cast<time_t>(time.tv_nsec) / 1000000;
return CP56Time2a_createFromMsTimestamp(NULL, time_ms);
}
2022-07-27 10:42:49 +00:00
static timespec cp56time2a_to_timespec(CP56Time2a cp56time2a) {
auto time_ms = CP56Time2a_toMsTimestamp(cp56time2a);
timespec time{};
time.tv_nsec = time_ms % 1000 * 1000;
time.tv_sec = time_ms / 1000;
return time;
}
ASDUData ASDUData::parse(json_t *json_signal, std::optional<ASDUData> last_data,
bool duplicate_ioa_is_sequence) {
json_error_t err;
char const *asdu_type_name = nullptr;
int with_timestamp = -1;
char const *asdu_type_id = nullptr;
std::optional<int> ioa_sequence_start = std::nullopt;
int ioa = -1;
if (json_unpack_ex(json_signal, &err, 0, "{ s?: s, s?: b, s?: s, s: i }",
"asdu_type", &asdu_type_name, "with_timestamp",
&with_timestamp, "asdu_type_id", &asdu_type_id, "ioa",
&ioa))
throw ConfigError(json_signal, err, "node-config-node-iec60870-5-104");
2022-07-27 15:46:17 +02:00
// Increase the ioa if it is found twice to make it a sequence
if (duplicate_ioa_is_sequence && last_data &&
ioa == last_data->ioa_sequence_start) {
ioa = last_data->ioa + 1;
ioa_sequence_start = last_data->ioa_sequence_start;
}
if ((asdu_type_name && asdu_type_id) || (!asdu_type_name && !asdu_type_id))
throw RuntimeError("Please specify one of asdu_type or asdu_type_id", ioa);
auto asdu_data =
asdu_type_name ? ASDUData::lookupName(
asdu_type_name,
with_timestamp != -1 ? with_timestamp != 0 : false,
ioa, ioa_sequence_start.value_or(ioa))
: ASDUData::lookupTypeId(asdu_type_id, ioa,
ioa_sequence_start.value_or(ioa));
if (!asdu_data.has_value())
throw RuntimeError("Found invalid asdu_type or asdu_type_id");
if (asdu_type_id && with_timestamp != -1 &&
asdu_data->hasTimestamp() != with_timestamp)
throw RuntimeError(
"Found mismatch between asdu_type_id {} and with_timestamp {}",
asdu_type_id, with_timestamp != 0);
return *asdu_data;
};
std::optional<ASDUData> ASDUData::lookupTypeId(char const *type_id, int ioa,
int ioa_sequence_start) {
auto check = [type_id](Descriptor descriptor) {
return !strcmp(descriptor.type_id, type_id);
};
auto descriptor = std::find_if(begin(descriptors), end(descriptors), check);
if (descriptor != end(descriptors))
return ASDUData{&*descriptor, ioa, ioa_sequence_start};
else
return std::nullopt;
}
std::optional<ASDUData> ASDUData::lookupName(char const *name,
bool with_timestamp, int ioa,
int ioa_sequence_start) {
auto check = [name, with_timestamp](Descriptor descriptor) {
return !strcmp(descriptor.name, name) &&
descriptor.has_timestamp == with_timestamp;
};
auto descriptor = std::find_if(begin(descriptors), end(descriptors), check);
if (descriptor != end(descriptors))
return ASDUData{&*descriptor, ioa, ioa_sequence_start};
else
return std::nullopt;
}
std::optional<ASDUData> ASDUData::lookupType(int type, int ioa,
int ioa_sequence_start) {
auto check = [type](Descriptor descriptor) {
return descriptor.type == type;
};
auto descriptor = std::find_if(begin(descriptors), end(descriptors), check);
if (descriptor != end(descriptors))
return ASDUData{&*descriptor, ioa, ioa_sequence_start};
else
return std::nullopt;
}
bool ASDUData::hasTimestamp() const { return descriptor->has_timestamp; }
ASDUData::Type ASDUData::type() const { return descriptor->type; }
char const *ASDUData::name() const { return descriptor->name; }
ASDUData::Type ASDUData::typeWithoutTimestamp() const {
return descriptor->type_without_timestamp;
}
2022-05-23 21:06:27 +00:00
ASDUData ASDUData::withoutTimestamp() const {
return ASDUData::lookupType(typeWithoutTimestamp(), ioa, ioa_sequence_start)
.value();
2022-05-23 21:06:27 +00:00
}
SignalType ASDUData::signalType() const { return descriptor->signal_type; }
std::optional<ASDUData::Sample>
ASDUData::checkASDU(CS101_ASDU const &asdu) const {
if (CS101_ASDU_getTypeID(asdu) != static_cast<int>(descriptor->type))
return std::nullopt;
for (int i = 0; i < CS101_ASDU_getNumberOfElements(asdu); i++) {
InformationObject io = CS101_ASDU_getElement(asdu, i);
if (ioa != InformationObject_getObjectAddress(io)) {
InformationObject_destroy(io);
continue;
}
SignalData signal_data;
QualityDescriptor quality;
switch (typeWithoutTimestamp()) {
case ASDUData::SCALED_INT: {
auto scaled_int = reinterpret_cast<MeasuredValueScaled>(io);
2022-07-27 10:42:49 +00:00
int scaled_int_value = MeasuredValueScaled_getValue(scaled_int);
signal_data.i = static_cast<int64_t>(scaled_int_value);
2022-07-27 10:42:49 +00:00
quality = MeasuredValueScaled_getQuality(scaled_int);
break;
}
case ASDUData::NORMALIZED_FLOAT: {
auto normalized_float = reinterpret_cast<MeasuredValueNormalized>(io);
2022-07-27 10:42:49 +00:00
float normalized_float_value =
MeasuredValueNormalized_getValue(normalized_float);
signal_data.f = static_cast<double>(normalized_float_value);
2022-07-27 10:42:49 +00:00
quality = MeasuredValueNormalized_getQuality(normalized_float);
break;
}
case ASDUData::DOUBLE_POINT: {
auto double_point = reinterpret_cast<DoublePointInformation>(io);
2022-07-27 10:42:49 +00:00
DoublePointValue double_point_value =
DoublePointInformation_getValue(double_point);
signal_data.i = static_cast<int64_t>(double_point_value);
quality = DoublePointInformation_getQuality(double_point);
break;
}
case ASDUData::SINGLE_POINT: {
auto single_point = reinterpret_cast<SinglePointInformation>(io);
2022-07-27 10:42:49 +00:00
bool single_point_value = SinglePointInformation_getValue(single_point);
signal_data.b = static_cast<bool>(single_point_value);
quality = SinglePointInformation_getQuality(single_point);
break;
}
case ASDUData::SHORT_FLOAT: {
auto short_float = reinterpret_cast<MeasuredValueShort>(io);
2022-07-27 10:42:49 +00:00
float short_float_value = MeasuredValueShort_getValue(short_float);
signal_data.f = static_cast<double>(short_float_value);
2022-07-27 10:42:49 +00:00
quality = MeasuredValueShort_getQuality(short_float);
break;
}
default:
throw RuntimeError{"unsupported asdu type"};
}
std::optional<CP56Time2a> time_cp56;
switch (type()) {
case ASDUData::SCALED_INT_WITH_TIMESTAMP: {
auto scaled_int = reinterpret_cast<MeasuredValueScaledWithCP56Time2a>(io);
2022-07-27 10:42:49 +00:00
time_cp56 = MeasuredValueScaledWithCP56Time2a_getTimestamp(scaled_int);
break;
}
case ASDUData::NORMALIZED_FLOAT_WITH_TIMESTAMP: {
auto normalized_float =
reinterpret_cast<MeasuredValueNormalizedWithCP56Time2a>(io);
2022-07-27 10:42:49 +00:00
time_cp56 =
MeasuredValueNormalizedWithCP56Time2a_getTimestamp(normalized_float);
break;
}
case ASDUData::DOUBLE_POINT_WITH_TIMESTAMP: {
auto double_point = reinterpret_cast<DoublePointWithCP56Time2a>(io);
time_cp56 = DoublePointWithCP56Time2a_getTimestamp(double_point);
break;
}
case ASDUData::SINGLE_POINT_WITH_TIMESTAMP: {
auto single_point = reinterpret_cast<SinglePointWithCP56Time2a>(io);
time_cp56 = SinglePointWithCP56Time2a_getTimestamp(single_point);
break;
}
case ASDUData::SHORT_FLOAT_WITH_TIMESTAMP: {
auto short_float = reinterpret_cast<MeasuredValueShortWithCP56Time2a>(io);
2022-07-27 10:42:49 +00:00
time_cp56 = MeasuredValueShortWithCP56Time2a_getTimestamp(short_float);
break;
}
default:
2022-07-27 15:46:17 +02:00
time_cp56 = std::nullopt;
}
InformationObject_destroy(io);
std::optional<timespec> timestamp =
time_cp56.has_value()
? std::optional{cp56time2a_to_timespec(*time_cp56)}
: std::nullopt;
return ASDUData::Sample{signal_data, quality, timestamp};
}
return std::nullopt;
}
bool ASDUData::addSampleToASDU(CS101_ASDU &asdu,
ASDUData::Sample sample) const {
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wmaybe-uninitialized"
std::optional<CP56Time2a> timestamp =
sample.timestamp.has_value()
? std::optional{timespec_to_cp56time2a(*sample.timestamp)}
: std::nullopt;
InformationObject io = nullptr;
switch (descriptor->type) {
2022-07-27 10:42:49 +00:00
case ASDUData::SCALED_INT: {
auto scaled_int_value = static_cast<int16_t>(sample.signal_data.i & 0xFFFF);
2022-07-27 10:42:49 +00:00
auto scaled_int =
MeasuredValueScaled_create(NULL, ioa, scaled_int_value, sample.quality);
io = reinterpret_cast<InformationObject>(scaled_int);
2022-07-27 10:42:49 +00:00
break;
}
2022-07-27 10:42:49 +00:00
case ASDUData::NORMALIZED_FLOAT: {
auto normalized_float_value = static_cast<float>(sample.signal_data.f);
2022-07-27 10:42:49 +00:00
auto normalized_float = MeasuredValueNormalized_create(
NULL, ioa, normalized_float_value, sample.quality);
io = reinterpret_cast<InformationObject>(normalized_float);
2022-07-27 10:42:49 +00:00
break;
}
2022-07-27 10:42:49 +00:00
case ASDUData::DOUBLE_POINT: {
auto double_point_value =
static_cast<DoublePointValue>(sample.signal_data.i & 0x3);
2022-07-27 10:42:49 +00:00
auto double_point = DoublePointInformation_create(
NULL, ioa, double_point_value, sample.quality);
io = reinterpret_cast<InformationObject>(double_point);
2022-07-27 10:42:49 +00:00
break;
}
2022-07-27 10:42:49 +00:00
case ASDUData::SINGLE_POINT: {
auto single_point_value = sample.signal_data.b;
auto single_point = SinglePointInformation_create(
NULL, ioa, single_point_value, sample.quality);
io = reinterpret_cast<InformationObject>(single_point);
2022-07-27 10:42:49 +00:00
break;
}
2022-07-27 10:42:49 +00:00
case ASDUData::SHORT_FLOAT: {
auto short_float_value = static_cast<float>(sample.signal_data.f);
2022-07-27 10:42:49 +00:00
auto short_float =
MeasuredValueShort_create(NULL, ioa, short_float_value, sample.quality);
io = reinterpret_cast<InformationObject>(short_float);
2022-07-27 10:42:49 +00:00
break;
}
2022-07-27 10:42:49 +00:00
case ASDUData::SCALED_INT_WITH_TIMESTAMP: {
auto scaled_int_value = static_cast<int16_t>(sample.signal_data.i & 0xFFFF);
2022-07-27 10:42:49 +00:00
auto scaled_int = MeasuredValueScaledWithCP56Time2a_create(
NULL, ioa, scaled_int_value, sample.quality, timestamp.value());
io = reinterpret_cast<InformationObject>(scaled_int);
2022-07-27 10:42:49 +00:00
break;
}
2022-07-27 10:42:49 +00:00
case ASDUData::NORMALIZED_FLOAT_WITH_TIMESTAMP: {
auto normalized_float_value = static_cast<float>(sample.signal_data.f);
2022-07-27 10:42:49 +00:00
auto normalized_float = MeasuredValueNormalizedWithCP56Time2a_create(
NULL, ioa, normalized_float_value, sample.quality, timestamp.value());
io = reinterpret_cast<InformationObject>(normalized_float);
2022-07-27 10:42:49 +00:00
break;
}
2022-07-27 10:42:49 +00:00
case ASDUData::DOUBLE_POINT_WITH_TIMESTAMP: {
auto double_point_value =
static_cast<DoublePointValue>(sample.signal_data.i & 0x3);
2022-07-27 10:42:49 +00:00
auto double_point = DoublePointWithCP56Time2a_create(
NULL, ioa, double_point_value, sample.quality, timestamp.value());
io = reinterpret_cast<InformationObject>(double_point);
2022-07-27 10:42:49 +00:00
break;
}
2022-07-27 10:42:49 +00:00
case ASDUData::SINGLE_POINT_WITH_TIMESTAMP: {
auto single_point_value = sample.signal_data.b;
auto single_point = SinglePointWithCP56Time2a_create(
NULL, ioa, single_point_value, sample.quality, timestamp.value());
io = reinterpret_cast<InformationObject>(single_point);
2022-07-27 10:42:49 +00:00
break;
}
2022-07-27 10:42:49 +00:00
case ASDUData::SHORT_FLOAT_WITH_TIMESTAMP: {
auto short_float_value = static_cast<float>(sample.signal_data.f);
2022-07-27 10:42:49 +00:00
auto short_float = MeasuredValueShortWithCP56Time2a_create(
NULL, ioa, short_float_value, sample.quality, timestamp.value());
io = reinterpret_cast<InformationObject>(short_float);
2022-07-27 10:42:49 +00:00
break;
}
2022-07-27 15:46:17 +02:00
default:
throw RuntimeError{"invalid asdu data type"};
}
bool successfully_added = CS101_ASDU_addInformationObject(asdu, io);
InformationObject_destroy(io);
return successfully_added;
#pragma GCC diagnostic pop
}
ASDUData::ASDUData(ASDUData::Descriptor const *descriptor, int ioa,
int ioa_sequence_start)
: ioa(ioa), ioa_sequence_start(ioa_sequence_start), descriptor(descriptor) {
}
void SlaveNode::createSlave() noexcept {
2022-07-27 15:46:17 +02:00
// Destroy slave id it was already created
destroySlave();
2022-07-27 15:46:17 +02:00
// Create the slave object
server.slave =
CS104_Slave_create(server.low_priority_queue, server.high_priority_queue);
CS104_Slave_setServerMode(server.slave, CS104_MODE_SINGLE_REDUNDANCY_GROUP);
CS104_Slave_setMaxOpenConnections(server.slave, 1);
2022-07-27 15:46:17 +02:00
// Configure the slave according to config
server.asdu_app_layer_parameters =
CS104_Slave_getAppLayerParameters(server.slave);
CS104_APCIParameters apci_parameters =
CS104_Slave_getConnectionParameters(server.slave);
2022-07-27 15:46:17 +02:00
if (server.apci_t0)
apci_parameters->t0 = *server.apci_t0;
2022-07-27 15:46:17 +02:00
if (server.apci_t1)
apci_parameters->t1 = *server.apci_t1;
2022-07-27 15:46:17 +02:00
if (server.apci_t2)
apci_parameters->t2 = *server.apci_t2;
2022-07-27 15:46:17 +02:00
if (server.apci_t3)
apci_parameters->t3 = *server.apci_t3;
2022-07-27 15:46:17 +02:00
if (server.apci_k)
apci_parameters->k = *server.apci_k;
2022-07-27 15:46:17 +02:00
if (server.apci_w)
apci_parameters->w = *server.apci_w;
CS104_Slave_setLocalAddress(server.slave, server.local_address.c_str());
CS104_Slave_setLocalPort(server.slave, server.local_port);
2022-07-27 15:46:17 +02:00
// Setup callbacks into the class
CS104_Slave_setClockSyncHandler(
server.slave,
[](void *tcp_node, IMasterConnection connection, CS101_ASDU asdu,
CP56Time2a new_time) {
auto self = static_cast<SlaveNode const *>(tcp_node);
return self->onClockSync(connection, asdu, new_time);
},
this);
CS104_Slave_setInterrogationHandler(
server.slave,
[](void *tcp_node, IMasterConnection connection, CS101_ASDU asdu,
QualifierOfInterrogation qoi) {
auto self = static_cast<SlaveNode const *>(tcp_node);
return self->onInterrogation(connection, asdu, qoi);
},
this);
CS104_Slave_setASDUHandler(
server.slave,
[](void *tcp_node, IMasterConnection connection, CS101_ASDU asdu) {
auto self = static_cast<SlaveNode const *>(tcp_node);
return self->onASDU(connection, asdu);
},
this);
CS104_Slave_setConnectionEventHandler(
server.slave,
[](void *tcp_node, IMasterConnection connection,
CS104_PeerConnectionEvent event) {
auto self = static_cast<SlaveNode const *>(tcp_node);
self->debugPrintConnection(connection, event);
},
this);
CS104_Slave_setRawMessageHandler(
server.slave,
[](void *tcp_node, IMasterConnection connection, uint8_t *message,
int message_size, bool sent) {
auto self = static_cast<SlaveNode const *>(tcp_node);
self->debugPrintMessage(connection, message, message_size, sent);
},
this);
server.state = SlaveNode::Server::READY;
}
2022-07-27 15:46:17 +02:00
void SlaveNode::destroySlave() noexcept {
2022-07-27 15:46:17 +02:00
if (server.state == SlaveNode::Server::NONE)
return;
stopSlave();
CS104_Slave_destroy(server.slave);
server.state = SlaveNode::Server::NONE;
}
2022-07-27 15:46:17 +02:00
void SlaveNode::startSlave() noexcept(false) {
2022-07-27 15:46:17 +02:00
if (server.state == SlaveNode::Server::NONE)
createSlave();
else
stopSlave();
2022-07-27 15:46:17 +02:00
server.state = SlaveNode::Server::READY;
CS104_Slave_start(server.slave);
if (!CS104_Slave_isRunning(server.slave))
throw std::runtime_error{"iec60870-5-104 server could not be started"};
}
void SlaveNode::stopSlave() noexcept {
if (server.state != SlaveNode::Server::READY ||
!CS104_Slave_isRunning(server.slave))
return;
server.state = SlaveNode::Server::STOPPED;
if (CS104_Slave_getNumberOfQueueEntries(server.slave, NULL) != 0)
logger->info("Waiting for last messages in queue");
// Wait for all messages to be send before really stopping
while ((CS104_Slave_getNumberOfQueueEntries(server.slave, NULL) != 0) &&
(CS104_Slave_getOpenConnections(server.slave) != 0))
std::this_thread::sleep_for(100ms);
CS104_Slave_stop(server.slave);
}
void SlaveNode::debugPrintMessage(IMasterConnection connection,
uint8_t *message, int message_size,
bool sent) const noexcept {
/// TODO: debug print the message bytes as trace
}
void SlaveNode::debugPrintConnection(
IMasterConnection connection,
CS104_PeerConnectionEvent event) const noexcept {
switch (event) {
case CS104_CON_EVENT_CONNECTION_OPENED:
logger->info("Client connected");
break;
2022-07-27 10:42:49 +00:00
case CS104_CON_EVENT_CONNECTION_CLOSED:
logger->info("Client disconnected");
break;
case CS104_CON_EVENT_ACTIVATED:
2022-07-27 15:46:17 +02:00
logger->info("Connection activated");
break;
2022-07-27 10:42:49 +00:00
case CS104_CON_EVENT_DEACTIVATED:
2022-07-27 15:46:17 +02:00
logger->info("Connection closed");
break;
}
}
bool SlaveNode::onClockSync(IMasterConnection connection, CS101_ASDU asdu,
CP56Time2a new_time) const noexcept {
logger->warn("Received clock sync command (unimplemented)");
return true;
}
bool SlaveNode::onInterrogation(IMasterConnection connection, CS101_ASDU asdu,
QualifierOfInterrogation qoi) const noexcept {
switch (qoi) {
// Send last values without timestamps
case IEC60870_QOI_STATION: {
IMasterConnection_sendACT_CON(connection, asdu, false);
logger->debug("Received general interrogation");
auto guard = std::lock_guard{output.last_values_mutex};
for (auto const &asdu_type : output.asdu_types) {
for (unsigned i = 0; i < output.mapping.size();) {
auto signal_asdu = CS101_ASDU_create(
IMasterConnection_getApplicationLayerParameters(connection), false,
CS101_COT_INTERROGATED_BY_STATION, 0, server.common_address, false,
false);
do {
auto asdu_data = output.mapping[i];
auto last_value = output.last_values[i];
if (asdu_data.type() != asdu_type)
2022-07-27 15:46:17 +02:00
continue;
if (!asdu_data.withoutTimestamp().addSampleToASDU(
signal_asdu,
ASDUData::Sample{last_value, IEC60870_QUALITY_GOOD,
std::nullopt}))
break;
} while (++i < output.mapping.size());
if (CS101_ASDU_getNumberOfElements(asdu) != 0)
IMasterConnection_sendASDU(connection, signal_asdu);
CS101_ASDU_destroy(signal_asdu);
}
}
IMasterConnection_sendACT_TERM(connection, asdu);
2022-07-27 10:42:49 +00:00
break;
}
// Negative acknowledgement
default:
IMasterConnection_sendACT_CON(connection, asdu, true);
2022-07-27 15:46:17 +02:00
logger->warn("Ignoring interrogation type {}", qoi);
}
2022-07-27 10:42:49 +00:00
return true;
}
bool SlaveNode::onASDU(IMasterConnection connection,
CS101_ASDU asdu) const noexcept {
2022-07-27 15:46:17 +02:00
logger->warn("Ignoring ASDU type {}", CS101_ASDU_getTypeID(asdu));
return true;
}
void SlaveNode::sendPeriodicASDUsForSample(Sample const *sample) const
noexcept(false) {
2022-07-27 15:46:17 +02:00
// ASDUs may only carry one type of ASDU
for (auto const &type : output.asdu_types) {
2022-07-27 15:46:17 +02:00
// Search all occurrences of this ASDU type
for (unsigned signal = 0;
signal < MIN(sample->length, output.mapping.size());) {
2022-07-27 15:46:17 +02:00
// Create an ASDU for periodic transmission
CS101_ASDU asdu = CS101_ASDU_create(server.asdu_app_layer_parameters, 0,
CS101_COT_PERIODIC, 0,
server.common_address, false, false);
do {
auto &asdu_data = output.mapping[signal];
2022-07-27 15:46:17 +02:00
// This signal_data does not belong in this ASDU
if (asdu_data.type() != type)
continue;
auto timestamp = (sample->flags & (int)SampleFlags::HAS_TS_ORIGIN)
? std::optional{sample->ts.origin}
: std::nullopt;
if (asdu_data.hasTimestamp() && !timestamp.has_value())
throw RuntimeError("Received sample without timestamp for ASDU type "
"with mandatory timestamp");
if (asdu_data.signalType() != sample_format(sample, signal))
throw RuntimeError("Expected signal type {}, but received {}",
signalTypeToString(asdu_data.signalType()),
signalTypeToString(sample_format(sample, signal)));
if (asdu_data.addSampleToASDU(asdu,
ASDUData::Sample{sample->data[signal],
IEC60870_QUALITY_GOOD,
timestamp}) == false)
// ASDU is full -> dispatch -> create a new one
break;
} while (++signal < MIN(sample->length, output.mapping.size()));
if (CS101_ASDU_getNumberOfElements(asdu) != 0)
CS104_Slave_enqueueASDU(server.slave, asdu);
CS101_ASDU_destroy(asdu);
}
}
}
int SlaveNode::_write(Sample *samples[], unsigned sample_count) {
if (server.state != SlaveNode::Server::READY)
return -1;
2022-07-27 15:46:17 +02:00
for (unsigned sample_index = 0; sample_index < sample_count; sample_index++) {
Sample const *sample = samples[sample_index];
2022-07-27 15:46:17 +02:00
// Update last_values
output.last_values_mutex.lock();
for (unsigned i = 0; i < MIN(sample->length, output.last_values.size());
i++)
output.last_values[i] = sample->data[i];
output.last_values_mutex.unlock();
sendPeriodicASDUsForSample(sample);
}
return sample_count;
}
SlaveNode::SlaveNode(const uuid_t &id, const std::string &name)
: Node(id, name) {
server.state = SlaveNode::Server::NONE;
2022-07-27 15:46:17 +02:00
// Server config (use explicit defaults)
server.local_address = "0.0.0.0";
server.local_port = 2404;
server.common_address = 1;
server.low_priority_queue = 100;
server.high_priority_queue = 100;
// Config (use lib60870 defaults if std::nullopt)
server.apci_t0 = std::nullopt;
server.apci_t1 = std::nullopt;
server.apci_t2 = std::nullopt;
server.apci_t3 = std::nullopt;
server.apci_k = std::nullopt;
server.apci_w = std::nullopt;
2022-07-27 15:46:17 +02:00
// Output config
output.enabled = false;
output.mapping = {};
output.asdu_types = {};
output.last_values = {};
}
SlaveNode::~SlaveNode() { destroySlave(); }
int SlaveNode::parse(json_t *json) {
int ret = Node::parse(json);
if (ret)
return ret;
json_error_t err;
auto signals = getOutputSignals();
json_t *json_out = nullptr;
char const *address = nullptr;
2022-07-27 10:06:16 +00:00
int apci_t0 = -1;
int apci_t1 = -1;
int apci_t2 = -1;
int apci_t3 = -1;
int apci_k = -1;
int apci_w = -1;
2022-07-27 15:46:17 +02:00
ret = json_unpack_ex(
json, &err, 0,
"{ s?: o, s?: s, s?: i, s?: i, s?: i, s?: i, s?: i, s?: i, s?: i, s?: i, "
"s?: i, s?: i }",
"out", &json_out, "address", &address, "port", &server.local_port, "ca",
&server.common_address, "low_priority_queue", &server.low_priority_queue,
2022-07-27 10:06:16 +00:00
"high_priority_queue", &server.high_priority_queue, "apci_t0", &apci_t0,
"apci_t1", &apci_t1, "apci_t2", &apci_t2, "apci_t3", &apci_t3, "apci_k",
&apci_k, "apci_w", &apci_w);
2022-07-27 15:46:17 +02:00
if (ret)
throw ConfigError(json, err, "node-config-node-iec60870-5-104");
2022-07-27 15:46:17 +02:00
if (apci_t0 != -1)
server.apci_t0 = apci_t0;
2022-07-27 15:46:17 +02:00
if (apci_t1 != -1)
server.apci_t1 = apci_t1;
2022-07-27 15:46:17 +02:00
if (apci_t2 != -1)
server.apci_t2 = apci_t2;
2022-07-27 15:46:17 +02:00
if (apci_t3 != -1)
server.apci_t3 = apci_t3;
2022-07-27 15:46:17 +02:00
if (apci_k != -1)
server.apci_k = apci_k;
2022-07-27 15:46:17 +02:00
if (apci_w != -1)
server.apci_w = apci_w;
if (address)
server.local_address = address;
json_t *json_signals = nullptr;
int duplicate_ioa_is_sequence = false;
if (json_out) {
output.enabled = true;
ret = json_unpack_ex(json_out, &err, 0, "{ s: o, s?: b }", "signals",
&json_signals, "duplicate_ioa_is_sequence",
&duplicate_ioa_is_sequence);
2022-07-27 15:46:17 +02:00
if (ret)
throw ConfigError(json_out, err, "node-config-node-iec60870-5-104");
}
if (json_signals) {
json_t *json_signal;
size_t i;
std::optional<ASDUData> last_data = std::nullopt;
json_array_foreach(json_signals, i, json_signal) {
auto signal = signals ? signals->getByIndex(i) : Signal::Ptr{};
auto asdu_data =
ASDUData::parse(json_signal, last_data, duplicate_ioa_is_sequence);
last_data = asdu_data;
SignalData initial_value;
if (signal) {
2022-07-27 15:46:17 +02:00
if (signal->type != asdu_data.signalType()) {
throw RuntimeError(
"Type mismatch! Expected type {} for signal {}, but found {}",
signalTypeToString(asdu_data.signalType()), signal->name,
signalTypeToString(signal->type));
2022-07-27 15:46:17 +02:00
}
switch (signal->type) {
2022-07-27 10:42:49 +00:00
case SignalType::BOOLEAN:
initial_value.b = false;
break;
2022-07-27 10:42:49 +00:00
case SignalType::INTEGER:
initial_value.i = 0;
break;
2022-07-27 10:42:49 +00:00
case SignalType::FLOAT:
initial_value.f = 0;
break;
2022-07-27 15:46:17 +02:00
default:
throw RuntimeError{"unsupported signal type"};
}
} else
initial_value.f = 0.0;
output.mapping.push_back(asdu_data);
output.last_values.push_back(initial_value);
}
}
for (auto const &asdu_data : output.mapping) {
if (std::find(begin(output.asdu_types), end(output.asdu_types),
asdu_data.type()) == end(output.asdu_types))
output.asdu_types.push_back(asdu_data.type());
}
return 0;
}
int SlaveNode::start() {
startSlave();
2022-07-27 15:56:32 +02:00
return Node::start();
}
int SlaveNode::stop() {
stopSlave();
2022-07-27 15:56:32 +02:00
return Node::stop();
}
2022-07-27 15:46:17 +02:00
// Register node
static char name[] = "iec60870-5-104";
static char description[] = "Provide values as protocol slave";
2022-07-27 15:56:32 +02:00
static NodePlugin<SlaveNode, name, description,
(int)NodeFactory::Flags::SUPPORTS_WRITE, 1>
p;