add slave report id command for master and slave (backport v2)

This commit is contained in:
Alex Lisitsyn
2025-02-27 18:36:41 +08:00
parent 9b24aa41a7
commit 9890b3ce4d
31 changed files with 663 additions and 167 deletions

View File

@@ -163,6 +163,15 @@ menu "Modbus configuration"
Most significant byte of ID is used as short device ID and
other three bytes used as long ID.
config FMB_CONTROLLER_SLAVE_ID_MAX_SIZE
int "Modbus Slave ID maximum buffer size (bytes)"
range 4 255
default 32
depends on FMB_CONTROLLER_SLAVE_ID_SUPPORT
help
Modbus slave ID buffer size used to store vendor specific ID information
for the <Report Slave ID> command.
config FMB_CONTROLLER_NOTIFY_TIMEOUT
int "Modbus controller notification timeout (ms)"
range 0 200

View File

@@ -386,17 +386,43 @@ The function is similar to previous function but allows to set the data of a cha
.. code:: c
static void *master_handle = NULL;
....
uint8_t type = 0; // Type of parameter
uint8_t temp_data[4] = {0}; // temporary buffer
// Read the characteristic from slave and save the data to temp_data instance
esp_err_t err = mbc_master_set_parameter(master_handle, CID_TEMP_DATA_2, (uint8_t*)temp_data, &type);
if (err == ESP_OK) {
ESP_LOGI(TAG, "Set parameter data successfully.");
} else {
ESP_LOGE(TAG, "Set data fail, err = 0x%x (%s).", (int)err, (char*)esp_err_to_name(err));
}
static void *master_handle = NULL;
....
uint8_t type = 0; // Type of parameter
uint8_t temp_data[4] = {0}; // temporary buffer
// Read the characteristic from slave and save the data to temp_data instance
esp_err_t err = mbc_master_set_parameter(master_handle, CID_TEMP_DATA_2, (uint8_t*)temp_data, &type);
if (err == ESP_OK) {
ESP_LOGI(TAG, "Set parameter data successfully.");
} else {
ESP_LOGE(TAG, "Set data fail, err = 0x%x (%s).", (int)err, (char*)esp_err_to_name(err));
}
The master supports the <0x11 - Report Slave ID> Modbus command to read vendor specific information from the slave. It uses the :cpp:func:`mbc_master_send_request` function to send request.
The example to retrieve the slave identificator from slave:
.. code:: c
#define MB_DEVICE_ADDR1 1 // the slave UID to retrieve information
...
static void *master_handle = NULL; // the master handler is initialized previously
...
// Set the request stucture for the master to send the <Report Slave ID> command
mb_param_request_t req = {
.slave_addr = MB_DEVICE_ADDR1, // the UID of the device to get the information,
.command = 0x11, // the <Report Slave ID> command,
.reg_start = 0, // is obsolete, need to be zero for this request,
.reg_size = (CONFIG_FMB_CONTROLLER_SLAVE_ID_MAX_SIZE >> 1) // size of the buffer in registers to save ID
};
uint8_t info_buf[CONFIG_FMB_CONTROLLER_SLAVE_ID_MAX_SIZE] = {0};
// Send the request to slave
err = mbc_master_send_request(master_handle, &req, &info_buf[0]);
if (err != ESP_OK) {
ESP_LOGE("SLAVE_INFO", "Read slave info fail.");
} else {
ESP_LOG_BUFFER_HEX_LEVEL("SLAVE_INFO", (void*)info_buf, sizeof(info_buf), ESP_LOG_WARN);
}
.. _modbus_api_master_destroy:

View File

@@ -60,12 +60,14 @@ The function initializes Modbus communication descriptors for each type of Modbu
reg_area.start_offset = MB_REG_HOLDING_START_AREA0; // Offset of register area in Modbus protocol
reg_area.address = (void*)&holding_reg_area[0]; // Set pointer to storage instance
reg_area.size = (sizeof(holding_reg_area) << 1); // Set the size of register storage area in bytes!
reg_area.access = MB_ACCESS_RW; // Set the access rights for the area
ESP_ERROR_CHECK(mbc_slave_set_descriptor(slave_handle, reg_area));
reg_area.type = MB_PARAM_INPUT;
reg_area.start_offset = MB_REG_INPUT_START_AREA0;
reg_area.address = (void*)&input_reg_area[0];
reg_area.size = (sizeof(input_reg_area) << 1);
reg_area.access = MB_ACCESS_RW;
ESP_ERROR_CHECK(mbc_slave_set_descriptor(slave_handle, reg_area));
@@ -96,6 +98,69 @@ Example initialization of mapped values:
ESP_LOGI("TEST", "Test value ghefcdab: %lf", mb_get_double_ghefcdab(&holding_double_ghefcdab[0]));
...
The slave communication object supports initialization of special object identification structure which is vendor specific and can clarify some slave specific information for each slave object. The API functions below can be used to set and get this information from the slave object accordingly.
This information set in the slave can be retrieved by master object using the standard Modbus command `0x11 - <Report Slave ID>`.
:cpp:func:`mbc_set_slave_id`
Allows to set vendor specific slave ID for the concrete slave object.
.. note:: Each slave object sets the short default identificator defined in ``CONFIG_FMB_CONTROLLER_SLAVE_ID`` Kconfig value on start. This can be overridden by this API function. The option ``CONFIG_FMB_CONTROLLER_SLAVE_ID_SUPPORT`` allows disabling this functionality and the option ``CONFIG_FMB_CONTROLLER_SLAVE_ID_MAX_SIZE`` defines the maximum size of the slave identification structure.
Example of initialization for slave ID:
.. code:: c
#include "mbcontroller.h" // for mbcontroller defines and api
...
static void *mbc_slave_handle = NULL;
mb_communication_info_t comm_config = {
.ser_opts.port = MB_PORT_NUM,
.ser_opts.mode = MB_RTU,
.ser_opts.baudrate = MB_DEV_SPEED,
.ser_opts.parity = MB_PARITY_NONE,
.ser_opts.uid = MB_SLAVE_ADDR,
.ser_opts.data_bits = UART_DATA_8_BITS,
.ser_opts.stop_bits = UART_STOP_BITS_1
};
// Initialization of Modbus slave controller object
ESP_ERROR_CHECK(mbc_slave_create_serial(&comm_config, &mbc_slave_handle));
// Starts of modbus controller and stack
esp_err_t err = mbc_slave_start(mbc_slave_handle);
const char *pdevice_name = "my_slave_device_description"; // the vendor specific part for slave to be retrieved by master
bool is_started = (bool)(err == ESP_OK); // running status of the slave to be reported
// This is the way to set Slave ID information to retrieve it by master using <Report Slave ID> command.
esp_err_t err = mbc_set_slave_id(mbc_slave_handle, comm_config.ser_opts.uid, is_started, (uint8_t *)pdevice_name, strlen(pdevice_name));
if (err == ESP_OK) {
ESP_LOG_BUFFER_HEX_LEVEL("SET_SLAVE_ID", (void*)pdevice_name, strlen(pdevice_name), ESP_LOG_WARN);
} else {
ESP_LOGE("SET_SLAVE_ID", "Set slave ID fail, err=%d.", err);
}
...
:cpp:func:`mbc_get_slave_id`
Allows to get actual slave UID, running status of slave and vendor specific data. The default object identificator is defined by option ``CONFIG_FMB_CONTROLLER_SLAVE_ID`` as ``01 ff 33 22 11`` (slave UID, running state, extended vendor data structure) and can be overridden in user application.
Example to get the actual slave identificator:
.. code:: c
#include "mbcontroller.h"
#include "sdkconfig.h"
...
static void *mbc_slave_handle = NULL; // the object is initialized and started
// the vendor specific part of structure for slave to be retrieved by master
uint8_t current_slave_id[CONFIG_FMB_CONTROLLER_SLAVE_ID_MAX_SIZE] = {0};
esp_err_t err = mbc_get_slave_id(mbc_slave_handle, &current_slave_id[0], &length);
if (err == ESP_OK) {
ESP_LOGW("GET_SLAVE_ID", "Get slave ID, length=%u.", length);
ESP_LOG_BUFFER_HEX_LEVEL("GET_SLAVE_ID", (void*)current_slave_id, length, ESP_LOG_WARN);
} else {
ESP_LOGE("GET_SLAVE_ID", "Get slave ID fail, err=%d.", err);
}
...
.. _modbus_api_slave_communication:
Slave Communication

View File

@@ -310,6 +310,32 @@ static void master_operation_func(void *arg)
ESP_LOGI(TAG, "Start modbus test...");
#if MB_FUNC_OTHER_REP_SLAVEID_ENABLED
// Command - 17 (0x11) Report Slave ID
// The command contains vendor specific data and should be interpreted accordingly.
// This version of command handler needs to define the maximum number
// of registers that can be returned from concrete slave (buffer size).
// The returned slave info data will be stored into the `info_buf`.
// Request fields: slave_addr - the UID of slave, reg_start - not used,
// reg_size = max size of buffer (registers).
mb_param_request_t req = {
.slave_addr = MB_DEVICE_ADDR1, // slave UID to retrieve ID
.command = 0x11, // the <Report Slave ID> command,
.reg_start = 0, // must be zero,
.reg_size = (CONFIG_FMB_CONTROLLER_SLAVE_ID_MAX_SIZE >> 1) // the expected length of buffer in registers
};
uint8_t info_buf[CONFIG_FMB_CONTROLLER_SLAVE_ID_MAX_SIZE] = {0}; // The buffer to save slave ID
// Send the request to retrieve slave ID information from slave (vendor specific command)
err = mbc_master_send_request(master_handle, &req, &info_buf[0]);
if (err != ESP_OK) {
ESP_LOGE("SLAVE_INFO", "Read slave info fail.");
} else {
ESP_LOG_BUFFER_HEX_LEVEL("SLAVE_INFO", (void*)info_buf, sizeof(info_buf), ESP_LOG_WARN);
}
#endif
for(uint16_t retry = 0; retry <= MASTER_MAX_RETRY && (!alarm_state); retry++) {
// Read all found characteristics from slave(s)
for (uint16_t cid = 0; (err != ESP_ERR_NOT_FOUND) && cid < MASTER_MAX_CIDS; cid++) {

View File

@@ -49,6 +49,26 @@ static const char *TAG = "SLAVE_TEST";
static void *mbc_slave_handle = NULL;
#if CONFIG_FMB_CONTROLLER_SLAVE_ID_SUPPORT
#define MB_SLAVE_NAME_MAX_LEN 32
#define INIT_DEV_ID(struct_name, uid, running, serial, name) static struct { \
uint8_t slave_uid; \
uint8_t is_running; \
uint8_t length; \
uint8_t marker; \
uint32_t serial_number; \
char dev_name[MB_SLAVE_NAME_MAX_LEN]; \
} struct_name = { \
.slave_uid = (uid), \
.is_running = (running), \
.marker = 0x55, \
.length = (sizeof(struct_name) - sizeof(struct_name.dev_name) \
+ strlen((name)) - 2), \
.serial_number =(serial), \
.dev_name = name, \
};
#endif
// Set register values into known state
static void setup_reg_data(void)
{
@@ -131,7 +151,7 @@ static void setup_reg_data(void)
void app_main(void)
{
mb_param_info_t reg_info; // keeps the Modbus registers access information
mb_register_area_descriptor_t reg_area; // Modbus register area descriptor structure
mb_register_area_descriptor_t reg_area = {0}; // Modbus register area descriptor structure
// Set UART log level
esp_log_level_set(TAG, ESP_LOG_INFO);
@@ -164,6 +184,7 @@ void app_main(void)
reg_area.address = (void*)&holding_reg_params.holding_data0; // Set pointer to storage instance
// Set the size of register storage instance in bytes
reg_area.size = MB_REG_HOLDING_START_AREA0_SIZE;
reg_area.access = MB_ACCESS_RW;
ESP_ERROR_CHECK(mbc_slave_set_descriptor(mbc_slave_handle, reg_area));
// The second register area
@@ -171,6 +192,7 @@ void app_main(void)
reg_area.start_offset = MB_REG_HOLDING_START_AREA1;
reg_area.address = (void*)&holding_reg_params.holding_data4;
reg_area.size = MB_REG_HOLDING_START_AREA1_SIZE;
reg_area.access = MB_ACCESS_RW;
ESP_ERROR_CHECK(mbc_slave_set_descriptor(mbc_slave_handle, reg_area));
#if CONFIG_FMB_EXT_TYPE_SUPPORT
@@ -179,6 +201,7 @@ void app_main(void)
reg_area.start_offset = MB_REG_HOLDING_START_AREA2;
reg_area.address = (void*)&holding_reg_params.holding_u8_a;
reg_area.size = MB_REG_HOLDING_START_AREA2_SIZE;
reg_area.access = MB_ACCESS_RW;
ESP_ERROR_CHECK(mbc_slave_set_descriptor(mbc_slave_handle, reg_area));
#endif
@@ -199,6 +222,7 @@ void app_main(void)
reg_area.start_offset = MB_REG_COILS_START;
reg_area.address = (void*)&coil_reg_params;
reg_area.size = sizeof(coil_reg_params);
reg_area.access = MB_ACCESS_RW;
ESP_ERROR_CHECK(mbc_slave_set_descriptor(mbc_slave_handle, reg_area));
// Initialization of Discrete Inputs register area
@@ -210,9 +234,6 @@ void app_main(void)
setup_reg_data(); // Set values into known state
// Starts of modbus controller and stack
ESP_ERROR_CHECK(mbc_slave_start(mbc_slave_handle));
// Set UART pin numbers
ESP_ERROR_CHECK(uart_set_pin(MB_PORT_NUM, CONFIG_MB_UART_TXD,
CONFIG_MB_UART_RXD, CONFIG_MB_UART_RTS,
@@ -220,6 +241,24 @@ void app_main(void)
// Set UART driver mode to Half Duplex
ESP_ERROR_CHECK(uart_set_mode(MB_PORT_NUM, UART_MODE_RS485_HALF_DUPLEX));
// Starts of modbus controller and stack
esp_err_t err = mbc_slave_start(mbc_slave_handle);
#if CONFIG_FMB_CONTROLLER_SLAVE_ID_SUPPORT
// Initialize the new slave identificator structure (example)
INIT_DEV_ID(new_id_struct, 0x00, 0x00, 0x11223344, "esp_modbus_serial_slave");
uint8_t is_running = (bool)(err == ESP_OK);
// This is the way to set Slave ID fields to retrieve it by master using report slave ID command.
err = mbc_set_slave_id(mbc_slave_handle, comm_config.ser_opts.uid, is_running, (uint8_t *)&new_id_struct.length, new_id_struct.length);
if (err == ESP_OK) {
ESP_LOGW("SET_SLAVE_ID", "dev_name: %s", (char*)new_id_struct.dev_name);
ESP_LOG_BUFFER_HEX_LEVEL("SET_SLAVE_ID", (void*)&new_id_struct.length, new_id_struct.length, ESP_LOG_WARN);
} else {
ESP_LOGE("SET_SLAVE_ID", "Set slave ID fail, err=%d.", err);
}
#endif
ESP_LOGI(TAG, "Modbus slave stack initialized.");
ESP_LOGI(TAG, "Start modbus test...");

View File

@@ -67,5 +67,5 @@ def test_modbus_serial_communication(config: str, dut: Tuple[ModbusTestDut, Modb
@pytest.mark.multi_dut_modbus_generic
@pytest.mark.parametrize('config', ['dummy_config'])
def test_modbus_serial_generic() -> None:
def test_modbus_serial_generic(config) -> None:
print('The generic serial example tests are not provided yet.')

View File

@@ -3,7 +3,6 @@
*
* SPDX-License-Identifier: Apache-2.0
*/
#include "esp_err.h" // for esp_err_t
#include "mbc_master.h" // for master interface define
#include "esp_modbus_master.h" // for public interface defines
@@ -228,10 +227,28 @@ esp_err_t mbc_master_stop(void *ctx)
/* ----------------------- Callback functions for Modbus stack ---------------------------------*/
// These are executed by modbus stack to read appropriate type of registers.
mb_err_enum_t mbc_reg_common_cb(mb_base_t *inst, uint8_t *pdata, uint16_t address, uint16_t bytes)
{
MB_RETURN_ON_FALSE((pdata), MB_EINVAL, TAG, "incorrect parameters provided.");
mb_master_options_t *popts = MB_MASTER_GET_OPTS(MB_MASTER_GET_IFACE_FROM_BASE(inst));
uint16_t reg_len = popts->reg_buffer_size;
uint8_t *ppar_buffer = (uint8_t *)popts->reg_buffer_ptr; // Get instance address
mb_err_enum_t status = MB_ENOERR;
if (ppar_buffer && !address && (bytes >= 2) && (((reg_len << 1) >= bytes))){
CRITICAL_SECTION(inst->lock) {
memmove(ppar_buffer, pdata, bytes);
}
} else {
status = MB_ENORES;
}
return status;
}
/**
* Modbus master input register callback function.
*
* @param ctx interface context pointer
* @param inst interface context pointer
* @param reg_buffer input register buffer
* @param reg_addr input register address
* @param num_regs input register number
@@ -272,7 +289,7 @@ mb_err_enum_t mbc_reg_input_master_cb(mb_base_t *inst, uint8_t *reg_buffer, uint
/**
* Modbus master holding register callback function.
*
* @param ctx interface context pointer
* @param inst interface context pointer
* @param reg_buffer holding register buffer
* @param reg_addr holding register address
* @param num_regs holding register number
@@ -330,7 +347,7 @@ mb_err_enum_t mbc_reg_holding_master_cb(mb_base_t *inst, uint8_t *reg_buffer, ui
/**
* Modbus master coils callback function.
*
* @param ctx interface context pointer
* @param inst interface context pointer
* @param reg_buffer coils buffer
* @param reg_addr coils address
* @param ncoils coils number
@@ -393,7 +410,7 @@ mb_err_enum_t mbc_reg_coils_master_cb(mb_base_t *inst, uint8_t *reg_buffer, uint
/**
* Modbus master discrete callback function.
*
* @param ctx - pointer to interface structure
* @param inst - pointer to interface structure
* @param reg_buffer discrete buffer
* @param reg_addr discrete address
* @param n_discrete discrete number

View File

@@ -14,7 +14,7 @@
#include "mb_utils.h" // for stack bit setting utilities
#ifdef CONFIG_FMB_CONTROLLER_SLAVE_ID_SUPPORT
#if CONFIG_FMB_CONTROLLER_SLAVE_ID_SUPPORT
#define MB_ID_BYTE0(id) ((uint8_t)(id))
#define MB_ID_BYTE1(id) ((uint8_t)(((uint16_t)(id) >> 8) & 0xFF))
@@ -138,6 +138,37 @@ esp_err_t mbc_slave_unlock(void *ctx)
return ESP_OK;
}
#if CONFIG_FMB_CONTROLLER_SLAVE_ID_SUPPORT
/**
* Set object ID for the Modbus controller
*/
esp_err_t mbc_set_slave_id(void *ctx, uint8_t slave_addr, bool is_running, uint8_t const *pdata, uint8_t data_len)
{
MB_RETURN_ON_FALSE(ctx, ESP_ERR_INVALID_STATE, TAG,
"Slave interface is not correctly initialized.");
mbs_controller_iface_t *pmbs_controller = MB_SLAVE_GET_IFACE(ctx);
// The Report Slave ID functionality is useful for TCP and gateway,
// so the design decision is to keep this functionality for all slaves
// Set the slave ID if the KConfig option is selected
mb_err_enum_t status = mbs_set_slave_id(pmbs_controller->mb_base, slave_addr, is_running, (uint8_t *)pdata, data_len);
MB_RETURN_ON_FALSE((status == MB_ENOERR), ESP_ERR_INVALID_STATE, TAG, "mb stack set slave ID failure.");
return MB_ERR_TO_ESP_ERR(status);
}
/**
* Get object ID from the Modbus controller
*/
esp_err_t mbc_get_slave_id(void *ctx, uint8_t const *pdata, uint8_t *pdata_len)
{
MB_RETURN_ON_FALSE(ctx, ESP_ERR_INVALID_STATE, TAG,
"Slave interface is not correctly initialized.");
mbs_controller_iface_t *pmbs_controller = MB_SLAVE_GET_IFACE(ctx);
mb_err_enum_t status = mbs_get_slave_id(pmbs_controller->mb_base, (uint8_t *)pdata, pdata_len);
MB_RETURN_ON_FALSE((status == MB_ENOERR), ESP_ERR_INVALID_STATE, TAG, "mb stack get slave ID failure.");
return MB_ERR_TO_ESP_ERR(status);
}
#endif
/**
* Start Modbus controller start function
*/
@@ -149,10 +180,11 @@ esp_err_t mbc_slave_start(void *ctx)
mbs_controller_iface_t *mbs_controller = MB_SLAVE_GET_IFACE(ctx);
MB_RETURN_ON_FALSE(mbs_controller->start, ESP_ERR_INVALID_STATE, TAG,
"Slave interface is not correctly configured.");
#ifdef CONFIG_FMB_CONTROLLER_SLAVE_ID_SUPPORT
// Set the slave ID if the KConfig option is selected
mb_err_enum_t status = mb_set_slv_id(mbs_controller->mb_base, MB_SLAVE_ID_SHORT, true, (uint8_t *)mb_slave_id, sizeof(mb_slave_id));
MB_RETURN_ON_FALSE((status == MB_ENOERR), ESP_ERR_INVALID_STATE, TAG, "mb stack set slave ID failure.");
uint8_t slave_uid = mbs_controller->opts.comm_opts.common_opts.uid;
#if CONFIG_FMB_CONTROLLER_SLAVE_ID_SUPPORT
// Set the default slave ID if the KConfig option is selected
error = mbc_set_slave_id(mbs_controller, slave_uid, true, (uint8_t *)mb_slave_id, sizeof(mb_slave_id));
MB_RETURN_ON_FALSE((error == ESP_OK), ESP_ERR_INVALID_STATE, TAG, "mb stack set slave ID failure.");
#endif
error = mbs_controller->start(ctx);
MB_RETURN_ON_FALSE((error == ESP_OK), ESP_ERR_INVALID_STATE, TAG,

View File

@@ -120,6 +120,45 @@ typedef enum {
MB_PARAM_UNKNOWN = 0xFF
} mb_param_type_t;
#define mb_err_var esp_err##__func__##__line__
#define esp_err_var mb_error##__func__##__line__
#define MB_ERR_TO_ESP_ERR(error_code) (__extension__( \
{ \
mb_err_enum_t mb_err_var = (mb_err_enum_t)error_code; \
esp_err_t esp_err_var = ESP_FAIL; \
switch(mb_err_var) { \
case MB_ENOERR: \
esp_err_var = ESP_OK; \
break; \
case MB_ENOREG: \
esp_err_var = ESP_ERR_NOT_SUPPORTED; \
break; \
case MB_ETIMEDOUT: \
esp_err_var = ESP_ERR_TIMEOUT; \
break; \
case MB_EILLFUNC: \
case MB_EINVAL: \
esp_err_var = ESP_ERR_INVALID_RESPONSE; \
break; \
case MB_ERECVDATA: \
esp_err_var = ESP_ERR_INVALID_RESPONSE; \
break; \
case MB_EBUSY: \
case MB_EILLSTATE: \
case MB_EPORTERR: \
case MB_ENORES: \
case MB_ENOCONN: \
esp_err_var = ESP_ERR_INVALID_STATE; \
break; \
default: \
ESP_LOGE(TAG, "%s: Incorrect return code (%x) ", __FUNCTION__, (int)mb_err_var); \
esp_err_var = ESP_FAIL; \
break; \
} \
(esp_err_var); \
} \
))
typedef enum _mb_comm_mode mb_mode_type_t;
typedef struct mb_base_t mb_base_t;

View File

@@ -201,6 +201,39 @@ esp_err_t mbc_slave_get_param_info(void *ctx, mb_param_info_t *reg_info, uint32_
*/
esp_err_t mbc_slave_set_descriptor(void *ctx, mb_register_area_descriptor_t descr_data);
// The support of <0x11 - Report Slave ID> command is intentionally included for TCP slave as well!
#if CONFIG_FMB_CONTROLLER_SLAVE_ID_SUPPORT
/**
* @brief Set Modbus slave identificator
*
* @param[in] ctx context pointer of the initialized modbus interface
* @param[in] uid - unit identifier (short slave address) for Modbus slave
* @param[in] is_running - Modbus slave running status
* @param[in] pdata - pointer to data buffer for extended context (vendor specific)
* @param[in] data_len - length of data buffer (the length of the extended context)
*
* @return
* - ESP_OK: The appropriate identificator is set
* - ESP_ERR_INVALID_ARG: The argument is incorrect
*/
esp_err_t mbc_set_slave_id(void *ctx, uint8_t uid, bool is_running, uint8_t const *pdata, uint8_t data_len);
/*! \brief Get slave identificator from the modbus object.
*
* This function is used to get the Slave ID array for modbus object.
*
* \param ctx - context pointer of the initialized modbus interface
* \param pdata - the pointer to store object ID array from the modbus object
* \param[in/out] pdata_len - in: length of the allocated pdata array,
* out: returns the actual length of object id.
* returns the modbus error code = ESP_OK, if set correctly,
* ESP_ERR_INVALID_RESPONSE - if the object ID is not set,
* ESP_ERR_INVALID_STATE - no space to store object ID in the pdata buffer,
* or incorrect arguments are provided
*/
esp_err_t mbc_get_slave_id(void *ctx, uint8_t const *pdata, uint8_t *pdata_len);
#endif
/**
* @brief Holding register read/write callback function
*

View File

@@ -10,6 +10,8 @@
#include "freertos/FreeRTOS.h" // for task creation and queue access
#include "freertos/task.h" // for task api access
#include "freertos/event_groups.h" // for event groups
#include "freertos/semphr.h" // for semaphore
#include "freertos/queue.h" // for queue api access
#include "driver/uart.h" // for UART types
#include "errno.h" // for errno
#include "esp_log.h" // for log write
@@ -49,6 +51,7 @@ typedef struct {
uint16_t reg_buffer_size; /*!< Modbus data buffer size */
TaskHandle_t task_handle; /*!< Modbus task handle */
EventGroupHandle_t event_group_handle; /*!< Modbus controller event group */
SemaphoreHandle_t mbm_sema; /*!< Modbus controller semaphore */
const mb_parameter_descriptor_t *param_descriptor_table; /*!< Modbus controller parameter description table */
size_t mbm_param_descriptor_size; /*!< Modbus controller parameter description table size */
} mb_master_options_t;
@@ -57,7 +60,7 @@ typedef esp_err_t (*iface_get_cid_info_fp)(void *, uint16_t, const mb_parameter_
typedef esp_err_t (*iface_get_parameter_fp)(void *, uint16_t, uint8_t *, uint8_t *); /*!< Interface get_parameter method */
typedef esp_err_t (*iface_get_parameter_with_fp)(void *, uint16_t, uint8_t, uint8_t *, uint8_t *); /*!< Interface get_parameter_with method */
typedef esp_err_t (*iface_send_request_fp)(void *, mb_param_request_t*, void *); /*!< Interface send_request method */
typedef esp_err_t (*iface_mbm_set_descriptor_fp)(void *, const mb_parameter_descriptor_t*, const uint16_t); /*!< Interface set_descriptor method */
typedef esp_err_t (*iface_mbm_set_descriptor_fp)(void *, const mb_parameter_descriptor_t*, const uint16_t); /*!< Interface set_descriptor method */
typedef esp_err_t (*iface_set_parameter_fp)(void *, uint16_t, uint8_t *, uint8_t *); /*!< Interface set_parameter method */
typedef esp_err_t (*iface_set_parameter_with_fp)(void *, uint16_t, uint8_t, uint8_t *, uint8_t *); /*!< Interface set_parameter_with method */
@@ -65,7 +68,7 @@ typedef esp_err_t (*iface_set_parameter_with_fp)(void *, uint16_t, uint8_t, uint
* @brief Modbus controller interface structure
*/
typedef struct {
mb_base_t *mb_base;
mb_base_t *mb_base;
// Master object interface options
mb_master_options_t opts;
bool is_active; /*!< Interface is active */

View File

@@ -119,6 +119,9 @@ static esp_err_t mbc_serial_master_delete(void *ctx)
mbm_opts->task_handle = NULL;
vEventGroupDelete(mbm_opts->event_group_handle);
mbm_opts->event_group_handle = NULL;
vSemaphoreDelete(mbm_opts->mbm_sema);
mbm_opts->mbm_sema = NULL;
// delete mb_base instance and all its allocations
mb_error = mbm_iface->mb_base->delete(mbm_iface->mb_base);
MB_RETURN_ON_FALSE((mb_error == MB_ENOERR), ESP_ERR_INVALID_STATE, TAG,
"mb stack delete failure, returned (0x%x).", (int)mb_error);
@@ -159,10 +162,8 @@ static esp_err_t mbc_serial_master_send_request(void *ctx, mb_param_request_t *r
MB_RETURN_ON_FALSE((data_ptr), ESP_ERR_INVALID_ARG, TAG, "mb incorrect data pointer.");
mb_err_enum_t mb_error = MB_EBUSY;
esp_err_t error = ESP_FAIL;
if (mb_port_event_res_take(mbm_controller_iface->mb_base->port_obj, pdMS_TO_TICKS(MB_MAX_RESP_DELAY_MS)))
{
if (xSemaphoreTake(mbm_opts->mbm_sema, pdMS_TO_TICKS(MB_MAX_RESP_DELAY_MS)) == pdTRUE) {
uint8_t mb_slave_addr = request->slave_addr;
uint8_t mb_command = request->command;
uint16_t mb_offset = request->reg_start;
@@ -172,11 +173,14 @@ static esp_err_t mbc_serial_master_send_request(void *ctx, mb_param_request_t *r
mbm_opts->reg_buffer_ptr = (uint8_t *)data_ptr;
mbm_opts->reg_buffer_size = mb_size;
mb_port_event_res_release(mbm_controller_iface->mb_base->port_obj);
// Calls appropriate request function to send request and waits response
switch (mb_command)
{
#if MB_FUNC_OTHER_REP_SLAVEID_ENABLED
case MB_FUNC_OTHER_REPORT_SLAVEID:
mb_error = mbm_rq_report_slave_id(mbm_controller_iface->mb_base, mb_slave_addr, pdMS_TO_TICKS(MB_MAX_RESP_DELAY_MS));
break;
#endif
#if MB_FUNC_READ_COILS_ENABLED
case MB_FUNC_READ_COILS:
@@ -257,39 +261,13 @@ static esp_err_t mbc_serial_master_send_request(void *ctx, mb_param_request_t *r
mb_error = MB_ENOREG;
break;
}
} else {
ESP_LOGD(TAG, "%s:MBC semaphore take fail.", __func__);
}
(void)xSemaphoreGive(mbm_opts->mbm_sema);
// Propagate the Modbus errors to higher level
switch (mb_error)
{
case MB_ENOERR:
error = ESP_OK;
break;
case MB_ENOREG:
error = ESP_ERR_NOT_SUPPORTED; // Invalid register request
break;
case MB_ETIMEDOUT:
error = ESP_ERR_TIMEOUT; // Slave did not send response
break;
case MB_EILLFUNC:
case MB_ERECVDATA:
error = ESP_ERR_INVALID_RESPONSE; // Invalid response from slave
break;
case MB_EBUSY:
error = ESP_ERR_INVALID_STATE; // Master is busy (previous request is pending)
break;
default:
ESP_LOGE(TAG, "%s: Incorrect return code (%x) ", __FUNCTION__, (uint16_t)mb_error);
error = ESP_FAIL;
break;
}
return error;
return MB_ERR_TO_ESP_ERR(mb_error);
}
static esp_err_t mbc_serial_master_get_cid_info(void *ctx, uint16_t cid, const mb_parameter_descriptor_t **param_buffer)
@@ -623,6 +601,10 @@ static esp_err_t mbc_serial_master_controller_create(void **ctx)
// Initialization of active context of the modbus controller
mbm_opts->event_group_handle = xEventGroupCreate();
MB_GOTO_ON_FALSE((mbm_opts->event_group_handle), ESP_ERR_INVALID_STATE, error, TAG, "mb event group error.");
mbm_opts->mbm_sema = xSemaphoreCreateBinary();
MB_GOTO_ON_FALSE((mbm_opts->mbm_sema != NULL), ESP_ERR_NO_MEM, error, TAG, "%s: mbm resource create error.", __func__);
(void)xSemaphoreGive(mbm_opts->mbm_sema);
// Create modbus controller task
status = xTaskCreatePinnedToCore((void *)&mbc_ser_master_task,
"mbc_ser_master",

View File

@@ -168,9 +168,8 @@ static esp_err_t mbc_tcp_master_send_request(void *ctx, mb_param_request_t *requ
MB_RETURN_ON_FALSE((data_ptr), ESP_ERR_INVALID_ARG, TAG, "mb incorrect data pointer.");
mb_err_enum_t mb_error = MB_EBUSY;
esp_err_t error = ESP_FAIL;
if (mb_port_event_res_take(mbm_controller_iface->mb_base->port_obj, pdMS_TO_TICKS(MB_MAX_RESP_DELAY_MS))) {
if (xSemaphoreTake(mbm_opts->mbm_sema, pdMS_TO_TICKS(MB_MAX_RESP_DELAY_MS)) == pdTRUE) {
uint8_t mb_slave_addr = request->slave_addr;
uint8_t mb_command = request->command;
uint16_t mb_offset = request->reg_start;
@@ -180,8 +179,6 @@ static esp_err_t mbc_tcp_master_send_request(void *ctx, mb_param_request_t *requ
mbm_opts->reg_buffer_ptr = (uint8_t *)data_ptr;
mbm_opts->reg_buffer_size = mb_size;
mb_port_event_res_release(mbm_controller_iface->mb_base->port_obj);
// Calls appropriate request function to send request and waits response
switch(mb_command) {
#if MB_FUNC_READ_COILS_ENABLED
@@ -263,38 +260,13 @@ static esp_err_t mbc_tcp_master_send_request(void *ctx, mb_param_request_t *requ
mb_error = MB_ENOREG;
break;
}
} else {
ESP_LOGD(TAG, "%s:MBC semaphore take fail.", __func__);
}
(void)xSemaphoreGive(mbm_opts->mbm_sema);
// Propagate the Modbus errors to higher level
switch(mb_error) {
case MB_ENOERR:
error = ESP_OK;
break;
case MB_ENOREG:
error = ESP_ERR_NOT_SUPPORTED; // Invalid register request
break;
case MB_ETIMEDOUT:
error = ESP_ERR_TIMEOUT; // Slave did not send response
break;
case MB_EILLFUNC:
case MB_ERECVDATA:
error = ESP_ERR_INVALID_RESPONSE; // Invalid response from slave
break;
case MB_EBUSY:
error = ESP_ERR_INVALID_STATE; // Master is busy (previous request is pending)
break;
default:
ESP_LOGE(TAG, "%s: Incorrect return code (%x) ", __FUNCTION__, (int)mb_error);
error = ESP_FAIL;
break;
}
return error;
return MB_ERR_TO_ESP_ERR(mb_error);
}
static esp_err_t mbc_tcp_master_get_cid_info(void *ctx, uint16_t cid, const mb_parameter_descriptor_t** param_buffer)
@@ -622,7 +594,8 @@ static esp_err_t mbc_tcp_master_delete(void *ctx)
mbm_opts->task_handle = NULL;
vEventGroupDelete(mbm_opts->event_group_handle);
mbm_opts->event_group_handle = NULL;
vSemaphoreDelete(mbm_opts->mbm_sema);
mbm_opts->mbm_sema = NULL;
mb_error = mbm_iface->mb_base->delete(mbm_iface->mb_base);
MB_RETURN_ON_FALSE((mb_error == MB_ENOERR), ESP_ERR_INVALID_STATE, TAG,
"mb stack delete failure, returned (0x%x).", (unsigned)mb_error);
@@ -650,6 +623,10 @@ esp_err_t mbc_tcp_master_controller_create(void ** ctx)
// Parameter change notification queue
mbm_opts->event_group_handle = xEventGroupCreate();
MB_GOTO_ON_FALSE((mbm_opts->event_group_handle), ESP_ERR_INVALID_STATE, error, TAG, "mb event group error.");
mbm_opts->mbm_sema = xSemaphoreCreateBinary();
MB_GOTO_ON_FALSE((mbm_opts->mbm_sema != NULL), ESP_ERR_NO_MEM, error, TAG, "%s: mbm resource create error.", __func__);
(void)xSemaphoreGive(mbm_opts->mbm_sema);
// Create modbus controller task
status = xTaskCreatePinnedToCore((void *)&modbus_tcp_master_task,
"mbm_ctrl_tcp_task",

View File

@@ -73,7 +73,6 @@ mb_exception_t mbs_fn_write_holding_reg(mb_base_t *inst, uint8_t *frame_ptr, uin
reg_addr = (uint16_t)(frame_ptr[MB_PDU_FUNC_WRITE_ADDR_OFF] << 8);
reg_addr |= (uint16_t)(frame_ptr[MB_PDU_FUNC_WRITE_ADDR_OFF + 1]);
reg_addr++;
/* Make callback to update the value. */
if (inst->rw_cbs.reg_holding_cb) {
reg_status = inst->rw_cbs.reg_holding_cb(inst, &frame_ptr[MB_PDU_FUNC_WRITE_VALUE_OFF], reg_addr, 1, MB_REG_WRITE);

View File

@@ -28,29 +28,109 @@
*
* File: $Id: mbfuncother.c, v 1.8 2006/12/07 22:10:34 wolti Exp $
*/
#include <mb_common.h>
#include <mb_proto.h>
#include <sys/param.h>
#include "mb_common.h"
#include "mb_proto.h"
#include "mb_slave.h"
#include "mb_master.h"
#if MB_FUNC_OTHER_REP_SLAVEID_ENABLED
#if MB_FUNC_OTHER_REP_SLAVEID_ENABLED && MB_FUNC_OTHER_REP_SLAVEID_BUF
#define MB_PDU_BYTECNT_OFF (MB_PDU_DATA_OFF + 0)
#define MB_PDU_FUNC_DATA_OFF (MB_PDU_DATA_OFF + 1)
#define MB_CMD_SL_ID_LEN (1)
#define MB_SLAVE_ID_CHUNK_SIZE (MIN(MB_FUNC_OTHER_REP_SLAVEID_BUF, 32))
/* ----------------------- Start implementation -----------------------------*/
mb_err_enum_t mb_set_slv_id(mb_base_t *inst, uint8_t slv_id, bool is_running, uint8_t const *slv_idstr, uint16_t slv_idstr_len)
{
mb_err_enum_t status = MB_ENOERR;
mb_exception_t mb_error_to_exception(mb_err_enum_t error_code);
/* the first byte and second byte in the buffer is reserved for
* the parameter slv_id and the running flag. The rest of
* the buffer is available for additional data. */
if (slv_idstr_len + 2 < MB_FUNC_OTHER_REP_SLAVEID_BUF) {
inst->slave_id_len = 0;
inst->slave_id[inst->slave_id_len++] = slv_id;
inst->slave_id[inst->slave_id_len++] = (uint8_t)(is_running ? 0xFF : 0x00);
if (slv_idstr_len > 0) {
memcpy(&inst->slave_id[inst->slave_id_len], slv_idstr,
(size_t)slv_idstr_len);
inst->slave_id_len += slv_idstr_len;
mb_err_enum_t mbm_rq_report_slave_id(mb_base_t *inst, uint8_t slave_addr, uint32_t timeout)
{
uint8_t *mb_frame_ptr = NULL;
mb_err_enum_t err = MB_ENOERR;
if (!inst || !inst->port_obj || (slave_addr > MB_ADDRESS_MAX)) {
err = MB_EINVAL;
} else if (!mb_port_event_res_take(inst->port_obj, timeout)) {
err = MB_EBUSY;
} else {
inst->get_send_buf(inst, &mb_frame_ptr);
inst->set_dest_addr(inst, slave_addr);
mb_frame_ptr[MB_PDU_FUNC_OFF] = MB_FUNC_OTHER_REPORT_SLAVEID;
inst->set_send_len(inst, MB_CMD_SL_ID_LEN);
(void)mb_port_event_post(inst->port_obj, EVENT(EV_FRAME_TRANSMIT | EV_TRANS_START));
err = mb_port_event_wait_req_finish(inst->port_obj);
}
return err;
}
mb_exception_t mbm_fn_report_slave_id(mb_base_t *inst, uint8_t *pframe, uint16_t *plen)
{
uint8_t byte_count = 0;
mb_exception_t status = MB_EX_NONE;
mb_err_enum_t err;
if (!inst || !plen || !pframe) {
status = MB_EX_SLAVE_DEVICE_FAILURE;
} else if (*plen <= MB_BUFFER_SIZE - 2) {
byte_count = pframe[MB_PDU_BYTECNT_OFF];
// Transfer data from command buffer.
err = mbc_reg_common_cb(inst, &pframe[MB_PDU_FUNC_DATA_OFF], 0, byte_count);
// If an err occured convert it into a Modbus exception.
if (err != MB_ENOERR) {
status = mb_error_to_exception(err);
}
} else {
// Can't be a valid request because the length is incorrect.
status = MB_EX_ILLEGAL_DATA_VALUE;
}
return status;
}
mb_exception_t mbs_fn_report_slave_id(mb_base_t *inst, uint8_t *pframe, uint16_t *plen_buf)
{
mb_exception_t status = MB_EX_NONE;
if (!inst || !pframe || !plen_buf || !inst->pobj_id || !inst->obj_id_len) {
status = MB_EX_SLAVE_DEVICE_FAILURE;
} else if ((inst->obj_id_len <= MB_BUFFER_SIZE - 2)
&& (*plen_buf == MB_CMD_SL_ID_LEN)) {
CRITICAL_SECTION(inst->lock) {
pframe[MB_PDU_FUNC_OFF] = MB_FUNC_OTHER_REPORT_SLAVEID; // rewrite the FC
*plen_buf = (uint16_t)(inst->obj_id_len);
pframe[MB_PDU_BYTECNT_OFF] = *plen_buf;
memcpy(&pframe[MB_PDU_FUNC_DATA_OFF], inst->pobj_id, (size_t)inst->obj_id_len);
*plen_buf += 2; // count function code + length in frame length
}
} else {
status = MB_EX_ILLEGAL_DATA_VALUE;
}
return status;
}
mb_err_enum_t mbs_set_slave_id(mb_base_t *inst, uint8_t slave_id, bool is_running, uint8_t const *pdata, uint8_t data_len)
{
mb_err_enum_t status = MB_ENOERR;
// the first byte and second byte in the buffer is reserved for
// the parameter slave_id and the running flag. The rest of
// the buffer is available for additional data.
if (inst && inst->lock && (data_len + 2 <= MB_FUNC_OTHER_REP_SLAVEID_BUF)) {
uint8_t chunk_num = ((data_len + 2) / MB_SLAVE_ID_CHUNK_SIZE) + 1;
if (!inst->pobj_id || inst->obj_id_chunks != chunk_num) {
CRITICAL_SECTION(inst->lock) {
inst->pobj_id = realloc(inst->pobj_id, (chunk_num * MB_SLAVE_ID_CHUNK_SIZE));
}
}
if (!inst->pobj_id) {
return MB_ENORES;
}
CRITICAL_SECTION(inst->lock) {
inst->obj_id_len = 0;
inst->pobj_id[inst->obj_id_len++] = slave_id;
inst->pobj_id[inst->obj_id_len++] = (uint8_t)(is_running ? 0xFF : 0x00);
if (data_len > 0) {
memcpy(&inst->pobj_id[inst->obj_id_len], pdata, (size_t)data_len);
inst->obj_id_len += data_len;
inst->obj_id_chunks = chunk_num;
}
}
} else {
status = MB_ENORES;
@@ -58,11 +138,25 @@ mb_err_enum_t mb_set_slv_id(mb_base_t *inst, uint8_t slv_id, bool is_running, ui
return status;
}
mb_exception_t mb_fn_report_slv_id(mb_base_t *inst, uint8_t *frame_ptr, uint16_t *len_buf)
mb_err_enum_t mbs_get_slave_id(mb_base_t *inst, uint8_t *pdata, uint8_t *pdata_len)
{
memcpy(&frame_ptr[MB_PDU_DATA_OFF], &inst->slave_id[0], (size_t)inst->slave_id_len);
*len_buf = (uint16_t)(MB_PDU_DATA_OFF + inst->slave_id_len);
return MB_EX_NONE;
mb_err_enum_t status = MB_ENOERR;
if (inst && inst->lock && pdata_len) {
if (!inst->pobj_id) {
return MB_ENOREG;
}
if (pdata && (*pdata_len >= inst->obj_id_len)) {
CRITICAL_SECTION(inst->lock) {
memcpy(pdata, &inst->pobj_id[0],(size_t)inst->obj_id_len);
}
} else {
status = MB_ENORES;
}
*pdata_len = inst->obj_id_len;
} else {
status = MB_EINVAL;
}
return status;
}
#endif

View File

@@ -126,3 +126,4 @@ mb_exception_t mb_error_to_exception(mb_err_enum_t error_code)
return status;
}

View File

@@ -133,8 +133,11 @@ struct mb_base_t
mb_trans_base_t *transp_obj;
mb_port_base_t *port_obj;
uint8_t slave_id[MB_FUNC_OTHER_REP_SLAVEID_BUF];
uint16_t slave_id_len;
#if MB_FUNC_OTHER_REP_SLAVEID_ENABLED
uint8_t *pobj_id;
uint16_t obj_id_len;
uint8_t obj_id_chunks;
#endif
mb_delete_fp delete;
mb_enable_fp enable;
@@ -166,7 +169,6 @@ mb_err_enum_t mbs_delete(mb_base_t *inst);
mb_err_enum_t mbs_enable(mb_base_t *inst);
mb_err_enum_t mbs_disable(mb_base_t *inst);
mb_err_enum_t mbs_poll(mb_base_t *inst);
mb_err_enum_t mbs_set_slv_id(mb_base_t *inst, uint8_t slv_id, bool is_running, uint8_t const *slv_idstr, uint16_t slv_idstr_len);
#if (CONFIG_FMB_COMM_MODE_RTU_EN || CONFIG_FMB_COMM_MODE_ASCII_EN)

View File

@@ -127,7 +127,7 @@ extern "C" {
* how to set this value. It is only used if MB_FUNC_OTHER_REP_SLAVEID_ENABLED
* is set to <code>1</code>.
*/
#define MB_FUNC_OTHER_REP_SLAVEID_BUF (32)
#define MB_FUNC_OTHER_REP_SLAVEID_BUF (CONFIG_FMB_CONTROLLER_SLAVE_ID_MAX_SIZE)
/*! \brief If the <em>Report Slave ID</em> function should be enabled. */
#define MB_FUNC_OTHER_REP_SLAVEID_ENABLED (CONFIG_FMB_CONTROLLER_SLAVE_ID_SUPPORT)

View File

@@ -14,9 +14,9 @@ extern "C" {
typedef struct mb_base_t mb_base_t;
#if MB_FUNC_OTHER_REP_SLAVEID_BUF
mb_exception_t mb_fn_report_slv_id(mb_base_t *inst, uint8_t *frame_ptr,uint16_t *len_buf);
mb_exception_t mbm_fn_report_slv_id(mb_base_t *inst, uint8_t *frame_ptr,uint16_t *len_buf);
#if MB_FUNC_OTHER_REP_SLAVEID_ENABLED
mb_exception_t mbs_fn_report_slave_id(mb_base_t *inst, uint8_t *frame_ptr,uint16_t *len_buf);
mb_exception_t mbm_fn_report_slave_id(mb_base_t *inst, uint8_t *frame_ptr,uint16_t *len_buf);
#endif
#if MB_FUNC_READ_INPUT_ENABLED

View File

@@ -23,6 +23,27 @@ mb_err_enum_t mbm_rq_read_coils(mb_base_t *inst, uint8_t snd_addr, uint16_t coil
mb_err_enum_t mbm_rq_write_coil(mb_base_t *inst, uint8_t snd_addr, uint16_t coil_addr, uint16_t coil_data, uint32_t tout);
mb_err_enum_t mbm_rq_write_multi_coils(mb_base_t *inst, uint8_t snd_addr, uint16_t coil_addr, uint16_t coil_num, uint8_t *data_ptr, uint32_t tout);
#if MB_FUNC_OTHER_REP_SLAVEID_ENABLED
mb_err_enum_t mbm_rq_report_slave_id(mb_base_t *inst, uint8_t slave_addr, uint32_t timeout);
mb_exception_t mbm_fn_report_slave_id(mb_base_t *inst, uint8_t * pframe, uint16_t *usLen);
/*! \ingroup modbus_registers
* \brief The common callback function used to transfer common data as bytes from command buffer in little endian format.
*
* \param pdata A pointer to data in command buffer to be transferred.
* \param address Unused for this function == 0.
* \param bytes Number of bytes the callback function must supply.
*
* \return The function must return one of the following error codes:
* - mb_err_enum_t::MB_ENOERR If no error occurred. In this case a normal
* Modbus response is sent.
* - mb_err_enum_t::MB_ENOREG if can not map the data of the registers
* - mb_err_enum_t::MB_EILLSTATE if can not procceed with data transfer due to critical error
* - mb_err_enum_t::MB_EINVAL if value data can not be transferred
*/
mb_err_enum_t mbc_reg_common_cb(mb_base_t *inst, uint8_t *pdata, uint16_t address, uint16_t bytes);
#endif
#ifdef __cplusplus
}
#endif

View File

@@ -11,8 +11,9 @@
extern "C" {
#endif
mb_err_enum_t mb_set_slv_id(mb_base_t *inst, uint8_t slv_id, bool is_running, uint8_t const *slv_idstr, uint16_t slv_idstr_len);
#if MB_FUNC_OTHER_REP_SLAVEID_ENABLED
mb_exception_t mbs_fn_report_slave_id(mb_base_t *inst, uint8_t *frame_ptr, uint16_t *plen_buf);
#endif
#ifdef __cplusplus
}

View File

@@ -100,23 +100,39 @@ void mb_util_set_bits(uint8_t *byte_buf, uint16_t bit_offset, uint8_t but_num, u
*/
uint8_t mb_util_get_bits(uint8_t *byte_buf, uint16_t bit_offset, uint8_t but_num);
#if MB_FUNC_OTHER_REP_SLAVEID_ENABLED
/*! \brief Standard function to set slave ID in the modbus object.
*
* This function is used to set the Slave ID array for modbus object.
* This ID can then be read over Modbus.
*
* \param inst - instance pointer to base modbus object
* \param slv_id - slave short address.
* \param slave_id - slave short address.
* \param is_running - true, if the slave is running, false otherwise
* \param slv_idstr - the pointer to slave ID array to set in the modbus object
* \param slv_idstr_len - slave ID array length
* \param pdata - the pointer to slave ID array to set in the modbus object
* \param len - slave ID array length
*
* returns the modbus error code = MB_ENOERR, if set correctly, MB_ENOREG, otherwise
* \endcode
*/
mb_err_enum_t mb_set_slv_id(mb_base_t *inst, uint8_t slv_id, bool is_running, uint8_t const *slv_idstr, uint16_t slv_idstr_len);
mb_err_enum_t mbs_set_slave_id(mb_base_t *inst, uint8_t sl_id, bool is_running, uint8_t const *pdata, uint16_t len);
/*! \brief Standard function to get slave ID from the modbus object.
*
* This function is used to get the Slave ID array for modbus object.
* This ID can then be read over Modbus.
*
* \param inst - instance pointer to base modbus object
* \param pdata - the pointer to store object ID array from the modbus object
* \param[in/out] pdata_len - input length of the allocated pdata array,
* returns the actual length of object id.
* returns the modbus error code = MB_ENOERR, if set correctly,
* MB_ENOREG - if the object ID is not set,
* MB_ENORES - no space to store object ID in the pdata buffer,
* MB_EINVAL - the arguments are not correct
*/
mb_err_enum_t mbs_get_slave_id(mb_base_t *inst, uint8_t *pdata, uint8_t *pdata_len);
#endif
#ifdef __cplusplus
}

View File

@@ -21,7 +21,7 @@ static const char *TAG = "mb_object.master";
static const mb_fn_handler_t master_handlers[MB_FUNC_HANDLERS_MAX] =
{
#if MB_FUNC_OTHER_REP_SLAVEID_ENABLED
{MB_FUNC_OTHER_REPORT_SLAVEID, (void *)mb_fn_report_slv_id},
{MB_FUNC_OTHER_REPORT_SLAVEID, (void *)mbm_fn_report_slave_id},
#endif
#if MB_FUNC_READ_INPUT_ENABLED
{MB_FUNC_READ_INPUT_REGISTER, (void *)mbm_fn_read_inp_reg},
@@ -109,6 +109,11 @@ mb_err_enum_t mbm_rtu_create(mb_serial_opts_t *ser_opts, void **in_out_obj)
mbm_obj->base.descr.is_master = true;
mbm_obj->base.descr.obj_name = (char *)TAG;
mbm_obj->base.descr.inst_index = mb_port_get_inst_counter_inc();
#if MB_FUNC_OTHER_REP_SLAVEID_ENABLED
mbm_obj->base.pobj_id = NULL;
mbm_obj->base.obj_id_len = 0;
mbm_obj->base.obj_id_chunks = 0;
#endif
int res = asprintf(&mbm_obj->base.descr.parent_name, "mbm_rtu@%p", mbm_obj->base.descr.parent);
MB_GOTO_ON_FALSE((res), MB_EILLSTATE, error,
TAG, "name alloc fail, err: %d", (int)res);
@@ -166,6 +171,11 @@ mb_err_enum_t mbm_ascii_create(mb_serial_opts_t *ser_opts, void **in_out_obj)
mbm_obj->base.descr.is_master = true;
mbm_obj->base.descr.obj_name = (char *)TAG;
mbm_obj->base.descr.inst_index = mb_port_get_inst_counter_inc();
#if MB_FUNC_OTHER_REP_SLAVEID_ENABLED
mbm_obj->base.pobj_id = NULL;
mbm_obj->base.obj_id_len = 0;
mbm_obj->base.obj_id_chunks = 0;
#endif
int res = asprintf(&mbm_obj->base.descr.parent_name, "mbm_ascii@%p", mbm_obj->base.descr.parent);
MB_GOTO_ON_FALSE((res), MB_EILLSTATE, error,
TAG, "name alloc fail, err: %d", (int)res);
@@ -223,6 +233,11 @@ mb_err_enum_t mbm_tcp_create(mb_tcp_opts_t *tcp_opts, void **in_out_obj)
mbm_obj->base.descr.is_master = true;
mbm_obj->base.descr.obj_name = (char *)TAG;
mbm_obj->base.descr.inst_index = mb_port_get_inst_counter_inc();
#if MB_FUNC_OTHER_REP_SLAVEID_ENABLED
mbm_obj->base.pobj_id = NULL;
mbm_obj->base.obj_id_len = 0;
mbm_obj->base.obj_id_chunks = 0;
#endif
int res = asprintf(&mbm_obj->base.descr.parent_name, "mbm_tcp#%p", mbm_obj->base.descr.parent);
MB_GOTO_ON_FALSE((res), MB_EILLSTATE, error,
TAG, "name alloc fail, err: %d", (int)res);
@@ -263,6 +278,16 @@ mb_err_enum_t mbm_delete(mb_base_t *inst)
// call destructor of the transport object
mbm_obj->base.transp_obj->frm_delete(inst->transp_obj);
}
#if MB_FUNC_OTHER_REP_SLAVEID_ENABLED
// check object ID
if (mbm_obj->base.pobj_id) {
free(mbm_obj->base.pobj_id);
mbm_obj->base.pobj_id = NULL;
mbm_obj->base.obj_id_len = 0;
mbm_obj->base.obj_id_chunks = 0;
ESP_LOGW(TAG, "%p, Master object ID is not supported!", mbm_obj);
}
#endif
// delete the modbus instance
free(mbm_obj->base.descr.parent_name);
CRITICAL_SECTION_CLOSE(inst->lock);

View File

@@ -18,10 +18,32 @@ static const char *TAG = "mb_object.slave";
#if (MB_SLAVE_ASCII_ENABLED || MB_SLAVE_RTU_ENABLED || MB_TCP_ENABLED)
#if (MB_SLAVE_ASCII_ENABLED || MB_SLAVE_RTU_ENABLED)
typedef struct _port_serial_opts mb_serial_opts_t;
#endif
typedef struct
{
mb_base_t base;
// here are slave object properties and methods
uint8_t mb_address;
mb_comm_mode_t cur_mode;
const mb_fn_handler_t *func_handlers;
mb_state_enum_t cur_state;
uint8_t *frame;
uint16_t length;
uint8_t func_code;
uint8_t rcv_addr;
uint64_t curr_trans_id;
volatile uint16_t *pdu_snd_len;
} mbs_object_t;
static mb_fn_handler_t slave_handlers[MB_FUNC_HANDLERS_MAX] =
{
#if MB_FUNC_OTHER_REP_SLAVEID_ENABLED
{MB_FUNC_OTHER_REPORT_SLAVEID, (void *)mb_fn_report_slv_id},
{MB_FUNC_OTHER_REPORT_SLAVEID, (void *)mbs_fn_report_slave_id},
#endif
#if MB_FUNC_READ_INPUT_ENABLED
{MB_FUNC_READ_INPUT_REGISTER, (void *)mbs_fn_read_input_reg},
@@ -52,31 +74,10 @@ static mb_fn_handler_t slave_handlers[MB_FUNC_HANDLERS_MAX] =
#endif
};
typedef struct
{
mb_base_t base;
// here are slave object properties and methods
uint8_t mb_address;
mb_comm_mode_t cur_mode;
mb_state_enum_t cur_state;
mb_fn_handler_t *func_handlers;
uint8_t *frame;
uint16_t length;
uint8_t func_code;
uint8_t rcv_addr;
uint64_t curr_trans_id;
volatile uint16_t *pdu_snd_len;
} mbs_object_t;
mb_err_enum_t mbs_tcp_create(mb_tcp_opts_t *tcp_opts, void **in_out_obj);
mb_err_enum_t mbs_delete(mb_base_t *inst);
mb_err_enum_t mbs_enable(mb_base_t *inst);
mb_err_enum_t mbs_disable(mb_base_t *inst);
mb_err_enum_t mbs_poll(mb_base_t *inst);
mb_err_enum_t mbs_set_slv_id(mb_base_t *inst, uint8_t slv_id, bool is_running, uint8_t const *slv_idstr, uint16_t slv_idstr_len);
typedef struct _port_serial_opts mb_serial_opts_t;
#if (MB_SLAVE_RTU_ENABLED)
@@ -98,6 +99,11 @@ mb_err_enum_t mbs_rtu_create(mb_serial_opts_t *ser_opts, void **in_out_obj)
mbs_obj->base.descr.is_master = false;
mbs_obj->base.descr.obj_name = (char *)TAG;
mbs_obj->base.descr.inst_index = mb_port_get_inst_counter_inc();
#if MB_FUNC_OTHER_REP_SLAVEID_ENABLED
mbs_obj->base.pobj_id = NULL;
mbs_obj->base.obj_id_len = 0;
mbs_obj->base.obj_id_chunks = 0;
#endif
int res = asprintf(&mbs_obj->base.descr.parent_name, "mbs_rtu@%p", *in_out_obj);
MB_GOTO_ON_FALSE((res), MB_EILLSTATE, error,
TAG, "name alloc fail, err: %d", (int)res);
@@ -148,6 +154,11 @@ mb_err_enum_t mbs_ascii_create(mb_serial_opts_t *ser_opts, void **in_out_obj)
mbs_obj->base.descr.is_master = false;
mbs_obj->base.descr.obj_name = (char *)TAG;
mbs_obj->base.descr.inst_index = mb_port_get_inst_counter_inc();
#if MB_FUNC_OTHER_REP_SLAVEID_ENABLED
mbs_obj->base.pobj_id = NULL;
mbs_obj->base.obj_id_len = 0;
mbs_obj->base.obj_id_chunks = 0;
#endif
int res = asprintf(&mbs_obj->base.descr.parent_name, "mbs_ascii@%p", *in_out_obj);
MB_GOTO_ON_FALSE((res), MB_EILLSTATE, error,
TAG, "name alloc fail, err: %d", (int)res);
@@ -199,6 +210,11 @@ mb_err_enum_t mbs_tcp_create(mb_tcp_opts_t *tcp_opts, void **in_out_obj)
mbs_obj->base.descr.is_master = false;
mbs_obj->base.descr.obj_name = (char *)TAG;
mbs_obj->base.descr.inst_index = mb_port_get_inst_counter_inc();
#if MB_FUNC_OTHER_REP_SLAVEID_ENABLED
mbs_obj->base.pobj_id = NULL;
mbs_obj->base.obj_id_len = 0;
mbs_obj->base.obj_id_chunks = 0;
#endif
int res = asprintf(&mbs_obj->base.descr.parent_name, "mbs_tcp@%p", *in_out_obj);
MB_GOTO_ON_FALSE((res), MB_EILLSTATE, error,
TAG, "name alloc fail, err: %d", (int)res);
@@ -239,6 +255,15 @@ mb_err_enum_t mbs_delete(mb_base_t *inst)
// call destructor of the transport object
MB_OBJ(mbs_obj->base.transp_obj)->frm_delete(inst->transp_obj);
}
#if MB_FUNC_OTHER_REP_SLAVEID_ENABLED
// delete allocated slave ID
if (mbs_obj->base.pobj_id) {
free(mbs_obj->base.pobj_id);
mbs_obj->base.pobj_id = NULL;
mbs_obj->base.obj_id_len = 0;
mbs_obj->base.obj_id_chunks = 0;
}
#endif
// delete the modbus instance
free(mbs_obj->base.descr.parent_name);
CRITICAL_SECTION_CLOSE(inst->lock);

View File

@@ -23,7 +23,7 @@ extern "C"
{
#endif
#define MB_SER_PDU_SIZE_MIN (4)
#define MB_SER_PDU_SIZE_MIN (3)
#define MB_TIMER_TICS_PER_MS (20UL) // Define number of timer reloads per 1 mS
#define MB_TIMER_TICK_TIME_US (1000 / MB_TIMER_TICS_PER_MS) // 50uS = one discreet for timer
#define MB_EVENT_QUEUE_TIMEOUT_MAX_MS (3000)

View File

@@ -22,7 +22,7 @@ extern "C" {
#if (CONFIG_FMB_COMM_MODE_ASCII_EN)
/* ----------------------- Defines ------------------------------------------*/
#define MB_ASCII_SER_PDU_SIZE_MIN 5 /*!< Minimum size of a Modbus ASCII frame. */
#define MB_ASCII_SER_PDU_SIZE_MIN 3 /*!< Minimum size of a Modbus ASCII frame. */
#define MB_ASCII_SER_PDU_SIZE_MAX MB_SER_PDU_SIZE_MAX * 2 /*!< Maximum size of a Modbus ASCII frame. */
#define MB_ASCII_SER_PDU_SIZE_LRC 1 /*!< Size of LRC field in PDU. */
#define MB_ASCII_SER_PDU_ADDR_OFF 0 /*!< Offset of slave address in Ser-PDU. */

View File

@@ -248,7 +248,7 @@ static bool mbm_rtu_transp_timer_expired(void *inst)
case MB_TMODE_RESPOND_TIMEOUT:
mb_port_event_set_err_type(transp->base.port_obj, EV_ERROR_RESPOND_TIMEOUT);
need_poll = mb_port_event_post(transp->base.port_obj, EVENT(EV_ERROR_PROCESS));
ESP_EARLY_LOGW(TAG, "%p:EV_ERROR_RESPOND_TIMEOUT", transp->base.descr.parent);
ESP_EARLY_LOGD(TAG, "%p:EV_ERROR_RESPOND_TIMEOUT", transp->base.descr.parent);
break;
case MB_TMODE_CONVERT_DELAY:

View File

@@ -245,6 +245,13 @@ class ModbusPDU10_Write_Multiple_Registers_Exception(Packet):
fields_desc = [ XByteField("funcCode", 0x90),
ByteEnumField("exceptCode", 1, modbus_exceptions)]
# Custom command
class ModbusPDUXX_Custom_Request(Packet):
name = "Custom Request"
fields_desc = [
FieldListField("customBytes", [0x00], XByteField("", 0x00))
]
# 0x11 - Report Slave Id
class ModbusPDU11_Report_Slave_Id(Packet):
name = "Report Slave Id"
@@ -252,10 +259,13 @@ class ModbusPDU11_Report_Slave_Id(Packet):
class ModbusPDU11_Report_Slave_Id_Answer(Packet):
name = "Report Slave Id Answer"
fields_desc = [ XByteField("funcCode", 0x11),
BitFieldLenField("byteCount", None, 8, length_of="slaveId"),
ConditionalField(StrLenField("slaveId", "", length_from = lambda pkt: pkt.byteCount), lambda pkt: pkt.byteCount>0),
ConditionalField(XByteField("runIdicatorStatus", 0x00), lambda pkt: pkt.byteCount>0)]
fields_desc = [
XByteField("funcCode", 0x11),
BitFieldLenField("byteCount", None, 8, length_of="slaveUId"),
ConditionalField(XByteField("slaveUid", 0x00), lambda pkt: pkt.byteCount>0),
ConditionalField(XByteField("runIdicatorStatus", 0x00), lambda pkt: pkt.byteCount>0),
ConditionalField(FieldListField("slaveIdent", [0x00], XByteField("", 0x00), count_from = lambda pkt: pkt.byteCount), lambda pkt: pkt.byteCount>0)
]
class ModbusPDU11_Report_Slave_Id_Exception(Packet):
name = "Report Slave Id Exception"
@@ -268,7 +278,7 @@ class ModbusADU_Request(ModbusMBAP):
fields_desc = [
XShortField("transId", 0x0000), # needs to be unique
XShortField("protoId", 0x0000), # needs to be zero (Modbus)
XShortField("len", None), # is calculated with payload
XShortField("len", None), # is calculated with payload
XByteField("unitId", 0x00)] # 0xFF or 0x00 should be used for Modbus over TCP/IP
def mb_get_last_exception(self):
@@ -433,6 +443,7 @@ class ModbusADU_Response(ModbusMBAP):
return ModbusPDU10_Write_Multiple_Registers_Exception
elif funcCode == 0x11:
print(f'Packet answer: {payload}, func: {funcCode}')
return ModbusPDU11_Report_Slave_Id_Answer
elif funcCode == 0x91:
self._mb_exception = int(payload[1])

View File

@@ -16,7 +16,7 @@ from scapy.error import Scapy_Exception
from robot.api.deco import keyword, library
from robot.api.logger import info, debug, trace, console
from ModbusSupport import modbus_exceptions, ModbusADU_Request, ModbusADU_Response, ModbusPDU03_Read_Holding_Registers, ModbusPDU10_Write_Multiple_Registers, \
from ModbusSupport import modbus_exceptions, ModbusADU_Request, ModbusADU_Response, ModbusPDUXX_Custom_Request, ModbusPDU11_Report_Slave_Id, ModbusPDU03_Read_Holding_Registers, ModbusPDU10_Write_Multiple_Registers, \
ModbusPDU04_Read_Input_Registers, ModbusPDU01_Read_Coils, ModbusPDU0F_Write_Multiple_Coils, ModbusPDU02_Read_Discrete_Inputs, ModbusPDU06_Write_Single_Register
# Disable debugging of dissector, and set default padding for scapy configuration class
@@ -33,6 +33,7 @@ MB_DEF_FUNC_HOLDING_WRITE = 0x10
MB_DEF_FUNC_INPUT_READ = 0x04
MB_DEF_FUNC_COILS_READ = 0x01
MB_DEF_FUNC_COILS_WRITE = 0x0F
MB_DEF_FUNC_REPORT_SLAVE_ID = 0x11
MB_DEF_QUANTITY = 1
MB_DEF_START_OFFS = 0x0001
MB_DEF_REQ_TOUT = 5.0
@@ -40,6 +41,11 @@ MB_DEF_REQ_TOUT = 5.0
MB_LOGGING_PATH = '.'
# The constructed packets for self testing
TEST_PACKET_REPORT_SLAVE_ID_CUSTOM = 'ModbusADU_Request(transId=MB_DEF_TRANS_ID, unitId=0x01, protoId=0)/\
ModbusPDUXX_Custom_Request(customBytes=[0x11])'
TEST_PACKET_REPORT_SLAVE_ID = 'ModbusADU_Request(transId=MB_DEF_TRANS_ID, unitId=0x01, protoId=0)/\
ModbusPDU11_Report_Slave_Id(funcCode=MB_DEF_FUNC_REPORT_SLAVE_ID)'
TEST_PACKET_HOLDING_READ = 'ModbusADU_Request(transId=MB_DEF_TRANS_ID, unitId=0x01, protoId=0, len=6)/\
ModbusPDU03_Read_Holding_Registers(funcCode=MB_DEF_FUNC_HOLDING_READ, startAddr=MB_DEF_START_OFFS, quantity=MB_DEF_QUANTITY)'
TEST_PACKET_HOLDING_WRITE = 'ModbusADU_Request(transId=MB_DEF_TRANS_ID, unitId=0x01, protoId=0)/\
@@ -130,6 +136,7 @@ class ModbusTestLib:
if self._connection is None:
raise ValueError("The connection is not active.")
packet = pkt.build()
print(f'send packet: {packet}')
try:
self._connection.send(packet)
ans = self._connection.sniff(filter=f"tcp and dst host {self.node_address} and dst port {self.node_port}",
@@ -424,6 +431,16 @@ class ModbusTestLib:
def self_test(self) -> None:
# type: () -> None
self.connect(ip_addr=MB_DEF_SERVER_IP, port=MB_DEF_PORT)
packet = self.create_request(TEST_PACKET_REPORT_SLAVE_ID_CUSTOM)
print(f"Test: 0x11 <Report Slave ID> packet: {packet}")
response = self.send_packet_and_get_response(packet, timeout=1, verbose=0)
assert response and len(response) > 1, "No response from slave"
print(f"Test: received: {bytes(response)}")
pdu = self.translate_response(response)
if pdu is not None:
print(f"Slave identificator structure: {pdu}")
print(f"PDU Exception: {self.check_response(pdu, packet.customBytes[0])}")
print(f'slaveUID: {pdu.slaveUid}, runIdicatorStatus: {pdu.runIdicatorStatus}, IdStruct: {pdu.slaveIdent}')
packet = self.create_request(TEST_PACKET_HOLDING_READ)
print(f"Test: Packet created: {packet}")
response = self.send_packet_and_get_response(packet, timeout=1, verbose=0)

View File

@@ -4,6 +4,7 @@ ${MODBUS_DEF_PORT} 1502
${PAR1} 1
${PAR2} 2
${FUNC_REPORT_SLAVE_ID} ${0x11}
${FUNC_WRITE_HOLDING_REGISTERS} ${0x10}
${FUNC_WRITE_HOLDING_REGISTER} ${0x06}
${FUNC_READ_HOLDING_REGISTERS} ${0x03}
@@ -17,6 +18,12 @@ Library Collections
Library ModbusTestLib.py WITH NAME ModbusTestLib
*** Keywords ***
Create Report Slave Id Request
[Arguments] ${uid} ${customData}
#${packet} = Create Request ModbusADU_Request(unitId=${uid}, protoId=0)/ModbusPDU11_Report_Slave_Id(funcCode=${FUNC_REPORT_SLAVE_ID})
${packet} = Create Request ModbusADU_Request(unitId=${uid}, protoId=0)/ModbusPDUXX_Custom_Request(customBytes=${customData})
RETURN ${packet}
Create Input Read Registers Request
[Arguments] ${uid} ${startAddr} ${quantity}
${packet} = Create Request ModbusADU_Request(unitId=${uid}, protoId=0, len=6)/ModbusPDU04_Read_Input_Registers(funcCode=${FUNC_READ_INPUT_REGISTERS}, startAddr=${startAddr}, quantity=${quantity})
@@ -57,11 +64,33 @@ Create Discrete Read Request
Log Packet: ${packet}
RETURN ${packet}
Report Slave Id
[Arguments] ${uid} ${customData} ${exception_expected}
${classId} = Get Class Id
Log Library ClassId: ${classId}
Log Get Slave Identificator UID:${uid}, Custom bytes: ${customData}
${req} = Create Report Slave Id Request ${uid} ${customData}
#Create Connection ${server} ${port}
${response_frame} = Send Packet And Get Response ${req}
Should Not Be Empty ${response_frame}
${packet} = Translate Response ${response_frame}
Should Be Equal As Integers ${req.transId} ${packet.transId}
#${exception} ${exp_message} = Check Response ${packet} ${req.funcCode}
${exception} ${exp_message} = Check Response ${packet} ${req.customBytes[0]}
Should Be Equal As Integers ${exception} ${exception_expected}
Log exception: (${exception}: ${exp_message}), expected: ${exception_expected}
IF ${exception} == ${0}
Log Bytes count is: ${packet.byteCount}
Log SlaveUID:${packet.slaveUid}, runIdicatorStatus:${packet.runIdicatorStatus}, Identificator:${packet.slaveIdent}
ELSE
Log "Exception is evaluated correctly (${exception}: ${exp_message}) == ${exception_expected}"
END
Read Input Registers
[Arguments] ${uid} ${start_addr} ${quantity} ${exception_expected}
${classId} = Get Class Id
Log Library ClassId: ${classId}
Log Read Input Registers with parameters UID:${uid}, offs:${start_addr}, quantity:${quantity}
Log Read Input Registers with parameters UID:${uid}, offs:${start_addr}, quantity:${quantity}
${req} = Create Input Read Registers Request ${uid} ${start_addr} ${quantity}
#Create Connection ${server} ${port}
${response_frame} = Send Packet And Get Response ${req}

View File

@@ -11,6 +11,13 @@ Suite Teardown Disconnect
${suiteConnection} None
*** Test Cases ***
Test Report Slave Id
[Documentation] Test reading slave UID, running status, identificator structure (use custom frame template)
[Template] Report Slave Id
0x01 [0x11] 0 # Try o send correct request to get Slave ID
0x01 [0x11, 0x02, 0x01, 0xff] 3 # Try to mimic incorrect request for Report Slave ID
0x01 [0x11, 0x00] 3
Test Read Holding Registers With Different Addresses And Quantities
[Documentation] Test reading holding registers from different addresses with different quantities
[Template] Read Holding Registers