PR-786 Address code review feedback from project owner

This commit is contained in:
Garret Fick 2019-11-21 16:06:44 -05:00
parent dc6d2fe0ca
commit 4627b60abe
No known key found for this signature in database
GPG Key ID: A1BBEF9D2AB249C6
42 changed files with 1227 additions and 595 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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