Merge pull request #91 from smartergridsolutions/feature/PR-740

Feature/pr 740 - Rework DNP3 to allow complete flexibility
This commit is contained in:
Thiago Alves 2019-11-05 15:49:55 -05:00 committed by GitHub
commit b7a72cca8c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 3194 additions and 1423 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
}
@ -110,7 +114,7 @@ elif [ "$1" == "linux" ]; then
echo "Installing OpenPLC on Linux"
linux_install_deps sudo
install_py_deps
install_py_deps sudo
install_py_deps "sudo -H"
cmake_build_and_test
@ -146,13 +150,14 @@ elif [ "$1" == "docker" ]; then
elif [ "$1" == "rpi" ]; then
echo "Installing OpenPLC on Raspberry Pi"
linux_install_deps sudo
sudo apt-get install -y wiringpi
install_py_deps
install_py_deps sudo
cmake_build_and_test
install_py_deps "sudo -H"
cmake_build_and_test
if [ $? -ne 0 ]; then
echo "Compilation finished with errors!"
exit 1
@ -178,9 +183,10 @@ elif [ "$1" == "neuron" ]; then
sudo systemctl stop evok.service
sudo systemctl disable evok.service
linux_install_deps sudo
linux_install_deps sudo
install_py_deps
install_py_deps sudo
install_py_deps sudo "sudo -H"
cmake_build_and_test

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1 @@
Here you will find all documentation related to the EtherNet/IP implementation on OpenPLC

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,260 @@
#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) {
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:");
auto name_value_start = name_start + 5;
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_value_start);
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_value_start, 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 GROUP_BINARY_COMMAND:
group_12_max_index = max(group_12_max_index, data_index);
break;
case GROUP_ANALOG_COMMAND:
group_41_max_index = max(group_41_max_index, data_index);
break;
case GROUP_BINARY_INPUT:
case GROUP_BINARY_OUTPUT_STATUS:
case GROUP_ANALOG_INPUT:
case GROUP_ANALOG_OUTPUT_STATUS:
case GROUP_COUNTER:
case GROUP_FROZEN_COUNTER:
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 GROUP_BINARY_COMMAND:
binary_commands.items[data_index] = var;
break;
case GROUP_ANALOG_COMMAND:
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 +344,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 +360,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 +382,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 +410,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 +437,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,149 @@
// 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;
const std::uint8_t GROUP_BINARY_COMMAND(12);
const std::uint8_t GROUP_ANALOG_COMMAND(41);
const std::uint8_t GROUP_BINARY_INPUT(1);
const std::uint8_t GROUP_BINARY_OUTPUT_STATUS(10);
const std::uint8_t GROUP_ANALOG_INPUT(30);
const std::uint8_t GROUP_ANALOG_OUTPUT_STATUS(40);
const std::uint8_t GROUP_COUNTER(20);
const std::uint8_t GROUP_FROZEN_COUNTER(21);
////////////////////////////////////////////////////////////////////////////////
/// \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 == GROUP_BINARY_INPUT || group == GROUP_BINARY_OUTPUT_STATUS) {
const GlueBoolGroup* bool_group = reinterpret_cast<const GlueBoolGroup*>(value);
if (group == GROUP_BINARY_INPUT) {
builder.Update(Binary(*(bool_group->values[0])), point_index_number);
} else {
builder.Update(BinaryOutputStatus(*(bool_group->values[0])), point_index_number);
}
} else if (group == GROUP_ANALOG_INPUT || group == GROUP_ANALOG_OUTPUT_STATUS) {
double double_val = cast_variable<double>(var);
if (group == GROUP_ANALOG_INPUT) {
builder.Update(Analog(double_val), point_index_number);
} else {
builder.Update(AnalogOutputStatus(double_val), point_index_number);
}
} else if (group == GROUP_COUNTER || group == GROUP_FROZEN_COUNTER) {
uint32_t int_val = cast_variable<uint32_t>(var);
if (group == GROUP_COUNTER) {
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

@ -16,7 +16,7 @@
// This file has all the EtherNet/IP functions supported by the OpenPLC. If any
// other function is to be added to the project, it must be added here
// Thiago Alves, Apr 2019
// Hannah Hanback, Sep 2019
//-----------------------------------------------------------------------------
#include <stdio.h>
@ -35,42 +35,105 @@
#define ENIP_MIN_LENGTH 28
struct enip_header
{
unsigned char *command;//[2];
unsigned char *length;//[2];
unsigned char *session_handle;//[4];
unsigned char *status;//[4];
unsigned char *sender_context;//[8];
unsigned char *options;//[4];
unsigned char *data;
};
thread_local unsigned char enip_session[4]; // opens thread for session
struct enip_data
{
unsigned char *interface_handle;
unsigned char *timeout;
unsigned char *item_count;
unsigned char *item1_id;
unsigned char *item1_length;
unsigned char *item1_data;
unsigned char *item2_id;
unsigned char *item2_length;
unsigned char *item2_data;
};
using namespace std;
thread_local unsigned char enip_session[4];
int respondToError(unsigned char *buffer, int buffer_size, int error_code)
{
return -1;
//-----------------------------------------------------------------------------
// Obtains the Length in Item2_Length as variable type uint16_t
// used to know the length of pccc data within Item2_Data
// ENIP Type: Unknown
//-----------------------------------------------------------------------------
int getEnipType(struct enip_data_Unknown *enipDataUnknown, struct enip_header *header)
{
if (header->command[0] == 65 || header->command[0] == 0x70)
{
// ENIP Type Unnecessary for command execution
// 0x65 --- register session
return 0;
}
else if (enipDataUnknown->item1_id[0] == 0x81)
{
// PCCC type 1 - Unknown
return 1;
}
else if (enipDataUnknown->item1_data[0] == 0xb2 && enipDataUnknown->item2_length[1] == 0x4b) // There is an offset of the bytes within the
{ // Unconnected and Connected type data
// PCCC type 2 - Unconnected Data Item // that is accounted for to check enipType // This means the labels "item1_data and item2_length
return 2; // do not contain what is stated but what it would be for the correct type
}
else if (enipDataUnknown->item1_data[0] == 0xb2 && ( (enipDataUnknown->item2_length[1]==0x54) || (enipDataUnknown->item2_length[1]==0x4e) ) ) //0x54 opens connection
{ //0x4e closes connection
// PCCC type 3 - Connected Data Item
return 3;
}
else if (enipDataUnknown->item1_id[0] == 0xa1)
{
// PCCC type 3 - Connected for 0x70 command SEND UNIT DATA
return 3;
}
else
{
// Unknown type ID
// Respond with error_code
return -1;
}
}
int parseEnipHeader(unsigned char *buffer, int buffer_size, struct enip_header *header)
{
//-----------------------------------------------------------------------------
// Obtains the Length in the Header Length as variable type uint16_t
// used to know the length of CIP object data
//-----------------------------------------------------------------------------
uint16_t get_HeaderLength(struct enip_header *header)
{
uint16_t dataLength = ((uint16_t)header->length[1] << 8) | (uint16_t)header->length[0];
return dataLength;
}
//-----------------------------------------------------------------------------
// Obtains the Length in Item2_Length as variable type uint16_t
// used to know the length of pccc data within Item2_Data
//-----------------------------------------------------------------------------
uint16_t get_Item2_DataLength(struct enip_data_Unknown *enipDataUnknown)
{
uint16_t dataLength = ((uint16_t)enipDataUnknown->item2_length[1] << 8) | (uint16_t)enipDataUnknown->item2_length[0];
return dataLength;
}
//-----------------------------------------------------------------------------
// Obtains the Length in Item2_Length as variable type uint16_t
// used to know the length of pccc data within Item2_Data
// ENIP Type: Unconnected
//-----------------------------------------------------------------------------
uint16_t get_Item2_DataLength_Unconnected(struct enip_data_Unconnected *enipDataUnconnected)
{
uint16_t dataLength = ((uint16_t)enipDataUnconnected->item2_length[1] << 8) | (uint16_t)enipDataUnconnected->item2_length[0];
return dataLength;
}
//-----------------------------------------------------------------------------
// Obtains the Length in Item2_Length as variable type uint16_t
// used to know the length of pccc data within Item2_Data
// ENIP Type: Connected_0x70
//-----------------------------------------------------------------------------
uint16_t get_Item2_DataLength_Connected_0x70(struct enip_data_Connected_0x70 *enipDataConnected_0x70)
{
uint16_t dataLength = ((uint16_t)enipDataConnected_0x70->item2_length[1] << 8) | (uint16_t)enipDataConnected_0x70->item2_length[0];
return dataLength;
}
//-----------------------------------------------------------------------------
// Parses the Header information into struct
//-----------------------------------------------------------------------------
int parseEnipHeader(unsigned char *buffer, int buffer_size, struct enip_header *header, struct enip_data_Unknown *enipDataUnknown)
{
//verify if message is big enough
if (buffer_size < ENIP_MIN_LENGTH)
return -1;
@ -81,45 +144,129 @@ int parseEnipHeader(unsigned char *buffer, int buffer_size, struct enip_header *
header->status = &buffer[8];
header->sender_context = &buffer[12];
header->options = &buffer[20];
/*
memcpy(header->command, &buffer[0], 2);
memcpy(header->length, &buffer[2], 2);
memcpy(header->session_handle, &buffer[4], 4);
memcpy(header->status, &buffer[8], 4);
memcpy(header->sender_context, &buffer[12], 8);
memcpy(header->options, &buffer[20], 4);
*/
header->data = &buffer[24];
uint16_t enip_data_size = ((uint16_t)header->length[1] << 8) | (uint16_t)header->length[0];
//verify if buffer_size matches enip_data_size
if (buffer_size - 24 < enip_data_size)
return -1;
return enip_data_size;
}
int parseEnipData(struct enip_header *header, struct enip_data *data)
//-----------------------------------------------------------------------------
// Parses the ENIP data from the buffer
// ENIP Type: UNKNOWN
//-----------------------------------------------------------------------------
void parseEnipUnknown(unsigned char *buffer, struct enip_data_Unknown *enipDataUnknown)
{
data->interface_handle = &header->data[0];
data->timeout = &header->data[4];
data->item_count = &header->data[6];
data->item1_id = &header->data[8];
data->item1_length = &header->data[10];
data->item1_data = &header->data[12];
uint16_t item_length = ((uint16_t)data->item1_length[1] << 8) | (uint16_t)data->item1_length[0];
data->item2_id = &header->data[12+item_length];
data->item2_length = &header->data[14+item_length];
data->item2_data = &header->data[16+item_length];
return 1;
enipDataUnknown->interface_handle = &buffer[24];
enipDataUnknown->timeout = &buffer[28];
enipDataUnknown->item_count = &buffer[30];
enipDataUnknown->item1_id = &buffer[32];
enipDataUnknown->item1_length = &buffer[34];
enipDataUnknown->item1_data = &buffer[36];
enipDataUnknown->item2_id = &buffer[37];
enipDataUnknown->item2_length = &buffer[39];
enipDataUnknown->item2_data = &buffer[41];
}
int registerEnipSession(struct enip_header *header)
//-----------------------------------------------------------------------------
// Parses the ENIP data from the buffer
// ENIP Type: Unconnected
//-----------------------------------------------------------------------------
void parseEnipUnconnected(unsigned char *buffer, struct enip_data_Unconnected *enipDataUnconnected)
{
enipDataUnconnected->interface_handle = &buffer[24];
enipDataUnconnected->timeout = &buffer[28];
enipDataUnconnected->item_count = &buffer[30];
enipDataUnconnected->item1_id = &buffer[32];
enipDataUnconnected->item1_length = &buffer[34];
enipDataUnconnected->item2_id = &buffer[36];
enipDataUnconnected->item2_length = &buffer[38];
enipDataUnconnected->service = &buffer[40]; //0x4b (Request)
enipDataUnconnected->request_pathSize = &buffer[41];//[1]
enipDataUnconnected->request_path = &buffer[42];//[4]
enipDataUnconnected->requestor_idLength = &buffer[46];//[1]
enipDataUnconnected->vendor_id = &buffer[47];//[2]
enipDataUnconnected->serial_number = &buffer[49];//[4]
enipDataUnconnected->data = &buffer[53];
}
//-----------------------------------------------------------------------------
// Parses the ENIP data from the buffer
// ENIP Type: Connected
//-----------------------------------------------------------------------------
void parseEnipConnected(unsigned char *buffer, struct enip_data_Connected *enipDataConnected)
{
enipDataConnected->interface_handle = &buffer[24];//[4]
enipDataConnected->timeout = &buffer[28];//[2]
enipDataConnected->item_count = &buffer[30];//[2]
enipDataConnected->item1_id = &buffer[32];//[2]
enipDataConnected->item1_length = &buffer[34];//[2]
enipDataConnected->item2_id = &buffer[36];//[2]
enipDataConnected->item2_length = &buffer[38];//[2]
enipDataConnected->service = &buffer[40];//[1] 0x4b (Request)
enipDataConnected->request_pathSize = &buffer[41];//[1] -----------size in words
enipDataConnected->request_path = &buffer[42];//[4]
enipDataConnected->actual_timeout = &buffer[46];//[2]
enipDataConnected->o2t_netConnectID = &buffer[48];//[4]
enipDataConnected->t2o_netConnectID = &buffer[52];//[4]
enipDataConnected->connect_serialNo = &buffer[56];//[2]
enipDataConnected->orig_vendorNo = &buffer[58];//[2]
enipDataConnected->orig_serialNo = &buffer[60];//[4]
enipDataConnected->timeout_multiplier = &buffer[64];//[1]
enipDataConnected->reserved = &buffer[65];//[3]
enipDataConnected->o2t_rpi = &buffer[68];//[4]
enipDataConnected->o2t_netConnectParam = &buffer[72];//[2]
enipDataConnected->t2o_rpi = &buffer[74];//[4]
enipDataConnected->t2o_netConnectParam = &buffer[78];//[2]
enipDataConnected->transport_trigger = &buffer[80];//[1]
enipDataConnected->connection_pathSize = &buffer[81];//[1] ----- size in words
enipDataConnected->connection_path = &buffer[82];//[?]
}
void parseEnipDataConnected_0x70(unsigned char *buffer, struct enip_data_Connected_0x70 *enipDataConnected_0x70)
{
enipDataConnected_0x70->interface_handle = &buffer[24];
enipDataConnected_0x70->timeout = &buffer[28];
enipDataConnected_0x70->item_count = &buffer[30];
enipDataConnected_0x70->item1_id = &buffer[32];
enipDataConnected_0x70->item1_length = &buffer[34];
enipDataConnected_0x70->connection_id = &buffer[36];
enipDataConnected_0x70->item2_id = &buffer[40];
enipDataConnected_0x70->item2_length = &buffer[42];
enipDataConnected_0x70->sequence_count = &buffer[44];
enipDataConnected_0x70->service = &buffer[46];
enipDataConnected_0x70->request_pathSize = &buffer[47];
enipDataConnected_0x70->request_path = &buffer[48];
enipDataConnected_0x70->requestor_id = &buffer[52];
enipDataConnected_0x70->pcccData = &buffer[59];
}
//-----------------------------------------------------------------------------
// Registers a ENIP Session
// Command Code: 0x65
//-----------------------------------------------------------------------------
int registerEnipSession(struct enip_header *header)
{
unsigned char r[4];
srand((unsigned)time(NULL));
@ -133,55 +280,312 @@ int registerEnipSession(struct enip_header *header)
}
////////////////////////////////////////////////////////////////////////////////
/// \brief This function must parse and process the client request and write
/// back the response for it.
/// \param *buffer
/// \param buffer_size
/// \return size of the response message in bytes.
////////////////////////////////////////////////////////////////////////////////
int processEnipMessage(unsigned char *buffer, int buffer_size)
//-----------------------------------------------------------------------------
// SendRRData
// Receives a PCCC msg and Responds
// Command Code: 0x65
//-----------------------------------------------------------------------------
int sendRRData(int enipType, struct enip_header *header, struct enip_data_Unknown *enipDataUnknown, struct enip_data_Unconnected *enipDataUnconnected, struct enip_data_Connected *enipDataConnected)
{
int error_code;
if (enipType == 1)
{
//writeDataContents(enipDataUnknown);
uint16_t currentHeaderLength = get_HeaderLength(header); // get length of current stored size
uint16_t currentPcccSize = get_Item2_DataLength(enipDataUnknown); // get length of stored pccc size
//change timeout value
enipDataUnknown->timeout[0] = 0x00;
enipDataUnknown->timeout[1] = 0x04;
//get pointer to beginning of pccc data to be passed
unsigned char* pcccData = enipDataUnknown->item2_data;
//send pccc Data to pccc.cpp to be parsed and craft response
// returns the new PCCC data size
uint16_t newPcccSize = processPCCCMessage(pcccData, currentPcccSize);
if (newPcccSize == -1)
return -1; //error in PCCC.cpp
//change enipDataUnknown->item2_length to match new pccc data size
enipDataUnknown->item2_length[0] = newPcccSize & 0xFF;
enipDataUnknown->item2_length[1] = newPcccSize >> 8;
//calculate new header length size
uint16_t len = currentHeaderLength - (currentPcccSize - newPcccSize);
//change header->length to match new enip data size
header->length[0] = len & 0xFF;
header->length[1] = len >> 8;
//calculate total size of enip response message in bytes
uint16_t messageSize = len + 24;
return messageSize; // total message size in bytes
}
else if (enipType == 2)
{
//change timeout value
enipDataUnconnected->timeout[0] = 0x00;
enipDataUnconnected->timeout[1] = 0x04;
uint16_t currentHeaderLength = get_HeaderLength(header); // get length of current stored size
uint16_t currentItem2Size = get_Item2_DataLength_Unconnected(enipDataUnconnected); // get length of stored pccc size
//get pointer to beginning of pccc data to be passed
unsigned char* pcccData = enipDataUnconnected->data;
//send pccc Data to pccc.cpp to be parsed and craft response
// returns the new PCCC data size
uint16_t newPcccSize = processPCCCMessage(pcccData, currentItem2Size - 13); // get length of new pccc size
if (newPcccSize == -1)
return -1; //error in PCCC.cpp
//item2_length is the length of the PCCC Data + 11 (11 for number of bytes after item2_length excluding PCCC Data)
uint16_t newItem2Size = 11 + newPcccSize;
//change enipDataUnconnected->item2_length to match new pccc data size
enipDataUnconnected->item2_length[0] = newItem2Size & 0xFF;
enipDataUnconnected->item2_length[1] = newItem2Size >> 8;
//calculate new header length size
uint16_t newHeaderLength = currentHeaderLength - (currentItem2Size - newItem2Size);
//change header->length to match new enip data size
header->length[0] = newHeaderLength & 0xFF;
header->length[1] = newHeaderLength >> 8;
//change service 0x4b to 0xcb
enipDataUnconnected->service[0] = 0xcb;
//change request path to 0
enipDataUnconnected->request_pathSize[0] = 0x00;
enipDataUnconnected->request_path[0] = 0x00;
enipDataUnconnected->request_path[1] = 0x00;
//move data forward
memmove(&enipDataUnconnected->request_path[2], enipDataUnconnected->requestor_idLength, newPcccSize + 7);//11);
//obtain total size of response message in bytes
uint16_t messageSize = newHeaderLength + 24; // 24 is the static header size
return messageSize; // total message size in bytes
}
else if (enipType == 3)
{
//change timeout value
enipDataConnected->timeout[0] = 0x00;
enipDataConnected->timeout[1] = 0x04;
//change item2_length value (always 30?)
enipDataConnected->item2_length[0] = 0x1e;
enipDataConnected->item2_length[1] = 0x00;
//change service response 0x54->0xd4
enipDataConnected->service[0] = 0xd4;
//change request path to 0
enipDataConnected->request_pathSize[0] = 0x00;
enipDataConnected->request_path[0] = 0x00;
enipDataConnected->request_path[1] = 0x00;
// change o2t_netConnectID
enipDataConnected->request_path[2] = 0x5a;
enipDataConnected->request_path[3] = 0xf0;
enipDataConnected->actual_timeout[0] = 0xb8;
enipDataConnected->actual_timeout[1] = 0x01;
// start at the back and move up forward
// overwrite t2o_netConnectParam with response of reserved 0x00 00
enipDataConnected->t2o_netConnectParam[0] = 0x00;
enipDataConnected->t2o_netConnectParam[1] = 0x00;
// move up to overwrite o2t_netConnectParam
memmove(&enipDataConnected->o2t_netConnectParam[0], enipDataConnected->t2o_rpi, 6); //6 = 80841e00 00 00
// move to overwrite timeout multiplier
memmove(&enipDataConnected->timeout_multiplier[0], enipDataConnected->o2t_rpi, 10);//10 = 80841e00 80841e00 0000
// move to overwrite o2t_netConnectID
memmove(&enipDataConnected->o2t_netConnectID[0], enipDataConnected->t2o_netConnectID, 22);
//change length inside header
header->length[0] = 0x2e;
header->length[1] = 0x00;
//calculate total size of response message in bytes
// this will be length from header +24
// uint16_t enip_dataSize = len + 24;
uint16_t messageSize = 70;
return messageSize;
}
else
{
// log error to openPLC
return -1;
}
}
//-----------------------------------------------------------------------------
// SendUnitData
// Receives a PCCC msg and Responds
// Command Code: 0x70
//-----------------------------------------------------------------------------
int sendUnitData(struct enip_header *header, struct enip_data_Connected_0x70 *enipDataConnected_0x70)
{
//change the service response 0x4b -> 0xcb
enipDataConnected_0x70->service[0] = 0xcb;
//overwrite request path
enipDataConnected_0x70->request_pathSize[0] = 0x00;
enipDataConnected_0x70->request_path[0] = 0x00;
enipDataConnected_0x70->request_path[1] = 0x00;
//get pointer to beginning of pccc data to be passed
unsigned char* pcccData = enipDataConnected_0x70->pcccData;
int masked_write_check = 0;
int write_check = 0;
int write_check_2 = 0;
if(pcccData[4] == 0xab)
{
masked_write_check = pcccData[5]; //Byte Size
}
if(pcccData[4] == 0xaa)
{
//write_check = 2;
write_check = 2;
}
if(pcccData[4] == 0xa2 && pcccData[7] == 0x8a)
{
write_check_2 = -6;
}
//if(pcccData[4] == 0xa2 && pcccData[7] == 0x89)
//{
//write_check_2 = -2;
//}
//get the current Item2_Length
uint16_t currentItem2Size = get_Item2_DataLength_Connected_0x70(enipDataConnected_0x70);
//calculate the currentPcccSize
uint16_t currentPcccSize = abs(currentItem2Size - 15);
//send pccc Data to pccc.cpp to be parsed and craft response
// returns the new PCCC data size
uint16_t newPcccSize = processPCCCMessage(pcccData, currentPcccSize);
if (newPcccSize == -1)
return -1; //error in PCCC.cpp
//calculate Data Sizes
uint16_t newItem2Size = newPcccSize + 13;
uint16_t newHeaderSize = newPcccSize + newItem2Size + 14 + masked_write_check + write_check + write_check_2; // Use to be '+ 16' uint16_t newHeaderSize = newPcccSize + newItem2Size + 16;
//change item2_length to match new cip data size
enipDataConnected_0x70->item2_length[0] = newItem2Size & 0xFF;
enipDataConnected_0x70->item2_length[1] = newItem2Size >> 8;
//change header->length to match new enip data size
header->length[0] = newHeaderSize & 0xFF;
header->length[1] = newHeaderSize >> 8;
//move data forward
memmove(&enipDataConnected_0x70->request_path[2], enipDataConnected_0x70->requestor_id, newPcccSize + 7);
//calculate total size of enip response message in bytes
uint16_t messageSize = newHeaderSize + 24;
return messageSize; // total message size in bytes
}
//-----------------------------------------------------------------------------
// This function must parse and process the client request and write back the
// response for it. The return value is the size of the response message in
// bytes.
//-----------------------------------------------------------------------------
int processEnipMessage(unsigned char *buffer, int buffer_size)
{
// initialize logging system
char log_msg[1000];
char *p = log_msg;
// initailize structs
struct enip_header header;
error_code = parseEnipHeader(buffer, buffer_size, &header);
if (error_code < 0)
return respondToError(buffer, buffer_size, error_code);
if (header.command[0] == 0x65)
struct enip_data_Unknown enipDataUnknown;
struct enip_data_Unconnected enipDataUnconnected;
struct enip_data_Connected enipDataConnected;
struct enip_data_Connected_0x70 enipDataConnected_0x70;
if (parseEnipHeader(buffer, buffer_size, &header, &enipDataUnknown) < 0)
{
return -1;
}
parseEnipUnknown(buffer, &enipDataUnknown);
// Register a Session
if (header.command[0] == 0x65)
return registerEnipSession(&header);
if (header.command[0] == 0x70) // Send Unit Data ---> works with Connected Type
{
parseEnipDataConnected_0x70(buffer, &enipDataConnected_0x70);
uint16_t size = sendUnitData(&header, &enipDataConnected_0x70);
return size; //sendUnitData()
}
//writeDataContents(&enipDataUnknown);
else if (header.command[0] == 0x6f)
{
struct enip_data data;
error_code = parseEnipData(&header, &data);
if (error_code < 0)
return respondToError(buffer, buffer_size, error_code);
if (data.item2_id[0] == 0x91)
{
//PCCC type 1 - Unknown
}
else if (data.item2_id[0] == 0xb2)
{
//PCCC type 2 - Unconnected Data Item
}
else if (data.item1_id[0] == 0xa1 && data.item2_id[0] == 0xb1)
{
//PCCC type 3 - Connected Data Item
}
else
{
//Unknown type ID. Respond with error_code
}
return 0;
}
// select Enip type-----------------------------
// 1 = UNKNOWN
// 2 = Unconnected
// 3 = Connected
// -1 = ERROR: unsupported enip type------------
int enipType = getEnipType(&enipDataUnknown, &header);
if (enipType == 2)
{
parseEnipUnconnected(buffer, &enipDataUnconnected);
}
else if (enipType == 3)
{
parseEnipConnected(buffer, &enipDataConnected);
}
else if (enipType < 0)
{
// log UNKNOWN Enip Type message to open plc
spdlog::info("ENIP: Received unsupported EtherNet/IP Type");
}
//writeDataContents(&enipDataUnknown);
//if (header.command[0] == 0x65) // Register a Session
// return registerEnipSession(&header);
if (header.command[0] == 0x6f) // Send RR Data
{
//writeDataContents(&enipDataUnknown);
uint16_t size = sendRRData(enipType, &header, &enipDataUnknown, &enipDataUnconnected, &enipDataConnected);
return size;
}
/*else if (header.command[0] == 0x70) // Send Unit Data ---> works with Connected Type
{
parseEnipDataConnected_0x70(buffer, &enipDataConnected_0x70);
uint16_t size = sendUnitData(&header, &enipDataConnected_0x70);
return size; //sendUnitData()
}*/
else
{
char log_msg[1000];
char *p = log_msg;
p += sprintf(p, "Unknown EtherNet/IP request: ");
for (int i = 0; i < buffer_size; i++)
{

126
runtime/core/enipStruct.h Normal file
View File

@ -0,0 +1,126 @@
//-----------------------------------------------------------------------------
// Copyright 2019 Thiago Alves
// This file is part of the OpenPLC Software Stack.
//
// OpenPLC is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// OpenPLC is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with OpenPLC. If not, see <http://www.gnu.org/licenses/>.
//------
//
// This file contains the structured used by enip.cpp to process
// EtherNet/IP requests.
// UAH, Sep 2019
//-----------------------------------------------------------------------------
struct enip_header
{
unsigned char *command;//[2];
unsigned char *length;//[2];
unsigned char *session_handle;//[4];
unsigned char *status;//[4];
unsigned char *sender_context;//[8];
unsigned char *options;//[4];
unsigned char *data;
};
struct enip_data_Unknown
{
unsigned char *interface_handle;//[4]
unsigned char *timeout;//[2]
unsigned char *item_count;//[2]
unsigned char *item1_id;//[2]
unsigned char *item1_length;//[2]
unsigned char *item1_data;//[1]
unsigned char *item2_id;//[2]
unsigned char *item2_length;//[2]
unsigned char *item2_data;
};
struct enip_data_Unconnected
{
unsigned char *interface_handle;//[4]
unsigned char *timeout;//[2]
unsigned char *item_count;//[2]
unsigned char *item1_id;//[2]
unsigned char *item1_length;//[2]
unsigned char *item2_id;//[2]
unsigned char *item2_length;//[2]
unsigned char *service;//[1] 0x4b (Request)
unsigned char *request_pathSize;//[1]
unsigned char *request_path;//[4]
unsigned char *requestor_idLength;//[1]
unsigned char *vendor_id;//[2]
unsigned char *serial_number;//[4]
unsigned char *data;
};
struct enip_data_Connected
{
unsigned char *interface_handle;//[4]
unsigned char *timeout;//[2]
unsigned char *item_count;//[2]
unsigned char *item1_id;//[2]
unsigned char *item1_length;//[2]
unsigned char *item2_id;//[2]
unsigned char *item2_length;//[2]
unsigned char *service;//[1] 0x4b (Request)
unsigned char *request_pathSize;//[1] -----------size in words
unsigned char *request_path;//[4]
unsigned char *actual_timeout;//[2]
unsigned char *o2t_netConnectID;//[4]
unsigned char *t2o_netConnectID;//[4]
unsigned char *connect_serialNo;//[2]
unsigned char *orig_vendorNo;//[2]
unsigned char *orig_serialNo;//[4]
unsigned char *timeout_multiplier;//[1]
unsigned char *reserved;//[3]
unsigned char *o2t_rpi;//[4]
unsigned char *o2t_netConnectParam;//[2]
unsigned char *t2o_rpi;//[4]
unsigned char *t2o_netConnectParam;//[2]
unsigned char *transport_trigger;//[1]
unsigned char *connection_pathSize;//[1] ----- size in words
unsigned char *connection_path;//[?]
};
struct enip_data_Connected_0x70
{
unsigned char *interface_handle;//[4]
unsigned char *timeout;//[2]
unsigned char *item_count;//[2]
unsigned char *item1_id;//[2] --- 0x a1 00
unsigned char *item1_length;//[2] --- 0x 04 00
unsigned char *connection_id;//[4]
unsigned char *item2_id;//[2] --- 0x b1 00
unsigned char *item2_length;//[2] length in bytes after this byte
unsigned char *sequence_count;//[2]
unsigned char *service;//[1] --- 0x4b
unsigned char *request_pathSize;//[1] --- 0x 20 67 24 01
unsigned char *request_path;//[4]
unsigned char *requestor_id;//[7]
unsigned char *pcccData;//[?]
};

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

@ -21,6 +21,7 @@
#include <pthread.h>
#include <cstdint>
#include <mutex>
#include "enipStruct.h" //This header file contains necessary structs for enip.cpp
/** \addtogroup openplc_runtime
* @{
@ -84,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
@ -149,6 +149,9 @@ void mapUnusedIO();
//enip.cpp
int processEnipMessage(unsigned char *buffer, int buffer_size);
//pccc.cpp ADDED Ulmer
uint16_t processPCCCMessage(unsigned char *buffer, int buffer_size);
//modbus_master.cpp
void initializeMB();
void *querySlaveDevices(void *arg);
@ -157,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

@ -44,7 +44,7 @@ IEC_BOOL __DEBUG;
IEC_LINT cycle_counter = 0;
static int tick = 0;
unsigned long __tick = 0;
std::mutex bufferLock; //mutex for the internal buffers
uint8_t run_openplc = 1; //Variable to control OpenPLC Runtime execution
@ -231,14 +231,13 @@ int main(int argc,char **argv)
glueVars();
updateBuffersIn(); //read input image
{
std::lock_guard<std::mutex> guard(bufferLock);
updateCustomIn();
updateBuffersIn_MB(); //update input image table with data from slave devices
handleSpecialFunctions();
config_run__(tick++); // execute plc program logic
config_run__(__tick++); // execute plc program logic
updateCustomOut();
updateBuffersOut_MB(); //update slave devices with data from the output image table
}

571
runtime/core/pccc.cpp Normal file
View File

@ -0,0 +1,571 @@
//-----------------------------------------------------------------------------
// Copyright 2019 Thiago Alves
// This file is part of the OpenPLC Software Stack.
//
// OpenPLC is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// OpenPLC is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with OpenPLC. If not, see <http://www.gnu.org/licenses/>.
//------
//
// This file has all the PCCC functions supported by the OpenPLC. If any
// other function is to be added to the project, it must be added here
// UAH, Sep 2019
//-----------------------------------------------------------------------------
//------------Libraries-------------//
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <time.h>
#include <string.h>
#include <math.h>
#include <spdlog/spdlog.h>
#include "ladder.h"
//--------------------------------------------------------------Defines--------------------------------------------------------------------------------//
/*------------Maximum/Minimum Sizes for each buffer------------------*/
#define MAX_DISCRETE_INPUT 1024 // Digital Inputs
#define MAX_COILS 1024 // Digital Outputs
#define MAX_HOLD_REGS 1024 // Pure registers Analog Outputs
//#define MAX_INP_REGS 1024 // Analog Inputs
#define MIN_16B_RANGE 1024 //Holding Register Size 16bit (memory)
#define MAX_16B_RANGE 2047 //Holding Register Size 16bit (memory)
#define MAX_32B_RANGE 2047 //Holding Register Size 32bit (memory)
/*------------File Type for PCCC--------------*/
#define PCCC_INPUT_LOGICAL_SLOT 0x8c
#define PCCC_OUTPUT_LOGICAL_SLOT 0x8b
#define PCCC_INTEGER 0x89
#define PCCC_FLOATING_POINT 0x8A
#define PCCC_FN_OUTPUT 0x00
#define PCCC_FN_INPUT 0x01
#define PCCC_FN_INT 0x07
#define PCCC_FN_FLOAT 0x08
/*----------------Define functions for bit/byte operations-------------------*/
#define bitRead(value, bit) (((value) >> (bit)) & 0x01)
#define bitSet(value, bit) ((value) |= (1UL << (bit)))
#define bitClear(value, bit) ((value) &= ~(1UL << (bit)))
#define bitWrite(value, bit, bitvalue) (bitvalue ? bitSet(value, bit) : bitClear(value, bit))
#define lowByte(w) ((unsigned char) ((w) & 0xff))
#define highByte(w) ((unsigned char) ((w) >> 8))
/*---------------------------------------------------------------------------*/
/*-----PLC Buffers - These buffer store the contents from OpenPLC-----------*/
IEC_BOOL pccc_discrete_input[MAX_DISCRETE_INPUT];
IEC_BOOL pccc_coils[MAX_COILS];
//IEC_UINT pccc_input_regs[MAX_INP_REGS];
IEC_UINT pccc_holding_regs[MAX_HOLD_REGS];
//-----------------------------------------------------------------------------------------------------------------------------------------------------//
thread_local int Pccc_MessageLength;
using namespace std;
//-----------------------------------------------------------Structure Defines--------------------------------------------------//
struct pccc_header //Structure for the Header Information for EthernetIP
{
unsigned char *Data;
unsigned char *Data_Size;
unsigned char *HD_length = 5;//[5] -> Typical Header Length for Command; Response Header Length is 4
unsigned char *HD_CMD_Code;//[1] -> Command Code
unsigned char *HD_Status;//[1] -> Status Code
unsigned char *HD_TransactionNum;//[2] -> Transaction Number
unsigned char *HD_Data_Function_Code;//[1] -> Function code MSB
unsigned char *HD_Ext_Status; //Ext Status -> only appended if Status = 0x0f[1]
unsigned char resp_cod_hex = 0x4f; //Response Hex Value
unsigned char *RP_CMD_Code = &resp_cod_hex;//[1] -> Reply Command Code = 0x4f
};
struct protected_logical_read_command //Struct for Reply and Command values of Read
{
unsigned char *CMD_Byte_Size;//[1]
unsigned char *RP_EXT_Status;//[1]* -> Ext Status -> only appended if Status = 0x0f[1]
};
struct protected_logical_write_command
{
unsigned char *CMD_Byte_Size;//[1]*
unsigned char *RP_EXT_Status;//[1]* -> Ext Status -> only appended if Status = 0x0f[1]
};
//--------------------------------------------------------------------------------------------------------------------------------------//
//------------------------Function Declaration---------------------------------//
uint16_t Command_Protocol(pccc_header header,unsigned char *buffer, int buffer_size);
uint16_t ParsePCCCData(unsigned char *buffer, int buffer_size);
uint16_t Protected_Logical_Read_Reply(pccc_header, unsigned char *buffer, int buffer_size);
uint16_t Protected_Logical_Write_Reply(pccc_header, unsigned char *buffer, int buffer_size);
void Pccc_ReadCoils(unsigned char *buffer, int buffer_size);
void Pccc_WriteCoil(unsigned char *buffer, int buffer_size);
void Pccc_ReadDiscreteInputs(unsigned char *buffer, int buffer_size);
void Pccc_ReadHoldingRegisters(unsigned char *buffer, int buffer_size);
//void Pccc_ReadInputRegisters(unsigned char *buffer, int buffer_size);
void Pccc_WriteRegister(unsigned char *buffer, int buffer_size);
int word_pccc(unsigned char byte1, unsigned char byte2);
int an_word_pccc(unsigned char byte1, unsigned char byte2);
//----------------------------------------------------------------------------//
//This function takes in the data from enip.cpp and places the data in the appropriate structure variables
uint16_t processPCCCMessage(unsigned char *buffer, int buffer_size)
{
/* Variables */
int new_pccc_length; //New PCCC Length
pccc_header header;
header.Data = buffer;
header.Data_Size = buffer_size;
/*Determine the new pccc length*/
new_pccc_length = ParsePCCCData(buffer,buffer_size);
return new_pccc_length; //Return the length to enip.cpp
}
uint16_t ParsePCCCData(unsigned char *buffer, int buffer_size)
{
/*Variables*/
int new_pccc_length; //Variable for new PCCC length
pccc_header header;
header.HD_CMD_Code = &buffer[0];//[1] -> Command Code
header.HD_Status = &buffer[1];////[1] -> Status Code
header.HD_TransactionNum = &buffer[2];//[2] -> Transaction Number
header.HD_Data_Function_Code = &buffer[4];//[1] -> Data Function Code
/*Determine what command is being requested*/
new_pccc_length = Command_Protocol(header,buffer,buffer_size);
return new_pccc_length; //Return the new pccc length
}
/* Determine the Command that is being requested to execute */
uint16_t Command_Protocol(pccc_header header, unsigned char *buffer, int buffer_size)
{
uint16_t var_pccc_length;
/*If Statement to determine the command code from the Command Packet*/
if(((unsigned int)*header.HD_CMD_Code == 0x0f) && ((unsigned int)*header.HD_Data_Function_Code == 0xA2))//Protected Logical Read
{
var_pccc_length = Protected_Logical_Read_Reply(header,buffer,buffer_size);
return var_pccc_length;
}
else if(((unsigned int)*header.HD_CMD_Code == 0x0f) && ( ((unsigned int)*header.HD_Data_Function_Code == 0xAA) || ((unsigned int)*header.HD_Data_Function_Code == 0xAB)))//Protected Logical Write
{
var_pccc_length = Protected_Logical_Write_Reply(header,buffer,buffer_size);
return var_pccc_length;
}
else
{
/*initialize logging system*/
unsigned char log_msg[1000];
unsigned char *p = log_msg;
spdlog::info("PCCC: Unsupported Command/Data Function Code!");
return -1;
}//return length as -1 to signify that the CMD Code/Function Code was not recognize
}
uint16_t Protected_Logical_Read_Reply(pccc_header header, unsigned char *buffer, int buffer_size)
{
/*Variables*/
protected_logical_read_command protected_LR;
protected_LR.CMD_Byte_Size = &buffer[5];//Byte Size of data to be read
/*Determining Data Length*/
unsigned int len_resp = 4;
len_resp = len_resp + (unsigned int)*protected_LR.CMD_Byte_Size;
/*check if the message is long enough- Left in for future error handling setup*/
/*if (buffer_size < 8)
{
//PCCC Error Handling; Make sure that the buffer size is at least 8
}*/
//****************** Read Coils **********************//
if(buffer[6] == PCCC_FN_OUTPUT && buffer[7] == PCCC_OUTPUT_LOGICAL_SLOT) // Done/Tested
{
Pccc_ReadCoils(buffer, buffer_size);
}
//*************** Read Discrete Inputs ***************//
else if(buffer[6] == PCCC_FN_INPUT && buffer[7] == PCCC_INPUT_LOGICAL_SLOT)// Done/Tested
{
Pccc_ReadDiscreteInputs(buffer, buffer_size);
}
//****************** Read Holding Registers[PURE, 16Bit Mem, 32bit MEM] ******************//
else if((buffer[6] == PCCC_FN_INT || buffer[6] == PCCC_FN_FLOAT) && (buffer[7] == PCCC_INTEGER || buffer[7] == PCCC_FLOATING_POINT))//Done/Tested
{
Pccc_ReadHoldingRegisters(buffer, buffer_size);
}
else
{
unsigned char log_msg[1000];
unsigned char *p = log_msg;
spdlog::info("PCCC: Error occurred while processing Protected Logical Read");
return -1;
}//return length as -1 to signify that the CMD Code/Function Code was not recognize
/*Creating the reply packet and memcpy the data into the buffer*/
memmove(&buffer[0], (unsigned int)header.RP_CMD_Code, 1); //0x4f Response Code
memmove(&buffer[1], (unsigned int)header.HD_Status, 1); //Same from COMMAND REQUEST
memmove(&buffer[2], (unsigned int)header.HD_TransactionNum, 2);//Same from COMMAND REQUEST
return len_resp; //Return the Resonse Packet Length for PCCC
}
uint16_t Protected_Logical_Write_Reply(pccc_header header,unsigned char *buffer, int buffer_size) // Connected
{
/*Variables*/
protected_logical_write_command protected_LW;
protected_LW.CMD_Byte_Size = &buffer[5];//Byte Size of data to be read
/*Determining link of new PCCC Packet*/
uint16_t len_resp = header.HD_length - 1;
/*Creating the reply packet and memcpy the data into the buffer*/
memmove(&buffer[0], (unsigned int)header.RP_CMD_Code, 1);
memmove(&buffer[1], (unsigned int)header.HD_Status, 1);
memmove(&buffer[2], (unsigned int)header.HD_TransactionNum, 2);
/*check if the message is long enough- Left in for future error handling setup*/
/*if (buffer_size < 8)
{
//PCCC Error Handling; Make sure that the buffer size is at least 8
}*/
//****************** Write Coil **********************//
if(buffer[6] == PCCC_FN_OUTPUT && buffer[7] == PCCC_OUTPUT_LOGICAL_SLOT)// Done/Tested
{
Pccc_WriteCoil(buffer, buffer_size);
}
//****************** Write Register ******************//
else if((buffer[6] == PCCC_FN_FLOAT || buffer[6] == PCCC_FN_INT) && (buffer[7] == PCCC_INTEGER || buffer[7] == PCCC_FLOATING_POINT))//Done/Tested
{
Pccc_WriteRegister(buffer, buffer_size);
}
//****************** Function Code Error ******************/
/*Left in for future error handling setup*/
else
{
//PCCC Error Handling; Make sure that the buffer size is at least 8. If none of the defined File Type and File Numbers match, error unrecognized File Type and File Number.
}
return len_resp;
}
//-----------------------------------------------------------------------------
// Concatenate two bytes into an int
//-----------------------------------------------------------------------------
int word_pccc(unsigned char byte1, unsigned char byte2)
{
int returnValue;
returnValue = (int)(byte1) | (int)byte2;
return returnValue;
}
//-----------------------------------------------------------------------------
// Concatenate two bytes into an int
//-----------------------------------------------------------------------------
int an_word_pccc(unsigned char byte1, unsigned char byte2)
{
int returnValue;
returnValue = (int)(byte1) | (int)(byte2 << 8);
return returnValue;
}
//-----------------------------------------------------------------------------
// Implementation of PCCC Read Coils
//-----------------------------------------------------------------------------
void Pccc_ReadCoils(unsigned char *buffer, int buffer_size) //Working QX Read
{
int Start, ByteDataLength, Mask;
/*check if the message is long enough- Left in for future error handling setup*/
/*if (buffer_size < 10)
{
//PCCC Error Handling (Fill in?); This Request must have at least 10 bytes. If it doesn't, its a corrupted message
}*/
Start = word_pccc(buffer[8],buffer[9]); //Start based on the Element and Subelemnt values in the Command Packet
Mask = log2( word_pccc(buffer[10],buffer[11]) ); //Save the byte size or byte data length to the variable from the command packet
ByteDataLength = buffer[5];
std::lock_guard<std::mutex> guard(bufferLock);
/*----Reading the values from the PLC bool_output buffer and writing to the PCCC buffer based on position----*/
for (int i = 0; i < ByteDataLength; i++)
{
for(int j = 0; j < 8; j++)
{
int position = Start + i * 8 + j;
if (position < MAX_COILS)
{
if(bool_output[position/8][position%8] != NULL)
{
bitWrite(buffer[4+i], j, *bool_output[position/8][position%8]);
}
else
{
bitWrite(buffer[4+i],j,0);
}
}
else
{
//PCCC Error Handling (Fill in?); If the position is greater than the MAX COILS, ERROR Overflow?
}
}
}
/*Left in for future error handling setup*/
/*if (pccc_error != ERR_NONE)
{
//PCCC Error Handling (Fill in?); Deetermine if there was an error:
}*/
}
//-----------------------------------------------------------------------------
// Implementation of PCCC Read Discrete Inputs
//-----------------------------------------------------------------------------
void Pccc_ReadDiscreteInputs(unsigned char *buffer, int buffer_size) //Working IX Read Only
{
int Start, ByteDataLength;
/*This Request must have at least 10 bytes. If it doesn't, its a corrupted messageLeft in for future error handling setup*/
/*if (buffer_size < 10)
{
//PCCC Error Handling (Fill in?); This Request must have at least 10 bytes. If it doesn't, its a corrupted message
}*/
Start = word_pccc(buffer[8],buffer[9]);//Start based on the Element and Subelemnt values in the Command Packet
ByteDataLength = buffer[5];//Save the byte size or byte data length to the variable from the command packet
std::lock_guard<std::mutex> guard(bufferLock);
/*--------Reading the values from the PLC bool_input buffer and writing to the PCCC buffer based on position--------*/
for (int i = 0; i < ByteDataLength; i++)
{
for(int j = 0; j < 8; j++)
{
int position = Start + i * 8 + j;
if (position < MAX_DISCRETE_INPUT)
{
if(bool_input[position/8][position%8] != NULL)
{
bitWrite(buffer[4+i], j, *bool_input[position/8][position%8]);
}
else
{
bitWrite(buffer[4+i],j,0);
}
}
else
{
//PCCC Error Handling (Fill in?); If the position is greater than the MAX, ERROR Overflow?
}
}
}
/*Left in for future error handling setup*/
/*if (mb_error != ERR_NONE)
{
//PCCC Error Handling (Fill in?); Deetermine if there was an error:
}*/
}
//-----------------------------------------------------------------------------
// Implementation of PCCC Read Holding Registers
//-----------------------------------------------------------------------------
void Pccc_ReadHoldingRegisters(unsigned char *buffer, int buffer_size) // QW Read
{
int Start, an_Start, WordDataLength, ByteDataLength;
/*this request must have at least 10 bytes. If it doesn't, it's a corrupted message - Left in for future error handling setup*/
/*if (buffer_size < 10)
{
//PCCC Error Handling (Fill in?); This Request must have at least 10 bytes. If it doesn't, its a corrupted message
}*/
Start = word_pccc(buffer[8],buffer[9]);//Start based on the Element and Subelemnt values in the Command Packet
ByteDataLength = buffer[5];//Save the byte size or byte data length to the variable from the command packet
WordDataLength = ByteDataLength / 2;//Calculate the word data length based on the byte data length
unsigned int Temp_FileT = buffer[7];//Value will be changed potentially during this process, save the File Type Value from command packet
unsigned int Temp_FileN = buffer[6];//Value will be changed potentially during this process, save the File Number Value from command packet
/*asked for too many registers - Left in for future error handling setup*/
/*if (ByteDataLength > 255)
{
//PCCC Error Handling (Fill in?); This Request must have at greater than 255 bytes. If it does, its a corrupted message
//return;
}*/
std::lock_guard<std::mutex> guard(bufferLock);
/*--------Reading the values from the PLC int_output, int_memory, and dint_memory buffer and writing to the PCCC buffer based on position--------*/
for(int i = 0; i < WordDataLength; i++)
{
int position = Start + i;
//int an_position = an_Start + i;
if ((position <= MIN_16B_RANGE) && (Temp_FileN == PCCC_FN_INT && Temp_FileT == PCCC_INTEGER))
{
if (int_output[position] != NULL)
{
buffer[ 4 + position * 2] = lowByte(*int_output[position]);
buffer[5 + position * 2] = highByte(*int_output[position]);
}
else
{
buffer[ 4 + position * 2] = 0;
buffer[5 + position * 2] = 0;
}
}
//accessing memory
//16-bit registers
else if ((position >= MIN_16B_RANGE && position <= MAX_16B_RANGE) && (Temp_FileN == PCCC_FN_INT && Temp_FileT == PCCC_INTEGER))
{
if (int_memory[position - MIN_16B_RANGE] != NULL)
{
buffer[ 4 + position * 2] = lowByte(*int_memory[position - MIN_16B_RANGE]);
buffer[5 + position * 2] = highByte(*int_memory[position - MIN_16B_RANGE]);
}
else
{
buffer[ 4 + position * 2] = 0;
buffer[5 + position * 2] = 0;
}
}
//32-bit registers
else if (Temp_FileN == PCCC_FN_FLOAT && Temp_FileT == PCCC_FLOATING_POINT && (position % 2 == 0))
{
position = position/2;
uint32_t tempValue = *dint_memory[position];
buffer[4+(4*position)] = tempValue;
buffer[5+(4*position)] = tempValue >> 8;
buffer[6+(4*position)] = tempValue >> 16;
buffer[7+(4*position)] = tempValue >> 24;
}
/*Left in for future error handling setup-Invalid Address*/
else
{
//PCCC Error Handling (Fill in?); If none of the above are recognized, error
}
}
}
//-----------------------------------------------------------------------------
// Implementation of PCCC Write Coil
//-----------------------------------------------------------------------------
void Pccc_WriteCoil(unsigned char *buffer, int buffer_size) //QX Write NEEDS WRITE MULTIPLE
{
int Start, Mask;
int mask_offset = 0;
/*Left in for future error handling setup*/
/*if(buffer_size < 10)
{
//ModbusError(buffer, ERR_ILLEGAL_DATA_ADDRESS);
//return;
}
*/
/*For the Write Mask, there has to be a maskoffset due to an extra two bytes */
if((unsigned int)buffer[4] == 0xAB)
{
mask_offset = buffer[5]; //Byte Size
}
Start = word_pccc(buffer[8],buffer[9]);//Start based on the Element and Subelemnt values in the Command Packet
Mask = log2( word_pccc(buffer[10],buffer[11]) );// Maskoffset based on the mask value in Masked Protected Write Command Packet
/*--------Determines if the values inside the PCCC data has a 1 or 0 in it. Writes that value to the bool_output based on the contents of the data in PCCC Buffer-------*/
if(Start < MAX_COILS)
{
unsigned char value;
if(word_pccc(buffer[10 + mask_offset],buffer[11 + mask_offset]) > 0)
{
value = 1;
}
else
{
value = 0;
}
std::lock_guard<std::mutex> guard(bufferLock);
if(bool_output[Start][Mask] != NULL)
{
*bool_output[Start][Mask] = value;
}
}
}
//-----------------------------------------------------------------------------
// Implementation of PCCC Write Holding Register
//-----------------------------------------------------------------------------
void Pccc_WriteRegister(unsigned char *buffer, int buffer_size) // QW Write
{
int Start, WordDataLength, ByteDataLength;
Start = word_pccc(buffer[8],buffer[9]);//Start based on the Element and Subelemnt values in the Command Packet
int an_Start = an_word_pccc(buffer[8],buffer[9]);//Different Start method for INTs based on the Element and Subelemnt values in the Command Packet
ByteDataLength = buffer[5];//Save the byte size or byte data length to the variable from the command packet
WordDataLength = ByteDataLength / 2;//Calculate the word data length based on the byte data length
unsigned int Temp_FileT = buffer[7];//Value will be changed potentially during this process, save the File Type Value from command packet
unsigned int Temp_FileN = buffer[6];//Value will be changed potentially during this process, save the File Number Value from command packet
std::lock_guard<std::mutex> guard(bufferLock);
/*--------Determines if the values inside the PCCC data has data. Writes that value to the appropriate PLC Buffer based on the contents of the data in PCCC Buffer-------*/
for(int i = 0; i < WordDataLength; i++)
{
int position = Start + i;
//analog outputs
if ((position <= MIN_16B_RANGE) && (Temp_FileN == PCCC_FN_INT && (Temp_FileT == PCCC_INTEGER)))
{
if (int_output[position] != NULL) *int_output[position] = an_word_pccc(buffer[10 + i], buffer[11 + i]);//look at this closer
}
//accessing memory
//16-bit registers
else if ((position >= MIN_16B_RANGE && position <= MAX_16B_RANGE) && (Temp_FileN == PCCC_FN_OUTPUT && (Temp_FileT == PCCC_INTEGER)))
{
if (int_memory[position - MIN_16B_RANGE] != NULL) *int_memory[position - MIN_16B_RANGE] = an_word_pccc(buffer[10 + i], buffer[11 + i]);//look at this closer
}
//32-bit registers
if (Temp_FileN == PCCC_FN_FLOAT && (Temp_FileT == PCCC_FLOATING_POINT))
{
if (dint_memory[position] != NULL)
{
uint32_t tempValue = buffer[10 + i] | buffer[11 + i] << 8 | buffer[12 + i] << 16 | buffer[13 + i] <<24;//look at this closer
*dint_memory[position] = tempValue;
i += 4;
}
else
{
pccc_holding_regs[position] = an_word_pccc(buffer[10 + i], buffer[11 + i]);//look at this closer might need to copy from temp
}
}
}
}

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

@ -1,3 +1,3 @@
#!/bin/bash
cd webserver
python webserver.py
python2 webserver.py

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

View File

@ -1,5 +1,6 @@
import os
import time, threading
from struct import *
from pymodbus.client.sync import ModbusTcpClient
self_path = os.path.dirname(__file__)
@ -28,7 +29,14 @@ def parse_st(st_file):
debug_data.name = tmp[0]
debug_data.location = tmp[2]
debug_data.type = tmp[4].split(';')[0]
debug_vars.append(debug_data)
#don't add special functions (%ML1024 and up) as they are not accessible
if (debug_data.location.find('ML')) > 0:
mb_address = debug_data.location.split('%ML')[1]
if (int(mb_address) < 1024):
debug_vars.append(debug_data)
else:
debug_vars.append(debug_data)
for debugs in debug_vars:
print('Name: ' + debugs.name)
@ -52,7 +60,10 @@ def modbus_monitor():
elif (debug_data.location.find('QX')) > 0:
#Reading Coils
mb_address = debug_data.location.split('%QX')[1].split('.')
result = mb_client.read_coils(int(mb_address[0])*8 + int(mb_address[1]), 1)
if (len(mb_address) < 2):
result = mb_client.read_coils(int(mb_address[0])*8, 1)
else:
result = mb_client.read_coils(int(mb_address[0])*8 + int(mb_address[1]), 1)
debug_data.value = result.bits[0]
elif (debug_data.location.find('IW')) > 0:
@ -75,11 +86,41 @@ def modbus_monitor():
elif (debug_data.location.find('MD')) > 0:
#Reading Double Memory
print('hi')
mb_address = debug_data.location.split('%MD')[1]
result = mb_client.read_holding_registers((int(mb_address)*2) + 2048, 2)
if (debug_data.type == 'SINT') or (debug_data.type == 'INT') or (debug_data.type == 'DINT'):
#signed integer
float_pack = pack('>HH', result.registers[0], result.registers[1])
debug_data.value = unpack('>i', float_pack)[0]
if (debug_data.type == 'USINT') or (debug_data.type == 'UINT') or (debug_data.type == 'UDINT'):
#unsigned integer
float_pack = pack('>HH', result.registers[0], result.registers[1])
debug_data.value = unpack('>I', float_pack)[0]
if (debug_data.type == 'REAL'):
#32-bit float
float_pack = pack('>HH', result.registers[0], result.registers[1])
debug_data.value = unpack('>f', float_pack)[0]
elif (debug_data.location.find('ML')) > 0:
#Reading Long Memory
print('hi')
mb_address = debug_data.location.split('%ML')[1]
result = mb_client.read_holding_registers((int(mb_address)*4) + 4096, 4)
if (debug_data.type == 'SINT') or (debug_data.type == 'INT') or (debug_data.type == 'DINT') or (debug_data.type == 'LINT'):
#signed integer
float_pack = pack('>HHHH', result.registers[0], result.registers[1], result.registers[2], result.registers[3])
debug_data.value = unpack('>q', float_pack)[0]
if (debug_data.type == 'USINT') or (debug_data.type == 'UINT') or (debug_data.type == 'UDINT') or (debug_data.type == 'ULINT'):
#unsigned integer
float_pack = pack('>HHHH', result.registers[0], result.registers[1], result.registers[2], result.registers[3])
debug_data.value = unpack('>Q', float_pack)[0]
if (debug_data.type == 'REAL') or (debug_data.type == 'LREAL'):
#64-bit float
float_pack = pack('>HHHH', result.registers[0], result.registers[1], result.registers[2], result.registers[3])
debug_data.value = unpack('>d', float_pack)[0]
if (monitor_active == True):

View File

@ -748,16 +748,59 @@ monitoring_head = """
font-family: arial, sans-serif;
}
input[type=text], input[type=password], select, textarea {
width: 100%;
padding: 12px 20px;
margin: 8px 0;
display: inline-block;
border: 1px solid #ccc;
border-radius: 4px;
box-sizing: border-box;
.form-inline
{
display: flex;
flex-flow: row wrap;
align-items: center;
}
.form-inline label
{
margin: 5px 10px 5px 0;
width: 130px;
}
.form-inline input
{
vertical-align: middle;
margin: 5px 10px 5px 0;
padding: 10px;
width: calc(100% - 250px);
background-color: #fff;
border: 1px solid #ddd;
}
.form-inline button
{
padding: 10px 20px;
background-color: #E02222;
width: 100px;
border: 1px solid #1F1F1F;
color: white;
cursor: pointer;
font-size: 14px;
font-family: "Roboto", sans-serif;
}
}
.form-inline button:hover
{
background-color: #B51A1A;
}
@media (max-width: 800px)
{
.form-inline input
{
margin: 10px 0;
}
.form-inline
{
flex-direction: column;
align-items: stretch;
}
}
</style>
<body onload='loadData()'>"""
@ -772,6 +815,7 @@ monitoring_tail = """
<script>
var req;
var refresh_rate = 100;
function loadData()
{
@ -802,29 +846,127 @@ monitoring_tail = """
req.send(null);
}
function updateRefreshRate()
{
html_refresh_text = document.getElementById('refresh_rate');
refresh_rate = parseInt(html_refresh_text.value);
if (refresh_rate < 100)
{
refresh_rate = 100
}
html_refresh_text.value = refresh_rate;
}
function processReqChange()
{
//If req shows 'complete'
if (req.readyState == 4)
{
mon_table = document.getElementById('monitor_table');
//If 'OK'
if (req.status == 200)
{
//Update table contents
mon_table.innerHTML = req.responseText;
//Start a new update timer
timeoutID = setTimeout('loadData()', refresh_rate);
}
}
}
</script>
</html>"""
point_info_tail = """
<br>
<br>
<br>
<br>
<center><input type="submit" value="Save Changes" class="button" style="width: 310px; height: 53px; margin: 0px 20px 0px 20px;"></center>
</form>
</div>
</div>
</div>
</body>
<script type="text/javascript">
var req;
window.onload = function()
{
setupSelector();
loadData();
}
function setupSelector()
{
var checkbox_element = document.getElementById('force_checkbox');
var selector_element = document.getElementById('forced_value');
if (checkbox_element.checked == true)
{
selector_element.disabled = false;
}
else
{
selector_element.disabled = true;
}
}
document.getElementById('force_checkbox').onchange = function()
{
setupSelector();
}
function loadData()
{
table_id = document.getElementById('point_id').value;
url = 'point-update?table_id=' + table_id;
try
{
req = new XMLHttpRequest();
} catch (e)
{
try
{
req = new ActiveXObject('Msxml2.XMLHTTP');
} catch (e)
{
try
{
req = new ActiveXObject('Microsoft.XMLHTTP');
} catch (oc)
{
alert('No AJAX Support');
return;
}
}
}
req.onreadystatechange = processReqChange;
req.open('GET', url, true);
req.send(null);
}
function processReqChange()
{
//If req shows 'complete'
if (req.readyState == 4)
{
mon_table = document.getElementById('monitor_table');
mon_point = document.getElementById('monitor_point');
//If 'OK'
if (req.status == 200)
{
//Update table contents
mon_table.innerHTML = req.responseText;
mon_point.innerHTML = req.responseText;
//Start a new update timer
timeoutID = setTimeout('loadData()', 100);
}
else
{
runtime_logs.value = 'There was a problem retrieving the logs. Error: ' + req.statusText;
timeoutID = setTimeout('loadData()', 500);
}
}
}
</script>
</html>"""

View File

@ -29,6 +29,7 @@ import pages
import openplc
import monitoring as monitor
import sys
import ctypes
import flask
import flask_login
@ -1331,7 +1332,12 @@ def monitoring():
<div style="w3-container">
<br>
<h2>Monitoring</h2>
<p>The table below displays a list of the OpenPLC points used by the currently running program. By clicking in one of the listed points it is possible to see more information about it and also to force it to be a different value.</p>
<form class="form-inline">
<label for="refresh_rate">Refresh Rate (ms):</label>
<input type="text" id="refresh_rate" value="100" name="refresh_rate">
<button type="button" onclick="updateRefreshRate()">Update</button>
</form>
<br>
<div id='monitor_table'>
<table>
<col width="50"><col width="10"><col width="10"><col width="10"><col width="100">
@ -1350,9 +1356,17 @@ def monitoring():
return_str += '<img src="/static/bool_false.png" alt="bool_false" style="width:40px;height:40px;vertical-align:middle; margin-right:10px">FALSE</td>'
else:
return_str += '<img src="/static/bool_true.png" alt="bool_true" style="width:40px;height:40px;vertical-align:middle; margin-right:10px">TRUE</td>'
else:
elif (debug_data.type == 'UINT'):
percentage = (debug_data.value*100)/65535
return_str += '<div class="w3-grey w3-round" style="height:40px"><div class="w3-container w3-blue w3-round" style="height:40px;width:' + str(int(percentage)) + '%"><p style="margin-top:10px">' + str(debug_data.value) + '</p></div></div></td>'
elif (debug_data.type == 'INT'):
percentage = ((debug_data.value + 32768)*100)/65535
debug_data.value = ctypes.c_short(debug_data.value).value
return_str += '<div class="w3-grey w3-round" style="height:40px"><div class="w3-container w3-blue w3-round" style="height:40px;width:' + str(int(percentage)) + '%"><p style="margin-top:10px">' + str(debug_data.value) + '</p></div></div></td>'
elif (debug_data.type == 'REAL') or (debug_data.type == 'LREAL'):
return_str += "{:10.4f}".format(debug_data.value)
else:
return_str += str(debug_data.value)
return_str += '</tr>'
data_index += 1
return_str += pages.monitoring_tail
@ -1394,9 +1408,17 @@ def monitor_update():
return_str += '<img src="/static/bool_false.png" alt="bool_false" style="width:40px;height:40px;vertical-align:middle; margin-right:10px">FALSE</td>'
else:
return_str += '<img src="/static/bool_true.png" alt="bool_true" style="width:40px;height:40px;vertical-align:middle; margin-right:10px">TRUE</td>'
else:
elif (debug_data.type == 'UINT'):
percentage = (debug_data.value*100)/65535
return_str += '<div class="w3-grey w3-round" style="height:40px"><div class="w3-container w3-blue w3-round" style="height:40px;width:' + str(int(percentage)) + '%"><p style="margin-top:10px">' + str(debug_data.value) + '</p></div></div></td>'
elif (debug_data.type == 'INT'):
percentage = ((debug_data.value + 32768)*100)/65535
debug_data.value = ctypes.c_short(debug_data.value).value
return_str += '<div class="w3-grey w3-round" style="height:40px"><div class="w3-container w3-blue w3-round" style="height:40px;width:' + str(int(percentage)) + '%"><p style="margin-top:10px">' + str(debug_data.value) + '</p></div></div></td>'
elif (debug_data.type == 'REAL') or (debug_data.type == 'LREAL'):
return_str += "{:10.4f}".format(debug_data.value)
else:
return_str += str(debug_data.value)
return_str += '</tr>'
data_index += 1
@ -1404,8 +1426,122 @@ def monitor_update():
</table>"""
return return_str
@app.route('/point-info', methods=['GET', 'POST'])
def point_info():
if (flask_login.current_user.is_authenticated == False):
return flask.redirect(flask.url_for('login'))
else:
#if (openplc_runtime.status() == "Compiling"): return draw_compiling_page()
point_id = flask.request.args.get('table_id')
debug_data = monitor.debug_vars[int(point_id)]
return_str = pages.w3_style + pages.settings_style + draw_top_div()
return_str += """
<div class='main'>
<div class='w3-sidebar w3-bar-block' style='width:250px; background-color:#3D3D3D'>
<br>
<br>
<a href="dashboard" class="w3-bar-item w3-button"><img src="/static/home-icon-64x64.png" alt="Dashboard" style="width:47px;height:32px;padding:0px 15px 0px 0px;float:left"><p style='font-family:"Roboto", sans-serif; font-size:20px; color:white;margin: 2px 0px 0px 0px'>Dashboard</p></a>
<a href='programs' class='w3-bar-item w3-button'><img src='/static/programs-icon-64x64.png' alt='Programs' style='width:47px;height:32px;padding:0px 15px 0px 0px;float:left'><p style='font-family:\"Roboto\", sans-serif; font-size:20px; color:white;margin: 2px 0px 0px 0px'>Programs</p></a>
<a href='modbus' class='w3-bar-item w3-button'><img src='/static/modbus-icon-512x512.png' alt='Modbus' style='width:47px;height:32px;padding:0px 15px 0px 0px;float:left'><p style='font-family:\"Roboto\", sans-serif; font-size:20px; color:white;margin: 2px 0px 0px 0px'>Slave Devices</p></a>
<a href="monitoring" class="w3-bar-item w3-button" style="background-color:#E02222; padding-right:0px;padding-top:0px;padding-bottom:0px"><img src="/static/monitoring-icon-64x64.png" alt="Monitoring" style="width:47px;height:39px;padding:7px 15px 0px 0px;float:left"><img src="/static/arrow.png" style="width:17px;height:49px;padding:0px 0px 0px 0px;margin: 0px 0px 0px 0px; float:right"><p style='font-family:"Roboto", sans-serif; font-size:20px; color:white;margin: 10px 0px 0px 0px'>Monitoring</p></a>
<a href='hardware' class='w3-bar-item w3-button'><img src='/static/hardware-icon-980x974.png' alt='Hardware' style='width:47px;height:32px;padding:0px 15px 0px 0px;float:left'><p style='font-family:\"Roboto\", sans-serif; font-size:20px; color:white;margin: 2px 0px 0px 0px'>Hardware</p></a>
<a href='users' class='w3-bar-item w3-button'><img src='/static/users-icon-64x64.png' alt='Users' style='width:47px;height:32px;padding:0px 15px 0px 0px;float:left'><p style='font-family:\"Roboto\", sans-serif; font-size:20px; color:white;margin: 2px 0px 0px 0px'>Users</p></a>
<a href='settings' class='w3-bar-item w3-button'><img src='/static/settings-icon-64x64.png' alt='Settings' style='width:47px;height:32px;padding:0px 15px 0px 0px;float:left'><p style='font-family:\"Roboto\", sans-serif; font-size:20px; color:white;margin: 2px 0px 0px 0px'>Settings</p></a>
<a href='logout' class='w3-bar-item w3-button'><img src='/static/logout-icon-64x64.png' alt='Logout' style='width:47px;height:32px;padding:0px 15px 0px 0px;float:left'><p style='font-family:\"Roboto\", sans-serif; font-size:20px; color:white;margin: 2px 0px 0px 0px'>Logout</p></a>
<br>
<br>"""
return_str += draw_status()
return_str += """
</div>
<div style="margin-left:320px; margin-right:70px">
<div style="w3-container">
<br>
<h2>Point Details</h2>
<br>
<div id='monitor_point'>
<input type='hidden' value='""" + point_id + """' id='point_id' name='point_id'/>
<p style='font-family:"Roboto", sans-serif; font-size:16px'><b>Point Name:</b> """ + debug_data.name + """</p>
<p style='font-family:"Roboto", sans-serif; font-size:16px'><b>Type:</b> """ + debug_data.type + """</p>
<p style='font-family:"Roboto", sans-serif; font-size:16px'><b>Location:</b> """ + debug_data.location + "</p>"
if (debug_data.type == 'BOOL'):
if (debug_data.value == 0):
return_str += """<p style='font-family:"Roboto", sans-serif; font-size:16px'><b>Status:</b> <img src="/static/bool_false.png" alt="bool_false" style="width:40px;height:40px;vertical-align:middle; margin-right:10px">FALSE</p>"""
else:
return_str += """<p style='font-family:"Roboto", sans-serif; font-size:16px'><b>Status:</b> <img src="/static/bool_true.png" alt="bool_true" style="width:40px;height:40px;vertical-align:middle; margin-right:10px">TRUE</p>"""
elif (debug_data.type == 'UINT'):
percentage = (debug_data.value*100)/65535
return_str += """<p style='font-family:"Roboto", sans-serif; font-size:16px'><b>Value: </b> <div class="w3-grey w3-round" style="height:40px"><div class="w3-container w3-blue w3-round" style="height:40px;width:""" + str(int(percentage)) + '%"><p style="margin-top:10px">' + str(debug_data.value) + '</p></div></div></p>'
elif (debug_data.type == 'INT'):
percentage = ((debug_data.value + 32768)*100)/65535
debug_data.value = ctypes.c_short(debug_data.value).value
return_str += """<p style='font-family:"Roboto", sans-serif; font-size:16px'><b>Value: </b> <div class="w3-grey w3-round" style="height:40px"><div class="w3-container w3-blue w3-round" style="height:40px;width:""" + str(int(percentage)) + '%"><p style="margin-top:10px">' + str(debug_data.value) + '</p></div></div></p>'
elif (debug_data.type == 'REAL') or (debug_data.type == 'LREAL'):
return_str += """<p style='font-family:"Roboto", sans-serif; font-size:16px'><b>Value: </b>""" + "{:10.4f}".format(debug_data.value) + "</p>"
else:
return_str += """<p style='font-family:"Roboto", sans-serif; font-size:16px'><b>Value: </b>""" + str(debug_data.value) + "</p>"
return_str += """<br>
<br>
</div>
<form action="/monitoring" method="post">
<label class="container">
<b>Force Point Value: </b>
<input id="force_checkbox" type="checkbox">
<span class="checkmark"></span>
</label>"""
if (debug_data.type == 'BOOL'):
return_str += """
<select id='forced_value' name='forced_value' style="width:200px;height:30px;font-size: 16px;font-family: 'Roboto', sans-serif;">
<option selected='selected' value='TRUE'>TRUE</option>
<option value='FALSE'>FALSE</option>
</select>"""
else:
return_str += """
<input type='text' id='forced_value' name='forced_value' style="width:200px;height:30px;font-size: 16px;font-family: 'Roboto', sans-serif;" value='0'>
"""
return_str += pages.point_info_tail
return return_str
@app.route('/point-update', methods=['GET', 'POST'])
def point_update():
if (flask_login.current_user.is_authenticated == False):
return flask.redirect(flask.url_for('login'))
else:
#if (openplc_runtime.status() == "Compiling"): return draw_compiling_page()
point_id = flask.request.args.get('table_id')
debug_data = monitor.debug_vars[int(point_id)]
return_str = """
<input type='hidden' value='""" + point_id + """' id='point_id' name='point_id'/>
<p style='font-family:"Roboto", sans-serif; font-size:16px'><b>Point Name:</b> """ + debug_data.name + """</p>
<p style='font-family:"Roboto", sans-serif; font-size:16px'><b>Type:</b> """ + debug_data.type + """</p>
<p style='font-family:"Roboto", sans-serif; font-size:16px'><b>Location:</b> """ + debug_data.location + "</p>"
if (debug_data.type == 'BOOL'):
if (debug_data.value == 0):
return_str += """<p style='font-family:"Roboto", sans-serif; font-size:16px'><b>Status:</b> <img src="/static/bool_false.png" alt="bool_false" style="width:40px;height:40px;vertical-align:middle; margin-right:10px">FALSE</p>"""
else:
return_str += """<p style='font-family:"Roboto", sans-serif; font-size:16px'><b>Status:</b> <img src="/static/bool_true.png" alt="bool_true" style="width:40px;height:40px;vertical-align:middle; margin-right:10px">TRUE</p>"""
elif (debug_data.type == 'UINT'):
percentage = (debug_data.value*100)/65535
return_str += """<p style='font-family:"Roboto", sans-serif; font-size:16px'><b>Value: </b> <div class="w3-grey w3-round" style="height:40px"><div class="w3-container w3-blue w3-round" style="height:40px;width:""" + str(int(percentage)) + '%"><p style="margin-top:10px">' + str(debug_data.value) + '</p></div></div></p>'
elif (debug_data.type == 'INT'):
percentage = ((debug_data.value + 32768)*100)/65535
debug_data.value = ctypes.c_short(debug_data.value).value
return_str += """<p style='font-family:"Roboto", sans-serif; font-size:16px'><b>Value: </b> <div class="w3-grey w3-round" style="height:40px"><div class="w3-container w3-blue w3-round" style="height:40px;width:""" + str(int(percentage)) + '%"><p style="margin-top:10px">' + str(debug_data.value) + '</p></div></div></p>'
elif (debug_data.type == 'REAL') or (debug_data.type == 'LREAL'):
return_str += """<p style='font-family:"Roboto", sans-serif; font-size:16px'><b>Value: </b>""" + "{:10.4f}".format(debug_data.value) + "</p>"
else:
return_str += """<p style='font-family:"Roboto", sans-serif; font-size:16px'><b>Value: </b>""" + str(debug_data.value) + "</p>"
return_str += """<br>
<br>"""
return return_str
@app.route('/hardware', methods=['GET', 'POST'])
def hardware():
if (flask_login.current_user.is_authenticated == False):