PR-758 Make it possible to run with DNP3 but not use the web server for configuration
This commit is contained in:
parent
06e2602e01
commit
6d083e5c85
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
||||
/** @}*/
|
||||
/** @}*/
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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_
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
||||
/** @}*/
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
}
|
|
@ -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_
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
Loading…
Reference in New Issue