From c1c9eb22b0a1d903775deb677328c20bd98ef087 Mon Sep 17 00:00:00 2001 From: Guillaume Souchere Date: Fri, 25 Jul 2025 16:07:42 +0200 Subject: [PATCH 1/4] feat(esp_commands): add esp_commands code --- .github/ISSUE_TEMPLATE/bug-report.yml | 1 + .github/workflows/upload_component.yml | 1 + .idf_build_apps.toml | 1 + esp_commands/.build-test-rules.yml | 6 + esp_commands/CMakeLists.txt | 11 + esp_commands/LICENSE | 202 ++++++ esp_commands/README.md | 0 esp_commands/esp_commands.c | 672 ++++++++++++++++++ esp_commands/esp_commands_helpers.c | 115 +++ esp_commands/esp_dynamic_commands.c | 121 ++++ esp_commands/idf_component.yml | 9 + esp_commands/include/esp_commands.h | 303 ++++++++ esp_commands/linker.lf | 13 + .../private_include/esp_commands_helpers.h | 27 + .../private_include/esp_dynamic_commands.h | 139 ++++ esp_commands/sbom_esp_commands.yml | 6 + 16 files changed, 1627 insertions(+) create mode 100644 esp_commands/.build-test-rules.yml create mode 100644 esp_commands/CMakeLists.txt create mode 100644 esp_commands/LICENSE create mode 100644 esp_commands/README.md create mode 100644 esp_commands/esp_commands.c create mode 100644 esp_commands/esp_commands_helpers.c create mode 100644 esp_commands/esp_dynamic_commands.c create mode 100644 esp_commands/idf_component.yml create mode 100644 esp_commands/include/esp_commands.h create mode 100644 esp_commands/linker.lf create mode 100644 esp_commands/private_include/esp_commands_helpers.h create mode 100644 esp_commands/private_include/esp_dynamic_commands.h create mode 100644 esp_commands/sbom_esp_commands.yml diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index 0aa182943f..fa5d8fc2af 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -32,6 +32,7 @@ body: - eigen - esp_daylight - esp_delta_ota + - esp_commands - esp_encrypted_img - esp_gcov - esp_isotp diff --git a/.github/workflows/upload_component.yml b/.github/workflows/upload_component.yml index 2679ded2d2..85f00821cb 100644 --- a/.github/workflows/upload_component.yml +++ b/.github/workflows/upload_component.yml @@ -34,6 +34,7 @@ jobs: dhara eigen esp_daylight + esp_commands esp_delta_ota esp_encrypted_img esp_gcov diff --git a/.idf_build_apps.toml b/.idf_build_apps.toml index 490475f60d..5fb566e026 100644 --- a/.idf_build_apps.toml +++ b/.idf_build_apps.toml @@ -8,6 +8,7 @@ manifest_file = [ "ccomp_timer/.build-test-rules.yml", "coremark/.build-test-rules.yml", "esp_daylight/.build-test-rules.yml", + "esp_commands/.build-test-rules.yml", "esp_encrypted_img/.build-test-rules.yml", "esp_gcov/.build-test-rules.yml", "esp_jpeg/.build-test-rules.yml", diff --git a/esp_commands/.build-test-rules.yml b/esp_commands/.build-test-rules.yml new file mode 100644 index 0000000000..89638d6281 --- /dev/null +++ b/esp_commands/.build-test-rules.yml @@ -0,0 +1,6 @@ +esp_commands/test_apps: + disable: + - if: IDF_VERSION_MAJOR < 6 + reason: "esp_commands is created based on commands.c in esp-idf version < 6.0" + - if: IDF_TARGET not in ["esp32", "esp32c3"] + reason: "Sufficient to test on one Xtensa and one RISC-V target" \ No newline at end of file diff --git a/esp_commands/CMakeLists.txt b/esp_commands/CMakeLists.txt new file mode 100644 index 0000000000..202fa7b933 --- /dev/null +++ b/esp_commands/CMakeLists.txt @@ -0,0 +1,11 @@ +idf_build_get_property(target IDF_TARGET) + +set(srcs "esp_commands.c" + "esp_dynamic_commands.c" + "esp_commands_helpers.c") + +idf_component_register( + SRCS ${srcs} + INCLUDE_DIRS include + PRIV_INCLUDE_DIRS private_include + LDFRAGMENTS linker.lf) diff --git a/esp_commands/LICENSE b/esp_commands/LICENSE new file mode 100644 index 0000000000..efca5e67a3 --- /dev/null +++ b/esp_commands/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright Espressif Systems + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/esp_commands/README.md b/esp_commands/README.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esp_commands/esp_commands.c b/esp_commands/esp_commands.c new file mode 100644 index 0000000000..b52f650cbe --- /dev/null +++ b/esp_commands/esp_commands.c @@ -0,0 +1,672 @@ +/* + * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include "esp_commands.h" +#include "esp_dynamic_commands.h" +#include "esp_commands_helpers.h" +#include "esp_err.h" + +/* Default foreground color */ +#define ANSI_COLOR_DEFAULT 39 + +/* Pointers to the first and last command in the dedicated section. + * See linker.lf for detailed information about the section */ +extern esp_command_t _esp_commands_start; +extern esp_command_t _esp_commands_end; + +typedef struct esp_command_sets { + esp_command_set_t static_set; + esp_command_set_t dynamic_set; +} esp_command_sets_t; + +/** run-time configuration options */ +static esp_commands_config_t s_config = { + .hint_bold = false, + .hint_color = ANSI_COLOR_DEFAULT, + .max_cmdline_args = 32, + .max_cmdline_length = 256 +}; + +/** + * @brief go through all commands registered in the + * memory section starting at _esp_commands_start + * and ending at _esp_commands_end OR go through all + * the commands listed in cmd_set if not NULL + */ +#define FOR_EACH_STATIC_COMMAND(cmd_set, cmd) \ + for (size_t _i = 0; \ + ((cmd_set) == NULL \ + ? (((cmd) = &_esp_commands_start + _i), \ + (&_esp_commands_start + _i) < &_esp_commands_end) \ + : (((cmd) = (cmd_set)->cmd_ptr_set[_i]), \ + _i < (cmd_set)->cmd_set_size)); \ + ++_i) + +/** + * @brief returns the number of commands registered + * in the .esp_commands section + */ +#define ESP_COMMANDS_COUNT (size_t)(&_esp_commands_end - &_esp_commands_start) + +/** + * @brief check the location of the pointer to esp_command_t + * + * @param cmd the pointer to the command to check + * @return true if the command was registered statically + * false if the command was registered dynamically + */ +static inline __attribute__((always_inline)) bool command_is_static(esp_command_t *cmd) +{ + if (cmd >= &_esp_commands_start && cmd <= &_esp_commands_end) { + return true; + } + return false; +} + +typedef bool (*walker_t)(void *walker_ctx, esp_command_t *cmd); +static inline __attribute__((always_inline)) +void go_through_commands(esp_command_sets_t *cmd_sets, void *cmd_walker_ctx, walker_t cmd_walker) +{ + if (!cmd_walker) { + return; + } + + esp_command_t *cmd = NULL; + bool continue_walk = false; + + /* cmd_sets is composed of 2 sets (static and dynamic). + * - If cmd_sets is NULL, go through all the statically AND dynamically registered commands. + * - If cmd_sets is not NULL and either the static or the dynamic set is empty, then the macros + * FOR_EACH_XX_COMMAND will not go through the whole list of static (resp. dynamic) commands but + * through the empty set, so no command will be walked. + */ + + esp_command_set_t *static_set = cmd_sets ? &cmd_sets->static_set : NULL; + /* it is possible that the set is empty, in which case set static_set to NULL + * to prevent the for loop to try to access a list of commands pointer set to NULL */ + if (static_set && !static_set->cmd_ptr_set) { + static_set = NULL; + } + FOR_EACH_STATIC_COMMAND(static_set, cmd) { + continue_walk = cmd_walker(cmd_walker_ctx, cmd); + if (!continue_walk) { + return; + } + } + + esp_command_set_t *dynamic_set = cmd_sets ? &cmd_sets->dynamic_set : NULL; + /* it is possible that the set is empty, in which case set dynamic_set to NULL + * to prevent the for loop to try to access a list of commands pointer set to NULL */ + if (dynamic_set && !dynamic_set->cmd_ptr_set) { + dynamic_set = NULL; + } + esp_dynamic_commands_lock(); + FOR_EACH_DYNAMIC_COMMAND(dynamic_set, cmd) { + continue_walk = cmd_walker(cmd_walker_ctx, cmd); + if (!continue_walk) { + esp_dynamic_commands_unlock(); + return; + } + } + esp_dynamic_commands_unlock(); +} + +typedef struct find_cmd_ctx { + const char *name; /*!< the name to check commands against */ + esp_command_t *cmd; /*!< the command matching the name */ +} find_cmd_ctx_t; + +static inline __attribute__((always_inline)) +bool compare_command_name(void *ctx, esp_command_t *cmd) +{ + /* called by esp_commands_find_command through go_through_commands, + * ctx cannot be NULL */ + find_cmd_ctx_t *cmd_ctx = (find_cmd_ctx_t *)ctx; + + /* called by go_through_commands, thus cmd cannot be NULL */ + if (strcmp(cmd->name, cmd_ctx->name) == 0) { + /* command found, store it in the ctx so esp_commands_find_command + * can process it. Notify go_through_commands to stop the walk by + * returning false */ + cmd_ctx->cmd = cmd; + return false; + } + + /* command not matching with the name from the ctx, continue the walk */ + return true; +} + +esp_err_t esp_commands_update_config(const esp_commands_config_t *config) +{ + if (!config || + (config->max_cmdline_args == 0) || + (config->max_cmdline_length == 0)) { + return ESP_ERR_INVALID_ARG; + } + + memcpy(&s_config, config, sizeof(s_config)); + + return ESP_OK; +} + +esp_err_t esp_commands_register_cmd(esp_command_t *cmd) +{ + if (cmd == NULL || + // (cmd->name == NULL || strchr(cmd->name, ' ') != NULL) || + cmd->func == NULL ) { + printf("this should not happen\n"); + return ESP_ERR_INVALID_ARG; + } + + /* try to find the command in the static and dynamic lists. + * if the dynamic list is empty, the mutex locking will fail + * in esp_commands_find_command and the function will return after + * checking the static list only. */ + esp_command_t *list_item_cmd = esp_commands_find_command((esp_command_sets_t *)NULL, cmd->name); + esp_err_t ret_val = ESP_FAIL; + if (!list_item_cmd) { + /* command with given name not found, it is a new command, we can allocate + * the list item and the command itself */ + ret_val = esp_dynamic_commands_add(cmd); + } else if (command_is_static(list_item_cmd)) { + /* a command with matching name is found in the list of commands + * that were registered at runtime, in which case it cannot be + * replaced with the new command */ + ret_val = ESP_FAIL; + } else { + /* an item with matching name was found in the list of dynamically + * registered commands. Replace the command on spot with the new esp_command_t. */ + ret_val = esp_dynamic_commands_replace(cmd); + } + + return ret_val; +} + +esp_err_t esp_commands_unregister_cmd(const char *cmd_name) +{ + /* only items dynamically registered can be unregistered. + * try to remove the item with the given name from the list + * of dynamically registered commands */ + esp_command_t *cmd = esp_commands_find_command((esp_command_sets_t *)NULL, cmd_name); + if (!cmd) { + return ESP_ERR_NOT_FOUND; + } else if (command_is_static(cmd)) { + return ESP_ERR_INVALID_ARG; + } else { + return esp_dynamic_commands_remove(cmd); + } +} + +esp_err_t esp_commands_execute(esp_command_set_handle_t cmd_set, const char *cmdline, int *cmd_ret) +{ + char **argv = (char **) calloc(s_config.max_cmdline_args, sizeof(char *)); + if (argv == NULL) { + return ESP_ERR_NO_MEM; + } + char *tmp_line_buf = (char *) calloc(1, s_config.max_cmdline_length); + if (!tmp_line_buf) { + free(argv); + return ESP_ERR_NO_MEM; + } + + strlcpy(tmp_line_buf, cmdline, s_config.max_cmdline_length); + + size_t argc = esp_commands_split_argv(tmp_line_buf, argv, s_config.max_cmdline_args); + if (argc == 0) { + free(argv); + free(tmp_line_buf); + return ESP_ERR_INVALID_ARG; + } + + /* help should always be executed, if cmd_sets is set or not */ + const esp_command_t *cmd = NULL; + bool is_cmd_help = false; + if (strcmp("help", argv[0]) == 0) { + /* find the help command in the list in .esp_commands section */ + cmd = esp_commands_find_command((esp_command_sets_t *)NULL, "help"); + is_cmd_help = true; + } else { + cmd = esp_commands_find_command(cmd_set, argv[0]); + } + + if (cmd == NULL) { + free(argv); + free(tmp_line_buf); + return ESP_ERR_NOT_FOUND; + } + if (cmd->func) { + if (is_cmd_help) { + // executing help command, pass the cmd_set as context + *cmd_ret = (*cmd->func)(cmd_set, argc, argv); + } else { + *cmd_ret = (*cmd->func)(cmd->func_ctx, argc, argv); + } + } + free(argv); + free(tmp_line_buf); + return ESP_OK; +} + +esp_command_t *esp_commands_find_command(esp_command_set_handle_t cmd_set, const char *name) +{ + /* no need to check that cmd_set is NULL, if it is, then FOR_EACH_XX_COMMAND + * will go through all registered commands */ + if (!name) { + return NULL; + } + + find_cmd_ctx_t ctx = { .cmd = NULL, .name = name }; + go_through_commands(cmd_set, &ctx, compare_command_name); + + /* if command was found during the walk, cmd field will be populated with + * the command matching the name given in parameter, otherwise it will still + * be NULL (value set as default value above) */ + return ctx.cmd; +} +typedef struct create_cmd_set_ctx { + esp_commands_get_field_t get_field; + const char *cmd_set_name; + esp_command_t **static_cmd_ptrs; + size_t static_cmd_count; + esp_command_t **dynamic_cmd_ptrs; + size_t dynamic_cmd_count; +} create_cmd_set_ctx_t; + +static inline __attribute__((always_inline)) +bool fill_temp_set_info(void *caller_ctx, esp_command_t *cmd) +{ + /* called by esp_commands_find_command through go_through_commands, + * ctx cannot be NULL */ + create_cmd_set_ctx_t *ctx = (create_cmd_set_ctx_t *)caller_ctx; + + /* called by go_through_commands, thus cmd cannot be NULL */ + if (strcmp(ctx->get_field(cmd), ctx->cmd_set_name) == 0) { + // it's a match, add the pointer to command to the cmd ptr set + if (command_is_static(cmd)) { + ctx->static_cmd_ptrs[ctx->static_cmd_count] = cmd; + ctx->static_cmd_count++; + } else { + ctx->dynamic_cmd_ptrs[ctx->dynamic_cmd_count] = cmd; + ctx->dynamic_cmd_count++; + } + } + + /* command not matching with the name from the ctx, continue the walk */ + return true; +} + +static inline __attribute__((always_inline)) +esp_err_t update_cmd_set_with_temp_info(esp_command_set_t *cmd_set, size_t cmd_count, esp_command_t **cmd_ptrs) +{ + if (cmd_count == 0) { + cmd_set->cmd_ptr_set = NULL; + cmd_set->cmd_set_size = 0; + } else { + const size_t alloc_cmd_ptrs_size = sizeof(esp_command_t *) * cmd_count; + cmd_set->cmd_ptr_set = malloc(alloc_cmd_ptrs_size); + if (!cmd_set->cmd_ptr_set) { + return ESP_ERR_NO_MEM; + } else { + /* copy the temp set of pointer in to the final destination */ + memcpy(cmd_set->cmd_ptr_set, cmd_ptrs, alloc_cmd_ptrs_size); + cmd_set->cmd_set_size = cmd_count; + } + } + return ESP_OK; +} + +esp_command_set_handle_t esp_commands_create_cmd_set(const char **cmd_set, const size_t cmd_set_size, esp_commands_get_field_t get_field) +{ + if (!cmd_set || cmd_set_size == 0) { + return NULL; + } + + esp_command_sets_t *cmd_ptr_sets = malloc(sizeof(esp_command_sets_t)); + if (!cmd_ptr_sets) { + return NULL; + } + + + esp_command_t *static_cmd_ptrs_temp[ESP_COMMANDS_COUNT]; + esp_command_t *dynamic_cmd_ptrs_temp[esp_dynamic_commands_get_number_of_cmd()]; + create_cmd_set_ctx_t ctx = { + .cmd_set_name = NULL, + .get_field = get_field, + .static_cmd_ptrs = static_cmd_ptrs_temp, + .static_cmd_count = 0, + .dynamic_cmd_ptrs = dynamic_cmd_ptrs_temp, + .dynamic_cmd_count = 0 + }; + + /* populate the temporary cmd pointer sets */ + for (size_t i = 0; i < cmd_set_size; i++) { + ctx.cmd_set_name = cmd_set[i]; + go_through_commands(NULL, &ctx, fill_temp_set_info); + } + + /* if no static command was found, return a static set with 0 items in it */ + esp_err_t ret_val = update_cmd_set_with_temp_info(&cmd_ptr_sets->static_set, + ctx.static_cmd_count, + ctx.static_cmd_ptrs); + if (ret_val == ESP_ERR_NO_MEM) { + free(cmd_ptr_sets); + return NULL; + } + + /* if no dynamic command was found, return a dynamic set with 0 items in it */ + ret_val = update_cmd_set_with_temp_info(&cmd_ptr_sets->dynamic_set, + ctx.dynamic_cmd_count, + ctx.dynamic_cmd_ptrs); + if (ret_val == ESP_ERR_NO_MEM) { + free(cmd_ptr_sets->static_set.cmd_ptr_set); + free(cmd_ptr_sets); + return NULL; + } + + return (esp_command_set_handle_t)cmd_ptr_sets; +} + +esp_command_set_handle_t esp_commands_concat_cmd_set(esp_command_set_handle_t cmd_set_a, esp_command_set_handle_t cmd_set_b) +{ + if (!cmd_set_a && !cmd_set_b) { + return NULL; + } else if (cmd_set_a && !cmd_set_b) { + return cmd_set_a; + } else if (!cmd_set_a && cmd_set_b) { + return cmd_set_b; + } + + /* Reaching this point, both cmd_set_a and cmd_set_b are set. + * Create a new cmd_set that can host the items from both sets, + * assign the items to the new set and free the input sets */ + esp_command_sets_t *concat_cmd_sets = malloc(sizeof(esp_command_sets_t)); + if (!concat_cmd_sets) { + return NULL; + } + const size_t new_static_set_size = cmd_set_a->static_set.cmd_set_size + cmd_set_b->static_set.cmd_set_size; + concat_cmd_sets->static_set.cmd_ptr_set = calloc(new_static_set_size, sizeof(esp_command_t *)); + if (!concat_cmd_sets->static_set.cmd_ptr_set) { + free(concat_cmd_sets); + return NULL; + } + + const size_t new_dynamic_set_size = cmd_set_a->dynamic_set.cmd_set_size + cmd_set_b->dynamic_set.cmd_set_size; + concat_cmd_sets->dynamic_set.cmd_ptr_set = calloc(new_dynamic_set_size, sizeof(esp_command_t *)); + if (!concat_cmd_sets->static_set.cmd_ptr_set) { + free(concat_cmd_sets->static_set.cmd_ptr_set); + free(concat_cmd_sets); + return NULL; + } + + /* update the new cmd set sizes */ + concat_cmd_sets->static_set.cmd_set_size = new_static_set_size; + concat_cmd_sets->dynamic_set.cmd_set_size = new_dynamic_set_size; + + /* fill the list of command pointers */ + memcpy(concat_cmd_sets->static_set.cmd_ptr_set, + cmd_set_a->static_set.cmd_ptr_set, + sizeof(esp_command_t *) * cmd_set_a->static_set.cmd_set_size); + memcpy(concat_cmd_sets->static_set.cmd_ptr_set + cmd_set_a->static_set.cmd_set_size, + cmd_set_b->static_set.cmd_ptr_set, + sizeof(esp_command_t *) * cmd_set_b->static_set.cmd_set_size); + + memcpy(concat_cmd_sets->dynamic_set.cmd_ptr_set, + cmd_set_a->dynamic_set.cmd_ptr_set, + sizeof(esp_command_t *) * cmd_set_a->dynamic_set.cmd_set_size); + memcpy(concat_cmd_sets->dynamic_set.cmd_ptr_set + cmd_set_a->dynamic_set.cmd_set_size, + cmd_set_b->dynamic_set.cmd_ptr_set, + sizeof(esp_command_t *) * cmd_set_b->dynamic_set.cmd_set_size); + + esp_commands_destroy_cmd_set(&cmd_set_a); + esp_commands_destroy_cmd_set(&cmd_set_b); + + return (esp_command_set_handle_t)concat_cmd_sets; +} + +void esp_commands_destroy_cmd_set(esp_command_set_handle_t *cmd_set) +{ + if (!cmd_set || !*cmd_set) { + return; + } + + if ((*cmd_set)->static_set.cmd_ptr_set) { + free((*cmd_set)->static_set.cmd_ptr_set); + } + + if ((*cmd_set)->dynamic_set.cmd_ptr_set) { + free((*cmd_set)->dynamic_set.cmd_ptr_set); + } + + free(*cmd_set); + *cmd_set = NULL; +} + +typedef struct call_completion_cb_ctx { + const char *buf; + const size_t buf_len; + esp_command_get_completion_t completion_cb; +} call_completion_cb_ctx_t; + +static bool call_completion_cb(void *caller_ctx, esp_command_t *cmd) +{ + call_completion_cb_ctx_t *ctx = (call_completion_cb_ctx_t *)caller_ctx; + + /* Check if command starts with buf */ + if (strncmp(ctx->buf, cmd->name, ctx->buf_len) == 0) { + ctx->completion_cb(cmd->name); + } + return true; +} + +void esp_commands_get_completion(esp_command_set_handle_t cmd_set, const char *buf, esp_command_get_completion_t completion_cb) +{ + size_t len = strlen(buf); + if (len == 0) { + return; + } + + call_completion_cb_ctx_t ctx = { + .buf = buf, + .buf_len = len, + .completion_cb = completion_cb + }; + go_through_commands(cmd_set, &ctx, call_completion_cb); +} + +const char *esp_commands_get_hint(esp_command_set_handle_t cmd_set, const char *buf, int *color, bool *bold) +{ + *color = s_config.hint_color; + *bold = s_config.hint_bold; + + esp_command_t *cmd = esp_commands_find_command(cmd_set, buf); + if (cmd && cmd->hint_cb != NULL) { + return cmd->hint_cb(cmd->func_ctx); + } + + return NULL; +} + +const char *esp_commands_get_glossary(esp_command_set_handle_t cmd_set, const char *buf) +{ + esp_command_t *cmd = esp_commands_find_command(cmd_set, buf); + if (cmd && cmd->glossary_cb != NULL) { + return cmd->glossary_cb(cmd->func_ctx); + } + + return NULL; +} + +/* -------------------------------------------------------------- */ +/* help command related code */ +/* -------------------------------------------------------------- */ + +static void print_arg_help(esp_command_t *it) +{ + /* First line: command name and hint + * Pad all the hints to the same column + */ + printf("%-s", it->name); + if (it->hint_cb) { + printf(" %s\n", it->hint_cb(it->func_ctx)); + } else { + printf("\n"); + } + + /* Second line: print help */ + /* TODO: replace the simple print with a function that + * replaces arg_print_formatted */ + if (it->help) { + printf(" %s\n", it->help); + } else { + printf(" -\n"); + } + + /* Third line: print the glossary*/ + if (it->glossary_cb) { + printf("%s\n", it->glossary_cb(it->func_ctx)); + } else { + printf(" -\n"); + } + + printf("\n"); +} + +static void print_arg_command(esp_command_t *it) +{ + printf("%-s", it->name); + if (it->hint_cb) { + printf(" %s\n", it->hint_cb(it->func_ctx)); + } +} + +typedef enum { + HELP_VERBOSE_LEVEL_0 = 0, + HELP_VERBOSE_LEVEL_1 = 1, + HELP_VERBOSE_LEVEL_MAX_NUM = 2 +} help_verbose_level_e; + +typedef void (*const fn_print_arg_t)(esp_command_t *); + +static fn_print_arg_t print_verbose_level_arr[HELP_VERBOSE_LEVEL_MAX_NUM] = { + print_arg_command, + print_arg_help, +}; + +typedef struct call_cmd_ctx { + help_verbose_level_e verbose_level; + const char *command_name; + bool command_found; +} call_cmd_ctx_t; + +static inline __attribute__((always_inline)) +bool call_command_funcs(void *caller_ctx, esp_command_t *cmd) +{ + call_cmd_ctx_t *ctx = (call_cmd_ctx_t *)caller_ctx; + + if (!ctx->command_name) { + /* ctx->command_name is empty, print all commands */ + print_verbose_level_arr[ctx->verbose_level](cmd); + } else if (ctx->command_name && + (strcmp(ctx->command_name, cmd->name) == 0)) { + /* we found the command name, print the help and return */ + print_verbose_level_arr[ctx->verbose_level](cmd); + ctx->command_found = true; + return false; + } + + return true; +} + +static int help_command(void *context, int argc, char **argv) +{ + char *command_name = NULL; + help_verbose_level_e verbose_level = HELP_VERBOSE_LEVEL_1; + + /* argc can never be superior to 4 given than the format is: + * help cmd_name -v 0 */ + if (argc <= 0 || argc > 4) { + /* unknown issue, return error */ + printf("help: invalid number of arguments %d\n", argc); + return 1; + } + + esp_command_sets_t *cmd_sets = (esp_command_sets_t *)context; + + if (argc > 1) { + /* more than 1 arg, figure out if only verbose level argument + * was passed and if a specific command was passed. + * start from the second argument since the first one is "help" */ + for (int i = 1; i < argc; i++) { + if ((strcmp(argv[i], "-v") == 0) || + (strcmp(argv[i], "--verbose") == 0)) { + /* check if the following argument is either 0, or 1 */ + if (i + 1 >= argc) { + /* format error, return with error */ + printf("help: arguments not provided in the right format\n"); + return 1; + } else if (strcmp(argv[i + 1], "0") == 0) { + verbose_level = 0; + } else if (strcmp(argv[i + 1], "1") == 0) { + verbose_level = 1; + } else { + /* wrong command format, return error */ + printf("help: invalid verbose level %s\n", argv[i + 1]); + return 1; + } + + /* we found the -v / --verbose, bump i to skip the value of + * the verbose argument since it was just parsed */ + i++; + } else { + /* the argument is not -v or --verbose, it is then the command name + * of which we should print the hint, store it for latter */ + command_name = argv[i]; + } + } + } + + /* at this point we should have figured out all the arguments of the help + * command. if command_name is NULL, then print all commands. if command_name + * is not NULL, find the command and only print the help for this command. if the + * command is not found, return with error */ + call_cmd_ctx_t ctx = { + .verbose_level = verbose_level, + .command_name = command_name, + .command_found = false + }; + go_through_commands(cmd_sets, &ctx, call_command_funcs); + + if (command_name && !ctx.command_found) { + printf("help: invalid command name %s\n", command_name); + return 1; + } + + return 0; +} + +static const char *get_help_hint(void *context) +{ + (void)context; + return "[] [-v <0|1>]"; +} + +static const char *get_help_glossary(void *context) +{ + (void)context; + return " Name of command\n" + " -v, --verbose <0|1> If specified, list console commands with given verbose level";; +} + +static const char help_str[] = "Print the summary of all registered commands if no arguments " + "are given, otherwise print summary of given command."; + +ESP_COMMAND_REGISTER(help, /* name of the heap command */ + help, /* group of the help command */ + help_str, /* help string of the help command */ + help_command, /* func */ + NULL, /* the context is null here, it will provided by the exec function */ + get_help_hint, /* hint callback */ + get_help_glossary); /* glossary callback */ diff --git a/esp_commands/esp_commands_helpers.c b/esp_commands/esp_commands_helpers.c new file mode 100644 index 0000000000..30947f32d5 --- /dev/null +++ b/esp_commands/esp_commands_helpers.c @@ -0,0 +1,115 @@ +/* + * SPDX-FileCopyrightText: 2016-2021 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include "esp_commands_helpers.h" +#include "esp_commands.h" + +#define SS_FLAG_ESCAPE 0x8 + +typedef enum { + /* parsing the space between arguments */ + SS_SPACE = 0x0, + /* parsing an argument which isn't quoted */ + SS_ARG = 0x1, + /* parsing a quoted argument */ + SS_QUOTED_ARG = 0x2, + /* parsing an escape sequence within unquoted argument */ + SS_ARG_ESCAPED = SS_ARG | SS_FLAG_ESCAPE, + /* parsing an escape sequence within a quoted argument */ + SS_QUOTED_ARG_ESCAPED = SS_QUOTED_ARG | SS_FLAG_ESCAPE, +} split_state_t; + +/* helper macro, called when done with an argument */ +#define END_ARG() do { \ + char_out = 0; \ + argv[argc++] = next_arg_start; \ + state = SS_SPACE; \ +} while(0) + +size_t esp_commands_split_argv(char *line, char **argv, size_t argv_size) +{ + const int QUOTE = '"'; + const int ESCAPE = '\\'; + const int SPACE = ' '; + split_state_t state = SS_SPACE; + size_t argc = 0; + char *next_arg_start = line; + char *out_ptr = line; + for (char *in_ptr = line; argc < argv_size - 1; ++in_ptr) { + int char_in = (unsigned char) * in_ptr; + if (char_in == 0) { + break; + } + int char_out = -1; + + switch (state) { + case SS_SPACE: + if (char_in == SPACE) { + /* skip space */ + } else if (char_in == QUOTE) { + next_arg_start = out_ptr; + state = SS_QUOTED_ARG; + } else if (char_in == ESCAPE) { + next_arg_start = out_ptr; + state = SS_ARG_ESCAPED; + } else { + next_arg_start = out_ptr; + state = SS_ARG; + char_out = char_in; + } + break; + + case SS_QUOTED_ARG: + if (char_in == QUOTE) { + END_ARG(); + } else if (char_in == ESCAPE) { + state = SS_QUOTED_ARG_ESCAPED; + } else { + char_out = char_in; + } + break; + + case SS_ARG_ESCAPED: + case SS_QUOTED_ARG_ESCAPED: + if (char_in == ESCAPE || char_in == QUOTE || char_in == SPACE) { + char_out = char_in; + } else { + /* unrecognized escape character, skip */ + } + state = (split_state_t)(state & (~SS_FLAG_ESCAPE)); + break; + + case SS_ARG: + if (char_in == SPACE) { + END_ARG(); + } else if (char_in == ESCAPE) { + state = SS_ARG_ESCAPED; + } else { + char_out = char_in; + } + break; + } + /* need to output anything? */ + if (char_out >= 0) { + *out_ptr = char_out; + ++out_ptr; + } + } + /* make sure the final argument is terminated */ + *out_ptr = 0; + /* finalize the last argument */ + if (state != SS_SPACE && argc < argv_size - 1) { + argv[argc++] = next_arg_start; + } + /* add a NULL at the end of argv */ + argv[argc] = NULL; + + return argc; +} diff --git a/esp_commands/esp_dynamic_commands.c b/esp_commands/esp_dynamic_commands.c new file mode 100644 index 0000000000..10f50d7258 --- /dev/null +++ b/esp_commands/esp_dynamic_commands.c @@ -0,0 +1,121 @@ +/* + * SPDX-FileCopyrightText: 2016-2021 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include "esp_dynamic_commands.h" +#include "esp_commands.h" + +#define CONTAINER_OF(ptr, type, member) \ + ((type *)((char *)(ptr) - offsetof(type, member))) + +static esp_command_internal_ll_t s_dynamic_cmd_list = SLIST_HEAD_INITIALIZER(esp_command_internal); +static size_t s_number_of_registered_commands = 0; +static SemaphoreHandle_t s_esp_commands_mutex = NULL; +static StaticSemaphore_t s_esp_commands_mutex_buf; + +void esp_dynamic_commands_lock(void) +{ + /* check if the mutex needs to be initialized and initialized it only + * is requested in by the state of the create parameter */ + if (s_esp_commands_mutex == NULL) { + s_esp_commands_mutex = xSemaphoreCreateMutexStatic(&s_esp_commands_mutex_buf); + assert(s_esp_commands_mutex != NULL); + } + + xSemaphoreTake(s_esp_commands_mutex, portMAX_DELAY); +} + +void esp_dynamic_commands_unlock(void) +{ + if (s_esp_commands_mutex == NULL) { + return; + } + xSemaphoreGive(s_esp_commands_mutex); +} + +const esp_command_internal_ll_t *esp_dynamic_commands_get_list(void) +{ + return &s_dynamic_cmd_list; +} + +esp_err_t esp_dynamic_commands_add(esp_command_t *cmd) +{ + if (!cmd) { + return ESP_ERR_INVALID_ARG; + } + + esp_command_internal_t *list_item = malloc(sizeof(esp_command_internal_t)); + if (!list_item) { + return ESP_ERR_NO_MEM; + } + + memcpy(&list_item->cmd, cmd, sizeof(esp_command_t)); + + esp_command_internal_t *last = NULL; + esp_command_internal_t *it = NULL; + + /* this could be called on an empty list, make sure the + * mutex is initialized */ + esp_dynamic_commands_lock(); + + SLIST_FOREACH(it, &s_dynamic_cmd_list, next_item) { + if (strcmp(it->cmd.name, list_item->cmd.name) > 0) { + break; + } + last = it; + } + + if (last == NULL) { + SLIST_INSERT_HEAD(&s_dynamic_cmd_list, list_item, next_item); + } else { + SLIST_INSERT_AFTER(last, list_item, next_item); + } + + s_number_of_registered_commands++; + + esp_dynamic_commands_unlock(); + + return ESP_OK; +} + +esp_err_t esp_dynamic_commands_replace(esp_command_t *item_cmd) +{ + esp_dynamic_commands_lock(); + + esp_command_internal_t *list_item = CONTAINER_OF(item_cmd, esp_command_internal_t, cmd); + memcpy(&list_item->cmd, item_cmd, sizeof(esp_command_t)); + + esp_dynamic_commands_unlock(); + + return ESP_OK; +} + +esp_err_t esp_dynamic_commands_remove(esp_command_t *item_cmd) +{ + esp_dynamic_commands_lock(); + + esp_command_internal_t *list_item = CONTAINER_OF(item_cmd, esp_command_internal_t, cmd); + SLIST_REMOVE(&s_dynamic_cmd_list, list_item, esp_command_internal, next_item); + + s_number_of_registered_commands--; + + esp_dynamic_commands_unlock(); + + free(list_item); + + return ESP_OK; +} + +size_t esp_dynamic_commands_get_number_of_cmd(void) +{ + esp_dynamic_commands_lock(); + size_t nb_of_registered_cmd = s_number_of_registered_commands; + esp_dynamic_commands_unlock(); + return nb_of_registered_cmd; +} diff --git a/esp_commands/idf_component.yml b/esp_commands/idf_component.yml new file mode 100644 index 0000000000..c58bc23f5d --- /dev/null +++ b/esp_commands/idf_component.yml @@ -0,0 +1,9 @@ +version: "1.0.0" +description: "esp_commands - Command handling component" +url: https://github.com/espressif/idf-extra-components/tree/master/esp_commands +dependencies: + idf: ">=6.0" +sbom: + manifests: + - path: sbom_esp_commands.yml + dest: . \ No newline at end of file diff --git a/esp_commands/include/esp_commands.h b/esp_commands/include/esp_commands.h new file mode 100644 index 0000000000..f8761dfa3a --- /dev/null +++ b/esp_commands/include/esp_commands.h @@ -0,0 +1,303 @@ +/* + * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include "esp_err.h" + +/** + * @brief Console command main function type with user context + * + * This function type is used to implement a console command. + * + * @param context User-defined context passed at invocation + * @param argc Number of arguments + * @param argv Array of argc entries, each pointing to a null-terminated string argument + * @return Return code of the console command; 0 indicates success + */ +typedef int (*esp_command_func_t)(void *context, int argc, char **argv); + +/** + * @brief Callback to generate a command hint + * + * This function is called to retrieve a short hint for a command, + * typically used for auto-completion or UI help. + * + * @param context Context registered when the command was registered + * @return Persistent string containing the generated hint + */ +typedef const char *(*esp_command_hint_t)(void *context); + +/** + * @brief Callback to generate a command glossary entry + * + * This function is called to retrieve detailed description or glossary + * information for a command. + * + * @param context Context registered when the command was registered + * @return Persistent string containing the generated glossary + */ +typedef const char *(*esp_command_glossary_t)(void *context); + +/** + * @brief Structure describing a console command + * + * @note Only one of `func` or `func_ctx` should be set. + * @note The `group` field allows categorizing commands into groups, + * which can simplify filtering or listing commands. + */ +typedef struct esp_command { + const char *name; /*!< Name of the command */ + const char *group; /*!< Command group to which this command belongs */ + const char *help; /*!< Short help text for the command */ + esp_command_func_t func; /*!< Function implementing the command */ + void *func_ctx; /*!< User-defined context for the command function */ + esp_command_hint_t hint_cb; /*!< Callback returning the hint for the command */ + esp_command_glossary_t glossary_cb; /*!< Callback returning the glossary for the command */ +} esp_command_t; + +/** + * @brief Macro to define a forced inline accessor for a string field of esp_command_t + * + * @param NAME Field name of the esp_command_t structure + */ +#define DEFINE_FIELD_ACCESSOR(NAME) \ + static inline __attribute__((always_inline)) \ + const char *get_##NAME(const esp_command_t *cmd) { \ + if (!cmd) { \ + return NULL; \ + } \ + return cmd->NAME; \ + } + +/** + * @brief Macro expanding to + * static inline __attribute__((always_inline)) const char *get_name(esp_command_t *cmd) { + * if (!cmd) { + * return NULL; + * } + * return cmd->name; + * } + */ +DEFINE_FIELD_ACCESSOR(name) + +/** + * @brief Macro expanding to + * static inline __attribute__((always_inline)) const char *get_group(esp_command_t *cmd) { + * if (!cmd) { + * return NULL; + * } + * return cmd->group; + * } + */ +DEFINE_FIELD_ACCESSOR(group) + +/** + * @brief Macro expanding to + * static inline __attribute__((always_inline)) const char *get_help(esp_command_t *cmd) { + * if (!cmd) { + * return NULL; + * } + * return cmd->help; + * } + */ +DEFINE_FIELD_ACCESSOR(help) + +/** + * @brief Macro to create the accessor function name for a field of esp_command_t + * + * @param NAME Field name of esp_command_t + */ +#define FIELD_ACCESSOR(NAME) get_##NAME + +/** + * @brief Configuration parameters for esp_commands_manager initialization + */ +typedef struct esp_commands_config { + size_t max_cmdline_length; /*!< Maximum length of the command line buffer, in bytes */ + size_t max_cmdline_args; /*!< Maximum number of command line arguments to parse */ + int hint_color; /*!< ANSI color code used for hint text */ + bool hint_bold; /*!< If true, display hint text in bold */ +} esp_commands_config_t; + +/** + * @brief Default configuration for esp_commands_manager + */ +#define ESP_COMMANDS_CONFIG_DEFAULT() \ +{ \ + .max_cmdline_length = 256, \ + .max_cmdline_args = 32, \ + .hint_color = 39, \ + .hint_bold = false \ +} + +/** + * @brief Callback for a completed command name + * + * This callback is called when a command is successfully completed. + * + * @param completed_cmd_name Completed command name + */ +typedef void (*esp_command_get_completion_t)(const char *completed_cmd_name); + +/** + * @brief Callback to retrieve a string field of esp_command_t + * + * @param cmd Command object + * @return Value of the requested string field + */ +typedef const char *(*esp_commands_get_field_t)(const esp_command_t *cmd); + +/** + * @brief Opaque handle to a set of commands + */ +typedef struct esp_command_sets *esp_command_set_handle_t; + +/** + * @brief Update the component configuration + * + * @param config Configuration data to update + * @return ESP_OK if successful + * ESP_ERR_INVALID_ARG if config pointer is NULL + */ +esp_err_t esp_commands_update_config(const esp_commands_config_t *config); + +/** + * @brief macro registering a command and placing it in a specific section of flash.rodata + * @note see the linker.lf file for more information concerning the section characteristics + */ +#define ESP_COMMAND_REGISTER(cmd_name, cmd_group, cmd_help, cmd_func, cmd_func_ctx, cmd_hint_cb, cmd_glossary_cb) \ + static_assert((cmd_func) != NULL); \ + static const esp_command_t cmd_name __attribute__((used, section(".esp_commands"))) = { \ + .name = #cmd_name, \ + .group = #cmd_group, \ + .help = cmd_help, \ + .func = cmd_func, \ + .func_ctx = cmd_func_ctx, \ + .hint_cb = cmd_hint_cb, \ + .glossary_cb = cmd_glossary_cb \ + }; + +/** + * @brief Register a command + * + * @param cmd Pointer to the command structure + * @return ESP_OK if successful + * Other esp_err_t on error + */ +esp_err_t esp_commands_register_cmd(esp_command_t *cmd); + +/** + * @brief Unregister a command by name or group + * + * @param cmd_name Name or group of the command to unregister + * @return ESP_OK if successful + * Other esp_err_t on error + */ +esp_err_t esp_commands_unregister_cmd(const char *cmd_name); + +/** + * @brief Execute a command line + * + * @param cmd_set Set of commands allowed to execute. If NULL, all registered commands are allowed + * @param cmd_line Command line string to execute + * @param cmd_ret Return value from the command function + * @return ESP_OK on success + * ESP_ERR_INVALID_ARG if the command line is empty or only whitespace + * ESP_ERR_NOT_FOUND if command is not found in cmd_set + * ESP_ERR_NO_MEM if internal memory allocation fails + */ +esp_err_t esp_commands_execute(esp_command_set_handle_t cmd_set, const char *cmdline, int *cmd_ret); + +/** + * @brief Find a command by name within a specific command set. + * + * This function searches a command whose name matches the provided string. + * + * @param cmd_set Handle to the command set to search in. Must be a valid + * `esp_command_set_handle_t` or `NULL` if the search should be performed + * on all statically adn dynamically registered commands. + * @param name String containing the name of the command to search for. + * + * @return pointer to the matching command or NULL if no command is found. + */ +esp_command_t *esp_commands_find_command(esp_command_set_handle_t cmd_set, const char *name); + +/** + * @brief Provide command completion for linenoise library + * + * @param cmd_set Set of commands allowed for completion. If NULL, all registered commands are used + * @param buf Input string typed by the user + * @param completion_cb Callback to return completed command names + */ +void esp_commands_get_completion(esp_command_set_handle_t cmd_set, const char *buf, esp_command_get_completion_t completion_cb); + +/** + * @brief Provide command hint for linenoise library + * + * @param cmd_set Set of commands allowed for hinting. If NULL, all registered commands are used + * @param buf Input string typed by the user + * @param[out] color ANSI color code for hint text + * @param[out] bold True if hint should be displayed in bold + * @return Persistent string containing the hint; must not be freed + */ +const char *esp_commands_get_hint(esp_command_set_handle_t cmd_set, const char *buf, int *color, bool *bold); + +/** + * @brief Retrieve glossary for a command line + * + * @param cmd_set Set of commands allowed + * @param buf Command line typed by the user + * @return Persistent string containing the glossary; must not be freed + */ +const char *esp_commands_get_glossary(esp_command_set_handle_t cmd_set, const char *buf); + +/** + * @brief Create a command set from an array of command names + * + * @param cmd_set Array of command names + * @param cmd_set_size Number of entries in cmd_set + * @param get_field Function to retrieve the field from esp_command_t for comparison + * @return Handle to the created command set + */ +esp_command_set_handle_t esp_commands_create_cmd_set(const char **cmd_set, const size_t cmd_set_size, esp_commands_get_field_t get_field); + +/** + * @brief Convenience macro to create a command set + * + * @param cmd_set Array of command names + * @param accessor Field accessor function + */ +#define ESP_COMMANDS_CREATE_CMD_SET(cmd_set, accessor) \ + esp_commands_create_cmd_set(cmd_set, sizeof(cmd_set) / sizeof((cmd_set)[0]), accessor) + +/** + * @brief Concatenate two command sets + * + * @note If one set is NULL, the other is returned + * @note If both are NULL, returns NULL + * @note Duplicates are not removed + * + * @param cmd_set_a First command set + * @param cmd_set_b Second command set + * @return New command set containing all commands from both sets + */ +esp_command_set_handle_t esp_commands_concat_cmd_set(esp_command_set_handle_t cmd_set_a, esp_command_set_handle_t cmd_set_b); + +/** + * @brief Destroy a command set + * + * @param cmd_set Pointer to the handle of the command set to destroy + */ +void esp_commands_destroy_cmd_set(esp_command_set_handle_t *cmd_set); + +#ifdef __cplusplus +} +#endif diff --git a/esp_commands/linker.lf b/esp_commands/linker.lf new file mode 100644 index 0000000000..c33eef3399 --- /dev/null +++ b/esp_commands/linker.lf @@ -0,0 +1,13 @@ +[sections:esp_commands] +entries: + .esp_commands + +[scheme:esp_commands_default] +entries: + esp_commands -> flash_rodata + +[mapping:esp_commands] +archive: * +entries: + * (esp_commands_default); + esp_commands -> flash_rodata KEEP() SORT(name) SURROUND(esp_commands) diff --git a/esp_commands/private_include/esp_commands_helpers.h b/esp_commands/private_include/esp_commands_helpers.h new file mode 100644 index 0000000000..4ee67164ac --- /dev/null +++ b/esp_commands/private_include/esp_commands_helpers.h @@ -0,0 +1,27 @@ +/* + * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +/** + * @brief Split a command line and populate argc and argv parameters + * + * @param line the line that has to be split into arguments + * @param argv array of arguments created from the line + * @param argv_size size of the argument array + * @return size_t number of arguments found in the line and stored + * in argv + */ +size_t esp_commands_split_argv(char *line, char **argv, size_t argv_size); + +#ifdef __cplusplus +} +#endif diff --git a/esp_commands/private_include/esp_dynamic_commands.h b/esp_commands/private_include/esp_dynamic_commands.h new file mode 100644 index 0000000000..1b9547ac47 --- /dev/null +++ b/esp_commands/private_include/esp_dynamic_commands.h @@ -0,0 +1,139 @@ +/* + * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include "sys/queue.h" +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" +#include "esp_commands.h" + +/** + * @brief Structure representing a fixed set of commands. + * + * This is typically used for static or predefined command lists. + */ +typedef struct esp_command_set { + esp_command_t **cmd_ptr_set; /*!< Array of pointers to commands. */ + size_t cmd_set_size; /*!< Number of commands in the set. */ +} esp_command_set_t; + +/** + * @brief Internal structure for a dynamically registered command. + * + * Each dynamic command is stored as an `esp_command_t` plus + * linked list metadata for insertion/removal. + */ +typedef struct esp_command_internal { + esp_command_t cmd; /*!< Command instance. */ + SLIST_ENTRY(esp_command_internal) next_item; /*!< Linked list entry metadata. */ +} esp_command_internal_t; + +/** + * @brief Linked list head type for dynamic command storage. + */ +typedef SLIST_HEAD(esp_command_internal_ll, esp_command_internal) esp_command_internal_ll_t; + +/** + * @brief Iterate over a set of commands, either from a static set or dynamic list. + * + * This macro supports iterating over: + * - A provided `esp_command_set_t` (static set), OR + * - The global dynamic command list if `cmd_set` is `NULL`. + * + * @param cmd_set Pointer to a command set (`esp_command_set_t`) or `NULL` for dynamic commands. + * @param item_cmd Iterator variable of type `esp_command_t *` that will point to each command. + * + * @note Internally, the macro uses `_node` and `_i` as hidden variables. + */ +#define FOR_EACH_DYNAMIC_COMMAND(cmd_set, item_cmd) \ + __attribute__((unused)) esp_command_internal_t *_node = \ + ((cmd_set) == NULL ? SLIST_FIRST(esp_dynamic_commands_get_list()) \ + : NULL); \ + __attribute__((unused)) size_t _i = 0; \ + for (; \ + ((cmd_set) == NULL \ + ? ((_node != NULL) && ((item_cmd) = &_node->cmd)) \ + : (_i < (cmd_set)->cmd_set_size && \ + ((item_cmd) = (cmd_set)->cmd_ptr_set[_i]))); \ + ((cmd_set) == NULL \ + ? (_node = SLIST_NEXT(_node, next_item)) \ + : (void)++_i)) + +/** + * @brief Acquire the dynamic commands lock. + * + * This function must be called before modifying or iterating over + * the dynamic command list to ensure thread safety. + */ +void esp_dynamic_commands_lock(void); + +/** + * @brief Release the dynamic commands lock. + * + * Call this after operations on the dynamic command list are complete. + */ +void esp_dynamic_commands_unlock(void); + +/** + * @brief Get the internal linked list of dynamic commands. + * + * @return Pointer to the dynamic command linked list head. + * + * @warning The returned list is internal; do not modify it directly. + * Use provided API functions to modify dynamic commands. + */ +const esp_command_internal_ll_t *esp_dynamic_commands_get_list(void); + +/** + * @brief Add a new command to the dynamic command list. + * + * @param cmd Pointer to the command to add. + * @return + * - `ESP_OK` on success. + * - Appropriate error code on failure. + * + * @note The function acquires the lock internally. + */ +esp_err_t esp_dynamic_commands_add(esp_command_t *cmd); + +/** + * @brief Replace an existing command in the dynamic command list. + * + * If a command with the same name exists, it will be replaced. + * + * @param item_cmd Pointer to the new command data. + * @return + * - `ESP_OK` on success. + * - Appropriate error code on failure. + */ +esp_err_t esp_dynamic_commands_replace(esp_command_t *item_cmd); + +/** + * @brief Remove a command from the dynamic command list. + * + * @param item_cmd Pointer to the command to remove. + * @return + * - `ESP_OK` on success. + * - Appropriate error code on failure. + */ +esp_err_t esp_dynamic_commands_remove(esp_command_t *item_cmd); + +/** + * @brief Get the number of registered dynamic commands. + * + * @return The total number of dynamic commands currently registered. + */ +size_t esp_dynamic_commands_get_number_of_cmd(void); + +#ifdef __cplusplus +} +#endif diff --git a/esp_commands/sbom_esp_commands.yml b/esp_commands/sbom_esp_commands.yml new file mode 100644 index 0000000000..f666a01477 --- /dev/null +++ b/esp_commands/sbom_esp_commands.yml @@ -0,0 +1,6 @@ +name: esp_commands +description: Command handling component +url: https://github.com/espressif/idf-extra-components/tree/master/esp_commands +version: 1.0.0 +cpe: cpe:2.3:a:espressif:esp_commands:{}:*:*:*:*:*:*:* +supplier: 'Organization: Espressif Systems' \ No newline at end of file From 27c08b234f72bd10f4f8c7571a6f70dcac55d209 Mon Sep 17 00:00:00 2001 From: Guillaume Souchere Date: Thu, 7 Aug 2025 14:16:23 +0200 Subject: [PATCH 2/4] feat(esp_commands): Add tests to the esp_commands component --- esp_commands/README.md | 181 +++++++ esp_commands/include/esp_commands.h | 2 +- esp_commands/test_apps/CMakeLists.txt | 5 + esp_commands/test_apps/main/CMakeLists.txt | 4 + esp_commands/test_apps/main/idf_component.yml | 4 + .../main/include/test_esp_commands_utils.h | 72 +++ .../test_apps/main/test_esp_commands.c | 452 ++++++++++++++++++ esp_commands/test_apps/main/test_main.c | 26 + esp_commands/test_apps/pytest_esp_commands.py | 9 + esp_commands/test_apps/sdkconfig.defaults | 1 + 10 files changed, 755 insertions(+), 1 deletion(-) create mode 100644 esp_commands/test_apps/CMakeLists.txt create mode 100644 esp_commands/test_apps/main/CMakeLists.txt create mode 100644 esp_commands/test_apps/main/idf_component.yml create mode 100644 esp_commands/test_apps/main/include/test_esp_commands_utils.h create mode 100644 esp_commands/test_apps/main/test_esp_commands.c create mode 100644 esp_commands/test_apps/main/test_main.c create mode 100644 esp_commands/test_apps/pytest_esp_commands.py create mode 100644 esp_commands/test_apps/sdkconfig.defaults diff --git a/esp_commands/README.md b/esp_commands/README.md index e69de29bb2..422b6b4b5f 100644 --- a/esp_commands/README.md +++ b/esp_commands/README.md @@ -0,0 +1,181 @@ +# ESP Commands + +The `esp_commands` component provides a flexible command registration and execution framework for ESP-IDF applications. +It allows applications to define console-like commands with metadata (help text, hints, glossary entries) and register them dynamically or statically. + +--- + +## Features + +- Define commands with: + - Command name + - Group categorization + - Help text + - Optional hints and glossary callbacks +- Register commands at runtime or at compile-time (via section placement macros). +- Execute commands from command line strings. +- Provide command completion, hints, and glossary callback registration mechanism. +- Create and manage subsets of commands (command sets). +- Customizable configuration for command parsing and hint display. + +--- + +## Configuration + +The component is initialized with a configuration struct: + +```c +esp_commands_config_t config = ESP_COMMANDS_CONFIG_DEFAULT(); +esp_commands_update_config(&config); +``` + +- `max_cmdline_length`: Maximum command line buffer length (bytes). +- `max_cmdline_args`: Maximum number of arguments parsed. +- `hint_color`: ANSI color code used for hints. +- `hint_bold`: Whether hints are displayed in bold. + +--- + +## Defining Commands + +### Command Structure + +A command is described by the `esp_command_t` struct: + +```c +typedef struct esp_command { + const char *name; /*!< Command name */ + const char *group; /*!< Group/category */ + const char *help; /*!< Short help text */ + esp_command_func_t func; /*!< Command implementation */ + void *func_ctx; /*!< User context */ + esp_command_hint_t hint_cb; /*!< Hint callback */ + esp_command_glossary_t glossary_cb; /*!< Glossary callback */ +} esp_command_t; +``` + +### Static Registration + +Use the `ESP_COMMAND_REGISTER` macro to register a command at compile time: + +```c +static int my_cmd(void *ctx, int argc, char **argv) { + printf("Hello from my_cmd!\n"); + return 0; +} + +ESP_COMMAND_REGISTER(my_cmd, tools, "Prints hello", my_cmd, NULL, NULL, NULL); +``` + +This places the command into the `.esp_commands` section. + +### Dynamic Registration + +Commands can also be registered/unregistered at runtime: + +```c +esp_command_t cmd = { + .name = "echo", + .group = "utils", + .help = "Echoes arguments back", + .func = echo_func, +}; + +esp_commands_register_cmd(&cmd); +esp_commands_unregister_cmd("echo"); +``` + +--- + +## Executing Commands + +Commands can be executed from a command line string: + +```c +int cmd_ret; +esp_err_t ret = esp_commands_execute(NULL, "my_cmd arg1 arg2", &cmd_ret); +``` + +- `cmd_set`: Limits execution to a set of commands (or `NULL` for all commands). +- `cmd_line`: String containing the command and arguments. +- `cmd_ret`: Receives the command function return value. + +--- + +## Command Completion, Hints, and Glossary + +Completion & Help APIs: + +```c +esp_commands_get_completion(NULL, "ec", completion_cb); +const char *hint = esp_commands_get_hint(NULL, "echo", &color, &bold); +const char *glossary = esp_commands_get_glossary(NULL, "echo"); +``` + +- **Completion**: Suggests matching commands. +- **Hint**: Provides a short usage hint. +- **Glossary**: Provides detailed command description. + +--- + +## Command Sets + +Command sets allow grouping subsets of commands for filtering: + +```c +const char *cmd_names[] = {"echo", "my_cmd"}; +esp_command_set_handle_t set = + ESP_COMMANDS_CREATE_CMD_SET(cmd_names, FIELD_ACCESSOR(name)); + +esp_commands_execute(set, "echo Hello!", NULL); +esp_commands_destroy_cmd_set(&set); +``` + +- Create sets by name, group, or other fields. +- Concatenate sets with `esp_commands_concat_cmd_set()`. +- Destroy sets when no longer needed. + +--- + +## Quick Start Example + +```c +#include +#include "esp_commands.h" + +// Example command function +static int hello_cmd(void *ctx, int argc, char **argv) { + printf("Hello, ESP Commands!\n"); + return 0; +} + +// Register command statically +ESP_COMMAND_REGISTER(hello_cmd, demo, "Prints a hello message", hello_cmd, NULL, NULL, NULL); + +void app_main(void) { + // Update configuration (optional) + esp_commands_config_t config = ESP_COMMANDS_CONFIG_DEFAULT(); + esp_commands_update_config(&config); + + // Execute command + int ret_val; + esp_err_t ret = esp_commands_execute(NULL, "hello_cmd", &ret_val); + if (ret == ESP_OK) { + printf("Command executed successfully, return value: %d\n", ret_val); + } else { + printf("Failed to execute command, error: %d\n", ret); + } +} +``` + +--- + +## API Reference + +- **Configuration**: `esp_commands_update_config()` +- **Registration**: `esp_commands_register_cmd()`, `esp_commands_unregister_cmd()` +- **Execution**: `esp_commands_execute()`, `esp_commands_find_command()` +- **Completion & Help APIs**: `esp_commands_get_completion()`, `esp_commands_get_hint()`, `esp_commands_get_glossary()` +- **Command Sets**: `esp_commands_create_cmd_set()`, `esp_commands_concat_cmd_set()`, `esp_commands_destroy_cmd_set()` + +--- diff --git a/esp_commands/include/esp_commands.h b/esp_commands/include/esp_commands.h index f8761dfa3a..56ebb501a7 100644 --- a/esp_commands/include/esp_commands.h +++ b/esp_commands/include/esp_commands.h @@ -223,7 +223,7 @@ esp_err_t esp_commands_execute(esp_command_set_handle_t cmd_set, const char *cmd * * @param cmd_set Handle to the command set to search in. Must be a valid * `esp_command_set_handle_t` or `NULL` if the search should be performed - * on all statically adn dynamically registered commands. + * on all statically and dynamically registered commands. * @param name String containing the name of the command to search for. * * @return pointer to the matching command or NULL if no command is found. diff --git a/esp_commands/test_apps/CMakeLists.txt b/esp_commands/test_apps/CMakeLists.txt new file mode 100644 index 0000000000..16e55d1d1d --- /dev/null +++ b/esp_commands/test_apps/CMakeLists.txt @@ -0,0 +1,5 @@ +cmake_minimum_required(VERSION 3.16) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +set(COMPONENTS main) +project(esp_commands_test) diff --git a/esp_commands/test_apps/main/CMakeLists.txt b/esp_commands/test_apps/main/CMakeLists.txt new file mode 100644 index 0000000000..de2402ea64 --- /dev/null +++ b/esp_commands/test_apps/main/CMakeLists.txt @@ -0,0 +1,4 @@ +idf_component_register(SRCS "test_esp_commands.c" "test_main.c" + PRIV_INCLUDE_DIRS "." "include" + PRIV_REQUIRES unity + WHOLE_ARCHIVE) diff --git a/esp_commands/test_apps/main/idf_component.yml b/esp_commands/test_apps/main/idf_component.yml new file mode 100644 index 0000000000..1abe4b08c4 --- /dev/null +++ b/esp_commands/test_apps/main/idf_component.yml @@ -0,0 +1,4 @@ +dependencies: + espressif/esp_commands: + version: "*" + override_path: "../.." diff --git a/esp_commands/test_apps/main/include/test_esp_commands_utils.h b/esp_commands/test_apps/main/include/test_esp_commands_utils.h new file mode 100644 index 0000000000..eab21e6a72 --- /dev/null +++ b/esp_commands/test_apps/main/include/test_esp_commands_utils.h @@ -0,0 +1,72 @@ +/* + * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#define NB_OF_REGISTERED_CMD 8 + +#define GET_NAME(NAME, SUFFIX) NAME##SUFFIX +#define GET_STR(STR) #STR + +#define CREATE_CMD_FUNC(NAME) \ + static int GET_NAME(NAME, _func)(void *ctx, int argc, char **argv) { \ + printf(GET_STR(NAME) GET_STR(_func)); \ + printf("\n"); \ + return 0; \ + } + +#define CREATE_FUNC(NAME, SUFFIX) \ + static const char *GET_NAME(NAME, SUFFIX)(void *context) { \ + return #NAME #SUFFIX; \ + } + +/* static command functions*/ +CREATE_CMD_FUNC(cmd_a) +CREATE_CMD_FUNC(cmd_b) +CREATE_CMD_FUNC(cmd_c) +CREATE_CMD_FUNC(cmd_d) +CREATE_CMD_FUNC(cmd_e) +CREATE_CMD_FUNC(cmd_f) +CREATE_CMD_FUNC(cmd_g) +CREATE_CMD_FUNC(cmd_h) + +/* static hint functions*/ +CREATE_FUNC(cmd_a, _hint) +CREATE_FUNC(cmd_b, _hint) +CREATE_FUNC(cmd_c, _hint) +CREATE_FUNC(cmd_d, _hint) +CREATE_FUNC(cmd_e, _hint) +CREATE_FUNC(cmd_f, _hint) +CREATE_FUNC(cmd_g, _hint) +CREATE_FUNC(cmd_h, _hint) + +/* static glossary functions*/ +CREATE_FUNC(cmd_a, _glossary) +CREATE_FUNC(cmd_b, _glossary) +CREATE_FUNC(cmd_c, _glossary) +CREATE_FUNC(cmd_d, _glossary) +CREATE_FUNC(cmd_e, _glossary) +CREATE_FUNC(cmd_f, _glossary) +CREATE_FUNC(cmd_g, _glossary) +CREATE_FUNC(cmd_h, _glossary) + +/* command registration */ +ESP_COMMAND_REGISTER(cmd_a, group_1, GET_STR(cmd_a_help), cmd_a_func, NULL, cmd_a_hint, cmd_a_glossary); +ESP_COMMAND_REGISTER(cmd_b, group_1, GET_STR(cmd_b_help), cmd_b_func, NULL, cmd_b_hint, cmd_b_glossary); +ESP_COMMAND_REGISTER(cmd_c, group_2, GET_STR(cmd_c_help), cmd_c_func, NULL, cmd_c_hint, cmd_c_glossary); +ESP_COMMAND_REGISTER(cmd_d, group_2, GET_STR(cmd_d_help), cmd_d_func, NULL, cmd_d_hint, cmd_d_glossary); +ESP_COMMAND_REGISTER(cmd_e, group_3, GET_STR(cmd_e_help), cmd_e_func, NULL, cmd_e_hint, cmd_e_glossary); +ESP_COMMAND_REGISTER(cmd_f, group_3, GET_STR(cmd_f_help), cmd_f_func, NULL, cmd_f_hint, cmd_f_glossary); +ESP_COMMAND_REGISTER(cmd_g, group_4, GET_STR(cmd_g_help), cmd_g_func, NULL, cmd_g_hint, cmd_g_glossary); +ESP_COMMAND_REGISTER(cmd_h, group_4, GET_STR(cmd_h_help), cmd_h_func, NULL, cmd_h_hint, cmd_h_glossary); + +#ifdef __cplusplus +} +#endif diff --git a/esp_commands/test_apps/main/test_esp_commands.c b/esp_commands/test_apps/main/test_esp_commands.c new file mode 100644 index 0000000000..ab1369540f --- /dev/null +++ b/esp_commands/test_apps/main/test_esp_commands.c @@ -0,0 +1,452 @@ +/* + * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include "unity.h" +#include "esp_commands.h" +#include "test_esp_commands_utils.h" + +/* + * IMPORTANT: + * - 8 commands are created in test_esp_commands_utils.h (cmd_a - cmd_h) + * - the commands are divided in 4 groups (group_1 - group_4) + * - each group contains 2 commands. + * - group_1 contains cmd_a and cmd_b, + * [...] + * - group_4 contains cmd_g and cmd_h + */ + +static void test_setup(void) +{ + const esp_commands_config_t config = ESP_COMMANDS_CONFIG_DEFAULT(); + TEST_ASSERT_EQUAL(ESP_OK, esp_commands_update_config(&config)); +} + +TEST_CASE("help command - called without command set", "[esp_commands]") +{ + test_setup(); + + /* call esp_commands_execute to run help command with verbosity 0 */ + int cmd_ret = -1; + TEST_ASSERT_EQUAL(ESP_OK, esp_commands_execute(NULL, "help -v 0", &cmd_ret)); + TEST_ASSERT_EQUAL(0, cmd_ret); + + /* call esp_commands_execute to run help command with verbosity 1 */ + cmd_ret = -1; + TEST_ASSERT_EQUAL(ESP_OK, esp_commands_execute(NULL, "help -v 1", &cmd_ret)); + TEST_ASSERT_EQUAL(0, cmd_ret); + + /* call esp_commands_execute to run help command on a registered command */ + cmd_ret = -1; + TEST_ASSERT_EQUAL(ESP_OK, esp_commands_execute(NULL, "help cmd_a -v 0", &cmd_ret)); + TEST_ASSERT_EQUAL(0, cmd_ret); + TEST_ASSERT_EQUAL(ESP_OK, esp_commands_execute(NULL, "help cmd_a -v 1", &cmd_ret)); + TEST_ASSERT_EQUAL(0, cmd_ret); + + /* call esp_commands_execute to run help command on an unregistered command */ + cmd_ret = -1; + TEST_ASSERT_EQUAL(ESP_OK, esp_commands_execute(NULL, "help cmd_w", &cmd_ret)); + TEST_ASSERT_EQUAL(1, cmd_ret); + + /* call esp_commands_execute to run help command on a registered command with wrong + * verbosity syntax */ + cmd_ret = -1; + TEST_ASSERT_EQUAL(ESP_OK, esp_commands_execute(NULL, "help cmd_a -v=1", &cmd_ret)); + TEST_ASSERT_EQUAL(1, cmd_ret); + + /* call esp_commands_execute to run help command with too many command names */ + cmd_ret = -1; + TEST_ASSERT_EQUAL(ESP_OK, esp_commands_execute(NULL, "help cmd_a cmd_b -v 1", &cmd_ret)); + TEST_ASSERT_EQUAL(1, cmd_ret); +} + +TEST_CASE("test command set error handling", "[esp_commands]") +{ + test_setup(); + + /* create a command set with NULL passed as list of command id */ + TEST_ASSERT_NULL(esp_commands_create_cmd_set(NULL, 2, FIELD_ACCESSOR(group))); + + /* create a command set with 0 as size of list of command id */ + const char *group_set_a[] = {"b", "group_4"}; + TEST_ASSERT_NULL(esp_commands_create_cmd_set(group_set_a, 0, FIELD_ACCESSOR(group))); + + /* concatenate 2 NULL sets */ + TEST_ASSERT_NULL(esp_commands_concat_cmd_set(NULL, NULL)); + + /* redefinition of esp_command_set_t so we can access the fields + * and test their values */ + typedef struct cmd_set { + esp_command_t **cmd_ptr_set; + size_t cmd_set_size; + } cmd_set_t; + + /* pass wrong command name in array, expect a non null command set handle with 0 items in it*/ + const char *group_set_b[] = {"group2", "group4"}; + esp_command_set_handle_t group_set_handle_b = esp_commands_create_cmd_set(group_set_b, 2, FIELD_ACCESSOR(group)); + cmd_set_t *cmd_set = (cmd_set_t *)group_set_handle_b; + TEST_ASSERT_NOT_NULL(group_set_handle_b); + TEST_ASSERT_NULL(cmd_set->cmd_ptr_set); + TEST_ASSERT_EQUAL(0, cmd_set->cmd_set_size); + + esp_commands_destroy_cmd_set(&group_set_handle_b); +} + +typedef struct cmd_test_sequence { + const char *cmd_list[NB_OF_REGISTERED_CMD]; + int expected_ret_val[NB_OF_REGISTERED_CMD]; +} cmd_test_sequence_t; + +static void run_cmd_test(esp_command_set_handle_t handle, const char **cmd_list, const int *expected_ret_val, size_t nb_cmds) +{ + for (size_t i = 0; i < nb_cmds; i++) { + int cmd_ret = -1; + esp_err_t expected = expected_ret_val[i] == 0 ? ESP_OK : ESP_ERR_NOT_FOUND; + TEST_ASSERT_EQUAL(expected, esp_commands_execute(handle, cmd_list[i], &cmd_ret)); + TEST_ASSERT_EQUAL(expected_ret_val[i], cmd_ret); + } +} + +TEST_CASE("test static command set", "[esp_commands]") +{ + test_setup(); + + const char *cmd_list[] = {"cmd_a", "cmd_b", "cmd_c", "cmd_d", "cmd_e", "cmd_f", "cmd_g", "cmd_h"}; + const size_t nb_cmds = sizeof(cmd_list) / sizeof(cmd_list[0]); + int expected_ret_val[nb_cmds]; + + /* create sets by group */ + const char *group_set_a[] = {"group_1", "group_3"}; + esp_command_set_handle_t handle_set_a = ESP_COMMANDS_CREATE_CMD_SET(group_set_a, FIELD_ACCESSOR(group)); + TEST_ASSERT_NOT_NULL(handle_set_a); + + const char *group_set_b[] = {"group_2", "group_4"}; + esp_command_set_handle_t handle_set_b = ESP_COMMANDS_CREATE_CMD_SET(group_set_b, FIELD_ACCESSOR(group)); + TEST_ASSERT_NOT_NULL(handle_set_b); + + /* test set_a by group */ + int tmp_ret[] = {0, 0, -1, -1, 0, 0, -1, -1}; + memcpy(expected_ret_val, tmp_ret, sizeof(tmp_ret)); + run_cmd_test(handle_set_a, cmd_list, expected_ret_val, nb_cmds); + + /* test set_b by group */ + int tmp_ret_b[] = {-1, -1, 0, 0, -1, -1, 0, 0}; + memcpy(expected_ret_val, tmp_ret_b, sizeof(tmp_ret_b)); + run_cmd_test(handle_set_b, cmd_list, expected_ret_val, nb_cmds); + + /* test help command with set of static commands */ + int cmd_ret; + TEST_ASSERT_EQUAL(ESP_OK, esp_commands_execute(handle_set_a, "help", &cmd_ret)); + TEST_ASSERT_EQUAL(0, cmd_ret); + TEST_ASSERT_EQUAL(ESP_OK, esp_commands_execute(handle_set_b, "help", &cmd_ret)); + TEST_ASSERT_EQUAL(0, cmd_ret); + + /* destroy sets */ + esp_commands_destroy_cmd_set(&handle_set_a); + esp_commands_destroy_cmd_set(&handle_set_b); + + /* create sets by name */ + const char *cmd_name_set_a[] = {"cmd_a", "cmd_b", "cmd_c"}; + handle_set_a = esp_commands_create_cmd_set(cmd_name_set_a, 3, FIELD_ACCESSOR(name)); + TEST_ASSERT_NOT_NULL(handle_set_a); + + const char *cmd_name_set_b[] = {"cmd_f", "cmd_g", "cmd_h"}; + handle_set_b = esp_commands_create_cmd_set(cmd_name_set_b, 3, FIELD_ACCESSOR(name)); + TEST_ASSERT_NOT_NULL(handle_set_b); + + int tmp_ret2[] = {0, 0, 0, -1, -1, -1, -1, -1}; + memcpy(expected_ret_val, tmp_ret2, sizeof(tmp_ret2)); + run_cmd_test(handle_set_a, cmd_list, expected_ret_val, nb_cmds); + + int tmp_ret3[] = {-1, -1, -1, -1, -1, 0, 0, 0}; + memcpy(expected_ret_val, tmp_ret3, sizeof(tmp_ret3)); + run_cmd_test(handle_set_b, cmd_list, expected_ret_val, nb_cmds); + + /* concatenate sets */ + esp_command_set_handle_t handle_set_c = esp_commands_concat_cmd_set(handle_set_a, handle_set_b); + TEST_ASSERT_NOT_NULL(handle_set_c); + + int tmp_ret4[] = {0, 0, 0, -1, -1, 0, 0, 0}; + memcpy(expected_ret_val, tmp_ret4, sizeof(tmp_ret4)); + run_cmd_test(handle_set_c, cmd_list, expected_ret_val, nb_cmds); + + esp_commands_destroy_cmd_set(&handle_set_c); +} + +static int dummy_cmd_func(void *context, int argc, char **argv) +{ + printf("dynamic command called\n"); + return 0; // always return success +} + +TEST_CASE("test dynamic command set", "[esp_commands]") +{ + test_setup(); + + const char *cmd_list[] = {"cmd_1", "cmd_2", "cmd_3", "cmd_4", "cmd_5", "cmd_6", "cmd_7", "cmd_8"}; + const size_t nb_cmds = sizeof(cmd_list) / sizeof(cmd_list[0]); + int expected_ret_val[nb_cmds]; + + /* dynamically register commands */ + for (size_t i = 0; i < nb_cmds; i++) { + esp_command_t cmd = { + .name = cmd_list[i], + .group = (i % 2 == 0) ? "group_a" : "group_b", + .help = "dummy help", + .func = dummy_cmd_func, // implement a simple dummy function returning i%2 + .func_ctx = NULL, + .hint_cb = NULL, + .glossary_cb = NULL + }; + TEST_ASSERT_EQUAL(ESP_OK, esp_commands_register_cmd(&cmd)); + } + + /* test execution by group_a */ + const char *group_set[] = {"group_a"}; + esp_command_set_handle_t handle_set_1 = ESP_COMMANDS_CREATE_CMD_SET(group_set, FIELD_ACCESSOR(group)); + TEST_ASSERT_NOT_NULL(handle_set_1); + + int tmp_ret[] = {0, -1, 0, -1, 0, -1, 0, -1}; + memcpy(expected_ret_val, tmp_ret, sizeof(tmp_ret)); + run_cmd_test(handle_set_1, cmd_list, expected_ret_val, nb_cmds); + + /* test execution by command name */ + const char *cmd_name_set[] = {"cmd_1", "cmd_2", "cmd_3"}; + esp_command_set_handle_t handle_set_2 = esp_commands_create_cmd_set(cmd_name_set, 3, FIELD_ACCESSOR(name)); + TEST_ASSERT_NOT_NULL(handle_set_2); + + int tmp_ret2[] = {0, 0, 0, -1, -1, -1, -1, -1}; + memcpy(expected_ret_val, tmp_ret2, sizeof(tmp_ret2)); + run_cmd_test(handle_set_2, cmd_list, expected_ret_val, nb_cmds); + + /* test help command with set of dynamic commands */ + int cmd_ret; + TEST_ASSERT_EQUAL(ESP_OK, esp_commands_execute(handle_set_1, "help", &cmd_ret)); + TEST_ASSERT_EQUAL(0, cmd_ret); + TEST_ASSERT_EQUAL(ESP_OK, esp_commands_execute(handle_set_2, "help", &cmd_ret)); + TEST_ASSERT_EQUAL(0, cmd_ret); + + + /* unregister dynamically registered commands */ + for (size_t i = 0; i < nb_cmds; i++) { + TEST_ASSERT_EQUAL(ESP_OK, esp_commands_unregister_cmd(cmd_list[i])); + } + + esp_commands_destroy_cmd_set(&handle_set_1); + esp_commands_destroy_cmd_set(&handle_set_2); +} + +TEST_CASE("test static and dynamic command sets", "[esp_commands]") +{ + test_setup(); + + // --- dynamic commands --- + const char *dyn_cmd_list[] = {"cmd_1", "cmd_2", "cmd_3", "cmd_4", "cmd_5", "cmd_6", "cmd_7", "cmd_8"}; + const size_t nb_dyn_cmds = sizeof(dyn_cmd_list) / sizeof(dyn_cmd_list[0]); + + for (size_t i = 0; i < nb_dyn_cmds; i++) { + esp_command_t cmd = { + .name = dyn_cmd_list[i], + .group = (i % 2 == 0) ? "group_a" : "group_b", + .help = "dummy help", + .func = dummy_cmd_func, + .func_ctx = NULL, + .hint_cb = NULL, + .glossary_cb = NULL + }; + TEST_ASSERT_EQUAL(ESP_OK, esp_commands_register_cmd(&cmd)); + } + + // --- create static command sets (already registered statically) --- + const char *static_groups[] = {"group_1", "group_3"}; + esp_command_set_handle_t handle_static_set = ESP_COMMANDS_CREATE_CMD_SET(static_groups, FIELD_ACCESSOR(group)); + TEST_ASSERT_NOT_NULL(handle_static_set); + + // --- create dynamic command sets --- + const char *dyn_groups[] = {"group_a"}; + esp_command_set_handle_t handle_dynamic_set = ESP_COMMANDS_CREATE_CMD_SET(dyn_groups, FIELD_ACCESSOR(group)); + TEST_ASSERT_NOT_NULL(handle_dynamic_set); + + // --- combine static and dynamic sets --- + esp_command_set_handle_t handle_combined_set = esp_commands_concat_cmd_set(handle_static_set, handle_dynamic_set); + TEST_ASSERT_NOT_NULL(handle_combined_set); + + // --- run tests for combined set --- + const char *all_cmds[] = {"cmd_a", "cmd_b", "cmd_c", "cmd_d", "cmd_e", "cmd_f", "cmd_g", "cmd_h", + "cmd_1", "cmd_2", "cmd_3", "cmd_4", "cmd_5", "cmd_6", "cmd_7", "cmd_8" + }; + int expected_ret[] = {0, 0, -1, -1, 0, 0, -1, -1, + 0, -1, 0, -1, 0, -1, 0, -1 + }; + + run_cmd_test(handle_combined_set, all_cmds, expected_ret, 16); + + /* test help command with set of dynamic commands */ + int cmd_ret; + TEST_ASSERT_EQUAL(ESP_OK, esp_commands_execute(handle_combined_set, "help", &cmd_ret)); + TEST_ASSERT_EQUAL(0, cmd_ret); + + // --- cleanup --- + esp_commands_destroy_cmd_set(&handle_combined_set); + + for (size_t i = 0; i < nb_dyn_cmds; i++) { + TEST_ASSERT_EQUAL(ESP_OK, esp_commands_unregister_cmd(dyn_cmd_list[i])); + } +} + +static size_t completion_nb_of_calls = 0; + +static void test_completion_cb(const char *completed_cmd_name) +{ + completion_nb_of_calls++; +} + +TEST_CASE("test completion callback", "[esp_commands]") +{ + test_setup(); + + /* create sets by group */ + const char *set_a[] = {"group_1", "group_3"}; + esp_command_set_handle_t handle_set_a = ESP_COMMANDS_CREATE_CMD_SET(set_a, FIELD_ACCESSOR(group)); + TEST_ASSERT_NOT_NULL(handle_set_a); + + /* register a command dynamically and add it to the set */ + esp_command_t cmd = { + .name = "dyn_cmd", + .group = "dyn_cmd_group", + .help = "dummy help", + .func = dummy_cmd_func, + .func_ctx = NULL, + .hint_cb = NULL, + .glossary_cb = NULL + }; + TEST_ASSERT_EQUAL(ESP_OK, esp_commands_register_cmd(&cmd)); + + const char *set_b[] = {"dyn_cmd"}; + esp_command_set_handle_t handle_set_b = ESP_COMMANDS_CREATE_CMD_SET(set_b, FIELD_ACCESSOR(name)); + TEST_ASSERT_NOT_NULL(handle_set_b); + esp_command_set_handle_t handle_concat_set = esp_commands_concat_cmd_set(handle_set_a, handle_set_b); + TEST_ASSERT_NOT_NULL(handle_concat_set); + + esp_commands_get_completion(NULL, "a", test_completion_cb); + TEST_ASSERT_EQUAL(0, completion_nb_of_calls); + + esp_commands_get_completion(handle_concat_set, "cmd_", test_completion_cb); + TEST_ASSERT_EQUAL(4, completion_nb_of_calls); + + /* reset the cb counter */ + completion_nb_of_calls = 0; + + esp_commands_get_completion(NULL, "cmd_", test_completion_cb); + TEST_ASSERT_EQUAL(8, completion_nb_of_calls); + + /* reset the cb counter */ + completion_nb_of_calls = 0; + + esp_commands_get_completion(NULL, "dyn", test_completion_cb); + TEST_ASSERT_EQUAL(1, completion_nb_of_calls); + + /* reset the cb counter */ + completion_nb_of_calls = 0; + + esp_commands_get_completion(handle_concat_set, "dyn", test_completion_cb); + TEST_ASSERT_EQUAL(1, completion_nb_of_calls); + + /* reset the cb counter */ + completion_nb_of_calls = 0; + + esp_commands_destroy_cmd_set(&handle_concat_set); + TEST_ASSERT_NULL(handle_concat_set); + + esp_commands_unregister_cmd("dyn_cmd"); +} + +typedef struct hint_cb_ctx { + const char *message; +} hint_cb_ctx_t; + +static const char *test_hint_cb(void *context) +{ + hint_cb_ctx_t *ctx = (hint_cb_ctx_t *)context; + return ctx->message; +} + +static const char *test_glossary_cb(void *context) +{ + hint_cb_ctx_t *ctx = (hint_cb_ctx_t *)context; + return ctx->message; +} + +TEST_CASE("test hint and glossary callbacks", "[esp_commands]") +{ + test_setup(); + + hint_cb_ctx_t ctx_a = { .message = "msg_a" }; + hint_cb_ctx_t ctx_b = { .message = "msg_b" }; + + esp_command_t cmd_a = { + .name = "dyn_cmd_a", + .group = "dyn_cmd_group", + .help = "dummy help", + .func = dummy_cmd_func, + .func_ctx = &ctx_a, + .hint_cb = test_hint_cb, + .glossary_cb = test_glossary_cb + }; + TEST_ASSERT_EQUAL(ESP_OK, esp_commands_register_cmd(&cmd_a)); + + esp_command_t cmd_b = { + .name = "dyn_cmd_b", + .group = "dyn_cmd_group", + .help = "dummy help", + .func = dummy_cmd_func, + .func_ctx = &ctx_b, + .hint_cb = test_hint_cb, + .glossary_cb = test_glossary_cb + }; + TEST_ASSERT_EQUAL(ESP_OK, esp_commands_register_cmd(&cmd_b)); + + bool bold = true; + int color = 0; + const char *dyn_cmd_a_msg_hint = esp_commands_get_hint(NULL, "dyn_cmd_a", &color, &bold); + TEST_ASSERT_EQUAL(0, strcmp(dyn_cmd_a_msg_hint, ctx_a.message)); + TEST_ASSERT_EQUAL(false, bold); /* bold set a false by default in the component config */ + TEST_ASSERT_EQUAL(39, color); /* color set to 39 by default in the component config */ + + const char *dyn_cmd_b_msg_hint = esp_commands_get_hint(NULL, "dyn_cmd_b", &color, &bold); + TEST_ASSERT_EQUAL(0, strcmp(dyn_cmd_b_msg_hint, ctx_b.message)); + + const char *dyn_cmd_a_msg_glossary = esp_commands_get_glossary(NULL, "dyn_cmd_a"); + TEST_ASSERT_EQUAL(0, strcmp(dyn_cmd_a_msg_glossary, ctx_a.message)); + + const char *dyn_cmd_b_msg_glossary = esp_commands_get_glossary(NULL, "dyn_cmd_b"); + TEST_ASSERT_EQUAL(0, strcmp(dyn_cmd_b_msg_glossary, ctx_b.message)); + + /* create a set with only dyn_cmd_a and check that the hint cb is called for + * dyn_cmd_a but not for dyn_cmd_b */ + const char *set[] = {"dyn_cmd_a"}; + esp_command_set_handle_t handle_set = ESP_COMMANDS_CREATE_CMD_SET(set, FIELD_ACCESSOR(name)); + TEST_ASSERT_NOT_NULL(handle_set); + + const char *dyn_cmd_a_msg_hint_bis = esp_commands_get_hint(handle_set, "dyn_cmd_a", &color, &bold); + TEST_ASSERT_EQUAL(0, strcmp(dyn_cmd_a_msg_hint_bis, ctx_a.message)); + + const char *dyn_cmd_b_msg_hint_bis = esp_commands_get_hint(handle_set, "dyn_cmd_b", &color, &bold); + TEST_ASSERT_NULL(dyn_cmd_b_msg_hint_bis); + + const char *dyn_cmd_a_msg_glossary_bis = esp_commands_get_glossary(handle_set, "dyn_cmd_a"); + TEST_ASSERT_EQUAL(0, strcmp(dyn_cmd_a_msg_glossary_bis, ctx_a.message)); + + const char *dyn_cmd_b_msg_glossary_bis = esp_commands_get_glossary(handle_set, "dyn_cmd_b"); + TEST_ASSERT_NULL(dyn_cmd_b_msg_glossary_bis); + + TEST_ASSERT_EQUAL(ESP_OK, esp_commands_unregister_cmd("dyn_cmd_a")); + TEST_ASSERT_EQUAL(ESP_OK, esp_commands_unregister_cmd("dyn_cmd_b")); + + esp_commands_destroy_cmd_set(&handle_set); +} diff --git a/esp_commands/test_apps/main/test_main.c b/esp_commands/test_apps/main/test_main.c new file mode 100644 index 0000000000..01c2785d05 --- /dev/null +++ b/esp_commands/test_apps/main/test_main.c @@ -0,0 +1,26 @@ +/* + * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "unity.h" +#include "unity_test_runner.h" +#include "esp_heap_caps.h" +#include "unity_test_utils_memory.h" + +void setUp(void) +{ + unity_utils_record_free_mem(); +} + +void tearDown(void) +{ + unity_utils_evaluate_leaks_direct(0); +} + +void app_main(void) +{ + printf("Running esp_commands component tests\n"); + unity_run_menu(); +} diff --git a/esp_commands/test_apps/pytest_esp_commands.py b/esp_commands/test_apps/pytest_esp_commands.py new file mode 100644 index 0000000000..862e20b907 --- /dev/null +++ b/esp_commands/test_apps/pytest_esp_commands.py @@ -0,0 +1,9 @@ +import pytest +from pytest_embedded import Dut +from pytest_embedded_idf.utils import idf_parametrize + + +@pytest.mark.generic +@pytest.mark.skip_if_soc("IDF_VERSION_MAJOR < 6") +def test_esp_commands(dut) -> None: + dut.run_all_single_board_cases() diff --git a/esp_commands/test_apps/sdkconfig.defaults b/esp_commands/test_apps/sdkconfig.defaults new file mode 100644 index 0000000000..5e7cb391c2 --- /dev/null +++ b/esp_commands/test_apps/sdkconfig.defaults @@ -0,0 +1 @@ +CONFIG_ESP_TASK_WDT_EN=n \ No newline at end of file From a191be58bdde5422d82a11251dde5095d84ab87a Mon Sep 17 00:00:00 2001 From: Guillaume Souchere Date: Wed, 1 Oct 2025 12:54:48 +0200 Subject: [PATCH 3/4] feat(esp_commands): Add a way to output data on specified file descriptor --- esp_commands/esp_commands.c | 97 +++++++++++++------ esp_commands/esp_commands_helpers.c | 1 - esp_commands/include/esp_commands.h | 44 +++++++-- .../private_include/esp_commands_helpers.h | 27 ------ .../main/include/test_esp_commands_utils.h | 2 +- .../test_apps/main/test_esp_commands.c | 43 ++++---- 6 files changed, 127 insertions(+), 87 deletions(-) delete mode 100644 esp_commands/private_include/esp_commands_helpers.h diff --git a/esp_commands/esp_commands.c b/esp_commands/esp_commands.c index b52f650cbe..099a3eebbc 100644 --- a/esp_commands/esp_commands.c +++ b/esp_commands/esp_commands.c @@ -4,9 +4,10 @@ * SPDX-License-Identifier: Apache-2.0 */ #include +#include +#include #include "esp_commands.h" #include "esp_dynamic_commands.h" -#include "esp_commands_helpers.h" #include "esp_err.h" /* Default foreground color */ @@ -24,6 +25,7 @@ typedef struct esp_command_sets { /** run-time configuration options */ static esp_commands_config_t s_config = { + .write_func = write, .hint_bold = false, .hint_color = ANSI_COLOR_DEFAULT, .max_cmdline_args = 32, @@ -149,15 +151,20 @@ esp_err_t esp_commands_update_config(const esp_commands_config_t *config) memcpy(&s_config, config, sizeof(s_config)); + /* if no write function was passed in parameter, + * default it to the posix write */ + if (s_config.write_func == NULL) { + s_config.write_func = write; + } + return ESP_OK; } esp_err_t esp_commands_register_cmd(esp_command_t *cmd) { if (cmd == NULL || - // (cmd->name == NULL || strchr(cmd->name, ' ') != NULL) || - cmd->func == NULL ) { - printf("this should not happen\n"); + (cmd->name == NULL || strchr(cmd->name, ' ') != NULL) || + (cmd->func == NULL)) { return ESP_ERR_INVALID_ARG; } @@ -200,7 +207,7 @@ esp_err_t esp_commands_unregister_cmd(const char *cmd_name) } } -esp_err_t esp_commands_execute(esp_command_set_handle_t cmd_set, const char *cmdline, int *cmd_ret) +esp_err_t esp_commands_execute(esp_command_set_handle_t cmd_set, const int cmd_fd, const char *cmdline, int *cmd_ret) { char **argv = (char **) calloc(s_config.max_cmdline_args, sizeof(char *)); if (argv == NULL) { @@ -237,12 +244,14 @@ esp_err_t esp_commands_execute(esp_command_set_handle_t cmd_set, const char *cmd free(tmp_line_buf); return ESP_ERR_NOT_FOUND; } + + const int fd_out = cmd_fd == -1 ? STDOUT_FILENO : cmd_fd; if (cmd->func) { if (is_cmd_help) { // executing help command, pass the cmd_set as context - *cmd_ret = (*cmd->func)(cmd_set, argc, argv); + *cmd_ret = (*cmd->func)(cmd_set, fd_out, argc, argv); } else { - *cmd_ret = (*cmd->func)(cmd->func_ctx, argc, argv); + *cmd_ret = (*cmd->func)(cmd->func_ctx, fd_out, argc, argv); } } free(argv); @@ -447,6 +456,7 @@ void esp_commands_destroy_cmd_set(esp_command_set_handle_t *cmd_set) typedef struct call_completion_cb_ctx { const char *buf; const size_t buf_len; + void *cb_ctx; esp_command_get_completion_t completion_cb; } call_completion_cb_ctx_t; @@ -456,12 +466,12 @@ static bool call_completion_cb(void *caller_ctx, esp_command_t *cmd) /* Check if command starts with buf */ if (strncmp(ctx->buf, cmd->name, ctx->buf_len) == 0) { - ctx->completion_cb(cmd->name); + ctx->completion_cb(ctx->cb_ctx, cmd->name); } return true; } -void esp_commands_get_completion(esp_command_set_handle_t cmd_set, const char *buf, esp_command_get_completion_t completion_cb) +void esp_commands_get_completion(esp_command_set_handle_t cmd_set, const char *buf, void *cb_ctx, esp_command_get_completion_t completion_cb) { size_t len = strlen(buf); if (len == 0) { @@ -471,6 +481,7 @@ void esp_commands_get_completion(esp_command_set_handle_t cmd_set, const char *b call_completion_cb_ctx_t ctx = { .buf = buf, .buf_len = len, + .cb_ctx = cb_ctx, .completion_cb = completion_cb }; go_through_commands(cmd_set, &ctx, call_completion_cb); @@ -503,43 +514,69 @@ const char *esp_commands_get_glossary(esp_command_set_handle_t cmd_set, const ch /* help command related code */ /* -------------------------------------------------------------- */ -static void print_arg_help(esp_command_t *it) +#define FDPRINTF(fd, fmt, ...) do { \ + char _buf[s_config.max_cmdline_length]; \ + int _len = snprintf(_buf, sizeof(_buf), fmt, ##__VA_ARGS__); \ + if (_len > 0) { \ + ssize_t _ignored __attribute__((unused)); \ + _ignored = write(fd, _buf, \ + _len < (int)sizeof(_buf) ? _len : (int)sizeof(_buf) - 1); \ + } \ +} while (0) + +static void print_arg_help(const int fd_out, esp_command_t *it) { /* First line: command name and hint * Pad all the hints to the same column */ - printf("%-s", it->name); + FDPRINTF(fd_out, "%-s", it->name); + + const char *hint = NULL; if (it->hint_cb) { - printf(" %s\n", it->hint_cb(it->func_ctx)); + hint = it->hint_cb(it->func_ctx); + } + + if (hint) { + FDPRINTF(fd_out, "%s\n", it->hint_cb(it->func_ctx)); } else { - printf("\n"); + FDPRINTF(fd_out, "\n"); } /* Second line: print help */ /* TODO: replace the simple print with a function that * replaces arg_print_formatted */ if (it->help) { - printf(" %s\n", it->help); + FDPRINTF(fd_out, " %s\n", it->help); } else { - printf(" -\n"); + FDPRINTF(fd_out, " -\n"); } /* Third line: print the glossary*/ + const char *glossary = NULL; if (it->glossary_cb) { - printf("%s\n", it->glossary_cb(it->func_ctx)); + glossary = it->glossary_cb(it->func_ctx); + } + + if (glossary) { + FDPRINTF(fd_out, " %s\n", it->glossary_cb(it->func_ctx)); } else { - printf(" -\n"); + FDPRINTF(fd_out, " -\n"); } - printf("\n"); + FDPRINTF(fd_out, "\n"); } -static void print_arg_command(esp_command_t *it) +static void print_arg_command(const int fd_out, esp_command_t *it) { - printf("%-s", it->name); + FDPRINTF(fd_out, "%-s", it->name); if (it->hint_cb) { - printf(" %s\n", it->hint_cb(it->func_ctx)); + const char *hint = it->hint_cb(it->func_ctx); + if (hint) { + FDPRINTF(fd_out, " %s", it->hint_cb(it->func_ctx)); + } } + + FDPRINTF(fd_out, "\n"); } typedef enum { @@ -548,7 +585,7 @@ typedef enum { HELP_VERBOSE_LEVEL_MAX_NUM = 2 } help_verbose_level_e; -typedef void (*const fn_print_arg_t)(esp_command_t *); +typedef void (*const fn_print_arg_t)(const int fd_out, esp_command_t *); static fn_print_arg_t print_verbose_level_arr[HELP_VERBOSE_LEVEL_MAX_NUM] = { print_arg_command, @@ -556,6 +593,7 @@ static fn_print_arg_t print_verbose_level_arr[HELP_VERBOSE_LEVEL_MAX_NUM] = { }; typedef struct call_cmd_ctx { + const int fd_out; help_verbose_level_e verbose_level; const char *command_name; bool command_found; @@ -568,11 +606,11 @@ bool call_command_funcs(void *caller_ctx, esp_command_t *cmd) if (!ctx->command_name) { /* ctx->command_name is empty, print all commands */ - print_verbose_level_arr[ctx->verbose_level](cmd); + print_verbose_level_arr[ctx->verbose_level](ctx->fd_out, cmd); } else if (ctx->command_name && (strcmp(ctx->command_name, cmd->name) == 0)) { /* we found the command name, print the help and return */ - print_verbose_level_arr[ctx->verbose_level](cmd); + print_verbose_level_arr[ctx->verbose_level](ctx->fd_out, cmd); ctx->command_found = true; return false; } @@ -580,7 +618,7 @@ bool call_command_funcs(void *caller_ctx, esp_command_t *cmd) return true; } -static int help_command(void *context, int argc, char **argv) +static int help_command(void *context, const int fd_out, int argc, char **argv) { char *command_name = NULL; help_verbose_level_e verbose_level = HELP_VERBOSE_LEVEL_1; @@ -589,7 +627,7 @@ static int help_command(void *context, int argc, char **argv) * help cmd_name -v 0 */ if (argc <= 0 || argc > 4) { /* unknown issue, return error */ - printf("help: invalid number of arguments %d\n", argc); + FDPRINTF(fd_out, "help: invalid number of arguments %d\n", argc); return 1; } @@ -605,7 +643,7 @@ static int help_command(void *context, int argc, char **argv) /* check if the following argument is either 0, or 1 */ if (i + 1 >= argc) { /* format error, return with error */ - printf("help: arguments not provided in the right format\n"); + FDPRINTF(fd_out, "help: arguments not provided in the right format\n"); return 1; } else if (strcmp(argv[i + 1], "0") == 0) { verbose_level = 0; @@ -613,7 +651,7 @@ static int help_command(void *context, int argc, char **argv) verbose_level = 1; } else { /* wrong command format, return error */ - printf("help: invalid verbose level %s\n", argv[i + 1]); + FDPRINTF(fd_out, "help: invalid verbose level %s\n", argv[i + 1]); return 1; } @@ -633,6 +671,7 @@ static int help_command(void *context, int argc, char **argv) * is not NULL, find the command and only print the help for this command. if the * command is not found, return with error */ call_cmd_ctx_t ctx = { + .fd_out = fd_out, .verbose_level = verbose_level, .command_name = command_name, .command_found = false @@ -640,7 +679,7 @@ static int help_command(void *context, int argc, char **argv) go_through_commands(cmd_sets, &ctx, call_command_funcs); if (command_name && !ctx.command_found) { - printf("help: invalid command name %s\n", command_name); + FDPRINTF(fd_out, "help: invalid command name %s\n", command_name); return 1; } diff --git a/esp_commands/esp_commands_helpers.c b/esp_commands/esp_commands_helpers.c index 30947f32d5..b2ebf875eb 100644 --- a/esp_commands/esp_commands_helpers.c +++ b/esp_commands/esp_commands_helpers.c @@ -8,7 +8,6 @@ #include #include #include -#include "esp_commands_helpers.h" #include "esp_commands.h" #define SS_FLAG_ESCAPE 0x8 diff --git a/esp_commands/include/esp_commands.h b/esp_commands/include/esp_commands.h index 56ebb501a7..3329bf1688 100644 --- a/esp_commands/include/esp_commands.h +++ b/esp_commands/include/esp_commands.h @@ -18,11 +18,12 @@ extern "C" { * This function type is used to implement a console command. * * @param context User-defined context passed at invocation + * @param fd_out The file descriptor to use to output data * @param argc Number of arguments * @param argv Array of argc entries, each pointing to a null-terminated string argument * @return Return code of the console command; 0 indicates success */ -typedef int (*esp_command_func_t)(void *context, int argc, char **argv); +typedef int (*esp_command_func_t)(void *context, const int fd_out, int argc, char **argv); /** * @brief Callback to generate a command hint @@ -117,14 +118,25 @@ DEFINE_FIELD_ACCESSOR(help) */ #define FIELD_ACCESSOR(NAME) get_##NAME +/** + * @brief Function pointer type for writing bytes. + * + * @param fd File descriptor. + * @param buf Buffer containing bytes to write. + * @param count Number of bytes to write. + * @return Number of bytes written, or -1 on error. + */ +typedef ssize_t (*esp_commands_write_t)(int fd, const void *buf, size_t count); + /** * @brief Configuration parameters for esp_commands_manager initialization */ typedef struct esp_commands_config { - size_t max_cmdline_length; /*!< Maximum length of the command line buffer, in bytes */ - size_t max_cmdline_args; /*!< Maximum number of command line arguments to parse */ - int hint_color; /*!< ANSI color code used for hint text */ - bool hint_bold; /*!< If true, display hint text in bold */ + esp_commands_write_t write_func; /*!< Write function to call when executing a command */ + size_t max_cmdline_length; /*!< Maximum length of the command line buffer, in bytes */ + size_t max_cmdline_args; /*!< Maximum number of command line arguments to parse */ + int hint_color; /*!< ANSI color code used for hint text */ + bool hint_bold; /*!< If true, display hint text in bold */ } esp_commands_config_t; /** @@ -143,9 +155,10 @@ typedef struct esp_commands_config { * * This callback is called when a command is successfully completed. * + * @param cb_ctx Opaque pointer pointing at the context passed to the callback * @param completed_cmd_name Completed command name */ -typedef void (*esp_command_get_completion_t)(const char *completed_cmd_name); +typedef void (*esp_command_get_completion_t)(void *cb_ctx, const char *completed_cmd_name); /** * @brief Callback to retrieve a string field of esp_command_t @@ -207,14 +220,15 @@ esp_err_t esp_commands_unregister_cmd(const char *cmd_name); * @brief Execute a command line * * @param cmd_set Set of commands allowed to execute. If NULL, all registered commands are allowed + * @param cmd_fd File descriptor used to output data * @param cmd_line Command line string to execute - * @param cmd_ret Return value from the command function + * @param cmd_ret Return value from the command function. If -1, standard output will be used. * @return ESP_OK on success * ESP_ERR_INVALID_ARG if the command line is empty or only whitespace * ESP_ERR_NOT_FOUND if command is not found in cmd_set * ESP_ERR_NO_MEM if internal memory allocation fails */ -esp_err_t esp_commands_execute(esp_command_set_handle_t cmd_set, const char *cmdline, int *cmd_ret); +esp_err_t esp_commands_execute(esp_command_set_handle_t cmd_set, const int cmd_fd, const char *cmdline, int *cmd_ret); /** * @brief Find a command by name within a specific command set. @@ -235,9 +249,10 @@ esp_command_t *esp_commands_find_command(esp_command_set_handle_t cmd_set, const * * @param cmd_set Set of commands allowed for completion. If NULL, all registered commands are used * @param buf Input string typed by the user + * @param cb_ctx context passed to the completion callback * @param completion_cb Callback to return completed command names */ -void esp_commands_get_completion(esp_command_set_handle_t cmd_set, const char *buf, esp_command_get_completion_t completion_cb); +void esp_commands_get_completion(esp_command_set_handle_t cmd_set, const char *buf, void *cb_ctx, esp_command_get_completion_t completion_cb); /** * @brief Provide command hint for linenoise library @@ -298,6 +313,17 @@ esp_command_set_handle_t esp_commands_concat_cmd_set(esp_command_set_handle_t cm */ void esp_commands_destroy_cmd_set(esp_command_set_handle_t *cmd_set); +/** + * @brief Split a command line and populate argc and argv parameters + * + * @param line the line that has to be split into arguments + * @param argv array of arguments created from the line + * @param argv_size size of the argument array + * @return size_t number of arguments found in the line and stored + * in argv + */ +size_t esp_commands_split_argv(char *line, char **argv, size_t argv_size); + #ifdef __cplusplus } #endif diff --git a/esp_commands/private_include/esp_commands_helpers.h b/esp_commands/private_include/esp_commands_helpers.h deleted file mode 100644 index 4ee67164ac..0000000000 --- a/esp_commands/private_include/esp_commands_helpers.h +++ /dev/null @@ -1,27 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD - * - * SPDX-License-Identifier: Apache-2.0 - */ -#pragma once - -#ifdef __cplusplus -extern "C" { -#endif - -#include - -/** - * @brief Split a command line and populate argc and argv parameters - * - * @param line the line that has to be split into arguments - * @param argv array of arguments created from the line - * @param argv_size size of the argument array - * @return size_t number of arguments found in the line and stored - * in argv - */ -size_t esp_commands_split_argv(char *line, char **argv, size_t argv_size); - -#ifdef __cplusplus -} -#endif diff --git a/esp_commands/test_apps/main/include/test_esp_commands_utils.h b/esp_commands/test_apps/main/include/test_esp_commands_utils.h index eab21e6a72..a2426eb5b6 100644 --- a/esp_commands/test_apps/main/include/test_esp_commands_utils.h +++ b/esp_commands/test_apps/main/include/test_esp_commands_utils.h @@ -16,7 +16,7 @@ extern "C" { #define GET_STR(STR) #STR #define CREATE_CMD_FUNC(NAME) \ - static int GET_NAME(NAME, _func)(void *ctx, int argc, char **argv) { \ + static int GET_NAME(NAME, _func)(void *ctx, const int out_fd, int argc, char **argv) { \ printf(GET_STR(NAME) GET_STR(_func)); \ printf("\n"); \ return 0; \ diff --git a/esp_commands/test_apps/main/test_esp_commands.c b/esp_commands/test_apps/main/test_esp_commands.c index ab1369540f..f3e402683c 100644 --- a/esp_commands/test_apps/main/test_esp_commands.c +++ b/esp_commands/test_apps/main/test_esp_commands.c @@ -6,6 +6,7 @@ #include #include +#include #include #include "unity.h" #include "esp_commands.h" @@ -33,35 +34,35 @@ TEST_CASE("help command - called without command set", "[esp_commands]") /* call esp_commands_execute to run help command with verbosity 0 */ int cmd_ret = -1; - TEST_ASSERT_EQUAL(ESP_OK, esp_commands_execute(NULL, "help -v 0", &cmd_ret)); + TEST_ASSERT_EQUAL(ESP_OK, esp_commands_execute(NULL, -1, "help -v 0", &cmd_ret)); TEST_ASSERT_EQUAL(0, cmd_ret); /* call esp_commands_execute to run help command with verbosity 1 */ cmd_ret = -1; - TEST_ASSERT_EQUAL(ESP_OK, esp_commands_execute(NULL, "help -v 1", &cmd_ret)); + TEST_ASSERT_EQUAL(ESP_OK, esp_commands_execute(NULL, STDOUT_FILENO, "help -v 1", &cmd_ret)); TEST_ASSERT_EQUAL(0, cmd_ret); /* call esp_commands_execute to run help command on a registered command */ cmd_ret = -1; - TEST_ASSERT_EQUAL(ESP_OK, esp_commands_execute(NULL, "help cmd_a -v 0", &cmd_ret)); + TEST_ASSERT_EQUAL(ESP_OK, esp_commands_execute(NULL, -1, "help cmd_a -v 0", &cmd_ret)); TEST_ASSERT_EQUAL(0, cmd_ret); - TEST_ASSERT_EQUAL(ESP_OK, esp_commands_execute(NULL, "help cmd_a -v 1", &cmd_ret)); + TEST_ASSERT_EQUAL(ESP_OK, esp_commands_execute(NULL, STDOUT_FILENO, "help cmd_a -v 1", &cmd_ret)); TEST_ASSERT_EQUAL(0, cmd_ret); /* call esp_commands_execute to run help command on an unregistered command */ cmd_ret = -1; - TEST_ASSERT_EQUAL(ESP_OK, esp_commands_execute(NULL, "help cmd_w", &cmd_ret)); + TEST_ASSERT_EQUAL(ESP_OK, esp_commands_execute(NULL, -1, "help cmd_w", &cmd_ret)); TEST_ASSERT_EQUAL(1, cmd_ret); /* call esp_commands_execute to run help command on a registered command with wrong * verbosity syntax */ cmd_ret = -1; - TEST_ASSERT_EQUAL(ESP_OK, esp_commands_execute(NULL, "help cmd_a -v=1", &cmd_ret)); + TEST_ASSERT_EQUAL(ESP_OK, esp_commands_execute(NULL, -1, "help cmd_a -v=1", &cmd_ret)); TEST_ASSERT_EQUAL(1, cmd_ret); /* call esp_commands_execute to run help command with too many command names */ cmd_ret = -1; - TEST_ASSERT_EQUAL(ESP_OK, esp_commands_execute(NULL, "help cmd_a cmd_b -v 1", &cmd_ret)); + TEST_ASSERT_EQUAL(ESP_OK, esp_commands_execute(NULL, -1, "help cmd_a cmd_b -v 1", &cmd_ret)); TEST_ASSERT_EQUAL(1, cmd_ret); } @@ -107,7 +108,7 @@ static void run_cmd_test(esp_command_set_handle_t handle, const char **cmd_list, for (size_t i = 0; i < nb_cmds; i++) { int cmd_ret = -1; esp_err_t expected = expected_ret_val[i] == 0 ? ESP_OK : ESP_ERR_NOT_FOUND; - TEST_ASSERT_EQUAL(expected, esp_commands_execute(handle, cmd_list[i], &cmd_ret)); + TEST_ASSERT_EQUAL(expected, esp_commands_execute(handle, -1, cmd_list[i], &cmd_ret)); TEST_ASSERT_EQUAL(expected_ret_val[i], cmd_ret); } } @@ -141,9 +142,9 @@ TEST_CASE("test static command set", "[esp_commands]") /* test help command with set of static commands */ int cmd_ret; - TEST_ASSERT_EQUAL(ESP_OK, esp_commands_execute(handle_set_a, "help", &cmd_ret)); + TEST_ASSERT_EQUAL(ESP_OK, esp_commands_execute(handle_set_a, -1, "help", &cmd_ret)); TEST_ASSERT_EQUAL(0, cmd_ret); - TEST_ASSERT_EQUAL(ESP_OK, esp_commands_execute(handle_set_b, "help", &cmd_ret)); + TEST_ASSERT_EQUAL(ESP_OK, esp_commands_execute(handle_set_b, -1, "help", &cmd_ret)); TEST_ASSERT_EQUAL(0, cmd_ret); /* destroy sets */ @@ -178,8 +179,10 @@ TEST_CASE("test static command set", "[esp_commands]") esp_commands_destroy_cmd_set(&handle_set_c); } -static int dummy_cmd_func(void *context, int argc, char **argv) +static int dummy_cmd_func(void *context, const int fd_out, int argc, char **argv) { + (void)fd_out; + (void)context; printf("dynamic command called\n"); return 0; // always return success } @@ -226,9 +229,9 @@ TEST_CASE("test dynamic command set", "[esp_commands]") /* test help command with set of dynamic commands */ int cmd_ret; - TEST_ASSERT_EQUAL(ESP_OK, esp_commands_execute(handle_set_1, "help", &cmd_ret)); + TEST_ASSERT_EQUAL(ESP_OK, esp_commands_execute(handle_set_1, -1, "help", &cmd_ret)); TEST_ASSERT_EQUAL(0, cmd_ret); - TEST_ASSERT_EQUAL(ESP_OK, esp_commands_execute(handle_set_2, "help", &cmd_ret)); + TEST_ASSERT_EQUAL(ESP_OK, esp_commands_execute(handle_set_2, -1, "help", &cmd_ret)); TEST_ASSERT_EQUAL(0, cmd_ret); @@ -288,7 +291,7 @@ TEST_CASE("test static and dynamic command sets", "[esp_commands]") /* test help command with set of dynamic commands */ int cmd_ret; - TEST_ASSERT_EQUAL(ESP_OK, esp_commands_execute(handle_combined_set, "help", &cmd_ret)); + TEST_ASSERT_EQUAL(ESP_OK, esp_commands_execute(handle_combined_set, -1, "help", &cmd_ret)); TEST_ASSERT_EQUAL(0, cmd_ret); // --- cleanup --- @@ -301,7 +304,7 @@ TEST_CASE("test static and dynamic command sets", "[esp_commands]") static size_t completion_nb_of_calls = 0; -static void test_completion_cb(const char *completed_cmd_name) +static void test_completion_cb(void *cb_ctx, const char *completed_cmd_name) { completion_nb_of_calls++; } @@ -333,28 +336,28 @@ TEST_CASE("test completion callback", "[esp_commands]") esp_command_set_handle_t handle_concat_set = esp_commands_concat_cmd_set(handle_set_a, handle_set_b); TEST_ASSERT_NOT_NULL(handle_concat_set); - esp_commands_get_completion(NULL, "a", test_completion_cb); + esp_commands_get_completion(NULL, "a", NULL, test_completion_cb); TEST_ASSERT_EQUAL(0, completion_nb_of_calls); - esp_commands_get_completion(handle_concat_set, "cmd_", test_completion_cb); + esp_commands_get_completion(handle_concat_set, "cmd_", NULL, test_completion_cb); TEST_ASSERT_EQUAL(4, completion_nb_of_calls); /* reset the cb counter */ completion_nb_of_calls = 0; - esp_commands_get_completion(NULL, "cmd_", test_completion_cb); + esp_commands_get_completion(NULL, "cmd_", NULL, test_completion_cb); TEST_ASSERT_EQUAL(8, completion_nb_of_calls); /* reset the cb counter */ completion_nb_of_calls = 0; - esp_commands_get_completion(NULL, "dyn", test_completion_cb); + esp_commands_get_completion(NULL, "dyn", NULL, test_completion_cb); TEST_ASSERT_EQUAL(1, completion_nb_of_calls); /* reset the cb counter */ completion_nb_of_calls = 0; - esp_commands_get_completion(handle_concat_set, "dyn", test_completion_cb); + esp_commands_get_completion(handle_concat_set, "dyn", NULL, test_completion_cb); TEST_ASSERT_EQUAL(1, completion_nb_of_calls); /* reset the cb counter */ From 01210ed380e84a80b64f66c06a7b01c6e7e6cbdd Mon Sep 17 00:00:00 2001 From: Guillaume Souchere Date: Tue, 7 Oct 2025 11:15:17 +0200 Subject: [PATCH 4/4] feat(esp_commands): Add support for linux --- esp_commands/.build-test-rules.yml | 8 +++----- esp_commands/CMakeLists.txt | 16 +++++++++++----- esp_commands/README.md | 6 ++++-- esp_commands/include/esp_commands.h | 5 ++++- esp_commands/linux/esp_commands.ld | 10 ++++++++++ esp_commands/test_apps/pytest_esp_commands.py | 3 ++- 6 files changed, 34 insertions(+), 14 deletions(-) create mode 100644 esp_commands/linux/esp_commands.ld diff --git a/esp_commands/.build-test-rules.yml b/esp_commands/.build-test-rules.yml index 89638d6281..1a0cbef46f 100644 --- a/esp_commands/.build-test-rules.yml +++ b/esp_commands/.build-test-rules.yml @@ -1,6 +1,4 @@ esp_commands/test_apps: - disable: - - if: IDF_VERSION_MAJOR < 6 - reason: "esp_commands is created based on commands.c in esp-idf version < 6.0" - - if: IDF_TARGET not in ["esp32", "esp32c3"] - reason: "Sufficient to test on one Xtensa and one RISC-V target" \ No newline at end of file + enable: + - if: IDF_TARGET == "linux" + reason: "Sufficient to test on Linux target" \ No newline at end of file diff --git a/esp_commands/CMakeLists.txt b/esp_commands/CMakeLists.txt index 202fa7b933..e8375badcd 100644 --- a/esp_commands/CMakeLists.txt +++ b/esp_commands/CMakeLists.txt @@ -1,11 +1,17 @@ -idf_build_get_property(target IDF_TARGET) +idf_build_get_property(idf_target IDF_TARGET) set(srcs "esp_commands.c" "esp_dynamic_commands.c" "esp_commands_helpers.c") idf_component_register( - SRCS ${srcs} - INCLUDE_DIRS include - PRIV_INCLUDE_DIRS private_include - LDFRAGMENTS linker.lf) + SRCS ${srcs} + INCLUDE_DIRS include + PRIV_INCLUDE_DIRS private_include + LDFRAGMENTS linker.lf +) + +if(${idf_target} STREQUAL "linux") + # Add custom ld file + target_link_options(${COMPONENT_TARGET} INTERFACE "-Wl,-T,${CMAKE_CURRENT_SOURCE_DIR}/linux/esp_commands.ld") +endif() diff --git a/esp_commands/README.md b/esp_commands/README.md index 422b6b4b5f..9542167053 100644 --- a/esp_commands/README.md +++ b/esp_commands/README.md @@ -29,6 +29,7 @@ esp_commands_config_t config = ESP_COMMANDS_CONFIG_DEFAULT(); esp_commands_update_config(&config); ``` +- `write_func`: The custom write function used by esp_commands to output data (default to posix write is not specified) - `max_cmdline_length`: Maximum command line buffer length (bytes). - `max_cmdline_args`: Maximum number of arguments parsed. - `hint_color`: ANSI color code used for hints. @@ -93,10 +94,11 @@ Commands can be executed from a command line string: ```c int cmd_ret; -esp_err_t ret = esp_commands_execute(NULL, "my_cmd arg1 arg2", &cmd_ret); +esp_err_t ret = esp_commands_execute(NULL, STDOUT_FILENO, "my_cmd arg1 arg2", &cmd_ret); ``` - `cmd_set`: Limits execution to a set of commands (or `NULL` for all commands). +- `cmd_fd`: the file descriptor on which the output of the command is directed - `cmd_line`: String containing the command and arguments. - `cmd_ret`: Receives the command function return value. @@ -114,7 +116,7 @@ const char *glossary = esp_commands_get_glossary(NULL, "echo"); - **Completion**: Suggests matching commands. - **Hint**: Provides a short usage hint. -- **Glossary**: Provides detailed command description. +- **Glossary**: Provides detailed command argument description. --- diff --git a/esp_commands/include/esp_commands.h b/esp_commands/include/esp_commands.h index 3329bf1688..a41adde0ed 100644 --- a/esp_commands/include/esp_commands.h +++ b/esp_commands/include/esp_commands.h @@ -188,7 +188,10 @@ esp_err_t esp_commands_update_config(const esp_commands_config_t *config); */ #define ESP_COMMAND_REGISTER(cmd_name, cmd_group, cmd_help, cmd_func, cmd_func_ctx, cmd_hint_cb, cmd_glossary_cb) \ static_assert((cmd_func) != NULL); \ - static const esp_command_t cmd_name __attribute__((used, section(".esp_commands"))) = { \ + /* Alignment attribute is required when building on linux target to prevent each input section */ \ + /* from inheriting its alignment from the object's file default one thus preventing gaps between */ \ + /* commands in the section. */ \ + static const esp_command_t cmd_name __attribute__((used, section(".esp_commands"), aligned(4))) = { \ .name = #cmd_name, \ .group = #cmd_group, \ .help = cmd_help, \ diff --git a/esp_commands/linux/esp_commands.ld b/esp_commands/linux/esp_commands.ld new file mode 100644 index 0000000000..d61d347254 --- /dev/null +++ b/esp_commands/linux/esp_commands.ld @@ -0,0 +1,10 @@ +SECTIONS +{ + .esp_commands : + { + PROVIDE(_esp_commands_start = .); + KEEP(*(SORT(.esp_commands*))) /* Concatenate all .esp_commands */ + PROVIDE(_esp_commands_end = .); + } +} +INSERT AFTER .rodata; diff --git a/esp_commands/test_apps/pytest_esp_commands.py b/esp_commands/test_apps/pytest_esp_commands.py index 862e20b907..a429261a00 100644 --- a/esp_commands/test_apps/pytest_esp_commands.py +++ b/esp_commands/test_apps/pytest_esp_commands.py @@ -4,6 +4,7 @@ @pytest.mark.generic -@pytest.mark.skip_if_soc("IDF_VERSION_MAJOR < 6") +@pytest.mark.parametrize( + 'target', ['linux'], indirect=['target']) def test_esp_commands(dut) -> None: dut.run_all_single_board_cases()