From 8f9b6fa7569f0265cf5613df644e8907f450ee35 Mon Sep 17 00:00:00 2001 From: Guillaume Souchere Date: Tue, 19 Aug 2025 09:22:28 +0200 Subject: [PATCH 1/8] feat(esp_repl): Add component to idf-extra-component --- .github/ISSUE_TEMPLATE/bug-report.yml | 1 + .github/workflows/upload_component.yml | 1 + .idf_build_apps.toml | 1 + esp_repl/.build-test-rules.yml | 6 + esp_repl/CMakeLists.txt | 8 + esp_repl/LICENSE | 202 ++++++++++++++++++++++ esp_repl/README.md | 0 esp_repl/esp_repl.c | 9 + esp_repl/idf_component.yml | 9 + esp_repl/include/esp_repl.h | 19 ++ esp_repl/sbom_esp_repl.yml | 6 + esp_repl/test_apps/CMakeLists.txt | 5 + esp_repl/test_apps/main/CMakeLists.txt | 4 + esp_repl/test_apps/main/idf_component.yml | 4 + esp_repl/test_apps/main/test_esp_repl.c | 11 ++ esp_repl/test_apps/main/test_main.c | 26 +++ esp_repl/test_apps/pytest_esp_repl.ppy | 9 + esp_repl/test_apps/sdkconfig.defaults | 1 + 18 files changed, 322 insertions(+) create mode 100644 esp_repl/.build-test-rules.yml create mode 100644 esp_repl/CMakeLists.txt create mode 100644 esp_repl/LICENSE create mode 100644 esp_repl/README.md create mode 100644 esp_repl/esp_repl.c create mode 100644 esp_repl/idf_component.yml create mode 100644 esp_repl/include/esp_repl.h create mode 100644 esp_repl/sbom_esp_repl.yml create mode 100644 esp_repl/test_apps/CMakeLists.txt create mode 100644 esp_repl/test_apps/main/CMakeLists.txt create mode 100644 esp_repl/test_apps/main/idf_component.yml create mode 100644 esp_repl/test_apps/main/test_esp_repl.c create mode 100644 esp_repl/test_apps/main/test_main.c create mode 100644 esp_repl/test_apps/pytest_esp_repl.ppy create mode 100644 esp_repl/test_apps/sdkconfig.defaults diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index 08b5b534f7..3d02ac62e0 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -40,6 +40,7 @@ body: - esp_jpeg - esp_lcd_qemu_rgb - esp_schedule + - esp_repl - esp_serial_slave_link - expat - fmt diff --git a/.github/workflows/upload_component.yml b/.github/workflows/upload_component.yml index 575a26cd6c..aa4fdcf79b 100644 --- a/.github/workflows/upload_component.yml +++ b/.github/workflows/upload_component.yml @@ -44,6 +44,7 @@ jobs: esp_linenoise esp_jpeg esp_schedule + esp_repl esp_serial_slave_link expat fmt diff --git a/.idf_build_apps.toml b/.idf_build_apps.toml index 47b6569445..97c4c97c49 100644 --- a/.idf_build_apps.toml +++ b/.idf_build_apps.toml @@ -15,6 +15,7 @@ manifest_file = [ "esp_jpeg/.build-test-rules.yml", "esp_linenoise/.build-test-rules.yml", "esp_schedule/.build-test-rules.yml", + "esp_repl/.build-test-rules.yml", "esp_serial_slave_link/.build-test-rules.yml", "expat/.build-test-rules.yml", "iqmath/.build-test-rules.yml", diff --git a/esp_repl/.build-test-rules.yml b/esp_repl/.build-test-rules.yml new file mode 100644 index 0000000000..fadd36694a --- /dev/null +++ b/esp_repl/.build-test-rules.yml @@ -0,0 +1,6 @@ +esp_repl/test_apps: + disable: + - if: IDF_VERSION_MAJOR < 6 + reason: "esp_repl is created based on the console component 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_repl/CMakeLists.txt b/esp_repl/CMakeLists.txt new file mode 100644 index 0000000000..cf5e519440 --- /dev/null +++ b/esp_repl/CMakeLists.txt @@ -0,0 +1,8 @@ +idf_build_get_property(target IDF_TARGET) + +set(srcs "esp_repl.c") + +idf_component_register( + SRCS ${srcs} + INCLUDE_DIRS include + PRIV_INCLUDE_DIRS private_include) diff --git a/esp_repl/LICENSE b/esp_repl/LICENSE new file mode 100644 index 0000000000..efca5e67a3 --- /dev/null +++ b/esp_repl/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_repl/README.md b/esp_repl/README.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esp_repl/esp_repl.c b/esp_repl/esp_repl.c new file mode 100644 index 0000000000..4ba6f1603d --- /dev/null +++ b/esp_repl/esp_repl.c @@ -0,0 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include "esp_repl.h" +#include "esp_err.h" + diff --git a/esp_repl/idf_component.yml b/esp_repl/idf_component.yml new file mode 100644 index 0000000000..97deec3c47 --- /dev/null +++ b/esp_repl/idf_component.yml @@ -0,0 +1,9 @@ +version: "1.0.0" +description: "esp_repl - Read Eval Print Loop component" +url: https://github.com/espressif/idf-extra-components/tree/master/esp_repl +dependencies: + idf: ">=6.0" +sbom: + manifests: + - path: sbom_esp_repl.yml + dest: . \ No newline at end of file diff --git a/esp_repl/include/esp_repl.h b/esp_repl/include/esp_repl.h new file mode 100644 index 0000000000..00a8d87ae0 --- /dev/null +++ b/esp_repl/include/esp_repl.h @@ -0,0 +1,19 @@ +/* + * 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" + + + +#ifdef __cplusplus +} +#endif diff --git a/esp_repl/sbom_esp_repl.yml b/esp_repl/sbom_esp_repl.yml new file mode 100644 index 0000000000..4c5714b8f4 --- /dev/null +++ b/esp_repl/sbom_esp_repl.yml @@ -0,0 +1,6 @@ +name: esp_repl +description: Command handling component +url: https://github.com/espressif/idf-extra-components/tree/master/esp_repl +version: 1.0.0 +cpe: cpe:2.3:a:espressif:esp_repl:{}:*:*:*:*:*:*:* +supplier: 'Organization: Espressif Systems' \ No newline at end of file diff --git a/esp_repl/test_apps/CMakeLists.txt b/esp_repl/test_apps/CMakeLists.txt new file mode 100644 index 0000000000..84918a9f3e --- /dev/null +++ b/esp_repl/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_repl_test) diff --git a/esp_repl/test_apps/main/CMakeLists.txt b/esp_repl/test_apps/main/CMakeLists.txt new file mode 100644 index 0000000000..bb6436a651 --- /dev/null +++ b/esp_repl/test_apps/main/CMakeLists.txt @@ -0,0 +1,4 @@ +idf_component_register(SRCS "test_esp_repl.c" "test_main.c" + PRIV_INCLUDE_DIRS "." "include" + PRIV_REQUIRES unity + WHOLE_ARCHIVE) diff --git a/esp_repl/test_apps/main/idf_component.yml b/esp_repl/test_apps/main/idf_component.yml new file mode 100644 index 0000000000..b666695051 --- /dev/null +++ b/esp_repl/test_apps/main/idf_component.yml @@ -0,0 +1,4 @@ +dependencies: + espressif/esp_repl: + version: "*" + override_path: "../.." diff --git a/esp_repl/test_apps/main/test_esp_repl.c b/esp_repl/test_apps/main/test_esp_repl.c new file mode 100644 index 0000000000..43816286ef --- /dev/null +++ b/esp_repl/test_apps/main/test_esp_repl.c @@ -0,0 +1,11 @@ +/* + * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include "unity.h" +#include "esp_repl.h" diff --git a/esp_repl/test_apps/main/test_main.c b/esp_repl/test_apps/main/test_main.c new file mode 100644 index 0000000000..70bbd496d5 --- /dev/null +++ b/esp_repl/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_repl component tests\n"); + unity_run_menu(); +} diff --git a/esp_repl/test_apps/pytest_esp_repl.ppy b/esp_repl/test_apps/pytest_esp_repl.ppy new file mode 100644 index 0000000000..cdbc3e419a --- /dev/null +++ b/esp_repl/test_apps/pytest_esp_repl.ppy @@ -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_repl(dut) -> None: + dut.run_all_single_board_cases() diff --git a/esp_repl/test_apps/sdkconfig.defaults b/esp_repl/test_apps/sdkconfig.defaults new file mode 100644 index 0000000000..5e7cb391c2 --- /dev/null +++ b/esp_repl/test_apps/sdkconfig.defaults @@ -0,0 +1 @@ +CONFIG_ESP_TASK_WDT_EN=n \ No newline at end of file From cafc6ea96d1bb7a33b190a6d2d89deebf8e9d079 Mon Sep 17 00:00:00 2001 From: Guillaume Souchere Date: Tue, 19 Aug 2025 12:48:12 +0200 Subject: [PATCH 2/8] feat(esp_repl): Implement the component logic --- esp_repl/esp_repl.c | 175 ++++++++++++++++++++++++++++++++++++ esp_repl/include/esp_repl.h | 172 +++++++++++++++++++++++++++++++++++ 2 files changed, 347 insertions(+) diff --git a/esp_repl/esp_repl.c b/esp_repl/esp_repl.c index 4ba6f1603d..cb64445e41 100644 --- a/esp_repl/esp_repl.c +++ b/esp_repl/esp_repl.c @@ -4,6 +4,181 @@ * SPDX-License-Identifier: Apache-2.0 */ #include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" #include "esp_repl.h" #include "esp_err.h" +typedef enum { + ESP_REPL_STATE_RUNNING, + ESP_REPL_STATE_STOPPED +} esp_repl_state_e; + +typedef struct esp_repl_state { + esp_repl_state_e state; + SemaphoreHandle_t mux; +} esp_repl_state_t; + +typedef struct esp_repl_instance { + esp_repl_handle_t self; + esp_repl_config_t config; + esp_repl_state_t state; +} esp_repl_instance_t; + +#define ESP_REPL_CHECK_INSTANCE(handle) do { \ + if((handle == NULL) || ((esp_repl_instance_t*)handle->self != (esp_repl_instance_t*)handle)) { \ + return ESP_ERR_INVALID_ARG; \ + } \ +} while(0) + +esp_err_t esp_repl_create(esp_repl_handle_t *handle, const esp_repl_config_t *config) +{ + if ((config->executor.func == NULL) || + (config->reader.func == NULL) || + (config->max_cmd_line_size == 0)) { + return ESP_ERR_INVALID_ARG; + } + + esp_repl_instance_t *instance = malloc(sizeof(esp_repl_instance_t)); + if (!instance) { + return ESP_ERR_NO_MEM; + } + + instance->config = *config; + instance->state.state = ESP_REPL_STATE_STOPPED; + instance->self = instance; + instance->state.mux = xSemaphoreCreateMutex(); + if (!instance->state.mux) { + free(instance); + return ESP_FAIL; + } + + /* take the mutex right away to prevent the task to start running until + * the user explicitly calls esp_repl_start */ + xSemaphoreTake(instance->state.mux, portMAX_DELAY); + + *handle = instance; + return ESP_OK; +} + +esp_err_t esp_repl_destroy(esp_repl_handle_t handle) +{ + ESP_REPL_CHECK_INSTANCE(handle); + esp_repl_state_t *state = &handle->state; + + /* the instance has to be not running for esp_repl to destroy it */ + if (state->state != ESP_REPL_STATE_STOPPED) { + return ESP_ERR_INVALID_STATE; + } + + vSemaphoreDelete(state->mux); + + free(handle); + + return ESP_OK; +} + +esp_err_t esp_repl_start(esp_repl_handle_t handle) +{ + ESP_REPL_CHECK_INSTANCE(handle); + esp_repl_state_t *state = &handle->state; + + if (state->state != ESP_REPL_STATE_STOPPED) { + return ESP_ERR_INVALID_STATE; + } + state->state = ESP_REPL_STATE_RUNNING; + xSemaphoreGive(state->mux); + + return ESP_OK; +} + +esp_err_t esp_repl_stop(esp_repl_handle_t handle) +{ + ESP_REPL_CHECK_INSTANCE(handle); + esp_repl_config_t *config = &handle->config; + esp_repl_state_t *state = &handle->state; + + if (state->state != ESP_REPL_STATE_RUNNING) { + return ESP_ERR_INVALID_STATE; + } + + /* update the state to force the while loop in esp_repl to return */ + state->state = ESP_REPL_STATE_STOPPED; + + /* Call the on_stop callback to let the user unblock reader.func, if provided */ + if (config->on_stop.func != NULL) { + config->on_stop.func(config->on_stop.ctx, handle); + } + + /* Wait for esp_repl() to finish and signal completion */ + xSemaphoreTake(state->mux, portMAX_DELAY); + + /* give it back so destroy can also take/give symmetrically */ + xSemaphoreGive(state->mux); + + return ESP_OK; +} + +void esp_repl(esp_repl_handle_t handle) +{ + if (!handle || handle->self != handle) { + return; + } + + esp_repl_config_t *config = &handle->config; + esp_repl_state_t *state = &handle->state; + + /* allocate memory for the command line buffer */ + const size_t cmd_line_size = config->max_cmd_line_size; + char *cmd_line = calloc(1, cmd_line_size); + if (!cmd_line) { + return; + } + + /* Waiting for task notify. This happens when `esp_repl_start` + * function is called. */ + xSemaphoreTake(state->mux, portMAX_DELAY); + + /* REPL loop */ + while (state->state == ESP_REPL_STATE_RUNNING) { + + /* try to read a command line */ + const esp_err_t read_ret = config->reader.func(config->reader.ctx, cmd_line, cmd_line_size); + + /* forward the raw command line to the pre executor callback (e.g., save in history). + * this callback is not necessary for the user to register, continue if it isn't */ + if (config->pre_executor.func != NULL) { + config->pre_executor.func(config->pre_executor.ctx, cmd_line, read_ret); + } + + /* at this point, if the command is NULL, skip the executing part */ + if (read_ret != ESP_OK) { + continue; + } + + /* try to run the command */ + int cmd_func_ret; + const esp_err_t exec_ret = config->executor.func(config->executor.ctx, cmd_line, &cmd_func_ret); + + /* forward the raw command line to the post executor callback (e.g., save in history). + * this callback is not necessary for the user to register, continue if it isn't */ + if (config->post_executor.func != NULL) { + config->post_executor.func(config->post_executor.ctx, cmd_line, exec_ret, cmd_func_ret); + } + + /* reset the cmd_line for next loop */ + memset(cmd_line, 0x00, cmd_line_size); + } + + /* free the memory allocated for the cmd_line buffer */ + free(cmd_line); + + /* release the semaphore to indicate esp_repl_stop that the esp_repl returned */ + xSemaphoreGive(state->mux); + + /* call the on_exit callback before returning from esp_repl */ + if (config->on_exit.func != NULL) { + config->on_exit.func(config->on_exit.ctx, handle); + } +} diff --git a/esp_repl/include/esp_repl.h b/esp_repl/include/esp_repl.h index 00a8d87ae0..f9a444e445 100644 --- a/esp_repl/include/esp_repl.h +++ b/esp_repl/include/esp_repl.h @@ -12,7 +12,179 @@ extern "C" { #include #include "esp_err.h" +/** + * @brief Handle to a REPL instance. + */ +typedef struct esp_repl_instance *esp_repl_handle_t; + +/** + * @brief Function prototype for reading input for the REPL. + * + * @param ctx User-defined context pointer. + * @param buf Buffer to store the read data. + * @param buf_size Size of the buffer in bytes. + * + * @return ESP_OK on success, error code otherwise. + */ +typedef esp_err_t (*esp_repl_reader_fn)(void *ctx, char *buf, size_t buf_size); + +/** + * @brief Reader configuration structure for the REPL. + */ +typedef struct esp_repl_reader { + esp_repl_reader_fn func; /**!< Function to read input */ + void *ctx; /**!< Context passed to the reader function */ +} esp_repl_reader_t; + +/** + * @brief Function prototype called before executing a command. + * + * @param ctx User-defined context pointer. + * @param buf Buffer containing the command. + * @param reader_ret_val Return value from the reader function. + * + * @return ESP_OK to continue execution, error code to abort. + */ +typedef esp_err_t (*esp_repl_pre_executor_fn)(void *ctx, char *buf, const esp_err_t reader_ret_val); + +/** + * @brief Pre-executor configuration structure for the REPL. + */ +typedef struct esp_repl_pre_executor { + esp_repl_pre_executor_fn func; /**!< Function to run before command execution */ + void *ctx; /**!< Context passed to the pre-executor function */ +} esp_repl_pre_executor_t; + +/** + * @brief Function prototype to execute a REPL command. + * + * @param ctx User-defined context pointer. + * @param buf Null-terminated command string. + * @param ret_val Pointer to store the command return value. + * + * @return ESP_OK on success, error code otherwise. + */ +typedef esp_err_t (*esp_repl_executor_fn)(void *ctx, const char *buf, int *ret_val); + +/** + * @brief Executor configuration structure for the REPL. + */ +typedef struct esp_repl_executor { + esp_repl_executor_fn func; /**!< Function to execute commands */ + void *ctx; /**!< Context passed to the executor function */ +} esp_repl_executor_t; + +/** + * @brief Function prototype called after executing a command. + * + * @param ctx User-defined context pointer. + * @param buf Command that was executed. + * @param executor_ret_val Return value from the executor function. + * @param cmd_ret_val Command-specific return value. + * + * @return ESP_OK on success, error code otherwise. + */ +typedef esp_err_t (*esp_repl_post_executor_fn)(void *ctx, const char *buf, const esp_err_t executor_ret_val, const int cmd_ret_val); + +/** + * @brief Post-executor configuration structure for the REPL. + */ +typedef struct esp_repl_post_executor { + esp_repl_post_executor_fn func; /**!< Function called after command execution */ + void *ctx; /**!< Context passed to the post-executor function */ +} esp_repl_post_executor_t; + +/** + * @brief Function prototype called when the REPL is stopping. + * + * This callback allows the user to unblock the reader (or perform other + * cleanup) so that the REPL can return from `esp_repl()`. + * + * @param ctx User-defined context pointer. + * @param handle Handle to the REPL instance. + */ +typedef void (*esp_repl_on_stop_fn)(void *ctx, esp_repl_handle_t handle); + +/** + * @brief Stop callback configuration structure for the REPL. + */ +typedef struct esp_repl_on_stop { + esp_repl_on_stop_fn func; /**!< Function called when REPL stop is requested */ + void *ctx; /**!< Context passed to the on_stop function */ +} esp_repl_on_stop_t; + +/** + * @brief Function prototype called when the REPL exits. + * + * @param ctx User-defined context pointer. + * @param handle Handle to the REPL instance. + */ +typedef void (*esp_repl_on_exit_fn)(void *ctx, esp_repl_handle_t handle); + +/** + * @brief Exit callback configuration structure for the REPL. + */ +typedef struct esp_repl_on_exit { + esp_repl_on_exit_fn func; /**!< Function called on REPL exit */ + void *ctx; /**!< Context passed to the exit function */ +} esp_repl_on_exit_t; + +/** + * @brief Configuration structure to initialize a REPL instance. + */ +typedef struct esp_repl_config { + size_t max_cmd_line_size; /**!< Maximum allowed command line size */ + esp_repl_reader_t reader; /**!< Reader callback and context */ + esp_repl_pre_executor_t pre_executor; /**!< Pre-executor callback and context */ + esp_repl_executor_t executor; /**!< Executor callback and context */ + esp_repl_post_executor_t post_executor; /**!< Post-executor callback and context */ + esp_repl_on_stop_t on_stop; /**!< Stop callback and context */ + esp_repl_on_exit_t on_exit; /**!< Exit callback and context */ +} esp_repl_config_t; + +/** + * @brief Create a REPL instance. + * + * @param handle Pointer to store the created REPL instance handle. + * @param config Pointer to the configuration structure. + * + * @return ESP_OK on success, error code otherwise. + */ +esp_err_t esp_repl_create(esp_repl_handle_t *handle, const esp_repl_config_t *config); +/** + * @brief Destroy a REPL instance. + * + * @param handle REPL instance handle to destroy. + * + * @return ESP_OK on success, error code otherwise. + */ +esp_err_t esp_repl_destroy(esp_repl_handle_t handle); + +/** + * @brief Start a REPL instance. + * + * @param handle REPL instance handle. + * + * @return ESP_OK on success, error code otherwise. + */ +esp_err_t esp_repl_start(esp_repl_handle_t handle); + +/** + * @brief Stop a REPL instance. + * + * @param handle REPL instance handle. + * + * @return ESP_OK on success, error code otherwise. + */ +esp_err_t esp_repl_stop(esp_repl_handle_t handle); + +/** + * @brief Run the REPL loop. + * + * @param handle REPL instance handle. + */ +void esp_repl(esp_repl_handle_t handle); #ifdef __cplusplus } From ef992d5ee01cff5d2647d8a213c9b15ee824fccb Mon Sep 17 00:00:00 2001 From: Guillaume Souchere Date: Wed, 20 Aug 2025 14:02:05 +0200 Subject: [PATCH 3/8] feat(esp_repl): add tests for esp_repl --- esp_repl/.build-test-rules.yml | 8 ++- esp_repl/test_apps/main/test_esp_repl.c | 66 ++++++++++++++++++++++++- 2 files changed, 68 insertions(+), 6 deletions(-) diff --git a/esp_repl/.build-test-rules.yml b/esp_repl/.build-test-rules.yml index fadd36694a..059bb5e635 100644 --- a/esp_repl/.build-test-rules.yml +++ b/esp_repl/.build-test-rules.yml @@ -1,6 +1,4 @@ esp_repl/test_apps: - disable: - - if: IDF_VERSION_MAJOR < 6 - reason: "esp_repl is created based on the console component 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" diff --git a/esp_repl/test_apps/main/test_esp_repl.c b/esp_repl/test_apps/main/test_esp_repl.c index 43816286ef..2f0ae1f9d6 100644 --- a/esp_repl/test_apps/main/test_esp_repl.c +++ b/esp_repl/test_apps/main/test_esp_repl.c @@ -6,6 +6,70 @@ #include #include -#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/semphr.h" #include "unity.h" #include "esp_repl.h" + +typedef struct esp_linenoise_dummy { + size_t value; +} esp_linenoise_dummy_t; +typedef struct esp_linenoise_dummy *esp_linenoise_handle_t; + +typedef struct esp_commands_dummy { + size_t value; +} esp_commands_dummy_t; +typedef struct esp_commands_dummy *esp_commands_handle_t; + +esp_err_t test_reader_non_blocking(esp_linenoise_handle_t handle, char *buf, size_t buf_size) +{ + return ESP_OK; +} + +esp_err_t test_pre_executor(void *ctx, char *buf, const esp_err_t reader_ret_val) +{ + return ESP_OK; +} + +esp_err_t test_executor(esp_commands_handle_t handle, const char *buf, int *ret_val) +{ + return ESP_OK; +} + +esp_err_t test_post_executor(void *ctx, const char *buf, const esp_err_t executor_ret_val, const int cmd_ret_val) +{ + return ESP_OK; +} + +void test_on_stop(void *ctx, esp_repl_instance_handle_t handle) +{ + return; +} + +void test_on_exit(void *ctx, esp_repl_instance_handle_t handle) +{ + return; +} + +TEST_CASE("esp_repl() called after successful init, with non blocking reader", "[esp_repl]") +{ + esp_commands_dummy_t dummy_esp_linenoise = {.value = 0x01 }; + esp_commands_dummy_t dummy_esp_commands = {.value = 0x02 }; + esp_repl_config_t config = { + .max_cmd_line_size = 256, + .reader = { .func = (esp_repl_reader_fn)test_reader_non_blocking, .ctx = &dummy_esp_linenoise }, + .pre_executor = { .func = test_pre_executor, .ctx = NULL }, + .executor = { .func = (esp_repl_executor_fn)test_executor, .ctx = &dummy_esp_commands }, + .post_executor = { .func = test_post_executor, .ctx = NULL }, + .on_stop = { .func = test_on_stop, .ctx = NULL }, + .on_exit = { .func = test_on_exit, .ctx = NULL } + }; + + esp_repl_instance_handle_t handle = NULL; + TEST_ASSERT_EQUAL(ESP_OK, esp_repl_create(&handle, &config)); + TEST_ASSERT_NOT_NULL(handle); + + xTaskCreate(esp_apptrace_send_uart_tx_task, "app_trace_uart_tx_task", 2500, hw_data, uart_prio, NULL); + +} \ No newline at end of file From 7eb4f81311750e75eb01ec82cb226495b5729ec6 Mon Sep 17 00:00:00 2001 From: Guillaume Souchere Date: Wed, 3 Sep 2025 10:40:03 +0200 Subject: [PATCH 4/8] feat(esp_repl): Add esp_linenoise and esp_commands dependencies --- esp_repl/CMakeLists.txt | 2 +- esp_repl/README.md | 118 ++++++++++ esp_repl/esp_repl.c | 107 ++++++--- esp_repl/idf_component.yml | 5 +- esp_repl/include/esp_repl.h | 81 +++---- esp_repl/test_apps/main/CMakeLists.txt | 3 +- esp_repl/test_apps/main/test_esp_repl.c | 289 +++++++++++++++++++++--- esp_repl/test_apps/pytest_esp_repl.ppy | 9 - esp_repl/test_apps/pytest_esp_repl.py | 8 + 9 files changed, 505 insertions(+), 117 deletions(-) delete mode 100644 esp_repl/test_apps/pytest_esp_repl.ppy create mode 100644 esp_repl/test_apps/pytest_esp_repl.py diff --git a/esp_repl/CMakeLists.txt b/esp_repl/CMakeLists.txt index cf5e519440..cc4beb0410 100644 --- a/esp_repl/CMakeLists.txt +++ b/esp_repl/CMakeLists.txt @@ -5,4 +5,4 @@ set(srcs "esp_repl.c") idf_component_register( SRCS ${srcs} INCLUDE_DIRS include - PRIV_INCLUDE_DIRS private_include) + REQUIRES esp_linenoise esp_commands) diff --git a/esp_repl/README.md b/esp_repl/README.md index e69de29bb2..38bc2d7c04 100644 --- a/esp_repl/README.md +++ b/esp_repl/README.md @@ -0,0 +1,118 @@ +# esp_repl Component + +The `esp_repl` component provides a **Runtime Evaluation Loop (REPL)** mechanism for ESP-IDF-based applications. +It allows developers to build interactive command-line interfaces (CLI) that support user-defined commands, history management, and customizable callbacks for command execution. + +This component integrates with [`esp_linenoise`](../esp_linenoise) for line editing and input handling, and with [`esp_commands`](../esp_commands) for command parsing and execution. + +--- + +## Features + +- Modular REPL management with explicit `start` and `stop` control +- Integration with [`esp_linenoise`](../esp_linenoise) for input and history +- Support for command sets through [`esp_commands`](../esp_commands) +- Configurable callbacks for: + - Pre-execution processing + - Post-execution handling + - On-stop and on-exit events +- Thread-safe operation using FreeRTOS semaphores +- Optional command history persistence to filesystem + +--- + +## Usage + +A typical use case involves: + +1. Initializing `esp_linenoise` and `esp_commands` +2. Creating the REPL instance with `esp_repl_create()` +3. Running `esp_repl()` in a task +4. Starting and stopping the REPL using `esp_repl_start()` and `esp_repl_stop()` +5. Destroying the instance with `esp_repl_destroy()` when done + +### Example + +```c +#include "esp_repl.h" +#include "esp_linenoise.h" +#include "esp_commands.h" +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +static const char *TAG = "repl_example"; + +void repl_task(void *arg) +{ + esp_repl_handle_t repl_hdl = (esp_repl_handle_t)arg; + + // Run REPL loop (blocking until esp_repl_stop() is called) + // The loop won't be reached until esp_repl_start() is called + esp_repl(repl_hdl); + + ESP_LOGI(TAG, "REPL task exiting"); + vTaskDelete(NULL); +} + +void app_main(void) +{ + esp_err_t ret; + esp_repl_handle_t repl = NULL; + + // Initialize esp_linenoise (mandatory) + esp_linenoise_handle_t esp_linenoise_hdl = esp_linenoise_create(); + + // Initialize command set (optional) + esp_command_set_handle_t esp_commands_cmd_set = esp_commands_create(); + + esp_repl_config_t repl_cfg = { + .linenoise_handle = esp_linenoise_hdl, + .command_set_handle = esp_commands_cmd_set, /* optional */ + .max_cmd_line_size = 256, + .history_save_path = "/spiffs/repl_history.txt", /* optional */ + }; + + ret = esp_repl_create(&repl_cfg, &repl); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Failed to create REPL instance (%s)", esp_err_to_name(ret)); + return; + } + + // Create REPL task + if (xTaskCreate(repl_task, "repl_task", 4096, repl, 5, NULL) != pdPASS) { + ESP_LOGE(TAG, "Failed to create REPL task"); + esp_repl_destroy(repl); + return; + } + + ESP_LOGI(TAG, "Starting REPL..."); + ret = esp_repl_start(repl); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Failed to start REPL (%s)", esp_err_to_name(ret)); + esp_repl_destroy(repl); + return; + } + + // Application logic can run in parallel while REPL runs in its own task + // [...] + vTaskDelay(pdMS_TO_TICKS(10000)); // Example delay + + // Stop REPL + ret = esp_repl_stop(repl); + if (ret != ESP_OK) { + ESP_LOGW(TAG, "Failed to stop REPL (%s)", esp_err_to_name(ret)); + } + + ESP_LOGI(TAG, "REPL exited"); + + // Destroy REPL instance and clean up + ret = esp_repl_destroy(repl); + if (ret != ESP_OK) { + ESP_LOGW(TAG, "Failed to destroy REPL instance cleanly (%s)", esp_err_to_name(ret)); + } + + ESP_LOGI(TAG, "REPL example finished"); +} + +``` diff --git a/esp_repl/esp_repl.c b/esp_repl/esp_repl.c index cb64445e41..40745c8741 100644 --- a/esp_repl/esp_repl.c +++ b/esp_repl/esp_repl.c @@ -1,15 +1,18 @@ + /* - * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD - * - * SPDX-License-Identifier: Apache-2.0 - */ +* SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD +* +* SPDX-License-Identifier: Apache-2.0 +*/ #include #include #include "freertos/FreeRTOS.h" #include "freertos/semphr.h" +#include "freertos/task.h" #include "esp_repl.h" #include "esp_err.h" - +#include "esp_commands.h" +#include "esp_linenoise.h" typedef enum { ESP_REPL_STATE_RUNNING, ESP_REPL_STATE_STOPPED @@ -17,25 +20,28 @@ typedef enum { typedef struct esp_repl_state { esp_repl_state_e state; + TaskHandle_t task_hdl; SemaphoreHandle_t mux; } esp_repl_state_t; typedef struct esp_repl_instance { - esp_repl_handle_t self; esp_repl_config_t config; esp_repl_state_t state; } esp_repl_instance_t; -#define ESP_REPL_CHECK_INSTANCE(handle) do { \ - if((handle == NULL) || ((esp_repl_instance_t*)handle->self != (esp_repl_instance_t*)handle)) { \ - return ESP_ERR_INVALID_ARG; \ - } \ -} while(0) +#define ESP_REPL_CHECK_INSTANCE(handle) do { \ + if(handle == NULL) { \ + return ESP_ERR_INVALID_ARG; \ + } \ + } while(0) -esp_err_t esp_repl_create(esp_repl_handle_t *handle, const esp_repl_config_t *config) +esp_err_t esp_repl_create(const esp_repl_config_t *config, esp_repl_handle_t *out_handle) { - if ((config->executor.func == NULL) || - (config->reader.func == NULL) || + if (!config || !out_handle) { + return ESP_ERR_INVALID_ARG; + } + + if ((config->linenoise_handle == NULL) || (config->max_cmd_line_size == 0)) { return ESP_ERR_INVALID_ARG; } @@ -47,7 +53,6 @@ esp_err_t esp_repl_create(esp_repl_handle_t *handle, const esp_repl_config_t * instance->config = *config; instance->state.state = ESP_REPL_STATE_STOPPED; - instance->self = instance; instance->state.mux = xSemaphoreCreateMutex(); if (!instance->state.mux) { free(instance); @@ -55,10 +60,10 @@ esp_err_t esp_repl_create(esp_repl_handle_t *handle, const esp_repl_config_t * } /* take the mutex right away to prevent the task to start running until - * the user explicitly calls esp_repl_start */ + * the user explicitly calls esp_repl_start */ xSemaphoreTake(instance->state.mux, portMAX_DELAY); - *handle = instance; + *out_handle = instance; return ESP_OK; } @@ -106,29 +111,60 @@ esp_err_t esp_repl_stop(esp_repl_handle_t handle) /* update the state to force the while loop in esp_repl to return */ state->state = ESP_REPL_STATE_STOPPED; - /* Call the on_stop callback to let the user unblock reader.func, if provided */ + /** This function forces esp_linenoise_get_line() to return. + * + * Return Values: + * - ESP_OK: Returned if the user has not provided a custom read and the abort operation succeeds. + * - ESP_ERR_INVALID_STATE: Returned if the user has provided a custom read. In this case, the user + * is responsible for implementing an abort mechanism that ensures a successful return from + * their custom read. This can be achieved by placing the logic in the on_stop callback. + * + * Behavior: + * - When a custom read is registered, ESP_ERR_INVALID_STATE indicates that esp_repl_stop() cannot + * forcibly return from the read. The user must handle the return of their custom read via on_stop(). + * - From the perspective of esp_repl_stop(), this scenario is treated as successful, and its + * return value should be set to ESP_OK. + */ + esp_err_t ret_val = esp_linenoise_abort(config->linenoise_handle); + if (ret_val == ESP_ERR_INVALID_STATE) { + ret_val = ESP_OK; + } + + /* Call the on_stop callback to let the user unblock esp_linenoise + * if a custom read is provided */ if (config->on_stop.func != NULL) { config->on_stop.func(config->on_stop.ctx, handle); } - /* Wait for esp_repl() to finish and signal completion */ - xSemaphoreTake(state->mux, portMAX_DELAY); - - /* give it back so destroy can also take/give symmetrically */ - xSemaphoreGive(state->mux); + /* Wait for esp_repl() to finish and signal completion, in the event of + * esp_repl_stop() is called from the same task running esp_repl() (e.g., + * called from a "quit" command), do not take the mutex to avoid a deadlock. + * + * If esp_repl_stop() is called from the same task, it assures that this task + * is not blocking in esp_linenoise_get_line() so the while loop in esp_repl() + * will return as we updated the state above */ + if (state->task_hdl && state->task_hdl != xTaskGetCurrentTaskHandle()) { + xSemaphoreTake(state->mux, portMAX_DELAY); + } - return ESP_OK; + return ret_val; } void esp_repl(esp_repl_handle_t handle) { - if (!handle || handle->self != handle) { + if (!handle) { return; } esp_repl_config_t *config = &handle->config; esp_repl_state_t *state = &handle->state; + /* get the task handle of the task running this function. + * It is necessary to gather this information in case esp_repl_stop() + * is called from the same task as the one running esp_repl() (e.g., + * through the execution of a command) */ + state->task_hdl = xTaskGetCurrentTaskHandle(); + /* allocate memory for the command line buffer */ const size_t cmd_line_size = config->max_cmd_line_size; char *cmd_line = calloc(1, cmd_line_size); @@ -137,17 +173,28 @@ void esp_repl(esp_repl_handle_t handle) } /* Waiting for task notify. This happens when `esp_repl_start` - * function is called. */ + * function is called. */ xSemaphoreTake(state->mux, portMAX_DELAY); + esp_linenoise_handle_t l_hdl = config->linenoise_handle; + esp_command_set_handle_t c_set = config->command_set_handle; + /* REPL loop */ while (state->state == ESP_REPL_STATE_RUNNING) { /* try to read a command line */ - const esp_err_t read_ret = config->reader.func(config->reader.ctx, cmd_line, cmd_line_size); + const esp_err_t read_ret = esp_linenoise_get_line(l_hdl, cmd_line, cmd_line_size); + + /* Add the command to the history */ + esp_linenoise_history_add(l_hdl, cmd_line); + + /* Save command history to filesystem */ + if (config->history_save_path) { + esp_linenoise_history_save(l_hdl, config->history_save_path); + } /* forward the raw command line to the pre executor callback (e.g., save in history). - * this callback is not necessary for the user to register, continue if it isn't */ + * this callback is not necessary for the user to register, continue if it isn't */ if (config->pre_executor.func != NULL) { config->pre_executor.func(config->pre_executor.ctx, cmd_line, read_ret); } @@ -159,10 +206,10 @@ void esp_repl(esp_repl_handle_t handle) /* try to run the command */ int cmd_func_ret; - const esp_err_t exec_ret = config->executor.func(config->executor.ctx, cmd_line, &cmd_func_ret); + const esp_err_t exec_ret = esp_commands_execute(c_set, -1, cmd_line, &cmd_func_ret); /* forward the raw command line to the post executor callback (e.g., save in history). - * this callback is not necessary for the user to register, continue if it isn't */ + * this callback is not necessary for the user to register, continue if it isn't */ if (config->post_executor.func != NULL) { config->post_executor.func(config->post_executor.ctx, cmd_line, exec_ret, cmd_func_ret); } diff --git a/esp_repl/idf_component.yml b/esp_repl/idf_component.yml index 97deec3c47..927250f173 100644 --- a/esp_repl/idf_component.yml +++ b/esp_repl/idf_component.yml @@ -2,7 +2,10 @@ version: "1.0.0" description: "esp_repl - Read Eval Print Loop component" url: https://github.com/espressif/idf-extra-components/tree/master/esp_repl dependencies: - idf: ">=6.0" + espressif/esp_linenoise: '*' + SoucheSouche/esp_commands: + version: "*" + registry_url: https://components-staging.espressif.com sbom: manifests: - path: sbom_esp_repl.yml diff --git a/esp_repl/include/esp_repl.h b/esp_repl/include/esp_repl.h index f9a444e445..80f3264c12 100644 --- a/esp_repl/include/esp_repl.h +++ b/esp_repl/include/esp_repl.h @@ -11,31 +11,14 @@ extern "C" { #include #include "esp_err.h" +#include "esp_linenoise.h" +#include "esp_commands.h" /** * @brief Handle to a REPL instance. */ typedef struct esp_repl_instance *esp_repl_handle_t; -/** - * @brief Function prototype for reading input for the REPL. - * - * @param ctx User-defined context pointer. - * @param buf Buffer to store the read data. - * @param buf_size Size of the buffer in bytes. - * - * @return ESP_OK on success, error code otherwise. - */ -typedef esp_err_t (*esp_repl_reader_fn)(void *ctx, char *buf, size_t buf_size); - -/** - * @brief Reader configuration structure for the REPL. - */ -typedef struct esp_repl_reader { - esp_repl_reader_fn func; /**!< Function to read input */ - void *ctx; /**!< Context passed to the reader function */ -} esp_repl_reader_t; - /** * @brief Function prototype called before executing a command. * @@ -45,7 +28,7 @@ typedef struct esp_repl_reader { * * @return ESP_OK to continue execution, error code to abort. */ -typedef esp_err_t (*esp_repl_pre_executor_fn)(void *ctx, char *buf, const esp_err_t reader_ret_val); +typedef esp_err_t (*esp_repl_pre_executor_fn)(void *ctx, const char *buf, esp_err_t reader_ret_val); /** * @brief Pre-executor configuration structure for the REPL. @@ -55,25 +38,6 @@ typedef struct esp_repl_pre_executor { void *ctx; /**!< Context passed to the pre-executor function */ } esp_repl_pre_executor_t; -/** - * @brief Function prototype to execute a REPL command. - * - * @param ctx User-defined context pointer. - * @param buf Null-terminated command string. - * @param ret_val Pointer to store the command return value. - * - * @return ESP_OK on success, error code otherwise. - */ -typedef esp_err_t (*esp_repl_executor_fn)(void *ctx, const char *buf, int *ret_val); - -/** - * @brief Executor configuration structure for the REPL. - */ -typedef struct esp_repl_executor { - esp_repl_executor_fn func; /**!< Function to execute commands */ - void *ctx; /**!< Context passed to the executor function */ -} esp_repl_executor_t; - /** * @brief Function prototype called after executing a command. * @@ -84,7 +48,7 @@ typedef struct esp_repl_executor { * * @return ESP_OK on success, error code otherwise. */ -typedef esp_err_t (*esp_repl_post_executor_fn)(void *ctx, const char *buf, const esp_err_t executor_ret_val, const int cmd_ret_val); +typedef esp_err_t (*esp_repl_post_executor_fn)(void *ctx, const char *buf, esp_err_t executor_ret_val, int cmd_ret_val); /** * @brief Post-executor configuration structure for the REPL. @@ -133,24 +97,25 @@ typedef struct esp_repl_on_exit { * @brief Configuration structure to initialize a REPL instance. */ typedef struct esp_repl_config { - size_t max_cmd_line_size; /**!< Maximum allowed command line size */ - esp_repl_reader_t reader; /**!< Reader callback and context */ - esp_repl_pre_executor_t pre_executor; /**!< Pre-executor callback and context */ - esp_repl_executor_t executor; /**!< Executor callback and context */ - esp_repl_post_executor_t post_executor; /**!< Post-executor callback and context */ - esp_repl_on_stop_t on_stop; /**!< Stop callback and context */ - esp_repl_on_exit_t on_exit; /**!< Exit callback and context */ + esp_linenoise_handle_t linenoise_handle; /**!< Handle to the esp_linenoise instance */ + esp_command_set_handle_t command_set_handle; /**!< Handle to a set of commands */ + size_t max_cmd_line_size; /**!< Maximum allowed command line size */ + const char *history_save_path; /**!< Path to file to save the history */ + esp_repl_pre_executor_t pre_executor; /**!< Pre-executor callback and context */ + esp_repl_post_executor_t post_executor; /**!< Post-executor callback and context */ + esp_repl_on_stop_t on_stop; /**!< Stop callback and context */ + esp_repl_on_exit_t on_exit; /**!< Exit callback and context */ } esp_repl_config_t; /** * @brief Create a REPL instance. * - * @param handle Pointer to store the created REPL instance handle. * @param config Pointer to the configuration structure. + * @param out_handle Pointer to store the created REPL instance handle. * * @return ESP_OK on success, error code otherwise. */ -esp_err_t esp_repl_create(esp_repl_handle_t *handle, const esp_repl_config_t *config); +esp_err_t esp_repl_create(const esp_repl_config_t *config, esp_repl_handle_t *out_handle); /** * @brief Destroy a REPL instance. @@ -173,6 +138,24 @@ esp_err_t esp_repl_start(esp_repl_handle_t handle); /** * @brief Stop a REPL instance. * + * @note This function will internally call 'esp_linenoise_abort' first to try to return from + * 'esp_linenoise_get_line'. If the user has provided a custom read to the esp_linenoise + * instance used by the esp_repl instance, it is the responsibility of the user to provide + * the mechanism to return from this custom read by providing a callback to the 'on_stop' field + * in the esp_repl_config_t. + * + * Return Values: + * - ESP_OK: Returned if the user has not provided a custom read and the abort operation succeeds. + * - ESP_ERR_INVALID_STATE: Returned if the user has provided a custom read. In this case, the user + * is responsible for implementing an abort mechanism that ensures a successful return from + * their custom read. This can be achieved by placing the logic in the on_stop callback. + * + * Behavior: + * - When a custom read is registered, ESP_ERR_INVALID_STATE indicates that esp_repl_stop() cannot + * forcibly return from the read. The user must handle the return themselves via on_stop(). + * - From the perspective of esp_repl_stop(), this scenario is treated as successful, and its + * return value should be set to ESP_OK. + * * @param handle REPL instance handle. * * @return ESP_OK on success, error code otherwise. diff --git a/esp_repl/test_apps/main/CMakeLists.txt b/esp_repl/test_apps/main/CMakeLists.txt index bb6436a651..faaab0e887 100644 --- a/esp_repl/test_apps/main/CMakeLists.txt +++ b/esp_repl/test_apps/main/CMakeLists.txt @@ -1,4 +1,5 @@ + idf_component_register(SRCS "test_esp_repl.c" "test_main.c" - PRIV_INCLUDE_DIRS "." "include" + PRIV_INCLUDE_DIRS "." PRIV_REQUIRES unity WHOLE_ARCHIVE) diff --git a/esp_repl/test_apps/main/test_esp_repl.c b/esp_repl/test_apps/main/test_esp_repl.c index 2f0ae1f9d6..0031e6b76e 100644 --- a/esp_repl/test_apps/main/test_esp_repl.c +++ b/esp_repl/test_apps/main/test_esp_repl.c @@ -6,70 +6,307 @@ #include #include +#include +#include +#include #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "freertos/semphr.h" #include "unity.h" #include "esp_repl.h" +#include "esp_linenoise.h" +#include "esp_commands.h" -typedef struct esp_linenoise_dummy { - size_t value; -} esp_linenoise_dummy_t; -typedef struct esp_linenoise_dummy *esp_linenoise_handle_t; +inline __attribute__((always_inline)) +uint32_t get_millis(void) +{ + struct timeval tv = { 0 }; + gettimeofday(&tv, NULL); + return tv.tv_sec * 1000 + tv.tv_usec / 1000; +} + +inline __attribute__((always_inline)) +void wait_ms(int ms) +{ + vTaskDelay(pdMS_TO_TICKS(ms)); +} -typedef struct esp_commands_dummy { - size_t value; -} esp_commands_dummy_t; -typedef struct esp_commands_dummy *esp_commands_handle_t; +/* fake the section expected by esp_commands, make sure end and starts + * at the same address so the esp_commands component sees no commands */ +esp_command_t _esp_commands_start; +extern esp_command_t _esp_commands_end __attribute__((alias("_esp_commands_start"))); -esp_err_t test_reader_non_blocking(esp_linenoise_handle_t handle, char *buf, size_t buf_size) +static int s_socket_fd[2]; +static size_t s_pre_executor_nb_of_calls = 0; +static size_t s_post_executor_nb_of_calls = 0; +static size_t s_on_stop_nb_of_calls = 0; +static size_t s_on_exit_nb_of_calls = 0; + +static void test_socket_setup(int socket_fd[2]) { - return ESP_OK; + TEST_ASSERT_EQUAL(0, socketpair(AF_UNIX, SOCK_STREAM, 0, socket_fd)); + + /* ensure reads are blocking */ + int flags = fcntl(socket_fd[0], F_GETFL, 0); + flags &= ~O_NONBLOCK; + fcntl(socket_fd[0], F_SETFL, flags); + + flags = fcntl(socket_fd[1], F_GETFL, 0); + flags &= ~O_NONBLOCK; + fcntl(socket_fd[1], F_SETFL, flags); } -esp_err_t test_pre_executor(void *ctx, char *buf, const esp_err_t reader_ret_val) +static void test_socket_teardown(int socket_fd[2]) { - return ESP_OK; + close(socket_fd[0]); + close(socket_fd[1]); } -esp_err_t test_executor(esp_commands_handle_t handle, const char *buf, int *ret_val) +static void test_send_characters(int socket_fd, const char *msg) { + wait_ms(100); + + const size_t msg_len = strlen(msg); + const int nwrite = write(socket_fd, msg, msg_len); + TEST_ASSERT_EQUAL(msg_len, nwrite); +} + +esp_err_t test_pre_executor(void *ctx, char *buf, const esp_err_t reader_ret_val) +{ + s_pre_executor_nb_of_calls++; return ESP_OK; } esp_err_t test_post_executor(void *ctx, const char *buf, const esp_err_t executor_ret_val, const int cmd_ret_val) { + s_post_executor_nb_of_calls++; return ESP_OK; } -void test_on_stop(void *ctx, esp_repl_instance_handle_t handle) +void test_on_stop(void *ctx, esp_repl_handle_t handle) { + s_on_stop_nb_of_calls++; return; } -void test_on_exit(void *ctx, esp_repl_instance_handle_t handle) +void test_on_exit(void *ctx, esp_repl_handle_t handle) { + s_on_exit_nb_of_calls++; return; } -TEST_CASE("esp_repl() called after successful init, with non blocking reader", "[esp_repl]") +/* Pass two semaphores: + * - start_sem: child gives it when it reached esp_repl (so main knows child started) + * - done_sem: child gives it just before deleting itself (so main can "join") + */ +typedef struct task_args { + SemaphoreHandle_t start_sem; + SemaphoreHandle_t done_sem; + esp_repl_handle_t hdl; +} task_args_t; + +static void repl_task(void *args) { - esp_commands_dummy_t dummy_esp_linenoise = {.value = 0x01 }; - esp_commands_dummy_t dummy_esp_commands = {.value = 0x02 }; - esp_repl_config_t config = { + task_args_t *task_args = (task_args_t *)args; + + /* signal to main that task started and esp_repl will run */ + xSemaphoreGive(task_args->start_sem); + + /* run the REPL loop (will return when stopped) */ + esp_repl(task_args->hdl); + + /* signal completion (emulates pthread_join notification) */ + xSemaphoreGive(task_args->done_sem); + + /* self-delete */ + vTaskDelete(NULL); +} + +TEST_CASE("esp_repl() loop calls all callbacks and exit on call to esp_repl_stop", "[esp_repl]") +{ + /* create semaphores */ + SemaphoreHandle_t start_sem = xSemaphoreCreateBinary(); + TEST_ASSERT_NOT_NULL(start_sem); + SemaphoreHandle_t done_sem = xSemaphoreCreateBinary(); + TEST_ASSERT_NOT_NULL(done_sem); + + /* ensure both semaphores are in the "taken/empty" state: + taking with 0 timeout guarantees they are empty afterwards + regardless of the create semantics on this FreeRTOS build. */ + xSemaphoreTake(start_sem, 0); + xSemaphoreTake(done_sem, 0); + + esp_linenoise_config_t linenoise_config; + esp_linenoise_get_instance_config_default(&linenoise_config); + test_socket_setup(s_socket_fd); + linenoise_config.in_fd = s_socket_fd[0]; + linenoise_config.out_fd = s_socket_fd[0]; + esp_linenoise_handle_t esp_linenoise_hdl; + TEST_ASSERT_EQUAL(ESP_OK, esp_linenoise_create_instance(&linenoise_config, &esp_linenoise_hdl)); + TEST_ASSERT_NOT_NULL(esp_linenoise_hdl); + + esp_repl_config_t repl_config = { + .linenoise_handle = esp_linenoise_hdl, + .command_set_handle = NULL, .max_cmd_line_size = 256, - .reader = { .func = (esp_repl_reader_fn)test_reader_non_blocking, .ctx = &dummy_esp_linenoise }, + .history_save_path = NULL, .pre_executor = { .func = test_pre_executor, .ctx = NULL }, - .executor = { .func = (esp_repl_executor_fn)test_executor, .ctx = &dummy_esp_commands }, .post_executor = { .func = test_post_executor, .ctx = NULL }, .on_stop = { .func = test_on_stop, .ctx = NULL }, .on_exit = { .func = test_on_exit, .ctx = NULL } }; - esp_repl_instance_handle_t handle = NULL; - TEST_ASSERT_EQUAL(ESP_OK, esp_repl_create(&handle, &config)); - TEST_ASSERT_NOT_NULL(handle); + esp_repl_handle_t repl_handle = NULL; + TEST_ASSERT_EQUAL(ESP_OK, esp_repl_create(&repl_config, &repl_handle)); + TEST_ASSERT_NOT_NULL(repl_handle); + + task_args_t args = {.start_sem = start_sem, .done_sem = done_sem, .hdl = repl_handle}; + + /* create the repl task */ + BaseType_t rc = xTaskCreate(repl_task, "repl_task", 4096, &args, 5, NULL); + TEST_ASSERT_EQUAL(pdPASS, rc); + + /* should fail before repl is started */ + TEST_ASSERT_NOT_EQUAL(ESP_OK, esp_repl_stop(repl_handle)); + + /* start repl */ + TEST_ASSERT_NOT_EQUAL(ESP_OK, esp_repl_start(NULL)); + TEST_ASSERT_EQUAL(ESP_OK, esp_repl_start(repl_handle)); + + wait_ms(100); + + /* wait for the repl task to signal it started */ + TEST_ASSERT_TRUE(xSemaphoreTake(start_sem, pdMS_TO_TICKS(2000))); + + /* send a dummy string new line terminated to trigger linenoise to return */ + const char *input_line = "dummy_message\n"; + test_send_characters(s_socket_fd[1], input_line); + + /* wait for a bit so esp_repl() has time to loop back into esp_linenoise_get_line */ + wait_ms(100); + + /* check that pre-executor, post-executor callbacks are called */ + TEST_ASSERT_EQUAL(1, s_pre_executor_nb_of_calls); + TEST_ASSERT_EQUAL(1, s_post_executor_nb_of_calls); - xTaskCreate(esp_apptrace_send_uart_tx_task, "app_trace_uart_tx_task", 2500, hw_data, uart_prio, NULL); + /* stop repl and wait for task to finish (emulate pthread_join) */ + TEST_ASSERT_NOT_EQUAL(ESP_OK, esp_repl_stop(NULL)); + TEST_ASSERT_EQUAL(ESP_OK, esp_repl_stop(repl_handle)); -} \ No newline at end of file + /* wait for the repl task to signal completion */ + TEST_ASSERT_TRUE(xSemaphoreTake(done_sem, pdMS_TO_TICKS(2000))); + + /* check that all callbacks were called the right number of times */ + TEST_ASSERT_EQUAL(1, s_on_stop_nb_of_calls); + TEST_ASSERT_EQUAL(1, s_on_exit_nb_of_calls); + TEST_ASSERT_EQUAL(2, s_pre_executor_nb_of_calls); + TEST_ASSERT_EQUAL(2, s_post_executor_nb_of_calls); + + /* make sure calling stop fails because the repl is no longer running */ + TEST_ASSERT_NOT_EQUAL(ESP_OK, esp_repl_stop(repl_handle)); + + /* reset the static variables */ + s_on_stop_nb_of_calls = 0; + s_on_exit_nb_of_calls = 0; + s_pre_executor_nb_of_calls = 0; + s_post_executor_nb_of_calls = 0; + + /* destroy the repl instance */ + TEST_ASSERT_NOT_EQUAL(ESP_OK, esp_repl_destroy(NULL)); + TEST_ASSERT_EQUAL(ESP_OK, esp_repl_destroy(repl_handle)); + + /* cleanup semaphores */ + vSemaphoreDelete(start_sem); + vSemaphoreDelete(done_sem); + + test_socket_teardown(s_socket_fd); +} + +static int quit_command(void *context, const int fd_out, int argc, char **argv) +{ + esp_repl_handle_t repl_handle = (esp_repl_handle_t)context; + TEST_ASSERT_EQUAL(ESP_OK, esp_repl_stop(repl_handle)); + + return 0; +} + +TEST_CASE("esp_repl() exits when esp_repl_stop() called from the task running esp_repl()", "[esp_repl]") +{ + /* create semaphores */ + SemaphoreHandle_t start_sem = xSemaphoreCreateBinary(); + TEST_ASSERT_NOT_NULL(start_sem); + SemaphoreHandle_t done_sem = xSemaphoreCreateBinary(); + TEST_ASSERT_NOT_NULL(done_sem); + + /* ensure both semaphores are in the "taken/empty" state: + taking with 0 timeout guarantees they are empty afterwards + regardless of the create semantics on this FreeRTOS build. */ + xSemaphoreTake(start_sem, 0); + xSemaphoreTake(done_sem, 0); + + esp_linenoise_config_t linenoise_config; + esp_linenoise_get_instance_config_default(&linenoise_config); + test_socket_setup(s_socket_fd); + linenoise_config.in_fd = s_socket_fd[0]; + linenoise_config.out_fd = s_socket_fd[0]; + esp_linenoise_handle_t esp_linenoise_hdl; + TEST_ASSERT_EQUAL(ESP_OK, esp_linenoise_create_instance(&linenoise_config, &esp_linenoise_hdl)); + TEST_ASSERT_NOT_NULL(esp_linenoise_hdl); + + esp_repl_config_t repl_config = { + .linenoise_handle = esp_linenoise_hdl, + .command_set_handle = NULL, + .max_cmd_line_size = 256, + .history_save_path = NULL, + .pre_executor = { .func = test_pre_executor, .ctx = NULL }, + .post_executor = { .func = test_post_executor, .ctx = NULL }, + .on_stop = { .func = test_on_stop, .ctx = NULL }, + .on_exit = { .func = test_on_exit, .ctx = NULL } + }; + + esp_repl_handle_t repl_handle = NULL; + TEST_ASSERT_EQUAL(ESP_OK, esp_repl_create(&repl_config, &repl_handle)); + TEST_ASSERT_NOT_NULL(repl_handle); + + task_args_t args = {.start_sem = start_sem, .done_sem = done_sem, .hdl = repl_handle}; + + /* create the repl task */ + BaseType_t rc = xTaskCreate(repl_task, "repl_task", 4096, &args, 5, NULL); + TEST_ASSERT_EQUAL(pdPASS, rc); + + /* register a quit command to esp_commands */ + esp_command_t quit_cmd = { + .name = "quit", + .group = "quit", + .help = "-", + .func = quit_command, + .func_ctx = repl_handle, + .hint_cb = NULL, + .glossary_cb = NULL + }; + TEST_ASSERT_EQUAL(ESP_OK, esp_commands_register_cmd(&quit_cmd)); + + /* start repl */ + TEST_ASSERT_EQUAL(ESP_OK, esp_repl_start(repl_handle)); + + wait_ms(100); + + /* wait for the repl task to signal it started */ + TEST_ASSERT_TRUE(xSemaphoreTake(start_sem, pdMS_TO_TICKS(2000))); + + /* send the quit command */ + const char *quit_cmd_line = "quit\n"; + test_send_characters(s_socket_fd[1], quit_cmd_line); + + /* wait for the repl task to signal completion */ + TEST_ASSERT_TRUE(xSemaphoreTake(done_sem, pdMS_TO_TICKS(2000))); + + /* destroy the repl instance */ + TEST_ASSERT_EQUAL(ESP_OK, esp_repl_destroy(repl_handle)); + + /* cleanup semaphores */ + vSemaphoreDelete(start_sem); + vSemaphoreDelete(done_sem); + + test_socket_teardown(s_socket_fd); +} diff --git a/esp_repl/test_apps/pytest_esp_repl.ppy b/esp_repl/test_apps/pytest_esp_repl.ppy deleted file mode 100644 index cdbc3e419a..0000000000 --- a/esp_repl/test_apps/pytest_esp_repl.ppy +++ /dev/null @@ -1,9 +0,0 @@ -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_repl(dut) -> None: - dut.run_all_single_board_cases() diff --git a/esp_repl/test_apps/pytest_esp_repl.py b/esp_repl/test_apps/pytest_esp_repl.py new file mode 100644 index 0000000000..c4ee9ec096 --- /dev/null +++ b/esp_repl/test_apps/pytest_esp_repl.py @@ -0,0 +1,8 @@ +import pytest + + +@pytest.mark.generic +@pytest.mark.parametrize( + 'target', ['linux'], indirect=['target']) +def test_esp_repl(dut) -> None: + dut.run_all_single_board_cases() From 5b3950eabbe19e56fb2105668306e8d97b9420f8 Mon Sep 17 00:00:00 2001 From: Guillaume Souchere Date: Tue, 28 Oct 2025 02:17:56 +0100 Subject: [PATCH 5/8] feat(esp_repl): Add option for esp_repl to register quit cmd --- esp_repl/CMakeLists.txt | 5 ++- esp_repl/Kconfig | 10 +++++ esp_repl/idf_component.yml | 4 +- esp_repl/{ => src}/esp_repl.c | 53 ++++++++++++++++++++++++- esp_repl/test_apps/main/test_esp_repl.c | 26 ++---------- esp_repl/test_apps/sdkconfig.defaults | 3 +- 6 files changed, 73 insertions(+), 28 deletions(-) create mode 100644 esp_repl/Kconfig rename esp_repl/{ => src}/esp_repl.c (77%) diff --git a/esp_repl/CMakeLists.txt b/esp_repl/CMakeLists.txt index cc4beb0410..1ea31c33e3 100644 --- a/esp_repl/CMakeLists.txt +++ b/esp_repl/CMakeLists.txt @@ -1,8 +1,9 @@ idf_build_get_property(target IDF_TARGET) -set(srcs "esp_repl.c") +set(srcs "src/esp_repl.c") idf_component_register( SRCS ${srcs} INCLUDE_DIRS include - REQUIRES esp_linenoise esp_commands) + REQUIRES esp_linenoise esp_commands + WHOLE_ARCHIVE) diff --git a/esp_repl/Kconfig b/esp_repl/Kconfig new file mode 100644 index 0000000000..32c071233a --- /dev/null +++ b/esp_repl/Kconfig @@ -0,0 +1,10 @@ +menu "esp_repl configuration" + + config ESP_REPL_HAS_QUIT_CMD + bool "Register quit command" + default n + help + Register a static command "quit" that allows the user to return from the esp_repl main loop. + The command is registered through the ESP_COMMAND_REGISTER macro provided by esp_commands component + and is placed in the dedicated flash section. +endmenu diff --git a/esp_repl/idf_component.yml b/esp_repl/idf_component.yml index 927250f173..35aa6e4d42 100644 --- a/esp_repl/idf_component.yml +++ b/esp_repl/idf_component.yml @@ -2,7 +2,9 @@ version: "1.0.0" description: "esp_repl - Read Eval Print Loop component" url: https://github.com/espressif/idf-extra-components/tree/master/esp_repl dependencies: - espressif/esp_linenoise: '*' + SoucheSouche/esp_linenoise: + version: "*" + registry_url: https://components-staging.espressif.com SoucheSouche/esp_commands: version: "*" registry_url: https://components-staging.espressif.com diff --git a/esp_repl/esp_repl.c b/esp_repl/src/esp_repl.c similarity index 77% rename from esp_repl/esp_repl.c rename to esp_repl/src/esp_repl.c index 40745c8741..e2e33b25cc 100644 --- a/esp_repl/esp_repl.c +++ b/esp_repl/src/esp_repl.c @@ -29,6 +29,31 @@ typedef struct esp_repl_instance { esp_repl_state_t state; } esp_repl_instance_t; +#if CONFIG_ESP_REPL_HAS_QUIT_CMD +#define _ESP_REPL_STRINGIFY(x) #x +#define ESP_REPL_STRINGIFY(x) _ESP_REPL_STRINGIFY(x) + +#define ESP_REPL_QUIT_CMD quit +#define ESP_REPL_QUIT_CMD_STR ESP_REPL_STRINGIFY(ESP_REPL_QUIT_CMD) +#define ESP_REPL_QUIT_CMD_SIZE strlen(ESP_REPL_QUIT_CMD_STR) + +/* dummy command function callback to allow the registration for the quit command + * to succeed */ +static int esp_repl_quit_cmd(void *context, esp_commands_exec_arg_t *cmd_args, int argc, char **argv) +{ + return 0; +} + +ESP_COMMAND_REGISTER(ESP_REPL_QUIT_CMD, + "esp_repl", + "This command will trigger the exit mechanism to exit from esp_repl() main loop", + esp_repl_quit_cmd, + NULL, + NULL, + NULL + ); +#endif // CONFIG_ESP_REPL_HAS_QUIT_CMD + #define ESP_REPL_CHECK_INSTANCE(handle) do { \ if(handle == NULL) { \ return ESP_ERR_INVALID_ARG; \ @@ -204,9 +229,35 @@ void esp_repl(esp_repl_handle_t handle) continue; } +#if CONFIG_ESP_REPL_HAS_QUIT_CMD + /* evaluate the command name. make sure that the first argument of the cmd_line + * is ESP_REPL_QUIT_CMD_STR and the character following that is either a space + * or a null character */ + if ((strncmp(ESP_REPL_QUIT_CMD_STR, cmd_line, ESP_REPL_QUIT_CMD_SIZE) == 0) && + ((cmd_line[ESP_REPL_QUIT_CMD_SIZE] == ' ') || (cmd_line[ESP_REPL_QUIT_CMD_SIZE] == '\0'))) { + /* quit command received, call esp_repl_stop() */ + if (esp_repl_stop(handle) == ESP_OK) { + /* if esp_repl_stop() was successful, retry the while condition. + * the esp_repl state should have been changed which will force + * the while to break */ + continue; + } + } +#endif // CONFIG_ESP_REPL_HAS_QUIT_CMD + /* try to run the command */ int cmd_func_ret; - const esp_err_t exec_ret = esp_commands_execute(c_set, -1, cmd_line, &cmd_func_ret); + esp_commands_exec_arg_t cmd_args; + esp_err_t get_ret = esp_linenoise_get_out_fd(handle->config.linenoise_handle, &(cmd_args.out_fd)); + if (get_ret != ESP_OK) { + cmd_args.out_fd = STDOUT_FILENO; + } + get_ret = esp_linenoise_get_write(handle->config.linenoise_handle, &(cmd_args.write_func)); + if (get_ret != ESP_OK) { + cmd_args.write_func = write; + } + + const esp_err_t exec_ret = esp_commands_execute(c_set, &cmd_args, cmd_line, &cmd_func_ret); /* forward the raw command line to the post executor callback (e.g., save in history). * this callback is not necessary for the user to register, continue if it isn't */ diff --git a/esp_repl/test_apps/main/test_esp_repl.c b/esp_repl/test_apps/main/test_esp_repl.c index 0031e6b76e..eb362414c8 100644 --- a/esp_repl/test_apps/main/test_esp_repl.c +++ b/esp_repl/test_apps/main/test_esp_repl.c @@ -71,13 +71,13 @@ static void test_send_characters(int socket_fd, const char *msg) TEST_ASSERT_EQUAL(msg_len, nwrite); } -esp_err_t test_pre_executor(void *ctx, char *buf, const esp_err_t reader_ret_val) +esp_err_t test_pre_executor(void *ctx, const char *buf, esp_err_t reader_ret_val) { s_pre_executor_nb_of_calls++; return ESP_OK; } -esp_err_t test_post_executor(void *ctx, const char *buf, const esp_err_t executor_ret_val, const int cmd_ret_val) +esp_err_t test_post_executor(void *ctx, const char *buf, esp_err_t executor_ret_val, int cmd_ret_val) { s_post_executor_nb_of_calls++; return ESP_OK; @@ -222,14 +222,6 @@ TEST_CASE("esp_repl() loop calls all callbacks and exit on call to esp_repl_stop test_socket_teardown(s_socket_fd); } -static int quit_command(void *context, const int fd_out, int argc, char **argv) -{ - esp_repl_handle_t repl_handle = (esp_repl_handle_t)context; - TEST_ASSERT_EQUAL(ESP_OK, esp_repl_stop(repl_handle)); - - return 0; -} - TEST_CASE("esp_repl() exits when esp_repl_stop() called from the task running esp_repl()", "[esp_repl]") { /* create semaphores */ @@ -274,18 +266,6 @@ TEST_CASE("esp_repl() exits when esp_repl_stop() called from the task running es BaseType_t rc = xTaskCreate(repl_task, "repl_task", 4096, &args, 5, NULL); TEST_ASSERT_EQUAL(pdPASS, rc); - /* register a quit command to esp_commands */ - esp_command_t quit_cmd = { - .name = "quit", - .group = "quit", - .help = "-", - .func = quit_command, - .func_ctx = repl_handle, - .hint_cb = NULL, - .glossary_cb = NULL - }; - TEST_ASSERT_EQUAL(ESP_OK, esp_commands_register_cmd(&quit_cmd)); - /* start repl */ TEST_ASSERT_EQUAL(ESP_OK, esp_repl_start(repl_handle)); @@ -295,7 +275,7 @@ TEST_CASE("esp_repl() exits when esp_repl_stop() called from the task running es TEST_ASSERT_TRUE(xSemaphoreTake(start_sem, pdMS_TO_TICKS(2000))); /* send the quit command */ - const char *quit_cmd_line = "quit\n"; + const char *quit_cmd_line = "quit \n"; test_send_characters(s_socket_fd[1], quit_cmd_line); /* wait for the repl task to signal completion */ diff --git a/esp_repl/test_apps/sdkconfig.defaults b/esp_repl/test_apps/sdkconfig.defaults index 5e7cb391c2..fb6d7dba93 100644 --- a/esp_repl/test_apps/sdkconfig.defaults +++ b/esp_repl/test_apps/sdkconfig.defaults @@ -1 +1,2 @@ -CONFIG_ESP_TASK_WDT_EN=n \ No newline at end of file +CONFIG_ESP_TASK_WDT_EN=n +CONFIG_ESP_REPL_HAS_QUIT_CMD=y \ No newline at end of file From d0fecb4bc8df12600a88776cee3c47e187e96825 Mon Sep 17 00:00:00 2001 From: Guillaume Souchere Date: Mon, 10 Nov 2025 08:40:02 +0100 Subject: [PATCH 6/8] feat(esp_repl): Add integration tests --- esp_repl/.build-test-rules.yml | 3 + esp_repl/idf_component.yml | 2 +- esp_repl/include/esp_repl.h | 17 ++ esp_repl/src/esp_repl.c | 9 +- esp_repl/test_apps/main/test_esp_repl.c | 273 ++++++++++++++---------- esp_repl/test_apps/pytest_esp_repl.py | 14 +- 6 files changed, 199 insertions(+), 119 deletions(-) diff --git a/esp_repl/.build-test-rules.yml b/esp_repl/.build-test-rules.yml index 059bb5e635..2edf165073 100644 --- a/esp_repl/.build-test-rules.yml +++ b/esp_repl/.build-test-rules.yml @@ -2,3 +2,6 @@ esp_repl/test_apps: enable: - if: IDF_TARGET == "linux" reason: "Sufficient to test on Linux target" + disable: + - if: IDF_VERSION_MAJOR <= 5 and IDF_VERSION_MINOR <= 4 + reason: "those versions of esp-idf do not support eventfd for linux target" \ No newline at end of file diff --git a/esp_repl/idf_component.yml b/esp_repl/idf_component.yml index 35aa6e4d42..d4cdb20984 100644 --- a/esp_repl/idf_component.yml +++ b/esp_repl/idf_component.yml @@ -1,4 +1,4 @@ -version: "1.0.0" +version: "0.1.0" description: "esp_repl - Read Eval Print Loop component" url: https://github.com/espressif/idf-extra-components/tree/master/esp_repl dependencies: diff --git a/esp_repl/include/esp_repl.h b/esp_repl/include/esp_repl.h index 80f3264c12..28215f096d 100644 --- a/esp_repl/include/esp_repl.h +++ b/esp_repl/include/esp_repl.h @@ -19,6 +19,22 @@ extern "C" { */ typedef struct esp_repl_instance *esp_repl_handle_t; +/** + * @brief Function prototype called at the beginning of esp_repl(). + * + * @param ctx User-defined context pointer. + * @param handle Handle to the REPL instance. + */ +typedef void (*esp_repl_on_enter_fn)(void *ctx, esp_repl_handle_t handle); + +/** + * @brief Enter callback configuration structure for the REPL. + */ +typedef struct esp_repl_on_enter { + esp_repl_on_enter_fn func; /**!< Function called at the beginning of esp_repl() */ + void *ctx; /**!< Context passed to the enter function */ +} esp_repl_on_enter_t; + /** * @brief Function prototype called before executing a command. * @@ -101,6 +117,7 @@ typedef struct esp_repl_config { esp_command_set_handle_t command_set_handle; /**!< Handle to a set of commands */ size_t max_cmd_line_size; /**!< Maximum allowed command line size */ const char *history_save_path; /**!< Path to file to save the history */ + esp_repl_on_enter_t on_enter; /**!< Enter callback and context */ esp_repl_pre_executor_t pre_executor; /**!< Pre-executor callback and context */ esp_repl_post_executor_t post_executor; /**!< Post-executor callback and context */ esp_repl_on_stop_t on_stop; /**!< Stop callback and context */ diff --git a/esp_repl/src/esp_repl.c b/esp_repl/src/esp_repl.c index e2e33b25cc..aa2905aabd 100644 --- a/esp_repl/src/esp_repl.c +++ b/esp_repl/src/esp_repl.c @@ -184,6 +184,13 @@ void esp_repl(esp_repl_handle_t handle) esp_repl_config_t *config = &handle->config; esp_repl_state_t *state = &handle->state; + /* trigger a user defined callback before the function gets into the while loop + * if the user wants to perform some logic that needs to be done within the task + * running the REPL */ + if (config->on_enter.func != NULL) { + config->on_enter.func(config->on_enter.ctx, handle); + } + /* get the task handle of the task running this function. * It is necessary to gather this information in case esp_repl_stop() * is called from the same task as the one running esp_repl() (e.g., @@ -257,7 +264,7 @@ void esp_repl(esp_repl_handle_t handle) cmd_args.write_func = write; } - const esp_err_t exec_ret = esp_commands_execute(c_set, &cmd_args, cmd_line, &cmd_func_ret); + const esp_err_t exec_ret = esp_commands_execute(cmd_line, &cmd_func_ret, c_set, &cmd_args); /* forward the raw command line to the post executor callback (e.g., save in history). * this callback is not necessary for the user to register, continue if it isn't */ diff --git a/esp_repl/test_apps/main/test_esp_repl.c b/esp_repl/test_apps/main/test_esp_repl.c index eb362414c8..4d63bfc0bc 100644 --- a/esp_repl/test_apps/main/test_esp_repl.c +++ b/esp_repl/test_apps/main/test_esp_repl.c @@ -7,7 +7,6 @@ #include #include #include -#include #include #include "freertos/FreeRTOS.h" #include "freertos/task.h" @@ -17,6 +16,10 @@ #include "esp_linenoise.h" #include "esp_commands.h" +#if CONFIG_IDF_TARGET_LINUX +#include +#endif + inline __attribute__((always_inline)) uint32_t get_millis(void) { @@ -31,44 +34,16 @@ void wait_ms(int ms) vTaskDelay(pdMS_TO_TICKS(ms)); } -/* fake the section expected by esp_commands, make sure end and starts - * at the same address so the esp_commands component sees no commands */ -esp_command_t _esp_commands_start; -extern esp_command_t _esp_commands_end __attribute__((alias("_esp_commands_start"))); - -static int s_socket_fd[2]; +static size_t s_on_enter_nb_of_calls = 0; static size_t s_pre_executor_nb_of_calls = 0; static size_t s_post_executor_nb_of_calls = 0; static size_t s_on_stop_nb_of_calls = 0; static size_t s_on_exit_nb_of_calls = 0; -static void test_socket_setup(int socket_fd[2]) +void test_on_enter(void *ctx, esp_repl_handle_t handle) { - TEST_ASSERT_EQUAL(0, socketpair(AF_UNIX, SOCK_STREAM, 0, socket_fd)); - - /* ensure reads are blocking */ - int flags = fcntl(socket_fd[0], F_GETFL, 0); - flags &= ~O_NONBLOCK; - fcntl(socket_fd[0], F_SETFL, flags); - - flags = fcntl(socket_fd[1], F_GETFL, 0); - flags &= ~O_NONBLOCK; - fcntl(socket_fd[1], F_SETFL, flags); -} - -static void test_socket_teardown(int socket_fd[2]) -{ - close(socket_fd[0]); - close(socket_fd[1]); -} - -static void test_send_characters(int socket_fd, const char *msg) -{ - wait_ms(100); - - const size_t msg_len = strlen(msg); - const int nwrite = write(socket_fd, msg, msg_len); - TEST_ASSERT_EQUAL(msg_len, nwrite); + s_on_enter_nb_of_calls++; + return; } esp_err_t test_pre_executor(void *ctx, const char *buf, esp_err_t reader_ret_val) @@ -122,56 +97,126 @@ static void repl_task(void *args) vTaskDelete(NULL); } -TEST_CASE("esp_repl() loop calls all callbacks and exit on call to esp_repl_stop", "[esp_repl]") +#if CONFIG_IDF_TARGET_LINUX +static int s_socket_fd[2]; + +static void test_socket_setup(int socket_fd[2]) +{ + TEST_ASSERT_EQUAL(0, socketpair(AF_UNIX, SOCK_STREAM, 0, socket_fd)); + + /* ensure reads are blocking */ + int flags = fcntl(socket_fd[0], F_GETFL, 0); + flags &= ~O_NONBLOCK; + fcntl(socket_fd[0], F_SETFL, flags); + + flags = fcntl(socket_fd[1], F_GETFL, 0); + flags &= ~O_NONBLOCK; + fcntl(socket_fd[1], F_SETFL, flags); +} + +static void test_socket_teardown(int socket_fd[2]) +{ + close(socket_fd[0]); + close(socket_fd[1]); +} + +static void test_send_characters(int socket_fd, const char *msg) +{ + wait_ms(100); + + const size_t msg_len = strlen(msg); + const int nwrite = write(socket_fd, msg, msg_len); + TEST_ASSERT_EQUAL(msg_len, nwrite); +} + +static void teardown_repl_instance(SemaphoreHandle_t *start_sem, SemaphoreHandle_t *done_sem, int socket_fd[2], + esp_linenoise_handle_t *linenoise_hdl, esp_repl_handle_t *repl_hdl) +{ + /* destroy the instance of resp_repl */ + TEST_ASSERT_EQUAL(ESP_OK, esp_repl_destroy(*repl_hdl)); + + /* delete the linenoise instance */ + TEST_ASSERT_EQUAL(ESP_OK, esp_linenoise_delete_instance(*linenoise_hdl)); + + /* cleanup semaphores */ + vSemaphoreDelete(*start_sem); + vSemaphoreDelete(*done_sem); + + /* close the socketpair */ + test_socket_teardown(socket_fd); + + s_on_stop_nb_of_calls = 0; + s_on_exit_nb_of_calls = 0; + s_on_enter_nb_of_calls = 0; + s_pre_executor_nb_of_calls = 0; + s_post_executor_nb_of_calls = 0; +} + +static void setup_repl_instance(SemaphoreHandle_t *start_sem, SemaphoreHandle_t *done_sem, int socket_fd[2], + esp_linenoise_handle_t *linenoise_hdl, esp_repl_handle_t *repl_hdl) { /* create semaphores */ - SemaphoreHandle_t start_sem = xSemaphoreCreateBinary(); + *start_sem = xSemaphoreCreateBinary(); TEST_ASSERT_NOT_NULL(start_sem); - SemaphoreHandle_t done_sem = xSemaphoreCreateBinary(); + *done_sem = xSemaphoreCreateBinary(); TEST_ASSERT_NOT_NULL(done_sem); + /* create the socket_pair */ + test_socket_setup(socket_fd); + /* ensure both semaphores are in the "taken/empty" state: - taking with 0 timeout guarantees they are empty afterwards - regardless of the create semantics on this FreeRTOS build. */ - xSemaphoreTake(start_sem, 0); - xSemaphoreTake(done_sem, 0); + taking with 0 timeout guarantees they are empty afterwards + regardless of the create semantics on this FreeRTOS build. */ + xSemaphoreTake(*start_sem, 0); + xSemaphoreTake(*done_sem, 0); esp_linenoise_config_t linenoise_config; esp_linenoise_get_instance_config_default(&linenoise_config); - test_socket_setup(s_socket_fd); - linenoise_config.in_fd = s_socket_fd[0]; - linenoise_config.out_fd = s_socket_fd[0]; - esp_linenoise_handle_t esp_linenoise_hdl; - TEST_ASSERT_EQUAL(ESP_OK, esp_linenoise_create_instance(&linenoise_config, &esp_linenoise_hdl)); - TEST_ASSERT_NOT_NULL(esp_linenoise_hdl); + linenoise_config.in_fd = socket_fd[0]; + linenoise_config.out_fd = socket_fd[0]; + TEST_ASSERT_EQUAL(ESP_OK, esp_linenoise_create_instance(&linenoise_config, linenoise_hdl)); + TEST_ASSERT_NOT_NULL(*linenoise_hdl); esp_repl_config_t repl_config = { - .linenoise_handle = esp_linenoise_hdl, + .linenoise_handle = *linenoise_hdl, .command_set_handle = NULL, .max_cmd_line_size = 256, .history_save_path = NULL, + .on_enter = { .func = test_on_enter, .ctx = NULL }, .pre_executor = { .func = test_pre_executor, .ctx = NULL }, .post_executor = { .func = test_post_executor, .ctx = NULL }, .on_stop = { .func = test_on_stop, .ctx = NULL }, .on_exit = { .func = test_on_exit, .ctx = NULL } }; - esp_repl_handle_t repl_handle = NULL; - TEST_ASSERT_EQUAL(ESP_OK, esp_repl_create(&repl_config, &repl_handle)); - TEST_ASSERT_NOT_NULL(repl_handle); + TEST_ASSERT_EQUAL(ESP_OK, esp_repl_create(&repl_config, repl_hdl)); + TEST_ASSERT_NOT_NULL(*repl_hdl); - task_args_t args = {.start_sem = start_sem, .done_sem = done_sem, .hdl = repl_handle}; + s_on_stop_nb_of_calls = 0; + s_on_exit_nb_of_calls = 0; + s_on_enter_nb_of_calls = 0; + s_pre_executor_nb_of_calls = 0; + s_post_executor_nb_of_calls = 0; +} + +TEST_CASE("esp_repl() loop calls all callbacks and exit on call to esp_repl_stop", "[esp_repl][host_test]") +{ + SemaphoreHandle_t start_sem, done_sem; + esp_linenoise_handle_t linenoise_hdl; + esp_repl_handle_t repl_hdl; + setup_repl_instance(&start_sem, &done_sem, s_socket_fd, &linenoise_hdl, &repl_hdl); /* create the repl task */ + task_args_t args = {.start_sem = start_sem, .done_sem = done_sem, .hdl = repl_hdl}; BaseType_t rc = xTaskCreate(repl_task, "repl_task", 4096, &args, 5, NULL); TEST_ASSERT_EQUAL(pdPASS, rc); /* should fail before repl is started */ - TEST_ASSERT_NOT_EQUAL(ESP_OK, esp_repl_stop(repl_handle)); + TEST_ASSERT_NOT_EQUAL(ESP_OK, esp_repl_stop(repl_hdl)); /* start repl */ TEST_ASSERT_NOT_EQUAL(ESP_OK, esp_repl_start(NULL)); - TEST_ASSERT_EQUAL(ESP_OK, esp_repl_start(repl_handle)); + TEST_ASSERT_EQUAL(ESP_OK, esp_repl_start(repl_hdl)); wait_ms(100); @@ -183,7 +228,7 @@ TEST_CASE("esp_repl() loop calls all callbacks and exit on call to esp_repl_stop test_send_characters(s_socket_fd[1], input_line); /* wait for a bit so esp_repl() has time to loop back into esp_linenoise_get_line */ - wait_ms(100); + wait_ms(500); /* check that pre-executor, post-executor callbacks are called */ TEST_ASSERT_EQUAL(1, s_pre_executor_nb_of_calls); @@ -191,83 +236,40 @@ TEST_CASE("esp_repl() loop calls all callbacks and exit on call to esp_repl_stop /* stop repl and wait for task to finish (emulate pthread_join) */ TEST_ASSERT_NOT_EQUAL(ESP_OK, esp_repl_stop(NULL)); - TEST_ASSERT_EQUAL(ESP_OK, esp_repl_stop(repl_handle)); + TEST_ASSERT_EQUAL(ESP_OK, esp_repl_stop(repl_hdl)); /* wait for the repl task to signal completion */ TEST_ASSERT_TRUE(xSemaphoreTake(done_sem, pdMS_TO_TICKS(2000))); /* check that all callbacks were called the right number of times */ TEST_ASSERT_EQUAL(1, s_on_stop_nb_of_calls); + TEST_ASSERT_EQUAL(1, s_on_enter_nb_of_calls); TEST_ASSERT_EQUAL(1, s_on_exit_nb_of_calls); TEST_ASSERT_EQUAL(2, s_pre_executor_nb_of_calls); TEST_ASSERT_EQUAL(2, s_post_executor_nb_of_calls); /* make sure calling stop fails because the repl is no longer running */ - TEST_ASSERT_NOT_EQUAL(ESP_OK, esp_repl_stop(repl_handle)); - - /* reset the static variables */ - s_on_stop_nb_of_calls = 0; - s_on_exit_nb_of_calls = 0; - s_pre_executor_nb_of_calls = 0; - s_post_executor_nb_of_calls = 0; + TEST_ASSERT_NOT_EQUAL(ESP_OK, esp_repl_stop(repl_hdl)); /* destroy the repl instance */ TEST_ASSERT_NOT_EQUAL(ESP_OK, esp_repl_destroy(NULL)); - TEST_ASSERT_EQUAL(ESP_OK, esp_repl_destroy(repl_handle)); - - /* cleanup semaphores */ - vSemaphoreDelete(start_sem); - vSemaphoreDelete(done_sem); - - test_socket_teardown(s_socket_fd); + teardown_repl_instance(&start_sem, &done_sem, s_socket_fd, &linenoise_hdl, &repl_hdl); } -TEST_CASE("esp_repl() exits when esp_repl_stop() called from the task running esp_repl()", "[esp_repl]") +TEST_CASE("esp_repl() exits when esp_repl_stop() called from the task running esp_repl()", "[esp_repl][host_test]") { - /* create semaphores */ - SemaphoreHandle_t start_sem = xSemaphoreCreateBinary(); - TEST_ASSERT_NOT_NULL(start_sem); - SemaphoreHandle_t done_sem = xSemaphoreCreateBinary(); - TEST_ASSERT_NOT_NULL(done_sem); - - /* ensure both semaphores are in the "taken/empty" state: - taking with 0 timeout guarantees they are empty afterwards - regardless of the create semantics on this FreeRTOS build. */ - xSemaphoreTake(start_sem, 0); - xSemaphoreTake(done_sem, 0); - - esp_linenoise_config_t linenoise_config; - esp_linenoise_get_instance_config_default(&linenoise_config); - test_socket_setup(s_socket_fd); - linenoise_config.in_fd = s_socket_fd[0]; - linenoise_config.out_fd = s_socket_fd[0]; - esp_linenoise_handle_t esp_linenoise_hdl; - TEST_ASSERT_EQUAL(ESP_OK, esp_linenoise_create_instance(&linenoise_config, &esp_linenoise_hdl)); - TEST_ASSERT_NOT_NULL(esp_linenoise_hdl); - - esp_repl_config_t repl_config = { - .linenoise_handle = esp_linenoise_hdl, - .command_set_handle = NULL, - .max_cmd_line_size = 256, - .history_save_path = NULL, - .pre_executor = { .func = test_pre_executor, .ctx = NULL }, - .post_executor = { .func = test_post_executor, .ctx = NULL }, - .on_stop = { .func = test_on_stop, .ctx = NULL }, - .on_exit = { .func = test_on_exit, .ctx = NULL } - }; - - esp_repl_handle_t repl_handle = NULL; - TEST_ASSERT_EQUAL(ESP_OK, esp_repl_create(&repl_config, &repl_handle)); - TEST_ASSERT_NOT_NULL(repl_handle); - - task_args_t args = {.start_sem = start_sem, .done_sem = done_sem, .hdl = repl_handle}; + SemaphoreHandle_t start_sem, done_sem; + esp_linenoise_handle_t linenoise_hdl; + esp_repl_handle_t repl_hdl; + setup_repl_instance(&start_sem, &done_sem, s_socket_fd, &linenoise_hdl, &repl_hdl); /* create the repl task */ + task_args_t args = {.start_sem = start_sem, .done_sem = done_sem, .hdl = repl_hdl}; BaseType_t rc = xTaskCreate(repl_task, "repl_task", 4096, &args, 5, NULL); TEST_ASSERT_EQUAL(pdPASS, rc); /* start repl */ - TEST_ASSERT_EQUAL(ESP_OK, esp_repl_start(repl_handle)); + TEST_ASSERT_EQUAL(ESP_OK, esp_repl_start(repl_hdl)); wait_ms(100); @@ -281,12 +283,55 @@ TEST_CASE("esp_repl() exits when esp_repl_stop() called from the task running es /* wait for the repl task to signal completion */ TEST_ASSERT_TRUE(xSemaphoreTake(done_sem, pdMS_TO_TICKS(2000))); - /* destroy the repl instance */ - TEST_ASSERT_EQUAL(ESP_OK, esp_repl_destroy(repl_handle)); + teardown_repl_instance(&start_sem, &done_sem, s_socket_fd, &linenoise_hdl, &repl_hdl); +} - /* cleanup semaphores */ - vSemaphoreDelete(start_sem); - vSemaphoreDelete(done_sem); +TEST_CASE("create and destroy several instances of esp_repl", "[esp_repl]") +{ + /* create semaphores */ + SemaphoreHandle_t start_sem_a, start_sem_b; + SemaphoreHandle_t done_sem_a, done_sem_b; + esp_repl_handle_t repl_hdl_a, repl_hdl_b; + esp_linenoise_handle_t linenoise_hdl_a, linenoise_hdl_b; + int socket_fd_a[2], socket_fd_b[2]; + + /* create 2 instances of esp_repl*/ + setup_repl_instance(&start_sem_a, &done_sem_a, socket_fd_a, &linenoise_hdl_a, &repl_hdl_a); + setup_repl_instance(&start_sem_b, &done_sem_b, socket_fd_b, &linenoise_hdl_b, &repl_hdl_b); + + /* create the repl task A */ + task_args_t args_a = {.start_sem = start_sem_a, .done_sem = done_sem_a, .hdl = repl_hdl_a}; + BaseType_t rc = xTaskCreate(repl_task, "repl_task_a", 4096, &args_a, 5, NULL); + TEST_ASSERT_EQUAL(pdPASS, rc); + + /* create the repl task B */ + task_args_t args_b = {.start_sem = start_sem_b, .done_sem = done_sem_b, .hdl = repl_hdl_b}; + rc = xTaskCreate(repl_task, "repl_task_b", 4096, &args_b, 5, NULL); + TEST_ASSERT_EQUAL(pdPASS, rc); + + /* start repl */ + TEST_ASSERT_EQUAL(ESP_OK, esp_repl_start(repl_hdl_a)); + TEST_ASSERT_EQUAL(ESP_OK, esp_repl_start(repl_hdl_b)); + wait_ms(500); - test_socket_teardown(s_socket_fd); + /* wait for the repl tasks to signal it started */ + TEST_ASSERT_TRUE(xSemaphoreTake(start_sem_a, pdMS_TO_TICKS(2000))); + TEST_ASSERT_TRUE(xSemaphoreTake(start_sem_b, pdMS_TO_TICKS(2000))); + + /* terminate instance A */ + TEST_ASSERT_EQUAL(ESP_OK, esp_repl_stop(repl_hdl_a)); + + /* wait for the repl task to signal completion */ + TEST_ASSERT_TRUE(xSemaphoreTake(done_sem_a, pdMS_TO_TICKS(2000))); + + /* terminate instance B */ + TEST_ASSERT_EQUAL(ESP_OK, esp_repl_stop(repl_hdl_b)); + + /* wait for the repl task to signal completion */ + TEST_ASSERT_TRUE(xSemaphoreTake(done_sem_b, pdMS_TO_TICKS(2000))); + + teardown_repl_instance(&start_sem_a, &done_sem_a, socket_fd_a, &linenoise_hdl_a, &repl_hdl_a); + teardown_repl_instance(&start_sem_b, &done_sem_b, socket_fd_b, &linenoise_hdl_b, &repl_hdl_b); } + +#endif // CONFIG_IDF_TARGET_LiNUX diff --git a/esp_repl/test_apps/pytest_esp_repl.py b/esp_repl/test_apps/pytest_esp_repl.py index c4ee9ec096..4d562afbde 100644 --- a/esp_repl/test_apps/pytest_esp_repl.py +++ b/esp_repl/test_apps/pytest_esp_repl.py @@ -1,8 +1,16 @@ import pytest +from pytest_embedded import Dut +from pytest_embedded_idf.utils import idf_parametrize +import glob +from pathlib import Path -@pytest.mark.generic -@pytest.mark.parametrize( - 'target', ['linux'], indirect=['target']) + +@pytest.mark.host_test +@pytest.mark.skipif( + not bool(glob.glob(f'{Path(__file__).parent.absolute()}/build*/')), + reason="Skip the idf version that did not build" +) +@pytest.mark.parametrize('target', ['linux'], indirect=['target']) def test_esp_repl(dut) -> None: dut.run_all_single_board_cases() From c0be11ad0b80c23359b53eddc367b947a0ffd96d Mon Sep 17 00:00:00 2001 From: Guillaume Souchere Date: Mon, 10 Nov 2025 13:55:29 +0100 Subject: [PATCH 7/8] feat(esp_cli): Rename component from esp_repl to esp_cli This component provides the whole chain, REPL, line editing and command handling. Therefore, it makes sense to rename it. --- .github/ISSUE_TEMPLATE/bug-report.yml | 2 +- .github/workflows/upload_component.yml | 2 +- .idf_build_apps.toml | 2 +- {esp_repl => esp_cli}/.build-test-rules.yml | 2 +- {esp_repl => esp_cli}/CMakeLists.txt | 4 +- esp_cli/Kconfig | 10 + {esp_repl => esp_cli}/LICENSE | 0 esp_cli/README.md | 118 +++++++++++ esp_cli/idf_component.yml | 10 + esp_cli/include/esp_cli.h | 191 ++++++++++++++++++ .../sbom_esp_cli.yml | 6 +- .../src/esp_repl.c => esp_cli/src/esp_cli.c | 160 +++++++-------- .../test_apps/CMakeLists.txt | 2 +- .../test_apps/main/CMakeLists.txt | 2 +- .../test_apps/main/idf_component.yml | 2 +- .../test_apps/main/test_esp_cli.c | 148 +++++++------- .../test_apps/main/test_main.c | 2 +- .../test_apps/pytest_esp_cli.py | 2 +- esp_cli/test_apps/sdkconfig.defaults | 2 + esp_repl/Kconfig | 10 - esp_repl/README.md | 118 ----------- esp_repl/idf_component.yml | 14 -- esp_repl/include/esp_repl.h | 191 ------------------ esp_repl/test_apps/sdkconfig.defaults | 2 - 24 files changed, 499 insertions(+), 503 deletions(-) rename {esp_repl => esp_cli}/.build-test-rules.yml (92%) rename {esp_repl => esp_cli}/CMakeLists.txt (67%) create mode 100644 esp_cli/Kconfig rename {esp_repl => esp_cli}/LICENSE (100%) create mode 100644 esp_cli/README.md create mode 100644 esp_cli/idf_component.yml create mode 100644 esp_cli/include/esp_cli.h rename esp_repl/sbom_esp_repl.yml => esp_cli/sbom_esp_cli.yml (67%) rename esp_repl/src/esp_repl.c => esp_cli/src/esp_cli.c (60%) rename {esp_repl => esp_cli}/test_apps/CMakeLists.txt (82%) rename {esp_repl => esp_cli}/test_apps/main/CMakeLists.txt (66%) rename {esp_repl => esp_cli}/test_apps/main/idf_component.yml (72%) rename esp_repl/test_apps/main/test_esp_repl.c => esp_cli/test_apps/main/test_esp_cli.c (60%) rename {esp_repl => esp_cli}/test_apps/main/test_main.c (88%) rename esp_repl/test_apps/pytest_esp_repl.py => esp_cli/test_apps/pytest_esp_cli.py (92%) create mode 100644 esp_cli/test_apps/sdkconfig.defaults delete mode 100644 esp_repl/Kconfig delete mode 100644 esp_repl/README.md delete mode 100644 esp_repl/idf_component.yml delete mode 100644 esp_repl/include/esp_repl.h delete mode 100644 esp_repl/test_apps/sdkconfig.defaults diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index 3d02ac62e0..36385d2d62 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -40,7 +40,7 @@ body: - esp_jpeg - esp_lcd_qemu_rgb - esp_schedule - - esp_repl + - esp_cli - esp_serial_slave_link - expat - fmt diff --git a/.github/workflows/upload_component.yml b/.github/workflows/upload_component.yml index aa4fdcf79b..6fced1f5ad 100644 --- a/.github/workflows/upload_component.yml +++ b/.github/workflows/upload_component.yml @@ -44,7 +44,7 @@ jobs: esp_linenoise esp_jpeg esp_schedule - esp_repl + esp_cli esp_serial_slave_link expat fmt diff --git a/.idf_build_apps.toml b/.idf_build_apps.toml index 97c4c97c49..075173b15d 100644 --- a/.idf_build_apps.toml +++ b/.idf_build_apps.toml @@ -15,7 +15,7 @@ manifest_file = [ "esp_jpeg/.build-test-rules.yml", "esp_linenoise/.build-test-rules.yml", "esp_schedule/.build-test-rules.yml", - "esp_repl/.build-test-rules.yml", + "esp_cli/.build-test-rules.yml", "esp_serial_slave_link/.build-test-rules.yml", "expat/.build-test-rules.yml", "iqmath/.build-test-rules.yml", diff --git a/esp_repl/.build-test-rules.yml b/esp_cli/.build-test-rules.yml similarity index 92% rename from esp_repl/.build-test-rules.yml rename to esp_cli/.build-test-rules.yml index 2edf165073..9a4d987954 100644 --- a/esp_repl/.build-test-rules.yml +++ b/esp_cli/.build-test-rules.yml @@ -1,4 +1,4 @@ -esp_repl/test_apps: +esp_cli/test_apps: enable: - if: IDF_TARGET == "linux" reason: "Sufficient to test on Linux target" diff --git a/esp_repl/CMakeLists.txt b/esp_cli/CMakeLists.txt similarity index 67% rename from esp_repl/CMakeLists.txt rename to esp_cli/CMakeLists.txt index 1ea31c33e3..eabac90883 100644 --- a/esp_repl/CMakeLists.txt +++ b/esp_cli/CMakeLists.txt @@ -1,9 +1,9 @@ idf_build_get_property(target IDF_TARGET) -set(srcs "src/esp_repl.c") +set(srcs "src/esp_cli.c") idf_component_register( SRCS ${srcs} INCLUDE_DIRS include - REQUIRES esp_linenoise esp_commands + REQUIRES esp_linenoise esp_cli_commands WHOLE_ARCHIVE) diff --git a/esp_cli/Kconfig b/esp_cli/Kconfig new file mode 100644 index 0000000000..e8f5223187 --- /dev/null +++ b/esp_cli/Kconfig @@ -0,0 +1,10 @@ +menu "esp_cli configuration" + + config ESP_CLI_HAS_QUIT_CMD + bool "Register quit command" + default n + help + Register a static command "quit" that allows the user to return from the esp_cli main loop. + The command is registered through the ESP_CLI_COMMAND_REGISTER macro provided by esp_cli_commands component + and is placed in the dedicated flash section. +endmenu diff --git a/esp_repl/LICENSE b/esp_cli/LICENSE similarity index 100% rename from esp_repl/LICENSE rename to esp_cli/LICENSE diff --git a/esp_cli/README.md b/esp_cli/README.md new file mode 100644 index 0000000000..e3c4128c21 --- /dev/null +++ b/esp_cli/README.md @@ -0,0 +1,118 @@ +# esp_cli Component + +The `esp_cli` component provides a **Runtime Evaluation Loop (REPL)** mechanism for ESP-IDF-based applications. +It allows developers to build interactive command-line interfaces (CLI) that support user-defined commands, history management, and customizable callbacks for command execution. + +This component integrates with [`esp_linenoise`](../esp_linenoise) for line editing and input handling, and with [`esp_cli_commands`](../esp_cli_commands) for command parsing and execution. + +--- + +## Features + +- Modular REPL management with explicit `start` and `stop` control +- Integration with [`esp_linenoise`](../esp_linenoise) for input and history +- Support for command sets through [`esp_cli_commands`](../esp_cli_commands) +- Configurable callbacks for: + - Pre-execution processing + - Post-execution handling + - On-stop and on-exit events +- Thread-safe operation using FreeRTOS semaphores +- Optional command history persistence to filesystem + +--- + +## Usage + +A typical use case involves: + +1. Initializing `esp_linenoise` and `esp_cli_commands` +2. Creating the esp_cli instance with `esp_cli_create()` +3. Running `esp_cli()` in a task +4. Starting and stopping the esp_cli using `esp_cli_start()` and `esp_cli_stop()` +5. Destroying the instance with `esp_cli_destroy()` when done + +### Example + +```c +#include "esp_cli.h" +#include "esp_linenoise.h" +#include "esp_cli_commands.h" +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +static const char *TAG = "repl_example"; + +void cli_task(void *arg) +{ + esp_cli_handle_t repl_hdl = (esp_cli_handle_t)arg; + + // Run REPL loop (blocking until esp_cli_stop() is called) + // The loop won't be reached until esp_cli_start() is called + esp_cli(repl_hdl); + + ESP_LOGI(TAG, "esp_cli instance task exiting"); + vTaskDelete(NULL); +} + +void app_main(void) +{ + esp_err_t ret; + esp_cli_handle_t cli = NULL; + + // Initialize esp_linenoise (mandatory) + esp_linenoise_handle_t esp_linenoise_hdl = esp_linenoise_create(); + + // Initialize command set (optional) + esp_cli_command_set_handle_t esp_cli_commands_cmd_set = esp_cli_commands_create(); + + esp_cli_config_t cli_cfg = { + .linenoise_handle = esp_linenoise_hdl, + .command_set_handle = esp_cli_commands_cmd_set, /* optional */ + .max_cmd_line_size = 256, + .history_save_path = "/spiffs/cli_history.txt", /* optional */ + }; + + ret = esp_cli_create(&cli_cfg, &cli); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Failed to create esp_cli instance (%s)", esp_err_to_name(ret)); + return; + } + + // Create esp_cli instance task + if (xTaskCreate(cli_task, "cli_task", 4096, cli, 5, NULL) != pdPASS) { + ESP_LOGE(TAG, "Failed to create esp_cli instance task"); + esp_cli_destroy(cli); + return; + } + + ESP_LOGI(TAG, "Starting esp_cli..."); + ret = esp_cli_start(cli); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Failed to start esp_cli (%s)", esp_err_to_name(ret)); + esp_cli_destroy(cli); + return; + } + + // Application logic can run in parallel while esp_cli instance runs in its own task + // [...] + vTaskDelay(pdMS_TO_TICKS(10000)); // Example delay + + // Stop esp_cli instance + ret = esp_cli_stop(cli); + if (ret != ESP_OK) { + ESP_LOGW(TAG, "Failed to stop esp_cli (%s)", esp_err_to_name(ret)); + } + + ESP_LOGI(TAG, "esp_cli exited"); + + // Destroy esp_cli instance and clean up + ret = esp_cli_destroy(cli); + if (ret != ESP_OK) { + ESP_LOGW(TAG, "Failed to destroy esp_cli instance cleanly (%s)", esp_err_to_name(ret)); + } + + ESP_LOGI(TAG, "esp_cli example finished"); +} + +``` diff --git a/esp_cli/idf_component.yml b/esp_cli/idf_component.yml new file mode 100644 index 0000000000..291bf09f87 --- /dev/null +++ b/esp_cli/idf_component.yml @@ -0,0 +1,10 @@ +version: "0.1.0" +description: "esp_cli - Read Eval Print Loop component" +url: https://github.com/espressif/idf-extra-components/tree/master/esp_cli +dependencies: + espressif/esp_linenoise: "*" + espressif/esp_cli_commands: "*" +sbom: + manifests: + - path: sbom_esp_cli.yml + dest: . \ No newline at end of file diff --git a/esp_cli/include/esp_cli.h b/esp_cli/include/esp_cli.h new file mode 100644 index 0000000000..700f05c1bd --- /dev/null +++ b/esp_cli/include/esp_cli.h @@ -0,0 +1,191 @@ +/* + * 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" +#include "esp_linenoise.h" +#include "esp_cli_commands.h" + +/** + * @brief Handle to a esp_cli instance. + */ +typedef struct esp_cli_instance *esp_cli_handle_t; + +/** + * @brief Function prototype called at the beginning of esp_cli(). + * + * @param ctx User-defined context pointer. + * @param handle Handle to the esp_cli instance. + */ +typedef void (*esp_cli_on_enter_fn)(void *ctx, esp_cli_handle_t handle); + +/** + * @brief Enter callback configuration structure for the esp_cli. + */ +typedef struct esp_cli_on_enter { + esp_cli_on_enter_fn func; /**!< Function called at the beginning of esp_cli() */ + void *ctx; /**!< Context passed to the enter function */ +} esp_cli_on_enter_t; + +/** + * @brief Function prototype called before executing a command. + * + * @param ctx User-defined context pointer. + * @param buf Buffer containing the command. + * @param reader_ret_val Return value from the reader function. + * + * @return ESP_OK to continue execution, error code to abort. + */ +typedef esp_err_t (*esp_cli_pre_executor_fn)(void *ctx, const char *buf, esp_err_t reader_ret_val); + +/** + * @brief Pre-executor configuration structure for the esp_cli. + */ +typedef struct esp_cli_pre_executor { + esp_cli_pre_executor_fn func; /**!< Function to run before command execution */ + void *ctx; /**!< Context passed to the pre-executor function */ +} esp_cli_pre_executor_t; + +/** + * @brief Function prototype called after executing a command. + * + * @param ctx User-defined context pointer. + * @param buf Command that was executed. + * @param executor_ret_val Return value from the executor function. + * @param cmd_ret_val Command-specific return value. + * + * @return ESP_OK on success, error code otherwise. + */ +typedef esp_err_t (*esp_cli_post_executor_fn)(void *ctx, const char *buf, esp_err_t executor_ret_val, int cmd_ret_val); + +/** + * @brief Post-executor configuration structure for the esp_cli. + */ +typedef struct esp_cli_post_executor { + esp_cli_post_executor_fn func; /**!< Function called after command execution */ + void *ctx; /**!< Context passed to the post-executor function */ +} esp_cli_post_executor_t; + +/** + * @brief Function prototype called when the esp_cli is stopping. + * + * This callback allows the user to unblock the reader (or perform other + * cleanup) so that the esp_cli can return from `esp_cli()`. + * + * @param ctx User-defined context pointer. + * @param handle Handle to the esp_cli instance. + */ +typedef void (*esp_cli_on_stop_fn)(void *ctx, esp_cli_handle_t handle); + +/** + * @brief Stop callback configuration structure for the esp_cli. + */ +typedef struct esp_cli_on_stop { + esp_cli_on_stop_fn func; /**!< Function called when esp_cli stop is requested */ + void *ctx; /**!< Context passed to the on_stop function */ +} esp_cli_on_stop_t; + +/** + * @brief Function prototype called when the esp_cli exits. + * + * @param ctx User-defined context pointer. + * @param handle Handle to the esp_cli instance. + */ +typedef void (*esp_cli_on_exit_fn)(void *ctx, esp_cli_handle_t handle); + +/** + * @brief Exit callback configuration structure for the esp_cli. + */ +typedef struct esp_cli_on_exit { + esp_cli_on_exit_fn func; /**!< Function called on esp_cli exit */ + void *ctx; /**!< Context passed to the exit function */ +} esp_cli_on_exit_t; + +/** + * @brief Configuration structure to initialize a esp_cli instance. + */ +typedef struct esp_cli_config { + esp_linenoise_handle_t linenoise_handle; /**!< Handle to the esp_linenoise instance */ + esp_cli_command_set_handle_t command_set_handle; /**!< Handle to a set of commands */ + size_t max_cmd_line_size; /**!< Maximum allowed command line size */ + const char *history_save_path; /**!< Path to file to save the history */ + esp_cli_on_enter_t on_enter; /**!< Enter callback and context */ + esp_cli_pre_executor_t pre_executor; /**!< Pre-executor callback and context */ + esp_cli_post_executor_t post_executor; /**!< Post-executor callback and context */ + esp_cli_on_stop_t on_stop; /**!< Stop callback and context */ + esp_cli_on_exit_t on_exit; /**!< Exit callback and context */ +} esp_cli_config_t; + +/** + * @brief Create a esp_cli instance. + * + * @param config Pointer to the configuration structure. + * @param out_handle Pointer to store the created esp_cli instance handle. + * + * @return ESP_OK on success, error code otherwise. + */ +esp_err_t esp_cli_create(const esp_cli_config_t *config, esp_cli_handle_t *out_handle); + +/** + * @brief Destroy a esp_cli instance. + * + * @param handle esp_cli instance handle to destroy. + * + * @return ESP_OK on success, error code otherwise. + */ +esp_err_t esp_cli_destroy(esp_cli_handle_t handle); + +/** + * @brief Start a esp_cli instance. + * + * @param handle esp_cli instance handle. + * + * @return ESP_OK on success, error code otherwise. + */ +esp_err_t esp_cli_start(esp_cli_handle_t handle); + +/** + * @brief Stop a esp_cli instance. + * + * @note This function will internally call 'esp_linenoise_abort' first to try to return from + * 'esp_linenoise_get_line'. If the user has provided a custom read to the esp_linenoise + * instance used by the esp_cli instance, it is the responsibility of the user to provide + * the mechanism to return from this custom read by providing a callback to the 'on_stop' field + * in the esp_cli_config_t. + * + * Return Values: + * - ESP_OK: Returned if the user has not provided a custom read and the abort operation succeeds. + * - ESP_ERR_INVALID_STATE: Returned if the user has provided a custom read. In this case, the user + * is responsible for implementing an abort mechanism that ensures a successful return from + * their custom read. This can be achieved by placing the logic in the on_stop callback. + * + * Behavior: + * - When a custom read is registered, ESP_ERR_INVALID_STATE indicates that esp_cli_stop() cannot + * forcibly return from the read. The user must handle the return themselves via on_stop(). + * - From the perspective of esp_cli_stop(), this scenario is treated as successful, and its + * return value should be set to ESP_OK. + * + * @param handle esp_cli instance handle. + * + * @return ESP_OK on success, error code otherwise. + */ +esp_err_t esp_cli_stop(esp_cli_handle_t handle); + +/** + * @brief Run the esp_cli loop. + * + * @param handle esp_cli instance handle. + */ +void esp_cli(esp_cli_handle_t handle); + +#ifdef __cplusplus +} +#endif diff --git a/esp_repl/sbom_esp_repl.yml b/esp_cli/sbom_esp_cli.yml similarity index 67% rename from esp_repl/sbom_esp_repl.yml rename to esp_cli/sbom_esp_cli.yml index 4c5714b8f4..1aa7d7af84 100644 --- a/esp_repl/sbom_esp_repl.yml +++ b/esp_cli/sbom_esp_cli.yml @@ -1,6 +1,6 @@ -name: esp_repl +name: esp_cli description: Command handling component -url: https://github.com/espressif/idf-extra-components/tree/master/esp_repl +url: https://github.com/espressif/idf-extra-components/tree/master/esp_cli version: 1.0.0 -cpe: cpe:2.3:a:espressif:esp_repl:{}:*:*:*:*:*:*:* +cpe: cpe:2.3:a:espressif:esp_cli:{}:*:*:*:*:*:*:* supplier: 'Organization: Espressif Systems' \ No newline at end of file diff --git a/esp_repl/src/esp_repl.c b/esp_cli/src/esp_cli.c similarity index 60% rename from esp_repl/src/esp_repl.c rename to esp_cli/src/esp_cli.c index aa2905aabd..224869877d 100644 --- a/esp_repl/src/esp_repl.c +++ b/esp_cli/src/esp_cli.c @@ -9,58 +9,58 @@ #include "freertos/FreeRTOS.h" #include "freertos/semphr.h" #include "freertos/task.h" -#include "esp_repl.h" +#include "esp_cli.h" #include "esp_err.h" -#include "esp_commands.h" +#include "esp_cli_commands.h" #include "esp_linenoise.h" typedef enum { - ESP_REPL_STATE_RUNNING, - ESP_REPL_STATE_STOPPED -} esp_repl_state_e; + ESP_CLI_STATE_RUNNING, + ESP_CLI_STATE_STOPPED +} esp_cli_state_e; -typedef struct esp_repl_state { - esp_repl_state_e state; +typedef struct esp_cli_state { + esp_cli_state_e state; TaskHandle_t task_hdl; SemaphoreHandle_t mux; -} esp_repl_state_t; +} esp_cli_state_t; -typedef struct esp_repl_instance { - esp_repl_config_t config; - esp_repl_state_t state; -} esp_repl_instance_t; +typedef struct esp_cli_instance { + esp_cli_config_t config; + esp_cli_state_t state; +} esp_cli_instance_t; -#if CONFIG_ESP_REPL_HAS_QUIT_CMD -#define _ESP_REPL_STRINGIFY(x) #x -#define ESP_REPL_STRINGIFY(x) _ESP_REPL_STRINGIFY(x) +#if CONFIG_ESP_CLI_HAS_QUIT_CMD +#define _ESP_CLI_STRINGIFY(x) #x +#define ESP_CLI_STRINGIFY(x) _ESP_CLI_STRINGIFY(x) -#define ESP_REPL_QUIT_CMD quit -#define ESP_REPL_QUIT_CMD_STR ESP_REPL_STRINGIFY(ESP_REPL_QUIT_CMD) -#define ESP_REPL_QUIT_CMD_SIZE strlen(ESP_REPL_QUIT_CMD_STR) +#define ESP_CLI_QUIT_CMD quit +#define ESP_CLI_QUIT_CMD_STR ESP_CLI_STRINGIFY(ESP_CLI_QUIT_CMD) +#define ESP_CLI_QUIT_CMD_SIZE strlen(ESP_CLI_QUIT_CMD_STR) /* dummy command function callback to allow the registration for the quit command * to succeed */ -static int esp_repl_quit_cmd(void *context, esp_commands_exec_arg_t *cmd_args, int argc, char **argv) +static int esp_cli_quit_cmd(void *context, esp_cli_commands_exec_arg_t *cmd_args, int argc, char **argv) { return 0; } -ESP_COMMAND_REGISTER(ESP_REPL_QUIT_CMD, - "esp_repl", - "This command will trigger the exit mechanism to exit from esp_repl() main loop", - esp_repl_quit_cmd, - NULL, - NULL, - NULL - ); -#endif // CONFIG_ESP_REPL_HAS_QUIT_CMD - -#define ESP_REPL_CHECK_INSTANCE(handle) do { \ +ESP_CLI_COMMAND_REGISTER(ESP_CLI_QUIT_CMD, + "esp_cli", + "This command will trigger the exit mechanism to exit from esp_cli() main loop", + esp_cli_quit_cmd, + NULL, + NULL, + NULL + ); +#endif // CONFIG_ESP_CLI_HAS_QUIT_CMD + +#define ESP_CLI_CHECK_INSTANCE(handle) do { \ if(handle == NULL) { \ return ESP_ERR_INVALID_ARG; \ } \ } while(0) -esp_err_t esp_repl_create(const esp_repl_config_t *config, esp_repl_handle_t *out_handle) +esp_err_t esp_cli_create(const esp_cli_config_t *config, esp_cli_handle_t *out_handle) { if (!config || !out_handle) { return ESP_ERR_INVALID_ARG; @@ -71,13 +71,13 @@ esp_err_t esp_repl_create(const esp_repl_config_t *config, esp_repl_handle_t *ou return ESP_ERR_INVALID_ARG; } - esp_repl_instance_t *instance = malloc(sizeof(esp_repl_instance_t)); + esp_cli_instance_t *instance = malloc(sizeof(esp_cli_instance_t)); if (!instance) { return ESP_ERR_NO_MEM; } instance->config = *config; - instance->state.state = ESP_REPL_STATE_STOPPED; + instance->state.state = ESP_CLI_STATE_STOPPED; instance->state.mux = xSemaphoreCreateMutex(); if (!instance->state.mux) { free(instance); @@ -85,20 +85,20 @@ esp_err_t esp_repl_create(const esp_repl_config_t *config, esp_repl_handle_t *ou } /* take the mutex right away to prevent the task to start running until - * the user explicitly calls esp_repl_start */ + * the user explicitly calls esp_cli_start */ xSemaphoreTake(instance->state.mux, portMAX_DELAY); *out_handle = instance; return ESP_OK; } -esp_err_t esp_repl_destroy(esp_repl_handle_t handle) +esp_err_t esp_cli_destroy(esp_cli_handle_t handle) { - ESP_REPL_CHECK_INSTANCE(handle); - esp_repl_state_t *state = &handle->state; + ESP_CLI_CHECK_INSTANCE(handle); + esp_cli_state_t *state = &handle->state; - /* the instance has to be not running for esp_repl to destroy it */ - if (state->state != ESP_REPL_STATE_STOPPED) { + /* the instance has to be not running for esp_cli to destroy it */ + if (state->state != ESP_CLI_STATE_STOPPED) { return ESP_ERR_INVALID_STATE; } @@ -109,32 +109,32 @@ esp_err_t esp_repl_destroy(esp_repl_handle_t handle) return ESP_OK; } -esp_err_t esp_repl_start(esp_repl_handle_t handle) +esp_err_t esp_cli_start(esp_cli_handle_t handle) { - ESP_REPL_CHECK_INSTANCE(handle); - esp_repl_state_t *state = &handle->state; + ESP_CLI_CHECK_INSTANCE(handle); + esp_cli_state_t *state = &handle->state; - if (state->state != ESP_REPL_STATE_STOPPED) { + if (state->state != ESP_CLI_STATE_STOPPED) { return ESP_ERR_INVALID_STATE; } - state->state = ESP_REPL_STATE_RUNNING; + state->state = ESP_CLI_STATE_RUNNING; xSemaphoreGive(state->mux); return ESP_OK; } -esp_err_t esp_repl_stop(esp_repl_handle_t handle) +esp_err_t esp_cli_stop(esp_cli_handle_t handle) { - ESP_REPL_CHECK_INSTANCE(handle); - esp_repl_config_t *config = &handle->config; - esp_repl_state_t *state = &handle->state; + ESP_CLI_CHECK_INSTANCE(handle); + esp_cli_config_t *config = &handle->config; + esp_cli_state_t *state = &handle->state; - if (state->state != ESP_REPL_STATE_RUNNING) { + if (state->state != ESP_CLI_STATE_RUNNING) { return ESP_ERR_INVALID_STATE; } - /* update the state to force the while loop in esp_repl to return */ - state->state = ESP_REPL_STATE_STOPPED; + /* update the state to force the while loop in esp_cli to return */ + state->state = ESP_CLI_STATE_STOPPED; /** This function forces esp_linenoise_get_line() to return. * @@ -145,9 +145,9 @@ esp_err_t esp_repl_stop(esp_repl_handle_t handle) * their custom read. This can be achieved by placing the logic in the on_stop callback. * * Behavior: - * - When a custom read is registered, ESP_ERR_INVALID_STATE indicates that esp_repl_stop() cannot + * - When a custom read is registered, ESP_ERR_INVALID_STATE indicates that esp_cli_stop() cannot * forcibly return from the read. The user must handle the return of their custom read via on_stop(). - * - From the perspective of esp_repl_stop(), this scenario is treated as successful, and its + * - From the perspective of esp_cli_stop(), this scenario is treated as successful, and its * return value should be set to ESP_OK. */ esp_err_t ret_val = esp_linenoise_abort(config->linenoise_handle); @@ -161,12 +161,12 @@ esp_err_t esp_repl_stop(esp_repl_handle_t handle) config->on_stop.func(config->on_stop.ctx, handle); } - /* Wait for esp_repl() to finish and signal completion, in the event of - * esp_repl_stop() is called from the same task running esp_repl() (e.g., + /* Wait for esp_cli() to finish and signal completion, in the event of + * esp_cli_stop() is called from the same task running esp_cli() (e.g., * called from a "quit" command), do not take the mutex to avoid a deadlock. * - * If esp_repl_stop() is called from the same task, it assures that this task - * is not blocking in esp_linenoise_get_line() so the while loop in esp_repl() + * If esp_cli_stop() is called from the same task, it assures that this task + * is not blocking in esp_linenoise_get_line() so the while loop in esp_cli() * will return as we updated the state above */ if (state->task_hdl && state->task_hdl != xTaskGetCurrentTaskHandle()) { xSemaphoreTake(state->mux, portMAX_DELAY); @@ -175,25 +175,25 @@ esp_err_t esp_repl_stop(esp_repl_handle_t handle) return ret_val; } -void esp_repl(esp_repl_handle_t handle) +void esp_cli(esp_cli_handle_t handle) { if (!handle) { return; } - esp_repl_config_t *config = &handle->config; - esp_repl_state_t *state = &handle->state; + esp_cli_config_t *config = &handle->config; + esp_cli_state_t *state = &handle->state; /* trigger a user defined callback before the function gets into the while loop * if the user wants to perform some logic that needs to be done within the task - * running the REPL */ + * running the esp_cli instance */ if (config->on_enter.func != NULL) { config->on_enter.func(config->on_enter.ctx, handle); } /* get the task handle of the task running this function. - * It is necessary to gather this information in case esp_repl_stop() - * is called from the same task as the one running esp_repl() (e.g., + * It is necessary to gather this information in case esp_cli_stop() + * is called from the same task as the one running esp_cli() (e.g., * through the execution of a command) */ state->task_hdl = xTaskGetCurrentTaskHandle(); @@ -204,15 +204,15 @@ void esp_repl(esp_repl_handle_t handle) return; } - /* Waiting for task notify. This happens when `esp_repl_start` + /* Waiting for task notify. This happens when `esp_cli_start` * function is called. */ xSemaphoreTake(state->mux, portMAX_DELAY); esp_linenoise_handle_t l_hdl = config->linenoise_handle; - esp_command_set_handle_t c_set = config->command_set_handle; + esp_cli_command_set_handle_t c_set = config->command_set_handle; - /* REPL loop */ - while (state->state == ESP_REPL_STATE_RUNNING) { + /* esp_cli REPL loop */ + while (state->state == ESP_CLI_STATE_RUNNING) { /* try to read a command line */ const esp_err_t read_ret = esp_linenoise_get_line(l_hdl, cmd_line, cmd_line_size); @@ -236,25 +236,25 @@ void esp_repl(esp_repl_handle_t handle) continue; } -#if CONFIG_ESP_REPL_HAS_QUIT_CMD +#if CONFIG_ESP_CLI_HAS_QUIT_CMD /* evaluate the command name. make sure that the first argument of the cmd_line - * is ESP_REPL_QUIT_CMD_STR and the character following that is either a space + * is ESP_CLI_QUIT_CMD_STR and the character following that is either a space * or a null character */ - if ((strncmp(ESP_REPL_QUIT_CMD_STR, cmd_line, ESP_REPL_QUIT_CMD_SIZE) == 0) && - ((cmd_line[ESP_REPL_QUIT_CMD_SIZE] == ' ') || (cmd_line[ESP_REPL_QUIT_CMD_SIZE] == '\0'))) { - /* quit command received, call esp_repl_stop() */ - if (esp_repl_stop(handle) == ESP_OK) { - /* if esp_repl_stop() was successful, retry the while condition. - * the esp_repl state should have been changed which will force + if ((strncmp(ESP_CLI_QUIT_CMD_STR, cmd_line, ESP_CLI_QUIT_CMD_SIZE) == 0) && + ((cmd_line[ESP_CLI_QUIT_CMD_SIZE] == ' ') || (cmd_line[ESP_CLI_QUIT_CMD_SIZE] == '\0'))) { + /* quit command received, call esp_cli_stop() */ + if (esp_cli_stop(handle) == ESP_OK) { + /* if esp_cli_stop() was successful, retry the while condition. + * the esp_cli state should have been changed which will force * the while to break */ continue; } } -#endif // CONFIG_ESP_REPL_HAS_QUIT_CMD +#endif // CONFIG_ESP_CLI_HAS_QUIT_CMD /* try to run the command */ int cmd_func_ret; - esp_commands_exec_arg_t cmd_args; + esp_cli_commands_exec_arg_t cmd_args; esp_err_t get_ret = esp_linenoise_get_out_fd(handle->config.linenoise_handle, &(cmd_args.out_fd)); if (get_ret != ESP_OK) { cmd_args.out_fd = STDOUT_FILENO; @@ -264,7 +264,7 @@ void esp_repl(esp_repl_handle_t handle) cmd_args.write_func = write; } - const esp_err_t exec_ret = esp_commands_execute(cmd_line, &cmd_func_ret, c_set, &cmd_args); + const esp_err_t exec_ret = esp_cli_commands_execute(cmd_line, &cmd_func_ret, c_set, &cmd_args); /* forward the raw command line to the post executor callback (e.g., save in history). * this callback is not necessary for the user to register, continue if it isn't */ @@ -279,10 +279,10 @@ void esp_repl(esp_repl_handle_t handle) /* free the memory allocated for the cmd_line buffer */ free(cmd_line); - /* release the semaphore to indicate esp_repl_stop that the esp_repl returned */ + /* release the semaphore to indicate esp_cli_stop that the esp_cli returned */ xSemaphoreGive(state->mux); - /* call the on_exit callback before returning from esp_repl */ + /* call the on_exit callback before returning from esp_cli */ if (config->on_exit.func != NULL) { config->on_exit.func(config->on_exit.ctx, handle); } diff --git a/esp_repl/test_apps/CMakeLists.txt b/esp_cli/test_apps/CMakeLists.txt similarity index 82% rename from esp_repl/test_apps/CMakeLists.txt rename to esp_cli/test_apps/CMakeLists.txt index 84918a9f3e..1ea6aec55b 100644 --- a/esp_repl/test_apps/CMakeLists.txt +++ b/esp_cli/test_apps/CMakeLists.txt @@ -2,4 +2,4 @@ cmake_minimum_required(VERSION 3.16) include($ENV{IDF_PATH}/tools/cmake/project.cmake) set(COMPONENTS main) -project(esp_repl_test) +project(esp_cli_test) diff --git a/esp_repl/test_apps/main/CMakeLists.txt b/esp_cli/test_apps/main/CMakeLists.txt similarity index 66% rename from esp_repl/test_apps/main/CMakeLists.txt rename to esp_cli/test_apps/main/CMakeLists.txt index faaab0e887..f02fe0b18c 100644 --- a/esp_repl/test_apps/main/CMakeLists.txt +++ b/esp_cli/test_apps/main/CMakeLists.txt @@ -1,5 +1,5 @@ -idf_component_register(SRCS "test_esp_repl.c" "test_main.c" +idf_component_register(SRCS "test_esp_cli.c" "test_main.c" PRIV_INCLUDE_DIRS "." PRIV_REQUIRES unity WHOLE_ARCHIVE) diff --git a/esp_repl/test_apps/main/idf_component.yml b/esp_cli/test_apps/main/idf_component.yml similarity index 72% rename from esp_repl/test_apps/main/idf_component.yml rename to esp_cli/test_apps/main/idf_component.yml index b666695051..3175bae677 100644 --- a/esp_repl/test_apps/main/idf_component.yml +++ b/esp_cli/test_apps/main/idf_component.yml @@ -1,4 +1,4 @@ dependencies: - espressif/esp_repl: + espressif/esp_cli: version: "*" override_path: "../.." diff --git a/esp_repl/test_apps/main/test_esp_repl.c b/esp_cli/test_apps/main/test_esp_cli.c similarity index 60% rename from esp_repl/test_apps/main/test_esp_repl.c rename to esp_cli/test_apps/main/test_esp_cli.c index 4d63bfc0bc..d7792d7441 100644 --- a/esp_repl/test_apps/main/test_esp_repl.c +++ b/esp_cli/test_apps/main/test_esp_cli.c @@ -12,9 +12,9 @@ #include "freertos/task.h" #include "freertos/semphr.h" #include "unity.h" -#include "esp_repl.h" +#include "esp_cli.h" #include "esp_linenoise.h" -#include "esp_commands.h" +#include "esp_cli_commands.h" #if CONFIG_IDF_TARGET_LINUX #include @@ -40,7 +40,7 @@ static size_t s_post_executor_nb_of_calls = 0; static size_t s_on_stop_nb_of_calls = 0; static size_t s_on_exit_nb_of_calls = 0; -void test_on_enter(void *ctx, esp_repl_handle_t handle) +void test_on_enter(void *ctx, esp_cli_handle_t handle) { s_on_enter_nb_of_calls++; return; @@ -58,37 +58,37 @@ esp_err_t test_post_executor(void *ctx, const char *buf, esp_err_t executor_ret_ return ESP_OK; } -void test_on_stop(void *ctx, esp_repl_handle_t handle) +void test_on_stop(void *ctx, esp_cli_handle_t handle) { s_on_stop_nb_of_calls++; return; } -void test_on_exit(void *ctx, esp_repl_handle_t handle) +void test_on_exit(void *ctx, esp_cli_handle_t handle) { s_on_exit_nb_of_calls++; return; } /* Pass two semaphores: - * - start_sem: child gives it when it reached esp_repl (so main knows child started) + * - start_sem: child gives it when it reached esp_cli() (so main knows child started) * - done_sem: child gives it just before deleting itself (so main can "join") */ typedef struct task_args { SemaphoreHandle_t start_sem; SemaphoreHandle_t done_sem; - esp_repl_handle_t hdl; + esp_cli_handle_t hdl; } task_args_t; -static void repl_task(void *args) +static void esp_cli_task(void *args) { task_args_t *task_args = (task_args_t *)args; - /* signal to main that task started and esp_repl will run */ + /* signal to main that task started and esp_cli() will run */ xSemaphoreGive(task_args->start_sem); - /* run the REPL loop (will return when stopped) */ - esp_repl(task_args->hdl); + /* run the esp_cli REPL loop (will return when stopped) */ + esp_cli(task_args->hdl); /* signal completion (emulates pthread_join notification) */ xSemaphoreGive(task_args->done_sem); @@ -129,11 +129,11 @@ static void test_send_characters(int socket_fd, const char *msg) TEST_ASSERT_EQUAL(msg_len, nwrite); } -static void teardown_repl_instance(SemaphoreHandle_t *start_sem, SemaphoreHandle_t *done_sem, int socket_fd[2], - esp_linenoise_handle_t *linenoise_hdl, esp_repl_handle_t *repl_hdl) +static void esp_cli_teardown(SemaphoreHandle_t *start_sem, SemaphoreHandle_t *done_sem, int socket_fd[2], + esp_linenoise_handle_t *linenoise_hdl, esp_cli_handle_t *cli_hdl) { - /* destroy the instance of resp_repl */ - TEST_ASSERT_EQUAL(ESP_OK, esp_repl_destroy(*repl_hdl)); + /* destroy the instance of esp_cli */ + TEST_ASSERT_EQUAL(ESP_OK, esp_cli_destroy(*cli_hdl)); /* delete the linenoise instance */ TEST_ASSERT_EQUAL(ESP_OK, esp_linenoise_delete_instance(*linenoise_hdl)); @@ -152,8 +152,8 @@ static void teardown_repl_instance(SemaphoreHandle_t *start_sem, SemaphoreHandle s_post_executor_nb_of_calls = 0; } -static void setup_repl_instance(SemaphoreHandle_t *start_sem, SemaphoreHandle_t *done_sem, int socket_fd[2], - esp_linenoise_handle_t *linenoise_hdl, esp_repl_handle_t *repl_hdl) +static void esp_cli_setup(SemaphoreHandle_t *start_sem, SemaphoreHandle_t *done_sem, int socket_fd[2], + esp_linenoise_handle_t *linenoise_hdl, esp_cli_handle_t *cli_hdl) { /* create semaphores */ *start_sem = xSemaphoreCreateBinary(); @@ -177,7 +177,7 @@ static void setup_repl_instance(SemaphoreHandle_t *start_sem, SemaphoreHandle_t TEST_ASSERT_EQUAL(ESP_OK, esp_linenoise_create_instance(&linenoise_config, linenoise_hdl)); TEST_ASSERT_NOT_NULL(*linenoise_hdl); - esp_repl_config_t repl_config = { + esp_cli_config_t cli_config = { .linenoise_handle = *linenoise_hdl, .command_set_handle = NULL, .max_cmd_line_size = 256, @@ -189,8 +189,8 @@ static void setup_repl_instance(SemaphoreHandle_t *start_sem, SemaphoreHandle_t .on_exit = { .func = test_on_exit, .ctx = NULL } }; - TEST_ASSERT_EQUAL(ESP_OK, esp_repl_create(&repl_config, repl_hdl)); - TEST_ASSERT_NOT_NULL(*repl_hdl); + TEST_ASSERT_EQUAL(ESP_OK, esp_cli_create(&cli_config, cli_hdl)); + TEST_ASSERT_NOT_NULL(*cli_hdl); s_on_stop_nb_of_calls = 0; s_on_exit_nb_of_calls = 0; @@ -199,46 +199,46 @@ static void setup_repl_instance(SemaphoreHandle_t *start_sem, SemaphoreHandle_t s_post_executor_nb_of_calls = 0; } -TEST_CASE("esp_repl() loop calls all callbacks and exit on call to esp_repl_stop", "[esp_repl][host_test]") +TEST_CASE("esp_cli() loop calls all callbacks and exit on call to esp_cli_stop", "[esp_cli][host_test]") { SemaphoreHandle_t start_sem, done_sem; esp_linenoise_handle_t linenoise_hdl; - esp_repl_handle_t repl_hdl; - setup_repl_instance(&start_sem, &done_sem, s_socket_fd, &linenoise_hdl, &repl_hdl); + esp_cli_handle_t cli_hdl; + esp_cli_setup(&start_sem, &done_sem, s_socket_fd, &linenoise_hdl, &cli_hdl); - /* create the repl task */ - task_args_t args = {.start_sem = start_sem, .done_sem = done_sem, .hdl = repl_hdl}; - BaseType_t rc = xTaskCreate(repl_task, "repl_task", 4096, &args, 5, NULL); + /* create the esp_cli instance task */ + task_args_t args = {.start_sem = start_sem, .done_sem = done_sem, .hdl = cli_hdl}; + BaseType_t rc = xTaskCreate(esp_cli_task, "esp_cli_task", 4096, &args, 5, NULL); TEST_ASSERT_EQUAL(pdPASS, rc); - /* should fail before repl is started */ - TEST_ASSERT_NOT_EQUAL(ESP_OK, esp_repl_stop(repl_hdl)); + /* should fail before esp_cli instance is started */ + TEST_ASSERT_NOT_EQUAL(ESP_OK, esp_cli_stop(cli_hdl)); - /* start repl */ - TEST_ASSERT_NOT_EQUAL(ESP_OK, esp_repl_start(NULL)); - TEST_ASSERT_EQUAL(ESP_OK, esp_repl_start(repl_hdl)); + /* start esp_cli instance */ + TEST_ASSERT_NOT_EQUAL(ESP_OK, esp_cli_start(NULL)); + TEST_ASSERT_EQUAL(ESP_OK, esp_cli_start(cli_hdl)); wait_ms(100); - /* wait for the repl task to signal it started */ + /* wait for the esp_cli task to signal it started */ TEST_ASSERT_TRUE(xSemaphoreTake(start_sem, pdMS_TO_TICKS(2000))); /* send a dummy string new line terminated to trigger linenoise to return */ const char *input_line = "dummy_message\n"; test_send_characters(s_socket_fd[1], input_line); - /* wait for a bit so esp_repl() has time to loop back into esp_linenoise_get_line */ + /* wait for a bit so esp_cli() has time to loop back into esp_linenoise_get_line */ wait_ms(500); /* check that pre-executor, post-executor callbacks are called */ TEST_ASSERT_EQUAL(1, s_pre_executor_nb_of_calls); TEST_ASSERT_EQUAL(1, s_post_executor_nb_of_calls); - /* stop repl and wait for task to finish (emulate pthread_join) */ - TEST_ASSERT_NOT_EQUAL(ESP_OK, esp_repl_stop(NULL)); - TEST_ASSERT_EQUAL(ESP_OK, esp_repl_stop(repl_hdl)); + /* stop esp_cli and wait for task to finish (emulate pthread_join) */ + TEST_ASSERT_NOT_EQUAL(ESP_OK, esp_cli_stop(NULL)); + TEST_ASSERT_EQUAL(ESP_OK, esp_cli_stop(cli_hdl)); - /* wait for the repl task to signal completion */ + /* wait for the esp_cli task to signal completion */ TEST_ASSERT_TRUE(xSemaphoreTake(done_sem, pdMS_TO_TICKS(2000))); /* check that all callbacks were called the right number of times */ @@ -248,90 +248,90 @@ TEST_CASE("esp_repl() loop calls all callbacks and exit on call to esp_repl_stop TEST_ASSERT_EQUAL(2, s_pre_executor_nb_of_calls); TEST_ASSERT_EQUAL(2, s_post_executor_nb_of_calls); - /* make sure calling stop fails because the repl is no longer running */ - TEST_ASSERT_NOT_EQUAL(ESP_OK, esp_repl_stop(repl_hdl)); + /* make sure calling stop fails because the esp_cli instance is no longer running */ + TEST_ASSERT_NOT_EQUAL(ESP_OK, esp_cli_stop(cli_hdl)); - /* destroy the repl instance */ - TEST_ASSERT_NOT_EQUAL(ESP_OK, esp_repl_destroy(NULL)); - teardown_repl_instance(&start_sem, &done_sem, s_socket_fd, &linenoise_hdl, &repl_hdl); + /* destroy the esp_cli instance */ + TEST_ASSERT_NOT_EQUAL(ESP_OK, esp_cli_destroy(NULL)); + esp_cli_teardown(&start_sem, &done_sem, s_socket_fd, &linenoise_hdl, &cli_hdl); } -TEST_CASE("esp_repl() exits when esp_repl_stop() called from the task running esp_repl()", "[esp_repl][host_test]") +TEST_CASE("esp_cli() exits when esp_cli_stop() called from the task running esp_cli()", "[esp_cli][host_test]") { SemaphoreHandle_t start_sem, done_sem; esp_linenoise_handle_t linenoise_hdl; - esp_repl_handle_t repl_hdl; - setup_repl_instance(&start_sem, &done_sem, s_socket_fd, &linenoise_hdl, &repl_hdl); + esp_cli_handle_t cli_hdl; + esp_cli_setup(&start_sem, &done_sem, s_socket_fd, &linenoise_hdl, &cli_hdl); - /* create the repl task */ - task_args_t args = {.start_sem = start_sem, .done_sem = done_sem, .hdl = repl_hdl}; - BaseType_t rc = xTaskCreate(repl_task, "repl_task", 4096, &args, 5, NULL); + /* create the esp_cli instance task */ + task_args_t args = {.start_sem = start_sem, .done_sem = done_sem, .hdl = cli_hdl}; + BaseType_t rc = xTaskCreate(esp_cli_task, "esp_cli_task", 4096, &args, 5, NULL); TEST_ASSERT_EQUAL(pdPASS, rc); - /* start repl */ - TEST_ASSERT_EQUAL(ESP_OK, esp_repl_start(repl_hdl)); + /* start esp_cli instance */ + TEST_ASSERT_EQUAL(ESP_OK, esp_cli_start(cli_hdl)); wait_ms(100); - /* wait for the repl task to signal it started */ + /* wait for the esp_cli instance task to signal it started */ TEST_ASSERT_TRUE(xSemaphoreTake(start_sem, pdMS_TO_TICKS(2000))); /* send the quit command */ const char *quit_cmd_line = "quit \n"; test_send_characters(s_socket_fd[1], quit_cmd_line); - /* wait for the repl task to signal completion */ + /* wait for the esp_cli instance task to signal completion */ TEST_ASSERT_TRUE(xSemaphoreTake(done_sem, pdMS_TO_TICKS(2000))); - teardown_repl_instance(&start_sem, &done_sem, s_socket_fd, &linenoise_hdl, &repl_hdl); + esp_cli_teardown(&start_sem, &done_sem, s_socket_fd, &linenoise_hdl, &cli_hdl); } -TEST_CASE("create and destroy several instances of esp_repl", "[esp_repl]") +TEST_CASE("create and destroy several instances of esp_cli", "[esp_cli]") { /* create semaphores */ SemaphoreHandle_t start_sem_a, start_sem_b; SemaphoreHandle_t done_sem_a, done_sem_b; - esp_repl_handle_t repl_hdl_a, repl_hdl_b; + esp_cli_handle_t cli_hdl_a, cli_hdl_b; esp_linenoise_handle_t linenoise_hdl_a, linenoise_hdl_b; int socket_fd_a[2], socket_fd_b[2]; - /* create 2 instances of esp_repl*/ - setup_repl_instance(&start_sem_a, &done_sem_a, socket_fd_a, &linenoise_hdl_a, &repl_hdl_a); - setup_repl_instance(&start_sem_b, &done_sem_b, socket_fd_b, &linenoise_hdl_b, &repl_hdl_b); + /* create 2 instances of esp_cli*/ + esp_cli_setup(&start_sem_a, &done_sem_a, socket_fd_a, &linenoise_hdl_a, &cli_hdl_a); + esp_cli_setup(&start_sem_b, &done_sem_b, socket_fd_b, &linenoise_hdl_b, &cli_hdl_b); - /* create the repl task A */ - task_args_t args_a = {.start_sem = start_sem_a, .done_sem = done_sem_a, .hdl = repl_hdl_a}; - BaseType_t rc = xTaskCreate(repl_task, "repl_task_a", 4096, &args_a, 5, NULL); + /* create the esp_cli instance task A */ + task_args_t args_a = {.start_sem = start_sem_a, .done_sem = done_sem_a, .hdl = cli_hdl_a}; + BaseType_t rc = xTaskCreate(esp_cli_task, "esp_cli_task_a", 4096, &args_a, 5, NULL); TEST_ASSERT_EQUAL(pdPASS, rc); - /* create the repl task B */ - task_args_t args_b = {.start_sem = start_sem_b, .done_sem = done_sem_b, .hdl = repl_hdl_b}; - rc = xTaskCreate(repl_task, "repl_task_b", 4096, &args_b, 5, NULL); + /* create the esp_cli instance task B */ + task_args_t args_b = {.start_sem = start_sem_b, .done_sem = done_sem_b, .hdl = cli_hdl_b}; + rc = xTaskCreate(esp_cli_task, "esp_cli_task_b", 4096, &args_b, 5, NULL); TEST_ASSERT_EQUAL(pdPASS, rc); - /* start repl */ - TEST_ASSERT_EQUAL(ESP_OK, esp_repl_start(repl_hdl_a)); - TEST_ASSERT_EQUAL(ESP_OK, esp_repl_start(repl_hdl_b)); + /* start esp_cli instance */ + TEST_ASSERT_EQUAL(ESP_OK, esp_cli_start(cli_hdl_a)); + TEST_ASSERT_EQUAL(ESP_OK, esp_cli_start(cli_hdl_b)); wait_ms(500); - /* wait for the repl tasks to signal it started */ + /* wait for the esp_cli instance tasks to signal it started */ TEST_ASSERT_TRUE(xSemaphoreTake(start_sem_a, pdMS_TO_TICKS(2000))); TEST_ASSERT_TRUE(xSemaphoreTake(start_sem_b, pdMS_TO_TICKS(2000))); /* terminate instance A */ - TEST_ASSERT_EQUAL(ESP_OK, esp_repl_stop(repl_hdl_a)); + TEST_ASSERT_EQUAL(ESP_OK, esp_cli_stop(cli_hdl_a)); - /* wait for the repl task to signal completion */ + /* wait for the esp_cli instance task to signal completion */ TEST_ASSERT_TRUE(xSemaphoreTake(done_sem_a, pdMS_TO_TICKS(2000))); /* terminate instance B */ - TEST_ASSERT_EQUAL(ESP_OK, esp_repl_stop(repl_hdl_b)); + TEST_ASSERT_EQUAL(ESP_OK, esp_cli_stop(cli_hdl_b)); - /* wait for the repl task to signal completion */ + /* wait for the esp_cli instance task to signal completion */ TEST_ASSERT_TRUE(xSemaphoreTake(done_sem_b, pdMS_TO_TICKS(2000))); - teardown_repl_instance(&start_sem_a, &done_sem_a, socket_fd_a, &linenoise_hdl_a, &repl_hdl_a); - teardown_repl_instance(&start_sem_b, &done_sem_b, socket_fd_b, &linenoise_hdl_b, &repl_hdl_b); + esp_cli_teardown(&start_sem_a, &done_sem_a, socket_fd_a, &linenoise_hdl_a, &cli_hdl_a); + esp_cli_teardown(&start_sem_b, &done_sem_b, socket_fd_b, &linenoise_hdl_b, &cli_hdl_b); } #endif // CONFIG_IDF_TARGET_LiNUX diff --git a/esp_repl/test_apps/main/test_main.c b/esp_cli/test_apps/main/test_main.c similarity index 88% rename from esp_repl/test_apps/main/test_main.c rename to esp_cli/test_apps/main/test_main.c index 70bbd496d5..cc8cce5b31 100644 --- a/esp_repl/test_apps/main/test_main.c +++ b/esp_cli/test_apps/main/test_main.c @@ -21,6 +21,6 @@ void tearDown(void) void app_main(void) { - printf("Running esp_repl component tests\n"); + printf("Running esp_cli component tests\n"); unity_run_menu(); } diff --git a/esp_repl/test_apps/pytest_esp_repl.py b/esp_cli/test_apps/pytest_esp_cli.py similarity index 92% rename from esp_repl/test_apps/pytest_esp_repl.py rename to esp_cli/test_apps/pytest_esp_cli.py index 4d562afbde..88faff4c51 100644 --- a/esp_repl/test_apps/pytest_esp_repl.py +++ b/esp_cli/test_apps/pytest_esp_cli.py @@ -12,5 +12,5 @@ reason="Skip the idf version that did not build" ) @pytest.mark.parametrize('target', ['linux'], indirect=['target']) -def test_esp_repl(dut) -> None: +def test_esp_cli(dut) -> None: dut.run_all_single_board_cases() diff --git a/esp_cli/test_apps/sdkconfig.defaults b/esp_cli/test_apps/sdkconfig.defaults new file mode 100644 index 0000000000..e1ee958dff --- /dev/null +++ b/esp_cli/test_apps/sdkconfig.defaults @@ -0,0 +1,2 @@ +CONFIG_ESP_TASK_WDT_EN=n +CONFIG_ESP_CLI_HAS_QUIT_CMD=y \ No newline at end of file diff --git a/esp_repl/Kconfig b/esp_repl/Kconfig deleted file mode 100644 index 32c071233a..0000000000 --- a/esp_repl/Kconfig +++ /dev/null @@ -1,10 +0,0 @@ -menu "esp_repl configuration" - - config ESP_REPL_HAS_QUIT_CMD - bool "Register quit command" - default n - help - Register a static command "quit" that allows the user to return from the esp_repl main loop. - The command is registered through the ESP_COMMAND_REGISTER macro provided by esp_commands component - and is placed in the dedicated flash section. -endmenu diff --git a/esp_repl/README.md b/esp_repl/README.md deleted file mode 100644 index 38bc2d7c04..0000000000 --- a/esp_repl/README.md +++ /dev/null @@ -1,118 +0,0 @@ -# esp_repl Component - -The `esp_repl` component provides a **Runtime Evaluation Loop (REPL)** mechanism for ESP-IDF-based applications. -It allows developers to build interactive command-line interfaces (CLI) that support user-defined commands, history management, and customizable callbacks for command execution. - -This component integrates with [`esp_linenoise`](../esp_linenoise) for line editing and input handling, and with [`esp_commands`](../esp_commands) for command parsing and execution. - ---- - -## Features - -- Modular REPL management with explicit `start` and `stop` control -- Integration with [`esp_linenoise`](../esp_linenoise) for input and history -- Support for command sets through [`esp_commands`](../esp_commands) -- Configurable callbacks for: - - Pre-execution processing - - Post-execution handling - - On-stop and on-exit events -- Thread-safe operation using FreeRTOS semaphores -- Optional command history persistence to filesystem - ---- - -## Usage - -A typical use case involves: - -1. Initializing `esp_linenoise` and `esp_commands` -2. Creating the REPL instance with `esp_repl_create()` -3. Running `esp_repl()` in a task -4. Starting and stopping the REPL using `esp_repl_start()` and `esp_repl_stop()` -5. Destroying the instance with `esp_repl_destroy()` when done - -### Example - -```c -#include "esp_repl.h" -#include "esp_linenoise.h" -#include "esp_commands.h" -#include "esp_log.h" -#include "freertos/FreeRTOS.h" -#include "freertos/task.h" - -static const char *TAG = "repl_example"; - -void repl_task(void *arg) -{ - esp_repl_handle_t repl_hdl = (esp_repl_handle_t)arg; - - // Run REPL loop (blocking until esp_repl_stop() is called) - // The loop won't be reached until esp_repl_start() is called - esp_repl(repl_hdl); - - ESP_LOGI(TAG, "REPL task exiting"); - vTaskDelete(NULL); -} - -void app_main(void) -{ - esp_err_t ret; - esp_repl_handle_t repl = NULL; - - // Initialize esp_linenoise (mandatory) - esp_linenoise_handle_t esp_linenoise_hdl = esp_linenoise_create(); - - // Initialize command set (optional) - esp_command_set_handle_t esp_commands_cmd_set = esp_commands_create(); - - esp_repl_config_t repl_cfg = { - .linenoise_handle = esp_linenoise_hdl, - .command_set_handle = esp_commands_cmd_set, /* optional */ - .max_cmd_line_size = 256, - .history_save_path = "/spiffs/repl_history.txt", /* optional */ - }; - - ret = esp_repl_create(&repl_cfg, &repl); - if (ret != ESP_OK) { - ESP_LOGE(TAG, "Failed to create REPL instance (%s)", esp_err_to_name(ret)); - return; - } - - // Create REPL task - if (xTaskCreate(repl_task, "repl_task", 4096, repl, 5, NULL) != pdPASS) { - ESP_LOGE(TAG, "Failed to create REPL task"); - esp_repl_destroy(repl); - return; - } - - ESP_LOGI(TAG, "Starting REPL..."); - ret = esp_repl_start(repl); - if (ret != ESP_OK) { - ESP_LOGE(TAG, "Failed to start REPL (%s)", esp_err_to_name(ret)); - esp_repl_destroy(repl); - return; - } - - // Application logic can run in parallel while REPL runs in its own task - // [...] - vTaskDelay(pdMS_TO_TICKS(10000)); // Example delay - - // Stop REPL - ret = esp_repl_stop(repl); - if (ret != ESP_OK) { - ESP_LOGW(TAG, "Failed to stop REPL (%s)", esp_err_to_name(ret)); - } - - ESP_LOGI(TAG, "REPL exited"); - - // Destroy REPL instance and clean up - ret = esp_repl_destroy(repl); - if (ret != ESP_OK) { - ESP_LOGW(TAG, "Failed to destroy REPL instance cleanly (%s)", esp_err_to_name(ret)); - } - - ESP_LOGI(TAG, "REPL example finished"); -} - -``` diff --git a/esp_repl/idf_component.yml b/esp_repl/idf_component.yml deleted file mode 100644 index d4cdb20984..0000000000 --- a/esp_repl/idf_component.yml +++ /dev/null @@ -1,14 +0,0 @@ -version: "0.1.0" -description: "esp_repl - Read Eval Print Loop component" -url: https://github.com/espressif/idf-extra-components/tree/master/esp_repl -dependencies: - SoucheSouche/esp_linenoise: - version: "*" - registry_url: https://components-staging.espressif.com - SoucheSouche/esp_commands: - version: "*" - registry_url: https://components-staging.espressif.com -sbom: - manifests: - - path: sbom_esp_repl.yml - dest: . \ No newline at end of file diff --git a/esp_repl/include/esp_repl.h b/esp_repl/include/esp_repl.h deleted file mode 100644 index 28215f096d..0000000000 --- a/esp_repl/include/esp_repl.h +++ /dev/null @@ -1,191 +0,0 @@ -/* - * 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" -#include "esp_linenoise.h" -#include "esp_commands.h" - -/** - * @brief Handle to a REPL instance. - */ -typedef struct esp_repl_instance *esp_repl_handle_t; - -/** - * @brief Function prototype called at the beginning of esp_repl(). - * - * @param ctx User-defined context pointer. - * @param handle Handle to the REPL instance. - */ -typedef void (*esp_repl_on_enter_fn)(void *ctx, esp_repl_handle_t handle); - -/** - * @brief Enter callback configuration structure for the REPL. - */ -typedef struct esp_repl_on_enter { - esp_repl_on_enter_fn func; /**!< Function called at the beginning of esp_repl() */ - void *ctx; /**!< Context passed to the enter function */ -} esp_repl_on_enter_t; - -/** - * @brief Function prototype called before executing a command. - * - * @param ctx User-defined context pointer. - * @param buf Buffer containing the command. - * @param reader_ret_val Return value from the reader function. - * - * @return ESP_OK to continue execution, error code to abort. - */ -typedef esp_err_t (*esp_repl_pre_executor_fn)(void *ctx, const char *buf, esp_err_t reader_ret_val); - -/** - * @brief Pre-executor configuration structure for the REPL. - */ -typedef struct esp_repl_pre_executor { - esp_repl_pre_executor_fn func; /**!< Function to run before command execution */ - void *ctx; /**!< Context passed to the pre-executor function */ -} esp_repl_pre_executor_t; - -/** - * @brief Function prototype called after executing a command. - * - * @param ctx User-defined context pointer. - * @param buf Command that was executed. - * @param executor_ret_val Return value from the executor function. - * @param cmd_ret_val Command-specific return value. - * - * @return ESP_OK on success, error code otherwise. - */ -typedef esp_err_t (*esp_repl_post_executor_fn)(void *ctx, const char *buf, esp_err_t executor_ret_val, int cmd_ret_val); - -/** - * @brief Post-executor configuration structure for the REPL. - */ -typedef struct esp_repl_post_executor { - esp_repl_post_executor_fn func; /**!< Function called after command execution */ - void *ctx; /**!< Context passed to the post-executor function */ -} esp_repl_post_executor_t; - -/** - * @brief Function prototype called when the REPL is stopping. - * - * This callback allows the user to unblock the reader (or perform other - * cleanup) so that the REPL can return from `esp_repl()`. - * - * @param ctx User-defined context pointer. - * @param handle Handle to the REPL instance. - */ -typedef void (*esp_repl_on_stop_fn)(void *ctx, esp_repl_handle_t handle); - -/** - * @brief Stop callback configuration structure for the REPL. - */ -typedef struct esp_repl_on_stop { - esp_repl_on_stop_fn func; /**!< Function called when REPL stop is requested */ - void *ctx; /**!< Context passed to the on_stop function */ -} esp_repl_on_stop_t; - -/** - * @brief Function prototype called when the REPL exits. - * - * @param ctx User-defined context pointer. - * @param handle Handle to the REPL instance. - */ -typedef void (*esp_repl_on_exit_fn)(void *ctx, esp_repl_handle_t handle); - -/** - * @brief Exit callback configuration structure for the REPL. - */ -typedef struct esp_repl_on_exit { - esp_repl_on_exit_fn func; /**!< Function called on REPL exit */ - void *ctx; /**!< Context passed to the exit function */ -} esp_repl_on_exit_t; - -/** - * @brief Configuration structure to initialize a REPL instance. - */ -typedef struct esp_repl_config { - esp_linenoise_handle_t linenoise_handle; /**!< Handle to the esp_linenoise instance */ - esp_command_set_handle_t command_set_handle; /**!< Handle to a set of commands */ - size_t max_cmd_line_size; /**!< Maximum allowed command line size */ - const char *history_save_path; /**!< Path to file to save the history */ - esp_repl_on_enter_t on_enter; /**!< Enter callback and context */ - esp_repl_pre_executor_t pre_executor; /**!< Pre-executor callback and context */ - esp_repl_post_executor_t post_executor; /**!< Post-executor callback and context */ - esp_repl_on_stop_t on_stop; /**!< Stop callback and context */ - esp_repl_on_exit_t on_exit; /**!< Exit callback and context */ -} esp_repl_config_t; - -/** - * @brief Create a REPL instance. - * - * @param config Pointer to the configuration structure. - * @param out_handle Pointer to store the created REPL instance handle. - * - * @return ESP_OK on success, error code otherwise. - */ -esp_err_t esp_repl_create(const esp_repl_config_t *config, esp_repl_handle_t *out_handle); - -/** - * @brief Destroy a REPL instance. - * - * @param handle REPL instance handle to destroy. - * - * @return ESP_OK on success, error code otherwise. - */ -esp_err_t esp_repl_destroy(esp_repl_handle_t handle); - -/** - * @brief Start a REPL instance. - * - * @param handle REPL instance handle. - * - * @return ESP_OK on success, error code otherwise. - */ -esp_err_t esp_repl_start(esp_repl_handle_t handle); - -/** - * @brief Stop a REPL instance. - * - * @note This function will internally call 'esp_linenoise_abort' first to try to return from - * 'esp_linenoise_get_line'. If the user has provided a custom read to the esp_linenoise - * instance used by the esp_repl instance, it is the responsibility of the user to provide - * the mechanism to return from this custom read by providing a callback to the 'on_stop' field - * in the esp_repl_config_t. - * - * Return Values: - * - ESP_OK: Returned if the user has not provided a custom read and the abort operation succeeds. - * - ESP_ERR_INVALID_STATE: Returned if the user has provided a custom read. In this case, the user - * is responsible for implementing an abort mechanism that ensures a successful return from - * their custom read. This can be achieved by placing the logic in the on_stop callback. - * - * Behavior: - * - When a custom read is registered, ESP_ERR_INVALID_STATE indicates that esp_repl_stop() cannot - * forcibly return from the read. The user must handle the return themselves via on_stop(). - * - From the perspective of esp_repl_stop(), this scenario is treated as successful, and its - * return value should be set to ESP_OK. - * - * @param handle REPL instance handle. - * - * @return ESP_OK on success, error code otherwise. - */ -esp_err_t esp_repl_stop(esp_repl_handle_t handle); - -/** - * @brief Run the REPL loop. - * - * @param handle REPL instance handle. - */ -void esp_repl(esp_repl_handle_t handle); - -#ifdef __cplusplus -} -#endif diff --git a/esp_repl/test_apps/sdkconfig.defaults b/esp_repl/test_apps/sdkconfig.defaults deleted file mode 100644 index fb6d7dba93..0000000000 --- a/esp_repl/test_apps/sdkconfig.defaults +++ /dev/null @@ -1,2 +0,0 @@ -CONFIG_ESP_TASK_WDT_EN=n -CONFIG_ESP_REPL_HAS_QUIT_CMD=y \ No newline at end of file From 976c603529c95bf2fbcc11c21ee6df8c0e6acc2f Mon Sep 17 00:00:00 2001 From: Guillaume Souchere Date: Fri, 14 Nov 2025 08:31:32 +0100 Subject: [PATCH 8/8] feat(esp_cli): move linux test to host_test and add test_apps --- esp_cli/.build-test-rules.yml | 12 +- esp_cli/host_test/CMakeLists.txt | 5 + esp_cli/host_test/main/CMakeLists.txt | 5 + esp_cli/host_test/main/idf_component.yml | 4 + esp_cli/host_test/main/test_esp_cli.c | 332 +++++++++++++++++++++++ esp_cli/host_test/main/test_main.c | 26 ++ esp_cli/host_test/pytest_host_esp_cli.py | 16 ++ esp_cli/host_test/sdkconfig.defaults | 2 + esp_cli/idf_component.yml | 2 +- esp_cli/test_apps/main/CMakeLists.txt | 2 +- esp_cli/test_apps/main/test_esp_cli.c | 266 +++++++++++------- esp_cli/test_apps/main/test_main.c | 5 +- esp_cli/test_apps/pytest_esp_cli.py | 5 +- esp_cli/test_apps/sdkconfig.defaults | 4 +- 14 files changed, 576 insertions(+), 110 deletions(-) create mode 100644 esp_cli/host_test/CMakeLists.txt create mode 100644 esp_cli/host_test/main/CMakeLists.txt create mode 100644 esp_cli/host_test/main/idf_component.yml create mode 100644 esp_cli/host_test/main/test_esp_cli.c create mode 100644 esp_cli/host_test/main/test_main.c create mode 100644 esp_cli/host_test/pytest_host_esp_cli.py create mode 100644 esp_cli/host_test/sdkconfig.defaults diff --git a/esp_cli/.build-test-rules.yml b/esp_cli/.build-test-rules.yml index 9a4d987954..6f7ffa1d16 100644 --- a/esp_cli/.build-test-rules.yml +++ b/esp_cli/.build-test-rules.yml @@ -1,7 +1,15 @@ -esp_cli/test_apps: +esp_cli/host_test: enable: - if: IDF_TARGET == "linux" reason: "Sufficient to test on Linux target" disable: - if: IDF_VERSION_MAJOR <= 5 and IDF_VERSION_MINOR <= 4 - reason: "those versions of esp-idf do not support eventfd for linux target" \ No newline at end of file + reason: "those versions of esp-idf do not support eventfd for linux target" + +esp_cli/test_apps: + enable: + - if: IDF_TARGET == "esp32s3" + reason: "Need support for USB Serial JTAG" + disable: + - if: IDF_VERSION_MAJOR <= 5 and IDF_VERSION_MINOR <= 3 + reason: "esp_vfs_fs_ops_t not available" diff --git a/esp_cli/host_test/CMakeLists.txt b/esp_cli/host_test/CMakeLists.txt new file mode 100644 index 0000000000..833cc3129f --- /dev/null +++ b/esp_cli/host_test/CMakeLists.txt @@ -0,0 +1,5 @@ +cmake_minimum_required(VERSION 3.22) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +set(COMPONENTS main) +project(esp_cli_host_test) diff --git a/esp_cli/host_test/main/CMakeLists.txt b/esp_cli/host_test/main/CMakeLists.txt new file mode 100644 index 0000000000..f02fe0b18c --- /dev/null +++ b/esp_cli/host_test/main/CMakeLists.txt @@ -0,0 +1,5 @@ + +idf_component_register(SRCS "test_esp_cli.c" "test_main.c" + PRIV_INCLUDE_DIRS "." + PRIV_REQUIRES unity + WHOLE_ARCHIVE) diff --git a/esp_cli/host_test/main/idf_component.yml b/esp_cli/host_test/main/idf_component.yml new file mode 100644 index 0000000000..3175bae677 --- /dev/null +++ b/esp_cli/host_test/main/idf_component.yml @@ -0,0 +1,4 @@ +dependencies: + espressif/esp_cli: + version: "*" + override_path: "../.." diff --git a/esp_cli/host_test/main/test_esp_cli.c b/esp_cli/host_test/main/test_esp_cli.c new file mode 100644 index 0000000000..cbe1892a42 --- /dev/null +++ b/esp_cli/host_test/main/test_esp_cli.c @@ -0,0 +1,332 @@ +/* + * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/semphr.h" +#include "unity.h" +#include "esp_cli.h" +#include "esp_linenoise.h" +#include "esp_cli_commands.h" + +#include + +inline __attribute__((always_inline)) +uint32_t get_millis(void) +{ + struct timeval tv = { 0 }; + gettimeofday(&tv, NULL); + return tv.tv_sec * 1000 + tv.tv_usec / 1000; +} + +inline __attribute__((always_inline)) +void wait_ms(int ms) +{ + vTaskDelay(pdMS_TO_TICKS(ms)); +} + +static size_t s_on_enter_nb_of_calls = 0; +static size_t s_pre_executor_nb_of_calls = 0; +static size_t s_post_executor_nb_of_calls = 0; +static size_t s_on_stop_nb_of_calls = 0; +static size_t s_on_exit_nb_of_calls = 0; + +void test_on_enter(void *ctx, esp_cli_handle_t handle) +{ + s_on_enter_nb_of_calls++; + return; +} + +esp_err_t test_pre_executor(void *ctx, const char *buf, esp_err_t reader_ret_val) +{ + s_pre_executor_nb_of_calls++; + return ESP_OK; +} + +esp_err_t test_post_executor(void *ctx, const char *buf, esp_err_t executor_ret_val, int cmd_ret_val) +{ + s_post_executor_nb_of_calls++; + return ESP_OK; +} + +void test_on_stop(void *ctx, esp_cli_handle_t handle) +{ + s_on_stop_nb_of_calls++; + return; +} + +void test_on_exit(void *ctx, esp_cli_handle_t handle) +{ + s_on_exit_nb_of_calls++; + return; +} + +/* Pass two semaphores: + * - start_sem: child gives it when it reached esp_cli() (so main knows child started) + * - done_sem: child gives it just before deleting itself (so main can "join") + */ +typedef struct task_args { + SemaphoreHandle_t start_sem; + SemaphoreHandle_t done_sem; + esp_cli_handle_t hdl; +} task_args_t; + +static void esp_cli_task(void *args) +{ + task_args_t *task_args = (task_args_t *)args; + + /* signal to main that task started and esp_cli() will run */ + xSemaphoreGive(task_args->start_sem); + + /* run the esp_cli REPL loop (will return when stopped) */ + esp_cli(task_args->hdl); + + /* signal completion (emulates pthread_join notification) */ + xSemaphoreGive(task_args->done_sem); + + /* self-delete */ + vTaskDelete(NULL); +} + +static int s_socket_fd[2]; + +static void test_socket_setup(int socket_fd[2]) +{ + TEST_ASSERT_EQUAL(0, socketpair(AF_UNIX, SOCK_STREAM, 0, socket_fd)); + + /* ensure reads are blocking */ + int flags = fcntl(socket_fd[0], F_GETFL, 0); + flags &= ~O_NONBLOCK; + fcntl(socket_fd[0], F_SETFL, flags); + + flags = fcntl(socket_fd[1], F_GETFL, 0); + flags &= ~O_NONBLOCK; + fcntl(socket_fd[1], F_SETFL, flags); +} + +static void test_socket_teardown(int socket_fd[2]) +{ + close(socket_fd[0]); + close(socket_fd[1]); +} + +static void test_send_characters(int socket_fd, const char *msg) +{ + wait_ms(100); + + const size_t msg_len = strlen(msg); + const int nwrite = write(socket_fd, msg, msg_len); + TEST_ASSERT_EQUAL(msg_len, nwrite); +} + +static void esp_cli_teardown(SemaphoreHandle_t *start_sem, SemaphoreHandle_t *done_sem, int socket_fd[2], + esp_linenoise_handle_t *linenoise_hdl, esp_cli_handle_t *cli_hdl) +{ + /* destroy the instance of esp_cli */ + TEST_ASSERT_EQUAL(ESP_OK, esp_cli_destroy(*cli_hdl)); + + /* delete the linenoise instance */ + TEST_ASSERT_EQUAL(ESP_OK, esp_linenoise_delete_instance(*linenoise_hdl)); + + /* cleanup semaphores */ + vSemaphoreDelete(*start_sem); + vSemaphoreDelete(*done_sem); + + /* close the socketpair */ + test_socket_teardown(socket_fd); + + s_on_stop_nb_of_calls = 0; + s_on_exit_nb_of_calls = 0; + s_on_enter_nb_of_calls = 0; + s_pre_executor_nb_of_calls = 0; + s_post_executor_nb_of_calls = 0; +} + +static void esp_cli_setup(SemaphoreHandle_t *start_sem, SemaphoreHandle_t *done_sem, int socket_fd[2], + esp_linenoise_handle_t *linenoise_hdl, esp_cli_handle_t *cli_hdl) +{ + /* create semaphores */ + *start_sem = xSemaphoreCreateBinary(); + TEST_ASSERT_NOT_NULL(start_sem); + *done_sem = xSemaphoreCreateBinary(); + TEST_ASSERT_NOT_NULL(done_sem); + + /* create the socket_pair */ + test_socket_setup(socket_fd); + + /* ensure both semaphores are in the "taken/empty" state: + taking with 0 timeout guarantees they are empty afterwards + regardless of the create semantics on this FreeRTOS build. */ + xSemaphoreTake(*start_sem, 0); + xSemaphoreTake(*done_sem, 0); + + esp_linenoise_config_t linenoise_config; + esp_linenoise_get_instance_config_default(&linenoise_config); + linenoise_config.in_fd = socket_fd[0]; + linenoise_config.out_fd = socket_fd[0]; + TEST_ASSERT_EQUAL(ESP_OK, esp_linenoise_create_instance(&linenoise_config, linenoise_hdl)); + TEST_ASSERT_NOT_NULL(*linenoise_hdl); + + esp_cli_config_t cli_config = { + .linenoise_handle = *linenoise_hdl, + .command_set_handle = NULL, + .max_cmd_line_size = 256, + .history_save_path = NULL, + .on_enter = { .func = test_on_enter, .ctx = NULL }, + .pre_executor = { .func = test_pre_executor, .ctx = NULL }, + .post_executor = { .func = test_post_executor, .ctx = NULL }, + .on_stop = { .func = test_on_stop, .ctx = NULL }, + .on_exit = { .func = test_on_exit, .ctx = NULL } + }; + + TEST_ASSERT_EQUAL(ESP_OK, esp_cli_create(&cli_config, cli_hdl)); + TEST_ASSERT_NOT_NULL(*cli_hdl); + + s_on_stop_nb_of_calls = 0; + s_on_exit_nb_of_calls = 0; + s_on_enter_nb_of_calls = 0; + s_pre_executor_nb_of_calls = 0; + s_post_executor_nb_of_calls = 0; +} + +TEST_CASE("esp_cli() loop calls all callbacks and exit on call to esp_cli_stop", "[esp_cli]") +{ + SemaphoreHandle_t start_sem, done_sem; + esp_linenoise_handle_t linenoise_hdl; + esp_cli_handle_t cli_hdl; + esp_cli_setup(&start_sem, &done_sem, s_socket_fd, &linenoise_hdl, &cli_hdl); + + /* create the esp_cli instance task */ + task_args_t args = {.start_sem = start_sem, .done_sem = done_sem, .hdl = cli_hdl}; + BaseType_t rc = xTaskCreate(esp_cli_task, "esp_cli_task", 4096, &args, 5, NULL); + TEST_ASSERT_EQUAL(pdPASS, rc); + + /* should fail before esp_cli instance is started */ + TEST_ASSERT_NOT_EQUAL(ESP_OK, esp_cli_stop(cli_hdl)); + + /* start esp_cli instance */ + TEST_ASSERT_NOT_EQUAL(ESP_OK, esp_cli_start(NULL)); + TEST_ASSERT_EQUAL(ESP_OK, esp_cli_start(cli_hdl)); + + wait_ms(100); + + /* wait for the esp_cli task to signal it started */ + TEST_ASSERT_TRUE(xSemaphoreTake(start_sem, pdMS_TO_TICKS(2000))); + + /* send a dummy string new line terminated to trigger linenoise to return */ + const char *input_line = "dummy_message\n"; + test_send_characters(s_socket_fd[1], input_line); + + /* wait for a bit so esp_cli() has time to loop back into esp_linenoise_get_line */ + wait_ms(500); + + /* check that pre-executor, post-executor callbacks are called */ + TEST_ASSERT_EQUAL(1, s_pre_executor_nb_of_calls); + TEST_ASSERT_EQUAL(1, s_post_executor_nb_of_calls); + + /* stop esp_cli and wait for task to finish (emulate pthread_join) */ + TEST_ASSERT_NOT_EQUAL(ESP_OK, esp_cli_stop(NULL)); + TEST_ASSERT_EQUAL(ESP_OK, esp_cli_stop(cli_hdl)); + + /* wait for the esp_cli task to signal completion */ + TEST_ASSERT_TRUE(xSemaphoreTake(done_sem, pdMS_TO_TICKS(2000))); + + /* check that all callbacks were called the right number of times */ + TEST_ASSERT_EQUAL(1, s_on_stop_nb_of_calls); + TEST_ASSERT_EQUAL(1, s_on_enter_nb_of_calls); + TEST_ASSERT_EQUAL(1, s_on_exit_nb_of_calls); + TEST_ASSERT_EQUAL(2, s_pre_executor_nb_of_calls); + TEST_ASSERT_EQUAL(2, s_post_executor_nb_of_calls); + + /* make sure calling stop fails because the esp_cli instance is no longer running */ + TEST_ASSERT_NOT_EQUAL(ESP_OK, esp_cli_stop(cli_hdl)); + + /* destroy the esp_cli instance */ + TEST_ASSERT_NOT_EQUAL(ESP_OK, esp_cli_destroy(NULL)); + esp_cli_teardown(&start_sem, &done_sem, s_socket_fd, &linenoise_hdl, &cli_hdl); +} + +TEST_CASE("esp_cli() exits when esp_cli_stop() called from the task running esp_cli()", "[esp_cli]") +{ + SemaphoreHandle_t start_sem, done_sem; + esp_linenoise_handle_t linenoise_hdl; + esp_cli_handle_t cli_hdl; + esp_cli_setup(&start_sem, &done_sem, s_socket_fd, &linenoise_hdl, &cli_hdl); + + /* create the esp_cli instance task */ + task_args_t args = {.start_sem = start_sem, .done_sem = done_sem, .hdl = cli_hdl}; + BaseType_t rc = xTaskCreate(esp_cli_task, "esp_cli_task", 4096, &args, 5, NULL); + TEST_ASSERT_EQUAL(pdPASS, rc); + + /* start esp_cli instance */ + TEST_ASSERT_EQUAL(ESP_OK, esp_cli_start(cli_hdl)); + + wait_ms(100); + + /* wait for the esp_cli instance task to signal it started */ + TEST_ASSERT_TRUE(xSemaphoreTake(start_sem, pdMS_TO_TICKS(2000))); + + /* send the quit command */ + const char *quit_cmd_line = "quit \n"; + test_send_characters(s_socket_fd[1], quit_cmd_line); + + /* wait for the esp_cli instance task to signal completion */ + TEST_ASSERT_TRUE(xSemaphoreTake(done_sem, pdMS_TO_TICKS(2000))); + + esp_cli_teardown(&start_sem, &done_sem, s_socket_fd, &linenoise_hdl, &cli_hdl); +} + +TEST_CASE("create and destroy several instances of esp_cli", "[esp_cli]") +{ + /* create semaphores */ + SemaphoreHandle_t start_sem_a, start_sem_b; + SemaphoreHandle_t done_sem_a, done_sem_b; + esp_cli_handle_t cli_hdl_a, cli_hdl_b; + esp_linenoise_handle_t linenoise_hdl_a, linenoise_hdl_b; + int socket_fd_a[2], socket_fd_b[2]; + + /* create 2 instances of esp_cli*/ + esp_cli_setup(&start_sem_a, &done_sem_a, socket_fd_a, &linenoise_hdl_a, &cli_hdl_a); + esp_cli_setup(&start_sem_b, &done_sem_b, socket_fd_b, &linenoise_hdl_b, &cli_hdl_b); + + /* create the esp_cli instance task A */ + task_args_t args_a = {.start_sem = start_sem_a, .done_sem = done_sem_a, .hdl = cli_hdl_a}; + BaseType_t rc = xTaskCreate(esp_cli_task, "esp_cli_task_a", 4096, &args_a, 5, NULL); + TEST_ASSERT_EQUAL(pdPASS, rc); + + /* create the esp_cli instance task B */ + task_args_t args_b = {.start_sem = start_sem_b, .done_sem = done_sem_b, .hdl = cli_hdl_b}; + rc = xTaskCreate(esp_cli_task, "esp_cli_task_b", 4096, &args_b, 5, NULL); + TEST_ASSERT_EQUAL(pdPASS, rc); + + /* start esp_cli instance */ + TEST_ASSERT_EQUAL(ESP_OK, esp_cli_start(cli_hdl_a)); + TEST_ASSERT_EQUAL(ESP_OK, esp_cli_start(cli_hdl_b)); + wait_ms(500); + + /* wait for the esp_cli instance tasks to signal it started */ + TEST_ASSERT_TRUE(xSemaphoreTake(start_sem_a, pdMS_TO_TICKS(2000))); + TEST_ASSERT_TRUE(xSemaphoreTake(start_sem_b, pdMS_TO_TICKS(2000))); + + /* terminate instance A */ + TEST_ASSERT_EQUAL(ESP_OK, esp_cli_stop(cli_hdl_a)); + + /* wait for the esp_cli instance task to signal completion */ + TEST_ASSERT_TRUE(xSemaphoreTake(done_sem_a, pdMS_TO_TICKS(2000))); + + /* terminate instance B */ + TEST_ASSERT_EQUAL(ESP_OK, esp_cli_stop(cli_hdl_b)); + + /* wait for the esp_cli instance task to signal completion */ + TEST_ASSERT_TRUE(xSemaphoreTake(done_sem_b, pdMS_TO_TICKS(2000))); + + esp_cli_teardown(&start_sem_a, &done_sem_a, socket_fd_a, &linenoise_hdl_a, &cli_hdl_a); + esp_cli_teardown(&start_sem_b, &done_sem_b, socket_fd_b, &linenoise_hdl_b, &cli_hdl_b); +} diff --git a/esp_cli/host_test/main/test_main.c b/esp_cli/host_test/main/test_main.c new file mode 100644 index 0000000000..c54c05befb --- /dev/null +++ b/esp_cli/host_test/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_cli component host tests\n"); + unity_run_menu(); +} diff --git a/esp_cli/host_test/pytest_host_esp_cli.py b/esp_cli/host_test/pytest_host_esp_cli.py new file mode 100644 index 0000000000..5002272702 --- /dev/null +++ b/esp_cli/host_test/pytest_host_esp_cli.py @@ -0,0 +1,16 @@ +import pytest +from pytest_embedded import Dut +from pytest_embedded_idf.utils import idf_parametrize +import glob +from pathlib import Path + + + +@pytest.mark.host_test +@pytest.mark.skipif( + not bool(glob.glob(f'{Path(__file__).parent.absolute()}/build*/')), + reason="Skip the idf version that did not build" +) +@pytest.mark.parametrize('target', ['linux'], indirect=['target']) +def host_test_esp_cli(dut) -> None: + dut.run_all_single_board_cases() diff --git a/esp_cli/host_test/sdkconfig.defaults b/esp_cli/host_test/sdkconfig.defaults new file mode 100644 index 0000000000..e1ee958dff --- /dev/null +++ b/esp_cli/host_test/sdkconfig.defaults @@ -0,0 +1,2 @@ +CONFIG_ESP_TASK_WDT_EN=n +CONFIG_ESP_CLI_HAS_QUIT_CMD=y \ No newline at end of file diff --git a/esp_cli/idf_component.yml b/esp_cli/idf_component.yml index 291bf09f87..9c3cbda376 100644 --- a/esp_cli/idf_component.yml +++ b/esp_cli/idf_component.yml @@ -1,5 +1,5 @@ version: "0.1.0" -description: "esp_cli - Read Eval Print Loop component" +description: "esp_cli — A command-line interface component that uses a REPL as its main execution loop." url: https://github.com/espressif/idf-extra-components/tree/master/esp_cli dependencies: espressif/esp_linenoise: "*" diff --git a/esp_cli/test_apps/main/CMakeLists.txt b/esp_cli/test_apps/main/CMakeLists.txt index f02fe0b18c..ecbcb8dcfd 100644 --- a/esp_cli/test_apps/main/CMakeLists.txt +++ b/esp_cli/test_apps/main/CMakeLists.txt @@ -1,5 +1,5 @@ idf_component_register(SRCS "test_esp_cli.c" "test_main.c" PRIV_INCLUDE_DIRS "." - PRIV_REQUIRES unity + PRIV_REQUIRES unity vfs esp_driver_uart esp_driver_usb_serial_jtag WHOLE_ARCHIVE) diff --git a/esp_cli/test_apps/main/test_esp_cli.c b/esp_cli/test_apps/main/test_esp_cli.c index d7792d7441..8c2ab9b86b 100644 --- a/esp_cli/test_apps/main/test_esp_cli.c +++ b/esp_cli/test_apps/main/test_esp_cli.c @@ -7,32 +7,25 @@ #include #include #include -#include + #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "freertos/semphr.h" #include "unity.h" + #include "esp_cli.h" #include "esp_linenoise.h" #include "esp_cli_commands.h" -#if CONFIG_IDF_TARGET_LINUX -#include -#endif - -inline __attribute__((always_inline)) -uint32_t get_millis(void) -{ - struct timeval tv = { 0 }; - gettimeofday(&tv, NULL); - return tv.tv_sec * 1000 + tv.tv_usec / 1000; -} - -inline __attribute__((always_inline)) -void wait_ms(int ms) -{ - vTaskDelay(pdMS_TO_TICKS(ms)); -} +#include "sdkconfig.h" +#include "esp_vfs.h" +#include "esp_vfs_common.h" +#include "driver/esp_private/uart_vfs.h" +#include "driver/uart_vfs.h" +#include "driver/uart.h" +#include "driver/esp_private/usb_serial_jtag_vfs.h" +#include "driver/usb_serial_jtag_vfs.h" +#include "driver/usb_serial_jtag.h" static size_t s_on_enter_nb_of_calls = 0; static size_t s_pre_executor_nb_of_calls = 0; @@ -97,40 +90,104 @@ static void esp_cli_task(void *args) vTaskDelete(NULL); } -#if CONFIG_IDF_TARGET_LINUX -static int s_socket_fd[2]; +static void test_uart_install(int *in_fd, int *out_fd) +{ + /* Minicom, screen, idf_monitor send CR when ENTER key is pressed */ + uart_vfs_dev_port_set_rx_line_endings(CONFIG_ESP_CONSOLE_UART_NUM, ESP_LINE_ENDINGS_CR); + /* Move the caret to the beginning of the next line on '\n' */ + uart_vfs_dev_port_set_tx_line_endings(CONFIG_ESP_CONSOLE_UART_NUM, ESP_LINE_ENDINGS_CRLF); + + /* Configure UART. Note that REF_TICK/XTAL is used so that the baud rate remains + * correct while APB frequency is changing in light sleep mode. + */ +#if SOC_UART_SUPPORT_REF_TICK + uart_sclk_t clk_source = UART_SCLK_REF_TICK; + // REF_TICK clock can't provide a high baudrate + if (CONFIG_ESP_CONSOLE_UART_BAUDRATE > 1 * 1000 * 1000) { + clk_source = UART_SCLK_DEFAULT; + ESP_LOGW(TAG, "light sleep UART wakeup might not work at the configured baud rate"); + } +#elif SOC_UART_SUPPORT_XTAL_CLK + uart_sclk_t clk_source = UART_SCLK_XTAL; +#else +#error "No UART clock source is aware of DFS" +#endif // SOC_UART_SUPPORT_xxx + const uart_config_t uart_config = { + .baud_rate = CONFIG_ESP_CONSOLE_UART_BAUDRATE, + .data_bits = UART_DATA_8_BITS, + .parity = UART_PARITY_DISABLE, + .stop_bits = UART_STOP_BITS_1, + .source_clk = clk_source, + }; + + uart_param_config(CONFIG_ESP_CONSOLE_UART_NUM, &uart_config); + + /* Install UART driver for interrupt-driven reads and writes */ + TEST_ASSERT_EQUAL(ESP_OK, uart_driver_install(CONFIG_ESP_CONSOLE_UART_NUM, 256, 0, 0, NULL, 0)); + + /* Tell VFS to use UART driver */ + uart_vfs_dev_use_driver(CONFIG_ESP_CONSOLE_UART_NUM); -static void test_socket_setup(int socket_fd[2]) + /* register the vfs, create a FD used to interface the UART */ + const esp_vfs_fs_ops_t *uart_vfs = esp_vfs_uart_get_vfs(); + TEST_ASSERT_EQUAL(ESP_OK, esp_vfs_register_fs("/dev/test_uart", uart_vfs, 0, NULL)); + /* open in blocking mode */ + const int uart_fd = open("/dev/test_uart/0", 0); + TEST_ASSERT(uart_fd != -1); + *in_fd = uart_fd; + *out_fd = uart_fd; +} + +static void test_uart_uninstall(const int fd) { - TEST_ASSERT_EQUAL(0, socketpair(AF_UNIX, SOCK_STREAM, 0, socket_fd)); + /* close the stream */ + const int ret = close(fd); + TEST_ASSERT(ret != -1); - /* ensure reads are blocking */ - int flags = fcntl(socket_fd[0], F_GETFL, 0); - flags &= ~O_NONBLOCK; - fcntl(socket_fd[0], F_SETFL, flags); + /* unregister the vfs */ + TEST_ASSERT_EQUAL(ESP_OK, esp_vfs_unregister("/dev/test_uart")); - flags = fcntl(socket_fd[1], F_GETFL, 0); - flags &= ~O_NONBLOCK; - fcntl(socket_fd[1], F_SETFL, flags); + /* uninstall the driver for the default uart */ + uart_vfs_dev_use_nonblocking(CONFIG_ESP_CONSOLE_UART_NUM); + TEST_ASSERT_EQUAL(ESP_OK, uart_driver_delete(CONFIG_ESP_CONSOLE_UART_NUM)); } -static void test_socket_teardown(int socket_fd[2]) +static void test_usj_install(int *in_fd, int *out_fd) { - close(socket_fd[0]); - close(socket_fd[1]); + /* Minicom, screen, idf_monitor send CR when ENTER key is pressed */ + usb_serial_jtag_vfs_set_rx_line_endings(ESP_LINE_ENDINGS_CR); + /* Move the caret to the beginning of the next line on '\n' */ + usb_serial_jtag_vfs_set_tx_line_endings(ESP_LINE_ENDINGS_CRLF); + + /* Install USB-SERIAL-JTAG driver for interrupt-driven reads and writes */ + usb_serial_jtag_driver_config_t usb_serial_jtag_config = USB_SERIAL_JTAG_DRIVER_CONFIG_DEFAULT(); + TEST_ASSERT_EQUAL(ESP_OK, usb_serial_jtag_driver_install(&usb_serial_jtag_config)); + + /* register the vfs, create a FD used to interface the USJ */ + const esp_vfs_fs_ops_t *usj_vfs = esp_vfs_usb_serial_jtag_get_vfs(); + TEST_ASSERT_EQUAL(ESP_OK, esp_vfs_register_fs("/dev/test_usj", usj_vfs, 0, NULL)); + /* open in blocking mode */ + const int usj_fd = open("/dev/test_usj/0", 0); + TEST_ASSERT(usj_fd != -1); + *in_fd = usj_fd; + *out_fd = usj_fd; } -static void test_send_characters(int socket_fd, const char *msg) +static void test_usj_uninstall(const int fd) { - wait_ms(100); + /* close the stream */ + const int ret = close(fd); + TEST_ASSERT(ret != -1); + + /* unregister the vfs */ + TEST_ASSERT_EQUAL(ESP_OK, esp_vfs_unregister("/dev/test_usj")); - const size_t msg_len = strlen(msg); - const int nwrite = write(socket_fd, msg, msg_len); - TEST_ASSERT_EQUAL(msg_len, nwrite); + /* uninstall the driver */ + TEST_ASSERT_EQUAL(ESP_OK, usb_serial_jtag_driver_uninstall()); } -static void esp_cli_teardown(SemaphoreHandle_t *start_sem, SemaphoreHandle_t *done_sem, int socket_fd[2], - esp_linenoise_handle_t *linenoise_hdl, esp_cli_handle_t *cli_hdl) +static void test_esp_cli_teardown(SemaphoreHandle_t *start_sem, SemaphoreHandle_t *done_sem, + esp_linenoise_handle_t *linenoise_hdl, esp_cli_handle_t *cli_hdl) { /* destroy the instance of esp_cli */ TEST_ASSERT_EQUAL(ESP_OK, esp_cli_destroy(*cli_hdl)); @@ -142,9 +199,6 @@ static void esp_cli_teardown(SemaphoreHandle_t *start_sem, SemaphoreHandle_t *do vSemaphoreDelete(*start_sem); vSemaphoreDelete(*done_sem); - /* close the socketpair */ - test_socket_teardown(socket_fd); - s_on_stop_nb_of_calls = 0; s_on_exit_nb_of_calls = 0; s_on_enter_nb_of_calls = 0; @@ -152,8 +206,8 @@ static void esp_cli_teardown(SemaphoreHandle_t *start_sem, SemaphoreHandle_t *do s_post_executor_nb_of_calls = 0; } -static void esp_cli_setup(SemaphoreHandle_t *start_sem, SemaphoreHandle_t *done_sem, int socket_fd[2], - esp_linenoise_handle_t *linenoise_hdl, esp_cli_handle_t *cli_hdl) +static void test_esp_cli_setup(SemaphoreHandle_t *start_sem, SemaphoreHandle_t *done_sem, int in_fd, int out_fd, + esp_linenoise_handle_t *linenoise_hdl, esp_cli_handle_t *cli_hdl) { /* create semaphores */ *start_sem = xSemaphoreCreateBinary(); @@ -161,9 +215,6 @@ static void esp_cli_setup(SemaphoreHandle_t *start_sem, SemaphoreHandle_t *done_ *done_sem = xSemaphoreCreateBinary(); TEST_ASSERT_NOT_NULL(done_sem); - /* create the socket_pair */ - test_socket_setup(socket_fd); - /* ensure both semaphores are in the "taken/empty" state: taking with 0 timeout guarantees they are empty afterwards regardless of the create semantics on this FreeRTOS build. */ @@ -172,8 +223,8 @@ static void esp_cli_setup(SemaphoreHandle_t *start_sem, SemaphoreHandle_t *done_ esp_linenoise_config_t linenoise_config; esp_linenoise_get_instance_config_default(&linenoise_config); - linenoise_config.in_fd = socket_fd[0]; - linenoise_config.out_fd = socket_fd[0]; + linenoise_config.in_fd = in_fd; + linenoise_config.out_fd = out_fd; TEST_ASSERT_EQUAL(ESP_OK, esp_linenoise_create_instance(&linenoise_config, linenoise_hdl)); TEST_ASSERT_NOT_NULL(*linenoise_hdl); @@ -199,16 +250,19 @@ static void esp_cli_setup(SemaphoreHandle_t *start_sem, SemaphoreHandle_t *done_ s_post_executor_nb_of_calls = 0; } -TEST_CASE("esp_cli() loop calls all callbacks and exit on call to esp_cli_stop", "[esp_cli][host_test]") +TEST_CASE("esp_cli() loop calls callbacks and exit on call to esp_cli_stop", "[esp_cli]") { SemaphoreHandle_t start_sem, done_sem; esp_linenoise_handle_t linenoise_hdl; esp_cli_handle_t cli_hdl; - esp_cli_setup(&start_sem, &done_sem, s_socket_fd, &linenoise_hdl, &cli_hdl); + int in_fd, out_fd; + + test_uart_install(&in_fd, &out_fd); + test_esp_cli_setup(&start_sem, &done_sem, in_fd, out_fd, &linenoise_hdl, &cli_hdl); /* create the esp_cli instance task */ task_args_t args = {.start_sem = start_sem, .done_sem = done_sem, .hdl = cli_hdl}; - BaseType_t rc = xTaskCreate(esp_cli_task, "esp_cli_task", 4096, &args, 5, NULL); + BaseType_t rc = xTaskCreate(esp_cli_task, "esp_cli_task", 2048, &args, 5, NULL); TEST_ASSERT_EQUAL(pdPASS, rc); /* should fail before esp_cli instance is started */ @@ -218,21 +272,11 @@ TEST_CASE("esp_cli() loop calls all callbacks and exit on call to esp_cli_stop", TEST_ASSERT_NOT_EQUAL(ESP_OK, esp_cli_start(NULL)); TEST_ASSERT_EQUAL(ESP_OK, esp_cli_start(cli_hdl)); - wait_ms(100); - /* wait for the esp_cli task to signal it started */ TEST_ASSERT_TRUE(xSemaphoreTake(start_sem, pdMS_TO_TICKS(2000))); - /* send a dummy string new line terminated to trigger linenoise to return */ - const char *input_line = "dummy_message\n"; - test_send_characters(s_socket_fd[1], input_line); - /* wait for a bit so esp_cli() has time to loop back into esp_linenoise_get_line */ - wait_ms(500); - - /* check that pre-executor, post-executor callbacks are called */ - TEST_ASSERT_EQUAL(1, s_pre_executor_nb_of_calls); - TEST_ASSERT_EQUAL(1, s_post_executor_nb_of_calls); + vTaskDelay(pdMS_TO_TICKS(500)); /* stop esp_cli and wait for task to finish (emulate pthread_join) */ TEST_ASSERT_NOT_EQUAL(ESP_OK, esp_cli_stop(NULL)); @@ -245,45 +289,20 @@ TEST_CASE("esp_cli() loop calls all callbacks and exit on call to esp_cli_stop", TEST_ASSERT_EQUAL(1, s_on_stop_nb_of_calls); TEST_ASSERT_EQUAL(1, s_on_enter_nb_of_calls); TEST_ASSERT_EQUAL(1, s_on_exit_nb_of_calls); - TEST_ASSERT_EQUAL(2, s_pre_executor_nb_of_calls); - TEST_ASSERT_EQUAL(2, s_post_executor_nb_of_calls); /* make sure calling stop fails because the esp_cli instance is no longer running */ TEST_ASSERT_NOT_EQUAL(ESP_OK, esp_cli_stop(cli_hdl)); /* destroy the esp_cli instance */ TEST_ASSERT_NOT_EQUAL(ESP_OK, esp_cli_destroy(NULL)); - esp_cli_teardown(&start_sem, &done_sem, s_socket_fd, &linenoise_hdl, &cli_hdl); -} + test_esp_cli_teardown(&start_sem, &done_sem, &linenoise_hdl, &cli_hdl); -TEST_CASE("esp_cli() exits when esp_cli_stop() called from the task running esp_cli()", "[esp_cli][host_test]") -{ - SemaphoreHandle_t start_sem, done_sem; - esp_linenoise_handle_t linenoise_hdl; - esp_cli_handle_t cli_hdl; - esp_cli_setup(&start_sem, &done_sem, s_socket_fd, &linenoise_hdl, &cli_hdl); - - /* create the esp_cli instance task */ - task_args_t args = {.start_sem = start_sem, .done_sem = done_sem, .hdl = cli_hdl}; - BaseType_t rc = xTaskCreate(esp_cli_task, "esp_cli_task", 4096, &args, 5, NULL); - TEST_ASSERT_EQUAL(pdPASS, rc); - - /* start esp_cli instance */ - TEST_ASSERT_EQUAL(ESP_OK, esp_cli_start(cli_hdl)); - - wait_ms(100); - - /* wait for the esp_cli instance task to signal it started */ - TEST_ASSERT_TRUE(xSemaphoreTake(start_sem, pdMS_TO_TICKS(2000))); + /* uninstall the uart driver */ + test_uart_uninstall(in_fd); - /* send the quit command */ - const char *quit_cmd_line = "quit \n"; - test_send_characters(s_socket_fd[1], quit_cmd_line); - - /* wait for the esp_cli instance task to signal completion */ - TEST_ASSERT_TRUE(xSemaphoreTake(done_sem, pdMS_TO_TICKS(2000))); - - esp_cli_teardown(&start_sem, &done_sem, s_socket_fd, &linenoise_hdl, &cli_hdl); + /* make sure the cleanup of the deleted task is done to not bias + * the memory leak calculations */ + vTaskDelay(pdMS_TO_TICKS(500)); } TEST_CASE("create and destroy several instances of esp_cli", "[esp_cli]") @@ -293,11 +312,15 @@ TEST_CASE("create and destroy several instances of esp_cli", "[esp_cli]") SemaphoreHandle_t done_sem_a, done_sem_b; esp_cli_handle_t cli_hdl_a, cli_hdl_b; esp_linenoise_handle_t linenoise_hdl_a, linenoise_hdl_b; - int socket_fd_a[2], socket_fd_b[2]; + int in_fd_uart, in_fd_usj, out_fd_uart, out_fd_usj; + + /* install uart and usb serial jtag drivers */ + test_uart_install(&in_fd_uart, &out_fd_uart); + test_usj_install(&in_fd_usj, &out_fd_usj); /* create 2 instances of esp_cli*/ - esp_cli_setup(&start_sem_a, &done_sem_a, socket_fd_a, &linenoise_hdl_a, &cli_hdl_a); - esp_cli_setup(&start_sem_b, &done_sem_b, socket_fd_b, &linenoise_hdl_b, &cli_hdl_b); + test_esp_cli_setup(&start_sem_a, &done_sem_a, in_fd_uart, out_fd_uart, &linenoise_hdl_a, &cli_hdl_a); + test_esp_cli_setup(&start_sem_b, &done_sem_b, in_fd_usj, out_fd_usj, &linenoise_hdl_b, &cli_hdl_b); /* create the esp_cli instance task A */ task_args_t args_a = {.start_sem = start_sem_a, .done_sem = done_sem_a, .hdl = cli_hdl_a}; @@ -312,7 +335,7 @@ TEST_CASE("create and destroy several instances of esp_cli", "[esp_cli]") /* start esp_cli instance */ TEST_ASSERT_EQUAL(ESP_OK, esp_cli_start(cli_hdl_a)); TEST_ASSERT_EQUAL(ESP_OK, esp_cli_start(cli_hdl_b)); - wait_ms(500); + vTaskDelay(pdMS_TO_TICKS(500)); /* wait for the esp_cli instance tasks to signal it started */ TEST_ASSERT_TRUE(xSemaphoreTake(start_sem_a, pdMS_TO_TICKS(2000))); @@ -330,8 +353,49 @@ TEST_CASE("create and destroy several instances of esp_cli", "[esp_cli]") /* wait for the esp_cli instance task to signal completion */ TEST_ASSERT_TRUE(xSemaphoreTake(done_sem_b, pdMS_TO_TICKS(2000))); - esp_cli_teardown(&start_sem_a, &done_sem_a, socket_fd_a, &linenoise_hdl_a, &cli_hdl_a); - esp_cli_teardown(&start_sem_b, &done_sem_b, socket_fd_b, &linenoise_hdl_b, &cli_hdl_b); + test_esp_cli_teardown(&start_sem_a, &done_sem_a, &linenoise_hdl_a, &cli_hdl_a); + test_esp_cli_teardown(&start_sem_b, &done_sem_b, &linenoise_hdl_b, &cli_hdl_b); + + test_uart_uninstall(in_fd_uart); + test_usj_uninstall(in_fd_usj); + + /* make sure the cleanup of the deleted task is done to not bias + * the memory leak calculations */ + vTaskDelay(pdMS_TO_TICKS(500)); } -#endif // CONFIG_IDF_TARGET_LiNUX +TEST_CASE("create more esp_linenoise instances that possible based on CONFIG_ESP_LINENOISE_MAX_INSTANCE_NB", "[esp_cli]") +{ + esp_linenoise_config_t linenoise_config; + esp_linenoise_get_instance_config_default(&linenoise_config); + + const size_t hdl_array_size = CONFIG_ESP_LINENOISE_MAX_INSTANCE_NB + 1; + esp_linenoise_handle_t hdl_array[hdl_array_size]; + memset(hdl_array, 0x00, sizeof(hdl_array)); + + /* try to create more instances than allowed */ + for (size_t i = 0; i < hdl_array_size; i++) { + if (i < CONFIG_ESP_LINENOISE_MAX_INSTANCE_NB) { + /* we don't exceed the max number of instance yet, success expected */ + TEST_ASSERT_EQUAL(ESP_OK, esp_linenoise_create_instance(&linenoise_config, &hdl_array[i])); + TEST_ASSERT_NOT_NULL(hdl_array[i]); + } else { + /* we exceed the max number of instance, failure expected */ + TEST_ASSERT_NOT_EQUAL(ESP_OK, esp_linenoise_create_instance(&linenoise_config, &hdl_array[i])); + TEST_ASSERT_NULL(hdl_array[i]); + } + } + + /* free the instances that were successfully created */\ + for (size_t i = 0; i < hdl_array_size; i++) { + if (hdl_array[i] != NULL) { + TEST_ASSERT_EQUAL(ESP_OK, esp_linenoise_delete_instance(hdl_array[i])); + hdl_array[i] = NULL; + } + } + + /* try to create an instance again and deleted */ + TEST_ASSERT_EQUAL(ESP_OK, esp_linenoise_create_instance(&linenoise_config, &hdl_array[0])); + TEST_ASSERT_NOT_NULL(hdl_array[0]); + TEST_ASSERT_EQUAL(ESP_OK, esp_linenoise_delete_instance(hdl_array[0])); +} diff --git a/esp_cli/test_apps/main/test_main.c b/esp_cli/test_apps/main/test_main.c index cc8cce5b31..0b12710589 100644 --- a/esp_cli/test_apps/main/test_main.c +++ b/esp_cli/test_apps/main/test_main.c @@ -16,7 +16,10 @@ void setUp(void) void tearDown(void) { - unity_utils_evaluate_leaks_direct(0); + /* the threshold is necessary because on esp_linenoise instance + * creation, a bunch of heap memory is being used to initialize (e.g., + * eventfd and vfs internals) */ + unity_utils_evaluate_leaks_direct(500); } void app_main(void) diff --git a/esp_cli/test_apps/pytest_esp_cli.py b/esp_cli/test_apps/pytest_esp_cli.py index 88faff4c51..c569296942 100644 --- a/esp_cli/test_apps/pytest_esp_cli.py +++ b/esp_cli/test_apps/pytest_esp_cli.py @@ -5,12 +5,11 @@ from pathlib import Path - -@pytest.mark.host_test +@pytest.mark.generic @pytest.mark.skipif( not bool(glob.glob(f'{Path(__file__).parent.absolute()}/build*/')), reason="Skip the idf version that did not build" ) -@pytest.mark.parametrize('target', ['linux'], indirect=['target']) +@pytest.mark.parametrize('target', ['esp32s3'], indirect=['target']) def test_esp_cli(dut) -> None: dut.run_all_single_board_cases() diff --git a/esp_cli/test_apps/sdkconfig.defaults b/esp_cli/test_apps/sdkconfig.defaults index e1ee958dff..6ef89c7fbe 100644 --- a/esp_cli/test_apps/sdkconfig.defaults +++ b/esp_cli/test_apps/sdkconfig.defaults @@ -1,2 +1,4 @@ +CONFIG_ESP_LINENOISE_MAX_INSTANCE_NB=2 CONFIG_ESP_TASK_WDT_EN=n -CONFIG_ESP_CLI_HAS_QUIT_CMD=y \ No newline at end of file +CONFIG_ESP_CLI_HAS_QUIT_CMD=y +CONFIG_VFS_SUPPORT_IO=y \ No newline at end of file