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日

#15 in #嵌入式-io

每月47次下载
embedded-cli 中使用

MIT/Apache

69KB
1.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 程序内存很有用)

如何使用

添加依赖项

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

[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

使用 help <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(左键键)用于在当前输入中移动光标

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

内存使用

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

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

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

依赖项

~1.2–1.7MB
~33K SLoC