Skip to content

Commit 6f34019

Browse files
committed
feat(esp_repl): Add esp_linenoise and esp_commands dependencies
1 parent 0c99662 commit 6f34019

File tree

8 files changed

+362
-94
lines changed

8 files changed

+362
-94
lines changed

esp_repl/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,4 @@ set(srcs "esp_repl.c")
55
idf_component_register(
66
SRCS ${srcs}
77
INCLUDE_DIRS include
8-
PRIV_INCLUDE_DIRS private_include)
8+
REQUIRES esp_linenoise esp_commands)

esp_repl/README.md

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
# esp_repl Component
2+
3+
The `esp_repl` component provides a **Runtime Evaluation Loop (REPL)** mechanism for ESP-IDF-based applications.
4+
It allows developers to build interactive command-line interfaces (CLI) that support user-defined commands, history management, and customizable callbacks for command execution.
5+
6+
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.
7+
8+
---
9+
10+
## Features
11+
12+
- Modular REPL management with explicit `start` and `stop` control
13+
- Integration with [`esp_linenoise`](../esp_linenoise) for input and history
14+
- Support for command sets through [`esp_commands`](../esp_commands)
15+
- Configurable callbacks for:
16+
- Pre-execution processing
17+
- Post-execution handling
18+
- On-stop and on-exit events
19+
- Thread-safe operation using FreeRTOS semaphores
20+
- Optional command history persistence to filesystem
21+
22+
---
23+
24+
## Usage
25+
26+
A typical use case involves:
27+
28+
1. Initializing `esp_linenoise` and `esp_commands`
29+
2. Creating the REPL instance with `esp_repl_create()`
30+
3. Running `esp_repl()` in a task
31+
4. Starting and stopping the REPL using `esp_repl_start()` and `esp_repl_stop()`
32+
5. Destroying the instance with `esp_repl_destroy()` when done
33+
34+
### Example
35+
36+
```c
37+
#include "esp_repl.h"
38+
#include "esp_linenoise.h"
39+
#include "esp_commands.h"
40+
#include "esp_log.h"
41+
#include "freertos/FreeRTOS.h"
42+
#include "freertos/task.h"
43+
44+
static const char *TAG = "repl_example";
45+
46+
void repl_task(void *arg)
47+
{
48+
esp_repl_handle_t repl_hdl = (esp_repl_handle_t)arg;
49+
50+
// Run REPL loop (blocking until esp_repl_stop() is called)
51+
// The loop won't be reached until esp_repl_start() is called
52+
esp_repl(repl_hdl);
53+
54+
ESP_LOGI(TAG, "REPL task exiting");
55+
vTaskDelete(NULL);
56+
}
57+
58+
void app_main(void)
59+
{
60+
esp_err_t ret;
61+
esp_repl_handle_t repl = NULL;
62+
63+
// Initialize esp_linenoise (mandatory)
64+
esp_linenoise_handle_t esp_linenoise_hdl = esp_linenoise_create();
65+
66+
// Initialize command set (optional)
67+
esp_command_set_handle_t esp_commands_cmd_set = esp_commands_create();
68+
69+
esp_repl_config_t repl_cfg = {
70+
.linenoise_handle = esp_linenoise_hdl,
71+
.command_set_handle = esp_commands_cmd_set, /* optional */
72+
.max_cmd_line_size = 256,
73+
.history_save_path = "/spiffs/repl_history.txt", /* optional */
74+
};
75+
76+
ret = esp_repl_create(&repl, &repl_cfg);
77+
if (ret != ESP_OK) {
78+
ESP_LOGE(TAG, "Failed to create REPL instance (%s)", esp_err_to_name(ret));
79+
return;
80+
}
81+
82+
// Create REPL task
83+
if (xTaskCreate(repl_task, "repl_task", 4096, repl, 5, NULL) != pdPASS) {
84+
ESP_LOGE(TAG, "Failed to create REPL task");
85+
esp_repl_destroy(repl);
86+
return;
87+
}
88+
89+
ESP_LOGI(TAG, "Starting REPL...");
90+
ret = esp_repl_start(repl);
91+
if (ret != ESP_OK) {
92+
ESP_LOGE(TAG, "Failed to start REPL (%s)", esp_err_to_name(ret));
93+
esp_repl_destroy(repl);
94+
return;
95+
}
96+
97+
// Application logic can run in parallel while REPL runs in its own task
98+
// [...]
99+
vTaskDelay(pdMS_TO_TICKS(10000)); // Example delay
100+
101+
// Stop REPL
102+
ret = esp_repl_stop(repl);
103+
if (ret != ESP_OK) {
104+
ESP_LOGW(TAG, "Failed to stop REPL (%s)", esp_err_to_name(ret));
105+
}
106+
107+
ESP_LOGI(TAG, "REPL exited");
108+
109+
// Destroy REPL instance and clean up
110+
ret = esp_repl_destroy(repl);
111+
if (ret != ESP_OK) {
112+
ESP_LOGW(TAG, "Failed to destroy REPL instance cleanly (%s)", esp_err_to_name(ret));
113+
}
114+
115+
ESP_LOGI(TAG, "REPL example finished");
116+
}
117+
118+
```

esp_repl/esp_repl.c

Lines changed: 46 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
1+
12
/*
2-
* SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
3-
*
4-
* SPDX-License-Identifier: Apache-2.0
5-
*/
3+
* SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
4+
*
5+
* SPDX-License-Identifier: Apache-2.0
6+
*/
67
#include <stdbool.h>
78
#include <string.h>
89
#include "freertos/FreeRTOS.h"
910
#include "freertos/semphr.h"
1011
#include "esp_repl.h"
1112
#include "esp_err.h"
12-
13+
#include "esp_commands.h"
14+
#include "esp_linenoise.h"
1315
typedef enum {
1416
ESP_REPL_STATE_RUNNING,
1517
ESP_REPL_STATE_STOPPED
@@ -27,15 +29,14 @@ typedef struct esp_repl_instance {
2729
} esp_repl_instance_t;
2830

2931
#define ESP_REPL_CHECK_INSTANCE(handle) do { \
30-
if((handle == NULL) || ((esp_repl_instance_t*)handle->self != (esp_repl_instance_t*)handle)) { \
31-
return ESP_ERR_INVALID_ARG; \
32-
} \
33-
} while(0)
32+
if((handle == NULL) || ((esp_repl_instance_t*)handle->self != (esp_repl_instance_t*)handle)) { \
33+
return ESP_ERR_INVALID_ARG; \
34+
} \
35+
} while(0)
3436

35-
esp_err_t esp_repl_create(esp_repl_handle_t *handle, const esp_repl_config_t *config)
37+
esp_err_t esp_repl_create(esp_repl_handle_t *handle, const esp_repl_config_t *config)
3638
{
37-
if ((config->executor.func == NULL) ||
38-
(config->reader.func == NULL) ||
39+
if ((config->linenoise_handle == NULL) ||
3940
(config->max_cmd_line_size == 0)) {
4041
return ESP_ERR_INVALID_ARG;
4142
}
@@ -55,7 +56,7 @@ esp_err_t esp_repl_create(esp_repl_handle_t *handle, const esp_repl_config_t *
5556
}
5657

5758
/* take the mutex right away to prevent the task to start running until
58-
* the user explicitly calls esp_repl_start */
59+
* the user explicitly calls esp_repl_start */
5960
xSemaphoreTake(instance->state.mux, portMAX_DELAY);
6061

6162
*handle = instance;
@@ -106,7 +107,21 @@ esp_err_t esp_repl_stop(esp_repl_handle_t handle)
106107
/* update the state to force the while loop in esp_repl to return */
107108
state->state = ESP_REPL_STATE_STOPPED;
108109

109-
/* Call the on_stop callback to let the user unblock reader.func, if provided */
110+
/* Call the abort function from esp_linenoise to force esp_linenoise_get_line to return.
111+
* This function is expected to return ESP_OK only if the user has registered a custom
112+
* read to the esp_linenoise instance and if the abort function succeeds.
113+
* ESP_ERR_INVALID_STATE is expected to be returned by esp_linenoise_abort if it is called
114+
* when the user has registered a custom read to the esp_linenoise instance. From the point
115+
* of view of esp_repl_stop, this return value if indicating that the user will have to take
116+
* care of returning from its own custom read by himself through the call of the on_stop callback,
117+
* therefore set the return value of esp_repl_stop to ESP_OK. */
118+
esp_err_t ret_val = esp_linenoise_abort(config->linenoise_handle);
119+
if (ret_val == ESP_ERR_INVALID_STATE) {
120+
ret_val = ESP_OK;
121+
}
122+
123+
/* Call the on_stop callback to let the user unblock esp_linenoise
124+
* if a custom read is provided */
110125
if (config->on_stop.func != NULL) {
111126
config->on_stop.func(config->on_stop.ctx, handle);
112127
}
@@ -117,7 +132,7 @@ esp_err_t esp_repl_stop(esp_repl_handle_t handle)
117132
/* give it back so destroy can also take/give symmetrically */
118133
xSemaphoreGive(state->mux);
119134

120-
return ESP_OK;
135+
return ret_val;
121136
}
122137

123138
void esp_repl(esp_repl_handle_t handle)
@@ -137,17 +152,28 @@ void esp_repl(esp_repl_handle_t handle)
137152
}
138153

139154
/* Waiting for task notify. This happens when `esp_repl_start`
140-
* function is called. */
155+
* function is called. */
141156
xSemaphoreTake(state->mux, portMAX_DELAY);
142157

158+
esp_linenoise_handle_t l_hdl = config->linenoise_handle;
159+
esp_command_set_handle_t c_set = config->command_set_handle;
160+
143161
/* REPL loop */
144162
while (state->state == ESP_REPL_STATE_RUNNING) {
145163

146164
/* try to read a command line */
147-
const esp_err_t read_ret = config->reader.func(config->reader.ctx, cmd_line, cmd_line_size);
165+
const esp_err_t read_ret = esp_linenoise_get_line(l_hdl, cmd_line, cmd_line_size);
166+
167+
/* Add the command to the history */
168+
esp_linenoise_history_add(l_hdl, cmd_line);
169+
170+
/* Save command history to filesystem */
171+
if (config->history_save_path) {
172+
esp_linenoise_history_save(l_hdl, config->history_save_path);
173+
}
148174

149175
/* forward the raw command line to the pre executor callback (e.g., save in history).
150-
* this callback is not necessary for the user to register, continue if it isn't */
176+
* this callback is not necessary for the user to register, continue if it isn't */
151177
if (config->pre_executor.func != NULL) {
152178
config->pre_executor.func(config->pre_executor.ctx, cmd_line, read_ret);
153179
}
@@ -159,10 +185,10 @@ void esp_repl(esp_repl_handle_t handle)
159185

160186
/* try to run the command */
161187
int cmd_func_ret;
162-
const esp_err_t exec_ret = config->executor.func(config->executor.ctx, cmd_line, &cmd_func_ret);
188+
const esp_err_t exec_ret = esp_commands_execute(c_set, -1, cmd_line, &cmd_func_ret);
163189

164190
/* forward the raw command line to the post executor callback (e.g., save in history).
165-
* this callback is not necessary for the user to register, continue if it isn't */
191+
* this callback is not necessary for the user to register, continue if it isn't */
166192
if (config->post_executor.func != NULL) {
167193
config->post_executor.func(config->post_executor.ctx, cmd_line, exec_ret, cmd_func_ret);
168194
}

esp_repl/idf_component.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@ version: "1.0.0"
22
description: "esp_repl - Read Eval Print Loop component"
33
url: https://github.com/espressif/idf-extra-components/tree/master/esp_repl
44
dependencies:
5-
idf: ">=6.0"
5+
SoucheSouche/esp_linenoise:
6+
version: "*"
7+
registry_url: https://components-staging.espressif.com
8+
SoucheSouche/esp_commands:
9+
version: "*"
10+
registry_url: https://components-staging.espressif.com
611
sbom:
712
manifests:
813
- path: sbom_esp_repl.yml

esp_repl/include/esp_repl.h

Lines changed: 10 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -11,31 +11,14 @@ extern "C" {
1111

1212
#include <stdbool.h>
1313
#include "esp_err.h"
14+
#include "esp_linenoise.h"
15+
#include "esp_commands.h"
1416

1517
/**
1618
* @brief Handle to a REPL instance.
1719
*/
1820
typedef struct esp_repl_instance *esp_repl_handle_t;
1921

20-
/**
21-
* @brief Function prototype for reading input for the REPL.
22-
*
23-
* @param ctx User-defined context pointer.
24-
* @param buf Buffer to store the read data.
25-
* @param buf_size Size of the buffer in bytes.
26-
*
27-
* @return ESP_OK on success, error code otherwise.
28-
*/
29-
typedef esp_err_t (*esp_repl_reader_fn)(void *ctx, char *buf, size_t buf_size);
30-
31-
/**
32-
* @brief Reader configuration structure for the REPL.
33-
*/
34-
typedef struct esp_repl_reader {
35-
esp_repl_reader_fn func; /**!< Function to read input */
36-
void *ctx; /**!< Context passed to the reader function */
37-
} esp_repl_reader_t;
38-
3922
/**
4023
* @brief Function prototype called before executing a command.
4124
*
@@ -55,25 +38,6 @@ typedef struct esp_repl_pre_executor {
5538
void *ctx; /**!< Context passed to the pre-executor function */
5639
} esp_repl_pre_executor_t;
5740

58-
/**
59-
* @brief Function prototype to execute a REPL command.
60-
*
61-
* @param ctx User-defined context pointer.
62-
* @param buf Null-terminated command string.
63-
* @param ret_val Pointer to store the command return value.
64-
*
65-
* @return ESP_OK on success, error code otherwise.
66-
*/
67-
typedef esp_err_t (*esp_repl_executor_fn)(void *ctx, const char *buf, int *ret_val);
68-
69-
/**
70-
* @brief Executor configuration structure for the REPL.
71-
*/
72-
typedef struct esp_repl_executor {
73-
esp_repl_executor_fn func; /**!< Function to execute commands */
74-
void *ctx; /**!< Context passed to the executor function */
75-
} esp_repl_executor_t;
76-
7741
/**
7842
* @brief Function prototype called after executing a command.
7943
*
@@ -133,13 +97,14 @@ typedef struct esp_repl_on_exit {
13397
* @brief Configuration structure to initialize a REPL instance.
13498
*/
13599
typedef struct esp_repl_config {
136-
size_t max_cmd_line_size; /**!< Maximum allowed command line size */
137-
esp_repl_reader_t reader; /**!< Reader callback and context */
138-
esp_repl_pre_executor_t pre_executor; /**!< Pre-executor callback and context */
139-
esp_repl_executor_t executor; /**!< Executor callback and context */
140-
esp_repl_post_executor_t post_executor; /**!< Post-executor callback and context */
141-
esp_repl_on_stop_t on_stop; /**!< Stop callback and context */
142-
esp_repl_on_exit_t on_exit; /**!< Exit callback and context */
100+
esp_linenoise_handle_t linenoise_handle; /**!< Handle to the esp_linenoise instance */
101+
esp_command_set_handle_t command_set_handle; /**!< Handle to a set of commands */
102+
size_t max_cmd_line_size; /**!< Maximum allowed command line size */
103+
const char *history_save_path; /**!< Path to file to save the history */
104+
esp_repl_pre_executor_t pre_executor; /**!< Pre-executor callback and context */
105+
esp_repl_post_executor_t post_executor; /**!< Post-executor callback and context */
106+
esp_repl_on_stop_t on_stop; /**!< Stop callback and context */
107+
esp_repl_on_exit_t on_exit; /**!< Exit callback and context */
143108
} esp_repl_config_t;
144109

145110
/**
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1+
12
idf_component_register(SRCS "test_esp_repl.c" "test_main.c"
2-
PRIV_INCLUDE_DIRS "." "include"
3+
PRIV_INCLUDE_DIRS "."
34
PRIV_REQUIRES unity
45
WHOLE_ARCHIVE)

0 commit comments

Comments
 (0)