Merge pull request #103 from smartergridsolutions/feature/PR-786-2
PR-786 Address code review feedback on spacing
This commit is contained in:
commit
84aeba7d36
|
@ -32,7 +32,10 @@
|
|||
// handles initializing glue and reading configuration to start up the
|
||||
// runtime according to the config.
|
||||
|
||||
struct PlcConfig {
|
||||
/// @brief Data object for reading configuration information from the
|
||||
/// configuration file.
|
||||
struct PlcConfig
|
||||
{
|
||||
/// A list of services that we will enable once the runtime has
|
||||
/// started.
|
||||
std::vector<std::string> services;
|
||||
|
@ -40,24 +43,37 @@ struct PlcConfig {
|
|||
|
||||
/// Handle reading values from the configuration file
|
||||
int config_handler(void* user_data, const char* section,
|
||||
const char* name, const char* value) {
|
||||
const char* name, const char* value)
|
||||
{
|
||||
auto config = reinterpret_cast<PlcConfig*>(user_data);
|
||||
|
||||
if (oplc::ini_matches("logging", "level", section, name)) {
|
||||
if (strcmp(value, "trace") == 0) {
|
||||
if (oplc::ini_matches("logging", "level", section, name))
|
||||
{
|
||||
if (strcmp(value, "trace") == 0)
|
||||
{
|
||||
spdlog::set_level(spdlog::level::trace);
|
||||
} else if (strcmp(value, "debug") == 0) {
|
||||
}
|
||||
else if (strcmp(value, "debug") == 0)
|
||||
{
|
||||
spdlog::set_level(spdlog::level::debug);
|
||||
} else if (strcmp(value, "info") == 0) {
|
||||
}
|
||||
else if (strcmp(value, "info") == 0)
|
||||
{
|
||||
spdlog::set_level(spdlog::level::info);
|
||||
} else if (strcmp(value, "warn") == 0) {
|
||||
}
|
||||
else if (strcmp(value, "warn") == 0)
|
||||
{
|
||||
spdlog::set_level(spdlog::level::warn);
|
||||
} else if (strcmp(value, "error") == 0) {
|
||||
}
|
||||
else if (strcmp(value, "error") == 0)
|
||||
{
|
||||
spdlog::set_level(spdlog::level::err);
|
||||
}
|
||||
} else if (strcmp("enabled", name) == 0
|
||||
&& oplc::ini_atob(value)
|
||||
&& services_find(section)) {
|
||||
}
|
||||
else if (strcmp("enabled", name) == 0
|
||||
&& oplc::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);
|
||||
|
@ -67,7 +83,8 @@ int config_handler(void* user_data, const char* section,
|
|||
}
|
||||
|
||||
/// Initialize the PLC runtime
|
||||
void bootstrap() {
|
||||
void bootstrap()
|
||||
{
|
||||
//======================================================
|
||||
// BOOSTRAP CONFIGURATION
|
||||
//======================================================
|
||||
|
@ -82,7 +99,8 @@ void bootstrap() {
|
|||
// 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) {
|
||||
if (ini_parse(config_path, config_handler, &config) < 0)
|
||||
{
|
||||
spdlog::info("Config file {} could not be read", config_path);
|
||||
// If we don't have the config file, then default to always
|
||||
// starting the interactive server.
|
||||
|
@ -133,13 +151,15 @@ void bootstrap() {
|
|||
struct sched_param sp;
|
||||
sp.sched_priority = 30;
|
||||
spdlog::info("Setting main thread priority to RT");
|
||||
if (pthread_setschedparam(pthread_self(), SCHED_FIFO, &sp)) {
|
||||
if (pthread_setschedparam(pthread_self(), SCHED_FIFO, &sp))
|
||||
{
|
||||
spdlog::warn("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)) {
|
||||
if (mlockall(MCL_FUTURE|MCL_CURRENT))
|
||||
{
|
||||
spdlog::warn("Failed to lock memory");
|
||||
}
|
||||
#endif
|
||||
|
@ -151,7 +171,8 @@ void bootstrap() {
|
|||
// 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) {
|
||||
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);
|
||||
|
|
|
@ -63,8 +63,6 @@
|
|||
* @{
|
||||
*/
|
||||
|
||||
#define OPLC_CYCLE 50000000
|
||||
|
||||
using namespace std;
|
||||
using namespace opendnp3;
|
||||
using namespace openpal;
|
||||
|
@ -74,13 +72,16 @@ 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 {
|
||||
class Dnp3ToSpdLogger final : public openpal::ILogHandler, private openpal::Uncopyable
|
||||
{
|
||||
public:
|
||||
virtual void Log(const openpal::LogEntry& entry) override {
|
||||
virtual void Log(const openpal::LogEntry& entry) override
|
||||
{
|
||||
spdlog::info("{}", entry.message);
|
||||
}
|
||||
|
||||
static std::shared_ptr<openpal::ILogHandler>Create() {
|
||||
static std::shared_ptr<openpal::ILogHandler>Create()
|
||||
{
|
||||
return std::make_shared<Dnp3ToSpdLogger>();
|
||||
}
|
||||
|
||||
|
@ -89,7 +90,8 @@ class Dnp3ToSpdLogger final : public openpal::ILogHandler, private openpal::Unco
|
|||
|
||||
/// @brief Trim from both ends (in place), removing only whitespace.
|
||||
/// @param s The string to trim
|
||||
inline void trim(string& s) {
|
||||
inline void trim(string& s)
|
||||
{
|
||||
// Trim from the left
|
||||
s.erase(s.begin(), find_if(s.begin(), s.end(),
|
||||
not1(ptr_fun<int, int>(isspace))));
|
||||
|
@ -102,10 +104,12 @@ inline void trim(string& s) {
|
|||
/// @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) {
|
||||
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) {
|
||||
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;
|
||||
|
@ -123,7 +127,8 @@ int8_t get_group_number(const string& value) {
|
|||
/// @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) {
|
||||
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) {
|
||||
|
@ -144,18 +149,21 @@ int16_t get_data_index(const string& value) {
|
|||
/// @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) {
|
||||
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) {
|
||||
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) {
|
||||
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 "";
|
||||
|
@ -175,7 +183,8 @@ void bind_variables(const vector<string>& binding_defs,
|
|||
const GlueVariablesBinding& binding,
|
||||
Dnp3IndexedGroup& binary_commands,
|
||||
Dnp3IndexedGroup& analog_commands,
|
||||
Dnp3MappedGroup& measurements) {
|
||||
Dnp3MappedGroup& measurements)
|
||||
{
|
||||
int16_t group_12_max_index(-1);
|
||||
int16_t group_41_max_index(-1);
|
||||
int16_t measurements_size(0);
|
||||
|
@ -184,24 +193,28 @@ void bind_variables(const vector<string>& binding_defs,
|
|||
// That means more work up front, but save space over the application
|
||||
// lifetime.
|
||||
vector<tuple<string, int8_t, int16_t>> binding_infos;
|
||||
for (auto it = binding_defs.begin(); it != binding_defs.end(); ++it) {
|
||||
for (auto it = binding_defs.begin(); it != binding_defs.end(); ++it)
|
||||
{
|
||||
// Find the name of the located variable
|
||||
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 (name.empty() || group_number < 0 || data_index < 0)
|
||||
{
|
||||
// If one of the items is not valid, then don't handle further
|
||||
continue;
|
||||
}
|
||||
|
||||
const GlueVariable* var = binding.find(name);
|
||||
if (!var) {
|
||||
if (!var)
|
||||
{
|
||||
spdlog::error("Unable to bind DNP3 location {} because it is not defined in the application", name);
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (group_number) {
|
||||
switch (group_number)
|
||||
{
|
||||
case GROUP_BINARY_COMMAND:
|
||||
group_12_max_index = max(group_12_max_index, data_index);
|
||||
break;
|
||||
|
@ -223,19 +236,22 @@ void bind_variables(const vector<string>& binding_defs,
|
|||
binding_infos.push_back(make_tuple(name, group_number, data_index));
|
||||
}
|
||||
|
||||
if (group_12_max_index >= 0) {
|
||||
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) {
|
||||
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) {
|
||||
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];
|
||||
|
@ -243,13 +259,15 @@ void bind_variables(const vector<string>& binding_defs,
|
|||
|
||||
// Now bind each glue variable into the structure
|
||||
uint16_t meas_index(0);
|
||||
for (auto it = binding_infos.begin(); it != binding_infos.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);
|
||||
const GlueVariable* var = binding.find(name);
|
||||
|
||||
switch (group_number) {
|
||||
switch (group_number)
|
||||
{
|
||||
case GROUP_BINARY_COMMAND:
|
||||
binary_commands.items[data_index] = var;
|
||||
break;
|
||||
|
@ -268,7 +286,8 @@ void bind_variables(const vector<string>& binding_defs,
|
|||
|
||||
/// Container for reading in configuration from the config.ini
|
||||
/// This is populated with values from the config file.
|
||||
struct Dnp3Config {
|
||||
struct Dnp3Config
|
||||
{
|
||||
Dnp3Config() :
|
||||
poll_interval(std::chrono::milliseconds(250)),
|
||||
port(20000),
|
||||
|
@ -291,52 +310,88 @@ struct Dnp3Config {
|
|||
};
|
||||
|
||||
int dnp3s_cfg_handler(void* user_data, const char* section,
|
||||
const char* name, const char* value) {
|
||||
if (strcmp("dnp3s", section) != 0) {
|
||||
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) {
|
||||
if (strcmp(name, "bind_location") == 0)
|
||||
{
|
||||
config->bindings.push_back(value);
|
||||
} else if (strcmp(name, "port") == 0) {
|
||||
}
|
||||
else if (strcmp(name, "port") == 0)
|
||||
{
|
||||
config->port = atoi(value);
|
||||
} else if (strcmp(name, "local_address") == 0) {
|
||||
}
|
||||
else if (strcmp(name, "local_address") == 0)
|
||||
{
|
||||
config->link.LocalAddr = atoi(value);
|
||||
} else if (strcmp(name, "remote_address") == 0) {
|
||||
}
|
||||
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) {
|
||||
}
|
||||
else if (strcmp(name, "keep_alive_timeout") == 0)
|
||||
{
|
||||
if (strcmp(value, "MAX") == 0)
|
||||
{
|
||||
config->link.KeepAliveTimeout = openpal::TimeDuration::Max();
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
config->link.KeepAliveTimeout = openpal::TimeDuration::Seconds(atoi(value));
|
||||
}
|
||||
} else if (strcmp(name, "enable_unsolicited") == 0) {
|
||||
}
|
||||
else if (strcmp(name, "enable_unsolicited") == 0)
|
||||
{
|
||||
config->outstation.params.allowUnsolicited = oplc::ini_atob(value);
|
||||
} else if (strcmp(name, "select_timeout") == 0) {
|
||||
}
|
||||
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) {
|
||||
}
|
||||
else if (strcmp(name, "max_controls_per_request") == 0)
|
||||
{
|
||||
config->outstation.params.maxControlsPerRequest = atoi(value);
|
||||
} else if (strcmp(name, "max_rx_frag_size") == 0) {
|
||||
}
|
||||
else if (strcmp(name, "max_rx_frag_size") == 0)
|
||||
{
|
||||
config->outstation.params.maxRxFragSize = atoi(value);
|
||||
} else if (strcmp(name, "max_tx_frag_size") == 0) {
|
||||
}
|
||||
else if (strcmp(name, "max_tx_frag_size") == 0)
|
||||
{
|
||||
config->outstation.params.maxTxFragSize = atoi(value);
|
||||
} else if (strcmp(name, "event_buffer_size") == 0) {
|
||||
}
|
||||
else if (strcmp(name, "event_buffer_size") == 0)
|
||||
{
|
||||
config->outstation.eventBufferConfig = EventBufferConfig::AllTypes(atoi(value));
|
||||
} else if (strcmp(name, "sol_confirm_timeout") == 0) {
|
||||
}
|
||||
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) {
|
||||
}
|
||||
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) {
|
||||
}
|
||||
else if (strcmp(name, "unsol_retry_timeout") == 0)
|
||||
{
|
||||
config->outstation.params.unsolRetryTimeout =
|
||||
openpal::TimeDuration::Milliseconds(atoi(value));
|
||||
} else if (strcmp(name, "enabled") == 0) {
|
||||
}
|
||||
else if (strcmp(name, "enabled") == 0)
|
||||
{
|
||||
// Nothing to do here - we already know this is enabled
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
spdlog::warn("Unknown configuration item {}", name);
|
||||
return -1;
|
||||
}
|
||||
|
@ -350,7 +405,8 @@ OutstationStackConfig dnp3_create_config(istream& cfg_stream,
|
|||
Dnp3IndexedGroup& analog_commands,
|
||||
Dnp3MappedGroup& measurements,
|
||||
uint16_t& port,
|
||||
std::chrono::milliseconds& poll_interval) {
|
||||
std::chrono::milliseconds& poll_interval)
|
||||
{
|
||||
// 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
|
||||
|
@ -385,10 +441,11 @@ OutstationStackConfig dnp3_create_config(istream& cfg_stream,
|
|||
return config;
|
||||
}
|
||||
|
||||
void dnp3s_start_server(unique_ptr<istream, function<void(istream*)>>& cfg_stream,
|
||||
void dnp3s_start_server(oplc::config_stream& cfg_stream,
|
||||
const char* cfg_overrides,
|
||||
volatile bool& run,
|
||||
const GlueVariablesBinding& glue_variables) {
|
||||
const GlueVariablesBinding& glue_variables)
|
||||
{
|
||||
const uint32_t FILTERS = levels::NORMAL;
|
||||
|
||||
Dnp3IndexedGroup binary_commands = {0};
|
||||
|
@ -432,7 +489,9 @@ void dnp3s_start_server(unique_ptr<istream, function<void(istream*)>>& cfg_strea
|
|||
spdlog::info("DNP3 outstation enabled on port {0:d}", port);
|
||||
|
||||
// Run this until we get a signal to stop.
|
||||
while (run) {
|
||||
while (run)
|
||||
{
|
||||
// Create a scope to auto-release the lock
|
||||
{
|
||||
// Create a scope so we release the log after the read/write
|
||||
lock_guard<mutex> guard(*glue_variables.buffer_lock);
|
||||
|
@ -457,12 +516,9 @@ void dnp3s_start_server(unique_ptr<istream, function<void(istream*)>>& cfg_strea
|
|||
|
||||
/// @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("../etc/config.ini"), [](istream* s)
|
||||
{
|
||||
reinterpret_cast<ifstream*>(s)->close();
|
||||
delete s;
|
||||
});
|
||||
void dnp3s_service_run(const GlueVariablesBinding& binding, volatile bool& run, const char* config)
|
||||
{
|
||||
auto cfg_stream = oplc::open_config();
|
||||
dnp3s_start_server(cfg_stream, config, run, binding);
|
||||
}
|
||||
|
||||
|
|
|
@ -55,7 +55,8 @@ const std::uint8_t GROUP_FROZEN_COUNTER(21);
|
|||
/// 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 Dnp3IndexedGroup {
|
||||
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
|
||||
|
@ -67,7 +68,8 @@ struct Dnp3IndexedGroup {
|
|||
/// @brief Defines a glue variable that is mapped to a particular group and
|
||||
/// variation for DNP3.
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
struct DNP3MappedGlueVariable {
|
||||
struct DNP3MappedGlueVariable
|
||||
{
|
||||
/// The DNP3 group for this variable.
|
||||
std::uint8_t group;
|
||||
/// The DNP3 point index number for this variable.
|
||||
|
@ -81,7 +83,8 @@ struct DNP3MappedGlueVariable {
|
|||
/// to DNP3. You can essentially iterate over this list to find every
|
||||
/// located variable that is mapped.
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
struct Dnp3MappedGroup {
|
||||
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
|
||||
|
@ -89,10 +92,13 @@ struct Dnp3MappedGroup {
|
|||
DNP3MappedGlueVariable* items;
|
||||
|
||||
/// Gets the number of items in the specified group.
|
||||
std::uint16_t group_size(const std::uint8_t 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) {
|
||||
for (std::uint16_t i(0); i < size; ++i)
|
||||
{
|
||||
if (items[i].group == group)
|
||||
{
|
||||
num += 1;
|
||||
}
|
||||
}
|
||||
|
@ -103,7 +109,8 @@ struct Dnp3MappedGroup {
|
|||
////////////////////////////////////////////////////////////////////////////////
|
||||
/// @brief The mapping of glue variables into this DNP3 outstation.
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
class Dnp3BoundGlueVariables {
|
||||
class Dnp3BoundGlueVariables
|
||||
{
|
||||
public:
|
||||
Dnp3BoundGlueVariables(
|
||||
std::mutex* buffer_lock,
|
||||
|
|
|
@ -39,10 +39,12 @@ Dnp3Publisher::Dnp3Publisher(
|
|||
{ }
|
||||
|
||||
template<class T>
|
||||
T cast_variable(const GlueVariable* var) {
|
||||
T cast_variable(const GlueVariable* var)
|
||||
{
|
||||
T val;
|
||||
void* value = var->value;
|
||||
switch (var->type) {
|
||||
switch (var->type)
|
||||
{
|
||||
case IECVT_SINT:
|
||||
{
|
||||
IEC_SINT* tval = reinterpret_cast<IEC_SINT*>(value);
|
||||
|
@ -101,38 +103,54 @@ T cast_variable(const GlueVariable* var) {
|
|||
/// 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.
|
||||
uint32_t Dnp3Publisher::ExchangeGlue() {
|
||||
uint32_t Dnp3Publisher::ExchangeGlue()
|
||||
{
|
||||
uint32_t num_writes(0);
|
||||
asiodnp3::UpdateBuilder builder;
|
||||
|
||||
spdlog::trace("Writing glue variables to DNP3 points");
|
||||
|
||||
for (uint16_t i(0); i < measurements.size; ++i) {
|
||||
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;
|
||||
|
||||
if (group == GROUP_BINARY_INPUT || group == GROUP_BINARY_OUTPUT_STATUS) {
|
||||
if (group == GROUP_BINARY_INPUT || group == GROUP_BINARY_OUTPUT_STATUS)
|
||||
{
|
||||
const GlueBoolGroup* bool_group = reinterpret_cast<const GlueBoolGroup*>(value);
|
||||
if (group == GROUP_BINARY_INPUT) {
|
||||
if (group == GROUP_BINARY_INPUT)
|
||||
{
|
||||
builder.Update(Binary(*(bool_group->values[0])), point_index_number);
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Update(BinaryOutputStatus(*(bool_group->values[0])), point_index_number);
|
||||
}
|
||||
} else if (group == GROUP_ANALOG_INPUT || group == GROUP_ANALOG_OUTPUT_STATUS) {
|
||||
}
|
||||
else if (group == GROUP_ANALOG_INPUT || group == GROUP_ANALOG_OUTPUT_STATUS)
|
||||
{
|
||||
double double_val = cast_variable<double>(var);
|
||||
if (group == GROUP_ANALOG_INPUT) {
|
||||
if (group == GROUP_ANALOG_INPUT)
|
||||
{
|
||||
builder.Update(Analog(double_val), point_index_number);
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Update(AnalogOutputStatus(double_val), point_index_number);
|
||||
}
|
||||
} else if (group == GROUP_COUNTER || group == GROUP_FROZEN_COUNTER) {
|
||||
}
|
||||
else if (group == GROUP_COUNTER || group == GROUP_FROZEN_COUNTER)
|
||||
{
|
||||
uint32_t int_val = cast_variable<uint32_t>(var);
|
||||
if (group == GROUP_COUNTER) {
|
||||
if (group == GROUP_COUNTER)
|
||||
{
|
||||
builder.Update(Counter(int_val), point_index_number);
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Update(FrozenCounter(int_val), point_index_number);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,22 +29,21 @@ namespace asiodnp3 {
|
|||
class IOutstation;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
/// @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 {
|
||||
/// information over DNP3, including inputs, outputs, memory.
|
||||
class Dnp3Publisher
|
||||
{
|
||||
public:
|
||||
/// \brief Constructs a new instance of the publisher object.
|
||||
/// @brief Constructs a new instance of the publisher object.
|
||||
/// @param outstation The outstation that is ourselves
|
||||
/// @param measurements The buffers that we use for data transfer
|
||||
Dnp3Publisher(
|
||||
std::shared_ptr<asiodnp3::IOutstation> outstation,
|
||||
const Dnp3MappedGroup& measurements);
|
||||
|
||||
/// \brief Publish the values from the in-memory buffers to DNP3 points.
|
||||
/// @brief Publish the values from the in-memory buffers to DNP3 points.
|
||||
///
|
||||
/// Writing to the points executes asynchronously. This function returns
|
||||
/// once the points have been queued to write but in general will return
|
||||
|
|
|
@ -26,49 +26,47 @@ 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;
|
||||
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) {
|
||||
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
|
||||
/// @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.
|
||||
/// @param start The start index of valid ranges.
|
||||
/// @param end One past the last valid index.
|
||||
/// @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) {
|
||||
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;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
/// \brief Map the DNP3 index to a command status value. The status value is successful
|
||||
/// @brief Map the DNP3 index to a command status value. The status value is successful
|
||||
/// if the index maps to something in the valid range, otherwise, maps to out of range.
|
||||
/// @param start The start index of valid ranges.
|
||||
/// @param end One past the last valid index.
|
||||
/// @param offset The offset defined for this set of values.
|
||||
/// @param dnp3_index The index of the point for DNP3.
|
||||
/// @return CommandStatus::SUCCESS if the DNP3 index is in range, otherwise, CommandStatus::OUT_OF_RANGE.
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
inline CommandStatus mapDnp3IndexToStatus(uint16_t start, uint16_t stop, uint16_t offset, uint16_t dnp3_index) {
|
||||
return mapDnp3IndexToGlueIndex(start, stop, offset, dnp3_index) >= 0 ? CommandStatus::SUCCESS : CommandStatus::OUT_OF_RANGE;
|
||||
inline CommandStatus mapDnp3IndexToStatus(uint16_t start, uint16_t stop, uint16_t offset, uint16_t dnp3_index)
|
||||
{
|
||||
return mapDnp3IndexToGlueIndex(start, stop, offset, dnp3_index) >= 0
|
||||
? CommandStatus::SUCCESS : CommandStatus::OUT_OF_RANGE;
|
||||
}
|
||||
|
||||
Dnp3Receiver::Dnp3Receiver(const Dnp3IndexedGroup& binary_commands, const Dnp3IndexedGroup& analog_commands) :
|
||||
|
@ -80,36 +78,47 @@ Dnp3Receiver::Dnp3Receiver(const Dnp3IndexedGroup& binary_commands, const Dnp3In
|
|||
{
|
||||
// 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 (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);
|
||||
if (analog_commands_cache != nullptr)
|
||||
{
|
||||
memset(analog_commands_cache, 0,
|
||||
sizeof(CacheItem<double>) * analog_commands.size);
|
||||
}
|
||||
}
|
||||
|
||||
Dnp3Receiver::~Dnp3Receiver() {
|
||||
if (binary_commands_cache) {
|
||||
Dnp3Receiver::~Dnp3Receiver()
|
||||
{
|
||||
if (binary_commands_cache)
|
||||
{
|
||||
delete[] binary_commands_cache;
|
||||
}
|
||||
if (analog_commands_cache) {
|
||||
if (analog_commands_cache)
|
||||
{
|
||||
delete[] analog_commands_cache;
|
||||
}
|
||||
}
|
||||
|
||||
/// CROB
|
||||
CommandStatus Dnp3Receiver::Select(const ControlRelayOutputBlock& command, uint16_t index) {
|
||||
CommandStatus Dnp3Receiver::Select(const ControlRelayOutputBlock& command, uint16_t index)
|
||||
{
|
||||
spdlog::trace("DNP3 select CROB index");
|
||||
return mapIsValidDnp3Index(index, binary_commands);
|
||||
}
|
||||
|
||||
CommandStatus Dnp3Receiver::Operate(const ControlRelayOutputBlock& command, uint16_t index, OperateType opType) {
|
||||
CommandStatus Dnp3Receiver::Operate(const ControlRelayOutputBlock& command, uint16_t index, OperateType opType)
|
||||
{
|
||||
auto code = command.functionCode;
|
||||
|
||||
if (!isValidDnp3Index(index, binary_commands)) {
|
||||
if (!isValidDnp3Index(index, binary_commands))
|
||||
{
|
||||
return CommandStatus::OUT_OF_RANGE;
|
||||
}
|
||||
if (code != ControlCode::LATCH_ON && code != ControlCode::LATCH_OFF) {
|
||||
if (code != ControlCode::LATCH_ON && code != ControlCode::LATCH_OFF)
|
||||
{
|
||||
return CommandStatus::NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
|
@ -121,45 +130,53 @@ CommandStatus Dnp3Receiver::Operate(const ControlRelayOutputBlock& command, uint
|
|||
}
|
||||
|
||||
// AnalogOut 16 (Int)
|
||||
CommandStatus Dnp3Receiver::Select(const AnalogOutputInt16& command, uint16_t index) {
|
||||
CommandStatus Dnp3Receiver::Select(const AnalogOutputInt16& command, uint16_t index)
|
||||
{
|
||||
spdlog::trace("DNP3 select AO int16 point status");
|
||||
return mapIsValidDnp3Index(index, analog_commands);
|
||||
}
|
||||
|
||||
CommandStatus Dnp3Receiver::Operate(const AnalogOutputInt16& command, uint16_t index, OperateType opType) {
|
||||
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->CacheUpdatedValue<int16_t>(command.value, index);
|
||||
}
|
||||
|
||||
// AnalogOut 32 (Int)
|
||||
CommandStatus Dnp3Receiver::Select(const AnalogOutputInt32& command, uint16_t index) {
|
||||
CommandStatus Dnp3Receiver::Select(const AnalogOutputInt32& command, uint16_t index)
|
||||
{
|
||||
spdlog::trace("DNP3 select AO int32 point status");
|
||||
return mapIsValidDnp3Index(index, analog_commands);
|
||||
}
|
||||
|
||||
CommandStatus Dnp3Receiver::Operate(const AnalogOutputInt32& command, uint16_t index, OperateType opType) {
|
||||
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->CacheUpdatedValue<int32_t>(command.value, index);
|
||||
}
|
||||
|
||||
// AnalogOut 32 (Float)
|
||||
CommandStatus Dnp3Receiver::Select(const AnalogOutputFloat32& command, uint16_t index) {
|
||||
CommandStatus Dnp3Receiver::Select(const AnalogOutputFloat32& command, uint16_t index)
|
||||
{
|
||||
spdlog::trace("DNP3 select AO float32 point status");
|
||||
return mapIsValidDnp3Index(index, analog_commands);
|
||||
}
|
||||
|
||||
CommandStatus Dnp3Receiver::Operate(const AnalogOutputFloat32& command, uint16_t index, OperateType opType) {
|
||||
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->CacheUpdatedValue<float>(command.value, index);
|
||||
}
|
||||
|
||||
// AnalogOut 64
|
||||
CommandStatus Dnp3Receiver::Select(const AnalogOutputDouble64& command, uint16_t index) {
|
||||
CommandStatus Dnp3Receiver::Select(const AnalogOutputDouble64& command, uint16_t index)
|
||||
{
|
||||
spdlog::trace("DNP3 select AO double64 point status");
|
||||
return mapIsValidDnp3Index(index, analog_commands);
|
||||
}
|
||||
|
||||
CommandStatus Dnp3Receiver::Operate(const AnalogOutputDouble64& command, uint16_t index, OperateType opType) {
|
||||
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->CacheUpdatedValue<double>(command.value, index);
|
||||
}
|
||||
|
@ -172,8 +189,10 @@ CommandStatus Dnp3Receiver::Operate(const AnalogOutputDouble64& command, uint16_
|
|||
/// @param value The value received over DNP3.
|
||||
/// @param dnp3_index The index of the value.
|
||||
template<class T>
|
||||
CommandStatus Dnp3Receiver::CacheUpdatedValue(T value, uint16_t dnp3_index) {
|
||||
if (!isValidDnp3Index(dnp3_index, analog_commands)) {
|
||||
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;
|
||||
}
|
||||
|
@ -187,15 +206,18 @@ CommandStatus Dnp3Receiver::CacheUpdatedValue(T value, uint16_t dnp3_index) {
|
|||
|
||||
/// @brief Write the point values into the glue variables for each value that
|
||||
/// was received.
|
||||
void Dnp3Receiver::ExchangeGlue() {
|
||||
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) {
|
||||
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) {
|
||||
if (!variable)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -203,18 +225,21 @@ void Dnp3Receiver::ExchangeGlue() {
|
|||
}
|
||||
}
|
||||
|
||||
for (uint16_t data_index(0); data_index < analog_commands.size; ++data_index) {
|
||||
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) {
|
||||
if (!variable)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
double value = analog_commands_cache[data_index].value;
|
||||
void* value_container = variable->value;
|
||||
switch (variable->type) {
|
||||
switch (variable->type)
|
||||
{
|
||||
case IECVT_BYTE:
|
||||
{
|
||||
*(reinterpret_cast<IEC_BYTE*>(value_container)) = static_cast<IEC_BYTE>(value);
|
||||
|
@ -290,11 +315,13 @@ void Dnp3Receiver::ExchangeGlue() {
|
|||
}
|
||||
}
|
||||
|
||||
void Dnp3Receiver::Start() {
|
||||
void Dnp3Receiver::Start()
|
||||
{
|
||||
spdlog::trace("DNP3 receiver started");
|
||||
}
|
||||
|
||||
void Dnp3Receiver::End() {
|
||||
void Dnp3Receiver::End()
|
||||
{
|
||||
spdlog::trace("DNP3 receiver stopped");
|
||||
}
|
||||
|
||||
|
|
|
@ -25,11 +25,10 @@
|
|||
* @{
|
||||
*/
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
/// \brief The receiver defines the interface between the DNP3 channel
|
||||
/// @brief The receiver defines the interface between the DNP3 channel
|
||||
/// and the glue arrays that are written to from DNP3.
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
class Dnp3Receiver : public opendnp3::ICommandHandler {
|
||||
class Dnp3Receiver : public opendnp3::ICommandHandler
|
||||
{
|
||||
public:
|
||||
/// 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.
|
||||
|
@ -68,7 +67,8 @@ class Dnp3Receiver : public opendnp3::ICommandHandler {
|
|||
opendnp3::CommandStatus CacheUpdatedValue(T value, std::uint16_t dnp3_index);
|
||||
|
||||
template <typename T>
|
||||
struct CacheItem {
|
||||
struct CacheItem
|
||||
{
|
||||
volatile bool has_value;
|
||||
volatile T value;
|
||||
};
|
||||
|
|
|
@ -32,11 +32,14 @@ using namespace std;
|
|||
const GlueVariable* GlueVariablesBinding::find(IecLocationDirection dir,
|
||||
IecLocationSize size,
|
||||
uint16_t msi,
|
||||
uint8_t lsi) const {
|
||||
for (uint16_t i = 0; i < this->size; ++i) {
|
||||
uint8_t lsi) const
|
||||
{
|
||||
for (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) {
|
||||
&& cur_var.msi == msi && cur_var.lsi == lsi)
|
||||
{
|
||||
return &glue_variables[i];
|
||||
}
|
||||
}
|
||||
|
@ -44,13 +47,16 @@ const GlueVariable* GlueVariablesBinding::find(IecLocationDirection dir,
|
|||
return nullptr;
|
||||
}
|
||||
|
||||
const GlueVariable* GlueVariablesBinding::find(const string& location) const {
|
||||
if (location.length() < 4 || location[0] != '%') {
|
||||
const GlueVariable* GlueVariablesBinding::find(const string& location) const
|
||||
{
|
||||
if (location.length() < 4 || location[0] != '%')
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
IecLocationDirection direction;
|
||||
switch (location[1]) {
|
||||
switch (location[1])
|
||||
{
|
||||
case 'I':
|
||||
direction = IECLDT_IN;
|
||||
break;
|
||||
|
@ -65,7 +71,8 @@ const GlueVariable* GlueVariablesBinding::find(const string& location) const {
|
|||
}
|
||||
|
||||
IecLocationSize size;
|
||||
switch (location[2]) {
|
||||
switch (location[2])
|
||||
{
|
||||
case 'X':
|
||||
size = IECLST_BIT;
|
||||
break;
|
||||
|
@ -90,7 +97,8 @@ const GlueVariable* GlueVariablesBinding::find(const string& location) const {
|
|||
|
||||
// Do we have more characters left in the string to read for lsi?
|
||||
size_t start_lsi = end_msi + 1 - location.c_str();
|
||||
if (start_lsi >= location.length()) {
|
||||
if (start_lsi >= location.length())
|
||||
{
|
||||
find(direction, size, msi, 0);
|
||||
}
|
||||
|
||||
|
@ -101,12 +109,15 @@ const GlueVariable* GlueVariablesBinding::find(const string& location) const {
|
|||
}
|
||||
|
||||
int32_t GlueVariablesBinding::find_max_msi(IecGlueValueType type,
|
||||
IecLocationDirection dir) const {
|
||||
IecLocationDirection dir) const
|
||||
{
|
||||
int32_t max_index(-1);
|
||||
|
||||
const GlueVariable* glue_variables = this->glue_variables;
|
||||
for (size_t index = 0; index < this->size; ++index) {
|
||||
if (type == glue_variables[index].type && dir == glue_variables[index].dir) {
|
||||
for (size_t index = 0; index < this->size; ++index)
|
||||
{
|
||||
if (type == glue_variables[index].type && dir == glue_variables[index].dir)
|
||||
{
|
||||
max_index = max(max_index, static_cast<int32_t>(glue_variables[index].msi));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,8 +33,9 @@
|
|||
|
||||
#ifndef OPLC_IEC_GLUE_DIRECTION
|
||||
#define OPLC_IEC_GLUE_DIRECTION
|
||||
enum IecLocationDirection {
|
||||
IECLDT_IN,
|
||||
enum IecLocationDirection
|
||||
{
|
||||
IECLDT_IN = 0,
|
||||
IECLDT_OUT,
|
||||
IECLDT_MEM,
|
||||
};
|
||||
|
@ -42,9 +43,10 @@ enum IecLocationDirection {
|
|||
|
||||
#ifndef OPLC_IEC_GLUE_SIZE
|
||||
#define OPLC_IEC_GLUE_SIZE
|
||||
enum IecLocationSize {
|
||||
enum IecLocationSize
|
||||
{
|
||||
/// Variables that are a single bit
|
||||
IECLST_BIT,
|
||||
IECLST_BIT = 0,
|
||||
/// Variables that are 1 byte
|
||||
IECLST_BYTE,
|
||||
/// Variables that are 2 bytes
|
||||
|
@ -64,8 +66,9 @@ enum IecLocationSize {
|
|||
/// by the /// Defines the type of a glue variable (so that we can read and
|
||||
/// write). This definition must be consistent with what is produced
|
||||
/// by the @ref glue_generator.
|
||||
enum IecGlueValueType {
|
||||
IECVT_BOOL,
|
||||
enum IecGlueValueType
|
||||
{
|
||||
IECVT_BOOL = 0,
|
||||
IECVT_BYTE,
|
||||
IECVT_SINT,
|
||||
IECVT_USINT,
|
||||
|
@ -94,7 +97,8 @@ enum IecGlueValueType {
|
|||
///
|
||||
/// This definition must be consistent with what is produced by the @ref
|
||||
/// glue_generator.
|
||||
struct GlueBoolGroup {
|
||||
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.
|
||||
|
@ -118,7 +122,8 @@ struct GlueBoolGroup {
|
|||
///
|
||||
/// This definition must be consistent with what is produced by the
|
||||
/// @ref glue_generator.
|
||||
struct GlueVariable {
|
||||
struct GlueVariable
|
||||
{
|
||||
/// 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.
|
||||
|
@ -142,7 +147,8 @@ struct GlueVariable {
|
|||
/// This structure wraps up items that are available as globals, but this
|
||||
/// allows a straightforward way to inject definitions into tests, so it is
|
||||
/// preferred to use this structure rather than globals.
|
||||
class GlueVariablesBinding {
|
||||
class GlueVariablesBinding
|
||||
{
|
||||
public:
|
||||
/// Initialize a new instance of the glue bindings.
|
||||
/// @param buffer_lock A lock for accessing the value part of bindings
|
||||
|
|
|
@ -24,13 +24,15 @@
|
|||
#include <istream>
|
||||
#include <memory>
|
||||
|
||||
namespace oplc {
|
||||
namespace oplc
|
||||
{
|
||||
|
||||
/// 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 converted value.
|
||||
inline bool ini_atob(const char* value) {
|
||||
inline bool ini_atob(const char* value)
|
||||
{
|
||||
return strcmp("true", value) == 0;
|
||||
}
|
||||
|
||||
|
@ -43,7 +45,8 @@ inline bool ini_atob(const char* value) {
|
|||
inline bool ini_matches(const char* section_expected,
|
||||
const char* value_expected,
|
||||
const char* section,
|
||||
const char* value) {
|
||||
const char* value)
|
||||
{
|
||||
return strcmp(section_expected, section) == 0
|
||||
&& strcmp(value_expected, value) == 0;
|
||||
}
|
||||
|
@ -53,9 +56,11 @@ inline bool ini_matches(const char* section_expected,
|
|||
/// @param num Maximum number of characters to be copied into str.
|
||||
/// @param stream The stream object. The string must be null terminated.
|
||||
/// @return the string or null if cannot read more.
|
||||
static char* istream_fgets(char* str, int num, void* stream) {
|
||||
static char* istream_fgets(char* str, int num, void* stream)
|
||||
{
|
||||
auto st = reinterpret_cast<std::istream*>(stream);
|
||||
if (!st->good() || st->eof()) {
|
||||
if (!st->good() || st->eof())
|
||||
{
|
||||
// We previously reached the end of the file, so return the end signal.
|
||||
return nullptr;
|
||||
}
|
||||
|
@ -69,13 +74,15 @@ typedef std::unique_ptr<std::istream, std::function<void(std::istream*)>> config
|
|||
|
||||
/// Open the standard configuration file as an closable stream.
|
||||
/// @return A stream for the configuration file.
|
||||
inline config_stream open_config() {
|
||||
inline config_stream open_config()
|
||||
{
|
||||
return config_stream(
|
||||
new std::ifstream("../etc/config.ini"),
|
||||
[] (std::istream* s) {
|
||||
reinterpret_cast<std::ifstream*>(s)->close();
|
||||
delete s;
|
||||
}
|
||||
[] (std::istream* s)
|
||||
{
|
||||
reinterpret_cast<std::ifstream*>(s)->close();
|
||||
delete s;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -69,27 +69,32 @@ using namespace std;
|
|||
|
||||
/// @brief Start the Enip Thread
|
||||
/// @param *arg
|
||||
void *enipThread(void *arg) {
|
||||
void *enipThread(void *arg)
|
||||
{
|
||||
startServer(enip_port, run_enip, &processEnipMessage, nullptr);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
/// @brief Read the argument from a command function
|
||||
/// @param *command
|
||||
int readCommandArgument(const char *command) {
|
||||
int readCommandArgument(const char *command)
|
||||
{
|
||||
int i = 0;
|
||||
int j = 0;
|
||||
char argument[BUFFER_MAX_SIZE];
|
||||
|
||||
while (command[i] != '(' && command[i] != '\0') {
|
||||
while (command[i] != '(' && command[i] != '\0')
|
||||
{
|
||||
i++;
|
||||
}
|
||||
|
||||
if (command[i] == '(') {
|
||||
if (command[i] == '(')
|
||||
{
|
||||
i++;
|
||||
}
|
||||
|
||||
while (command[i] != ')' && command[i] != '\0') {
|
||||
while (command[i] != ')' && command[i] != '\0')
|
||||
{
|
||||
argument[j] = command[i];
|
||||
i++;
|
||||
j++;
|
||||
|
@ -110,13 +115,15 @@ std::int8_t copy_command_config(const char *source, char target[],
|
|||
size_t target_size) {
|
||||
// Search through source until we find the closing ")"
|
||||
size_t end_index = 0;
|
||||
while (source[end_index] != ')' && source[end_index] != '\0') {
|
||||
while (source[end_index] != ')' && source[end_index] != '\0')
|
||||
{
|
||||
end_index++;
|
||||
}
|
||||
|
||||
// If the size is such that we would not be able to assign the
|
||||
// terminating null, then return an error.
|
||||
if (end_index >= (target_size - 1)) {
|
||||
if (end_index >= (target_size - 1))
|
||||
{
|
||||
target[0] = '\0';
|
||||
return -1;
|
||||
}
|
||||
|
@ -138,13 +145,15 @@ std::int8_t copy_command_config(const char *source, char target[],
|
|||
/// @param port
|
||||
/// @return the file descriptor for the socket, or less than 0 if a socket
|
||||
/// if an error occurred.
|
||||
int interactive_open_socket(uint16_t port) {
|
||||
int interactive_open_socket(uint16_t port)
|
||||
{
|
||||
int socket_fd;
|
||||
struct sockaddr_in server_addr;
|
||||
|
||||
// Create TCP Socket
|
||||
socket_fd = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (socket_fd < 0) {
|
||||
if (socket_fd < 0)
|
||||
{
|
||||
spdlog::error("Interactive Server: error creating stream socket => {}",
|
||||
strerror(errno));
|
||||
return -1;
|
||||
|
@ -153,7 +162,8 @@ int interactive_open_socket(uint16_t port) {
|
|||
// Set SO_REUSEADDR
|
||||
int enable = 1;
|
||||
if (setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR,
|
||||
&enable, sizeof(int)) < 0) {
|
||||
&enable, sizeof(int)) < 0)
|
||||
{
|
||||
perror("setsockopt(SO_REUSEADDR) failed");
|
||||
}
|
||||
|
||||
|
@ -167,7 +177,8 @@ int interactive_open_socket(uint16_t port) {
|
|||
|
||||
// Bind socket
|
||||
if (bind(socket_fd, (struct sockaddr *)&server_addr,
|
||||
sizeof(server_addr)) < 0) {
|
||||
sizeof(server_addr)) < 0)
|
||||
{
|
||||
spdlog::error("Interactive Server: error binding socket => {}",
|
||||
strerror(errno));
|
||||
return -2;
|
||||
|
@ -187,7 +198,8 @@ int interactive_open_socket(uint16_t port) {
|
|||
/// @param run A flag that is set to false when we should stop polling.
|
||||
/// @param socket_fd The socket file descriptor we are listening on.
|
||||
/// @return the client file descriptor.
|
||||
int interactive_wait_new_client(volatile bool& run, int socket_fd) {
|
||||
int interactive_wait_new_client(volatile bool& run, int socket_fd)
|
||||
{
|
||||
int client_fd;
|
||||
struct sockaddr_in client_addr;
|
||||
socklen_t client_len;
|
||||
|
@ -195,12 +207,14 @@ int interactive_wait_new_client(volatile bool& run, int socket_fd) {
|
|||
spdlog::trace("Interactive Server: waiting for new client...");
|
||||
|
||||
client_len = sizeof(client_addr);
|
||||
while (run) {
|
||||
while (run)
|
||||
{
|
||||
// Non-blocking call
|
||||
client_fd = accept(socket_fd, (struct sockaddr *)&client_addr,
|
||||
&client_len);
|
||||
|
||||
if (client_fd > 0) {
|
||||
if (client_fd > 0)
|
||||
{
|
||||
SetSocketBlockingEnabled(client_fd, true);
|
||||
break;
|
||||
}
|
||||
|
@ -214,12 +228,14 @@ int interactive_wait_new_client(volatile bool& run, int socket_fd) {
|
|||
/// Handle a single command from the client.
|
||||
/// @param command The command as a text string.
|
||||
/// @param client_fd The file descriptor to write a response to.
|
||||
void interactive_client_command(const char* command, int client_fd) {
|
||||
void interactive_client_command(const char* command, int client_fd)
|
||||
{
|
||||
// A buffer for the command configuration information.
|
||||
char command_buffer[BUFFER_MAX_SIZE];
|
||||
|
||||
std::unique_lock<std::mutex> lock(command_mutex, std::defer_lock);
|
||||
if (!lock.try_lock()) {
|
||||
unique_lock<mutex> lock(command_mutex, defer_lock);
|
||||
if (!lock.try_lock())
|
||||
{
|
||||
spdlog::trace("Command skipped - already processing {}", command);
|
||||
int count_char = sprintf(command_buffer, "Another command in progress...\n");
|
||||
write(client_fd, command_buffer, count_char);
|
||||
|
@ -228,30 +244,37 @@ void interactive_client_command(const char* command, int client_fd) {
|
|||
|
||||
spdlog::trace("Process command received {}", command);
|
||||
|
||||
if (strncmp(command, "quit()", 6) == 0) {
|
||||
if (strncmp(command, "quit()", 6) == 0)
|
||||
{
|
||||
spdlog::info("Issued quit() command");
|
||||
run_openplc = 0;
|
||||
}
|
||||
else if (strncmp(command, "start_modbus(", 13) == 0) {
|
||||
else if (strncmp(command, "start_modbus(", 13) == 0)
|
||||
{
|
||||
ServiceDefinition* def = services_find("modbusslave");
|
||||
if (def && copy_command_config(command + 13, command_buffer, BUFFER_MAX_SIZE) == 0) {
|
||||
if (def && copy_command_config(command + 13, command_buffer, BUFFER_MAX_SIZE) == 0)
|
||||
{
|
||||
def->start(command_buffer);
|
||||
}
|
||||
}
|
||||
else if (strncmp(command, "stop_modbus()", 13) == 0) {
|
||||
else if (strncmp(command, "stop_modbus()", 13) == 0)
|
||||
{
|
||||
ServiceDefinition* def = services_find("modbusslave");
|
||||
if (def) {
|
||||
def->stop();
|
||||
}
|
||||
}
|
||||
#ifdef OPLC_DNP3_OUTSTATION
|
||||
else if (strncmp(command, "start_dnp3(", 11) == 0) {
|
||||
else if (strncmp(command, "start_dnp3(", 11) == 0)
|
||||
{
|
||||
ServiceDefinition* def = services_find("dnp3s");
|
||||
if (def && copy_command_config(command + 11, command_buffer, BUFFER_MAX_SIZE) == 0) {
|
||||
if (def && copy_command_config(command + 11, command_buffer, BUFFER_MAX_SIZE) == 0)
|
||||
{
|
||||
def->start(command_buffer);
|
||||
}
|
||||
}
|
||||
else if (strncmp(command, "stop_dnp3()", 11) == 0) {
|
||||
else if (strncmp(command, "stop_dnp3()", 11) == 0)
|
||||
{
|
||||
ServiceDefinition* def = services_find("dnp3s");
|
||||
if (def) {
|
||||
def->stop();
|
||||
|
@ -261,7 +284,8 @@ void interactive_client_command(const char* command, int client_fd) {
|
|||
else if (strncmp(command, "start_enip(", 11) == 0) {
|
||||
spdlog::info("Issued start_enip() command to start on port: {}", readCommandArgument(command));
|
||||
enip_port = readCommandArgument(command);
|
||||
if (run_enip) {
|
||||
if (run_enip)
|
||||
{
|
||||
spdlog::info("EtherNet/IP server already active. Restarting on port: {}", enip_port);
|
||||
//Stop Enip server
|
||||
run_enip = 0;
|
||||
|
@ -272,7 +296,8 @@ void interactive_client_command(const char* command, int client_fd) {
|
|||
run_enip = 1;
|
||||
pthread_create(&enip_thread, NULL, enipThread, NULL);
|
||||
}
|
||||
else if (strncmp(command, "stop_enip()", 11) == 0) {
|
||||
else if (strncmp(command, "stop_enip()", 11) == 0)
|
||||
{
|
||||
spdlog::info("Issued stop_enip() command");
|
||||
if (run_enip)
|
||||
{
|
||||
|
@ -281,9 +306,11 @@ void interactive_client_command(const char* command, int client_fd) {
|
|||
spdlog::info("EtherNet/IP server was stopped");
|
||||
}
|
||||
}
|
||||
else if (strncmp(command, "start_pstorage(", 15) == 0) {
|
||||
else if (strncmp(command, "start_pstorage(", 15) == 0)
|
||||
{
|
||||
ServiceDefinition* def = services_find("pstorage");
|
||||
if (def && copy_command_config(command + 15, command_buffer, BUFFER_MAX_SIZE) == 0) {
|
||||
if (def && copy_command_config(command + 15, command_buffer, BUFFER_MAX_SIZE) == 0)
|
||||
{
|
||||
def->start(command_buffer);
|
||||
}
|
||||
}
|
||||
|
@ -293,20 +320,23 @@ void interactive_client_command(const char* command, int client_fd) {
|
|||
def->stop();
|
||||
}
|
||||
}
|
||||
else if (strncmp(command, "runtime_logs()", 14) == 0) {
|
||||
else if (strncmp(command, "runtime_logs()", 14) == 0)
|
||||
{
|
||||
spdlog::debug("Issued runtime_logs() command");
|
||||
std::string data = log_sink->data();
|
||||
write(client_fd, data.c_str(), data.size());
|
||||
return;
|
||||
}
|
||||
else if (strncmp(command, "exec_time()", 11) == 0) {
|
||||
else if (strncmp(command, "exec_time()", 11) == 0)
|
||||
{
|
||||
time_t end_time;
|
||||
time(&end_time);
|
||||
int count_char = sprintf(command_buffer, "%llu\n", (unsigned long long)difftime(end_time, start_time));
|
||||
write(client_fd, command_buffer, count_char);
|
||||
return;
|
||||
}
|
||||
else {
|
||||
else
|
||||
{
|
||||
int count_char = sprintf(command_buffer, "Error: unrecognized command\n");
|
||||
write(client_fd, command_buffer, count_char);
|
||||
return;
|
||||
|
@ -317,7 +347,8 @@ void interactive_client_command(const char* command, int client_fd) {
|
|||
}
|
||||
|
||||
/// Defines the arguments that client threads receive.
|
||||
struct ClientArgs {
|
||||
struct ClientArgs
|
||||
{
|
||||
/// The client file descriptor for reading and writing.
|
||||
int client_fd;
|
||||
/// A flag that can indicate termination of the thread.
|
||||
|
@ -328,22 +359,26 @@ struct ClientArgs {
|
|||
/// @param arguments The arguments to the thread - must be the client arguments
|
||||
/// struture. This thread will be responsible for freeing the arguments.
|
||||
/// @return always nullptr.
|
||||
void* interactive_client_run(void* arguments) {
|
||||
void* interactive_client_run(void* arguments)
|
||||
{
|
||||
auto client_args = reinterpret_cast<ClientArgs*>(arguments);
|
||||
|
||||
char buffer[BUFFER_MAX_SIZE];
|
||||
int message_size;
|
||||
|
||||
while (*client_args->run) {
|
||||
while (*client_args->run)
|
||||
{
|
||||
memset(buffer, 0, BUFFER_MAX_SIZE);
|
||||
message_size = read(client_args->client_fd, buffer, BUFFER_MAX_SIZE);
|
||||
|
||||
if (message_size <= 0) {
|
||||
if (message_size <= 0)
|
||||
{
|
||||
spdlog::trace("Interactive Server: client ID: {} has closed the connection", client_args->client_fd);
|
||||
break;
|
||||
}
|
||||
|
||||
if (message_size > BUFFER_MAX_SIZE) {
|
||||
if (message_size > BUFFER_MAX_SIZE)
|
||||
{
|
||||
spdlog::error("Interactive Server: Message size is too large for client {}", client_args->client_fd);
|
||||
break;
|
||||
}
|
||||
|
@ -352,8 +387,10 @@ void* interactive_client_run(void* arguments) {
|
|||
char* start = buffer;
|
||||
uint16_t cur_pos = 0;
|
||||
|
||||
while (cur_pos < BUFFER_MAX_SIZE && buffer[cur_pos] != '\0') {
|
||||
if (buffer[cur_pos] == '\n' || buffer[cur_pos] == '\r') {
|
||||
while (cur_pos < BUFFER_MAX_SIZE && buffer[cur_pos] != '\0')
|
||||
{
|
||||
if (buffer[cur_pos] == '\n' || buffer[cur_pos] == '\r')
|
||||
{
|
||||
// Terminate the command
|
||||
buffer[cur_pos] = '\0';
|
||||
interactive_client_command(start, client_args->client_fd);
|
||||
|
@ -376,16 +413,19 @@ void* interactive_client_run(void* arguments) {
|
|||
void interactive_run(oplc::config_stream& cfg_stream,
|
||||
const char* cfg_overrides,
|
||||
const GlueVariablesBinding& bindings,
|
||||
volatile bool& run) {
|
||||
volatile bool& run)
|
||||
{
|
||||
const uint16_t port = 43628;
|
||||
int socket_fd = interactive_open_socket(port);
|
||||
|
||||
// Listen for new connections to our socket. When we have a new
|
||||
// connection, we spawn a new thread to handle that connection.
|
||||
while (run) {
|
||||
while (run)
|
||||
{
|
||||
int client_fd = interactive_wait_new_client(run, socket_fd);
|
||||
|
||||
if (client_fd < 0) {
|
||||
if (client_fd < 0)
|
||||
{
|
||||
spdlog::error("Interactive Server: Error accepting client!");
|
||||
continue;
|
||||
}
|
||||
|
@ -395,9 +435,12 @@ void interactive_run(oplc::config_stream& cfg_stream,
|
|||
|
||||
spdlog::trace("Interactive Server: Client accepted! Creating thread for the new client ID: {}", client_fd);
|
||||
int ret = pthread_create(&thread, NULL, interactive_client_run, args);
|
||||
if (ret == 0) {
|
||||
if (ret == 0)
|
||||
{
|
||||
pthread_detach(thread);
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
delete args;
|
||||
}
|
||||
}
|
||||
|
@ -406,12 +449,14 @@ void interactive_run(oplc::config_stream& cfg_stream,
|
|||
}
|
||||
|
||||
void interactive_service_run(const GlueVariablesBinding& binding,
|
||||
volatile bool& run, const char* config) {
|
||||
volatile bool& run, const char* config)
|
||||
{
|
||||
auto cfg_stream = oplc::open_config();
|
||||
interactive_run(cfg_stream, config, binding, run);
|
||||
}
|
||||
|
||||
void initialize_logging(int argc, char **argv) {
|
||||
void initialize_logging(int argc, char **argv)
|
||||
{
|
||||
log_sink.reset(new buffered_sink(log_buffer, LOG_BUFFER_SIZE));
|
||||
spdlog::default_logger()->sinks().push_back(log_sink);
|
||||
}
|
||||
|
|
|
@ -33,7 +33,8 @@
|
|||
/// use to query logs in memory so that they can be provided to a front
|
||||
/// end in an efficient (but not necessarily complete) manner.
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
class buffered_sink : public spdlog::sinks::base_sink<std::mutex> {
|
||||
class buffered_sink : public spdlog::sinks::base_sink<std::mutex>
|
||||
{
|
||||
public:
|
||||
/// \brief Initialize a new instance of the sink with the provided buffer.
|
||||
///
|
||||
|
@ -50,12 +51,14 @@ class buffered_sink : public spdlog::sinks::base_sink<std::mutex> {
|
|||
|
||||
/// \brief Gets the data from this sink.
|
||||
/// \return The data formatted as a string.
|
||||
std::string data() {
|
||||
std::string data()
|
||||
{
|
||||
return std::string(reinterpret_cast<char*>(this->buffer));
|
||||
}
|
||||
|
||||
protected:
|
||||
void sink_it_(const spdlog::details::log_msg& msg) override {
|
||||
void sink_it_(const spdlog::details::log_msg& msg) override
|
||||
{
|
||||
fmt::memory_buffer formatted;
|
||||
sink::formatter_->format(msg, formatted);
|
||||
|
||||
|
@ -63,7 +66,8 @@ class buffered_sink : public spdlog::sinks::base_sink<std::mutex> {
|
|||
|
||||
// Where will our message end?
|
||||
std::size_t next_end_pos = end_pos + msg_size;
|
||||
if (next_end_pos > buffer_size - 1) {
|
||||
if (next_end_pos > buffer_size - 1)
|
||||
{
|
||||
// If adding this message would put us past the end
|
||||
// of the buffer, then start back at the beginning.
|
||||
end_pos = 0;
|
||||
|
|
|
@ -55,9 +55,11 @@ uint8_t run_openplc = 1; // Variable to control OpenPLC Runtime execution
|
|||
/// \param ts
|
||||
/// \param delay in milliseconds
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
void sleep_until(struct timespec *ts, int delay) {
|
||||
void sleep_until(struct timespec *ts, int delay)
|
||||
{
|
||||
ts->tv_nsec += delay;
|
||||
if (ts->tv_nsec >= 1000*1000*1000) {
|
||||
if (ts->tv_nsec >= 1000*1000*1000)
|
||||
{
|
||||
ts->tv_nsec -= 1000*1000*1000;
|
||||
ts->tv_sec++;
|
||||
}
|
||||
|
@ -71,9 +73,12 @@ void sleep_until(struct timespec *ts, int delay) {
|
|||
/// \param
|
||||
/// \return
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
bool pinNotPresent(int *ignored_vector, int vector_size, int pinNumber) {
|
||||
for (int i = 0; i < vector_size; i++) {
|
||||
if (ignored_vector[i] == pinNumber) {
|
||||
bool pinNotPresent(int *ignored_vector, int vector_size, int pinNumber)
|
||||
{
|
||||
for (int i = 0; i < vector_size; i++)
|
||||
{
|
||||
if (ignored_vector[i] == pinNumber)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -84,21 +89,26 @@ bool pinNotPresent(int *ignored_vector, int vector_size, int pinNumber) {
|
|||
////////////////////////////////////////////////////////////////////////////////
|
||||
/// \brief Disables all outputs
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
void disableOutputs() {
|
||||
void disableOutputs()
|
||||
{
|
||||
// Disable digital outputs
|
||||
for (int i = 0; i < BUFFER_SIZE; i++) {
|
||||
for (int j = 0; j < 8; j++) {
|
||||
for (int i = 0; i < BUFFER_SIZE; i++)
|
||||
{
|
||||
for (int j = 0; j < 8; j++)
|
||||
{
|
||||
if (bool_output[i][j] != NULL) *bool_output[i][j] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Disable byte outputs
|
||||
for (int i = 0; i < BUFFER_SIZE; i++) {
|
||||
for (int i = 0; i < BUFFER_SIZE; i++)
|
||||
{
|
||||
if (byte_output[i] != NULL) *byte_output[i] = 0;
|
||||
}
|
||||
|
||||
// Disable analog outputs
|
||||
for (int i = 0; i < BUFFER_SIZE; i++) {
|
||||
for (int i = 0; i < BUFFER_SIZE; i++)
|
||||
{
|
||||
if (int_output[i] != NULL) *int_output[i] = 0;
|
||||
}
|
||||
}
|
||||
|
@ -106,7 +116,8 @@ void disableOutputs() {
|
|||
////////////////////////////////////////////////////////////////////////////////
|
||||
/// \brief Special Functions
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
void handleSpecialFunctions() {
|
||||
void handleSpecialFunctions()
|
||||
{
|
||||
// Current time [%ML1024]
|
||||
struct tm *current_time;
|
||||
time_t rawtime;
|
||||
|
@ -116,24 +127,28 @@ void handleSpecialFunctions() {
|
|||
current_time = localtime(&rawtime);
|
||||
|
||||
rawtime = rawtime - timezone;
|
||||
if (current_time->tm_isdst > 0) {
|
||||
if (current_time->tm_isdst > 0)
|
||||
{
|
||||
rawtime = rawtime + 3600;
|
||||
}
|
||||
|
||||
if (special_functions[0] != NULL) {
|
||||
if (special_functions[0] != NULL)
|
||||
{
|
||||
*special_functions[0] = rawtime;
|
||||
}
|
||||
|
||||
// Number of cycles [%ML1025]
|
||||
cycle_counter++;
|
||||
if (special_functions[1] != NULL) {
|
||||
if (special_functions[1] != NULL)
|
||||
{
|
||||
*special_functions[1] = cycle_counter;
|
||||
}
|
||||
|
||||
// Insert other special functions below
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
initialize_logging(argc, argv);
|
||||
spdlog::info("OpenPLC Runtime starting...");
|
||||
|
||||
|
@ -152,7 +167,8 @@ int main(int argc, char **argv) {
|
|||
//======================================================
|
||||
// MAIN LOOP
|
||||
//======================================================
|
||||
while (run_openplc) {
|
||||
while (run_openplc)
|
||||
{
|
||||
// Read input image - this method tries to get the lock
|
||||
// so don't put it in the lock context.
|
||||
updateBuffersIn();
|
||||
|
|
|
@ -53,11 +53,14 @@ const uint8_t BOOL_BIT_MASK[8] = {
|
|||
|
||||
inline void initialize_mapped_from_group(uint16_t msi,
|
||||
const GlueBoolGroup* group,
|
||||
vector<MappedBool>& buffer) {
|
||||
vector<MappedBool>& buffer)
|
||||
{
|
||||
auto start_index = msi * BOOL_PER_GROUP;
|
||||
for (size_t bool_index = 0; bool_index < BOOL_PER_GROUP; ++bool_index) {
|
||||
for (size_t bool_index = 0; bool_index < BOOL_PER_GROUP; ++bool_index)
|
||||
{
|
||||
auto mapped_index = start_index + bool_index;
|
||||
if (group->values[bool_index]) {
|
||||
if (group->values[bool_index])
|
||||
{
|
||||
buffer[mapped_index].value = group->values[bool_index];
|
||||
buffer[mapped_index].cached_value = *group->values[bool_index];
|
||||
}
|
||||
|
@ -80,12 +83,14 @@ IndexedStrategy::IndexedStrategy(const GlueVariablesBinding& bindings) :
|
|||
const int32_t max_coil_index = bindings.find_max_msi(IECVT_BOOL, IECLDT_OUT);
|
||||
const int32_t max_di_index = bindings.find_max_msi(IECVT_BOOL, IECLDT_IN);
|
||||
|
||||
if (max_coil_index >= 0) {
|
||||
if (max_coil_index >= 0)
|
||||
{
|
||||
coil_read_buffer.resize((max_coil_index + 1) * BOOL_PER_GROUP);
|
||||
coil_write_buffer.resize((max_coil_index + 1) * BOOL_PER_GROUP);
|
||||
}
|
||||
|
||||
if (max_di_index >= 0) {
|
||||
if (max_di_index >= 0)
|
||||
{
|
||||
di_read_buffer.resize((max_di_index + 1) * BOOL_PER_GROUP);
|
||||
}
|
||||
|
||||
|
@ -93,35 +98,48 @@ IndexedStrategy::IndexedStrategy(const GlueVariablesBinding& bindings) :
|
|||
// Note that if we have persistent storage, then the values have already
|
||||
// been initialized.
|
||||
const GlueVariable* glue_variables = bindings.glue_variables;
|
||||
for (size_t index = 0; index < bindings.size; ++index) {
|
||||
for (size_t index = 0; index < bindings.size; ++index)
|
||||
{
|
||||
IecGlueValueType type = glue_variables[index].type;
|
||||
IecLocationDirection dir = glue_variables[index].dir;
|
||||
uint16_t msi = glue_variables[index].msi;
|
||||
|
||||
if (type == IECVT_BOOL) {
|
||||
if (type == IECVT_BOOL)
|
||||
{
|
||||
const GlueBoolGroup* group = reinterpret_cast<const GlueBoolGroup*>(glue_variables[index].value);
|
||||
// The first coil index for this variable is always
|
||||
// multiplied by the number of booleans in that group
|
||||
// If this index is out of range, then skip it.
|
||||
if (dir == IECLDT_OUT) {
|
||||
if (dir == IECLDT_OUT)
|
||||
{
|
||||
initialize_mapped_from_group(msi, group, coil_read_buffer);
|
||||
} else if (dir == IECLDT_IN) {
|
||||
}
|
||||
else if (dir == IECLDT_IN)
|
||||
{
|
||||
initialize_mapped_from_group(msi, group, di_read_buffer);
|
||||
}
|
||||
}
|
||||
else if (type == IECVT_INT) {
|
||||
if (dir == IECLDT_OUT) {
|
||||
else if (type == IECVT_INT)
|
||||
{
|
||||
if (dir == IECLDT_OUT)
|
||||
{
|
||||
int_register_read_buffer[msi].init(reinterpret_cast<IEC_INT*>(glue_variables[index].value));
|
||||
} else if (dir == IECLDT_MEM && msi >= MIN_16B_RANGE && msi < MAX_16B_RANGE) {
|
||||
}
|
||||
else if (dir == IECLDT_MEM && msi >= MIN_16B_RANGE && msi < MAX_16B_RANGE)
|
||||
{
|
||||
intm_register_read_buffer[msi - MIN_16B_RANGE].init(reinterpret_cast<IEC_INT*>(glue_variables[index].value));
|
||||
} else if (dir == IECLDT_IN) {
|
||||
}
|
||||
else if (dir == IECLDT_IN)
|
||||
{
|
||||
int_input_read_buffer[msi].init(reinterpret_cast<IEC_INT*>(glue_variables[index].value));
|
||||
}
|
||||
}
|
||||
else if (type == IECVT_DINT && dir == IECLDT_MEM && msi <= MIN_32B_RANGE && msi < MAX_32B_RANGE) {
|
||||
else if (type == IECVT_DINT && dir == IECLDT_MEM && msi <= MIN_32B_RANGE && msi < MAX_32B_RANGE)
|
||||
{
|
||||
dintm_register_read_buffer[msi - MIN_32B_RANGE].init(reinterpret_cast<IEC_DINT*>(glue_variables[index].value));
|
||||
}
|
||||
else if (type == IECVT_LINT && dir == IECLDT_MEM && msi <= MIN_64B_RANGE && msi < MAX_64B_RANGE) {
|
||||
else if (type == IECVT_LINT && dir == IECLDT_MEM && msi <= MIN_64B_RANGE && msi < MAX_64B_RANGE)
|
||||
{
|
||||
lintm_register_read_buffer[msi - MIN_64B_RANGE].init(reinterpret_cast<IEC_LINT*>(glue_variables[index].value));
|
||||
}
|
||||
}
|
||||
|
@ -136,16 +154,20 @@ IndexedStrategy::IndexedStrategy(const GlueVariablesBinding& bindings) :
|
|||
/// and a local cache of the value.
|
||||
template <typename T>
|
||||
void exchange(array<PendingValue<T>, NUM_REGISTER_VALUES>& write_buffer,
|
||||
array<MappedValue<T>, NUM_REGISTER_VALUES>& read_buffer) {
|
||||
for (size_t index = 0; index < write_buffer.size(); ++index) {
|
||||
array<MappedValue<T>, NUM_REGISTER_VALUES>& read_buffer)
|
||||
{
|
||||
for (size_t index = 0; index < write_buffer.size(); ++index)
|
||||
{
|
||||
// Skip any index that is not mapped to a located variable.
|
||||
if (!read_buffer[index].value) {
|
||||
if (!read_buffer[index].value)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// If there was a write that hasn't been written, then transfer the
|
||||
// value to the read buffer.
|
||||
if (write_buffer[index].has_pending) {
|
||||
if (write_buffer[index].has_pending)
|
||||
{
|
||||
*read_buffer[index].value = write_buffer[index].value;
|
||||
write_buffer[index].has_pending = false;
|
||||
}
|
||||
|
@ -156,7 +178,8 @@ void exchange(array<PendingValue<T>, NUM_REGISTER_VALUES>& write_buffer,
|
|||
}
|
||||
}
|
||||
|
||||
void IndexedStrategy::Exchange() {
|
||||
void IndexedStrategy::Exchange()
|
||||
{
|
||||
lock_guard<mutex> guard(*this->glue_mutex);
|
||||
|
||||
// Since we already figured out the mapping in an efficient structure
|
||||
|
@ -166,8 +189,10 @@ void IndexedStrategy::Exchange() {
|
|||
// Update the read caches for coils and discrete inputs.
|
||||
// Only the coils can be set, so we only need to check for pending
|
||||
// writes to those.
|
||||
for (size_t index = 0; index < coil_write_buffer.size(); ++index) {
|
||||
if (coil_write_buffer[index].has_pending) {
|
||||
for (size_t index = 0; index < coil_write_buffer.size(); ++index)
|
||||
{
|
||||
if (coil_write_buffer[index].has_pending)
|
||||
{
|
||||
coil_write_buffer[index].has_pending = false;
|
||||
*coil_read_buffer[index].value = coil_write_buffer[index].value;
|
||||
}
|
||||
|
@ -175,7 +200,8 @@ void IndexedStrategy::Exchange() {
|
|||
}
|
||||
|
||||
// Update the boolean values.
|
||||
for (size_t index = 0; index < di_read_buffer.size(); ++index) {
|
||||
for (size_t index = 0; index < di_read_buffer.size(); ++index)
|
||||
{
|
||||
di_read_buffer[index].update_cache();
|
||||
}
|
||||
|
||||
|
@ -184,15 +210,18 @@ void IndexedStrategy::Exchange() {
|
|||
exchange(dintm_register_write_buffer, dintm_register_read_buffer);
|
||||
exchange(lintm_register_write_buffer, lintm_register_read_buffer);
|
||||
|
||||
for (size_t index = 0; index < int_input_read_buffer.size(); ++index) {
|
||||
for (size_t index = 0; index < int_input_read_buffer.size(); ++index)
|
||||
{
|
||||
int_input_read_buffer[index].update_cache();
|
||||
}
|
||||
}
|
||||
|
||||
modbus_errno IndexedStrategy::WriteCoil(uint16_t coil_index, bool value) {
|
||||
modbus_errno IndexedStrategy::WriteCoil(uint16_t coil_index, bool value)
|
||||
{
|
||||
lock_guard<mutex> guard(buffer_mutex);
|
||||
if (coil_index < coil_write_buffer.size()
|
||||
&& coil_read_buffer[coil_index].value) {
|
||||
&& coil_read_buffer[coil_index].value)
|
||||
{
|
||||
coil_write_buffer[coil_index].set(value);
|
||||
return 0;
|
||||
}
|
||||
|
@ -201,13 +230,16 @@ modbus_errno IndexedStrategy::WriteCoil(uint16_t coil_index, bool value) {
|
|||
|
||||
modbus_errno IndexedStrategy::WriteMultipleCoils(uint16_t coil_start_index,
|
||||
uint16_t num_coils,
|
||||
uint8_t* values) {
|
||||
if (coil_start_index + num_coils >= coil_write_buffer.size()) {
|
||||
uint8_t* values)
|
||||
{
|
||||
if (coil_start_index + num_coils >= coil_write_buffer.size())
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
lock_guard<mutex> guard(buffer_mutex);
|
||||
for (uint16_t index = 0; index < num_coils; ++index) {
|
||||
for (uint16_t index = 0; index < num_coils; ++index)
|
||||
{
|
||||
// Get the value from the packed structure
|
||||
bool value = values[index / 8] & BOOL_BIT_MASK[index % 8];
|
||||
coil_write_buffer[coil_start_index + index].set(value);
|
||||
|
@ -218,22 +250,26 @@ modbus_errno IndexedStrategy::WriteMultipleCoils(uint16_t coil_start_index,
|
|||
|
||||
modbus_errno IndexedStrategy::ReadCoils(uint16_t coil_start_index,
|
||||
uint16_t num_values,
|
||||
uint8_t* values) {
|
||||
uint8_t* values)
|
||||
{
|
||||
return this->ReadBools(coil_read_buffer, coil_start_index, num_values, values);
|
||||
}
|
||||
|
||||
modbus_errno IndexedStrategy::ReadDiscreteInputs(uint16_t di_start_index,
|
||||
uint16_t num_values,
|
||||
uint8_t* values) {
|
||||
uint8_t* values)
|
||||
{
|
||||
return this->ReadBools(di_read_buffer, di_start_index, num_values, values);
|
||||
}
|
||||
|
||||
modbus_errno IndexedStrategy::ReadBools(const vector<MappedBool>& buffer,
|
||||
uint16_t start_index,
|
||||
uint16_t num_values,
|
||||
uint8_t* values) {
|
||||
uint8_t* values)
|
||||
{
|
||||
auto max_index = start_index + num_values - 1;
|
||||
if (max_index >= buffer.size()) {
|
||||
if (max_index >= buffer.size())
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
@ -241,7 +277,8 @@ modbus_errno IndexedStrategy::ReadBools(const vector<MappedBool>& buffer,
|
|||
memset(values, 0, num_values / 8 + 1);
|
||||
|
||||
lock_guard<mutex> guard(buffer_mutex);
|
||||
for (uint16_t index = 0; index < num_values; ++index) {
|
||||
for (uint16_t index = 0; index < num_values; ++index)
|
||||
{
|
||||
values[index / 8] |= buffer[start_index + index].cached_value ? BOOL_BIT_MASK[index % 8] : 0;
|
||||
}
|
||||
|
||||
|
@ -250,22 +287,29 @@ modbus_errno IndexedStrategy::ReadBools(const vector<MappedBool>& buffer,
|
|||
|
||||
modbus_errno IndexedStrategy::WriteHoldingRegisters(uint16_t hr_start_index,
|
||||
uint16_t num_registers,
|
||||
uint8_t* value) {
|
||||
uint8_t* value)
|
||||
{
|
||||
// Each holding register is 2 bytes. Depending on the destination value
|
||||
// we are either updating the entire value or part of the value.
|
||||
// Here we go through each index and update the appropriate part of the value.
|
||||
lock_guard<mutex> guard(buffer_mutex);
|
||||
|
||||
for (uint16_t index = 0; index < num_registers; ++index) {
|
||||
for (uint16_t index = 0; index < num_registers; ++index)
|
||||
{
|
||||
uint16_t hr_index = hr_start_index + index;
|
||||
uint16_t word = mb_to_word(value[0], value[1]);
|
||||
|
||||
if (hr_index < MIN_16B_RANGE) {
|
||||
if (hr_index < MIN_16B_RANGE)
|
||||
{
|
||||
int_register_write_buffer[hr_index].set(word);
|
||||
} else if (hr_index < MAX_16B_RANGE) {
|
||||
}
|
||||
else if (hr_index < MAX_16B_RANGE)
|
||||
{
|
||||
hr_index -= MIN_16B_RANGE;
|
||||
intm_register_write_buffer[hr_index].set(word);
|
||||
} else if (hr_index < MAX_32B_RANGE) {
|
||||
}
|
||||
else if (hr_index < MAX_32B_RANGE)
|
||||
{
|
||||
hr_index -= MIN_32B_RANGE;
|
||||
// The word we got is part of a larger 32-bit value, and we will
|
||||
// bit shift to write the appropriate part. Resize to 32-bits
|
||||
|
@ -274,16 +318,21 @@ modbus_errno IndexedStrategy::WriteHoldingRegisters(uint16_t hr_start_index,
|
|||
PendingValue<IEC_DINT>& dst = dintm_register_write_buffer[hr_index / 2];
|
||||
dst.has_pending = true;
|
||||
|
||||
if (hr_index % 2 == 0) {
|
||||
if (hr_index % 2 == 0)
|
||||
{
|
||||
// First word
|
||||
dst.value = dst.value & 0x0000ffff;
|
||||
dst.value = dst.value | partial_value;
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
// Second word
|
||||
dst.value = dst.value & 0xffff0000;
|
||||
dst.value = dst.value | partial_value;
|
||||
}
|
||||
} else if (hr_index < MAX_64B_RANGE) {
|
||||
}
|
||||
else if (hr_index < MAX_64B_RANGE)
|
||||
{
|
||||
hr_index -= MIN_64B_RANGE;
|
||||
// Same as with a 32-bit value, here we are updating part of a
|
||||
// 64-bit value, so resize so we can bit-shift appropriately.
|
||||
|
@ -292,7 +341,8 @@ modbus_errno IndexedStrategy::WriteHoldingRegisters(uint16_t hr_start_index,
|
|||
dst.has_pending = true;
|
||||
|
||||
auto word_index = hr_index % 4;
|
||||
switch (word_index) {
|
||||
switch (word_index)
|
||||
{
|
||||
case 0:
|
||||
// First word
|
||||
dst.value = dst.value & 0x0000ffffffffffff;
|
||||
|
@ -314,7 +364,9 @@ modbus_errno IndexedStrategy::WriteHoldingRegisters(uint16_t hr_start_index,
|
|||
dst.value = dst.value | partial_value;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
@ -327,33 +379,53 @@ modbus_errno IndexedStrategy::WriteHoldingRegisters(uint16_t hr_start_index,
|
|||
modbus_errno IndexedStrategy::ReadHoldingRegisters(uint16_t hr_start_index, uint16_t num_registers, uint8_t* value) {
|
||||
lock_guard<mutex> guard(buffer_mutex);
|
||||
|
||||
for (uint16_t index = 0; index < num_registers; ++index) {
|
||||
for (uint16_t index = 0; index < num_registers; ++index)
|
||||
{
|
||||
uint16_t hr_index = hr_start_index + index;
|
||||
uint16_t val;
|
||||
if (hr_index < MIN_16B_RANGE) {
|
||||
if (hr_index < MIN_16B_RANGE)
|
||||
{
|
||||
val = int_register_read_buffer[hr_index].cached_value;
|
||||
} else if (hr_index < MAX_16B_RANGE) {
|
||||
}
|
||||
else if (hr_index < MAX_16B_RANGE)
|
||||
{
|
||||
hr_index -= MIN_16B_RANGE;
|
||||
val = intm_register_read_buffer[hr_index].cached_value;
|
||||
} else if (hr_index < MAX_32B_RANGE) {
|
||||
}
|
||||
else if (hr_index < MAX_32B_RANGE)
|
||||
{
|
||||
hr_index -= MIN_32B_RANGE;
|
||||
if (hr_index % 2 == 0) {
|
||||
if (hr_index % 2 == 0)
|
||||
{
|
||||
val = (uint16_t)(dintm_register_read_buffer[hr_index / 2].cached_value >> 16);
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
val = (uint16_t)(dintm_register_read_buffer[hr_index / 2].cached_value & 0xffff);
|
||||
}
|
||||
} else if (hr_index < MAX_64B_RANGE) {
|
||||
}
|
||||
else if (hr_index < MAX_64B_RANGE)
|
||||
{
|
||||
hr_index -= MIN_64B_RANGE;
|
||||
if (hr_index %4 == 0) {
|
||||
if (hr_index %4 == 0)
|
||||
{
|
||||
val = (uint16_t)(lintm_register_read_buffer[hr_index / 4].cached_value >> 48);
|
||||
} else if (hr_index %4 == 1) {
|
||||
}
|
||||
else if (hr_index %4 == 1)
|
||||
{
|
||||
val = (uint16_t)(lintm_register_read_buffer[hr_index / 4].cached_value >> 32);
|
||||
} else if (hr_index %4 == 2) {
|
||||
}
|
||||
else if (hr_index %4 == 2)
|
||||
{
|
||||
val = (uint16_t)(lintm_register_read_buffer[hr_index / 4].cached_value >> 16);
|
||||
} else if (hr_index %4 == 3) {
|
||||
}
|
||||
else if (hr_index %4 == 3)
|
||||
{
|
||||
val = (uint16_t)(lintm_register_read_buffer[hr_index / 4].cached_value & 0xffff);
|
||||
}
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
@ -365,13 +437,16 @@ modbus_errno IndexedStrategy::ReadHoldingRegisters(uint16_t hr_start_index, uint
|
|||
return 0;
|
||||
}
|
||||
|
||||
modbus_errno IndexedStrategy::ReadInputRegisters(uint16_t hr_start_index, uint16_t num_registers, uint8_t* value) {
|
||||
modbus_errno IndexedStrategy::ReadInputRegisters(uint16_t hr_start_index, uint16_t num_registers, uint8_t* value)
|
||||
{
|
||||
lock_guard<mutex> guard(buffer_mutex);
|
||||
|
||||
for (uint16_t index = 0; index < num_registers; ++index) {
|
||||
for (uint16_t index = 0; index < num_registers; ++index)
|
||||
{
|
||||
uint16_t hr_index = hr_start_index + index;
|
||||
|
||||
if (hr_index >= MIN_16B_RANGE) {
|
||||
if (hr_index >= MIN_16B_RANGE)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
|
|
@ -30,13 +30,16 @@ const std::uint16_t NUM_REGISTER_VALUES(1024);
|
|||
|
||||
/// Defines the mapping between a located boolean value
|
||||
/// and a cache of the value for reading with modbus.
|
||||
struct MappedBool {
|
||||
struct MappedBool
|
||||
{
|
||||
MappedBool() : cached_value(0), value(nullptr) {}
|
||||
IEC_BOOL cached_value;
|
||||
IEC_BOOL *value;
|
||||
|
||||
inline void update_cache() {
|
||||
if (this->value) {
|
||||
inline void update_cache()
|
||||
{
|
||||
if (this->value)
|
||||
{
|
||||
this->cached_value = *this->value;
|
||||
}
|
||||
}
|
||||
|
@ -44,13 +47,15 @@ struct MappedBool {
|
|||
|
||||
/// Defines a write that has been submitted via Modbus
|
||||
/// but may not have been applied to the located variable yet.
|
||||
struct PendingBool {
|
||||
struct PendingBool
|
||||
{
|
||||
PendingBool() : has_pending(false), value(0) {}
|
||||
bool has_pending;
|
||||
IEC_BOOL value;
|
||||
|
||||
/// Set the value and mark it as updated.
|
||||
inline void set(IEC_BOOL val) {
|
||||
inline void set(IEC_BOOL val)
|
||||
{
|
||||
this->has_pending = true;
|
||||
this->value = val;
|
||||
}
|
||||
|
@ -59,19 +64,22 @@ struct PendingBool {
|
|||
/// Defines the mapping between a located value
|
||||
/// and a cache of the value for reading with modbus.
|
||||
template <typename T>
|
||||
struct MappedValue {
|
||||
struct MappedValue
|
||||
{
|
||||
MappedValue() : cached_value(0), value(nullptr) {}
|
||||
T cached_value;
|
||||
T* value;
|
||||
|
||||
/// Initialize the glue link and the cached value.
|
||||
/// @param val The glue variable to initialize from.
|
||||
inline void init(T* val) {
|
||||
inline void init(T* val)
|
||||
{
|
||||
this->value = val;
|
||||
this->cached_value = *val;
|
||||
}
|
||||
|
||||
inline void update_cache() {
|
||||
inline void update_cache()
|
||||
{
|
||||
if (this->value) {
|
||||
this->cached_value = *this->value;
|
||||
}
|
||||
|
@ -81,13 +89,15 @@ struct MappedValue {
|
|||
/// Defines a write that has been submitted via Modbus
|
||||
/// but may not have been applied to the located variable yet.
|
||||
template <typename T>
|
||||
struct PendingValue {
|
||||
struct PendingValue
|
||||
{
|
||||
PendingValue() : has_pending(false), value(0) {}
|
||||
bool has_pending;
|
||||
T value;
|
||||
|
||||
/// Set the value and mark it as updated.
|
||||
inline void set(T val) {
|
||||
inline void set(T val)
|
||||
{
|
||||
this->has_pending = true;
|
||||
this->value = val;
|
||||
}
|
||||
|
@ -101,7 +111,8 @@ typedef std::uint8_t modbus_errno;
|
|||
/// constructed so that we can quickly apply changes. This obviously uses
|
||||
/// more memory, but is far more efficient over the lifetime of the
|
||||
/// application.
|
||||
class IndexedStrategy {
|
||||
class IndexedStrategy
|
||||
{
|
||||
public:
|
||||
/// Initialize a new instance of the strategy using the bindings.
|
||||
IndexedStrategy(const GlueVariablesBinding& bindings);
|
||||
|
|
|
@ -22,16 +22,19 @@
|
|||
* @{
|
||||
*/
|
||||
|
||||
inline std::int16_t mb_to_word(std::uint8_t byte1, std::uint8_t byte2) {
|
||||
inline std::int16_t mb_to_word(std::uint8_t byte1, std::uint8_t byte2)
|
||||
{
|
||||
std::int16_t returnValue = (std::int16_t)(byte1 << 8) | (std::int16_t)byte2;
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
inline std::uint8_t mb_low_byte(std::uint16_t w) {
|
||||
inline std::uint8_t mb_low_byte(std::uint16_t w)
|
||||
{
|
||||
return (std::uint8_t) ((w) & 0xff);
|
||||
}
|
||||
|
||||
inline std::uint8_t mb_high_byte(std::uint16_t w) {
|
||||
inline std::uint8_t mb_high_byte(std::uint16_t w)
|
||||
{
|
||||
return (std::uint8_t) ((w) >> 8);
|
||||
}
|
||||
|
||||
|
|
|
@ -73,7 +73,8 @@ int modbus_error(unsigned char *buffer, int mb_error)
|
|||
}
|
||||
|
||||
inline int read_sizes(unsigned char* buffer, int buffer_size, int16_t& start,
|
||||
int16_t& num_items) {
|
||||
int16_t& num_items)
|
||||
{
|
||||
// This request must have at least 12 bytes. If it doesn't, it's a corrupted message
|
||||
if (buffer_size < 12)
|
||||
{
|
||||
|
@ -87,8 +88,8 @@ inline int read_sizes(unsigned char* buffer, int buffer_size, int16_t& start,
|
|||
}
|
||||
|
||||
inline int read_sized_bytes(unsigned char* buffer, int buffer_size, int16_t& start,
|
||||
int16_t& num_coils, int16_t& num_bytes) {
|
||||
|
||||
int16_t& num_coils, int16_t& num_bytes)
|
||||
{
|
||||
int ret = read_sizes(buffer, buffer_size, start, num_coils);
|
||||
|
||||
// Calculate the size of the message in bytes - they are packed into
|
||||
|
@ -96,7 +97,8 @@ inline int read_sized_bytes(unsigned char* buffer, int buffer_size, int16_t& sta
|
|||
// if that many were requested.
|
||||
num_bytes = ((num_coils + 7) / 8);
|
||||
|
||||
if (num_bytes > 255) {
|
||||
if (num_bytes > 255)
|
||||
{
|
||||
return modbus_error(buffer, ERR_ILLEGAL_DATA_ADDRESS);
|
||||
}
|
||||
|
||||
|
@ -112,7 +114,8 @@ int read_coils(unsigned char *buffer, int buffer_size, IndexedStrategy* strategy
|
|||
int16_t coil_data_length;
|
||||
int16_t byte_data_length;
|
||||
int ret = read_sized_bytes(buffer, buffer_size, start, coil_data_length, byte_data_length);
|
||||
if (ret != 0) {
|
||||
if (ret != 0)
|
||||
{
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
@ -124,7 +127,8 @@ int read_coils(unsigned char *buffer, int buffer_size, IndexedStrategy* strategy
|
|||
buffer[8] = byte_data_length;
|
||||
|
||||
modbus_errno err = strategy->ReadCoils(start, coil_data_length, reinterpret_cast<uint8_t*>(buffer + 9));
|
||||
if (err) {
|
||||
if (err)
|
||||
{
|
||||
return modbus_error(buffer, ERR_ILLEGAL_DATA_ADDRESS);
|
||||
}
|
||||
|
||||
|
@ -140,7 +144,8 @@ int read_discrete_inputs(unsigned char *buffer, int buffer_size, IndexedStrategy
|
|||
int16_t input_data_length;
|
||||
int16_t byte_data_length;
|
||||
int ret = read_sized_bytes(buffer, buffer_size, start, input_data_length, byte_data_length);
|
||||
if (ret != 0) {
|
||||
if (ret != 0)
|
||||
{
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
@ -150,7 +155,8 @@ int read_discrete_inputs(unsigned char *buffer, int buffer_size, IndexedStrategy
|
|||
buffer[8] = byte_data_length; //Number of bytes of data
|
||||
|
||||
modbus_errno err = strategy->ReadDiscreteInputs(start, input_data_length, reinterpret_cast<uint8_t*>(buffer + 9));
|
||||
if (err) {
|
||||
if (err)
|
||||
{
|
||||
return modbus_error(buffer, ERR_ILLEGAL_DATA_ADDRESS);
|
||||
}
|
||||
|
||||
|
@ -165,7 +171,8 @@ int read_holding_registers(unsigned char *buffer, int buffer_size, IndexedStrate
|
|||
int16_t start;
|
||||
int16_t num_registers;
|
||||
int ret = read_sizes(buffer, buffer_size, start, num_registers);
|
||||
if (ret != 0) {
|
||||
if (ret != 0)
|
||||
{
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
@ -177,7 +184,8 @@ int read_holding_registers(unsigned char *buffer, int buffer_size, IndexedStrate
|
|||
buffer[8] = byte_data_length; //Number of bytes of data
|
||||
|
||||
modbus_errno err = strategy->ReadHoldingRegisters(start, num_registers, reinterpret_cast<uint8_t*>(buffer + 9));
|
||||
if (err) {
|
||||
if (err)
|
||||
{
|
||||
return modbus_error(buffer, ERR_ILLEGAL_DATA_ADDRESS);
|
||||
}
|
||||
|
||||
|
@ -192,7 +200,8 @@ int read_input_registers(unsigned char *buffer, int buffer_size, IndexedStrategy
|
|||
int16_t start;
|
||||
int16_t num_registers;
|
||||
int ret = read_sizes(buffer, buffer_size, start, num_registers);
|
||||
if (ret != 0) {
|
||||
if (ret != 0)
|
||||
{
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
@ -204,7 +213,8 @@ int read_input_registers(unsigned char *buffer, int buffer_size, IndexedStrategy
|
|||
buffer[8] = byte_data_length; //Number of bytes of data
|
||||
|
||||
modbus_errno err = strategy->ReadInputRegisters(start, num_registers, reinterpret_cast<uint8_t*>(buffer + 9));
|
||||
if (err) {
|
||||
if (err)
|
||||
{
|
||||
return modbus_error(buffer, ERR_ILLEGAL_DATA_ADDRESS);
|
||||
}
|
||||
|
||||
|
@ -217,7 +227,8 @@ int write_coil(unsigned char* buffer, int buffer_size, IndexedStrategy* strategy
|
|||
bool value = mb_to_word(buffer[10], buffer[11]) != 0;
|
||||
|
||||
modbus_errno err = strategy->WriteCoil(start, value);
|
||||
if (err) {
|
||||
if (err)
|
||||
{
|
||||
return modbus_error(buffer, ERR_ILLEGAL_DATA_ADDRESS);
|
||||
}
|
||||
|
||||
|
@ -232,7 +243,8 @@ int write_holding_register(unsigned char* buffer, int buffer_size, IndexedStrate
|
|||
int16_t start = mb_to_word(buffer[8], buffer[9]);
|
||||
|
||||
modbus_errno err = strategy->WriteHoldingRegisters(start, 1, buffer + 9);
|
||||
if (err) {
|
||||
if (err)
|
||||
{
|
||||
return modbus_error(buffer, ERR_ILLEGAL_DATA_ADDRESS);
|
||||
}
|
||||
|
||||
|
@ -248,7 +260,8 @@ int write_multiple_coils(unsigned char* buffer, int buffer_size, IndexedStrategy
|
|||
int16_t input_data_length;
|
||||
int16_t byte_data_length;
|
||||
int ret = read_sized_bytes(buffer, buffer_size, start, input_data_length, byte_data_length);
|
||||
if (ret != 0) {
|
||||
if (ret != 0)
|
||||
{
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
@ -258,7 +271,8 @@ int write_multiple_coils(unsigned char* buffer, int buffer_size, IndexedStrategy
|
|||
}
|
||||
|
||||
modbus_errno err = strategy->WriteMultipleCoils(start, input_data_length, buffer + 13);
|
||||
if (err) {
|
||||
if (err)
|
||||
{
|
||||
return modbus_error(buffer, ERR_ILLEGAL_DATA_ADDRESS);
|
||||
}
|
||||
|
||||
|
@ -273,17 +287,20 @@ int write_multiple_registers(unsigned char* buffer, int buffer_size, IndexedStra
|
|||
int16_t start;
|
||||
int16_t num_registers;
|
||||
int ret = read_sizes(buffer, buffer_size, start, num_registers);
|
||||
if (ret != 0) {
|
||||
if (ret != 0)
|
||||
{
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Check that we have enough bytes
|
||||
if (buffer_size < (num_registers * 2 + 13) || buffer[12] != num_registers * 2) {
|
||||
if (buffer_size < (num_registers * 2 + 13) || buffer[12] != num_registers * 2)
|
||||
{
|
||||
return modbus_error(buffer, ERR_ILLEGAL_DATA_VALUE);
|
||||
}
|
||||
|
||||
modbus_errno err = strategy->WriteHoldingRegisters(start, num_registers, buffer + 13);
|
||||
if (err) {
|
||||
if (err)
|
||||
{
|
||||
return modbus_error(buffer, ERR_ILLEGAL_DATA_ADDRESS);
|
||||
}
|
||||
|
||||
|
@ -298,7 +315,7 @@ int write_multiple_registers(unsigned char* buffer, int buffer_size, IndexedStra
|
|||
/// @param buffer_size
|
||||
/// @param user_data The mapping strategy.
|
||||
/// @return size of the response message in bytes
|
||||
int modbus_process_message(unsigned char *buffer, int buffer_size, void* user_data)
|
||||
int16_t modbus_process_message(unsigned char *buffer, int16_t buffer_size, void* user_data)
|
||||
{
|
||||
auto strategy = reinterpret_cast<IndexedStrategy*>(user_data);
|
||||
|
||||
|
@ -308,7 +325,8 @@ int modbus_process_message(unsigned char *buffer, int buffer_size, void* user_da
|
|||
return modbus_error(buffer, ERR_ILLEGAL_FUNCTION);
|
||||
}
|
||||
|
||||
switch (buffer[7]) {
|
||||
switch (buffer[7])
|
||||
{
|
||||
case MB_FC_READ_COILS:
|
||||
return read_coils(buffer, buffer_size, strategy);
|
||||
case MB_FC_READ_INPUTS:
|
||||
|
@ -332,7 +350,8 @@ int modbus_process_message(unsigned char *buffer, int buffer_size, void* user_da
|
|||
|
||||
/// Arguments that are passed to the thread to exchange modbus data with the
|
||||
/// runtime.
|
||||
struct ModbusExchangeArgs {
|
||||
struct ModbusExchangeArgs
|
||||
{
|
||||
IndexedStrategy* strategy;
|
||||
volatile bool* run;
|
||||
std::chrono::milliseconds interval;
|
||||
|
@ -340,10 +359,12 @@ struct ModbusExchangeArgs {
|
|||
|
||||
/// The main function for the thread that is responsible for exchanging
|
||||
/// modbus data with the located variables.
|
||||
void* modbus_exchange_data(void* args) {
|
||||
void* modbus_exchange_data(void* args)
|
||||
{
|
||||
auto exchange_args = reinterpret_cast<ModbusExchangeArgs*>(args);
|
||||
|
||||
while (*exchange_args->run) {
|
||||
while (*exchange_args->run)
|
||||
{
|
||||
spdlog::trace("Exchanging modbus master data");
|
||||
exchange_args->strategy->Exchange();
|
||||
this_thread::sleep_for(exchange_args->interval);
|
||||
|
@ -356,7 +377,8 @@ void* modbus_exchange_data(void* args) {
|
|||
|
||||
/// Container for reading in configuration from the config.ini
|
||||
/// This is populated with values from the config file.
|
||||
struct ModbusSlaveConfig {
|
||||
struct ModbusSlaveConfig
|
||||
{
|
||||
ModbusSlaveConfig() :
|
||||
address("127.0.0.1"),
|
||||
port(502),
|
||||
|
@ -391,20 +413,29 @@ struct ModbusSlaveConfig {
|
|||
};
|
||||
|
||||
int modbus_slave_cfg_handler(void* user_data, const char* section,
|
||||
const char* name, const char* value) {
|
||||
if (strcmp("modbuslave", section) != 0) {
|
||||
const char* name, const char* value)
|
||||
{
|
||||
if (strcmp("modbuslave", section) != 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
auto config = reinterpret_cast<ModbusSlaveConfig*>(user_data);
|
||||
|
||||
if (strcmp(name, "port") == 0) {
|
||||
if (strcmp(name, "port") == 0)
|
||||
{
|
||||
config->port = atoi(value);
|
||||
} else if (strcmp(name, "address") == 0) {
|
||||
}
|
||||
else if (strcmp(name, "address") == 0)
|
||||
{
|
||||
config->address = value;
|
||||
} else if (strcmp(name, "enabled") == 0) {
|
||||
}
|
||||
else if (strcmp(name, "enabled") == 0)
|
||||
{
|
||||
// Nothing to do here - we already know this is enabled
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
spdlog::warn("Unknown configuration item {}", name);
|
||||
return -1;
|
||||
}
|
||||
|
@ -415,26 +446,32 @@ int modbus_slave_cfg_handler(void* user_data, const char* section,
|
|||
int8_t modbus_slave_run(oplc::config_stream& cfg_stream,
|
||||
const char* cfg_overrides,
|
||||
const GlueVariablesBinding& bindings,
|
||||
volatile bool& run) {
|
||||
volatile bool& run)
|
||||
{
|
||||
ModbusSlaveConfig config;
|
||||
|
||||
ini_parse_stream(oplc::istream_fgets, cfg_stream.get(), modbus_slave_cfg_handler, &config);
|
||||
ini_parse_stream(oplc::istream_fgets, cfg_stream.get(),
|
||||
modbus_slave_cfg_handler, &config);
|
||||
|
||||
cfg_stream.reset(nullptr);
|
||||
|
||||
IndexedStrategy strategy(bindings);
|
||||
|
||||
pthread_t exchange_data_thread;
|
||||
auto args = new ModbusExchangeArgs {
|
||||
auto args = new ModbusExchangeArgs
|
||||
{
|
||||
.strategy=&strategy,
|
||||
.run=&run,
|
||||
.interval=std::chrono::milliseconds(100)
|
||||
};
|
||||
|
||||
int ret = pthread_create(&exchange_data_thread, NULL, modbus_exchange_data, args);
|
||||
if (ret == 0) {
|
||||
if (ret == 0)
|
||||
{
|
||||
pthread_detach(exchange_data_thread);
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
delete args;
|
||||
}
|
||||
|
||||
|
@ -447,8 +484,8 @@ int8_t modbus_slave_run(oplc::config_stream& cfg_stream,
|
|||
}
|
||||
|
||||
void modbus_slave_service_run(const GlueVariablesBinding& binding,
|
||||
volatile bool& run, const char* config) {
|
||||
|
||||
volatile bool& run, const char* config)
|
||||
{
|
||||
auto cfg_stream = oplc::open_config();
|
||||
modbus_slave_run(cfg_stream, config, binding, run);
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
class GlueVariablesBinding;
|
||||
|
||||
/// @brief Process an individual modbus message.
|
||||
int modbus_process_message(unsigned char *buffer, int buffer_size,
|
||||
int16_t modbus_process_message(unsigned char *buffer, int16_t buffer_size,
|
||||
void* user_data);
|
||||
|
||||
/// @brief Start the modbus slave server.
|
||||
|
|
|
@ -63,8 +63,10 @@ const size_t FILE_HEADER_SIZE(extent<decltype(FILE_HEADER)>::value);
|
|||
/// Get the size of the particular location in bytes.
|
||||
/// @param size the size type.
|
||||
/// @return The required storage size in number of bytes.
|
||||
inline uint8_t get_size_bytes(IecLocationSize size) {
|
||||
switch (size) {
|
||||
inline uint8_t get_size_bytes(IecLocationSize size)
|
||||
{
|
||||
switch (size)
|
||||
{
|
||||
case IECLST_BIT:
|
||||
return 1;
|
||||
case IECLST_BYTE:
|
||||
|
@ -83,13 +85,16 @@ inline uint8_t get_size_bytes(IecLocationSize size) {
|
|||
/// Get the total number of bytes required to store the bindings.
|
||||
/// @param bindings The bindings that we want to store.
|
||||
/// @return The total number of bytes required.
|
||||
size_t get_size_bytes(const GlueVariablesBinding& bindings) {
|
||||
size_t get_size_bytes(const GlueVariablesBinding& bindings)
|
||||
{
|
||||
size_t size(0);
|
||||
|
||||
for (uint16_t index(0); index < bindings.size; ++index) {
|
||||
for (uint16_t index(0); index < bindings.size; ++index)
|
||||
{
|
||||
const GlueVariable& glue = bindings.glue_variables[index];
|
||||
|
||||
if (glue.dir != IECLDT_MEM) {
|
||||
if (glue.dir != IECLDT_MEM)
|
||||
{
|
||||
// We only care about items that are stored in memory
|
||||
continue;
|
||||
}
|
||||
|
@ -100,16 +105,20 @@ size_t get_size_bytes(const GlueVariablesBinding& bindings) {
|
|||
return size;
|
||||
}
|
||||
|
||||
inline uint8_t mask_index(GlueBoolGroup* group, uint8_t i) {
|
||||
if (!group->values[i]) {
|
||||
inline uint8_t mask_index(GlueBoolGroup* group, uint8_t i)
|
||||
{
|
||||
if (!group->values[i])
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (*group->values) ? (1 << i) : 0;
|
||||
}
|
||||
|
||||
inline void set_index(GlueBoolGroup* group, uint8_t i, uint8_t v) {
|
||||
if (group->values[i]) {
|
||||
inline void set_index(GlueBoolGroup* group, uint8_t i, uint8_t v)
|
||||
{
|
||||
if (group->values[i])
|
||||
{
|
||||
(*group->values[i]) = ((1 << i) & v) ? TRUE: FALSE;
|
||||
}
|
||||
}
|
||||
|
@ -118,21 +127,25 @@ inline void set_index(GlueBoolGroup* group, uint8_t i, uint8_t v) {
|
|||
/// @param bindings The bindings that we want to copy from.
|
||||
/// @param buffer The buffer that we are copying into.
|
||||
/// @return The number of bytes that were written into the buffer.
|
||||
size_t pstorage_copy_glue(const GlueVariablesBinding& bindings, char* buffer) {
|
||||
size_t pstorage_copy_glue(const GlueVariablesBinding& bindings, char* buffer)
|
||||
{
|
||||
lock_guard<mutex> guard(*bindings.buffer_lock);
|
||||
|
||||
size_t num_written(0);
|
||||
for (uint16_t index(0); index < bindings.size; ++index) {
|
||||
for (uint16_t index(0); index < bindings.size; ++index)
|
||||
{
|
||||
const GlueVariable& glue = bindings.glue_variables[index];
|
||||
|
||||
if (glue.dir != IECLDT_MEM) {
|
||||
if (glue.dir != IECLDT_MEM)
|
||||
{
|
||||
// We only care about items that are stored in memory
|
||||
continue;
|
||||
}
|
||||
|
||||
uint8_t num_bytes = get_size_bytes(glue.size);
|
||||
|
||||
if (glue.size == IECLST_BIT) {
|
||||
if (glue.size == IECLST_BIT)
|
||||
{
|
||||
GlueBoolGroup* group = reinterpret_cast<GlueBoolGroup*>(glue.value);
|
||||
uint8_t bools_as_byte = mask_index(group, 0)
|
||||
| mask_index(group, 1)
|
||||
|
@ -143,7 +156,9 @@ size_t pstorage_copy_glue(const GlueVariablesBinding& bindings, char* buffer) {
|
|||
| mask_index(group, 6)
|
||||
| mask_index(group, 7);
|
||||
memcpy(buffer, &bools_as_byte, 1);
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
// Write the number of bytes to the buffer
|
||||
memcpy(buffer, glue.value, num_bytes);
|
||||
}
|
||||
|
@ -158,7 +173,8 @@ size_t pstorage_copy_glue(const GlueVariablesBinding& bindings, char* buffer) {
|
|||
|
||||
/// Container for reading in configuration from the config.ini
|
||||
/// This is populated with values from the config file.
|
||||
struct PstorageConfig {
|
||||
struct PstorageConfig
|
||||
{
|
||||
PstorageConfig() :
|
||||
poll_interval(chrono::seconds(10))
|
||||
{}
|
||||
|
@ -166,20 +182,27 @@ struct PstorageConfig {
|
|||
};
|
||||
|
||||
int pstorage_cfg_handler(void* user_data, const char* section,
|
||||
const char* name, const char* value) {
|
||||
if (strcmp("pstorage", section) != 0) {
|
||||
const char* name, const char* value)
|
||||
{
|
||||
if (strcmp("pstorage", section) != 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
auto config = reinterpret_cast<PstorageConfig*>(user_data);
|
||||
|
||||
if (strcmp(name, "poll_interval") == 0) {
|
||||
if (strcmp(name, "poll_interval") == 0)
|
||||
{
|
||||
// We do not allow a poll period of less than 1 second as that
|
||||
// might cause lock contention problems.
|
||||
config->poll_interval = chrono::seconds(max(1, atoi(value)));
|
||||
} else if (strcmp(name, "enabled") == 0) {
|
||||
}
|
||||
else if (strcmp(name, "enabled") == 0)
|
||||
{
|
||||
// Nothing to do here - we already know this is enabled
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
spdlog::warn("Unknown configuration item {}", name);
|
||||
return -1;
|
||||
}
|
||||
|
@ -191,7 +214,8 @@ int8_t pstorage_run(oplc::config_stream& cfg_stream,
|
|||
const char* cfg_overrides,
|
||||
const GlueVariablesBinding& bindings,
|
||||
volatile bool& run,
|
||||
function<ostream*(void)> stream_fn) {
|
||||
function<ostream*(void)> stream_fn)
|
||||
{
|
||||
PstorageConfig config;
|
||||
ini_parse_stream(oplc::istream_fgets, cfg_stream.get(),
|
||||
pstorage_cfg_handler, &config);
|
||||
|
@ -200,7 +224,8 @@ int8_t pstorage_run(oplc::config_stream& cfg_stream,
|
|||
// will close the reference to the file
|
||||
cfg_stream.reset(nullptr);
|
||||
|
||||
if (strlen(cfg_overrides) > 0) {
|
||||
if (strlen(cfg_overrides) > 0)
|
||||
{
|
||||
config.poll_interval = chrono::seconds(max(1, atoi(cfg_overrides)));
|
||||
}
|
||||
|
||||
|
@ -216,18 +241,22 @@ int8_t pstorage_run(oplc::config_stream& cfg_stream,
|
|||
|
||||
// If the required size from bindings is greater than the configured
|
||||
// size, then just exit
|
||||
if (get_size_bytes(bindings) > extent<decltype(buffer)>::value) {
|
||||
if (get_size_bytes(bindings) > extent<decltype(buffer)>::value)
|
||||
{
|
||||
spdlog::error("Stored variables too large for persistent storage");
|
||||
return -1;
|
||||
}
|
||||
|
||||
while (run) {
|
||||
while (run)
|
||||
{
|
||||
size_t num_written = pstorage_copy_glue(bindings, buffer);
|
||||
|
||||
if (memcmp(buffer, buffer_old, num_written) != 0) {
|
||||
if (memcmp(buffer, buffer_old, num_written) != 0)
|
||||
{
|
||||
// Try to open the file to do the initial write
|
||||
unique_ptr<ostream> out_stream(stream_fn());
|
||||
if (!out_stream) {
|
||||
if (!out_stream)
|
||||
{
|
||||
spdlog::error("Unable to open persistent storage file for writing");
|
||||
return -2;
|
||||
}
|
||||
|
@ -241,7 +270,9 @@ int8_t pstorage_run(oplc::config_stream& cfg_stream,
|
|||
|
||||
// We should be able to avoid this memory copy entirely
|
||||
memcpy(buffer_old, buffer, num_written);
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
spdlog::debug("Skip persistent write because unchanged values");
|
||||
}
|
||||
|
||||
|
@ -257,13 +288,16 @@ int8_t pstorage_run(oplc::config_stream& cfg_stream,
|
|||
}
|
||||
|
||||
inline int8_t read_and_check(istream& input_stream, const char header[],
|
||||
char buffer[], size_t count) {
|
||||
if (!input_stream.read(buffer, count)) {
|
||||
char buffer[], size_t count)
|
||||
{
|
||||
if (!input_stream.read(buffer, count))
|
||||
{
|
||||
spdlog::warn("Unable to read header from persistence file stream");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (memcmp(header, buffer, count) != 0) {
|
||||
if (memcmp(header, buffer, count) != 0)
|
||||
{
|
||||
spdlog::warn("Header does not match expected in persistence file ");
|
||||
return -2;
|
||||
}
|
||||
|
@ -272,47 +306,55 @@ inline int8_t read_and_check(istream& input_stream, const char header[],
|
|||
}
|
||||
|
||||
int8_t pstorage_read(istream& input_stream,
|
||||
const GlueVariablesBinding& bindings) {
|
||||
const GlueVariablesBinding& bindings)
|
||||
{
|
||||
// Read the file header - we define the file header as a constant that
|
||||
// must be present as the header. We don't allow UTF BOMs here.
|
||||
char header_check[FILE_HEADER_SIZE];
|
||||
if (read_and_check(input_stream, FILE_HEADER, header_check, FILE_HEADER_SIZE) != 0) {
|
||||
if (read_and_check(input_stream, FILE_HEADER, header_check, FILE_HEADER_SIZE) != 0)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Check endianness of the written file
|
||||
char endianness_expected[2] = { IS_BIG_ENDIAN, '\n'};
|
||||
char endianness_check[2];
|
||||
if (read_and_check(input_stream, endianness_expected, header_check, 2) != 0) {
|
||||
if (read_and_check(input_stream, endianness_expected, header_check, 2) != 0)
|
||||
{
|
||||
return -2;
|
||||
}
|
||||
|
||||
// We have a digest in the header to try to prevent accidentally using
|
||||
// the wrong persistence file for a particular runtime.
|
||||
char checksum_check[32];
|
||||
if (read_and_check(input_stream, bindings.checksum, checksum_check, 32) != 0) {
|
||||
if (read_and_check(input_stream, bindings.checksum, checksum_check, 32) != 0)
|
||||
{
|
||||
return -3;
|
||||
}
|
||||
|
||||
// Just add one newline character
|
||||
char padding_expected[1] = { '\n' };
|
||||
char padding_check[1];
|
||||
if (read_and_check(input_stream, padding_expected, padding_check, 1) != 0) {
|
||||
if (read_and_check(input_stream, padding_expected, padding_check, 1) != 0)
|
||||
{
|
||||
return -4;
|
||||
}
|
||||
|
||||
// Now we know that the format is right, so read in the rest. We read
|
||||
// variable by variable so that we can assign into the right value.
|
||||
for (uint16_t index(0); index < bindings.size; ++index) {
|
||||
for (uint16_t index(0); index < bindings.size; ++index)
|
||||
{
|
||||
const GlueVariable& glue = bindings.glue_variables[index];
|
||||
|
||||
if (glue.dir != IECLDT_MEM) {
|
||||
if (glue.dir != IECLDT_MEM)
|
||||
{
|
||||
// We only care about items that are stored in memory
|
||||
continue;
|
||||
}
|
||||
|
||||
uint8_t num_bytes;
|
||||
switch (glue.size) {
|
||||
switch (glue.size)
|
||||
{
|
||||
case IECLST_BIT:
|
||||
num_bytes = 1;
|
||||
break;
|
||||
|
@ -337,7 +379,8 @@ int8_t pstorage_read(istream& input_stream,
|
|||
// 8 here is the maximum buffer size that we need based on
|
||||
// the types that we support.
|
||||
char buffer[8];
|
||||
if (!input_stream.read(buffer, num_bytes)) {
|
||||
if (!input_stream.read(buffer, num_bytes))
|
||||
{
|
||||
spdlog::error("Persistent storage file too short; partially read");
|
||||
return -6;
|
||||
}
|
||||
|
@ -346,7 +389,8 @@ int8_t pstorage_read(istream& input_stream,
|
|||
// value or a group of booleans.
|
||||
// We don't actually care what the contents are - we just populate as
|
||||
// though they are raw bytes
|
||||
if (glue.size == IECLST_BIT) {
|
||||
if (glue.size == IECLST_BIT)
|
||||
{
|
||||
GlueBoolGroup* group = reinterpret_cast<GlueBoolGroup*>(glue.value);
|
||||
uint8_t value = static_cast<uint8_t>(buffer[0]);
|
||||
set_index(group, 0, value);
|
||||
|
@ -357,7 +401,9 @@ int8_t pstorage_read(istream& input_stream,
|
|||
set_index(group, 5, value);
|
||||
set_index(group, 6, value);
|
||||
set_index(group, 7, value);
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
memcpy(glue.value, buffer, num_bytes);
|
||||
}
|
||||
}
|
||||
|
@ -367,9 +413,11 @@ int8_t pstorage_read(istream& input_stream,
|
|||
return 0;
|
||||
}
|
||||
|
||||
void pstorage_service_init(const GlueVariablesBinding& binding) {
|
||||
void pstorage_service_init(const GlueVariablesBinding& binding)
|
||||
{
|
||||
ifstream stream("persistent.file", ios::binary);
|
||||
if (!stream) {
|
||||
if (!stream)
|
||||
{
|
||||
spdlog::info("Skipped load persistence because file cannot be read.");
|
||||
return;
|
||||
}
|
||||
|
@ -378,15 +426,18 @@ void pstorage_service_init(const GlueVariablesBinding& binding) {
|
|||
spdlog::info("Storage read completed with result {}", result);
|
||||
}
|
||||
|
||||
void pstorage_service_finalize(const GlueVariablesBinding& binding) {
|
||||
void pstorage_service_finalize(const GlueVariablesBinding& binding)
|
||||
{
|
||||
// We don't current do anything on finalize (although we probably should)
|
||||
}
|
||||
|
||||
void pstorage_service_run(const GlueVariablesBinding& binding,
|
||||
volatile bool& run, const char* config) {
|
||||
volatile bool& run, const char* config)
|
||||
{
|
||||
// We don't allow a poll duration of less than one second otherwise
|
||||
// that can have detrimental effects on performance
|
||||
auto create_stream = []() {
|
||||
auto create_stream = []()
|
||||
{
|
||||
return new ofstream("persistent.file", ios::binary);
|
||||
};
|
||||
|
||||
|
|
|
@ -95,7 +95,7 @@ int createSocket(uint16_t port)
|
|||
|
||||
//Create TCP Socket
|
||||
socket_fd = socket(AF_INET,SOCK_STREAM,0);
|
||||
if (socket_fd<0)
|
||||
if (socket_fd < 0)
|
||||
{
|
||||
spdlog::error("Server: error creating stream socket => {}", strerror(errno));
|
||||
return -1;
|
||||
|
@ -175,7 +175,8 @@ int listenToClient(int client_fd, unsigned char *buffer)
|
|||
}
|
||||
|
||||
/// Arguments passed to the server thread.
|
||||
struct ServerArgs {
|
||||
struct ServerArgs
|
||||
{
|
||||
/// The client file descriptor for reading and writing.
|
||||
int client_fd;
|
||||
/// Set to false when the server should terminate.
|
||||
|
@ -200,9 +201,6 @@ void *handleConnections(void *arguments)
|
|||
|
||||
while(*args->run)
|
||||
{
|
||||
//unsigned char buffer[NET_BUFFER_SIZE];
|
||||
//int messageSize;
|
||||
|
||||
messageSize = listenToClient(args->client_fd, buffer);
|
||||
if (messageSize <= 0 || messageSize > NET_BUFFER_SIZE)
|
||||
{
|
||||
|
@ -253,7 +251,8 @@ void startServer(uint16_t port, volatile bool& run_server, process_message_fn pr
|
|||
}
|
||||
|
||||
pthread_t thread;
|
||||
auto args = new ServerArgs {
|
||||
auto args = new ServerArgs
|
||||
{
|
||||
.client_fd=client_fd,
|
||||
.run=&run_server,
|
||||
.process_message=process_message,
|
||||
|
@ -261,9 +260,12 @@ void startServer(uint16_t port, volatile bool& run_server, process_message_fn pr
|
|||
};
|
||||
spdlog::trace("Server: Client accepted! Creating thread for the new client ID: {}...", client_fd);
|
||||
int success = pthread_create(&thread, NULL, handleConnections, args);
|
||||
if (success == 0) {
|
||||
if (success == 0)
|
||||
{
|
||||
pthread_detach(thread);
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
delete args;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -61,28 +61,33 @@ ServiceDefinition::ServiceDefinition(const char* name,
|
|||
config_buffer()
|
||||
{}
|
||||
|
||||
void ServiceDefinition::initialize() {
|
||||
void ServiceDefinition::initialize()
|
||||
{
|
||||
GlueVariablesBinding bindings(&bufferLock, OPLCGLUE_GLUE_SIZE,
|
||||
oplc_glue_vars, OPLCGLUE_MD5_DIGEST);
|
||||
this->init_fn(bindings);
|
||||
}
|
||||
|
||||
void ServiceDefinition::finalize() {
|
||||
void ServiceDefinition::finalize()
|
||||
{
|
||||
GlueVariablesBinding bindings(&bufferLock, OPLCGLUE_GLUE_SIZE,
|
||||
oplc_glue_vars, OPLCGLUE_MD5_DIGEST);
|
||||
this->finalize_fn(bindings);
|
||||
}
|
||||
|
||||
void ServiceDefinition::start(const char* config) {
|
||||
void ServiceDefinition::start(const char* config)
|
||||
{
|
||||
// TODO there is a race condition here in creating the thread. This race
|
||||
// condition is old so I'm not trying to solve it now.
|
||||
if (this->running) {
|
||||
if (this->running)
|
||||
{
|
||||
spdlog::debug("{} cannot start because it is running.", this->name);
|
||||
return;
|
||||
}
|
||||
|
||||
size_t config_len = strlen(config);
|
||||
if (config_len > MAX_INTERACTIVE_CONFIG_SIZE - 1) {
|
||||
if (config_len > MAX_INTERACTIVE_CONFIG_SIZE - 1)
|
||||
{
|
||||
spdlog::warn("{} cannot be started because config is longer than {}.",
|
||||
this->name, MAX_INTERACTIVE_CONFIG_SIZE);
|
||||
return;
|
||||
|
@ -99,19 +104,24 @@ void ServiceDefinition::start(const char* config) {
|
|||
pthread_setname_np(this->thread, this->name);
|
||||
}
|
||||
|
||||
void ServiceDefinition::stop() {
|
||||
void ServiceDefinition::stop()
|
||||
{
|
||||
// TODO there is a threading issue here with access to the thread
|
||||
// and detecting if the service is running.
|
||||
if (this->running) {
|
||||
if (this->running)
|
||||
{
|
||||
spdlog::info("Stopping service {}", this->name);
|
||||
this->running = false;
|
||||
pthread_join(this->thread, nullptr);
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
spdlog::debug("Service {} was not running", this->name);
|
||||
}
|
||||
}
|
||||
|
||||
void* ServiceDefinition::run_service(void* user_data) {
|
||||
void* ServiceDefinition::run_service(void* user_data)
|
||||
{
|
||||
auto service = reinterpret_cast<ServiceDefinition*>(user_data);
|
||||
|
||||
GlueVariablesBinding bindings(&bufferLock, OPLCGLUE_GLUE_SIZE,
|
||||
|
|
|
@ -45,7 +45,8 @@ typedef std::function<void(const GlueVariablesBinding& binding, volatile bool& r
|
|||
/// @note There is presently no way to pass state from init to finalize
|
||||
/// or from start to stop. That's only because we haven't had such a need
|
||||
/// yet. If that comes up, then we'll add that.
|
||||
class ServiceDefinition final {
|
||||
class ServiceDefinition final
|
||||
{
|
||||
public:
|
||||
/// Initialize a new instance of a service definition that can be started
|
||||
/// and stopped but does not participate in initialize or finalize.
|
||||
|
|
|
@ -39,7 +39,8 @@ ServiceDefinition* services[] = {
|
|||
#endif
|
||||
};
|
||||
|
||||
ServiceDefinition* services_find(const char* name) {
|
||||
ServiceDefinition* services_find(const char* name)
|
||||
{
|
||||
ServiceDefinition** item = find_if(begin(services), end(services), [name] (ServiceDefinition* def) {
|
||||
return strcmp(def->id(), name) == 0;
|
||||
});
|
||||
|
@ -47,19 +48,22 @@ ServiceDefinition* services_find(const char* name) {
|
|||
return (item != end(services)) ? *item : nullptr;
|
||||
}
|
||||
|
||||
void services_stop() {
|
||||
void services_stop()
|
||||
{
|
||||
for_each(begin(services), end(services), [] (ServiceDefinition* def) {
|
||||
def->stop();
|
||||
});
|
||||
}
|
||||
|
||||
void services_init() {
|
||||
void services_init()
|
||||
{
|
||||
for_each(begin(services), end(services), [] (ServiceDefinition* def) {
|
||||
def->initialize();
|
||||
});
|
||||
}
|
||||
|
||||
void services_finalize() {
|
||||
void services_finalize()
|
||||
{
|
||||
for_each(begin(services), end(services), [] (ServiceDefinition* def) {
|
||||
def->finalize();
|
||||
});
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
|
||||
class ServiceDefinition;
|
||||
|
||||
/// Finds the service in the registry by the name of the service.
|
||||
/// @brief 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* services_find(const char* name);
|
||||
|
|
|
@ -17,19 +17,17 @@
|
|||
// of the ability to compile everything together, which is useful since
|
||||
// much of this is only determined when we have a structured text input file.
|
||||
|
||||
|
||||
/** @addtogroup example_app
|
||||
* @{ */
|
||||
|
||||
|
||||
extern "C" {
|
||||
void config_init__(void)
|
||||
{}
|
||||
|
||||
void config_run__(unsigned long tick)
|
||||
void config_run__(unsigned long tick) // NS
|
||||
{}
|
||||
}
|
||||
|
||||
unsigned long long common_ticktime__ = 50000000ULL; /*ns*/
|
||||
} // extern "C"
|
||||
unsigned long long common_ticktime__ = 50000000ULL; /*ns*/ // NS
|
||||
|
||||
/** @} */
|
||||
/** @} */
|
||||
|
|
|
@ -15,7 +15,6 @@
|
|||
/** @addtogroup example_app
|
||||
* @{ */
|
||||
|
||||
|
||||
// A simple application that defines the items normally that are provided
|
||||
// by the glue generator and MATIEC. This allows us to do a simple check
|
||||
// of the ability to compile everything together, which is useful since
|
||||
|
|
|
@ -1,11 +1,23 @@
|
|||
// 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.
|
||||
|
||||
/** @addtogroup example_app
|
||||
* @{ */
|
||||
|
||||
|
||||
struct modbus_t;
|
||||
|
||||
int modbus_set_slave(modbus_t *ctx, int slave)
|
||||
int modbus_set_slave(modbus_t *ctx, int slave) // NS
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
|
|
@ -26,44 +26,48 @@
|
|||
|
||||
|
||||
TIME __CURRENT_TIME;
|
||||
extern unsigned long long common_ticktime__;
|
||||
extern unsigned long long common_ticktime__; // NS
|
||||
|
||||
#ifndef OPLC_IEC_GLUE_VALUE_TYPE
|
||||
#define OPLC_IEC_GLUE_VALUE_TYPE
|
||||
enum IecGlueValueType {
|
||||
IECVT_BOOL,
|
||||
IECVT_BYTE,
|
||||
IECVT_SINT,
|
||||
IECVT_USINT,
|
||||
IECVT_INT,
|
||||
IECVT_UINT,
|
||||
IECVT_WORD,
|
||||
IECVT_DINT,
|
||||
IECVT_UDINT,
|
||||
IECVT_DWORD,
|
||||
IECVT_REAL,
|
||||
IECVT_LREAL,
|
||||
IECVT_LWORD,
|
||||
IECVT_LINT,
|
||||
IECVT_ULINT,
|
||||
IECVT_UNASSIGNED,
|
||||
enum IecGlueValueType
|
||||
{
|
||||
IECVT_BOOL = 0,
|
||||
IECVT_BYTE,
|
||||
IECVT_SINT,
|
||||
IECVT_USINT,
|
||||
IECVT_INT,
|
||||
IECVT_UINT,
|
||||
IECVT_WORD,
|
||||
IECVT_DINT,
|
||||
IECVT_UDINT,
|
||||
IECVT_DWORD,
|
||||
IECVT_REAL,
|
||||
IECVT_LREAL,
|
||||
IECVT_LWORD,
|
||||
IECVT_LINT,
|
||||
IECVT_ULINT,
|
||||
IECVT_UNASSIGNED,
|
||||
};
|
||||
#endif // OPLC_IEC_GLUE_VALUE_TYPE
|
||||
|
||||
#ifndef OPLC_GLUE_VARIABLE
|
||||
#define OPLC_GLUE_VARIABLE
|
||||
|
||||
/// Defines the mapping for a glued variable.
|
||||
struct GlueVariable {
|
||||
struct GlueVariable
|
||||
{
|
||||
/// The type of the glue variable.
|
||||
IecGlueValueType type;
|
||||
/// A pointer to the memory address for reading/writing the value.
|
||||
void* value;
|
||||
};
|
||||
|
||||
#endif // OPLC_GLUE_VARIABLE
|
||||
|
||||
//Internal buffers for I/O and memory. These buffers are defined in the
|
||||
//auto-generated glueVars.cpp file
|
||||
#define BUFFER_SIZE 1024
|
||||
const uint16_t BUFFER_SIZE = 1024;
|
||||
|
||||
//Booleans
|
||||
IEC_BOOL* bool_input[BUFFER_SIZE][8];
|
||||
|
@ -92,31 +96,31 @@ IEC_LINT* special_functions[BUFFER_SIZE];
|
|||
#include "LOCATED_VARIABLES.h"
|
||||
#undef __LOCATED_VAR
|
||||
|
||||
void glueVars()
|
||||
void glueVars() // NS
|
||||
{
|
||||
}
|
||||
|
||||
/// The size of the array of input variables
|
||||
extern std::uint16_t const OPLCGLUE_INPUT_SIZE(0);
|
||||
GlueVariable oplc_input_vars[] = {
|
||||
{ IECVT_UNASSIGNED, nullptr },
|
||||
{ IECVT_UNASSIGNED, nullptr },
|
||||
};
|
||||
|
||||
/// The size of the array of output variables
|
||||
extern std::uint16_t const OPLCGLUE_OUTPUT_SIZE(0);
|
||||
GlueVariable oplc_output_vars[] = {
|
||||
{ IECVT_UNASSIGNED, nullptr },
|
||||
{ IECVT_UNASSIGNED, nullptr },
|
||||
};
|
||||
|
||||
void updateTime()
|
||||
void updateTime() // NS
|
||||
{
|
||||
__CURRENT_TIME.tv_nsec += common_ticktime__;
|
||||
__CURRENT_TIME.tv_nsec += common_ticktime__;
|
||||
|
||||
if (__CURRENT_TIME.tv_nsec >= 1000000000)
|
||||
{
|
||||
__CURRENT_TIME.tv_nsec -= 1000000000;
|
||||
__CURRENT_TIME.tv_sec += 1;
|
||||
}
|
||||
if (__CURRENT_TIME.tv_nsec >= 1000000000)
|
||||
{
|
||||
__CURRENT_TIME.tv_nsec -= 1000000000;
|
||||
__CURRENT_TIME.tv_sec += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/** @} */
|
|
@ -31,10 +31,11 @@ using namespace std;
|
|||
SCENARIO("create_config", "")
|
||||
{
|
||||
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};
|
||||
oplc::config_stream cfg_stream(new stringstream(""),
|
||||
[](istream* s) { delete s; });
|
||||
Dnp3IndexedGroup binary_commands = { 0 };
|
||||
Dnp3IndexedGroup analog_commands = { 0 };
|
||||
Dnp3MappedGroup measurements = { 0 };
|
||||
chrono::milliseconds poll_interval(1);
|
||||
std::uint16_t port;
|
||||
|
||||
|
@ -44,7 +45,9 @@ SCENARIO("create_config", "")
|
|||
{
|
||||
GlueVariablesBinding bindings(&glue_mutex, 0, nullptr, nullptr);
|
||||
std::stringstream input_stream;
|
||||
const OutstationStackConfig config(dnp3_create_config(input_stream, bindings, binary_commands, analog_commands, measurements, port, poll_interval));
|
||||
auto config(dnp3_create_config(input_stream, bindings,
|
||||
binary_commands, analog_commands,
|
||||
measurements, port, poll_interval));
|
||||
|
||||
REQUIRE(config.dbConfig.binary.IsEmpty());
|
||||
REQUIRE(config.dbConfig.doubleBinary.IsEmpty());
|
||||
|
@ -61,15 +64,18 @@ SCENARIO("create_config", "")
|
|||
REQUIRE(config.link.RemoteAddr == 1);
|
||||
}
|
||||
|
||||
WHEN("stream specifies size based on glue variables for one boolean")
|
||||
WHEN("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, nullptr);
|
||||
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, poll_interval));
|
||||
auto input = "[dnp3s]\nbind_location=name:%QX0.0,group:1,index:0,";
|
||||
std::stringstream input_stream(input);
|
||||
auto config(dnp3_create_config(input_stream, bindings,
|
||||
binary_commands, analog_commands,
|
||||
measurements, port, poll_interval));
|
||||
|
||||
REQUIRE(config.dbConfig.binary.Size() == 1);
|
||||
REQUIRE(config.dbConfig.doubleBinary.Size() == 0);
|
||||
|
@ -89,15 +95,18 @@ SCENARIO("create_config", "")
|
|||
REQUIRE(measurements.items[0].variable == &(glue_vars[0]));
|
||||
}
|
||||
|
||||
WHEN("stream specifies size based on glue variables for one boolean at index 1")
|
||||
WHEN("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, nullptr);
|
||||
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, poll_interval));
|
||||
auto input = "[dnp3s]\nbind_location=name:%IX0.0,group:1,index:1,";
|
||||
std::stringstream input_stream(input);
|
||||
auto config(dnp3_create_config(input_stream, bindings,
|
||||
binary_commands, analog_commands,
|
||||
measurements, port, poll_interval));
|
||||
|
||||
REQUIRE(config.dbConfig.binary.Size() == 1);
|
||||
REQUIRE(config.dbConfig.doubleBinary.Size() == 0);
|
||||
|
@ -117,15 +126,18 @@ SCENARIO("create_config", "")
|
|||
REQUIRE(measurements.items[0].variable == &(glue_vars[0]));
|
||||
}
|
||||
|
||||
WHEN("stream specifies size based on glue variables for one boolean command at index 1")
|
||||
WHEN("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, nullptr);
|
||||
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, poll_interval));
|
||||
auto input = "[dnp3s]\nbind_location=name:%IX0.0,group:12,index:1,"
|
||||
std::stringstream input_stream(input);
|
||||
auto config(dnp3_create_config(input_stream, bindings,
|
||||
binary_commands, analog_commands,
|
||||
measurements, port, poll_interval));
|
||||
|
||||
REQUIRE(config.dbConfig.binary.Size() == 0);
|
||||
REQUIRE(config.dbConfig.doubleBinary.Size() == 0);
|
||||
|
@ -147,15 +159,18 @@ SCENARIO("create_config", "")
|
|||
REQUIRE(binary_commands.items[1] == &(glue_vars[0]));
|
||||
}
|
||||
|
||||
WHEN("stream specifies size based on glue variables for one real at index 1")
|
||||
WHEN("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, nullptr);
|
||||
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, poll_interval));
|
||||
auto input = "[dnp3s]\nbind_location=name:%QD0,group:30,index:1,";
|
||||
std::stringstream input_stream(input);
|
||||
auto config(dnp3_create_config(input_stream, bindings,
|
||||
binary_commands, analog_commands,
|
||||
measurements, port, poll_interval));
|
||||
|
||||
REQUIRE(config.dbConfig.binary.Size() == 0);
|
||||
REQUIRE(config.dbConfig.doubleBinary.Size() == 0);
|
||||
|
@ -186,7 +201,8 @@ SCENARIO("dnp3s_start_server", "")
|
|||
// Configure this to start and then immediately terminate
|
||||
// the run flag is set to false. This should just return quickly
|
||||
volatile bool run_dnp3(false);
|
||||
unique_ptr<istream, std::function<void(istream*)>> cfg_stream(new stringstream(""), [](istream* s) { delete s; });
|
||||
oplc::config_stream cfg_stream(new stringstream(""),
|
||||
[](istream* s) { delete s; });
|
||||
GlueVariablesBinding bindings(&glue_mutex, 0, nullptr, nullptr);
|
||||
|
||||
dnp3s_start_server(cfg_stream, "20000", run_dnp3, bindings);
|
||||
|
|
|
@ -31,7 +31,7 @@ using namespace fakeit;
|
|||
using namespace opendnp3;
|
||||
|
||||
/// An implementation of the update handler that caches the updates that were
|
||||
/// requested. This imlementation allows us to spy on the behaviour and to know
|
||||
/// requested. This implementation allows us to spy on the behaviour and to know
|
||||
/// during the tests whether the correct updates were called.
|
||||
class UpdateCaptureHandler : public opendnp3::IUpdateHandler {
|
||||
public:
|
||||
|
@ -41,40 +41,66 @@ public:
|
|||
vector<pair<double, uint16_t>> analog_output;
|
||||
|
||||
virtual ~UpdateCaptureHandler() {}
|
||||
virtual bool Update(const Binary& meas, uint16_t index, EventMode mode) {
|
||||
virtual bool Update(const Binary& meas, uint16_t index, EventMode mode)
|
||||
{
|
||||
binary.push_back(std::make_pair(meas.value, index));
|
||||
return true;
|
||||
}
|
||||
virtual bool Update(const DoubleBitBinary& meas, uint16_t index, EventMode mode) {
|
||||
/// Simple mocked implementation.
|
||||
/// @copydoc
|
||||
virtual bool Update(const DoubleBitBinary& meas, uint16_t index, EventMode mode)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
virtual bool Update(const Analog& meas, uint16_t index, EventMode mode) {
|
||||
/// Simple mocked implementation.
|
||||
/// @copydoc
|
||||
virtual bool Update(const Analog& meas, uint16_t index, EventMode mode)
|
||||
{
|
||||
analog.push_back(std::make_pair(meas.value, index));
|
||||
return true;
|
||||
}
|
||||
virtual bool Update(const Counter& meas, uint16_t index, EventMode mode) {
|
||||
/// Simple mocked implementation.
|
||||
/// @copydoc
|
||||
virtual bool Update(const Counter& meas, uint16_t index, EventMode mode)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
virtual bool Update(const FrozenCounter& meas, uint16_t index, EventMode mode) {
|
||||
/// Simple mocked implementation.
|
||||
/// @copydoc
|
||||
virtual bool Update(const FrozenCounter& meas, uint16_t index, EventMode mode)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
virtual bool Update(const BinaryOutputStatus& meas, uint16_t index, EventMode mode) {
|
||||
/// Simple mocked implementation.
|
||||
/// @copydoc
|
||||
virtual bool Update(const BinaryOutputStatus& meas, uint16_t index, EventMode mode)
|
||||
{
|
||||
binary_output.push_back(std::make_pair(meas.value, index));
|
||||
return true;
|
||||
}
|
||||
virtual bool Update(const AnalogOutputStatus& meas, uint16_t index, EventMode mode) {
|
||||
/// Simple mocked implementation.
|
||||
/// @copydoc
|
||||
virtual bool Update(const AnalogOutputStatus& meas, uint16_t index, EventMode mode)
|
||||
{
|
||||
analog_output.push_back(std::make_pair(meas.value, index));
|
||||
return true;
|
||||
}
|
||||
virtual bool Update(const TimeAndInterval& meas, uint16_t index) {
|
||||
/// Simple mocked implementation.
|
||||
/// @copydoc
|
||||
virtual bool Update(const TimeAndInterval& meas, uint16_t index)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
virtual bool Modify(FlagsType type, uint16_t start, uint16_t stop, uint8_t flags) {
|
||||
/// Simple mocked implementation.
|
||||
/// @copydoc
|
||||
virtual bool Modify(FlagsType type, uint16_t start, uint16_t stop, uint8_t flags)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
SCENARIO("dnp3 publisher", "ExchangeGlue") {
|
||||
SCENARIO("dnp3 publisher", "ExchangeGlue")
|
||||
{
|
||||
Mock<asiodnp3::IOutstation> mock_outstation;
|
||||
UpdateCaptureHandler update_handler;
|
||||
|
||||
|
@ -86,18 +112,26 @@ SCENARIO("dnp3 publisher", "ExchangeGlue") {
|
|||
Dnp3MappedGroup measurements = {0};
|
||||
Dnp3Publisher publisher(outstation, measurements);
|
||||
|
||||
GIVEN("No glued measurements") {
|
||||
GIVEN("No glued measurements")
|
||||
{
|
||||
auto num_writes = publisher.ExchangeGlue();
|
||||
THEN("Writes nothing") {
|
||||
THEN("Writes nothing")
|
||||
{
|
||||
REQUIRE(num_writes == 0);
|
||||
}
|
||||
}
|
||||
|
||||
GIVEN("Boolean input variable at offset 0") {
|
||||
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 } };
|
||||
auto group = GlueBoolGroup {
|
||||
.index = 0,
|
||||
.values = { &bool_val, nullptr, nullptr, nullptr,
|
||||
nullptr, nullptr, nullptr, nullptr }
|
||||
};
|
||||
|
||||
const GlueVariable glue_var = { IECLDT_OUT, IECLST_BIT, 0, 0, IECVT_BOOL, &group };
|
||||
const GlueVariable glue_var = { IECLDT_OUT, IECLST_BIT, 0, 0,
|
||||
IECVT_BOOL, &group };
|
||||
DNP3MappedGlueVariable mapped_vars[] = { {
|
||||
.group = 1,
|
||||
.point_index_number = 0,
|
||||
|
@ -109,11 +143,13 @@ SCENARIO("dnp3 publisher", "ExchangeGlue") {
|
|||
measurements.items = mapped_vars;
|
||||
Dnp3Publisher publisher(outstation, measurements);
|
||||
|
||||
WHEN("value is false") {
|
||||
WHEN("value is false")
|
||||
{
|
||||
bool_val = false;
|
||||
auto num_writes = publisher.ExchangeGlue();
|
||||
|
||||
THEN("Writes binary input false") {
|
||||
THEN("Writes binary input false")
|
||||
{
|
||||
REQUIRE(num_writes == 1);
|
||||
Verify(Method(mock_outstation, Apply)).Exactly(Once);
|
||||
REQUIRE(update_handler.binary.size() == 1);
|
||||
|
@ -122,11 +158,13 @@ SCENARIO("dnp3 publisher", "ExchangeGlue") {
|
|||
}
|
||||
}
|
||||
|
||||
WHEN("value is true") {
|
||||
WHEN("value is true")
|
||||
{
|
||||
bool_val = true;
|
||||
auto num_writes = publisher.ExchangeGlue();
|
||||
|
||||
THEN("Writes binary input true") {
|
||||
THEN("Writes binary input true")
|
||||
{
|
||||
REQUIRE(num_writes == 1);
|
||||
Verify(Method(mock_outstation, Apply)).Exactly(Once);
|
||||
REQUIRE(update_handler.binary.size() == 1);
|
||||
|
@ -136,11 +174,17 @@ SCENARIO("dnp3 publisher", "ExchangeGlue") {
|
|||
}
|
||||
}
|
||||
|
||||
GIVEN("Boolean output status variable at offset 0") {
|
||||
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 } };
|
||||
auto group = GlueBoolGroup {
|
||||
.index = 0,
|
||||
.values = { &bool_val, nullptr, nullptr, nullptr,
|
||||
nullptr, nullptr, nullptr, nullptr }
|
||||
};
|
||||
|
||||
const GlueVariable glue_var = { IECLDT_OUT, IECLST_BIT, 0, 0, IECVT_BOOL, &group };
|
||||
const GlueVariable glue_var = { IECLDT_OUT, IECLST_BIT, 0, 0,
|
||||
IECVT_BOOL, &group };
|
||||
DNP3MappedGlueVariable mapped_vars[] = { {
|
||||
.group = 10,
|
||||
.point_index_number = 0,
|
||||
|
@ -152,11 +196,13 @@ SCENARIO("dnp3 publisher", "ExchangeGlue") {
|
|||
measurements.items = mapped_vars;
|
||||
Dnp3Publisher publisher(outstation, measurements);
|
||||
|
||||
WHEN("value is false") {
|
||||
WHEN("value is false")
|
||||
{
|
||||
bool_val = false;
|
||||
auto num_writes = publisher.ExchangeGlue();
|
||||
|
||||
THEN("Writes binary input false") {
|
||||
THEN("Writes binary input false")
|
||||
{
|
||||
REQUIRE(num_writes == 1);
|
||||
Verify(Method(mock_outstation, Apply)).Exactly(Once);
|
||||
REQUIRE(update_handler.binary_output.size() == 1);
|
||||
|
@ -165,11 +211,13 @@ SCENARIO("dnp3 publisher", "ExchangeGlue") {
|
|||
}
|
||||
}
|
||||
|
||||
WHEN("value is true") {
|
||||
WHEN("value is true")
|
||||
{
|
||||
bool_val = true;
|
||||
auto num_writes = publisher.ExchangeGlue();
|
||||
|
||||
THEN("Writes binary input true") {
|
||||
THEN("Writes binary input true")
|
||||
{
|
||||
REQUIRE(num_writes == 1);
|
||||
Verify(Method(mock_outstation, Apply)).Exactly(Once);
|
||||
REQUIRE(update_handler.binary_output.size() == 1);
|
||||
|
@ -179,10 +227,12 @@ SCENARIO("dnp3 publisher", "ExchangeGlue") {
|
|||
}
|
||||
}
|
||||
|
||||
GIVEN("Real variable at offset 0") {
|
||||
GIVEN("Real variable at offset 0")
|
||||
{
|
||||
IEC_REAL real_val(9);
|
||||
|
||||
const GlueVariable glue_var = { IECLDT_OUT, IECLST_DOUBLEWORD, 0, 0, IECVT_REAL, &real_val };
|
||||
const GlueVariable glue_var = { IECLDT_OUT, IECLST_DOUBLEWORD, 0, 0,
|
||||
IECVT_REAL, &real_val };
|
||||
DNP3MappedGlueVariable mapped_vars[] = { {
|
||||
.group = 30,
|
||||
.point_index_number = 0,
|
||||
|
@ -194,10 +244,12 @@ SCENARIO("dnp3 publisher", "ExchangeGlue") {
|
|||
measurements.items = mapped_vars;
|
||||
Dnp3Publisher publisher(outstation, measurements);
|
||||
|
||||
WHEN("value is 9") {
|
||||
WHEN("value is 9")
|
||||
{
|
||||
auto num_writes = publisher.ExchangeGlue();
|
||||
|
||||
THEN("Writes binary input false") {
|
||||
THEN("Writes binary input false")
|
||||
{
|
||||
REQUIRE(num_writes == 1);
|
||||
Verify(Method(mock_outstation, Apply)).Exactly(Once);
|
||||
REQUIRE(update_handler.analog.size() == 1);
|
||||
|
@ -207,10 +259,12 @@ SCENARIO("dnp3 publisher", "ExchangeGlue") {
|
|||
}
|
||||
}
|
||||
|
||||
GIVEN("Real status variable at offset 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 };
|
||||
const GlueVariable glue_var = { IECLDT_OUT, IECLST_DOUBLEWORD, 0, 0,
|
||||
IECVT_REAL, &real_val };
|
||||
DNP3MappedGlueVariable mapped_vars[] = { {
|
||||
.group = 40,
|
||||
.point_index_number = 0,
|
||||
|
@ -222,10 +276,12 @@ SCENARIO("dnp3 publisher", "ExchangeGlue") {
|
|||
measurements.items = mapped_vars;
|
||||
Dnp3Publisher publisher(outstation, measurements);
|
||||
|
||||
WHEN("value is 9") {
|
||||
WHEN("value is 9")
|
||||
{
|
||||
auto num_writes = publisher.ExchangeGlue();
|
||||
|
||||
THEN("Writes binary input false") {
|
||||
THEN("Writes binary input false")
|
||||
{
|
||||
REQUIRE(num_writes == 1);
|
||||
Verify(Method(mock_outstation, Apply)).Exactly(Once);
|
||||
REQUIRE(update_handler.analog_output.size() == 1);
|
||||
|
|
|
@ -55,9 +55,14 @@ SCENARIO("dnp3 receiver", "Receiver") {
|
|||
|
||||
GIVEN("One boolean command") {
|
||||
IEC_BOOL bool_val(0);
|
||||
auto group = GlueBoolGroup { .index=0, .values={ &bool_val, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr } };
|
||||
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_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;
|
||||
|
@ -73,7 +78,8 @@ 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(receiver.Operate(crob, 0, OperateType::DirectOperate)
|
||||
== CommandStatus::SUCCESS);
|
||||
|
||||
receiver.ExchangeGlue();
|
||||
|
||||
|
@ -86,7 +92,8 @@ 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(receiver.Operate(crob, 0, OperateType::DirectOperate)
|
||||
== CommandStatus::SUCCESS);
|
||||
|
||||
receiver.ExchangeGlue();
|
||||
|
||||
|
@ -97,7 +104,8 @@ SCENARIO("dnp3 receiver", "Receiver") {
|
|||
|
||||
GIVEN("One analog 16 output glue") {
|
||||
IEC_SINT int_val(0);
|
||||
const GlueVariable glue_var = { IECLDT_IN, IECLST_BYTE, 0, 0, IECVT_SINT, &int_val };
|
||||
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;
|
||||
|
@ -113,7 +121,8 @@ SCENARIO("dnp3 receiver", "Receiver") {
|
|||
|
||||
THEN("sets output to 9") {
|
||||
REQUIRE(receiver.Select(aoi, 0) == CommandStatus::SUCCESS);
|
||||
REQUIRE(receiver.Operate(aoi, 0, OperateType::DirectOperate) == CommandStatus::SUCCESS);
|
||||
REQUIRE(receiver.Operate(aoi, 0, OperateType::DirectOperate)
|
||||
== CommandStatus::SUCCESS);
|
||||
|
||||
receiver.ExchangeGlue();
|
||||
|
||||
|
@ -124,7 +133,8 @@ SCENARIO("dnp3 receiver", "Receiver") {
|
|||
|
||||
GIVEN("One analog 32 output glue") {
|
||||
IEC_INT int_val(0);
|
||||
const GlueVariable glue_var = { IECLDT_IN, IECLST_WORD, 0, 0, IECVT_INT, &int_val };
|
||||
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;
|
||||
|
@ -141,7 +151,8 @@ 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(receiver.Operate(aoi, 0, OperateType::DirectOperate)
|
||||
== CommandStatus::SUCCESS);
|
||||
|
||||
receiver.ExchangeGlue();
|
||||
|
||||
|
@ -152,7 +163,8 @@ SCENARIO("dnp3 receiver", "Receiver") {
|
|||
|
||||
GIVEN("One float 32 output glue") {
|
||||
IEC_LINT int_val(0);
|
||||
const GlueVariable glue_var = { IECLDT_IN, IECLST_DOUBLEWORD, 0, 0, IECVT_LINT, &int_val };
|
||||
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;
|
||||
|
|
|
@ -1,4 +1,18 @@
|
|||
// 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.
|
||||
|
||||
int mcp23008Setup(const int pinBase, const int i2cAddress)
|
||||
{
|
||||
return 0;
|
||||
return 0;
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
// 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.
|
|
@ -1,17 +1,31 @@
|
|||
// 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.
|
||||
|
||||
#define INPUT 0
|
||||
#define OUTPUT 1
|
||||
|
||||
#define PUD_DOWN 1
|
||||
#define PUD_UP 2
|
||||
#define PUD_DOWN 1
|
||||
#define PUD_UP 2
|
||||
|
||||
#define PWM_OUTPUT 2
|
||||
#define PWM_MODE_MS 0
|
||||
#define PWM_OUTPUT 2
|
||||
#define PWM_MODE_MS 0
|
||||
|
||||
int wiringPiSetup(void) {}
|
||||
|
||||
int digitalRead(int pin)
|
||||
{
|
||||
return 0;
|
||||
return 0;
|
||||
}
|
||||
void digitalWrite(int pin, int value) {}
|
||||
void pwmWrite(int pin, int value) {}
|
||||
|
|
|
@ -1,8 +1,22 @@
|
|||
// 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.
|
||||
|
||||
int wiringPiI2CWrite(int fd, int data)
|
||||
{
|
||||
return 0;
|
||||
return 0;
|
||||
}
|
||||
int wiringPiI2CSetup(const int devId)
|
||||
{
|
||||
return 0;
|
||||
return 0;
|
||||
}
|
|
@ -1,9 +1,23 @@
|
|||
// 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.
|
||||
|
||||
int wiringPiSPIDataRW(int channel, unsigned char* data, int len)
|
||||
{
|
||||
return 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int wiringPiSPISetup(int channel, int speed)
|
||||
{
|
||||
return 0;
|
||||
return 0;
|
||||
}
|
||||
|
|
|
@ -1,13 +1,27 @@
|
|||
// 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.
|
||||
|
||||
int serialOpen(const char* device, const int baud)
|
||||
{
|
||||
return 0;
|
||||
return 0;
|
||||
}
|
||||
void serialPutchar(const int fd, const unsigned char c) {}
|
||||
int serialDataAvail(const int fd)
|
||||
{
|
||||
return 0;
|
||||
return 0;
|
||||
}
|
||||
int serialGetchar(const int fd)
|
||||
{
|
||||
return 0;
|
||||
return 0;
|
||||
}
|
|
@ -28,7 +28,11 @@ SCENARIO("indexed_strategy", "")
|
|||
GIVEN("glue variables with single bit output glue variable")
|
||||
{
|
||||
IEC_BOOL bool_val(0);
|
||||
auto group = GlueBoolGroup { .index=0, .values={ &bool_val, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr } };
|
||||
auto group = GlueBoolGroup {
|
||||
.index = 0,
|
||||
.values = { &bool_val, nullptr, nullptr, nullptr,
|
||||
nullptr, nullptr, nullptr, nullptr }
|
||||
};
|
||||
|
||||
const GlueVariable glue_vars[] = {
|
||||
{ IECLDT_OUT, IECLST_BIT, 0, 0, IECVT_BOOL, &group },
|
||||
|
|
|
@ -40,14 +40,14 @@ SCENARIO("slave", "")
|
|||
IEC_BOOL bool_val7(1);
|
||||
IEC_BOOL bool_val8(1);
|
||||
auto group0 = GlueBoolGroup {
|
||||
.index=0,
|
||||
.values={
|
||||
.index = 0,
|
||||
.values = {
|
||||
&bool_val0, &bool_val1, &bool_val2, &bool_val3, &bool_val4, &bool_val5, &bool_val6, &bool_val7
|
||||
}
|
||||
};
|
||||
auto group1 = GlueBoolGroup {
|
||||
.index=1,
|
||||
.values={
|
||||
.index = 1,
|
||||
.values = {
|
||||
&bool_val8, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr
|
||||
}
|
||||
};
|
||||
|
@ -65,7 +65,7 @@ SCENARIO("slave", "")
|
|||
{
|
||||
// FS, Start, Num, CRC
|
||||
uint8_t buffer[BUF_SIZE] = { HEADER, 1, 0, 0, 0, 9, 0 };
|
||||
int size = modbus_process_message(buffer, BUF_SIZE, &strategy);
|
||||
int16_t size = modbus_process_message(buffer, BUF_SIZE, &strategy);
|
||||
REQUIRE(buffer[7] == 0x01);
|
||||
REQUIRE(buffer[8] == 0x02);
|
||||
REQUIRE(buffer[9] == 0xCD);
|
||||
|
@ -77,7 +77,7 @@ SCENARIO("slave", "")
|
|||
{
|
||||
// FS, Start, Num, CRC
|
||||
uint8_t buffer[BUF_SIZE] = { HEADER, 1, 0, 1, 0, 8, 0 };
|
||||
int size = modbus_process_message(buffer, BUF_SIZE, &strategy);
|
||||
int16_t size = modbus_process_message(buffer, BUF_SIZE, &strategy);
|
||||
REQUIRE(buffer[7] == 0x01);
|
||||
REQUIRE(buffer[8] == 0x01);
|
||||
REQUIRE(buffer[9] == 0xE6);
|
||||
|
@ -88,7 +88,7 @@ SCENARIO("slave", "")
|
|||
{
|
||||
// FS, Start, Num, By, Values, CRC
|
||||
uint8_t buffer[BUF_SIZE] = { HEADER, 15, 0, 0, 0, 9, 2, 0xCD, 0x01, 0 };
|
||||
int size = modbus_process_message(buffer, BUF_SIZE, &strategy);
|
||||
int16_t size = modbus_process_message(buffer, BUF_SIZE, &strategy);
|
||||
REQUIRE(buffer[7] == 15);
|
||||
REQUIRE(buffer[8] == 0);
|
||||
REQUIRE(buffer[9] == 0);
|
||||
|
@ -110,14 +110,14 @@ SCENARIO("slave", "")
|
|||
IEC_BOOL bool_val7(1);
|
||||
IEC_BOOL bool_val8(1);
|
||||
auto group0 = GlueBoolGroup {
|
||||
.index=0,
|
||||
.values={
|
||||
.index = 0,
|
||||
.values = {
|
||||
&bool_val0, &bool_val1, &bool_val2, &bool_val3, &bool_val4, &bool_val5, &bool_val6, &bool_val7
|
||||
}
|
||||
};
|
||||
auto group1 = GlueBoolGroup {
|
||||
.index=1,
|
||||
.values={
|
||||
.index = 1,
|
||||
.values = {
|
||||
&bool_val8, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr
|
||||
}
|
||||
};
|
||||
|
@ -135,7 +135,7 @@ SCENARIO("slave", "")
|
|||
{
|
||||
// FS, Start, Num, CRC
|
||||
uint8_t buffer[BUF_SIZE] = { HEADER, 2, 0, 0, 0, 9, 0 };
|
||||
int size = modbus_process_message(buffer, BUF_SIZE, &strategy);
|
||||
int16_t size = modbus_process_message(buffer, BUF_SIZE, &strategy);
|
||||
REQUIRE(buffer[7] == 0x02);
|
||||
REQUIRE(buffer[8] == 0x02);
|
||||
REQUIRE(buffer[9] == 0xCD);
|
||||
|
@ -147,7 +147,7 @@ SCENARIO("slave", "")
|
|||
{
|
||||
// FS, Start, Num, CRC
|
||||
uint8_t buffer[BUF_SIZE] = { HEADER, 2, 0, 1, 0, 8, 0 };
|
||||
int size = modbus_process_message(buffer, BUF_SIZE, &strategy);
|
||||
int16_t size = modbus_process_message(buffer, BUF_SIZE, &strategy);
|
||||
REQUIRE(buffer[7] == 0x02);
|
||||
REQUIRE(buffer[8] == 0x01);
|
||||
REQUIRE(buffer[9] == 0xE6);
|
||||
|
@ -176,7 +176,7 @@ SCENARIO("slave", "")
|
|||
{
|
||||
// FS, Start, Num, CRC
|
||||
uint8_t buffer[BUF_SIZE] = { HEADER, 3, 0, 0, 0, 1, 0 };
|
||||
int size = modbus_process_message(buffer, BUF_SIZE, &strategy);
|
||||
int16_t size = modbus_process_message(buffer, BUF_SIZE, &strategy);
|
||||
REQUIRE(buffer[7] == 0x03);
|
||||
REQUIRE(buffer[8] == 0x02);
|
||||
REQUIRE(buffer[9] == 0x00);
|
||||
|
@ -188,7 +188,7 @@ SCENARIO("slave", "")
|
|||
{
|
||||
// FS, Start, Num, CRC
|
||||
uint8_t buffer[BUF_SIZE] = { HEADER, 3, 0x04, 0, 0, 1, 0 };
|
||||
int size = modbus_process_message(buffer, BUF_SIZE, &strategy);
|
||||
int16_t size = modbus_process_message(buffer, BUF_SIZE, &strategy);
|
||||
REQUIRE(buffer[7] == 0x03);
|
||||
REQUIRE(buffer[8] == 0x02);
|
||||
REQUIRE(buffer[9] == 0x00);
|
||||
|
@ -200,7 +200,7 @@ SCENARIO("slave", "")
|
|||
{
|
||||
// FS, Start, Val, CRC
|
||||
uint8_t buffer[BUF_SIZE] = { HEADER, 6, 0, 0, 0, 0x03, 0 };
|
||||
int size = modbus_process_message(buffer, BUF_SIZE, &strategy);
|
||||
int16_t size = modbus_process_message(buffer, BUF_SIZE, &strategy);
|
||||
REQUIRE(buffer[7] == 0x06);
|
||||
REQUIRE(buffer[8] == 0x00);
|
||||
REQUIRE(buffer[9] == 0x00);
|
||||
|
@ -213,7 +213,7 @@ SCENARIO("slave", "")
|
|||
{
|
||||
// FS, Start, Num, By, Val, CRC
|
||||
uint8_t buffer[BUF_SIZE] = { HEADER, 16, 0, 0, 0, 1, 2, 0, 0x02, 0 };
|
||||
int size = modbus_process_message(buffer, BUF_SIZE, &strategy);
|
||||
int16_t size = modbus_process_message(buffer, BUF_SIZE, &strategy);
|
||||
REQUIRE(buffer[7] == 16);
|
||||
REQUIRE(buffer[8] == 0x00);
|
||||
REQUIRE(buffer[9] == 0x00);
|
||||
|
@ -240,7 +240,7 @@ SCENARIO("slave", "")
|
|||
{
|
||||
// FS, Start, Num, CRC
|
||||
uint8_t buffer[BUF_SIZE] = { HEADER, 4, 0, 0, 0, 1, 0 };
|
||||
int size = modbus_process_message(buffer, BUF_SIZE, &strategy);
|
||||
int16_t size = modbus_process_message(buffer, BUF_SIZE, &strategy);
|
||||
REQUIRE(buffer[7] == 0x04);
|
||||
REQUIRE(buffer[8] == 0x02);
|
||||
REQUIRE(buffer[9] == 0x00);
|
||||
|
@ -253,8 +253,8 @@ SCENARIO("slave", "")
|
|||
{
|
||||
IEC_BOOL bool_val1(0);
|
||||
auto group1 = GlueBoolGroup {
|
||||
.index=0,
|
||||
.values={
|
||||
.index = 0,
|
||||
.values = {
|
||||
&bool_val1, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr
|
||||
}
|
||||
};
|
||||
|
@ -270,7 +270,7 @@ SCENARIO("slave", "")
|
|||
{
|
||||
// FS, Start, Num, CRC
|
||||
uint8_t buffer[BUF_SIZE] = { HEADER, 5, 0, 0, 0xFF, 0, 0 };
|
||||
int size = modbus_process_message(buffer, BUF_SIZE, &strategy);
|
||||
int16_t size = modbus_process_message(buffer, BUF_SIZE, &strategy);
|
||||
REQUIRE(buffer[7] == 0x05);
|
||||
REQUIRE(buffer[8] == 0x00);
|
||||
REQUIRE(buffer[9] == 0x00);
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
#include "catch.hpp"
|
||||
|
||||
using Catch::Matchers::Contains;
|
||||
using namespace spdlog;
|
||||
|
||||
SCENARIO("logsink", "")
|
||||
{
|
||||
|
@ -41,7 +42,8 @@ SCENARIO("logsink", "")
|
|||
|
||||
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()));
|
||||
details::log_msg msg(&logger_name, level::info,
|
||||
string_view_t(buf.data(), buf.size()));
|
||||
|
||||
sink->log(msg);
|
||||
std::string data = sink->data();
|
||||
|
@ -54,11 +56,13 @@ SCENARIO("logsink", "")
|
|||
|
||||
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()));
|
||||
details::log_msg msg1(&logger_name, level::info,
|
||||
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()));
|
||||
details::log_msg msg2(&logger_name, level::info,
|
||||
string_view_t(buf2.data(), buf2.size()));
|
||||
|
||||
sink->log(msg1);
|
||||
sink->log(msg2);
|
||||
|
@ -73,11 +77,13 @@ SCENARIO("logsink", "")
|
|||
|
||||
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()));
|
||||
details::log_msg msg1(&logger_name, level::info,
|
||||
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()));
|
||||
details::log_msg msg2(&logger_name, level::info,
|
||||
string_view_t(buf2.data(), buf2.size()));
|
||||
|
||||
sink->log(msg1);
|
||||
sink->log(msg2);
|
||||
|
@ -86,17 +92,20 @@ SCENARIO("logsink", "")
|
|||
REQUIRE_THAT(data, Contains("There\n"));
|
||||
}
|
||||
|
||||
WHEN("message is longer than buffer size then truncates but still has newline")
|
||||
WHEN("message is longer than buffer size truncates 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::format_to(buf, "012345678901234567890123456789"\
|
||||
"01234567890123456789ABCDEFG");
|
||||
details::log_msg msg(&logger_name, level::info,
|
||||
string_view_t(buf.data(), buf.size()));
|
||||
|
||||
sink->log(msg);
|
||||
std::string data = sink->data();
|
||||
REQUIRE_THAT(data, Contains("0123456789012345678901234567890123456789012345678"));
|
||||
REQUIRE_THAT(data, Contains("012345678901234567890123456789"\
|
||||
"0123456789012345678"));
|
||||
}
|
||||
|
||||
WHEN("multiple messages exhaust buffer then starts from beginning")
|
||||
|
@ -105,11 +114,13 @@ SCENARIO("logsink", "")
|
|||
|
||||
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()));
|
||||
details::log_msg msg(&logger_name, level::info,
|
||||
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()));
|
||||
details::log_msg msg2(&logger_name, level::info,
|
||||
string_view_t(buf2.data(), buf2.size()));
|
||||
|
||||
sink->log(msg);
|
||||
sink->log(msg);
|
||||
|
|
|
@ -26,43 +26,52 @@ using namespace std;
|
|||
|
||||
#define IS_BIG_ENDIAN (*(uint16_t *)"\0\xff" < 0x100)
|
||||
|
||||
const char VALID_HEADER[] = { (char)137, 'O', 'P', 'L', 'C', 'P', 'S', '\n', 'v', 0, '\n' };
|
||||
const char* CHECKSUM_HEADER = "16d15b8416040cce48b111ce03ee3dab";
|
||||
|
||||
SCENARIO("pstorage_read", "") {
|
||||
const char VALID_HEADER[] = { static_cast<char>(137), 'O', 'P', 'L', 'C',
|
||||
'P', 'S', '\n', 'v', 0, '\n' };
|
||||
const char* CHECKSUM = "16d15b8416040cce48b111ce03ee3dab";
|
||||
|
||||
SCENARIO("pstorage_read", "")
|
||||
{
|
||||
mutex glue_mutex;
|
||||
stringstream input_stream;
|
||||
input_stream.write(VALID_HEADER, 11);
|
||||
char endian_header[2] = { IS_BIG_ENDIAN, '\n'};
|
||||
input_stream.write(endian_header, 2);
|
||||
input_stream.write(CHECKSUM_HEADER, strlen(CHECKSUM_HEADER));
|
||||
input_stream.write(CHECKSUM, strlen(CHECKSUM));
|
||||
input_stream.put('\n');
|
||||
|
||||
GIVEN("simple glue variables") {
|
||||
GIVEN("simple glue variables")
|
||||
{
|
||||
IEC_LWORD lword_var = 0;
|
||||
IEC_SINT usint_var = 0;
|
||||
IEC_BOOL bool_var = 0;
|
||||
GlueBoolGroup grp { .index=0, .values = { &bool_var, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr} };
|
||||
GlueBoolGroup grp {
|
||||
.index = 0,
|
||||
.values = { &bool_var, nullptr, nullptr, nullptr,
|
||||
nullptr, nullptr, nullptr, nullptr}
|
||||
};
|
||||
const GlueVariable glue_vars[] = {
|
||||
{ IECLDT_MEM, IECLST_DOUBLEWORD, 0, 0, IECVT_LWORD, &lword_var },
|
||||
{ IECLDT_MEM, IECLST_BYTE, 0, 0, IECVT_USINT, &usint_var },
|
||||
{ IECLDT_MEM, IECLST_BIT, 0, 0, IECVT_BOOL, &grp },
|
||||
};
|
||||
GlueVariablesBinding bindings(&glue_mutex, 3, glue_vars, CHECKSUM_HEADER);
|
||||
GlueVariablesBinding bindings(&glue_mutex, 3, glue_vars, CHECKSUM);
|
||||
|
||||
WHEN("no data") {
|
||||
WHEN("no data")
|
||||
{
|
||||
input_stream.seekg(0);
|
||||
REQUIRE(pstorage_read(input_stream, bindings) != 0);
|
||||
}
|
||||
|
||||
WHEN("data too short") {
|
||||
WHEN("data too short")
|
||||
{
|
||||
input_stream << '1';
|
||||
input_stream.seekg(0);
|
||||
REQUIRE(pstorage_read(input_stream, bindings) != 0);
|
||||
}
|
||||
|
||||
WHEN("data is valid and all zero") {
|
||||
WHEN("data is valid and all zero")
|
||||
{
|
||||
char buffer[6] = {0};
|
||||
input_stream.write(buffer, 6);
|
||||
input_stream.seekg(0);
|
||||
|
@ -73,7 +82,8 @@ SCENARIO("pstorage_read", "") {
|
|||
REQUIRE(bool_var == 0);
|
||||
}
|
||||
|
||||
WHEN("data is valid and all one") {
|
||||
WHEN("data is valid and all one")
|
||||
{
|
||||
IEC_LWORD lword_initial_value = 1;
|
||||
IEC_USINT usint_initial_value = 1;
|
||||
char buffer[6] = {0, 0, 0, 0, 1, 1};
|
||||
|
@ -92,17 +102,23 @@ SCENARIO("pstorage_read", "") {
|
|||
}
|
||||
}
|
||||
|
||||
GIVEN("one bool group") {
|
||||
GIVEN("one bool group")
|
||||
{
|
||||
IEC_BOOL bool_var0 = 0;
|
||||
IEC_BOOL bool_var1 = 0;
|
||||
IEC_BOOL bool_var7 = 0;
|
||||
GlueBoolGroup grp { .index=0, .values = { &bool_var0, &bool_var1, nullptr, nullptr, nullptr, nullptr, nullptr, &bool_var7} };
|
||||
GlueBoolGroup grp {
|
||||
.index = 0,
|
||||
.values = { &bool_var0, &bool_var1, nullptr, nullptr,
|
||||
nullptr, nullptr, nullptr, &bool_var7}
|
||||
};
|
||||
const GlueVariable glue_vars[] = {
|
||||
{ IECLDT_MEM, IECLST_BIT, 0, 0, IECVT_BOOL, &grp },
|
||||
};
|
||||
GlueVariablesBinding bindings(&glue_mutex, 1, glue_vars, CHECKSUM_HEADER);
|
||||
GlueVariablesBinding bindings(&glue_mutex, 1, glue_vars, CHECKSUM);
|
||||
|
||||
WHEN("data is valid and mixture of bits set") {
|
||||
WHEN("data is valid and mixture of bits set")
|
||||
{
|
||||
// We don't (in general) know the endianness to know
|
||||
// the byte order, so we initialize the buffer based on
|
||||
// the actual memory layout
|
||||
|
@ -119,33 +135,43 @@ SCENARIO("pstorage_read", "") {
|
|||
}
|
||||
}
|
||||
|
||||
SCENARIO("pstorage_run") {
|
||||
SCENARIO("pstorage_run")
|
||||
{
|
||||
mutex glue_mutex;
|
||||
stringstream input_stream;
|
||||
input_stream.write(VALID_HEADER, 11);
|
||||
char endian_header[2] = { IS_BIG_ENDIAN, '\n'};
|
||||
input_stream.write(endian_header, 2);
|
||||
input_stream.write(CHECKSUM_HEADER, strlen(CHECKSUM_HEADER));
|
||||
input_stream.write(CHECKSUM, strlen(CHECKSUM));
|
||||
input_stream.put('\n');
|
||||
|
||||
GIVEN("glue variables and stream") {
|
||||
GIVEN("glue variables and stream")
|
||||
{
|
||||
IEC_LWORD lword_var = 1;
|
||||
IEC_SINT usint_var = 2;
|
||||
IEC_BOOL bool_var = 1;
|
||||
GlueBoolGroup grp { .index=0, .values = { &bool_var, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr} };
|
||||
GlueBoolGroup grp {
|
||||
.index = 0,
|
||||
.values = { &bool_var, nullptr, nullptr, nullptr,
|
||||
nullptr, nullptr, nullptr, nullptr}
|
||||
};
|
||||
const GlueVariable glue_vars[] = {
|
||||
{ IECLDT_MEM, IECLST_DOUBLEWORD, 0, 0, IECVT_LWORD, &lword_var },
|
||||
{ IECLDT_MEM, IECLST_BYTE, 0, 0, IECVT_USINT, &usint_var },
|
||||
{ IECLDT_MEM, IECLST_BIT, 0, 0, IECVT_BOOL, &grp },
|
||||
};
|
||||
GlueVariablesBinding bindings(&glue_mutex, 3, glue_vars, CHECKSUM_HEADER);
|
||||
GlueVariablesBinding bindings(&glue_mutex, 3, glue_vars, CHECKSUM);
|
||||
|
||||
unique_ptr<istream, std::function<void(istream*)>> cfg_stream(new stringstream(""), [](istream* s) { delete s; });
|
||||
oplc::config_stream cfg_stream(new stringstream(""),
|
||||
[](istream* s) { delete s; });
|
||||
|
||||
WHEN("write once") {
|
||||
WHEN("write once")
|
||||
{
|
||||
volatile bool run = false;
|
||||
auto create_stream = []() { return new stringstream(); };
|
||||
REQUIRE(pstorage_run(cfg_stream, "0", bindings, run, create_stream) == 0);
|
||||
auto result = pstorage_run(cfg_stream, "0", bindings,
|
||||
run, create_stream);
|
||||
REQUIRE(result == 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -67,7 +67,7 @@ extern unsigned long long common_ticktime__;
|
|||
#ifndef OPLC_IEC_GLUE_DIRECTION
|
||||
#define OPLC_IEC_GLUE_DIRECTION
|
||||
enum IecLocationDirection {
|
||||
IECLDT_IN,
|
||||
IECLDT_IN = 0,
|
||||
IECLDT_OUT,
|
||||
IECLDT_MEM,
|
||||
};
|
||||
|
@ -77,7 +77,7 @@ enum IecLocationDirection {
|
|||
#define OPLC_IEC_GLUE_SIZE
|
||||
enum IecLocationSize {
|
||||
/// Variables that are a single bit
|
||||
IECLST_BIT,
|
||||
IECLST_BIT = 0,
|
||||
/// Variables that are 1 byte
|
||||
IECLST_BYTE,
|
||||
/// Variables that are 2 bytes
|
||||
|
@ -92,7 +92,7 @@ enum IecLocationSize {
|
|||
#ifndef OPLC_IEC_GLUE_VALUE_TYPE
|
||||
#define OPLC_IEC_GLUE_VALUE_TYPE
|
||||
enum IecGlueValueType {
|
||||
IECVT_BOOL,
|
||||
IECVT_BOOL = 0,
|
||||
IECVT_BYTE,
|
||||
IECVT_SINT,
|
||||
IECVT_USINT,
|
||||
|
|
Loading…
Reference in New Issue