diff --git a/modbus/mb_objects/functions/mbfunccoils_master.c b/modbus/mb_objects/functions/mbfunccoils_master.c index ced88e2a..b975eb7e 100644 --- a/modbus/mb_objects/functions/mbfunccoils_master.c +++ b/modbus/mb_objects/functions/mbfunccoils_master.c @@ -3,7 +3,7 @@ * * SPDX-License-Identifier: BSD-3-Clause * - * SPDX-FileContributor: 2020-2025 Espressif Systems (Shanghai) CO LTD + * SPDX-FileContributor: 2020-2026 Espressif Systems (Shanghai) CO LTD */ /* * FreeModbus Library: A portable Modbus implementation for Modbus ASCII/RTU. @@ -85,12 +85,20 @@ mb_exception_t mb_error_to_exception(mb_err_enum_t error_code); mb_err_enum_t mbm_rq_read_coils(mb_base_t *inst, uint8_t snd_addr, uint16_t coil_addr, uint16_t coil_num, uint32_t tout) { uint8_t *mb_frame_ptr; + if (snd_addr > MB_ADDRESS_MAX) { return MB_EINVAL; } + + /* The broadcast coils read request is not allowed. */ + if (!snd_addr) { + return MB_ENOREG; + } + if (!mb_port_event_res_take(inst->port_obj, tout)) { return MB_EBUSY; } + inst->get_send_buf(inst, &mb_frame_ptr); inst->set_dest_addr(inst, snd_addr); @@ -118,9 +126,12 @@ mb_exception_t mbm_fn_read_coils(mb_base_t *inst, uint8_t *frame_ptr, uint16_t * mb_exception_t status = MB_EX_NONE; mb_err_enum_t reg_status = MB_EILLFUNC; - /* If this request is broadcast, and it's read mode. This request don't need execute. */ + if (!len_buf || !frame_ptr || !inst) { + return MB_EINVAL; + } + if (inst->transp_obj->frm_is_bcast(inst->transp_obj)) { - status = MB_EX_NONE; + status = MB_EX_ILLEGAL_DATA_ADDRESS; } else if (*len_buf >= MB_PDU_SIZE_MIN + MB_PDU_FUNC_READ_SIZE_MIN) { inst->get_send_buf(inst, &mb_frame_ptr); @@ -215,7 +226,12 @@ mb_exception_t mbm_fn_write_coil(mb_base_t *inst, uint8_t *frame_ptr, uint16_t * (void)inst; (void)frame_ptr; - if (*len_buf == (MB_PDU_FUNC_WRITE_SIZE + MB_PDU_SIZE_MIN)) { + if (!len_buf || !frame_ptr || !inst) { + return MB_EINVAL; + } + + if (*len_buf == (MB_PDU_FUNC_WRITE_SIZE + MB_PDU_SIZE_MIN) + || inst->transp_obj->frm_is_bcast(inst->transp_obj)) { status = MB_EX_NONE; } else { /* Can't be a valid write coil register request because the length @@ -298,8 +314,13 @@ mb_exception_t mbm_fn_write_multi_coils(mb_base_t *inst, uint8_t *frame_ptr, uin uint8_t byte_cnt_verify; mb_err_enum_t reg_status = MB_EILLFUNC; - /* If this request is broadcast, the *len_buf is not need check. */ - if ((*len_buf == MB_PDU_FUNC_WRITE_MUL_SIZE) || inst->transp_obj->frm_is_bcast(inst->transp_obj)) { + if (!len_buf || !frame_ptr || !inst) { + return MB_EINVAL; + } + + /* If this request is broadcast, do not need to call r/w callback. */ + if ((*len_buf == MB_PDU_FUNC_WRITE_MUL_SIZE) + || inst->transp_obj->frm_is_bcast(inst->transp_obj)) { inst->get_send_buf(inst, &mb_frame_ptr); reg_address = (uint16_t)(frame_ptr[MB_PDU_FUNC_WRITE_MUL_ADDR_OFF] << 8); reg_address |= (uint16_t)(frame_ptr[MB_PDU_FUNC_WRITE_MUL_ADDR_OFF + 1]); diff --git a/modbus/mb_objects/functions/mbfuncdisc_master.c b/modbus/mb_objects/functions/mbfuncdisc_master.c index 297363ce..9e8d9ba3 100644 --- a/modbus/mb_objects/functions/mbfuncdisc_master.c +++ b/modbus/mb_objects/functions/mbfuncdisc_master.c @@ -3,7 +3,7 @@ * * SPDX-License-Identifier: BSD-3-Clause * - * SPDX-FileContributor: 2020-2025 Espressif Systems (Shanghai) CO LTD + * SPDX-FileContributor: 2020-2026 Espressif Systems (Shanghai) CO LTD */ /* * FreeModbus Library: A portable Modbus implementation for Modbus ASCII/RTU. @@ -70,6 +70,12 @@ mb_err_enum_t mbm_rq_read_discrete_inputs(mb_base_t *inst, uint8_t snd_addr, uin if (!inst || (snd_addr > MB_ADDRESS_MAX)) { return MB_EINVAL; } + + /* The broadcast discrete read request is nt supported. */ + if (!snd_addr) { + return MB_ENOREG; + } + if (!mb_port_event_res_take(inst->port_obj, tout)) { return MB_EBUSY; } @@ -99,8 +105,13 @@ mb_exception_t mbm_fn_read_discrete_inputs(mb_base_t *inst, uint8_t *frame_ptr, mb_exception_t status = MB_EX_NONE; mb_err_enum_t reg_status = MB_EILLFUNC; + + if (!len_buf || !frame_ptr || !inst) { + return MB_EINVAL; + } + if (inst->transp_obj->frm_is_bcast(inst->transp_obj)) { - status = MB_EX_NONE; + status = MB_EX_ILLEGAL_DATA_ADDRESS; } else if (*len_buf >= MB_PDU_SIZE_MIN + MB_PDU_FUNC_READ_SIZE_MIN) { inst->get_send_buf(inst, &mb_frame_ptr); reg_address = (uint16_t)(mb_frame_ptr[MB_PDU_REQ_READ_ADDR_OFF] << 8); diff --git a/modbus/mb_objects/functions/mbfuncholding_master.c b/modbus/mb_objects/functions/mbfuncholding_master.c index 998648bc..f3eecb96 100644 --- a/modbus/mb_objects/functions/mbfuncholding_master.c +++ b/modbus/mb_objects/functions/mbfuncholding_master.c @@ -3,7 +3,7 @@ * * SPDX-License-Identifier: BSD-3-Clause * - * SPDX-FileContributor: 2020-2025 Espressif Systems (Shanghai) CO LTD + * SPDX-FileContributor: 2020-2026 Espressif Systems (Shanghai) CO LTD */ /* * FreeModbus Library: A portable Modbus implementation for Modbus ASCII/RTU. @@ -86,7 +86,7 @@ mb_exception_t mb_error_to_exception(mb_err_enum_t error_code); /** * This function will request write holding register. * - * @param snd_addr salve address + * @param snd_addr slave address * @param reg_addr register start address * @param reg_data register data to be written * @param timeout timeout (-1 will waiting forever) @@ -113,7 +113,7 @@ mb_err_enum_t mbm_rq_write_holding_reg(mb_base_t *inst, uint8_t snd_addr, uint16 mb_frame_ptr[MB_PDU_REQ_WRITE_VALUE_OFF] = reg_data >> 8; mb_frame_ptr[MB_PDU_REQ_WRITE_VALUE_OFF + 1] = reg_data; - inst->set_send_len(inst, MB_PDU_SIZE_MIN + MB_PDU_REQ_READ_SIZE); + inst->set_send_len(inst, MB_PDU_SIZE_MIN + MB_PDU_REQ_WRITE_SIZE); (void)mb_port_event_post(inst->port_obj, EVENT(EV_FRAME_TRANSMIT | EV_TRANS_START)); return mb_port_event_wait_req_finish(inst->port_obj); @@ -124,7 +124,14 @@ mb_exception_t mbm_fn_write_holding_reg(mb_base_t *inst, uint8_t *frame_ptr, uin mb_exception_t status = MB_EX_NONE; mb_err_enum_t reg_status = MB_EILLFUNC; - if (*len_buf == (MB_PDU_SIZE_MIN + MB_PDU_FUNC_WRITE_SIZE)) { + if (!len_buf || !frame_ptr || !inst) { + return MB_EINVAL; + } + + /* For broadcast request do not check the buffer size. */ + if (*len_buf == (MB_PDU_SIZE_MIN + MB_PDU_FUNC_WRITE_SIZE) + || inst->transp_obj->frm_is_bcast(inst->transp_obj)) { + ESP_LOGD(__func__, "Length: %u", *len_buf); uint16_t reg_address; reg_address = (uint16_t)(frame_ptr[MB_PDU_FUNC_WRITE_ADDR_OFF] << 8); reg_address |= (uint16_t)(frame_ptr[MB_PDU_FUNC_WRITE_ADDR_OFF + 1]); @@ -152,7 +159,7 @@ mb_exception_t mbm_fn_write_holding_reg(mb_base_t *inst, uint8_t *frame_ptr, uin /** * This function will request write multiple holding register. * - * @param snd_addr salve address + * @param snd_addr slave address * @param reg_addr register start address * @param reg_num register total number * @param data_ptr data to be written @@ -207,8 +214,13 @@ mb_exception_t mbm_fn_write_multi_holding_reg(mb_base_t *inst, uint8_t *frame_pt uint16_t byte_count; mb_err_enum_t reg_status = MB_EILLFUNC; - /* If this request is broadcast, the *len_buf is not need check. */ - if ((*len_buf == MB_PDU_SIZE_MIN + MB_PDU_FUNC_WRITE_MUL_SIZE) || inst->transp_obj->frm_is_bcast(inst->transp_obj)) { + if (!len_buf || !frame_ptr || !inst) { + return MB_EINVAL; + } + + if ((*len_buf == MB_PDU_SIZE_MIN + MB_PDU_FUNC_WRITE_MUL_SIZE) + || inst->transp_obj->frm_is_bcast(inst->transp_obj)) { + ESP_LOGD(__func__, "Length: %u", *len_buf); inst->get_send_buf(inst, &mb_frame_ptr); reg_address = (uint16_t)(mb_frame_ptr[MB_PDU_REQ_WRITE_MUL_ADDR_OFF] << 8); reg_address |= (uint16_t)(mb_frame_ptr[MB_PDU_REQ_WRITE_MUL_ADDR_OFF + 1]); @@ -245,7 +257,7 @@ mb_exception_t mbm_fn_write_multi_holding_reg(mb_base_t *inst, uint8_t *frame_pt /** * This function will request read holding register. * - * @param snd_addr salve address + * @param snd_addr slave address * @param reg_addr register start address * @param reg_num register total number * @param timeout timeout (-1 will waiting forever) @@ -259,6 +271,12 @@ mb_err_enum_t mbm_rq_read_holding_reg(mb_base_t *inst, uint8_t snd_addr, uint16_ if (!inst || (snd_addr > MB_ADDRESS_MAX)) { return MB_EINVAL; } + + /* The broadcast read holding registers request is not supported. */ + if (!snd_addr) { + return MB_ENOREG; + } + if (!mb_port_event_res_take(inst->port_obj, tout)) { return MB_EBUSY; } @@ -287,14 +305,11 @@ mb_exception_t mbm_fn_read_holding_reg(mb_base_t *inst, uint8_t *frame_ptr, uint uint16_t reg_count; mb_err_enum_t reg_status = MB_EILLFUNC; - /* If this request is broadcast, and it's read mode. This request don't need execute. */ - bool is_broadcast = false; - - is_broadcast = inst->transp_obj->frm_is_bcast(inst->transp_obj); - - if (is_broadcast == true) { - status = MB_EX_NONE; - } else if (*len_buf >= MB_PDU_SIZE_MIN + MB_PDU_FUNC_READ_SIZE_MIN) { + ESP_LOGD(__func__, "Buffer length: %u, bcast: %u", *len_buf, (unsigned)inst->transp_obj->frm_is_bcast(inst->transp_obj)); + ESP_LOG_BUFFER_HEX_LEVEL(__func__, (void *)frame_ptr, *len_buf, ESP_LOG_DEBUG); + if (inst->transp_obj->frm_is_bcast(inst->transp_obj)) { + status = MB_EX_ILLEGAL_DATA_ADDRESS; + } else if ((*len_buf >= MB_PDU_SIZE_MIN + MB_PDU_FUNC_READ_SIZE_MIN)) { inst->get_send_buf(inst, &mb_frame_ptr); reg_address = (uint16_t)(mb_frame_ptr[MB_PDU_REQ_READ_ADDR_OFF] << 8); reg_address |= (uint16_t)(mb_frame_ptr[MB_PDU_REQ_READ_ADDR_OFF + 1]); @@ -332,7 +347,7 @@ mb_exception_t mbm_fn_read_holding_reg(mb_base_t *inst, uint8_t *frame_ptr, uint /** * This function will request read and write holding register. * - * @param snd_addr salve address + * @param snd_addr slave address * @param rd_reg_addr read register start address * @param rd_reg_num read register total number * @param data_ptr data to be written @@ -393,10 +408,14 @@ mb_exception_t mbm_fn_rw_multi_holding_regs(mb_base_t *inst, uint8_t *frame_ptr, uint16_t reg_rd_address, reg_wr_address; uint8_t *mb_frame_ptr; - /* If this request is broadcast, and it's read mode. This request don't need execute. */ - if (inst->transp_obj->frm_is_bcast(inst->transp_obj)) { - status = MB_EX_NONE; - } else if (*len_buf >= MB_PDU_SIZE_MIN + MB_PDU_FUNC_READWRITE_SIZE_MIN) { + if (!len_buf || !frame_ptr || !inst) { + return MB_EINVAL; + } + + /* If this request is broadcast, do not check the length */ + if ((*len_buf >= MB_PDU_SIZE_MIN + MB_PDU_FUNC_READWRITE_SIZE_MIN) + || inst->transp_obj->frm_is_bcast(inst->transp_obj)) { + ESP_LOGD(__func__, "Length: %u", *len_buf); inst->get_send_buf(inst, &mb_frame_ptr); reg_rd_address = (uint16_t)(mb_frame_ptr[MB_PDU_REQ_READWRITE_READ_ADDR_OFF] << 8); reg_rd_address |= (uint16_t)(mb_frame_ptr[MB_PDU_REQ_READWRITE_READ_ADDR_OFF + 1]); diff --git a/modbus/mb_objects/functions/mbfuncinput_master.c b/modbus/mb_objects/functions/mbfuncinput_master.c index 15317a68..2243a62f 100644 --- a/modbus/mb_objects/functions/mbfuncinput_master.c +++ b/modbus/mb_objects/functions/mbfuncinput_master.c @@ -3,7 +3,7 @@ * * SPDX-License-Identifier: BSD-3-Clause * - * SPDX-FileContributor: 2020-2025 Espressif Systems (Shanghai) CO LTD + * SPDX-FileContributor: 2020-2026 Espressif Systems (Shanghai) CO LTD */ /* * FreeModbus Library: A portable Modbus implementation for Modbus ASCII/RTU. @@ -73,9 +73,16 @@ mb_err_enum_t mbm_rq_read_inp_reg(mb_base_t *inst, uint8_t snd_addr, uint16_t re if (!inst || (snd_addr > MB_ADDRESS_MAX)) { return MB_EINVAL; } + + /* The broadcast read input request is not supported. */ + if (!snd_addr) { + return MB_ENOREG; + } + if (!mb_port_event_res_take(inst->port_obj, tout)) { return MB_EBUSY; } + inst->get_send_buf(inst, &mb_frame_ptr); inst->set_dest_addr(inst, snd_addr); @@ -101,9 +108,12 @@ mb_exception_t mbm_fn_read_inp_reg(mb_base_t *inst, uint8_t *frame_ptr, uint16_t uint16_t reg_address; uint8_t *mb_frame_ptr; - /* If this request is broadcast, and it's read mode. This request don't need execute. */ + if (!len_buf || !frame_ptr || !inst) { + return MB_EINVAL; + } + if (inst->transp_obj->frm_is_bcast(inst->transp_obj)) { - status = MB_EX_NONE; + status = MB_EX_ILLEGAL_DATA_ADDRESS; } else if (*len_buf >= MB_PDU_SIZE_MIN + MB_PDU_FUNC_READ_SIZE_MIN) { inst->get_send_buf(inst, &mb_frame_ptr); diff --git a/modbus/mb_objects/mb_master.c b/modbus/mb_objects/mb_master.c index 208bfaac..c9e5bc95 100644 --- a/modbus/mb_objects/mb_master.c +++ b/modbus/mb_objects/mb_master.c @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2021-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -486,7 +486,11 @@ static void mbm_set_dest_addr(mb_base_t *inst, uint8_t dest_addr) static uint8_t mbm_get_dest_addr(mb_base_t *inst) { mbm_object_t *mbm_obj = MB_GET_OBJ_CTX(inst, mbm_object_t, base); - return mbm_obj->master_dst_addr; + uint8_t dest_addr = 0; + CRITICAL_SECTION(inst->lock) { + dest_addr = mbm_obj->master_dst_addr; + } + return dest_addr; } void mbm_error_cb_respond_timeout(mb_base_t *inst, uint8_t dest_addr, const uint8_t *pdu_data, uint16_t pdu_length) @@ -517,7 +521,6 @@ mb_err_enum_t mbm_poll(mb_base_t *inst) { mbm_object_t *mbm_obj = MB_GET_OBJ_CTX(inst, mbm_object_t, base);; - uint16_t length; mb_exception_t exception; mb_err_enum_t status = MB_ENOERR; mb_event_t event; @@ -599,25 +602,16 @@ mb_err_enum_t mbm_poll(mb_base_t *inst) if (MB_OBJ(inst->transp_obj)->frm_is_bcast(inst->transp_obj) && ((mbm_obj->cur_mode == MB_RTU) || (mbm_obj->cur_mode == MB_ASCII))) { mbm_obj->rcv_frame = mbm_obj->snd_frame; + mbm_obj->pdu_rcv_len = mbm_obj->pdu_snd_len; + mbm_obj->rcv_addr = mbm_obj->master_dst_addr; } MB_RETURN_ON_FALSE(mbm_obj->rcv_frame, MB_EILLSTATE, TAG, MB_OBJ_FMT", receive buffer initialization fail.", MB_OBJ_PARENT(inst)); ESP_LOGD(TAG, MB_OBJ_FMT":EV_EXECUTE", MB_OBJ_PARENT(inst)); mbm_obj->func_code = mbm_obj->rcv_frame[MB_PDU_FUNC_OFF]; exception = MB_EX_ILLEGAL_FUNCTION; - /* If master request is broadcast, - * the master needs to execute function for all slaves. - */ - if (MB_OBJ(inst->transp_obj)->frm_is_bcast(inst->transp_obj)) { - length = mbm_obj->pdu_snd_len; - for (int j = 1; j <= MB_MASTER_TOTAL_SLAVE_NUM; j++) { - mbm_set_dest_addr(inst, j); - exception = mbm_check_invoke_handler(inst, mbm_obj->func_code, mbm_obj->rcv_frame, &length); - } - } else { - ESP_LOGD(TAG, MB_OBJ_FMT": function (0x%x), invoke handler.", MB_OBJ_PARENT(inst), (int)mbm_obj->func_code); - exception = mbm_check_invoke_handler(inst, mbm_obj->func_code, mbm_obj->rcv_frame, &mbm_obj->pdu_rcv_len); - } + ESP_LOGD(TAG, MB_OBJ_FMT": function (0x%x), addr=%u, invoke handler.", MB_OBJ_PARENT(inst), (int)mbm_obj->func_code, (unsigned)mbm_obj->rcv_addr); + exception = mbm_check_invoke_handler(inst, mbm_obj->func_code, mbm_obj->rcv_frame, &mbm_obj->pdu_rcv_len); /* If master has exception, will send error process event. Otherwise the master is idle.*/ if (exception != MB_EX_NONE) { mb_port_event_set_err_type(MB_OBJ(inst->port_obj), EV_ERROR_EXECUTE_FUNCTION); diff --git a/test_apps/broadcast_test/CMakeLists.txt b/test_apps/broadcast_test/CMakeLists.txt new file mode 100644 index 00000000..266331d3 --- /dev/null +++ b/test_apps/broadcast_test/CMakeLists.txt @@ -0,0 +1,15 @@ +# This is the project CMakeLists.txt file for the test subproject +cmake_minimum_required(VERSION 3.22) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) + +set(EXTRA_COMPONENT_DIRS "../test_common") + +# The workaround for the test_utils under ESP-IDF v6.0 +if("${IDF_VERSION_MAJOR}.${IDF_VERSION_MINOR}" VERSION_GREATER "5.5") + list(APPEND EXTRA_COMPONENT_DIRS "$ENV{IDF_PATH}/tools/test_apps/components") +else() + list(APPEND EXTRA_COMPONENT_DIRS "$ENV{IDF_PATH}/tools/unit-test-app/components") +endif() + +project(test_broadcast) diff --git a/test_apps/broadcast_test/README.md b/test_apps/broadcast_test/README.md new file mode 100644 index 00000000..28ab9f5f --- /dev/null +++ b/test_apps/broadcast_test/README.md @@ -0,0 +1,4 @@ +| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C6 | ESP32-H2 | ESP32-S2 | ESP32-S3 | +| ----------------- | ----- | -------- | -------- | -------- | -------- | -------- | -------- | + +This test app is used to test modbus interface feature. diff --git a/test_apps/broadcast_test/main/CMakeLists.txt b/test_apps/broadcast_test/main/CMakeLists.txt new file mode 100644 index 00000000..d0dc01af --- /dev/null +++ b/test_apps/broadcast_test/main/CMakeLists.txt @@ -0,0 +1,18 @@ +set(PROJECT "test_bcast") + +set(srcs "test_app_main.c" + "${PROJECT}_serial.c" +) + +idf_component_register(SRCS ${srcs} + PRIV_REQUIRES test_utils test_common unity + ) + +# The workaround for WHOLE_ARCHIVE which is absent in v4.4 +set_property(TARGET ${COMPONENT_LIB} APPEND PROPERTY INTERFACE_LINK_LIBRARIES "-u mb_test_include_bcast_serial") + +# Workaround to avoid static analysis false positives for some components. +if(CONFIG_FMB_COMPILER_STATIC_ANALYZER_ENABLE AND CMAKE_C_COMPILER_ID STREQUAL "GNU") + target_compile_options(${COMPONENT_LIB} PRIVATE "-fanalyzer") + message(STATUS "Static analyzer build for ${PROJECT_NAME}.") +endif() diff --git a/test_apps/broadcast_test/main/Kconfig.projbuild b/test_apps/broadcast_test/main/Kconfig.projbuild new file mode 100644 index 00000000..b56bf41e --- /dev/null +++ b/test_apps/broadcast_test/main/Kconfig.projbuild @@ -0,0 +1,45 @@ +menu "Modbus Test Configuration" + + config MB_PORT_ADAPTER_EN + bool "Enable Modbus port adapter to substitute hardware layer for test." + default n + help + When option is enabled the port communication layer is substituted by + port adapter layer to allow testing of higher layers without access to physical layer. + + config MB_TEST_SLAVE_TASK_PRIO + int "Modbus master test task priority" + range 4 23 + default 4 + help + Modbus master task priority for the test. + + config MB_TEST_MASTER_TASK_PRIO + int "Modbus slave test task priority" + range 4 23 + default 4 + help + Modbus slave task priority for the test. + + config MB_TEST_COMM_CYCLE_COUNTER + int "Modbus test communication cycle counter" + range 10 1000 + default 10 + help + Modbus communication cycle counter for test. + + config MB_TEST_LEAK_WARN_LEVEL + int "Modbus test leak warning level" + range 4 256 + default 32 + help + Modbus test leak warning level. + + config MB_TEST_LEAK_CRITICAL_LEVEL + int "Modbus test leak critical level" + range 4 1024 + default 64 + help + Modbus test leak critical level. + +endmenu diff --git a/test_apps/broadcast_test/main/idf_component.yml b/test_apps/broadcast_test/main/idf_component.yml new file mode 100644 index 00000000..8dee941e --- /dev/null +++ b/test_apps/broadcast_test/main/idf_component.yml @@ -0,0 +1,5 @@ +dependencies: + idf: ">=5.0" + espressif/esp-modbus: + version: "^2" + override_path: "../../../" diff --git a/test_apps/broadcast_test/main/test_app_main.c b/test_apps/broadcast_test/main/test_app_main.c new file mode 100644 index 00000000..ccf63d5e --- /dev/null +++ b/test_apps/broadcast_test/main/test_app_main.c @@ -0,0 +1,22 @@ +/* + * SPDX-FileCopyrightText: 2023-2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: CC0-1.0 + */ + +#include "unity.h" +#include "test_common.h" + +void app_main(void) +{ +#if !CONFIG_LOG_DEFAULT_LEVEL_DEBUG + esp_log_level_set("mbc_tcp.slave", ESP_LOG_DEBUG); + esp_log_level_set("mbc_serial.slave", ESP_LOG_DEBUG); + esp_log_level_set("mb_object.slave", ESP_LOG_DEBUG); +#else + // Disable VFS logs as they are too verbose + esp_log_level_set("vfs_calls", ESP_LOG_NONE); +#endif + printf("Modbus RS485 multi-device test cases/n"); + unity_run_menu(); +} diff --git a/test_apps/broadcast_test/main/test_bcast_serial.c b/test_apps/broadcast_test/main/test_bcast_serial.c new file mode 100644 index 00000000..1b6efef7 --- /dev/null +++ b/test_apps/broadcast_test/main/test_bcast_serial.c @@ -0,0 +1,210 @@ +/* + * SPDX-FileCopyrightText: 2023-2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +#include "unity.h" + +#include "sdkconfig.h" +#include "test_common.h" +#include "test_utils.h" + +#if __has_include("unity_test_utils.h") +// unity test utils are used +#include "unity_test_utils.h" +#else +// Unit_test_app utils from test_utils ("test_utils.h"), v4.4 +#define unity_utils_task_delete test_utils_task_delete +#endif + +#define TEST_SER_PORT_NUM (1) +#define TEST_TASK_TIMEOUT_MS (160000) +#define TEST_SEND_TOUT_US (30000) +#define TEST_RESP_TOUT_MS (1000) +#define TEST_BAUD_RATE (115200) + +#if CONFIG_IDF_TARGET_ESP32 +#define TEST_SER_PIN_RX (22) +#define TEST_SER_PIN_TX (23) +// RTS for RS485 Half-Duplex Mode manages DE/~RE +#define TEST_SER_PIN_RTS (18) +#else +#define TEST_SER_PIN_RX (4) +#define TEST_SER_PIN_TX (5) +#define TEST_SER_PIN_RTS (10) +#endif + +#define TEST_MASTER_RESPOND_TOUT_MS (CONFIG_FMB_MASTER_TIMEOUT_MS_RESPOND) + +// The workaround to statically link the whole test library +__attribute__((unused)) bool mb_test_include_bcast_serial = true; + +#define TAG "MODBUS_SERIAL_COMM_TEST" + +#if (CONFIG_FMB_COMM_MODE_RTU_EN || CONFIG_FMB_COMM_MODE_ASCII_EN) + +#define MB_DEVICE_ADDR0 0 + +// Example Data (Object) Dictionary for Modbus parameters +static const mb_parameter_descriptor_t descriptors[] = { + { + CID_DEV_REG0, STR("MB_hold_reg-0"), STR("Data"), MB_DEVICE_ADDR0, MB_PARAM_HOLDING, 0, 1, + 0, PARAM_TYPE_U16, 2, OPTS(0, 0, 0), PAR_PERMS_READ_WRITE_TRIGGER + }, + { + CID_DEV_REG1, STR("MB_hold_reg-1"), STR("Data"), MB_DEVICE_ADDR0, MB_PARAM_HOLDING, 1, 1, + 0, PARAM_TYPE_U16, 2, OPTS(0, 0, 0), PAR_PERMS_READ_WRITE_TRIGGER + }, + { + CID_DEV_REG2, STR("MB_hold_reg-2"), STR("Data"), MB_DEVICE_ADDR0, MB_PARAM_HOLDING, 2, 1, + 0, PARAM_TYPE_U16, 2, OPTS(0, 0, 0), PAR_PERMS_READ_WRITE_TRIGGER + }, + { + CID_DEV_REG3, STR("MB_hold_reg-3"), STR("Data"), MB_DEVICE_ADDR0, MB_PARAM_HOLDING, 3, 1, + 0, PARAM_TYPE_U16, 2, OPTS(0, 0, 0), PAR_PERMS_READ_WRITE_TRIGGER + }, + { + CID_DEV_REG_COUNT, STR("CYCLE_COUNTER"), STR("Data"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, 4, 1, + 0, PARAM_TYPE_U16, 2, OPTS(0, 0, 0), PAR_PERMS_READ_WRITE_TRIGGER + } +}; + +// The number of parameters in the table +const uint16_t num_descriptors = (sizeof(descriptors) / sizeof(descriptors[0])); + +static void test_modbus_rs485_rtu_slave(void) +{ + mb_communication_info_t slave_config1 = { + .ser_opts.port = TEST_SER_PORT_NUM, + .ser_opts.mode = MB_RTU, + .ser_opts.uid = MB_DEVICE_ADDR1, + .ser_opts.data_bits = UART_DATA_8_BITS, + .ser_opts.stop_bits = UART_STOP_BITS_1, + .ser_opts.baudrate = TEST_BAUD_RATE, + .ser_opts.parity = UART_PARITY_DISABLE, + .ser_opts.response_tout_ms = TEST_MASTER_RESPOND_TOUT_MS, + .ser_opts.test_tout_us = TEST_SEND_TOUT_US + }; + + TaskHandle_t slave_task_handle = test_common_slave_serial_create(&slave_config1, 0); + + // Set driver mode to Half Duplex + TEST_ESP_OK(uart_set_mode(slave_config1.ser_opts.port, UART_MODE_RS485_HALF_DUPLEX)); + TEST_ESP_OK(uart_set_pin(slave_config1.ser_opts.port, TEST_SER_PIN_TX, + TEST_SER_PIN_RX, TEST_SER_PIN_RTS, UART_PIN_NO_CHANGE)); + + ESP_LOGI(TAG, "Slave RTU is started. (%s).", __func__); + + unity_send_signal("Slave_ready"); + unity_wait_for_signal("Master_started"); + + test_common_task_start(slave_task_handle, 1); + TEST_ASSERT_TRUE(test_common_task_wait_done(slave_task_handle, pdMS_TO_TICKS(TEST_TASK_TIMEOUT_MS))); +} + +static void test_modbus_rs485_rtu_master(void) +{ + ESP_LOGI(TAG, "Master RTU is started (%s).", __func__); + unity_wait_for_signal("Slave_ready"); + unity_send_signal("Master_started"); + + // Initialize and start Modbus controller + mb_communication_info_t master_config = { + .ser_opts.port = TEST_SER_PORT_NUM, + .ser_opts.mode = MB_RTU, + .ser_opts.data_bits = UART_DATA_8_BITS, + .ser_opts.stop_bits = UART_STOP_BITS_1, + .ser_opts.baudrate = TEST_BAUD_RATE, + .ser_opts.parity = UART_PARITY_DISABLE, + .ser_opts.response_tout_ms = TEST_MASTER_RESPOND_TOUT_MS, + .ser_opts.test_tout_us = TEST_SEND_TOUT_US + }; + + TaskHandle_t master_task_handle = test_common_master_serial_create(&master_config, 0, &descriptors[0], num_descriptors); + + // Set driver mode to Half Duplex + TEST_ESP_OK(uart_set_mode(master_config.ser_opts.port, UART_MODE_RS485_HALF_DUPLEX)); + TEST_ESP_OK(uart_set_pin(master_config.ser_opts.port, TEST_SER_PIN_TX, + TEST_SER_PIN_RX, TEST_SER_PIN_RTS, UART_PIN_NO_CHANGE)); + void *handle = test_common_task_get_instance(master_task_handle); + uint16_t reg = 0; + uint8_t type = 0; + esp_err_t err = mbc_master_get_parameter(handle, CID_DEV_REG0, (uint8_t *)®, &type); + TEST_ESP_ERR(ESP_ERR_NOT_SUPPORTED, err); // Broadcast read request is not supported + test_common_task_start(master_task_handle, 1); + + TEST_ASSERT_TRUE(test_common_task_wait_done(master_task_handle, pdMS_TO_TICKS(TEST_TASK_TIMEOUT_MS))); +} + +/* + * Modbus RS485 RTU multi device test case + */ +TEST_CASE_MULTIPLE_DEVICES("Modbus RS485 RTU multi device broadcast case.", "[modbus][test_env=multi_dut_modbus_serial]", test_modbus_rs485_rtu_slave, test_modbus_rs485_rtu_master); + +static void test_modbus_rs485_ascii_slave(void) +{ + mb_communication_info_t slave_config1 = { + .ser_opts.port = TEST_SER_PORT_NUM, + .ser_opts.mode = MB_ASCII, + .ser_opts.uid = MB_DEVICE_ADDR1, + .ser_opts.data_bits = UART_DATA_8_BITS, + .ser_opts.stop_bits = UART_STOP_BITS_1, + .ser_opts.baudrate = TEST_BAUD_RATE, + .ser_opts.parity = UART_PARITY_DISABLE, + .ser_opts.response_tout_ms = TEST_MASTER_RESPOND_TOUT_MS, + .ser_opts.test_tout_us = TEST_SEND_TOUT_US + }; + + TaskHandle_t slave_task_handle = test_common_slave_serial_create(&slave_config1, 0); + + TEST_ESP_OK(uart_set_pin(slave_config1.ser_opts.port, TEST_SER_PIN_TX, + TEST_SER_PIN_RX, TEST_SER_PIN_RTS, UART_PIN_NO_CHANGE)); + + // Set driver mode to Half Duplex + TEST_ESP_OK(uart_set_mode(slave_config1.ser_opts.port, UART_MODE_RS485_HALF_DUPLEX)); + + ESP_LOGI(TAG, "Slave ASCII is started. (%s).", __func__); + + unity_send_signal("Slave_ready"); + unity_wait_for_signal("Master_started"); + + test_common_task_start(slave_task_handle, 1); + TEST_ASSERT_TRUE(test_common_task_wait_done(slave_task_handle, pdMS_TO_TICKS(TEST_TASK_TIMEOUT_MS))); +}; + +static void test_modbus_rs485_ascii_master(void) +{ + ESP_LOGI(TAG, "Master ASCII is started (%s).", __func__); + unity_wait_for_signal("Slave_ready"); + + // Initialize and start Modbus controller + mb_communication_info_t master_config = { + .ser_opts.port = TEST_SER_PORT_NUM, + .ser_opts.mode = MB_ASCII, + .ser_opts.data_bits = UART_DATA_8_BITS, + .ser_opts.stop_bits = UART_STOP_BITS_1, + .ser_opts.baudrate = TEST_BAUD_RATE, + .ser_opts.parity = UART_PARITY_DISABLE, + .ser_opts.response_tout_ms = TEST_MASTER_RESPOND_TOUT_MS, + .ser_opts.test_tout_us = TEST_SEND_TOUT_US + }; + + TaskHandle_t master_task_handle = test_common_master_serial_create(&master_config, 0, &descriptors[0], num_descriptors); + + // Set driver mode to Half Duplex + TEST_ESP_OK(uart_set_mode(master_config.ser_opts.port, UART_MODE_RS485_HALF_DUPLEX)); + TEST_ESP_OK(uart_set_pin(master_config.ser_opts.port, TEST_SER_PIN_TX, + TEST_SER_PIN_RX, TEST_SER_PIN_RTS, UART_PIN_NO_CHANGE)); + unity_send_signal("Master_started"); + + test_common_task_start(master_task_handle, 1); + TEST_ASSERT_TRUE(test_common_task_wait_done(master_task_handle, pdMS_TO_TICKS(TEST_TASK_TIMEOUT_MS))); +} + +/* + * Modbus RS485 ASCII multi device test case + */ +TEST_CASE_MULTIPLE_DEVICES("Modbus RS485 ASCII multi device broadcast case.", "[modbus][test_env=multi_dut_modbus_serial]", test_modbus_rs485_ascii_slave, test_modbus_rs485_ascii_master); + + +#endif diff --git a/test_apps/broadcast_test/pytest_bcast_comm.py b/test_apps/broadcast_test/pytest_bcast_comm.py new file mode 100644 index 00000000..cf062073 --- /dev/null +++ b/test_apps/broadcast_test/pytest_bcast_comm.py @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: 2022-2026 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: CC0-1.0 + +import pytest + + +@pytest.mark.parametrize("target", ["esp32"], indirect=True) +@pytest.mark.parametrize("count, config", [(2, "serial")], indirect=True) +@pytest.mark.multi_dut_modbus_serial +def test_modbus_comm_bcast_serial(case_tester) -> None: # type: ignore + for case in case_tester.test_menu: + if ( + case.attributes.get("test_env", "multi_dut_modbus_serial") + == "multi_dut_modbus_serial" + ): + print(f"Test case: {case.name}") + case_tester.run_multi_dev_case(case=case, reset=True) diff --git a/test_apps/broadcast_test/sdkconfig.ci.defaults b/test_apps/broadcast_test/sdkconfig.ci.defaults new file mode 100644 index 00000000..383ed734 --- /dev/null +++ b/test_apps/broadcast_test/sdkconfig.ci.defaults @@ -0,0 +1,25 @@ +# +# Default configuration options +# +CONFIG_FMB_TIMER_USE_ISR_DISPATCH_METHOD=y +CONFIG_FMB_MASTER_DELAY_MS_CONVERT=300 +CONFIG_FMB_MASTER_TIMEOUT_MS_RESPOND=2000 +CONFIG_FMB_PORT_TASK_STACK_SIZE=4096 +CONFIG_FMB_PORT_TASK_PRIO=10 + +# Avoid CI issues "Warning: The smallest app partition is nearly full (5% free space left)!" +CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y +CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y + +CONFIG_MB_PORT_ADAPTER_EN=n +CONFIG_MB_TEST_SLAVE_TASK_PRIO=5 +CONFIG_MB_TEST_MASTER_TASK_PRIO=5 +CONFIG_MB_TEST_COMM_CYCLE_COUNTER=30 + +CONFIG_ESP_TASK_WDT_EN=n +CONFIG_ESP_TASK_WDT_INIT=n + +# Enable debug logging +CONFIG_LOG_DEFAULT_LEVEL_DEBUG=n +CONFIG_LOG_MAXIMUM_LEVEL_DEBUG=y +CONFIG_MB_CONSOLE_HELPER_ENABLED=y diff --git a/test_apps/broadcast_test/sdkconfig.ci.serial b/test_apps/broadcast_test/sdkconfig.ci.serial new file mode 100644 index 00000000..e8bdee33 --- /dev/null +++ b/test_apps/broadcast_test/sdkconfig.ci.serial @@ -0,0 +1,15 @@ +# +# Serial-specific configuration +# +CONFIG_MB_TEST_SLAVE_TASK_PRIO=5 +CONFIG_MB_TEST_MASTER_TASK_PRIO=5 + +CONFIG_FMB_COMM_MODE_RTU_EN=y +CONFIG_FMB_COMM_MODE_ASCII_EN=y +CONFIG_FMB_COMM_MODE_TCP_EN=n +CONFIG_FMB_TCP_UID_ENABLED=n +CONFIG_MB_TEST_COMM_CYCLE_COUNTER=10 +CONFIG_MB_PORT_ADAPTER_EN=n +CONFIG_MB_TEST_LEAK_CRITICAL_LEVEL=128 +CONFIG_MB_TEST_LEAK_WARN_LEVEL=128 +CONFIG_UNITY_ENABLE_FIXTURE=y diff --git a/test_apps/test_common/test_common.c b/test_apps/test_common/test_common.c index f0c8af18..23d8f68d 100644 --- a/test_apps/test_common/test_common.c +++ b/test_apps/test_common/test_common.c @@ -7,6 +7,7 @@ #include "freertos/portmacro.h" #include "freertos/queue.h" +#include "esp_modbus_common.h" #include "port_adapter.h" #include "mb_common.h" #include "mbc_slave.h" @@ -139,6 +140,27 @@ void test_task_add_entry(TaskHandle_t task_handle, void *inst) xSemaphoreTake(new_entry->task_sema_handle, 1); } +static void *test_task_find_object(bool is_master) +{ + task_entry_t *it, *pfound = NULL; + if (LIST_EMPTY(&s_task_list)) { + return NULL; + } + + portENTER_CRITICAL(&s_list_spinlock); + LIST_FOREACH(it, &s_task_list, entries) { + if (it) { + mb_controller_common_t *pobj = (mb_controller_common_t *)it->inst_handle; + if (pobj && pobj->mb_base->descr.is_master == is_master) { + pfound = (void *)pobj; + break; + } + } + } + portEXIT_CRITICAL(&s_list_spinlock); + return pfound; +} + static task_entry_t *test_task_find_entry(TaskHandle_t task_handle) { TEST_ASSERT_NOT_NULL(task_handle); @@ -436,7 +458,19 @@ esp_err_t test_common_read_modbus_parameter(void *handle, uint16_t cid, uint16_t esp_err_t err = mbc_master_get_cid_info(handle, cid, ¶m_descriptor); if ((err != ESP_ERR_NOT_FOUND) && (param_descriptor != NULL)) { uint8_t type = 0; - err = mbc_master_get_parameter(handle, cid, (uint8_t *)par_data, &type); + if (param_descriptor->mb_slave_addr) { + err = mbc_master_get_parameter(handle, cid, (uint8_t *)par_data, &type); + } else { + // if the address is broadcast, try to read from first registered slave intentionally + void *pobj = test_task_find_object(false); + uint8_t uid = 0x01; + if (pobj) { + mbs_controller_iface_t *pctrl_obj = ((mbs_controller_iface_t *)pobj); + uid = pctrl_obj->opts.comm_opts.common_opts.uid; + } + ESP_LOGD(TAG, "Use slave object: %p, UID:%u", pobj, uid); + err = mbc_master_get_parameter_with(handle, cid, uid, (uint8_t *)par_data, &type); + } if (err == ESP_OK) { ESP_LOGI(TAG, "%p, CHAR #%u %s (%s) value = (0x%04x) parameter read successful.", handle, @@ -509,6 +543,7 @@ static void test_master_task(void *arg) for (cycle_counter = 0; cycle_counter <= TEST_TASK_CYCLE_COUNTER; cycle_counter++) { switch (req_type) { case RT_HOLDING_RD: + err = test_common_read_modbus_parameter(mbm_handle, CID_DEV_REG0, &holding_registers[CID_DEV_REG0]); CHECK_PAR_VALUE(CID_DEV_REG0, err, holding_registers[CID_DEV_REG0], TEST_REG_VAL1); @@ -675,7 +710,7 @@ TaskHandle_t test_common_slave_serial_create(mb_communication_info_t *pconfig, u test_common_slave_setup_start(mbs_handle); - if (priority) { + if (!priority) { priority = TEST_TASK_PRIO_SLAVE; } @@ -709,7 +744,7 @@ TaskHandle_t test_common_master_tcp_create(mb_communication_info_t *pconfig, uin TEST_ESP_OK(mbc_master_start(mbm_handle)); ESP_LOGI(TAG, "%p, modbus master start...", mbm_handle) ; - if (priority) { + if (!priority) { priority = TEST_TASK_PRIO_MASTER; }