You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Is an async version of this library planned? Have you used it in async applications? If yes, how? The embassy-net tcp socket only implements embedded_io_async traits.
No, currently there is no plans to support async IO.
Take a look at similar issue: #13
You can add RX and TX buffers and fill RX in your async code. Then in some dedicated "thread" you will take all bytes one by one from RX buffer, pass to cli. In cli pass writer that will add bytes to TX buffer. And in your async code take bytes from TX buffer and write to your IO backend. Of course you will have to handle overflows somehow.
Approach with RX/TX buffers could be supported by library one day but currently I don't have resources for that. But PRs are welcome
I don't use it in async projects, so I don't have a writer. As I said, just create two buffers (RX and TX) behind a mutex. You can do something like this for TX (and for RX everything is similar). Mostly generated by chatgpt, but you get the idea:
use embedded_io::WriteasEmbeddedWrite;// Import the embedded_io Write traituse heapless::spsc::Queue;use critical_section::Mutex;pubstructBufferWriter<constBUF_SIZE:usize>{queue:Mutex<Queue<u8,BUF_SIZE>>,// Thread-safe queue for TX buffer}impl<constBUF_SIZE:usize>BufferWriter<BUF_SIZE>{pubconstfnnew() -> Self{Self{queue:Mutex::new(Queue::new()),}}/// Push a byte into the buffer. Returns `Err` if the buffer is full.pubfnpush(&self,byte:u8) -> Result<(),()>{
critical_section::with(|cs| {letmut queue = self.queue.borrow_ref_mut(cs);
queue.enqueue(byte).map_err(|_| ())// Return error if the buffer is full})}/// Pop a byte from the buffer. Returns `None` if the buffer is empty.pubfnpop(&self) -> Option<u8>{
critical_section::with(|cs| {letmut queue = self.queue.borrow_ref_mut(cs);
queue.dequeue()})}/// Check if the buffer is empty.pubfnis_empty(&self) -> bool{
critical_section::with(|cs| {let queue = self.queue.borrow_ref(cs);
queue.is_empty()})}/// Check if the buffer is full.pubfnis_full(&self) -> bool{
critical_section::with(|cs| {let queue = self.queue.borrow_ref(cs);
queue.is_full()})}}/// Implement the embedded_io::Write traitimpl<constBUF_SIZE:usize>EmbeddedWriteforBufferWriter<BUF_SIZE>{typeError = ();// Define the error type (simple `()` for now)fnwrite(&mutself,data:&[u8]) -> Result<usize,Self::Error>{letmut written = 0;for&byte in data {ifself.push(byte).is_err(){break;// Stop writing if the buffer is full}
written += 1;}Ok(written)// Return the number of bytes successfully written}fnflush(&mutself) -> Result<(),Self::Error>{// For this implementation, flushing is a no-opOk(())}}
This writer is passed to cli. And somewhere in your code you should do writer.pop() and write received byte to your async IO.
No, as I said, I'm not using async in my embedded projects. And in sync projects writer is just a small wrapper around a buffer. So there's nothing to extract to separate crate.
There's also BBQueue which is a queue that can be filled/drained both sync and async, so you can use that as the output of your CLI and then drain it in an async worker.
The issue why async support would be useful is not that the read/write needs to be async. The biggest issue is that process_byte is not async. So we cannot actually perform async tasks from within shell commands.
Using Embassy, I've come up with a solution based on pipes that implements both the buffer-based console IO concept and allows commands to run in an async context. It depends on being able to run embassy executors at multiple priorities, (with the console itself running at lower priority than everything else) and ends up blocking all progress on the lower-priority executor while a command is executing.
For input to and output from the console, define two pipes with somewhat arbitrary capacity that are analogous to stdin and stdout:
use embassy_sync::pipe;use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;typeConsolePipe = pipe::Pipe<CriticalSectionRawMutex,64>;staticCONSOLE_IN_PIPE:ConsolePipe = pipe::Pipe::new();staticCONSOLE_OUT_PIPE:ConsolePipe = pipe::Pipe::new();
I have two executors, one in thread mode and one in interrupt mode (I'm targeting RP2350, but most systems should have similar capability):
A task running on the interrupt executor shuttles bytes between external IO and the console pipes. In my application it's USB-CDC, but could be a regular UART or anything else.
#[embassy_executor::task]asyncfnrun_console_io() -> ! {use embassy_futures::select::select;loop{select(async{// Buffer size is arbitrary, can be adjusted depending on your needsletmut buf = [0u8;64];loop{let n = todo!("read from hardware into buf");CONSOLE_IN_PIPE.write_all(&buf[..n]).await;}},async{// Also arbitrary sizeletmut buf = [0u8;64];loop{let n = CONSOLE_OUT_PIPE.read(&mut buf).await;todo!("write buf[..n] to hardware");}}).await;// If hardware I/O is fallible, handle errors here or inside the inner loops}}
A small adapter to convert the async pipe to blocking I/O as expected by the CLI:
use core::sync::atomic::{AtomicUsize,Ordering};structSynchronousPipeBridge<'a>(pub&'staticConsolePipe,&'aAtomicUsize,);use core::convert::Infallible;use embedded_io::{ErrorTypeasBlockingErrorType,WriteasBlockingWrite};impl<'a>BlockingErrorTypeforSynchronousPipeBridge<'a>{typeError = Infallible;}impl<'a>BlockingWriteforSynchronousPipeBridge<'a>{fnwrite(&mutself,buf:&[u8]) -> Result<usize,Infallible>{matchself.0.try_write(buf){Ok(n) => Ok(n),Err(_) => {// No space available in the pipe. Silently drop the write but record how many bytes have been// lost, so we can report the overrun.self.1.fetch_add(buf.len(),Ordering::AcqRel);Ok(buf.len())}}}fnflush(&mutself) -> Result<(),Infallible>{Ok(())}}
In a task that runs in thread mode, drive the console:
#[embassy_executor::task]asyncfnconsole_task(spawner:SendSpawner){let r = &CONSOLE_IN_PIPE;let dropped_count = AtomicUsize::new(0);let w = SynchronousPipeBridge::new(&CONSOLE_OUT_PIPE,&dropped_count);letmut cli = CliBuilder::default().writer(w).build().unwrap();loop{let dropped = dropped_count.swap(0,Ordering::AcqRel);if dropped > 0{// Write a warning to logs or something}letmut c = 0u8;
r.read_exact(slice::from_mut(&c)).await.expect("Pipe read is infallible");letmut processor = Command::processor(|_cli, cmd| {// See belowhandle_command(&cmd, spawner);Ok(())}
cli.process_byte::<Command<'_>,_>(c,&mut processor).expect("pipe writes are infallible");}}
The final piece is bridging from the synchronous command processor into an async task, with a little bit of unsafe to avoid needing to copy things into the task. In the interest of efficiency I'm using wfe/sev because I'm not aiming for great portability beyond Cortex-M and they allow the system to idle in a lower-power mode, but they can be omitted without harm.
use core::sync::atomic::AtomicBool;use cortex_m::{
asm::{sev, wfe},
peripheral::{SCB, scb}};fnhandle_command(command:&Command<'_>,spawner:SendSpawner){staticDONE:AtomicBool = AtomicBool::new(false);fnmark_done(){DONE.store(true,Ordering::Release);sev();}// Before each call, the task is not doneDONE.store(false,Ordering::Release);// Launder parameter lifetime. This is safe as long as the task never refers to its// reference parameters after calling mark_done().let command = unsafe{
core::mem::transmute::<&'_Command<'_>,&'staticCommand<'static>>(command)};
spawner.spawn(handle_command_task(&CONSOLE_IN_PIPE, command, mark_done).expect("handle_command is not reentrant"));// Wait for task to complete. SendSpawner always executes tasks on an interrupt executor,// so verifying we're currently in thread mode ensures the task is running at higher priority// and this won't deadlock.assert_eq!(SCB::vect_active(), scb::VectActive::ThreadMode);while !DONE.load(Ordering::Acquire){wfe();}}#[embassy_executor::task]asyncfnhandle_command_task(writer:&'staticConsolePipe,command:&'staticCommand<'static>,mark_done:fn(),){
scopeguard::defer! {
mark_done();}todo!("process command, write output to writer");}
The portable version of this polls DONE in a tight loop, but you could insert some other useful work or delay there as well.
I made a draft PR using async closures, changed like 20 lines of code in total, desktop example works, other existing blocking ones obviously don't. #28
Activity
funbiscuit commentedon Dec 9, 2024
No, currently there is no plans to support async IO.
Take a look at similar issue: #13
You can add RX and TX buffers and fill RX in your async code. Then in some dedicated "thread" you will take all bytes one by one from RX buffer, pass to cli. In cli pass writer that will add bytes to TX buffer. And in your async code take bytes from TX buffer and write to your IO backend. Of course you will have to handle overflows somehow.
Approach with RX/TX buffers could be supported by library one day but currently I don't have resources for that. But PRs are welcome
tommasoclini commentedon Dec 9, 2024
What writer do you use for writing to a buffer? Do you implement it for every async project? Is it in a crates.io crate?
funbiscuit commentedon Dec 10, 2024
I don't use it in async projects, so I don't have a writer. As I said, just create two buffers (RX and TX) behind a mutex. You can do something like this for TX (and for RX everything is similar). Mostly generated by chatgpt, but you get the idea:
This writer is passed to cli. And somewhere in your code you should do
writer.pop()and write received byte to your async IO.tommasoclini commentedon Dec 10, 2024
So you just implement it every time, right?
funbiscuit commentedon Dec 10, 2024
No, as I said, I'm not using async in my embedded projects. And in sync projects writer is just a small wrapper around a buffer. So there's nothing to extract to separate crate.
tommasoclini commentedon Dec 10, 2024
Can you show me an example? I don't get how I'm supposed to flush the buffer out if I move the writer to the cli when I build it.
funbiscuit commentedon Dec 10, 2024
Take a look at api of heapless queue. There is a method for splitting:
https://docs.rs/heapless/latest/heapless/spsc/index.html
writer then will have consumer and you use producer to take out bytes that were written
Finomnis commentedon May 14, 2026
There's also
BBQueuewhich is a queue that can be filled/drained both sync and async, so you can use that as the output of your CLI and then drain it in an async worker.Finomnis commentedon May 19, 2026
The issue why async support would be useful is not that the read/write needs to be async. The biggest issue is that
process_byteis not async. So we cannot actually perform async tasks from within shell commands.tari commentedon Jun 10, 2026
Using Embassy, I've come up with a solution based on pipes that implements both the buffer-based console IO concept and allows commands to run in an async context. It depends on being able to run embassy executors at multiple priorities, (with the console itself running at lower priority than everything else) and ends up blocking all progress on the lower-priority executor while a command is executing.
For input to and output from the console, define two pipes with somewhat arbitrary capacity that are analogous to stdin and stdout:
I have two executors, one in thread mode and one in interrupt mode (I'm targeting RP2350, but most systems should have similar capability):
A task running on the interrupt executor shuttles bytes between external IO and the console pipes. In my application it's USB-CDC, but could be a regular UART or anything else.
A small adapter to convert the async pipe to blocking I/O as expected by the CLI:
In a task that runs in thread mode, drive the console:
The final piece is bridging from the synchronous command processor into an async task, with a little bit of
unsafeto avoid needing to copy things into the task. In the interest of efficiency I'm usingwfe/sevbecause I'm not aiming for great portability beyond Cortex-M and they allow the system to idle in a lower-power mode, but they can be omitted without harm.The portable version of this polls
DONEin a tight loop, but you could insert some other useful work or delay there as well.tommasoclini commentedon Jun 10, 2026
async closures have been stable for a while, could these be used instead in the processor function?
Finomnis commentedon Jun 10, 2026
@tommasoclini then the processor function itself would have to become async, which would be quite a rewrite
tommasoclini commentedon Jun 10, 2026
Sooner or later I think async is the way to go anyway
tommasoclini commentedon Jun 10, 2026
I made a draft PR using async closures, changed like 20 lines of code in total, desktop example works, other existing blocking ones obviously don't.
#28
tommasoclini commentedon Jul 31, 2026
@tari you can take a look at #30 if you need async functionality.