Embassy Integration
The ferrite-embassy crate provides Embassy executor tasks for periodic and triggered data uploads. This guide covers setup, transport implementation, task spawning, and advanced patterns.
Overview
Embassy is an async runtime for embedded Rust. The ferrite-sdk Embassy integration provides two #[embassy_executor::task] functions:
ferrite_upload_task-- uploads on a fixed interval usingembassy_time::Timer.ferrite_upload_task_with_trigger-- uploads on a fixed interval OR immediately whentrigger_upload_now()is called, whichever comes first.
Both tasks run forever (return type -> !) and should be spawned once at startup.
Dependencies
[dependencies]
ferrite-sdk = { git = "https://github.com/mighty840/ferrite-sdk", features = ["cortex-m", "defmt", "embassy"] }
ferrite-embassy = { git = "https://github.com/mighty840/ferrite-sdk" }
embassy-executor = { version = "0.6", features = ["task-arena-size-65536", "arch-cortex-m", "executor-thread"] }
embassy-time = "0.3"
embassy-sync = "0.5"
embassy-futures = "0.1"The embassy feature on ferrite-sdk enables the AsyncChunkTransport trait and the async upload_async method on UploadManager.
Implementing AsyncChunkTransport
You need to provide a type that implements AsyncChunkTransport. The trait has one required method and three optional ones:
pub trait AsyncChunkTransport {
type Error: core::fmt::Debug;
/// Send a single encoded chunk (up to 256 bytes).
async fn send_chunk(&mut self, chunk: &[u8]) -> Result<(), Self::Error>;
/// Return true if the transport is available for sending.
fn is_available(&self) -> bool { true }
/// Called before a batch upload begins.
async fn begin_session(&mut self) -> Result<(), Self::Error> { Ok(()) }
/// Called after a batch upload completes.
async fn end_session(&mut self) -> Result<(), Self::Error> { Ok(()) }
}UART transport example
use embassy_nrf::uarte::{self, UarteTx};
use ferrite_sdk::transport::AsyncChunkTransport;
pub struct UartTransport {
tx: UarteTx<'static, embassy_nrf::peripherals::UARTE0>,
}
impl UartTransport {
pub fn new(tx: UarteTx<'static, embassy_nrf::peripherals::UARTE0>) -> Self {
Self { tx }
}
}
impl AsyncChunkTransport for UartTransport {
type Error = uarte::Error;
async fn send_chunk(&mut self, chunk: &[u8]) -> Result<(), Self::Error> {
self.tx.write(chunk).await
}
}BLE transport example
For BLE, you typically write chunks as GATT notifications. Here is a sketch using nrf-softdevice:
use nrf_softdevice::ble::gatt_server;
pub struct BleTransport {
conn: gatt_server::Connection,
tx_handle: u16,
}
impl AsyncChunkTransport for BleTransport {
type Error = gatt_server::NotifyValueError;
async fn send_chunk(&mut self, chunk: &[u8]) -> Result<(), Self::Error> {
gatt_server::notify_value(&self.conn, self.tx_handle, chunk)
}
fn is_available(&self) -> bool {
self.conn.is_connected()
}
}HTTP transport example
If your device has a TCP/IP stack (e.g., via ESP32 coprocessor or Ethernet), you can buffer chunks and send them as a single HTTP POST:
pub struct HttpTransport {
buffer: heapless::Vec<u8, 4096>,
// ... TCP socket handle
}
impl AsyncChunkTransport for HttpTransport {
type Error = TcpError;
async fn send_chunk(&mut self, chunk: &[u8]) -> Result<(), Self::Error> {
// Buffer chunks; flush on end_session
self.buffer.extend_from_slice(chunk).map_err(|_| TcpError::BufferFull)?;
Ok(())
}
async fn end_session(&mut self) -> Result<(), Self::Error> {
// Send all buffered chunks as one HTTP POST to /ingest/chunks
let body = &self.buffer[..];
self.tcp_post("/ingest/chunks", body).await?;
self.buffer.clear();
Ok(())
}
}Spawning the upload task
Periodic upload
The simplest pattern -- upload every N seconds:
use ferrite_embassy::upload_task::ferrite_upload_task;
use embassy_time::Duration;
#[embassy_executor::main]
async fn main(spawner: Spawner) {
// ... init SDK, configure peripherals ...
let transport = UartTransport::new(uart_tx);
spawner.spawn(ferrite_upload_task(transport, Duration::from_secs(60))).unwrap();
}The task logs upload results via defmt:
INFO ferrite upload ok: 4 chunks, 218 bytesIf the transport fails:
WARN ferrite upload failed: TransportError(Overrun)Triggered upload
Use ferrite_upload_task_with_trigger when you want to upload immediately after a specific event (e.g., detecting a previous crash on boot):
use ferrite_embassy::upload_task::{ferrite_upload_task_with_trigger, trigger_upload_now};
#[embassy_executor::main]
async fn main(spawner: Spawner) {
// ... init SDK ...
let transport = UartTransport::new(uart_tx);
spawner.spawn(
ferrite_upload_task_with_trigger(transport, Duration::from_secs(60))
).unwrap();
// If we recovered from a fault, upload immediately
if ferrite_sdk::fault::last_fault().is_some() {
defmt::info!("fault record found from previous boot, triggering upload");
trigger_upload_now();
}
}trigger_upload_now() is safe to call from any context -- interrupts, other tasks, or the main task. It uses a Channel<CriticalSectionRawMutex, (), 1> internally, so multiple rapid triggers coalesce into a single upload cycle.
The task uses embassy_futures::select to race the periodic timer against the trigger channel. Whichever fires first causes an upload. After the upload completes, any extra pending triggers are drained so the task does not immediately loop again.
Upload session internals
Each upload cycle calls UploadManager::upload_async(), which:
- Checks
is_available()-- if false, returnsErr(TransportUnavailable)without touching any data. - Calls
begin_session(). - Collects all chunks inside a critical section (to avoid holding the critical section across async points).
- Sends each chunk via
send_chunk(). - On success, clears all SDK buffers inside a critical section.
- Calls
end_session().
The critical section in step 3 is brief -- it only copies data out of the SDK state into a temporary heapless::Vec of chunks. The actual transport I/O happens outside the critical section, so it does not block interrupts.
Error handling
If send_chunk() returns an error for any chunk, the entire session is aborted. Data is not cleared from the SDK buffers, so it will be retried on the next upload cycle. This means:
- Metrics are retained until successfully uploaded.
- Fault records survive across multiple failed upload attempts.
- Trace data persists (but may be overwritten by new frames if the circular buffer wraps).
The upload task never panics or exits. Errors are logged via defmt and the task sleeps until the next interval or trigger.
Task arena sizing
The upload task allocates a temporary buffer of up to 32 chunks (each up to 256 bytes) on the Embassy task's stack. Make sure your task-arena-size is large enough. The default 65536 is sufficient for most applications. If you have very constrained memory, you can reduce it but watch for stack overflows.