#embedded-io #command-line-arguments #command-arguments #arguments-parser #cli

无std embedded-cli

为嵌入式系统(如Arduino或STM32)提供自动补全、帮助和历史记录的CLI

5个版本

0.2.1 2024年3月2日
0.2.0 2024年2月7日
0.1.2 2024年1月28日
0.1.1 2024年1月21日
0.1.0 2024年1月7日

#113 in 嵌入式开发

MIT/Apache

100KB
2.5K SLoC

embedded-cli

嵌入式系统的命令行界面

Crates.io License License Build Status Coverage Status

Arduino Nano上CLI的演示。内存使用:16KiB的ROM和0.6KiB的静态RAM。大部分静态RAM用于帮助字符串。

Arduino Demo

双授权,受Apache 2.0MIT授权。

此库尚不稳定,意味着其API可能会更改。一些API可能有点丑陋,但现在我认为没有更好的解决方案。如果您有建议,请打开Issue或Pull Request。

功能

  • 静态分配
  • UTF-8支持
  • 无动态派遣
  • 可配置内存使用
  • 使用枚举声明命令
  • 选项和标志支持
  • 子命令支持
  • 左右支持(在当前输入中移动)
  • 解析常见类型的参数
  • 命令名称自动补全(使用Tab键)
  • 历史记录(使用上下键导航)
  • 帮助(从文档注释生成)
  • 使用ufmt格式化写入
  • 优化时生成的代码中无panic分支
  • 支持任何字节流接口(embedded_io::Write作为输出流,输入字节逐个提供)
  • 通过ANSI转义序列着色
  • 通过搜索当前输入导航历史记录
  • 支持在用户宏中包裹生成的str切片(对Arduino progmem很有用)

如何使用

添加依赖项

embedded-cli和必要的crate添加到您的应用程序中

[dependencies]
embedded-cli = "0.2.1"
embedded-io = "0.6.1"
ufmt = "0.2.0"

实现字节写入器

定义一个用于输出字节的写入器

struct Writer {
    // necessary fields (for example, uart tx handle)
};

impl embedded_io::ErrorType for Writer {
    // your error type
}

impl embedded_io::Write for Writer {
    fn write(&mut self, buf: &[u8]) -> Result<usize, Self::Error> {
        todo!()
    }

    fn flush(&mut self) -> Result<(), Self::Error> {
        todo!()
    }
}

构建CLI实例

构建一个命令行界面(CLI),指定用于命令缓冲区(存储用户按回车键前的字节)和历史缓冲区(用户可以通过上下键导航)的内存量。

let (command_buffer, history_buffer) = unsafe {
        static mut COMMAND_BUFFER: [u8; 32] = [0; 32];
        static mut HISTORY_BUFFER: [u8; 32] = [0; 32];
        (COMMAND_BUFFER.as_mut(), HISTORY_BUFFER.as_mut())
    };
let mut cli = CliBuilder::default()
    .writer(writer)
    .command_buffer(command_buffer)
    .history_buffer(history_buffer)
    .build()
    .ok()?;

在这个例子中,我们使用了静态互斥缓冲区,因此我们没有使用堆栈内存。请注意,我们没有调用 unwrap()。在嵌入式代码中保持没有恐慌非常重要,因为每次恐慌都会大大增加RAM和ROM的使用量。而大多数嵌入式系统并没有很多这种资源。

描述您的命令

使用枚举和宏定义您的命令结构

use embedded_cli::Command;

#[derive(Command)]
enum Base<'a> {
    /// Say hello to World or someone else
    Hello {
        /// To whom to say hello (World by default)
        name: Option<&'a str>,
    },

    /// Stop CLI and exit
    Exit,
}

生成的帮助信息将使用文档注释

将输入传递给CLI并处理命令

然后,您可以准备好将所有传入的字节提供给CLI并处理命令

use ufmt::uwrite;

// read byte from somewhere (for example, uart)
// let byte = nb::block!(rx.read()).void_unwrap();

let _ = cli.process_byte::<Base, _>(
    byte,
    &mut Base::processor(|cli, command| {
        match command {
            Base::Hello { name } => {
                // last write in command callback may or may not
                // end with newline. so both uwrite!() and uwriteln!()
                // will give identical results
                uwrite!(cli.writer(), "Hello, {}", name.unwrap_or("World"))?;
            }
            Base::Exit => {
                // We can write via normal function if formatting not needed
                cli.writer().write_str("Cli can't shutdown now")?;
            }
        }
        Ok(())
    }),
);

将命令拆分为模块

如果您有很多命令,将它们拆分为多个枚举并将它们的逻辑放入多个模块可能很有用。这也可以通过命令组来实现。

创建额外的命令枚举

#[derive(Command)]
#[command(help_title = "Manage Hardware")]
enum GetCommand {
    /// Get current LED value
    GetLed {
        /// ID of requested LED
        led: u8,
    },

    /// Get current ADC value
    GetAdc {
        /// ID of requested ADC
        adc: u8,
    },
}

将命令分组到新的枚举中

#[derive(CommandGroup)]
enum Group<'a> {
    Base(Base<'a>),
    Get(GetCommand),
    
    /// This variant will capture everything, that
    /// other commands didn't parse. You don't need
    /// to add it, just for example
    Other(RawCommand<'a>),
}

然后以类似的方式处理它

let _ = cli.process_byte::<Group, _>(
    byte,
    &mut Group::processor(|cli, command| {
        match command {
            Group::Base(cmd) => todo!("process base command"),
            Group::Get(cmd) => todo!("process get command"),
            Group::Other(cmd) => todo!("process all other, not parsed commands"),
        }
        Ok(())
    }),
);

您可以在这里检查完整的Arduino示例。还有一个桌面示例,在正常终端中运行。这样您就可以在不闪烁真实设备的情况下与CLI交互。

参数解析

命令可以有任意数量的参数。参数类型必须实现FromArgument特性

struct CustomArg<'a> {
    // fields
}

impl<'a> embedded_cli::arguments::FromArgument<'a> for CustomArg<'a> {
    fn from_arg(arg: &'a str) -> Result<Self, &'static str>
    where
        Self: Sized {
        todo!()
    }
}

库为以下类型提供了实现

  • 所有数字(u8, i8, u16, i16, u32, i32, u64, i64, u128, i128, usize, isize, f32, f64)
  • 布尔值(bool)
  • 字符(char)
  • 字符串切片(&str)

如果您需要其他类型,请创建一个问题。

输入标记化

CLI使用空白字符(标准ASCII空白字符,代码0x20)将输入拆分为命令及其参数。如果您想要提供一个包含空格的参数,只需将其用引号括起来即可。

输入 参数1 参数2 备注
cmd abc def abc def 空格被视为参数分隔符
cmd "abc def" abc def 要使用参数内的空格,请用引号将其括起来
cmd "abc\" d\\ef" abc" d\ef 要使用引号或斜杠,请用\转义它们
cmd "abc def" test abc def test 您可以混合带引号的参数和非带引号的参数
cmd "abc def"test abc def test 带引号参数之间的空格是可选的
cmd "abc def""test 2" abc def test 2 带引号参数之间的空格是可选的

生成帮助信息

当使用Command派生宏时,它将自动从文档注释中生成帮助信息

#[derive(Command)]
enum Base<'a> {
    /// Say hello to World or someone else
    Hello {
        /// To whom to say hello (World by default)
        name: Option<&'a str>,
    },

    /// Stop CLI and exit
    Exit,
}

使用help列出所有命令

$ help
Commands:
  hello  Say hello to World or someone else
  exit   Stop CLI and exit

使用<COMMAND>获取特定命令的帮助信息

$ help hello
Say hello to World or someone else

Usage: hello [NAME]

Arguments:
  [NAME]  To whom to say hello (World by default)

Options:
  -h, --help  Print help

或使用<COMMAND> --help<COMMAND> -h

$ exit --help
Stop CLI and exit

Usage: exit

Options:
  -h, --help  Print help

用户指南

您需要通过CLI运行的设备(通常是通过UART)开始通信。终端对于获得正确体验是必需的。以下控制序列受到支持:

  • \r或\n发送一个命令(\r\n也受支持)
  • \b删除最后输入的字符
  • \t尝试自动完成当前输入
  • Esc[A(向上键)和Esc[B(向下键)在历史记录中导航
  • Esc[C(右键)和Esc[D(左键)在当前输入中移动光标

如果您通过串口运行CLI(例如在Arduino上使用其UART-USB转换器),您可以使用例如PuTTYtio

内存使用

内存使用取决于crate的版本、启用的功能和您命令的复杂性。以下是在不同功能启用时arduino 示例的内存使用情况。未来的版本中内存使用可能会改变,但我会尽量保持此表更新。

功能 ROM,字节 静态RAM,字节
10066 317
自动完成 11926 333
历史记录 12170 358
自动完成 历史记录 13434 374
帮助 14296 587
自动完成 帮助 15608 599
历史记录 帮助 16390 628
自动完成 历史记录 帮助 16284 640

此表使用此脚本生成。如表所示,启用帮助会显著增加内存使用,因为帮助通常需要存储大量文本。同时启用所有功能将ROM使用量几乎翻倍,与所有功能禁用相比。

依赖项

~2MB
~42K SLoC