diff --git a/components/mb_console_helper/CMakeLists.txt b/components/mb_console_helper/CMakeLists.txt new file mode 100644 index 0000000..1497451 --- /dev/null +++ b/components/mb_console_helper/CMakeLists.txt @@ -0,0 +1,8 @@ +idf_component_register(SRCS "mb_console.c" + INCLUDE_DIRS "include" + REQUIRES console + LDFRAGMENTS linker.lf) + +if(CONFIG_MB_CONSOLE_CMD_AUTO_REGISTRATION) + target_link_libraries(${COMPONENT_LIB} PRIVATE "-u mb_console_cmd_mb_register") +endif() diff --git a/components/mb_console_helper/Kconfig.projbuild b/components/mb_console_helper/Kconfig.projbuild new file mode 100644 index 0000000..6c2b0df --- /dev/null +++ b/components/mb_console_helper/Kconfig.projbuild @@ -0,0 +1,16 @@ +menu "Modbus Console Helper" + + config MB_CONSOLE_HELPER_ENABLED + bool "Enable console helper in examples" + default y + help + Enable the helper component to track and send commands + via the console (UART/USB). + + config MB_CONSOLE_CMD_AUTO_REGISTRATION + bool "Enable Console command Modbus Auto-registration" + default y + help + Enabling this allows for the autoregistration of the Modbus command. + +endmenu diff --git a/components/mb_console_helper/idf_component.yml b/components/mb_console_helper/idf_component.yml new file mode 100644 index 0000000..f13fe32 --- /dev/null +++ b/components/mb_console_helper/idf_component.yml @@ -0,0 +1,3 @@ +dependencies: + idf: + version: ">=5.0" diff --git a/components/mb_console_helper/include/mb_console.h b/components/mb_console_helper/include/mb_console.h new file mode 100644 index 0000000..04240d4 --- /dev/null +++ b/components/mb_console_helper/include/mb_console.h @@ -0,0 +1,82 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include +#include "esp_err.h" +#include "sdkconfig.h" + +#define MB_CMD_NO_EVENTS 0 +#define MB_CMD_START BIT0 +#define MB_CMD_STOP BIT1 +#define MB_CMD_CONFIG_END BIT2 +#define MB_CMD_MAX_CFG_COUNT 50 + +#if __cplusplus +extern "C" { +#endif + + /* This structure describes the plugin to the rest of the application */ + typedef struct { + /* A pointer to the name of the command */ + const char *name; + + /* A function which performs auto-registration of console commands */ + esp_err_t (*plugin_regd_fn)(void); + } console_cmd_plugin_desc_t; + + /** + * @brief Initialize the console helper component + * + * @param callback Function to call when a command is processed + * @return + * - ESP_OK - initialization is completed, otherwise reports the error code. + */ + esp_err_t mb_console_init(); + + /** + * @brief Check for console input message during timeout + * + * @param event console event corresponded to command + * @param tout_ms timeout in milliseconds to wait for the event + * + * @return event bits that were set, or MB_CMD_NO_EVENTS if timeout occurred without receiving the event + */ + int mb_console_event_check(int event, uint32_t tout_ms); + +#if CONFIG_MB_CONSOLE_HELPER_ENABLED + /** + * @brief Registers the mb command. + * + * @return + * - ESP_OK - command registration is completed, otherwise reports the error code. + */ + esp_err_t mb_console_cmd_mb_register(void); + + /** + * @brief Add legacy configuration registration function + * @param config_table - pointer to the configuration table, which is an array of strings with NULL terminator. + * The console helper will update the entries in this table with the values received from the console. + * @return + * - ESP_OK - command registration is completed, otherwise reports the error code. + */ + esp_err_t mb_console_register_configs(char **config_table); + + /** + * @brief Destroy modbus console + * + * @return + * - ESP_OK - destroy is completed successfully, , otherwise reports the error code. + */ + esp_err_t mb_console_destroy(void); + +#else +#error "Console helper is disabled." +#endif + +#if __cplusplus +} +#endif diff --git a/components/mb_console_helper/linker.lf b/components/mb_console_helper/linker.lf new file mode 100644 index 0000000..3a6ff24 --- /dev/null +++ b/components/mb_console_helper/linker.lf @@ -0,0 +1,13 @@ +[sections:console_cmd_desc] +entries: + .console_cmd_desc + +[scheme:console_cmd_desc_default] +entries: + console_cmd_desc -> flash_rodata + +[mapping:console_cmd_desc] +archive: * +entries: + * (console_cmd_desc_default); + console_cmd_desc -> flash_rodata KEEP() SORT(name) SURROUND(console_cmd_array) diff --git a/components/mb_console_helper/mb_console.c b/components/mb_console_helper/mb_console.c new file mode 100644 index 0000000..630b869 --- /dev/null +++ b/components/mb_console_helper/mb_console.c @@ -0,0 +1,451 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/event_groups.h" +#include "freertos/semphr.h" + +#include "esp_console.h" +#include "esp_log.h" +#include "argtable3/argtable3.h" + +#include "mb_console.h" + +#if CONFIG_MB_CONSOLE_HELPER_ENABLED + +static const char *TAG = "mb_console"; + +/* FreeRTOS event group to command received */ +static EventGroupHandle_t s_mb_event_group; +// Mutex to protect configuration table updates +static SemaphoreHandle_t s_config_table_lock; + +#if CONFIG_MB_CONSOLE_CMD_AUTO_REGISTRATION + +static char **s_config_table = NULL; +static esp_console_repl_t *repl = NULL; + +static struct { + struct arg_str *config_str; + struct arg_end *end; +} add_config_args; + +// Supports simple Modbus command arguments for now +static struct { + struct arg_str *command; + struct arg_str *instance; + struct arg_end *end; +} mb_args; + +esp_err_t mb_console_cmd_mb_register(void); + +/** + * Static registration of this plugin is achieved by defining the plugin description + * structure and placing it into .console_cmd_desc section. + * The name of the section and its placement is determined by linker.lf file in 'plugins' component. + */ +static const console_cmd_plugin_desc_t __attribute__((section(".console_cmd_desc"), used)) PLUGIN = { + .name = "console_cmd_mb", + .plugin_regd_fn = &mb_console_cmd_mb_register +}; +#endif + +static int do_mb_cmd(int argc, char **argv) +{ + int nerrors = arg_parse(argc, argv, (void **)&mb_args); + if (nerrors != 0) { + arg_print_errors(stderr, mb_args.end, argv[0]); + return 1; + } + const char *inst = mb_args.instance->sval[0]; + if (strcmp(inst, "instances") == 0 || + strcmp(inst, "masters") == 0 || + strcmp(inst, "slaves") == 0) { + if (strcmp(mb_args.command->sval[0], "start") == 0) { + ESP_LOGI(TAG, "Start modbus %s.", mb_args.instance->sval[0]); + xEventGroupSetBits(s_mb_event_group, MB_CMD_START); + return 0; + } else if (strcmp(mb_args.command->sval[0], "stop") == 0) { + ESP_LOGI(TAG, "Stop modbus %s.", mb_args.instance->sval[0]); + xEventGroupSetBits(s_mb_event_group, MB_CMD_STOP); + return 0; + } + } + + return 1; +} + +static char *console_cmd_scan_config(int *index, uint16_t *port_ptr, const char *buffer) +{ + if (!buffer || !index) { + return NULL; + } + char *ip_str = NULL; + int a[8] = {0}; + int buf_cnt = 0; + uint16_t port_val = 0; +#if !CONFIG_EXAMPLE_CONNECT_IPV6 + buf_cnt = sscanf(buffer, "%d=%d.%d.%d.%d;%" PRIu16, index, &a[0], &a[1], &a[2], &a[3], &port_val); + if (buf_cnt == 6) { + if (-1 == asprintf(&ip_str, "%02x;%d.%d.%d.%d;%" PRIu16, (int)(*index + 1), a[0], a[1], a[2], a[3], port_val)) { + abort(); + } + } else if (buf_cnt == 5) { + if (-1 == asprintf(&ip_str, "%02x;%d.%d.%d.%d", (int)(*index + 1), a[0], a[1], a[2], a[3])) { + abort(); + } + } else { + return NULL; + } +#else + buf_cnt = sscanf(buffer, "%d=%04x:%04x:%04x:%04x:%04x:%04x:%04x:%04x;%" PRIu16, index, &a[0], &a[1], &a[2], &a[3], &a[4], &a[5], &a[6], &a[7], &port_val); + if (buf_cnt == 9) { + if (-1 == asprintf(&ip_str, "%02x;%04x:%04x:%04x:%04x:%04x:%04x:%04x:%04x;%" PRIu16, (int)(*index + 1), a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], port_val)) { + abort(); + } + } else if (buf_cnt == 8) { + if (-1 == asprintf(&ip_str, "%02x;%04x:%04x:%04x:%04x:%04x:%04x:%04x:%04x", (int)(*index + 1), a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7])) { + abort(); + } + } else { + return NULL; + } +#endif + if (port_ptr) { + *port_ptr = port_val; + } + printf("IP string: %s\r\n", ip_str); + return ip_str; +} + +static int console_cmd_check_table(char **config_table, int *free_slot_cnt_ptr, int *first_free_slot_ptr) +{ + if (!config_table || !config_table[0]) { + ESP_LOGE(TAG, "Configuration table is not correctly initialized."); + return -1; + } + + int cnt = 0; + int free_slot_cnt = 0; + int first_free_slot = -1; + + for (cnt = 0; cnt < MB_CMD_MAX_CFG_COUNT && config_table[cnt]; ++cnt) { + if (strcmp("FROM_STDIN", config_table[cnt]) == 0) { + free_slot_cnt++; + first_free_slot = first_free_slot < 0 ? cnt : first_free_slot; + } + } + if (cnt == MB_CMD_MAX_CFG_COUNT && config_table[cnt]) { + ESP_LOGE(TAG, "Configuration table is not terminated correctly: %d", cnt); + return -1; + } + if (free_slot_cnt_ptr) { + *free_slot_cnt_ptr = free_slot_cnt; + } + if (first_free_slot_ptr) { + *first_free_slot_ptr = first_free_slot; + } + ESP_LOGI(TAG, "Configuration table length: %d, free slots: %d", cnt, free_slot_cnt); + return cnt; +} + +static int do_add_config(int argc, char **argv) +{ + int nerrors = arg_parse(argc, argv, (void **)&add_config_args); + if (nerrors != 0) { + arg_print_errors(stderr, add_config_args.end, argv[0]); + return 1; + } + + if (!s_config_table) { + ESP_LOGE(TAG, "Configuration table is not set."); + return 1; + } + + if (!s_config_table_lock) { + ESP_LOGE(TAG, "Configuration table mutex is not initialized."); + return 1; + } + + int cnt = 0; + int free_slot_cnt = 0; + int first_free_slot = -1; + int ret = 1; // default to error + + if (xSemaphoreTake(s_config_table_lock, portMAX_DELAY) != pdTRUE) { + ESP_LOGE(TAG, "Failed to lock configuration table mutex."); + return 1; + } + + do { + cnt = console_cmd_check_table(s_config_table, &free_slot_cnt, &first_free_slot); + if (cnt <= 0) { + ESP_LOGE(TAG, "Configuration table is not valid."); + break; + } + + int index = 0; + char *config_str = console_cmd_scan_config(&index, NULL, add_config_args.config_str->sval[0]); + + if (!config_str) { + ESP_LOGE(TAG, "Incorrect config string: %s", add_config_args.config_str->sval[0]); + break; + } + + ESP_LOGI(TAG, "Config table index %d(%s), free slot: %d, cnt: %d, free_slots_cnt: %d", index, config_str, first_free_slot, cnt, free_slot_cnt); + + if (index >= cnt) { + ESP_LOGE(TAG, "Incorrect config IP index: %d > %d", index, cnt); + free (config_str); + break; + } + + // Allocate and store the IP string + if (s_config_table[index] && + (strcmp("FROM_STDIN", s_config_table[index]) == 0) && + (index < MB_CMD_MAX_CFG_COUNT) && + (index < cnt) + ) { + s_config_table[index] = config_str; + } else { + ESP_LOGI(TAG, "Leave IP(%d) = [%s] set manually.", index, s_config_table[index]); + free (config_str); + break; + } + + ESP_LOGI(TAG, "Config[%d] set to %s", index, s_config_table[index]); + if (first_free_slot + 1 == cnt) { + ESP_LOGI(TAG, "All %d configs are set.", cnt); + xEventGroupSetBits(s_mb_event_group, MB_CMD_CONFIG_END); + } else { + ESP_LOGI(TAG, "Waiting IP(%d) from stdin:", first_free_slot + 1); + } + ret = 0; + } while (0); + + xSemaphoreGive(s_config_table_lock); + return ret; +} + +// Registers the basic modbus command for start and stop events +esp_err_t mb_console_cmd_mb_register(void) +{ + esp_err_t ret; + + // Support for just simple commands for now + mb_args.command = arg_str1(NULL, NULL, "", "Command (start, stop)."); + mb_args.instance = arg_str1(NULL, NULL, "", "Instance types (instances, slaves, masters, id)"); + mb_args.end = arg_end(2); + + const esp_console_cmd_t mb_cmd = { + .command = "mb", + .help = "Send simple modbus action command", + .hint = NULL, + .func = &do_mb_cmd, + .argtable = &mb_args + }; + + ret = esp_console_cmd_register(&mb_cmd); + if (ret) { + ESP_LOGE(TAG, "Unable to register modbus command"); + } + + return ret; +} + +esp_err_t mb_console_register_configs(char **config_table) +{ + if (!config_table) { + ESP_LOGE(TAG, "Incorrect configuration table."); + return ESP_ERR_INVALID_ARG; + } + + if (!s_config_table_lock) { + ESP_LOGE(TAG, "Configuration table mutex is not initialized."); + return ESP_ERR_INVALID_STATE; + } + + int free_slot_cnt = 0; + int first_free_slot = -1; + + if (xSemaphoreTake(s_config_table_lock, portMAX_DELAY) != pdTRUE) { + ESP_LOGE(TAG, "Failed to lock configuration table mutex."); + return ESP_ERR_INVALID_STATE; + } + + int cnt = console_cmd_check_table(config_table, &free_slot_cnt, &first_free_slot); + if (cnt <= 0 || first_free_slot < 0) { + ESP_LOGE(TAG, "Configuration table is not valid."); + xSemaphoreGive(s_config_table_lock); + return ESP_ERR_INVALID_ARG; + } + + s_config_table = config_table; + xSemaphoreGive(s_config_table_lock); + add_config_args.config_str = arg_str1(NULL, NULL, "", "IP config (e.g. \"01=192.168.1.5;1502)\""); + add_config_args.end = arg_end(1); + + // Use the command similar to legacy string + const esp_console_cmd_t cmd = { + .command = "IP", + .help = "Register configuration", + .hint = NULL, + .func = &do_add_config, + .argtable = &add_config_args + }; + + esp_err_t ret = esp_console_cmd_register(&cmd); + if (ret) { + ESP_LOGE(TAG, "Unable to register %s", cmd.command); + return ret; + } + ESP_LOGI(TAG, "Waiting IP(%d) from stdin:", first_free_slot); + return ESP_OK; +} + +esp_err_t mb_console_init() +{ +#if CONFIG_MB_CONSOLE_HELPER_ENABLED + ESP_LOGI(TAG, "Initialize console helper."); + + if (s_config_table_lock) { + ESP_LOGE(TAG, "Failed to init command console. Already installed?"); + return ESP_ERR_INVALID_STATE; + } + + esp_console_repl_config_t repl_config = ESP_CONSOLE_REPL_CONFIG_DEFAULT(); + esp_err_t ret = ESP_FAIL; + + // install console REPL environment +#if defined(CONFIG_ESP_CONSOLE_UART_DEFAULT) || defined(CONFIG_ESP_CONSOLE_UART_CUSTOM) + esp_console_dev_uart_config_t hw_config = ESP_CONSOLE_DEV_UART_CONFIG_DEFAULT(); + ret = esp_console_new_repl_uart(&hw_config, &repl_config, &repl); + +#elif defined(CONFIG_ESP_CONSOLE_USB_CDC) + esp_console_dev_usb_cdc_config_t hw_config = ESP_CONSOLE_DEV_CDC_CONFIG_DEFAULT(); + ret = esp_console_new_repl_usb_cdc(&hw_config, &repl_config, &repl); + +#elif defined(CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG) + esp_console_dev_usb_serial_jtag_config_t hw_config = ESP_CONSOLE_DEV_USB_SERIAL_JTAG_CONFIG_DEFAULT(); + ret = esp_console_new_repl_usb_serial_jtag(&hw_config, &repl_config, &repl); + +#else +#error Unsupported console type +#endif + + if (ret) { + ESP_LOGE(TAG, "Failed to init repl: %s", esp_err_to_name(ret)); + return ret; + } + + extern const console_cmd_plugin_desc_t _console_cmd_array_start; + extern const console_cmd_plugin_desc_t _console_cmd_array_end; + + ESP_LOGI(TAG, "List of Console commands:\n"); + for (const console_cmd_plugin_desc_t *it = &_console_cmd_array_start; it != &_console_cmd_array_end; ++it) { + ESP_LOGI(TAG, "- Command '%s', function plugin_regd_fn=%p\n", it->name, it->plugin_regd_fn); + if (it->plugin_regd_fn != NULL) { + ret = (it->plugin_regd_fn)(); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Failed to register console commands: %s", esp_err_to_name(ret)); + } + } + } + + s_mb_event_group = xEventGroupCreate(); + if (!s_mb_event_group) { + ESP_LOGE(TAG, "Failed to create event group."); + return ESP_ERR_NO_MEM; + } + + s_config_table_lock = xSemaphoreCreateMutex(); + if (!s_config_table_lock) { + ESP_LOGE(TAG, "Failed to create configuration table mutex."); + vEventGroupDelete(s_mb_event_group); + return ESP_ERR_NO_MEM; + } + + ret = esp_console_start_repl(repl); + if (ret) { + ESP_LOGE(TAG, "Failed to start console commands: %s", esp_err_to_name(ret)); + return ret; + } + + + ESP_LOGI(TAG, "Console helper initialized with config table protection."); +#endif + return ESP_OK; +} + +int mb_console_event_check(int event, uint32_t tout_ms) +{ + if (!event || !tout_ms) { + return MB_CMD_NO_EVENTS; + } + + if (!s_mb_event_group) { + ESP_LOGW(TAG, "Console helper is not initialized, cannot check for commands."); + return MB_CMD_NO_EVENTS; + } + + EventBits_t event_mask = event; + EventBits_t event_bits = MB_CMD_NO_EVENTS; +#if CONFIG_MB_CONSOLE_HELPER_ENABLED + event_bits = xEventGroupWaitBits(s_mb_event_group, + event_mask ? event_mask : MB_CMD_STOP | MB_CMD_START, + pdTRUE, // Clear bits before returning + pdFALSE, + pdMS_TO_TICKS(tout_ms)); +#endif + return (int) event_bits; +} + +esp_err_t mb_console_destroy(void) +{ + esp_err_t err = ESP_ERR_NOT_SUPPORTED; +#if CONFIG_MB_CONSOLE_HELPER_ENABLED + ESP_LOGI(TAG, "Destroying console helper..."); + + if (xSemaphoreTake(s_config_table_lock, pdMS_TO_TICKS(1000)) == pdTRUE) { + xSemaphoreGive(s_config_table_lock); + } + + if (s_config_table) { + for (int i = 0; s_config_table[i]; ++i) { + if (strcmp("FROM_STDIN", s_config_table[i]) != 0) { + free(s_config_table[i]); + } + s_config_table[i] = NULL; + } + } + + if (s_config_table_lock) { + vSemaphoreDelete(s_config_table_lock); + s_config_table_lock = NULL; + } + if (s_mb_event_group) { + vEventGroupDelete(s_mb_event_group); + s_mb_event_group = NULL; + } + + // It is enough to call repl destructor, esp_console_deinit() call is performed from there + if (repl && repl->del) { + err = repl->del(repl); + if (err) { + ESP_LOGE(TAG, "Failed to stop repl: %s", esp_err_to_name(err)); + } + } + repl = NULL; + + ESP_LOGI(TAG, "Console helper destroyed."); +#endif + return err; +} + +#endif diff --git a/conftest.py b/conftest.py index 4e5951b..59cd6cc 100644 --- a/conftest.py +++ b/conftest.py @@ -410,7 +410,7 @@ class ModbusTestDut(IdfDut): if self.dut_list is not None: for dut_instance in self.dut_list: self.logger.info("Sending destroy message to others DUT Instances") - dut_instance.write("Destroy instances\n") + dut_instance.write("mb stop instances\n") return None def get_avg_response_time_master(self) -> int: @@ -433,7 +433,7 @@ class ModbusTestDut(IdfDut): def send_message_destroy_dut(self) -> None: """The function sends message to end caller DUT""" self.logger.info("Sending destroy message to this DUT") - self.write("Destroy instances\n") + self.write("mb stop instances\n") return None def add_request_response( @@ -504,7 +504,7 @@ class ModbusTestDut(IdfDut): ) if isinstance(slave_ip, str): for addr_num in range(0, self.TEST_MAX_CIDS): - message: str = r"IP{}={}".format(addr_num, slave_ip) + message: str = r"IP {}={}".format(addr_num, slave_ip) if isinstance(port, str) or isinstance(port, int): message += r";{}".format(str(port)) message += r"\r\n" diff --git a/examples/tcp/mb_tcp_master/main/Kconfig.projbuild b/examples/tcp/mb_tcp_master/main/Kconfig.projbuild index 31e15ae..854732e 100644 --- a/examples/tcp/mb_tcp_master/main/Kconfig.projbuild +++ b/examples/tcp/mb_tcp_master/main/Kconfig.projbuild @@ -10,6 +10,7 @@ menu "Modbus TCP Example Configuration" bool "Resolve Modbus slave addresses using mDNS service." config MB_SLAVE_IP_FROM_STDIN + select MB_CONSOLE_HELPER_ENABLED bool "Configure Modbus slave addresses from stdin" endchoice diff --git a/examples/tcp/mb_tcp_master/main/idf_component.yml b/examples/tcp/mb_tcp_master/main/idf_component.yml index 5f3cb58..a797443 100644 --- a/examples/tcp/mb_tcp_master/main/idf_component.yml +++ b/examples/tcp/mb_tcp_master/main/idf_component.yml @@ -8,5 +8,7 @@ dependencies: version: "^1" mb_example_common: path: "../../../mb_example_common" + mb_console_helper: + path: "../../../../components/mb_console_helper" protocol_examples_common: path: ${IDF_PATH}/examples/common_components/protocol_examples_common diff --git a/examples/tcp/mb_tcp_master/main/tcp_master.c b/examples/tcp/mb_tcp_master/main/tcp_master.c index 4ba43bb..69ee871 100644 --- a/examples/tcp/mb_tcp_master/main/tcp_master.c +++ b/examples/tcp/mb_tcp_master/main/tcp_master.c @@ -24,8 +24,9 @@ #include "mbcontroller.h" #include "sdkconfig.h" -#define MB_TCP_PORT (CONFIG_FMB_TCP_PORT_DEFAULT) // TCP port used by example +#include "mb_console.h" +#define MB_TCP_PORT (CONFIG_FMB_TCP_PORT_DEFAULT) // TCP port used by example // The number of parameters that intended to be used in the particular control process #define MASTER_MAX_CIDS num_device_parameters @@ -85,6 +86,8 @@ #define MB_MDNS_INSTANCE(pref) pref"mb_master_tcp" #define MB_CUST_DATA_LEN 100 // The length of custom command buffer +#define MB_CMD_CONFIGURATION_TOUT_MS 120000 +#define MB_CMD_CHECK_TOUT_MS 50 static const char *TAG = "MASTER_TEST"; @@ -92,7 +95,7 @@ static const char *TAG = "MASTER_TEST"; // Each address in the table is a index of TCP slave ip address in mb_communication_info_t::tcp_ip_addr table enum { MB_DEVICE_ADDR1 = 1, // Slave UID = 1 - //MB_DEVICE_ADDR1, + MB_DEVICE_ADDR2, //MB_DEVICE_ADDR3, MB_DEVICE_COUNT = 2 }; @@ -149,7 +152,7 @@ const mb_parameter_descriptor_t device_parameters[] = { OPTS( TEST_TEMP_MIN, TEST_TEMP_MAX, 0 ), PAR_PERMS_READ_WRITE_TRIGGER }, { - CID_HOLD_DATA_0, STR("Humidity_1"), STR("%rH"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, + CID_HOLD_DATA_0, STR("Humidity_1"), STR("%rH"), MB_DEVICE_ADDR2, MB_PARAM_HOLDING, TEST_HOLD_REG_START(holding_data0), TEST_HOLD_REG_SIZE(holding_data0), HOLD_OFFSET(holding_data0), PARAM_TYPE_FLOAT, 4, OPTS( TEST_HUMI_MIN, TEST_HUMI_MAX, 0 ), PAR_PERMS_READ_WRITE_TRIGGER @@ -317,14 +320,12 @@ const size_t ip_table_sz; char *slave_ip_address_table[MB_DEVICE_COUNT + 1] = { #if CONFIG_MB_SLAVE_IP_FROM_STDIN "FROM_STDIN", // Address corresponds to MB_DEVICE_ADDR1 and set to predefined value by user - //"FROM_STDIN", // Address corresponds to MB_DEVICE_ADDR2 and set to predefined value by user - //"FROM_STDIN", // Address corresponds to MB_DEVICE_ADDR3 and set to predefined value by user + "FROM_STDIN", // Address corresponds to MB_DEVICE_ADDR2 and set to predefined value by user NULL // End of table condition (must be included) #elif CONFIG_MB_MDNS_IP_RESOLVER // This is workaround for the test to use the same slave for all CIDs and ignore UID setting in the slave "01;mb_slave_tcp_01;1502", - // "02;mb_slave_tcp_01;502", - //"03;mb_slave_tcp_01;1502", + "02;mb_slave_tcp_01;502", NULL // End of table condition (must be included) #endif }; @@ -332,83 +333,6 @@ char *slave_ip_address_table[MB_DEVICE_COUNT + 1] = { const size_t ip_table_sz = (size_t)(sizeof(slave_ip_address_table) / sizeof(slave_ip_address_table[0])); static char my_custom_data[MB_CUST_DATA_LEN] = {0}; // custom data buffer to handle slave response -#if CONFIG_MB_SLAVE_IP_FROM_STDIN - -// Scan IP address according to IPV settings -char *master_scan_addr(int *index, char *buffer) -{ - char *ip_str = NULL; - int a[8] = {0}; - int buf_cnt = 0; -#if !CONFIG_EXAMPLE_CONNECT_IPV6 - buf_cnt = sscanf(buffer, "IP%d=" IPSTR, index, &a[0], &a[1], &a[2], &a[3]); - if (buf_cnt == 5) { - if (-1 == asprintf(&ip_str, "%02x;" IPSTR, (int)(*index + 1), a[0], a[1], a[2], a[3])) { - abort(); - } - } -#else - buf_cnt = sscanf(buffer, "IP%d="IPV6STR, index, &a[0], &a[1], &a[2], &a[3], &a[4], &a[5], &a[6], &a[7]); - if (buf_cnt == 9) { - if (-1 == asprintf(&ip_str, "%02x;" IPV6STR, (int)(*index + 1), a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7])) { - abort(); - } - } -#endif - printf("IP string: %s", ip_str); - return ip_str; -} - -static int master_get_slave_ip_stdin(char **addr_table) -{ - char buf[128]; - int index; - char *ip_str = NULL; - int buf_cnt = 0; - int ip_cnt = 0; - - if (!addr_table) { - return 0; - } - - ESP_ERROR_CHECK(example_configure_stdin_stdout()); - while (1) { - if (addr_table[ip_cnt] && strcmp(addr_table[ip_cnt], "FROM_STDIN") == 0) { - printf("Waiting IP%d from stdin:\r\n", (int)ip_cnt); - while (fgets(buf, sizeof(buf), stdin) == NULL) { - fputs(buf, stdout); - } - buf_cnt = strlen(buf); - buf[buf_cnt - 1] = '\0'; - fputc('\n', stdout); - ip_str = master_scan_addr(&index, buf); - if (ip_str != NULL) { - ESP_LOGI(TAG, "IP(%d) = [%s] set from stdin.", (int)ip_cnt, ip_str); - if ((ip_cnt >= ip_table_sz) || (index != ip_cnt)) { - addr_table[ip_cnt] = NULL; - break; - } - addr_table[ip_cnt++] = ip_str; - } else { - // End of configuration - addr_table[ip_cnt++] = NULL; - break; - } - } else { - if (addr_table[ip_cnt]) { - ESP_LOGI(TAG, "Leave IP(%d) = [%s] set manually.", (int)ip_cnt, addr_table[ip_cnt]); - ip_cnt++; - } else { - ESP_LOGI(TAG, "IP(%d) is not set in the table.", (int)ip_cnt); - break; - } - } - } - return ip_cnt; -} - -#endif - static void master_destroy_slave_list(char **table, size_t ip_table_size) { for (int i = 0; ((i < ip_table_size) && table[i] != NULL); i++) { @@ -463,11 +387,11 @@ static void *master_get_param_data(const mb_parameter_descriptor_t *param_descri if (err == ESP_OK) { \ bool is_correct = true; \ if (pdescr->param_opts.opt3) { \ - for EACH_ITEM(pinst, pdescr->param_size / sizeof(*item_ptr)) { \ - if (*item_ptr != (typeof(*(pinst)))pdescr->param_opts.opt3) { \ + for EACH_ITEM(pinst, pdescr->param_size / sizeof(*item_ptr)) { \ + if (*item_ptr != (typeof(*(pinst)))pdescr->param_opts.opt3) { \ *item_ptr = (typeof(*(pinst)))pdescr->param_opts.opt3; \ - ESP_LOGD(TAG, "%p Characteristic #%d (%s), initialize to 0x%" PRIx16 ".", \ - master_handle, \ + ESP_LOGD(TAG, "%p Characteristic #%d (%s), initialize to 0x%" PRIx16 ".", \ + master_handle, \ (int)pdescr->cid, \ (char *)pdescr->param_key, \ (uint16_t)pdescr->param_opts.opt3); \ @@ -515,6 +439,11 @@ static void master_operation_func(void *arg) bool alarm_state = false; const mb_parameter_descriptor_t *param_descriptor = NULL; + // Wait for start instances command + if (!mb_console_event_check(MB_CMD_START, MB_CMD_CHECK_TOUT_MS)) { + ESP_LOGE(TAG, "Start TCP master after timeout."); + } + ESP_LOGI(TAG, "Master TCP is started."); char *pcustom_string = "Master"; @@ -657,6 +586,10 @@ static void master_operation_func(void *arg) } } vTaskDelay(POLL_TIMEOUT_TICS); // timeout between polls + if (mb_console_event_check(MB_CMD_STOP, MB_CMD_CHECK_TOUT_MS)) { + ESP_LOGI(TAG, "Intentionally stop modbus test..."); + break; + } } } vTaskDelay(UPDATE_CIDS_TIMEOUT_TICS); @@ -710,13 +643,22 @@ static esp_err_t init_services(mb_tcp_addr_type_t ip_addr_type) #endif #if CONFIG_MB_SLAVE_IP_FROM_STDIN - int ip_cnt = master_get_slave_ip_stdin(slave_ip_address_table); - if (ip_cnt) { - ESP_LOGI(TAG, "Configured %d IP address.", ip_cnt); - } else { - ESP_LOGE(TAG, "Fail to get IP address from stdin. Continue."); +#if CONFIG_MB_CONSOLE_HELPER_ENABLED + mb_console_init(); + result = mb_console_register_configs(slave_ip_address_table); + MB_RETURN_ON_FALSE((result == ESP_OK), ESP_ERR_INVALID_STATE, + TAG, + "Could not init CONFIG mode, returns(0x%x).", + (int)result); + ESP_LOGI(TAG, "System initialized in CONFIG mode."); + ESP_LOGI(TAG, "Usage example: IP 0=192.168.1.5;1502 -> then: mb start instances"); + if (!mb_console_event_check(MB_CMD_CONFIG_END, MB_CMD_CONFIGURATION_TOUT_MS)) { + ESP_LOGE(TAG, "Configuration timeout reached."); return ESP_ERR_NOT_FOUND; } +#else +#error "The MB_CONSOLE_HELPER_ENABLED is required for setting configs from STDIN." +#endif #endif return ESP_OK; } @@ -845,7 +787,6 @@ void app_main(void) .tcp_opts.response_tout_ms = CONFIG_FMB_MASTER_TIMEOUT_MS_RESPOND, .tcp_opts.ip_netif_ptr = (void *)get_example_netif() }; - ESP_ERROR_CHECK(master_init(&tcp_master_config)); master_operation_func(NULL); diff --git a/examples/tcp/mb_tcp_master/sdkconfig.defaults b/examples/tcp/mb_tcp_master/sdkconfig.defaults index d85c11c..b8d680a 100644 --- a/examples/tcp/mb_tcp_master/sdkconfig.defaults +++ b/examples/tcp/mb_tcp_master/sdkconfig.defaults @@ -21,3 +21,4 @@ CONFIG_EXAMPLE_WIFI_SSID="${CI_WIFI_SSID}" CONFIG_EXAMPLE_WIFI_PASSWORD="${CI_WIFI_PASSW}" CONFIG_LOG_DEFAULT_LEVEL_DEBUG=n CONFIG_LOG_MAXIMUM_LEVEL_DEBUG=y +CONFIG_MB_CONSOLE_HELPER_ENABLED=y diff --git a/examples/tcp/mb_tcp_slave/main/idf_component.yml b/examples/tcp/mb_tcp_slave/main/idf_component.yml index 1714d89..2201e60 100644 --- a/examples/tcp/mb_tcp_slave/main/idf_component.yml +++ b/examples/tcp/mb_tcp_slave/main/idf_component.yml @@ -7,5 +7,7 @@ dependencies: version: "^1.0.0" mb_example_common: path: "../../../mb_example_common" + mb_console_helper: + path: "../../../../components/mb_console_helper" protocol_examples_common: path: ${IDF_PATH}/examples/common_components/protocol_examples_common diff --git a/examples/tcp/mb_tcp_slave/main/tcp_slave.c b/examples/tcp/mb_tcp_slave/main/tcp_slave.c index ad5995f..bd04af4 100644 --- a/examples/tcp/mb_tcp_slave/main/tcp_slave.c +++ b/examples/tcp/mb_tcp_slave/main/tcp_slave.c @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2016-2025 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2016-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -28,7 +28,10 @@ #include "mbcontroller.h" // for mbcontroller defines and api #include "modbus_params.h" // for modbus parameters structures +#include "mb_console.h" + #define MB_TCP_PORT_NUMBER (CONFIG_FMB_TCP_PORT_DEFAULT) +#define MB_CMD_CHECK_TOUT_MS (50) // Defines below are used to define register start address for each type of Modbus registers #define HOLD_OFFSET(field) ((uint16_t)(offsetof(holding_reg_params_t, field) >> 1)) @@ -59,7 +62,6 @@ #define MB_SLAVE_ADDR (CONFIG_MB_SLAVE_ADDR) #define MB_CUST_DATA_MAX_LEN (100) - static const char *TAG = "SLAVE_TEST"; static void *slave_handle = NULL; @@ -141,66 +143,79 @@ static void setup_reg_data(void) static void slave_operation_func(void *arg) { - mb_param_info_t reg_info; // keeps the Modbus registers access information + mb_param_info_t reg_info = {0}; // keeps the Modbus registers access information + esp_err_t err = ESP_ERR_TIMEOUT; + + // Check start event + mb_console_event_check(MB_CMD_START, MB_CMD_CHECK_TOUT_MS); ESP_LOGI(TAG, "Slave TCP is started"); ESP_LOGI(TAG, "Start modbus test..."); // The cycle below will be terminated when parameter holding_data0 // incremented each access cycle reaches the CHAN_DATA_MAX_VAL value. for (; holding_reg_params.holding_data0 < MB_CHAN_DATA_MAX_VAL;) { - // Check for read/write events of Modbus master for certain events - (void)mbc_slave_check_event(slave_handle, MB_READ_WRITE_MASK); // checks every type of event from specific slave , parameter queue - ESP_ERROR_CHECK_WITHOUT_ABORT(mbc_slave_get_param_info(slave_handle, ®_info, MB_PAR_INFO_GET_TOUT)); // get latest info from parameter queue - const char *rw_str = (reg_info.type & MB_READ_MASK) ? "READ" : "WRITE"; //only checks read mask, assumes write if not - // Filter events and process them accordingly - if (reg_info.type & (MB_EVENT_HOLDING_REG_WR | MB_EVENT_HOLDING_REG_RD)) { - // Get parameter information from parameter queue - ESP_LOGI(TAG, "OBJ %p, HOLDING %s (%u us), ADDR:%u, TYPE:%u, INST_ADDR:0x%.4x, SIZE:%u", - slave_handle, - rw_str, - (unsigned)reg_info.time_stamp, - (unsigned)reg_info.mb_offset, - (unsigned)reg_info.type, - (int)reg_info.address, - (unsigned)reg_info.size); - if (reg_info.address == (uint8_t *)&holding_reg_params.holding_data0) { - (void)mbc_slave_lock(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; - ESP_LOGI(TAG, "Riched maximum value"); - } - (void)mbc_slave_unlock(slave_handle); - } - } else if (reg_info.type & MB_EVENT_INPUT_REG_RD) { - ESP_LOGI(TAG, "OBJ %p, INPUT READ (%" PRIu32 " us), ADDR:%u, TYPE:%u, INST_ADDR:0x%" PRIx32 ", SIZE:%u", - slave_handle, - reg_info.time_stamp, - (unsigned)reg_info.mb_offset, - (unsigned)reg_info.type, - (uint32_t)reg_info.address, - (unsigned)reg_info.size); - } else if (reg_info.type & MB_EVENT_DISCRETE_RD) { - ESP_LOGI(TAG, "OBJ %p, DISCRETE READ (%" PRIu32 " us), ADDR:%u, TYPE:%u, INST_ADDR:0x%" PRIx32 ", SIZE:%u", - slave_handle, - reg_info.time_stamp, - (unsigned)reg_info.mb_offset, - (unsigned)reg_info.type, - (uint32_t)reg_info.address, - (unsigned)reg_info.size); - } else if (reg_info.type & (MB_EVENT_COILS_RD | MB_EVENT_COILS_WR)) { - ESP_LOGI(TAG, "OBJ %p, COILS %s (%" PRIu32 " us), ADDR:%u, TYPE:%u, INST_ADDR:0x%" PRIx32 ", SIZE:%u", - slave_handle, - rw_str, - reg_info.time_stamp, - (unsigned)reg_info.mb_offset, - (unsigned)reg_info.type, - (uint32_t)reg_info.address, - (unsigned)reg_info.size); - if (coil_reg_params.coils_port1 == 0xFF) { - ESP_LOGI(TAG, "Stop polling."); + // Check for read/write events of Modbus master for certain event + err = mbc_slave_get_param_info(slave_handle, ®_info, MB_PAR_INFO_GET_TOUT); // get latest info from parameter queue + if (err == ESP_ERR_TIMEOUT) { + if (mb_console_event_check(MB_CMD_STOP, MB_CMD_CHECK_TOUT_MS)) { + ESP_LOGI(TAG, "Intentionally stop modbus test..."); break; } + reg_info.type = MB_EVENT_NO_EVENTS; + } + + if ((err != ESP_ERR_TIMEOUT) && (reg_info.type & MB_READ_WRITE_MASK)) { + const char *rw_str = (reg_info.type & MB_READ_MASK) ? "READ" : "WRITE"; // only checks read mask, assumes write if not + // Filter events and process them accordingly + if (reg_info.type & (MB_EVENT_HOLDING_REG_WR | MB_EVENT_HOLDING_REG_RD)) { + // Get parameter information from parameter queue + ESP_LOGI(TAG, "OBJ %p, HOLDING %s (%u us), ADDR:%u, TYPE:%u, INST_ADDR:0x%.4x, SIZE:%u", + slave_handle, + rw_str, + (unsigned)reg_info.time_stamp, + (unsigned)reg_info.mb_offset, + (unsigned)reg_info.type, + (int)reg_info.address, + (unsigned)reg_info.size); + if (reg_info.address == (uint8_t *)&holding_reg_params.holding_data0) { + (void)mbc_slave_lock(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; + ESP_LOGI(TAG, "Riched maximum value"); + } + (void)mbc_slave_unlock(slave_handle); + } + } else if (reg_info.type & MB_EVENT_INPUT_REG_RD) { + ESP_LOGI(TAG, "OBJ %p, INPUT READ (%" PRIu32 " us), ADDR:%u, TYPE:%u, INST_ADDR:0x%" PRIx32 ", SIZE:%u", + slave_handle, + reg_info.time_stamp, + (unsigned)reg_info.mb_offset, + (unsigned)reg_info.type, + (uint32_t)reg_info.address, + (unsigned)reg_info.size); + } else if (reg_info.type & MB_EVENT_DISCRETE_RD) { + ESP_LOGI(TAG, "OBJ %p, DISCRETE READ (%" PRIu32 " us), ADDR:%u, TYPE:%u, INST_ADDR:0x%" PRIx32 ", SIZE:%u", + slave_handle, + reg_info.time_stamp, + (unsigned)reg_info.mb_offset, + (unsigned)reg_info.type, + (uint32_t)reg_info.address, + (unsigned)reg_info.size); + } else if (reg_info.type & (MB_EVENT_COILS_RD | MB_EVENT_COILS_WR)) { + ESP_LOGI(TAG, "OBJ %p, COILS %s (%" PRIu32 " us), ADDR:%u, TYPE:%u, INST_ADDR:0x%" PRIx32 ", SIZE:%u", + slave_handle, + rw_str, + reg_info.time_stamp, + (unsigned)reg_info.mb_offset, + (unsigned)reg_info.type, + (uint32_t)reg_info.address, + (unsigned)reg_info.size); + if (coil_reg_params.coils_port1 == 0xFF) { + ESP_LOGI(TAG, "Stop polling."); + break; + } + } } } // Destroy of Modbus controller on alarm @@ -215,6 +230,7 @@ static esp_err_t init_services(void) ESP_ERROR_CHECK(nvs_flash_erase()); result = nvs_flash_init(); } + mb_console_init(); MB_RETURN_ON_FALSE((result == ESP_OK), ESP_ERR_INVALID_STATE, TAG, "nvs_flash_init fail, returns(0x%x).", @@ -460,7 +476,6 @@ void app_main(void) .tcp_opts.uid = MB_SLAVE_ADDR }; - ESP_ERROR_CHECK(slave_init(&tcp_slave_config_1)); ESP_LOGI(TAG, "Slave TCP #1 is started (%s)", __func__); diff --git a/examples/tcp/mb_tcp_slave/sdkconfig.ci.ethernet b/examples/tcp/mb_tcp_slave/sdkconfig.ci.ethernet index 1110f53..4706989 100644 --- a/examples/tcp/mb_tcp_slave/sdkconfig.ci.ethernet +++ b/examples/tcp/mb_tcp_slave/sdkconfig.ci.ethernet @@ -11,7 +11,7 @@ CONFIG_FMB_COMM_MODE_ASCII_EN=n CONFIG_FMB_MASTER_TIMEOUT_MS_RESPOND=3000 CONFIG_FMB_MASTER_DELAY_MS_CONVERT=300 CONFIG_FMB_EXT_TYPE_SUPPORT=y -CONFIG_FMB_TCP_UID_ENABLED=y +CONFIG_FMB_TCP_UID_ENABLED=n CONFIG_FMB_TIMER_USE_ISR_DISPATCH_METHOD=y CONFIG_MB_SLAVE_ADDR=1 CONFIG_EXAMPLE_CONNECT_IPV6=n diff --git a/examples/tcp/mb_tcp_slave/sdkconfig.ci.wifi b/examples/tcp/mb_tcp_slave/sdkconfig.ci.wifi index 5c74c7b..12f475b 100644 --- a/examples/tcp/mb_tcp_slave/sdkconfig.ci.wifi +++ b/examples/tcp/mb_tcp_slave/sdkconfig.ci.wifi @@ -8,7 +8,7 @@ CONFIG_FMB_COMM_MODE_ASCII_EN=n CONFIG_FMB_EXT_TYPE_SUPPORT=y CONFIG_FMB_MASTER_TIMEOUT_MS_RESPOND=3000 CONFIG_FMB_MASTER_DELAY_MS_CONVERT=300 -CONFIG_FMB_TCP_UID_ENABLED=y +CONFIG_FMB_TCP_UID_ENABLED=n CONFIG_MB_SLAVE_ADDR=1 CONFIG_EXAMPLE_CONNECT_IPV6=n CONFIG_FMB_TIMER_USE_ISR_DISPATCH_METHOD=y diff --git a/examples/tcp/mb_tcp_slave/sdkconfig.defaults b/examples/tcp/mb_tcp_slave/sdkconfig.defaults index 6c33141..1b620a8 100644 --- a/examples/tcp/mb_tcp_slave/sdkconfig.defaults +++ b/examples/tcp/mb_tcp_slave/sdkconfig.defaults @@ -10,7 +10,7 @@ CONFIG_FMB_COMM_MODE_RTU_EN=n CONFIG_FMB_COMM_MODE_ASCII_EN=n CONFIG_FMB_MASTER_TIMEOUT_MS_RESPOND=2000 CONFIG_FMB_MASTER_DELAY_MS_CONVERT=300 -CONFIG_FMB_TCP_UID_ENABLED=y +CONFIG_FMB_TCP_UID_ENABLED=n CONFIG_MB_SLAVE_ADDR=1 CONFIG_EXAMPLE_CONNECT_IPV6=n CONFIG_FMB_TIMER_USE_ISR_DISPATCH_METHOD=y diff --git a/examples/tcp/pytest_mb_tcp_master_slave.py b/examples/tcp/pytest_mb_tcp_master_slave.py index 08b1e81..0616c29 100644 --- a/examples/tcp/pytest_mb_tcp_master_slave.py +++ b/examples/tcp/pytest_mb_tcp_master_slave.py @@ -202,9 +202,9 @@ def test_modbus_tcp_communication( @pytest.mark.parametrize("target", ["esp32"], indirect=True) @pytest.mark.multi_dut_modbus_generic @pytest.mark.parametrize("config", ["dummy_config"]) -def test_modbus_tcp_generic(config) -> None: +def test_modbus_tcp_generic(config: str) -> None: logger.info("The generic tcp example tests are not provided yet.") if __name__ == "__main__": - pytest.main(["pytest_mb_tcp_instances.py"]) + pytest.main(["pytest_mb_tcp_master_slave.py"]) diff --git a/test_apps/console_helper_test/CMakeLists.txt b/test_apps/console_helper_test/CMakeLists.txt new file mode 100644 index 0000000..98f7f92 --- /dev/null +++ b/test_apps/console_helper_test/CMakeLists.txt @@ -0,0 +1,9 @@ +# The following lines of boilerplate have to be in your project's CMakeLists +# in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.22) + +set(EXCLUDE_COMPONENTS freemodbus) +list(APPEND EXTRA_COMPONENT_DIRS "../../components/mb_console_helper") + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(modbus_console_helper_test) diff --git a/test_apps/console_helper_test/main/CMakeLists.txt b/test_apps/console_helper_test/main/CMakeLists.txt new file mode 100644 index 0000000..20f3e23 --- /dev/null +++ b/test_apps/console_helper_test/main/CMakeLists.txt @@ -0,0 +1,10 @@ +set(PROJECT_NAME "modbus_console_helper_test") + +idf_component_register(SRCS "mb_console_app.c" + REQUIRES mb_console_helper console) + +# Workaround to avoid static analysis false positives for some components. +if(CONFIG_FMB_COMPILER_STATIC_ANALYZER_ENABLE AND CMAKE_C_COMPILER_ID STREQUAL "GNU") + target_compile_options(${COMPONENT_LIB} PRIVATE "-fanalyzer") + message(STATUS "Static analyzer build for ${PROJECT_NAME}.") +endif() diff --git a/test_apps/console_helper_test/main/mb_console_app.c b/test_apps/console_helper_test/main/mb_console_app.c new file mode 100644 index 0000000..c5b6b54 --- /dev/null +++ b/test_apps/console_helper_test/main/mb_console_app.c @@ -0,0 +1,80 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include +#include + +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_log.h" +#include "esp_err.h" +#include "esp_console.h" + +#include "mb_console.h" + +const char *TAG = "console_helper_test"; + +#if !CONFIG_MB_CONSOLE_HELPER_ENABLED +#error "The MB_CONSOLE_HELPER_ENABLED option must be enabled for this test app." +#endif + +// Simple config table: one entry waiting from stdin, NULL-terminated +#define APP_CFG_COUNT 3 +#define APP_CFG_TIMEOUT_MS 500 +char *app_config_table[APP_CFG_COUNT + 1] = { NULL }; + +void app_main(void) +{ + ESP_LOGI(TAG, "Console helper test app starting."); + + // Prepare the config table entries; "FROM_STDIN" will be replaced by the helper on configuration. + for (int i = 0; i < APP_CFG_COUNT; ++i) { + app_config_table[i] = "FROM_STDIN"; + } + app_config_table[APP_CFG_COUNT] = NULL; // terminator of the table + + // Init console helper and register config table. + esp_err_t err = mb_console_init(); + if (err != ESP_OK) { + ESP_LOGE(TAG, "mb_console_init failed: %s", esp_err_to_name(err)); + } else { + ESP_LOGI(TAG, "mb_console_init OK"); + } + + err = mb_console_register_configs(app_config_table); + if (err != ESP_OK) { + ESP_LOGE(TAG, "mb_console_register_configs failed: %s", esp_err_to_name(err)); + } + + for (;;) { + int ev = mb_console_event_check(MB_CMD_CONFIG_END | MB_CMD_START | MB_CMD_STOP, APP_CFG_TIMEOUT_MS); + if (ev & MB_CMD_CONFIG_END) { + /* print out configured addresses */ + for (int i = 0; i < APP_CFG_COUNT; ++i) { + if (app_config_table[i]) { + ESP_LOGI(TAG, "Config[%d] set to %s\n", i, app_config_table[i]); + } else { + ESP_LOGI(TAG, "ConfigTable[%d]=NULL", i); + } + } + } + if (ev & MB_CMD_START) { + ESP_LOGI(TAG, "Start modbus instances."); + } + if (ev & MB_CMD_STOP) { + ESP_LOGI(TAG, "Stop modbus instances."); + break; + } + vTaskDelay(pdMS_TO_TICKS(APP_CFG_TIMEOUT_MS)); + } + + err = mb_console_destroy(); + if (err == ESP_OK) { + ESP_LOGI(TAG, "Console helper destroyed."); + } else { + ESP_LOGE(TAG, "Console helper destroy failed."); + } +} diff --git a/test_apps/console_helper_test/sdkconfig.defaults b/test_apps/console_helper_test/sdkconfig.defaults new file mode 100644 index 0000000..06f3de0 --- /dev/null +++ b/test_apps/console_helper_test/sdkconfig.defaults @@ -0,0 +1,4 @@ +CONFIG_MB_CONSOLE_HELPER_ENABLED=y +CONFIG_MB_CONSOLE_CMD_AUTO_REGISTRATION=y +CONFIG_ESP_CONSOLE_UART_NUM=0 +CONFIG_LOG_DEFAULT_LEVEL_INFO=y diff --git a/test_apps/console_helper_test/test_console_helper.py b/test_apps/console_helper_test/test_console_helper.py new file mode 100644 index 0000000..125edf9 --- /dev/null +++ b/test_apps/console_helper_test/test_console_helper.py @@ -0,0 +1,39 @@ +# SPDX-FileCopyrightText: 2016-2026 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Apache-2.0 + +import pytest + +from pytest_embedded import Dut + +MB_APP_WAIT_TOUT_SEC = 120 + + +@pytest.mark.parametrize("target", ["esp32"], indirect=True) +@pytest.mark.multi_dut_modbus_generic +def test_mb_console_helper_flow( + dut: Dut, +) -> None: + # Wait for the helper to request IP from stdin (the register function prints this) + dut.expect(r"Waiting IP\([0-9]{1,2}\) from stdin:", timeout=10) + + # Send multiple config strings matching app_config_table entries (indices 0,1,2) + dut.write("IP 00=192.168.1.5;1502\n") + dut.write("IP 01=10.0.0.3;1502\n") + dut.write("IP 02=172.16.0.10;502\n") + + # After sending, helper/app should confirm the configured table entries. + # The test app prints lines like: "Config[0] set to " + dut.expect(r"Config\[0\] set to .*192\.168\.1\.5.*1502", timeout=5) + dut.expect(r"Config\[1\] set to .*10\.0\.0\.3.*1502", timeout=5) + dut.expect(r"Config\[2\] set to .*172\.16\.0\.10.*502", timeout=5) + + # Trigger start command and ensure app logs the Start event + dut.write("mb start instances\n") + dut.expect("Start modbus instances.", timeout=5) + + # Trigger stop command and ensure app logs the Stop event + dut.write("mb stop instances\n") + dut.expect("Stop modbus instances.", timeout=5) + dut.expect("Destroying console helper...", timeout=5) + dut.write("\r\n") # release repl correctly + dut.expect("Console helper destroyed.", timeout=10) diff --git a/test_apps/physical_tests/CMakeLists.txt b/test_apps/physical_tests/CMakeLists.txt index 8170ace..63d6b4d 100644 --- a/test_apps/physical_tests/CMakeLists.txt +++ b/test_apps/physical_tests/CMakeLists.txt @@ -5,6 +5,9 @@ include($ENV{IDF_PATH}/tools/cmake/project.cmake) set(EXTRA_COMPONENT_DIRS "../test_common") +# Include console command helper component +list(APPEND EXTRA_COMPONENT_DIRS "../../components/mb_console_helper") + # The workaround for the test_utils under ESP-IDF v6.0 if("${IDF_VERSION_MAJOR}.${IDF_VERSION_MINOR}" VERSION_GREATER "5.5") list(APPEND EXTRA_COMPONENT_DIRS "$ENV{IDF_PATH}/tools/test_apps/components") diff --git a/test_apps/tcp_instances_tests/mb_tcp_master_instances/main/idf_component.yml b/test_apps/tcp_instances_tests/mb_tcp_master_instances/main/idf_component.yml index d45f708..be91b58 100644 --- a/test_apps/tcp_instances_tests/mb_tcp_master_instances/main/idf_component.yml +++ b/test_apps/tcp_instances_tests/mb_tcp_master_instances/main/idf_component.yml @@ -8,5 +8,7 @@ dependencies: version: "^1.0.0" mb_example_common: path: "../../../../tools/mb_example_common" + mb_console_helper: + path: "../../../../components/mb_console_helper" protocol_examples_common: path: ${IDF_PATH}/examples/common_components/protocol_examples_common diff --git a/test_apps/tcp_instances_tests/mb_tcp_slave_instances/main/idf_component.yml b/test_apps/tcp_instances_tests/mb_tcp_slave_instances/main/idf_component.yml index 9dcd985..c6bd303 100644 --- a/test_apps/tcp_instances_tests/mb_tcp_slave_instances/main/idf_component.yml +++ b/test_apps/tcp_instances_tests/mb_tcp_slave_instances/main/idf_component.yml @@ -7,5 +7,7 @@ dependencies: version: "^1.0.0" mb_example_common: path: "../../../../tools/mb_example_common" + mb_console_helper: + path: "../../../../components/mb_console_helper" protocol_examples_common: path: ${IDF_PATH}/examples/common_components/protocol_examples_common diff --git a/test_apps/test_common/CMakeLists.txt b/test_apps/test_common/CMakeLists.txt index 0909732..75debd6 100644 --- a/test_apps/test_common/CMakeLists.txt +++ b/test_apps/test_common/CMakeLists.txt @@ -1,10 +1,13 @@ cmake_minimum_required(VERSION 3.22) idf_component_register(SRCS "test_common.c" INCLUDE_DIRS "include" - REQUIRES unity esp_timer) + REQUIRES unity esp_timer mb_console_helper) set(EXTRA_COMPONENT_DIRS) +# Include console command helper component +list(APPEND EXTRA_COMPONENT_DIRS "../../components/mb_console_helper") + if("${IDF_VERSION_MAJOR}.${IDF_VERSION_MINOR}" VERSION_GREATER "5.5") list(APPEND EXTRA_COMPONENT_DIRS "$ENV{IDF_PATH}/tools/test_apps/components") else() diff --git a/test_apps/test_common/include/test_common.h b/test_apps/test_common/include/test_common.h index 2fe8236..fd107b7 100644 --- a/test_apps/test_common/include/test_common.h +++ b/test_apps/test_common/include/test_common.h @@ -5,6 +5,7 @@ */ #include #include +#include #include #include "unity.h" diff --git a/test_apps/test_common/test_common.c b/test_apps/test_common/test_common.c index b506dd2..f0c8af1 100644 --- a/test_apps/test_common/test_common.c +++ b/test_apps/test_common/test_common.c @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2018-2025 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2018-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -22,6 +22,10 @@ #include "esp_heap_trace.h" #endif +#if CONFIG_MB_CONSOLE_HELPER_ENABLED +#include "mb_console.h" +#endif + #define TEST_TASK_CYCLE_COUNTER (CONFIG_MB_TEST_COMM_CYCLE_COUNTER) #define TEST_BUSY_TASK_PRIO (20) @@ -44,7 +48,7 @@ #define TEST_NOTIFY_DONE_TOUT (200 / portTICK_PERIOD_MS) #define TAG "TEST_COMMON" -#define MSG_DESTROY "Destroy instances\n\0" +#define MSG_DESTROY "mb stop instances\n\0" typedef enum { RT_HOLDING_RD, @@ -171,8 +175,15 @@ void test_common_task_notify_stop_all() bool test_common_wait_check_destroy_message(char *message, uint32_t timeout_ms) { + bool result = false; /* Read line from console, non-blocking function, timeout in ms */ - +#if CONFIG_MB_CONSOLE_HELPER_ENABLED + if (mb_console_event_check(MB_CMD_STOP, MB_PAR_INFO_TOUT) == MB_CMD_STOP) { + ESP_LOGD(TAG, "Destroy message matched, notifying to destroy instances."); + result = true; + } +#else + // If console helper is not enabled, use standard input to read the message. char buffer[64] = {0}; //fixed size buffer to store the input from stdin ESP_LOGD(TAG, "Waiting for destroy message: \"%s\" (timeout: %lu ms)", message, timeout_ms); @@ -184,10 +195,12 @@ bool test_common_wait_check_destroy_message(char *message, uint32_t timeout_ms) if (strcmp(buffer, message) == 0) { ESP_LOGD(TAG, "Destroy message matched, notifying to destroy instances."); - return true; + result = true; + } else { + ESP_LOGD(TAG, "Timeout waiting for destroy message."); } - ESP_LOGD(TAG, "Timeout waiting for destroy message."); - return false; +#endif + return result; } void test_common_task_notify_done(TaskHandle_t task_handle) @@ -332,6 +345,10 @@ void test_common_start() ESP_ERROR_CHECK( heap_trace_init_standalone(trace_record, NUM_RECORDS) ); #endif +#if CONFIG_MB_CONSOLE_HELPER_ENABLED + mb_console_init(); +#endif + before_free_8bit = heap_caps_get_free_size(MALLOC_CAP_8BIT); before_free_32bit = heap_caps_get_free_size(MALLOC_CAP_32BIT);