PR-758 Make it possible to run with DNP3 but not use the web server for configuration

This commit is contained in:
Garret Fick 2019-11-08 13:10:28 -05:00
parent 06e2602e01
commit 6d083e5c85
12 changed files with 379 additions and 247 deletions

155
runtime/core/bootstrap.cpp Normal file
View File

@ -0,0 +1,155 @@
// Copyright 2018 Thiago Alves
// 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 <string>
#include <vector>
#ifdef __linux__
#include <pthread.h>
#include <sys/mman.h>
#endif // __linux__
#include <spdlog/spdlog.h>
#include <ini.h>
#include "ini_util.h"
#include "ladder.h"
#include "service/service_definition.h"
#include "service/service_registry.h"
// Bootstrap is what we need to do in order to start the runtime. This
// handles initializing glue and reading configuration to start up the
// runtime according to the config.
struct PlcConfig {
/// A list of services that we will enable once the runtime has
/// started.
std::vector<std::string> services;
};
/// Handle reading values from the configuration file
int config_handler(void* user_data, const char* section,
const char* name, const char* value) {
auto config = reinterpret_cast<PlcConfig*>(user_data);
if (ini_matches("logging", "level", section, name)) {
if (strcmp(value, "debug") == 0) {
spdlog::set_level(spdlog::level::debug);
} else if (strcmp(value, "info") == 0) {
spdlog::set_level(spdlog::level::info);
} else if (strcmp(value, "warn") == 0) {
spdlog::set_level(spdlog::level::warn);
} else if (strcmp(value, "error") == 0) {
spdlog::set_level(spdlog::level::err);
}
} else if (strcmp("enabled", name) == 0 && ini_atob(value) && services_find(section)) {
// This is the name of service that we can enable, so add it
// to the list of services that we will enable.
config->services.push_back(section);
}
}
/// Initialize the PLC runtime
void bootstrap() {
//======================================================
// BOOSTRAP CONFIGURATION
//======================================================
// Read the configuration file to initialize which features
// we will have available on the PLC. This should be the
// first thing we do because it may change the logging level
// and we want that to happen early on.
PlcConfig config;
// We just assume that the file we are reading with the
// configuration information in in the etc subfolder and use
// a relative path to find it.
const char* config_path = "../etc/config.ini";
if (ini_parse(config_path, config_handler, &config) < 0) {
spdlog::info("Config file {} could not be read", config_path);
}
//======================================================
// PLC INITIALIZATION
//======================================================
// Defined by the MATIEC output to initialize itself
config_init__();
// Initialize the binding between located variables and
// our internal buffers. Required for items that depend on
// the sized-array representations.
glueVars();
//======================================================
// HARDWARE INITIALIZATION
//======================================================
// Perform any hardware specific initialization that is required. This is
// a standard part the platform where we have implemented capabilities
// for specific hardware targes.
initializeHardware();
initializeMB();
// User provided logic that runs on initialization.
initCustomLayer();
updateBuffersIn();
updateCustomIn();
updateBuffersOut();
updateCustomOut();
glueVars();
mapUnusedIO();
//======================================================
// SERVICE INITIALIZATION
//======================================================
// Initializes any services that is known and wants to participate
// in bootstrapping.
services_init();
#ifdef __linux__
//======================================================
// REAL-TIME INITIALIZATION
//======================================================
// Set our thread to real time priority
struct sched_param sp;
sp.sched_priority = 30;
spdlog::info("Setting main thread priority to RT");
if(pthread_setschedparam(pthread_self(), SCHED_FIFO, &sp))
{
spdlog::warn("WARNING: Failed to set main thread to real-time priority");
}
// Lock memory to ensure no swapping is done.
spdlog::info("Locking main thread memory");
if(mlockall(MCL_FUTURE|MCL_CURRENT))
{
spdlog::warn("WARNING: Failed to lock memory");
}
#endif
//======================================================
// SERVICE START
//======================================================
// Our next step here is to start the main loop, so start any
// services that we want now.
for (auto it = config.services.begin(); it != config.services.end(); ++it)
{
const char* service_config = "";
ServiceDefinition* def = services_find(it->c_str());
def->start(service_config);
}
}

View File

@ -47,11 +47,13 @@
#include <openpal/util/Uncopyable.h>
#include <spdlog/spdlog.h>
#include <ini.h>
#include "ladder.h"
#include "dnp3.h"
#include "dnp3_publisher.h"
#include "dnp3_receiver.h"
#include "../ini_util.h"
#include "../service/service_definition.h"
@ -165,12 +167,12 @@ string get_located_name(const string& value) {
/// @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] binding_defs The list of all binding 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,
void bind_variables(const vector<string>& binding_defs,
const GlueVariablesBinding& binding,
Dnp3IndexedGroup& binary_commands,
Dnp3IndexedGroup& analog_commands,
@ -182,21 +184,15 @@ void bind_variables(const vector<pair<string, string>>& config_items,
// 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;
}
vector<tuple<string, int8_t, int16_t>> binding_infos;
for (auto it = binding_defs.begin(); it != binding_defs.end(); ++it) {
// 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);
string name = get_located_name((*it));
int8_t group_number = get_group_number((*it));
int16_t data_index = get_data_index((*it));
if (name.empty() || group_number < 0 || data_index < 0) {
// If one of the items is not valid, then don't handle furhter
// If one of the items is not valid, then don't handle further
continue;
}
@ -216,10 +212,10 @@ void bind_variables(const vector<pair<string, string>>& config_items,
measurements_size += 1;
break;
default:
spdlog::error("DNP3 bind_location unknown group config item {}", (*it).second);
spdlog::error("DNP3 bind_location unknown group config item {}", (*it));
}
bindings.push_back(make_tuple(name, group_number, data_index));
binding_infos.push_back(make_tuple(name, group_number, data_index));
}
if (group_12_max_index >= 0) {
@ -242,7 +238,7 @@ void bind_variables(const vector<pair<string, string>>& config_items,
// Now bind each glue variable into the structure
uint16_t meas_index(0);
for (auto it = bindings.begin(); it != bindings.end(); ++it) {
for (auto it = binding_infos.begin(); it != binding_infos.end(); ++it) {
string name = std::get<0>(*it);
int8_t group_number = std::get<1>(*it);
int16_t data_index = std::get<2>(*it);
@ -270,37 +266,95 @@ void bind_variables(const vector<pair<string, string>>& config_items,
}
}
/// Container for reading in configuration from the config.ini
/// This is populated with values from the config file.
struct Dnp3Config {
Dnp3Config() :
port(20000),
link(false, false)
{}
uint16_t port;
/// Outstation config
opendnp3::OutstationConfig outstation;
/// Link layer config
opendnp3::LinkConfig link;
vector<string> bindings;
};
int dnp3s_cfg_handler(void* user_data, const char* section,
const char* name, const char* value) {
if (strcmp("dnp3s", section) != 0) {
return 0;
}
auto config = reinterpret_cast<Dnp3Config*>(user_data);
// First check for a binding, because we expect to have many more of those.
if (strcmp(name, "bind_location") == 0) {
config->bindings.push_back(value);
} else if (strcmp(name, "port") == 0) {
config->port = atoi(value);
} else if (strcmp(name, "local_address") == 0) {
config->link.LocalAddr = atoi(value);
} else if (strcmp(name, "remote_address") == 0) {
config->link.RemoteAddr = atoi(value);
} else if (strcmp(name, "keep_alive_timeout") == 0) {
if (strcmp(value, "MAX") == 0) {
config->link.KeepAliveTimeout = openpal::TimeDuration::Max();
} else {
config->link.KeepAliveTimeout = openpal::TimeDuration::Seconds(atoi(value));
}
} else if (strcmp(name, "enable_unsolicited") == 0) {
config->outstation.params.allowUnsolicited = ini_atob(value);
} else if (strcmp(name, "select_timeout") == 0) {
config->outstation.params.selectTimeout = openpal::TimeDuration::Seconds(atoi(value));
} else if (strcmp(name, "max_controls_per_request") == 0) {
config->outstation.params.maxControlsPerRequest = atoi(value);
} else if (strcmp(name, "max_rx_frag_size") == 0) {
config->outstation.params.maxRxFragSize = atoi(value);
} else if (strcmp(name, "max_tx_frag_size") == 0) {
config->outstation.params.maxTxFragSize = atoi(value);
} else if (strcmp(name, "event_buffer_size") == 0) {
config->outstation.eventBufferConfig = EventBufferConfig::AllTypes(atoi(value));
} else if (strcmp(name, "sol_confirm_timeout") == 0) {
config->outstation.params.solConfirmTimeout =
openpal::TimeDuration::Milliseconds(atoi(value));
} else if (strcmp(name, "unsol_confirm_timeout") == 0) {
config->outstation.params.unsolConfirmTimeout =
openpal::TimeDuration::Milliseconds(atoi(value));
} else if (strcmp(name, "unsol_retry_timeout") == 0) {
config->outstation.params.unsolRetryTimeout =
openpal::TimeDuration::Milliseconds(atoi(value));
} else {
spdlog::warn("Unknown configuration item {}", name);
return -1;
}
return 0;
}
OutstationStackConfig dnp3_create_config(istream& cfg_stream,
const GlueVariablesBinding& binding,
Dnp3IndexedGroup& binary_commands,
Dnp3IndexedGroup& analog_commands,
Dnp3MappedGroup& measurements) {
Dnp3MappedGroup& measurements,
uint16_t& port) {
// 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
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 == string::npos || line[0] == '#') {
continue;
}
string token = line.substr(0, pos);
string value = line.substr(pos + 1);
trim(token);
trim(value);
cfg_values.push_back(make_pair(token, value));
}
Dnp3Config dnp3_config;
ini_parse_stream(istream_fgets, &cfg_stream, dnp3s_cfg_handler, &dnp3_config);
// 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,
bind_variables(dnp3_config.bindings, binding, binary_commands,
analog_commands, measurements);
auto config = asiodnp3::OutstationStackConfig(DatabaseSizes(
@ -314,58 +368,16 @@ OutstationStackConfig dnp3_create_config(istream& cfg_stream,
measurements.group_size(50) // Time and interval
));
// Finally, handle the remaining itemss
for (auto it = cfg_values.begin(); it != cfg_values.end(); ++it) {
auto token = it->first;
auto value = it->second;
try {
if (token == "local_address") {
config.link.LocalAddr = atoi(value.c_str());
} else if (token == "remote_address") {
config.link.RemoteAddr = atoi(value.c_str());
} else if (token == "keep_alive_timeout") {
if (value == "MAX") {
config.link.KeepAliveTimeout = openpal::TimeDuration::Max();
} else {
config.link.KeepAliveTimeout = openpal::TimeDuration::Seconds(atoi(value.c_str()));
}
} else if (token == "enable_unsolicited") {
if (token == "True") {
config.outstation.params.allowUnsolicited = true;
} else {
config.outstation.params.allowUnsolicited = false;
}
} else if (token == "select_timeout") {
config.outstation.params.selectTimeout = openpal::TimeDuration::Seconds(atoi(value.c_str()));
} else if (token == "max_controls_per_request") {
config.outstation.params.maxControlsPerRequest = atoi(value.c_str());
} else if (token == "max_rx_frag_size") {
config.outstation.params.maxRxFragSize = atoi(value.c_str());
} else if (token == "max_tx_frag_size") {
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 == "sol_confirm_timeout") {
config.outstation.params.solConfirmTimeout =
openpal::TimeDuration::Milliseconds(atoi(value.c_str()));
} else if (token == "unsol_confirm_timeout") {
config.outstation.params.unsolConfirmTimeout =
openpal::TimeDuration::Milliseconds(atoi(value.c_str()));
} else if (token == "unsol_retry_timeout") {
config.outstation.params.unsolRetryTimeout =
openpal::TimeDuration::Milliseconds(atoi(value.c_str()));
}
}
catch (...) {
spdlog::error("Malformed line {} = {}", token, value);
exit(1);
}
}
config.outstation = dnp3_config.outstation;
config.link = dnp3_config.link;
port = dnp3_config.port;
return config;
}
void dnp3s_start_server(int port,
unique_ptr<istream, function<void(istream*)>>& cfg_stream,
void dnp3s_start_server(unique_ptr<istream, function<void(istream*)>>& cfg_stream,
const char* cfg_overrides,
volatile bool& run,
const GlueVariablesBinding& glue_variables) {
const uint32_t FILTERS = levels::NORMAL;
@ -373,9 +385,13 @@ void dnp3s_start_server(int port,
Dnp3IndexedGroup binary_commands = {0};
Dnp3IndexedGroup analog_commands = {0};
Dnp3MappedGroup measurements = {0};
uint16_t port;
auto config(dnp3_create_config(*cfg_stream, glue_variables,
binary_commands, analog_commands,
measurements));
measurements, port));
// If we have a config override, then check for the port number
port = strlen(cfg_overrides) > 0 ? atoi(cfg_overrides) : port;
// We are done with the file, so release the unique ptr. Normally this
// will close the reference to the file
@ -414,7 +430,7 @@ void dnp3s_start_server(int port,
{
// Create a scope so we release the log after the read/write
lock_guard<mutex> guard(*glue_variables.buffer_lock);
// Readn and write DNP3
// Read and write DNP3
int num_writes = publisher->ExchangeGlue();
receiver->ExchangeGlue();
spdlog::trace("{} data points written to outstation", num_writes);
@ -433,20 +449,17 @@ void dnp3s_start_server(int port,
spdlog::info("DNP3 Server deactivated");
}
////////////////////////////////////////////////////////////////////////////////
/// @brief Function to begin DNP3 server functions. This is the normal way that
/// the DNP3 server is started.
////////////////////////////////////////////////////////////////////////////////
void dnp3s_service_run(const GlueVariablesBinding& binding, volatile bool& run, const char* config) {
unique_ptr<istream, function<void(istream*)>> cfg_stream(new ifstream("./../webserver/dnp3.cfg"), [](istream* s)
unique_ptr<istream, function<void(istream*)>> cfg_stream(new ifstream("../etc/config.ini"), [](istream* s)
{
reinterpret_cast<ifstream*>(s)->close();
delete s;
});
int port = strlen(config) > 0 ? atoi(config) : 20000;
dnp3s_start_server(port, cfg_stream, run, binding);
dnp3s_start_server(cfg_stream, config, run, binding);
}
#endif // OPLC_DNP3_OUTSTATION
/** @}*/
/** @}*/

View File

@ -139,7 +139,8 @@ asiodnp3::OutstationStackConfig dnp3_create_config(std::istream& cfg_stream,
const GlueVariablesBinding& binding,
Dnp3IndexedGroup& binary_commands,
Dnp3IndexedGroup& analog_commands,
Dnp3MappedGroup& measurements);
Dnp3MappedGroup& measurements,
uint16_t& port);
////////////////////////////////////////////////////////////////////////////////
/// @brief Start the DNP3 server running on the specified port and configured
@ -147,16 +148,16 @@ asiodnp3::OutstationStackConfig dnp3_create_config(std::istream& cfg_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 cfg_overrides A config string with the port number.
/// @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 dnp3s_start_server(int port,
std::unique_ptr<std::istream, std::function<void(std::istream*)>>& cfg_stream,
void dnp3s_start_server(std::unique_ptr<std::istream, std::function<void(std::istream*)>>& cfg_stream,
const char* cfg_overrides,
volatile bool& run,
const GlueVariablesBinding& glue_variables);

67
runtime/core/ini_util.h Normal file
View File

@ -0,0 +1,67 @@
// 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 CORE_INI_UTIL_H_
#define CORE_INI_UTIL_H_
/** \addtogroup openplc_runtime
* @{
*/
#include <cstring>
#include <istream>
/// Convert a boolean value in the INI file to a boolean.
/// The value must be "true", otherwise it is interpreted as false.
/// @param value the value to convert
/// @return The value.
inline bool ini_atob(const char* value) {
return strcmp("true", value) == 0;
}
/// Is the section and value equal to the expected section and value?
/// @param section_expected The expected section.
/// @param value_expected The expected value.
/// @param section The current section.
/// @param value The current value.
/// @return true if both the section and value match, otherwise false.
inline bool ini_matches(const char* section_expected,
const char* value_expected,
const char* section,
const char* value) {
return strcmp(section_expected, section) == 0
&& strcmp(value_expected, value) == 0;
}
/// Implementation for fgets based on istream
/// @param pointe rto an array of chars where the string read is copied
/// @param num Maximum number of characters to be copied into str
/// @param stream The stream object. The string must be null terminated.
/// @param Return the string or null if cannot read more.
static char* istream_fgets(char* str, int num, void* stream) {
auto st = reinterpret_cast<std::istream*>(stream);
if (!st || st->eof()) {
// We previously reached the end of the file, so return the end signal.
return nullptr;
}
st->getline(str, num);
return str;
}
/** @}*/
#endif // CORE_INI_UTIL_H_

View File

@ -51,7 +51,6 @@ bool run_modbus = 0;
uint16_t modbus_port = 502;
bool run_enip = 0;
uint16_t enip_port = 44818;
uint16_t pstorage_polling = 10;
unsigned char server_command[1024];
int command_index = 0;
bool processing_command = 0;
@ -61,7 +60,6 @@ time_t end_time;
//Global Threads
pthread_t modbus_thread;
pthread_t enip_thread;
pthread_t pstorage_thread;
//Log Buffer
#define LOG_BUFFER_SIZE 1000000
@ -269,7 +267,7 @@ void processCommand(unsigned char *buffer, int client_fd)
pthread_join(modbus_thread, NULL);
spdlog::info("Modbus server was stopped");
}
stop_services();
services_stop();
run_openplc = 0;
processing_command = false;
}
@ -308,7 +306,7 @@ void processCommand(unsigned char *buffer, int client_fd)
else if (strncmp(buffer, "start_dnp3(", 11) == 0)
{
processing_command = true;
ServiceDefinition* def = find_service("dnp3s");
ServiceDefinition* def = services_find("dnp3s");
if (def && copy_command_config(buffer + 11, command_config, COMMAND_CONFIG_SIZE) == 0) {
def->start(command_config);
}
@ -317,7 +315,7 @@ void processCommand(unsigned char *buffer, int client_fd)
else if (strncmp(buffer, "stop_dnp3()", 11) == 0)
{
processing_command = true;
ServiceDefinition* def = find_service("dnp3s");
ServiceDefinition* def = services_find("dnp3s");
if (def) {
def->stop();
}
@ -387,7 +385,7 @@ void processCommand(unsigned char *buffer, int client_fd)
else if (strncmp(buffer, "start_pstorage(", 15) == 0)
{
processing_command = true;
ServiceDefinition* def = find_service("pstorage");
ServiceDefinition* def = services_find("pstorage");
if (def && copy_command_config(buffer + 15, command_config, COMMAND_CONFIG_SIZE) == 0) {
def->start(command_config);
}
@ -396,7 +394,7 @@ void processCommand(unsigned char *buffer, int client_fd)
else if (strncmp(buffer, "stop_pstorage()", 15) == 0)
{
processing_command = true;
ServiceDefinition* def = find_service("pstorage");
ServiceDefinition* def = services_find("pstorage");
if (def) {
def->stop();
}

View File

@ -155,9 +155,6 @@ void *querySlaveDevices(void *arg);
void updateBuffersIn_MB();
void updateBuffersOut_MB();
#ifdef OPLC_DNP3_OUTSTATION
//dnp3.cpp
void dnp3StartServer(int port, bool* run, const GlueVariablesBinding& binding);
#endif
void bootstrap();
/** @}*/

View File

@ -168,58 +168,12 @@ int main(int argc,char **argv)
initializeLogging(argc, argv);
spdlog::info("OpenPLC Runtime starting...");
//======================================================
// PLC INITIALIZATION
//======================================================
bootstrap();
// Start the thread for the interactive server
time(&start_time);
pthread_t interactive_thread;
pthread_create(&interactive_thread, NULL, interactiveServerThread, NULL);
config_init__();
glueVars();
//======================================================
// HARDWARE INITIALIZATION
//======================================================
initializeHardware();
initializeMB();
initCustomLayer();
updateBuffersIn();
updateCustomIn();
updateBuffersOut();
updateCustomOut();
//======================================================
// PERSISTENT STORAGE INITIALIZATION
//======================================================
glueVars();
mapUnusedIO();
ServiceDefinition* pstorageDef = find_service("pstorage");
if (pstorageDef) {
pstorageDef->initialize();
}
//pthread_t persistentThread;
//pthread_create(&persistentThread, NULL, persistentStorage, NULL);
#ifdef __linux__
//======================================================
// REAL-TIME INITIALIZATION
//======================================================
// Set our thread to real time priority
struct sched_param sp;
sp.sched_priority = 30;
spdlog::info("Setting main thread priority to RT");
if(pthread_setschedparam(pthread_self(), SCHED_FIFO, &sp))
{
spdlog::warn("WARNING: Failed to set main thread to real-time priority");
}
// Lock memory to ensure no swapping is done.
spdlog::info("Locking main thread memory");
if(mlockall(MCL_FUTURE|MCL_CURRENT))
{
spdlog::warn("WARNING: Failed to lock memory");
}
#endif
//gets the starting point for the clock
spdlog::debug("Getting current time");
@ -257,6 +211,8 @@ int main(int argc,char **argv)
//======================================================
// SHUTTING DOWN OPENPLC RUNTIME
//======================================================
services_stop();
pthread_join(interactive_thread, NULL);
spdlog::debug("Disabling outputs...");
disableOutputs();

View File

@ -31,7 +31,7 @@ ServiceDefinition* services[] = {
#endif
};
ServiceDefinition* find_service(const char* name) {
ServiceDefinition* services_find(const char* name) {
ServiceDefinition** item = std::find_if(std::begin(services), std::end(services), [name] (ServiceDefinition* def) {
return strcmp(def->id(), name) == 0;
});
@ -39,8 +39,20 @@ ServiceDefinition* find_service(const char* name) {
return (item != std::end(services)) ? *item : nullptr;
}
void stop_services() {
void services_stop() {
std::for_each(std::begin(services), std::end(services), [] (ServiceDefinition* def){
def->stop();
});
}
void services_init() {
std::for_each(std::begin(services), std::end(services), [] (ServiceDefinition* def){
def->initialize();
});
}
void services_finalize() {
std::for_each(std::begin(services), std::end(services), [] (ServiceDefinition* def){
def->finalize();
});
}

View File

@ -24,11 +24,17 @@ class ServiceDefinition;
/// Finds the service in the registry by the name of the service.
/// @param name The identifier for the service.
/// @return The service if found, or nullptr if there is no such service.
ServiceDefinition* find_service(const char* name);
ServiceDefinition* services_find(const char* name);
/// Stop all known services.
void stop_services();
void services_stop();
/// Initialize all known services.
void services_init();
/// Finalize all known services.
void services_finalize();
/** @}*/
#endif // CORE_SERVICE_SERVICE_DEFINITION_H_
#endif // CORE_SERVICE_SERVICE_DEFINITION_H_

View File

@ -21,6 +21,7 @@ include_directories(../core)
include_directories(../core/lib)
include_directories(../vendor/catch2-2.7.0)
include_directories(../vendor/fakeit-2.0.5)
include_directories(../vendor/inih-r46)
if (NOT OPLC_DNP3_OUTSTATION)
message(WARNING "Building of tests does not have DNP3 outstation enabled")
@ -30,7 +31,7 @@ endif()
file(GLOB oplctest_SRC *.cpp **/*.cpp)
file(GLOB oplc_core_SRC ../core/pstorage.cpp ../core/dnp3s/*.cpp)
add_executable(oplc_unit_test ${oplctest_SRC} ${oplc_core_SRC} ../core/glue.cpp )
add_executable(oplc_unit_test ${oplctest_SRC} ${oplc_core_SRC} ../core/glue.cpp ../vendor/inih-r46/ini.c)
target_link_libraries(oplc_unit_test ${OPLC_PTHREAD})
if (OPLC_DNP3_OUTSTATION)

View File

@ -36,6 +36,7 @@ SCENARIO("create_config", "")
Dnp3IndexedGroup binary_commands = {0};
Dnp3IndexedGroup analog_commands = {0};
Dnp3MappedGroup measurements = {0};
std::uint16_t port;
GIVEN("<input stream>")
{
@ -43,7 +44,7 @@ SCENARIO("create_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));
const OutstationStackConfig config(dnp3_create_config(input_stream, bindings, binary_commands, analog_commands, measurements, port));
REQUIRE(config.dbConfig.binary.IsEmpty());
REQUIRE(config.dbConfig.doubleBinary.IsEmpty());
@ -67,8 +68,8 @@ SCENARIO("create_config", "")
{ 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));
std::stringstream input_stream("[dnp3s]\nbind_location=name:%QX0.0,group:1,index:0,");
const OutstationStackConfig config(dnp3_create_config(input_stream, bindings, binary_commands, analog_commands, measurements, port));
REQUIRE(config.dbConfig.binary.Size() == 1);
REQUIRE(config.dbConfig.doubleBinary.Size() == 0);
@ -95,8 +96,8 @@ SCENARIO("create_config", "")
{ 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));
std::stringstream input_stream("[dnp3s]\nbind_location=name:%IX0.0,group:1,index:1,");
const OutstationStackConfig config(dnp3_create_config(input_stream, bindings, binary_commands, analog_commands, measurements, port));
REQUIRE(config.dbConfig.binary.Size() == 1);
REQUIRE(config.dbConfig.doubleBinary.Size() == 0);
@ -123,8 +124,8 @@ SCENARIO("create_config", "")
{ 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));
std::stringstream input_stream("[dnp3s]\nbind_location=name:%IX0.0,group:12,index:1,");
const OutstationStackConfig config(dnp3_create_config(input_stream, bindings, binary_commands, analog_commands, measurements, port));
REQUIRE(config.dbConfig.binary.Size() == 0);
REQUIRE(config.dbConfig.doubleBinary.Size() == 0);
@ -153,8 +154,8 @@ SCENARIO("create_config", "")
{ 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));
std::stringstream input_stream("[dnp3s]\nbind_location=name:%QD0,group:30,index:1,");
const OutstationStackConfig config(dnp3_create_config(input_stream, bindings, binary_commands, analog_commands, measurements, port));
REQUIRE(config.dbConfig.binary.Size() == 0);
REQUIRE(config.dbConfig.doubleBinary.Size() == 0);
@ -188,7 +189,7 @@ SCENARIO("dnp3s_start_server", "")
unique_ptr<istream, std::function<void(istream*)>> cfg_stream(new stringstream(""), [](istream* s) { delete s; });
GlueVariablesBinding bindings(&glue_mutex, 0, nullptr);
dnp3s_start_server(20000, cfg_stream, run_dnp3, bindings);
dnp3s_start_server(cfg_stream, "20000", run_dnp3, bindings);
}
}

View File

@ -1,75 +0,0 @@
# ----------------------------------------------------------------
# Configuration file for DNP3
#-----------------------------------------------------------------
# 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
#-----------------------------------------------------------------
# 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
#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