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 中使用
69KB
1.5K SLoC
embedded-cli
嵌入式系统的命令行界面
Arduino Nano 上 CLI 运行演示。内存使用:16KiB 的 ROM 和 0.6KiB 的静态 RAM。大部分静态 RAM 用于帮助字符串。
双许可协议下使用 Apache 2.0 或 MIT。
该库尚不稳定,意味着其 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,您可以使用例如PuTTY或tio。
内存使用
内存使用取决于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