PR-790 Fix pass at replacing direct array access with glue variables structure
This commit is contained in:
parent
47482d8488
commit
75d65b1605
|
@ -66,12 +66,29 @@ enabled = true
|
|||
|
||||
; A user defined name for the connection. This name appears in log messages.
|
||||
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.
|
||||
poll_interval.0 = 500000
|
||||
; 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
|
||||
protocol.0 = tcp
|
||||
slave_id.0 = 1
|
||||
ip_address.0 = 127.0.0.1
|
||||
|
|
|
@ -109,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");
|
||||
}
|
||||
|
||||
//======================================================
|
||||
|
|
|
@ -0,0 +1,262 @@
|
|||
// 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_
|
|
@ -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];
|
||||
|
|
|
@ -27,6 +27,9 @@
|
|||
* @{
|
||||
*/
|
||||
|
||||
#define IEC_TRUE 1
|
||||
#define IEC_FALSE 0
|
||||
|
||||
#ifndef BUFFER_SIZE
|
||||
#define BUFFER_SIZE 1024
|
||||
#endif
|
||||
|
|
|
@ -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_id(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);
|
||||
if (ret != 0) {
|
||||
|
@ -75,15 +81,18 @@ inline int cmpnameid(const char* name, const char* expected,
|
|||
}
|
||||
|
||||
size_t name_len = strlen(name);
|
||||
if (name_len > expected_len + 1 && name[expected_len] == '.') {
|
||||
*index = atoi(name + (expected_len + 1));
|
||||
return 0;
|
||||
if (name_len >= expected_len + 2 && name[expected_len] == '.') {
|
||||
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.
|
||||
|
|
|
@ -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,15 +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 updateBuffersIn_MB();
|
||||
void updateBuffersOut_MB();
|
||||
|
||||
void bootstrap();
|
||||
|
||||
/** @}*/
|
||||
|
|
|
@ -166,30 +166,24 @@ 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();
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(bufferLock);
|
||||
// Make sure the buffer pointers are correct and
|
||||
// attached to the user variables
|
||||
glueVars();
|
||||
|
||||
// Read input image
|
||||
updateBuffersIn();
|
||||
|
||||
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
|
||||
|
|
|
@ -15,496 +15,140 @@
|
|||
|
||||
#include <ini.h>
|
||||
#include <spdlog/spdlog.h>
|
||||
#ifndef OPLC_UNIT_TEST
|
||||
#include <modbus.h>
|
||||
#endif // OPLC_UNIT_TEST
|
||||
|
||||
#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_id(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_id(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_id(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_id(name, "slave_id", max, &index) == 0)
|
||||
{
|
||||
masters[index].slave_id = atoi(value);
|
||||
}
|
||||
else if (oplc::strcmp_with_id(name, "response_timeout", max, &index) == 0)
|
||||
{
|
||||
masters[index].response_timeout = chrono::microseconds(atoi(value));
|
||||
}
|
||||
else if (oplc::strcmp_with_id(name, "poll_interval", max, &index) == 0)
|
||||
{
|
||||
masters[index].poll_interval = chrono::microseconds(atoi(value));
|
||||
}
|
||||
else if (oplc::strcmp_with_id(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_id(name, "ip_port", max, &index) == 0)
|
||||
{
|
||||
masters[index].ip_port = atoi(value);
|
||||
}
|
||||
else if (oplc::strcmp_with_id(name, "rtu_baud_rate", max, &index) == 0)
|
||||
{
|
||||
masters[index].rtu_baud_rate = atoi(value);
|
||||
}
|
||||
else if (oplc::strcmp_with_id(name, "rtu_parity", max, &index) == 0)
|
||||
{
|
||||
masters[index].rtu_parity = atoi(value);
|
||||
}
|
||||
else if (oplc::strcmp_with_id(name, "rtu_data_bit", max, &index) == 0)
|
||||
{
|
||||
masters[index].rtu_data_bit = atoi(value);
|
||||
}
|
||||
else if (oplc::strcmp_with_id(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);
|
||||
}
|
||||
|
||||
/** @}*/
|
||||
|
|
|
@ -16,22 +16,105 @@
|
|||
#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_
|
||||
|
|
|
@ -0,0 +1,478 @@
|
|||
// 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_id(name, "discrete_inputs_start", max, &index) == 0)
|
||||
{
|
||||
mappings->at(index).discrete_inputs.start_address = atoi(value);
|
||||
}
|
||||
else if (oplc::strcmp_with_id(name, "discrete_inputs_size", max, &index) == 0)
|
||||
{
|
||||
mappings->at(index).discrete_inputs.num_regs = atoi(value);
|
||||
}
|
||||
else if (oplc::strcmp_with_id(name, "coils_start", max, &index) == 0)
|
||||
{
|
||||
mappings->at(index).coils.start_address = atoi(value);
|
||||
}
|
||||
else if (oplc::strcmp_with_id(name, "coils_size", max, &index) == 0)
|
||||
{
|
||||
mappings->at(index).coils.num_regs = atoi(value);
|
||||
}
|
||||
else if (oplc::strcmp_with_id(name, "input_registers_start", max, &index) == 0)
|
||||
{
|
||||
mappings->at(index).input_registers.start_address = atoi(value);
|
||||
}
|
||||
else if (oplc::strcmp_with_id(name, "input_registers_size", max, &index) == 0)
|
||||
{
|
||||
mappings->at(index).input_registers.num_regs = atoi(value);
|
||||
}
|
||||
else if (oplc::strcmp_with_id(name, "holding_registers_read_start", max, &index) == 0)
|
||||
{
|
||||
mappings->at(index).holding_read_registers.start_address = atoi(value);
|
||||
}
|
||||
else if (oplc::strcmp_with_id(name, "holding_registers_read_size", max, &index) == 0)
|
||||
{
|
||||
mappings->at(index).holding_read_registers.num_regs = atoi(value);
|
||||
}
|
||||
else if (oplc::strcmp_with_id(name, "holding_registers_start", max, &index) == 0)
|
||||
{
|
||||
mappings->at(index).holding_registers.start_address = atoi(value);
|
||||
}
|
||||
else if (oplc::strcmp_with_id(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;
|
||||
}
|
||||
|
||||
|
||||
/// 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);
|
||||
|
||||
// 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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
|
||||
/** @}*/
|
|
@ -0,0 +1,114 @@
|
|||
// 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
|
||||
|
||||
struct InputOutputBuffer {
|
||||
std::uint8_t bool_input_buf[MAX_MB_IO];
|
||||
std::uint8_t bool_output_buf[MAX_MB_IO];
|
||||
std::uint16_t int_input_buf[MAX_MB_IO];
|
||||
std::uint16_t int_output_buf[MAX_MB_IO];
|
||||
|
||||
/// A count of how many errors have occurred since the last
|
||||
/// exchange. This value should be reset to 0 after each
|
||||
/// write-cycle.
|
||||
IEC_LINT error;
|
||||
};
|
||||
|
||||
/// @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_
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -20,25 +20,19 @@
|
|||
#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
|
||||
};
|
||||
|
||||
|
@ -71,3 +65,15 @@ void services_finalize()
|
|||
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();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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_
|
||||
|
|
|
@ -24,7 +24,7 @@ using namespace std;
|
|||
|
||||
#define HEADER 0, 0, 0, 0, 0, 0, 0
|
||||
|
||||
SCENARIO("slave", "")
|
||||
SCENARIO("modbusslave", "")
|
||||
{
|
||||
mutex glue_mutex;
|
||||
|
||||
|
|
Loading…
Reference in New Issue