From c343e08600eaeceaa281fdbc8859db0170e92b9d Mon Sep 17 00:00:00 2001 From: aleks Date: Sun, 10 Dec 2023 15:22:41 +0100 Subject: [PATCH] add test applications and unit tests --- .../mb_serial_slave/main/serial_slave.c | 4 +- test_apps/.build-test-rules.yml | 14 + test_apps/adapter_tests/.gitignore | 1 + test_apps/adapter_tests/CMakeLists.txt | 8 + test_apps/adapter_tests/README.md | 4 + test_apps/adapter_tests/main/CMakeLists.txt | 10 + .../adapter_tests/main/Kconfig.projbuild | 45 ++ test_apps/adapter_tests/main/component.mk | 4 + .../adapter_tests/main/idf_component.yml | 6 + test_apps/adapter_tests/main/test_app_main.c | 19 + .../main/test_modbus_adapter_comm.c | 249 +++++++ .../adapter_tests/pytest_mb_comm_adapter.py | 17 + test_apps/adapter_tests/sdkconfig.ci.serial | 23 + test_apps/adapter_tests/sdkconfig.ci.tcp | 23 + test_apps/adapter_tests/sdkconfig.defaults | 21 + test_apps/conftest.py | 115 +++ test_apps/physical_tests/.gitignore | 2 + test_apps/physical_tests/CMakeLists.txt | 8 + test_apps/physical_tests/README.md | 4 + test_apps/physical_tests/main/CMakeLists.txt | 12 + .../physical_tests/main/Kconfig.projbuild | 45 ++ test_apps/physical_tests/main/component.mk | 4 + .../physical_tests/main/idf_component.yml | 6 + test_apps/physical_tests/main/test_app_main.c | 24 + .../test_modbus_rs485_comm_master_slave.c | 220 ++++++ .../pytest_mb_comm_multi_dev.py | 24 + test_apps/physical_tests/sdkconfig.ci.serial | 23 + test_apps/physical_tests/sdkconfig.defaults | 20 + test_apps/test_common/CMakeLists.txt | 9 + test_apps/test_common/README.md | 3 + test_apps/test_common/include/test_common.h | 68 ++ .../test_common/mb_utest_lib/CMakeLists.txt | 72 ++ .../test_common/mb_utest_lib/port_adapter.c | 671 ++++++++++++++++++ .../test_common/mb_utest_lib/port_adapter.h | 47 ++ .../test_common/mb_utest_lib/port_stubs.c | 173 +++++ .../test_common/mb_utest_lib/port_stubs.h | 70 ++ test_apps/test_common/sdkconfig.defaults | 21 + test_apps/test_common/test_common.c | 527 ++++++++++++++ .../mb_controller_common/CMakeLists.txt | 9 + .../unit_tests/mb_controller_common/README.md | 4 + .../mocked_esp_modbus/CMakeLists.txt | 9 + .../mocked_esp_modbus/mock/mock_config.yaml | 9 + .../mocked_esp_modbus/test_mbm_object.h | 117 +++ .../mb_controller_common/main/CMakeLists.txt | 11 + .../main/Kconfig.projbuild | 45 ++ .../mb_controller_common/main/component.mk | 4 + .../main/idf_component.yml | 6 + .../mb_controller_common/main/test_app_main.c | 19 + .../main/test_mb_controller_common.c | 245 +++++++ .../pytest_mb_controller_common.py | 16 + .../mb_controller_common/sdkconfig.ci.serial | 23 + .../mb_controller_mapping/CMakeLists.txt | 17 + .../mb_controller_mapping/README.md | 4 + .../mocked_esp_modbus/CMakeLists.txt | 9 + .../mocked_esp_modbus/mock/mock_config.yaml | 9 + .../mocked_esp_modbus/test_mbm_object.h | 118 +++ .../mb_controller_mapping/main/CMakeLists.txt | 10 + .../main/Kconfig.projbuild | 45 ++ .../mb_controller_mapping/main/component.mk | 4 + .../main/idf_component.yml | 6 + .../main/test_app_main.c | 19 + .../main/test_mb_controller_unit.c | 487 +++++++++++++ .../pytest_mb_controller_mapping.py | 16 + .../mb_controller_mapping/sdkconfig.ci.serial | 19 + .../mb_controller_mapping/sdkconfig.default | 17 + .../unit_tests/test_stubs/CMakeLists.txt | 6 + .../test_stubs/include/mb_object_stub.h | 14 + .../test_stubs/include/transport_common.h | 46 ++ .../test_stubs/src/mb_object_stub.c | 159 +++++ .../test_stubs/src/mb_transport_stub.c | 300 ++++++++ 70 files changed, 4436 insertions(+), 2 deletions(-) create mode 100644 test_apps/.build-test-rules.yml create mode 100644 test_apps/adapter_tests/.gitignore create mode 100644 test_apps/adapter_tests/CMakeLists.txt create mode 100644 test_apps/adapter_tests/README.md create mode 100644 test_apps/adapter_tests/main/CMakeLists.txt create mode 100644 test_apps/adapter_tests/main/Kconfig.projbuild create mode 100644 test_apps/adapter_tests/main/component.mk create mode 100644 test_apps/adapter_tests/main/idf_component.yml create mode 100644 test_apps/adapter_tests/main/test_app_main.c create mode 100644 test_apps/adapter_tests/main/test_modbus_adapter_comm.c create mode 100644 test_apps/adapter_tests/pytest_mb_comm_adapter.py create mode 100644 test_apps/adapter_tests/sdkconfig.ci.serial create mode 100644 test_apps/adapter_tests/sdkconfig.ci.tcp create mode 100644 test_apps/adapter_tests/sdkconfig.defaults create mode 100644 test_apps/conftest.py create mode 100644 test_apps/physical_tests/.gitignore create mode 100644 test_apps/physical_tests/CMakeLists.txt create mode 100644 test_apps/physical_tests/README.md create mode 100644 test_apps/physical_tests/main/CMakeLists.txt create mode 100644 test_apps/physical_tests/main/Kconfig.projbuild create mode 100644 test_apps/physical_tests/main/component.mk create mode 100644 test_apps/physical_tests/main/idf_component.yml create mode 100644 test_apps/physical_tests/main/test_app_main.c create mode 100644 test_apps/physical_tests/main/test_modbus_rs485_comm_master_slave.c create mode 100644 test_apps/physical_tests/pytest_mb_comm_multi_dev.py create mode 100644 test_apps/physical_tests/sdkconfig.ci.serial create mode 100644 test_apps/physical_tests/sdkconfig.defaults create mode 100644 test_apps/test_common/CMakeLists.txt create mode 100644 test_apps/test_common/README.md create mode 100644 test_apps/test_common/include/test_common.h create mode 100644 test_apps/test_common/mb_utest_lib/CMakeLists.txt create mode 100644 test_apps/test_common/mb_utest_lib/port_adapter.c create mode 100644 test_apps/test_common/mb_utest_lib/port_adapter.h create mode 100644 test_apps/test_common/mb_utest_lib/port_stubs.c create mode 100644 test_apps/test_common/mb_utest_lib/port_stubs.h create mode 100644 test_apps/test_common/sdkconfig.defaults create mode 100644 test_apps/test_common/test_common.c create mode 100644 test_apps/unit_tests/mb_controller_common/CMakeLists.txt create mode 100644 test_apps/unit_tests/mb_controller_common/README.md create mode 100644 test_apps/unit_tests/mb_controller_common/components/mocked_esp_modbus/CMakeLists.txt create mode 100644 test_apps/unit_tests/mb_controller_common/components/mocked_esp_modbus/mock/mock_config.yaml create mode 100644 test_apps/unit_tests/mb_controller_common/components/mocked_esp_modbus/test_mbm_object.h create mode 100644 test_apps/unit_tests/mb_controller_common/main/CMakeLists.txt create mode 100644 test_apps/unit_tests/mb_controller_common/main/Kconfig.projbuild create mode 100644 test_apps/unit_tests/mb_controller_common/main/component.mk create mode 100644 test_apps/unit_tests/mb_controller_common/main/idf_component.yml create mode 100644 test_apps/unit_tests/mb_controller_common/main/test_app_main.c create mode 100644 test_apps/unit_tests/mb_controller_common/main/test_mb_controller_common.c create mode 100644 test_apps/unit_tests/mb_controller_common/pytest_mb_controller_common.py create mode 100644 test_apps/unit_tests/mb_controller_common/sdkconfig.ci.serial create mode 100644 test_apps/unit_tests/mb_controller_mapping/CMakeLists.txt create mode 100644 test_apps/unit_tests/mb_controller_mapping/README.md create mode 100644 test_apps/unit_tests/mb_controller_mapping/components/mocked_esp_modbus/CMakeLists.txt create mode 100644 test_apps/unit_tests/mb_controller_mapping/components/mocked_esp_modbus/mock/mock_config.yaml create mode 100644 test_apps/unit_tests/mb_controller_mapping/components/mocked_esp_modbus/test_mbm_object.h create mode 100644 test_apps/unit_tests/mb_controller_mapping/main/CMakeLists.txt create mode 100644 test_apps/unit_tests/mb_controller_mapping/main/Kconfig.projbuild create mode 100644 test_apps/unit_tests/mb_controller_mapping/main/component.mk create mode 100644 test_apps/unit_tests/mb_controller_mapping/main/idf_component.yml create mode 100644 test_apps/unit_tests/mb_controller_mapping/main/test_app_main.c create mode 100644 test_apps/unit_tests/mb_controller_mapping/main/test_mb_controller_unit.c create mode 100644 test_apps/unit_tests/mb_controller_mapping/pytest_mb_controller_mapping.py create mode 100644 test_apps/unit_tests/mb_controller_mapping/sdkconfig.ci.serial create mode 100644 test_apps/unit_tests/mb_controller_mapping/sdkconfig.default create mode 100644 test_apps/unit_tests/test_stubs/CMakeLists.txt create mode 100644 test_apps/unit_tests/test_stubs/include/mb_object_stub.h create mode 100644 test_apps/unit_tests/test_stubs/include/transport_common.h create mode 100644 test_apps/unit_tests/test_stubs/src/mb_object_stub.c create mode 100644 test_apps/unit_tests/test_stubs/src/mb_transport_stub.c diff --git a/examples/serial/mb_serial_slave/main/serial_slave.c b/examples/serial/mb_serial_slave/main/serial_slave.c index e2715b3..0a54f48 100644 --- a/examples/serial/mb_serial_slave/main/serial_slave.c +++ b/examples/serial/mb_serial_slave/main/serial_slave.c @@ -191,12 +191,12 @@ void app_main(void) (unsigned)reg_info.size); if (reg_info.address == (uint8_t*)&holding_reg_params.holding_data0) { - (void)mbc_slave_lock(slave_handle); + (void)mbc_slave_lock(mbc_slave_handle); holding_reg_params.holding_data0 += MB_CHAN_DATA_OFFSET; if (holding_reg_params.holding_data0 >= (MB_CHAN_DATA_MAX_VAL - MB_CHAN_DATA_OFFSET)) { coil_reg_params.coils_port1 = 0xFF; } - (void)mbc_slave_unlock(slave_handle); + (void)mbc_slave_unlock(mbc_slave_handle); } } else if (reg_info.type & MB_EVENT_INPUT_REG_RD) { ESP_LOGI(TAG, "INPUT READ (%" PRIu32 " us), ADDR:%u, TYPE:%u, INST_ADDR:0x%" PRIx32 ", SIZE:%u", diff --git a/test_apps/.build-test-rules.yml b/test_apps/.build-test-rules.yml new file mode 100644 index 0000000..684c87b --- /dev/null +++ b/test_apps/.build-test-rules.yml @@ -0,0 +1,14 @@ +physical_tests: + disable: + - if: CONFIG_NAME != "serial" or IDF_TARGET != "esp32" +# disable_test: +# - if: IDF_TARGET != "esp32" +# reason: only manual test is performed for other targets + + + + + + + + diff --git a/test_apps/adapter_tests/.gitignore b/test_apps/adapter_tests/.gitignore new file mode 100644 index 0000000..a348e50 --- /dev/null +++ b/test_apps/adapter_tests/.gitignore @@ -0,0 +1 @@ +/__pycache__/ diff --git a/test_apps/adapter_tests/CMakeLists.txt b/test_apps/adapter_tests/CMakeLists.txt new file mode 100644 index 0000000..391abd1 --- /dev/null +++ b/test_apps/adapter_tests/CMakeLists.txt @@ -0,0 +1,8 @@ +# This is the project CMakeLists.txt file for the test subproject +cmake_minimum_required(VERSION 3.16) + +set(EXTRA_COMPONENT_DIRS "$ENV{IDF_PATH}/tools/unit-test-app/components") +list(APPEND EXTRA_COMPONENT_DIRS "../test_common") + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(test_comm_adapter) diff --git a/test_apps/adapter_tests/README.md b/test_apps/adapter_tests/README.md new file mode 100644 index 0000000..a1a8536 --- /dev/null +++ b/test_apps/adapter_tests/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. diff --git a/test_apps/adapter_tests/main/CMakeLists.txt b/test_apps/adapter_tests/main/CMakeLists.txt new file mode 100644 index 0000000..0a7af86 --- /dev/null +++ b/test_apps/adapter_tests/main/CMakeLists.txt @@ -0,0 +1,10 @@ +set(srcs "test_app_main.c" + "test_modbus_adapter_comm.c" +) + +# In order for the cases defined by `TEST_CASE` to be linked into the final elf, +idf_component_register(SRCS ${srcs} + PRIV_REQUIRES cmock test_common unity test_utils + ) # + +set_property(TARGET ${COMPONENT_LIB} APPEND PROPERTY INTERFACE_LINK_LIBRARIES "-u mb_test_include_impl") diff --git a/test_apps/adapter_tests/main/Kconfig.projbuild b/test_apps/adapter_tests/main/Kconfig.projbuild new file mode 100644 index 0000000..5a837ad --- /dev/null +++ b/test_apps/adapter_tests/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 y + 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 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/adapter_tests/main/component.mk b/test_apps/adapter_tests/main/component.mk new file mode 100644 index 0000000..a98f634 --- /dev/null +++ b/test_apps/adapter_tests/main/component.mk @@ -0,0 +1,4 @@ +# +# "main" pseudo-component makefile. +# +# (Uses default behaviour of compiling all source files in directory, adding 'include' to include path.) diff --git a/test_apps/adapter_tests/main/idf_component.yml b/test_apps/adapter_tests/main/idf_component.yml new file mode 100644 index 0000000..4a811ac --- /dev/null +++ b/test_apps/adapter_tests/main/idf_component.yml @@ -0,0 +1,6 @@ +dependencies: + idf: ">=4.3" + espressif/esp-modbus: + version: "^2.0" + override_path: "../../../" + diff --git a/test_apps/adapter_tests/main/test_app_main.c b/test_apps/adapter_tests/main/test_app_main.c new file mode 100644 index 0000000..da68c55 --- /dev/null +++ b/test_apps/adapter_tests/main/test_app_main.c @@ -0,0 +1,19 @@ +/* + * SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: CC0-1.0 + */ + +#include "unity.h" +#include "unity_test_runner.h" +#include "unity_fixture.h" + +static void run_all_tests(void) +{ + RUN_TEST_GROUP(modbus_adapter_comm_basic); +} + +void app_main(void) +{ + UNITY_MAIN_FUNC(run_all_tests); +} diff --git a/test_apps/adapter_tests/main/test_modbus_adapter_comm.c b/test_apps/adapter_tests/main/test_modbus_adapter_comm.c new file mode 100644 index 0000000..c7401be --- /dev/null +++ b/test_apps/adapter_tests/main/test_modbus_adapter_comm.c @@ -0,0 +1,249 @@ +/* + * SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +#include "unity_fixture.h" + +#include "test_utils.h" + +#if __has_include("unity_test_utils.h") +#define UNITY_TEST_UTILS_INCLUDED +// unity test utils are used +#include "unity_test_utils.h" +#include "unity_test_utils_memory.h" +#else +// Unit_test_app utils from test_utils ("test_utils.h"), v4.4 +#define unity_utils_task_delete test_utils_task_delete +#endif + +#include "sdkconfig.h" +#include "test_common.h" + +#define TEST_SER_PORT_NUM 1 +#define TEST_TCP_PORT_NUM 1502 +#define TEST_TASKS_NUM 3 +#define TEST_TASK_TIMEOUT_MS 30000 +#define TEST_LEAK_WARN 32 +#define TEST_LEAK_CRITICAL 64 +#define TEST_SLAVE_SEND_TOUT_US 30000 +#define TEST_MASTER_SEND_TOUT_US 30000 + +#define TEST_MASTER_RESPOND_TOUT_MS CONFIG_FMB_MASTER_TIMEOUT_MS_RESPOND + +#define TAG "MODBUS_SERIAL_TEST" + +// The workaround to statically link whole test library +__attribute__((unused)) bool mb_test_include_impl = 1; + +// 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_ADDR1, 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_ADDR1, 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_ADDR1, 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_ADDR2, 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} +}; + +// Calculate number of parameters in the table +const uint16_t num_descriptors = (sizeof(descriptors) / sizeof(descriptors[0])); + +TaskHandle_t task_handles[TEST_TASKS_NUM] = {}; + +TEST_GROUP(modbus_adapter_comm_basic); + +TEST_SETUP(modbus_adapter_comm_basic) +{ + test_common_start(); +} + +TEST_TEAR_DOWN(modbus_adapter_comm_basic) +{ + uint32_t test_task = 0; + uint32_t test_task_count = 0; + int i = 0; + + // Trigger start of test task intentionally + for (i = 0; i < TEST_TASKS_NUM; i++) { + test_common_task_notify_start(task_handles[i], 1); + } + + for (i = 0; (i < TEST_TASKS_NUM); i++) { + test_task = test_common_wait_done(pdMS_TO_TICKS(TEST_TASK_TIMEOUT_MS)); + if (test_task) { + unity_utils_task_delete((TaskHandle_t)test_task); + ESP_LOGI(TAG, "Task %" PRIx32 " is complited.", test_task); + test_task_count++; + } + } + + vTaskDelay(5); // Let the test tasks with lower priority to suspend or delete itself from test_common + + TEST_ASSERT_EQUAL(TEST_TASKS_NUM, test_task_count); + ESP_LOGI(TAG, "Test done successfully."); + + test_common_stop(); +} + +#if (CONFIG_FMB_COMM_MODE_RTU_EN || CONFIG_FMB_COMM_MODE_ASCII_EN) + +TEST(modbus_adapter_comm_basic, test_modbus_adapter_rtu) +{ + 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_2, + .ser_opts.baudrate = 115200, + .ser_opts.parity = UART_PARITY_DISABLE, + .ser_opts.response_tout_ms = 1, + .ser_opts.test_tout_us = TEST_SLAVE_SEND_TOUT_US + }; + + task_handles[0] = test_slave_serial_create(&slave_config1); + + mb_communication_info_t slave_config2 = { + .ser_opts.port = TEST_SER_PORT_NUM, + .ser_opts.mode = MB_RTU, + .ser_opts.uid = MB_DEVICE_ADDR2, + .ser_opts.data_bits = UART_DATA_8_BITS, + .ser_opts.stop_bits = UART_STOP_BITS_2, + .ser_opts.baudrate = 115200, + .ser_opts.parity = UART_PARITY_DISABLE, + .ser_opts.response_tout_ms = 1, + .ser_opts.test_tout_us = TEST_SLAVE_SEND_TOUT_US + }; + + task_handles[1] = test_slave_serial_create(&slave_config2); + + // 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_2, + .ser_opts.baudrate = 115200, + .ser_opts.parity = UART_PARITY_DISABLE, + .ser_opts.response_tout_ms = TEST_MASTER_RESPOND_TOUT_MS, + .ser_opts.test_tout_us = TEST_MASTER_SEND_TOUT_US + }; + + task_handles[2] = test_master_serial_create(&master_config, &descriptors[0], num_descriptors); +} + +TEST(modbus_adapter_comm_basic, test_modbus_adapter_ascii) +{ + 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_2, + .ser_opts.baudrate = 115200, + .ser_opts.parity = UART_PARITY_DISABLE, + .ser_opts.response_tout_ms = 1, + .ser_opts.test_tout_us = TEST_SLAVE_SEND_TOUT_US + }; + + task_handles[0] = test_slave_serial_create(&slave_config1); + + mb_communication_info_t slave_config2 = { + .ser_opts.port = TEST_SER_PORT_NUM, + .ser_opts.mode = MB_ASCII, + .ser_opts.uid = MB_DEVICE_ADDR2, + .ser_opts.data_bits = UART_DATA_8_BITS, + .ser_opts.stop_bits = UART_STOP_BITS_2, + .ser_opts.baudrate = 115200, + .ser_opts.parity = UART_PARITY_DISABLE, + .ser_opts.response_tout_ms = 1, + .ser_opts.test_tout_us = TEST_SLAVE_SEND_TOUT_US + }; + + task_handles[1] = test_slave_serial_create(&slave_config2); + + 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_2, + .ser_opts.baudrate = 115200, + .ser_opts.parity = UART_PARITY_DISABLE, + .ser_opts.response_tout_ms = TEST_MASTER_RESPOND_TOUT_MS, + .ser_opts.test_tout_us = TEST_MASTER_SEND_TOUT_US + }; + + task_handles[2] = test_master_serial_create(&master_config, &descriptors[0], num_descriptors); +} + +#endif + +#if (CONFIG_FMB_COMM_MODE_TCP_EN) + +const char *slave_tcp_addr_table[] = { + "01;mb_slave_tcp_01;1502", // Corresponds to characteristic MB_DEVICE_ADDR1 "mb_slave_tcp_01" + "200;mb_slave_tcp_c8;1502", // Corresponds to characteristic MB_DEVICE_ADDR2 "mb_slave_tcp_C8" + NULL // End of table condition (must be included) +}; + +TEST(modbus_adapter_comm_basic, test_modbus_adapter_tcp) +{ + mb_communication_info_t tcp_slave_cfg_1 = { + .tcp_opts.port = TEST_TCP_PORT_NUM, + .tcp_opts.mode = MB_TCP, + .tcp_opts.addr_type = MB_IPV4, + .tcp_opts.ip_addr_table = NULL, + .tcp_opts.uid = MB_DEVICE_ADDR1, + .tcp_opts.start_disconnected = true, + .tcp_opts.response_tout_ms = 1, + .tcp_opts.test_tout_us = TEST_SLAVE_SEND_TOUT_US + }; + + task_handles[0] = test_slave_tcp_create(&tcp_slave_cfg_1); + + mb_communication_info_t tcp_slave_cfg_2 = { + .tcp_opts.port = TEST_TCP_PORT_NUM, + .tcp_opts.mode = MB_TCP, + .tcp_opts.addr_type = MB_IPV4, + .tcp_opts.ip_addr_table = NULL, + .tcp_opts.uid = MB_DEVICE_ADDR2, + .tcp_opts.start_disconnected = true, + .tcp_opts.response_tout_ms = TEST_MASTER_RESPOND_TOUT_MS, + .tcp_opts.test_tout_us = TEST_SLAVE_SEND_TOUT_US + }; + + task_handles[1] = test_slave_tcp_create(&tcp_slave_cfg_2); + + // Initialize and start Modbus controller + mb_communication_info_t tcp_master_cfg_1 = { + .tcp_opts.port = TEST_TCP_PORT_NUM, + .tcp_opts.mode = MB_TCP, + .tcp_opts.addr_type = MB_IPV4, + .tcp_opts.ip_addr_table = (void *)slave_tcp_addr_table, + .tcp_opts.uid = 0, + .tcp_opts.start_disconnected = false, + .tcp_opts.response_tout_ms = TEST_MASTER_RESPOND_TOUT_MS, + .tcp_opts.test_tout_us = TEST_MASTER_SEND_TOUT_US + }; + + task_handles[2] = test_master_tcp_create(&tcp_master_cfg_1, &descriptors[0], num_descriptors); +} + +#endif + +TEST_GROUP_RUNNER(modbus_adapter_comm_basic) +{ +#if (CONFIG_FMB_COMM_MODE_RTU_EN && CONFIG_FMB_COMM_MODE_ASCII_EN) + RUN_TEST_CASE(modbus_adapter_comm_basic, test_modbus_adapter_rtu); + RUN_TEST_CASE(modbus_adapter_comm_basic, test_modbus_adapter_ascii); +#endif + +#if (CONFIG_FMB_COMM_MODE_TCP_EN) + RUN_TEST_CASE(modbus_adapter_comm_basic, test_modbus_adapter_tcp); +#endif +} diff --git a/test_apps/adapter_tests/pytest_mb_comm_adapter.py b/test_apps/adapter_tests/pytest_mb_comm_adapter.py new file mode 100644 index 0000000..7615486 --- /dev/null +++ b/test_apps/adapter_tests/pytest_mb_comm_adapter.py @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: CC0-1.0 + +import pytest +from pytest_embedded import Dut + + +CONFIGS = [ + pytest.param('serial', marks=[pytest.mark.esp32, pytest.mark.esp32s2, pytest.mark.esp32s3, pytest.mark.esp32c3]), + pytest.param('tcp', marks=[pytest.mark.esp32, pytest.mark.esp32s2, pytest.mark.esp32s3, pytest.mark.esp32c3]), +] + + +@pytest.mark.generic_multi_device +@pytest.mark.parametrize('config', CONFIGS, indirect=True) +def test_modbus_comm_adapter(dut: Dut) -> None: + dut.expect_unity_test_output() diff --git a/test_apps/adapter_tests/sdkconfig.ci.serial b/test_apps/adapter_tests/sdkconfig.ci.serial new file mode 100644 index 0000000..8b0e7a7 --- /dev/null +++ b/test_apps/adapter_tests/sdkconfig.ci.serial @@ -0,0 +1,23 @@ +# This file was generated using idf.py save-defconfig. It can be edited manually. +# Espressif IoT Development Framework (ESP-IDF) Project Minimal Configuration +# +# +# Modbus configuration +# +CONFIG_UNITY_ENABLE_FIXTURE=y +CONFIG_FMB_PORT_TASK_STACK_SIZE=4096 +CONFIG_FMB_PORT_TASK_PRIO=10 +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=y +CONFIG_FMB_MASTER_TIMEOUT_MS_RESPOND=2000 +CONFIG_FMB_MASTER_DELAY_MS_CONVERT=300 +CONFIG_FMB_TIMER_USE_ISR_DISPATCH_METHOD=y +CONFIG_MB_PORT_ADAPTER_EN=y +CONFIG_MB_TEST_SLAVE_TASK_PRIO=4 +CONFIG_MB_TEST_MASTER_TASK_PRIO=4 +CONFIG_MB_TEST_COMM_CYCLE_COUNTER=10 +CONFIG_MB_TEST_LEAK_CRITICAL_LEVEL=128 +CONFIG_MB_TEST_LEAK_WARN_LEVEL=128 + diff --git a/test_apps/adapter_tests/sdkconfig.ci.tcp b/test_apps/adapter_tests/sdkconfig.ci.tcp new file mode 100644 index 0000000..ea87e56 --- /dev/null +++ b/test_apps/adapter_tests/sdkconfig.ci.tcp @@ -0,0 +1,23 @@ +# This file was generated using idf.py save-defconfig. It can be edited manually. +# Espressif IoT Development Framework (ESP-IDF) Project Minimal Configuration +# +# +# Modbus configuration +# +CONFIG_UNITY_ENABLE_FIXTURE=y +CONFIG_FMB_PORT_TASK_STACK_SIZE=4096 +CONFIG_FMB_PORT_TASK_PRIO=10 +CONFIG_FMB_COMM_MODE_RTU_EN=n +CONFIG_FMB_COMM_MODE_ASCII_EN=n +CONFIG_FMB_COMM_MODE_TCP_EN=y +CONFIG_FMB_TCP_UID_ENABLED=y +CONFIG_FMB_MASTER_TIMEOUT_MS_RESPOND=2000 +CONFIG_FMB_MASTER_DELAY_MS_CONVERT=300 +CONFIG_FMB_TIMER_USE_ISR_DISPATCH_METHOD=y +CONFIG_MB_PORT_ADAPTER_EN=y +CONFIG_MB_TEST_SLAVE_TASK_PRIO=4 +CONFIG_MB_TEST_MASTER_TASK_PRIO=4 +CONFIG_MB_TEST_COMM_CYCLE_COUNTER=10 +CONFIG_MB_TEST_LEAK_CRITICAL_LEVEL=128 +CONFIG_MB_TEST_LEAK_WARN_LEVEL=128 + diff --git a/test_apps/adapter_tests/sdkconfig.defaults b/test_apps/adapter_tests/sdkconfig.defaults new file mode 100644 index 0000000..4bafbd1 --- /dev/null +++ b/test_apps/adapter_tests/sdkconfig.defaults @@ -0,0 +1,21 @@ +# This file was generated using idf.py save-defconfig. It can be edited manually. +# Espressif IoT Development Framework (ESP-IDF) Project Minimal Configuration +# +# +# Modbus configuration +# +CONFIG_UNITY_ENABLE_FIXTURE=y +CONFIG_APP_BUILD_USE_FLASH_SECTIONS=n +CONFIG_FMB_PORT_TASK_STACK_SIZE=4096 +CONFIG_FMB_PORT_TASK_PRIO=10 +CONFIG_FMB_COMM_MODE_RTU_EN=y +CONFIG_FMB_COMM_MODE_ASCII_EN=y +CONFIG_FMB_COMM_MODE_TCP_EN=y +CONFIG_FMB_TCP_UID_ENABLED=y +CONFIG_FMB_MASTER_TIMEOUT_MS_RESPOND=2000 +CONFIG_FMB_MASTER_DELAY_MS_CONVERT=300 +CONFIG_FMB_TIMER_USE_ISR_DISPATCH_METHOD=y +CONFIG_MB_PORT_ADAPTER_EN=y +CONFIG_MB_TEST_MASTER_TASK_PRIO=4 +CONFIG_MB_TEST_SLAVE_TASK_PRIO=4 +CONFIG_MB_TEST_COMM_CYCLE_COUNTER=10 diff --git a/test_apps/conftest.py b/test_apps/conftest.py new file mode 100644 index 0000000..24edeff --- /dev/null +++ b/test_apps/conftest.py @@ -0,0 +1,115 @@ +# SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=W0621 # redefined-outer-name + +import logging +import os +import sys +from datetime import datetime +from enum import Enum +from typing import Any, Callable, Dict, Match, Optional, TextIO, Tuple + +import pexpect +import pytest +from pytest_embedded_idf import CaseTester +from _pytest.config import Config +from _pytest.fixtures import FixtureRequest +from _pytest.monkeypatch import MonkeyPatch +from pytest_embedded.plugin import multi_dut_argument, multi_dut_fixture +from pytest_embedded_idf.app import IdfApp +from pytest_embedded_idf.dut import IdfDut +from pytest_embedded_idf.serial import IdfSerial + +DEFAULT_SDKCONFIG = 'default' + + +############ +# Fixtures # +############ + + +@pytest.fixture +def case_tester(dut: IdfDut, **kwargs): # type: ignore + yield CaseTester(dut, **kwargs) + + +@pytest.fixture(scope='session', autouse=True) +def session_tempdir() -> str: + + _tmpdir = os.path.join( + os.path.dirname(__file__), + 'pytest_embedded_log', + datetime.now().strftime('%Y-%m-%d_%H-%M-%S'), + ) + os.makedirs(_tmpdir, exist_ok=True) + return _tmpdir + + +@pytest.fixture(autouse=True) +@multi_dut_fixture +def junit_properties( + test_case_name: str, record_xml_attribute: Callable[[str, object], None] +) -> None: + """ + This fixture is autoused and will modify the junit report test case name to .. + """ + record_xml_attribute('name', test_case_name) + + +@pytest.fixture(scope='module') +def monkeypatch_module(request: FixtureRequest) -> MonkeyPatch: + mp = MonkeyPatch() + request.addfinalizer(mp.undo) + return mp + + +@pytest.fixture +@multi_dut_argument +def config(request: FixtureRequest) -> str: + return getattr(request, 'param', None) or DEFAULT_SDKCONFIG + + +@pytest.fixture +@multi_dut_fixture +def build_dir(app_path: str, target: Optional[str], config: Optional[str]) -> str: + """ + Check local build dir with the following priority: + + 1. build__ + 2. build_ + 3. build_ + 4. build + + Args: + app_path: app path + target: target + config: config + + Returns: + valid build directory + """ + check_dirs = [] + if target is not None and config is not None: + check_dirs.append(f'build_{target}_{config}') + if target is not None: + check_dirs.append(f'build_{target}') + if config is not None: + check_dirs.append(f'build_{config}') + check_dirs.append('build') + + for check_dir in check_dirs: + binary_path = os.path.join(app_path, check_dir) + if os.path.isdir(binary_path): + logging.info(f'find valid binary path: {binary_path}') + return check_dir + + logging.warning( + 'checking binary path: %s... missing... try another place', binary_path + ) + + recommend_place = check_dirs[0] + logging.error( + f'no build dir valid. Please build the binary via "idf.py -B {recommend_place} build" and run pytest again' + ) + sys.exit(1) diff --git a/test_apps/physical_tests/.gitignore b/test_apps/physical_tests/.gitignore new file mode 100644 index 0000000..5e16a9c --- /dev/null +++ b/test_apps/physical_tests/.gitignore @@ -0,0 +1,2 @@ +/pytest_embedded_log/ +/__pycache__/ diff --git a/test_apps/physical_tests/CMakeLists.txt b/test_apps/physical_tests/CMakeLists.txt new file mode 100644 index 0000000..5f208ff --- /dev/null +++ b/test_apps/physical_tests/CMakeLists.txt @@ -0,0 +1,8 @@ +# This is the project CMakeLists.txt file for the test subproject +cmake_minimum_required(VERSION 3.16) + +set(EXTRA_COMPONENT_DIRS "$ENV{IDF_PATH}/tools/unit-test-app/components") +list(APPEND EXTRA_COMPONENT_DIRS "../test_common") + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(test_modbus_comm_multi_dev) diff --git a/test_apps/physical_tests/README.md b/test_apps/physical_tests/README.md new file mode 100644 index 0000000..a1a8536 --- /dev/null +++ b/test_apps/physical_tests/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. diff --git a/test_apps/physical_tests/main/CMakeLists.txt b/test_apps/physical_tests/main/CMakeLists.txt new file mode 100644 index 0000000..3fa9270 --- /dev/null +++ b/test_apps/physical_tests/main/CMakeLists.txt @@ -0,0 +1,12 @@ +set(srcs "test_app_main.c" + "test_modbus_rs485_comm_master_slave.c" +) + +# In order for the cases defined by `TEST_CASE` to be linked into the final elf, +idf_component_register(SRCS ${srcs} + PRIV_REQUIRES cmock test_utils test_common unity + ) + +# The workaround for WHOLE_ARCHIVE is absent in v4.4 +set_property(TARGET ${COMPONENT_LIB} APPEND PROPERTY INTERFACE_LINK_LIBRARIES "-u mb_test_include_impl") + diff --git a/test_apps/physical_tests/main/Kconfig.projbuild b/test_apps/physical_tests/main/Kconfig.projbuild new file mode 100644 index 0000000..4b8402c --- /dev/null +++ b/test_apps/physical_tests/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 y + 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/physical_tests/main/component.mk b/test_apps/physical_tests/main/component.mk new file mode 100644 index 0000000..a98f634 --- /dev/null +++ b/test_apps/physical_tests/main/component.mk @@ -0,0 +1,4 @@ +# +# "main" pseudo-component makefile. +# +# (Uses default behaviour of compiling all source files in directory, adding 'include' to include path.) diff --git a/test_apps/physical_tests/main/idf_component.yml b/test_apps/physical_tests/main/idf_component.yml new file mode 100644 index 0000000..4a811ac --- /dev/null +++ b/test_apps/physical_tests/main/idf_component.yml @@ -0,0 +1,6 @@ +dependencies: + idf: ">=4.3" + espressif/esp-modbus: + version: "^2.0" + override_path: "../../../" + diff --git a/test_apps/physical_tests/main/test_app_main.c b/test_apps/physical_tests/main/test_app_main.c new file mode 100644 index 0000000..425e6c1 --- /dev/null +++ b/test_apps/physical_tests/main/test_app_main.c @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: CC0-1.0 + */ + +#include "unity.h" +#include "test_common.h" + +void setUp(void) +{ + test_common_start(); +} + +void tearDown(void) +{ + test_common_stop(); +} + +void app_main(void) +{ + printf("Modbus RS485 multi-device test cases/n"); + unity_run_menu(); +} diff --git a/test_apps/physical_tests/main/test_modbus_rs485_comm_master_slave.c b/test_apps/physical_tests/main/test_modbus_rs485_comm_master_slave.c new file mode 100644 index 0000000..3baa1dc --- /dev/null +++ b/test_apps/physical_tests/main/test_modbus_rs485_comm_master_slave.c @@ -0,0 +1,220 @@ +/* + * SPDX-FileCopyrightText: 2023 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 (30000) +#define TEST_SEND_TOUT_US (30000) +#define TEST_RESP_TOUT_MS (1000) + +#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) +#define TEST_BAUD_RATE (115200) +#elif CONFIG_IDF_TARGET_ESP32C3 +#define TEST_SER_PIN_RX (4) +#define TEST_SER_PIN_TX (5) +#define TEST_SER_PIN_RTS (10) +#define TEST_BAUD_RATE (115200) +#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_impl = 1; + +#define TAG "MODBUS_SERIAL_COMM_TEST" + +#if (CONFIG_FMB_COMM_MODE_RTU_EN || CONFIG_FMB_COMM_MODE_ASCII_EN) + +// 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_ADDR1, 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_ADDR1, 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_ADDR1, 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_ADDR1, 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_task_start_wait_done(TaskHandle_t task_handle) +{ + uint32_t test_task = 0; + + // Start test sequence intentionally in the task + test_common_task_notify_start(task_handle, 1); + + for (int i = 0; (i < 2); i++) { + test_task = test_common_wait_done(pdMS_TO_TICKS(TEST_TASK_TIMEOUT_MS)); + if (test_task == (uint32_t)task_handle) { + unity_utils_task_delete((TaskHandle_t)test_task); + break; + } + } + + vTaskDelay(5); // A small delay to let the test lower priority task delete itself + if (test_task != (uint32_t)task_handle) { + ESP_LOGI(TAG, "Could not complete task 0x%" PRIx32" after %d ms, force kill the task.", + (uint32_t)task_handle, TEST_TASK_TIMEOUT_MS); + unity_utils_task_delete((TaskHandle_t)task_handle); + } + + TEST_ASSERT_EQUAL(test_task, (uint32_t)task_handle); + ESP_LOGI(TAG, "Test task 0x%" PRIx32 ", done successfully.", (uint32_t)task_handle); +} + +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 + }; + + 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)); + + TaskHandle_t slave_task_handle = test_slave_serial_create(&slave_config1); + + // 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 RTU is started. (%s).", __func__); + + unity_send_signal("Slave_ready"); + unity_wait_for_signal("Master_started"); + + test_task_start_wait_done(slave_task_handle); + +} + +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_master_serial_create(&master_config, &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)); + + test_task_start_wait_done(master_task_handle); +} + +/* + * Modbus RS485 RTU multi device test case + */ +TEST_CASE_MULTIPLE_DEVICES("Modbus RS485 RTU multi device master - slave 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 + }; + + 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)); + + TaskHandle_t slave_task_handle = test_slave_serial_create(&slave_config1); + + // 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_task_start_wait_done(slave_task_handle); + +} + +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_master_serial_create(&master_config, &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_task_start_wait_done(master_task_handle); +} + +/* + * Modbus RS485 ASCII multi device test case + */ +TEST_CASE_MULTIPLE_DEVICES("Modbus RS485 ASCII multi device master - slave case.", "[modbus][test_env=multi_dut_modbus_serial]", test_modbus_rs485_ascii_slave, test_modbus_rs485_ascii_master); + + +#endif \ No newline at end of file diff --git a/test_apps/physical_tests/pytest_mb_comm_multi_dev.py b/test_apps/physical_tests/pytest_mb_comm_multi_dev.py new file mode 100644 index 0000000..b936924 --- /dev/null +++ b/test_apps/physical_tests/pytest_mb_comm_multi_dev.py @@ -0,0 +1,24 @@ +# SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: CC0-1.0 + +import pytest +from pytest_embedded_idf import CaseTester + +@pytest.mark.esp32 +@pytest.mark.multi_dut_modbus_serial +@pytest.mark.parametrize('count, config', [(2, 'serial')], indirect=True) +def test_modbus_comm_multi_dev_serial(case_tester) -> None: # type: ignore + #case_tester.run_all_multi_dev_cases(reset=True) + for case in case_tester.test_menu: + if case.attributes.get('test_env', 'multi_dut_modbus_serial') == 'multi_dut_modbus_serial': + print(case.name) + case_tester.run_multi_dev_case(case=case, reset=True) + + +# @pytest.mark.esp32 +# @pytest.mark.multi_dut_modbus_tcp +# #@pytest.mark.parametrize('count, config', [(2, 'tcp')], indirect=True) +# def test_modbus_comm_multi_dev_tcp(case_tester) -> None: # type: ignore +# for case in case_tester.test_menu: +# if case.attributes.get('test_env', 'multi_dut_modbus_tcp') == 'multi_dut_modbus_tcp': +# case_tester.run_multi_dev_case(case=case, reset=True) \ No newline at end of file diff --git a/test_apps/physical_tests/sdkconfig.ci.serial b/test_apps/physical_tests/sdkconfig.ci.serial new file mode 100644 index 0000000..bdf4a1f --- /dev/null +++ b/test_apps/physical_tests/sdkconfig.ci.serial @@ -0,0 +1,23 @@ +# This file was generated using idf.py save-defconfig. It can be edited manually. +# Espressif IoT Development Framework (ESP-IDF) Project Minimal Configuration +# +# +# Modbus configuration +# +CONFIG_UNITY_ENABLE_FIXTURE=y +CONFIG_FMB_PORT_TASK_STACK_SIZE=4096 +CONFIG_FMB_PORT_TASK_PRIO=10 +CONFIG_FMB_COMM_MODE_RTU_EN=y +CONFIG_FMB_COMM_MODE_ASCII_EN=y +CONFIG_FMB_COMM_MODE_TCP_EN=y +CONFIG_FMB_TCP_UID_ENABLED=y +CONFIG_FMB_MASTER_TIMEOUT_MS_RESPOND=2000 +CONFIG_FMB_MASTER_DELAY_MS_CONVERT=300 +CONFIG_FMB_TIMER_USE_ISR_DISPATCH_METHOD=y +CONFIG_MB_TEST_SLAVE_TASK_PRIO=5 +CONFIG_MB_TEST_MASTER_TASK_PRIO=5 +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 + diff --git a/test_apps/physical_tests/sdkconfig.defaults b/test_apps/physical_tests/sdkconfig.defaults new file mode 100644 index 0000000..7cc5a83 --- /dev/null +++ b/test_apps/physical_tests/sdkconfig.defaults @@ -0,0 +1,20 @@ +# This file was generated using idf.py save-defconfig. It can be edited manually. +# Espressif IoT Development Framework (ESP-IDF) Project Minimal Configuration +# +# +# Modbus configuration +# +CONFIG_UNITY_ENABLE_FIXTURE=y +CONFIG_FMB_PORT_TASK_STACK_SIZE=4096 +CONFIG_FMB_PORT_TASK_PRIO=10 +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=y +CONFIG_FMB_MASTER_TIMEOUT_MS_RESPOND=2000 +CONFIG_FMB_MASTER_DELAY_MS_CONVERT=300 +CONFIG_FMB_TIMER_USE_ISR_DISPATCH_METHOD=y +CONFIG_MB_PORT_ADAPTER_EN=n +CONFIG_MB_TEST_MASTER_TASK_PRIO=5 +CONFIG_MB_TEST_MASTER_TASK_PRIO=5 +CONFIG_MB_TEST_COMM_CYCLE_COUNTER=10 diff --git a/test_apps/test_common/CMakeLists.txt b/test_apps/test_common/CMakeLists.txt new file mode 100644 index 0000000..e5114cf --- /dev/null +++ b/test_apps/test_common/CMakeLists.txt @@ -0,0 +1,9 @@ +cmake_minimum_required(VERSION 3.16) +idf_component_register(SRCS "test_common.c" + INCLUDE_DIRS "include" + REQUIRES unity) + +set(EXTRA_COMPONENT_DIRS "$ENV{IDF_PATH}/tools/unit-test-app/components") + +add_subdirectory(mb_utest_lib) +target_link_libraries(${COMPONENT_LIB} PUBLIC mb_ut_lib) diff --git a/test_apps/test_common/README.md b/test_apps/test_common/README.md new file mode 100644 index 0000000..bc45836 --- /dev/null +++ b/test_apps/test_common/README.md @@ -0,0 +1,3 @@ +# Test common + +This directory contains component that is common for Modbus master and slave tests. diff --git a/test_apps/test_common/include/test_common.h b/test_apps/test_common/include/test_common.h new file mode 100644 index 0000000..13de60e --- /dev/null +++ b/test_apps/test_common/include/test_common.h @@ -0,0 +1,68 @@ +/* + * SPDX-FileCopyrightText: 2018-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include +#include "unity.h" +#include "unity_test_runner.h" + +#include "mbcontroller.h" // for common Modbus defines +#include "esp_log.h" + +#include "sdkconfig.h" + +#define STR(fieldname) ((const char *)(fieldname)) +#define OPTS(min_val, max_val, step_val) \ +{ \ + .opt1 = min_val, .opt2 = max_val, .opt3 = step_val \ +} + +// Enumeration of modbus slave addresses accessed by master device +enum +{ + MB_DEVICE_ADDR1 = 1, + MB_DEVICE_ADDR2 = 200 +}; + +// Enumeration of all supported CIDs for device (used in parameter definition table) +enum { + CID_DEV_REG0 = 0, + CID_DEV_REG1, + CID_DEV_REG2, + CID_DEV_REG3, + CID_DEV_REG_COUNT +}; + +// Enumeration of predefined test values +enum { + TEST_REG_VAL1 = 0x1111, + TEST_REG_VAL2 = 0x2222, + TEST_REG_VAL3 = 0x3333, + TEST_REG_VAL4 = 0x4444 +}; + +/** + * @brief Helper test functions for multi instance modbus master - slave test + * + */ +TaskHandle_t test_slave_serial_create(mb_communication_info_t *pconfig); +TaskHandle_t test_master_serial_create(mb_communication_info_t *pconfig, const mb_parameter_descriptor_t *pdescr, uint16_t descr_size); +TaskHandle_t test_slave_tcp_create(mb_communication_info_t *pconfig); +TaskHandle_t test_master_tcp_create(mb_communication_info_t *pconfig, const mb_parameter_descriptor_t *pdescr, uint16_t descr_size); +TaskHandle_t test_slave_start_busy_task(); +// slave setup register area helper +void test_slave_setup_start(void *mbs_handle); +// Helper function to read characteristic from slave +esp_err_t read_modbus_parameter(void *handle, uint16_t cid, uint16_t *par_data); +// Helper function to write characteristic into slave +esp_err_t write_modbus_parameter(void *handle, uint16_t cid, uint16_t *par_data); + +// Common test check leak function +void test_common_check_leak(size_t before_free, size_t after_free, const char *type, size_t warn_threshold, size_t critical_threshold); +void test_slave_stop_busy_task(TaskHandle_t busy_task_handle); +uint32_t test_common_wait_done(TickType_t timeout_ticks); +void test_common_start(); +void test_common_stop(); +void test_common_task_notify_start(TaskHandle_t task_handle, uint32_t value); diff --git a/test_apps/test_common/mb_utest_lib/CMakeLists.txt b/test_apps/test_common/mb_utest_lib/CMakeLists.txt new file mode 100644 index 0000000..588d50c --- /dev/null +++ b/test_apps/test_common/mb_utest_lib/CMakeLists.txt @@ -0,0 +1,72 @@ +message(STATUS "mb_ut_lib: ${CMAKE_CURRENT_LIST_DIR}, ${CONFIG_MB_UTEST}") + +add_library(mb_ut_lib "${CMAKE_CURRENT_LIST_DIR}/port_adapter.c" + "${CMAKE_CURRENT_LIST_DIR}/port_stubs.c") + +idf_component_get_property(dir esp-modbus COMPONENT_DIR) +target_include_directories(mb_ut_lib PUBLIC + "${CMAKE_CURRENT_LIST_DIR}" + "${CMAKE_CURRENT_LIST_DIR}/include" + "${dir}/modbus/mb_controller/common" + "${dir}/modbus/mb_controller/common/include" + "${dir}/modbus/mb_controller/serial" + "${dir}/modbus/mb_controller/tcp" + "${dir}/modbus/mb_objects/include" + "${dir}/modbus/mb_ports/common" + "${dir}/modbus/mb_ports/serial" + "${dir}/modbus/mb_ports/tcp" + "${dir}/modbus/mb_transports" + "${dir}/modbus/mb_transports/rtu" + "${dir}/modbus/mb_transports/ascii" + "${dir}/modbus/mb_transports/tcp" + ) + +idf_component_get_property(driver_lib driver COMPONENT_LIB) +target_link_libraries(mb_ut_lib PUBLIC ${driver_lib}) +idf_component_get_property(timer_lib esp_timer COMPONENT_LIB) +target_link_libraries(mb_ut_lib PUBLIC ${timer_lib}) +idf_component_get_property(netif_lib esp_netif COMPONENT_LIB) +target_link_libraries(mb_ut_lib PUBLIC ${netif_lib}) +idf_component_get_property(test_utils_lib test_utils COMPONENT_LIB) +target_link_libraries(mb_ut_lib PUBLIC ${test_utils_lib}) + + +# Wrap port functions to substitute port with port_adapter object +if(CONFIG_MB_PORT_ADAPTER_EN) + + set(WRAP_FUNCTIONS + mbm_port_tcp_create + mbm_port_tcp_set_conn_cb + mbm_port_tcp_get_slave_info + mbm_port_tcp_send_data + mbm_port_tcp_recv_data + mbm_port_tcp_enable + mbm_port_tcp_disable + mbm_port_tcp_delete + # mbm_port_timer_expired + mbs_port_tcp_create + mbs_port_tcp_enable + mbs_port_tcp_disable + mbs_port_tcp_send_data + mbs_port_tcp_recv_data + mbs_port_tcp_delete + mb_port_evt_post + mb_port_evt_get + mb_port_ser_create + mb_port_ser_recv_data + mb_port_ser_send_data + mb_port_ser_enable + mb_port_ser_disable + mb_port_ser_delete + ) + +foreach(wrap ${WRAP_FUNCTIONS}) + target_link_libraries(mb_ut_lib PUBLIC "-Wl,--undefined=${wrap}") + target_link_libraries(mb_ut_lib PUBLIC "-Wl,--wrap=${wrap}") + #target_link_libraries(mb_ut_lib INTERFACE "-u __wrap_${wrap}") +endforeach() + +endif() + +# allow multiple symbol definitions +target_link_libraries(mb_ut_lib PUBLIC "-Wl,--allow-multiple-definition") \ No newline at end of file diff --git a/test_apps/test_common/mb_utest_lib/port_adapter.c b/test_apps/test_common/mb_utest_lib/port_adapter.c new file mode 100644 index 0000000..6506d3d --- /dev/null +++ b/test_apps/test_common/mb_utest_lib/port_adapter.c @@ -0,0 +1,671 @@ +/* + * SPDX-FileCopyrightText: 2018-2021 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include "freertos/FreeRTOS.h" +#include "freertos/queue.h" + +#include "esp_timer.h" +#include "sdkconfig.h" +#include "esp_log.h" +#include "esp_err.h" + +#include "mb_common.h" +#include "esp_modbus_common.h" +#include "mbc_slave.h" + +#include "mb_common.h" +#include "port_common.h" +#include "mb_config.h" +#include "port_serial_common.h" +#include "port_adapter.h" +#include "mb_port_types.h" +#include "port_stubs.h" + +#ifdef __cplusplus +extern "C" +{ +#endif + +/* ----------------------- Defines ------------------------------------------*/ + +#define MB_ADAPTER_TASK_STACK_SIZE (CONFIG_FMB_PORT_TASK_STACK_SIZE) +#define MB_ADAPTER_MAX_PORTS (8) +#define MB_ADAPTER_RX_QUEUE_MAX_SIZE (CONFIG_FMB_QUEUE_LENGTH * MB_ADAPTER_MAX_PORTS) +#define MB_ADAPTER_TX_QUEUE_MAX_SIZE (CONFIG_FMB_QUEUE_LENGTH * MB_ADAPTER_MAX_PORTS) +#define MB_ADAPTER_QUEUE_TIMEOUT (200 / portTICK_PERIOD_MS) +#define MB_ADAPTER_QUEUE_SET_MAX_LEN ((sizeof(frame_entry_t) + sizeof(mb_uid_info_t)) * MB_ADAPTER_MAX_PORTS) // +#define MB_ADAPTER_CONN_TIMEOUT (200 / portTICK_PERIOD_MS) + +typedef struct _mb_adapter_port_entry +{ + mb_port_base_t base; + uint8_t rx_buffer[CONFIG_FMB_BUFFER_SIZE]; + uint16_t recv_length; + uint64_t send_time_stamp; + uint64_t recv_time_stamp; + uint64_t test_timeout_us; + uint32_t flags; + mb_uid_info_t addr_info; + QueueHandle_t rx_queue; + QueueHandle_t tx_queue; + QueueHandle_t conn_queue; /*!< conection queue handle */ + SemaphoreHandle_t conn_sema_handle; /*!< connection blocking semaphore handle */ + esp_timer_handle_t timer_handle; + EventGroupHandle_t event_group_handle; + LIST_ENTRY(_mb_adapter_port_entry) entries; +} mb_port_adapter_t; + +/* ----------------------- Static variables & functions ----------------------*/ +static const char *TAG = "mb_port.test_adapter"; + +static LIST_HEAD(mb_port_inst, _mb_adapter_port_entry) s_port_list = LIST_HEAD_INITIALIZER(s_port_list); +static uint32_t s_port_list_counter = 0; /*!< port registered instance counter */ + +// The queue set for the receive task +static QueueSetHandle_t queue_set = NULL; +static TaskHandle_t adapter_task_handle; /*!< receive task handle */ + +IRAM_ATTR +static bool mb_port_adapter_tmr_expired(void *inst) +{ + mb_port_adapter_t *port_obj = __containerof(inst, mb_port_adapter_t, base); + + bool need_poll = false; + mb_tmr_mode_enum_t timer_mode = mb_port_get_cur_tmr_mode(&port_obj->base); + + mb_port_tmr_disable(&port_obj->base); + + switch (timer_mode) + { + case MB_TMODE_T35: + need_poll = mb_port_evt_post(&port_obj->base, EVENT(EV_READY)); + ESP_EARLY_LOGD(TAG, "%p:EV_READY", port_obj->base.descr.parent); + break; + + case MB_TMODE_RESPOND_TIMEOUT: + mb_port_evt_set_err_type(&port_obj->base, EV_ERROR_RESPOND_TIMEOUT); + need_poll = mb_port_evt_post(&port_obj->base, EVENT(EV_ERROR_PROCESS)); + + ESP_EARLY_LOGW(TAG, "%p:EV_ERROR_RESPOND_TIMEOUT", port_obj->base.descr.parent); + break; + + case MB_TMODE_CONVERT_DELAY: + /* If timer mode is convert delay, the master event then turns EV_MASTER_EXECUTE status. */ + need_poll = mb_port_evt_post(&port_obj->base, EVENT(EV_EXECUTE)); + ESP_EARLY_LOGD(TAG, "%p:MB_TMODE_CONVERT_DELAY", port_obj->base.descr.parent); + break; + + default: + need_poll = mb_port_evt_post(&port_obj->base, EVENT(EV_READY)); + break; + } + + return need_poll; +} + +void mb_port_adapter_set_response_time(mb_port_base_t *inst, uint64_t resp_time) +{ + mb_port_adapter_t *port_obj = __containerof(inst, mb_port_adapter_t, base); + atomic_store(&(port_obj->test_timeout_us), resp_time); +} + +int mb_port_adapter_get_rx_buffer(mb_port_base_t *inst, uint8_t **ppfame, int *plen) +{ + MB_RETURN_ON_FALSE((ppfame && plen), -1, TAG, "mb serial get buffer failure."); + mb_port_adapter_t *port_obj = __containerof(inst, mb_port_adapter_t, base); + int sz = port_obj->recv_length; + if (*ppfame && *plen >= port_obj->recv_length) + { + CRITICAL_SECTION(inst->lock) + { + memcpy(*ppfame, port_obj->rx_buffer, sz); + } + } + else + { + *ppfame = port_obj->rx_buffer; + *plen = sz; + } + return sz; +} + +int mb_port_adapter_get_tx_buffer(mb_port_base_t *inst, uint8_t **ppfame, int *plen) +{ + MB_RETURN_ON_FALSE((ppfame && plen), -1, TAG, "mb serial get buffer failure."); + mb_port_adapter_t *port_obj = __containerof(inst, mb_port_adapter_t, base); + int sz = port_obj->recv_length; + if (*ppfame && *plen >= port_obj->recv_length) + { + CRITICAL_SECTION(inst->lock) + { + memcpy(*ppfame, port_obj->rx_buffer, sz); + } + } + else + { + *ppfame = port_obj->rx_buffer; + *plen = sz; + } + return sz; +} + +void mb_port_adapter_set_flag(mb_port_base_t *inst, mb_queue_flags_t mask) +{ + mb_port_adapter_t *port_obj = __containerof(inst, mb_port_adapter_t, base); + EventBits_t bits = xEventGroupSetBits(port_obj->event_group_handle, (EventBits_t)mask); + ESP_LOGV(TAG, "%s: set flag (0x%x).", inst->descr.parent_name, (int)bits); +} + +void mb_port_adapter_clear_flag(mb_port_base_t *inst, mb_queue_flags_t mask) +{ + mb_port_adapter_t *port_obj = __containerof(inst, mb_port_adapter_t, base); + EventBits_t bits = xEventGroupClearBits(port_obj->event_group_handle, (EventBits_t)mask); + ESP_LOGV(TAG, "%s: clear flag (0x%x).", inst->descr.parent_name, (int)bits); +} + +uint16_t mb_port_adapter_wait_flag(mb_port_base_t *inst, uint16_t mask, uint32_t timeout) +{ + mb_port_adapter_t *port_obj = __containerof(inst, mb_port_adapter_t, base); + EventBits_t bits = xEventGroupWaitBits(port_obj->event_group_handle, // The event group being tested. + (EventBits_t)mask, // The bits within the event group to wait for. + pdTRUE, // Masked bits should be cleared before returning. + pdFALSE, // Don't wait for both bits, either bit will do. + (TickType_t)timeout); // Wait during timeout for either bit to be set. + ESP_LOGV(TAG, "%s: get flag (0x%x).", inst->descr.parent_name, (int)bits); + return (uint16_t)bits; +} + +// Timer task to send notification on timeout expiration +IRAM_ATTR +static void mb_port_adapter_timer_cb(void *param) +{ + mb_port_adapter_t *port_obj = __containerof(param, mb_port_adapter_t, base); + uint8_t temp_buffer[CONFIG_FMB_BUFFER_SIZE] = {0}; + mb_port_adapter_t *it; + + if (!LIST_EMPTY(&s_port_list)) + { + // send the queued frame to all registered ports with the same port number + int sz = queue_pop(port_obj->tx_queue, (void *)&temp_buffer[0], CONFIG_FMB_BUFFER_SIZE, NULL); + LIST_FOREACH(it, &s_port_list, entries) + { + if (it && (it != port_obj) && + (port_obj->addr_info.port == it->addr_info.port) && (sz != -1) + && (port_obj->addr_info.proto == it->addr_info.proto) + && (!port_obj->addr_info.uid || !it->addr_info.uid)) + { + // Send the data to all ports with the same communication port setting except itself + queue_push(it->rx_queue, (void *)&temp_buffer[0], sz, NULL); + mb_port_adapter_set_flag(&port_obj->base, MB_QUEUE_FLAG_SENT); + ESP_LOGW(TAG, "Send (%d bytes) from %s to %s. ", (int)sz, port_obj->base.descr.parent_name, it->base.descr.parent_name); + } + } + } +} + +bool mb_port_adapter_is_connected(void *inst) +{ + mb_port_adapter_t *port_obj = __containerof(inst, mb_port_adapter_t, base); + if (queue_is_empty(port_obj->conn_queue) + && port_obj->base.descr.is_master) { + return true; + } + return false; +} + +static void mb_port_adapter_conn_logic(void *inst, mb_uid_info_t *paddr_info) +{ + bool slave_found = false; + mb_port_adapter_t *slave = NULL; + mb_port_adapter_t *port_obj = __containerof(inst, mb_port_adapter_t, base); + + if (port_obj->base.descr.is_master) { // master object + LIST_FOREACH(slave, &s_port_list, entries) { + if ((paddr_info->uid == slave->addr_info.uid) + && !slave->base.descr.is_master + && (paddr_info->port == slave->addr_info.port)) { + // Register each slave object + ESP_LOGW(TAG, "Check connection state of object #%d(%s), uid: %d, port: %d, %s", + paddr_info->index, paddr_info->node_name_str, + paddr_info->uid, paddr_info->port, + (paddr_info->state == MB_SOCK_STATE_CONNECTED) ? "CONNECTED" : "DISCONNECTED"); + if ((paddr_info->state != MB_SOCK_STATE_CONNECTED) || (paddr_info->inst != inst)) { + (void)xQueueSend(slave->conn_queue, &port_obj->addr_info, MB_ADAPTER_QUEUE_TIMEOUT); + } else { + mb_port_adapter_set_flag(inst, MB_QUEUE_FLAG_CONNECTED); + } + slave_found = true; + break; + } + } + if (!slave_found) { + // reactivate the connection set + ESP_LOGE(TAG, "Slave #%d(%s), uid: %d, port: %d is not found, reconnect.", + paddr_info->index, paddr_info->node_name_str, paddr_info->uid, paddr_info->port); + (void)xQueueSend(port_obj->conn_queue, paddr_info, MB_ADAPTER_QUEUE_TIMEOUT); + vTaskDelay(MB_ADAPTER_CONN_TIMEOUT); + } + } else { // slave connection logic + ESP_LOGW(TAG, "Register connection in adapter object #%d(%s), uid: %d, port: %d, to master %s", + port_obj->addr_info.index, port_obj->addr_info.node_name_str, + port_obj->addr_info.uid, port_obj->addr_info.port, paddr_info->node_name_str); + // Mimic connection logic for each slave here + //mb_port_adapter_slave_connect(it); + port_obj->addr_info.state = MB_SOCK_STATE_CONNECTED; + mb_port_adapter_t *master = (mb_port_adapter_t *)(paddr_info->inst); + port_obj->addr_info.inst = paddr_info->inst; // link slave with master + (void)xQueueSend(master->conn_queue, &port_obj->addr_info, MB_ADAPTER_QUEUE_TIMEOUT); + } +} + +// UART receive event task +static void mb_port_adapter_task(void *p_args) +{ + QueueSetMemberHandle_t active_queue = NULL; + mb_port_adapter_t *it = NULL; + mb_uid_info_t addr_info; + frame_entry_t frame_entry; + + while (1) + { + if (!LIST_EMPTY(&s_port_list)) + { + active_queue = xQueueSelectFromSet(queue_set, MB_ADAPTER_QUEUE_TIMEOUT); + LIST_FOREACH(it, &s_port_list, entries) + { + if (it && active_queue && (it->rx_queue == active_queue)) { + if (xQueuePeek(it->rx_queue, &frame_entry, 0) == pdTRUE) { + it->recv_length = frame_entry.len; + mb_port_evt_post(&it->base, EVENT(EV_FRAME_RECEIVED, frame_entry.len, NULL, 0)); + ESP_LOGW(TAG, "%s, frame %d bytes is ready.", (it->base.descr.parent_name), (int)frame_entry.len); + } + } else if (it && (it->conn_queue == active_queue)) { + if (xQueueReceive(it->conn_queue, &addr_info, MB_ADAPTER_QUEUE_TIMEOUT) == pdTRUE) { + mb_port_adapter_conn_logic(it, &addr_info); + } + } + } + } + else + { + vTaskDelay(1); + } + } + vTaskDelete(NULL); +} + +static mb_err_enum_t mb_port_adapter_connect(mb_tcp_opts_t *tcp_opts, void *pobject) +{ + char **paddr_table = tcp_opts->ip_addr_table; + mb_uid_info_t uid_info; + mb_port_adapter_t *port_obj = __containerof(pobject, mb_port_adapter_t, base); + + MB_RETURN_ON_FALSE((paddr_table && *paddr_table && (tcp_opts->mode == MB_TCP)), + MB_EINVAL, TAG, + "%s, invalid address table.", port_obj->base.descr.parent_name); + int count = 0; + while (*paddr_table) + { + int res = port_scan_addr_string((char *)*paddr_table, &uid_info); + if (res > 0) + { + ESP_LOGW(TAG, "Config: %s, IP: %s, port: %d, slave_addr: %d, ip_ver: %s", + (char *)*paddr_table, uid_info.ip_addr_str, uid_info.port, + uid_info.uid, (uid_info.addr_type == MB_IPV4 ? "IPV4" : "IPV6")); + uid_info.index = count++; + free(uid_info.ip_addr_str); + uid_info.ip_addr_str = (char *)*paddr_table; + uid_info.node_name_str = uid_info.ip_addr_str; + if (xQueueSend(port_obj->conn_queue, &uid_info, MB_EVENT_QUEUE_TIMEOUT_MAX) != pdTRUE) + { + ESP_LOGE(TAG, "can not send info to connection queue."); + }; + // Mimic connection event + if (!tcp_opts->start_disconnected) { + uint16_t event = mb_port_adapter_wait_flag(pobject, MB_QUEUE_FLAG_CONNECTED, MB_ADAPTER_CONN_TIMEOUT); + if (!event) { + ESP_LOGE(TAG, "Could not connect to slave %s during timeout.", (char *)*paddr_table); + } + } + } + else + { + ESP_LOGE(TAG, "unable to open slave: %s, check configuration.", (char *)*paddr_table); + } + paddr_table++; + } + ESP_LOGW(TAG, "parsed and added %d slave configurations.", count); + return count ? MB_ENOERR : MB_EINVAL; +} + +mb_err_enum_t mb_port_adapter_create(mb_uid_info_t *paddr_info, mb_port_base_t **in_out_obj) +{ + mb_port_adapter_t *padapter = NULL; + mb_err_enum_t ret = MB_EILLSTATE; + padapter = (mb_port_adapter_t *)calloc(1, sizeof(mb_port_adapter_t)); + + MB_GOTO_ON_FALSE((padapter && paddr_info && in_out_obj), MB_EILLSTATE, error, TAG, "mb serial port creation error."); + + CRITICAL_SECTION_INIT(padapter->base.lock); + padapter->base.descr = ((mb_port_base_t *)*in_out_obj)->descr; + padapter->addr_info = *paddr_info; + + esp_timer_create_args_t timer_conf = { + .callback = mb_port_adapter_timer_cb, + .arg = padapter, + .dispatch_method = ESP_TIMER_TASK, + .name = padapter->base.descr.parent_name}; + // Create Modbus timer handlers for streams + MB_GOTO_ON_ERROR(esp_timer_create(&timer_conf, &padapter->timer_handle), + error, TAG, "create input stream timer failed."); + + padapter->rx_queue = queue_create(MB_ADAPTER_RX_QUEUE_MAX_SIZE); + MB_GOTO_ON_FALSE(padapter->rx_queue, MB_EILLSTATE, error, TAG, "create rx queue failed"); + padapter->tx_queue = queue_create(MB_ADAPTER_TX_QUEUE_MAX_SIZE); + MB_GOTO_ON_FALSE(padapter->tx_queue, MB_EILLSTATE, error, TAG, "create tx queue failed"); + padapter->event_group_handle = xEventGroupCreate(); + MB_GOTO_ON_FALSE((padapter->event_group_handle), MB_EILLSTATE, error, TAG, + "%p, event group create error.", *in_out_obj); + + if (!s_port_list_counter) + { + // Create a task to handle UART events + BaseType_t status = xTaskCreatePinnedToCore(mb_port_adapter_task, "adapt_rx_task", + MB_ADAPTER_TASK_STACK_SIZE, + &padapter->base, CONFIG_FMB_PORT_TASK_PRIO, + &adapter_task_handle, CONFIG_FMB_PORT_TASK_AFFINITY); + // Force exit from function with failure + MB_GOTO_ON_FALSE((status == pdPASS), MB_EILLSTATE, error, TAG, + "serial task creation error, returned (0x%x).", (int)status); + // Create the queue set to handle clients + queue_set = xQueueCreateSet(MB_ADAPTER_QUEUE_SET_MAX_LEN); + MB_GOTO_ON_FALSE((queue_set), MB_EILLSTATE, error, TAG, "can not create queue set."); + } + // Add connection set for master object only + padapter->conn_queue = xQueueCreate(MB_ADAPTER_MAX_PORTS, sizeof(mb_uid_info_t)); + MB_GOTO_ON_FALSE(padapter->conn_queue, MB_EILLSTATE, error, TAG, "create conn queue failed"); + MB_GOTO_ON_FALSE((queue_set && xQueueAddToSet(padapter->conn_queue, queue_set)), + MB_EILLSTATE, error, TAG, "can not add conn queue to queue set."); + // Add rx queue to set + MB_GOTO_ON_FALSE((queue_set && xQueueAddToSet(padapter->rx_queue, queue_set)), + MB_EILLSTATE, error, TAG, "can not add rx queue to queue set."); + + MB_GOTO_ON_FALSE((s_port_list_counter <= MB_ADAPTER_MAX_PORTS), MB_EILLSTATE, error, + TAG, "adapter exceeded maximum number of ports = %d", MB_ADAPTER_MAX_PORTS); + + // register new port instance in the list + LIST_INSERT_HEAD(&s_port_list, padapter, entries); + s_port_list_counter++; + char *pstr; + int res = asprintf(&pstr, "%d;%s;%u", (unsigned)paddr_info->uid, + padapter->base.descr.parent_name, (unsigned)paddr_info->port); + MB_GOTO_ON_FALSE((res), MB_EILLSTATE, error, + TAG, "object adress info alloc fail, err: %d", (int)res); + padapter->base.cb.tmr_expired = mb_port_adapter_tmr_expired; + padapter->base.cb.tx_empty = NULL; + padapter->base.cb.byte_rcvd = NULL; + padapter->base.arg = (void *)padapter; + + padapter->addr_info.state = MB_SOCK_STATE_CONNECTING; + padapter->addr_info.inst = padapter; + padapter->addr_info.node_name_str = pstr; + padapter->addr_info.ip_addr_str = pstr; + *in_out_obj = &(padapter->base); + ESP_LOGW(TAG, "created object @%p, from parent %p", padapter, padapter->base.descr.parent); + return MB_ENOERR; + +error: + if (padapter) { + mb_port_adapter_delete(&padapter->base); + } + return ret; +} + +mb_err_enum_t mb_port_adapter_tcp_create(mb_tcp_opts_t *tcp_opts, mb_port_base_t **in_out_obj) +{ + mb_uid_info_t addr_info = { + .ip_addr_str = NULL, + .index = s_port_list_counter, + .addr_type = MB_IPV4, + .uid = tcp_opts->uid, + .port = tcp_opts->port, + .proto = MB_TCP, + .state = MB_SOCK_STATE_UNDEF + }; + + mb_port_base_t *pobj = *in_out_obj; + mb_err_enum_t ret = mb_port_adapter_create(&addr_info, &pobj); + + if ((ret == MB_ENOERR) && pobj) { + // Parse master config and register dependent objects + if (pobj->descr.is_master) { + ESP_LOGI(TAG, "Parsing of config for %s", pobj->descr.parent_name); + ret |= mb_port_adapter_connect(tcp_opts, pobj); + MB_GOTO_ON_FALSE((ret == MB_ENOERR), MB_EILLSTATE, error, TAG, + "%s, could not parse config, err=%x.", pobj->descr.parent_name, (int)ret); + } + ESP_LOGW(TAG, "%s, set test time to %" PRIu64, pobj->descr.parent_name, tcp_opts->test_tout_us); + mb_port_adapter_set_response_time(pobj, (tcp_opts->test_tout_us)); + } + *in_out_obj = pobj; + return ret; + +error: + if (pobj) { + mb_port_adapter_delete(pobj); + } + return ret; +} + + +#if (CONFIG_FMB_COMM_MODE_ASCII_EN || CONFIG_FMB_COMM_MODE_RTU_EN) + +mb_err_enum_t mb_port_adapter_ser_create(mb_serial_opts_t *ser_opts, mb_port_base_t **in_out_obj) +{ + mb_uid_info_t addr_info = { + .ip_addr_str = NULL, // unknown + .index = s_port_list_counter, + .addr_type = MB_NOIP, + .uid = ser_opts->uid, + .port = ser_opts->port, + .proto = ser_opts->mode, + .state = MB_SOCK_STATE_UNDEF + }; + + mb_port_base_t *pobj = *in_out_obj; + mb_err_enum_t ret = mb_port_adapter_create(&addr_info, &pobj); + if ((ret == MB_ENOERR) && pobj) { + ESP_LOGW(TAG, "%s, set test time to %d", pobj->descr.parent_name, (int)(ser_opts->test_tout_us)); + mb_port_adapter_set_response_time(pobj, (ser_opts->test_tout_us)); + *in_out_obj = pobj; + } + return ret; +} +#endif + +void mb_port_adapter_delete(mb_port_base_t *inst) +{ + mb_port_adapter_t *port_obj = __containerof(inst, mb_port_adapter_t, base); + + if (port_obj->rx_queue && !queue_is_empty(port_obj->rx_queue)) + { + queue_flush(port_obj->rx_queue); + } + if (port_obj->tx_queue && !queue_is_empty(port_obj->tx_queue)) + { + queue_flush(port_obj->tx_queue); + } + if (port_obj && port_obj->event_group_handle) + { + vEventGroupDelete(port_obj->event_group_handle); + port_obj->event_group_handle = NULL; + } + if (port_obj && port_obj->timer_handle) + { + esp_timer_stop(port_obj->timer_handle); + esp_timer_delete(port_obj->timer_handle); + port_obj->timer_handle = NULL; + } + CRITICAL_SECTION_CLOSE(inst->lock); + LIST_REMOVE(port_obj, entries); + if (s_port_list_counter) + { + atomic_store(&(s_port_list_counter), (s_port_list_counter - 1)); + if (queue_set && port_obj && port_obj->rx_queue) + { + xQueueRemoveFromSet(port_obj->rx_queue, queue_set); + } + if (port_obj && port_obj->conn_queue && queue_set) + { + xQueueRemoveFromSet(port_obj->conn_queue, queue_set); + } + } + if (!s_port_list_counter) + { + if (port_obj && adapter_task_handle) { + vTaskDelete(adapter_task_handle); + adapter_task_handle = NULL; + } + vQueueDelete(queue_set); + queue_set = NULL; + } + if (port_obj && port_obj->rx_queue && port_obj->tx_queue) + { + queue_delete(port_obj->rx_queue); + queue_delete(port_obj->tx_queue); + port_obj->rx_queue = NULL; + port_obj->tx_queue = NULL; + } + if (port_obj && port_obj->conn_queue) + { + vQueueDelete(port_obj->conn_queue); + port_obj->conn_queue = NULL; + } + if (port_obj && port_obj->addr_info.node_name_str) + { + free(port_obj->addr_info.node_name_str); + port_obj->addr_info.node_name_str = NULL; + port_obj->addr_info.ip_addr_str = NULL; + } + free(port_obj); +} + +static esp_err_t mb_port_adapter_set_timer(mb_port_base_t *inst, uint64_t time_diff_us) +{ + mb_port_adapter_t *port_obj = __containerof(inst, mb_port_adapter_t, base); + esp_timer_stop(port_obj->timer_handle); + esp_err_t ret = esp_timer_start_once(port_obj->timer_handle, time_diff_us); + MB_RETURN_ON_FALSE((ret == ESP_OK), + ESP_ERR_INVALID_STATE, TAG, + "%s, could not start timer, err=%x.", inst->descr.parent_name, (int)ret); + return ESP_OK; +} + +bool mb_port_adapter_recv_data(mb_port_base_t *inst, uint8_t **ppframe, uint16_t *plength) +{ + MB_RETURN_ON_FALSE((ppframe && plength), false, TAG, "mb serial get buffer failure."); + mb_port_adapter_t *port_obj = __containerof(inst, mb_port_adapter_t, base); + int length = *plength ? *plength : port_obj->recv_length; + + if (length) + { + CRITICAL_SECTION_LOCK(inst->lock); + int length = queue_pop(port_obj->rx_queue, &port_obj->rx_buffer[0], CONFIG_FMB_BUFFER_SIZE, NULL); + if (length) + { + mb_port_tmr_disable(inst); + ESP_LOGW(TAG, "%s, received data: %d bytes.", inst->descr.parent_name, length); + // Stop timer because the new data is received + // Store the timestamp of received frame + port_obj->recv_time_stamp = esp_timer_get_time(); + *ppframe = &port_obj->rx_buffer[0]; + ESP_LOG_BUFFER_HEX_LEVEL(MB_STR_CAT(inst->descr.parent_name, ":PORT_RECV"), + (void *)&port_obj->rx_buffer[0], (uint16_t)length, ESP_LOG_WARN); + } + CRITICAL_SECTION_UNLOCK(inst->lock); + } + else + { + ESP_LOGE(TAG, "%s: junk data (%d bytes) received. ", inst->descr.parent_name, length); + } + *plength = length; + return true; +} + +bool mb_port_adapter_send_data(mb_port_base_t *inst, uint8_t address, uint8_t *pframe, uint16_t length) +{ + bool res = false; + mb_port_adapter_t *port_obj = __containerof(inst, mb_port_adapter_t, base); + uint64_t time_diff = atomic_load(&port_obj->test_timeout_us); + + if (pframe && length) + { + CRITICAL_SECTION_LOCK(inst->lock); + esp_err_t err = queue_push(port_obj->tx_queue, (void *)pframe, length, NULL); + CRITICAL_SECTION_UNLOCK(inst->lock); + MB_RETURN_ON_FALSE((err == ESP_OK), + false, TAG, "%s, could not send the data into queue.", inst->descr.parent_name); + MB_RETURN_ON_FALSE((mb_port_adapter_set_timer(inst, time_diff) == ESP_OK), + false, TAG, "%s, could not set output timer.", inst->descr.parent_name); + // Wait for send buffer complition + uint16_t flags = mb_port_adapter_wait_flag(inst, MB_QUEUE_FLAG_SENT, MB_EVENT_QUEUE_TIMEOUT_MAX); + port_obj->send_time_stamp = esp_timer_get_time(); + // Print sent packet, the tag used is more clear to see + ESP_LOG_BUFFER_HEX_LEVEL(MB_STR_CAT(inst->descr.parent_name, ":PORT_SEND"), + (void *)pframe, length, ESP_LOG_WARN); + (void)mb_port_evt_post(inst, EVENT(EV_FRAME_SENT)); + ESP_LOGW(TAG, "%s, tx completed, flags = 0x%04x.", inst->descr.parent_name, (int)flags); + res = true; + + } + else + { + ESP_LOGE(TAG, "send callback %p, %u. ", pframe, (unsigned)length); + } + return res; +} + +void mb_port_adapter_enable(mb_port_base_t *inst) +{ + ESP_LOGD(TAG, "adapter tcp enable port."); +} + +void mb_port_adapter_disable(mb_port_base_t *inst) +{ + ESP_LOGD(TAG, "adapter tcp disable port."); +} + +mb_uid_info_t *mb_port_adapter_get_slave_info(mb_port_base_t *inst, uint8_t slave_addr, mb_sock_state_t exp_state) +{ + mb_port_adapter_t *it = NULL; + + if (!LIST_EMPTY(&s_port_list)) + { + LIST_FOREACH(it, &s_port_list, entries) + { + if ((it->addr_info.uid == slave_addr) && (it->base.descr.is_master == false)) + { + return (&it->addr_info); + } + } + } + return NULL; +} + +void mb_port_adapter_tcp_set_conn_cb(mb_port_base_t *inst, void *conn_fp, void *arg) +{ + void (*on_conn_done_cb)(void *) = conn_fp; + + if (mb_port_adapter_is_connected(inst)) { + if (on_conn_done_cb && arg) { + on_conn_done_cb(arg); + } + } +} + +#ifdef __cplusplus +} +#endif diff --git a/test_apps/test_common/mb_utest_lib/port_adapter.h b/test_apps/test_common/mb_utest_lib/port_adapter.h new file mode 100644 index 0000000..e1a7c33 --- /dev/null +++ b/test_apps/test_common/mb_utest_lib/port_adapter.h @@ -0,0 +1,47 @@ +/* + * SPDX-FileCopyrightText: 2018-2021 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include "esp_log.h" +#include "mb_common.h" +#include + +#include "port_tcp_utils.h" +#include "port_common.h" + +typedef enum +{ + MB_QUEUE_FLAG_EMPTY = 0x0000, + MB_QUEUE_FLAG_SENT = 0x0001, + MB_QUEUE_FLAG_RECV = 0x0002, + MB_QUEUE_FLAG_CONNECTED = 0x0004 +} mb_queue_flags_t; + +#define MB_QUEUE_FLAGS (MB_QUEUE_FLAG_SENT | MB_QUEUE_FLAG_RECV | MB_QUEUE_FLAG_CONNECTED) + +typedef struct _uid_info mb_uid_info_t; + +#if (CONFIG_FMB_COMM_MODE_ASCII_EN || CONFIG_FMB_COMM_MODE_RTU_EN) +mb_err_enum_t mb_port_adapter_ser_create(mb_serial_opts_t *ser_opts, mb_port_base_t **in_out_obj); +#endif + +#if (CONFIG_FMB_COMM_MODE_TCP_EN) +mb_err_enum_t mb_port_adapter_tcp_create(mb_tcp_opts_t *tcp_opts, mb_port_base_t **in_out_obj); +#endif + +void mb_port_adapter_delete(mb_port_base_t *inst); +void mb_port_adapter_set_response_time(mb_port_base_t *inst, uint64_t resp_time); +int mb_port_adapter_get_rx_buffer(mb_port_base_t *inst, uint8_t **ppfame, int *plength); + +bool mb_port_adapter_send_data(mb_port_base_t *inst, uint8_t address, uint8_t *pframe, uint16_t length); +bool mb_port_adapter_recv_data(mb_port_base_t *inst, uint8_t **ppframe, uint16_t *plength); +void mb_port_adapter_enable(mb_port_base_t *inst); +void mb_port_adapter_disable(mb_port_base_t *inst); +void mb_port_adapter_tcp_set_conn_cb(mb_port_base_t *inst, void *conn_fp, void *arg); +void mb_port_adapter_tcp_set_conn_time(mb_port_base_t *inst, void *conn_fp, void *arg); +mb_uid_info_t *mb_port_adapter_get_slave_info(mb_port_base_t *inst, uint8_t slave_addr, mb_sock_state_t exp_state); diff --git a/test_apps/test_common/mb_utest_lib/port_stubs.c b/test_apps/test_common/mb_utest_lib/port_stubs.c new file mode 100644 index 0000000..b2f1295 --- /dev/null +++ b/test_apps/test_common/mb_utest_lib/port_stubs.c @@ -0,0 +1,173 @@ +/* + * SPDX-FileCopyrightText: 2018-2021 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include "freertos/FreeRTOS.h" +#include "freertos/queue.h" + +#include "esp_timer.h" +#include "sdkconfig.h" +#include "esp_log.h" +#include "esp_err.h" + +#include "mb_common.h" +#include "esp_modbus_common.h" +#include "mbc_slave.h" + +#include "mb_common.h" +#include "port_common.h" +#include "mb_config.h" +#include "port_serial_common.h" +#include "port_tcp_common.h" +#include "port_adapter.h" +#include "port_stubs.h" + +#ifdef __cplusplus +extern "C" { +#endif + +static const char *TAG = "port_stub"; + +// Below are function wrappers to substitute actual port object with the adapter object for test purpose + +#if (CONFIG_FMB_COMM_MODE_ASCII_EN || CONFIG_FMB_COMM_MODE_RTU_EN) + +mb_err_enum_t __wrap_mb_port_ser_create(mb_serial_opts_t *ser_opts, mb_port_base_t **in_out_obj) +{ + return mb_port_adapter_ser_create(ser_opts, in_out_obj); +} + +void __wrap_mb_port_ser_delete(mb_port_base_t *inst) +{ + mb_port_adapter_delete(inst); +} + +bool __wrap_mb_port_ser_recv_data(mb_port_base_t *inst, uint8_t **ppframe, uint16_t *plength) +{ + return mb_port_adapter_recv_data(inst, ppframe, plength); +} + +bool __wrap_mb_port_ser_send_data(mb_port_base_t *inst, uint8_t *pframe, uint16_t length) +{ + return mb_port_adapter_send_data(inst, 0, pframe, length); +} + +void __wrap_mb_port_ser_enable(mb_port_base_t *inst) +{ + mb_port_adapter_enable(inst); +} + +void __wrap_mb_port_ser_disable(mb_port_base_t *inst) +{ + mb_port_adapter_disable(inst); +} + +#endif + +IRAM_ATTR +bool __wrap_mb_port_evt_get(mb_port_base_t *inst, mb_event_t *pevent) +{ + bool result = __real_mb_port_evt_get(inst, pevent); + ESP_LOGW(TAG, "%s, get event:%x.", inst->descr.parent_name, pevent->event); + return result; +} + +IRAM_ATTR +bool __wrap_mb_port_evt_post(mb_port_base_t *inst, mb_event_t event) +{ + bool result = __real_mb_port_evt_post(inst, event); + return result; +} + +#if (CONFIG_FMB_COMM_MODE_TCP_EN) + +// Below are the TCP port function wrappers to exchange the port layer to TCP adapter +mb_err_enum_t __wrap_mbm_port_tcp_create(mb_tcp_opts_t *tcp_opts, mb_port_base_t **in_out_obj) +{ + ESP_LOGW(TAG, "master tcp adapter installed."); + return mb_port_adapter_tcp_create(tcp_opts, in_out_obj); +} + +bool __wrap_mbm_port_tcp_recv_data(mb_port_base_t *inst, uint8_t **ppframe, uint16_t *plength) +{ + return mb_port_adapter_recv_data(inst, ppframe, plength); +} + +bool __wrap_mbm_port_tcp_send_data(mb_port_base_t *inst, uint8_t address, uint8_t *pframe, uint16_t length) +{ + return mb_port_adapter_send_data(inst, address, pframe, length); +} + +void __wrap_mbm_port_tcp_delete(mb_port_base_t *inst) +{ + mb_port_adapter_delete(inst); +} + +void __wrap_mbm_port_tcp_enable(mb_port_base_t *inst) +{ + ESP_LOGW(TAG, "adapter master tcp enable port."); +} + +void __wrap_mbm_port_tcp_disable(mb_port_base_t *inst) +{ + ESP_LOGW(TAG, "adapter master tcp disable port."); +} + +void __wrap_mbm_port_tcp_set_conn_cb(mb_port_base_t *inst, void *conn_fp, void *arg) +{ + ESP_LOGW(TAG, "adapter set connection callback."); + mb_port_adapter_tcp_set_conn_cb(inst, conn_fp, arg); +} + +mb_uid_info_t *__wrap_mbm_port_tcp_get_slave_info(mb_port_base_t *inst, uint8_t slave_addr, mb_sock_state_t exp_state) +{ + ESP_LOGW(TAG, "adapter get slave #%d info.", slave_addr); + return mb_port_adapter_get_slave_info(inst, slave_addr, exp_state); +} + +// IRAM_ATTR +// bool __wrap_mbm_port_timer_expired(void *inst) +// { +// return mb_port_adapter_tmr_expired(inst); +// } + +// Wrappers for modbus slave tcp + +mb_err_enum_t __wrap_mbs_port_tcp_create(mb_tcp_opts_t *tcp_opts, mb_port_base_t **in_out_obj) +{ + ESP_LOGW(TAG, "install slave tcp adapter."); + return mb_port_adapter_tcp_create(tcp_opts, in_out_obj); +} + +bool __wrap_mbs_port_tcp_recv_data(mb_port_base_t *inst, uint8_t **ppframe, uint16_t *plength) +{ + return mb_port_adapter_recv_data(inst, ppframe, plength); +} + +bool __wrap_mbs_port_tcp_send_data(mb_port_base_t *inst, uint8_t *pframe, uint16_t length) +{ + return mb_port_adapter_send_data(inst, 0, pframe, length); +} + +void __wrap_mbs_port_tcp_delete(mb_port_base_t *inst) +{ + mb_port_adapter_delete(inst); +} + +void __wrap_mbs_port_tcp_enable(mb_port_base_t *inst) +{ + ESP_LOGW(TAG, "adapter slave tcp enable port."); +} + +void __wrap_mbs_port_tcp_disable(mb_port_base_t *inst) +{ + ESP_LOGW(TAG, "adapter slave tcp disable port."); +} + +#endif + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/test_apps/test_common/mb_utest_lib/port_stubs.h b/test_apps/test_common/mb_utest_lib/port_stubs.h new file mode 100644 index 0000000..70c919e --- /dev/null +++ b/test_apps/test_common/mb_utest_lib/port_stubs.h @@ -0,0 +1,70 @@ +/* + * SPDX-FileCopyrightText: 2018-2021 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include "esp_log.h" +#include "mb_common.h" +#include "mb_port_types.h" + +// Serial port function wrappers + +bool __wrap_mb_port_evt_get(mb_port_base_t *inst, mb_event_t *pevent); +bool __wrap_mb_port_evt_post(mb_port_base_t *inst, mb_event_t event); +extern bool __real_mb_port_evt_get(mb_port_base_t *inst, mb_event_t *pevent); +extern bool __real_mb_port_evt_post(mb_port_base_t *inst, mb_event_t event); + +#if (CONFIG_FMB_COMM_MODE_ASCII_EN || CONFIG_FMB_COMM_MODE_RTU_EN) + +extern void __real_mb_port_ser_enable(mb_port_base_t *inst); +extern void __real_mb_port_ser_disable(mb_port_base_t *inst); +extern bool __real_mb_port_ser_send_data(mb_port_base_t *inst, uint8_t *p_ser_frame, uint16_t ser_length); +extern bool __real_mb_port_ser_recv_data(mb_port_base_t *inst, uint8_t **pp_ser_frame, uint16_t *p_ser_length); +extern void __real_mb_port_ser_delete(mb_port_base_t *inst); + +mb_err_enum_t __wrap_mb_port_ser_create(mb_serial_opts_t *ser_opts, mb_port_base_t **in_out_obj); +void __wrap_mb_port_ser_enable(mb_port_base_t *inst); +void __wrap_mb_port_ser_disable(mb_port_base_t *inst); +bool __wrap_mb_port_ser_send_data(mb_port_base_t *inst, uint8_t *p_ser_frame, uint16_t ser_length); +bool __wrap_mb_port_ser_recv_data(mb_port_base_t *inst, uint8_t **pp_ser_frame, uint16_t *p_ser_length); +void __wrap_mb_port_ser_delete(mb_port_base_t *inst); + +#endif + +// TCP port function wrappers + +mb_err_enum_t __wrap_mbm_port_tcp_create(mb_tcp_opts_t *tcp_opts, mb_port_base_t **in_out_obj); +void __wrap_mbm_port_tcp_delete(mb_port_base_t *inst); + +bool __wrap_mbm_port_tcp_send_data(mb_port_base_t *inst, uint8_t address, uint8_t *pframe, uint16_t length); +bool __wrap_mbm_port_tcp_recv_data(mb_port_base_t *inst, uint8_t **ppframe, uint16_t *plength); +void __wrap_mbm_port_tcp_enable(mb_port_base_t *inst); +void __wrap_mbm_port_tcp_disable(mb_port_base_t *inst); +void __wrap_mbm_port_tcp_set_conn_cb(mb_port_base_t *inst, void *conn_fp, void *arg); +mb_uid_info_t *__wrap_mbm_port_tcp_get_slave_info(mb_port_base_t *inst, uint8_t uid, mb_sock_state_t exp_state); +//bool __wrap_mbm_port_timer_expired(void *inst); + +extern void __real_mbm_port_tcp_enable(mb_port_base_t *inst); +extern void __real_mbm_port_tcp_disable(mb_port_base_t *inst); +extern void __real_mbm_port_tcp_set_conn_cb(mb_port_base_t *inst, void *conn_fp, void *arg); +extern mb_uid_info_t *__real_mbm_port_tcp_get_slave_info(mb_port_base_t *inst, uint8_t uid, mb_sock_state_t exp_state); +extern bool __real_mbm_port_tcp_send_data(mb_port_base_t *inst, uint8_t address, uint8_t *pframe, uint16_t length); +extern bool __real_mbm_port_tcp_recv_data(mb_port_base_t *inst, uint8_t **ppframe, uint16_t *plength); + +mb_err_enum_t __wrap_mbs_port_tcp_create(mb_tcp_opts_t *tcp_opts, mb_port_base_t **in_out_obj); +void __wrap_mbs_port_tcp_delete(mb_port_base_t *inst); +bool __wrap_mbs_port_tcp_send_data(mb_port_base_t *inst, uint8_t *pframe, uint16_t length); +bool __wrap_mbs_port_tcp_recv_data(mb_port_base_t *inst, uint8_t **ppframe, uint16_t *plength); +void __wrap_mbs_port_tcp_enable(mb_port_base_t *inst); +void __wrap_mbs_port_tcp_disable(mb_port_base_t *inst); + +mb_err_enum_t __real_mbs_port_tcp_create(mb_tcp_opts_t *tcp_opts, mb_port_base_t **in_out_obj); +extern void __real_mbs_port_tcp_delete(mb_port_base_t *inst); +extern bool __real_mbs_port_tcp_send_data(mb_port_base_t *inst, uint8_t *pframe, uint16_t length); +extern bool __real_mbs_port_tcp_recv_data(mb_port_base_t *inst, uint8_t **ppframe, uint16_t *plength); +extern void __real_mbs_port_tcp_enable(mb_port_base_t *inst); +extern void __real_mbs_port_tcp_disable(mb_port_base_t *inst); \ No newline at end of file diff --git a/test_apps/test_common/sdkconfig.defaults b/test_apps/test_common/sdkconfig.defaults new file mode 100644 index 0000000..241fb10 --- /dev/null +++ b/test_apps/test_common/sdkconfig.defaults @@ -0,0 +1,21 @@ +# This file was generated using idf.py save-defconfig. It can be edited manually. +# Espressif IoT Development Framework (ESP-IDF) Project Minimal Configuration +# +# +# Modbus configuration +# +CONFIG_UNITY_ENABLE_FIXTURE=y +CONFIG_FMB_PORT_TASK_STACK_SIZE=4096 +CONFIG_FMB_PORT_TASK_PRIO=10 +CONFIG_FMB_COMM_MODE_RTU_EN=y +CONFIG_FMB_COMM_MODE_ASCII_EN=y +CONFIG_FMB_COMM_MODE_TCP_EN=y +CONFIG_FMB_TCP_UID_ENABLED=y +CONFIG_FMB_MASTER_TIMEOUT_MS_RESPOND=2000 +CONFIG_FMB_MASTER_DELAY_MS_CONVERT=300 +CONFIG_FMB_TIMER_USE_ISR_DISPATCH_METHOD=y +CONFIG_MB_PORT_ADAPTER_EN=n +CONFIG_MB_TEST_MASTER_TASK_PRIO=4 +CONFIG_MB_TEST_SLAVE_TASK_PRIO=4 +CONFIG_MB_TEST_COMM_CYCLE_COUNTER=10 + diff --git a/test_apps/test_common/test_common.c b/test_apps/test_common/test_common.c new file mode 100644 index 0000000..ecaff6d --- /dev/null +++ b/test_apps/test_common/test_common.c @@ -0,0 +1,527 @@ +/* + * SPDX-FileCopyrightText: 2018-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include "freertos/FreeRTOS.h" +#include "freertos/portmacro.h" +#include "freertos/queue.h" + +#include "port_adapter.h" +#include "mb_common.h" +#include "mbc_slave.h" +#include "mbc_master.h" + +#include "test_common.h" +#include "esp_heap_caps.h" + +#include "sdkconfig.h" + +#ifdef CONFIG_HEAP_TRACING +#include "esp_heap_trace.h" +#endif + +#define TEST_TASK_PRIO_MASTER CONFIG_MB_TEST_MASTER_TASK_PRIO +#define TEST_TASK_PRIO_SLAVE CONFIG_MB_TEST_SLAVE_TASK_PRIO +#define TEST_TASK_STACK_SIZE 5120 +#define TEST_TASK_CYCLE_COUNTER CONFIG_MB_TEST_COMM_CYCLE_COUNTER +#define TEST_BUSY_TASK_PRIO 20 + +#define TEST_REG_START_AREA0 (0x0000) +#define TEST_READ_MASK (MB_EVENT_HOLDING_REG_RD | MB_EVENT_INPUT_REG_RD | MB_EVENT_DISCRETE_RD | MB_EVENT_COILS_RD) +#define TEST_WRITE_MASK (MB_EVENT_HOLDING_REG_WR | MB_EVENT_COILS_WR) +#define TEST_READ_WRITE_MASK (TEST_WRITE_MASK | TEST_READ_MASK) + +#define TEST_BUSY_COUNT 150000 + +#define TEST_PAR_INFO_GET_TOUT (10) +#define TEST_SEND_TIMEOUT (200 / portTICK_PERIOD_MS) +#define TEST_TASK_START_TIMEOUT (5000 / portTICK_PERIOD_MS) + +#define TEST_NOTIF_SEND_TOUT 100 +#define TEST_NOTIF_SIZE 20 +#define TEST_ALLOW_PROC_FAIL 2 // percentage of allowed failures + +#define TEST_TASK_TICK_TIME (20 / portTICK_PERIOD_MS) + +#define TAG "TEST_COMMON" + +typedef enum { + RT_HOLDING_RD, + RT_HOLDING_WR +} mb_access_t; + +static uint16_t holding_registers[16] = {0}; +static uint16_t input_registers[8] = {0}; +static uint16_t coil_registers[10] = {0}; + +const uint16_t holding_registers_counter = (sizeof(holding_registers) / sizeof(holding_registers[0])); +const uint16_t input_registers_counter = (sizeof(input_registers) / sizeof(input_registers[0])); +const uint16_t coil_registers_counter = (sizeof(coil_registers) / sizeof(coil_registers[0])); + +static QueueHandle_t tasks_done_queue = NULL; +static int test_error_counter = 0; +static int test_good_counter = 0; + +static size_t before_free_8bit = 0; +static size_t before_free_32bit = 0; + +#ifdef CONFIG_HEAP_TRACING +#define NUM_RECORDS 200 +static heap_trace_record_t trace_record[NUM_RECORDS]; +#endif + +#define CHECK_PAR_VALUE(par, err, value, expected) \ + do \ + { \ + if ((err != ESP_OK) || (((uint16_t)value) != ((uint16_t)expected))) \ + { \ + ESP_LOGE(TAG, "CHAR #%u, value: 0x%" PRIx16 ", expected: 0x%" PRIx16 ", error = %d.", \ + (unsigned)par, ((uint16_t)value), ((uint16_t)expected), (int)err); \ + TEST_ASSERT((++test_error_counter * 100 / (TEST_TASK_CYCLE_COUNTER * CID_DEV_REG_COUNT)) <= TEST_ALLOW_PROC_FAIL); \ + } \ + else \ + { \ + ESP_LOGI(TAG, "CHAR #%u, value is ok.", (unsigned)par); \ + test_good_counter++; \ + } \ + } while (0) + +static void test_busy_task(void *phandle) +{ + spinlock_t spin_lock; + SPIN_LOCK_INIT(spin_lock); + ESP_EARLY_LOGW(TAG, "test task"); + while(1) { + SPIN_LOCK_ENTER(spin_lock); + for (int i = 0; i < TEST_BUSY_COUNT; i++){ + ; + } + SPIN_LOCK_EXIT(spin_lock); + vTaskDelay(1); + } +} + +// Start the high priority task to mimic the case when the modbus +// tasks do not get time quota from RTOS. +TaskHandle_t test_slave_start_busy_task() +{ + TaskHandle_t busy_task_handle = NULL; + TEST_ASSERT_TRUE(xTaskCreatePinnedToCore(test_busy_task, "busy_task", + TEST_TASK_STACK_SIZE, + NULL, TEST_BUSY_TASK_PRIO, + &busy_task_handle, MB_PORT_TASK_AFFINITY)); + return busy_task_handle; +} + +void test_slave_stop_busy_task(TaskHandle_t busy_task_handle) +{ + vTaskDelete(busy_task_handle); +} + +void test_common_start() +{ +#ifdef CONFIG_HEAP_TRACING + ESP_ERROR_CHECK( heap_trace_init_standalone(trace_record, NUM_RECORDS) ); +#endif + + before_free_8bit = heap_caps_get_free_size(MALLOC_CAP_8BIT); + before_free_32bit = heap_caps_get_free_size(MALLOC_CAP_32BIT); + +#ifdef CONFIG_HEAP_TRACING + ESP_ERROR_CHECK( heap_trace_start(HEAP_TRACE_LEAKS) ); +#endif + + tasks_done_queue = xQueueCreate(TEST_NOTIF_SIZE, sizeof(uint32_t)); +} + +uint32_t test_common_wait_done(TickType_t timeout_ticks) +{ + uint32_t tmp_val = 0; + + if (xQueueReceive(tasks_done_queue, &tmp_val, timeout_ticks) == pdTRUE) { + return tmp_val; + } + return 0; +} + +static uint32_t test_comon_notify_done(uint32_t value) +{ + uint32_t tmp_val = value; + return (uint32_t)xQueueSend(tasks_done_queue, (const void*)&tmp_val, TEST_NOTIF_SEND_TOUT); +} + +static uint32_t test_common_task_wait_start(TickType_t timeout_ticks) +{ + static uint32_t notify_value = 0; + + if (xTaskNotifyWait(0, 0, ¬ify_value, timeout_ticks) == pdTRUE) { + ESP_LOGW(TAG, "Task: 0x%" PRIx32 ", get notify value = %d", + (uint32_t)xTaskGetCurrentTaskHandle(), (int)notify_value); + return pdTRUE; + } + return 0; +} + +void test_common_task_notify_start(TaskHandle_t task_handle, uint32_t value) +{ + ESP_LOGW(TAG, "Notify task 0x%" PRIx32, (uint32_t)task_handle); + TEST_ASSERT_EQUAL_INT(xTaskNotify(task_handle, value, eSetValueWithOverwrite), pdTRUE); +} + +void test_common_stop() +{ + vQueueDelete(tasks_done_queue); + holding_registers[CID_DEV_REG_COUNT] = 0; + test_error_counter = 0; + test_good_counter = 0; + + /* check if unit test has caused heap corruption in any heap */ + TEST_ASSERT_MESSAGE(heap_caps_check_integrity(MALLOC_CAP_INVALID, true), "The test has corrupted the heap"); + +#ifdef CONFIG_HEAP_TRACING + ESP_ERROR_CHECK( heap_trace_stop() ); + heap_trace_dump(); +#endif + + size_t after_free_8bit = heap_caps_get_free_size(MALLOC_CAP_8BIT); + size_t after_free_32bit = heap_caps_get_free_size(MALLOC_CAP_32BIT); + test_common_check_leak(before_free_8bit, after_free_8bit, "8BIT", + CONFIG_MB_TEST_LEAK_WARN_LEVEL, CONFIG_MB_TEST_LEAK_CRITICAL_LEVEL); + test_common_check_leak(before_free_32bit, after_free_32bit, "32BIT", + CONFIG_MB_TEST_LEAK_WARN_LEVEL, CONFIG_MB_TEST_LEAK_CRITICAL_LEVEL); +} + +void test_common_check_leak(size_t before_free, + size_t after_free, + const char *type, + size_t warn_threshold, + size_t critical_threshold) +{ + int free_delta = (int)after_free - (int)before_free; + printf("MALLOC_CAP_%s usage: Free memory delta: %d Leak threshold: -%u \n", + type, + free_delta, + critical_threshold); + + if (free_delta > 0) { + return; // free memory went up somehow + } + + size_t leaked = (size_t)(free_delta * -1); + if (leaked <= warn_threshold) { + return; + } + + printf("MALLOC_CAP_%s %s leak: Before %u bytes free, After %u bytes free (delta %u)\n", + type, + leaked <= critical_threshold ? "potential" : "critical", + before_free, after_free, leaked); + fflush(stdout); + TEST_ASSERT_MESSAGE(leaked <= critical_threshold, "The test leaked too much memory"); +} + +// Helper function to read one characteristic from slave +esp_err_t read_modbus_parameter(void *handle, uint16_t cid, uint16_t *par_data) +{ + const mb_parameter_descriptor_t *param_descriptor = NULL; + + 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 (err == ESP_OK) + { + ESP_LOGI(TAG, "%p, CHAR #%u %s (%s) value = (0x%04x) parameter read successful.", + handle, + param_descriptor->cid, + param_descriptor->param_key, + param_descriptor->param_units, + *(uint16_t *)par_data); + } + else + { + ESP_LOGE(TAG, "%p, CHAR #%u (%s) read fail, err = 0x%x (%s).", + handle, + param_descriptor->cid, + param_descriptor->param_key, + (int)err, + (char *)esp_err_to_name(err)); + } + } + return err; +} + +// Helper function to write one characteristic to slave +esp_err_t write_modbus_parameter(void *handle, uint16_t cid, uint16_t *par_data) +{ + const mb_parameter_descriptor_t *param_descriptor = NULL; + + 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; // type of parameter from dictionary + err = mbc_master_set_parameter(handle, cid, (uint8_t *)par_data, &type); + if (err == ESP_OK) + { + ESP_LOGI(TAG, "%p, CHAR #%u %s (%s) value = (0x%04x), write successful.", + handle, + param_descriptor->cid, + param_descriptor->param_key, + param_descriptor->param_units, + *(uint16_t *)par_data); + } + else + { + ESP_LOGE(TAG, "%p, CHAR #%u (%s) write fail, err = 0x%x (%s).", + handle, + param_descriptor->cid, + param_descriptor->param_key, + (int)err, + (char *)esp_err_to_name(err)); + } + } + return err; +} + +// This is user function to read and write modbus holding registers +static void test_master_task(void *arg) +{ + void *mbm_handle = arg; + //mbm_controller_iface_t *pctrl_obj = ((mbm_controller_iface_t *)mbm_handle); + + static mb_access_t req_type = RT_HOLDING_RD; + esp_err_t err = ESP_FAIL; + uint16_t cycle_counter = 0; + + // Wait task start notification during timeout + test_common_task_wait_start(TEST_TASK_START_TIMEOUT); + + holding_registers[CID_DEV_REG0] = TEST_REG_VAL1; + holding_registers[CID_DEV_REG1] = TEST_REG_VAL2; + holding_registers[CID_DEV_REG2] = TEST_REG_VAL3; + holding_registers[CID_DEV_REG3] = TEST_REG_VAL4; + holding_registers[CID_DEV_REG_COUNT] = cycle_counter; + write_modbus_parameter(mbm_handle, CID_DEV_REG0, &holding_registers[CID_DEV_REG0]); + write_modbus_parameter(mbm_handle, CID_DEV_REG1, &holding_registers[CID_DEV_REG1]); + write_modbus_parameter(mbm_handle, CID_DEV_REG2, &holding_registers[CID_DEV_REG2]); + write_modbus_parameter(mbm_handle, CID_DEV_REG3, &holding_registers[CID_DEV_REG3]); + for (cycle_counter = 0; cycle_counter <= TEST_TASK_CYCLE_COUNTER; cycle_counter++) + { + switch (req_type) + { + case RT_HOLDING_RD: + err = 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); + + err = read_modbus_parameter(mbm_handle, CID_DEV_REG1, &holding_registers[CID_DEV_REG1]); + CHECK_PAR_VALUE(CID_DEV_REG1, err, holding_registers[CID_DEV_REG1], TEST_REG_VAL2); + + err = read_modbus_parameter(mbm_handle, CID_DEV_REG2, &holding_registers[CID_DEV_REG2]); + CHECK_PAR_VALUE(CID_DEV_REG2, err, holding_registers[CID_DEV_REG2], TEST_REG_VAL3); + + err = read_modbus_parameter(mbm_handle, CID_DEV_REG3, &holding_registers[CID_DEV_REG3]); + CHECK_PAR_VALUE(CID_DEV_REG3, err, holding_registers[CID_DEV_REG3], TEST_REG_VAL4); + req_type = RT_HOLDING_WR; + break; + + case RT_HOLDING_WR: + err = write_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); + + err = write_modbus_parameter(mbm_handle, CID_DEV_REG1, &holding_registers[CID_DEV_REG1]); + CHECK_PAR_VALUE(CID_DEV_REG1, err, holding_registers[CID_DEV_REG1], TEST_REG_VAL2); + + err = write_modbus_parameter(mbm_handle, CID_DEV_REG2, &holding_registers[CID_DEV_REG2]); + CHECK_PAR_VALUE(CID_DEV_REG2, err, holding_registers[CID_DEV_REG2], TEST_REG_VAL3); + + err = write_modbus_parameter(mbm_handle, CID_DEV_REG3, &holding_registers[CID_DEV_REG3]); + CHECK_PAR_VALUE(CID_DEV_REG3, err, holding_registers[CID_DEV_REG3], TEST_REG_VAL4); + req_type = RT_HOLDING_RD; + break; + + default: + break; + } + write_modbus_parameter(mbm_handle, CID_DEV_REG_COUNT, &cycle_counter); + vTaskDelay(TEST_TASK_TICK_TIME); // Let the IDLE task to trigger + if (holding_registers[CID_DEV_REG_COUNT] >= TEST_TASK_CYCLE_COUNTER) { + ESP_LOGI(TAG, "Stop master: %p.", mbm_handle); + break; + } + } + ESP_LOGI(TAG, "Destroy master, inst: %p.", mbm_handle); + TEST_ESP_OK(mbc_master_delete(mbm_handle)); + test_comon_notify_done((uint32_t)xTaskGetCurrentTaskHandle()); + vTaskSuspend(NULL); +} + +static void test_slave_task(void *arg) +{ + void *mbs_handle = arg; + mbs_controller_iface_t *pctrl_obj = ((mbs_controller_iface_t *)mbs_handle); + mb_param_info_t reg_info; // keeps the Modbus registers access information + + test_common_task_wait_start(TEST_TASK_START_TIMEOUT); + + while(1) { + // Get parameter information from parameter queue + esp_err_t err = mbc_slave_get_param_info(mbs_handle, ®_info, TEST_PAR_INFO_GET_TOUT); + const char *rw_str = (reg_info.type & TEST_READ_MASK) ? "READ" : "WRITE"; + + // Filter events and process them accordingly + if ((err != ESP_ERR_TIMEOUT) && (reg_info.type & TEST_READ_WRITE_MASK)) + { + // Get parameter information from parameter queue + ESP_LOGI("SLAVE", "OBJ %p, %s (%" PRIu32 " us), SL: %u, REG:%u, TYPE:%u, INST_ADDR:0x%" PRIx32 "(0x%" PRIx16 "), SIZE:%u", + (void *)pctrl_obj->mb_base->descr.parent, + rw_str, + (uint32_t)reg_info.time_stamp, + (unsigned)pctrl_obj->opts.comm_opts.common_opts.uid, + (unsigned)reg_info.mb_offset, + (unsigned)reg_info.type, + (uint32_t)reg_info.address, + *(uint16_t *)reg_info.address, + (unsigned)reg_info.size); + } + vTaskDelay(TEST_TASK_TICK_TIME); // Let IDLE task to trigger + if (holding_registers[CID_DEV_REG_COUNT] >= TEST_TASK_CYCLE_COUNTER) { + ESP_LOGI(TAG, "Stop slave: %p.", mbs_handle); + vTaskDelay(TEST_TASK_TICK_TIME); // Let master to get response from slave prior to close + break; + } + } + ESP_LOGI(TAG, "Destroy slave, inst: %p.", mbs_handle); + TEST_ESP_OK(mbc_slave_delete(mbs_handle)); + test_comon_notify_done((uint32_t)xTaskGetCurrentTaskHandle()); + vTaskSuspend(NULL); +} + +void test_slave_setup_start(void *mbs_handle) +{ + TEST_ASSERT_TRUE(mbs_handle); + mb_register_area_descriptor_t reg_area; + + reg_area.type = MB_PARAM_HOLDING; + reg_area.start_offset = TEST_REG_START_AREA0; + reg_area.address = (void *)&holding_registers[CID_DEV_REG0]; + reg_area.size = holding_registers_counter << 1; + TEST_ESP_OK(mbc_slave_set_descriptor(mbs_handle, reg_area)); + + reg_area.type = MB_PARAM_INPUT; + reg_area.start_offset = TEST_REG_START_AREA0; + reg_area.address = (void *)&input_registers[CID_DEV_REG0]; + reg_area.size = input_registers_counter << 1; + TEST_ESP_OK(mbc_slave_set_descriptor(mbs_handle, reg_area)); + + reg_area.type = MB_PARAM_COIL; + reg_area.start_offset = TEST_REG_START_AREA0; + reg_area.address = (void *)&coil_registers[CID_DEV_REG0]; + reg_area.size = coil_registers_counter; + TEST_ESP_OK(mbc_slave_set_descriptor(mbs_handle, reg_area)); + TEST_ESP_OK(mbc_slave_start(mbs_handle)); +} + +#if (CONFIG_FMB_COMM_MODE_RTU_EN || CONFIG_FMB_COMM_MODE_ASCII_EN) + +TaskHandle_t test_master_serial_create(mb_communication_info_t *pconfig, const mb_parameter_descriptor_t *pdescr, uint16_t descr_size) +{ + if (!pconfig || !pdescr) { + ESP_LOGI(TAG, "invalid master configuration."); + } + + void *mbm_handle = NULL; + TaskHandle_t master_task_handle = NULL; + + TEST_ESP_OK(mbc_master_create_serial(pconfig, &mbm_handle)); + mbm_controller_iface_t *pbase = mbm_handle; + + TEST_ESP_OK(mbc_master_set_descriptor(mbm_handle, pdescr, descr_size)); + ESP_LOGI(TAG, "%p, modbus master stack is initialized", mbm_handle); + + TEST_ESP_OK(mbc_master_start(mbm_handle)); + ESP_LOGI(TAG, "%p, modbus master start...", mbm_handle) ; + + char* port_name = pbase->mb_base->descr.parent_name; + TEST_ASSERT_TRUE(xTaskCreatePinnedToCore(test_master_task, port_name, + TEST_TASK_STACK_SIZE, + mbm_handle, (TEST_TASK_PRIO_MASTER), + &master_task_handle, MB_PORT_TASK_AFFINITY)); + return master_task_handle; +} + +TaskHandle_t test_slave_serial_create(mb_communication_info_t *pconfig) +{ + if (!pconfig) { + ESP_LOGI(TAG, "invalid slave configuration."); + } + + void *mbs_handle = NULL; + TaskHandle_t slave_task_handle = NULL; + + TEST_ESP_OK(mbc_slave_create_serial(pconfig, &mbs_handle)); + + mbs_controller_iface_t *pbase = mbs_handle; + + test_slave_setup_start(mbs_handle); + + TEST_ASSERT_TRUE(xTaskCreatePinnedToCore(test_slave_task, pbase->mb_base->descr.parent_name, + TEST_TASK_STACK_SIZE, + mbs_handle, (TEST_TASK_PRIO_SLAVE), + &slave_task_handle, MB_PORT_TASK_AFFINITY)); + return slave_task_handle; +} + +#endif + +#if (CONFIG_FMB_COMM_MODE_TCP_EN) + +TaskHandle_t test_master_tcp_create(mb_communication_info_t *pconfig, const mb_parameter_descriptor_t *pdescr, uint16_t descr_size) +{ + if (!pconfig || !pdescr) { + ESP_LOGI(TAG, "invalid master configuration."); + } + + void *mbm_handle = NULL; + TaskHandle_t master_task_handle = NULL; + + TEST_ESP_OK(mbc_master_create_tcp(pconfig, &mbm_handle)); + mbm_controller_iface_t *pbase = mbm_handle; + + TEST_ESP_OK(mbc_master_set_descriptor(mbm_handle, pdescr, descr_size)); + ESP_LOGI(TAG, "%p, modbus master stack is initialized", mbm_handle); + + TEST_ESP_OK(mbc_master_start(mbm_handle)); + ESP_LOGI(TAG, "%p, modbus master start...", mbm_handle) ; + + char *port_name = pbase->mb_base->descr.parent_name; + TEST_ASSERT_TRUE(xTaskCreatePinnedToCore(test_master_task, port_name, + TEST_TASK_STACK_SIZE, + mbm_handle, (TEST_TASK_PRIO_MASTER), + &master_task_handle, MB_PORT_TASK_AFFINITY)); + return master_task_handle; +} + +TaskHandle_t test_slave_tcp_create(mb_communication_info_t *pconfig) +{ + if (!pconfig) { + ESP_LOGI(TAG, "invalid slave configuration."); + } + + void *mbs_handle = NULL; + TaskHandle_t slave_task_handle = NULL; + + TEST_ESP_OK(mbc_slave_create_tcp(pconfig, &mbs_handle)); + + mbs_controller_iface_t *pbase = mbs_handle; + test_slave_setup_start(mbs_handle); + + TEST_ASSERT_TRUE(xTaskCreatePinnedToCore(test_slave_task, pbase->mb_base->descr.parent_name, + TEST_TASK_STACK_SIZE, + mbs_handle, (TEST_TASK_PRIO_SLAVE), + &slave_task_handle, MB_PORT_TASK_AFFINITY)); + return slave_task_handle; +} + +#endif diff --git a/test_apps/unit_tests/mb_controller_common/CMakeLists.txt b/test_apps/unit_tests/mb_controller_common/CMakeLists.txt new file mode 100644 index 0000000..79c2793 --- /dev/null +++ b/test_apps/unit_tests/mb_controller_common/CMakeLists.txt @@ -0,0 +1,9 @@ +# This is the project CMakeLists.txt file for the test subproject +cmake_minimum_required(VERSION 3.16) + +set(EXTRA_COMPONENT_DIRS "$ENV{IDF_PATH}/tools/unit-test-app/components") +list(APPEND EXTRA_COMPONENT_DIRS "../../test_common") +list(APPEND EXTRA_COMPONENT_DIRS "../test_stubs") + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(test_mb_controller_common_unit) diff --git a/test_apps/unit_tests/mb_controller_common/README.md b/test_apps/unit_tests/mb_controller_common/README.md new file mode 100644 index 0000000..a1a8536 --- /dev/null +++ b/test_apps/unit_tests/mb_controller_common/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. diff --git a/test_apps/unit_tests/mb_controller_common/components/mocked_esp_modbus/CMakeLists.txt b/test_apps/unit_tests/mb_controller_common/components/mocked_esp_modbus/CMakeLists.txt new file mode 100644 index 0000000..aa13055 --- /dev/null +++ b/test_apps/unit_tests/mb_controller_common/components/mocked_esp_modbus/CMakeLists.txt @@ -0,0 +1,9 @@ +# NOTE: This kind of mocking currently works on Linux targets only. +# On Espressif chips, too many dependencies are missing at the moment. + +idf_component_mock(INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR} + REQUIRES test_common cmock + MOCK_HEADER_FILES ${CMAKE_CURRENT_SOURCE_DIR}/test_mbm_object.h) + + + diff --git a/test_apps/unit_tests/mb_controller_common/components/mocked_esp_modbus/mock/mock_config.yaml b/test_apps/unit_tests/mb_controller_common/components/mocked_esp_modbus/mock/mock_config.yaml new file mode 100644 index 0000000..596255b --- /dev/null +++ b/test_apps/unit_tests/mb_controller_common/components/mocked_esp_modbus/mock/mock_config.yaml @@ -0,0 +1,9 @@ + :cmock: + :plugins: + - expect + - expect_any_args + - return_thru_ptr + - array + - ignore + - ignore_arg + - callback diff --git a/test_apps/unit_tests/mb_controller_common/components/mocked_esp_modbus/test_mbm_object.h b/test_apps/unit_tests/mb_controller_common/components/mocked_esp_modbus/test_mbm_object.h new file mode 100644 index 0000000..2fc1fee --- /dev/null +++ b/test_apps/unit_tests/mb_controller_common/components/mocked_esp_modbus/test_mbm_object.h @@ -0,0 +1,117 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include + +#include "mb_config.h" +#include "mb_common.h" +#include "mb_port_types.h" + +#include "sdkconfig.h" + +/* Common definitions */ + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct mb_base_t mb_base_t; /*!< Type of modbus object */ +typedef struct mb_port_base_t mb_port_base_t; + +bool mb_port_evt_get(mb_port_base_t *inst, mb_event_t *pevent); +bool mb_port_evt_post(mb_port_base_t *inst, mb_event_t event); +bool mb_port_evt_res_take(mb_port_base_t *inst, uint32_t timeout); +void mb_port_evt_res_release(mb_port_base_t *inst); +mb_err_enum_t mb_port_evt_wait_req_finish(mb_port_base_t *inst); + +#if (CONFIG_FMB_COMM_MODE_ASCII_EN || CONFIG_FMB_COMM_MODE_RTU_EN) + +typedef struct _port_serial_opts mb_serial_opts_t; + +mb_err_enum_t mbs_rtu_create(mb_serial_opts_t *ser_opts, void **in_out_obj); +mb_err_enum_t mbs_ascii_create(mb_serial_opts_t *ser_opts, void **in_out_obj); + +mb_err_enum_t mbm_rtu_create(mb_serial_opts_t *ser_opts, void **in_out_obj); +mb_err_enum_t mbm_ascii_create(mb_serial_opts_t *ser_opts, void **in_out_obj); + +mb_err_enum_t mbm_rq_read_inp_reg(mb_base_t *inst, uint8_t snd_addr, uint16_t reg_addr, uint16_t reg_num, uint32_t tout); +mb_err_enum_t mbm_rq_write_holding_reg(mb_base_t *inst, uint8_t snd_addr, uint16_t reg_addr, uint16_t reg_data, uint32_t tout); +mb_err_enum_t mbm_rq_write_multi_holding_reg(mb_base_t *inst, uint8_t snd_addr, uint16_t reg_addr, uint16_t reg_wr_addr, uint16_t *data_ptr, uint32_t tout); +mb_err_enum_t mbm_rq_read_holding_reg(mb_base_t *inst, uint8_t snd_addr, uint16_t reg_addr, uint16_t reg_num, uint32_t tout); +mb_err_enum_t mbm_rq_rw_multi_holding_reg(mb_base_t *inst, uint8_t snd_addr, uint16_t rd_reg_addr, + uint16_t rd_reg_num, uint16_t *data_ptr, uint16_t wr_reg_addr, uint16_t wr_reg_num, uint32_t tout); +mb_err_enum_t mbm_rq_read_discrete_inputs(mb_base_t *inst, uint8_t snd_addr, uint16_t discrete_addr, uint16_t discrete_num, uint32_t tout); +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); +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_BUF +mb_exception_t mb_fn_report_slv_id(mb_base_t *inst, uint8_t *frame_ptr,uint16_t *len_buf); +#endif + +#if MB_FUNC_READ_INPUT_ENABLED +mb_exception_t mbs_fn_read_input_reg(mb_base_t *inst, uint8_t *frame_ptr,uint16_t *len_buf); +#endif + +#if MB_FUNC_READ_HOLDING_ENABLED +mb_exception_t mbs_fn_read_holding_reg(mb_base_t *inst, uint8_t *frame_ptr,uint16_t *len_buf); +#endif + +#if MB_FUNC_WRITE_HOLDING_ENABLED +mb_exception_t mbs_fn_write_holding_reg(mb_base_t *inst, uint8_t *frame_ptr, uint16_t *len_buf); +#endif + +#if MB_FUNC_WRITE_MULTIPLE_HOLDING_ENABLED +mb_exception_t mbs_fn_write_multi_holding_reg(mb_base_t *inst, uint8_t *frame_ptr, uint16_t *len_buf); +#endif + +#if MB_FUNC_READ_COILS_ENABLED +mb_exception_t mbs_fn_read_coils(mb_base_t *inst, uint8_t *frame_ptr,uint16_t *len_buf); +#endif + +#if MB_FUNC_WRITE_COIL_ENABLED +mb_exception_t mbs_fn_write_coil(mb_base_t *inst, uint8_t *frame_ptr, uint16_t *len_buf); +#endif + +#if MB_FUNC_WRITE_MULTIPLE_COILS_ENABLED +mb_exception_t mbs_fn_write_multi_coils(mb_base_t *inst, uint8_t *frame_ptr, uint16_t *len_buf); +#endif + +#if MB_FUNC_READ_DISCRETE_INPUTS_ENABLED +mb_exception_t mbs_fn_read_discrete_inp(mb_base_t *inst, uint8_t *frame_ptr, uint16_t *len_buf); +#endif + +#if MB_FUNC_READWRITE_HOLDING_ENABLED +mb_exception_t mbs_fn_rw_multi_holding_reg(mb_base_t *inst, uint8_t *frame_ptr,uint16_t *len_buf); +#endif + +#endif + +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); + +#if (CONFIG_FMB_COMM_MODE_TCP_EN) + +typedef struct _port_tcp_opts mb_tcp_opts_t; + +mb_err_enum_t mbm_tcp_create(mb_tcp_opts_t *tcp_opts, void **in_out_obj); + +#endif + +mb_err_enum_t mbm_delete(mb_base_t *inst); +mb_err_enum_t mbm_enable(mb_base_t *inst); +mb_err_enum_t mbm_disable(mb_base_t *inst); +mb_err_enum_t mbm_poll(mb_base_t *inst); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/test_apps/unit_tests/mb_controller_common/main/CMakeLists.txt b/test_apps/unit_tests/mb_controller_common/main/CMakeLists.txt new file mode 100644 index 0000000..d1bb8b3 --- /dev/null +++ b/test_apps/unit_tests/mb_controller_common/main/CMakeLists.txt @@ -0,0 +1,11 @@ +set(srcs "test_app_main.c" + "test_mb_controller_common.c" +) + +# In order for the cases defined by `TEST_CASE` to be linked into the final elf, +idf_component_register(SRCS ${srcs} + PRIV_REQUIRES test_stubs mocked_esp_modbus test_common cmock test_utils unity) + +# The workaround for WHOLE_ARCHIVE is absent in v4.4 +set_property(TARGET ${COMPONENT_LIB} APPEND PROPERTY INTERFACE_LINK_LIBRARIES "-u mb_test_include_impl") +#target_compile_options(${COMPONENT_LIB} PUBLIC -fsanitize=address = -lasan) diff --git a/test_apps/unit_tests/mb_controller_common/main/Kconfig.projbuild b/test_apps/unit_tests/mb_controller_common/main/Kconfig.projbuild new file mode 100644 index 0000000..ce29a07 --- /dev/null +++ b/test_apps/unit_tests/mb_controller_common/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 y + 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 communication cycle counter for test" + 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/unit_tests/mb_controller_common/main/component.mk b/test_apps/unit_tests/mb_controller_common/main/component.mk new file mode 100644 index 0000000..a98f634 --- /dev/null +++ b/test_apps/unit_tests/mb_controller_common/main/component.mk @@ -0,0 +1,4 @@ +# +# "main" pseudo-component makefile. +# +# (Uses default behaviour of compiling all source files in directory, adding 'include' to include path.) diff --git a/test_apps/unit_tests/mb_controller_common/main/idf_component.yml b/test_apps/unit_tests/mb_controller_common/main/idf_component.yml new file mode 100644 index 0000000..18e8f75 --- /dev/null +++ b/test_apps/unit_tests/mb_controller_common/main/idf_component.yml @@ -0,0 +1,6 @@ +dependencies: + idf: ">=4.3" + espressif/esp-modbus: + version: "^2.0" + override_path: "../../../../" + diff --git a/test_apps/unit_tests/mb_controller_common/main/test_app_main.c b/test_apps/unit_tests/mb_controller_common/main/test_app_main.c new file mode 100644 index 0000000..f1b9310 --- /dev/null +++ b/test_apps/unit_tests/mb_controller_common/main/test_app_main.c @@ -0,0 +1,19 @@ +/* + * SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: CC0-1.0 + */ + +#include "unity.h" +#include "unity_test_runner.h" +#include "unity_fixture.h" + +static void run_all_tests(void) +{ + RUN_TEST_GROUP(unit_test_controller); +} + +void app_main(void) +{ + UNITY_MAIN_FUNC(run_all_tests); +} diff --git a/test_apps/unit_tests/mb_controller_common/main/test_mb_controller_common.c b/test_apps/unit_tests/mb_controller_common/main/test_mb_controller_common.c new file mode 100644 index 0000000..4281d9d --- /dev/null +++ b/test_apps/unit_tests/mb_controller_common/main/test_mb_controller_common.c @@ -0,0 +1,245 @@ +/* + * SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +#include "unity_fixture.h" + +#include "sdkconfig.h" +#include "test_common.h" +#include "mbc_master.h" +#include "mbc_slave.h" + +#include "Mocktest_mbm_object.h" +#include "mb_object_stub.h" + +#define TEST_SER_PORT_NUM 1 +#define TEST_TCP_PORT_NUM 1502 +#define TEST_TASKS_NUM 3 +#define TEST_TASK_TIMEOUT_MS 30000 +#define TEST_ALLOWED_LEAK 32 +#define TEST_SLAVE_SEND_TOUT_US 30000 +#define TEST_MASTER_SEND_TOUT_US 30000 + +#define TEST_MASTER_RESPOND_TOUT_MS CONFIG_FMB_MASTER_TIMEOUT_MS_RESPOND + +#define TAG "MODBUS_SERIAL_TEST" + +// The workaround to statically link whole test library + __attribute__((unused)) bool mb_test_include_impl = 1; + +enum { + CID_DEV_REG0_INPUT, + CID_DEV_REG0_HOLD, + CID_DEV_REG1_INPUT, + CID_DEV_REG0_COIL, + CID_DEV_REG0_DISCRITE, + CID_DEV_REG_CNT +}; + +// Example Data (Object) Dictionary for Modbus parameters +static const mb_parameter_descriptor_t descriptors[] = { + {CID_DEV_REG0_INPUT, STR("MB_input_reg-0"), STR("Data"), MB_DEVICE_ADDR1, MB_PARAM_INPUT, 0, 1, + 0, PARAM_TYPE_U16, 2, OPTS(0, 0, 0), PAR_PERMS_READ}, + {CID_DEV_REG0_HOLD, STR("MB_hold_reg-0"), STR("Data"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, 1, 1, + 0, PARAM_TYPE_U16, 2, OPTS(0, 0, 0), PAR_PERMS_READ_WRITE_TRIGGER}, + {CID_DEV_REG1_INPUT, STR("MB_input_reg-1"), STR("Data"), MB_DEVICE_ADDR1, MB_PARAM_INPUT, 2, 1, + 0, PARAM_TYPE_U16, 2, OPTS(0, 0, 0), PAR_PERMS_READ_WRITE_TRIGGER}, + {CID_DEV_REG0_COIL, STR("MB_coil_reg-0"), STR("bit"), MB_DEVICE_ADDR1, MB_PARAM_COIL, 3, 8, + 0, PARAM_TYPE_U16, 2, OPTS(0, 0, 0), PAR_PERMS_READ_WRITE_TRIGGER}, + {CID_DEV_REG0_DISCRITE, STR("MB_discr_reg-0"), STR("bit"), MB_DEVICE_ADDR1, MB_PARAM_DISCRETE, 4, 1, + 0, PARAM_TYPE_U16, 2, OPTS(0, 0, 0), PAR_PERMS_READ_WRITE_TRIGGER}, + {CID_DEV_REG_CNT, 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}, +}; + +// Calculate number of parameters in the table +const uint16_t num_descriptors = (sizeof(descriptors) / sizeof(descriptors[0])); + +TEST_GROUP(unit_test_controller); + +TEST_SETUP(unit_test_controller) +{ + test_common_start(); +} + +TEST_TEAR_DOWN(unit_test_controller) +{ + test_common_stop(); +} + +TEST(unit_test_controller, test_setup_destroy_master) +{ + mb_communication_info_t master_config = { + .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_2, + .ser_opts.baudrate = 115200, + .ser_opts.parity = UART_PARITY_DISABLE, + .ser_opts.response_tout_ms = 1, + .ser_opts.test_tout_us = TEST_SLAVE_SEND_TOUT_US + }; + + ESP_LOGI(TAG, "TEST: Verify master create-destroy sequence."); + + void *mbm_handle = NULL; + mb_base_t *pmb_base = NULL; + TEST_ESP_ERR(MB_ENOERR, mb_stub_create(&master_config.ser_opts, (void *)&pmb_base)); + + mbm_rtu_create_ExpectAnyArgsAndReturn(MB_ENOERR); + mbm_rtu_create_ReturnThruPtr_in_out_obj((void **)&pmb_base); + TEST_ESP_OK(mbc_master_create_serial(&master_config, &mbm_handle)); + TEST_ESP_OK(mbc_master_set_descriptor(mbm_handle, &descriptors[0], num_descriptors)); + TEST_ESP_OK(mbc_master_delete(mbm_handle)); + + master_config.ser_opts.mode = MB_ASCII; + mbm_handle = NULL; + pmb_base = NULL; + + mbm_ascii_create_ExpectAnyArgsAndReturn(MB_EINVAL); + TEST_ESP_ERR(ESP_ERR_INVALID_STATE, mbc_master_create_serial(&master_config, &mbm_handle)); + TEST_ESP_ERR(ESP_ERR_INVALID_STATE, mbc_master_set_descriptor(mbm_handle, &descriptors[0], num_descriptors)); + TEST_ESP_ERR(ESP_ERR_INVALID_STATE, mbc_master_delete(mbm_handle)); + ESP_LOGI(TAG, "Test passed successfully."); +} + +TEST(unit_test_controller, test_setup_destroy_slave) +{ + // Initialize and start Modbus controller + mb_communication_info_t slave_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_2, + .ser_opts.baudrate = 115200, + .ser_opts.parity = UART_PARITY_DISABLE, + .ser_opts.response_tout_ms = TEST_MASTER_RESPOND_TOUT_MS, + .ser_opts.test_tout_us = TEST_MASTER_SEND_TOUT_US + }; + + ESP_LOGI(TAG, "TEST: Verify slave create-destroy sequence."); + void *mbs_handle = NULL; + mb_base_t *pmb_base = mbs_handle; + TEST_ESP_ERR(MB_ENOERR, mb_stub_create(&slave_config.ser_opts, (void *)&pmb_base)); + mbs_rtu_create_ExpectAndReturn(&slave_config.ser_opts, (void *)pmb_base, MB_ENOERR); + mbs_rtu_create_IgnoreArg_in_out_obj(); + mbs_rtu_create_ReturnThruPtr_in_out_obj((void **)&pmb_base); + TEST_ESP_OK(mbc_slave_create_serial(&slave_config, &mbs_handle)); + TEST_ESP_OK(mbc_slave_delete(mbs_handle)); + + slave_config.ser_opts.mode = MB_ASCII; + mbs_handle = NULL; + mbs_ascii_create_ExpectAnyArgsAndReturn(MB_EILLSTATE); + TEST_ESP_ERR(ESP_ERR_INVALID_STATE, mbc_slave_create_serial(&slave_config, &mbs_handle)); + TEST_ESP_ERR(ESP_ERR_INVALID_STATE, mbc_slave_delete(mbs_handle)); + ESP_LOGI(TAG, "Test passed successfully."); +} + +esp_err_t test_master_registers(int par_index, mb_err_enum_t mb_err) +{ + mb_communication_info_t master_config = { + .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_2, + .ser_opts.baudrate = 115200, + .ser_opts.parity = UART_PARITY_DISABLE, + .ser_opts.response_tout_ms = 1, + .ser_opts.test_tout_us = TEST_SLAVE_SEND_TOUT_US + }; + mb_base_t *pmb_base = NULL; // fake mb_base handle + void *mbm_handle = NULL; + + TEST_ESP_ERR(MB_ENOERR, mb_stub_create(&master_config.ser_opts, (void *)&pmb_base)); + pmb_base->port_obj = (mb_port_base_t *)0x44556677; + mbm_rtu_create_ExpectAnyArgsAndReturn(MB_ENOERR); + mbm_rtu_create_ReturnThruPtr_in_out_obj((void **)&pmb_base); + TEST_ESP_OK(mbc_master_create_serial(&master_config, &mbm_handle)); + TEST_ESP_OK(mbc_master_set_descriptor(mbm_handle, &descriptors[0], num_descriptors)); + mb_port_evt_res_take_ExpectAnyArgsAndReturn(true); + mb_port_evt_res_release_ExpectAnyArgs(); + TEST_ESP_OK(mbc_master_start(mbm_handle)); + + const mb_parameter_descriptor_t *param_descriptor = NULL; + esp_err_t err = mbc_master_get_cid_info(mbm_handle, par_index, ¶m_descriptor); + if ((err != ESP_ERR_NOT_FOUND) && (param_descriptor != NULL)) + { + TEST_ASSERT_EQUAL_HEX32(&descriptors[par_index], param_descriptor); + uint8_t type = 0; // type of parameter from dictionary + uint8_t *pdata = (uint8_t *)calloc(1, param_descriptor->mb_size + 1); + ESP_LOGI(TAG, "Test CID #%d, %s, %s", param_descriptor->cid, param_descriptor->param_key, param_descriptor->param_units); + // This is to check the request function is called with appropriate params. + switch(param_descriptor->mb_param_type) { \ + case MB_PARAM_INPUT: \ + mbm_rq_read_inp_reg_ExpectAndReturn(pmb_base, \ + param_descriptor->mb_slave_addr, \ + param_descriptor->mb_reg_start, \ + param_descriptor->mb_size, \ + 1, \ + mb_err); \ + mbm_rq_read_inp_reg_IgnoreArg_tout(); \ + break; \ + case MB_PARAM_HOLDING: + mbm_rq_read_holding_reg_ExpectAndReturn(pmb_base, \ + param_descriptor->mb_slave_addr, \ + param_descriptor->mb_reg_start, \ + param_descriptor->mb_size, \ + 1, \ + mb_err); \ + mbm_rq_read_holding_reg_IgnoreArg_tout(); \ + break; \ + case MB_PARAM_COIL: \ + mbm_rq_read_coils_ExpectAndReturn(pmb_base, \ + param_descriptor->mb_slave_addr, \ + param_descriptor->mb_reg_start, \ + param_descriptor->mb_size, \ + 1, \ + mb_err); \ + mbm_rq_read_coils_IgnoreArg_tout(); \ + break; \ + case MB_PARAM_DISCRETE: \ + mbm_rq_read_discrete_inputs_ExpectAndReturn(pmb_base, \ + param_descriptor->mb_slave_addr, \ + param_descriptor->mb_reg_start, \ + param_descriptor->mb_size, \ + 1, \ + mb_err); \ + mbm_rq_read_discrete_inputs_IgnoreArg_tout(); \ + break; \ + default: + TEST_FAIL(); \ + break; \ + } + err = mbc_master_get_parameter(mbm_handle, par_index, pdata, &type); \ + free(pdata); + } + TEST_ESP_OK(mbc_master_stop(mbm_handle)); + TEST_ESP_OK(mbc_master_delete(mbm_handle)); + ESP_LOGI(TAG, "Test passed successfully."); + return err; +} + +// Check if modbus controller object forms correct modbus request from data dictionary +// and is able to transfer data using mb_object. Check possible errors returned back from +// mb_object and make sure the modbus controller handles them correctly. +TEST(unit_test_controller, test_master_send_request) +{ + TEST_ESP_ERR(ESP_OK, test_master_registers(CID_DEV_REG0_INPUT, MB_ENOERR)); + TEST_ESP_ERR(ESP_ERR_TIMEOUT, test_master_registers(CID_DEV_REG0_INPUT, MB_ETIMEDOUT)); + TEST_ESP_ERR(ESP_OK, test_master_registers(CID_DEV_REG0_HOLD, MB_ENOERR)); + TEST_ESP_ERR(ESP_ERR_TIMEOUT, test_master_registers(CID_DEV_REG0_HOLD, MB_ETIMEDOUT)); + TEST_ESP_ERR(ESP_OK, test_master_registers(CID_DEV_REG0_COIL, MB_ENOERR)); + TEST_ESP_ERR(ESP_ERR_TIMEOUT, test_master_registers(CID_DEV_REG0_COIL, MB_ETIMEDOUT)); + TEST_ESP_ERR(ESP_OK, test_master_registers(CID_DEV_REG0_DISCRITE, MB_ENOERR)); + TEST_ESP_ERR(ESP_ERR_TIMEOUT, test_master_registers(CID_DEV_REG0_DISCRITE, MB_ETIMEDOUT)); +} + +TEST_GROUP_RUNNER(unit_test_controller) +{ + RUN_TEST_CASE(unit_test_controller, test_setup_destroy_master); + RUN_TEST_CASE(unit_test_controller, test_setup_destroy_slave); + RUN_TEST_CASE(unit_test_controller, test_master_send_request); +} diff --git a/test_apps/unit_tests/mb_controller_common/pytest_mb_controller_common.py b/test_apps/unit_tests/mb_controller_common/pytest_mb_controller_common.py new file mode 100644 index 0000000..87eb152 --- /dev/null +++ b/test_apps/unit_tests/mb_controller_common/pytest_mb_controller_common.py @@ -0,0 +1,16 @@ +# SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: CC0-1.0 + +import pytest +from pytest_embedded import Dut + + +CONFIGS = [ + pytest.param('serial', marks=[pytest.mark.esp32, pytest.mark.esp32s2, pytest.mark.esp32s3, pytest.mark.esp32c3]), +] + + +@pytest.mark.generic_multi_device +@pytest.mark.parametrize('config', CONFIGS, indirect=True) +def test_modbus_controller_common(dut: Dut) -> None: + dut.expect_unity_test_output() diff --git a/test_apps/unit_tests/mb_controller_common/sdkconfig.ci.serial b/test_apps/unit_tests/mb_controller_common/sdkconfig.ci.serial new file mode 100644 index 0000000..8b0e7a7 --- /dev/null +++ b/test_apps/unit_tests/mb_controller_common/sdkconfig.ci.serial @@ -0,0 +1,23 @@ +# This file was generated using idf.py save-defconfig. It can be edited manually. +# Espressif IoT Development Framework (ESP-IDF) Project Minimal Configuration +# +# +# Modbus configuration +# +CONFIG_UNITY_ENABLE_FIXTURE=y +CONFIG_FMB_PORT_TASK_STACK_SIZE=4096 +CONFIG_FMB_PORT_TASK_PRIO=10 +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=y +CONFIG_FMB_MASTER_TIMEOUT_MS_RESPOND=2000 +CONFIG_FMB_MASTER_DELAY_MS_CONVERT=300 +CONFIG_FMB_TIMER_USE_ISR_DISPATCH_METHOD=y +CONFIG_MB_PORT_ADAPTER_EN=y +CONFIG_MB_TEST_SLAVE_TASK_PRIO=4 +CONFIG_MB_TEST_MASTER_TASK_PRIO=4 +CONFIG_MB_TEST_COMM_CYCLE_COUNTER=10 +CONFIG_MB_TEST_LEAK_CRITICAL_LEVEL=128 +CONFIG_MB_TEST_LEAK_WARN_LEVEL=128 + diff --git a/test_apps/unit_tests/mb_controller_mapping/CMakeLists.txt b/test_apps/unit_tests/mb_controller_mapping/CMakeLists.txt new file mode 100644 index 0000000..1272dd2 --- /dev/null +++ b/test_apps/unit_tests/mb_controller_mapping/CMakeLists.txt @@ -0,0 +1,17 @@ +# This is the project CMakeLists.txt file for the test subproject +cmake_minimum_required(VERSION 3.16) + +set(EXTRA_COMPONENT_DIRS "$ENV{IDF_PATH}/tools/unit-test-app/components") +list(APPEND EXTRA_COMPONENT_DIRS "../../test_common") +list(APPEND EXTRA_COMPONENT_DIRS "../test_stubs") + +#set(COMPONENTS driver esp_timer esp_event esp_netif main) + +# list(APPEND EXTRA_COMPONENT_DIRS +# "$ENV{IDF_PATH}/tools/mocks/lwip/" +# "$ENV{IDF_PATH}/tools/mocks/freertos/" +# "$ENV{IDF_PATH}/tools/mocks/esp_timer/" +# ) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(test_mb_controller_mapping_unit) diff --git a/test_apps/unit_tests/mb_controller_mapping/README.md b/test_apps/unit_tests/mb_controller_mapping/README.md new file mode 100644 index 0000000..a1a8536 --- /dev/null +++ b/test_apps/unit_tests/mb_controller_mapping/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. diff --git a/test_apps/unit_tests/mb_controller_mapping/components/mocked_esp_modbus/CMakeLists.txt b/test_apps/unit_tests/mb_controller_mapping/components/mocked_esp_modbus/CMakeLists.txt new file mode 100644 index 0000000..0b50b5c --- /dev/null +++ b/test_apps/unit_tests/mb_controller_mapping/components/mocked_esp_modbus/CMakeLists.txt @@ -0,0 +1,9 @@ +# NOTE: This kind of mocking currently works on Linux targets only. +# On Espressif chips, too many dependencies are missing at the moment. + +idf_component_mock(INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR} + REQUIRES cmock test_common + MOCK_HEADER_FILES ${CMAKE_CURRENT_SOURCE_DIR}/test_mbm_object.h) + + + diff --git a/test_apps/unit_tests/mb_controller_mapping/components/mocked_esp_modbus/mock/mock_config.yaml b/test_apps/unit_tests/mb_controller_mapping/components/mocked_esp_modbus/mock/mock_config.yaml new file mode 100644 index 0000000..596255b --- /dev/null +++ b/test_apps/unit_tests/mb_controller_mapping/components/mocked_esp_modbus/mock/mock_config.yaml @@ -0,0 +1,9 @@ + :cmock: + :plugins: + - expect + - expect_any_args + - return_thru_ptr + - array + - ignore + - ignore_arg + - callback diff --git a/test_apps/unit_tests/mb_controller_mapping/components/mocked_esp_modbus/test_mbm_object.h b/test_apps/unit_tests/mb_controller_mapping/components/mocked_esp_modbus/test_mbm_object.h new file mode 100644 index 0000000..ebd548a --- /dev/null +++ b/test_apps/unit_tests/mb_controller_mapping/components/mocked_esp_modbus/test_mbm_object.h @@ -0,0 +1,118 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include + +#include "mb_config.h" +#include "mb_common.h" +#include "mb_port_types.h" + +#include "sdkconfig.h" + +/* Common definitions */ + +#ifdef __cplusplus +extern "C" { +#endif + +#define MB_PDU_REQ_READ_ADDR_OFF (MB_PDU_DATA_OFF + 0) +#define MB_PDU_REQ_READ_REGCNT_OFF (MB_PDU_DATA_OFF + 2) +#define MB_PDU_REQ_READ_SIZE (4) +#define MB_PDU_FUNC_READ_REGCNT_MAX (0x007D) +#define MB_PDU_FUNC_READ_BYTECNT_OFF (MB_PDU_DATA_OFF + 0) +#define MB_PDU_FUNC_READ_VALUES_OFF (MB_PDU_DATA_OFF + 1) +#define MB_PDU_FUNC_READ_SIZE_MIN (1) + +#define MB_PDU_REQ_WRITE_ADDR_OFF (MB_PDU_DATA_OFF + 0) +#define MB_PDU_REQ_WRITE_VALUE_OFF (MB_PDU_DATA_OFF + 2) +#define MB_PDU_REQ_WRITE_SIZE (4) +#define MB_PDU_FUNC_WRITE_ADDR_OFF (MB_PDU_DATA_OFF + 0) +#define MB_PDU_FUNC_WRITE_VALUE_OFF (MB_PDU_DATA_OFF + 2) +#define MB_PDU_FUNC_WRITE_SIZE (4) + +#define MB_PDU_REQ_WRITE_MUL_ADDR_OFF (MB_PDU_DATA_OFF + 0) +#define MB_PDU_REQ_WRITE_MUL_REGCNT_OFF (MB_PDU_DATA_OFF + 2) +#define MB_PDU_REQ_WRITE_MUL_BYTECNT_OFF (MB_PDU_DATA_OFF + 4) +#define MB_PDU_REQ_WRITE_MUL_VALUES_OFF (MB_PDU_DATA_OFF + 5) +#define MB_PDU_REQ_WRITE_MUL_SIZE_MIN (5) +#define MB_PDU_REQ_WRITE_MUL_REGCNT_MAX (0x0078) +#define MB_PDU_FUNC_WRITE_MUL_ADDR_OFF (MB_PDU_DATA_OFF + 0) +#define MB_PDU_FUNC_WRITE_MUL_REGCNT_OFF (MB_PDU_DATA_OFF + 2) +#define MB_PDU_FUNC_WRITE_MUL_SIZE (4) + +#define MB_PDU_REQ_READWRITE_READ_ADDR_OFF (MB_PDU_DATA_OFF + 0) +#define MB_PDU_REQ_READWRITE_READ_REGCNT_OFF (MB_PDU_DATA_OFF + 2) +#define MB_PDU_REQ_READWRITE_WRITE_ADDR_OFF (MB_PDU_DATA_OFF + 4) +#define MB_PDU_REQ_READWRITE_WRITE_REGCNT_OFF (MB_PDU_DATA_OFF + 6) +#define MB_PDU_REQ_READWRITE_WRITE_BYTECNT_OFF (MB_PDU_DATA_OFF + 8) +#define MB_PDU_REQ_READWRITE_WRITE_VALUES_OFF (MB_PDU_DATA_OFF + 9) +#define MB_PDU_REQ_READWRITE_SIZE_MIN (9) +#define MB_PDU_FUNC_READWRITE_READ_BYTECNT_OFF (MB_PDU_DATA_OFF + 0) +#define MB_PDU_FUNC_READWRITE_READ_VALUES_OFF (MB_PDU_DATA_OFF + 1) +#define MB_PDU_FUNC_READWRITE_SIZE_MIN (1) + +#define MB_PDU_REQ_READ_ADDR_OFF (MB_PDU_DATA_OFF + 0) +#define MB_PDU_REQ_READ_COILCNT_OFF (MB_PDU_DATA_OFF + 2) +#define MB_PDU_REQ_READ_SIZE (4) +#define MB_PDU_FUNC_READ_COILCNT_OFF (MB_PDU_DATA_OFF + 0) +#define MB_PDU_FUNC_READ_VALUES_OFF (MB_PDU_DATA_OFF + 1) +#define MB_PDU_FUNC_READ_SIZE_MIN (1) + +#define MB_PDU_REQ_READ_ADDR_OFF (MB_PDU_DATA_OFF + 0) +#define MB_PDU_REQ_READ_DISCCNT_OFF (MB_PDU_DATA_OFF + 2) +#define MB_PDU_REQ_READ_SIZE (4) +#define MB_PDU_FUNC_READ_DISCCNT_OFF (MB_PDU_DATA_OFF + 0) +#define MB_PDU_FUNC_READ_VALUES_OFF (MB_PDU_DATA_OFF + 1) +#define MB_PDU_FUNC_READ_SIZE_MIN (1) +#define MB_PDU_REQ_WRITE_MUL_COILCNT_OFF (MB_PDU_DATA_OFF + 2) + +typedef struct mb_base_t mb_base_t; /*!< Type of modbus object */ +typedef struct mb_port_base_t mb_port_base_t; + +bool mb_port_evt_get(mb_port_base_t *inst, mb_event_t *pevent); +bool mb_port_evt_post(mb_port_base_t *inst, mb_event_t event); +bool mb_port_evt_res_take(mb_port_base_t *inst, uint32_t timeout); +void mb_port_evt_res_release(mb_port_base_t *inst); +mb_err_enum_t mb_port_evt_wait_req_finish(mb_port_base_t *inst); + +#if (CONFIG_FMB_COMM_MODE_ASCII_EN || CONFIG_FMB_COMM_MODE_RTU_EN) + +typedef struct _port_serial_opts mb_serial_opts_t; + +mb_err_enum_t mbs_rtu_create(mb_serial_opts_t *ser_opts, void **in_out_obj); +mb_err_enum_t mbs_ascii_create(mb_serial_opts_t *ser_opts, void **in_out_obj); + +mb_err_enum_t mbm_rtu_create(mb_serial_opts_t *ser_opts, void **in_out_obj); +mb_err_enum_t mbm_ascii_create(mb_serial_opts_t *ser_opts, void **in_out_obj); + + +#endif + +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); + +#if (CONFIG_FMB_COMM_MODE_TCP_EN) + +typedef struct _port_tcp_opts mb_tcp_opts_t; + +mb_err_enum_t mbm_tcp_create(mb_tcp_opts_t *tcp_opts, void **in_out_obj); + +#endif + +mb_err_enum_t mbm_delete(mb_base_t *inst); +mb_err_enum_t mbm_enable(mb_base_t *inst); +mb_err_enum_t mbm_disable(mb_base_t *inst); +mb_err_enum_t mbm_poll(mb_base_t *inst); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/test_apps/unit_tests/mb_controller_mapping/main/CMakeLists.txt b/test_apps/unit_tests/mb_controller_mapping/main/CMakeLists.txt new file mode 100644 index 0000000..7e929e3 --- /dev/null +++ b/test_apps/unit_tests/mb_controller_mapping/main/CMakeLists.txt @@ -0,0 +1,10 @@ +set(srcs "test_app_main.c" + "test_mb_controller_unit.c" +) + +# In order for the cases defined by `TEST_CASE` to be linked into the final elf, +idf_component_register(SRCS ${srcs} + PRIV_REQUIRES cmock unity test_stubs test_utils mocked_esp_modbus ) # test_common + +# 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_impl") \ No newline at end of file diff --git a/test_apps/unit_tests/mb_controller_mapping/main/Kconfig.projbuild b/test_apps/unit_tests/mb_controller_mapping/main/Kconfig.projbuild new file mode 100644 index 0000000..ce29a07 --- /dev/null +++ b/test_apps/unit_tests/mb_controller_mapping/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 y + 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 communication cycle counter for test" + 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/unit_tests/mb_controller_mapping/main/component.mk b/test_apps/unit_tests/mb_controller_mapping/main/component.mk new file mode 100644 index 0000000..a98f634 --- /dev/null +++ b/test_apps/unit_tests/mb_controller_mapping/main/component.mk @@ -0,0 +1,4 @@ +# +# "main" pseudo-component makefile. +# +# (Uses default behaviour of compiling all source files in directory, adding 'include' to include path.) diff --git a/test_apps/unit_tests/mb_controller_mapping/main/idf_component.yml b/test_apps/unit_tests/mb_controller_mapping/main/idf_component.yml new file mode 100644 index 0000000..18e8f75 --- /dev/null +++ b/test_apps/unit_tests/mb_controller_mapping/main/idf_component.yml @@ -0,0 +1,6 @@ +dependencies: + idf: ">=4.3" + espressif/esp-modbus: + version: "^2.0" + override_path: "../../../../" + diff --git a/test_apps/unit_tests/mb_controller_mapping/main/test_app_main.c b/test_apps/unit_tests/mb_controller_mapping/main/test_app_main.c new file mode 100644 index 0000000..7e23d41 --- /dev/null +++ b/test_apps/unit_tests/mb_controller_mapping/main/test_app_main.c @@ -0,0 +1,19 @@ +/* + * SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: CC0-1.0 + */ + +#include "unity.h" +#include "unity_test_runner.h" +#include "unity_fixture.h" + +static void run_all_tests(void) +{ + RUN_TEST_GROUP(unit_test_controller); +} + +void app_main(void) +{ + UNITY_MAIN_FUNC(run_all_tests); +} diff --git a/test_apps/unit_tests/mb_controller_mapping/main/test_mb_controller_unit.c b/test_apps/unit_tests/mb_controller_mapping/main/test_mb_controller_unit.c new file mode 100644 index 0000000..b9d0c50 --- /dev/null +++ b/test_apps/unit_tests/mb_controller_mapping/main/test_mb_controller_unit.c @@ -0,0 +1,487 @@ +/* + * SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +#include "unity_fixture.h" + +#include "sdkconfig.h" +#include "test_common.h" +#include "mbc_master.h" +#include "mbc_slave.h" + +#include "Mocktest_mbm_object.h" +#include "mb_object_stub.h" + +#define TEST_SER_PORT_NUM 1 +#define TEST_TCP_PORT_NUM 1502 +#define TEST_TASKS_NUM 3 +#define TEST_TASK_TIMEOUT_MS 30000 +#define TEST_ALLOWED_LEAK 32 +#define TEST_SLAVE_SEND_TOUT_US 30000 +#define TEST_MASTER_SEND_TOUT_US 30000 + +#define TEST_MASTER_RESPOND_TOUT_MS CONFIG_FMB_MASTER_TIMEOUT_MS_RESPOND + +#define TAG "MB_CONTROLLER_TEST" + +// The workaround to statically link whole test library +__attribute__((unused)) bool mb_test_include_impl = 1; + +enum +{ + CID_DEV_REG0_INPUT, + CID_DEV_REG0_HOLD, + CID_DEV_REG1_INPUT, + CID_DEV_REG0_COIL, + CID_DEV_REG0_DISCRITE, + CID_DEV_INPUT_AREA, + CID_DEV_HOLD_AREA, + CID_DEV_COIL_AREA, + CID_DEV_DISCR_AREA, +}; + +#define TEST_AREA0_REG_OFFS 2 +#define TEST_HOLD_AREA0_REG_SZ 10 +#define TEST_COIL_AREA0_REG_SZ 10 +#define TEST_INPUT_AREA0_REG_SZ 10 +#define TEST_DISCR_AREA0_REG_SZ 10 + +static uint16_t input_registers[TEST_INPUT_AREA0_REG_SZ + 1] = {0}; +static uint16_t hold_registers[TEST_HOLD_AREA0_REG_SZ + 1] = {0}; +static uint16_t coil_registers[TEST_COIL_AREA0_REG_SZ + 1] = {0}; +static uint16_t discr_registers[TEST_COIL_AREA0_REG_SZ + 1] = {0}; + +// Example Data (Object) Dictionary for Modbus parameters +static const mb_parameter_descriptor_t descriptors[] = { + {CID_DEV_REG0_INPUT, STR("MB_input_reg-0"), STR("Data"), MB_DEVICE_ADDR1, MB_PARAM_INPUT, 0, 2, + (uint32_t)&input_registers[0], PARAM_TYPE_U32, 4, OPTS(0, 0, 0), PAR_PERMS_READ}, + {CID_DEV_REG0_HOLD, STR("MB_hold_reg-0"), STR("Data"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, 1, 2, + (uint32_t)&hold_registers[1], PARAM_TYPE_U32, 4, OPTS(0, 0, 0), PAR_PERMS_READ_WRITE_TRIGGER}, + {CID_DEV_REG1_INPUT, STR("MB_input_reg-1"), STR("Data"), MB_DEVICE_ADDR1, MB_PARAM_INPUT, 2, 1, + 0, PARAM_TYPE_U16, 2, OPTS(0, 0, 0), PAR_PERMS_READ_WRITE_TRIGGER}, + {CID_DEV_REG0_COIL, STR("MB_coil_reg-0"), STR("Bit"), MB_DEVICE_ADDR1, MB_PARAM_COIL, 3, TEST_COIL_AREA0_REG_SZ, + 0, PARAM_TYPE_U16, 2, OPTS(0x03ff, 0, 0), PAR_PERMS_READ_WRITE_TRIGGER}, + {CID_DEV_REG0_DISCRITE, STR("MB_discr_reg-0"), STR("Bit"), MB_DEVICE_ADDR1, MB_PARAM_DISCRETE, 4, TEST_DISCR_AREA0_REG_SZ, + 0, PARAM_TYPE_U16, 2, OPTS(0x03fe, 0, 0), PAR_PERMS_READ_WRITE_TRIGGER}, + {CID_DEV_INPUT_AREA, STR("MB_input_area"), STR("Data"), MB_DEVICE_ADDR1, MB_PARAM_INPUT, TEST_AREA0_REG_OFFS, TEST_INPUT_AREA0_REG_SZ, + (uint32_t)&input_registers[1], PARAM_TYPE_ASCII, (TEST_INPUT_AREA0_REG_SZ * 2), OPTS(0, 0, 0), PAR_PERMS_READ}, + {CID_DEV_HOLD_AREA, STR("MB_holding_area"), STR("Data"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, TEST_AREA0_REG_OFFS, TEST_HOLD_AREA0_REG_SZ, + (uint32_t)&hold_registers[1], PARAM_TYPE_ASCII, (TEST_HOLD_AREA0_REG_SZ * 2), OPTS(0, 0, 0), PAR_PERMS_READ}, + {CID_DEV_COIL_AREA, STR("MB_coil_area"), STR("Bit"), MB_DEVICE_ADDR1, MB_PARAM_COIL, TEST_AREA0_REG_OFFS, TEST_COIL_AREA0_REG_SZ, + (uint32_t)&coil_registers[1], PARAM_TYPE_U16, ((TEST_COIL_AREA0_REG_SZ >> 3) + 1), OPTS(0, 0, 0), PAR_PERMS_READ}, + {CID_DEV_DISCR_AREA, STR("MB_discr_area"), STR("Bit"), MB_DEVICE_ADDR1, MB_PARAM_COIL, TEST_AREA0_REG_OFFS, TEST_DISCR_AREA0_REG_SZ, + (uint32_t)&discr_registers[1], PARAM_TYPE_U16, ((TEST_DISCR_AREA0_REG_SZ >> 3) + 1), OPTS(0, 0, 0), PAR_PERMS_READ}, +}; + +// Calculate number of parameters in the table +const uint16_t num_descriptors = (sizeof(descriptors) / sizeof(descriptors[0])); + +TEST_GROUP(unit_test_controller); + +TEST_SETUP(unit_test_controller) +{ + test_common_start(); +} + +TEST_TEAR_DOWN(unit_test_controller) +{ + test_common_stop(); +} + +static void test_slave_check_descriptor(int par_index) +{ + mb_communication_info_t slave_config = { + .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 = 115200, + .ser_opts.parity = UART_PARITY_DISABLE, + .ser_opts.response_tout_ms = 0, + .ser_opts.test_tout_us = 0 + }; + + void *mbs_handle = NULL; + mb_base_t *pmb_base = NULL; // fake mb_base handle + + TEST_ESP_ERR(MB_ENOERR, mb_stub_create(&slave_config.ser_opts, (void *)&pmb_base)); + pmb_base->port_obj = (mb_port_base_t *)0x44556677; + mbs_rtu_create_ExpectAnyArgsAndReturn(MB_ENOERR); + mbs_rtu_create_ReturnThruPtr_in_out_obj((void **)&pmb_base); + + TEST_ESP_OK(mbc_slave_create_serial(&slave_config, &mbs_handle)); + TEST_ASSERT(mbs_handle); + + mbs_controller_iface_t *mbs_iface = (mbs_controller_iface_t *)mbs_handle; + //mb_slave_options_t *mbs_opts = MB_SLAVE_GET_OPTS(mbs_iface); + TEST_ASSERT_EQUAL_HEX32(mbs_iface->mb_base, pmb_base); + + TEST_ASSERT_EQUAL_HEX32(pmb_base->rw_cbs.reg_input_cb, mbc_reg_input_slave_cb); + TEST_ASSERT_EQUAL_HEX32(pmb_base->rw_cbs.reg_holding_cb, mbc_reg_holding_slave_cb); + TEST_ASSERT_EQUAL_HEX32(pmb_base->rw_cbs.reg_coils_cb, mbc_reg_coils_slave_cb); + TEST_ASSERT_EQUAL_HEX32(pmb_base->rw_cbs.reg_discrete_cb, mbc_reg_discrete_slave_cb); + + mb_parameter_descriptor_t *pdescr = (mb_parameter_descriptor_t *)&descriptors[par_index]; + mb_register_area_descriptor_t reg_area; + ESP_LOGI(TAG, "Test CID #%d, %s, %s", pdescr->cid, pdescr->param_key, pdescr->param_units); + + uint16_t n_bytes = ((pdescr->mb_param_type == MB_PARAM_INPUT) || (pdescr->mb_param_type == MB_PARAM_HOLDING)) + ? (pdescr->mb_size << 1) : ((pdescr->mb_size >> 3) + 1); + + // First define the correct area + reg_area.type = pdescr->mb_param_type; + reg_area.start_offset = pdescr->mb_reg_start; + reg_area.address = (void *)pdescr->param_offset; + reg_area.size = n_bytes; + ESP_LOGI(TAG, "Area (type, reg_start, address, size): %d, %u, 0x%" PRIx32 ", %d, is defined.", + (int)reg_area.type, (unsigned)reg_area.start_offset, (uint32_t)reg_area.address, (int)reg_area.size); + TEST_ESP_OK(mbc_slave_set_descriptor(mbs_handle, reg_area)); + + // Check additional area overlapped + reg_area.start_offset = (pdescr->mb_reg_start + pdescr->mb_size - 2); + reg_area.size = 2; + ESP_LOGI(TAG, "Area overlapped (type, reg_start, address, size): %d, %u, 0x%" PRIx32 ", %d.", + (int)reg_area.type, (unsigned)reg_area.start_offset, (uint32_t)reg_area.address, (int)reg_area.size); + TEST_ESP_ERR(ESP_ERR_INVALID_ARG, mbc_slave_set_descriptor(mbs_handle, reg_area)); + + reg_area.start_offset = pdescr->mb_reg_start; + reg_area.size = n_bytes; + reg_area.address = (void *)pdescr->param_offset - 2; + ESP_LOGI(TAG, "Area redefine (type, reg_start, address, size): %d, %u, 0x%" PRIx32 ", %d.", + (int)reg_area.type, (unsigned)reg_area.start_offset, (uint32_t)reg_area.address, (int)reg_area.size); + TEST_ESP_ERR(ESP_ERR_INVALID_ARG, mbc_slave_set_descriptor(mbs_handle, reg_area)); + + TEST_ESP_OK(mbc_slave_delete(mbs_handle)); // the destructor of mb controller destroys the fake mb_object as well + TEST_ASSERT_EQUAL_HEX(mb_port_get_inst_counter(), 0); + ESP_LOGI(TAG, "Test passed successfully."); +} + +static esp_err_t test_master_read_req(int par_index, mb_err_enum_t mb_err) +{ + mb_communication_info_t master_config = { + .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_2, + .ser_opts.baudrate = 115200, + .ser_opts.parity = UART_PARITY_DISABLE, + .ser_opts.response_tout_ms = 1, + .ser_opts.test_tout_us = TEST_SLAVE_SEND_TOUT_US}; + mb_base_t *pmb_base = NULL; // fake mb_base handle + void *mbm_handle = NULL; + + TEST_ESP_ERR(MB_ENOERR, mb_stub_create(&master_config.ser_opts, (void *)&pmb_base)); + pmb_base->port_obj = (mb_port_base_t *)0x44556677; + mbm_rtu_create_ExpectAnyArgsAndReturn(MB_ENOERR); + mbm_rtu_create_ReturnThruPtr_in_out_obj((void **)&pmb_base); + TEST_ESP_OK(mbc_master_create_serial(&master_config, &mbm_handle)); + TEST_ESP_OK(mbc_master_set_descriptor(mbm_handle, &descriptors[0], num_descriptors)); + mb_port_evt_post_ExpectAndReturn(pmb_base->port_obj, EVENT(EV_FRAME_TRANSMIT | EV_TRANS_START), true); + TEST_ESP_OK(mbc_master_start(mbm_handle)); + mb_port_evt_wait_req_finish_ExpectAndReturn(pmb_base->port_obj, mb_err); + + const mb_parameter_descriptor_t *param_descriptor = NULL; + TEST_ESP_OK(mbc_master_get_cid_info(mbm_handle, par_index, ¶m_descriptor)); + TEST_ASSERT_EQUAL_HEX32(&descriptors[par_index], param_descriptor); + uint8_t type = 0; // type of parameter from dictionary + uint8_t pdata[100] = {0}; + ESP_LOGI(TAG, "Test CID #%d, %s, %s", param_descriptor->cid, param_descriptor->param_key, param_descriptor->param_units); + mb_port_evt_res_take_ExpectAnyArgsAndReturn(true); + mb_port_evt_res_release_ExpectAnyArgs(); + mb_port_evt_res_take_ExpectAnyArgsAndReturn(true); + mb_port_evt_res_release_ExpectAnyArgs(); + + // Call the read method of modbus controller + esp_err_t err = mbc_master_get_parameter(mbm_handle, par_index, pdata, &type); + uint8_t *mb_frame_ptr = NULL; + // get send buffer back using the fake mb_object + pmb_base->get_send_buf(pmb_base, &mb_frame_ptr); + TEST_ASSERT_EQUAL_HEX8(pmb_base->get_dest_addr(pmb_base), param_descriptor->mb_slave_addr); + uint8_t send_len = pmb_base->get_send_len(pmb_base); + TEST_ASSERT_EQUAL_HEX8(send_len, (MB_PDU_SIZE_MIN + MB_PDU_REQ_READ_SIZE)); + // Check that request function forms correct buffer + switch (param_descriptor->mb_param_type) + { + case MB_PARAM_INPUT: + // TEST_CHECK_EQ + TEST_ASSERT_EQUAL_HEX8(mb_frame_ptr[MB_PDU_FUNC_OFF], MB_FUNC_READ_INPUT_REGISTER); + TEST_ASSERT_EQUAL_HEX8(mb_frame_ptr[MB_PDU_REQ_READ_ADDR_OFF], (param_descriptor->mb_reg_start >> 8)); + TEST_ASSERT_EQUAL_HEX8(mb_frame_ptr[MB_PDU_REQ_READ_ADDR_OFF + 1], (param_descriptor->mb_reg_start & 0x00FF)); + TEST_ASSERT_EQUAL_HEX8(mb_frame_ptr[MB_PDU_REQ_READ_REGCNT_OFF], (param_descriptor->mb_size >> 8)); + TEST_ASSERT_EQUAL_HEX8(mb_frame_ptr[MB_PDU_REQ_READ_REGCNT_OFF + 1], (param_descriptor->mb_size & 0x00FF)); + ESP_LOG_BUFFER_HEX_LEVEL(TAG, (void *)mb_frame_ptr, send_len, ESP_LOG_INFO); + break; + case MB_PARAM_HOLDING: + TEST_ASSERT_EQUAL_HEX8(mb_frame_ptr[MB_PDU_FUNC_OFF], MB_FUNC_READ_HOLDING_REGISTER); + TEST_ASSERT_EQUAL_HEX8(mb_frame_ptr[MB_PDU_REQ_READ_ADDR_OFF], (param_descriptor->mb_reg_start >> 8)); + TEST_ASSERT_EQUAL_HEX8(mb_frame_ptr[MB_PDU_REQ_READ_ADDR_OFF + 1], (param_descriptor->mb_reg_start & 0x00FF)); + TEST_ASSERT_EQUAL_HEX8(mb_frame_ptr[MB_PDU_REQ_READ_REGCNT_OFF], (param_descriptor->mb_size >> 8)); + TEST_ASSERT_EQUAL_HEX8(mb_frame_ptr[MB_PDU_REQ_READ_REGCNT_OFF + 1], (param_descriptor->mb_size & 0x00FF)); + ESP_LOG_BUFFER_HEX_LEVEL(TAG, (void *)mb_frame_ptr, send_len, ESP_LOG_INFO); + break; + case MB_PARAM_COIL: + TEST_ASSERT_EQUAL_HEX8(mb_frame_ptr[MB_PDU_FUNC_OFF], MB_FUNC_READ_COILS); + TEST_ASSERT_EQUAL_HEX8(mb_frame_ptr[MB_PDU_REQ_READ_ADDR_OFF], (param_descriptor->mb_reg_start >> 8)); + TEST_ASSERT_EQUAL_HEX8(mb_frame_ptr[MB_PDU_REQ_READ_ADDR_OFF + 1], (param_descriptor->mb_reg_start & 0x00FF)); + TEST_ASSERT_EQUAL_HEX8(mb_frame_ptr[MB_PDU_REQ_READ_COILCNT_OFF], (param_descriptor->mb_size >> 8)); + TEST_ASSERT_EQUAL_HEX8(mb_frame_ptr[MB_PDU_REQ_READ_COILCNT_OFF + 1], (param_descriptor->mb_size & 0x00FF)); + ESP_LOG_BUFFER_HEX_LEVEL(TAG, (void *)mb_frame_ptr, send_len, ESP_LOG_INFO); + break; + case MB_PARAM_DISCRETE: + TEST_ASSERT_EQUAL_HEX8(mb_frame_ptr[MB_PDU_FUNC_OFF], MB_FUNC_READ_DISCRETE_INPUTS); + TEST_ASSERT_EQUAL_HEX8(mb_frame_ptr[MB_PDU_REQ_READ_ADDR_OFF], (param_descriptor->mb_reg_start >> 8)); + TEST_ASSERT_EQUAL_HEX8(mb_frame_ptr[MB_PDU_REQ_READ_ADDR_OFF + 1], (param_descriptor->mb_reg_start & 0x00FF)); + TEST_ASSERT_EQUAL_HEX8(mb_frame_ptr[MB_PDU_REQ_READ_DISCCNT_OFF], (param_descriptor->mb_size >> 8)); + TEST_ASSERT_EQUAL_HEX8(mb_frame_ptr[MB_PDU_REQ_READ_DISCCNT_OFF + 1], (param_descriptor->mb_size & 0x00FF)); + ESP_LOG_BUFFER_HEX_LEVEL(TAG, (void *)mb_frame_ptr, send_len, ESP_LOG_INFO); + break; + default: + TEST_FAIL(); + break; + } + TEST_ESP_OK(mbc_master_stop(mbm_handle)); + TEST_ESP_OK(mbc_master_delete(mbm_handle)); // the destructor of mb controller destroys the fake mb_object as well + TEST_ASSERT_EQUAL_HEX(mb_port_get_inst_counter(), 0); + ESP_LOGI(TAG, "Test passed successfully."); + return err; +} + +static esp_err_t test_master_write_req(int par_index, mb_err_enum_t mb_err) +{ + mb_communication_info_t master_config = { + .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_2, + .ser_opts.baudrate = 115200, + .ser_opts.parity = UART_PARITY_DISABLE, + .ser_opts.response_tout_ms = 1, + .ser_opts.test_tout_us = TEST_SLAVE_SEND_TOUT_US}; + mb_base_t *pmb_base = NULL; // fake mb_base handle + void *mbm_handle = NULL; + + TEST_ESP_ERR(MB_ENOERR, mb_stub_create(&master_config.ser_opts, (void *)&pmb_base)); + pmb_base->port_obj = (mb_port_base_t *)0x44556677; + mbm_rtu_create_ExpectAnyArgsAndReturn(MB_ENOERR); + mbm_rtu_create_ReturnThruPtr_in_out_obj((void **)&pmb_base); + TEST_ESP_OK(mbc_master_create_serial(&master_config, &mbm_handle)); + TEST_ESP_OK(mbc_master_set_descriptor(mbm_handle, &descriptors[0], num_descriptors)); + mb_port_evt_post_ExpectAndReturn(pmb_base->port_obj, EVENT(EV_FRAME_TRANSMIT | EV_TRANS_START), true); + mb_port_evt_wait_req_finish_ExpectAndReturn(pmb_base->port_obj, mb_err); + TEST_ESP_OK(mbc_master_start(mbm_handle)); + + const mb_parameter_descriptor_t *param_descriptor = NULL; + TEST_ESP_OK(mbc_master_get_cid_info(mbm_handle, par_index, ¶m_descriptor)); + TEST_ASSERT_EQUAL_HEX32(&descriptors[par_index], param_descriptor); + uint8_t type = 0; // type of parameter from dictionary + uint8_t reg_data[] = {0x11, 0x22, 0x33, 0x44}; + ESP_LOGI(TAG, "Test CID #%d, %s, %s", param_descriptor->cid, param_descriptor->param_key, param_descriptor->param_units); + mb_port_evt_res_take_ExpectAnyArgsAndReturn(true); + mb_port_evt_res_release_ExpectAnyArgs(); + mb_port_evt_res_take_ExpectAnyArgsAndReturn(true); + mb_port_evt_res_release_ExpectAnyArgs(); + + // Call the read method of modbus controller + esp_err_t err = mbc_master_set_parameter(mbm_handle, par_index, reg_data, &type); + uint8_t *mb_frame_ptr = NULL; + // get send buffer back using the fake mb_object + pmb_base->get_send_buf(pmb_base, &mb_frame_ptr); + TEST_ASSERT_EQUAL_HEX8(pmb_base->get_dest_addr(pmb_base), param_descriptor->mb_slave_addr); + uint8_t send_len = pmb_base->get_send_len(pmb_base); + // Check that request function forms correct buffer + switch (param_descriptor->mb_param_type) + { + case MB_PARAM_HOLDING: + TEST_ASSERT_EQUAL_HEX8(mb_frame_ptr[MB_PDU_FUNC_OFF], MB_FUNC_WRITE_MULTIPLE_REGISTERS); + TEST_ASSERT_EQUAL_HEX8(mb_frame_ptr[MB_PDU_REQ_WRITE_MUL_ADDR_OFF], (param_descriptor->mb_reg_start >> 8)); + TEST_ASSERT_EQUAL_HEX8(mb_frame_ptr[MB_PDU_REQ_WRITE_MUL_ADDR_OFF + 1], (param_descriptor->mb_reg_start & 0x00FF)); + TEST_ASSERT_EQUAL_HEX8(mb_frame_ptr[MB_PDU_REQ_WRITE_MUL_REGCNT_OFF], (param_descriptor->mb_size >> 8)); + TEST_ASSERT_EQUAL_HEX8(mb_frame_ptr[MB_PDU_REQ_WRITE_MUL_REGCNT_OFF + 1], (param_descriptor->mb_size & 0x00FF)); + TEST_ASSERT_EQUAL_HEX8(mb_frame_ptr[MB_PDU_REQ_WRITE_MUL_BYTECNT_OFF], (param_descriptor->mb_size << 1)); + TEST_ASSERT_EQUAL_HEX8(mb_frame_ptr[MB_PDU_REQ_WRITE_MUL_BYTECNT_OFF], sizeof(reg_data)); + TEST_ASSERT_EQUAL_HEX8(send_len, ((MB_PDU_SIZE_MIN + MB_PDU_REQ_WRITE_MUL_SIZE_MIN + 2 * param_descriptor->mb_size))); + for (int i = 0; (i < param_descriptor->mb_size); i++) + { + TEST_ASSERT_EQUAL_HEX8(reg_data[0], mb_frame_ptr[MB_PDU_REQ_WRITE_MUL_VALUES_OFF + 1]); + TEST_ASSERT_EQUAL_HEX8(reg_data[1], mb_frame_ptr[MB_PDU_REQ_WRITE_MUL_VALUES_OFF]); + } + ESP_LOG_BUFFER_HEX_LEVEL(TAG, (void *)mb_frame_ptr, send_len, ESP_LOG_INFO); + // TEST_ESP_ERR(MB_ENOERR, mbs_fn_write_holding_reg(pmb_base, mb_frame_ptr, &send_len)); + break; + case MB_PARAM_COIL: + TEST_ASSERT_EQUAL_HEX8(mb_frame_ptr[MB_PDU_FUNC_OFF], MB_FUNC_WRITE_MULTIPLE_COILS); + TEST_ASSERT_EQUAL_HEX8(mb_frame_ptr[MB_PDU_REQ_WRITE_MUL_ADDR_OFF], (param_descriptor->mb_reg_start >> 8)); + TEST_ASSERT_EQUAL_HEX8(mb_frame_ptr[MB_PDU_REQ_WRITE_MUL_ADDR_OFF + 1], (param_descriptor->mb_reg_start & 0x00FF)); + TEST_ASSERT_EQUAL_HEX8(mb_frame_ptr[MB_PDU_REQ_WRITE_MUL_COILCNT_OFF], (param_descriptor->mb_size >> 8)); + TEST_ASSERT_EQUAL_HEX8(mb_frame_ptr[MB_PDU_REQ_WRITE_MUL_COILCNT_OFF + 1], (param_descriptor->mb_size & 0x00FF)); + uint8_t byte_cnt = (param_descriptor->mb_size & 0x0007) ? ((param_descriptor->mb_size >> 3) + 1) : (param_descriptor->mb_size >> 3); + TEST_ASSERT_EQUAL_HEX8(mb_frame_ptr[MB_PDU_REQ_WRITE_MUL_BYTECNT_OFF], byte_cnt); + TEST_ASSERT_EQUAL_HEX8(send_len, (MB_PDU_SIZE_MIN + MB_PDU_REQ_WRITE_MUL_SIZE_MIN + byte_cnt)); + TEST_ASSERT_EQUAL_HEX8(reg_data[0], mb_frame_ptr[MB_PDU_REQ_WRITE_MUL_VALUES_OFF]); + ESP_LOG_BUFFER_HEX_LEVEL(TAG, (void *)mb_frame_ptr, send_len, ESP_LOG_INFO); + break; + default: + TEST_FAIL(); + break; + } + TEST_ESP_OK(mbc_master_stop(mbm_handle)); + TEST_ESP_OK(mbc_master_delete(mbm_handle)); + TEST_ASSERT_EQUAL_HEX(mb_port_get_inst_counter(), 0); + ESP_LOGI(TAG, "Test passed successfully."); + return err; +} + +static esp_err_t test_master_check_callback(int par_index, mb_err_enum_t mb_err) +{ + mb_communication_info_t master_config = { + .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_2, + .ser_opts.baudrate = 115200, + .ser_opts.parity = UART_PARITY_DISABLE, + .ser_opts.response_tout_ms = 1, + .ser_opts.test_tout_us = TEST_SLAVE_SEND_TOUT_US}; + mb_base_t *pmb_base = NULL; // fake mb_base handle + void *mbm_handle = NULL; + + TEST_ESP_ERR(MB_ENOERR, mb_stub_create(&master_config.ser_opts, (void *)&pmb_base)); + pmb_base->port_obj = (mb_port_base_t *)0x44556677; + mbm_rtu_create_ExpectAnyArgsAndReturn(MB_ENOERR); + mbm_rtu_create_ReturnThruPtr_in_out_obj((void **)&pmb_base); + TEST_ESP_OK(mbc_master_create_serial(&master_config, &mbm_handle)); + TEST_ESP_OK(mbc_master_set_descriptor(mbm_handle, &descriptors[0], num_descriptors)); + mbm_controller_iface_t *mbm_controller_iface = (mbm_controller_iface_t *)mbm_handle; + mb_master_options_t *mbm_opts = MB_MASTER_GET_OPTS(mbm_controller_iface); + TEST_ASSERT_EQUAL_HEX32(mbm_controller_iface->mb_base, pmb_base); + + TEST_ASSERT_EQUAL_HEX32(pmb_base->rw_cbs.reg_input_cb, mbc_reg_input_master_cb); + TEST_ASSERT_EQUAL_HEX32(pmb_base->rw_cbs.reg_holding_cb, mbc_reg_holding_master_cb); + TEST_ASSERT_EQUAL_HEX32(pmb_base->rw_cbs.reg_coils_cb, mbc_reg_coils_master_cb); + TEST_ASSERT_EQUAL_HEX32(pmb_base->rw_cbs.reg_discrete_cb, mbc_reg_discrete_master_cb); + + TEST_ESP_OK(mbc_master_start(mbm_handle)); + + const mb_parameter_descriptor_t *param_descriptor = NULL; + TEST_ESP_OK(mbc_master_get_cid_info(mbm_handle, par_index, ¶m_descriptor)); + TEST_ASSERT_EQUAL_HEX32(&descriptors[par_index], param_descriptor); + uint8_t reg_data_in[] = {0x11, 0x22, 0x33, 0x44}; + uint8_t reg_data_out[4] = {0}; + mbm_opts->reg_buffer_size = param_descriptor->mb_size; + mbm_opts->reg_buffer_ptr = ®_data_out[0]; + esp_err_t err = ESP_FAIL; + uint8_t byte_cnt = 0; + // Check that request function forms correct buffer + switch (param_descriptor->mb_param_type) + { + case MB_PARAM_HOLDING: + err = mbc_reg_holding_master_cb(pmb_base, reg_data_in, param_descriptor->mb_reg_start, + param_descriptor->mb_size, MB_REG_READ); + for (int i = 0; (i < param_descriptor->mb_size); i++) + { + TEST_ASSERT_EQUAL_HEX8(reg_data_in[(i << 1)], reg_data_out[(i << 1) + 1]); + TEST_ASSERT_EQUAL_HEX8(reg_data_in[(i << 1) + 1], reg_data_out[(i << 1)]); + } + ESP_LOG_BUFFER_HEX_LEVEL(TAG ", INPUT_BUFF", (void *)reg_data_in, (param_descriptor->mb_size << 1), ESP_LOG_INFO); + ESP_LOG_BUFFER_HEX_LEVEL(TAG ", OUTPUT_BUFF", (void *)reg_data_out, (param_descriptor->mb_size << 1), ESP_LOG_INFO); + break; + + case MB_PARAM_INPUT: + err = mbc_reg_input_master_cb(pmb_base, reg_data_in, param_descriptor->mb_reg_start, + param_descriptor->mb_size); + ESP_LOG_BUFFER_HEX_LEVEL(TAG ", INPUT_BUFF", (void *)reg_data_in, (param_descriptor->mb_size << 1), ESP_LOG_INFO); + for (int i = 0; (i < param_descriptor->mb_size); i++) + { + TEST_ASSERT_EQUAL_HEX8(reg_data_in[(i << 1)], reg_data_out[(i << 1) + 1]); + TEST_ASSERT_EQUAL_HEX8(reg_data_in[(i << 1) + 1], reg_data_out[(i << 1)]); + } + ESP_LOG_BUFFER_HEX_LEVEL(TAG ", OUTPUT_BUFF", (void *)reg_data_out, (param_descriptor->mb_size << 1), ESP_LOG_INFO); + break; + + case MB_PARAM_COIL: + reg_data_in[0] = 0xFF; + reg_data_in[1] = 0xFF; + err = mbc_reg_coils_master_cb(pmb_base, reg_data_in, param_descriptor->mb_reg_start, param_descriptor->mb_size, MB_REG_READ); + byte_cnt = (param_descriptor->mb_size & 0x0007) ? ((param_descriptor->mb_size >> 3) + 1) : (param_descriptor->mb_size >> 3); + ESP_LOG_BUFFER_HEX_LEVEL(TAG ", INPUT_BUFF", (void *)reg_data_in, byte_cnt, ESP_LOG_INFO); + TEST_ASSERT_EQUAL_HEX8((reg_data_out[0] & param_descriptor->param_opts.opt1), param_descriptor->param_opts.opt1); + TEST_ASSERT_EQUAL_HEX8((reg_data_out[1] & ((param_descriptor->param_opts.opt1 >> 8) & 0xFF)), ((param_descriptor->param_opts.opt1 >> 8) & 0xFF)); + ESP_LOG_BUFFER_HEX_LEVEL(TAG ", OUTPUT_BUFF", (void *)reg_data_out, byte_cnt, ESP_LOG_INFO); + break; + + case MB_PARAM_DISCRETE: + reg_data_in[0] = 0xFF; + reg_data_in[1] = 0xFF; + err = mbc_reg_discrete_master_cb(pmb_base, reg_data_in, param_descriptor->mb_reg_start, param_descriptor->mb_size); + byte_cnt = (param_descriptor->mb_size & 0x0007) ? ((param_descriptor->mb_size >> 3) + 1) : (param_descriptor->mb_size >> 3); + ESP_LOG_BUFFER_HEX_LEVEL(TAG ", INPUT_BUFF", (void *)reg_data_in, byte_cnt, ESP_LOG_INFO); + TEST_ASSERT_EQUAL_HEX8((reg_data_out[0] & param_descriptor->param_opts.opt1), param_descriptor->param_opts.opt1); + TEST_ASSERT_EQUAL_HEX8((reg_data_out[1] & ((param_descriptor->param_opts.opt1 >> 8) & 0xFF)), ((param_descriptor->param_opts.opt1 >> 8) & 0xFF)); + ESP_LOG_BUFFER_HEX_LEVEL(TAG ", OUTPUT_BUFF", (void *)reg_data_out, byte_cnt, ESP_LOG_INFO); + break; + + default: + break; + } + TEST_ESP_OK(mbc_master_stop(mbm_handle)); + TEST_ESP_OK(mbc_master_delete(mbm_handle)); + ESP_LOGI(TAG, "Test passed successfully."); + return err; +} + +// Check if modbus controller object forms correct modbus request from data dictionary +// and is able to transfer data using mb_object. Check possible errors returned back from +// mb_object and make sure the modbus controller handles them correctly. +TEST(unit_test_controller, test_master_send_read_request) +{ + ESP_LOGI(TAG, "TEST: Check the modbus master controller handles read requests correctly."); + TEST_ESP_ERR(ESP_OK, test_master_read_req(CID_DEV_REG0_INPUT, MB_ENOERR)); + TEST_ESP_ERR(ESP_ERR_TIMEOUT, test_master_read_req(CID_DEV_REG0_INPUT, MB_ETIMEDOUT)); + TEST_ESP_ERR(ESP_ERR_INVALID_RESPONSE, test_master_read_req(CID_DEV_REG0_INPUT, MB_ERECVDATA)); + TEST_ESP_ERR(ESP_OK, test_master_read_req(CID_DEV_REG0_HOLD, MB_ENOERR)); + TEST_ESP_ERR(ESP_ERR_INVALID_RESPONSE, test_master_read_req(CID_DEV_REG0_HOLD, MB_ERECVDATA)); + TEST_ESP_ERR(ESP_OK, test_master_read_req(CID_DEV_REG0_COIL, MB_ENOERR)); + TEST_ESP_ERR(ESP_ERR_TIMEOUT, test_master_read_req(CID_DEV_REG0_COIL, MB_ETIMEDOUT)); + TEST_ESP_ERR(ESP_OK, test_master_read_req(CID_DEV_REG0_DISCRITE, MB_ENOERR)); + TEST_ESP_ERR(ESP_ERR_TIMEOUT, test_master_read_req(CID_DEV_REG0_DISCRITE, MB_ETIMEDOUT)); +} + +TEST(unit_test_controller, test_master_send_write_request) +{ + ESP_LOGI(TAG, "TEST: Check the modbus master controller handles write requests correctly."); + TEST_ESP_ERR(ESP_OK, test_master_write_req(CID_DEV_REG0_HOLD, MB_ENOERR)); + TEST_ESP_ERR(ESP_ERR_INVALID_RESPONSE, test_master_write_req(CID_DEV_REG0_HOLD, MB_ERECVDATA)); + TEST_ESP_ERR(ESP_OK, test_master_write_req(CID_DEV_REG0_COIL, MB_ENOERR)); + TEST_ESP_ERR(ESP_ERR_TIMEOUT, test_master_write_req(CID_DEV_REG0_COIL, MB_ETIMEDOUT)); +} + +TEST(unit_test_controller, test_slave_check_area_descriptor) +{ + ESP_LOGI(TAG, "TEST: Check the modbus master controller defines the area descriptors correctly."); + test_slave_check_descriptor(CID_DEV_INPUT_AREA); + test_slave_check_descriptor(CID_DEV_HOLD_AREA); + test_slave_check_descriptor(CID_DEV_COIL_AREA); + test_slave_check_descriptor(CID_DEV_DISCR_AREA); +} + +TEST(unit_test_controller, test_master_register_callbacks) +{ + ESP_LOGI(TAG, "TEST: Check the modbus master controller handles mapping callback functions correctly."); + TEST_ESP_ERR(ESP_OK, test_master_check_callback(CID_DEV_REG0_HOLD, MB_ENOERR)); + TEST_ESP_ERR(ESP_OK, test_master_check_callback(CID_DEV_REG0_INPUT, MB_ENOERR)); + TEST_ESP_ERR(ESP_OK, test_master_check_callback(CID_DEV_REG0_COIL, MB_ENOERR)); + TEST_ESP_ERR(ESP_OK, test_master_check_callback(CID_DEV_REG0_DISCRITE, MB_ENOERR)); +} + +TEST_GROUP_RUNNER(unit_test_controller) +{ + RUN_TEST_CASE(unit_test_controller, test_master_send_read_request); + RUN_TEST_CASE(unit_test_controller, test_master_send_write_request); + RUN_TEST_CASE(unit_test_controller, test_master_register_callbacks); + RUN_TEST_CASE(unit_test_controller, test_slave_check_area_descriptor); +} diff --git a/test_apps/unit_tests/mb_controller_mapping/pytest_mb_controller_mapping.py b/test_apps/unit_tests/mb_controller_mapping/pytest_mb_controller_mapping.py new file mode 100644 index 0000000..24ce1aa --- /dev/null +++ b/test_apps/unit_tests/mb_controller_mapping/pytest_mb_controller_mapping.py @@ -0,0 +1,16 @@ +# SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: CC0-1.0 + +import pytest +from pytest_embedded import Dut + + +CONFIGS = [ + pytest.param('serial', marks=[pytest.mark.esp32, pytest.mark.esp32s2, pytest.mark.esp32s3, pytest.mark.esp32c3]), +] + + +@pytest.mark.generic_multi_device +@pytest.mark.parametrize('config', CONFIGS, indirect=True) +def test_modbus_controller_mapping(dut: Dut) -> None: + dut.expect_unity_test_output() diff --git a/test_apps/unit_tests/mb_controller_mapping/sdkconfig.ci.serial b/test_apps/unit_tests/mb_controller_mapping/sdkconfig.ci.serial new file mode 100644 index 0000000..927e15a --- /dev/null +++ b/test_apps/unit_tests/mb_controller_mapping/sdkconfig.ci.serial @@ -0,0 +1,19 @@ +# This file was generated using idf.py save-defconfig. It can be edited manually. +# Espressif IoT Development Framework (ESP-IDF) Project Minimal Configuration +# +# +# Modbus configuration +# +CONFIG_UNITY_ENABLE_FIXTURE=y +CONFIG_FMB_PORT_TASK_STACK_SIZE=4096 +CONFIG_FMB_PORT_TASK_PRIO=10 +CONFIG_FMB_COMM_MODE_RTU_EN=y +CONFIG_FMB_COMM_MODE_ASCII_EN=y +CONFIG_FMB_COMM_MODE_TCP_EN=n +CONFIG_FMB_MASTER_TIMEOUT_MS_RESPOND=2000 +CONFIG_FMB_MASTER_DELAY_MS_CONVERT=300 +CONFIG_FMB_TIMER_USE_ISR_DISPATCH_METHOD=y +CONFIG_MB_PORT_ADAPTER_EN=y +CONFIG_MB_TEST_LEAK_CRITICAL_LEVEL=128 +CONFIG_MB_TEST_LEAK_WARN_LEVEL=128 + diff --git a/test_apps/unit_tests/mb_controller_mapping/sdkconfig.default b/test_apps/unit_tests/mb_controller_mapping/sdkconfig.default new file mode 100644 index 0000000..61feae0 --- /dev/null +++ b/test_apps/unit_tests/mb_controller_mapping/sdkconfig.default @@ -0,0 +1,17 @@ +# This file was generated using idf.py save-defconfig. It can be edited manually. +# Espressif IoT Development Framework (ESP-IDF) Project Minimal Configuration +# +# +# Modbus configuration +# +CONFIG_UNITY_ENABLE_FIXTURE=y +CONFIG_FMB_PORT_TASK_STACK_SIZE=4096 +CONFIG_FMB_PORT_TASK_PRIO=10 +CONFIG_FMB_COMM_MODE_RTU_EN=y +CONFIG_FMB_COMM_MODE_ASCII_EN=y +CONFIG_FMB_COMM_MODE_TCP_EN=n +CONFIG_FMB_MASTER_TIMEOUT_MS_RESPOND=2000 +CONFIG_FMB_MASTER_DELAY_MS_CONVERT=300 +CONFIG_FMB_TIMER_USE_ISR_DISPATCH_METHOD=y +CONFIG_MB_PORT_ADAPTER_EN=y + diff --git a/test_apps/unit_tests/test_stubs/CMakeLists.txt b/test_apps/unit_tests/test_stubs/CMakeLists.txt new file mode 100644 index 0000000..cf9fd3d --- /dev/null +++ b/test_apps/unit_tests/test_stubs/CMakeLists.txt @@ -0,0 +1,6 @@ +cmake_minimum_required(VERSION 3.16) +set(srcs "src/mb_object_stub.c") + +idf_component_register( SRCS ${srcs} + INCLUDE_DIRS "include" + REQUIRES test_common unity) diff --git a/test_apps/unit_tests/test_stubs/include/mb_object_stub.h b/test_apps/unit_tests/test_stubs/include/mb_object_stub.h new file mode 100644 index 0000000..873a47e --- /dev/null +++ b/test_apps/unit_tests/test_stubs/include/mb_object_stub.h @@ -0,0 +1,14 @@ +/* + * SPDX-FileCopyrightText: 2018-2021 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include "esp_log.h" +#include "mb_common.h" +#include "mb_port_types.h" + +mb_err_enum_t mb_stub_create(mb_serial_opts_t *ser_opts, void **in_out_obj); diff --git a/test_apps/unit_tests/test_stubs/include/transport_common.h b/test_apps/unit_tests/test_stubs/include/transport_common.h new file mode 100644 index 0000000..02d7f61 --- /dev/null +++ b/test_apps/unit_tests/test_stubs/include/transport_common.h @@ -0,0 +1,46 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include "mb_types.h" +#include "port_common.h" +#include "mb_port_types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct mb_trans_base_t mb_trans_base_t; /*!< Type of moddus transport object */ +typedef struct _obj_descr obj_descr_t; + +typedef void (*mb_frm_start_fp)(mb_trans_base_t *transport); +typedef void (*mb_frm_stop_fp)(mb_trans_base_t *transport); +typedef mb_err_enum_t (*mb_frm_rcv_fp)(mb_trans_base_t *transport, uint8_t *rcv_addr_buf, uint8_t **frame_ptr_buf, uint16_t *len_buf); +typedef mb_err_enum_t (*mb_frm_snd_fp)(mb_trans_base_t *transport, uint8_t slv_addr, const uint8_t *frame_ptr, uint16_t len); +typedef void (*mb_get_rx_frm_fp) (mb_trans_base_t *transport, uint8_t **frame_ptr_buf); +typedef void (*mb_get_tx_frm_fp) (mb_trans_base_t *transport, uint8_t **frame_ptr_buf); +typedef bool (*mb_get_fp)(mb_trans_base_t *inst); + +struct mb_trans_base_t +{ + obj_descr_t descr; + + _lock_t lock; + mb_port_base_t *port_obj; + + mb_frm_start_fp frm_start; + mb_frm_stop_fp frm_stop; + mb_get_fp frm_delete; + mb_frm_snd_fp frm_send; + mb_frm_rcv_fp frm_rcv; + mb_get_rx_frm_fp get_rx_frm; + mb_get_rx_frm_fp get_tx_frm; + mb_get_fp frm_is_bcast; +}; //!< Transport methods + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/test_apps/unit_tests/test_stubs/src/mb_object_stub.c b/test_apps/unit_tests/test_stubs/src/mb_object_stub.c new file mode 100644 index 0000000..eb4f259 --- /dev/null +++ b/test_apps/unit_tests/test_stubs/src/mb_object_stub.c @@ -0,0 +1,159 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "mb_config.h" +#include "mb_common.h" +#include "mb_proto.h" +#include "mb_func.h" +#include "mb_master.h" +#include "transport_common.h" +#include "port_common.h" +#include "ascii_transport.h" +#include "rtu_transport.h" +#include "tcp_transport.h" + +static const char *TAG = "mb_object.master.stub"; + +#if (MB_MASTER_ASCII_ENABLED || MB_MASTER_RTU_ENABLED || MB_MASTER_TCP_ENABLED) + +typedef struct +{ + mb_base_t base; + uint16_t pdu_snd_len; + uint8_t dst_addr; + uint8_t snd_buf[MB_RTU_SER_PDU_SIZE_MAX]; +} mb_object_t; + +mb_err_enum_t mb_tcp_create(mb_tcp_opts_t *tcp_opts, void **in_out_obj); + +mb_err_enum_t mb_delete(mb_base_t *inst); +mb_err_enum_t mb_enable(mb_base_t *inst); +mb_err_enum_t mb_disable(mb_base_t *inst); +mb_err_enum_t mb_poll(mb_base_t *inst); + +static void mb_set_pdu_send_length(mb_base_t *inst, uint16_t length); +static uint16_t mb_get_pdu_send_length(mb_base_t *inst); +static void mb_set_dest_addr(mb_base_t *inst, uint8_t dest_addr); +static uint8_t mb_get_dest_addr(mb_base_t *inst); +static void mb_get_pdu_send_buf(mb_base_t *inst, uint8_t **pbuf); + +#if (MB_MASTER_ASCII_ENABLED || MB_MASTER_RTU_ENABLED) + +typedef struct _port_serial_opts mb_serial_opts_t; + +mb_err_enum_t mb_stub_create(mb_serial_opts_t *ser_opts, void **in_out_obj) +{ + MB_RETURN_ON_FALSE((ser_opts && in_out_obj), MB_EINVAL, TAG, "invalid options for the instance."); + mb_err_enum_t ret = MB_ENOERR; + mb_object_t *mb_obj = NULL; + mb_obj = (mb_object_t *)calloc(1, sizeof(mb_object_t)); + MB_GOTO_ON_FALSE((mb_obj), MB_EILLSTATE, error, TAG, "no mem for mb master instance."); + CRITICAL_SECTION_INIT(mb_obj->base.lock); + ESP_LOGW(TAG, "Create fake mb_base object."); + mb_obj->base.delete = mb_delete; + mb_obj->base.enable = mb_enable; + mb_obj->base.disable = mb_disable; + mb_obj->base.poll = mb_poll; + mb_obj->base.set_dest_addr = mb_set_dest_addr; + mb_obj->base.get_dest_addr = mb_get_dest_addr; + mb_obj->base.set_send_len = mb_set_pdu_send_length; + mb_obj->base.get_send_len = mb_get_pdu_send_length; + mb_obj->base.get_send_buf = mb_get_pdu_send_buf; + mb_obj->base.descr.parent = *in_out_obj; + mb_obj->base.descr.is_master = true; + mb_obj->base.descr.obj_name = (char *)TAG; + mb_obj->base.descr.inst_index = mb_port_get_inst_counter_inc(); + *in_out_obj = (void *)mb_obj; + return MB_ENOERR; + +error: + CRITICAL_SECTION_CLOSE(mb_obj->base.lock); + free(mb_obj); + mb_port_get_inst_counter_dec(); + ESP_LOGW(TAG, "Delete fake mb_base object."); + return ret; +} + +#endif + +mb_err_enum_t mb_delete(mb_base_t *inst) +{ + mb_object_t *mb_obj = __containerof(inst, mb_object_t, base); + CRITICAL_SECTION_CLOSE(mb_obj->base.lock); + free(mb_obj); + mb_port_get_inst_counter_dec(); + ESP_LOGW(TAG, "Delete fake mb_base object."); + return MB_ENOERR; +} + +mb_err_enum_t mb_enable(mb_base_t *inst) +{ + ESP_LOGW(TAG, "Enable fake mb_base object."); + mb_err_enum_t status = MB_ENOERR; + return status; +} + +mb_err_enum_t mb_disable(mb_base_t *inst) +{ + mb_err_enum_t status = MB_ENOERR; + ESP_LOGW(TAG, "Disable fake mb_base object."); + return status; +} + +static void mb_get_pdu_send_buf(mb_base_t *inst, uint8_t **pbuf) +{ + mb_object_t *mb_obj = __containerof(inst, mb_object_t, base); + if (pbuf) { + *pbuf = mb_obj->snd_buf; + } +} + +// __attribute__((unused)) +// static void mb_get_pdu_recv_buf(mb_base_t *inst, uint8_t **pbuf) +// { +// //mb_object_t *mb_obj = __containerof(inst, mb_object_t, base); +// } + +static void mb_set_pdu_send_length(mb_base_t *inst, uint16_t length) +{ + mb_object_t *mb_obj = __containerof(inst, mb_object_t, base); + mb_obj->pdu_snd_len = length; +} + +__attribute__((unused)) +static uint16_t mb_get_pdu_send_length(mb_base_t *inst) +{ + mb_object_t *mb_obj = __containerof(inst, mb_object_t, base); + return mb_obj->pdu_snd_len; +} + +static void mb_set_dest_addr(mb_base_t *inst, uint8_t dest_addr) +{ + mb_object_t *mb_obj = __containerof(inst, mb_object_t, base); + mb_obj->dst_addr = dest_addr; +} + +static uint8_t mb_get_dest_addr(mb_base_t *inst) +{ + mb_object_t *mb_obj = __containerof(inst, mb_object_t, base); + return mb_obj->dst_addr; +} + +mb_err_enum_t mb_poll(mb_base_t *inst) +{ + mb_err_enum_t status = MB_ENOERR; + ESP_LOGW(TAG, "Poll function called of fake mb_base object."); + //mb_object_t *mb_obj = __containerof(inst, mb_object_t, base); + vTaskDelay(1); + //mb_port_evt_get_ExpectAnyArgsAndReturn(true); + // mb_event_t event = {0}; + // if (mb_port_evt_get(mb_obj->base.port_obj, &event)) { + + // } + return status; +} + +#endif diff --git a/test_apps/unit_tests/test_stubs/src/mb_transport_stub.c b/test_apps/unit_tests/test_stubs/src/mb_transport_stub.c new file mode 100644 index 0000000..5112dec --- /dev/null +++ b/test_apps/unit_tests/test_stubs/src/mb_transport_stub.c @@ -0,0 +1,300 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "rtu_transport.h" +#include "port_serial_common.h" +#include "port_common.h" + +#include "mb_config.h" + +#if (CONFIG_FMB_COMM_MODE_RTU_EN) + +static const char *TAG = "mb_transp.rtu_master"; + +typedef struct +{ + mb_trans_base_t base; + mb_port_base_t *port_obj; + uint8_t snd_buf[MB_RTU_SER_PDU_SIZE_MAX]; + uint8_t rcv_buf[MB_RTU_SER_PDU_SIZE_MAX]; + uint16_t snd_pdu_len; + uint8_t *snd_buf_cur; + uint16_t snd_buf_cnt; + uint16_t rcv_buf_pos; + bool frame_is_broadcast; + volatile mb_tmr_mode_enum_t cur_tmr_mode; + mb_rtu_state_enum_t state; +} mbm_rtu_transp_t; + +mb_err_enum_t mbm_rtu_transp_create(mb_serial_opts_t *ser_opts, void **in_out_inst); +static void mbm_rtu_transp_start(mb_trans_base_t *inst); +static void mbm_rtu_transp_stop(mb_trans_base_t *inst); +static mb_err_enum_t mbm_rtu_transp_receive(mb_trans_base_t *inst, uint8_t *rcv_addr_buf, uint8_t **frame_ptr_buf, uint16_t *len_buf); +static mb_err_enum_t mbm_rtu_transp_send(mb_trans_base_t *inst, uint8_t slv_addr, const uint8_t *frame_ptr, uint16_t len); +static bool mbm_rtu_transp_rcv_fsm(mb_trans_base_t *inst); +static bool mbm_rtu_transp_snd_fsm(mb_trans_base_t *inst); +static bool mbm_rtu_transp_tmr_35_expired(void *inst); +static void mbm_rtu_transp_get_snd_buf(mb_trans_base_t *inst, uint8_t **frame_ptr_buf); +static void mbm_rtu_transp_get_rcv_buf(mb_trans_base_t *inst, uint8_t **frame_ptr_buf); +// static uint16_t mbm_rtu_transp_get_snd_len(mb_trans_base_t *inst); +static void mbm_rtu_transp_set_snd_len(mb_trans_base_t *inst, uint16_t snd_pdu_len); +static bool mbm_rtu_transp_rq_is_bcast(mb_trans_base_t *inst); +bool mbm_rtu_transp_delete(mb_trans_base_t *inst); + +mb_err_enum_t mbm_rtu_transp_create(mb_serial_opts_t *ser_opts, void **in_out_inst) +{ + MB_RETURN_ON_FALSE((ser_opts && in_out_inst), MB_EINVAL, TAG, "invalid options for the instance."); + mb_err_enum_t ret = MB_ENOERR; + mbm_rtu_transp_t *transp = NULL; + transp = (mbm_rtu_transp_t *)calloc(1, sizeof(mbm_rtu_transp_t)); + MB_RETURN_ON_FALSE(transp, MB_EILLSTATE, TAG, "no mem for %s instance.", TAG); + CRITICAL_SECTION_INIT(transp->base.lock); + CRITICAL_SECTION_LOCK(transp->base.lock); + transp->base.frm_rcv = mbm_rtu_transp_receive; + transp->base.frm_send = mbm_rtu_transp_send; + transp->base.frm_start = mbm_rtu_transp_start; + transp->base.frm_stop = mbm_rtu_transp_stop; + transp->base.get_rx_frm = mbm_rtu_transp_get_rcv_buf; + transp->base.get_tx_frm = mbm_rtu_transp_get_snd_buf; + transp->base.frm_delete = mbm_rtu_transp_delete; + transp->base.frm_is_bcast = mbm_rtu_transp_rq_is_bcast; + // Copy parent object descriptor + transp->base.descr = ((mb_port_base_t *)*in_out_inst)->descr; + transp->base.descr.obj_name = (char *)TAG; + mb_port_base_t *port_obj = (mb_port_base_t *)*in_out_inst; + ret = mb_port_ser_create(ser_opts, &port_obj); + MB_GOTO_ON_FALSE((ret == MB_ENOERR), MB_EILLSTATE, error, TAG, "serial port creation, err: %d", ret); + ret = mb_port_tmr_create(port_obj, MB_RTU_GET_T35_VAL(ser_opts->baudrate)); + MB_GOTO_ON_FALSE((ret == MB_ENOERR), MB_EILLSTATE, error, TAG, "timer port creation, err: %d", ret); + // Override default response time if defined + if (ser_opts->response_tout_ms) { + mb_port_tmr_set_response_time(port_obj, ser_opts->response_tout_ms); + } + ret = mb_port_evt_create(port_obj); + MB_GOTO_ON_FALSE((ret == MB_ENOERR), MB_EILLSTATE, error, TAG, "event port creation, err: %d", ret); + transp->base.port_obj = port_obj; + // Set callback function pointer for the timer + port_obj->cb.tmr_expired = mbm_rtu_transp_tmr_35_expired; + port_obj->cb.tx_empty = NULL; + port_obj->cb.byte_rcvd = NULL; + port_obj->arg = (void *)transp; + transp->port_obj = port_obj; // register the created port object + *in_out_inst = &(transp->base); + ESP_LOGD(TAG, "created %s object @%p", TAG, transp); + CRITICAL_SECTION_UNLOCK(transp->base.lock); + return MB_ENOERR; + +error: + if (port_obj->timer_obj) { + mb_port_tmr_delete(port_obj); + } + if (port_obj->event_obj) { + mb_port_evt_delete(port_obj); + } + if (port_obj) { + mb_port_ser_delete(port_obj); + } + CRITICAL_SECTION_CLOSE(transp->base.lock); + free(transp); + return ret; +} + +bool mbm_rtu_transp_delete(mb_trans_base_t *inst) +{ + mbm_rtu_transp_t *transp = __containerof(inst, mbm_rtu_transp_t, base); + mb_port_ser_delete(transp->base.port_obj); + mb_port_tmr_delete(transp->base.port_obj); + mb_port_evt_delete(transp->base.port_obj); + CRITICAL_SECTION_CLOSE(inst->lock); + free(transp); + return true; +} + +static void mbm_rtu_transp_start(mb_trans_base_t *inst) +{ + mbm_rtu_transp_t *transp = __containerof(inst, mbm_rtu_transp_t, base); + transp->state = MB_RTU_STATE_INIT; + CRITICAL_SECTION(inst->lock) { + mb_port_ser_enable(inst->port_obj); + mb_port_tmr_enable(inst->port_obj); + }; + /* No special startup required for RTU. */ + (void)mb_port_evt_post(transp->base.port_obj, EVENT(EV_READY)); +} + +static void mbm_rtu_transp_stop(mb_trans_base_t *inst) +{ + CRITICAL_SECTION(inst->lock) { + mb_port_ser_disable(inst->port_obj); + mb_port_tmr_disable(inst->port_obj); + }; +} + +static mb_err_enum_t mbm_rtu_transp_receive(mb_trans_base_t *inst, uint8_t *prcv_addr, uint8_t **ppframe_buf, uint16_t *pbuf_len) +{ + mbm_rtu_transp_t *transp = __containerof(inst, mbm_rtu_transp_t, base); + + if (!pbuf_len || !prcv_addr || !ppframe_buf || !pbuf_len) { + return MB_EIO; + } + + mb_err_enum_t status = MB_ENOERR; + + uint8_t *pbuf = (uint8_t *)transp->rcv_buf; + uint16_t length = *pbuf_len; + + if (mb_port_ser_recv_data(inst->port_obj, &pbuf, &length) == false) { + *pbuf_len = 0; + return MB_EPORTERR; + } + + assert(length < MB_RTU_SER_PDU_SIZE_MAX); + assert(pbuf); + + /* Check length and CRC checksum */ + if ((length >= MB_RTU_SER_PDU_SIZE_MIN) + && (mb_crc16((uint8_t *)pbuf, length) == 0)) { + /* Save the address field. All frames are passed to the upper layed + * and the decision if a frame is used is done there. + */ + *prcv_addr = pbuf[MB_SER_PDU_ADDR_OFF]; + + /* Total length of Modbus-PDU is Modbus-Serial-Line-PDU minus + * size of address field and CRC checksum. + */ + *pbuf_len = (uint16_t)(length - MB_SER_PDU_PDU_OFF - MB_SER_PDU_SIZE_CRC); + transp->rcv_buf_pos = length; + + /* Return the start of the Modbus PDU to the caller. */ + *ppframe_buf = (uint8_t *)&pbuf[MB_SER_PDU_PDU_OFF]; + } else { + status = MB_EIO; + } + return status; +} + +static mb_err_enum_t mbm_rtu_transp_send(mb_trans_base_t *inst, uint8_t slv_addr, const uint8_t *frame_ptr, uint16_t frame_len) +{ + mbm_rtu_transp_t *transp = __containerof(inst, mbm_rtu_transp_t, base); + mb_err_enum_t status = MB_ENOERR; + uint16_t crc16 = 0; + + if (slv_addr > MB_MASTER_TOTAL_SLAVE_NUM) { + return MB_EINVAL; + } + + if (frame_ptr && frame_len) { + /* First byte before the Modbus-PDU is the slave address. */ + transp->snd_buf_cur = (uint8_t *)frame_ptr - 1; + transp->snd_buf_cnt = 1; + + /* Now copy the Modbus-PDU into the Modbus-Serial-Line-PDU. */ + transp->snd_buf_cur[MB_SER_PDU_ADDR_OFF] = slv_addr; + transp->snd_buf_cnt += frame_len; + /* Calculate CRC16 checksum for Modbus-Serial-Line-PDU. */ + crc16 = mb_crc16((uint8_t *) transp->snd_buf_cur, transp->snd_buf_cnt); + transp->snd_buf_cur[transp->snd_buf_cnt++] = (uint8_t)(crc16 & 0xFF); + transp->snd_buf_cur[transp->snd_buf_cnt++] = (uint8_t)(crc16 >> 8); + + bool ret = mb_port_ser_send_data(inst->port_obj, (uint8_t *)transp->snd_buf_cur, transp->snd_buf_cnt); + if (!ret) { + return MB_EPORTERR; + } + transp->frame_is_broadcast = (slv_addr == MB_ADDRESS_BROADCAST) ? true : false; + // If the frame is broadcast, master will enable timer of convert delay, + // else master will enable timer of respond timeout. */ + if (transp->frame_is_broadcast) { + mb_port_tmr_convert_delay_enable(transp->base.port_obj); + } else { + mb_port_tmr_respond_timeout_enable(transp->base.port_obj); + } + + } else { + status = MB_EIO; + } + return status; +} + +__attribute__((unused)) +static bool mbm_rtu_transp_rcv_fsm(mb_trans_base_t *inst) +{ + return false; +} + +__attribute__((unused)) +static bool mbm_rtu_transp_snd_fsm(mb_trans_base_t *inst) +{ + return false; +} + + +static bool mbm_rtu_transp_tmr_35_expired(void *inst) +{ + mbm_rtu_transp_t *transp = __containerof(inst, mbm_rtu_transp_t, base); + + bool need_poll = false; + mb_tmr_mode_enum_t timer_mode = mb_port_get_cur_tmr_mode(transp->base.port_obj); + + mb_port_tmr_disable(transp->base.port_obj); + + switch(timer_mode) { + case MB_TMODE_T35: + //need_poll = mb_port_evt_post(transp->base.port_obj, EVENT(EV_READY)); + //ESP_EARLY_LOGD(TAG, "%p:EV_READY", transp->base.descr.parent); + break; + + case MB_TMODE_RESPOND_TIMEOUT: + mb_port_evt_set_err_type(transp->base.port_obj, EV_ERROR_RESPOND_TIMEOUT); + need_poll = mb_port_evt_post(transp->base.port_obj, EVENT(EV_ERROR_PROCESS)); + ESP_EARLY_LOGW(TAG, "%p:EV_ERROR_RESPOND_TIMEOUT", transp->base.descr.parent); + break; + + case MB_TMODE_CONVERT_DELAY: + /* If timer mode is convert delay, the master event then turns EV_MASTER_EXECUTE status. */ + need_poll = mb_port_evt_post(transp->base.port_obj, EVENT(EV_EXECUTE)); + ESP_EARLY_LOGD(TAG, "%p:MB_TMODE_CONVERT_DELAY", transp->base.descr.parent); + break; + + default: + need_poll = mb_port_evt_post(transp->base.port_obj, EVENT(EV_READY)); + break; + } + + return need_poll; +} + +static void mbm_rtu_transp_get_rcv_buf(mb_trans_base_t *inst, uint8_t **frame_ptr_buf) +{ + mbm_rtu_transp_t *transp = __containerof(inst, mbm_rtu_transp_t, base); + CRITICAL_SECTION(inst->lock) { + *frame_ptr_buf = (uint8_t *)&transp->rcv_buf[MB_PDU_FUNC_OFF]; + } +} + +static void mbm_rtu_transp_get_snd_buf(mb_trans_base_t *inst, uint8_t **frame_ptr_buf) +{ + mbm_rtu_transp_t *transp = __containerof(inst, mbm_rtu_transp_t, base); + CRITICAL_SECTION(inst->lock) { + *frame_ptr_buf = (uint8_t *)&transp->snd_buf[MB_RTU_SER_PDU_PDU_OFF]; + } +} + +__attribute__((unused)) +static void mbm_rtu_transp_set_snd_len(mb_trans_base_t *inst, uint16_t snd_pdu_len) +{ + mbm_rtu_transp_t *transp = __containerof(inst, mbm_rtu_transp_t, base); + CRITICAL_SECTION(inst->lock) { + transp->snd_buf_cnt = snd_pdu_len; + } +} + +static bool mbm_rtu_transp_rq_is_bcast(mb_trans_base_t *inst) +{ + mbm_rtu_transp_t *transp = __containerof(inst, mbm_rtu_transp_t, base); + return transp->frame_is_broadcast; +} + +#endif