命令行接口(CLI)框架开发:构建灵活的嵌入式交互系统¶
学习目标¶
完成本教程后,你将能够:
- 理解CLI框架的设计原理和架构
- 掌握命令解析和参数处理技术
- 实现命令注册和动态扩展机制
- 设计完善的帮助系统和错误处理
- 实现命令历史和自动补全功能
- 掌握CLI框架的测试和调试方法
- 能够设计和实现一个完整的CLI框架
- 理解CLI在嵌入式系统中的应用场景
前置要求¶
在开始学习前,建议你具备:
知识要求: - 熟悉C语言(指针、结构体、函数指针) - 了解字符串处理和解析技术 - 掌握状态机设计模式 - 理解模块化编程思想
技能要求: - 能够使用串口通信 - 会使用终端工具(PuTTY、minicom等) - 了解基本的调试技术
开发环境: - 任何支持C语言的嵌入式开发板 - 串口调试工具 - 开发IDE(Arduino IDE、STM32CubeIDE等)
CLI框架概述¶
什么是CLI框架¶
CLI(Command Line Interface,命令行接口)框架是一种通过文本命令与系统交互的接口系统。在嵌入式系统中,CLI提供了一种灵活、强大的调试和控制方式。
核心概念:
- 命令(Command)
- 用户输入的操作指令
- 由命令名和参数组成
-
触发特定的功能执行
-
参数(Arguments)
- 命令的输入数据
- 可以是必需或可选的
-
支持多种数据类型
-
解析器(Parser)
- 分析用户输入
- 提取命令和参数
-
验证输入合法性
-
执行器(Executor)
- 调用对应的命令处理函数
- 传递解析后的参数
- 返回执行结果
CLI框架的优势¶
为什么要使用CLI框架?
- 调试便利
- 实时查看系统状态
- 动态修改参数配置
-
快速定位问题
-
灵活控制
- 无需重新编译即可测试
- 支持复杂的操作序列
-
易于自动化测试
-
可扩展性
- 动态添加新命令
- 模块化命令管理
-
支持插件机制
-
用户友好
- 提供帮助信息
- 命令自动补全
-
历史记录功能
-
生产应用
- 现场调试和维护
- 系统配置和升级
- 故障诊断和日志查看
CLI框架的应用场景¶
| 应用领域 | 典型场景 | 命令示例 |
|---|---|---|
| 系统调试 | 查看状态、修改参数 | status, set param value |
| 设备控制 | 控制外设、测试功能 | led on, motor speed 100 |
| 网络配置 | 配置IP、查看连接 | ifconfig, ping host |
| 文件管理 | 读写文件、查看目录 | ls, cat file.txt |
| 性能分析 | 查看资源、性能测试 | top, benchmark |
CLI框架架构¶
┌─────────────────────────────────────────┐
│ 用户输入层 │
│ (串口/USB/网络/本地终端) │
└──────────────┬──────────────────────────┘
│
↓
┌─────────────────────────────────────────┐
│ 输入缓冲和编辑层 │
│ (行编辑、历史记录、自动补全) │
└──────────────┬──────────────────────────┘
│
↓
┌─────────────────────────────────────────┐
│ 命令解析层 │
│ (词法分析、语法分析、参数提取) │
└──────────────┬──────────────────────────┘
│
↓
┌─────────────────────────────────────────┐
│ 命令查找和匹配层 │
│ (命令表查找、权限检查) │
└──────────────┬──────────────────────────┘
│
↓
┌─────────────────────────────────────────┐
│ 命令执行层 │
│ (调用处理函数、传递参数) │
└──────────────┬──────────────────────────┘
│
↓
┌─────────────────────────────────────────┐
│ 输出格式化层 │
│ (结果格式化、错误提示) │
└─────────────────────────────────────────┘
准备工作¶
硬件准备¶
| 名称 | 数量 | 说明 | 参考型号 |
|---|---|---|---|
| 开发板 | 1 | 任意MCU开发板 | Arduino Uno, ESP32, STM32 |
| USB线 | 1 | 用于串口通信 | USB-A to Micro/Type-C |
| LED灯 | 3 | 用于演示控制 | 5mm LED |
| 电阻 | 3 | 220Ω限流电阻 | ¼W |
软件准备¶
开发环境: - Arduino IDE 2.0+ 或 - PlatformIO 或 - STM32CubeIDE
串口工具: - PuTTY(Windows) - minicom(Linux) - screen(macOS) - Arduino Serial Monitor
推荐配置: - 波特率:115200 - 数据位:8 - 停止位:1 - 校验位:无 - 流控:无
步骤1:实现基础CLI框架¶
1.1 定义核心数据结构¶
首先定义CLI框架的基本数据结构:
// cli_framework.h
#ifndef CLI_FRAMEWORK_H
#define CLI_FRAMEWORK_H
#include <stdint.h>
#include <stdbool.h>
// 配置参数
#define CLI_MAX_COMMANDS 20 // 最大命令数
#define CLI_MAX_CMD_LEN 16 // 命令名最大长度
#define CLI_MAX_ARGS 8 // 最大参数数量
#define CLI_MAX_ARG_LEN 32 // 参数最大长度
#define CLI_INPUT_BUF_SIZE 128 // 输入缓冲区大小
#define CLI_HISTORY_SIZE 5 // 历史记录数量
// 命令处理函数类型
typedef int (*cli_cmd_func_t)(int argc, char *argv[]);
// 命令结构体
typedef struct {
const char *name; // 命令名
const char *help; // 帮助信息
cli_cmd_func_t func; // 处理函数
uint8_t min_args; // 最少参数数量
uint8_t max_args; // 最多参数数量
} cli_command_t;
// CLI上下文结构体
typedef struct {
cli_command_t commands[CLI_MAX_COMMANDS]; // 命令表
uint8_t cmd_count; // 已注册命令数
char input_buf[CLI_INPUT_BUF_SIZE]; // 输入缓冲区
uint16_t input_pos; // 当前输入位置
char *argv[CLI_MAX_ARGS]; // 参数数组
int argc; // 参数数量
} cli_context_t;
// 公共接口
void cli_init(void);
int cli_register_command(const char *name, const char *help,
cli_cmd_func_t func, uint8_t min_args, uint8_t max_args);
void cli_process_char(char c);
void cli_run(void);
#endif // CLI_FRAMEWORK_H
代码说明:
- cli_command_t:命令定义结构,包含命令名、帮助信息和处理函数
- cli_context_t:CLI上下文,管理所有命令和输入状态
- 使用函数指针实现命令的动态注册和调用
1.2 实现命令注册机制¶
// cli_framework.c
#include "cli_framework.h"
#include <string.h>
#include <stdio.h>
// 全局CLI上下文
static cli_context_t g_cli;
// 初始化CLI框架
void cli_init(void) {
memset(&g_cli, 0, sizeof(cli_context_t));
g_cli.cmd_count = 0;
g_cli.input_pos = 0;
Serial.println("\n=================================");
Serial.println(" Embedded CLI Framework v1.0");
Serial.println("=================================");
Serial.println("Type 'help' for available commands\n");
Serial.print("> ");
}
// 注册命令
int cli_register_command(const char *name, const char *help,
cli_cmd_func_t func, uint8_t min_args, uint8_t max_args) {
// 检查是否已满
if (g_cli.cmd_count >= CLI_MAX_COMMANDS) {
Serial.println("Error: Command table full");
return -1;
}
// 检查命令名是否已存在
for (int i = 0; i < g_cli.cmd_count; i++) {
if (strcmp(g_cli.commands[i].name, name) == 0) {
Serial.print("Error: Command '");
Serial.print(name);
Serial.println("' already exists");
return -1;
}
}
// 添加命令
cli_command_t *cmd = &g_cli.commands[g_cli.cmd_count];
cmd->name = name;
cmd->help = help;
cmd->func = func;
cmd->min_args = min_args;
cmd->max_args = max_args;
g_cli.cmd_count++;
return 0;
}
代码说明:
- cli_init():初始化CLI框架,清空命令表
- cli_register_command():注册新命令,检查重复和容量
- 使用静态全局变量管理CLI状态
1.3 实现命令解析器¶
// 解析输入行为命令和参数
static int cli_parse_line(char *line) {
g_cli.argc = 0;
char *token;
char *saveptr;
// 使用strtok_r分割字符串(线程安全)
token = strtok_r(line, " \t", &saveptr);
while (token != NULL && g_cli.argc < CLI_MAX_ARGS) {
g_cli.argv[g_cli.argc++] = token;
token = strtok_r(NULL, " \t", &saveptr);
}
return g_cli.argc;
}
// 查找命令
static cli_command_t* cli_find_command(const char *name) {
for (int i = 0; i < g_cli.cmd_count; i++) {
if (strcmp(g_cli.commands[i].name, name) == 0) {
return &g_cli.commands[i];
}
}
return NULL;
}
// 执行命令
static void cli_execute_command(void) {
if (g_cli.argc == 0) {
return; // 空行,忽略
}
// 查找命令
cli_command_t *cmd = cli_find_command(g_cli.argv[0]);
if (cmd == NULL) {
Serial.print("Error: Unknown command '");
Serial.print(g_cli.argv[0]);
Serial.println("'");
Serial.println("Type 'help' for available commands");
return;
}
// 检查参数数量
int arg_count = g_cli.argc - 1; // 减去命令名本身
if (arg_count < cmd->min_args) {
Serial.print("Error: Too few arguments (need at least ");
Serial.print(cmd->min_args);
Serial.println(")");
Serial.print("Usage: ");
Serial.println(cmd->help);
return;
}
if (arg_count > cmd->max_args) {
Serial.print("Error: Too many arguments (max ");
Serial.print(cmd->max_args);
Serial.println(")");
return;
}
// 执行命令
int result = cmd->func(g_cli.argc, g_cli.argv);
if (result != 0) {
Serial.print("Command returned error code: ");
Serial.println(result);
}
}
代码说明:
- cli_parse_line():将输入行分割为命令和参数
- cli_find_command():在命令表中查找命令
- cli_execute_command():验证参数并执行命令
1.4 实现输入处理¶
// 处理单个字符输入
void cli_process_char(char c) {
// 处理回车
if (c == '\r' || c == '\n') {
Serial.println(); // 换行
// 添加字符串结束符
g_cli.input_buf[g_cli.input_pos] = '\0';
// 解析并执行命令
if (g_cli.input_pos > 0) {
cli_parse_line(g_cli.input_buf);
cli_execute_command();
}
// 重置输入缓冲区
g_cli.input_pos = 0;
memset(g_cli.input_buf, 0, CLI_INPUT_BUF_SIZE);
// 显示提示符
Serial.print("> ");
return;
}
// 处理退格
if (c == '\b' || c == 127) { // Backspace or DEL
if (g_cli.input_pos > 0) {
g_cli.input_pos--;
Serial.print("\b \b"); // 删除字符的VT100序列
}
return;
}
// 处理可打印字符
if (c >= 32 && c <= 126) {
if (g_cli.input_pos < CLI_INPUT_BUF_SIZE - 1) {
g_cli.input_buf[g_cli.input_pos++] = c;
Serial.print(c); // 回显字符
}
return;
}
// 忽略其他控制字符
}
// CLI主循环
void cli_run(void) {
while (Serial.available() > 0) {
char c = Serial.read();
cli_process_char(c);
}
}
代码说明:
- cli_process_char():处理单个输入字符
- 支持回车执行、退格删除、字符回显
- cli_run():在主循环中调用,处理所有可用输入
步骤2:实现内置命令¶
2.1 实现help命令¶
// 内置命令:help
int cmd_help(int argc, char *argv[]) {
Serial.println("\nAvailable commands:");
Serial.println("==================");
for (int i = 0; i < g_cli.cmd_count; i++) {
cli_command_t *cmd = &g_cli.commands[i];
// 打印命令名(左对齐,宽度15)
Serial.print(" ");
Serial.print(cmd->name);
// 填充空格
int spaces = 15 - strlen(cmd->name);
for (int j = 0; j < spaces; j++) {
Serial.print(" ");
}
// 打印帮助信息
Serial.println(cmd->help);
}
Serial.println();
return 0;
}
2.2 实现clear命令¶
// 内置命令:clear(清屏)
int cmd_clear(int argc, char *argv[]) {
// 发送VT100清屏序列
Serial.print("\033[2J"); // 清屏
Serial.print("\033[H"); // 光标移到左上角
Serial.println("Embedded CLI Framework v1.0");
Serial.println("Type 'help' for available commands\n");
return 0;
}
2.3 实现echo命令¶
// 内置命令:echo(回显参数)
int cmd_echo(int argc, char *argv[]) {
for (int i = 1; i < argc; i++) {
Serial.print(argv[i]);
if (i < argc - 1) {
Serial.print(" ");
}
}
Serial.println();
return 0;
}
2.4 实现version命令¶
// 内置命令:version(显示版本信息)
int cmd_version(int argc, char *argv[]) {
Serial.println("\nEmbedded CLI Framework");
Serial.println("Version: 1.0.0");
Serial.println("Build Date: " __DATE__ " " __TIME__);
Serial.println("Author: Embedded Platform");
Serial.println();
return 0;
}
步骤3:实现应用命令¶
3.1 LED控制命令¶
// LED引脚定义
#define LED_RED_PIN 9
#define LED_GREEN_PIN 10
#define LED_BLUE_PIN 11
// 命令:led <color> <on|off>
int cmd_led(int argc, char *argv[]) {
if (argc < 3) {
Serial.println("Usage: led <red|green|blue|all> <on|off>");
return -1;
}
const char *color = argv[1];
const char *state = argv[2];
// 解析状态
bool turn_on;
if (strcmp(state, "on") == 0) {
turn_on = true;
} else if (strcmp(state, "off") == 0) {
turn_on = false;
} else {
Serial.println("Error: State must be 'on' or 'off'");
return -1;
}
// 控制LED
if (strcmp(color, "red") == 0) {
digitalWrite(LED_RED_PIN, turn_on ? HIGH : LOW);
Serial.print("Red LED turned ");
Serial.println(turn_on ? "ON" : "OFF");
} else if (strcmp(color, "green") == 0) {
digitalWrite(LED_GREEN_PIN, turn_on ? HIGH : LOW);
Serial.print("Green LED turned ");
Serial.println(turn_on ? "ON" : "OFF");
} else if (strcmp(color, "blue") == 0) {
digitalWrite(LED_BLUE_PIN, turn_on ? HIGH : LOW);
Serial.print("Blue LED turned ");
Serial.println(turn_on ? "ON" : "OFF");
} else if (strcmp(color, "all") == 0) {
digitalWrite(LED_RED_PIN, turn_on ? HIGH : LOW);
digitalWrite(LED_GREEN_PIN, turn_on ? HIGH : LOW);
digitalWrite(LED_BLUE_PIN, turn_on ? HIGH : LOW);
Serial.print("All LEDs turned ");
Serial.println(turn_on ? "ON" : "OFF");
} else {
Serial.println("Error: Unknown color");
Serial.println("Available: red, green, blue, all");
return -1;
}
return 0;
}
3.2 系统状态命令¶
// 命令:status(显示系统状态)
int cmd_status(int argc, char *argv[]) {
Serial.println("\n=== System Status ===");
// 运行时间
unsigned long uptime = millis() / 1000;
Serial.print("Uptime: ");
Serial.print(uptime / 3600);
Serial.print("h ");
Serial.print((uptime % 3600) / 60);
Serial.print("m ");
Serial.print(uptime % 60);
Serial.println("s");
// 内存使用(Arduino)
extern int __heap_start, *__brkval;
int free_memory;
if ((int)__brkval == 0) {
free_memory = ((int)&free_memory) - ((int)&__heap_start);
} else {
free_memory = ((int)&free_memory) - ((int)__brkval);
}
Serial.print("Free Memory: ");
Serial.print(free_memory);
Serial.println(" bytes");
// LED状态
Serial.println("\nLED Status:");
Serial.print(" Red: ");
Serial.println(digitalRead(LED_RED_PIN) ? "ON" : "OFF");
Serial.print(" Green: ");
Serial.println(digitalRead(LED_GREEN_PIN) ? "ON" : "OFF");
Serial.print(" Blue: ");
Serial.println(digitalRead(LED_BLUE_PIN) ? "ON" : "OFF");
Serial.println("====================\n");
return 0;
}
3.3 配置读写命令¶
// 简单的配置存储(使用全局变量)
static struct {
int brightness;
int blink_interval;
char device_name[32];
} g_config = {
.brightness = 100,
.blink_interval = 500,
.device_name = "MyDevice"
};
// 命令:config get <key>
// 命令:config set <key> <value>
int cmd_config(int argc, char *argv[]) {
if (argc < 2) {
Serial.println("Usage:");
Serial.println(" config get <key>");
Serial.println(" config set <key> <value>");
Serial.println("\nAvailable keys:");
Serial.println(" brightness, blink_interval, device_name");
return -1;
}
const char *action = argv[1];
if (strcmp(action, "get") == 0) {
if (argc < 3) {
Serial.println("Error: Missing key");
return -1;
}
const char *key = argv[2];
if (strcmp(key, "brightness") == 0) {
Serial.print("brightness = ");
Serial.println(g_config.brightness);
} else if (strcmp(key, "blink_interval") == 0) {
Serial.print("blink_interval = ");
Serial.println(g_config.blink_interval);
} else if (strcmp(key, "device_name") == 0) {
Serial.print("device_name = ");
Serial.println(g_config.device_name);
} else {
Serial.println("Error: Unknown key");
return -1;
}
} else if (strcmp(action, "set") == 0) {
if (argc < 4) {
Serial.println("Error: Missing key or value");
return -1;
}
const char *key = argv[2];
const char *value = argv[3];
if (strcmp(key, "brightness") == 0) {
int val = atoi(value);
if (val < 0 || val > 100) {
Serial.println("Error: Brightness must be 0-100");
return -1;
}
g_config.brightness = val;
Serial.println("OK");
} else if (strcmp(key, "blink_interval") == 0) {
int val = atoi(value);
if (val < 100 || val > 5000) {
Serial.println("Error: Interval must be 100-5000 ms");
return -1;
}
g_config.blink_interval = val;
Serial.println("OK");
} else if (strcmp(key, "device_name") == 0) {
strncpy(g_config.device_name, value, sizeof(g_config.device_name) - 1);
g_config.device_name[sizeof(g_config.device_name) - 1] = '\0';
Serial.println("OK");
} else {
Serial.println("Error: Unknown key");
return -1;
}
} else {
Serial.println("Error: Action must be 'get' or 'set'");
return -1;
}
return 0;
}
步骤4:实现高级特性¶
4.1 命令历史记录¶
// 历史记录结构
static struct {
char history[CLI_HISTORY_SIZE][CLI_INPUT_BUF_SIZE];
int count;
int current;
} g_history = {0};
// 添加到历史记录
static void cli_add_history(const char *line) {
if (strlen(line) == 0) {
return; // 不保存空行
}
// 检查是否与最后一条相同
if (g_history.count > 0) {
int last = (g_history.count - 1) % CLI_HISTORY_SIZE;
if (strcmp(g_history.history[last], line) == 0) {
return; // 不保存重复命令
}
}
// 添加到历史记录
int index = g_history.count % CLI_HISTORY_SIZE;
strncpy(g_history.history[index], line, CLI_INPUT_BUF_SIZE - 1);
g_history.history[index][CLI_INPUT_BUF_SIZE - 1] = '\0';
g_history.count++;
g_history.current = g_history.count;
}
// 获取历史记录
static const char* cli_get_history(int offset) {
if (g_history.count == 0) {
return NULL;
}
int index = g_history.current + offset;
if (index < 0 || index >= g_history.count) {
return NULL;
}
g_history.current = index;
return g_history.history[index % CLI_HISTORY_SIZE];
}
// 处理方向键(上下翻历史)
static void cli_handle_arrow_key(char c) {
static int arrow_state = 0;
static char arrow_seq[3] = {0};
// VT100方向键序列:ESC [ A/B/C/D
if (c == 27) { // ESC
arrow_state = 1;
return;
}
if (arrow_state == 1 && c == '[') {
arrow_state = 2;
return;
}
if (arrow_state == 2) {
arrow_state = 0;
if (c == 'A') { // 上箭头
const char *hist = cli_get_history(-1);
if (hist != NULL) {
// 清除当前行
for (int i = 0; i < g_cli.input_pos; i++) {
Serial.print("\b \b");
}
// 显示历史命令
strcpy(g_cli.input_buf, hist);
g_cli.input_pos = strlen(hist);
Serial.print(hist);
}
} else if (c == 'B') { // 下箭头
const char *hist = cli_get_history(1);
if (hist != NULL) {
// 清除当前行
for (int i = 0; i < g_cli.input_pos; i++) {
Serial.print("\b \b");
}
// 显示历史命令
strcpy(g_cli.input_buf, hist);
g_cli.input_pos = strlen(hist);
Serial.print(hist);
}
}
}
}
// 命令:history(显示历史记录)
int cmd_history(int argc, char *argv[]) {
Serial.println("\nCommand History:");
Serial.println("================");
int start = (g_history.count > CLI_HISTORY_SIZE) ?
(g_history.count - CLI_HISTORY_SIZE) : 0;
for (int i = start; i < g_history.count; i++) {
int index = i % CLI_HISTORY_SIZE;
Serial.print(" ");
Serial.print(i + 1);
Serial.print(": ");
Serial.println(g_history.history[index]);
}
Serial.println();
return 0;
}
4.2 命令自动补全¶
// Tab自动补全
static void cli_auto_complete(void) {
if (g_cli.input_pos == 0) {
return;
}
// 查找匹配的命令
int match_count = 0;
cli_command_t *match_cmd = NULL;
for (int i = 0; i < g_cli.cmd_count; i++) {
if (strncmp(g_cli.commands[i].name, g_cli.input_buf, g_cli.input_pos) == 0) {
match_count++;
match_cmd = &g_cli.commands[i];
}
}
if (match_count == 0) {
// 没有匹配
Serial.print("\a"); // 响铃
} else if (match_count == 1) {
// 唯一匹配,自动补全
const char *name = match_cmd->name;
int len = strlen(name);
// 补全剩余字符
for (int i = g_cli.input_pos; i < len; i++) {
g_cli.input_buf[g_cli.input_pos++] = name[i];
Serial.print(name[i]);
}
// 添加空格
g_cli.input_buf[g_cli.input_pos++] = ' ';
Serial.print(' ');
} else {
// 多个匹配,显示所有可能
Serial.println();
Serial.println("Possible commands:");
for (int i = 0; i < g_cli.cmd_count; i++) {
if (strncmp(g_cli.commands[i].name, g_cli.input_buf, g_cli.input_pos) == 0) {
Serial.print(" ");
Serial.println(g_cli.commands[i].name);
}
}
// 重新显示提示符和当前输入
Serial.print("> ");
Serial.print(g_cli.input_buf);
}
}
// 修改cli_process_char以支持Tab补全
void cli_process_char_enhanced(char c) {
// 处理Tab键
if (c == '\t') {
cli_auto_complete();
return;
}
// 处理方向键
if (c == 27 || (c == '[' && /* 在ESC序列中 */)) {
cli_handle_arrow_key(c);
return;
}
// 原有的字符处理逻辑...
// (与之前的cli_process_char相同)
}
4.3 参数类型验证¶
// 参数验证辅助函数
bool cli_validate_int(const char *str, int *value, int min, int max) {
char *endptr;
long val = strtol(str, &endptr, 10);
// 检查是否完全转换
if (*endptr != '\0') {
Serial.println("Error: Invalid integer");
return false;
}
// 检查范围
if (val < min || val > max) {
Serial.print("Error: Value must be between ");
Serial.print(min);
Serial.print(" and ");
Serial.println(max);
return false;
}
*value = (int)val;
return true;
}
bool cli_validate_float(const char *str, float *value, float min, float max) {
char *endptr;
float val = strtof(str, &endptr);
if (*endptr != '\0') {
Serial.println("Error: Invalid float");
return false;
}
if (val < min || val > max) {
Serial.print("Error: Value must be between ");
Serial.print(min);
Serial.print(" and ");
Serial.println(max);
return false;
}
*value = val;
return true;
}
// 使用示例
int cmd_pwm(int argc, char *argv[]) {
if (argc < 3) {
Serial.println("Usage: pwm <pin> <duty_cycle>");
return -1;
}
int pin, duty;
// 验证引脚号
if (!cli_validate_int(argv[1], &pin, 0, 13)) {
return -1;
}
// 验证占空比
if (!cli_validate_int(argv[2], &duty, 0, 255)) {
return -1;
}
// 设置PWM
analogWrite(pin, duty);
Serial.print("PWM on pin ");
Serial.print(pin);
Serial.print(" set to ");
Serial.println(duty);
return 0;
}
步骤5:完整示例程序¶
5.1 主程序¶
// main.ino - 完整的CLI框架示例
#include "cli_framework.h"
// LED引脚定义
#define LED_RED_PIN 9
#define LED_GREEN_PIN 10
#define LED_BLUE_PIN 11
// 前向声明所有命令函数
int cmd_help(int argc, char *argv[]);
int cmd_clear(int argc, char *argv[]);
int cmd_echo(int argc, char *argv[]);
int cmd_version(int argc, char *argv[]);
int cmd_led(int argc, char *argv[]);
int cmd_status(int argc, char *argv[]);
int cmd_config(int argc, char *argv[]);
int cmd_history(int argc, char *argv[]);
int cmd_pwm(int argc, char *argv[]);
int cmd_reset(int argc, char *argv[]);
void setup() {
// 初始化串口
Serial.begin(115200);
while (!Serial) {
; // 等待串口连接(仅用于原生USB端口)
}
// 配置LED引脚
pinMode(LED_RED_PIN, OUTPUT);
pinMode(LED_GREEN_PIN, OUTPUT);
pinMode(LED_BLUE_PIN, OUTPUT);
// 初始化所有LED为关闭状态
digitalWrite(LED_RED_PIN, LOW);
digitalWrite(LED_GREEN_PIN, LOW);
digitalWrite(LED_BLUE_PIN, LOW);
// 初始化CLI框架
cli_init();
// 注册内置命令
cli_register_command("help", "Show available commands",
cmd_help, 0, 0);
cli_register_command("clear", "Clear screen",
cmd_clear, 0, 0);
cli_register_command("echo", "Echo arguments",
cmd_echo, 0, CLI_MAX_ARGS);
cli_register_command("version", "Show version information",
cmd_version, 0, 0);
cli_register_command("history", "Show command history",
cmd_history, 0, 0);
// 注册应用命令
cli_register_command("led", "Control LED: led <color> <on|off>",
cmd_led, 2, 2);
cli_register_command("status", "Show system status",
cmd_status, 0, 0);
cli_register_command("config", "Get/set configuration: config <get|set> <key> [value]",
cmd_config, 2, 3);
cli_register_command("pwm", "Set PWM: pwm <pin> <duty_cycle>",
cmd_pwm, 2, 2);
cli_register_command("reset", "Reset system",
cmd_reset, 0, 0);
Serial.println("System initialized successfully");
Serial.print("> ");
}
void loop() {
// 运行CLI框架
cli_run();
// 其他任务可以在这里执行
// 例如:传感器读取、状态更新等
}
// 命令:reset(重启系统)
int cmd_reset(int argc, char *argv[]) {
Serial.println("Resetting system...");
delay(100);
// 软件复位(Arduino)
asm volatile ("jmp 0");
return 0;
}
预期运行效果:
=================================
Embedded CLI Framework v1.0
=================================
Type 'help' for available commands
System initialized successfully
> help
Available commands:
==================
help Show available commands
clear Clear screen
echo Echo arguments
version Show version information
history Show command history
led Control LED: led <color> <on|off>
status Show system status
config Get/set configuration: config <get|set> <key> [value]
pwm Set PWM: pwm <pin> <duty_cycle>
reset Reset system
> led red on
Red LED turned ON
> led green on
Green LED turned ON
> status
=== System Status ===
Uptime: 0h 2m 15s
Free Memory: 1234 bytes
LED Status:
Red: ON
Green: ON
Blue: OFF
====================
> config set brightness 75
OK
> config get brightness
brightness = 75
>
测试和验证¶
测试用例设计¶
基本功能测试:
void test_cli_basic() {
Serial.println("\n=== CLI Basic Tests ===");
// 测试1:命令注册
int result = cli_register_command("test", "Test command",
cmd_echo, 0, 1);
if (result == 0) {
Serial.println("✓ Test 1: Command registration passed");
} else {
Serial.println("✗ Test 1: Command registration failed");
}
// 测试2:命令查找
cli_command_t *cmd = cli_find_command("help");
if (cmd != NULL && strcmp(cmd->name, "help") == 0) {
Serial.println("✓ Test 2: Command lookup passed");
} else {
Serial.println("✗ Test 2: Command lookup failed");
}
// 测试3:参数解析
char test_line[] = "led red on";
int argc = cli_parse_line(test_line);
if (argc == 3 && strcmp(g_cli.argv[0], "led") == 0) {
Serial.println("✓ Test 3: Argument parsing passed");
} else {
Serial.println("✗ Test 3: Argument parsing failed");
}
Serial.println("======================\n");
}
性能测试¶
void test_cli_performance() {
Serial.println("\n=== CLI Performance Tests ===");
// 测试命令查找性能
unsigned long start = micros();
for (int i = 0; i < 1000; i++) {
cli_find_command("status");
}
unsigned long elapsed = micros() - start;
Serial.print("Command lookup (1000 iterations): ");
Serial.print(elapsed);
Serial.print(" us (avg: ");
Serial.print(elapsed / 1000.0);
Serial.println(" us)");
// 测试命令解析性能
char test_line[] = "config set brightness 100";
start = micros();
for (int i = 0; i < 1000; i++) {
strcpy(g_cli.input_buf, test_line);
cli_parse_line(g_cli.input_buf);
}
elapsed = micros() - start;
Serial.print("Command parsing (1000 iterations): ");
Serial.print(elapsed);
Serial.print(" us (avg: ");
Serial.print(elapsed / 1000.0);
Serial.println(" us)");
Serial.println("============================\n");
}
故障排除¶
问题1:命令无响应¶
现象: - 输入命令后没有任何反应 - 串口无输出
可能原因: 1. 串口波特率不匹配 2. 行结束符设置错误 3. 输入缓冲区溢出
解决方法:
// 添加调试输出
void cli_process_char_debug(char c) {
Serial.print("Received char: ");
Serial.print((int)c);
Serial.print(" (");
Serial.print(c);
Serial.println(")");
// 原有处理逻辑...
}
// 检查串口配置
void check_serial_config() {
Serial.println("Serial Configuration:");
Serial.print("Baud rate: ");
Serial.println(Serial.baudRate());
Serial.println("Please ensure terminal settings:");
Serial.println("- Baud: 115200");
Serial.println("- Data: 8 bits");
Serial.println("- Parity: None");
Serial.println("- Stop: 1 bit");
Serial.println("- Line ending: CR or LF");
}
问题2:内存不足¶
现象: - 系统运行一段时间后崩溃 - 命令执行异常
可能原因: 1. 输入缓冲区过大 2. 历史记录占用过多内存 3. 命令表过大
解决方法:
// 减小缓冲区大小
#define CLI_INPUT_BUF_SIZE 64 // 从128减到64
#define CLI_HISTORY_SIZE 3 // 从5减到3
#define CLI_MAX_COMMANDS 10 // 从20减到10
// 监控内存使用
void cli_print_memory_usage() {
Serial.println("\n=== Memory Usage ===");
Serial.print("CLI Context: ");
Serial.print(sizeof(cli_context_t));
Serial.println(" bytes");
Serial.print("History: ");
Serial.print(sizeof(g_history));
Serial.println(" bytes");
extern int __heap_start, *__brkval;
int free_memory;
if ((int)__brkval == 0) {
free_memory = ((int)&free_memory) - ((int)&__heap_start);
} else {
free_memory = ((int)&free_memory) - ((int)__brkval);
}
Serial.print("Free RAM: ");
Serial.print(free_memory);
Serial.println(" bytes");
Serial.println("===================\n");
}
问题3:特殊字符处理错误¶
现象: - 方向键显示乱码 - 退格键不工作 - Tab键无效
可能原因: 1. 终端不支持VT100序列 2. 字符编码问题 3. 控制字符处理不当
解决方法:
// 添加终端兼容性检测
void cli_detect_terminal() {
Serial.println("Detecting terminal capabilities...");
Serial.println("Press UP arrow key:");
unsigned long start = millis();
while (millis() - start < 5000) {
if (Serial.available()) {
char c = Serial.read();
Serial.print("Received: 0x");
Serial.println((int)c, HEX);
}
}
}
// 提供简化模式(不使用VT100)
#define CLI_SIMPLE_MODE 1
#if CLI_SIMPLE_MODE
// 不使用VT100序列
#define CLI_CLEAR_SCREEN() Serial.println("\n\n\n")
#define CLI_BACKSPACE() Serial.print("X")
#else
// 使用VT100序列
#define CLI_CLEAR_SCREEN() Serial.print("\033[2J\033[H")
#define CLI_BACKSPACE() Serial.print("\b \b")
#endif
最佳实践¶
设计原则¶
- 模块化设计
- 命令处理函数独立
- 核心框架与应用分离
-
易于添加新命令
-
错误处理
- 明确的错误提示
- 参数验证
-
异常情况处理
-
用户友好
- 清晰的帮助信息
- 命令自动补全
-
历史记录支持
-
性能优化
- 高效的命令查找
- 最小化内存使用
- 避免阻塞操作
代码组织¶
推荐的文件结构:
project/
├── main.ino # 主程序
├── cli_framework.h # CLI框架接口
├── cli_framework.c # CLI框架实现
├── cli_commands.h # 命令声明
├── cli_commands.c # 命令实现
├── cli_builtin.c # 内置命令
└── cli_config.h # 配置参数
扩展机制¶
动态命令注册:
// 支持运行时注册命令
typedef struct {
const char *module_name;
void (*init_func)(void);
} cli_module_t;
// 模块表
const cli_module_t cli_modules[] = {
{"led", cli_led_module_init},
{"sensor", cli_sensor_module_init},
{"network", cli_network_module_init},
};
// 初始化所有模块
void cli_init_modules(void) {
for (int i = 0; i < sizeof(cli_modules)/sizeof(cli_module_t); i++) {
Serial.print("Loading module: ");
Serial.println(cli_modules[i].module_name);
cli_modules[i].init_func();
}
}
// LED模块初始化
void cli_led_module_init(void) {
cli_register_command("led", "Control LED", cmd_led, 2, 2);
cli_register_command("blink", "Blink LED", cmd_blink, 1, 2);
}
命令别名:
// 命令别名支持
typedef struct {
const char *alias;
const char *command;
} cli_alias_t;
cli_alias_t g_aliases[] = {
{"?", "help"},
{"ls", "status"},
{"cls", "clear"},
};
// 解析别名
const char* cli_resolve_alias(const char *name) {
for (int i = 0; i < sizeof(g_aliases)/sizeof(cli_alias_t); i++) {
if (strcmp(g_aliases[i].alias, name) == 0) {
return g_aliases[i].command;
}
}
return name;
}
实战案例:IoT设备管理CLI¶
案例需求¶
设计一个IoT设备的管理CLI系统:
功能要求: - 网络配置(WiFi连接、IP设置) - 传感器管理(读取数据、配置参数) - 数据上报(查看状态、手动上报) - 系统管理(重启、升级、日志)
完整实现¶
// iot_cli.ino
// WiFi配置命令
int cmd_wifi(int argc, char *argv[]) {
if (argc < 2) {
Serial.println("Usage:");
Serial.println(" wifi scan - Scan WiFi networks");
Serial.println(" wifi connect <ssid> <password>");
Serial.println(" wifi disconnect");
Serial.println(" wifi status");
return -1;
}
const char *action = argv[1];
if (strcmp(action, "scan") == 0) {
Serial.println("Scanning WiFi networks...");
// 实际扫描代码
Serial.println("Found networks:");
Serial.println(" 1. MyWiFi (-45 dBm)");
Serial.println(" 2. Office (-67 dBm)");
} else if (strcmp(action, "connect") == 0) {
if (argc < 4) {
Serial.println("Error: Missing SSID or password");
return -1;
}
const char *ssid = argv[2];
const char *password = argv[3];
Serial.print("Connecting to ");
Serial.print(ssid);
Serial.println("...");
// 实际连接代码
// WiFi.begin(ssid, password);
Serial.println("Connected successfully");
} else if (strcmp(action, "disconnect") == 0) {
Serial.println("Disconnecting...");
// WiFi.disconnect();
Serial.println("Disconnected");
} else if (strcmp(action, "status") == 0) {
Serial.println("\n=== WiFi Status ===");
Serial.println("Status: Connected");
Serial.println("SSID: MyWiFi");
Serial.println("IP: 192.168.1.100");
Serial.println("Signal: -45 dBm");
Serial.println("==================\n");
} else {
Serial.println("Error: Unknown action");
return -1;
}
return 0;
}
// 传感器命令
int cmd_sensor(int argc, char *argv[]) {
if (argc < 2) {
Serial.println("Usage:");
Serial.println(" sensor list - List all sensors");
Serial.println(" sensor read <id> - Read sensor data");
Serial.println(" sensor config <id> <param> <value>");
return -1;
}
const char *action = argv[1];
if (strcmp(action, "list") == 0) {
Serial.println("\n=== Sensors ===");
Serial.println("ID Type Status");
Serial.println("-- ---------- ------");
Serial.println("0 Temperature OK");
Serial.println("1 Humidity OK");
Serial.println("2 Pressure OK");
Serial.println("===============\n");
} else if (strcmp(action, "read") == 0) {
if (argc < 3) {
Serial.println("Error: Missing sensor ID");
return -1;
}
int id = atoi(argv[2]);
Serial.print("Reading sensor ");
Serial.print(id);
Serial.println("...");
// 模拟读取
switch (id) {
case 0:
Serial.println("Temperature: 25.3°C");
break;
case 1:
Serial.println("Humidity: 65.2%");
break;
case 2:
Serial.println("Pressure: 1013.25 hPa");
break;
default:
Serial.println("Error: Invalid sensor ID");
return -1;
}
} else if (strcmp(action, "config") == 0) {
if (argc < 5) {
Serial.println("Error: Missing parameters");
return -1;
}
int id = atoi(argv[2]);
const char *param = argv[3];
const char *value = argv[4];
Serial.print("Configuring sensor ");
Serial.print(id);
Serial.print(": ");
Serial.print(param);
Serial.print(" = ");
Serial.println(value);
Serial.println("OK");
} else {
Serial.println("Error: Unknown action");
return -1;
}
return 0;
}
// 数据上报命令
int cmd_report(int argc, char *argv[]) {
if (argc < 2) {
Serial.println("Usage:");
Serial.println(" report status - Show report status");
Serial.println(" report now - Report immediately");
Serial.println(" report interval <sec> - Set report interval");
return -1;
}
const char *action = argv[1];
if (strcmp(action, "status") == 0) {
Serial.println("\n=== Report Status ===");
Serial.println("Interval: 60 seconds");
Serial.println("Last report: 15 seconds ago");
Serial.println("Total reports: 1234");
Serial.println("Failed: 5");
Serial.println("====================\n");
} else if (strcmp(action, "now") == 0) {
Serial.println("Reporting data...");
// 实际上报代码
delay(500);
Serial.println("Report sent successfully");
} else if (strcmp(action, "interval") == 0) {
if (argc < 3) {
Serial.println("Error: Missing interval value");
return -1;
}
int interval = atoi(argv[2]);
if (interval < 10 || interval > 3600) {
Serial.println("Error: Interval must be 10-3600 seconds");
return -1;
}
Serial.print("Report interval set to ");
Serial.print(interval);
Serial.println(" seconds");
} else {
Serial.println("Error: Unknown action");
return -1;
}
return 0;
}
// 日志命令
int cmd_log(int argc, char *argv[]) {
if (argc < 2) {
Serial.println("Usage:");
Serial.println(" log show [level] - Show logs");
Serial.println(" log clear - Clear logs");
Serial.println(" log level <level> - Set log level");
return -1;
}
const char *action = argv[1];
if (strcmp(action, "show") == 0) {
Serial.println("\n=== System Logs ===");
Serial.println("[INFO ] System started");
Serial.println("[INFO ] WiFi connected");
Serial.println("[WARN ] Sensor 2 timeout");
Serial.println("[ERROR] Report failed");
Serial.println("[INFO ] Report retry success");
Serial.println("===================\n");
} else if (strcmp(action, "clear") == 0) {
Serial.println("Logs cleared");
} else if (strcmp(action, "level") == 0) {
if (argc < 3) {
Serial.println("Error: Missing level");
Serial.println("Levels: DEBUG, INFO, WARN, ERROR");
return -1;
}
const char *level = argv[2];
Serial.print("Log level set to ");
Serial.println(level);
} else {
Serial.println("Error: Unknown action");
return -1;
}
return 0;
}
void setup() {
Serial.begin(115200);
while (!Serial);
// 初始化CLI
cli_init();
// 注册基础命令
cli_register_command("help", "Show available commands",
cmd_help, 0, 0);
cli_register_command("version", "Show version",
cmd_version, 0, 0);
cli_register_command("status", "Show system status",
cmd_status, 0, 0);
cli_register_command("reset", "Reset system",
cmd_reset, 0, 0);
// 注册IoT命令
cli_register_command("wifi", "WiFi management",
cmd_wifi, 1, 3);
cli_register_command("sensor", "Sensor management",
cmd_sensor, 1, 5);
cli_register_command("report", "Data reporting",
cmd_report, 1, 2);
cli_register_command("log", "Log management",
cmd_log, 1, 2);
Serial.println("IoT Device Management System Ready");
Serial.print("> ");
}
void loop() {
cli_run();
}
运行效果:
=================================
Embedded CLI Framework v1.0
=================================
Type 'help' for available commands
IoT Device Management System Ready
> wifi scan
Scanning WiFi networks...
Found networks:
1. MyWiFi (-45 dBm)
2. Office (-67 dBm)
> wifi connect MyWiFi password123
Connecting to MyWiFi...
Connected successfully
> sensor list
=== Sensors ===
ID Type Status
-- ---------- ------
0 Temperature OK
1 Humidity OK
2 Pressure OK
===============
> sensor read 0
Reading sensor 0...
Temperature: 25.3°C
> report now
Reporting data...
Report sent successfully
>
总结¶
通过本教程,你学习了:
- ✅ CLI框架的设计原理和架构
- ✅ 命令注册和动态扩展机制
- ✅ 命令解析和参数处理技术
- ✅ 输入缓冲和行编辑功能
- ✅ 命令历史和自动补全实现
- ✅ 参数验证和错误处理
- ✅ 模块化设计和代码组织
- ✅ 实际项目中的应用案例
关键要点:
- CLI框架是嵌入式系统的重要工具
- 提供灵活的调试接口
- 支持动态配置和控制
-
便于现场维护和诊断
-
模块化设计至关重要
- 核心框架与应用分离
- 命令独立实现
-
易于扩展和维护
-
用户体验很重要
- 清晰的帮助信息
- 友好的错误提示
-
便捷的辅助功能
-
性能和资源平衡
- 合理使用内存
- 高效的命令查找
- 避免阻塞操作
进阶挑战¶
尝试以下挑战来巩固学习:
- 挑战1:增强的参数解析
- 支持引号字符串("hello world")
- 支持转义字符(\n, \t)
- 支持选项参数(-v, --verbose)
-
实现管道和重定向(cmd1 | cmd2)
-
挑战2:脚本执行
- 从文件读取命令序列
- 支持条件执行(if/else)
- 支持循环(for/while)
-
实现变量和表达式
-
挑战3:远程CLI
- 通过网络访问CLI
- 支持多用户并发
- 实现权限管理
-
加密通信
-
挑战4:图形化CLI
- 使用ANSI颜色
- 实现菜单系统
- 支持表格显示
- 添加进度条
延伸阅读¶
推荐资源:
- 开源CLI框架
- microrl - 轻量级嵌入式CLI库
- linenoise - 小型行编辑库
-
cmd - ESP32 CLI框架
-
相关技术
- VT100终端控制序列
- GNU Readline库
-
书籍
- 《The Art of Unix Programming》- 命令行设计哲学
- 《Linux Command Line and Shell Scripting Bible》
- 《Embedded Systems Architecture》
下一步学习:
参考资料¶
- POSIX Shell Command Language Specification
- "The Design and Implementation of the 4.4BSD Operating System"
- GNU Bash Reference Manual
- VT100 User Guide - Digital Equipment Corporation
反馈:如果你在学习过程中遇到问题或有改进建议,欢迎在评论区留言!
版权声明:本教程采用 CC BY-SA 4.0 许可协议。