Merge pull request #106 from smartergridsolutions/feature/PR-790

Extend the service model to support before and after cycle operations
This commit is contained in:
Thiago Alves 2019-11-27 12:01:17 -05:00 committed by GitHub
commit b989ae10d7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 1317 additions and 623 deletions

View File

@ -57,7 +57,7 @@ binding = sized
; modbus interface. This starts capabilities to poll one or more Modbus
; servers and exchange data with the located variables.
enabled = true
enabled = false
; We support multiple Modbus masters. Each master should specify
; a complete set of configuration information within this section.
@ -65,31 +65,52 @@ enabled = true
; index of the master. Indices start at 0 and go up from there.
; A user defined name for the connection. This name appears in log messages.
name.0 = 1
; name.0 = 1
; How long to wait for a response before indicating an error.
; Measured in microseconds.
; response_timeout.0 = 100000
; The rate at which to poll the server. Measured in microseconds.
; The actual poll rate may be greater than this depending on response times
; from the remote server and availability of resources on the local host.
; poll_interval.0 = 250000
; The protocol to use for connection. This value must be one of:
; tcp
; rtu
; If you set this to any other value, then this configuration item group
; is not created. You might use this to disable connecting to a particular
; Modbus slave.
protocol.0 = tcp
slave_id.0 = 1
ip_address.0 = 127.0.0.1
ip_port.0 = 1000
; rtu_baud_rate.0 =
; rtu_parity.0
; rtu_data_bit.0
; rtu_stop_bit.0
discrete_inputs_start.0 = 0
discrete_inputs_size.0 = 1
;coils_start.0
;coils_size.0
;input_registers_start.0
;input_registers_size.0
;holding_registers_read_start.0
;holding_registers_read_size.0
;holding_registers_start.0
;holding_registers_size.0
; protocol.0 = tcp
; When there are communication failures, should we keep trying
; or backoff intelligently. The default behaviour is no backoff
; but you can specify one of the following:
;
; none - Same as default, no strategy.
; linear_bounded - Back off linearly based on the number of
; failed attempts. Bounded to 10x the poll
; interval.
; backoff_strategy.0 = linear_bounded
; slave_id.0 = 1
; ip_address.0 = 127.0.0.1
; ip_port.0 = 1000
; rtu_baud_rate.0 =
; rtu_parity.0 =
; rtu_data_bit.0 =
; rtu_stop_bit.0 =
; discrete_inputs_start.0 = 0
; discrete_inputs_size.0 = 1
; coils_start.0
; coils_size.0
; input_registers_start.0
; input_registers_size.0
; holding_registers_read_start.0
; holding_registers_read_size.0
; holding_registers_start.0
; holding_registers_size.0
; ---------------------------------------------------------
; ---------------------------------------------------------
@ -102,7 +123,8 @@ enabled = false
; How long should we wait between write cycle. The persistent storage
; checks at this rate for changes and only persists to disk if a
; value has changed within the poll period
poll_interval = 10
; poll_interval = 10
; poll_interval = 10
; ---------------------------------------------------------
; ---------------------------------------------------------
@ -115,7 +137,7 @@ enabled = false
; Bind OpenPLC bit-sized output 0.0 to DNP3 binary input at index 0
; Note that the second hierarchical index must be 0
bind_location = name:%QX0.0,group:1,index:0,
; bind_location = name:%QX0.0,group:1,index:0,
; Bind OpenPLC word-sized output 2 to DNP3 analog input at index 1
; bind_location = name:%QW2,group:30,index:1,
@ -127,33 +149,32 @@ bind_location = name:%QX0.0,group:1,index:0,
; bind_location = name:%QL2,group:30,index:10,
; Bind OpenPLC bit-sized input 0.0 to DNP3 analog input at index 10
bind_location = name:%IX0.0,group:12,index:0,
bind_location = name:%IX1.0,group:12,index:1,
; bind_location = name:%IX0.0,group:12,index:0,
; bind_location = name:%IX1.0,group:12,index:1,
; Bind OpenPLC word-sized input 2 to DNP3 analog command at index 0
; bind_location = name:%IW2,group:41,index:0,
; TCP Settings
; ------------
address = 127.0.0.1
port = 20000
; address = 127.0.0.1
; port = 20000
; Link Settings
; -------------
; The remote and local address
local_address = 10
remote_address = 1
; local_address = 10
; remote_address = 1
; Keep alive timeout. A value in seconds, or the keyword MAX
; keep_alive_timeout = MAX
; Parameters
; ----------
; Enable unsolicited reporting if master allows it
enable_unsolicited = true
; enable_unsolicited = true
; How long (seconds) the outstation will allow a operate
; to follow a select

View File

@ -69,6 +69,10 @@ int config_handler(void* user_data, const char* section,
{
spdlog::set_level(spdlog::level::err);
}
else
{
spdlog::warn("Unknown log level {}", value);
}
}
else if (strcmp("enabled", name) == 0
&& oplc::ini_atob(value)
@ -105,6 +109,7 @@ void bootstrap()
// If we don't have the config file, then default to always
// starting the interactive server.
config.services.push_back("interactive");
config.services.push_back("modbusmaster");
}
//======================================================

264
runtime/core/buffer.h Normal file
View File

@ -0,0 +1,264 @@
// Copyright 2019 Smarter Grid Solutions
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http ://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissionsand
// limitations under the License.
#ifndef RUNTIME_CORE_BUFFER_H_
#define RUNTIME_CORE_BUFFER_H_
#include <array>
#include <cstring>
#include "glue.h"
/** \addtogroup openplc_runtime
* @{
*/
namespace oplc
{
template<std::size_t N>
struct PackedBoolMappedValues
{
std::array<std::uint8_t, N> cached_values;
std::array<GlueBoolGroup*, N> located_values;
std::size_t max_index;
PackedBoolMappedValues() : max_index(N)
{}
/// @brief Update the cache values with the value from the located
/// variable that was mapped. This only updated cache items for which
/// there is a mapping. Unmapped items are not modified.
void update_cache()
{
for (auto i = 0; i < max_index; ++i)
{
if (located_values[i])
{
auto group = located_values[i];
std::uint8_t bit_set =
(group->values[0] && *group->values[0] ? 0x01 : 0)
| (group->values[1] && *group->values[1] ? 0x02 : 0)
| (group->values[2] && *group->values[2] ? 0x03 : 0)
| (group->values[3] && *group->values[3] ? 0x04 : 0)
| (group->values[4] && *group->values[4] ? 0x10 : 0)
| (group->values[5] && *group->values[5] ? 0x20 : 0)
| (group->values[6] && *group->values[6] ? 0x40 : 0)
| (group->values[7] && *group->values[7] ? 0x80 : 0);
cached_values[i] = bit_set;
}
}
}
/// @brief Update the located values with the value from the cache
/// variable that was mapped. This only updated located items for which
/// there is a mapping. Unmapped items are not modified.
void update_located()
{
for (auto i = 0; i < max_index; ++i)
{
if (located_values[i])
{
auto group = located_values[i];
std::uint8_t cache_item = cached_values[i];
if (group->values[0])
{
*group->values[0] = cache_item & 0x01 ? IEC_TRUE : IEC_FALSE;
}
if (group->values[1])
{
*group->values[1] = cache_item & 0x02 ? IEC_TRUE : IEC_FALSE;
}
if (group->values[2])
{
*group->values[2] = cache_item & 0x04 ? IEC_TRUE : IEC_FALSE;
}
if (group->values[3])
{
*group->values[3] = cache_item & 0x08 ? IEC_TRUE : IEC_FALSE;
}
if (group->values[4])
{
*group->values[4] = cache_item & 0x10 ? IEC_TRUE : IEC_FALSE;
}
if (group->values[5])
{
*group->values[5] = cache_item & 0x20 ? IEC_TRUE : IEC_FALSE;
}
if (group->values[6])
{
*group->values[6] = cache_item & 0x40 ? IEC_TRUE : IEC_FALSE;
}
if (group->values[7])
{
*group->values[7] = cache_item & 0x80 ? IEC_TRUE : IEC_FALSE;
}
}
}
}
/// @brief Copy data from the cache to the destination. This copies from
/// the specified starting index the specified number of items. For
/// example, if the underlying type of this required 2-bytes per item
/// and you copy 10 items, then the destination buffer must be at least
/// 20 bytes in length.
/// @param dest The destination buffer to copy to.
/// @param cache_start An offset from the start of the cache containing
/// the start index in the cache.
/// @param cache_count The number of bytes in the cache to copy into the
/// destination.
void copy_from_cache(std::uint8_t* dest, std::size_t cache_start, std::size_t cache_count)
{
std::memcpy(dest, cached_values.data() + cache_start, cache_count);
}
/// @brief Copy data from the source buffer into this cache. This copies
/// from the beginning fo the source buffer into the cache, starting
/// at the specified index in the cache.
void copy_to_cache(std::uint8_t* src, std::size_t src_size, std::size_t cache_start)
{
std::memcpy(cached_values.data() + cache_start, src, src_size);
}
};
/// A set of mapped values that are stored contiguously.
/// Prefer this structure over the simple MappedValue when
/// allocating ranges of located values and where the cached_values
/// buffer will be used directly for IO (to avoid one copy). However,
/// the tradeoff is that updating the cache must iterate over the entire
/// located values.
template <typename T, std::size_t N>
struct ContiguousMappedValues
{
std::array<T, N> cached_values;
std::array<T*, N> located_values;
std::size_t max_index;
ContiguousMappedValues() : max_index(N)
{}
/// @brief Assign a located value into the structure, keeping track
/// of the maximum assigned index.
/// @param index The index in the located variables to assign.
/// @param value Pointer to the located variable that is being assigned.
void assign(size_t index, T* value)
{
located_values[index] = value;
max_index = std::max(index, max_index);
}
/// @brief Update the cache values with the value from the located
/// variable that was mapped. This only updated cache items for which
/// there is a mapping. Unmapped items are not modified.
void update_cache()
{
for (auto i = 0; i < max_index; ++i)
{
if (located_values[i])
{
cached_values[i] = *located_values[i];
}
}
}
void update_located()
{
for (auto i = 0; i < max_index; ++i)
{
if (located_values[i])
{
*located_values[i] = cached_values[i];
}
}
}
/// @brief Copy data from the cache to the destination. This copies from
/// the specified starting index the specified number of items. For
/// example, if the underlying type of this required 2-bytes per item
/// and you copy 10 items, then the destination buffer must be at least
/// 20 bytes in length.
/// @param dest The destination buffer to copy to.
/// @param cache_start An offset from the start of the cache containing
/// the start index in the cache.
/// @param cache_count The number of items (not bytes) in the cache to
/// copy into the destination.
void copy_from_cache(std::uint8_t* dest, std::size_t cache_start, std::size_t cache_count)
{
std::memcpy(dest, cached_values.data() + cache_start, cache_count * sizeof(T));
}
/// @brief Copy data from the source buffer into this cache. This copies
/// from the beginning fo the source buffer into the cache, starting
/// at the specified index in the cache. Note that the index in the cache
/// is measured in items, not bytes.
/// If the source size is not an multiple of the item size, then this will
/// write part of a cache item, which is probably not desired.
void copy_to_cache(std::uint8_t* src, std::size_t src_size, std::size_t cache_start)
{
std::memcpy(cached_values.data() + cache_start * sizeof(T), src, src_size);
}
};
/// Defines the mapping between a located value
/// and a cached of the located value. This permits
/// an efficient mechanism to exchange data between
/// the cache and located value.
template <typename T>
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)
{
this->value = val;
this->cached_value = *val;
}
inline void update_cache()
{
if (this->value)
{
this->cached_value = *this->value;
}
}
};
/// Defines a write that has been submitted via a protocol
/// but may not have been applied to the located variable yet.
/// Normally this would be employed with an array of MappedValue
/// items having the same indices for efficient lookup to the
/// located variable.
template <typename T>
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) {
this->has_pending = true;
this->value = val;
}
};
} // namespace oplc
/** @}*/
#endif // RUNTIME_CORE_MODBUSMASTER_MASTER_H_

View File

@ -615,7 +615,7 @@ int sendUnitData(struct enip_header *header, struct enip_data_Connected_0x70 *en
// response for it. The return value is the size of the response message in
// bytes.
//-----------------------------------------------------------------------------
int processEnipMessage(unsigned char *buffer, int buffer_size, void* user_data)
int16_t processEnipMessage(unsigned char *buffer, int16_t buffer_size, void* user_data)
{
// initialize logging system
char log_msg[1000];

View File

@ -27,6 +27,9 @@
* @{
*/
#define IEC_TRUE 1
#define IEC_FALSE 0
#ifndef BUFFER_SIZE
#define BUFFER_SIZE 1024
#endif

View File

@ -20,6 +20,7 @@
*/
#include <cstring>
#include <functional>
#include <fstream>
#include <istream>
#include <memory>
@ -51,7 +52,7 @@ inline bool ini_matches(const char* section_expected,
&& strcmp(value_expected, value) == 0;
}
/// Compare a INI declaration name that has a postfix with an index,
/// @brief Compare a INI declaration name that has a postfix with an index,
/// for example "example.1". This returns 0 if the provided name matches
/// the expected value and there is a postfix. Returns the postfix
/// number to the caller.
@ -65,9 +66,14 @@ inline bool ini_matches(const char* section_expected,
/// a marker for the index.
/// @param index If the return value of this function is 0, then this will
/// contain the index that was read as the postfix.
/// @param max_index The maximum index this will return. If the read index
/// is greater than this value, then this will return non-zero.
/// This maximum is designed to prevent unintentionally allocating far more
/// space than would be expected under normal use.
/// @return 0 if there is a match, otherwise non-zero.
inline int cmpnameid(const char* name, const char* expected,
std::uint8_t* index)
inline int strcmp_with_index(const char* name, const char* expected,
std::uint8_t max_index,
std::uint8_t* index)
{
size_t expected_len = strlen(expected);
int ret = strncmp(name, expected, expected_len);
@ -77,16 +83,20 @@ inline int cmpnameid(const char* name, const char* expected,
}
size_t name_len = strlen(name);
if (name_len > expected_len + 1 && name[expected_len] == '.')
if (name_len >= expected_len + 2 && name[expected_len] == '.')
{
*index = atoi(name + (expected_len + 1));
return 0;
auto read_index = atoi(name + (expected_len + 1));
if (read_index <= max_index)
{
*index = read_index;
return 0;
}
}
return -1;
}
/// Implementation for fgets based on istream.
/// @brief Implementation for fgets based on istream.
/// @param str pointer to an array of chars where the string read is copied.
/// @param num Maximum number of characters to be copied into str.
/// @param stream The stream object. The string must be null terminated.

View File

@ -285,8 +285,7 @@ void interactive_client_command(const char* command, int client_fd)
{
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;

View File

@ -122,7 +122,7 @@ extern uint8_t run_openplc;
void handleSpecialFunctions();
// server.cpp
typedef int (*process_message_fn)(unsigned char *buffer, int buffer_size, void* user_data);
typedef std::int16_t (*process_message_fn)(unsigned char *buffer, std::int16_t buffer_size, void* user_data);
void startServer(uint16_t port, volatile bool& run_server, process_message_fn process_message, void* user_data);
int getSO_ERROR(int fd);
void closeSocket(int fd);
@ -134,18 +134,11 @@ extern bool run_enip;
extern time_t start_time;
// enip.cpp
int processEnipMessage(unsigned char *buffer, int buffer_size, void* user_data);
std::int16_t processEnipMessage(unsigned char *buffer, std::int16_t buffer_size, void* user_data);
// pccc.cpp ADDED Ulmer
uint16_t processPCCCMessage(unsigned char *buffer, int buffer_size);
// modbus_master.cpp
void initializeMB();
void *querySlaveDevices(void *arg);
void updateBuffersIn_MB();
void updateBuffersOut_MB();
void bootstrap();
/** @}*/

View File

@ -168,10 +168,10 @@ int main(int argc, char **argv)
//======================================================
while (run_openplc)
{
// Read input image - this method tries to get the lock
// so don't put it in the lock context.
updateBuffersIn();
{
std::lock_guard<std::mutex> guard(bufferLock);
// Make sure the buffer pointers are correct and
@ -183,13 +183,14 @@ int main(int argc, char **argv)
updateCustomIn();
// Update input image table with data from slave devices
updateBuffersIn_MB();
services_before_cycle();
handleSpecialFunctions();
// Execute plc program logic
config_run__(__tick++);
updateCustomOut();
// Update slave devices with data from the output image table
updateBuffersOut_MB();
services_after_cycle();
}
// Write output image - this method tries to get the lock

View File

@ -15,496 +15,141 @@
#include <ini.h>
#include <spdlog/spdlog.h>
#ifndef OPLC_UNIT_TEST
#include <modbus.h>
#endif // OPLC_UNIT_TEST
#include <modbus.h>
#include <cstdint>
#include <cstring>
#include <algorithm>
#include <chrono>
#include <mutex>
#include <string>
#include <vector>
#include "ladder.h"
#include "master.h"
#include "master_indexed.h"
#include "../glue.h"
#include "../ini_util.h"
using namespace std;
/** \addtogroup openplc_runtime
* @{
*/
oplc::modbusm::MasterConfig::MasterConfig() :
name{'\0'},
protocol(ProtocolInvalid),
backoff_strategy(BackoffStrategyNone),
ip_address{'\0'},
response_timeout(chrono::milliseconds(250)),
poll_interval(chrono::milliseconds(500))
{}
#define MAX_MB_IO 400
uint8_t bool_input_buf[MAX_MB_IO];
uint8_t bool_output_buf[MAX_MB_IO];
uint16_t int_input_buf[MAX_MB_IO];
uint16_t int_output_buf[MAX_MB_IO];
pthread_mutex_t ioLock;
/// \brief This function is called by the OpenPLC in a loop. Here the internal
/// buffers must be updated to reflect the actual Input state.
void updateBuffersIn_MB()
/// @brief Create the context for the device according to the configuration
/// parameters provided by the user.
/// @return the created context.
modbus_t* oplc::modbusm::MasterConfig::create() const
{
pthread_mutex_lock(&ioLock);
for (int i = 0; i < MAX_MB_IO; i++)
{
if (bool_input[100+(i/8)][i%8] != NULL) *bool_input[100+(i/8)][i%8] = bool_input_buf[i];
if (int_input[100+i] != NULL) *int_input[100+i] = int_input_buf[i];
}
pthread_mutex_unlock(&ioLock);
}
/// \brief This function is called by the OpenPLC in a loop. Here the internal buffers
/// must be updated to reflect the actual Output state.
void updateBuffersOut_MB()
{
pthread_mutex_lock(&ioLock);
for (int i = 0; i < MAX_MB_IO; i++)
{
if (bool_output[100+(i/8)][i%8] != NULL) bool_output_buf[i] = *bool_output[100+(i/8)][i%8];
if (int_output[100+i] != NULL) int_output_buf[i] = *int_output[100+i];
}
pthread_mutex_unlock(&ioLock);
}
/// Defines the protocol that is selected by ser configuration.
enum MasterProtocol {
ProtocolInvalid,
ProtocolTcp,
ProtocolRtu,
};
/// How big of a buffer do we reserve for string items.
const uint8_t MASTER_ITEM_SIZE(100);
/// Define the mapping for modbus addresses located variables.
struct ModbusAddress
{
uint16_t start_address;
uint16_t num_regs;
};
/// Defines the configuration information for a particular modbus
/// master.
struct Master {
/// A name, mostly for the purpose of logging.
char name[MASTER_ITEM_SIZE];
/// Which protocol do we use for communication.
MasterProtocol protocol;
/// The ID of the slave.
uint8_t slave_id;
/// The IP address (if using TCP).
char ip_address[MASTER_ITEM_SIZE];
/// The port (if using TCP).
uint16_t ip_port;
uint16_t rtu_baud_rate;
uint8_t rtu_parity;
uint16_t rtu_data_bit;
uint16_t rtu_stop_bit;
/// The context for communcating with the device.
modbus_t* mb_ctx;
/// Device specific timeout.
uint16_t timeout;
/// Is the device currently connected.
bool is_connected;
struct ModbusAddress discrete_inputs;
struct ModbusAddress coils;
struct ModbusAddress input_registers;
struct ModbusAddress holding_read_registers;
struct ModbusAddress holding_registers;
Master() :
name{'\0'},
protocol(ProtocolInvalid),
ip_address{'\0'},
mb_ctx(nullptr),
is_connected(false)
{}
/// Create the context for the device according to the configuration
/// parameters provided by the user.
void create() {
if (protocol == ProtocolTcp) {
mb_ctx = modbus_new_tcp(ip_address, ip_port);
} else if (protocol == ProtocolRtu) {
mb_ctx = modbus_new_rtu(ip_address, rtu_baud_rate, rtu_parity, rtu_data_bit, rtu_stop_bit);
}
modbus_set_slave(mb_ctx, slave_id);
uint32_t to_sec = timeout / 1000;
uint32_t to_usec = (timeout % 1000) * 1000;
modbus_set_response_timeout(mb_ctx, to_sec, to_usec);
if (protocol == ProtocolTcp)
{
mb_ctx = modbus_new_tcp(ip_address, ip_port);
}
};
/// Configuration structure that is passed into the ini parsing library.
/// This structure is populated as we process configuration items.
struct ModbusMasterConfig {
chrono::milliseconds polling_period;
vector<Master>* masters;
/// Get (or create) the configuration items at the specified index.
/// This ensure that this index is addessable.
Master* config_item(uint8_t index) {
size_t required_size = max(masters->size(), static_cast<size_t>(index + 1));
if (masters->size() < required_size) {
masters->resize(index + 1);
}
return &masters->at(index);
}
};
/// Callback function for the ini parser. This function is called for every
/// configuration item.
int modbus_master_cfg_handler(void* user_data, const char* section,
const char* name, const char* value) {
if (strcmp("modbusmaster", section) != 0) {
return 0;
else if (protocol == ProtocolRtu)
{
mb_ctx = modbus_new_rtu(ip_address, rtu_baud_rate, rtu_parity, rtu_data_bit, rtu_stop_bit);
}
auto config = reinterpret_cast<ModbusMasterConfig*>(user_data);
modbus_set_slave(mb_ctx, slave_id);
auto sec = chrono::duration_cast<chrono::seconds>(response_timeout);
auto usec = response_timeout - sec;
modbus_set_response_timeout(mb_ctx, sec.count(), usec.count());
return mb_ctx;
}
std::int16_t oplc::modbusm::common_cfg_handler(
std::array<oplc::modbusm::MasterConfig, MODBUS_MASTER_MAX>& masters,
const char* name,
const char* value)
{
const uint8_t max = MODBUS_MASTER_MAX - 1;
uint8_t index;
if (oplc::cmpnameid(name, "name", &index) == 0) {
strncpy(config->config_item(index)->name, value, MASTER_ITEM_SIZE);
config->config_item(index)->name[MASTER_ITEM_SIZE - 1] = '\0';
} else if (oplc::cmpnameid(name, "protocol", &index) == 0) {
if (strcmp(value, "tcp") == 0) {
config->config_item(index)->protocol = ProtocolTcp;
} else if (strcmp(value, "rtu") == 0) {
config->config_item(index)->protocol = ProtocolRtu;
if (oplc::strcmp_with_index(name, "name", max, &index) == 0)
{
strncpy(masters[index].name, value, MASTER_ITEM_SIZE);
masters[index].name[MASTER_ITEM_SIZE - 1] = '\0';
}
else if (oplc::strcmp_with_index(name, "protocol", max, &index) == 0)
{
if (strcmp(value, "tcp") == 0)
{
masters[index].protocol = ProtocolTcp;
}
} else if (oplc::cmpnameid(name, "slave_id", &index) == 0) {
config->config_item(index)->slave_id = atoi(value);
} else if (oplc::cmpnameid(name, "ip_address", &index) == 0) {
strncpy(config->config_item(index)->ip_address, value, MASTER_ITEM_SIZE);
config->config_item(index)->ip_address[MASTER_ITEM_SIZE - 1] = '\0';
} else if (oplc::cmpnameid(name, "ip_port", &index) == 0) {
config->config_item(index)->ip_port = atoi(value);
} else if (oplc::cmpnameid(name, "rtu_baud_rate", &index) == 0) {
config->config_item(index)->rtu_baud_rate = atoi(value);
} else if (oplc::cmpnameid(name, "rtu_parity", &index) == 0) {
config->config_item(index)->rtu_parity = atoi(value);
} else if (oplc::cmpnameid(name, "rtu_data_bit", &index) == 0) {
config->config_item(index)->rtu_data_bit = atoi(value);
} else if (oplc::cmpnameid(name, "rtu_stop_bit", &index) == 0) {
config->config_item(index)->rtu_stop_bit = atoi(value);
} else if (oplc::cmpnameid(name, "discrete_inputs_start", &index) == 0) {
config->config_item(index)->discrete_inputs.start_address = atoi(value);
} else if (oplc::cmpnameid(name, "discrete_inputs_size", &index) == 0) {
config->config_item(index)->discrete_inputs.num_regs = atoi(value);
} else if (oplc::cmpnameid(name, "coils_start", &index) == 0) {
config->config_item(index)->coils.start_address = atoi(value);
} else if (oplc::cmpnameid(name, "coils_size", &index) == 0) {
config->config_item(index)->coils.num_regs = atoi(value);
} else if (oplc::cmpnameid(name, "input_registers_start", &index) == 0) {
config->config_item(index)->input_registers.start_address = atoi(value);
} else if (oplc::cmpnameid(name, "input_registers_size", &index) == 0) {
config->config_item(index)->input_registers.num_regs = atoi(value);
} else if (oplc::cmpnameid(name, "holding_registers_read_start", &index) == 0) {
config->config_item(index)->holding_read_registers.start_address = atoi(value);
} else if (oplc::cmpnameid(name, "holding_registers_read_size", &index) == 0) {
config->config_item(index)->holding_read_registers.num_regs = atoi(value);
} else if (oplc::cmpnameid(name, "holding_registers_start", &index) == 0) {
config->config_item(index)->holding_registers.start_address = atoi(value);
} else if (oplc::cmpnameid(name, "holding_registers_size", &index) == 0) {
config->config_item(index)->holding_registers.num_regs = atoi(value);
} else if (strcmp(name, "enabled") == 0) {
// Nothing to do here - we already know this is enabled
} else {
spdlog::warn("Unknown configuration item {}", name);
else if (strcmp(value, "rtu") == 0)
{
masters[index].protocol = ProtocolRtu;
}
else
{
spdlog::warn("Unknown protocol configuration value {}", value);
}
}
else if (oplc::strcmp_with_index(name, "backoff_strategy", max, &index) == 0)
{
if (strcmp(value, "linear_bounded") == 0)
{
masters[index].backoff_strategy = BackoffStrategyLinear;
}
else
{
spdlog::warn("Unknown backoff strategy configuration value {}", value);
}
}
else if (oplc::strcmp_with_index(name, "slave_id", max, &index) == 0)
{
masters[index].slave_id = atoi(value);
}
else if (oplc::strcmp_with_index(name, "response_timeout", max, &index) == 0)
{
masters[index].response_timeout = chrono::microseconds(atoi(value));
}
else if (oplc::strcmp_with_index(name, "poll_interval", max, &index) == 0)
{
masters[index].poll_interval = chrono::microseconds(atoi(value));
}
else if (oplc::strcmp_with_index(name, "ip_address", max, &index) == 0)
{
strncpy(masters[index].ip_address, value, MASTER_ITEM_SIZE);
masters[index].ip_address[MASTER_ITEM_SIZE - 1] = '\0';
}
else if (oplc::strcmp_with_index(name, "ip_port", max, &index) == 0)
{
masters[index].ip_port = atoi(value);
}
else if (oplc::strcmp_with_index(name, "rtu_baud_rate", max, &index) == 0)
{
masters[index].rtu_baud_rate = atoi(value);
}
else if (oplc::strcmp_with_index(name, "rtu_parity", max, &index) == 0)
{
masters[index].rtu_parity = atoi(value);
}
else if (oplc::strcmp_with_index(name, "rtu_data_bit", max, &index) == 0)
{
masters[index].rtu_data_bit = atoi(value);
}
else if (oplc::strcmp_with_index(name, "rtu_stop_bit", max, &index) == 0)
{
masters[index].rtu_stop_bit = atoi(value);
}
else
{
return -1;
}
return 0;
}
/// Arguments provided to the master polling thread.
struct MasterArgs {
volatile bool* run;
chrono::milliseconds polling_period;
vector<Master>* masters;
};
/// Polls modbus slaves. This is the main function created by this modbus
/// master.
void* modbus_master_poll_slaves(void* args) {
auto master_args = reinterpret_cast<MasterArgs*>(args);
while (*master_args->run) {
uint16_t bool_input_index = 0;
uint16_t bool_output_index = 0;
uint16_t int_input_index = 0;
uint16_t int_output_index = 0;
for (size_t i = 0; i < master_args->masters->size(); i++) {
Master& master = master_args->masters->at(i);
//Verify if device is connected
if (!master.is_connected) {
spdlog::info("Device {} is disconnected. Attempting to reconnect...", master.name);
if (modbus_connect(master.mb_ctx) == -1)
{
spdlog::error("Connection failed on MB device {}: {}", master.name, modbus_strerror(errno));
if (special_functions[2] != NULL) *special_functions[2]++;
// Because this device is not connected, we skip those input registers
bool_input_index += (master.discrete_inputs.num_regs);
int_input_index += (master.input_registers.num_regs);
int_input_index += (master.holding_read_registers.num_regs);
bool_output_index += (master.coils.num_regs);
int_output_index += (master.holding_registers.num_regs);
}
else
{
spdlog::info("Connected to MB device {}", master.name);
master.is_connected = true;
}
}
if (master.is_connected)
{
struct timespec ts;
ts.tv_sec = 0;
ts.tv_nsec = (1000*1000*1000*28)/master.rtu_baud_rate;
//Read discrete inputs
if (master.discrete_inputs.num_regs != 0)
{
uint8_t *tempBuff;
tempBuff = (uint8_t *)malloc(master.discrete_inputs.num_regs);
nanosleep(&ts, NULL);
int return_val = modbus_read_input_bits(master.mb_ctx, master.discrete_inputs.start_address,
master.discrete_inputs.num_regs, tempBuff);
if (return_val == -1)
{
if (master.protocol == ProtocolTcp)
{
modbus_close(master.mb_ctx);
master.is_connected = false;
}
spdlog::info("Modbus Read Discrete Input Registers failed on MB device {}: {}", master.name, modbus_strerror(errno));
bool_input_index += (master.discrete_inputs.num_regs);
if (special_functions[2] != NULL) *special_functions[2]++;
}
else
{
pthread_mutex_lock(&ioLock);
for (int j = 0; j < return_val; j++)
{
bool_input_buf[bool_input_index] = tempBuff[j];
bool_input_index++;
}
pthread_mutex_unlock(&ioLock);
}
free(tempBuff);
}
//Write coils
if (master.coils.num_regs != 0)
{
uint8_t *tempBuff;
tempBuff = (uint8_t *)malloc(master.coils.num_regs);
pthread_mutex_lock(&ioLock);
for (int j = 0; j < master.coils.num_regs; j++)
{
tempBuff[j] = bool_output_buf[bool_output_index];
bool_output_index++;
}
pthread_mutex_unlock(&ioLock);
nanosleep(&ts, NULL);
int return_val = modbus_write_bits(master.mb_ctx, master.coils.start_address, master.coils.num_regs, tempBuff);
if (return_val == -1)
{
if (master.protocol == ProtocolTcp)
{
modbus_close(master.mb_ctx);
master.is_connected = false;
}
spdlog::error("Modbus Write Coils failed on MB device {}: {}", master.name, modbus_strerror(errno));
if (special_functions[2] != NULL) *special_functions[2]++;
}
free(tempBuff);
}
//Read input registers
if (master.input_registers.num_regs != 0)
{
uint16_t *tempBuff;
tempBuff = (uint16_t *)malloc(2*master.input_registers.num_regs);
nanosleep(&ts, NULL);
int return_val = modbus_read_input_registers(master.mb_ctx, master.input_registers.start_address,
master.input_registers.num_regs, tempBuff);
if (return_val == -1)
{
if (master.protocol == ProtocolTcp)
{
modbus_close(master.mb_ctx);
master.is_connected = false;
}
spdlog::error("Modbus Read Input Registers failed on MB device {}: {}", master.name, modbus_strerror(errno));
int_input_index += (master.input_registers.num_regs);
if (special_functions[2] != NULL) *special_functions[2]++;
}
else
{
pthread_mutex_lock(&ioLock);
for (int j = 0; j < return_val; j++)
{
int_input_buf[int_input_index] = tempBuff[j];
int_input_index++;
}
pthread_mutex_unlock(&ioLock);
}
free(tempBuff);
}
//Read holding registers
if (master.holding_read_registers.num_regs != 0)
{
uint16_t *tempBuff;
tempBuff = (uint16_t *)malloc(2*master.holding_read_registers.num_regs);
nanosleep(&ts, NULL);
int return_val = modbus_read_registers(master.mb_ctx, master.holding_read_registers.start_address,
master.holding_read_registers.num_regs, tempBuff);
if (return_val == -1)
{
if (master.protocol == ProtocolTcp)
{
modbus_close(master.mb_ctx);
master.is_connected = false;
}
spdlog::error("Modbus Read Holding Registers failed on MB device {}: {}", master.name, modbus_strerror(errno));
int_input_index += (master.holding_read_registers.num_regs);
if (special_functions[2] != NULL) *special_functions[2]++;
}
else
{
pthread_mutex_lock(&ioLock);
for (int j = 0; j < return_val; j++)
{
int_input_buf[int_input_index] = tempBuff[j];
int_input_index++;
}
pthread_mutex_unlock(&ioLock);
}
free(tempBuff);
}
//Write holding registers
if (master.holding_registers.num_regs != 0)
{
uint16_t *tempBuff;
tempBuff = (uint16_t *)malloc(2*master.holding_registers.num_regs);
pthread_mutex_lock(&ioLock);
for (int j = 0; j < master.holding_registers.num_regs; j++)
{
tempBuff[j] = int_output_buf[int_output_index];
int_output_index++;
}
pthread_mutex_unlock(&ioLock);
nanosleep(&ts, NULL);
int return_val = modbus_write_registers(master.mb_ctx, master.holding_registers.start_address,
master.holding_registers.num_regs, tempBuff);
if (return_val == -1)
{
if (master.protocol == ProtocolTcp)
{
modbus_close(master.mb_ctx);
master.is_connected = false;
}
spdlog::error("Modbus Write Holding Registers failed on MB device {}: {}", master.name, modbus_strerror(errno));
if (special_functions[2] != NULL) *special_functions[2]++;
}
free(tempBuff);
}
}
}
this_thread::sleep_for(master_args->polling_period);
}
return 0;
}
/// Run the modbus master. This function does not return until
/// this service is terminated.
void modbus_master_run(oplc::config_stream& cfg_stream,
const char* cfg_overrides,
const GlueVariablesBinding& bindings,
volatile bool& run) {
// Read the configuration information for the masters
vector<Master> master_defs;
ModbusMasterConfig config {
.polling_period = chrono::milliseconds(100),
.masters = &master_defs,
};
ini_parse_stream(oplc::istream_fgets, cfg_stream.get(),
modbus_master_cfg_handler, &config);
cfg_stream.reset(nullptr);
// Create the context for each master
for (size_t index = 0; index < master_defs.size(); ++index) {
master_defs[index].create();
}
//Initialize comm error counter
if (special_functions[2] != NULL) {
*special_functions[2] = 0;
}
// Start a unified polling thread for all masters
auto master_args = new MasterArgs {
.run = &run,
.polling_period = chrono::milliseconds(config.polling_period),
.masters = &master_defs };
pthread_t thread;
int ret = pthread_create(&thread, nullptr, modbus_master_poll_slaves, master_args);
if (ret == 0) {
pthread_detach(thread);
} else {
delete master_args;
}
while (run) {
// Sleep for a while to determine if we should terminate
// A better approach is targeted as a future story
this_thread::sleep_for(chrono::milliseconds(500));
}
// Terminate the unified polling thread. It is important to wait
// here because we passed information to the modbus thread that is
// on this stack.
pthread_join(thread, nullptr);
}
void modbus_master_service_run(const GlueVariablesBinding& binding,
volatile bool& run, const char* config) {
auto cfg_stream = oplc::open_config();
modbus_master_run(cfg_stream, config, binding, run);
}
/** @}*/

View File

@ -16,22 +16,106 @@
#ifndef RUNTIME_CORE_MODBUSMASTER_MASTER_H_
#define RUNTIME_CORE_MODBUSMASTER_MASTER_H_
#include <cstdint>
#include <chrono>
#include <array>
#ifndef OPLC_UNIT_TEST
#include <modbus.h>
#endif // OPLC_UNIT_TEST
/** \addtogroup openplc_runtime
* @{
*/
class GlueVariablesBinding;
/// @brief Start the modbus master service.
namespace oplc
{
namespace modbusm
{
#define MODBUS_MASTER_MAX 3
// These definitions declare common items that are shared between different
// modbus master implementations. Their purpose is to make it easy to
// configure different mappings, but share much of the definition.
/// @brief Defines the protocol that is selected by user configuration.
enum MasterProtocol
{
/// @brief Indicates a protocol that is unknown.
ProtocolInvalid = 0,
/// @brief Use the TCP protocol.
ProtocolTcp,
/// @brief Use the RTU protocol.
ProtocolRtu,
};
enum BackoffStrategy
{
/// Don't use any strategy.
BackoffStrategyNone = 0,
/// Backoff according to the number of failed attempts.
BackoffStrategyLinear,
};
/// How big of a buffer do we reserve for string items.
const std::uint8_t MASTER_ITEM_SIZE(100);
/// @brief Defines the configuration information for a particular modbus
/// master.
struct MasterConfig
{
/// A name, mostly for the purpose of logging.
char name[MASTER_ITEM_SIZE];
/// Which protocol do we use for communication.
MasterProtocol protocol;
/// Which strategy do we use for failed communication.
BackoffStrategy backoff_strategy;
/// The ID of the slave.
std::uint8_t slave_id;
/// The IP address (if using TCP).
char ip_address[MASTER_ITEM_SIZE];
/// The port (if using TCP).
std::uint16_t ip_port;
std::uint16_t rtu_baud_rate;
std::uint8_t rtu_parity;
std::uint16_t rtu_data_bit;
std::uint16_t rtu_stop_bit;
/// Device specific timeout.
std::chrono::microseconds response_timeout;
/// Device specific polling period
std::chrono::microseconds poll_interval;
MasterConfig();
/// @brief Create the context for the device according to the
/// configuration parameters provided by the user.
/// @return the created context.
modbus_t* create() const;
};
/// @brief Read common configuration information that relates to defining a
/// modbus master. This function would normally be called from the
/// config handler implementation that is specific to a binding strategy.
///
/// @param glue_variables The glue variables that may be bound into this
/// service.
/// @param run A signal for running this service. This service terminates when
/// this signal is false.
/// @param config The custom configuration for this service.
void modbus_master_service_run(const GlueVariablesBinding& binding,
volatile bool& run, const char* config);
/// This function does not take the section name - it is assumed that this
/// is called from a context that already filters out unrelated sections.
///
/// @param masters An array of master definitions that
/// contains the read definitions.
/// @param section The config item name from the configuration file.
/// @param value. The config item value.
/// @return 0 if the name and value were handled, otherwise -1.
std::int16_t common_cfg_handler(
std::array<MasterConfig, MODBUS_MASTER_MAX>& masters,
const char* name, const char* value
);
} // namespace modbusm
} // namespace opld
/** @}*/
#endif // RUNTIME_CORE_MODBUSSLAVE_MASTER_H_
#endif // RUNTIME_CORE_MODBUSMASTER_MASTER_H_

View File

@ -0,0 +1,486 @@
// Copyright 2015 Thiago Alves
// Copyright 2019 Smarter Grid Solutions
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http ://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissionsand
// limitations under the License.
#include <cstdint>
#include <array>
#include <chrono>
#include <mutex>
#include <thread>
#ifndef OPLC_UNIT_TEST
#include <modbus.h>
#endif // OPLC_UNIT_TEST
#include <spdlog/spdlog.h>
#include <ini.h>
#include "buffer.h"
#include "ini_util.h"
#include "master_indexed.h"
#include "master.h"
using namespace std;
using namespace oplc::modbusm;
/** \addtogroup openplc_runtime
* @{
*/
#define MAX_MB_IO 400
const uint8_t MAPPED_GLUE_START = 100;
oplc::ContiguousMappedValues<IEC_BOOL, MAX_MB_IO> bool_input_buf;
/// Boolean values that will be written to the remote server.
oplc::ContiguousMappedValues<IEC_BOOL, MAX_MB_IO> bool_output_buf;
/// Values that will be written to the remote server.
oplc::ContiguousMappedValues<uint16_t, MAX_MB_IO> int_output_buf;
/// Values that are read from the remote server to located variables.
oplc::ContiguousMappedValues<uint16_t, MAX_MB_IO> int_input_buf;
/// If set, then when there are errors, increment this value.
const GlueVariable* error_var = nullptr;
mutex io_lock;
/// @brief This function is called by the OpenPLC in a loop. Here the internal
/// buffers must be updated to reflect the actual Input state.
void oplc::modbusm::modbus_master_before_cycle()
{
lock_guard<mutex> guard(io_lock);
bool_input_buf.update_located();
int_input_buf.update_located();
}
/// @brief This function is called by the OpenPLC in a loop. Here the internal buffers
/// must be updated to reflect the actual Output state.
void oplc::modbusm::modbus_master_after_cycle()
{
lock_guard<mutex> guard(io_lock);
bool_output_buf.update_cache();
int_output_buf.update_cache();
}
/// @brief Configuration information that is supplied to the config handler.
struct IndexedMasterConfig
{
array<MasterConfig, MODBUS_MASTER_MAX>* masters;
array<IndexedMapping, MODBUS_MASTER_MAX>* mappings;
};
/// @brief Callback function for the ini parser. This function is called for
/// every configuration item.
int cfg_handler(void* user_data, const char* section,
const char* name, const char* value)
{
if (strcmp("modbusmaster", section) != 0)
{
return 0;
}
auto config = reinterpret_cast<IndexedMasterConfig*>(user_data);
auto mappings = config->mappings;
const uint8_t max = MODBUS_MASTER_MAX - 1;
uint8_t index;
// First check if this is a common item.
if (oplc::modbusm::common_cfg_handler(*config->masters, name, value) == 0)
{
// It was already handled
}
else if (oplc::strcmp_with_index(name, "discrete_inputs_start", max, &index) == 0)
{
mappings->at(index).discrete_inputs.start_address = atoi(value);
}
else if (oplc::strcmp_with_index(name, "discrete_inputs_size", max, &index) == 0)
{
mappings->at(index).discrete_inputs.num_regs = atoi(value);
}
else if (oplc::strcmp_with_index(name, "coils_start", max, &index) == 0)
{
mappings->at(index).coils.start_address = atoi(value);
}
else if (oplc::strcmp_with_index(name, "coils_size", max, &index) == 0)
{
mappings->at(index).coils.num_regs = atoi(value);
}
else if (oplc::strcmp_with_index(name, "input_registers_start", max, &index) == 0)
{
mappings->at(index).input_registers.start_address = atoi(value);
}
else if (oplc::strcmp_with_index(name, "input_registers_size", max, &index) == 0)
{
mappings->at(index).input_registers.num_regs = atoi(value);
}
else if (oplc::strcmp_with_index(name, "holding_registers_read_start", max, &index) == 0)
{
mappings->at(index).holding_read_registers.start_address = atoi(value);
}
else if (oplc::strcmp_with_index(name, "holding_registers_read_size", max, &index) == 0)
{
mappings->at(index).holding_read_registers.num_regs = atoi(value);
}
else if (oplc::strcmp_with_index(name, "holding_registers_start", max, &index) == 0)
{
mappings->at(index).holding_registers.start_address = atoi(value);
}
else if (oplc::strcmp_with_index(name, "holding_registers_size", max, &index) == 0)
{
mappings->at(index).holding_registers.num_regs = atoi(value);
}
else if (strcmp(name, "enabled") == 0)
{
// Nothing to do here - we already know this is enabled
}
else
{
spdlog::warn("Unknown configuration item {}", name);
return -1;
}
return 0;
}
/// Arguments provided to the master polling thread.
struct MasterArgs
{
volatile bool* run;
const MasterConfig* master;
const IndexedMapping* mapping;
};
#define MODBUS_HANDLE_ERROR(ret, type_name) \
if (ret == -1)\
{\
modbus_master_handle_error(master, mb_ctx, is_connected, type_name);\
}
#define IF_NOT_MODBUS_ERROR(ret, type_name) \
if (ret == -1)\
{\
modbus_master_handle_error(master, mb_ctx, is_connected, type_name);\
}\
else
void modbus_master_handle_error(const MasterConfig* master, modbus_t* mb_ctx,
bool& is_connected, const char* type_name)
{
if (master->protocol == ProtocolTcp)
{
modbus_close(mb_ctx);
is_connected = false;
}
spdlog::info("Modbus {} failed on MB device {}: {}",
type_name, master->name, modbus_strerror(errno));
// TODO accessing this value is not property protected - this is old
// and it should be fixed
if (error_var != nullptr)
{
*reinterpret_cast<IEC_LINT*>(error_var->value) += 1;
}
}
void* oplc::modbusm::modbus_master_indexed_poll(void* args)
{
auto master_args = reinterpret_cast<MasterArgs*>(args);
auto master = master_args->master;
auto mapping = reinterpret_cast<const IndexedMapping*>(master_args->mapping);
// The context for communcating with the device.
modbus_t* mb_ctx = master->create();
bool is_connected = false;
// Declare a common buffer for all read operations. We read into this
// buffer so that we can minimize the exclusive lock time that we need
// to exchange data with the main loop.
// This is bigger than the biggest segment we allow.
// TODO validate that the bounds provided are smaller than this buffer.
uint8_t rw_buffer[MAX_MB_IO * 2];
// The number of consecutive failed attempts
int32_t failed_attempts = 0;
struct timespec ts;
ts.tv_sec = 0;
ts.tv_nsec = (1000*1000*1000*28L) / master->rtu_baud_rate;
while (*master_args->run)
{
auto start = chrono::steady_clock::now();
// Verify if device is connected
if (!is_connected)
{
spdlog::debug("Device {} is disconnected. Reconnecting.", master->name);
if (modbus_connect(mb_ctx) == -1)
{
spdlog::error("Connection failed on MB device {}: {}", master->name, modbus_strerror(errno));
// TODO accessing this value is not property protected - this is old
// and it should be fixe
if (error_var != nullptr)
{
*reinterpret_cast<IEC_LINT*>(error_var->value) += 1;
}
failed_attempts += 1;
}
else
{
spdlog::info("Connected to MB device {}", master->name);
is_connected = true;
failed_attempts = 0;
}
}
if (is_connected)
{
// Read discrete inputs
if (mapping->discrete_inputs.num_regs != 0)
{
nanosleep(&ts, nullptr);
int result = modbus_read_input_bits(mb_ctx, mapping->discrete_inputs.start_address,
mapping->discrete_inputs.num_regs, rw_buffer);
IF_NOT_MODBUS_ERROR(result, "read discrete input")
{
lock_guard<mutex> guard(io_lock);
bool_input_buf.copy_to_cache(rw_buffer, mapping->discrete_inputs.num_regs, mapping->discrete_inputs.buffer_offset);
}
}
// Write coils
if (mapping->coils.num_regs != 0)
{
{
lock_guard<mutex> guard(io_lock);
bool_output_buf.copy_from_cache(rw_buffer, mapping->coils.num_regs, mapping->coils.buffer_offset);
}
nanosleep(&ts, NULL);
int result = modbus_write_bits(mb_ctx, mapping->coils.start_address, mapping->coils.num_regs, rw_buffer);
MODBUS_HANDLE_ERROR(result, "write coils");
}
// Read input registers
if (mapping->input_registers.num_regs != 0)
{
auto rw16_buffer = reinterpret_cast<uint16_t*>(rw_buffer);
nanosleep(&ts, NULL);
int result = modbus_read_input_registers(mb_ctx, mapping->input_registers.start_address,
mapping->input_registers.num_regs, rw16_buffer);
IF_NOT_MODBUS_ERROR(result, "read input registers")
{
lock_guard<mutex> guard(io_lock);
int_input_buf.copy_to_cache(rw_buffer, result * 2, mapping->input_registers.buffer_offset);
}
}
// Read holding registers
if (mapping->holding_read_registers.num_regs != 0)
{
auto rw16_buffer = reinterpret_cast<uint16_t*>(rw_buffer);
nanosleep(&ts, NULL);
int result = modbus_read_registers(mb_ctx, mapping->holding_read_registers.start_address,
mapping->holding_read_registers.num_regs, rw16_buffer);
IF_NOT_MODBUS_ERROR(result, "read holding registers")
{
lock_guard<mutex> guard(io_lock);
int_input_buf.copy_to_cache(rw_buffer, result * 2, mapping->holding_read_registers.buffer_offset);
}
}
// Write holding registers
if (mapping->holding_registers.num_regs != 0)
{
auto rw16_buffer = reinterpret_cast<uint16_t*>(rw_buffer);
{
lock_guard<mutex> guard(io_lock);
int_output_buf.copy_from_cache(rw_buffer, mapping->holding_registers.buffer_offset, mapping->holding_registers.num_regs);
}
nanosleep(&ts, NULL);
int return_val = modbus_write_registers(mb_ctx, mapping->holding_registers.start_address,
mapping->holding_registers.num_regs, rw16_buffer);
MODBUS_HANDLE_ERROR(return_val, "write holding registers");
}
}
if (failed_attempts && master->backoff_strategy == BackoffStrategyLinear)
{
auto scale_factor = std::min(10, failed_attempts);
this_thread::sleep_until(start + (master->poll_interval * scale_factor));
}
else
{
this_thread::sleep_until(start + master->poll_interval);
}
}
modbus_free(mb_ctx);
return 0;
}
/// Assign the mappings from our bindings according to the strategy
/// that this master expect. In general, we map items starting from
/// 100 to the modbus master.
void assign_mappings(const GlueVariablesBinding& bindings)
{
// Initialize comm error counter. For historical reasons,
// this is defined to be at index 1026 in the IEC_LINT with
// the memory storage class.
error_var = bindings.find(IECLDT_MEM, IECLST_LONGWORD, 1026, 0);
if (error_var != nullptr)
{
*reinterpret_cast<IEC_LINT*>(error_var->value) = 0;
}
// Assign the buffered mapping values using the glue variables
for (auto i = 0; i < bindings.size; ++i)
{
auto glue_var = bindings.glue_variables[i];
// We defined that modbus master mappings start at 100, so ignore
// anything that is not in the right range.
if (glue_var.msi < MAPPED_GLUE_START
|| glue_var.msi >= MAPPED_GLUE_START + MAX_MB_IO)
{
continue;
}
if (glue_var.dir == IECLDT_OUT && glue_var.size == IECLST_WORD)
{
int_output_buf.assign(glue_var.msi - MAPPED_GLUE_START,
reinterpret_cast<IEC_UINT*>(glue_var.value));
}
else if (glue_var.dir == IECLDT_IN && glue_var.size == IECLST_WORD)
{
int_input_buf.assign(glue_var.msi - MAPPED_GLUE_START,
reinterpret_cast<IEC_UINT*>(glue_var.value));
}
else if (glue_var.dir == IECLDT_OUT && glue_var.size == IECLST_BIT)
{
auto group = reinterpret_cast<GlueBoolGroup*>(glue_var.value);
// We assign the index, one by one, into the mapping
for (auto i = 0; i < 8; ++i)
{
auto offset = glue_var.msi - MAPPED_GLUE_START;
bool_input_buf.assign(offset * 8 + i, group->values[i]);
}
}
else if (glue_var.dir == IECLDT_IN && glue_var.size == IECLST_BIT)
{
auto group = reinterpret_cast<GlueBoolGroup*>(glue_var.value);
// We assign the index, one by one, into the mapping
for (auto i = 0; i < 8; ++i)
{
auto offset = glue_var.msi - MAPPED_GLUE_START;
bool_output_buf.assign(offset * 8 + i, group->values[i]);
}
}
}
}
/// Run the modbus master. This function does not return until
/// this service is terminated.
void modbus_master_run(oplc::config_stream& cfg_stream,
const char* cfg_overrides,
const GlueVariablesBinding& bindings,
volatile bool& run)
{
// Read the configuration information for the masters
array<MasterConfig, MODBUS_MASTER_MAX> master_defs;
array<IndexedMapping, MODBUS_MASTER_MAX> mapping_defs;
array<pthread_t, MODBUS_MASTER_MAX> threads;
IndexedMasterConfig config
{
.masters = &master_defs,
.mappings = &mapping_defs,
};
ini_parse_stream(oplc::istream_fgets, cfg_stream.get(),
cfg_handler, &config);
cfg_stream.reset(nullptr);
assign_mappings(bindings);
// Create a thread for polling each master. We use multiple threads
// so that communication timeouts related to one master will not
// impact others.
for (size_t index = 0; index < master_defs.size(); ++index)
{
if (index != 0)
{
// Initialize the mapping offset from the prior master definition.
// This sets things so that each master knows which part of the
// exchange that it will write to.
mapping_defs[index].set_offsets(mapping_defs[index - 1]);
}
// This might happen if it is disabled or there are gaps in the
// the indices for the declared devices.
if (master_defs[index].protocol == ProtocolInvalid)
{
continue;
}
auto master_args = new MasterArgs
{
.run = &run,
.master = &master_defs[index],
.mapping = &mapping_defs[index],
};
int ret = pthread_create(&threads[index], nullptr,
oplc::modbusm::modbus_master_indexed_poll,
master_args);
if (ret == 0)
{
pthread_detach(threads[index]);
}
else
{
delete master_args;
}
}
while (run)
{
// Sleep for a while to determine if we should terminate
// A better approach is targeted as a future story
this_thread::sleep_for(chrono::milliseconds(500));
}
// Terminate the unified polling thread. It is important to wait
// here because we passed information to the modbus thread that is
// on this stack.
for (size_t index = 0; index < threads.size(); ++index)
{
if (threads[index])
{
pthread_join(threads[index], nullptr);
}
}
}
void oplc::modbusm::modbus_master_service_run(const GlueVariablesBinding& binding,
volatile bool& run,
const char* config)
{
auto cfg_stream = oplc::open_config();
modbus_master_run(cfg_stream, config, binding, run);
}
/** @}*/

View File

@ -0,0 +1,102 @@
// Copyright 2015 Thiago Alves
// Copyright 2019 Smarter Grid Solutions
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http ://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissionsand
// limitations under the License.
#ifndef RUNTIME_CORE_MODBUSMASTER_INDEXED_STRATEGY_H_
#define RUNTIME_CORE_MODBUSMASTER_INDEXED_STRATEGY_H_
#include <cstdint>
#include "glue.h"
#include "master.h"
namespace oplc
{
namespace modbusm
{
/** \addtogroup openplc_runtime
* @{
*/
#define MAX_MB_IO 400
/// @brief Define the mapping for modbus addresses to located variables
/// that is is based on a sequence starting an an offset in the located
/// variables and an offset in the Modbus server.
struct ModbusAddress
{
/// The offset in the mapped buffer for exchange with the PLC.
std::uint16_t buffer_offset;
/// The start address on the remote server
std::uint16_t start_address;
/// The number of addresses on the remote server.
std::uint16_t num_regs;
inline void set_offset(ModbusAddress& prior)
{
buffer_offset = prior.buffer_offset + prior.num_regs;
}
};
struct IndexedMapping
{
ModbusAddress discrete_inputs;
ModbusAddress coils;
ModbusAddress input_registers;
ModbusAddress holding_read_registers;
ModbusAddress holding_registers;
void set_offsets(IndexedMapping& prior)
{
// Index for the bool inputs
discrete_inputs.set_offset(prior.discrete_inputs);
// Index for the bool outputs
coils.set_offset(prior.coils);
// Index for the int inputs
input_registers.set_offset(prior.input_registers);
holding_read_registers.set_offset(prior.holding_read_registers);
// Index for the int outputs
holding_registers.set_offset(prior.holding_registers);
}
};
/// @brief The main entry point for the thread that is responsible for polling
/// the remote server.
/// @param args The supplied data to the thread. This must be a pointer to
/// the IndexedMaster structure.
void* modbus_master_indexed_poll(void* args);
/// @brief Start the modbus master service.
///
/// @param glue_variables The glue variables that may be bound into this
/// service.
/// @param run A signal for running this service. This service terminates when
/// this signal is false.
/// @param config The custom configuration for this service.
void modbus_master_service_run(const GlueVariablesBinding& binding,
volatile bool& run, const char* config);
/// @brief Lifecycle method called just prior to the PLC cycle.
void modbus_master_before_cycle();
/// @brief Lifecycle method called just after the PLC cycle.
void modbus_master_after_cycle();
} // namespace modbusm
} // namespace oplc
/** @}*/
#endif // RUNTIME_CORE_MODBUSMASTER_INDEXED_STRATEGY_H_

View File

@ -153,8 +153,8 @@ IndexedStrategy::IndexedStrategy(const GlueVariablesBinding& bindings) :
/// @param read_buffer The read buffer that contains the glue variable
/// 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)
void exchange(array<oplc::PendingValue<T>, NUM_REGISTER_VALUES>& write_buffer,
array<oplc::MappedValue<T>, NUM_REGISTER_VALUES>& read_buffer)
{
for (size_t index = 0; index < write_buffer.size(); ++index)
{
@ -315,7 +315,7 @@ modbus_errno IndexedStrategy::WriteHoldingRegisters(uint16_t hr_start_index,
// bit shift to write the appropriate part. Resize to 32-bits
// so we can shift appropriately.
uint32_t partial_value = (uint32_t) word;
PendingValue<IEC_DINT>& dst = dintm_register_write_buffer[hr_index / 2];
oplc::PendingValue<IEC_DINT>& dst = dintm_register_write_buffer[hr_index / 2];
dst.has_pending = true;
if (hr_index % 2 == 0)
@ -337,7 +337,7 @@ modbus_errno IndexedStrategy::WriteHoldingRegisters(uint16_t hr_start_index,
// 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.
uint64_t partial_value = (uint64_t) word;
PendingValue<IEC_LINT>& dst = lintm_register_write_buffer[hr_index / 4];
oplc::PendingValue<IEC_LINT>& dst = lintm_register_write_buffer[hr_index / 4];
dst.has_pending = true;
auto word_index = hr_index % 4;

View File

@ -18,6 +18,7 @@
#include <mutex>
#include <vector>
#include "buffer.h"
#include "lib/iec_types_all.h"
/** \addtogroup openplc_runtime
@ -61,48 +62,6 @@ struct PendingBool
}
};
/// Defines the mapping between a located value
/// and a cache of the value for reading with modbus.
template <typename T>
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)
{
this->value = val;
this->cached_value = *val;
}
inline void update_cache()
{
if (this->value) {
this->cached_value = *this->value;
}
}
};
/// 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
{
PendingValue() : has_pending(false), value(0) {}
bool has_pending;
T value;
/// Set the value and mark it as updated.
inline void set(T val)
{
this->has_pending = true;
this->value = val;
}
};
typedef std::uint8_t modbus_errno;
/// Implements a strategy that maps between modbus and located variables
@ -189,19 +148,19 @@ class IndexedStrategy
std::vector<PendingBool> coil_write_buffer;
std::vector<MappedBool> di_read_buffer;
std::array<PendingValue<IEC_INT>, NUM_REGISTER_VALUES> int_register_write_buffer;
std::array<MappedValue<IEC_INT>, NUM_REGISTER_VALUES> int_register_read_buffer;
std::array<oplc::PendingValue<IEC_INT>, NUM_REGISTER_VALUES> int_register_write_buffer;
std::array<oplc::MappedValue<IEC_INT>, NUM_REGISTER_VALUES> int_register_read_buffer;
std::array<PendingValue<IEC_INT>, NUM_REGISTER_VALUES> intm_register_write_buffer;
std::array<MappedValue<IEC_INT>, NUM_REGISTER_VALUES> intm_register_read_buffer;
std::array<oplc::PendingValue<IEC_INT>, NUM_REGISTER_VALUES> intm_register_write_buffer;
std::array<oplc::MappedValue<IEC_INT>, NUM_REGISTER_VALUES> intm_register_read_buffer;
std::array<PendingValue<IEC_DINT>, NUM_REGISTER_VALUES> dintm_register_write_buffer;
std::array<MappedValue<IEC_DINT>, NUM_REGISTER_VALUES> dintm_register_read_buffer;
std::array<oplc::PendingValue<IEC_DINT>, NUM_REGISTER_VALUES> dintm_register_write_buffer;
std::array<oplc::MappedValue<IEC_DINT>, NUM_REGISTER_VALUES> dintm_register_read_buffer;
std::array<PendingValue<IEC_LINT>, NUM_REGISTER_VALUES> lintm_register_write_buffer;
std::array<MappedValue<IEC_LINT>, NUM_REGISTER_VALUES> lintm_register_read_buffer;
std::array<oplc::PendingValue<IEC_LINT>, NUM_REGISTER_VALUES> lintm_register_write_buffer;
std::array<oplc::MappedValue<IEC_LINT>, NUM_REGISTER_VALUES> lintm_register_read_buffer;
std::array<MappedValue<IEC_INT>, NUM_REGISTER_VALUES> int_input_read_buffer;
std::array<oplc::MappedValue<IEC_INT>, NUM_REGISTER_VALUES> int_input_read_buffer;
// Protects access to the cached values in this class.
std::mutex buffer_mutex;

View File

@ -429,9 +429,13 @@ int modbus_slave_cfg_handler(void* user_data, const char* section,
else if (strcmp(name, "address") == 0)
{
config->address = value;
}
else if (strcmp(name, "binding") == 0)
{
// Nothing to do here - we only support one binding strategy.
}
else if (strcmp(name, "enabled") == 0)
{
else if (strcmp(name, "enabled") == 0)
{
// Nothing to do here - we already know this is enabled
}
else

View File

@ -21,41 +21,62 @@
using namespace std;
void null_handler(const GlueVariablesBinding& binding) {}
ServiceInitFunction null_init_fn(null_handler);
ServiceFinalizeFunction null_finalize_fn(null_handler);
void null_binding_handler(const GlueVariablesBinding& binding) {}
void null_handler() {}
ServiceDefinition::ServiceDefinition(const char* name,
ServiceStartFunction& start_fn) :
service_start_fn start_fn) :
name(name),
init_fn(null_init_fn),
finalize_fn(null_finalize_fn),
init_fn(null_binding_handler),
finalize_fn(null_binding_handler),
start_fn(start_fn),
before_cycle_fn(null_handler),
after_cycle_fn(null_handler),
running(false),
thread(0),
config_buffer()
{}
ServiceDefinition::ServiceDefinition(const char* name,
ServiceStartFunction& start_fn,
ServiceInitFunction& init_fn) :
service_start_fn start_fn,
service_init_fn init_fn) :
name(name),
start_fn(start_fn),
init_fn(init_fn),
finalize_fn(null_finalize_fn),
finalize_fn(null_binding_handler),
before_cycle_fn(null_handler),
after_cycle_fn(null_handler),
running(false),
thread(0),
config_buffer()
{}
ServiceDefinition::ServiceDefinition(const char* name,
ServiceStartFunction& start_fn,
ServiceInitFunction& init_fn,
ServiceFinalizeFunction& finalize_fn) :
service_start_fn start_fn,
service_init_fn init_fn,
service_finalize_fn finalize_fn) :
name(name),
start_fn(start_fn),
init_fn(init_fn),
finalize_fn(finalize_fn),
before_cycle_fn(null_handler),
after_cycle_fn(null_handler),
running(false),
thread(0),
config_buffer()
{}
ServiceDefinition::ServiceDefinition(const char* name,
service_start_fn start_fn,
service_before_cycle_fn before_cycle_fn,
service_after_cycle_fn fafter_cycle_fn) :
name(name),
start_fn(start_fn),
init_fn(null_binding_handler),
finalize_fn(null_binding_handler),
before_cycle_fn(before_cycle_fn),
after_cycle_fn(fafter_cycle_fn),
running(false),
thread(0),
config_buffer()
@ -120,6 +141,22 @@ void ServiceDefinition::stop()
}
}
void ServiceDefinition::before_cycle()
{
if (this->running)
{
this->before_cycle_fn();
}
}
void ServiceDefinition::after_cycle()
{
if (this->running)
{
this->after_cycle_fn();
}
}
void* ServiceDefinition::run_service(void* user_data)
{
auto service = reinterpret_cast<ServiceDefinition*>(user_data);

View File

@ -17,7 +17,6 @@
#include <pthread.h>
#include <cstdint>
#include <functional>
/** \addtogroup openplc_runtime
* @{
@ -27,9 +26,11 @@ class GlueVariablesBinding;
const std::size_t MAX_INTERACTIVE_CONFIG_SIZE(1024);
typedef std::function<void(const GlueVariablesBinding& binding)> ServiceInitFunction;
typedef std::function<void(const GlueVariablesBinding& binding)> ServiceFinalizeFunction;
typedef std::function<void(const GlueVariablesBinding& binding, volatile bool& run, const char* config)> ServiceStartFunction;
typedef void (*service_init_fn) (const GlueVariablesBinding& binding);
typedef void (*service_finalize_fn) (const GlueVariablesBinding& binding);
typedef void (*service_start_fn) (const GlueVariablesBinding& binding, volatile bool& run, const char* config);
typedef void (*service_before_cycle_fn) ();
typedef void (*service_after_cycle_fn) ();
/// A service is the primary extension point for adding support for new
/// protocols or hardware specific capabilities. You should not implement
@ -42,6 +43,10 @@ typedef std::function<void(const GlueVariablesBinding& binding, volatile bool& r
/// keep only a very short lock on the glue variables so that it cannot
/// prevent the main PLC loop from accessing the variables.
///
/// Services can also participate with the runtime with the before and after
/// cycle functions. These functions do not receive the mutex because the
/// PLC cycle already has the mutex ownership.
///
/// @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.
@ -52,15 +57,15 @@ class ServiceDefinition final
/// and stopped but does not participate in initialize or finalize.
/// @param name The unique name of this service.
/// @param start_fn A function to start the service.
ServiceDefinition(const char* name, ServiceStartFunction& start_fn);
ServiceDefinition(const char* name, service_start_fn start_fn);
/// Initialize a new instance of a service definition that can be started
/// and stopped and participates in initialize.
/// @param name The unique name of this service.
/// @param start_fn A function to start the service.
/// @param init_fn A function to run when the runtime initializes.
ServiceDefinition(const char* name, ServiceStartFunction& start_fn,
ServiceInitFunction& init_fn);
ServiceDefinition(const char* name, service_start_fn start_fn,
service_init_fn init_fn);
/// Initialize a new instance of a service that participates in all
/// lifecycle events (initialize, finalize, start, stop).
@ -68,9 +73,19 @@ class ServiceDefinition final
/// @param start_fn A function to start the service.
/// @param init_fn A function to run when the runtime initializes.
/// @param finalize_fn A function to run when the runtime finalizes.
ServiceDefinition(const char* name, ServiceStartFunction& start_fn,
ServiceInitFunction& init_fn,
ServiceFinalizeFunction& finalize_fn);
ServiceDefinition(const char* name, service_start_fn start_fn,
service_init_fn init_fn,
service_finalize_fn finalize_fn);
/// Initialize a new instance of a service that participates in the start
/// lifecycle and in cycle events.
/// @param name The unique name of this service.
/// @param start_fn A function to start the service.
/// @param before_cycle_fn A function to run before each PLC cycle.
/// @param after_cycle_fn A function to run after each PLC cycle.
ServiceDefinition(const char* name, service_start_fn start_fn,
service_before_cycle_fn before_cycle_fn,
service_after_cycle_fn fafter_cycle_fn);
/// Lifecycle method for when the runtime starts. This is called for
/// the service before the runtime loop beings. This does not mean
@ -85,6 +100,9 @@ class ServiceDefinition final
/// Lifecycle method for when this service has been stopped on demand.
void stop();
void before_cycle();
void after_cycle();
/// Get the descriptive identifier for this service type.
const char* id() const { return this->name; }
@ -101,11 +119,15 @@ class ServiceDefinition final
/// The type name of the service.
const char* name;
/// The function to initialize the service.
ServiceInitFunction& init_fn;
service_init_fn init_fn;
/// The function to finalize the service.
ServiceFinalizeFunction& finalize_fn;
service_finalize_fn finalize_fn;
/// The function to start the service.
ServiceStartFunction& start_fn;
service_start_fn start_fn;
/// The function to run before the PLC cycle.
service_before_cycle_fn before_cycle_fn;
/// The function to run after the PLC cycle.
service_after_cycle_fn after_cycle_fn;
/// Is the service running.
volatile bool running;
/// The thread the service is running on.

View File

@ -20,31 +20,26 @@
#include "interactive_server.h"
#include "pstorage.h"
#include "../modbusslave/slave.h"
#include "../modbusmaster/master.h"
#include "../modbusmaster/master_indexed.h"
#include "../dnp3s/dnp3.h"
using namespace std;
ServiceInitFunction pstorage_init_fn(pstorage_service_init);
ServiceStartFunction pstorage_start_service_fn(pstorage_service_run);
ServiceStartFunction dnp3s_start_service_fn(dnp3s_service_run);
ServiceStartFunction interactive_start_service_fn(interactive_service_run);
ServiceStartFunction modbus_slave_start_service_fn(modbus_slave_service_run);
ServiceStartFunction modbus_master_start_service_fn(modbus_master_service_run);
using namespace oplc::modbusm;
ServiceDefinition* services[] = {
new ServiceDefinition("interactive", interactive_start_service_fn),
new ServiceDefinition("pstorage", pstorage_start_service_fn, pstorage_init_fn),
new ServiceDefinition("modbusslave", modbus_slave_start_service_fn),
new ServiceDefinition("modbusmaster", modbus_master_start_service_fn),
new ServiceDefinition("interactive", interactive_service_run),
new ServiceDefinition("pstorage", pstorage_service_run, pstorage_service_init),
new ServiceDefinition("modbusslave", modbus_slave_service_run),
new ServiceDefinition("modbusmaster", modbus_master_service_run, modbus_master_before_cycle, modbus_master_after_cycle),
#ifdef OPLC_DNP3_OUTSTATION
new ServiceDefinition("dnp3s", dnp3s_start_service_fn),
new ServiceDefinition("dnp3s", dnp3s_service_run),
#endif
};
ServiceDefinition* services_find(const char* name)
{
ServiceDefinition** item = find_if(begin(services), end(services), [name] (ServiceDefinition* def) {
ServiceDefinition** item = find_if(begin(services), end(services), [name] (ServiceDefinition* def)
{
return strcmp(def->id(), name) == 0;
});
@ -53,21 +48,38 @@ ServiceDefinition* services_find(const char* name)
void services_stop()
{
for_each(begin(services), end(services), [] (ServiceDefinition* def) {
for_each(begin(services), end(services), [] (ServiceDefinition* def)
{
def->stop();
});
}
void services_init()
{
for_each(begin(services), end(services), [] (ServiceDefinition* def) {
for_each(begin(services), end(services), [] (ServiceDefinition* def)
{
def->initialize();
});
}
void services_finalize()
{
for_each(begin(services), end(services), [] (ServiceDefinition* def) {
for_each(begin(services), end(services), [] (ServiceDefinition* def)
{
def->finalize();
});
}
void services_before_cycle() {
std::for_each(std::begin(services), std::end(services), [] (ServiceDefinition* def)
{
def->before_cycle();
});
}
void services_after_cycle() {
std::for_each(std::begin(services), std::end(services), [] (ServiceDefinition* def)
{
def->after_cycle();
});
}

View File

@ -35,6 +35,12 @@ void services_init();
/// Finalize all known services.
void services_finalize();
/// Run the service function before the cycle.
void services_before_cycle();
/// Run the service function after the cycle.
void services_after_cycle();
/** @}*/
#endif // RUNTIME_CORE_SERVICE_SERVICE_DEFINITION_H_

41
runtime/filefilter.txt Normal file
View File

@ -0,0 +1,41 @@
- lib
- vendor
- hardware_layers
- test/mock_headers
- test
~ RULE_3_1_A_do_not_start_filename_with_underbar
~ RULE_3_2_B_do_not_use_same_filename_more_than_once
~ RULE_3_2_CD_do_not_use_special_characters_in_filename
~ RULE_3_2_F_use_representitive_classname_for_cpp_filename
~ RULE_3_2_H_do_not_use_uppercase_for_c_filename
~ RULE_3_3_A_start_function_name_with_is_or_has_when_return_bool
~ RULE_3_3_A_start_function_name_with_lowercase_unix
~ RULE_3_3_B_start_private_function_name_with_underbar
~ RULE_4_1_A_B_use_space_for_indentation
~ RULE_4_1_B_indent_each_enum_item_in_enum_block
~ RULE_4_1_B_locate_each_enum_item_in_seperate_line
~ RULE_4_1_C_align_long_function_parameter_list
~ RULE_4_1_E_align_conditions
~ RULE_4_2_A_A_space_around_operator
~ RULE_4_2_A_B_space_around_word
~ RULE_4_4_A_do_not_write_over_120_columns_per_line
~ RULE_4_5_A_brace_for_namespace_should_be_located_in_seperate_line
~ RULE_4_5_A_braces_for_function_definition_should_be_located_in_seperate_line
~ RULE_4_5_A_braces_for_type_definition_should_be_located_in_seperate_line
~ RULE_4_5_A_braces_inside_of_function_should_be_located_in_end_of_line
~ RULE_4_5_A_indent_blocks_inside_of_function
~ RULE_4_5_A_matching_braces_inside_of_function_should_be_located_same_column
~ RULE_4_5_B_use_braces_even_for_one_statement
~ RULE_6_1_A_do_not_omit_function_parameter_names
~ RULE_6_1_E_do_not_use_more_than_5_paramters_in_function
~ RULE_6_1_G_write_less_than_200_lines_for_function
~ RULE_6_2_A_do_not_use_system_dependent_type
~ RULE_6_4_B_initialize_first_item_of_enum
~ RULE_6_5_B_do_not_use_lowercase_for_macro_constants
~ RULE_6_5_B_do_not_use_macro_for_constants
~ RULE_7_1_B_A_do_not_use_double_assignment
~ RULE_7_2_B_do_not_use_goto_statement
~ RULE_8_1_A_provide_file_info_comment
~ RULE_9_1_A_do_not_use_hardcorded_include_path
~ RULE_9_2_D_use_reentrant_function
~ RULE_A_3_avoid_too_deep_blocks

View File

@ -133,7 +133,7 @@ SCENARIO("create_config", "")
{ IECLDT_IN, IECLST_BIT, 0, 0, IECVT_BOOL, &bool_var },
};
GlueVariablesBinding bindings(&glue_mutex, 1, glue_vars, nullptr);
auto input = "[dnp3s]\nbind_location=name:%IX0.0,group:12,index:1,"
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,

View File

@ -24,7 +24,7 @@ using namespace std;
#define HEADER 0, 0, 0, 0, 0, 0, 0
SCENARIO("slave", "")
SCENARIO("modbusslave", "")
{
mutex glue_mutex;