Implementation of Silicon Labs' Asynchronous Serial Host protocol v2 (ASHv2), host side.
Silicon Labs publishes the documentation online:
The crate currently provides:
- Frame parsing/encoding for
DATA,ACK,NAK,RST,RST-ACK, andERROR. - CRC-16 validation/generation for all supported frame types.
- Byte stuffing/unstuffing and ASH payload randomization (masking/unmasking).
- Async actor futures created with
start(...), with caller-owned transmitter/receiver execution. - Async serial I/O through
async-serialport, with receiver-side chunking viaReaderStream. - Automatic initial reset handshake (
RST->RST-ACK) before normal traffic. - Automatic handling of inbound
ACK/NAKand retransmission of queuedDATAframes. - Automatic reset/recovery on protocol errors (
ERROR,RST, and selected I/O failures).
Important behavior details:
start(...)splits the native serial port into async worker, transmitter, and receiver futures, then returns them in a namedFuturescontainer for the caller to spawn or poll.- The crate does not spawn Tokio tasks internally.
Handle::send(payload).awaitconfirms local transmission attempt (I/O success), not the remote ASH response payload.Handle::send(payload).awaitreturnsErrorKind::NotConnectedwhile the ASH link is not established.- When the transmit window is full, the transmitter requeues the payload request without delay.
- Incoming
DATApayloads are delivered through the response channel passed tostart(...). - Payload type is
heapless::Vec<u8, MAX_PAYLOAD_SIZE>(MAX_PAYLOAD_SIZEdefaults to128).
Compile-time tunables (via const_env):
ASHV2_MAX_PAYLOAD_SIZE(default:128)ASHV2_T_RSTACK_MAX_MILLIS(default:3200)ASHV2_TX_K(default:5)ASHV2_T_RX_ACK_MAX_MILLIS(default:3200)
use ashv2::{FlowControl, open, start};
use tokio::sync::mpsc::channel;
#[tokio::main]
async fn main() {
// Open serial port connected to the NCP.
// Baud rate is derived from flow control by the crate.
let serial_port = open("/dev/ttyUSB0", FlowControl::Hardware)
.expect("Failed to open serial port");
// Channel for inbound ASH DATA payloads from the NCP.
let (response_tx, mut response_rx) = channel(64);
// Create ASH actor futures and spawn them on this application's runtime.
let (handle, futures) = start(serial_port, response_tx);
let serial_worker = tokio::spawn(futures.serial_worker);
let transmitter = tokio::spawn(futures.transmitter);
let receiver = tokio::spawn(futures.receiver);
// Example EZSP "version" request payload.
let request_payload = [0x00, 0x00, 0x00, 0x02].into_iter().collect();
handle
.send(request_payload)
.await
.expect("Failed to transmit request frame");
// Receive inbound DATA payload from NCP.
if let Some(response_payload) = response_rx.recv().await {
println!("Received response payload: {response_payload:?}");
}
// Request graceful shutdown, then join the runtime-owned futures.
handle
.terminate()
.await
.expect("Failed to request actor termination");
receiver.await.expect("Receiver task failed");
transmitter.await.expect("Transmitter task failed");
let _serial_port = serial_worker.await.expect("Serial worker task failed");
}The CI workflow currently runs:
cargo +nightly fmt --checkcargo clippy --all-features -- -A clippy::multiple_crate_versions -D warningscargo test --all-featurescargo build --all-features --releasecargo vet check
This project is free software and is not affiliated with Silicon Labs.
Special thanks to Simon Farnsworth, Kevin Reid, and the community at https://users.rust-lang.org/.