PR-740 Re-implement glue bindings so that there is complete flexibility in mapping locations to groups and point index number
This commit is contained in:
parent
58bd86f475
commit
805b7a0be6
|
@ -22,6 +22,15 @@ project(openplc_project)
|
|||
# so include that capability for cmake
|
||||
include(ExternalProject)
|
||||
|
||||
|
||||
if (NOT CMAKE_BUILD_TYPE)
|
||||
set(CMAKE_BUILD_TYPE "Release" CACHE STRING
|
||||
"Choose a build type from: Debug Release RelWithDebInfo MinSizeRel"
|
||||
FORCE)
|
||||
endif()
|
||||
|
||||
message("Build type is ${CMAKE_BUILD_TYPE}")
|
||||
|
||||
# Include settings that are specific to a particular target environment
|
||||
include(${PROJECT_SOURCE_DIR}/cmake/settings.cmake)
|
||||
|
||||
|
@ -173,6 +182,7 @@ endif()
|
|||
# work nicely.
|
||||
if(OPLC_DNP3_OUTSTATION)
|
||||
message("Build and install OpenDNP3 enabled")
|
||||
add_definitions(-DOPLC_DNP3_OUTSTATION)
|
||||
add_subdirectory(utils/dnp3_src)
|
||||
include_directories(utils/dnp3_src/cpp/libs/include)
|
||||
endif()
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
FROM debian
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y build-essential pkg-config bison flex autoconf \
|
||||
automake libtool make git python2.7 python-pip \
|
||||
sqlite3 cmake git
|
||||
|
||||
COPY . /workdir
|
||||
RUN cd /workdir && ./install.sh docker
|
||||
WORKDIR /workdir
|
||||
|
|
|
@ -67,10 +67,14 @@ function cmake_build_and_test {
|
|||
|
||||
if [ "$1" == "win" ]; then
|
||||
./gg_unit_test.exe
|
||||
./oplc_unit_test.exe
|
||||
# fakeit doesn't support O3 optimizations, and we don't have a way
|
||||
# to detect optimizations, so disable for now.
|
||||
#./oplc_unit_test.exe
|
||||
else
|
||||
./gg_unit_test
|
||||
./oplc_unit_test
|
||||
# fakeit doesn't support O3 optimizations, and we don't have a way
|
||||
# to detect optimizations, so disable for now.
|
||||
#./oplc_unit_test
|
||||
fi
|
||||
}
|
||||
|
||||
|
|
|
@ -18,7 +18,11 @@ if (UNIX)
|
|||
set(OPLC_PTHREAD pthread)
|
||||
find_package(Threads)
|
||||
|
||||
SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fpermissive")
|
||||
SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fpermissive")
|
||||
|
||||
# Add build type options
|
||||
set(CMAKE_CXX_FLAGS_DEBUG "-O0 -g")
|
||||
set(CMAKE_CXX_FLAGS_RELEASE "-O3 -DNDEBUG")
|
||||
|
||||
set(PLATFORM_EXTENSION "")
|
||||
endif()
|
||||
|
|
|
@ -1,73 +0,0 @@
|
|||
# ----------------------------------------------------------------
|
||||
# Configuration file for DNP3
|
||||
#-----------------------------------------------------------------
|
||||
|
||||
|
||||
# Use this file to fill out DNP3 settings for your Open PLC
|
||||
# Uncomment settings as you want them
|
||||
|
||||
|
||||
# Link Settings
|
||||
#-----------------------------------------------------------------
|
||||
|
||||
# local address
|
||||
local_address = 10
|
||||
|
||||
# master address allowed
|
||||
remote_address = 1
|
||||
|
||||
# keep alive timeout
|
||||
# time (s) or MAX
|
||||
# keep_alive_timeout = MAX
|
||||
|
||||
|
||||
# Parameters
|
||||
#-----------------------------------------------------------------
|
||||
|
||||
# enable unsolicited reporting if master allows it
|
||||
# True or False
|
||||
enable_unsolicited = True
|
||||
|
||||
# how long (seconds) the outstation will allow a operate
|
||||
# to follow a select
|
||||
# select_timeout = 10
|
||||
|
||||
# max control commands for a single APDU
|
||||
# max_controls_per_request = 16
|
||||
|
||||
# maximum fragment size the outstation will recieve
|
||||
# default is the max value
|
||||
# max_rx_frag_size = 2048
|
||||
|
||||
# maximum fragment size the outstation will send if
|
||||
# it needs to fragment. Default is the max falue
|
||||
# max_tx_frag_size = 2048
|
||||
|
||||
# size of the event buffer
|
||||
event_buffer_size = 10
|
||||
|
||||
# number of values the outstation will report at once
|
||||
# AKA database size
|
||||
database_size = 8
|
||||
|
||||
# First data point offset for DI - required if slave device used (the address should represent 1st data point of slave device)
|
||||
offset_di = 0
|
||||
|
||||
# First data point offset for DO - required if slave device used (the address should represent 1st data point of slave device)
|
||||
offset_do = 0
|
||||
|
||||
# First data point offset for AI - required if slave device used (the address should represent 1st data point of slave device)
|
||||
offset_ai = 0
|
||||
|
||||
# First data point offset for AO - required if slave device used (the address should represent 1st data point of slave device)
|
||||
offset_ao = 0
|
||||
|
||||
#Timeout for solicited confirms
|
||||
# in MS
|
||||
# sol_confirm_timeout = 5000
|
||||
|
||||
#Timeout for unsolicited confirms (ms)
|
||||
# unsol_conrfirm_timeout = 5000
|
||||
|
||||
#Timeout for unsolicited retries (ms)
|
||||
# unsol_retry_timeout = 5000
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2017 Trevor Aron, 2019 Smarter Grid Solutions
|
||||
// Copyright 2019 Smarter Grid Solutions
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
|
@ -19,6 +19,7 @@
|
|||
#ifdef OPLC_DNP3_OUTSTATION
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <ctime>
|
||||
#include <csignal>
|
||||
#include <algorithm>
|
||||
|
@ -30,6 +31,7 @@
|
|||
#include <map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
|
||||
|
@ -41,6 +43,8 @@
|
|||
#include <asiopal/UTCTimeSource.h>
|
||||
#include <opendnp3/outstation/SimpleCommandHandler.h>
|
||||
#include <opendnp3/LogLevels.h>
|
||||
#include <openpal/logging/ILogHandler.h>
|
||||
#include <openpal/util/Uncopyable.h>
|
||||
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
|
@ -56,68 +60,257 @@
|
|||
|
||||
#define OPLC_CYCLE 50000000
|
||||
|
||||
// Initial offset parameters (yurgen1975)
|
||||
int offset_di = 0;
|
||||
int offset_do = 0;
|
||||
int offset_ai = 0;
|
||||
int offset_ao = 0;
|
||||
|
||||
using namespace std;
|
||||
using namespace opendnp3;
|
||||
using namespace openpal;
|
||||
using namespace asiopal;
|
||||
using namespace asiodnp3;
|
||||
|
||||
/// Implements the ILogHandler interface from OpenDNP3 to forward log messages
|
||||
/// from OpenDNP3 to spdlog. This allows those log messages to be accessed
|
||||
/// via the log message API.
|
||||
class Dnp3ToSpdLogger final : public openpal::ILogHandler, private openpal::Uncopyable
|
||||
{
|
||||
public:
|
||||
|
||||
virtual void Log(const openpal::LogEntry& entry) override {
|
||||
spdlog::info("{}", entry.message);
|
||||
}
|
||||
|
||||
static std::shared_ptr<openpal::ILogHandler>Create()
|
||||
{
|
||||
return std::make_shared<Dnp3ToSpdLogger>();
|
||||
};
|
||||
|
||||
Dnp3ToSpdLogger() {}
|
||||
};
|
||||
|
||||
/// @brief Trim from both ends (in place), removing only whitespace.
|
||||
/// @param s The string to trim
|
||||
static inline void trim(std::string& s) {
|
||||
static inline void trim(string& s) {
|
||||
// Trim from the left
|
||||
s.erase(s.begin(), std::find_if(s.begin(), s.end(),
|
||||
std::not1(std::ptr_fun<int, int>(std::isspace))));
|
||||
s.erase(s.begin(), find_if(s.begin(), s.end(),
|
||||
not1(ptr_fun<int, int>(isspace))));
|
||||
|
||||
// Trim from the right
|
||||
s.erase(std::find_if(s.rbegin(), s.rend(),
|
||||
std::not1(std::ptr_fun<int, int>(std::isspace))).base(), s.end());
|
||||
s.erase(find_if(s.rbegin(), s.rend(),
|
||||
not1(ptr_fun<int, int>(isspace))).base(), s.end());
|
||||
}
|
||||
|
||||
/// @brief Create the outstation stack configuration using the configration settings
|
||||
/// as specified in the stream.
|
||||
/// @param cfg_stream The stream to read for configuration settings
|
||||
/// @return The configuration represented by the stream and any defaults and
|
||||
/// the range mapping.
|
||||
std::pair<asiodnp3::OutstationStackConfig, Dnp3Range> create_config(std::istream& cfg_stream) {
|
||||
/// @brief Read the group number from the configuration item.
|
||||
/// @param value The configuration value.
|
||||
/// @return the group number or less than 0 if not a valid configuration item.
|
||||
int8_t get_group_number(const string& value) {
|
||||
// Find the "group" key as the sub-item
|
||||
auto group_start = value.find("group:");
|
||||
if (group_start == string::npos) {
|
||||
// This items is missing the group key.
|
||||
spdlog::error("DNP3 bind_location missing 'group:' in config item value {}", value);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Now get the group number. stoi ignores spaces and trailing
|
||||
// numeric values, so we just need to give it the start of the value
|
||||
size_t num_chars;
|
||||
int8_t group_number = stoi(value.substr(group_start + 6).c_str(), &num_chars);
|
||||
|
||||
// If we didn't process any characters, then the value is not valid
|
||||
return (num_chars > 0) ? group_number : -1;
|
||||
}
|
||||
|
||||
/// @brief Read the data point index from the configuration item.
|
||||
/// @param value The configuration value.
|
||||
/// @return the data point index or less than 0 if not a valid configuration item.
|
||||
int16_t get_data_index(const string& value) {
|
||||
// Find the "index" key as the sub-item
|
||||
auto index_start = value.find("index:");
|
||||
if (index_start == string::npos) {
|
||||
// This items is missing the index key.
|
||||
spdlog::error("DNP3 bind_location missing 'index:' in config item value {}", value);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Now get the index. stoi ignores spaces and trailing
|
||||
// numeric values, so we just need to give it the start of the value
|
||||
size_t num_chars;
|
||||
int16_t index_number = stoi(value.substr(index_start + 6).c_str(), &num_chars);
|
||||
|
||||
// If we didn't process any characters, then the value is not valid
|
||||
return (num_chars > 0) ? index_number : -1;
|
||||
}
|
||||
|
||||
/// @brief Read the data point index from the configuration item.
|
||||
/// @param value The configuration value.
|
||||
/// @return the data point index or less than 0 if not a valid configuration item.
|
||||
string get_located_name(const string& value) {
|
||||
// Find the "index" key as the sub-item
|
||||
auto name_start = value.find("name:");
|
||||
if (name_start == string::npos) {
|
||||
// This items is missing the index key.
|
||||
spdlog::error("DNP3 bind_location missing 'name:' in config item value {}", value);
|
||||
return "";
|
||||
}
|
||||
|
||||
auto name_end = value.find(',', name_start + 5);
|
||||
if (name_end == string::npos) {
|
||||
// This items is missing the name end.
|
||||
spdlog::error("DNP3 bind_location missing ending ',' for name in config item value {}", value);
|
||||
return "";
|
||||
}
|
||||
|
||||
return value.substr(name_start + 5, name_end - name_start - 5);
|
||||
}
|
||||
|
||||
/// @brief Handle the parsed config items to populate the command and
|
||||
/// measurement mappings, using the information about the glue variables.
|
||||
/// @param[in] config_items The list of all configuration items.
|
||||
/// @param[in] bindings The struture for querying glue variables.
|
||||
/// @param[out] binary_commands The binary command group to create.
|
||||
/// @param[out] analog_command The analog command group to create.
|
||||
/// @param[out] measurements The measurements list to create.
|
||||
void bind_variables(const vector<pair<string, string>>& config_items,
|
||||
const GlueVariablesBinding& binding,
|
||||
Dnp3IndexedGroup& binary_commands,
|
||||
Dnp3IndexedGroup& analog_commands,
|
||||
Dnp3MappedGroup& measurements) {
|
||||
int16_t group_12_max_index(-1);
|
||||
int16_t group_41_max_index(-1);
|
||||
int16_t measurements_size(0);
|
||||
|
||||
// We do this in several passes so that we can efficiently allocate memory.
|
||||
// That means more work up front, but save space over the application
|
||||
// lifetime.
|
||||
vector<tuple<string, int8_t, int16_t>> bindings;
|
||||
for (auto it = config_items.begin(); it != config_items.end(); ++it) {
|
||||
if ((*it).first != "bind_location") {
|
||||
// We are searching through all configuration items, so ignore any
|
||||
// items that are not associated with a bind location.
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find the name of the located variable
|
||||
string name = get_located_name((*it).second);
|
||||
int8_t group_number = get_group_number((*it).second);
|
||||
int16_t data_index = get_data_index((*it).second);
|
||||
|
||||
if (name.empty() || group_number < 0 || data_index < 0) {
|
||||
// If one of the items is not valid, then don't handle furhter
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (group_number) {
|
||||
case 12:
|
||||
group_12_max_index = max(group_12_max_index, data_index);
|
||||
break;
|
||||
case 41:
|
||||
group_41_max_index = max(group_41_max_index, data_index);
|
||||
break;
|
||||
case 1:
|
||||
case 10:
|
||||
case 30:
|
||||
case 40:
|
||||
measurements_size += 1;
|
||||
break;
|
||||
default:
|
||||
spdlog::error("DNP3 bind_location unknown group config item {}", (*it).second);
|
||||
}
|
||||
|
||||
bindings.push_back(make_tuple(name, group_number, data_index));
|
||||
}
|
||||
|
||||
if (group_12_max_index >= 0) {
|
||||
binary_commands.size = group_12_max_index + 1;
|
||||
binary_commands.items = new ConstGlueVariable*[binary_commands.size];
|
||||
memset(binary_commands.items, 0, sizeof(ConstGlueVariable*) * binary_commands.size);
|
||||
}
|
||||
|
||||
if (group_41_max_index >= 0) {
|
||||
analog_commands.size = group_41_max_index + 1;
|
||||
analog_commands.items = new ConstGlueVariable*[analog_commands.size];
|
||||
memset(analog_commands.items, 0, sizeof(ConstGlueVariable*) * analog_commands.size);
|
||||
}
|
||||
|
||||
if (measurements_size > 0) {
|
||||
// We don't need to memset here because we will populate the entire array
|
||||
measurements.size = measurements_size;
|
||||
measurements.items = new DNP3MappedGlueVariable[measurements.size];
|
||||
}
|
||||
|
||||
// Now bind each glue variable into the structure
|
||||
uint16_t meas_index(0);
|
||||
for (auto it = bindings.begin(); it != bindings.end(); ++it) {
|
||||
string name = std::get<0>(*it);
|
||||
int8_t group_number = std::get<1>(*it);
|
||||
int16_t data_index = std::get<2>(*it);
|
||||
|
||||
const GlueVariable* var = binding.find(name);
|
||||
if (!var) {
|
||||
spdlog::error("Unable to bind location {} because it is not defined in the application", name);
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (group_number) {
|
||||
case 12:
|
||||
binary_commands.items[data_index] = var;
|
||||
break;
|
||||
case 41:
|
||||
analog_commands.items[data_index] = var;
|
||||
break;
|
||||
default:
|
||||
measurements.items[meas_index].group = group_number;
|
||||
measurements.items[meas_index].point_index_number = data_index;
|
||||
measurements.items[meas_index].variable = var;
|
||||
meas_index += 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
OutstationStackConfig dnp3_create_config(istream& cfg_stream,
|
||||
const GlueVariablesBinding& binding,
|
||||
Dnp3IndexedGroup& binary_commands,
|
||||
Dnp3IndexedGroup& analog_commands,
|
||||
Dnp3MappedGroup& measurements) {
|
||||
// We need to know the size of the database (number of points) before
|
||||
// we can do anything. To avoid doing two passes of the stream, read
|
||||
// everything into a map, then get the database size, and finally
|
||||
// process the remaining items
|
||||
std::map<std::string, std::string> cfg_values;
|
||||
std::string line;
|
||||
vector<pair<string, string>> cfg_values;
|
||||
string line;
|
||||
while (getline(cfg_stream, line)) {
|
||||
// Skip comment lines or those that are not a key-value pair
|
||||
auto pos = line.find('=');
|
||||
if (pos == std::string::npos || line[0] == '#') {
|
||||
if (pos == string::npos || line[0] == '#') {
|
||||
continue;
|
||||
}
|
||||
|
||||
std::string token = line.substr(0, pos);
|
||||
std::string value = line.substr(pos + 1);
|
||||
string token = line.substr(0, pos);
|
||||
string value = line.substr(pos + 1);
|
||||
trim(token);
|
||||
trim(value);
|
||||
|
||||
cfg_values[token] = value;
|
||||
cfg_values.push_back(make_pair(token, value));
|
||||
}
|
||||
|
||||
// Now that we know if we have the database size, we can ceate the stack config
|
||||
auto default_size = 10;
|
||||
auto db_size = cfg_values.find("database_size");
|
||||
if (db_size != cfg_values.end()) {
|
||||
default_size = atoi(db_size->second.c_str());
|
||||
cfg_values.erase(db_size);
|
||||
}
|
||||
// We need to know the size of the DNP3 database (the size of each of the
|
||||
// groups) before we can create the configuration. We can figure that out
|
||||
// based on the binding of located variables, so we need to process that
|
||||
// first
|
||||
bind_variables(cfg_values, binding, binary_commands,
|
||||
analog_commands, measurements);
|
||||
|
||||
auto config = asiodnp3::OutstationStackConfig(DatabaseSizes::AllTypes(default_size));
|
||||
Dnp3Range range = { 0, OPLCGLUE_INPUT_SIZE, 0, 0, OPLCGLUE_OUTPUT_SIZE, 0, 0, BUFFER_SIZE, 0, 0, BUFFER_SIZE, 0};
|
||||
auto config = asiodnp3::OutstationStackConfig(DatabaseSizes(
|
||||
measurements.group_size(1), // Binary
|
||||
measurements.group_size(3), // Double binary
|
||||
measurements.group_size(30), // Analog
|
||||
measurements.group_size(20), // Counter
|
||||
measurements.group_size(21), // Frozen counter
|
||||
measurements.group_size(10), // Binary output status
|
||||
measurements.group_size(40), // Analog output status
|
||||
measurements.group_size(50) // Time and interval
|
||||
));
|
||||
|
||||
// Finally, handle the remaining items
|
||||
// Finally, handle the remaining itemss
|
||||
for (auto it = cfg_values.begin(); it != cfg_values.end(); ++it) {
|
||||
auto token = it->first;
|
||||
auto value = it->second;
|
||||
|
@ -148,14 +341,6 @@ std::pair<asiodnp3::OutstationStackConfig, Dnp3Range> create_config(std::istream
|
|||
config.outstation.params.maxTxFragSize = atoi(value.c_str());
|
||||
} else if (token == "event_buffer_size") {
|
||||
config.outstation.eventBufferConfig = EventBufferConfig::AllTypes(atoi(value.c_str()));
|
||||
} else if (token == "offset_di") {
|
||||
range.bool_inputs_offset = atoi(value.c_str());
|
||||
} else if (token == "offset_do") {
|
||||
range.bool_outputs_offset = atoi(value.c_str());
|
||||
} else if (token == "offset_ai") {
|
||||
range.inputs_offset = atoi(value.c_str());
|
||||
} else if (token == "offset_ao") {
|
||||
range.outputs_offset = atoi(value.c_str());
|
||||
} else if (token == "sol_confirm_timeout") {
|
||||
config.outstation.params.solConfirmTimeout =
|
||||
openpal::TimeDuration::Milliseconds(atoi(value.c_str()));
|
||||
|
@ -172,23 +357,21 @@ std::pair<asiodnp3::OutstationStackConfig, Dnp3Range> create_config(std::istream
|
|||
exit(1);
|
||||
}
|
||||
}
|
||||
return std::make_pair(config, range);
|
||||
return config;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
/// @brief Start the DNP3 server running on the specified port and configured
|
||||
/// using the specified stream.
|
||||
///
|
||||
/// The stream is specified as a function so that this function will close the
|
||||
/// stream as soon as it is done with the stream.
|
||||
/// @param port The port to listen on
|
||||
/// @param cfg_stream_fn An input stream to read configuration information from. This will be reset
|
||||
/// once use of the stream has been completed.
|
||||
/// @param run A signal for running this server. This server terminates when this signal is false.
|
||||
void dnp3StartServer(int port, std::unique_ptr<std::istream, std::function<void(std::istream*)>>& cfg_stream, const bool* run) {
|
||||
void dnp3StartServer(int port,
|
||||
unique_ptr<istream, function<void(istream*)>>& cfg_stream,
|
||||
bool* run,
|
||||
const GlueVariablesBinding& glue_variables) {
|
||||
const uint32_t FILTERS = levels::NORMAL;
|
||||
|
||||
std::pair<asiodnp3::OutstationStackConfig, Dnp3Range> config_range(create_config(*cfg_stream));
|
||||
Dnp3IndexedGroup binary_commands = {0};
|
||||
Dnp3IndexedGroup analog_commands = {0};
|
||||
Dnp3MappedGroup measurements = {0};
|
||||
auto config(dnp3_create_config(*cfg_stream, glue_variables,
|
||||
binary_commands, analog_commands,
|
||||
measurements));
|
||||
|
||||
// We are done with the file, so release the unique ptr. Normally this
|
||||
// will close the reference to the file
|
||||
|
@ -196,38 +379,27 @@ void dnp3StartServer(int port, std::unique_ptr<std::istream, std::function<void(
|
|||
|
||||
// Allocate a single thread to the pool since this is a single outstation
|
||||
// Log messages to the console
|
||||
DNP3Manager manager(1, ConsoleLogger::Create());
|
||||
DNP3Manager manager(1, Dnp3ToSpdLogger::Create());
|
||||
|
||||
// Create a listener server
|
||||
auto channel = manager.AddTCPServer("DNP3_Server", FILTERS, ChannelRetry::Default(), "0.0.0.0", port, PrintingChannelListener::Create());
|
||||
|
||||
// We provide variable bindings into DNP3 so that it can support multiple outstations.
|
||||
// There are two pieces to this - the glue and the defined range for this instance.
|
||||
std::shared_ptr<GlueVariables> glue_variables = std::make_shared<GlueVariables>(
|
||||
&bufferLock,
|
||||
&(bool_input[0][0]),
|
||||
&(bool_output[0][0]),
|
||||
OPLCGLUE_INPUT_SIZE,
|
||||
oplc_input_vars,
|
||||
OPLCGLUE_OUTPUT_SIZE,
|
||||
oplc_output_vars);
|
||||
|
||||
// Create a new outstation with a log level, command handler, and
|
||||
// config info this returns a thread-safe interface used for
|
||||
// updating the outstation's database.
|
||||
std::shared_ptr<ICommandHandler> cc = std::make_shared<Dnp3Receiver>(glue_variables, config_range.second);
|
||||
shared_ptr<Dnp3Receiver> receiver = make_shared<Dnp3Receiver>(binary_commands, analog_commands);
|
||||
auto outstation = channel->AddOutstation(
|
||||
"outstation",
|
||||
cc,
|
||||
receiver,
|
||||
DefaultOutstationApplication::Create(),
|
||||
config_range.first);
|
||||
config);
|
||||
|
||||
// Enable the outstation and start communications
|
||||
outstation->Enable();
|
||||
{
|
||||
auto publisher = std::make_shared<Dnp3Publisher>(outstation, glue_variables, config_range.second);
|
||||
auto publisher = make_shared<Dnp3Publisher>(outstation, measurements);
|
||||
|
||||
spdlog::info("DNP3 outstation enabled on port {0:d} with range {1}", port, config_range.second.ToString().c_str());
|
||||
spdlog::info("DNP3 outstation enabled on port {0:d}", port);
|
||||
|
||||
// Continuously update
|
||||
struct timespec timer_start;
|
||||
|
@ -235,8 +407,15 @@ void dnp3StartServer(int port, std::unique_ptr<std::istream, std::function<void(
|
|||
|
||||
// Run this until we get a signal to stop.
|
||||
while (*run) {
|
||||
int num_writes = publisher->WriteToPoints();
|
||||
spdlog::trace("{} data points written to outstation", num_writes);
|
||||
{
|
||||
// Create a scope so we release the log after the read/write
|
||||
lock_guard<mutex> guard(*glue_variables.buffer_lock);
|
||||
// Readn and write DNP3
|
||||
int num_writes = publisher->ExchangeGlue();
|
||||
receiver->ExchangeGlue();
|
||||
spdlog::trace("{} data points written to outstation", num_writes);
|
||||
}
|
||||
|
||||
sleep_until(&timer_start, OPLC_CYCLE);
|
||||
}
|
||||
|
||||
|
@ -255,13 +434,13 @@ void dnp3StartServer(int port, std::unique_ptr<std::istream, std::function<void(
|
|||
/// the DNP3 server is started.
|
||||
/// @param port The port to run against.
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
void dnp3StartServer(int port) {
|
||||
std::unique_ptr<std::istream, std::function<void(std::istream*)>> cfg_stream(new std::ifstream("./../webserver/dnp3.cfg"), [](std::istream* s)
|
||||
void dnp3StartServer(int port, bool* run, const GlueVariablesBinding& binding) {
|
||||
unique_ptr<istream, function<void(istream*)>> cfg_stream(new ifstream("./../webserver/dnp3.cfg"), [](istream* s)
|
||||
{
|
||||
reinterpret_cast<std::ifstream*>(s)->close();
|
||||
reinterpret_cast<ifstream*>(s)->close();
|
||||
delete s;
|
||||
});
|
||||
dnp3StartServer(port, cfg_stream, &run_dnp3);
|
||||
dnp3StartServer(port, cfg_stream, run, binding);
|
||||
}
|
||||
|
||||
#endif // OPLC_DNP3_OUTSTATION
|
||||
|
|
|
@ -12,50 +12,140 @@
|
|||
// See the License for the specific language governing permissionsand
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef CORE_DNP3_H_
|
||||
#define CORE_DNP3_H_
|
||||
#ifndef CORE_DNP3_DNP3_H_
|
||||
#define CORE_DNP3_DNP3_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <istream>
|
||||
#include <mutex>
|
||||
#include <utility>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
namespace asiodnp3 {
|
||||
class OutstationStackConfig;
|
||||
}
|
||||
|
||||
struct GlueVariable;
|
||||
struct GlueVariablesBinding;
|
||||
|
||||
/** \addtogroup openplc_runtime
|
||||
* @{
|
||||
*/
|
||||
|
||||
typedef const GlueVariable ConstGlueVariable;
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
/// \brief Defines an offest mapping for DNP3 to glue variables.
|
||||
///
|
||||
/// This structure allows you to specify valid ranges for
|
||||
/// the glue mapping. This could allow you to divide up
|
||||
/// the glue into multiple outstations.
|
||||
/// @brief Defines a list of mapped variables for a particular group.
|
||||
/// These are indexed for fast lookup based on the point index number and are
|
||||
/// therefore used for commands that are received from the DNP3 master.
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
struct Dnp3Range {
|
||||
std::uint16_t inputs_start;
|
||||
std::uint16_t inputs_end;
|
||||
std::int16_t inputs_offset;
|
||||
struct Dnp3IndexedGroup {
|
||||
/// The size of the items array
|
||||
std::uint16_t size;
|
||||
/// The items array. Members in this array may be nullptr if they are
|
||||
/// not mapped to a glue variable.
|
||||
ConstGlueVariable** items;
|
||||
};
|
||||
|
||||
std::uint16_t outputs_start;
|
||||
std::uint16_t outputs_end;
|
||||
std::int16_t outputs_offset;
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
/// @brief Defines a glue variable that is mapped to a particular group and
|
||||
/// variation for DNP3.
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
struct DNP3MappedGlueVariable {
|
||||
/// The DNP3 group for this variable.
|
||||
std::uint8_t group;
|
||||
/// The DNP3 point index number for this variable.
|
||||
std::uint16_t point_index_number;
|
||||
/// The located variable that is glued to location.
|
||||
const GlueVariable* variable;
|
||||
};
|
||||
|
||||
std::uint16_t bool_inputs_start;
|
||||
std::uint16_t bool_inputs_end;
|
||||
std::int16_t bool_inputs_offset;
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
/// @brief Defines the list of variables that are mapped from located variables
|
||||
/// to DNP3. You can essentially iterate over this list to find every
|
||||
/// located variable that is mapped.
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
struct Dnp3MappedGroup {
|
||||
/// The size of the items array
|
||||
std::uint16_t size;
|
||||
/// The items array. Members in this array may be nullptr if they are
|
||||
/// not mapped to a glue variable.
|
||||
DNP3MappedGlueVariable* items;
|
||||
|
||||
std::uint16_t bool_outputs_start;
|
||||
std::uint16_t bool_outputs_end;
|
||||
std::int16_t bool_outputs_offset;
|
||||
|
||||
std::string ToString() {
|
||||
std::stringstream ss;
|
||||
ss << "DNP3 range (S,E,O) " << "I: " << this->inputs_start << ' ' << this->inputs_end << ' ' << this->inputs_offset;
|
||||
ss << "; O: " << this->outputs_start << ' ' << this->outputs_end << ' ' << this->outputs_offset;
|
||||
ss << "; BI: " << this->bool_inputs_start << ' ' << this->bool_inputs_end << ' ' << this->bool_inputs_offset;
|
||||
ss << "; BO: " << this->bool_outputs_start << ' ' << this->bool_outputs_end << ' ' << this->bool_outputs_offset;
|
||||
return ss.str();
|
||||
/// Gets the number of items in the specified group.
|
||||
std::uint16_t group_size(const std::uint8_t group) {
|
||||
std::uint16_t num(0);
|
||||
for (std::uint16_t i(0); i < size; ++i) {
|
||||
if (items[i].group == group) {
|
||||
num += 1;
|
||||
}
|
||||
}
|
||||
return num;
|
||||
}
|
||||
};
|
||||
|
||||
#endif // CORE_DNP3_H_
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
/// @brief The mapping of glue variables into this DNP3 outstation.
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
struct Dnp3BoundGlueVariables {
|
||||
Dnp3BoundGlueVariables(
|
||||
std::mutex* buffer_lock,
|
||||
std::uint16_t binary_commands_size,
|
||||
std::uint16_t analog_commands_size,
|
||||
std::uint16_t measurements_size
|
||||
) :
|
||||
|
||||
buffer_lock(buffer_lock),
|
||||
binary_commands({ .size=0, .items=nullptr }),
|
||||
analog_commands({ .size=0, .items=nullptr }),
|
||||
measurements({ .size=0, .items=nullptr })
|
||||
{}
|
||||
|
||||
/// @brief Mutex for the glue variables associated with this structures.
|
||||
std::mutex* buffer_lock;
|
||||
|
||||
/// @brief Structure of bound glue variables for binary commands
|
||||
Dnp3IndexedGroup binary_commands;
|
||||
/// @brief Structure of bound glue variables for analog commands
|
||||
Dnp3IndexedGroup analog_commands;
|
||||
|
||||
/// @brief All measurements that are sent from this outstation to the
|
||||
/// master.
|
||||
Dnp3MappedGroup measurements;
|
||||
};
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
/// @brief Create the outstation stack configuration using the configration
|
||||
/// settings as specified in the stream.
|
||||
/// @param cfg_stream The stream to read for configuration settings.
|
||||
/// @param binding The mutex for exclusive access to glue variable values.
|
||||
/// @return The configuration represented by the stream and any defaults and
|
||||
/// the range mapping.
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
asiodnp3::OutstationStackConfig dnp3_create_config(std::istream& cfg_stream,
|
||||
const GlueVariablesBinding& binding,
|
||||
Dnp3IndexedGroup& binary_commands,
|
||||
Dnp3IndexedGroup& analog_commands,
|
||||
Dnp3MappedGroup& measurements);
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
/// @brief Start the DNP3 server running on the specified port and configured
|
||||
/// using the specified stream.
|
||||
///
|
||||
/// The stream is specified as a function so that this function will close the
|
||||
/// stream as soon as it is done with the stream.
|
||||
/// @param port The port to listen on.
|
||||
/// @param cfg_stream An input stream to read configuration information
|
||||
/// from. This will be reset once use of the stream has
|
||||
/// been completed.
|
||||
/// @param run A signal for running this server. This server terminates when
|
||||
/// this signal is false.
|
||||
/// @param glue_variables The glue variables that may be bound into this
|
||||
/// server.
|
||||
void dnp3StartServer(int port,
|
||||
std::unique_ptr<std::istream, std::function<void(std::istream*)>>& cfg_stream,
|
||||
bool* run,
|
||||
const GlueVariablesBinding& glue_variables);
|
||||
|
||||
#endif // CORE_DNP3_DNP3_H_
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
//------------------------------------------------------------------
|
||||
//Function to begin DNP3 server functions
|
||||
//------------------------------------------------------------------
|
||||
void dnp3StartServer(int port)
|
||||
{
|
||||
}
|
|
@ -22,217 +22,124 @@
|
|||
#include "glue.h"
|
||||
|
||||
using namespace opendnp3;
|
||||
using namespace std;
|
||||
|
||||
/** \addtogroup openplc_runtime
|
||||
* @{
|
||||
*/
|
||||
|
||||
|
||||
// Initialize a new instance of the publisher.
|
||||
/// Initialize a new instance of the publisher.
|
||||
Dnp3Publisher::Dnp3Publisher(
|
||||
std::shared_ptr<asiodnp3::IOutstation> outstation,
|
||||
std::shared_ptr<GlueVariables> glue_variables,
|
||||
Dnp3Range range) :
|
||||
shared_ptr<asiodnp3::IOutstation> outstation,
|
||||
const Dnp3MappedGroup& measurements) :
|
||||
|
||||
outstation(outstation),
|
||||
glue_variables(glue_variables),
|
||||
range(range)
|
||||
measurements(measurements)
|
||||
{ }
|
||||
|
||||
template<class T>
|
||||
T cast_variable(const GlueVariable* var) {
|
||||
T val;
|
||||
void* value = var->value;
|
||||
switch (var->type) {
|
||||
case IECVT_SINT:
|
||||
{
|
||||
IEC_SINT* tval = reinterpret_cast<IEC_SINT*>(value);
|
||||
val = *tval;
|
||||
break;
|
||||
}
|
||||
case IECVT_USINT:
|
||||
{
|
||||
IEC_USINT* tval = reinterpret_cast<IEC_USINT*>(value);
|
||||
val = *tval;
|
||||
break;
|
||||
}
|
||||
case IECVT_INT:
|
||||
{
|
||||
IEC_INT* tval = reinterpret_cast<IEC_INT*>(value);
|
||||
val = *tval;
|
||||
break;
|
||||
}
|
||||
case IECVT_UINT:
|
||||
{
|
||||
IEC_UINT* tval = reinterpret_cast<IEC_UINT*>(value);
|
||||
val = *tval;
|
||||
break;
|
||||
}
|
||||
case IECVT_DINT:
|
||||
{
|
||||
IEC_DINT* tval = reinterpret_cast<IEC_DINT*>(value);
|
||||
val = *tval;
|
||||
break;
|
||||
}
|
||||
case IECVT_UDINT:
|
||||
{
|
||||
IEC_UDINT* tval = reinterpret_cast<IEC_UDINT*>(value);
|
||||
val = *tval;
|
||||
break;
|
||||
}
|
||||
case IECVT_REAL:
|
||||
{
|
||||
IEC_REAL* tval = reinterpret_cast<IEC_REAL*>(value);
|
||||
val = *tval;
|
||||
break;
|
||||
}
|
||||
case IECVT_LREAL:
|
||||
{
|
||||
IEC_LREAL* tval = reinterpret_cast<IEC_LREAL*>(value);
|
||||
val = *tval;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return val;
|
||||
}
|
||||
|
||||
/// Writes mapped values in the glue variables to the DNP3 outstation channel.
|
||||
/// This function would normally be called on a timer to write the data.
|
||||
/// Writing to the DNP3 channel happens asynchronously, so completion of this
|
||||
/// function doesn't mean anything has been sent.
|
||||
/// @return the number of values that were sent to the channel.
|
||||
std::uint32_t Dnp3Publisher::WriteToPoints() {
|
||||
std::uint32_t num_writes(0);
|
||||
uint32_t Dnp3Publisher::ExchangeGlue() {
|
||||
uint32_t num_writes(0);
|
||||
asiodnp3::UpdateBuilder builder;
|
||||
|
||||
std::lock_guard<std::mutex> lock(*glue_variables->buffer_lock);
|
||||
|
||||
spdlog::trace("Writing glue variables to DNP3 points");
|
||||
|
||||
// Writes data points to the outstation. We support two capabilities here:
|
||||
//
|
||||
// * A set of ranges that we will map so that not all of the data needs to
|
||||
// be available over DNP3
|
||||
// * An offset of the index in the glue variables to the index of the DNP3 points.
|
||||
// That is, the offset can take glue variable at index 10 and offset by -5 so that
|
||||
// it is written to point 5.
|
||||
for (uint16_t i(0); i < measurements.size; ++i) {
|
||||
const DNP3MappedGlueVariable& mapping = measurements.items[i];
|
||||
const uint8_t group = mapping.group;
|
||||
const uint16_t point_index_number = mapping.point_index_number;
|
||||
const GlueVariable* var = mapping.variable;
|
||||
void* value = var->value;
|
||||
|
||||
// Update Discrete input (Binary input)
|
||||
for (auto i = range.bool_inputs_start; i < range.bool_inputs_end; i++) {
|
||||
if (glue_variables->BoolInputAt(i, 0) == nullptr) {
|
||||
continue;
|
||||
if (group == 1 || group == 10) {
|
||||
const GlueBoolGroup* bool_group = reinterpret_cast<const GlueBoolGroup*>(value);
|
||||
if (group == 1) {
|
||||
builder.Update(Binary(*(bool_group->values[0])), point_index_number);
|
||||
} else {
|
||||
builder.Update(BinaryOutputStatus(*(bool_group->values[0])), point_index_number);
|
||||
}
|
||||
|
||||
} else if (group == 30 || group == 40) {
|
||||
double double_val = cast_variable<double>(var);
|
||||
if (group == 30) {
|
||||
builder.Update(Analog(double_val), point_index_number);
|
||||
} else {
|
||||
builder.Update(AnalogOutputStatus(double_val), point_index_number);
|
||||
}
|
||||
|
||||
} else if (group == 20 || group == 21) {
|
||||
uint32_t int_val = cast_variable<uint32_t>(var);
|
||||
if (group == 20) {
|
||||
builder.Update(Counter(int_val), point_index_number);
|
||||
} else {
|
||||
builder.Update(FrozenCounter(int_val), point_index_number);
|
||||
}
|
||||
}
|
||||
auto db_index = i - range.bool_inputs_offset;
|
||||
if (db_index < 0) {
|
||||
continue;
|
||||
}
|
||||
builder.Update(Binary(*glue_variables->BoolInputAt(i, 0)), db_index);
|
||||
num_writes += 1;
|
||||
}
|
||||
|
||||
// Update Coils (Binary Output)
|
||||
for (auto i = range.bool_outputs_start; i < range.bool_outputs_end; i++) {
|
||||
if (glue_variables->BoolOutputAt(i, 0) == nullptr) {
|
||||
continue;
|
||||
}
|
||||
auto db_index = i - range.bool_outputs_offset;
|
||||
if (db_index < 0) {
|
||||
continue;
|
||||
}
|
||||
builder.Update(BinaryOutputStatus(*glue_variables->BoolOutputAt(i, 0)), db_index);
|
||||
num_writes += 1;
|
||||
}
|
||||
|
||||
// Write the generic types
|
||||
for (auto i = range.inputs_start; i < range.inputs_end; i++) {
|
||||
void* value = glue_variables->inputs[i].value;
|
||||
if (!value) {
|
||||
// If this slot in the glue is not mapped to a memory location, then
|
||||
// it is not a defined address and we must skip it.
|
||||
continue;
|
||||
}
|
||||
|
||||
// We allow the user to arbitrarily shift the point index for DNP3. This
|
||||
// might allow someone to have two outstations, both starting at index
|
||||
// 0, but referring to different points.
|
||||
std::uint16_t point_index = i - range.inputs_offset;
|
||||
|
||||
num_writes += 1;
|
||||
switch (glue_variables->inputs[i].type) {
|
||||
case IECVT_SINT:
|
||||
{
|
||||
IEC_SINT* tval = reinterpret_cast<IEC_SINT*>(value);
|
||||
builder.Update(Analog(*tval), point_index);
|
||||
break;
|
||||
}
|
||||
case IECVT_USINT:
|
||||
{
|
||||
IEC_USINT* tval = reinterpret_cast<IEC_USINT*>(value);
|
||||
builder.Update(Counter(*tval), point_index);
|
||||
break;
|
||||
}
|
||||
case IECVT_INT:
|
||||
{
|
||||
IEC_INT* tval = reinterpret_cast<IEC_INT*>(value);
|
||||
builder.Update(Analog(*tval), point_index);
|
||||
break;
|
||||
}
|
||||
case IECVT_UINT:
|
||||
{
|
||||
IEC_UINT* tval = reinterpret_cast<IEC_UINT*>(value);
|
||||
builder.Update(Counter(*tval), point_index);
|
||||
break;
|
||||
}
|
||||
case IECVT_DINT:
|
||||
{
|
||||
IEC_DINT* tval = reinterpret_cast<IEC_DINT*>(value);
|
||||
builder.Update(Analog(*tval), point_index);
|
||||
break;
|
||||
}
|
||||
case IECVT_UDINT:
|
||||
{
|
||||
IEC_UDINT* tval = reinterpret_cast<IEC_UDINT*>(value);
|
||||
builder.Update(Counter(*tval), point_index);
|
||||
break;
|
||||
}
|
||||
case IECVT_REAL:
|
||||
{
|
||||
IEC_REAL* tval = reinterpret_cast<IEC_REAL*>(value);
|
||||
builder.Update(Analog(*tval), point_index);
|
||||
break;
|
||||
}
|
||||
case IECVT_LREAL:
|
||||
{
|
||||
IEC_LREAL* tval = reinterpret_cast<IEC_LREAL*>(value);
|
||||
builder.Update(Analog(*tval), point_index);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
// We assumed above that we successfully found a value to write, but
|
||||
// we were wrong. This undoes the increment since we don't actually
|
||||
// support this particular type.
|
||||
num_writes -= 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (auto i = range.outputs_start; i < range.outputs_end; i++) {
|
||||
void* value = glue_variables->outputs[i].value;
|
||||
if (!value) {
|
||||
// If this slot in the glue is not mapped to a memory location, then
|
||||
// it is not a defined address and we must skip it.
|
||||
continue;
|
||||
}
|
||||
|
||||
// We allow the user to arbitrarily shift the point index for DNP3. This
|
||||
// might allow someone to have two outstations, both starting at index
|
||||
// 0, but referring to different points.
|
||||
std::uint16_t point_index = i - range.outputs_offset;
|
||||
|
||||
num_writes += 1;
|
||||
switch (glue_variables->outputs[i].type) {
|
||||
case IECVT_SINT:
|
||||
{
|
||||
IEC_SINT* tval = reinterpret_cast<IEC_SINT*>(value);
|
||||
builder.Update(AnalogOutputStatus(*tval), point_index);
|
||||
break;
|
||||
}
|
||||
case IECVT_USINT:
|
||||
{
|
||||
IEC_USINT* tval = reinterpret_cast<IEC_USINT*>(value);
|
||||
builder.Update(Counter(*tval), point_index);
|
||||
break;
|
||||
}
|
||||
case IECVT_INT:
|
||||
{
|
||||
IEC_INT* tval = reinterpret_cast<IEC_INT*>(value);
|
||||
builder.Update(AnalogOutputStatus(*tval), point_index);
|
||||
break;
|
||||
}
|
||||
case IECVT_UINT:
|
||||
{
|
||||
IEC_UINT* tval = reinterpret_cast<IEC_UINT*>(value);
|
||||
builder.Update(Counter(*tval), point_index);
|
||||
break;
|
||||
}
|
||||
case IECVT_DINT:
|
||||
{
|
||||
IEC_DINT* tval = reinterpret_cast<IEC_DINT*>(value);
|
||||
builder.Update(AnalogOutputStatus(*tval), point_index);
|
||||
break;
|
||||
}
|
||||
case IECVT_UDINT:
|
||||
{
|
||||
IEC_UDINT* tval = reinterpret_cast<IEC_UDINT*>(value);
|
||||
builder.Update(Counter(*tval), point_index);
|
||||
break;
|
||||
}
|
||||
case IECVT_REAL:
|
||||
{
|
||||
IEC_REAL* tval = reinterpret_cast<IEC_REAL*>(value);
|
||||
builder.Update(AnalogOutputStatus(*tval), point_index);
|
||||
break;
|
||||
}
|
||||
case IECVT_LREAL:
|
||||
{
|
||||
IEC_LREAL* tval = reinterpret_cast<IEC_LREAL*>(value);
|
||||
builder.Update(AnalogOutputStatus(*tval), point_index);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
// We assumed above that we successfully found a value to write, but
|
||||
// we were wrong. This undoes the increment since we don't actually
|
||||
// support this particular type.
|
||||
num_writes -= 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
outstation->Apply(builder.Build());
|
||||
|
|
|
@ -12,8 +12,8 @@
|
|||
// See the License for the specific language governing permissionsand
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef CORE_DNP3_PUBLISHER_H_
|
||||
#define CORE_DNP3_PUBLISHER_H_
|
||||
#ifndef CORE_DNP3_DNP3_PUBLISHER_H_
|
||||
#define CORE_DNP3_DNP3_PUBLISHER_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
|
@ -25,27 +25,24 @@
|
|||
* @{
|
||||
*/
|
||||
|
||||
struct GlueVariables;
|
||||
struct GlueRange;
|
||||
namespace asiodnp3 {
|
||||
class IOutstation;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
/// \brief The publisher defines the interface between the glue arrays
|
||||
/// @brief The publisher defines the interface between the glue arrays
|
||||
/// of variables that are read from PLC application and written to
|
||||
/// the DNP3 channel. This published all of the available glue
|
||||
/// information over DNP3, incuding inputs, outputs, memory.
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
class Dnp3Publisher {
|
||||
public:
|
||||
/// \brief Constructs a new instance of the publihser object.
|
||||
/// \brief Constructs a new instance of the publisher object.
|
||||
/// @param outstation The outstation that is ourselves
|
||||
/// @param glue_variables The buffers that we use for data transfer
|
||||
/// @param measurements The buffers that we use for data transfer
|
||||
Dnp3Publisher(
|
||||
std::shared_ptr<asiodnp3::IOutstation> outstation,
|
||||
std::shared_ptr<GlueVariables> glue_variables,
|
||||
Dnp3Range range);
|
||||
const Dnp3MappedGroup& measurements);
|
||||
|
||||
/// \brief Publish the values from the in-memory buffers to DNP3 points.
|
||||
///
|
||||
|
@ -53,19 +50,16 @@ class Dnp3Publisher {
|
|||
/// once the points have been queued to write but in general will return
|
||||
/// before the write has actually happened.
|
||||
/// @return the number of points that were queued to write.
|
||||
std::uint32_t WriteToPoints();
|
||||
std::uint32_t ExchangeGlue();
|
||||
|
||||
private:
|
||||
/// The outstation that we are subscribed to.
|
||||
const std::shared_ptr<asiodnp3::IOutstation> outstation;
|
||||
|
||||
/// The buffers for data transfer.
|
||||
const std::shared_ptr<GlueVariables> glue_variables;
|
||||
|
||||
/// The range and offsets into the glue that are valid for this instance.
|
||||
const Dnp3Range range;
|
||||
const Dnp3MappedGroup& measurements;
|
||||
};
|
||||
|
||||
#endif // CORE_DNP3_PUBLISHER_H_
|
||||
#endif // CORE_DNP3_DNP3_PUBLISHER_H_
|
||||
|
||||
/** @}*/
|
|
@ -14,6 +14,7 @@
|
|||
|
||||
#ifdef OPLC_DNP3_OUTSTATION
|
||||
|
||||
#include <cstring>
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
#include "dnp3_receiver.h"
|
||||
|
@ -25,6 +26,24 @@ using namespace std;
|
|||
* @{
|
||||
*/
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
/// @brief Is the specified DNP3 point something that we can map?
|
||||
/// @param data_index The index of the point from DNP3.
|
||||
/// @param group The group based on the command type.
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
inline CommandStatus mapIsValidDnp3Index(uint16_t data_index, const Dnp3IndexedGroup& group) {
|
||||
return (data_index < group.size && group.items[data_index]->value) ? CommandStatus::SUCCESS : CommandStatus::OUT_OF_RANGE;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
/// @brief Is the specified DNP3 point something that we can map?
|
||||
/// @param data_index The index of the point from DNP3.
|
||||
/// @param group The group based on the command type.
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
inline bool isValidDnp3Index(uint16_t data_index, const Dnp3IndexedGroup& group) {
|
||||
return (data_index < group.size && group.items[data_index]->value);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
/// \brief Maps the DNP3 index to the index in our glue variables, returning
|
||||
/// index or < 0 if the value is not in the range of mapped glue variables.
|
||||
|
@ -33,7 +52,7 @@ using namespace std;
|
|||
/// @param offset The offset defined for this set of values.
|
||||
/// @param dnp3_index The index of the point for DNP3.
|
||||
/// @return Non-negative glue index if the dnp3 index is valid, otherwise negative.
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
inline int16_t mapDnp3IndexToGlueIndex(uint16_t start, uint16_t stop, uint16_t offset, uint16_t dnp3_index) {
|
||||
int16_t glue_index = dnp3_index + offset;
|
||||
return glue_index >= start && glue_index < stop ? glue_index : -1;
|
||||
|
@ -52,194 +71,214 @@ inline CommandStatus mapDnp3IndexToStatus(uint16_t start, uint16_t stop, uint16_
|
|||
return mapDnp3IndexToGlueIndex(start, stop, offset, dnp3_index) >= 0 ? CommandStatus::SUCCESS : CommandStatus::OUT_OF_RANGE;
|
||||
}
|
||||
|
||||
/// Initialize a new instance of the DNP3 receiver. The receiver listens for point value updates
|
||||
/// over the DNP3 channel and then maps those to the glue variables.
|
||||
/// @param glue_variables The glue variables that we map onto.
|
||||
/// @param range The valid range in the glue that we allow.
|
||||
Dnp3Receiver::Dnp3Receiver(
|
||||
std::shared_ptr<GlueVariables> glue_variables,
|
||||
Dnp3Range range) :
|
||||
Dnp3Receiver::Dnp3Receiver(const Dnp3IndexedGroup& binary_commands, const Dnp3IndexedGroup& analog_commands) :
|
||||
|
||||
glue_variables(glue_variables),
|
||||
range(range)
|
||||
{ }
|
||||
binary_commands(binary_commands),
|
||||
analog_commands(analog_commands),
|
||||
binary_commands_cache(binary_commands.size > 0 ? new CacheItem<bool>[binary_commands.size] : nullptr),
|
||||
analog_commands_cache(analog_commands.size > 0 ? new CacheItem<double>[analog_commands.size] : nullptr)
|
||||
{
|
||||
// We need to zero out the caches so that we don't think there is something
|
||||
// that we can handle on the first cycle
|
||||
if (binary_commands_cache != nullptr) {
|
||||
memset(binary_commands_cache, 0, sizeof(CacheItem<bool>) * binary_commands.size);
|
||||
}
|
||||
if (analog_commands_cache != nullptr) {
|
||||
memset(analog_commands_cache, 0, sizeof(CacheItem<double>) * analog_commands.size);
|
||||
}
|
||||
}
|
||||
|
||||
/// CROB
|
||||
CommandStatus Dnp3Receiver::Select(const ControlRelayOutputBlock& command, uint16_t index) {
|
||||
spdlog::trace("DNP3 select CROB index");
|
||||
return mapDnp3IndexToStatus(range.bool_outputs_start, range.bool_outputs_end, range.bool_outputs_offset, index);
|
||||
return mapIsValidDnp3Index(index, binary_commands);
|
||||
}
|
||||
|
||||
CommandStatus Dnp3Receiver::Operate(const ControlRelayOutputBlock& command, uint16_t index, OperateType opType) {
|
||||
auto code = command.functionCode;
|
||||
auto glue_index = mapDnp3IndexToGlueIndex(range.bool_outputs_start, range.bool_outputs_end, range.bool_outputs_offset, index);
|
||||
|
||||
if (glue_index < 0) {
|
||||
if (!isValidDnp3Index(index, binary_commands)) {
|
||||
return CommandStatus::OUT_OF_RANGE;
|
||||
} else if (code != ControlCode::LATCH_ON && code != ControlCode::LATCH_OFF) {
|
||||
}
|
||||
if (code != ControlCode::LATCH_ON && code != ControlCode::LATCH_OFF) {
|
||||
return CommandStatus::NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
IEC_BOOL crob_val = (code == ControlCode::LATCH_ON);
|
||||
lock_guard<mutex> cache_guard(cache_mutex);
|
||||
binary_commands_cache[index].has_value = true;
|
||||
binary_commands_cache[index].value = (code == ControlCode::LATCH_ON);
|
||||
|
||||
IEC_BOOL* glue = glue_variables->BoolOutputAt(index, 0);
|
||||
if (glue != nullptr) {
|
||||
std::lock_guard<std::mutex> lock(*glue_variables->buffer_lock);
|
||||
*glue = crob_val;
|
||||
}
|
||||
spdlog::trace("DNP3 CROB: {} written at index: {}", code == ControlCode::LATCH_ON ? "True": "False", index);
|
||||
return CommandStatus::SUCCESS;
|
||||
}
|
||||
|
||||
// AnalogOut 16 (Int)
|
||||
CommandStatus Dnp3Receiver::Select(const AnalogOutputInt16& command, uint16_t index) {
|
||||
CommandStatus status = mapDnp3IndexToStatus(range.outputs_start, range.outputs_end, range.outputs_offset, index);
|
||||
spdlog::trace("DNP3 select AO int16 point status");
|
||||
return status;
|
||||
return mapIsValidDnp3Index(index, analog_commands);
|
||||
}
|
||||
|
||||
CommandStatus Dnp3Receiver::Operate(const AnalogOutputInt16& command, uint16_t index, OperateType opType) {
|
||||
spdlog::trace("DNP3 select AO int16 point status: {} written at index: {}", command.value, index);
|
||||
return this->UpdateGlueVariable<int16_t>(command.value, index);
|
||||
return this->CacheUpdatedValue<int16_t>(command.value, index);
|
||||
}
|
||||
|
||||
// AnalogOut 32 (Int)
|
||||
CommandStatus Dnp3Receiver::Select(const AnalogOutputInt32& command, uint16_t index) {
|
||||
CommandStatus status = mapDnp3IndexToStatus(range.outputs_start, range.outputs_end, range.outputs_offset, index);
|
||||
spdlog::trace("DNP3 select AO int32 point status");
|
||||
return status;
|
||||
spdlog::trace("DNP3 select AO int32 point status");
|
||||
return mapIsValidDnp3Index(index, analog_commands);
|
||||
}
|
||||
|
||||
CommandStatus Dnp3Receiver::Operate(const AnalogOutputInt32& command, uint16_t index, OperateType opType) {
|
||||
spdlog::trace("DNP3 select AO int32 point status: {} written at index: {}", command.value, index);
|
||||
return this->UpdateGlueVariable<int32_t>(command.value, index);
|
||||
return this->CacheUpdatedValue<int32_t>(command.value, index);
|
||||
}
|
||||
|
||||
// AnalogOut 32 (Float)
|
||||
CommandStatus Dnp3Receiver::Select(const AnalogOutputFloat32& command, uint16_t index) {
|
||||
CommandStatus status = mapDnp3IndexToStatus(range.outputs_start, range.outputs_end, range.outputs_offset, index);
|
||||
spdlog::trace("DNP3 select AO float32 point status");
|
||||
return status;
|
||||
return mapIsValidDnp3Index(index, analog_commands);
|
||||
}
|
||||
|
||||
CommandStatus Dnp3Receiver::Operate(const AnalogOutputFloat32& command, uint16_t index, OperateType opType) {
|
||||
spdlog::trace("DNP3 select AO float32 point status: {} written at index: {}", command.value, index);
|
||||
return this->UpdateGlueVariable<float>(command.value, index);
|
||||
return this->CacheUpdatedValue<float>(command.value, index);
|
||||
}
|
||||
|
||||
// AnalogOut 64
|
||||
CommandStatus Dnp3Receiver::Select(const AnalogOutputDouble64& command, uint16_t index) {
|
||||
CommandStatus status = mapDnp3IndexToStatus(range.outputs_start, range.outputs_end, range.outputs_offset, index);
|
||||
spdlog::trace("DNP3 select AO double64 point status");
|
||||
return status;
|
||||
return mapIsValidDnp3Index(index, analog_commands);
|
||||
}
|
||||
|
||||
CommandStatus Dnp3Receiver::Operate(const AnalogOutputDouble64& command, uint16_t index, OperateType opType) {
|
||||
spdlog::trace("DNP3 select AO double64 point status: {} written at index: {}", command.value, index);
|
||||
return this->UpdateGlueVariable<double>(command.value, index);
|
||||
return this->CacheUpdatedValue<double>(command.value, index);
|
||||
}
|
||||
|
||||
/// @brief Update a value in our cache. This cache is designed to operate fast
|
||||
/// so that we return immediately without waiting on the glue variables lock.
|
||||
/// This is quite important because the lock on the glue variables can be very
|
||||
/// long (the cycle time for the PLC logic) and that would be far too long to
|
||||
/// wait for DNP3 values.
|
||||
/// @param value The value received over DNP3.
|
||||
/// @param dnp3_index The index of the value.
|
||||
template<class T>
|
||||
CommandStatus Dnp3Receiver::UpdateGlueVariable(T value, uint16_t dnp3_index) const {
|
||||
int16_t glue_index = mapDnp3IndexToGlueIndex(range.outputs_start, range.outputs_end, range.outputs_offset, dnp3_index);
|
||||
|
||||
if (glue_index < 0 || glue_index >= glue_variables->outputs_size) {
|
||||
spdlog::trace("DNP3 update point is out of mapped glue range");
|
||||
CommandStatus Dnp3Receiver::CacheUpdatedValue(T value, uint16_t dnp3_index) {
|
||||
if (!isValidDnp3Index(dnp3_index, analog_commands)) {
|
||||
spdlog::trace("DNP3 update point at index {} is not glued", dnp3_index);
|
||||
return CommandStatus::OUT_OF_RANGE;
|
||||
}
|
||||
|
||||
// Next check if we have a value mapped at that particular index in the glue variables (the IO locations can have holes)
|
||||
IecGlueValueType type = glue_variables->outputs[glue_index].type;
|
||||
void* value_container = glue_variables->outputs[glue_index].value;
|
||||
lock_guard<mutex> cache_guard(cache_mutex);
|
||||
analog_commands_cache[dnp3_index].has_value = true;
|
||||
analog_commands_cache[dnp3_index].value = static_cast<double>(value);
|
||||
|
||||
if (type == IECVT_UNASSIGNED || value_container == nullptr) {
|
||||
spdlog::trace("DNP3 update point for glue index is not mapped");
|
||||
return CommandStatus::OUT_OF_RANGE;
|
||||
}
|
||||
return CommandStatus::SUCCESS;
|
||||
}
|
||||
|
||||
// We have a container we can write to, but we need to update the value as appropriate
|
||||
// for the particular value container type.
|
||||
std::lock_guard<std::mutex> lock(*glue_variables->buffer_lock);
|
||||
CommandStatus status = CommandStatus::SUCCESS;
|
||||
switch (type) {
|
||||
case IECVT_BYTE:
|
||||
{
|
||||
*(reinterpret_cast<IEC_BYTE*>(value_container)) = static_cast<IEC_BYTE>(value);
|
||||
break;
|
||||
}
|
||||
case IECVT_SINT:
|
||||
{
|
||||
*(reinterpret_cast<IEC_SINT*>(value_container)) = static_cast<IEC_SINT>(value);
|
||||
break;
|
||||
}
|
||||
case IECVT_USINT:
|
||||
{
|
||||
*(reinterpret_cast<IEC_USINT*>(value_container)) = static_cast<IEC_USINT>(value);
|
||||
break;
|
||||
}
|
||||
case IECVT_INT:
|
||||
{
|
||||
*(reinterpret_cast<IEC_INT*>(value_container)) = static_cast<IEC_INT>(value);
|
||||
break;
|
||||
}
|
||||
case IECVT_UINT:
|
||||
{
|
||||
*(reinterpret_cast<IEC_USINT*>(value_container)) = static_cast<IEC_USINT>(value);
|
||||
break;
|
||||
}
|
||||
case IECVT_WORD:
|
||||
{
|
||||
*(reinterpret_cast<IEC_WORD*>(value_container)) = static_cast<IEC_WORD>(value);
|
||||
break;
|
||||
}
|
||||
case IECVT_DINT:
|
||||
{
|
||||
*(reinterpret_cast<IEC_DINT*>(value_container)) = static_cast<IEC_DINT>(value);
|
||||
break;
|
||||
}
|
||||
case IECVT_UDINT:
|
||||
{
|
||||
*(reinterpret_cast<IEC_UDINT*>(value_container)) = static_cast<IEC_UDINT>(value);
|
||||
break;
|
||||
}
|
||||
case IECVT_DWORD:
|
||||
{
|
||||
*(reinterpret_cast<IEC_DWORD*>(value_container)) = static_cast<IEC_DWORD>(value);
|
||||
break;
|
||||
}
|
||||
case IECVT_REAL:
|
||||
{
|
||||
*(reinterpret_cast<IEC_REAL*>(value_container)) = static_cast<IEC_REAL>(value);
|
||||
break;
|
||||
}
|
||||
case IECVT_LREAL:
|
||||
{
|
||||
*(reinterpret_cast<IEC_LREAL*>(value_container)) = static_cast<IEC_LREAL>(value);
|
||||
break;
|
||||
}
|
||||
case IECVT_LWORD:
|
||||
{
|
||||
*(reinterpret_cast<IEC_LWORD*>(value_container)) = static_cast<IEC_LWORD>(value);
|
||||
break;
|
||||
}
|
||||
case IECVT_LINT:
|
||||
{
|
||||
*(reinterpret_cast<IEC_LINT*>(value_container)) = static_cast<IEC_LINT>(value);
|
||||
break;
|
||||
}
|
||||
case IECVT_ULINT:
|
||||
{
|
||||
*(reinterpret_cast<IEC_ULINT*>(value_container)) = static_cast<IEC_ULINT>(value);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
status = CommandStatus::NOT_SUPPORTED;
|
||||
break;
|
||||
/// @brief Write the point values into the glue variables for each value that
|
||||
/// was received.
|
||||
void Dnp3Receiver::ExchangeGlue() {
|
||||
// Acquire the locks to do the data exchange
|
||||
lock_guard<mutex> cache_guard(cache_mutex);
|
||||
|
||||
for (uint16_t data_index(0); data_index < binary_commands.size; ++data_index) {
|
||||
if (binary_commands_cache[data_index].has_value) {
|
||||
binary_commands_cache[data_index].has_value = false;
|
||||
const GlueVariable* variable = binary_commands.items[data_index];
|
||||
if (!variable) {
|
||||
continue;
|
||||
}
|
||||
|
||||
(*((GlueBoolGroup*)variable->value)->values[0]) = binary_commands_cache[data_index].value;
|
||||
}
|
||||
}
|
||||
|
||||
return status;
|
||||
for (uint16_t data_index(0); data_index < analog_commands.size; ++data_index) {
|
||||
if (analog_commands_cache[data_index].has_value) {
|
||||
analog_commands_cache[data_index].has_value = false;
|
||||
|
||||
const GlueVariable* variable = analog_commands.items[data_index];
|
||||
if (!variable) {
|
||||
continue;
|
||||
}
|
||||
|
||||
double value = analog_commands_cache[data_index].value;
|
||||
void* value_container = variable->value;
|
||||
switch (variable->type) {
|
||||
case IECVT_BYTE:
|
||||
{
|
||||
*(reinterpret_cast<IEC_BYTE*>(value_container)) = static_cast<IEC_BYTE>(value);
|
||||
break;
|
||||
}
|
||||
case IECVT_SINT:
|
||||
{
|
||||
*(reinterpret_cast<IEC_SINT*>(value_container)) = static_cast<IEC_SINT>(value);
|
||||
break;
|
||||
}
|
||||
case IECVT_USINT:
|
||||
{
|
||||
*(reinterpret_cast<IEC_USINT*>(value_container)) = static_cast<IEC_USINT>(value);
|
||||
break;
|
||||
}
|
||||
case IECVT_INT:
|
||||
{
|
||||
*(reinterpret_cast<IEC_INT*>(value_container)) = static_cast<IEC_INT>(value);
|
||||
break;
|
||||
}
|
||||
case IECVT_UINT:
|
||||
{
|
||||
*(reinterpret_cast<IEC_USINT*>(value_container)) = static_cast<IEC_USINT>(value);
|
||||
break;
|
||||
}
|
||||
case IECVT_WORD:
|
||||
{
|
||||
*(reinterpret_cast<IEC_WORD*>(value_container)) = static_cast<IEC_WORD>(value);
|
||||
break;
|
||||
}
|
||||
case IECVT_DINT:
|
||||
{
|
||||
*(reinterpret_cast<IEC_DINT*>(value_container)) = static_cast<IEC_DINT>(value);
|
||||
break;
|
||||
}
|
||||
case IECVT_UDINT:
|
||||
{
|
||||
*(reinterpret_cast<IEC_UDINT*>(value_container)) = static_cast<IEC_UDINT>(value);
|
||||
break;
|
||||
}
|
||||
case IECVT_DWORD:
|
||||
{
|
||||
*(reinterpret_cast<IEC_DWORD*>(value_container)) = static_cast<IEC_DWORD>(value);
|
||||
break;
|
||||
}
|
||||
case IECVT_REAL:
|
||||
{
|
||||
*(reinterpret_cast<IEC_REAL*>(value_container)) = static_cast<IEC_REAL>(value);
|
||||
break;
|
||||
}
|
||||
case IECVT_LREAL:
|
||||
{
|
||||
*(reinterpret_cast<IEC_LREAL*>(value_container)) = static_cast<IEC_LREAL>(value);
|
||||
break;
|
||||
}
|
||||
case IECVT_LWORD:
|
||||
{
|
||||
*(reinterpret_cast<IEC_LWORD*>(value_container)) = static_cast<IEC_LWORD>(value);
|
||||
break;
|
||||
}
|
||||
case IECVT_LINT:
|
||||
{
|
||||
*(reinterpret_cast<IEC_LINT*>(value_container)) = static_cast<IEC_LINT>(value);
|
||||
break;
|
||||
}
|
||||
case IECVT_ULINT:
|
||||
{
|
||||
*(reinterpret_cast<IEC_ULINT*>(value_container)) = static_cast<IEC_ULINT>(value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Dnp3Receiver::Start() {
|
||||
|
@ -250,7 +289,6 @@ void Dnp3Receiver::End() {
|
|||
spdlog::info("DNP3 receiver stopped");
|
||||
}
|
||||
|
||||
|
||||
#endif // OPLC_DNP3_OUTSTATION
|
||||
|
||||
/** @}*/
|
|
@ -12,11 +12,10 @@
|
|||
// See the License for the specific language governing permissionsand
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef CORE_DNP3_RECEIVER_H_
|
||||
#define CORE_DNP3_RECEIVER_H_
|
||||
#ifndef CORE_DNP3_DNP3_RECEIVER_H_
|
||||
#define CORE_DNP3_DNP3_RECEIVER_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <opendnp3/outstation/SimpleCommandHandler.h>
|
||||
|
||||
#include "dnp3.h"
|
||||
|
@ -32,9 +31,11 @@
|
|||
////////////////////////////////////////////////////////////////////////////////
|
||||
class Dnp3Receiver : public opendnp3::ICommandHandler {
|
||||
public:
|
||||
Dnp3Receiver(
|
||||
std::shared_ptr<GlueVariables> glue_variables,
|
||||
Dnp3Range range);
|
||||
/// Initialize a new instance of the DNP3 receiver. The receiver listens for point value updates
|
||||
/// over the DNP3 channel and then maps those to the glue variables.
|
||||
/// @param binary_commands The glue variables for the binary commands.
|
||||
/// @param analog_commands The glue variables for the analog commands.
|
||||
Dnp3Receiver(const Dnp3IndexedGroup& binary_commands, const Dnp3IndexedGroup& analog_commands);
|
||||
|
||||
opendnp3::CommandStatus Select(const opendnp3::ControlRelayOutputBlock& command, std::uint16_t index) override;
|
||||
|
||||
|
@ -56,22 +57,32 @@ class Dnp3Receiver : public opendnp3::ICommandHandler {
|
|||
|
||||
opendnp3::CommandStatus Operate(const opendnp3::AnalogOutputDouble64& command, std::uint16_t index, opendnp3::OperateType opType) override;
|
||||
|
||||
void ExchangeGlue();
|
||||
protected:
|
||||
void Start() final;
|
||||
void End() final;
|
||||
|
||||
private:
|
||||
template<class T>
|
||||
opendnp3::CommandStatus UpdateGlueVariable(T value, std::uint16_t dnp3_index) const;
|
||||
opendnp3::CommandStatus CacheUpdatedValue(T value, std::uint16_t dnp3_index);
|
||||
|
||||
template <typename T>
|
||||
struct CacheItem {
|
||||
bool has_value;
|
||||
T value;
|
||||
};
|
||||
|
||||
private:
|
||||
/// The buffers for data transfer.
|
||||
std::shared_ptr<GlueVariables> glue_variables;
|
||||
const Dnp3IndexedGroup& binary_commands;
|
||||
const Dnp3IndexedGroup& analog_commands;
|
||||
|
||||
/// The offsets into the glue that are valid for this instance.
|
||||
const Dnp3Range range;
|
||||
std::mutex cache_mutex;
|
||||
|
||||
CacheItem<bool>* const binary_commands_cache;
|
||||
CacheItem<double>* const analog_commands_cache;
|
||||
};
|
||||
|
||||
#endif // CORE_DNP3_RECEIVER_H_
|
||||
#endif // CORE_DNP3_DNP3_RECEIVER_H_
|
||||
|
||||
/** @}*/
|
||||
|
|
|
@ -515,8 +515,8 @@ int sendUnitData(struct enip_header *header, struct enip_data_Connected_0x70 *en
|
|||
int processEnipMessage(unsigned char *buffer, int buffer_size)
|
||||
{
|
||||
// initialize logging system
|
||||
unsigned char log_msg[1000];
|
||||
unsigned char *p = log_msg;
|
||||
char log_msg[1000];
|
||||
char *p = log_msg;
|
||||
|
||||
// initailize structs
|
||||
struct enip_header header;
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
// Copyright 2019 Smarter Grid Solutions
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http ://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissionsand
|
||||
// limitations under the License.
|
||||
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
|
||||
#include "glue.h"
|
||||
#include "ladder.h"
|
||||
|
||||
/// @brief Locates a partiular glue variable from the list of all glue
|
||||
/// variables.
|
||||
/// @param dir The direction of the variable.
|
||||
/// @param size The size of the variable.
|
||||
/// @param msi The most significant index of the variable.
|
||||
/// @param lsi The least significant index of the variable (only relevant
|
||||
/// for boolean types).
|
||||
/// @return The variable, or nullptr if there is no such variable.
|
||||
const GlueVariable* GlueVariablesBinding::find(IecLocationDirection dir,
|
||||
IecLocationSize size,
|
||||
std::uint16_t msi,
|
||||
std::uint8_t lsi) const {
|
||||
for (std::uint16_t i = 0; i < this->size; ++i) {
|
||||
const GlueVariable& cur_var = glue_variables[i];
|
||||
if (cur_var.dir == dir && cur_var.size == size && cur_var.msi == msi && cur_var.lsi == lsi) {
|
||||
return &glue_variables[i];
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const GlueVariable* GlueVariablesBinding::find(const std::string& location) const {
|
||||
if (location.length() < 4 || location[0] != '%') {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
IecLocationDirection direction;
|
||||
switch (location[1]) {
|
||||
case 'I':
|
||||
direction = IECLDT_IN;
|
||||
break;
|
||||
case 'Q':
|
||||
direction = IECLDT_OUT;
|
||||
break;
|
||||
case 'M':
|
||||
direction = IECLDT_MEM;
|
||||
break;
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
IecLocationSize size;
|
||||
switch (location[2]) {
|
||||
case 'X':
|
||||
size = IECLST_BIT;
|
||||
break;
|
||||
case 'B':
|
||||
size = IECLST_BYTE;
|
||||
break;
|
||||
case 'W':
|
||||
size = IECLST_WORD;
|
||||
break;
|
||||
case 'D':
|
||||
size = IECLST_DOUBLEWORD;
|
||||
break;
|
||||
case 'L':
|
||||
size = IECLST_LONGWORD;
|
||||
break;
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
char* end_msi;
|
||||
long msi = strtol(location.c_str() + 3, &end_msi, 10);
|
||||
|
||||
// Do we have more characters left in the string to read for lsi?
|
||||
std::size_t start_lsi = end_msi + 1 - location.c_str();
|
||||
if (start_lsi >= location.length()) {
|
||||
find(direction, size, msi, 0);
|
||||
}
|
||||
|
||||
char* end_lsi;
|
||||
long lsi = strtol(end_msi + 1, &end_lsi, 10);
|
||||
|
||||
return find(direction, size, msi, lsi);
|
||||
}
|
|
@ -16,6 +16,7 @@
|
|||
#define CORE_GLUE_H
|
||||
|
||||
#include <cstdint>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
|
||||
|
@ -29,6 +30,30 @@
|
|||
#define BUFFER_SIZE 1024
|
||||
#endif
|
||||
|
||||
#ifndef OPLC_IEC_GLUE_DIRECTION
|
||||
#define OPLC_IEC_GLUE_DIRECTION
|
||||
enum IecLocationDirection {
|
||||
IECLDT_IN,
|
||||
IECLDT_OUT,
|
||||
IECLDT_MEM,
|
||||
};
|
||||
#endif // OPLC_IEC_GLUE_DIRECTION
|
||||
|
||||
#ifndef OPLC_IEC_GLUE_SIZE
|
||||
#define OPLC_IEC_GLUE_SIZE
|
||||
enum IecLocationSize {
|
||||
/// Variables that are a single bit
|
||||
IECLST_BIT,
|
||||
/// Variables that are 1 byte
|
||||
IECLST_BYTE,
|
||||
/// Variables that are 2 bytes
|
||||
IECLST_WORD,
|
||||
/// Variables that are 4 bytes, including REAL
|
||||
IECLST_DOUBLEWORD,
|
||||
/// Variables that are 8 bytes, including LREAL
|
||||
IECLST_LONGWORD,
|
||||
};
|
||||
#endif // OPLC_IEC_GLUE_SIZE
|
||||
|
||||
#ifndef OPLC_IEC_GLUE_VALUE_TYPE
|
||||
#define OPLC_IEC_GLUE_VALUE_TYPE
|
||||
|
@ -54,127 +79,105 @@ enum IecGlueValueType {
|
|||
IECVT_LWORD,
|
||||
IECVT_LINT,
|
||||
IECVT_ULINT,
|
||||
/// This is not a normal type and won't appear in the glue variables
|
||||
/// here. But it does allow you to create your own indexed mapping
|
||||
/// and have a way to indicate a value that is not assigned a type.
|
||||
IECVT_UNASSIGNED
|
||||
};
|
||||
#endif // OPLC_IEC_GLUE_VALUE_TYPE
|
||||
|
||||
#ifndef OPLC_GLUE_BOOL_GROUP
|
||||
#define OPLC_GLUE_BOOL_GROUP
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
/// @brief Defines the mapping for a group of glued boolean variable.
|
||||
///
|
||||
/// This definition must be consistent with what is produced by the @ref glue_generator.
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
struct GlueBoolGroup {
|
||||
/// The first index for this array. If we are iterating over the glue
|
||||
/// variables, then this index is superfluous, but it is very helpful
|
||||
/// for debugging.
|
||||
std::uint16_t index;
|
||||
/// The values in this group. If the value is not assigned, then the
|
||||
/// value at the index points to nullptr.
|
||||
IEC_BOOL* values[8];
|
||||
};
|
||||
#endif // OPLC_GLUE_BOOL_GROUP
|
||||
|
||||
#ifndef OPLC_GLUE_VARIABLE
|
||||
#define OPLC_GLUE_VARIABLE
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
/// @brief Defines the mapping for a glued variable.
|
||||
/// @brief Defines the mapping for a glued variable. This defines a simple, space
|
||||
/// efficient lookup table. It has all of the mapping information that you
|
||||
/// need to find the variable based on the location name (e.g. %IB1.1). While
|
||||
/// this is space efficient, this should be searched once to construct a fast
|
||||
/// lookup into this table used for the remainder of the application lifecycle.
|
||||
///
|
||||
/// This definition must be consistent with what is produced by the @ref glue_generator.
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
struct GlueVariable {
|
||||
/// The type of the glue variable.
|
||||
/// The direction of the variable - this is determined by I/Q/M.
|
||||
IecLocationDirection dir;
|
||||
/// The size of the variable - this is determined by X/B/W/D/L.
|
||||
IecLocationSize size;
|
||||
/// The most significant index for the variable. This is the part of the
|
||||
/// name, converted to an integer, before the period.
|
||||
std::uint16_t msi;
|
||||
/// The least significant index (sub-index) for the variable. This is the
|
||||
/// part of the name, converted to an integer, after the period. It is
|
||||
/// only relevant for boolean (bit) values.
|
||||
std::uint8_t lsi;
|
||||
/// The type of the glue variable. This is used so that we correctly
|
||||
/// write into the value type.
|
||||
IecGlueValueType type;
|
||||
/// A pointer to the memory address for reading/writing the value.
|
||||
void* value;
|
||||
};
|
||||
#endif // OPLC_GLUE_VARIABLE
|
||||
#endif // OPLC_GLUE_VARIABLE
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
/// @brief Defines a collection of buffers by which we exchange data
|
||||
/// between the runtime and peripherals/middleware.
|
||||
///
|
||||
/// Except for the boolean buffers, each is an array of the same length
|
||||
/// where each value points to a memory location for data
|
||||
/// exchange. That is, the values of the arrays only indirectly
|
||||
/// indicate the held value.
|
||||
///
|
||||
/// In the case of booleans, there is a further level of indirection
|
||||
/// in that each location has as many as 8 child bits.
|
||||
///
|
||||
/// Access to the buffers is protected by a common mutex. You need
|
||||
/// to acquire the lock prior to reading or writing from the buffers.
|
||||
///
|
||||
/// Inputs are normally only populated by hardwired devices. They
|
||||
/// are inputs to the runtime. Outputs are normally populated by
|
||||
/// the runtime and are available as outputs from the runtime. The
|
||||
/// precise use of these conventions ultimately depends on the
|
||||
/// implementation of the communcation driver.
|
||||
///
|
||||
/// A value that is not mapped is marked with NULL and must not be used.
|
||||
/// @brief Defines accessors for glue variables.
|
||||
/// This structure wraps up items that are available as globals, but this allows
|
||||
/// a straighforward way to inject definitions into tests, so it is preferred
|
||||
/// to use this structure rather than globals.
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
struct GlueVariables {
|
||||
struct GlueVariablesBinding {
|
||||
|
||||
GlueVariables(
|
||||
std::mutex* buffer_lock,
|
||||
IEC_BOOL** bool_inputs,
|
||||
IEC_BOOL** bool_outputs,
|
||||
std::uint16_t inputs_size,
|
||||
GlueVariable* inputs,
|
||||
std::uint16_t outputs_size,
|
||||
GlueVariable* outputs) :
|
||||
GlueVariablesBinding(std::mutex* buffer_lock, const std::uint16_t size, const GlueVariable* glue_variables) :
|
||||
buffer_lock(buffer_lock),
|
||||
size(size),
|
||||
glue_variables(glue_variables)
|
||||
|
||||
buffer_lock(buffer_lock),
|
||||
bool_inputs(bool_inputs),
|
||||
bool_outputs(bool_outputs),
|
||||
inputs_size(inputs_size),
|
||||
inputs(inputs),
|
||||
outputs_size(outputs_size),
|
||||
outputs(outputs)
|
||||
{}
|
||||
{}
|
||||
|
||||
GlueVariables(const GlueVariables& copy) :
|
||||
|
||||
buffer_lock(copy.buffer_lock),
|
||||
bool_inputs(copy.bool_inputs),
|
||||
bool_outputs(copy.bool_outputs),
|
||||
inputs_size(copy.inputs_size),
|
||||
inputs(copy.inputs),
|
||||
outputs_size(copy.outputs_size),
|
||||
outputs(copy.outputs)
|
||||
{}
|
||||
|
||||
/// @brief Mutex for this structure
|
||||
/// @brief Mutex for the glue variables
|
||||
std::mutex* buffer_lock;
|
||||
|
||||
// Booleans - these are mapped separately because they have an additional
|
||||
// level of nesting.
|
||||
/// @brief The size of the glue variables array
|
||||
std::uint16_t size;
|
||||
|
||||
/// Mapped to IXx_x locations. This is a a 2D array where the second index
|
||||
/// must have a length of 8.
|
||||
IEC_BOOL** bool_inputs;
|
||||
/// @brief The glue variables array
|
||||
const GlueVariable* glue_variables;
|
||||
|
||||
/// Mapped to QXx_x locations. This is a a 2D array where the second index
|
||||
/// must have a length of 8.
|
||||
IEC_BOOL** bool_outputs;
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
/// @brief Find a glue varia&glue_mutexble based on the specification of the variable.
|
||||
/// @return the variable or null if there is no variable that matches all
|
||||
/// criteria in the specification.
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
const GlueVariable* find(IecLocationDirection dir,
|
||||
IecLocationSize size,
|
||||
std::uint16_t msi,
|
||||
std::uint8_t lsi) const;
|
||||
|
||||
/// @brief Gets the boolean input at the specified primary and secondary index.
|
||||
/// @param prim The primary (first) index in the 2-D array
|
||||
/// @param sec The secondary index in the 2-D array.
|
||||
/// @return A pointer to the boolean value.
|
||||
inline IEC_BOOL* BoolInputAt(std::uint16_t prim, std::uint16_t sec) {
|
||||
std::uint16_t idx = prim * 8 + sec;
|
||||
return this->bool_inputs[idx];
|
||||
}
|
||||
|
||||
/// @brief Gets the boolean output at the specified primary and secondary index.
|
||||
/// @param prim The primary (first) index in the 2-D array
|
||||
/// @param sec The secondary index in the 2-D array.
|
||||
/// @return A pointer to the boolean value.
|
||||
inline IEC_BOOL* BoolOutputAt(std::uint16_t prim, std::uint16_t sec) {
|
||||
std::uint16_t idx = prim * 8 + sec;
|
||||
return this->bool_outputs[idx];
|
||||
}
|
||||
|
||||
/// @brief The size of the inputs array. You can index up to inputs_size - 1
|
||||
/// in the array.
|
||||
const std::uint16_t inputs_size;
|
||||
|
||||
/// @brief The input glue variables array. The number of items in the array is
|
||||
/// given by inputs_size.
|
||||
GlueVariable* const inputs;
|
||||
|
||||
/// @brief The size of the outputs array. You can index up to outputs_size - 1
|
||||
/// in the array.
|
||||
const std::uint16_t outputs_size;
|
||||
|
||||
/// @brief The output glue variables array. The number of items in the array is
|
||||
/// given by outputs_size.
|
||||
GlueVariable* const outputs;
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
/// @brief Find a glue variable based on the location of the variable, for example
|
||||
/// %IX0.1
|
||||
/// @return the variable or null if there is no variable that matches all
|
||||
/// criteria in the specification.
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
const GlueVariable* find(const std::string& location) const;
|
||||
};
|
||||
|
||||
#endif // CORE_GLUE_H
|
||||
|
|
|
@ -26,36 +26,38 @@
|
|||
#include "ladder.h"
|
||||
#include "custom_layer.h"
|
||||
|
||||
/** \addtogroup openplc_runtime
|
||||
* @{
|
||||
*/
|
||||
/** @addtogroup blank Blank
|
||||
* \brief A template with placeholder functions
|
||||
* \ingroup hardware_layers
|
||||
* @{ */
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
/// @brief This function is called by the main OpenPLC routine when it
|
||||
/// is initializing. All hardware initialization procedures should be here.
|
||||
/// @see finalizeHardware()
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
/// \brief Initialization procedures
|
||||
///
|
||||
/// This function is called by the main OpenPLC routine when it is initializing.
|
||||
/// Hardware initialization procedures should be here.
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
void initializeHardware()
|
||||
{
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
/// @brief This function is called by the main OpenPLC routine when it is
|
||||
/// finalizing. Resource clearing procedures should be here.
|
||||
/// @see initializeHardware()
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
/// \brief Resource clearing
|
||||
///
|
||||
/// This function is called by the main OpenPLC routine when it is finalizing.
|
||||
/// Resource clearing procedures should be here.
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
void finalizeHardware()
|
||||
{
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
/// @brief Update input buffers
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
/// \brief Update internal buffers
|
||||
///
|
||||
/// This function is called by the OpenPLC in a loop. Here the internal buffers
|
||||
/// must be updated to reflect the actual Input state. The mutex bufferLock
|
||||
/// must be used to protect access to the buffers on a threaded environment.
|
||||
/// @see updateBuffersOut()
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
void updateBuffersIn()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(bufferLock); //lock mutex
|
||||
|
@ -71,14 +73,13 @@ void updateBuffersIn()
|
|||
**************************************************/
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
/// @brief Update output buffers
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
/// \brief Update output buffers
|
||||
///
|
||||
/// This function is called by the OpenPLC in a loop. Here the internal buffers
|
||||
/// must be updated to reflect the actual Output state. The mutex bufferLock
|
||||
/// must be used to protect access to the buffers on a threaded environment.
|
||||
/// @see updateBuffersIn()
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
void updateBuffersOut()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(bufferLock); //lock mutex
|
||||
|
@ -94,5 +95,4 @@ void updateBuffersOut()
|
|||
**************************************************/
|
||||
}
|
||||
|
||||
/** @}*/
|
||||
|
||||
/** @} */
|
|
@ -35,6 +35,7 @@
|
|||
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
#include "glue.h"
|
||||
#include "ladder.h"
|
||||
#include "logsink.h"
|
||||
|
||||
|
@ -86,7 +87,8 @@ void *modbusThread(void *arg)
|
|||
///////////////////////////////////////////////////////////////////////////////
|
||||
void *dnp3Thread(void *arg)
|
||||
{
|
||||
dnp3StartServer(dnp3_port);
|
||||
GlueVariablesBinding binding(&bufferLock, OPLCGLUE_GLUE_SIZE, oplc_glue_vars);
|
||||
dnp3StartServer(dnp3_port, &run_dnp3, binding);
|
||||
return nullptr;
|
||||
}
|
||||
#endif
|
||||
|
@ -130,7 +132,7 @@ int readCommandArgument(unsigned char *command)
|
|||
argument[j] = '\0';
|
||||
}
|
||||
|
||||
return atoi(argument);
|
||||
return atoi((char *)argument);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
|
|
@ -85,11 +85,10 @@ extern std::mutex bufferLock;
|
|||
extern unsigned long long common_ticktime__;
|
||||
|
||||
struct GlueVariable;
|
||||
struct GlueVariablesBinding;
|
||||
|
||||
extern const std::uint16_t OPLCGLUE_INPUT_SIZE;
|
||||
extern GlueVariable oplc_input_vars[];
|
||||
extern const std::uint16_t OPLCGLUE_OUTPUT_SIZE;
|
||||
extern GlueVariable oplc_output_vars[];
|
||||
extern const std::uint16_t OPLCGLUE_GLUE_SIZE;
|
||||
extern const GlueVariable oplc_glue_vars[];
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
//FUNCTION PROTOTYPES
|
||||
|
@ -161,7 +160,7 @@ void updateBuffersOut_MB();
|
|||
|
||||
#ifdef OPLC_DNP3_OUTSTATION
|
||||
//dnp3.cpp
|
||||
void dnp3StartServer(int port);
|
||||
void dnp3StartServer(int port, bool* run, const GlueVariablesBinding& binding);
|
||||
#endif
|
||||
|
||||
//persistent_storage.cpp
|
||||
|
|
|
@ -99,10 +99,10 @@ int createSocket(uint16_t port)
|
|||
|
||||
//Set SO_REUSEADDR
|
||||
int enable = 1;
|
||||
if (setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int)) < 0)
|
||||
if (setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int)) < 0)
|
||||
{
|
||||
spdlog::error("setsockopt(SO_REUSEADDR) failed");
|
||||
}
|
||||
spdlog::error("setsockopt(SO_REUSEADDR) failed");
|
||||
}
|
||||
|
||||
|
||||
SetSocketBlockingEnabled(socket_fd, false);
|
||||
|
@ -213,7 +213,7 @@ void *handleConnections(void *arguments)
|
|||
else if (protocol_type == ENIP_PROTOCOL)
|
||||
run_server = &run_enip;
|
||||
|
||||
spdlog::debug("Server: Thread created for client ID: {}", client_fd);
|
||||
spdlog::debug("Server: Thread created for client ID: {}", client_fd);
|
||||
|
||||
while(*run_server)
|
||||
{
|
||||
|
@ -226,11 +226,11 @@ void *handleConnections(void *arguments)
|
|||
// something has gone wrong or the client has closed connection
|
||||
if (messageSize == 0)
|
||||
{
|
||||
spdlog::debug("Server: client ID: {} has closed the connection", client_fd);
|
||||
spdlog::debug("Server: client ID: {} has closed the connection", client_fd);
|
||||
}
|
||||
else
|
||||
{
|
||||
spdlog::error("Server: Something is wrong with the client ID: {} message Size : {}", client_fd, messageSize);
|
||||
spdlog::error("Server: Something is wrong with the client ID: {} message Size : {}", client_fd, messageSize);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
@ -240,7 +240,7 @@ void *handleConnections(void *arguments)
|
|||
|
||||
spdlog::debug("Closing client socket and calling pthread_exit");
|
||||
close(client_fd);
|
||||
spdlog::info("Terminating server connections thread");
|
||||
spdlog::info("Terminating server connections thread");
|
||||
pthread_exit(NULL);
|
||||
}
|
||||
|
||||
|
@ -274,14 +274,14 @@ void startServer(uint16_t port, int protocol_type)
|
|||
client_fd = waitForClient(socket_fd, protocol_type); //block until a client connects
|
||||
if (client_fd < 0)
|
||||
{
|
||||
spdlog::info("Server: Error accepting client!");
|
||||
spdlog::info("Server: Error accepting client!");
|
||||
}
|
||||
else
|
||||
{
|
||||
int arguments[2];
|
||||
pthread_t thread;
|
||||
int ret = -1;
|
||||
spdlog::debug("Server: Client accepted! Creating thread for the new client ID: {}...", client_fd);
|
||||
spdlog::debug("Server: Client accepted! Creating thread for the new client ID: {}...", client_fd);
|
||||
arguments[0] = client_fd;
|
||||
arguments[1] = protocol_type;
|
||||
ret = pthread_create(&thread, NULL, handleConnections, (void*)arguments);
|
||||
|
@ -293,7 +293,7 @@ void startServer(uint16_t port, int protocol_type)
|
|||
}
|
||||
close(socket_fd);
|
||||
close(client_fd);
|
||||
spdlog::info("Terminating server thread");
|
||||
spdlog::info("Terminating server thread");
|
||||
}
|
||||
|
||||
/** @}*/
|
||||
|
|
|
@ -14,7 +14,9 @@
|
|||
|
||||
|
||||
import socket
|
||||
import time
|
||||
import unittest
|
||||
from pymodbus.client.sync import ModbusTcpClient as ModbusClient
|
||||
|
||||
class IntegrationTest(unittest.TestCase):
|
||||
|
||||
|
@ -26,5 +28,27 @@ class IntegrationTest(unittest.TestCase):
|
|||
print(data)
|
||||
s.close()
|
||||
|
||||
def test_connect_with_modbus(self):
|
||||
client = ModbusClient('localhost', port=502)
|
||||
unit=0x01
|
||||
|
||||
# Write the value true to the coil
|
||||
client.write_coil(1, True, unit=unit)
|
||||
time.sleep(1)
|
||||
|
||||
rr = client.read_coils(0, 1, unit=unit)
|
||||
bit_value = rr.getBit(0)
|
||||
|
||||
self.assertTrue(bit_value)
|
||||
|
||||
# Write the value false to the coil
|
||||
client.write_coil(1, False, unit=unit)
|
||||
time.sleep(1)
|
||||
|
||||
rr = client.read_coils(0, 1, unit=unit)
|
||||
bit_value = rr.getBit(0)
|
||||
|
||||
self.assertFalse(bit_value)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
|
@ -22,10 +22,14 @@ include_directories(../core/lib)
|
|||
include_directories(../vendor/catch2-2.7.0)
|
||||
include_directories(../vendor/fakeit-2.0.5)
|
||||
|
||||
if (NOT OPLC_DNP3_OUTSTATION)
|
||||
message(WARNING "Building of tests does not have DNP3 outstation enabled")
|
||||
endif()
|
||||
|
||||
# This is all of our test files
|
||||
file(GLOB oplctest_SRC *.cpp)
|
||||
|
||||
add_executable(oplc_unit_test ${oplctest_SRC} ../core/dnp3_publisher.cpp ../core/dnp3_receiver.cpp)
|
||||
add_executable(oplc_unit_test ${oplctest_SRC} ../core/glue.cpp ../core/dnp3.cpp ../core/dnp3_publisher.cpp ../core/dnp3_receiver.cpp)
|
||||
|
||||
target_link_libraries(oplc_unit_test ${OPLC_PTHREAD})
|
||||
if (OPLC_DNP3_OUTSTATION)
|
||||
|
|
|
@ -1,169 +0,0 @@
|
|||
// Copyright 2019 Smarter Grid Solutions
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http ://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissionsand
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef TEST_GLUE_TEST_HELPERS_H
|
||||
#define TEST_GLUE_TEST_HELPERS_H
|
||||
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
|
||||
#define TEST_BUFFER_SIZE 1024
|
||||
|
||||
/// Allocate a pointer to a new glue structure for the specific data type.
|
||||
/// Initializes the values in the structure to the specified initial value.
|
||||
/// @param buffer_size The size of the buffer to allocate.
|
||||
/// @param start The first index to assign a default value
|
||||
/// @param end One past the index to assign a default value.
|
||||
/// @param default_value the default value to assign to members.
|
||||
static inline GlueVariable* glue_alloc(std::uint16_t buffer_size, std::uint16_t start = 0, std::uint16_t stop = 0) {
|
||||
GlueVariable* buffer = new GlueVariable[buffer_size];
|
||||
|
||||
for (auto i = 0; i < buffer_size; ++i) {
|
||||
buffer[i].type = IECVT_UNASSIGNED;
|
||||
buffer[i].value = nullptr;
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
/// Allocate a pointer to a new glue structure for the boolean type - the boolean type has
|
||||
/// an additional level of nesting in comparision to other types.
|
||||
/// Initializes the values in the structure to the specified initial value.
|
||||
/// @param buffer_size The size of the buffer to allocate.
|
||||
/// @param start The first index to assign a default value
|
||||
/// @param end One past the index to assign a default value.
|
||||
/// @param default_value the default value to assign to members.
|
||||
static inline IEC_BOOL** glue_boolean_alloc(std::uint16_t buffer_size, std::uint16_t start = 0, std::uint16_t stop = 0) {
|
||||
IEC_BOOL** buffer = new IEC_BOOL*[buffer_size * 8];
|
||||
|
||||
for (auto i = 0; i < buffer_size * 8; ++i) {
|
||||
if (i >= start && i < stop) {
|
||||
buffer[i] = new IEC_BOOL;
|
||||
}
|
||||
else {
|
||||
buffer[i] = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
/// Free a glue structure that was previously allocated by glue_alloc.
|
||||
/// @param buffer The buffer to free.
|
||||
/// @param buffer_size The size of the buffer to free.
|
||||
static inline void glue_free(GlueVariable* buffer, std::uint16_t buffer_size) {
|
||||
for (auto i = 0; i < buffer_size; ++i) {
|
||||
void* value_ptr = buffer[i].value;
|
||||
switch (buffer[i].type) {
|
||||
case IECVT_BOOL:
|
||||
delete reinterpret_cast<IEC_BOOL*>(value_ptr);
|
||||
break;
|
||||
case IECVT_BYTE:
|
||||
delete reinterpret_cast<IEC_BYTE*>(value_ptr);
|
||||
break;
|
||||
case IECVT_SINT:
|
||||
delete reinterpret_cast<IEC_SINT*>(value_ptr);
|
||||
break;
|
||||
case IECVT_USINT:
|
||||
delete reinterpret_cast<IEC_USINT*>(value_ptr);
|
||||
break;
|
||||
case IECVT_INT:
|
||||
delete reinterpret_cast<IEC_INT*>(value_ptr);
|
||||
break;
|
||||
case IECVT_UINT:
|
||||
delete reinterpret_cast<IEC_UINT*>(value_ptr);
|
||||
break;
|
||||
case IECVT_WORD:
|
||||
delete reinterpret_cast<IEC_WORD*>(value_ptr);
|
||||
break;
|
||||
case IECVT_DINT:
|
||||
delete reinterpret_cast<IEC_DINT*>(value_ptr);
|
||||
break;
|
||||
case IECVT_UDINT:
|
||||
delete reinterpret_cast<IEC_UDINT*>(value_ptr);
|
||||
break;
|
||||
case IECVT_DWORD:
|
||||
delete reinterpret_cast<IEC_DWORD*>(value_ptr);
|
||||
break;
|
||||
case IECVT_REAL:
|
||||
delete reinterpret_cast<IEC_REAL*>(value_ptr);
|
||||
break;
|
||||
case IECVT_LREAL:
|
||||
delete reinterpret_cast<IEC_LREAL*>(value_ptr);
|
||||
break;
|
||||
case IECVT_LWORD:
|
||||
delete reinterpret_cast<IEC_LWORD*>(value_ptr);
|
||||
break;
|
||||
case IECVT_LINT:
|
||||
delete reinterpret_cast<IEC_LINT*>(value_ptr);
|
||||
break;
|
||||
case IECVT_ULINT:
|
||||
delete reinterpret_cast<IEC_ULINT*>(value_ptr);
|
||||
break;
|
||||
case IECVT_UNASSIGNED:
|
||||
break;
|
||||
}
|
||||
}
|
||||
delete[] buffer;
|
||||
}
|
||||
|
||||
/// Free a glue structure that was previously allocated by glue_boolean_alloc.
|
||||
/// @param buffer The buffer to free.
|
||||
/// @param buffer_size The size of the buffer to free.
|
||||
static inline void glue_boolean_free(IEC_BOOL** buffer, std::uint16_t buffer_size) {
|
||||
for (auto i = 0; i < buffer_size * 8; ++i) {
|
||||
if (buffer[i] != nullptr) {
|
||||
delete buffer[i];
|
||||
}
|
||||
}
|
||||
delete[] buffer;
|
||||
}
|
||||
|
||||
/// Creates an instance of the GlueVariables structure suitable for testing. The all children
|
||||
/// are appropriately initialized with a buffer of a default size.
|
||||
/// @return the allocated glue variables.
|
||||
static inline std::shared_ptr<GlueVariables> make_vars() {
|
||||
//Booleans
|
||||
IEC_BOOL** bool_input = glue_boolean_alloc(TEST_BUFFER_SIZE, 0, TEST_BUFFER_SIZE);
|
||||
IEC_BOOL** bool_output = glue_boolean_alloc(TEST_BUFFER_SIZE, 0, TEST_BUFFER_SIZE);
|
||||
|
||||
//Bytes
|
||||
GlueVariable* inputs = glue_alloc(TEST_BUFFER_SIZE, 0, TEST_BUFFER_SIZE);
|
||||
GlueVariable* outputs = glue_alloc(TEST_BUFFER_SIZE, 0, TEST_BUFFER_SIZE);
|
||||
|
||||
auto mutex = new std::mutex;
|
||||
|
||||
return std::shared_ptr<GlueVariables>(new GlueVariables{
|
||||
mutex,
|
||||
|
||||
bool_input,
|
||||
bool_output,
|
||||
|
||||
TEST_BUFFER_SIZE,
|
||||
inputs,
|
||||
TEST_BUFFER_SIZE,
|
||||
outputs,
|
||||
}, [](GlueVariables* vars) {
|
||||
// Specialize the destructor for the shared pointer so that this will
|
||||
// clean up everything on destruction.
|
||||
delete vars->buffer_lock;
|
||||
glue_boolean_free(vars->bool_inputs, TEST_BUFFER_SIZE);
|
||||
glue_boolean_free(vars->bool_outputs, TEST_BUFFER_SIZE);
|
||||
glue_free(vars->inputs, TEST_BUFFER_SIZE);
|
||||
glue_free(vars->outputs, TEST_BUFFER_SIZE);
|
||||
delete vars;
|
||||
});
|
||||
}
|
||||
|
||||
#endif // TEST_GLUE_TEST_HELPERS_H
|
|
@ -17,90 +17,179 @@
|
|||
#include <cstdint>
|
||||
#include <utility>
|
||||
#include <mutex>
|
||||
#include <asiodnp3/OutstationStackConfig.h>
|
||||
|
||||
#include "catch.hpp"
|
||||
#include "fakeit.hpp"
|
||||
|
||||
// The current DNP3 file does not expose anything and my current goal is to minimize
|
||||
// changes. For now, just include it here so that we can we test it. This is needed
|
||||
// because the structure currently depends on a number of globals that make testing
|
||||
// a bit hard.
|
||||
void sleep_until(timespec*, int) {}
|
||||
bool run_dnp3(false);
|
||||
#include "dnp3.cpp"
|
||||
#include "glue.h"
|
||||
IEC_BOOL* bool_output[BUFFER_SIZE][8];
|
||||
IEC_BOOL* bool_input[BUFFER_SIZE][8];
|
||||
std::mutex bufferLock;
|
||||
const std::uint16_t OPLCGLUE_INPUT_SIZE(1);
|
||||
GlueVariable oplc_input_vars[] = {
|
||||
{ IECVT_UNASSIGNED, nullptr },
|
||||
};
|
||||
const std::uint16_t OPLCGLUE_OUTPUT_SIZE(1);
|
||||
GlueVariable oplc_output_vars[] = {
|
||||
{ IECVT_UNASSIGNED, nullptr },
|
||||
};
|
||||
#include "dnp3.h"
|
||||
|
||||
using namespace asiodnp3;
|
||||
using namespace std;
|
||||
|
||||
SCENARIO("create_config", "")
|
||||
{
|
||||
GIVEN("<input stream>")
|
||||
{
|
||||
WHEN("stream is empty creates default config")
|
||||
{
|
||||
std::stringstream input_stream;
|
||||
pair<OutstationStackConfig, Dnp3Range> config_range(create_config(input_stream));
|
||||
OutstationStackConfig config = config_range.first;
|
||||
mutex glue_mutex;
|
||||
unique_ptr<istream, std::function<void(istream*)>> cfg_stream(new stringstream(""), [](istream* s) { delete s; });
|
||||
Dnp3IndexedGroup binary_commands = {0};
|
||||
Dnp3IndexedGroup analog_commands = {0};
|
||||
Dnp3MappedGroup measurements = {0};
|
||||
|
||||
REQUIRE(config.dbConfig.binary.IsEmpty());
|
||||
REQUIRE(config.dbConfig.doubleBinary.IsEmpty());
|
||||
REQUIRE(config.dbConfig.analog.IsEmpty());
|
||||
REQUIRE(config.dbConfig.counter.IsEmpty());
|
||||
REQUIRE(config.dbConfig.frozenCounter.IsEmpty());
|
||||
REQUIRE(config.dbConfig.boStatus.IsEmpty());
|
||||
REQUIRE(config.dbConfig.aoStatus.IsEmpty());
|
||||
REQUIRE(config.dbConfig.timeAndInterval.IsEmpty());
|
||||
REQUIRE(config.link.IsMaster == false);
|
||||
REQUIRE(config.link.UseConfirms == false);
|
||||
REQUIRE(config.link.NumRetry == 0);
|
||||
REQUIRE(config.link.LocalAddr == 1024);
|
||||
REQUIRE(config.link.RemoteAddr == 1);
|
||||
}
|
||||
GIVEN("<input stream>")
|
||||
{
|
||||
WHEN("stream is empty creates default config")
|
||||
{
|
||||
GlueVariablesBinding bindings(&glue_mutex, 0, nullptr);
|
||||
std::stringstream input_stream;
|
||||
const OutstationStackConfig config(dnp3_create_config(input_stream, bindings, binary_commands, analog_commands, measurements));
|
||||
|
||||
WHEN("stream only specifies default size")
|
||||
{
|
||||
std::stringstream input_stream("database_size=1");
|
||||
pair<OutstationStackConfig, Dnp3Range> config_range(create_config(input_stream));
|
||||
OutstationStackConfig config = config_range.first;
|
||||
REQUIRE(config.dbConfig.binary.IsEmpty());
|
||||
REQUIRE(config.dbConfig.doubleBinary.IsEmpty());
|
||||
REQUIRE(config.dbConfig.analog.IsEmpty());
|
||||
REQUIRE(config.dbConfig.counter.IsEmpty());
|
||||
REQUIRE(config.dbConfig.frozenCounter.IsEmpty());
|
||||
REQUIRE(config.dbConfig.boStatus.IsEmpty());
|
||||
REQUIRE(config.dbConfig.aoStatus.IsEmpty());
|
||||
REQUIRE(config.dbConfig.timeAndInterval.IsEmpty());
|
||||
REQUIRE(config.link.IsMaster == false);
|
||||
REQUIRE(config.link.UseConfirms == false);
|
||||
REQUIRE(config.link.NumRetry == 0);
|
||||
REQUIRE(config.link.LocalAddr == 1024);
|
||||
REQUIRE(config.link.RemoteAddr == 1);
|
||||
}
|
||||
|
||||
REQUIRE(config.dbConfig.binary.Size() == 1);
|
||||
REQUIRE(config.dbConfig.doubleBinary.Size() == 1);
|
||||
REQUIRE(config.dbConfig.analog.Size() == 1);
|
||||
REQUIRE(config.dbConfig.counter.Size() == 1);
|
||||
REQUIRE(config.dbConfig.frozenCounter.Size() == 1);
|
||||
REQUIRE(config.dbConfig.boStatus.Size() == 1);
|
||||
REQUIRE(config.dbConfig.aoStatus.Size() == 1);
|
||||
REQUIRE(config.dbConfig.timeAndInterval.Size() == 1);
|
||||
REQUIRE(config.link.IsMaster == false);
|
||||
REQUIRE(config.link.UseConfirms == false);
|
||||
REQUIRE(config.link.NumRetry == 0);
|
||||
REQUIRE(config.link.LocalAddr == 1024);
|
||||
REQUIRE(config.link.RemoteAddr == 1);
|
||||
}
|
||||
}
|
||||
WHEN("stream specifies size based on glue variables for one boolean")
|
||||
{
|
||||
bool bool_var;
|
||||
const GlueVariable glue_vars[] = {
|
||||
{ IECLDT_OUT, IECLST_BIT, 0, 0, IECVT_BOOL, &bool_var },
|
||||
};
|
||||
GlueVariablesBinding bindings(&glue_mutex, 1, glue_vars);
|
||||
std::stringstream input_stream("bind_location=name:%QX0.0,group:1,index:0,");
|
||||
const OutstationStackConfig config(dnp3_create_config(input_stream, bindings, binary_commands, analog_commands, measurements));
|
||||
|
||||
REQUIRE(config.dbConfig.binary.Size() == 1);
|
||||
REQUIRE(config.dbConfig.doubleBinary.Size() == 0);
|
||||
REQUIRE(config.dbConfig.analog.Size() == 0);
|
||||
REQUIRE(config.dbConfig.counter.Size() == 0);
|
||||
REQUIRE(config.dbConfig.frozenCounter.Size() == 0);
|
||||
REQUIRE(config.dbConfig.boStatus.Size() == 0);
|
||||
REQUIRE(config.dbConfig.aoStatus.Size() == 0);
|
||||
REQUIRE(config.dbConfig.timeAndInterval.Size() == 0);
|
||||
REQUIRE(config.link.IsMaster == false);
|
||||
REQUIRE(config.link.UseConfirms == false);
|
||||
REQUIRE(config.link.NumRetry == 0);
|
||||
REQUIRE(config.link.LocalAddr == 1024);
|
||||
REQUIRE(config.link.RemoteAddr == 1);
|
||||
|
||||
// We should have bound the one variable
|
||||
REQUIRE(measurements.items[0].variable == &(glue_vars[0]));
|
||||
}
|
||||
|
||||
WHEN("stream specifies size based on glue variables for one boolean at index 1")
|
||||
{
|
||||
bool bool_var;
|
||||
const GlueVariable glue_vars[] = {
|
||||
{ IECLDT_IN, IECLST_BIT, 0, 0, IECVT_BOOL, &bool_var },
|
||||
};
|
||||
GlueVariablesBinding bindings(&glue_mutex, 1, glue_vars);
|
||||
std::stringstream input_stream("bind_location=name:%IX0.0,group:1,index:1,");
|
||||
const OutstationStackConfig config(dnp3_create_config(input_stream, bindings, binary_commands, analog_commands, measurements));
|
||||
|
||||
REQUIRE(config.dbConfig.binary.Size() == 1);
|
||||
REQUIRE(config.dbConfig.doubleBinary.Size() == 0);
|
||||
REQUIRE(config.dbConfig.analog.Size() == 0);
|
||||
REQUIRE(config.dbConfig.counter.Size() == 0);
|
||||
REQUIRE(config.dbConfig.frozenCounter.Size() == 0);
|
||||
REQUIRE(config.dbConfig.boStatus.Size() == 0);
|
||||
REQUIRE(config.dbConfig.aoStatus.Size() == 0);
|
||||
REQUIRE(config.dbConfig.timeAndInterval.Size() == 0);
|
||||
REQUIRE(config.link.IsMaster == false);
|
||||
REQUIRE(config.link.UseConfirms == false);
|
||||
REQUIRE(config.link.NumRetry == 0);
|
||||
REQUIRE(config.link.LocalAddr == 1024);
|
||||
REQUIRE(config.link.RemoteAddr == 1);
|
||||
|
||||
// We should have bound the one variable
|
||||
REQUIRE(measurements.items[0].variable == &(glue_vars[0]));
|
||||
}
|
||||
|
||||
WHEN("stream specifies size based on glue variables for one boolean command at index 1")
|
||||
{
|
||||
bool bool_var;
|
||||
const GlueVariable glue_vars[] = {
|
||||
{ IECLDT_IN, IECLST_BIT, 0, 0, IECVT_BOOL, &bool_var },
|
||||
};
|
||||
GlueVariablesBinding bindings(&glue_mutex, 1, glue_vars);
|
||||
std::stringstream input_stream("bind_location=name:%IX0.0,group:12,index:1,");
|
||||
const OutstationStackConfig config(dnp3_create_config(input_stream, bindings, binary_commands, analog_commands, measurements));
|
||||
|
||||
REQUIRE(config.dbConfig.binary.Size() == 0);
|
||||
REQUIRE(config.dbConfig.doubleBinary.Size() == 0);
|
||||
REQUIRE(config.dbConfig.analog.Size() == 0);
|
||||
REQUIRE(config.dbConfig.counter.Size() == 0);
|
||||
REQUIRE(config.dbConfig.frozenCounter.Size() == 0);
|
||||
REQUIRE(config.dbConfig.boStatus.Size() == 0);
|
||||
REQUIRE(config.dbConfig.aoStatus.Size() == 0);
|
||||
REQUIRE(config.dbConfig.timeAndInterval.Size() == 0);
|
||||
REQUIRE(config.link.IsMaster == false);
|
||||
REQUIRE(config.link.UseConfirms == false);
|
||||
REQUIRE(config.link.NumRetry == 0);
|
||||
REQUIRE(config.link.LocalAddr == 1024);
|
||||
REQUIRE(config.link.RemoteAddr == 1);
|
||||
|
||||
// We should have bound the one variable
|
||||
REQUIRE(measurements.size == 0);
|
||||
REQUIRE(binary_commands.size == 2);
|
||||
REQUIRE(binary_commands.items[1] == &(glue_vars[0]));
|
||||
}
|
||||
|
||||
WHEN("stream specifies size based on glue variables for one real at index 1")
|
||||
{
|
||||
IEC_REAL real_var(9);
|
||||
const GlueVariable glue_vars[] = {
|
||||
{ IECLDT_OUT, IECLST_DOUBLEWORD, 0, 0, IECVT_REAL, &real_var },
|
||||
};
|
||||
GlueVariablesBinding bindings(&glue_mutex, 1, glue_vars);
|
||||
std::stringstream input_stream("bind_location=name:%QD0,group:30,index:1,");
|
||||
const OutstationStackConfig config(dnp3_create_config(input_stream, bindings, binary_commands, analog_commands, measurements));
|
||||
|
||||
REQUIRE(config.dbConfig.binary.Size() == 0);
|
||||
REQUIRE(config.dbConfig.doubleBinary.Size() == 0);
|
||||
REQUIRE(config.dbConfig.analog.Size() == 1);
|
||||
REQUIRE(config.dbConfig.counter.Size() == 0);
|
||||
REQUIRE(config.dbConfig.frozenCounter.Size() == 0);
|
||||
REQUIRE(config.dbConfig.boStatus.Size() == 0);
|
||||
REQUIRE(config.dbConfig.aoStatus.Size() == 0);
|
||||
REQUIRE(config.dbConfig.timeAndInterval.Size() == 0);
|
||||
REQUIRE(config.link.IsMaster == false);
|
||||
REQUIRE(config.link.UseConfirms == false);
|
||||
REQUIRE(config.link.NumRetry == 0);
|
||||
REQUIRE(config.link.LocalAddr == 1024);
|
||||
REQUIRE(config.link.RemoteAddr == 1);
|
||||
|
||||
// We should have bound the one variable
|
||||
REQUIRE(measurements.items[0].variable->value == &real_var);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SCENARIO("dnp3StartServer", "")
|
||||
{
|
||||
WHEN("provides configuration stream but not run")
|
||||
{
|
||||
// Configure this to start and then immediately terminate
|
||||
// the run flag is set to false. This should just return quickly
|
||||
bool run_dnp3(false);
|
||||
unique_ptr<istream, std::function<void(istream*)>> cfg_stream(new stringstream(""), [](istream* s) { delete s; });
|
||||
dnp3StartServer(20000, cfg_stream, &run_dnp3);
|
||||
}
|
||||
mutex glue_mutex;
|
||||
|
||||
WHEN("provides configuration stream but not run")
|
||||
{
|
||||
// Configure this to start and then immediately terminate
|
||||
// the run flag is set to false. This should just return quickly
|
||||
bool run_dnp3(false);
|
||||
unique_ptr<istream, std::function<void(istream*)>> cfg_stream(new stringstream(""), [](istream* s) { delete s; });
|
||||
GlueVariablesBinding bindings(&glue_mutex, 0, nullptr);
|
||||
|
||||
dnp3StartServer(20000, cfg_stream, &run_dnp3, bindings);
|
||||
}
|
||||
}
|
||||
|
||||
#endif // OPLC_DNP3_OUTSTATION
|
||||
|
|
|
@ -14,16 +14,19 @@
|
|||
|
||||
#ifdef OPLC_DNP3_OUTSTATION
|
||||
|
||||
#include <memory>
|
||||
#include <cstdint>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include <asiodnp3/IOutstation.h>
|
||||
|
||||
#include "catch.hpp"
|
||||
#include "fakeit.hpp"
|
||||
|
||||
#include "glue.h"
|
||||
#include "glue_test_helpers.h"
|
||||
#include "dnp3.h"
|
||||
#include "dnp3_publisher.h"
|
||||
|
||||
using namespace std;
|
||||
using namespace fakeit;
|
||||
using namespace opendnp3;
|
||||
|
||||
|
@ -32,10 +35,10 @@ using namespace opendnp3;
|
|||
/// during the tests whether the correct updates were called.
|
||||
class UpdateCaptureHandler : public opendnp3::IUpdateHandler {
|
||||
public:
|
||||
std::vector<std::pair<bool, std::uint16_t>> binary;
|
||||
std::vector<std::pair<bool, std::uint16_t>> binaryOutput;
|
||||
std::vector<std::pair<double, std::uint16_t>> analog;
|
||||
std::vector<std::pair<double, std::uint16_t>> analogOutput;
|
||||
vector<pair<bool, uint16_t>> binary;
|
||||
vector<pair<bool, uint16_t>> binary_output;
|
||||
vector<pair<double, uint16_t>> analog;
|
||||
vector<pair<double, uint16_t>> analog_output;
|
||||
|
||||
virtual ~UpdateCaptureHandler() {}
|
||||
virtual bool Update(const Binary& meas, uint16_t index, EventMode mode) {
|
||||
|
@ -56,11 +59,11 @@ public:
|
|||
return true;
|
||||
}
|
||||
virtual bool Update(const BinaryOutputStatus& meas, uint16_t index, EventMode mode) {
|
||||
binaryOutput.push_back(std::make_pair(meas.value, index));
|
||||
binary_output.push_back(std::make_pair(meas.value, index));
|
||||
return true;
|
||||
}
|
||||
virtual bool Update(const AnalogOutputStatus& meas, uint16_t index, EventMode mode) {
|
||||
analogOutput.push_back(std::make_pair(meas.value, index));
|
||||
analog_output.push_back(std::make_pair(meas.value, index));
|
||||
return true;
|
||||
}
|
||||
virtual bool Update(const TimeAndInterval& meas, uint16_t index) {
|
||||
|
@ -71,146 +74,163 @@ public:
|
|||
}
|
||||
};
|
||||
|
||||
SCENARIO("dnp3 publisher", "WriteToPoints") {
|
||||
SCENARIO("dnp3 publisher", "ExchangeGlue") {
|
||||
Mock<asiodnp3::IOutstation> mock_outstation;
|
||||
UpdateCaptureHandler updateHandler;
|
||||
UpdateCaptureHandler update_handler;
|
||||
|
||||
When(Method(mock_outstation, Apply)).AlwaysDo([&](const asiodnp3::Updates& updates) {
|
||||
updates.Apply(updateHandler);
|
||||
updates.Apply(update_handler);
|
||||
});
|
||||
auto outstation = std::shared_ptr<asiodnp3::IOutstation>(&mock_outstation.get(), [](asiodnp3::IOutstation*) {});
|
||||
auto variables = make_vars();
|
||||
auto range = Dnp3Range{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
|
||||
|
||||
|
||||
GIVEN("No glue range") {
|
||||
Dnp3Publisher mapper(outstation, variables, range);
|
||||
auto num_writes = mapper.WriteToPoints();
|
||||
Dnp3MappedGroup measurements = {0};
|
||||
Dnp3Publisher publisher(outstation, measurements);
|
||||
|
||||
GIVEN("No glued measurements") {
|
||||
auto num_writes = publisher.ExchangeGlue();
|
||||
THEN("Writes nothing") {
|
||||
REQUIRE(num_writes == 0);
|
||||
}
|
||||
}
|
||||
|
||||
GIVEN("Boolean input variable at offset 0") {
|
||||
range.bool_inputs_start = 0;
|
||||
range.bool_inputs_end = 1;
|
||||
Dnp3Publisher mapper(outstation, variables, range);
|
||||
GIVEN("Boolean input variable at offset 0") {
|
||||
IEC_BOOL bool_val(0);
|
||||
auto group = GlueBoolGroup { .index=0, .values={ &bool_val, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr } };
|
||||
|
||||
WHEN("value is false") {
|
||||
*variables->bool_inputs[0] = false;
|
||||
auto num_writes = mapper.WriteToPoints();
|
||||
const GlueVariable glue_var = { IECLDT_OUT, IECLST_BIT, 0, 0, IECVT_BOOL, &group };
|
||||
DNP3MappedGlueVariable mapped_vars[] = { {
|
||||
.group = 1,
|
||||
.point_index_number = 0,
|
||||
.variable = &glue_var
|
||||
} };
|
||||
|
||||
THEN("Writes binary input false") {
|
||||
REQUIRE(num_writes == 1);
|
||||
Verify(Method(mock_outstation, Apply)).Exactly(Once);
|
||||
REQUIRE(updateHandler.binary.size() == 1);
|
||||
REQUIRE(updateHandler.binary[0].first == false);
|
||||
REQUIRE(updateHandler.binary[0].second == 0);
|
||||
}
|
||||
}
|
||||
|
||||
WHEN("value is true") {
|
||||
*variables->bool_inputs[0] = true;
|
||||
auto num_writes = mapper.WriteToPoints();
|
||||
|
||||
THEN("Writes binary input true") {
|
||||
REQUIRE(num_writes == 1);
|
||||
Verify(Method(mock_outstation, Apply)).Exactly(Once);
|
||||
REQUIRE(updateHandler.binary.size() == 1);
|
||||
REQUIRE(updateHandler.binary[0].first == true);
|
||||
REQUIRE(updateHandler.binary[0].second == 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GIVEN("Boolean input variable at offset 8") {
|
||||
WHEN("Range is set and value is true") {
|
||||
range.bool_inputs_start = 8;
|
||||
range.bool_inputs_end = 9;
|
||||
Dnp3Publisher mapper(outstation, variables, range);
|
||||
|
||||
*variables->bool_inputs[1] = true;
|
||||
auto num_writes = mapper.WriteToPoints();
|
||||
|
||||
THEN("Writes binary input") {
|
||||
REQUIRE(num_writes == 1);
|
||||
Verify(Method(mock_outstation, Apply)).Exactly(Once);
|
||||
REQUIRE(updateHandler.binary.size() == 1);
|
||||
REQUIRE(updateHandler.binary[0].first == true);
|
||||
REQUIRE(updateHandler.binary[0].second == 8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GIVEN("One boolean output status variable at offset 0") {
|
||||
range.bool_outputs_start = 0;
|
||||
range.bool_outputs_end = 1;
|
||||
Dnp3Publisher mapper(outstation, variables, range);
|
||||
Dnp3MappedGroup measurements;
|
||||
measurements.size = 1;
|
||||
measurements.items = mapped_vars;
|
||||
Dnp3Publisher publisher(outstation, measurements);
|
||||
|
||||
WHEN("value is false") {
|
||||
*variables->bool_outputs[0] = false;
|
||||
auto num_writes = mapper.WriteToPoints();
|
||||
bool_val = false;
|
||||
auto num_writes = publisher.ExchangeGlue();
|
||||
|
||||
THEN("Writes binary output false") {
|
||||
THEN("Writes binary input false") {
|
||||
REQUIRE(num_writes == 1);
|
||||
Verify(Method(mock_outstation, Apply)).Exactly(Once);
|
||||
REQUIRE(updateHandler.binaryOutput.size() == 1);
|
||||
REQUIRE(updateHandler.binaryOutput[0].first == false);
|
||||
REQUIRE(updateHandler.binaryOutput[0].second == 0);
|
||||
REQUIRE(update_handler.binary.size() == 1);
|
||||
REQUIRE(update_handler.binary[0].first == false);
|
||||
REQUIRE(update_handler.binary[0].second == 0);
|
||||
}
|
||||
}
|
||||
|
||||
WHEN("value is true") {
|
||||
*variables->bool_outputs[0] = true;
|
||||
auto num_writes = mapper.WriteToPoints();
|
||||
bool_val = true;
|
||||
auto num_writes = publisher.ExchangeGlue();
|
||||
|
||||
THEN("Writes binary output true") {
|
||||
THEN("Writes binary input true") {
|
||||
REQUIRE(num_writes == 1);
|
||||
Verify(Method(mock_outstation, Apply)).Exactly(Once);
|
||||
REQUIRE(updateHandler.binaryOutput.size() == 1);
|
||||
REQUIRE(updateHandler.binaryOutput[0].first == true);
|
||||
REQUIRE(updateHandler.binaryOutput[0].second == 0);
|
||||
REQUIRE(update_handler.binary.size() == 1);
|
||||
REQUIRE(update_handler.binary[0].first == true);
|
||||
REQUIRE(update_handler.binary[0].second == 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GIVEN("One input register") {
|
||||
range.inputs_start = 0;
|
||||
range.inputs_end = 1;
|
||||
Dnp3Publisher mapper(outstation, variables, range);
|
||||
GIVEN("Boolean output status variable at offset 0") {
|
||||
IEC_BOOL bool_val(0);
|
||||
auto group = GlueBoolGroup { .index=0, .values={ &bool_val, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr } };
|
||||
|
||||
WHEN("Value is 9") {
|
||||
variables->inputs[0].type = IECVT_INT;
|
||||
variables->inputs[0].value = new IEC_INT{ 9 };
|
||||
auto num_writes = mapper.WriteToPoints();
|
||||
const GlueVariable glue_var = { IECLDT_OUT, IECLST_BIT, 0, 0, IECVT_BOOL, &group };
|
||||
DNP3MappedGlueVariable mapped_vars[] = { {
|
||||
.group = 10,
|
||||
.point_index_number = 0,
|
||||
.variable = &glue_var
|
||||
} };
|
||||
|
||||
THEN("Writes analog output 9") {
|
||||
Dnp3MappedGroup measurements;
|
||||
measurements.size = 1;
|
||||
measurements.items = mapped_vars;
|
||||
Dnp3Publisher publisher(outstation, measurements);
|
||||
|
||||
WHEN("value is false") {
|
||||
bool_val = false;
|
||||
auto num_writes = publisher.ExchangeGlue();
|
||||
|
||||
THEN("Writes binary input false") {
|
||||
REQUIRE(num_writes == 1);
|
||||
Verify(Method(mock_outstation, Apply)).Exactly(Once);
|
||||
REQUIRE(updateHandler.analog.size() == 1);
|
||||
REQUIRE(updateHandler.analog[0].first == 9);
|
||||
REQUIRE(updateHandler.analog[0].second == 0);
|
||||
REQUIRE(update_handler.binary_output.size() == 1);
|
||||
REQUIRE(update_handler.binary_output[0].first == false);
|
||||
REQUIRE(update_handler.binary_output[0].second == 0);
|
||||
}
|
||||
}
|
||||
|
||||
WHEN("value is true") {
|
||||
bool_val = true;
|
||||
auto num_writes = publisher.ExchangeGlue();
|
||||
|
||||
THEN("Writes binary input true") {
|
||||
REQUIRE(num_writes == 1);
|
||||
Verify(Method(mock_outstation, Apply)).Exactly(Once);
|
||||
REQUIRE(update_handler.binary_output.size() == 1);
|
||||
REQUIRE(update_handler.binary_output[0].first == true);
|
||||
REQUIRE(update_handler.binary_output[0].second == 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GIVEN("One output register") {
|
||||
range.outputs_start = 0;
|
||||
range.outputs_end = 1;
|
||||
Dnp3Publisher mapper(outstation, variables, range);
|
||||
GIVEN("Real variable at offset 0") {
|
||||
IEC_REAL real_val(9);
|
||||
|
||||
WHEN("Value is 9") {
|
||||
variables->outputs[0].type = IECVT_INT;
|
||||
variables->outputs[0].value = new IEC_INT{ 9 };
|
||||
auto num_writes = mapper.WriteToPoints();
|
||||
const GlueVariable glue_var = { IECLDT_OUT, IECLST_DOUBLEWORD, 0, 0, IECVT_REAL, &real_val };
|
||||
DNP3MappedGlueVariable mapped_vars[] = { {
|
||||
.group = 30,
|
||||
.point_index_number = 0,
|
||||
.variable = &glue_var
|
||||
} };
|
||||
|
||||
THEN("Writes analog output 9") {
|
||||
Dnp3MappedGroup measurements;
|
||||
measurements.size = 1;
|
||||
measurements.items = mapped_vars;
|
||||
Dnp3Publisher publisher(outstation, measurements);
|
||||
|
||||
WHEN("value is 9") {
|
||||
auto num_writes = publisher.ExchangeGlue();
|
||||
|
||||
THEN("Writes binary input false") {
|
||||
REQUIRE(num_writes == 1);
|
||||
Verify(Method(mock_outstation, Apply)).Exactly(Once);
|
||||
REQUIRE(updateHandler.analogOutput.size() == 1);
|
||||
REQUIRE(updateHandler.analogOutput[0].first == 9);
|
||||
REQUIRE(updateHandler.analogOutput[0].second == 0);
|
||||
REQUIRE(update_handler.analog.size() == 1);
|
||||
REQUIRE(update_handler.analog[0].first == 9);
|
||||
REQUIRE(update_handler.analog[0].second == 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GIVEN("Real status variable at offset 0") {
|
||||
IEC_REAL real_val(9);
|
||||
|
||||
const GlueVariable glue_var = { IECLDT_OUT, IECLST_DOUBLEWORD, 0, 0, IECVT_REAL, &real_val };
|
||||
DNP3MappedGlueVariable mapped_vars[] = { {
|
||||
.group = 40,
|
||||
.point_index_number = 0,
|
||||
.variable = &glue_var
|
||||
} };
|
||||
|
||||
Dnp3MappedGroup measurements;
|
||||
measurements.size = 1;
|
||||
measurements.items = mapped_vars;
|
||||
Dnp3Publisher publisher(outstation, measurements);
|
||||
|
||||
WHEN("value is 9") {
|
||||
auto num_writes = publisher.ExchangeGlue();
|
||||
|
||||
THEN("Writes binary input false") {
|
||||
REQUIRE(num_writes == 1);
|
||||
Verify(Method(mock_outstation, Apply)).Exactly(Once);
|
||||
REQUIRE(update_handler.analog_output.size() == 1);
|
||||
REQUIRE(update_handler.analog_output[0].first == 9);
|
||||
REQUIRE(update_handler.analog_output[0].second == 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,17 +18,15 @@
|
|||
|
||||
#include "dnp3_receiver.h"
|
||||
#include "glue.h"
|
||||
#include "glue_test_helpers.h"
|
||||
|
||||
using namespace std;
|
||||
using namespace opendnp3;
|
||||
|
||||
SCENARIO("dnp3 receiver", "Receiver") {
|
||||
|
||||
auto variables = make_vars();
|
||||
auto range = Dnp3Range{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
|
||||
|
||||
Dnp3IndexedGroup binary_commands = {0};
|
||||
Dnp3IndexedGroup analog_commands = {0};
|
||||
GIVEN("No glue range") {
|
||||
Dnp3Receiver receiver(variables, range);
|
||||
Dnp3Receiver receiver(binary_commands, analog_commands);
|
||||
WHEN("Select ControlRelayOutputBlock") {
|
||||
ControlRelayOutputBlock crob;
|
||||
REQUIRE(receiver.Select(crob, 0) == CommandStatus::OUT_OF_RANGE);
|
||||
|
@ -55,10 +53,16 @@ SCENARIO("dnp3 receiver", "Receiver") {
|
|||
}
|
||||
}
|
||||
|
||||
GIVEN("One boolean output glue") {
|
||||
range.bool_outputs_start = 0;
|
||||
range.bool_outputs_end = 1;
|
||||
Dnp3Receiver receiver(variables, range);
|
||||
GIVEN("One boolean command") {
|
||||
IEC_BOOL bool_val(0);
|
||||
auto group = GlueBoolGroup { .index=0, .values={ &bool_val, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr } };
|
||||
|
||||
const GlueVariable glue_var = { IECLDT_IN, IECLST_BIT, 0, 0, IECVT_BOOL, &group };
|
||||
const GlueVariable* glue_vars[] = { &glue_var };
|
||||
binary_commands.size = 1;
|
||||
binary_commands.items = glue_vars;
|
||||
|
||||
Dnp3Receiver receiver(binary_commands, analog_commands);
|
||||
WHEN("Select ControlRelayOutputBlock") {
|
||||
ControlRelayOutputBlock crob;
|
||||
REQUIRE(receiver.Select(crob, 0) == CommandStatus::SUCCESS);
|
||||
|
@ -70,7 +74,10 @@ SCENARIO("dnp3 receiver", "Receiver") {
|
|||
THEN("sets output to true") {
|
||||
REQUIRE(receiver.Select(crob, 0) == CommandStatus::SUCCESS);
|
||||
REQUIRE(receiver.Operate(crob, 0, OperateType::DirectOperate) == CommandStatus::SUCCESS);
|
||||
REQUIRE(*variables->bool_outputs[0]);
|
||||
|
||||
receiver.ExchangeGlue();
|
||||
|
||||
REQUIRE(bool_val);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -80,18 +87,22 @@ SCENARIO("dnp3 receiver", "Receiver") {
|
|||
THEN("sets output to false") {
|
||||
REQUIRE(receiver.Select(crob, 0) == CommandStatus::SUCCESS);
|
||||
REQUIRE(receiver.Operate(crob, 0, OperateType::DirectOperate) == CommandStatus::SUCCESS);
|
||||
REQUIRE(!(*variables->bool_outputs[0]));
|
||||
|
||||
receiver.ExchangeGlue();
|
||||
|
||||
REQUIRE(!bool_val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GIVEN("One analog 16 output glue") {
|
||||
range.outputs_start = 0;
|
||||
range.outputs_end = 1;
|
||||
variables->outputs[0].type = IECVT_INT;
|
||||
variables->outputs[0].value = new IEC_INT{ 0 };
|
||||
Dnp3Receiver receiver(variables, range);
|
||||
IEC_SINT int_val(0);
|
||||
const GlueVariable glue_var = { IECLDT_IN, IECLST_BYTE, 0, 0, IECVT_SINT, &int_val };
|
||||
const GlueVariable* glue_vars[] = { &glue_var };
|
||||
analog_commands.size = 1;
|
||||
analog_commands.items = glue_vars;
|
||||
|
||||
Dnp3Receiver receiver(binary_commands, analog_commands);
|
||||
WHEN("Select AnalogOutputInt16") {
|
||||
AnalogOutputInt16 aoi;
|
||||
REQUIRE(receiver.Select(aoi, 0) == CommandStatus::SUCCESS);
|
||||
|
@ -100,20 +111,25 @@ SCENARIO("dnp3 receiver", "Receiver") {
|
|||
WHEN("Operate AnalogOutputInt16 int value") {
|
||||
AnalogOutputInt16 aoi(9);
|
||||
|
||||
THEN("sets output to true") {
|
||||
THEN("sets output to 9") {
|
||||
REQUIRE(receiver.Select(aoi, 0) == CommandStatus::SUCCESS);
|
||||
REQUIRE(receiver.Operate(aoi, 0, OperateType::DirectOperate) == CommandStatus::SUCCESS);
|
||||
REQUIRE(*reinterpret_cast<IEC_INT*>(variables->outputs[0].value) == 9);
|
||||
|
||||
receiver.ExchangeGlue();
|
||||
|
||||
REQUIRE(int_val == 9);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GIVEN("One analog 32 output glue") {
|
||||
range.outputs_start = 0;
|
||||
range.outputs_end = 1;
|
||||
variables->outputs[0].type = IECVT_INT;
|
||||
variables->outputs[0].value = new IEC_INT{ 0 };
|
||||
Dnp3Receiver receiver(variables, range);
|
||||
IEC_INT int_val(0);
|
||||
const GlueVariable glue_var = { IECLDT_IN, IECLST_WORD, 0, 0, IECVT_INT, &int_val };
|
||||
const GlueVariable* glue_vars[] = { &glue_var };
|
||||
analog_commands.size = 1;
|
||||
analog_commands.items = glue_vars;
|
||||
|
||||
Dnp3Receiver receiver(binary_commands, analog_commands);
|
||||
|
||||
WHEN("Select AnalogOutputInt32") {
|
||||
AnalogOutputInt32 aoi;
|
||||
|
@ -126,17 +142,22 @@ SCENARIO("dnp3 receiver", "Receiver") {
|
|||
THEN("sets output to true") {
|
||||
REQUIRE(receiver.Select(aoi, 0) == CommandStatus::SUCCESS);
|
||||
REQUIRE(receiver.Operate(aoi, 0, OperateType::DirectOperate) == CommandStatus::SUCCESS);
|
||||
REQUIRE(*reinterpret_cast<IEC_INT*>(variables->outputs[0].value) == 9);
|
||||
|
||||
receiver.ExchangeGlue();
|
||||
|
||||
REQUIRE(int_val == 9);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GIVEN("One float 32 output glue") {
|
||||
range.outputs_start = 0;
|
||||
range.outputs_end = 1;
|
||||
variables->outputs[0].type = IECVT_INT;
|
||||
variables->outputs[0].value = new IEC_INT{ 0 };
|
||||
Dnp3Receiver receiver(variables, range);
|
||||
IEC_LINT int_val(0);
|
||||
const GlueVariable glue_var = { IECLDT_IN, IECLST_DOUBLEWORD, 0, 0, IECVT_LINT, &int_val };
|
||||
const GlueVariable* glue_vars[] = { &glue_var };
|
||||
analog_commands.size = 1;
|
||||
analog_commands.items = glue_vars;
|
||||
|
||||
Dnp3Receiver receiver(binary_commands, analog_commands);
|
||||
|
||||
WHEN("Select AnalogOutputFloat32") {
|
||||
AnalogOutputFloat32 aoi;
|
||||
|
@ -149,30 +170,10 @@ SCENARIO("dnp3 receiver", "Receiver") {
|
|||
THEN("sets output to true") {
|
||||
REQUIRE(receiver.Select(aoi, 0) == CommandStatus::SUCCESS);
|
||||
REQUIRE(receiver.Operate(aoi, 0, OperateType::DirectOperate) == CommandStatus::SUCCESS);
|
||||
REQUIRE(*reinterpret_cast<IEC_INT*>(variables->outputs[0].value) == 9);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GIVEN("One double 64 output glue") {
|
||||
range.outputs_start = 0;
|
||||
range.outputs_end = 1;
|
||||
variables->outputs[0].type = IECVT_INT;
|
||||
variables->outputs[0].value = new IEC_INT{ 0 };
|
||||
Dnp3Receiver receiver(variables, range);
|
||||
receiver.ExchangeGlue();
|
||||
|
||||
WHEN("Select AnalogOutputDouble64") {
|
||||
AnalogOutputDouble64 aoi;
|
||||
REQUIRE(receiver.Select(aoi, 0) == CommandStatus::SUCCESS);
|
||||
}
|
||||
|
||||
WHEN("Operate AnalogOutputDouble64 value") {
|
||||
AnalogOutputDouble64 aoi(9);
|
||||
|
||||
THEN("sets output to true") {
|
||||
REQUIRE(receiver.Select(aoi, 0) == CommandStatus::SUCCESS);
|
||||
REQUIRE(receiver.Operate(aoi, 0, OperateType::DirectOperate) == CommandStatus::SUCCESS);
|
||||
REQUIRE(*reinterpret_cast<IEC_INT*>(variables->outputs[0].value) == 9);
|
||||
REQUIRE(int_val == 9);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,9 +24,10 @@ SCENARIO("logsink", "")
|
|||
{
|
||||
GIVEN("initialized sink")
|
||||
{
|
||||
unsigned char buffer[50];
|
||||
unsigned char buffer[50];
|
||||
buffer[0] = '\0';
|
||||
auto sink = std::make_shared<buffered_sink>(buffer, 50);
|
||||
sink->set_pattern("%v");
|
||||
sink->set_pattern("%v");
|
||||
|
||||
WHEN("get data before any messages")
|
||||
{
|
||||
|
@ -34,90 +35,90 @@ SCENARIO("logsink", "")
|
|||
REQUIRE(data.size() == 0);
|
||||
}
|
||||
|
||||
WHEN("has one message published then returns the message")
|
||||
{
|
||||
std::string logger_name = "test";
|
||||
WHEN("has one message published then returns the message")
|
||||
{
|
||||
std::string logger_name = "test";
|
||||
|
||||
fmt::memory_buffer buf;
|
||||
fmt::format_to(buf, "Hello");
|
||||
spdlog::details::log_msg msg(&logger_name, spdlog::level::info, spdlog::string_view_t(buf.data(), buf.size()));
|
||||
fmt::memory_buffer buf;
|
||||
fmt::format_to(buf, "Hello");
|
||||
spdlog::details::log_msg msg(&logger_name, spdlog::level::info, spdlog::string_view_t(buf.data(), buf.size()));
|
||||
|
||||
sink->log(msg);
|
||||
std::string data = sink->data();
|
||||
REQUIRE_THAT(data, Contains("Hello\n"));
|
||||
}
|
||||
sink->log(msg);
|
||||
std::string data = sink->data();
|
||||
REQUIRE_THAT(data, Contains("Hello\n"));
|
||||
}
|
||||
|
||||
WHEN("has two messages published then return both messages")
|
||||
{
|
||||
std::string logger_name = "test";
|
||||
WHEN("has two messages published then return both messages")
|
||||
{
|
||||
std::string logger_name = "test";
|
||||
|
||||
fmt::memory_buffer buf1;
|
||||
fmt::format_to(buf1, "Hello");
|
||||
spdlog::details::log_msg msg1(&logger_name, spdlog::level::info, spdlog::string_view_t(buf1.data(), buf1.size()));
|
||||
fmt::memory_buffer buf1;
|
||||
fmt::format_to(buf1, "Hello");
|
||||
spdlog::details::log_msg msg1(&logger_name, spdlog::level::info, spdlog::string_view_t(buf1.data(), buf1.size()));
|
||||
|
||||
fmt::memory_buffer buf2;
|
||||
fmt::format_to(buf2, "There");
|
||||
spdlog::details::log_msg msg2(&logger_name, spdlog::level::info, spdlog::string_view_t(buf2.data(), buf2.size()));
|
||||
fmt::memory_buffer buf2;
|
||||
fmt::format_to(buf2, "There");
|
||||
spdlog::details::log_msg msg2(&logger_name, spdlog::level::info, spdlog::string_view_t(buf2.data(), buf2.size()));
|
||||
|
||||
sink->log(msg1);
|
||||
sink->log(msg2);
|
||||
std::string data = sink->data();
|
||||
REQUIRE_THAT(data, Contains("Hello\n"));
|
||||
REQUIRE_THAT(data, Contains("There\n"));
|
||||
}
|
||||
sink->log(msg1);
|
||||
sink->log(msg2);
|
||||
std::string data = sink->data();
|
||||
REQUIRE_THAT(data, Contains("Hello\n"));
|
||||
REQUIRE_THAT(data, Contains("There\n"));
|
||||
}
|
||||
|
||||
WHEN("has two messages published then return both messages")
|
||||
{
|
||||
std::string logger_name = "test";
|
||||
WHEN("has two messages published then return both messages")
|
||||
{
|
||||
std::string logger_name = "test";
|
||||
|
||||
fmt::memory_buffer buf1;
|
||||
fmt::format_to(buf1, "Hello");
|
||||
spdlog::details::log_msg msg1(&logger_name, spdlog::level::info, spdlog::string_view_t(buf1.data(), buf1.size()));
|
||||
fmt::memory_buffer buf1;
|
||||
fmt::format_to(buf1, "Hello");
|
||||
spdlog::details::log_msg msg1(&logger_name, spdlog::level::info, spdlog::string_view_t(buf1.data(), buf1.size()));
|
||||
|
||||
fmt::memory_buffer buf2;
|
||||
fmt::format_to(buf2, "There");
|
||||
spdlog::details::log_msg msg2(&logger_name, spdlog::level::info, spdlog::string_view_t(buf2.data(), buf2.size()));
|
||||
fmt::memory_buffer buf2;
|
||||
fmt::format_to(buf2, "There");
|
||||
spdlog::details::log_msg msg2(&logger_name, spdlog::level::info, spdlog::string_view_t(buf2.data(), buf2.size()));
|
||||
|
||||
sink->log(msg1);
|
||||
sink->log(msg2);
|
||||
std::string data = sink->data();
|
||||
REQUIRE_THAT(data, Contains("Hello\n"));
|
||||
REQUIRE_THAT(data, Contains("There\n"));
|
||||
}
|
||||
sink->log(msg1);
|
||||
sink->log(msg2);
|
||||
std::string data = sink->data();
|
||||
REQUIRE_THAT(data, Contains("Hello\n"));
|
||||
REQUIRE_THAT(data, Contains("There\n"));
|
||||
}
|
||||
|
||||
WHEN("message is longer than buffer size then truncates but still has newline")
|
||||
{
|
||||
std::string logger_name = "test";
|
||||
WHEN("message is longer than buffer size then truncates but still has newline")
|
||||
{
|
||||
std::string logger_name = "test";
|
||||
|
||||
fmt::memory_buffer buf;
|
||||
fmt::format_to(buf, "01234567890123456789012345678901234567890123456789ABCDEFG");
|
||||
spdlog::details::log_msg msg(&logger_name, spdlog::level::info, spdlog::string_view_t(buf.data(), buf.size()));
|
||||
fmt::memory_buffer buf;
|
||||
fmt::format_to(buf, "01234567890123456789012345678901234567890123456789ABCDEFG");
|
||||
spdlog::details::log_msg msg(&logger_name, spdlog::level::info, spdlog::string_view_t(buf.data(), buf.size()));
|
||||
|
||||
sink->log(msg);
|
||||
std::string data = sink->data();
|
||||
REQUIRE_THAT(data, Contains("0123456789012345678901234567890123456789012345678"));
|
||||
}
|
||||
sink->log(msg);
|
||||
std::string data = sink->data();
|
||||
REQUIRE_THAT(data, Contains("0123456789012345678901234567890123456789012345678"));
|
||||
}
|
||||
|
||||
WHEN("multiple messages exhaust buffer then starts from beginning")
|
||||
{
|
||||
std::string logger_name = "test";
|
||||
WHEN("multiple messages exhaust buffer then starts from beginning")
|
||||
{
|
||||
std::string logger_name = "test";
|
||||
|
||||
fmt::memory_buffer buf;
|
||||
fmt::format_to(buf, "0123456789");
|
||||
spdlog::details::log_msg msg(&logger_name, spdlog::level::info, spdlog::string_view_t(buf.data(), buf.size()));
|
||||
fmt::memory_buffer buf;
|
||||
fmt::format_to(buf, "0123456789");
|
||||
spdlog::details::log_msg msg(&logger_name, spdlog::level::info, spdlog::string_view_t(buf.data(), buf.size()));
|
||||
|
||||
fmt::memory_buffer buf2;
|
||||
fmt::format_to(buf2, "ABCDEFG");
|
||||
spdlog::details::log_msg msg2(&logger_name, spdlog::level::info, spdlog::string_view_t(buf2.data(), buf2.size()));
|
||||
fmt::memory_buffer buf2;
|
||||
fmt::format_to(buf2, "ABCDEFG");
|
||||
spdlog::details::log_msg msg2(&logger_name, spdlog::level::info, spdlog::string_view_t(buf2.data(), buf2.size()));
|
||||
|
||||
sink->log(msg);
|
||||
sink->log(msg);
|
||||
sink->log(msg);
|
||||
sink->log(msg);
|
||||
sink->log(msg);
|
||||
sink->log(msg2);
|
||||
std::string data = sink->data();
|
||||
REQUIRE_THAT(data, Contains("ABCDEFG\n"));
|
||||
}
|
||||
sink->log(msg);
|
||||
sink->log(msg);
|
||||
sink->log(msg);
|
||||
sink->log(msg);
|
||||
sink->log(msg);
|
||||
sink->log(msg2);
|
||||
std::string data = sink->data();
|
||||
REQUIRE_THAT(data, Contains("ABCDEFG\n"));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -17,15 +17,15 @@
|
|||
#include <algorithm>
|
||||
#include <iostream>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <list>
|
||||
#include <string>
|
||||
|
||||
using namespace std;
|
||||
|
||||
/**
|
||||
* \defgroup glue_generator Glue Generator
|
||||
* @brief Glue generator produces a binding headers for connecting the MATIEC Structured Text to C application code to the OpenPLC runtime.
|
||||
* @brief Glue generator produces a binding headers for connecting the MATIEC
|
||||
* Structured Text to C application code to the OpenPLC runtime.
|
||||
*
|
||||
*/
|
||||
|
||||
|
@ -58,6 +58,31 @@ void generateHeader(ostream& glueVars) {
|
|||
TIME __CURRENT_TIME;
|
||||
extern unsigned long long common_ticktime__;
|
||||
|
||||
#ifndef OPLC_IEC_GLUE_DIRECTION
|
||||
#define OPLC_IEC_GLUE_DIRECTION
|
||||
enum IecLocationDirection {
|
||||
IECLDT_IN,
|
||||
IECLDT_OUT,
|
||||
IECLDT_MEM,
|
||||
};
|
||||
#endif // OPLC_IEC_GLUE_DIRECTION
|
||||
|
||||
#ifndef OPLC_IEC_GLUE_SIZE
|
||||
#define OPLC_IEC_GLUE_SIZE
|
||||
enum IecLocationSize {
|
||||
/// Variables that are a single bit
|
||||
IECLST_BIT,
|
||||
/// Variables that are 1 byte
|
||||
IECLST_BYTE,
|
||||
/// Variables that are 2 bytes
|
||||
IECLST_WORD,
|
||||
/// Variables that are 4 bytes, including REAL
|
||||
IECLST_DOUBLEWORD,
|
||||
/// Variables that are 8 bytes, including LREAL
|
||||
IECLST_LONGWORD,
|
||||
};
|
||||
#endif // OPLC_IEC_GLUE_SIZE
|
||||
|
||||
#ifndef OPLC_IEC_GLUE_VALUE_TYPE
|
||||
#define OPLC_IEC_GLUE_VALUE_TYPE
|
||||
enum IecGlueValueType {
|
||||
|
@ -76,16 +101,51 @@ enum IecGlueValueType {
|
|||
IECVT_LWORD,
|
||||
IECVT_LINT,
|
||||
IECVT_ULINT,
|
||||
/// This is not a normal type and won't appear in the glue variables
|
||||
/// here. But it does allow you to create your own indexed mapping
|
||||
/// and have a way to indicate a value that is not assigned a type.
|
||||
IECVT_UNASSIGNED,
|
||||
};
|
||||
#endif // OPLC_IEC_GLUE_VALUE_TYPE
|
||||
|
||||
#ifndef OPLC_GLUE_BOOL_GROUP
|
||||
#define OPLC_GLUE_BOOL_GROUP
|
||||
/// Defines the mapping for a glued variable that is a boolean array.
|
||||
/// The boolean array is sub-indiced as a group, for example all of the
|
||||
/// values %IX0.0 through %IX0.1 share the same group. The size of the
|
||||
/// group is fixed at 8 values, but some may be unassigned.
|
||||
struct GlueBoolGroup {
|
||||
/// The first index for this array. If we are iterating over the glue
|
||||
/// variables, then this index is superfluous, but it is very helpful
|
||||
/// for debugging.
|
||||
std::uint16_t index;
|
||||
/// The values in this group. If the value is not assigned, then the
|
||||
/// value at the index points to nullptr.
|
||||
IEC_BOOL* values[8];
|
||||
};
|
||||
#endif // OPLC_GLUE_BOOL_GROUP
|
||||
|
||||
#ifndef OPLC_GLUE_VARIABLE
|
||||
#define OPLC_GLUE_VARIABLE
|
||||
/// Defines the mapping for a glued variable.
|
||||
/// Defines the mapping for a glued variable. This defines a simple, space
|
||||
/// efficient lookup table. It has all of the mapping information that you
|
||||
/// need to find the variable based on the location name (e.g. %IB1.1). While
|
||||
/// this is space efficient, this should be searched once to construct a fast
|
||||
/// lookup into this table used for the remainder of the application lifecycle.
|
||||
struct GlueVariable {
|
||||
|
||||
/// The type of the glue variable.
|
||||
/// The direction of the variable - this is determined by I/Q/M.
|
||||
IecLocationDirection dir;
|
||||
/// The size of the variable - this is determined by X/B/W/D/L.
|
||||
IecLocationSize size;
|
||||
/// The most significant index for the variable. This is the part of the
|
||||
/// name, converted to an integer, before the period.
|
||||
std::uint16_t msi;
|
||||
/// The least significant index (sub-index) for the variable. This is the
|
||||
/// part of the name, converted to an integer, after the period. It is
|
||||
/// only relevant for boolean (bit) values.
|
||||
std::uint8_t lsi;
|
||||
/// The type of the glue variable. This is used so that we correctly
|
||||
/// write into the value type.
|
||||
IecGlueValueType type;
|
||||
/// A pointer to the memory address for reading/writing the value.
|
||||
void* value;
|
||||
|
@ -93,25 +153,34 @@ struct GlueVariable {
|
|||
#endif // OPLC_GLUE_VARIABLE
|
||||
|
||||
// Internal buffers for I/O and memory. These buffers are defined in the
|
||||
// auto-generated glueVars.cpp file
|
||||
// auto-generated glueVars.cpp file.
|
||||
// Inputs: I
|
||||
// Outputs: Q
|
||||
// Memory: M
|
||||
#define BUFFER_SIZE 1024
|
||||
|
||||
// Booleans
|
||||
IEC_BOOL *bool_input[BUFFER_SIZE][8];
|
||||
IEC_BOOL *bool_output[BUFFER_SIZE][8];
|
||||
// Booleans - defined by the "X" width
|
||||
IEC_BOOL *bool_input[BUFFER_SIZE][8] = {};
|
||||
IEC_BOOL *bool_output[BUFFER_SIZE][8] = {};
|
||||
|
||||
// Bytes
|
||||
IEC_BYTE *byte_input[BUFFER_SIZE];
|
||||
IEC_BYTE *byte_output[BUFFER_SIZE];
|
||||
// Bytes - defined by the "B" width
|
||||
IEC_BYTE *byte_input[BUFFER_SIZE] = {};
|
||||
IEC_BYTE *byte_output[BUFFER_SIZE] = {};
|
||||
|
||||
// Analog I/O
|
||||
IEC_UINT *int_input[BUFFER_SIZE];
|
||||
IEC_UINT *int_output[BUFFER_SIZE];
|
||||
// Words - defined by the "W" width
|
||||
IEC_UINT *int_input[BUFFER_SIZE] = {};
|
||||
IEC_UINT *int_output[BUFFER_SIZE] = {};
|
||||
IEC_UINT *int_memory[BUFFER_SIZE] = {};
|
||||
|
||||
// Memory
|
||||
IEC_UINT *int_memory[BUFFER_SIZE];
|
||||
IEC_DINT *dint_memory[BUFFER_SIZE];
|
||||
IEC_LINT *lint_memory[BUFFER_SIZE];
|
||||
// Double words - defined by the "D" width
|
||||
// This is also valid size for a REAL but we don't allow
|
||||
// them in this structure
|
||||
IEC_DINT *dint_memory[BUFFER_SIZE] = {};
|
||||
|
||||
// Longs - defined by the "L" width
|
||||
// This is also valid size for a LREAL but we don't allow
|
||||
// them in this structure
|
||||
IEC_LINT *lint_memory[BUFFER_SIZE] = {};
|
||||
|
||||
// Special Functions
|
||||
IEC_LINT *special_functions[BUFFER_SIZE];
|
||||
|
@ -246,60 +315,51 @@ void glueVar(ostream& glueVars, const char *varName, uint16_t pos1,
|
|||
}
|
||||
}
|
||||
|
||||
void generateIntegratedGlue(ostream& glueVars, const list<IecVar>& all_vars,
|
||||
int32_t max_index) {
|
||||
// We want to build 4 arrays here - inputs, outputs, boolean inputs and
|
||||
// boolean outputs. To do that, we need to divide the list into the
|
||||
// relevant parts so that we can assign the mapping location at compile
|
||||
// time. A little upfront processing here means saves some work at runtime.
|
||||
const char* fromDirectionFlag(const char flag) {
|
||||
switch (flag) {
|
||||
case 'I':
|
||||
return "IN";
|
||||
case 'Q':
|
||||
return "OUT";
|
||||
default:
|
||||
return "MEM";
|
||||
}
|
||||
}
|
||||
|
||||
// Allocate arrays with sufficient space so we don't have to check later.
|
||||
vector<const IecVar*> input_vars(max_index + 1);
|
||||
vector<const IecVar*> output_vars(max_index + 1);
|
||||
int32_t max_input(-1);
|
||||
int32_t max_output(-1);
|
||||
const char* fromSizeFlag(const char flag) {
|
||||
switch (flag) {
|
||||
case 'X':
|
||||
return "BIT";
|
||||
case 'B':
|
||||
return "BYTE";
|
||||
case 'W':
|
||||
return "WORD";
|
||||
case 'D':
|
||||
return "DOUBLEWORD";
|
||||
case 'L':
|
||||
default:
|
||||
return "LONGWORD";
|
||||
}
|
||||
}
|
||||
|
||||
void generateIntegratedGlue(ostream& glueVars, const list<IecVar>& all_vars) {
|
||||
glueVars << "/// The size of the array of glue variables.\n";
|
||||
glueVars << "extern std::uint16_t const OPLCGLUE_GLUE_SIZE(";
|
||||
glueVars << all_vars.size() << ");\n";
|
||||
|
||||
glueVars << "/// The packed glue variables.\n";
|
||||
glueVars << "extern const GlueVariable oplc_glue_vars[] = {\n";
|
||||
for (auto it = all_vars.begin(); it != all_vars.end(); ++it) {
|
||||
const char direction = (*it).name[2];
|
||||
const int32_t pos1 = (*it).pos1;
|
||||
if ((*it).type.compare("BOOL") != 0) {
|
||||
if (direction == 'I') {
|
||||
max_input = max(pos1, max_input);
|
||||
input_vars[pos1] = &(*it);
|
||||
} else if (direction == 'Q') {
|
||||
max_output = max(pos1, max_output);
|
||||
output_vars[pos1] = &(*it);
|
||||
}
|
||||
}
|
||||
}
|
||||
const char directionFlag = (*it).name[2];
|
||||
const char sizeFlag = (*it).name[3];
|
||||
|
||||
// Now that things are sorted, we are ready to write them out.
|
||||
glueVars << "/// The size of the array of input variables.\n";
|
||||
glueVars << "extern std::uint16_t const OPLCGLUE_INPUT_SIZE(";
|
||||
glueVars << max_input + 1 << ");\n";
|
||||
|
||||
glueVars << "GlueVariable oplc_input_vars[] = {\n";
|
||||
for (auto i = 0; i < max_input + 1; i++) {
|
||||
if (input_vars[i] != nullptr) {
|
||||
glueVars << " { IECVT_" << input_vars[i]->type << ", ";
|
||||
glueVars << input_vars[i]->name << " },\n";
|
||||
} else {
|
||||
glueVars << " { IECVT_UNASSIGNED, nullptr },\n";
|
||||
}
|
||||
}
|
||||
glueVars << "};\n\n";
|
||||
|
||||
glueVars << "/// The size of the array of output variables.\n";
|
||||
glueVars << "extern std::uint16_t const OPLCGLUE_OUTPUT_SIZE(";
|
||||
glueVars << max_output + 1 << ");\n";
|
||||
glueVars << "GlueVariable oplc_output_vars[] = {\n";
|
||||
for (auto i = 0; i < max_output + 1; i++) {
|
||||
if (output_vars[i] != nullptr) {
|
||||
glueVars << " { IECVT_" << output_vars[i]->type << ", ";
|
||||
glueVars << output_vars[i]->name << " },\n";
|
||||
} else {
|
||||
glueVars << " { IECVT_UNASSIGNED, nullptr },\n";
|
||||
}
|
||||
glueVars << " {";
|
||||
glueVars << " IECLDT_" << fromDirectionFlag(directionFlag) << ",";
|
||||
glueVars << " IECLST_" << fromSizeFlag(sizeFlag) << ",";
|
||||
glueVars << " " << (*it).pos1 << ",";
|
||||
glueVars << " " << (*it).pos2 << ",";
|
||||
glueVars << " IECVT_" << (*it).type << ", ";
|
||||
glueVars << " " << (*it).name << " },\n";
|
||||
}
|
||||
glueVars << "};\n\n";
|
||||
}
|
||||
|
@ -352,7 +412,7 @@ void generateBody(istream& locatedVars, ostream& glueVars) {
|
|||
glueVars << "}\n\n";
|
||||
|
||||
// Generate the unified glue variables
|
||||
generateIntegratedGlue(glueVars, all_vars, max_index);
|
||||
generateIntegratedGlue(glueVars, all_vars);
|
||||
}
|
||||
|
||||
/// This is our main function. We define it with a different name and then
|
||||
|
|
|
@ -36,6 +36,15 @@ GlueVariable oplc_input_vars[] = {\n\
|
|||
extern std::uint16_t const OPLCGLUE_OUTPUT_SIZE(0);\n\
|
||||
GlueVariable oplc_output_vars[] = {\n\
|
||||
};\n\n"
|
||||
#define EMPTY_INPUT_BOOL "/// Size of the array of input boolean variables.\n\
|
||||
extern std::uint16_t const OPLCGLUE_INPUT_BOOL_SIZE(0);\n\
|
||||
GlueBoolGroup oplc_bool_input_vars[] = {\n\
|
||||
};\n\n"
|
||||
#define EMPTY_OUTPUT_BOOL "/// Size of the array of output boolean variables.\n\
|
||||
extern std::uint16_t const OPLCGLUE_OUTPUT_BOOL_SIZE(0);\n\
|
||||
GlueBoolGroup oplc_bool_output_vars[] = {\n\
|
||||
};\n\n"
|
||||
#define GLUE_PREFIX "/// The size of the array of glue variables.\n"
|
||||
|
||||
|
||||
SCENARIO("Commmand line", "[main]") {
|
||||
|
@ -59,47 +68,59 @@ SCENARIO("", "") {
|
|||
std::stringstream input_stream("__LOCATED_VAR(BOOL,__IX0,I,X,0)");
|
||||
generateBody(input_stream, output_stream);
|
||||
|
||||
const char* expected = PREFIX "\tbool_input[0][0] = __IX0;\n" POSTFIX EMPTY_INPUT EMPTY_OUTPUT;
|
||||
const char* expected = PREFIX "\tbool_input[0][0] = __IX0;\n" POSTFIX GLUE_PREFIX\
|
||||
"extern std::uint16_t const OPLCGLUE_GLUE_SIZE(1);\n\
|
||||
/// The packed glue variables.\n\
|
||||
extern const GlueVariable oplc_glue_vars[] = {\n\
|
||||
{ IECLDT_IN, IECLST_BIT, 0, 0, IECVT_BOOL, __IX0 },\n\
|
||||
};\n\n";
|
||||
REQUIRE(output_stream.str() == expected);
|
||||
}
|
||||
|
||||
WHEN("Contains single BOOL at %QX0") {
|
||||
std::stringstream input_stream("__LOCATED_VAR(BOOL,__QX0,Q,X,0)");
|
||||
generateBody(input_stream, output_stream);
|
||||
REQUIRE(output_stream.str() == PREFIX "\tbool_output[0][0] = __QX0;\n" POSTFIX EMPTY_INPUT EMPTY_OUTPUT);
|
||||
const char* expected = PREFIX "\tbool_output[0][0] = __QX0;\n" POSTFIX GLUE_PREFIX\
|
||||
"extern std::uint16_t const OPLCGLUE_GLUE_SIZE(1);\n\
|
||||
/// The packed glue variables.\n\
|
||||
extern const GlueVariable oplc_glue_vars[] = {\n\
|
||||
{ IECLDT_OUT, IECLST_BIT, 0, 0, IECVT_BOOL, __QX0 },\n\
|
||||
};\n\n";
|
||||
REQUIRE(output_stream.str() == expected);
|
||||
}
|
||||
|
||||
WHEN("Contains single BYTE at %IB0") {
|
||||
std::stringstream input_stream("__LOCATED_VAR(BYTE,__IB0,I,B,0)");
|
||||
generateBody(input_stream, output_stream);
|
||||
const char* expected = PREFIX"\tbyte_input[0] = __IB0;\n" POSTFIX "/// The size of the array of input variables.\n\
|
||||
extern std::uint16_t const OPLCGLUE_INPUT_SIZE(1);\n\
|
||||
GlueVariable oplc_input_vars[] = {\n\
|
||||
{ IECVT_BYTE, __IB0 },\n\
|
||||
};\n\n" EMPTY_OUTPUT;
|
||||
const char* expected = PREFIX"\tbyte_input[0] = __IB0;\n" POSTFIX GLUE_PREFIX\
|
||||
"extern std::uint16_t const OPLCGLUE_GLUE_SIZE(1);\n\
|
||||
/// The packed glue variables.\n\
|
||||
extern const GlueVariable oplc_glue_vars[] = {\n\
|
||||
{ IECLDT_IN, IECLST_BYTE, 0, 0, IECVT_BYTE, __IB0 },\n\
|
||||
};\n\n";
|
||||
REQUIRE(output_stream.str() == expected);
|
||||
}
|
||||
|
||||
WHEN("Contains single SINT at %IB1") {
|
||||
std::stringstream input_stream("__LOCATED_VAR(SINT,__IB1,I,B,1)");
|
||||
generateBody(input_stream, output_stream);
|
||||
const char* expected = PREFIX "\tbyte_input[1] = __IB1;\n" POSTFIX "/// The size of the array of input variables.\n\
|
||||
extern std::uint16_t const OPLCGLUE_INPUT_SIZE(2);\n\
|
||||
GlueVariable oplc_input_vars[] = {\n\
|
||||
{ IECVT_UNASSIGNED, nullptr },\n\
|
||||
{ IECVT_SINT, __IB1 },\n\
|
||||
};\n\n" EMPTY_OUTPUT;
|
||||
const char* expected = PREFIX "\tbyte_input[1] = __IB1;\n" POSTFIX GLUE_PREFIX\
|
||||
"extern std::uint16_t const OPLCGLUE_GLUE_SIZE(1);\n\
|
||||
/// The packed glue variables.\n\
|
||||
extern const GlueVariable oplc_glue_vars[] = {\n\
|
||||
{ IECLDT_IN, IECLST_BYTE, 1, 0, IECVT_SINT, __IB1 },\n\
|
||||
};\n\n";
|
||||
REQUIRE(output_stream.str() == expected);
|
||||
}
|
||||
|
||||
WHEN("Contains single SINT at %QB1") {
|
||||
std::stringstream input_stream("__LOCATED_VAR(SINT,__QB1,Q,B,1)");
|
||||
generateBody(input_stream, output_stream);
|
||||
const char* expected = PREFIX "\tbyte_output[1] = __QB1;\n" POSTFIX EMPTY_INPUT "/// The size of the array of output variables.\n\
|
||||
extern std::uint16_t const OPLCGLUE_OUTPUT_SIZE(2);\n\
|
||||
GlueVariable oplc_output_vars[] = {\n\
|
||||
{ IECVT_UNASSIGNED, nullptr },\n\
|
||||
{ IECVT_SINT, __QB1 },\n\
|
||||
const char* expected = PREFIX "\tbyte_output[1] = __QB1;\n" POSTFIX GLUE_PREFIX\
|
||||
"extern std::uint16_t const OPLCGLUE_GLUE_SIZE(1);\n\
|
||||
/// The packed glue variables.\n\
|
||||
extern const GlueVariable oplc_glue_vars[] = {\n\
|
||||
{ IECLDT_OUT, IECLST_BYTE, 1, 0, IECVT_SINT, __QB1 },\n\
|
||||
};\n\n";
|
||||
REQUIRE(output_stream.str() == expected);
|
||||
}
|
||||
|
@ -107,34 +128,35 @@ GlueVariable oplc_output_vars[] = {\n\
|
|||
WHEN("Contains single USINT at %IB2") {
|
||||
std::stringstream input_stream("__LOCATED_VAR(USINT,__IB2,I,B,2)");
|
||||
generateBody(input_stream, output_stream);
|
||||
const char* expected = PREFIX "\tbyte_input[2] = __IB2;\n" POSTFIX "/// The size of the array of input variables.\n\
|
||||
extern std::uint16_t const OPLCGLUE_INPUT_SIZE(3);\n\
|
||||
GlueVariable oplc_input_vars[] = {\n\
|
||||
{ IECVT_UNASSIGNED, nullptr },\n\
|
||||
{ IECVT_UNASSIGNED, nullptr },\n\
|
||||
{ IECVT_USINT, __IB2 },\n\
|
||||
};\n\n" EMPTY_OUTPUT;
|
||||
const char* expected = PREFIX "\tbyte_input[2] = __IB2;\n" POSTFIX GLUE_PREFIX\
|
||||
"extern std::uint16_t const OPLCGLUE_GLUE_SIZE(1);\n\
|
||||
/// The packed glue variables.\n\
|
||||
extern const GlueVariable oplc_glue_vars[] = {\n\
|
||||
{ IECLDT_IN, IECLST_BYTE, 2, 0, IECVT_USINT, __IB2 },\n\
|
||||
};\n\n";
|
||||
REQUIRE(output_stream.str() == expected);
|
||||
}
|
||||
|
||||
WHEN("Contains single WORD at %IW0") {
|
||||
std::stringstream input_stream("__LOCATED_VAR(WORD,__IW0,I,W,0)");
|
||||
generateBody(input_stream, output_stream);
|
||||
const char* expected = PREFIX "\tint_input[0] = __IW0;\n" POSTFIX "/// The size of the array of input variables.\n\
|
||||
extern std::uint16_t const OPLCGLUE_INPUT_SIZE(1);\n\
|
||||
GlueVariable oplc_input_vars[] = {\n\
|
||||
{ IECVT_WORD, __IW0 },\n\
|
||||
};\n\n" EMPTY_OUTPUT;
|
||||
const char* expected = PREFIX "\tint_input[0] = __IW0;\n" POSTFIX GLUE_PREFIX\
|
||||
"extern std::uint16_t const OPLCGLUE_GLUE_SIZE(1);\n\
|
||||
/// The packed glue variables.\n\
|
||||
extern const GlueVariable oplc_glue_vars[] = {\n\
|
||||
{ IECLDT_IN, IECLST_WORD, 0, 0, IECVT_WORD, __IW0 },\n\
|
||||
};\n\n";
|
||||
REQUIRE(output_stream.str() == expected);
|
||||
}
|
||||
|
||||
WHEN("Contains single WORD at %QW0") {
|
||||
std::stringstream input_stream("__LOCATED_VAR(WORD,__QW0,Q,W,0)");
|
||||
generateBody(input_stream, output_stream);
|
||||
const char* expected = PREFIX "\tint_output[0] = __QW0;\n" POSTFIX EMPTY_INPUT "/// The size of the array of output variables.\n\
|
||||
extern std::uint16_t const OPLCGLUE_OUTPUT_SIZE(1);\n\
|
||||
GlueVariable oplc_output_vars[] = {\n\
|
||||
{ IECVT_WORD, __QW0 },\n\
|
||||
const char* expected = PREFIX "\tint_output[0] = __QW0;\n" POSTFIX GLUE_PREFIX\
|
||||
"extern std::uint16_t const OPLCGLUE_GLUE_SIZE(1);\n\
|
||||
/// The packed glue variables.\n\
|
||||
extern const GlueVariable oplc_glue_vars[] = {\n\
|
||||
{ IECLDT_OUT, IECLST_WORD, 0, 0, IECVT_WORD, __QW0 },\n\
|
||||
};\n\n";
|
||||
REQUIRE(output_stream.str() == expected);
|
||||
}
|
||||
|
@ -142,12 +164,12 @@ GlueVariable oplc_output_vars[] = {\n\
|
|||
WHEN("Contains single INT at %IW1") {
|
||||
std::stringstream input_stream("__LOCATED_VAR(INT,__IW1,I,W,1)");
|
||||
generateBody(input_stream, output_stream);
|
||||
const char* expected = PREFIX "\tint_input[1] = __IW1;\n" POSTFIX "/// The size of the array of input variables.\n\
|
||||
extern std::uint16_t const OPLCGLUE_INPUT_SIZE(2);\n\
|
||||
GlueVariable oplc_input_vars[] = {\n\
|
||||
{ IECVT_UNASSIGNED, nullptr },\n\
|
||||
{ IECVT_INT, __IW1 },\n\
|
||||
};\n\n" EMPTY_OUTPUT;
|
||||
const char* expected = PREFIX "\tint_input[1] = __IW1;\n" POSTFIX GLUE_PREFIX\
|
||||
"extern std::uint16_t const OPLCGLUE_GLUE_SIZE(1);\n\
|
||||
/// The packed glue variables.\n\
|
||||
extern const GlueVariable oplc_glue_vars[] = {\n\
|
||||
{ IECLDT_IN, IECLST_WORD, 1, 0, IECVT_INT, __IW1 },\n\
|
||||
};\n\n";
|
||||
REQUIRE(output_stream.str() == expected);
|
||||
}
|
||||
|
||||
|
@ -155,13 +177,12 @@ GlueVariable oplc_input_vars[] = {\n\
|
|||
std::stringstream input_stream("__LOCATED_VAR(UINT,__IW2,I,W,2)");
|
||||
generateBody(input_stream, output_stream);
|
||||
|
||||
const char* expected = PREFIX "\tint_input[2] = __IW2;\n" POSTFIX "/// The size of the array of input variables.\n\
|
||||
extern std::uint16_t const OPLCGLUE_INPUT_SIZE(3);\n\
|
||||
GlueVariable oplc_input_vars[] = {\n\
|
||||
{ IECVT_UNASSIGNED, nullptr },\n\
|
||||
{ IECVT_UNASSIGNED, nullptr },\n\
|
||||
{ IECVT_UINT, __IW2 },\n\
|
||||
};\n\n" EMPTY_OUTPUT;
|
||||
const char* expected = PREFIX "\tint_input[2] = __IW2;\n" POSTFIX GLUE_PREFIX\
|
||||
"extern std::uint16_t const OPLCGLUE_GLUE_SIZE(1);\n\
|
||||
/// The packed glue variables.\n\
|
||||
extern const GlueVariable oplc_glue_vars[] = {\n\
|
||||
{ IECLDT_IN, IECLST_WORD, 2, 0, IECVT_UINT, __IW2 },\n\
|
||||
};\n\n";
|
||||
REQUIRE(output_stream.str() == expected);
|
||||
}
|
||||
|
||||
|
@ -170,46 +191,62 @@ GlueVariable oplc_input_vars[] = {\n\
|
|||
generateBody(input_stream, output_stream);
|
||||
|
||||
// Note that the type-separate glue does not support REAL types
|
||||
const char* expected = PREFIX POSTFIX "/// The size of the array of input variables.\n\
|
||||
extern std::uint16_t const OPLCGLUE_INPUT_SIZE(11);\n\
|
||||
GlueVariable oplc_input_vars[] = {\n\
|
||||
{ IECVT_REAL, __ID0 },\n\
|
||||
{ IECVT_UNASSIGNED, nullptr },\n\
|
||||
{ IECVT_UNASSIGNED, nullptr },\n\
|
||||
{ IECVT_UNASSIGNED, nullptr },\n\
|
||||
{ IECVT_UNASSIGNED, nullptr },\n\
|
||||
{ IECVT_UNASSIGNED, nullptr },\n\
|
||||
{ IECVT_UNASSIGNED, nullptr },\n\
|
||||
{ IECVT_UNASSIGNED, nullptr },\n\
|
||||
{ IECVT_UNASSIGNED, nullptr },\n\
|
||||
{ IECVT_UNASSIGNED, nullptr },\n\
|
||||
{ IECVT_REAL, __ID10 },\n\
|
||||
};\n\n" EMPTY_OUTPUT;
|
||||
const char* expected = PREFIX POSTFIX GLUE_PREFIX\
|
||||
"extern std::uint16_t const OPLCGLUE_GLUE_SIZE(2);\n\
|
||||
/// The packed glue variables.\n\
|
||||
extern const GlueVariable oplc_glue_vars[] = {\n\
|
||||
{ IECLDT_IN, IECLST_DOUBLEWORD, 0, 0, IECVT_REAL, __ID0 },\n\
|
||||
{ IECLDT_IN, IECLST_DOUBLEWORD, 10, 0, IECVT_REAL, __ID10 },\n\
|
||||
};\n\n";
|
||||
REQUIRE(output_stream.str() == expected);
|
||||
}
|
||||
|
||||
WHEN("Contains single INT at %MW2") {
|
||||
std::stringstream input_stream("__LOCATED_VAR(INT,__MW2,M,W,2)");
|
||||
generateBody(input_stream, output_stream);
|
||||
REQUIRE(output_stream.str() == PREFIX "\tint_memory[2] = __MW2;\n" POSTFIX EMPTY_INPUT EMPTY_OUTPUT);
|
||||
const char* expected = PREFIX "\tint_memory[2] = __MW2;\n" POSTFIX GLUE_PREFIX\
|
||||
"extern std::uint16_t const OPLCGLUE_GLUE_SIZE(1);\n\
|
||||
/// The packed glue variables.\n\
|
||||
extern const GlueVariable oplc_glue_vars[] = {\n\
|
||||
{ IECLDT_MEM, IECLST_WORD, 2, 0, IECVT_INT, __MW2 },\n\
|
||||
};\n\n";
|
||||
REQUIRE(output_stream.str() == expected);
|
||||
}
|
||||
|
||||
WHEN("Contains single DWORD at %MD0") {
|
||||
std::stringstream input_stream("__LOCATED_VAR(DWORD,__MD2,M,D,2)");
|
||||
generateBody(input_stream, output_stream);
|
||||
REQUIRE(output_stream.str() == PREFIX "\tdint_memory[2] = (IEC_DINT *)__MD2;\n" POSTFIX EMPTY_INPUT EMPTY_OUTPUT);
|
||||
const char* expected = PREFIX "\tdint_memory[2] = (IEC_DINT *)__MD2;\n" POSTFIX GLUE_PREFIX\
|
||||
"extern std::uint16_t const OPLCGLUE_GLUE_SIZE(1);\n\
|
||||
/// The packed glue variables.\n\
|
||||
extern const GlueVariable oplc_glue_vars[] = {\n\
|
||||
{ IECLDT_MEM, IECLST_DOUBLEWORD, 2, 0, IECVT_DWORD, __MD2 },\n\
|
||||
};\n\n";
|
||||
REQUIRE(output_stream.str() == expected);
|
||||
}
|
||||
|
||||
WHEN("Contains single LINT at %ML1") {
|
||||
std::stringstream input_stream("__LOCATED_VAR(LINT,__ML1,M,L,1)");
|
||||
generateBody(input_stream, output_stream);
|
||||
REQUIRE(output_stream.str() == PREFIX "\tlint_memory[1] = (IEC_LINT *)__ML1;\n" POSTFIX EMPTY_INPUT EMPTY_OUTPUT);
|
||||
std::stringstream input_stream("__LOCATED_VAR(LINT,__ML1,M,L,1)");
|
||||
generateBody(input_stream, output_stream);
|
||||
const char* expected = PREFIX "\tlint_memory[1] = (IEC_LINT *)__ML1;\n" POSTFIX GLUE_PREFIX\
|
||||
"extern std::uint16_t const OPLCGLUE_GLUE_SIZE(1);\n\
|
||||
/// The packed glue variables.\n\
|
||||
extern const GlueVariable oplc_glue_vars[] = {\n\
|
||||
{ IECLDT_MEM, IECLST_LONGWORD, 1, 0, IECVT_LINT, __ML1 },\n\
|
||||
};\n\n";
|
||||
REQUIRE(output_stream.str() == expected);
|
||||
}
|
||||
|
||||
WHEN("Contains single LINT at %ML1024") {
|
||||
std::stringstream input_stream("__LOCATED_VAR(LINT,__ML1024,M,L,1024)");
|
||||
generateBody(input_stream, output_stream);
|
||||
REQUIRE(output_stream.str() == PREFIX "\tspecial_functions[0] = (IEC_LINT *)__ML1024;\n" POSTFIX EMPTY_INPUT EMPTY_OUTPUT);
|
||||
const char* expected = PREFIX "\tspecial_functions[0] = (IEC_LINT *)__ML1024;\n" POSTFIX GLUE_PREFIX\
|
||||
"extern std::uint16_t const OPLCGLUE_GLUE_SIZE(1);\n\
|
||||
/// The packed glue variables.\n\
|
||||
extern const GlueVariable oplc_glue_vars[] = {\n\
|
||||
{ IECLDT_MEM, IECLST_LONGWORD, 1024, 0, IECVT_LINT, __ML1024 },\n\
|
||||
};\n\n";
|
||||
REQUIRE(output_stream.str() == expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,9 +3,27 @@
|
|||
#-----------------------------------------------------------------
|
||||
|
||||
|
||||
# Use this file to fill out DNP3 settings for your Open PLC
|
||||
# Use this file to fill out DNP3 settings for your OpenPLC application
|
||||
# Uncomment settings as you want them
|
||||
|
||||
# Location Bindings
|
||||
#-----------------------------------------------------------------
|
||||
|
||||
# Bind OpenPLC bit-sized output 0.0 to DNP3 binary input at index 0
|
||||
# Note that the second hierarchical index must be 0
|
||||
# bind_address = name:%QX0.0,group:1,index:0,
|
||||
|
||||
# Bind OpenPLC word-sized output 2 to DNP3 analog input at index 1
|
||||
# bind_address = name:%QW2,group:30,index:1,
|
||||
|
||||
# Bind OpenPLC word-sized output 2 to DNP3 analog output status at index 1
|
||||
# bind_address = name:%QW2,group:40,index:1,
|
||||
|
||||
# Bind OpenPLC long word-sized output 2 to DNP3 analog input at index 10
|
||||
# bind_address = name:%QL2,group:30,index:10,
|
||||
|
||||
# Bind OpenPLC word-sized input 2 to DNP3 analog command at index 0
|
||||
# bind_address = name:%IW2,group:41,index:0,
|
||||
|
||||
# Link Settings
|
||||
#-----------------------------------------------------------------
|
||||
|
@ -46,22 +64,6 @@ enable_unsolicited = True
|
|||
# size of the event buffer
|
||||
event_buffer_size = 10
|
||||
|
||||
# number of values the outstation will report at once
|
||||
# AKA database size
|
||||
database_size = 8
|
||||
|
||||
# First data point offset for DI - required if slave device used (the address should represent 1st data point of slave device)
|
||||
offset_di = 0
|
||||
|
||||
# First data point offset for DO - required if slave device used (the address should represent 1st data point of slave device)
|
||||
offset_do = 0
|
||||
|
||||
# First data point offset for AI - required if slave device used (the address should represent 1st data point of slave device)
|
||||
offset_ai = 0
|
||||
|
||||
# First data point offset for AO - required if slave device used (the address should represent 1st data point of slave device)
|
||||
offset_ao = 0
|
||||
|
||||
#Timeout for solicited confirms
|
||||
# in MS
|
||||
# sol_confirm_timeout = 5000
|
||||
|
|
Loading…
Reference in New Issue