跳转至

命令行接口(CLI)框架开发:构建灵活的嵌入式交互系统

学习目标

完成本教程后,你将能够:

  • 理解CLI框架的设计原理和架构
  • 掌握命令解析和参数处理技术
  • 实现命令注册和动态扩展机制
  • 设计完善的帮助系统和错误处理
  • 实现命令历史和自动补全功能
  • 掌握CLI框架的测试和调试方法
  • 能够设计和实现一个完整的CLI框架
  • 理解CLI在嵌入式系统中的应用场景

前置要求

在开始学习前,建议你具备:

知识要求: - 熟悉C语言(指针、结构体、函数指针) - 了解字符串处理和解析技术 - 掌握状态机设计模式 - 理解模块化编程思想

技能要求: - 能够使用串口通信 - 会使用终端工具(PuTTY、minicom等) - 了解基本的调试技术

开发环境: - 任何支持C语言的嵌入式开发板 - 串口调试工具 - 开发IDE(Arduino IDE、STM32CubeIDE等)

CLI框架概述

什么是CLI框架

CLI(Command Line Interface,命令行接口)框架是一种通过文本命令与系统交互的接口系统。在嵌入式系统中,CLI提供了一种灵活、强大的调试和控制方式。

核心概念

  1. 命令(Command)
  2. 用户输入的操作指令
  3. 由命令名和参数组成
  4. 触发特定的功能执行

  5. 参数(Arguments)

  6. 命令的输入数据
  7. 可以是必需或可选的
  8. 支持多种数据类型

  9. 解析器(Parser)

  10. 分析用户输入
  11. 提取命令和参数
  12. 验证输入合法性

  13. 执行器(Executor)

  14. 调用对应的命令处理函数
  15. 传递解析后的参数
  16. 返回执行结果

CLI框架的优势

为什么要使用CLI框架?

  1. 调试便利
  2. 实时查看系统状态
  3. 动态修改参数配置
  4. 快速定位问题

  5. 灵活控制

  6. 无需重新编译即可测试
  7. 支持复杂的操作序列
  8. 易于自动化测试

  9. 可扩展性

  10. 动态添加新命令
  11. 模块化命令管理
  12. 支持插件机制

  13. 用户友好

  14. 提供帮助信息
  15. 命令自动补全
  16. 历史记录功能

  17. 生产应用

  18. 现场调试和维护
  19. 系统配置和升级
  20. 故障诊断和日志查看

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

最佳实践

设计原则

  1. 模块化设计
  2. 命令处理函数独立
  3. 核心框架与应用分离
  4. 易于添加新命令

  5. 错误处理

  6. 明确的错误提示
  7. 参数验证
  8. 异常情况处理

  9. 用户友好

  10. 清晰的帮助信息
  11. 命令自动补全
  12. 历史记录支持

  13. 性能优化

  14. 高效的命令查找
  15. 最小化内存使用
  16. 避免阻塞操作

代码组织

推荐的文件结构

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框架的设计原理和架构
  • ✅ 命令注册和动态扩展机制
  • ✅ 命令解析和参数处理技术
  • ✅ 输入缓冲和行编辑功能
  • ✅ 命令历史和自动补全实现
  • ✅ 参数验证和错误处理
  • ✅ 模块化设计和代码组织
  • ✅ 实际项目中的应用案例

关键要点

  1. CLI框架是嵌入式系统的重要工具
  2. 提供灵活的调试接口
  3. 支持动态配置和控制
  4. 便于现场维护和诊断

  5. 模块化设计至关重要

  6. 核心框架与应用分离
  7. 命令独立实现
  8. 易于扩展和维护

  9. 用户体验很重要

  10. 清晰的帮助信息
  11. 友好的错误提示
  12. 便捷的辅助功能

  13. 性能和资源平衡

  14. 合理使用内存
  15. 高效的命令查找
  16. 避免阻塞操作

进阶挑战

尝试以下挑战来巩固学习:

  1. 挑战1:增强的参数解析
  2. 支持引号字符串("hello world")
  3. 支持转义字符(\n, \t)
  4. 支持选项参数(-v, --verbose)
  5. 实现管道和重定向(cmd1 | cmd2)

  6. 挑战2:脚本执行

  7. 从文件读取命令序列
  8. 支持条件执行(if/else)
  9. 支持循环(for/while)
  10. 实现变量和表达式

  11. 挑战3:远程CLI

  12. 通过网络访问CLI
  13. 支持多用户并发
  14. 实现权限管理
  15. 加密通信

  16. 挑战4:图形化CLI

  17. 使用ANSI颜色
  18. 实现菜单系统
  19. 支持表格显示
  20. 添加进度条

延伸阅读

推荐资源

  1. 开源CLI框架
  2. microrl - 轻量级嵌入式CLI库
  3. linenoise - 小型行编辑库
  4. cmd - ESP32 CLI框架

  5. 相关技术

  6. VT100终端控制序列
  7. GNU Readline库
  8. 命令行界面设计指南

  9. 书籍

  10. 《The Art of Unix Programming》- 命令行设计哲学
  11. 《Linux Command Line and Shell Scripting Bible》
  12. 《Embedded Systems Architecture》

下一步学习

参考资料

  1. POSIX Shell Command Language Specification
  2. "The Design and Implementation of the 4.4BSD Operating System"
  3. GNU Bash Reference Manual
  4. VT100 User Guide - Digital Equipment Corporation

反馈:如果你在学习过程中遇到问题或有改进建议,欢迎在评论区留言!

版权声明:本教程采用 CC BY-SA 4.0 许可协议。