ESP32 ESP-IDF console 组件
它包含了开发基于串口的交互式控制终端所需要的所有模块,主要支持以下功能:
行编辑,由 linenoise 库具体实现,它支持处理退格键和方向键,支持回看命令的历史记录,支持命令的自动补全和参数提示。
将命令行拆分为参数列表。
参数解析,由 argtable3 库具体实现,该库提供解析 GNU 样式的命令行参数的 API。
用于注册和调度命令的函数。
帮助创建 REPL (Read-Evaluate-Print-Loop) 环境的函数。
配置
Linenoise 库不需要显式地初始化,但是在调用行编辑函数之前,可能需要对某些配置的默认值稍作修改。
linenoiseClearScreen()
使用转义字符清除终端屏幕,并将光标定位在左上角。
linenoiseSetMultiLine()
在单行和多行编辑模式之间进行切换。单行模式下,如果命令的长度超过终端的宽度,会在行内滚动命令文本以显示文本的结尾,在这种情况下,文本的开头部分会被隐藏。单行模式在每次按下按键时发送给屏幕刷新的数据比较少,与多行模式相比更不容易发生故障。另一方面,在单行模式下编辑命令和复制命令将变得更加困难。默认情况下开启的是单行模式。
linenoiseAllowEmpty()
设置 linenoise 库收到空行的解析行为,设置为 true 时返回长度为零的字符串 ("") ,设置为 false 时返回 NULL。默认情况下,将返回长度为零的字符串。
linenoiseSetMaxLineLen()
设置 linenoise 库中每行的最大长度,默认长度为 4096 字节,可以通过更新该默认值来优化 RAM 内存的使用。
主循环
linenoise()
在大多数情况下,控制台应用程序都会具有相同的工作形式——在某个循环中不断读取输入的内容,然后解析再处理。 linenoise() 是专门用来获取用户按键输入的函数,当回车键被按下后会便返回完整的一行内容。因此可以用它来完成前面循环中的“读取”任务。
linenoiseFree()
必须调用此函数才能释放从 linenoise() 函数获取的命令行缓冲区。
提示和补全
linenoiseSetCompletionCallback()
当用户按下制表键时, linenoise 会调用 补全回调函数 ,该回调函数会检查当前已经输入的内容,然后调用 linenoiseAddCompletion() 函数来提供所有可能的补全后的命令列表。启用补全功能,需要事先调用 linenoiseSetCompletionCallback() 函数来注册补全回调函数。
console 组件提供了一个现成的函数来为注册的命令提供补全功能 esp_console_get_completion()。
linenoiseAddCompletion()
补全回调函数会通过调用此函数来通知 linenoise 库当前键入命令所有可能的补全结果。
linenoiseSetHintsCallback()
每当用户的输入改变时, linenoise 就会调用此回调函数,检查到目前为止输入的命令行内容,然后提供带有提示信息的字符串(例如命令参数列表),然后会在同一行上用不同的颜色显示出该文本。
linenoiseSetFreeHintsCallback()
如果 提示回调函数 返回的提示字符串是动态分配的或者需要以其它方式回收,就需要使用 linenoiseSetFreeHintsCallback() 注册具体的清理函数。
历史记录
linenoiseHistorySetMaxLen()
该函数设置要保留在内存中的最近输入的命令的数量。用户通过使用向上/向下箭头来导航历史记录。
linenoiseHistoryAdd()
Linenoise 不会自动向历史记录中添加命令,应用程序需要调用此函数来将命令字符串添加到历史记录中。
linenoiseHistorySave()
该函数将命令的历史记录从 RAM 中保存为文本文件,例如保存到 SD 卡或者 Flash 的文件系统中。
linenoiseHistoryLoad()
与 linenoiseHistorySave 相对应,从文件中加载历史记录。
linenoiseHistoryFree()
释放用于存储命令历史记录的内存。当使用完 linenoise 库后需要调用此函数。
将命令行拆分成参数列表
console 组件提供 esp_console_split_argv() 函数来将命令行字符串拆分为参数列表。该函数会返回参数的数量(argc)和一个指针数组,该指针数组可以作为 argv 参数传递给任何接受 argc,argv 格式参数的函数。
根据以下规则来将命令行拆分成参数列表:
参数由空格分隔
如果参数本身需要使用空格,可以使用 \ (反斜杠)对它们进行转义
其它能被识别的转义字符有 \\ (显示反斜杠本身)和 \" (显示双引号)
可以使用双引号来引用参数,引号只可能出现在参数的开头和结尾。参数中的引号必须如上所述进行转义。参数周围的引号会被 esp_console_split_argv() 函数删除
示例:
abc def 1 20 .3 ⟶ [ abc, def, 1, 20, .3 ]
abc "123 456" def ⟶ [ abc, 123 456, def ]
`a\ b\\c\" ⟶ [ a b\c" ]
参数解析
对于参数解析,console 组件使用 argtable3 库。
下面列出argtable3 库使用说明。
命令的注册与调度
console 组件包含了一些工具函数,用来注册命令,将用户输入的命令和已经注册的命令进行匹配,使用命令行输入的参数调用命令。
应用程序首先调用 esp_console_init() 来初始化命令注册模块,然后调用 esp_console_cmd_register() 函数注册命令处理程序。
对于每个命令,应用程序需要提供以下信息(需要以 esp_console_cmd_t 结构体的形式给出):
命令名字(不含空格的字符串)
帮助文档,解释该命令的用途
可选的提示文本,列出命令的参数。如果应用程序使用 Argtable3 库来解析参数,则可以通过提供指向 argtable 参数定义结构体的指针来自动生成提示文本
命令处理函数
命令注册模块还提供了其它函数:
esp_console_run()
该函数接受命令行字符串,使用 esp_console_split_argv() 函数将其拆分为 argc/argv 形式的参数列表,在已经注册的组件列表中查找命令,如果找到,则执行其对应的处理程序。
esp_console_register_help_command()
将 help 命令添加到已注册命令列表中,此命令将会以列表的方式打印所有注册的命令及其参数和帮助文本。
esp_console_get_completion()
与 linenoise 库中的 linenoiseSetCompletionCallback() 一同使用的回调函数,根据已经注册的命令列表为 linenoise 提供补全功能。
esp_console_get_hint()
与 linenoise 库中 linenoiseSetHintsCallback() 一同使用的回调函数,为 linenoise 提供已经注册的命令的参数提示功能。
初始化 REPL 环境
除了上述的各种函数,console 组件还提供了一些 API 来帮助创建一个基本的 REPL 环境。
在一个典型的 console 应用中,你只需要调用 esp_console_new_repl_uart(),它会为你初始化好构建在 UART 基础上的 REPL 环境,其中包括安装 UART 驱动,基本的 console 配置,创建一个新的线程来执行 REPL 任务,注册一些基本的命令(比如 help 命令)。
之后你可以使用 esp_console_cmd_register() 来注册其它命令。REPL 环境在初始化后需要再调用 esp_console_start_repl() 函数才能开始运行。
构建一个使用例子
以上来自ESP32开发文档,详细描述了组件功能和一些API。
如果第一次使用此功能,虽然都知道每个API和一些知识,但却始终无法搭好使用范例,一些API不清楚来龙去脉,参数不知如何是好等。此时只需要弄清参数解析argtable3 库和行编辑linenoise 库就不怕不会了。
参数解析argtable3 库和行编辑linenoise 库的使用方法后面再介绍。先分析一下ESP32怎样将这个库组合在一起的。
argtable3库的调用入口
乐鑫将argtable3库的入口作了一次封装,并定义了esp_console_cmd_t这个结构体,通过调用esp_console_cmd_t 中的.func 回调中调用argtable3 中的arg_parse()进行命令解析。
typedef struct {
/**
* Command name. Must not be NULL, must not contain spaces.
* The pointer must be valid until the call to esp_console_deinit.
*/
const char *command;
/**
* Help text for the command, shown by help command.
* If set, the pointer must be valid until the call to esp_console_deinit.
* If not set, the command will not be listed in 'help' output.
*/
const char *help;
/**
* Hint text, usually lists possible arguments.
* If set to NULL, and 'argtable' field is non-NULL, hint will be generated
* automatically
*/
const char *hint;
/**
* Pointer to a function which implements the command.
*/
esp_console_cmd_func_t func;
/**
* Array or structure of pointers to arg_xxx structures, may be NULL.
* Used to generate hint text if 'hint' is set to NULL.
* Array/structure which this field points to must end with an arg_end.
* Only used for the duration of esp_console_cmd_register call.
*/
void *argtable;
} esp_console_cmd_t;
因为argtable3对不周命令的解析都是通过arg_parse()返回值进行判断区分的,而ESP32却是通过esp_console_run进行命令匹配的。而这部分在示例代码有给出如下:
/* Main loop */
while(true) {
/* Get a line using linenoise.
* The line is returned when ENTER is pressed.
*/
char* line = linenoise(prompt);
if (line == NULL) { /* Break on EOF or error */
break;
}
/* Add the command to the history if not empty*/
if (strlen(line) > 0) {
linenoiseHistoryAdd(line);
#if CONFIG_STORE_HISTORY
/* Save command history to filesystem */
linenoiseHistorySave(HISTORY_PATH);
#endif
}
/* Try to run the command */
int ret;
esp_err_t err = esp_console_run(line, &ret);
if (err == ESP_ERR_NOT_FOUND) {
printf("Unrecognized command\n");
} else if (err == ESP_ERR_INVALID_ARG) {
// command was empty
} else if (err == ESP_OK && ret != ESP_OK) {
printf("Command returned non-zero error code: 0x%x (%s)\n", ret, esp_err_to_name(ret));
} else if (err != ESP_OK) {
printf("Internal error: %s\n", esp_err_to_name(err));
}
/* linenoise allocates line buffer on the heap, so need to free it */
linenoiseFree(line);
}
示例代码中也包含linenoise 库的使用。esp_console_run代码如下:
esp_err_t esp_console_run(const char *cmdline, int *cmd_ret)
{
if (s_tmp_line_buf == NULL) {
return ESP_ERR_INVALID_STATE;
}
char **argv = (char **) calloc(s_config.max_cmdline_args, sizeof(char *));
if (argv == NULL) {
return ESP_ERR_NO_MEM;
}
strlcpy(s_tmp_line_buf, cmdline, s_config.max_cmdline_length);
size_t argc = esp_console_split_argv(s_tmp_line_buf, argv,
s_config.max_cmdline_args);
if (argc == 0) {
free(argv);
return ESP_ERR_INVALID_ARG;
}
const cmd_item_t *cmd = find_command_by_name(argv[0]);
if (cmd == NULL) {
free(argv);
return ESP_ERR_NOT_FOUND;
}
*cmd_ret = (*cmd->func)(argc, argv);
free(argv);
return ESP_OK;
}