diff --git a/test_apps/broadcast_test/CMakeLists.txt b/test_apps/broadcast_test/CMakeLists.txt new file mode 100644 index 0000000..266331d --- /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 0000000..28ab9f5 --- /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 0000000..d0dc01a --- /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 0000000..b56bf41 --- /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 0000000..8dee941 --- /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 0000000..ccf63d5 --- /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 0000000..1b6efef --- /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 0000000..cf06207 --- /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 0000000..383ed73 --- /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 0000000..e8bdee3 --- /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 366a741..23d8f68 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); @@ -438,8 +460,16 @@ esp_err_t test_common_read_modbus_parameter(void *handle, uint16_t cid, uint16_t uint8_t type = 0; 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 slave intentionally - err = mbc_master_get_parameter_with(handle, cid, param_descriptor->mb_slave_addr + 1, (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.",