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:
Garret Fick 2019-10-22 11:16:14 -04:00
parent 58bd86f475
commit 805b7a0be6
No known key found for this signature in database
GPG Key ID: A1BBEF9D2AB249C6
29 changed files with 1619 additions and 1286 deletions

View File

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

View File

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

View File

@ -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
}

View File

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

View File

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

View File

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

View File

@ -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_

View File

@ -1,6 +0,0 @@
//------------------------------------------------------------------
//Function to begin DNP3 server functions
//------------------------------------------------------------------
void dnp3StartServer(int port)
{
}

View File

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

View File

@ -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_
/** @}*/

View File

@ -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
/** @}*/

View File

@ -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_
/** @}*/

View File

@ -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;

97
runtime/core/glue.cpp Normal file
View File

@ -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);
}

View File

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

View File

@ -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()
**************************************************/
}
/** @}*/
/** @} */

View File

@ -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);
}
///////////////////////////////////////////////////////////////////////////////

View File

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

View File

@ -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");
}
/** @}*/

View File

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

View File

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

View File

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

View File

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

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

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

View File

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

View File

@ -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);
}
}
}

View File

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