This document describes the internal architecture of this crate as currently implemented. The crate implements the host side of ASHv2 over caller-provided asynchronous byte streams. The transport is commonly a serial link to an NCP, but the core architecture is independent of how that stream is opened and configured.
At runtime, the crate is centered around start(...), which creates two asynchronous
actor futures:
Transmitter: owns the suppliedAsyncWriteimplementation and connection state.Receiver: owns the suppliedAsyncReadimplementation and inbound frame handling.
start(reader, writer, response) takes transport halves that the caller has already opened,
configured, and split. It returns Handle, the user-facing send handle, and a Futures
container with transmitter and receiver futures that the caller must spawn or poll on their async
runtime. Incoming payloads are pushed to the user-provided response channel.
flowchart TD
App[Application]
Transport[Caller-owned transport setup]
Start[start]
Handle[Handle]
Futures[Futures]
Tx[Transmitter future]
Rx[Receiver future]
Reader[AsyncRead implementation]
Writer[AsyncWrite implementation]
MsgQ[(tokio mpsc Message queue)]
RespQ[(tokio mpsc Payload queue)]
App --> Transport
Transport --> Reader
Transport --> Writer
App -->|reader + writer + response channel| Start
Reader --> Start
Writer --> Start
Start -->|returns| Handle
Start -->|returns| Futures
Futures --> Tx
Futures --> Rx
App -->|send payload| Handle
Handle -->|Message::Payload| MsgQ
MsgQ --> Tx
Tx -->|write frames| Writer
Reader -->|read frames| Rx
Rx -->|inbound payload| RespQ
RespQ --> App
Rx -->|ACK/NAK/RST/RST-ACK/ERROR as Message| MsgQ
Handle -.->|last handle dropped closes queue| MsgQ
Tx -->|sets running=false on exit| Rx
src/actor/*start(...), internal message bus, and caller-owned future lifecycle.
src/actor/receiver/buffer.rs- Receive-side chunk buffering, byte scanning, control-byte handling, unstuffing, and frame parsing.
src/actor/transmitter/buffer.rs- Transmit-side frame serialization, byte stuffing, frame termination, and asynchronous writes.
src/frame/*- Frame data structures and binary conversion for
DATA,ACK,NAK,RST,RST-ACK,ERROR.
- Frame data structures and binary conversion for
src/frame/headers/*- Bit-level header composition/parsing for
DATA,ACK,NAK.
- Bit-level header composition/parsing for
src/protocol/randomization.rs- ASH payload randomization (masking).
src/protocol/stuffing.rs- Byte stuffing and unstuffing around control bytes.
src/validate.rs- CRC-16-IBM-3740 validation.
src/ezsp/*(featureezsp)- Optional adapters from typed EZSP frames to ASHv2 payloads and back.
The transmitter is the owner of connection state (Uninitialized, Connected, Failed).
On startup it sends RST, waits for RST-ACK, and only then handles payload traffic normally.
stateDiagram-v2
[*] --> Uninitialized
Uninitialized --> Connected: valid RST-ACK (version=2, in time)
Uninitialized --> Uninitialized: resend RST / requeue messages
Connected --> Failed: I/O error or inbound RST/ERROR
Failed --> Uninitialized: reset() sends RST
Connected --> Connected: DATA/ACK/NAK exchange
- App calls
Handle::send(payload).await. HandlesendsMessage::Payloadinto the transmitter queue with a oneshot response channel.- Transmitter creates a
DATAframe:- sets frame number (
u8, masked to 3 bits for modulo-8 behavior), - sets current ACK number,
- masks payload bytes,
- computes CRC.
- sets frame number (
- Transmitter writes via write buffer:
- convert frame to bytes,
- stuff reserved control bytes,
- append
FLAG (0x7E), - write to the caller-provided
AsyncWriteimplementation.
- Transmitter stores transmission metadata for ACK/NAK-based completion/retransmission.
- The caller-provided
AsyncReadimplementation supplies inbound bytes. - The receiver buffer retains any unconsumed bytes between reads.
- Receiver reads bytes until
FLAG. - Receiver handles control bytes (
CANCEL,SUBSTITUTE,XON,XOFF,WAKE) and un-stuffs payload bytes. - Parsed bytes are converted into a typed frame and CRC-validated.
- Receiver behavior by frame type:
DATA: sequence check, sendACKorNAK, unmask payload, forward to response channel.ACK: notify transmitter to retire sent frames up to ACK number.NAK: notify transmitter to retransmit matching sent frame.RST,RST-ACK,ERROR: forward to transmitter for connection-state handling.
sequenceDiagram
participant A as Application
participant H as Handle
participant T as Transmitter
participant S as Byte Stream
participant R as Receiver
participant Q as Response Channel
A->>H: send(payload)
H->>T: Message::Payload
T->>S: DATA(frame, masked payload)
S->>R: inbound frame bytes
R->>T: Message::ReceivedAck / ReceivedNak
R->>Q: unmasked payload
Q->>A: Payload
The core API is generic over separate Tokio AsyncRead and AsyncWrite implementations. It does
not depend on serialport or async-serialport, and does not open, configure, or split a serial
port. Those transport-specific operations belong to the calling application.
The receiver buffer reads chunks directly from the supplied reader and retains bytes after a completed frame for the next read. The transmitter buffer writes fully encoded and stuffed frames through the supplied writer.
flowchart TD
Setup[Caller transport setup]
Reader[AsyncRead]
Writer[AsyncWrite]
RxBuffer[Receiver Buffer]
TxBuffer[Transmitter Buffer]
Receiver[Receiver future]
Transmitter[Transmitter future]
Setup --> Reader
Setup --> Writer
Reader --> RxBuffer
RxBuffer --> Receiver
Transmitter --> TxBuffer
TxBuffer --> Writer
The ezsp feature enables the public ashv2::ezsp module and its optional ezsp and
le-stream dependencies. It does not change the core transport or actor API.
ashv2::ezsp::TransmitteraliasesHandle, whoseezsp::Transmitimplementation encodes typed EZSP headers and parameters intoPayload.ashv2::ezsp::Receiverowns the response channel's receiver, decodes each inboundPayloadinto a typed EZSP frame, and implementsezsp::Receive. The EZSP layer passes its negotiated protocol version into each receive call.
flowchart LR
EzspApi[EZSP API]
EzspTx[EZSP Transmitter]
Handle[ASHv2 Handle]
Actors[ASHv2 actors]
PayloadQ[(Payload channel)]
EzspRx[EZSP Receiver]
EzspApi -->|typed request| EzspTx
EzspTx -->|encoded Payload| Handle
Handle --> Actors
Actors -->|inbound Payload| PayloadQ
PayloadQ --> EzspRx
EzspRx -->|typed response| EzspApi
The actor does not use a terminate message. Each clone of the user-facing Handle keeps the
outbound message queue open. After every handle has been dropped, the queue closes. The
transmitter drains any messages already in the queue and then exits its main loop.
When the transmitter exits, it clears the shared running flag observed by the receiver, causing the receiver to terminate. The caller must continue polling or awaiting both returned futures during this process. Their transport halves are dropped when the futures complete; any additional transport resource cleanup is the caller's responsibility.
sequenceDiagram
participant A as Application
participant H as Handle clones
participant Q as Message queue
participant T as Transmitter
participant R as Receiver
A->>H: drop every handle
H-->>Q: last sender dropped
Q-->>T: queue drained and closed
T->>T: exit transmit loop
T-->>R: running = false
R->>R: exit receive loop
| Frame | Header Pattern | Purpose | Key fields |
|---|---|---|---|
DATA |
bit7 = 0 | Carry protocol payload data | frame number (3 bits), retransmit flag, ACK number (3 bits), payload, CRC |
ACK |
0b1000_xxxx with type bits for ACK |
Positive acknowledgement of received data | ACK number, nRDY, CRC |
NAK |
0b1010_xxxx with type bits for NAK |
Negative acknowledgement, requests retransmit | ACK number, nRDY, CRC |
RST |
0xC0 |
Request reset / restart link establishment | fixed header, CRC |
RST-ACK |
0xC1 |
Confirm reset and report reset reason/version | protocol version, reset code, CRC |
ERROR |
0xC2 |
Signal protocol/link error | protocol version, error code, CRC |
- bits
6..4: frame number (u8, lower 3 significant bits) - bit
3: retransmit flag - bits
2..0: ACK number (u8, lower 3 significant bits)
- ACK base:
0b1000_0000 - NAK base:
0b1010_0000 - bit
3:nRDY - bits
2..0: ACK number (u8, lower 3 significant bits)
- Sliding window capacity is
TX_K(default5), stored in a fixed-capacity queue. - Payload requests are requeued without delay when the sliding window is full.
- Payload requests remain queued until the initial reset handshake completes.
- Each queued transmission tracks:
- send time (
Instant), - frame number,
- retransmit count.
- send time (
- On inbound
ACK, matching transmitted frames are retired. - On inbound
NAK, matching frame is removed and retransmitted with retransmit flag set. - Timed-out transmissions are dropped when processing ACK/NAK maintenance.
- After too many retransmissions (
ACK_TIMEOUTS = 4in current code), transmit returns timeout error.
- CRC algorithm:
CRC-16-IBM-3740. - CRC is computed over frame bytes excluding the CRC field itself.
- Receiver validates CRC per frame type before semantic handling.
- Invalid CRC in inbound
DATAtriggersNAK; invalid control frames are ignored with warning logs.
ASH payload randomization is implemented as XOR masking on payload bytes (not on headers/CRC):
- Generator state defaults:
- seed:
0x42 - feedback mask:
0xB8 - flag bit:
0x01
- seed:
- For each generated mask byte:
- output current
randomvalue, - shift
randomright by one, - if previous output had flag bit set, XOR shifted value with
0xB8.
- output current
Each payload byte is XORed with one generated mask byte.
Important properties:
- Symmetric transform: applying
mask()twice restores original bytes. - Used on send path before CRC computation for
DATA. - Used on receive path before delivering payload to the application.
To preserve frame boundaries and control semantics on the serial stream, reserved bytes are escaped.
Reserved bytes:
0x7D(ESCAPE)0x7E(FLAG)0x11(XON)0x13(XOFF)0x18(SUBSTITUTE)0x1A(CANCEL)
For each reserved byte in frame content:
- insert
ESCAPE (0x7D)before it, - toggle bit 5 of original byte (
byte ^= 0x20).
After stuffing all bytes, append final FLAG (0x7E) to terminate the frame.
The receiver scans bytes until FLAG.
- On
ESCAPE, it removes that byte and marks the next byte for de-escaping. - The next byte is restored by toggling bit 5 (
byte ^= 0x20).
Control-byte handling during stream parsing:
FLAG (0x7E): frame boundary.CANCEL (0x1A): clear current buffer and error state.SUBSTITUTE (0x18): set error condition; current frame is discarded on nextFLAG.XON/XOFF: consumed as flow-control indications (not frame payload data).WAKE (0xFF): treated as wake signal when buffer is empty.
Compile-time environment overridable constants:
ASHV2_MAX_PAYLOAD_SIZE(default128)ASHV2_T_RSTACK_MAX_MILLIS(default3200)ASHV2_TX_K(default5)ASHV2_T_RX_ACK_MAX_MILLIS(default3200)
GitHub Actions workflow (.github/workflows/rust.yml) runs:
- formatting check (
cargo +nightly fmt --check) - clippy with warnings denied
- tests (
cargo test --all-features) - release build (
cargo build --all-features --release) cargo vetsupply-chain checks