PR-765 Start the modbus master using the same service based approach

This commit is contained in:
Garret Fick 2019-11-20 14:14:48 -05:00
parent 5c7ead0873
commit 5fe3358462
13 changed files with 755 additions and 654 deletions

View File

@ -2,6 +2,7 @@
guidelines = 80, 120
trim_trailing_whitespace = true
insert_final_newline = true
indent_style = space
[*.sh]
end_of_line = lf

View File

@ -227,7 +227,7 @@ if(OPLC_MAIN_PROGRAM)
message("Compile main program enabled")
set(OPLC_USER_DIR ${PROJECT_SOURCE_DIR}/etc/src)
file(GLOB oplc_SRC runtime/core/*.cpp runtime/core/dnp3s/*.cpp runtime/core/modbusslave/*.cpp runtime/core/service/*.cpp runtime/vendor/inih-r46/*.c)
file(GLOB oplc_SRC runtime/core/*.cpp runtime/core/dnp3s/*.cpp runtime/core/modbusslave/*.cpp runtime/core/modbusmaster/*.cpp runtime/core/service/*.cpp runtime/vendor/inih-r46/*.c)
include_directories(${OPLC_USER_DIR})
include_directories(runtime/core)

View File

@ -69,3 +69,12 @@ _Devices can be passed to the `docker` daemon using the `-v` flag (e.g. `-v /dev
```bash
docker run -it --rm --privileged -p 8080:8080 openplc:v3
```
## Running Standalone
The normal approach for running OpenPLC is though the web interface. However,
it is possible to run OpenPLC without the web interface using configuration
information supplied in a configuration file.
See the file `config.ini.example` in this repository for information about
how to run standalone.

180
config.ini.example Normal file
View File

@ -0,0 +1,180 @@
; This configuration file enables configuring OpenPLC capabilities
; when the OpenPLC runtime starts. These capabilities can be configured
; without using the web front end and enable running the OpenPLC runtime
; standalone.
;
; This file must be located in the "etc" folder and must be named
; "config.ini".
; ---------------------------------------------------------
; ---------------------------------------------------------
[logging]
; Level defines the minimum level for a message to appear
; as a log message. Messages are a lower level are not
; output.
;
; Level may be one of the following values, ordered from
; lowest to highest:
; trace, debug, info, warn, error
level = info
; ---------------------------------------------------------
; ---------------------------------------------------------
[interactive]
; The interactive server is what connects the web front end to the runtime
; You need this enabled if you want to be able to enable/disable services
; at runtime, view logs, etc.
;
; If you set this value to false and use the OpenPLC web interface to
; start the runtime, the web interface will not be able to communicate
; with the runtime.
enabled = true
; ---------------------------------------------------------
; ---------------------------------------------------------
[modbusslave]
; Modbus slave enables reading and writing located variables through the
; modbus interface. This starts a Modbus server (also know as a slave)
; running in the OpenPLC runtime.
enabled = false
; TCP Settings
; ------------
port = 502
address = 127.0.0.1
; How we bind located variables to modbus registers.
; This may be one of:
; sized - indicates to use the size of a located variable to determine
; the register type
binding = sized
; ---------------------------------------------------------
; ---------------------------------------------------------
[modbusmaster]
; Modbus master enables reading and writing located variables through the
; modbus interface. This starts capabilties to poll one or more Modbus
; servers and exchange data with the located variables.
enabled = true
; We support multiple modbus masters. Each master should specify
; a complete set of configuration information within this section.
; Different masters are identified by a postfix which includes the
; index of the master. Indices start at 0 and go up from there.
name.0 = 1
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
; ---------------------------------------------------------
; ---------------------------------------------------------
[pstorage]
; The pstorage service reads and writes persistent storage. It enables
; the runtime to restore important variables to the previous value
; if the runtime is restarated.
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
; ---------------------------------------------------------
; ---------------------------------------------------------
[dnp3s]
; The dnp3s enables a DNP3 outstation.
enabled = false
; Location Bindings
; -----------------
; Bind OpenPLC bit-sized output 0.0 to DNP3 binary input at index 0
; Note that the second hierarchical index must be 0
bind_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,
; Bind OpenPLC word-sized output 2 to DNP3 analog output status at index 1
; bind_location = name:%QW2,group:40,index:1,
; Bind OpenPLC long word-sized output 2 to DNP3 analog input at index 10
; 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 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
; Link Settings
; -------------
; The remote and local address
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
; How long (seconds) the outstation will allow a operate
; to follow a select
; select_timeout = 10
; max control commands for a single APDU
; max_controls_per_request = 16
; maximum fragment size the outstation will recieve
; default is the max value
; max_rx_frag_size = 2048
; maximum fragment size the outstation will send if
; it needs to fragment. Default is the max falue
; max_tx_frag_size = 2048
; size of the event buffer
; event_buffer_size = 10
; Timeout for solicited confirms (milliseconds)
; sol_confirm_timeout = 5000
; Timeout for unsolicited confirms (milliseconds)
; unsol_conrfirm_timeout = 5000
; Timeout for unsolicited retries (milliseconds)
; unsol_retry_timeout = 5000
; The rate as which data is exchanged between DNP3 and the runtime
; (milliseconds)
; poll_interval = 250

View File

@ -26,7 +26,7 @@ endif()
include_directories(lib)
# The primary source is everything in this directory
file(GLOB oplc_SRC *.cpp dnp3s/*.cpp service/*.cpp modbusslave/*.cpp)
file(GLOB oplc_SRC *.cpp dnp3s/*.cpp service/*.cpp modbusslave/*.cpp modbusmaster/*.cpp)
message("In runtime")
message(${oplc_SRC})

View File

@ -130,7 +130,6 @@ void bootstrap()
// a standard part the platform where we have implemented capabilities
// for specific hardware targes.
initializeHardware();
initializeMB();
// User provided logic that runs on initialization.
initCustomLayer();
updateBuffersIn();
@ -143,7 +142,7 @@ void bootstrap()
// SERVICE INITIALIZATION
//======================================================
// Initializes any services that is known and wants to participate
// Initializes any services that are known and want to participate
// in bootstrapping.
services_init();

View File

@ -51,6 +51,38 @@ 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,
/// 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.
///
/// The intention of this function is to create a standard mechanism to
/// declare configuration items that are indexed. The index is normally
/// used to lookup in an array for the slot where to populate the value.
///
/// @param name The name to test. This is the value read from the INI file.
/// @param expected The name to test against, without the separating period or
/// 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.
/// @return 0 if there is a match, otherwise non-zero.
inline int cmpnameid(const char* name, const char* expected,
std::uint8_t* index) {
size_t expected_len = strlen(expected);
int ret = strncmp(name, expected, expected_len);
if (ret != 0) {
return ret;
}
size_t name_len = strlen(name);
if (name_len > expected_len + 1 && name[expected_len] == '.') {
*index = atoi(name + (expected_len + 1));
return 0;
}
return -1;
}
/// 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.

View File

@ -140,8 +140,6 @@ int processEnipMessage(unsigned char *buffer, int buffer_size, void* user_data);
uint16_t processPCCCMessage(unsigned char *buffer, int buffer_size);
// modbus_master.cpp
void initializeMB();
void *querySlaveDevices(void *arg);
void updateBuffersIn_MB();
void updateBuffersOut_MB();

View File

@ -1,647 +0,0 @@
// Copyright 2015 Thiago Alves
//
// 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.
// This file is responsible for parse and discovery of slave devices by parsing
// the mbconfig.cfg file. This code also updates OpenPLC internal buffers with
// the data queried from the slave devices.
// Thiago Alves, Jul 2018
//-----------------------------------------------------------------------------
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <modbus.h>
#include <errno.h>
#include <string.h>
#include <pthread.h>
#include <chrono>
#include <iostream>
#include <fstream>
#include <string>
#include <thread>
#include <spdlog/spdlog.h>
#include "ladder.h"
/** \addtogroup openplc_runtime
* @{
*/
#define MB_TCP 1
#define MB_RTU 2
#define MAX_MB_IO 400
using namespace std;
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;
struct MB_address
{
uint16_t start_address;
uint16_t num_regs;
};
struct MB_device
{
modbus_t *mb_ctx;
char dev_name[100];
uint8_t protocol;
char dev_address[100];
uint16_t ip_port;
int rtu_baud;
char rtu_parity;
int rtu_data_bit;
int rtu_stop_bit;
uint8_t dev_id;
bool isConnected;
struct MB_address discrete_inputs;
struct MB_address coils;
struct MB_address input_registers;
struct MB_address holding_read_registers;
struct MB_address holding_registers;
};
struct MB_device *mb_devices;
uint8_t num_devices;
uint16_t polling_period = 100;
uint16_t timeout = 1000;
////////////////////////////////////////////////////////////////////////////////
/// \brief Finds the data between the separators on the line provided
/// \param *line
/// \param *buf
/// \param separator1
/// \param separator2
////////////////////////////////////////////////////////////////////////////////
void getData(char *line, char *buf, char separator1, char separator2)
{
int i=0, j=0;
buf[j] = '\0';
while (line[i] != separator1 && line[i] != '\0')
{
i++;
}
i++;
while (line[i] != separator2 && line[i] != '\0')
{
buf[j] = line[i];
i++;
j++;
buf[j] = '\0';
}
}
////////////////////////////////////////////////////////////////////////////////
/// \brief Get the number of the Modbus device
/// \param *line
/// \return int with no
////////////////////////////////////////////////////////////////////////////////
int getDeviceNumber(char *line)
{
char temp[5];
int i = 0, j = 6;
while (line[j] != '.')
{
temp[i] = line[j];
i++;
j++;
temp[i] = '\0';
}
return(atoi(temp));
}
////////////////////////////////////////////////////////////////////////////////
/// \brief get the type of function or parameter for the Modbus device
/// \param *line
/// \param *parameter
////////////////////////////////////////////////////////////////////////////////
void getFunction(char *line, char *parameter)
{
int i = 0, j = 0;
while (line[j] != '.')
{
j++;
}
j++;
while (line[j] != ' ' && line[j] != '=' && line[j] != '(')
{
parameter[i] = line[j];
i++;
j++;
parameter[i] = '\0';
}
}
////////////////////////////////////////////////////////////////////////////////
/// \brief Parses the mbconfig.cfg file
////////////////////////////////////////////////////////////////////////////////
void parseConfig()
{
string line;
char line_str[1024];
ifstream cfgfile("mbconfig.cfg");
if (cfgfile.is_open())
{
while (getline(cfgfile, line))
{
strncpy(line_str, line.c_str(), 1024);
if (line_str[0] != '#' && strlen(line_str) > 1)
{
if (!strncmp(line_str, "Num_Devices", 11))
{
char temp_buffer[5];
getData(line_str, temp_buffer, '"', '"');
num_devices = atoi(temp_buffer);
mb_devices = (struct MB_device *)malloc(num_devices*sizeof(struct MB_device));
}
else if (!strncmp(line_str, "Polling_Period", 14))
{
char temp_buffer[10];
getData(line_str, temp_buffer, '"', '"');
polling_period = atoi(temp_buffer);
}
else if (!strncmp(line_str, "Timeout", 7))
{
char temp_buffer[10];
getData(line_str, temp_buffer, '"', '"');
timeout = atoi(temp_buffer);
}
else if (!strncmp(line_str, "device", 6))
{
int deviceNumber = getDeviceNumber(line_str);
char functionType[100];
getFunction(line_str, functionType);
if (!strncmp(functionType, "name", 4))
{
getData(line_str, mb_devices[deviceNumber].dev_name, '"', '"');
}
else if (!strncmp(functionType, "protocol", 8))
{
char temp_buffer[5];
getData(line_str, temp_buffer, '"', '"');
if (!strncmp(temp_buffer, "TCP", 3))
mb_devices[deviceNumber].protocol = MB_TCP;
else if (!strncmp(temp_buffer, "RTU", 3))
mb_devices[deviceNumber].protocol = MB_RTU;
}
else if (!strncmp(functionType, "slave_id", 8))
{
char temp_buffer[5];
getData(line_str, temp_buffer, '"', '"');
mb_devices[deviceNumber].dev_id = atoi(temp_buffer);
}
else if (!strncmp(functionType, "address", 7))
{
getData(line_str, mb_devices[deviceNumber].dev_address, '"', '"');
}
else if (!strncmp(functionType, "IP_Port", 7))
{
char temp_buffer[6];
getData(line_str, temp_buffer, '"', '"');
mb_devices[deviceNumber].ip_port = atoi(temp_buffer);
}
else if (!strncmp(functionType, "RTU_Baud_Rate", 13))
{
char temp_buffer[10];
getData(line_str, temp_buffer, '"', '"');
mb_devices[deviceNumber].rtu_baud = atoi(temp_buffer);
}
else if (!strncmp(functionType, "RTU_Parity", 10))
{
char temp_buffer[3];
getData(line_str, temp_buffer, '"', '"');
mb_devices[deviceNumber].rtu_parity = temp_buffer[0];
}
else if (!strncmp(functionType, "RTU_Data_Bits", 13))
{
char temp_buffer[6];
getData(line_str, temp_buffer, '"', '"');
mb_devices[deviceNumber].rtu_data_bit = atoi(temp_buffer);
}
else if (!strncmp(functionType, "RTU_Stop_Bits", 13))
{
char temp_buffer[20];
getData(line_str, temp_buffer, '"', '"');
mb_devices[deviceNumber].rtu_stop_bit = atoi(temp_buffer);
}
else if (!strncmp(functionType, "Discrete_Inputs_Start", 21))
{
char temp_buffer[10];
getData(line_str, temp_buffer, '"', '"');
mb_devices[deviceNumber].discrete_inputs.start_address = atoi(temp_buffer);
}
else if (!strncmp(functionType, "Discrete_Inputs_Size", 20))
{
char temp_buffer[10];
getData(line_str, temp_buffer, '"', '"');
mb_devices[deviceNumber].discrete_inputs.num_regs = atoi(temp_buffer);
}
else if (!strncmp(functionType, "Coils_Start", 11))
{
char temp_buffer[10];
getData(line_str, temp_buffer, '"', '"');
mb_devices[deviceNumber].coils.start_address = atoi(temp_buffer);
}
else if (!strncmp(functionType, "Coils_Size", 10))
{
char temp_buffer[10];
getData(line_str, temp_buffer, '"', '"');
mb_devices[deviceNumber].coils.num_regs = atoi(temp_buffer);
}
else if (!strncmp(functionType, "Input_Registers_Start", 21))
{
char temp_buffer[10];
getData(line_str, temp_buffer, '"', '"');
mb_devices[deviceNumber].input_registers.start_address = atoi(temp_buffer);
}
else if (!strncmp(functionType, "Input_Registers_Size", 20))
{
char temp_buffer[10];
getData(line_str, temp_buffer, '"', '"');
mb_devices[deviceNumber].input_registers.num_regs = atoi(temp_buffer);
}
else if (!strncmp(functionType, "Holding_Registers_Read_Start", 28))
{
char temp_buffer[10];
getData(line_str, temp_buffer, '"', '"');
mb_devices[deviceNumber].holding_read_registers.start_address = atoi(temp_buffer);
}
else if (!strncmp(functionType, "Holding_Registers_Read_Size", 27))
{
char temp_buffer[10];
getData(line_str, temp_buffer, '"', '"');
mb_devices[deviceNumber].holding_read_registers.num_regs = atoi(temp_buffer);
}
else if (!strncmp(functionType, "Holding_Registers_Start", 23))
{
char temp_buffer[10];
getData(line_str, temp_buffer, '"', '"');
mb_devices[deviceNumber].holding_registers.start_address = atoi(temp_buffer);
}
else if (!strncmp(functionType, "Holding_Registers_Size", 22))
{
char temp_buffer[10];
getData(line_str, temp_buffer, '"', '"');
mb_devices[deviceNumber].holding_registers.num_regs = atoi(temp_buffer);
}
}
}
}
}
else
{
spdlog::info("Skipping configuration of Slave Devices (mbconfig.cfg file not found)");
}
//Parser Debug
///*
for (int i = 0; i < num_devices; i++)
{
printf("Device %d\n", i);
printf("Name: %s\n", mb_devices[i].dev_name);
printf("Protocol: %d\n", mb_devices[i].protocol);
printf("Address: %s\n", mb_devices[i].dev_address);
printf("IP Port: %d\n", mb_devices[i].ip_port);
printf("Baud rate: %d\n", mb_devices[i].rtu_baud);
printf("Parity: %c\n", mb_devices[i].rtu_parity);
printf("Data Bits: %d\n", mb_devices[i].rtu_data_bit);
printf("Stop Bits: %d\n", mb_devices[i].rtu_stop_bit);
printf("DI Start: %d\n", mb_devices[i].discrete_inputs.start_address);
printf("DI Size: %d\n", mb_devices[i].discrete_inputs.num_regs);
printf("Coils Start: %d\n", mb_devices[i].coils.start_address);
printf("Coils Size: %d\n", mb_devices[i].coils.num_regs);
printf("IR Start: %d\n", mb_devices[i].input_registers.start_address);
printf("IR Size: %d\n", mb_devices[i].input_registers.num_regs);
printf("HR Start: %d\n", mb_devices[i].holding_registers.start_address);
printf("HR Size: %d\n", mb_devices[i].holding_registers.num_regs);
printf("\n\n");
}
//*/
}
////////////////////////////////////////////////////////////////////////////////
/// \brief Thread to poll each slave device
////////////////////////////////////////////////////////////////////////////////
void *querySlaveDevices(void *arg)
{
while (run_openplc)
{
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 (int i = 0; i < num_devices; i++)
{
//Verify if device is connected
if (!mb_devices[i].isConnected)
{
spdlog::info("Device {} is disconnected. Attempting to reconnect...", mb_devices[i].dev_name);
if (modbus_connect(mb_devices[i].mb_ctx) == -1)
{
spdlog::error("Connection failed on MB device {}: {}", mb_devices[i].dev_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 += (mb_devices[i].discrete_inputs.num_regs);
int_input_index += (mb_devices[i].input_registers.num_regs);
int_input_index += (mb_devices[i].holding_read_registers.num_regs);
bool_output_index += (mb_devices[i].coils.num_regs);
int_output_index += (mb_devices[i].holding_registers.num_regs);
}
else
{
spdlog::info("Connected to MB device {}", mb_devices[i].dev_name);
mb_devices[i].isConnected = true;
}
}
if (mb_devices[i].isConnected)
{
struct timespec ts;
ts.tv_sec = 0;
ts.tv_nsec = (1000*1000*1000*28)/mb_devices[i].rtu_baud;
//Read discrete inputs
if (mb_devices[i].discrete_inputs.num_regs != 0)
{
uint8_t *tempBuff;
tempBuff = (uint8_t *)malloc(mb_devices[i].discrete_inputs.num_regs);
nanosleep(&ts, NULL);
int return_val = modbus_read_input_bits(mb_devices[i].mb_ctx, mb_devices[i].discrete_inputs.start_address,
mb_devices[i].discrete_inputs.num_regs, tempBuff);
if (return_val == -1)
{
if (mb_devices[i].protocol != MB_RTU)
{
modbus_close(mb_devices[i].mb_ctx);
mb_devices[i].isConnected = false;
}
spdlog::info("Modbus Read Discrete Input Registers failed on MB device {}: {}", mb_devices[i].dev_name, modbus_strerror(errno));
bool_input_index += (mb_devices[i].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 (mb_devices[i].coils.num_regs != 0)
{
uint8_t *tempBuff;
tempBuff = (uint8_t *)malloc(mb_devices[i].coils.num_regs);
pthread_mutex_lock(&ioLock);
for (int j = 0; j < mb_devices[i].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(mb_devices[i].mb_ctx, mb_devices[i].coils.start_address, mb_devices[i].coils.num_regs, tempBuff);
if (return_val == -1)
{
if (mb_devices[i].protocol != MB_RTU)
{
modbus_close(mb_devices[i].mb_ctx);
mb_devices[i].isConnected = false;
}
spdlog::error("Modbus Write Coils failed on MB device {}: {}", mb_devices[i].dev_name, modbus_strerror(errno));
if (special_functions[2] != NULL) *special_functions[2]++;
}
free(tempBuff);
}
//Read input registers
if (mb_devices[i].input_registers.num_regs != 0)
{
uint16_t *tempBuff;
tempBuff = (uint16_t *)malloc(2*mb_devices[i].input_registers.num_regs);
nanosleep(&ts, NULL);
int return_val = modbus_read_input_registers( mb_devices[i].mb_ctx, mb_devices[i].input_registers.start_address,
mb_devices[i].input_registers.num_regs, tempBuff);
if (return_val == -1)
{
if (mb_devices[i].protocol != MB_RTU)
{
modbus_close(mb_devices[i].mb_ctx);
mb_devices[i].isConnected = false;
}
spdlog::error("Modbus Read Input Registers failed on MB device {}: {}", mb_devices[i].dev_name, modbus_strerror(errno));
int_input_index += (mb_devices[i].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 (mb_devices[i].holding_read_registers.num_regs != 0)
{
uint16_t *tempBuff;
tempBuff = (uint16_t *)malloc(2*mb_devices[i].holding_read_registers.num_regs);
nanosleep(&ts, NULL);
int return_val = modbus_read_registers(mb_devices[i].mb_ctx, mb_devices[i].holding_read_registers.start_address,
mb_devices[i].holding_read_registers.num_regs, tempBuff);
if (return_val == -1)
{
if (mb_devices[i].protocol != MB_RTU)
{
modbus_close(mb_devices[i].mb_ctx);
mb_devices[i].isConnected = false;
}
spdlog::error("Modbus Read Holding Registers failed on MB device {}: {}", mb_devices[i].dev_name, modbus_strerror(errno));
int_input_index += (mb_devices[i].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 (mb_devices[i].holding_registers.num_regs != 0)
{
uint16_t *tempBuff;
tempBuff = (uint16_t *)malloc(2*mb_devices[i].holding_registers.num_regs);
pthread_mutex_lock(&ioLock);
for (int j = 0; j < mb_devices[i].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(mb_devices[i].mb_ctx, mb_devices[i].holding_registers.start_address,
mb_devices[i].holding_registers.num_regs, tempBuff);
if (return_val == -1)
{
if (mb_devices[i].protocol != MB_RTU)
{
modbus_close(mb_devices[i].mb_ctx);
mb_devices[i].isConnected = false;
}
spdlog::error("Modbus Write Holding Registers failed on MB device {}: {}", mb_devices[i].dev_name, modbus_strerror(errno));
if (special_functions[2] != NULL) *special_functions[2]++;
}
free(tempBuff);
}
}
}
this_thread::sleep_for(chrono::milliseconds(polling_period));
}
return 0;
}
////////////////////////////////////////////////////////////////////////////////
/// \brief This function is called by the main OpenPLC routine when it is
/// initializing. Modbus master initialization procedures are here.
////////////////////////////////////////////////////////////////////////////////
void initializeMB()
{
parseConfig();
for (int i = 0; i < num_devices; i++)
{
if (mb_devices[i].protocol == MB_TCP)
{
mb_devices[i].mb_ctx = modbus_new_tcp(mb_devices[i].dev_address, mb_devices[i].ip_port);
}
else if (mb_devices[i].protocol == MB_RTU)
{
mb_devices[i].mb_ctx = modbus_new_rtu( mb_devices[i].dev_address, mb_devices[i].rtu_baud,
mb_devices[i].rtu_parity, mb_devices[i].rtu_data_bit,
mb_devices[i].rtu_stop_bit);
}
//slave id
modbus_set_slave(mb_devices[i].mb_ctx, mb_devices[i].dev_id);
//timeout
uint32_t to_sec = timeout / 1000;
uint32_t to_usec = (timeout % 1000) * 1000;
modbus_set_response_timeout(mb_devices[i].mb_ctx, to_sec, to_usec);
}
//Initialize comm error counter
if (special_functions[2] != NULL) *special_functions[2] = 0;
if (num_devices > 0)
{
pthread_t thread;
int ret = pthread_create(&thread, NULL, querySlaveDevices, NULL);
if (ret==0)
{
pthread_detach(thread);
}
}
}
////////////////////////////////////////////////////////////////////////////////
/// \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()
{
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);
}
/** @}*/

View File

@ -0,0 +1,489 @@
// 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 <ini.h>
#include <spdlog/spdlog.h>
#include <modbus.h>
#include <cstdint>
#include <cstring>
#include <algorithm>
#include <chrono>
#include <string>
#include <vector>
#include "ladder.h"
#include "master.h"
#include "../glue.h"
#include "../ini_util.h"
using namespace std;
/** \addtogroup openplc_runtime
* @{
*/
#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()
{
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);
}
enum MasterProtocol {
ProtocolInvalid,
ProtocolTcp,
ProtocolRtu,
};
const uint8_t MASTER_ITEM_SIZE(100);
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;
modbus_t* mb_ctx;
uint16_t timeout;
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)
{}
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);
}
};
struct ModbusMasterConfig {
chrono::milliseconds polling_period;
vector<Master>* masters;
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);
}
};
int modbus_master_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<ModbusMasterConfig*>(user_data);
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;
}
} 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);
return -1;
}
return 0;
}
struct MasterArgs {
volatile bool* run;
chrono::milliseconds polling_period;
vector<Master>* masters;
};
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;
}
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.
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

@ -0,0 +1,37 @@
// 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_MASTER_H_
#define RUNTIME_CORE_MODBUSMASTER_MASTER_H_
/** \addtogroup openplc_runtime
* @{
*/
class GlueVariablesBinding;
/// @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);
/** @}*/
#endif // RUNTIME_CORE_MODBUSSLAVE_MASTER_H_

View File

@ -31,7 +31,7 @@
#include "slave.h"
#include "indexed_strategy.h"
#include "mb_util.h"
#include "../ladder.h"
#include "../ladder.h"
#include "../glue.h"
#include "../ini_util.h"

View File

@ -20,6 +20,7 @@
#include "interactive_server.h"
#include "pstorage.h"
#include "../modbusslave/slave.h"
#include "../modbusmaster/master.h"
#include "../dnp3s/dnp3.h"
using namespace std;
@ -29,11 +30,13 @@ 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);
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),
#ifdef OPLC_DNP3_OUTSTATION
new ServiceDefinition("dnp3s", dnp3s_start_service_fn),
#endif