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 嵌入式开发
100KB
2.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 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转换器),您可以使用例如PuTTY或tio。
内存使用
内存使用取决于crate的版本、启用的功能和您命令的复杂性。以下是在不同功能启用时arduino 示例的内存使用情况。未来的版本中内存使用可能会改变,但我会尽量保持此表更新。
功能 | ROM,字节 | 静态RAM,字节 |
---|---|---|
10066 | 317 | |
自动完成 |
11926 | 333 |
历史记录 |
12170 | 358 |
自动完成 历史记录 |
13434 | 374 |
帮助 |
14296 | 587 |
自动完成 帮助 |
15608 | 599 |
历史记录 帮助 |
16390 | 628 |
自动完成 历史记录 帮助 |
16284 | 640 |
此表使用此脚本生成。如表所示,启用帮助会显著增加内存使用,因为帮助通常需要存储大量文本。同时启用所有功能将ROM使用量几乎翻倍,与所有功能禁用相比。
依赖项
~2MB
~42K SLoC