embassy-shell
A small no_std interactive shell in the spirit of bash, built for
embassy (any async executor works) and any
transport implementing embedded-io-async
— UART, USB CDC, TCP sockets, …
Think ushell, but alive, native
async/embassy, with a simpler command-registration API.
Features
- Bash-like line editing — cursor keys,
Home/End,Delete,Backspace,Ctrl-U(kill line),Ctrl-W(delete word),Ctrl-L(clear screen), UTF-8 input. - Tab completion — completes command names; per-command argument completion from fixed option lists or custom callbacks. Inserts the common prefix and lists candidates when ambiguous, like bash.
- History —
Up/Downnavigation, configurable size,historybuilt-in. - Ctrl-C interrupts a running command — the handler future is dropped (handlers should be cancel-safe).
- Simple command registration — closures returning a boxed future;
commands get an
Iooutput handle so they canawaitslow writes. - Built-ins
help,history,clear(can be shadowed by user commands). - Only
embedded-io+embedded-io-asyncas dependencies. Usesalloc(a global allocator is required). NotSend-bound, which matches embassy's per-core executor model.
Quick start
use Shell;
let mut shell = new;
// A command with free-form arguments.
shell.add_command;
// A command with a fixed set of first-argument options (`led on|off|blink`).
shell.add_command_with_options;
// Run over anything that is embedded-io-async Read + Write:
// embassy UART handles, embassy-usb CDC `SerialPort`, ...
shell.run.await?;
With embassy (UART)
async
The same works with embassy-usb CDC-ACM: pass the SerialPort's reader and
writer halves.
Command handlers
A handler is Fn(Args, Io) -> BoxFuture<'_, Result<()>>. Wrap an
async move block in Box::pin (this type-erases per-command future types
so all commands live in one table):
shell.add_command;
Args gives get(i), iter(), len() and rest() (the arguments joined
with spaces); quotes are handled bash-style (led "on blink" → one token).
Io provides print, println, write_all, flush and implements
embedded_io_async::Write, so you can hand it to other libraries.
Custom argument completion:
shell.add_command_with_completer;
Terminal keys
| Key | Action |
|---|---|
Enter |
run line |
Tab |
complete command / argument |
Up/Down |
history |
←/→ |
move cursor (ins-line editing supported) |
Home/End |
jump to start / end |
Backspace / Delete |
erase around cursor |
Ctrl-C |
clear line / interrupt running command |
Ctrl-U |
kill line |
Ctrl-W |
delete previous word |
Ctrl-L |
clear screen |
Examples
Eight ready-to-flash projects live in examples/. Each one is a
standalone crate (own Cargo.toml, .cargo/config.toml and runner), so
cd into it and use cargo check / cargo run.
Every example runs a shell with led on|off|toggle|blink [period_ms],
pwm <0-255>|off (the ESP32-C6 examples drive a WS2812 strip instead, via
rgb <r> <g> <b>|<color>|off) and status (plus reboot on the STM32 ones).
| Example | Transport | Console |
|---|---|---|
stm32f411-uart |
USART1 + DMA | serial, 115200 8N1 |
stm32f411-usb |
USB CDC-ACM | any COM terminal |
nanoch32v305-uart |
USART1 + DMA | serial, 115200 8N1 |
nanoch32v305-usb |
USB CDC-ACM | any COM terminal |
esp32c6-uart |
UART0 | serial, 115200 8N1 |
esp32c6-usb |
USB Serial/JTAG | any COM terminal |
esp32c3-uart |
UART0 | serial, 115200 8N1 |
esp32c3-usb |
USB Serial/JTAG | any COM terminal |
All examples log via defmt and are flashed with probe-rs: cargo run
builds, flashes and streams the logs (DEFMT_LOG=info is preset in each
.cargo/config.toml; override with e.g. DEFMT_LOG=debug cargo run).
STM32F411 Blackpill (STM32F411CE)
- Toolchain: stable,
thumbv7em-none-eabihf. Flash:cargo run(runner isprobe-rs run --chip STM32F411CE), defmt over SWD RTT. - Console (uart):
PA9= TX,PA10= RX, 115200 8N1. - USB (usb): built-in OTG FS on the
USBconnector,PA11= DM,PA12= DP. Uses the 25 MHz HSE crystal → 96 MHz core, PLL1_Q = 48 MHz for USB. - LED:
PC13(active low). The LED is also the PWM output:PC13has no hardware timer channel on the F411, so the PWM is generated in software with thespwmcrate — a 100 kHz TIM3 interrupt drives a 1 kHz software PWM (100 physical ticks per period), and error-feedback dithering maps the whole0..255brightness range onto 10 µs steps. embassy-time runs on TIM4.
NanoCH32V305 (CH32V305RBT6)
- Toolchain: nightly with
rust-src(customriscv32imfc-unknown-none-elfJSON target +-Zbuild-std). Flash:cargo run(runner isprobe-rs run --chip CH32V305RBT6 --connect-under-reset), defmt over SWD RTT. - Depends on
ch32-halpinned to a git revision. - Console (uart):
PA9= TX,PA10= RX, 115200 8N1. - USB (usb): OTG FS,
PA11= DM,PA12= DP, core clock 144 MHz from HSI (required for the 48 MHz USB clock). These pins are shared with the ISP bootloader: if the device never enumerates, make sure the board is not being held in the bootloader. - LED:
PA3(active low). The LED is also the PWM output:PA3= TIM2_CH4 (no remap), hardware PWM at 1 kHz with active-low polarity, so the user brightness0..255maps linearly onto the timer duty.
ESP32-C6 / ESP32-C3
- Toolchain: stable, built-in target
riscv32imac-unknown-none-elf(C6) /riscv32imc-unknown-none-elf(C3); esp-hal 1.1 +esp-rtos(embassy executor). Flash:cargo run(runner isprobe-rs run --chip esp32c6/--chip esp32c3). The C3 examples share the structure of the C6 ones (transport, shell, defmt setup) but drive a plain LED + LEDC PWM instead of a WS2812 strip, and differ in chip feature, target and UART0 pins. - Console (uart): UART0;
GPIO16= TX,GPIO17= RX (C6) orGPIO21= TX,GPIO20= RX (C3), 115200 8N1. - USB (usb): neither the C6 nor the C3 has USB-OTG; these examples use
the built-in USB Serial/JTAG peripheral (the same controller as the
board's flash port), which enumerates as a CDC device. Because debug and
USB Serial/JTAG share the connector, the runner adds
--connect-under-reset. - defmt: on by default in the uart example (RTT over SWD); off by default
in the usb example to avoid contending for the USB Serial/JTAG console —
enable with
cargo build --features defmt. - LED (C6 examples): WS2812 RGB LED on
GPIO8(D8of the ESP32-C6 Supermini), driven by the RMT peripheral (GRB frames on an 80 MHz tick, idle-high between frames). Commands:rgb <r> <g> <b>(each 0–255), named presets (red,green,blue,yellow,magenta,cyan,white) andrgb off;led on|off|toggle|blinkshows the last color (white by default). - LED (C3 examples): onboard blue LED on
GPIO8(active high). The LED is also the PWM output:GPIO8via LEDC low-speed channel 1, 1 kHz with 10-bit duty, so the user brightness0..255maps linearly onto the timer duty.
The shell itself is transport-agnostic: the UART examples plug a small
embedded-io-async adapter over the HAL's DMA-backed UART, the USB CDC ones
do the same over embassy-usb endpoints (see CdcReader/CdcWriter), and the
ESP examples use the HAL's async Rx/Tx halves directly.
Notes & limitations
allocis required (command table, line buffer, history).- Ctrl-C cancels the handler future: don't hold non-cancel-safe state
across
.awaitinside a command. - Bytes typed while a command runs are not echoed; only the most recent one is kept for the next line.
- Completion completes the token at the end of the line.
- ANSI/VT100 terminal assumed (
picocom,minicom, PuTTY, Windows Terminal, …); CR/LF/CRLF all accepted as Enter.
Status / ideas
-
heaplessback-end so the core works withoutalloc - password auth hook, command aliases
-
Sendvariant of the command table for multi-core setups
PRs welcome. MIT OR Apache-2.0 licensed.