From 188e445e802d1496aaeafdfb379a471b78f27406 Mon Sep 17 00:00:00 2001 From: Stephen Waits Date: Wed, 29 Apr 2026 23:56:43 -0600 Subject: [PATCH] docs: consolidate PROTOCOL.md to canonical home, move BLE_MUX_DESIGN_DOC into docs/ --- PROTOCOL.md | 1923 ----------------- README.md | 6 +- .../ble_mux_design.md | 0 3 files changed, 5 insertions(+), 1924 deletions(-) delete mode 100644 PROTOCOL.md rename BLE_MUX_DESIGN_DOC.md => docs/ble_mux_design.md (100%) diff --git a/PROTOCOL.md b/PROTOCOL.md deleted file mode 100644 index f22385c..0000000 --- a/PROTOCOL.md +++ /dev/null @@ -1,1923 +0,0 @@ -# DongLoRa Protocol v2 Specification - -**Version 1.0** - -A minimal, reliable, transport-agnostic wire protocol for exposing a LoRa radio transceiver as a USB-serial (or BLE / WiFi / TCP) device. This document is the normative reference for implementers of both host software and device firmware. - ---- - -## Table of Contents - -1. Overview -2. Frame Format -3. State Machine -4. Message Types -5. Common Data Types -6. Message Reference -7. Error Codes -8. Radio Chip ID Enum -9. Capability Bitmap -10. Modulation Parameters -11. Ordering and Interleaving -12. Flow Control and Backpressure -13. Multi-Client Operation (Future) -14. Host Behavior Requirements -15. Device Behavior Requirements -16. Version Compatibility -17. Appendix A — COBS Reference -18. Appendix B — CRC Reference -19. Appendix C — Wire Examples -20. Appendix D — Glossary - ---- - -## 1. Overview - -DongLoRa Protocol is the wire protocol spoken between a host application and firmware on a microcontroller attached to a Semtech LoRa transceiver (SX126x, SX127x, SX128x, LR11xx, LR2021, or similar). It exposes the transceiver's native capabilities over a byte-stream transport with no editorialization: the host asks, the chip does. - -### 1.1 Design Principles - -1. **Minimal.** Ten message types. No sub-protocols, no modes, no sessions. -2. **Transport-agnostic.** Runs identically over USB CDC-ACM, USB-to-UART bridges, plain UART, BLE, TCP, or any other ordered byte stream. -3. **No persistent state.** The device boots unconfigured and forgets everything on host disconnect. There is no device-side memory between sessions. -4. **No policy.** The firmware does not enforce regulatory limits, retry on failure, reorder TX, or apply profiles. The host is authoritative for all policy. -5. **Binary.** No text, no JSON, no AT commands. Every bit is accounted for. -6. **Host drives.** The device never initiates a conversation. Every device-to-host frame is either a response to a command or a radio-sourced event. - -### 1.2 Terminology - -- **Device**: MCU + radio transceiver + firmware that speaks DongLoRa Protocol. -- **Host**: Any software endpoint speaking DongLoRa Protocol. -- **Client**: In the multi-client future, an individual host connection. A single-client USB deployment has one client, which is the host. -- **Frame**: A single DongLoRa Protocol message on the wire, COBS-encoded and delimited. -- **Tag**: 16-bit correlation identifier for matching responses to commands. -- **CONFIGURED / UNCONFIGURED**: The two states of the device's state machine (Section 3). - ---- - -## 2. Frame Format - -### 2.1 Layout - -Every DongLoRa Protocol frame has this pre-encoding structure: - -``` -┌──────┬──────┬──────────┬───────┐ -│ type │ tag │ payload │ crc16 │ -│ u8 │ u16 │ 0..P │ u16 │ -└──────┴──────┴──────────┴───────┘ -``` - -`P` is the maximum payload size for the largest message type, computed as `max_payload_bytes + 20` bytes (to accommodate the `RX` event's 20-byte metadata prefix plus the `bytes` field). `max_payload_bytes` is reported by the device in `GET_INFO` (Section 6.2) and reflects the chip's maximum over-the-air packet length. - -The pre-encoding structure is then COBS-encoded (Section 2.4) and terminated with a single `0x00` byte on the wire: - -``` -┌────────────────────────────────────┬──────┐ -│ COBS(type | tag | payload | crc16) │ 0x00 │ -└────────────────────────────────────┴──────┘ -``` - -All multi-byte integer fields are **little-endian**. - -### 2.2 Fields - -**`type`** (u8): Message type identifier. See Section 4. - -**`tag`** (u16, little-endian): -- On host→device commands: a host-chosen correlation ID. MUST NOT be `0x0000`. -- On device→host command responses (`OK`, `ERR`, `TX_DONE`): echoes the tag of the originating command. -- On device→host asynchronous events (`RX`, asynchronous `ERR`): always `0x0000`. - -Tag uniqueness: a tag is **outstanding** from the moment the host transmits the command until the host has received the final response for that tag (`OK`, `ERR`, or for `TX` the `TX_DONE`). Hosts MUST NOT reuse a tag that is currently outstanding. Once a tag is closed it may be reused freely. A monotonic counter (starting at 1, wrapping at `0xFFFF` and skipping `0x0000`) is the recommended implementation. - -Tag = 0x0000 on a host→device command is a protocol violation. The device MUST treat such a frame as a framing error and discard it (emitting asynchronous `ERR(EFRAME)` per Section 2.5), because a response with tag 0x0000 would be indistinguishable from an asynchronous event. Hosts MUST never send tag 0x0000. - -**`payload`** (variable length, 0 to `max_payload_bytes + 20` bytes): -- Structure defined per message type (Section 6). -- Length is implicit from the frame boundary: the payload runs from the byte after the tag to two bytes before the end of the decoded frame. - -**`crc16`** (u16, little-endian): -- CRC-16/CCITT-FALSE, computed over `type || tag || payload` (pre-COBS bytes, not including the CRC itself). -- Polynomial: `0x1021` -- Initial value: `0xFFFF` -- No reflection (input or output) -- XOR out: `0x0000` -- Check value for ASCII `"123456789"`: `0x29B1` - -This variant is also known as CRC-16/AUTOSAR or CRC-16/IBM-3740. It is NOT the same as CRC-16/KERMIT, CRC-16/XMODEM, or CRC-16/CCITT (which use different initial values or reflect their input). - -### 2.3 Frame Size Bounds - -- Minimum pre-COBS frame: 5 bytes (type + tag + CRC, empty payload). -- Maximum pre-COBS frame: `5 + max_payload_bytes + 20` bytes. For a device reporting `max_payload_bytes = 255`, this is 280 bytes. -- COBS adds at most `ceil(pre_cobs_length / 254) + 1` bytes of overhead. -- Plus one `0x00` delimiter byte. - -Implementations SHOULD use buffer sizes of at least `max_payload_bytes + 32` bytes to accommodate all frames comfortably. - -### 2.4 COBS Encoding - -Frames are encoded with Consistent Overhead Byte Stuffing (COBS), using `0x00` as the framing byte. This guarantees that `0x00` never appears within an encoded frame, so a single `0x00` reliably marks frame boundaries. - -**Algorithm**: The standard COBS algorithm (Cheshire & Baker, 1999). Reference implementation in Appendix A. - -**Decoder resynchronization**: A receiver that opens the stream mid-frame, or that has lost bytes, discards received bytes until the next `0x00` and begins decoding at the byte following that `0x00`. No other synchronization mechanism is used. - -**Overhead**: At most `ceil(N / 254) + 1` bytes for a pre-encoded frame of N bytes, plus the trailing `0x00` delimiter. For typical frames (under 254 bytes), overhead is 2 bytes. - -### 2.5 Receiver Parsing Algorithm - -On receipt of a byte stream: - -1. Accumulate bytes until a `0x00` byte is received. That `0x00` is the frame delimiter. -2. COBS-decode the bytes that preceded the delimiter. -3. If the decoded length is less than 5 bytes, discard. Device MAY emit asynchronous `ERR(EFRAME)`. -4. Split the decoded bytes: `type` (1) || `tag` (2) || `payload` (variable) || `crc16` (2). -5. Compute CRC-16/CCITT-FALSE over `type || tag || payload`. If it does not match `crc16`, discard. Device MAY emit asynchronous `ERR(EFRAME)`. -6. Dispatch by `type`. Unknown `type` byte on the device side: emit `ERR(EUNKNOWN_CMD)` with the received tag. Unknown `type` on the host side: silently discard. - -A partial frame at stream close (bytes received without a terminating `0x00`) is always discarded. - ---- - -## 3. State Machine - -The device has exactly two states: **UNCONFIGURED** and **CONFIGURED**. - -``` - ┌──────────────────┐ - │ UNCONFIGURED │ ← initial state on boot - └──────────────────┘ - │ ▲ - SET_CONFIG │ │ inactivity timeout (1 s) - result= │ │ OR client disconnect - APPLIED ▼ │ - ┌──────────────────┐ - │ CONFIGURED │ - └──────────────────┘ -``` - -### 3.1 UNCONFIGURED - -- Entered on boot, after inactivity timeout, or on final client disconnect. -- Radio is in standby (TX disabled, RX disabled, minimal current). -- TX queue is empty. RX ring is empty. -- Accepted commands: `PING`, `GET_INFO`, `SET_CONFIG`. -- Other commands (`TX`, `RX_START`, `RX_STOP`) return `ERR(ENOTCONFIGURED)`. - -### 3.2 CONFIGURED - -- Entered when `SET_CONFIG` returns `result = APPLIED`. -- Radio parameters match the most recent successful `SET_CONFIG` payload. -- All commands accepted. -- Radio is idle (no RX, no TX) unless `RX_START` has been issued or a TX is in flight. - -### 3.3 Transitions - -**UNCONFIGURED → CONFIGURED**: successful `SET_CONFIG` with `result = APPLIED`. - -**CONFIGURED → UNCONFIGURED**: -- Inactivity timer expires (no frames received for 1000 ms), OR -- Final client disconnects at the transport layer. - -Note: `SET_CONFIG` results of `ALREADY_MATCHED` or `LOCKED_MISMATCH` do NOT transition state. They are responses observing the current configuration. - -**Within CONFIGURED**: `SET_CONFIG` may be issued again to change params (subject to the lock in multi-client, Section 13). See Section 3.5 for operations spanning this transition. - -### 3.4 Liveness Timer - -- Each client maintains an inactivity timer with a fixed period of **1000 ms**. -- The timer is **inert** until the first frame from that client has been received. A freshly-booted device with no activity does not time out. The timer starts when the first frame arrives and resets on every subsequent frame. -- The timer is reset whenever any byte stream activity results in a complete frame arriving, whether that frame parses successfully or not. Even a frame that fails CRC or COBS resets the timer — the bytes arrived, meaning the host is present. -- If the timer expires: - 1. All queued-but-not-started TXs issued by that client are discarded silently. No `TX_DONE` is emitted. - 2. Any TX on the air is allowed to complete (it cannot be cleanly aborted). No `TX_DONE` is emitted. - 3. In single-client: the device transitions to UNCONFIGURED. RX ring is cleared. Any continuous RX is aborted. The timer returns to the inert state until the next frame arrives. - 4. In multi-client: only that client's session state is cleaned up. See Section 13. - -A client whose inactivity timer has fired is considered disconnected. Transport-level disconnect events (USB detach, BLE disconnect, TCP close) have identical effect to timer expiration. - -### 3.5 Operations Spanning Reconfiguration - -**`SET_CONFIG` while a TX is on the air**: the in-progress TX is allowed to physically complete. Its `TX_DONE` is emitted normally. Any queued-but-not-yet-started TXs are cancelled and emit `TX_DONE(result=CANCELLED)`. Continuous RX, if active, is aborted. RX ring is cleared. New configuration takes effect after the in-flight TX finishes. - -**`SET_CONFIG` validation failure (`ERR(EPARAM)`, `ERR(ELENGTH)`, `ERR(EMODULATION)`)**: the chip is not touched. All parameters are validated before any SPI write to the radio. If validation fails, the radio and state machine are unchanged. - -**`SET_CONFIG` with no lock conflict, applied successfully**: the radio is driven through its configuration sequence (which includes any required image calibration, ~3–15 ms on SX126x). The `OK` response is not emitted until the radio has completed configuration and is ready to accept TX / RX commands. - -**Hardware failure during `SET_CONFIG`**: If the radio does not respond to SPI commands or returns unexpected status during configuration, the device returns `ERR(ERADIO)` synchronously with the command's tag. The radio is placed in the safest reachable state (typically standby) and the device transitions to UNCONFIGURED. No follow-up asynchronous `ERR` is emitted for the same fault. - -**Disconnect during `SET_CONFIG`**: If the client disconnects while the radio is being reconfigured (the ~15 ms calibration window), the configuration completes to the chip (it is not interrupted), the `OK` is generated and then silently dropped, and normal disconnect handling proceeds: the device transitions to UNCONFIGURED and re-idles the radio. No partial-configuration state is possible. - ---- - -## 4. Message Types - -| Hex | Direction | Name | Brief | -|------|-----------|--------------|-------------------------------------------| -| 0x01 | H→D | PING | Liveness | -| 0x02 | H→D | GET_INFO | Query capabilities and identity | -| 0x03 | H→D | SET_CONFIG | Configure the radio | -| 0x04 | H→D | TX | Transmit a packet | -| 0x05 | H→D | RX_START | Begin continuous reception | -| 0x06 | H→D | RX_STOP | End continuous reception | -| 0x80 | D→H | OK | Positive response | -| 0x81 | D→H | ERR | Negative response or asynchronous fault | -| 0xC0 | D→H | RX | Packet received | -| 0xC1 | D→H | TX_DONE | TX concluded (transmitted/busy/cancelled) | - -All other `type` byte values are reserved. Devices MUST reject unknown command types (bytes with MSB = 0) with `ERR(EUNKNOWN_CMD)` echoing the received tag. Hosts MUST silently discard frames with unknown device→host type bytes (MSB = 1). - ---- - -## 5. Common Data Types - -### 5.1 Scalar Encodings - -| Name | Width | Encoding | -|------|-------|------------------------------------------| -| u8 | 1 | Unsigned byte | -| u16 | 2 | Unsigned, little-endian | -| u32 | 4 | Unsigned, little-endian | -| u64 | 8 | Unsigned, little-endian | -| i8 | 1 | Signed byte, two's complement | -| i16 | 2 | Signed, little-endian, two's complement | -| i32 | 4 | Signed, little-endian, two's complement | - -### 5.2 Physical-Unit Encodings - -| Quantity | Encoding | -|------------------|----------------------------------------------------------| -| Frequency | `u32`, hertz | -| TX power | `i8`, dBm | -| RSSI | `i16`, tenths of dBm (e.g., −1234 = −123.4 dBm) | -| SNR | `i16`, tenths of dB (e.g., −45 = −4.5 dB) | -| Frequency error | `i32`, hertz | -| Time interval | `u32`, microseconds | -| Monotonic time | `u64`, microseconds since device boot (does not wrap) | - -### 5.3 Variable-Length Fields - -Fields whose length is not explicitly given in a message's payload definition extend from their stated start offset to the end of the frame's payload. Their length is implicit from the total frame length. - -Each message type has at most two variable-length fields, and they always appear last. When two appear together (e.g., the UIDs in `GET_INFO` response), explicit length prefixes disambiguate them. - ---- - -## 6. Message Reference - -### 6.1 PING (0x01, H→D) - -**Purpose**: Keepalive. Sent by the host when it has no other work to do, to prevent the device's inactivity timer from expiring. May also be used to measure round-trip latency. - -**Payload**: Empty (0 bytes). - -**Response**: `OK` with empty payload, tag echoed. - -**Valid in**: any state (UNCONFIGURED or CONFIGURED). - -**Errors**: none specific. - ---- - -### 6.2 GET_INFO (0x02, H→D) - -**Purpose**: Query the device's identity, firmware version, radio type, capabilities, and queue capacities. Optional — the host is not required to call it. Typically called once, after connecting. - -**Payload**: Empty. - -**Response**: `OK` with the following payload, tag echoed: - -``` -Offset Size Field Description - 0 1 proto_major u8, protocol major version (1 for this spec) - 1 1 proto_minor u8, protocol minor version (0 for this spec) - 2 1 fw_major u8, firmware major - 3 1 fw_minor u8, firmware minor - 4 1 fw_patch u8, firmware patch - 5 2 radio_chip_id u16, see Section 8 - 7 8 capability_bitmap u64, see Section 9 -15 2 supported_sf_bitmap u16, bit N = LoRa SF N is supported (N in 0..15) -17 2 supported_bw_bitmap u16, bit N = LoRa BW enum value N is supported (N in 0..15) -19 2 max_payload_bytes u16, max over-the-air payload -21 2 rx_queue_capacity u16, RX ring depth -23 2 tx_queue_capacity u16, TX queue depth -25 4 freq_min_hz u32, minimum carrier frequency the radio supports -29 4 freq_max_hz u32, maximum carrier frequency the radio supports -33 1 tx_power_min_dbm i8, minimum TX power (most negative) -34 1 tx_power_max_dbm i8, maximum TX power -35 1 mcu_uid_len u8, 0 to 32 -36 N mcu_uid N = mcu_uid_len -36+N 1 radio_uid_len u8, 0 to 16 -37+N M radio_uid M = radio_uid_len -``` - -Total payload size: `37 + mcu_uid_len + radio_uid_len` bytes. - -**Valid in**: any state. - -**Errors**: none specific. - -**Notes**: -- `radio_uid_len` MAY be 0 if the radio has no accessible unique identifier. -- `mcu_uid` and `radio_uid` are opaque byte strings; hosts SHOULD treat them as unique identifiers for the silicon and not interpret their internal structure. -- `capability_bitmap` tells the host which modulations and features this device supports before it attempts `SET_CONFIG`. -- `supported_sf_bitmap` and `supported_bw_bitmap` describe LoRa-specific capabilities. Each bit N corresponds to value N of the respective enum (SF index for SF, BW enum value for BW). Hosts SHOULD consult these before issuing `SET_CONFIG` to avoid trial-and-error. Rationale: some chips have gaps (e.g., LLCC68 supports SF5–SF11 but not SF12; SX127x supports SF6–SF12 but not SF5), which a min/max pair cannot express. -- Both bitmaps are meaningful only when `capability_bitmap` bit 0 (LoRa) is set. On non-LoRa-capable devices they MAY be zero. -- `rx_queue_capacity` and `tx_queue_capacity` help the host pipeline intelligently without overrunning the device. -- `freq_min_hz` / `freq_max_hz` bracket the RF front-end's supported range for this specific board. A chip's silicon may cover a wider range than the matching network permits, so these values reflect the board's effective tuning window. -- `tx_power_min_dbm` / `tx_power_max_dbm` bracket the chip's settable TX power register range. The host SHOULD validate its `tx_power_dbm` against these bounds before calling `SET_CONFIG`; values outside the range yield `ERR(EPARAM)`. -- All `GET_INFO` fields are stable for the lifetime of a session. The device MUST NOT change them after boot. Hosts MAY cache the response for the duration of the connection. -- If the firmware cannot identify its radio (e.g., SPI probe failed), `radio_chip_id` is `0x0000` and the host SHOULD abort — the device will not function. - ---- - -### 6.3 SET_CONFIG (0x03, H→D) - -**Purpose**: Configure the radio for a given modulation and parameter set. This is the only command that can transition the device from UNCONFIGURED to CONFIGURED. It may also be issued in CONFIGURED to change parameters. - -**Payload**: - -``` -Offset Size Field Description - 0 1 modulation_id u8, see Section 10 - 1 N params Modulation-specific struct (Section 10) -``` - -The length of `params` is determined entirely by `modulation_id` (Section 10). Sending a payload whose total length does not match `1 + expected_param_size` yields `ERR(ELENGTH)`. - -**Response (on success)**: `OK` with the following payload, tag echoed: - -``` -Offset Size Field Description - 0 1 result u8: 0=APPLIED, 1=ALREADY_MATCHED, 2=LOCKED_MISMATCH - 1 1 owner u8: 0=NONE, 1=MINE, 2=OTHER - 2 1 current_modulation u8, modulation currently in effect on the radio - 3 N current_params Parameters currently in effect -``` - -`current_modulation` and `current_params` always reflect what is actually programmed into the radio at the moment the response is constructed, regardless of whether this call caused a change. This gives the host a consistent ground truth in every case. - -**Result values**: -- **APPLIED (0)**: The request was accepted. The radio is now configured as requested. The calling client owns the lock (single-client: always `MINE`). -- **ALREADY_MATCHED (1)**: Another client holds the lock, but the active configuration byte-for-byte matches the requested one. No change was made. The calling client may proceed to use the radio cooperatively but does not own the lock. -- **LOCKED_MISMATCH (2)**: Another client holds the lock and the active configuration differs. No change was made. The calling client may inspect `current_params` to diff. - -**Owner values**: -- **NONE (0)**: No client currently holds the lock. -- **MINE (1)**: The calling client holds the lock. -- **OTHER (2)**: A different client holds the lock. - -**Valid in**: any state. - -**Errors** (all synchronous, with tag echoed): -- `ERR(EMODULATION)`: `modulation_id` is not supported on this chip. Check the capability bitmap. -- `ERR(ELENGTH)`: payload length does not match the expected size for `modulation_id`. -- `ERR(EPARAM)`: a parameter value is out of range (e.g., frequency outside chip's supported range, SF out of range, reserved field nonzero). - -**Atomicity**: All parameters are validated before any SPI write is issued to the radio. On validation failure the radio is untouched; the device returns `ERR` and remains in its prior state (UNCONFIGURED or CONFIGURED with prior params). Validation cannot leave the radio in an intermediate state. - -**Side effects on `APPLIED`**: -- Any in-progress TX is allowed to finish; its `TX_DONE` is emitted normally. -- All queued (not yet started) TXs are cancelled with `TX_DONE(result=CANCELLED)`. -- Any continuous RX is stopped. -- RX ring is cleared. -- Radio is reconfigured, calibrated if required, and left in idle. The `OK` response is not emitted until this sequence completes. - -**Notes**: -- **Auto-LDRO**: For LoRa, the device automatically enables Low Data Rate Optimization when `(2^SF) / BW_Hz > 16 ms` (the Semtech-recommended threshold). The host does not specify LDRO and is not informed of its state. -- Reconfiguring to identical parameters is a valid call. In single-client mode, the response is always `result = APPLIED, owner = MINE`. No short-circuit is performed; the radio is genuinely re-initialized. -- In multi-client mode, see Section 13.2 for lock behavior. - ---- - -### 6.4 TX (0x04, H→D) - -**Purpose**: Transmit a single packet. - -**Payload**: - -``` -Offset Size Field Description - 0 1 flags u8, see below - 1 N bytes Packet bytes, 1 ≤ N ≤ max_payload_bytes -``` - -**Flags** (bit 0 = LSB): -- **Bit 0 — `skip_cad`**: If 1, skip the default CAD-before-TX and transmit immediately. If 0, the device performs a CAD cycle first and transmits only if the channel is clear (emitting `TX_DONE(CHANNEL_BUSY)` otherwise). Default behavior (bit 0 = 0) is the recommended path. -- **Bits 1–7**: Reserved. MUST be 0. Device rejects frames with any reserved bit set via `ERR(EPARAM)`. - -**Response**: `OK` with empty payload, tag echoed. This response indicates the command was accepted and enqueued. It does NOT indicate the packet was transmitted. The transmission outcome is delivered later as a `TX_DONE` event with the same tag. - -**Asynchronous completion**: exactly one `TX_DONE` event with matching tag (Section 6.10), unless the command failed synchronously (received `ERR`, no `TX_DONE` follows) or the client disconnected before transmission (no `TX_DONE` emitted, since the client is gone). - -**Valid in**: CONFIGURED only. - -**Errors**: -- `ERR(ENOTCONFIGURED)`: device is UNCONFIGURED. -- `ERR(ELENGTH)`: N = 0 (zero-byte packets are not permitted), N > `max_payload_bytes`, or N exceeds the current modulation's per-mode maximum (which may be less than `max_payload_bytes`). -- `ERR(EPARAM)`: a reserved flag bit is set. -- `ERR(EBUSY)`: internal TX queue is full (already holds `tx_queue_capacity` pending TXs). Host should wait for an outstanding `TX_DONE` before retrying with a new tag. - -**Max payload note**: `max_payload_bytes` from `GET_INFO` is the chip-wide maximum. Individual modulations may have stricter limits (e.g., LR-FHSS payloads are ≤ ~100 bytes depending on configuration). A packet larger than the current modulation supports is rejected with `ERR(ELENGTH)`, even if it fits within `max_payload_bytes`. - -**CAD-default behavior on non-CAD modulations**: On modulations that do not support CAD (FSK, GFSK, FLRC, etc.), the `skip_cad` flag is silently ignored and the TX fires immediately. Host code does not change between LoRa and non-LoRa modes. - -**RX interruption by TX**: On half-duplex chips (all Semtech transceivers except when capability bit 22 is set), the radio briefly leaves RX to transmit and returns to RX afterward. A packet that was mid-reception during the transmission window is lost. On full-duplex chips, TX does not interrupt RX. - -**CAD timing**: For LoRa, the CAD duration depends on SF and BW. At SF7/BW125 it is ~4 ms; at SF12/BW125 it is ~130 ms. Hosts that need tight TX latency should either use a lower SF or set `skip_cad = 1`. - ---- - -### 6.5 RX_START (0x05, H→D) - -**Purpose**: Place the radio in continuous receive mode. Every received packet (including ones with bad CRC) produces an `RX` event. - -**Payload**: Empty. - -**Response**: `OK` with empty payload, tag echoed. - -**Valid in**: CONFIGURED only. - -**Errors**: -- `ERR(ENOTCONFIGURED)`: device is UNCONFIGURED. -- `ERR(EMODULATION)`: the currently-configured modulation does not support reception (e.g., LR-FHSS is transmit-only on all current Semtech silicon). - -**Notes**: -- Calling `RX_START` when already in continuous RX is a no-op. Response is `OK`. -- A `TX` interrupts RX, transmits, and the radio automatically returns to continuous RX afterward without a new `RX_START`. (On full-duplex chips, the interruption does not occur.) -- `SET_CONFIG` aborts continuous RX. Host must re-issue `RX_START` after reconfiguration if it still wants to receive. - ---- - -### 6.6 RX_STOP (0x06, H→D) - -**Purpose**: End continuous reception; radio returns to idle. - -**Payload**: Empty. - -**Response**: `OK` with empty payload, tag echoed. - -**Valid in**: CONFIGURED only. - -**Errors**: -- `ERR(ENOTCONFIGURED)`: device is UNCONFIGURED. - -**Notes**: -- Calling `RX_STOP` when not in continuous RX is a no-op. Response is `OK`. -- RX events already in the ring at the time of `RX_STOP` remain queued; the host continues to receive them as normal `RX` frames until the ring drains. - ---- - -### 6.7 OK (0x80, D→H) - -**Purpose**: Positive response to a host command. - -**Tag**: Echoes the originating command's tag. Never `0x0000`. - -**Payload**: Structure depends on originating command: -- `PING`, `TX`, `RX_START`, `RX_STOP`: empty. -- `GET_INFO`: info struct (Section 6.2). -- `SET_CONFIG`: result / owner / current-config struct (Section 6.3). - -**Notes**: To correctly interpret an `OK` payload, the host must know which command issued the echoed tag. Outstanding-tag bookkeeping is mandatory host-side. - ---- - -### 6.8 ERR (0x81, D→H) - -**Purpose**: Negative response to a command, OR asynchronous radio / protocol fault. - -**Tag**: -- Non-zero → synchronous error, tag echoes the originating command. The command was not carried out and no compensating event will follow. -- Zero → asynchronous error, not tied to any specific command. Fanned out to all connected clients in multi-client mode. - -**Payload**: - -``` -Offset Size Field Description - 0 2 code u16, error code (Section 7) -``` - ---- - -### 6.9 RX (0xC0, D→H) - -**Purpose**: Unsolicited event indicating a packet was received over the air (or, in multi-client mode, that another client transmitted and this is the loopback). - -**Tag**: Always `0x0000`. - -**Payload**: - -``` -Offset Size Field Description - 0 2 rssi i16, tenths of dBm - 2 2 snr i16, tenths of dB - 4 4 freq_err i32, hertz, estimated LO offset - 8 8 timestamp_us u64, µs since boot, captured at RxDone IRQ (packet end) -16 1 crc_valid u8: 0=CRC failed, 1=CRC passed or CRC not configured -17 2 packets_dropped u16, packets dropped since previous delivered RX -19 1 origin u8: 0=over-the-air, 1=device-local loopback -20 N bytes Packet payload, N determined by frame length -``` - -**Field semantics**: -- `timestamp_us` is captured the instant the radio asserts RxDone (the end-of-packet IRQ). -- `crc_valid = 0` packets are delivered to the host anyway, because they may still be of interest (debugging, partial capture). The bytes may be corrupt. -- When `crc_valid = 0`, `snr` and `freq_err` may be meaningless. -- `packets_dropped` is cleared to 0 on every delivered `RX` event. It represents losses since the last successful delivery. -- `origin = 1` is only possible in multi-client mode (Section 13). In single-client it is always 0. -- `bytes` has length `N ≥ 0` and always satisfies `N ≤ max_payload_bytes`. If the radio reports a length field larger than `max_payload_bytes` (possible on a corrupted PHY header), the device MUST truncate to `max_payload_bytes` and set `crc_valid = 0`. Hosts can rely on `N ≤ max_payload_bytes` unconditionally. - ---- - -### 6.10 TX_DONE (0xC1, D→H) - -**Purpose**: Asynchronous completion of a previously-issued `TX`. - -**Tag**: Echoes the tag of the originating `TX`. - -**Payload**: - -``` -Offset Size Field Description - 0 1 result u8: 0=TRANSMITTED, 1=CHANNEL_BUSY, 2=CANCELLED - 1 4 airtime_us u32, on-air duration in microseconds (0 for non-TRANSMITTED) -``` - -**Result values**: -- **TRANSMITTED (0)**: Packet went on the air. `airtime_us` is the measured or computed time-on-air. -- **CHANNEL_BUSY (1)**: CAD detected activity; TX was aborted before transmission. `airtime_us = 0` (CAD time itself is not counted). This result is only possible when `skip_cad = 0` was set in the originating TX **and** the current modulation supports CAD (LoRa only). Any TX with `skip_cad = 1`, or any TX on a non-CAD modulation, will never produce CHANNEL_BUSY. -- **CANCELLED (2)**: TX was discarded because `SET_CONFIG` or disconnect occurred before the packet reached the air. `airtime_us = 0`. - -**Notes**: -- Exactly one `TX_DONE` is emitted per `TX` that received an `OK`. A `TX` that received `ERR` produces no `TX_DONE`. -- On client disconnect, `TX_DONE` for cancelled TXs is not emitted — the client is gone. -- On `CHANNEL_BUSY`, hosts should apply randomized backoff and retry with a **new tag**. The prior tag's state is closed and the same tag cannot be reused while any earlier-issued outstanding tag is still open. - ---- - -## 7. Error Codes - -All error codes are `u16` little-endian. The tag on the enclosing `ERR` frame distinguishes context: non-zero tag = synchronous error tied to a specific command; tag 0x0000 = asynchronous error not tied to a command. The two tables below group codes by the context in which they are *typically* emitted, but the device MAY emit any code in either context when it fits semantically (e.g., `ERR(ERADIO)` is listed as asynchronous but is emitted synchronously when a hardware fault occurs during processing of a specific command, such as SPI failure during `SET_CONFIG`). - -### 7.1 Synchronous (tag ≠ 0), typical - -| Code | Name | Meaning | -|--------|-----------------|---------------------------------------------------------------| -| 0x0001 | EPARAM | A parameter value is out of range or invalid. | -| 0x0002 | ELENGTH | Payload length is wrong for the command or modulation. | -| 0x0003 | ENOTCONFIGURED | Command requires CONFIGURED; device is UNCONFIGURED. | -| 0x0004 | EMODULATION | Requested modulation is not supported by this chip. | -| 0x0005 | EUNKNOWN_CMD | Unknown command type byte. | -| 0x0006 | EBUSY | Transient: TX queue full. Host should wait and retry. | - -Codes 0x0000–0x00FF are reserved for synchronous errors. - -### 7.2 Asynchronous (tag = 0), typical - -| Code | Name | Meaning | -|--------|-----------|----------------------------------------------------------------------| -| 0x0101 | ERADIO | Radio SPI error or unexpected hardware state. | -| 0x0102 | EFRAME | Inbound frame had bad CRC, bad COBS, or wrong length. | -| 0x0103 | EINTERNAL | Firmware encountered an unexpected internal condition. | - -Codes 0x0100–0x01FF are reserved for asynchronous errors. Codes 0x0200 and above are reserved for future extensions. - -**Impact of asynchronous errors on pending commands**: An `EFRAME` on a frame that was *supposed* to be a response leaves the host's corresponding outstanding tag open indefinitely from the device's perspective. Hosts MUST implement their own command timeouts (Section 14.2) to avoid tag exhaustion or indefinite waiting. - ---- - -## 8. Radio Chip ID Enum - -| Value | Chip | Family | -|---------|--------|---------------------| -| 0x0000 | Unknown| — | -| 0x0001 | SX1261 | Gen 2 sub-GHz | -| 0x0002 | SX1262 | Gen 2 sub-GHz | -| 0x0003 | SX1268 | Gen 2 sub-GHz | -| 0x0004 | LLCC68 | Gen 2 sub-GHz | -| 0x0010 | SX1272 | Gen 1 sub-GHz | -| 0x0011 | SX1276 | Gen 1 sub-GHz | -| 0x0012 | SX1277 | Gen 1 sub-GHz | -| 0x0013 | SX1278 | Gen 1 sub-GHz | -| 0x0014 | SX1279 | Gen 1 sub-GHz | -| 0x0020 | SX1280 | Gen 2 2.4 GHz | -| 0x0021 | SX1281 | Gen 2 2.4 GHz | -| 0x0030 | LR1110 | Gen 3 multi-band | -| 0x0031 | LR1120 | Gen 3 multi-band | -| 0x0032 | LR1121 | Gen 3 multi-band | -| 0x0040 | LR2021 | Gen 4 multi-band | - -Values not listed here are reserved. Future chips will be assigned in the next available slot of their family's range. - ---- - -## 9. Capability Bitmap - -The `capability_bitmap` field in `GET_INFO` is a `u64`. Bits are numbered from LSB. Undefined bits MUST be 0 in v1.0. Hosts MUST ignore undefined bits. - -### 9.1 Modulations (bits 0–15) - -| Bit | Modulation | -|-----|------------------| -| 0 | LoRa | -| 1 | FSK | -| 2 | GFSK | -| 3 | LR-FHSS | -| 4 | FLRC | -| 5 | MSK | -| 6 | GMSK | -| 7 | BLE-compatible | -| 8–15| Reserved | - -A bit set here means the device's `SET_CONFIG` accepts the corresponding `modulation_id`. - -Multiple modulation bits being set does NOT imply the device can operate in multiple modulations simultaneously. The radio is configured for exactly one modulation at a time (whichever was most recently applied via `SET_CONFIG`). To switch modulations, issue another `SET_CONFIG`. - -### 9.2 Radio Features (bits 16–31) - -| Bit | Feature | -|-----|----------------------------------------------------------------------| -| 16 | CAD-before-TX available (firmware performs CAD in default TX path) | -| 17 | IQ inversion supported in LoRa mode | -| 18 | Ranging / time-of-flight distance measurement | -| 19 | GNSS scanning | -| 20 | WiFi MAC scanning | -| 21 | Spectral scan | -| 22 | Full-duplex (simultaneous TX + RX) | -| 23–31| Reserved | - -If bit 16 is 0, the device does not perform CAD before TX even for LoRa mode; the `skip_cad` flag has no effect. - -### 9.3 Protocol Features (bits 32–47) - -| Bit | Feature | -|-----|----------------------| -| 32 | Multi-client support | -| 33–47| Reserved | - -### 9.4 Reserved (bits 48–63) - -All reserved for future use. - ---- - -## 10. Modulation Parameters - -The `modulation_id` byte in `SET_CONFIG` selects one of the following parameter structs. Any value not listed here is reserved and yields `ERR(EMODULATION)` (if the chip conceptually supports the modulation but the ID is not yet defined) or `ERR(EMODULATION)` (if the chip does not support it at all). - -| modulation_id | Modulation | Fixed struct size | -|---------------|------------|------------------------| -| 0x01 | LoRa | 15 bytes | -| 0x02 | FSK / GFSK | 16 + `sync_word_len` | -| 0x03 | LR-FHSS | 10 bytes | -| 0x04 | FLRC | 13 bytes | - -### 10.1 LoRa (modulation_id = 0x01, 15 bytes) - -``` -Offset Size Field Range / notes - 0 4 freq_hz u32, chip-supported range (EPARAM if outside) - 4 1 sf u8, 5–12 (chip-dependent: SX127x is 6–12) - 5 1 bw u8, bandwidth enum (see below) - 6 1 cr u8, coding-rate enum (see below) - 7 2 preamble_len u16, in symbols (typical min 6) - 9 2 sync_word u16 (SX127x: uses low byte only; high byte MUST be 0) -11 1 tx_power_dbm i8, chip-dependent range -12 1 header_mode u8, 0=explicit, 1=implicit -13 1 payload_crc u8, 0=disabled, 1=enabled -14 1 iq_invert u8, 0=normal, 1=inverted -``` - -**Bandwidth enum** (the small integer on the wire, not the kHz value): - -| Value | Bandwidth | Applicable chip families | -|-------|------------|---------------------------------| -| 0 | 7.81 kHz | SX126x, SX127x, LR11xx, LR2021 | -| 1 | 10.42 kHz | SX126x, SX127x, LR11xx, LR2021 | -| 2 | 15.63 kHz | SX126x, SX127x, LR11xx, LR2021 | -| 3 | 20.83 kHz | SX126x, SX127x, LR11xx, LR2021 | -| 4 | 31.25 kHz | SX126x, SX127x, LR11xx, LR2021 | -| 5 | 41.67 kHz | SX126x, SX127x, LR11xx, LR2021 | -| 6 | 62.5 kHz | SX126x, SX127x, LR11xx, LR2021 | -| 7 | 125 kHz | all | -| 8 | 250 kHz | all | -| 9 | 500 kHz | all | -| 10 | 200 kHz | SX128x | -| 11 | 400 kHz | SX128x | -| 12 | 800 kHz | SX128x | -| 13 | 1600 kHz | SX128x | - -A value not supported by the current radio yields `ERR(EPARAM)`. Hosts SHOULD consult `radio_chip_id` and select from the applicable set. - -**Coding-rate enum**: - -| Value | Coding rate | -|-------|-------------| -| 0 | 4/5 | -| 1 | 4/6 | -| 2 | 4/7 | -| 3 | 4/8 | - -Any value outside these enumerations produces `ERR(EPARAM)`. - -### 10.2 FSK / GFSK (modulation_id = 0x02, variable size) - -``` -Offset Size Field Description - 0 4 freq_hz u32, chip-supported range - 4 4 bitrate_bps u32, bits per second - 8 4 freq_dev_hz u32, FSK frequency deviation -12 1 rx_bw u8, chip-specific RX bandwidth enum -13 2 preamble_len u16, in bits -15 1 sync_word_len u8, 0–8 -16 N sync_word N = sync_word_len bytes -``` - -Total struct size: `16 + sync_word_len` bytes. - -**Sync word byte order**: The byte at offset 16 of the payload is transmitted first on the air; subsequent bytes follow in order. Within each byte, the MSB is transmitted first (standard FSK/GFSK convention). - -GFSK is selected by the firmware when the chip supports Gaussian filtering at the requested bitrate; the wire protocol does not distinguish it from FSK beyond the capability bitmap. - -### 10.3 LR-FHSS (modulation_id = 0x03, 10 bytes) - -``` -Offset Size Field Description - 0 4 freq_hz u32, center frequency - 4 1 bw_enum u8, LR-FHSS occupied-bandwidth enum (see below) - 5 1 cr_enum u8, LR-FHSS coding-rate enum (see below) - 6 1 grid u8, 0=25.39 kHz grid, 1=3.9 kHz grid - 7 1 hopping u8, 0=disabled, 1=enabled - 8 1 tx_power_dbm i8 - 9 1 reserved u8, MUST be 0 (EPARAM otherwise) -``` - -**LR-FHSS bandwidth enum**: - -| Value | Occupied BW | -|-------|-------------| -| 0 | 39.06 kHz | -| 1 | 85.94 kHz | -| 2 | 136.72 kHz | -| 3 | 183.59 kHz | -| 4 | 335.94 kHz | -| 5 | 386.72 kHz | -| 6 | 722.66 kHz | -| 7 | 1523.44 kHz | - -**LR-FHSS coding-rate enum**: - -| Value | Coding rate | -|-------|-------------| -| 0 | 5/6 | -| 1 | 2/3 | -| 2 | 1/2 | -| 3 | 1/3 | - -### 10.4 FLRC (modulation_id = 0x04, 13 bytes) - -``` -Offset Size Field Description - 0 4 freq_hz u32, carrier frequency - 4 1 bitrate_enum u8, bitrate enum (see below) - 5 1 cr_enum u8, FLRC coding-rate (0=1/2, 1=3/4, 2=1/1) - 6 1 bt_enum u8, Gaussian BT product (0=off, 1=0.5, 2=1.0) - 7 1 preamble_len u8, preamble length enum (see below) - 8 4 sync_word u32, 32-bit sync word (transmitted MSB-first on air) -12 1 tx_power_dbm i8 -``` - -**FLRC bitrate enum**: - -| Value | Bitrate | -|-------|-------------| -| 0 | 2600 kbps | -| 1 | 2080 kbps | -| 2 | 1300 kbps | -| 3 | 1040 kbps | -| 4 | 650 kbps | -| 5 | 520 kbps | -| 6 | 325 kbps | -| 7 | 260 kbps | - -**FLRC preamble length enum**: - -| Value | Preamble bits | -|-------|---------------| -| 0 | 8 | -| 1 | 12 | -| 2 | 16 | -| 3 | 20 | -| 4 | 24 | -| 5 | 28 | -| 6 | 32 | - -Any enum value outside these tables yields `ERR(EPARAM)`. - -FLRC specifics vary between SX128x and LR2021; the firmware maps chip-specific register values to these byte values. Values that don't correspond to a supported setting on the current chip produce `ERR(EPARAM)`. - ---- - -## 11. Ordering and Interleaving - -These invariants define what hosts and devices may rely on. - -**Command responses are monotonic per session**: -- If the host sends command A (tag = T_A) before command B (tag = T_B), then the device's response to A (`OK` or `ERR` with tag T_A) is emitted before the response to B (`OK` or `ERR` with tag T_B). -- This does NOT apply across clients in multi-client mode — each client sees its own command responses in order, but responses across clients may interleave arbitrarily. - -**TX_DONE order matches TX order**: -- If TX with tag T_1 was accepted before TX with tag T_2 (same client), then `TX_DONE(T_1)` is emitted before `TX_DONE(T_2)`. -- This follows from the serial nature of the TX queue. - -**RX and async events are unordered relative to responses**: -- `RX` events and asynchronous `ERR` events (tag = 0) may appear at any point in the device→host stream, interleaved with command responses. -- A host reading the stream cannot assume that a response to a pending command will arrive before an `RX` event, even if the command was issued first. - -**Exactly-once semantics**: -- Every command that received `OK` or `ERR` is concluded (plus the additional `TX_DONE` for `TX` that got `OK`). -- No duplicated responses, no lost responses (absent transport-level failure). - ---- - -## 12. Flow Control and Backpressure - -### 12.1 Transport-Level Flow Control - -DongLoRa Protocol relies on the underlying byte-stream transport to provide flow control: -- **USB CDC-ACM / CDC-UART bridges**: bulk endpoints provide implicit flow control via NAKs when the receiver is not ready. -- **Plain UART**: no flow control (host SHOULD use a reasonable bitrate like 921600 bps and respect device processing time). -- **BLE**: L2CAP / GATT credit-based flow control. -- **TCP**: window-based flow control. - -If the device's internal receive buffer is full, it MAY stall the transport (stop ACKing / NAKing bytes) until buffer space is freed. - -### 12.2 Device-Side Queue Limits - -- **TX queue**: depth = `tx_queue_capacity` (from `GET_INFO`). Additional TX attempts return `ERR(EBUSY)`. Host should wait for an outstanding `TX_DONE` before retrying. -- **RX ring**: depth = `rx_queue_capacity`. On overflow, oldest-first drop policy applies; the drop count is delivered in the next `RX` event's `packets_dropped` field. - -### 12.3 Host-Side Expectations - -Hosts SHOULD: -- Size their RX decoder buffer to at least `max_payload_bytes + 32` bytes. -- Not pipeline more commands than `tx_queue_capacity` (for TX) without waiting for responses. -- Drain the device→host stream continuously. A host that stops reading will eventually stall the device. - ---- - -## 13. Multi-Client Operation (Future) - -This section describes behavior on devices advertising capability bit 32 (multi-client). Single-client USB deployments may skip this section but note that multi-client-aware fields (`result`, `owner`, `origin`) appear on both and are defined consistently. - -### 13.1 Per-Client Liveness - -Each client has an independent 1000 ms inactivity timer. A client whose timer expires is disconnected; other clients are unaffected. - -### 13.2 Configuration Lock - -- The first `SET_CONFIG` from any client, while no lock is held, acquires the lock. -- The locking client may issue further `SET_CONFIG` commands freely; they always apply and return `result=APPLIED, owner=MINE`. -- A non-locking client's `SET_CONFIG`: - - If requested params match active params byte-for-byte: `OK(result=ALREADY_MATCHED, owner=OTHER)`. No change. - - Otherwise: `OK(result=LOCKED_MISMATCH, owner=OTHER)`. No change. -- The lock is released when the locking client disconnects (transport-level OR liveness timeout). If other clients are still connected, the device remains in CONFIGURED with the current params until the next successful `SET_CONFIG` from any client (which becomes the new locker). If no clients remain, the device transitions to UNCONFIGURED. - -### 13.3 RX Distribution - -`RX` events and asynchronous `ERR` events (tag = 0) are fanned out to every connected client. - -### 13.4 TX Distribution and Cross-Client Loopback - -- Each client's TXs go into a single global FIFO. -- Each client's `TX_DONE` is delivered only to the originating client. -- Each successful on-air TX (`TX_DONE(result=TRANSMITTED)`) is additionally delivered to all **other** connected clients as an `RX` event with: - - `rssi`, `snr`, `freq_err` = 0 - - `timestamp_us` = captured at TxDone IRQ - - `crc_valid` = 1 - - `packets_dropped` = 0 - - `origin` = 1 - - `bytes` = the transmitted packet - -TXs that completed with `CHANNEL_BUSY` or `CANCELLED` did not go on the air and do NOT generate loopback events. - -### 13.5 Single-Client Compatibility - -In single-client mode: -- `owner` is always `NONE` (before `SET_CONFIG`) or `MINE` (after). -- `result` is always `APPLIED` for successful `SET_CONFIG`; `ALREADY_MATCHED` and `LOCKED_MISMATCH` never occur. -- `origin` on `RX` is always 0. - -### 13.6 External Multiplexer Pattern - -DongLoRa Protocol intentionally does not carry a client identifier on the wire. When multi-client behavior is needed without firmware-side support, an external software multiplexer can provide it: - -- The multiplexer holds one single-client DongLoRa Protocol connection to the dongle (the dongle's sole client). -- Upstream, the multiplexer accepts multiple connections from downstream applications. -- For each frame flowing upward from the dongle, the multiplexer dispatches to the owning upstream connection based on its own internal `device_tag → (upstream_connection, upstream_tag)` mapping; for each frame flowing downward, the multiplexer rewrites the upstream-chosen tag into a fresh dongle-side tag and records the mapping. -- If the multiplexer wants to expose multi-client semantics to its upstream clients (the `result` / `owner` / `origin` fields defined in this section), it implements the locking, echo, and fan-out logic itself — the dongle is unaware. - -This is identical to the work firmware would do in Section 13.1–13.4, just implemented one layer up. When firmware later gains native multi-client support (capability bit 32), upstream applications can migrate from talking to the multiplexer to talking to the firmware directly without changing their DongLoRa Protocol implementation: the wire format is unchanged, and the transport connection itself serves as the client identity. See Section 13.7. - -### 13.7 Client Identity is the Transport Connection - -DongLoRa Protocol does not encode a client identifier in any frame. Instead, each transport-level connection (one USB CDC-ACM session, one BLE GATT link, one TCP socket) IS the client identity from the firmware's perspective. Firmware with native multi-client support (capability bit 32) accepts multiple simultaneous transport connections and maintains per-connection state internally: which client owns the config lock, which client enqueued each pending TX, which connection to deliver each RX event on. - -Consequences: -- No HELO or handshake is needed. A client opens its transport and immediately begins sending DongLoRa Protocol frames. -- The wire format is identical between single-client and multi-client firmware. A host written against single-client DongLoRa Protocol works unchanged against multi-client firmware; fields whose values don't vary for single-client (`owner`, `result = ALREADY_MATCHED/LOCKED_MISMATCH`, `origin = 1`) simply don't occur for that host. -- Firmware implementing multi-client pays the complexity of per-connection bookkeeping; single-client firmware pays nothing. Neither is on the wire. - ---- - -## 14. Host Behavior Requirements - -### 14.1 MUST - -- Send at least one frame every 1000 ms while connected, to prevent the device's inactivity timer from expiring. `PING` exists for this purpose. -- Never use tag `0x0000` on outbound commands. -- Track outstanding tags; do not reuse a tag until its final response has been received (`OK`, `ERR`, or `TX_DONE` as applicable). -- Validate the CRC on every received frame; discard frames with mismatched CRC. -- Treat COBS decoding failures as framing errors; resynchronize on the next `0x00`. -- Set all reserved flag bits and reserved payload bytes to 0. - -### 14.2 SHOULD - -- Send keepalives at 500 ms intervals or better (2× margin on the 1000 ms device timeout). -- Use a monotonic tag counter starting at 1, wrapping at 0xFFFF and skipping 0x0000. -- Call `GET_INFO` once at connection to learn `max_payload_bytes`, `capability_bitmap`, queue capacities, frequency range, TX power range, and identity. -- Validate `freq_hz` and `tx_power_dbm` locally against `GET_INFO`-reported ranges before issuing `SET_CONFIG`. -- Apply a per-command timeout to detect hung or missing responses. A reasonable default: 2000 ms for commands that do not involve airtime; for `TX`, 2000 ms + the computed packet airtime + maximum expected CAD duration. -- On `TX_DONE(CHANNEL_BUSY)`, apply randomized exponential backoff before retrying with a new tag. -- On `RX(packets_dropped > 0)`, log or surface the loss. -- On receiving an unexpected `ERR(ENOTCONFIGURED)` in response to a command that was issued while the host believed the device to be CONFIGURED, treat this as a lost-session signal: the device has timed out or rebooted. Re-issue `SET_CONFIG`, then `RX_START` if continuous receive was previously active. Any TXs that were in-flight at the time are presumed not to have been transmitted. - -### 14.3 MUST NOT - -- Send commands with any reserved flag bit or reserved payload byte set. -- Assume any device-side state persists across disconnects. -- Assume response ordering across tags beyond what Section 11 guarantees. - ---- - -## 15. Device Behavior Requirements - -### 15.1 MUST - -- Boot into UNCONFIGURED state. -- Reject unknown command types with `ERR(EUNKNOWN_CMD)` echoing the tag. -- Validate every `SET_CONFIG` parameter before writing to the radio. On validation failure the chip is not touched and an `ERR` is returned. -- Auto-enable LDRO on LoRa when `(2^SF) / BW_Hz > 16 ms`. -- Echo the command's tag on every `OK`, `ERR`, and `TX_DONE` response. -- Use tag `0x0000` for `RX` events and asynchronous `ERR` events. -- Reset the inactivity timer on every received frame, including frames that fail CRC or COBS decoding (the bytes arrived — the host is present). -- Emit exactly one of `OK` / `ERR` per command, then exactly one `TX_DONE` additionally for `TX` commands that received `OK`. -- Block the `OK` response to `SET_CONFIG` until the radio is fully configured and ready for subsequent commands (including any required calibration). - -### 15.2 SHOULD - -- Emit asynchronous `ERR(EFRAME)` when an inbound frame fails CRC or COBS decoding, to aid host-side diagnosis. -- Preserve the RX ring across TX operations (TX briefly interrupts RX, but queued RX events are not cleared). -- Use a display (if present) to reflect state: splash when UNCONFIGURED, connected status when CONFIGURED. Display behavior is not part of the protocol. - -### 15.3 MUST NOT - -- Persist any configuration, session state, tag state, or queue contents across reboots or disconnects. -- Initiate communication with the host (never send frames when the host has sent none). -- Enforce regulatory limits (TX power caps, duty cycle, band restrictions). The device is a transparent passthrough; regulatory compliance is the host's responsibility. -- Silently drop or modify packet payloads on TX or RX. - ---- - -## 16. Version Compatibility - -### 16.1 Protocol Version - -`GET_INFO.proto_major` and `GET_INFO.proto_minor` identify the protocol version the device implements. This document specifies protocol version 1.0 (`proto_major = 1`, `proto_minor = 0`). - -### 16.2 Major Version Changes - -Different major versions are wire-incompatible. A host that does not recognize the device's reported `proto_major` MUST NOT attempt to use the device. - -### 16.3 Minor Version Changes - -Minor version increments are backward-compatible: they may add new message types, new capability bits, new modulations, new error codes, or new trailing fields on existing payloads. A host SHOULD continue to work with a device reporting a higher minor version, but will not have access to features added after its implementation. - -### 16.4 Rules for Minor Version Extensions - -Future minor versions MAY: -- Define new values in reserved ranges (message types, error codes, modulation IDs, chip IDs, capability bits). -- Append new fields to the end of existing payloads. Implementations use the frame length to determine the payload boundary; fields beyond what they understand are ignored. - -Future minor versions MUST NOT: -- Change the meaning, encoding, or position of any existing field. -- Remove or renumber existing message types, error codes, chip IDs, modulation IDs, or capability bits. -- Change the semantics of existing commands or events. - ---- - -## Appendix A — COBS Reference Implementation - -```c -#include -#include - -// Encode src[0..src_len-1] to dst. Returns bytes written to dst. -// dst must have room for src_len + ceil(src_len/254) + 1 bytes. -size_t cobs_encode(const uint8_t *src, size_t src_len, uint8_t *dst) { - size_t r = 0, w = 1; - size_t code_idx = 0; - uint8_t code = 1; - - while (r < src_len) { - if (src[r] == 0) { - dst[code_idx] = code; - code_idx = w++; - code = 1; - } else { - dst[w++] = src[r]; - code++; - if (code == 0xFF) { - dst[code_idx] = code; - code_idx = w++; - code = 1; - } - } - r++; - } - dst[code_idx] = code; - return w; -} - -// Decode src[0..src_len-1] to dst. Returns bytes written to dst, -// or 0 on decode error (corrupt input). -size_t cobs_decode(const uint8_t *src, size_t src_len, uint8_t *dst) { - size_t r = 0, w = 0; - while (r < src_len) { - uint8_t code = src[r++]; - if (code == 0 || r + code - 1 > src_len) return 0; - for (uint8_t i = 1; i < code; i++) dst[w++] = src[r++]; - if (code != 0xFF && r < src_len) dst[w++] = 0; - } - return w; -} -``` - ---- - -## Appendix B — CRC-16/CCITT-FALSE Reference Implementation - -```c -#include -#include - -uint16_t crc16_ccitt_false(const uint8_t *data, size_t len) { - uint16_t crc = 0xFFFF; - for (size_t i = 0; i < len; i++) { - crc ^= ((uint16_t)data[i]) << 8; - for (int b = 0; b < 8; b++) { - if (crc & 0x8000) crc = (crc << 1) ^ 0x1021; - else crc <<= 1; - } - } - return crc; -} -``` - -Test vector: `crc16_ccitt_false("123456789", 9)` = `0x29B1`. - ---- - -## Appendix C — Wire Examples - -### C.1 Reading these examples - -Every example below gives: - -- **Direction and type**: `H→D` or `D→H`, followed by the message name and the tag in hex. -- **Decoded field values** (for messages with structured payloads): what each field means, not just raw bytes. -- **Pre-COBS body**: the `type | tag | payload` bytes, then a `| crc=0x....` marker, then the full pre-encoding frame `type | tag | payload | crc_lo crc_hi`. -- **Wire bytes**: the COBS-encoded frame followed by the trailing `0x00` delimiter. This is what actually moves over the transport. - -All multi-byte integers are little-endian. All examples were generated by running the reference implementations in Appendices A and B against the specified input, and validated by round-trip. - -Conventions used throughout: - -- Tag values are chosen per section to keep examples distinct across the appendix. Real implementations use a monotonic counter from 1, skipping 0. -- Error cases assume the device is in the state described by the section intro (CONFIGURED unless noted otherwise). -- `...` in decoded fields means "same as previously shown, unchanged". Byte dumps are always complete. - ---- - -### C.2 Basic sequences (happy path) - -#### C.2.1 PING exchange - -The simplest possible DongLoRa Protocol transaction. Host sends PING, device responds with OK. - -``` -H→D PING tag=0x0001 - pre-COBS: 01 01 00 | crc=0xc89d → 01 01 00 9D C8 - on wire: 03 01 01 03 9D C8 00 - -D→H OK tag=0x0001 - pre-COBS: 80 01 00 | crc=0xc4f7 → 80 01 00 F7 C4 - on wire: 03 80 01 03 F7 C4 00 -``` - -#### C.2.2 GET_INFO exchange - -Host queries device capabilities. Response describes an SX1262-based board with LoRa, FSK, and CAD; a 64-slot RX ring; a 16-deep TX queue; 150–960 MHz tuning; and −9 to +22 dBm TX power. - -``` -H→D GET_INFO tag=0x0002 - pre-COBS: 02 02 00 | crc=0xc49e → 02 02 00 9E C4 - on wire: 03 02 02 03 9E C4 00 - -D→H OK tag=0x0002 - proto_major: 1 - proto_minor: 0 - fw_major: 0 - fw_minor: 1 - fw_patch: 0 - radio_chip_id: 0x0002 (SX1262) - capability_bitmap: 0x0000000000010003 (bits 0=LoRa, 1=FSK, 16=CAD) - supported_sf_bitmap: 0x1FE0 (bits 5..12 = SF5..SF12) - supported_bw_bitmap: 0x03FF (all sub-GHz BW enum values 0..9) - max_payload_bytes: 255 - rx_queue_capacity: 64 - tx_queue_capacity: 16 - freq_min_hz: 150000000 (150 MHz) - freq_max_hz: 960000000 (960 MHz) - tx_power_min_dbm: -9 - tx_power_max_dbm: 22 - mcu_uid_len: 8 - mcu_uid: DE AD BE EF 01 23 45 67 - radio_uid_len: 0 - radio_uid: (empty) - pre-COBS: 80 02 00 01 00 00 01 00 02 00 03 00 01 00 00 00 00 00 E0 1F FF 03 FF 00 40 00 10 00 80 D1 F0 08 00 70 38 39 F7 16 08 DE AD BE EF 01 23 45 67 00 | crc=0xa4fa → 80 02 00 01 00 00 01 00 02 00 03 00 01 00 00 00 00 00 E0 1F FF 03 FF 00 40 00 10 00 80 D1 F0 08 00 70 38 39 F7 16 08 DE AD BE EF 01 23 45 67 00 FA A4 - on wire: 03 80 02 02 01 01 02 01 02 02 02 03 02 01 01 01 01 01 06 E0 1F FF 03 FF 02 40 02 10 05 80 D1 F0 08 0F 70 38 39 F7 16 08 DE AD BE EF 01 23 45 67 03 FA A4 00 -``` - -#### C.2.3 SET_CONFIG for LoRa (EU868, SF7, BW125, CR 4/5) - -Standard LoRa configuration on EU868.1 MHz. Device applies and echoes the current params. - -``` -H→D SET_CONFIG tag=0x0003 - modulation_id: 0x01 (LoRa) - freq_hz: 868100000 (868.1 MHz) - sf: 7 - bw: 7 (125 kHz) - cr: 0 (4/5) - preamble_len: 8 - sync_word: 0x1424 - tx_power_dbm: 14 - header_mode: 0 (explicit) - payload_crc: 1 (on) - iq_invert: 0 (normal) - pre-COBS: 03 03 00 01 A0 27 BE 33 07 07 00 08 00 24 14 0E 00 01 00 | crc=0x1fd9 → 03 03 00 01 A0 27 BE 33 07 07 00 08 00 24 14 0E 00 01 00 D9 1F - on wire: 03 03 03 08 01 A0 27 BE 33 07 07 02 08 04 24 14 0E 02 01 03 D9 1F 00 - -D→H OK tag=0x0003 - result: 0 (APPLIED) - owner: 1 (MINE) - current_modulation: 0x01 (LoRa) - current_params: [same 15 bytes echoed back] - pre-COBS: 80 03 00 00 01 01 A0 27 BE 33 07 07 00 08 00 24 14 0E 00 01 00 | crc=0x91c8 → 80 03 00 00 01 01 A0 27 BE 33 07 07 00 08 00 24 14 0E 00 01 00 C8 91 - on wire: 03 80 03 01 09 01 01 A0 27 BE 33 07 07 02 08 04 24 14 0E 02 01 03 C8 91 00 -``` - -#### C.2.4 TX with default CAD, packet "Hello" - -Assumes C.2.3 has run. Host transmits `48 65 6C 6C 6F` ("Hello"). Device ACKs synchronously, performs CAD (~4 ms at SF7/BW125), transmits, and reports completion asynchronously. Airtime 30.976 ms = preamble (12.544 ms) + 18 payload symbols × 1.024 ms/symbol. - -``` -H→D TX tag=0x0004 - flags: 0x00 (CAD enabled) - data: 48 65 6C 6C 6F ("Hello") - pre-COBS: 04 04 00 00 48 65 6C 6C 6F | crc=0x4026 → 04 04 00 00 48 65 6C 6C 6F 26 40 - on wire: 03 04 04 01 08 48 65 6C 6C 6F 26 40 00 - -D→H OK tag=0x0004 - pre-COBS: 80 04 00 | crc=0x3b02 → 80 04 00 02 3B - on wire: 03 80 04 03 02 3B 00 - -D→H TX_DONE tag=0x0004 - result: 0 (TRANSMITTED) - airtime_us: 30976 (30.976 ms) - pre-COBS: C1 04 00 00 00 79 00 00 | crc=0xfae3 → C1 04 00 00 00 79 00 00 E3 FA - on wire: 03 C1 04 01 01 02 79 01 03 E3 FA 00 -``` - -#### C.2.5 TX with skip_cad, packet "URGENT" - -Host sets `skip_cad` to bypass CAD — typical for time-critical ACKs in protocols with their own MAC. No CAD time; straight to TX. - -``` -H→D TX tag=0x0005 - flags: 0x01 (skip_cad set) - data: 55 52 47 45 4E 54 ("URGENT") - pre-COBS: 04 05 00 01 55 52 47 45 4E 54 | crc=0x1cdb → 04 05 00 01 55 52 47 45 4E 54 DB 1C - on wire: 03 04 05 0A 01 55 52 47 45 4E 54 DB 1C 00 - -D→H OK tag=0x0005 - pre-COBS: 80 05 00 | crc=0x0833 → 80 05 00 33 08 - on wire: 03 80 05 03 33 08 00 - -D→H TX_DONE tag=0x0005 - result: 0 (TRANSMITTED) - airtime_us: 33792 (no CAD; straight to TX) - pre-COBS: C1 05 00 00 00 84 00 00 | crc=0xe381 → C1 05 00 00 00 84 00 00 81 E3 - on wire: 03 C1 05 01 01 02 84 01 03 81 E3 00 -``` - -#### C.2.6 Continuous RX loop - -Host enters continuous RX. Device emits RX events as packets arrive. Host sends a periodic PING to keep the session alive, then eventually stops RX. - -``` -H→D RX_START tag=0x0006 - pre-COBS: 05 06 00 | crc=0x8dca → 05 06 00 CA 8D - on wire: 03 05 06 03 CA 8D 00 - -D→H OK tag=0x0006 - pre-COBS: 80 06 00 | crc=0x5d60 → 80 06 00 60 5D - on wire: 03 80 06 03 60 5D 00 - -D→H RX tag=0x0000 - rssi: -735 (-73.5 dBm) - snr: 95 (+9.5 dB) - freq_err: -125 (-125 Hz) - timestamp_us: 42000000 (42.000 s since boot) - crc_valid: 1 (pass) - packets_dropped: 0 - origin: 0 (over-the-air) - data: 01 02 03 04 - pre-COBS: C0 00 00 21 FD 5F 00 83 FF FF FF 80 DE 80 02 00 00 00 00 01 00 00 00 01 02 03 04 | crc=0x8eb9 → C0 00 00 21 FD 5F 00 83 FF FF FF 80 DE 80 02 00 00 00 00 01 00 00 00 01 02 03 04 B9 8E - on wire: 02 C0 01 04 21 FD 5F 09 83 FF FF FF 80 DE 80 02 01 01 01 02 01 01 01 07 01 02 03 04 B9 8E 00 - -H→D PING tag=0x0007 - pre-COBS: 01 07 00 | crc=0x623b → 01 07 00 3B 62 - on wire: 03 01 07 03 3B 62 00 - -D→H OK tag=0x0007 - pre-COBS: 80 07 00 | crc=0x6e51 → 80 07 00 51 6E - on wire: 03 80 07 03 51 6E 00 - -H→D RX_STOP tag=0x0008 - pre-COBS: 06 08 00 | crc=0xf795 → 06 08 00 95 F7 - on wire: 03 06 08 03 95 F7 00 - -D→H OK tag=0x0008 - pre-COBS: 80 08 00 | crc=0x7e6f → 80 08 00 6F 7E - on wire: 03 80 08 03 6F 7E 00 -``` - ---- - -### C.3 Pipelining and batching - -#### C.3.1 Three pipelined TXs - -Host issues three TXs without waiting for intermediate responses. Each gets `OK` synchronously as it is enqueued; `TX_DONE` events arrive later in submission order. - -``` -H→D TX tag=0x0064 (data: 00 41 = flags + "A") - pre-COBS: 04 64 00 00 41 | crc=0x53cc → 04 64 00 00 41 CC 53 - on wire: 03 04 64 01 04 41 CC 53 00 - -H→D TX tag=0x0065 (data: 00 42 = flags + "B") - pre-COBS: 04 65 00 00 42 | crc=0x151b → 04 65 00 00 42 1B 15 - on wire: 03 04 65 01 04 42 1B 15 00 - -H→D TX tag=0x0066 (data: 00 43 = flags + "C") - pre-COBS: 04 66 00 00 43 | crc=0x9ee6 → 04 66 00 00 43 E6 9E - on wire: 03 04 66 01 04 43 E6 9E 00 - -D→H OK tag=0x0064 - pre-COBS: 80 64 00 | crc=0x3028 → 80 64 00 28 30 - on wire: 03 80 64 03 28 30 00 - -D→H OK tag=0x0065 - pre-COBS: 80 65 00 | crc=0x0319 → 80 65 00 19 03 - on wire: 03 80 65 03 19 03 00 - -D→H OK tag=0x0066 - pre-COBS: 80 66 00 | crc=0x564a → 80 66 00 4A 56 - on wire: 03 80 66 03 4A 56 00 - -D→H TX_DONE tag=0x0064 (result=0, airtime=25000 us) - pre-COBS: C1 64 00 00 A8 61 00 00 | crc=0xcc8e → C1 64 00 00 A8 61 00 00 8E CC - on wire: 03 C1 64 01 03 A8 61 01 03 8E CC 00 - -D→H TX_DONE tag=0x0065 - pre-COBS: C1 65 00 00 A8 61 00 00 | crc=0x74ef → C1 65 00 00 A8 61 00 00 EF 74 - on wire: 03 C1 65 01 03 A8 61 01 03 EF 74 00 - -D→H TX_DONE tag=0x0066 - pre-COBS: C1 66 00 00 A8 61 00 00 | crc=0xac6d → C1 66 00 00 A8 61 00 00 6D AC - on wire: 03 C1 66 01 03 A8 61 01 03 6D AC 00 -``` - -#### C.3.2 Batched frames in a single transport write (narrative) - -A host writing three small frames in quick succession may see them concatenated into a single USB bulk transfer, one BLE notification, or one TCP packet. The byte stream is: - -``` -[frame1 wire bytes] [frame2 wire bytes] [frame3 wire bytes] -``` - -For the C.3.1 example, the first three `TX` frames batched look like: - -``` -03 04 64 01 04 41 CC 53 00 03 04 65 01 04 42 1B 15 00 03 04 66 01 04 43 E6 9E 00 -``` - -That is, 27 bytes of raw transport data containing three DongLoRa Protocol frames. The receiver's COBS delimiter scanner handles this naturally: it reads bytes into a frame buffer until `0x00`, decodes that frame, flushes the buffer, and continues. No additional framing or length-prefix is involved. - ---- - -### C.4 State transitions - -#### C.4.1 Reconfigure while RX is active - -Host is in continuous RX, receives a packet, then reconfigures (changing SF from 7 to 9). Device aborts RX, clears the ring, reconfigures, and returns to standby. Host must re-issue `RX_START` to resume. - -``` -H→D RX_START tag=0x000A - pre-COBS: 05 0A 00 | crc=0xc8a7 → 05 0A 00 A7 C8 - on wire: 03 05 0A 03 A7 C8 00 - -D→H OK tag=0x000A - pre-COBS: 80 0A 00 | crc=0x180d → 80 0A 00 0D 18 - on wire: 03 80 0A 03 0D 18 00 - -D→H RX tag=0x0000 - rssi: -820, snr: 80, freq_err: 0, timestamp_us: 50000000, - crc_valid: 1, packets_dropped: 0, origin: 0, data: AA BB - pre-COBS: C0 00 00 CC FC 50 00 00 00 00 00 80 F0 FA 02 00 00 00 00 01 00 00 00 AA BB | crc=0xc054 → C0 00 00 CC FC 50 00 00 00 00 00 80 F0 FA 02 00 00 00 00 01 00 00 00 AA BB 54 C0 - on wire: 02 C0 01 04 CC FC 50 01 01 01 01 05 80 F0 FA 02 01 01 01 02 01 01 01 05 AA BB 54 C0 00 - -H→D SET_CONFIG tag=0x000B (LoRa, SF=9, other params same as C.2.3) - pre-COBS: 03 0B 00 01 A0 27 BE 33 09 07 00 08 00 24 14 0E 00 01 00 | crc=0xbdcc → 03 0B 00 01 A0 27 BE 33 09 07 00 08 00 24 14 0E 00 01 00 CC BD - on wire: 03 03 0B 08 01 A0 27 BE 33 09 07 02 08 04 24 14 0E 02 01 03 CC BD 00 - -D→H OK tag=0x000B - result: 0 (APPLIED), owner: 1 (MINE), current: LoRa SF=9 ... - pre-COBS: 80 0B 00 00 01 01 A0 27 BE 33 09 07 00 08 00 24 14 0E 00 01 00 | crc=0x7f0b → 80 0B 00 00 01 01 A0 27 BE 33 09 07 00 08 00 24 14 0E 00 01 00 0B 7F - on wire: 03 80 0B 01 09 01 01 A0 27 BE 33 09 07 02 08 04 24 14 0E 02 01 03 0B 7F 00 - -(radio is now idle; host must re-start RX) - -H→D RX_START tag=0x000C - pre-COBS: 05 0C 00 | crc=0x6201 → 05 0C 00 01 62 - on wire: 03 05 0C 03 01 62 00 - -D→H OK tag=0x000C - pre-COBS: 80 0C 00 | crc=0xb2ab → 80 0C 00 AB B2 - on wire: 03 80 0C 03 AB B2 00 -``` - -#### C.4.2 Reconfigure with pending TXs — they are cancelled - -Host queues two TXs, then before they complete, sends `SET_CONFIG`. Both queued TXs emit `TX_DONE(CANCELLED)`. The `SET_CONFIG` then applies. - -``` -H→D TX tag=0x0014 (data = "first") - pre-COBS: 04 14 00 00 66 69 72 73 74 | crc=0x1dc9 → 04 14 00 00 66 69 72 73 74 C9 1D - on wire: 03 04 14 01 08 66 69 72 73 74 C9 1D 00 - -D→H OK tag=0x0014 - pre-COBS: 80 14 00 | crc=0x3871 → 80 14 00 71 38 - on wire: 03 80 14 03 71 38 00 - -H→D TX tag=0x0015 (data = "second") - pre-COBS: 04 15 00 00 73 65 63 6F 6E 64 | crc=0xa89a → 04 15 00 00 73 65 63 6F 6E 64 9A A8 - on wire: 03 04 15 01 09 73 65 63 6F 6E 64 9A A8 00 - -D→H OK tag=0x0015 - pre-COBS: 80 15 00 | crc=0x0b40 → 80 15 00 40 0B - on wire: 03 80 15 03 40 0B 00 - -H→D SET_CONFIG tag=0x0016 (LoRa SF=10 — different from current config) - pre-COBS: 03 16 00 01 A0 27 BE 33 0A 07 00 08 00 24 14 0E 00 01 00 | crc=0xc630 → 03 16 00 01 A0 27 BE 33 0A 07 00 08 00 24 14 0E 00 01 00 30 C6 - on wire: 03 03 16 08 01 A0 27 BE 33 0A 07 02 08 04 24 14 0E 02 01 03 30 C6 00 - -D→H TX_DONE tag=0x0014 - result: 2 (CANCELLED), airtime_us: 0 - pre-COBS: C1 14 00 02 00 00 00 00 | crc=0xcf82 → C1 14 00 02 00 00 00 00 82 CF - on wire: 03 C1 14 02 02 01 01 01 03 82 CF 00 - -D→H TX_DONE tag=0x0015 - result: 2 (CANCELLED), airtime_us: 0 - pre-COBS: C1 15 00 02 00 00 00 00 | crc=0x77e3 → C1 15 00 02 00 00 00 00 E3 77 - on wire: 03 C1 15 02 02 01 01 01 03 E3 77 00 - -D→H OK tag=0x0016 - result: 0 (APPLIED), owner: 1 (MINE), current: LoRa SF=10 ... - pre-COBS: 80 16 00 00 01 01 A0 27 BE 33 0A 07 00 08 00 24 14 0E 00 01 00 | crc=0x3264 → 80 16 00 00 01 01 A0 27 BE 33 0A 07 00 08 00 24 14 0E 00 01 00 64 32 - on wire: 03 80 16 01 09 01 01 A0 27 BE 33 0A 07 02 08 04 24 14 0E 02 01 03 64 32 00 -``` - -#### C.4.3 Inactivity timeout and recovery - -Host goes silent for more than 1000 ms. Device transitions to UNCONFIGURED. When host returns and tries to TX, it gets `ERR(ENOTCONFIGURED)`. Host re-issues `SET_CONFIG` to recover. - -``` -H→D TX tag=0x001E (data = "before") - pre-COBS: 04 1E 00 00 62 65 66 6F 72 65 | crc=0x197f → 04 1E 00 00 62 65 66 6F 72 65 7F 19 - on wire: 03 04 1E 01 09 62 65 66 6F 72 65 7F 19 00 - -D→H OK tag=0x001E - pre-COBS: 80 1E 00 | crc=0xd7ba → 80 1E 00 BA D7 - on wire: 03 80 1E 03 BA D7 00 - -D→H TX_DONE tag=0x001E (TRANSMITTED, airtime 30976 us) - pre-COBS: C1 1E 00 00 00 79 00 00 | crc=0x3ed6 → C1 1E 00 00 00 79 00 00 D6 3E - on wire: 03 C1 1E 01 01 02 79 01 03 D6 3E 00 - - [ host sleeps 1.5 s — no frames sent ] - [ device's inactivity timer expires → UNCONFIGURED ] - -H→D TX tag=0x001F (data = "after") - pre-COBS: 04 1F 00 00 61 66 74 65 72 | crc=0x03ef → 04 1F 00 00 61 66 74 65 72 EF 03 - on wire: 03 04 1F 01 08 61 66 74 65 72 EF 03 00 - -D→H ERR tag=0x001F - code: 0x0003 (ENOTCONFIGURED) - pre-COBS: 81 1F 00 03 00 | crc=0x0397 → 81 1F 00 03 00 97 03 - on wire: 03 81 1F 02 03 03 97 03 00 - - (host realises session is lost; re-configures) - -H→D SET_CONFIG tag=0x0020 (LoRa SF=7, as before) - pre-COBS: 03 20 00 01 A0 27 BE 33 07 07 00 08 00 24 14 0E 00 01 00 | crc=0xea74 → 03 20 00 01 A0 27 BE 33 07 07 00 08 00 24 14 0E 00 01 00 74 EA - on wire: 03 03 20 08 01 A0 27 BE 33 07 07 02 08 04 24 14 0E 02 01 03 74 EA 00 - -D→H OK tag=0x0020 (APPLIED, MINE) - pre-COBS: 80 20 00 00 01 01 A0 27 BE 33 07 07 00 08 00 24 14 0E 00 01 00 | crc=0x19bb → 80 20 00 00 01 01 A0 27 BE 33 07 07 00 08 00 24 14 0E 00 01 00 BB 19 - on wire: 03 80 20 01 09 01 01 A0 27 BE 33 07 07 02 08 04 24 14 0E 02 01 03 BB 19 00 - -H→D TX tag=0x0021 (retry) - pre-COBS: 04 21 00 00 61 66 74 65 72 | crc=0xdb22 → 04 21 00 00 61 66 74 65 72 22 DB - on wire: 03 04 21 01 08 61 66 74 65 72 22 DB 00 - -D→H OK tag=0x0021 - pre-COBS: 80 21 00 | crc=0xc211 → 80 21 00 11 C2 - on wire: 03 80 21 03 11 C2 00 - -D→H TX_DONE tag=0x0021 (TRANSMITTED, airtime 30976 us) - pre-COBS: C1 21 00 00 00 79 00 00 | crc=0xedb2 → C1 21 00 00 00 79 00 00 B2 ED - on wire: 03 C1 21 01 01 02 79 01 03 B2 ED 00 -``` - ---- - -### C.5 Error responses - -#### C.5.1 TX attempted in UNCONFIGURED state - -Host sends TX without first calling `SET_CONFIG`. Device returns `ERR(ENOTCONFIGURED)`. - -``` -H→D TX tag=0x0028 (data = "hi") - pre-COBS: 04 28 00 00 68 69 | crc=0x7d24 → 04 28 00 00 68 69 24 7D - on wire: 03 04 28 01 05 68 69 24 7D 00 - -D→H ERR tag=0x0028 - code: 0x0003 (ENOTCONFIGURED) - pre-COBS: 81 28 00 03 00 | crc=0x7e53 → 81 28 00 03 00 53 7E - on wire: 03 81 28 02 03 03 53 7E 00 -``` - -#### C.5.2 TX with empty payload data - -Host sends TX with only the flags byte and no data. Device returns `ERR(ELENGTH)` because data must be at least 1 byte. - -``` -H→D TX tag=0x0029 - flags: 0 - data: (empty — invalid) - pre-COBS: 04 29 00 00 | crc=0x5666 → 04 29 00 00 66 56 - on wire: 03 04 29 01 03 66 56 00 - -D→H ERR tag=0x0029 - code: 0x0002 (ELENGTH) - pre-COBS: 81 29 00 02 00 | crc=0x3bd6 → 81 29 00 02 00 D6 3B - on wire: 03 81 29 02 02 03 D6 3B 00 -``` - -#### C.5.3 TX with reserved flag bit set - -Host sets bit 1 of `flags` (reserved). Device returns `ERR(EPARAM)`. - -``` -H→D TX tag=0x002A - flags: 0x02 (bit 1 is reserved) - data: 68 69 - pre-COBS: 04 2A 00 02 68 69 | crc=0x57c7 → 04 2A 00 02 68 69 C7 57 - on wire: 03 04 2A 06 02 68 69 C7 57 00 - -D→H ERR tag=0x002A - code: 0x0001 (EPARAM) - pre-COBS: 81 2A 00 01 00 | crc=0xf559 → 81 2A 00 01 00 59 F5 - on wire: 03 81 2A 02 01 03 59 F5 00 -``` - -#### C.5.4 TX queue full (EBUSY) - -Host has pipelined `tx_queue_capacity` TXs and none have completed. The next TX returns `ERR(EBUSY)`. Host waits for a `TX_DONE` to free a slot, then retries with a **new** tag. - -``` -H→D TX tag=0x002B (queue already full) - pre-COBS: 04 2B 00 00 6F 76 65 72 66 6C 6F 77 | crc=0x4824 → 04 2B 00 00 6F 76 65 72 66 6C 6F 77 24 48 - on wire: 03 04 2B 01 0B 6F 76 65 72 66 6C 6F 77 24 48 00 - -D→H ERR tag=0x002B - code: 0x0006 (EBUSY) - pre-COBS: 81 2B 00 06 00 | crc=0x1a7a → 81 2B 00 06 00 7A 1A - on wire: 03 81 2B 02 06 03 7A 1A 00 -``` - -#### C.5.5 TX collision (CHANNEL_BUSY) and retry - -CAD detects another transmitter. TX is aborted before airtime. Host backs off (randomised 20–100 ms) and retries with a new tag. - -``` -H→D TX tag=0x0032 (data = "retry-me") - pre-COBS: 04 32 00 00 72 65 74 72 79 2D 6D 65 | crc=0x0d16 → 04 32 00 00 72 65 74 72 79 2D 6D 65 16 0D - on wire: 03 04 32 01 0B 72 65 74 72 79 2D 6D 65 16 0D 00 - -D→H OK tag=0x0032 - pre-COBS: 80 32 00 | crc=0x9431 → 80 32 00 31 94 - on wire: 03 80 32 03 31 94 00 - -D→H TX_DONE tag=0x0032 - result: 1 (CHANNEL_BUSY), airtime_us: 0 - pre-COBS: C1 32 00 01 00 00 00 00 | crc=0xee83 → C1 32 00 01 00 00 00 00 83 EE - on wire: 03 C1 32 02 01 01 01 01 03 83 EE 00 - - [ random backoff ] - -H→D TX tag=0x0033 (same data, NEW tag) - pre-COBS: 04 33 00 00 72 65 74 72 79 2D 6D 65 | crc=0xd55f → 04 33 00 00 72 65 74 72 79 2D 6D 65 5F D5 - on wire: 03 04 33 01 0B 72 65 74 72 79 2D 6D 65 5F D5 00 - -D→H OK tag=0x0033 - pre-COBS: 80 33 00 | crc=0xa700 → 80 33 00 00 A7 - on wire: 03 80 33 01 02 A7 00 - -D→H TX_DONE tag=0x0033 - result: 0 (TRANSMITTED), airtime_us: 30976 - pre-COBS: C1 33 00 00 00 79 00 00 | crc=0xba2a → C1 33 00 00 00 79 00 00 2A BA - on wire: 03 C1 33 01 01 02 79 01 03 2A BA 00 -``` - -Note: the `OK` response for tag=0x0033 shows a COBS encoding quirk: the CRC is `0xA700`, so the low byte is `0x00`. COBS encodes this by inserting a `0x02` code byte that splits the run around the zero. - -#### C.5.6 Unknown command type byte - -Host sends a frame with `type = 0x10` (in the reserved H→D range). Device returns `ERR(EUNKNOWN_CMD)` with the received tag echoed. - -``` -H→D type=0x10 tag=0x003C - type: 0x10 (reserved, not yet assigned) - payload: DE AD - pre-COBS: 10 3C 00 DE AD | crc=0x24e2 → 10 3C 00 DE AD E2 24 - on wire: 03 10 3C 05 DE AD E2 24 00 - -D→H ERR tag=0x003C - code: 0x0005 (EUNKNOWN_CMD) - pre-COBS: 81 3C 00 05 00 | crc=0x05a3 → 81 3C 00 05 00 A3 05 - on wire: 03 81 3C 02 05 03 A3 05 00 -``` - -#### C.5.7 SET_CONFIG with truncated LoRa params - -Host declares LoRa modulation but includes only 10 bytes of params instead of the required 15. Device returns `ERR(ELENGTH)`. - -``` -H→D SET_CONFIG tag=0x0046 - modulation_id: 0x01 (LoRa) - params: (10 bytes — should be 15) - pre-COBS: 03 46 00 01 00 00 00 00 00 00 00 00 00 00 | crc=0x293b → 03 46 00 01 00 00 00 00 00 00 00 00 00 00 3B 29 - on wire: 03 03 46 02 01 01 01 01 01 01 01 01 01 01 03 3B 29 00 - -D→H ERR tag=0x0046 - code: 0x0002 (ELENGTH) - pre-COBS: 81 46 00 02 00 | crc=0xb6ea → 81 46 00 02 00 EA B6 - on wire: 03 81 46 02 02 03 EA B6 00 -``` - -#### C.5.8 SET_CONFIG with frequency outside chip range - -Host requests 2.45 GHz on a sub-GHz-only SX1262 (advertised range 150–960 MHz). Device returns `ERR(EPARAM)`. - -``` -H→D SET_CONFIG tag=0x0047 - modulation_id: 0x01 (LoRa) - freq_hz: 2450000000 (2.45 GHz — outside 150 MHz–960 MHz) - (other params as normal) - pre-COBS: 03 47 00 01 80 08 08 92 07 07 00 08 00 24 14 0E 00 01 00 | crc=0x4934 → 03 47 00 01 80 08 08 92 07 07 00 08 00 24 14 0E 00 01 00 34 49 - on wire: 03 03 47 08 01 80 08 08 92 07 07 02 08 04 24 14 0E 02 01 03 34 49 00 - -D→H ERR tag=0x0047 - code: 0x0001 (EPARAM) - pre-COBS: 81 47 00 01 00 | crc=0x950d → 81 47 00 01 00 0D 95 - on wire: 03 81 47 02 01 03 0D 95 00 -``` - -#### C.5.9 SET_CONFIG with unsupported modulation - -Host requests FLRC (2.4 GHz-only modulation) on a sub-GHz SX1262. Capability bitmap does not advertise FLRC. Device returns `ERR(EMODULATION)`. - -``` -H→D SET_CONFIG tag=0x0048 - modulation_id: 0x04 (FLRC — not supported on SX1262) - params: [13 bytes, ignored because modulation is unsupported] - pre-COBS: 03 48 00 04 00 00 00 00 00 00 00 00 00 00 00 00 00 | crc=0x96c2 → 03 48 00 04 00 00 00 00 00 00 00 00 00 00 00 00 00 C2 96 - on wire: 03 03 48 02 04 01 01 01 01 01 01 01 01 01 01 01 01 03 C2 96 00 - -D→H ERR tag=0x0048 - code: 0x0004 (EMODULATION) - pre-COBS: 81 48 00 04 00 | crc=0xbe16 → 81 48 00 04 00 16 BE - on wire: 03 81 48 02 04 03 16 BE 00 -``` - -#### C.5.10 Radio hardware fault during SET_CONFIG - -Radio SPI fails to respond during the configuration sequence. Device returns `ERR(ERADIO)` synchronously, transitions to UNCONFIGURED, and places the radio in its safest reachable state. - -``` -H→D SET_CONFIG tag=0x0049 (LoRa, standard params) - pre-COBS: 03 49 00 01 A0 27 BE 33 07 07 00 08 00 24 14 0E 00 01 00 | crc=0xe56a → 03 49 00 01 A0 27 BE 33 07 07 00 08 00 24 14 0E 00 01 00 6A E5 - on wire: 03 03 49 08 01 A0 27 BE 33 07 07 02 08 04 24 14 0E 02 01 03 6A E5 00 - -D→H ERR tag=0x0049 - code: 0x0101 (ERADIO) - pre-COBS: 81 49 00 01 01 | crc=0x2776 → 81 49 00 01 01 76 27 - on wire: 03 81 49 05 01 01 76 27 00 -``` - ---- - -### C.6 Asynchronous events - -Async events carry `tag = 0x0000`. They may appear interleaved anywhere in the D→H stream. - -#### C.6.1 RX with good CRC - -See C.2.6 above for an example within the continuous-RX flow. - -#### C.6.2 RX with crc_valid=0 (corrupt packet delivered anyway) - -Radio reports a packet, but PHY CRC failed. Device delivers the bytes with `crc_valid = 0`; the host decides whether to discard. RSSI and SNR reported but may not be meaningful. - -``` -D→H RX tag=0x0000 - rssi: -1050 (-105.0 dBm — weak signal) - snr: -80 (-8.0 dB — negative SNR) - freq_err: 2200 (2.2 kHz offset) - timestamp_us: 120000000 (120.000 s) - crc_valid: 0 (FAIL — bytes may be corrupt) - packets_dropped: 0 - origin: 0 (over-the-air) - data: 41 42 FF 00 CC - pre-COBS: C0 00 00 E6 FB B0 FF 98 08 00 00 00 0E 27 07 00 00 00 00 00 00 00 00 41 42 FF 00 CC | crc=0x3c04 → C0 00 00 E6 FB B0 FF 98 08 00 00 00 0E 27 07 00 00 00 00 00 00 00 00 41 42 FF 00 CC 04 3C - on wire: 02 C0 01 07 E6 FB B0 FF 98 08 01 01 04 0E 27 07 01 01 01 01 01 01 01 04 41 42 FF 04 CC 04 3C 00 -``` - -#### C.6.3 RX following buffer overrun - -Host was slow draining. Three packets arrived and were dropped because the ring was full. The next successfully-delivered RX reports `packets_dropped = 3`. The counter resets to 0 after this delivery. - -``` -D→H RX tag=0x0000 - rssi: -730 (-73.0 dBm) - snr: 100 (+10.0 dB) - freq_err: 50 (50 Hz) - timestamp_us: 130500000 - crc_valid: 1 (pass) - packets_dropped: 3 (three lost since previous delivery) - origin: 0 - data: 01 02 - pre-COBS: C0 00 00 26 FD 64 00 32 00 00 00 A0 45 C7 07 00 00 00 00 01 03 00 00 01 02 | crc=0x86a7 → C0 00 00 26 FD 64 00 32 00 00 00 A0 45 C7 07 00 00 00 00 01 03 00 00 01 02 A7 86 - on wire: 02 C0 01 04 26 FD 64 02 32 01 01 05 A0 45 C7 07 01 01 01 03 01 03 01 05 01 02 A7 86 00 -``` - -#### C.6.4 Malformed incoming frame triggers async ERR(EFRAME) - -Host (or noise on the line) sends a frame whose CRC does not match the body. Device discards the frame and emits an async `ERR(EFRAME)` with `tag = 0`. The liveness timer is still reset — the bytes arrived, so the host is present. - -``` -D→H ERR tag=0x0000 - code: 0x0102 (EFRAME) - pre-COBS: 81 00 00 02 01 | crc=0xefce → 81 00 00 02 01 CE EF - on wire: 02 81 01 05 02 01 CE EF 00 -``` - -Note: the same frame type (`ERR`, `0x81`) is used here as in synchronous error responses; the tag (`0x0000`) is the only discriminator. Host code distinguishes async faults from command responses by the tag value. - -#### C.6.5 Asynchronous radio fault - -Radio chip asserts an unexpected IRQ (e.g., an RxTimeout from a stale `RX_START` that the firmware already reset). No originating command. Device emits `ERR(ERADIO)` with `tag = 0`. - -``` -D→H ERR tag=0x0000 - code: 0x0101 (ERADIO) - pre-COBS: 81 00 00 01 01 | crc=0xba9d → 81 00 00 01 01 9D BA - on wire: 02 81 01 05 01 01 9D BA 00 -``` - ---- - -### C.7 Multi-client scenarios (informative) - -These examples assume firmware with multi-client capability bit 32 set. Single-client deployments will never see these responses. - -#### C.7.1 Second client finds matching config (ALREADY_MATCHED) - -Client A has locked LoRa SF7/BW125. Client B connects and requests identical params. Device reports `ALREADY_MATCHED` with `owner = OTHER`; Client B may proceed without changing anything. - -``` -H→D SET_CONFIG tag=0x0050 (from Client B; identical to Client A's config) - pre-COBS: 03 50 00 01 A0 27 BE 33 07 07 00 08 00 24 14 0E 00 01 00 | crc=0x16cb → 03 50 00 01 A0 27 BE 33 07 07 00 08 00 24 14 0E 00 01 00 CB 16 - on wire: 03 03 50 08 01 A0 27 BE 33 07 07 02 08 04 24 14 0E 02 01 03 CB 16 00 - -D→H OK tag=0x0050 - result: 1 (ALREADY_MATCHED) - owner: 2 (OTHER — Client A holds the lock) - current_modulation: 0x01 (LoRa) - current_params: [SF=7, BW=125 kHz, ... same as request] - pre-COBS: 80 50 00 01 02 01 A0 27 BE 33 07 07 00 08 00 24 14 0E 00 01 00 | crc=0xd834 → 80 50 00 01 02 01 A0 27 BE 33 07 07 00 08 00 24 14 0E 00 01 00 34 D8 - on wire: 03 80 50 0A 01 02 01 A0 27 BE 33 07 07 02 08 04 24 14 0E 02 01 03 34 D8 00 -``` - -#### C.7.2 Second client finds mismatched config (LOCKED_MISMATCH) - -Client A is locked on SF7. Client B requests SF9 with different TX power. Device rejects the change; returns the actual active params so Client B can diff and decide what to do. - -``` -H→D SET_CONFIG tag=0x0051 (from Client B; sf=9, tx_power_dbm=17) - pre-COBS: 03 51 00 01 A0 27 BE 33 09 07 00 08 00 24 14 11 00 01 00 | crc=0x7ac9 → 03 51 00 01 A0 27 BE 33 09 07 00 08 00 24 14 11 00 01 00 C9 7A - on wire: 03 03 51 08 01 A0 27 BE 33 09 07 02 08 04 24 14 11 02 01 03 C9 7A 00 - -D→H OK tag=0x0051 - result: 2 (LOCKED_MISMATCH) - owner: 2 (OTHER) - current_modulation: 0x01 (LoRa) - current_params: [SF=7, tx_power=14 — Client A's setting, unchanged] - pre-COBS: 80 51 00 02 02 01 A0 27 BE 33 07 07 00 08 00 24 14 0E 00 01 00 | crc=0xedf5 → 80 51 00 02 02 01 A0 27 BE 33 07 07 00 08 00 24 14 0E 00 01 00 F5 ED - on wire: 03 80 51 0A 02 02 01 A0 27 BE 33 07 07 02 08 04 24 14 0E 02 01 03 F5 ED 00 -``` - -#### C.7.3 Cross-client TX loopback (origin=1) - -Client A transmits. Client B, which is also RX-active on the same device, sees the packet as an RX event with the `origin = 1` flag set, distinguishing it from over-the-air traffic. - -``` -H→D TX tag=0x005A (from Client A; data = "BROADCAST") - pre-COBS: 04 5A 00 00 42 52 4F 41 44 43 41 53 54 | crc=0x7914 → 04 5A 00 00 42 52 4F 41 44 43 41 53 54 14 79 - on wire: 03 04 5A 01 0C 42 52 4F 41 44 43 41 53 54 14 79 00 - -D→H OK tag=0x005A (to Client A only) - pre-COBS: 80 5A 00 | crc=0x16b2 → 80 5A 00 B2 16 - on wire: 03 80 5A 03 B2 16 00 - -D→H TX_DONE tag=0x005A (to Client A only; TRANSMITTED) - pre-COBS: C1 5A 00 00 00 D8 00 00 | crc=0xa850 → C1 5A 00 00 00 D8 00 00 50 A8 - on wire: 03 C1 5A 01 01 02 D8 01 03 50 A8 00 - -D→H RX tag=0x0000 (to Client B only; local loopback, not OTA) - rssi: 0 (n/a for local loopback) - snr: 0 - freq_err: 0 - timestamp_us: 180000000 - crc_valid: 1 (local TX, assumed valid) - packets_dropped: 0 - origin: 1 (local loopback — NOT over the air) - data: 42 52 4F 41 44 43 41 53 54 ("BROADCAST") - pre-COBS: C0 00 00 00 00 00 00 00 00 00 00 00 95 BA 0A 00 00 00 00 01 00 00 01 42 52 4F 41 44 43 41 53 54 | crc=0x00e0 → C0 00 00 00 00 00 00 00 00 00 00 00 95 BA 0A 00 00 00 00 01 00 00 01 42 52 4F 41 44 43 41 53 54 E0 00 - on wire: 02 C0 01 01 01 01 01 01 01 01 01 01 04 95 BA 0A 01 01 01 02 01 01 0C 01 42 52 4F 41 44 43 41 53 54 E0 01 00 -``` - ---- - -### C.8 COBS edge cases - -#### C.8.1 Frame body contains no 0x00 bytes - -When the unencoded body has no zero bytes, COBS simply prepends one code byte giving the run length + 1. No internal code bytes are inserted. - -``` -H→D PING tag=0x0101 - (tag bytes 0x01 0x01 — no zeros anywhere in body) - pre-COBS: 01 01 01 | crc=0xd8bc → 01 01 01 BC D8 - on wire: 06 01 01 01 BC D8 00 -``` - -The leading `06` means "next zero (or end of frame) is 6 bytes away". - -#### C.8.2 Frame body contains many 0x00 bytes - -RX event with all-zero metadata and a 3-byte all-zero payload. COBS must replace each zero with a run-length code byte. This is the opposite extreme from C.8.1: many short runs, many code bytes. - -``` -D→H RX tag=0x0000 - rssi: 0, snr: 0, freq_err: 0 - timestamp_us: 256 (0x100) - crc_valid: 1, packets_dropped: 0, origin: 0 - data: 00 00 00 - pre-COBS: C0 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 00 00 00 01 00 00 00 00 00 00 | crc=0x74b4 → C0 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 00 00 00 01 00 00 00 00 00 00 B4 74 - on wire: 02 C0 01 01 01 01 01 01 01 01 01 01 02 01 01 01 01 01 01 02 01 01 01 01 01 01 03 B4 74 00 -``` - -The frequent `01` bytes in the wire output are COBS code bytes pointing "next zero is 1 byte away" — each represents a single zero in the original body. - -#### C.8.3 254-byte non-zero run triggers a 0xFF code byte - -When the body contains a run of 254 or more non-zero bytes, COBS inserts an extra code byte. This is the only case where encoding adds more than a fixed constant — it adds `ceil(N / 254)` overhead bytes for an N-byte run. - -Example: a TX command with tag=0x0101, skip_cad flag, and 253 bytes of non-zero data. Total pre-COBS body is 259 bytes with no zeros (header, flags byte, all 253 data bytes, and both CRC bytes happen to be non-zero). - -``` - Body composition (259 bytes, all non-zero): - type: 04 (1 byte) - tag: 01 01 (2 bytes) - flags: 01 (1 byte) - data: 01 02 03 ... FD (253 bytes, values 0x01..0xFD) - crc: 53 46 (2 bytes; CRC = 0x4653) - - COBS output (261 bytes): - Position 0: FF ← code byte: "next zero (or end of frame) - would be 255 bytes away; bail and start - a fresh run here" - Positions 1..254: 04 01 01 01 01 02 03 ... FC (first 254 source bytes) - Position 255: 06 ← new code byte - Positions 256..260: FD 53 46 (remaining 5 source bytes: - the last data byte FD, - plus CRC 53 46) - (code says "6 bytes" = 5 data + the - virtual end-of-frame position) - - Wire: 261 encoded bytes + 1 trailing 0x00 delimiter = 262 bytes total. -``` - -**Buffer sizing consequence**: a host encoding an N-byte frame needs at most `N + ceil(N / 254) + 1` bytes of output buffer (plus the trailing delimiter). For any frame under 254 bytes, overhead is exactly 1 byte (the leading code byte) plus the trailing delimiter. At exactly 254 bytes or more, a second code byte is added. At 508+, a third. Real DongLoRa Protocol frames almost always fall in the single-overhead-byte range. - ---- - -### C.9 Parser resynchronization (narrative) - -#### C.9.1 Mid-stream connection - -A host process opens the transport while the device is mid-frame (for example, a previous host process was killed and a new one is starting up). The incoming byte stream looks like: - -``` -... 14 0E 02 01 03 0B 7F 00 03 05 0C 03 01 62 00 ... - |← tail of previous frame →| |← next complete frame →| -``` - -The new host discards incoming bytes until it sees a `0x00`. That `0x00` closes whatever partial frame was in flight when the host started reading (which is discarded). Reading resumes with the byte immediately following: `03 05 0C 03 01 62 00` is the next complete frame, which COBS-decodes cleanly and verifies CRC. Normal operation proceeds. - -No special handshake, no timeout — the self-synchronising property of COBS makes mid-stream recovery deterministic and instantaneous. - -#### C.9.2 Single-frame corruption - -The host receives three frames in rapid succession. The middle one has a bit error in transit (say, flipped by a noisy USB cable) that corrupts one of the data bytes. The host: - -1. Reads to the first `0x00`, decodes the first frame, verifies CRC → OK, processes it. -2. Reads to the second `0x00`, decodes the second frame, **CRC check fails** → discards the frame silently. Does not emit any response (it cannot determine the originating tag reliably from a corrupted frame). -3. Reads to the third `0x00`, decodes the third frame, verifies CRC → OK, processes it. - -The outstanding tag of the discarded frame remains open on the host side. The host's per-command timeout (Section 14.2) will eventually fire, at which point the host may re-issue the command with a new tag. In practice, transport-level bit errors on USB are very rare; this path is defensive rather than routine. - -The device, in the symmetrical case, emits an async `ERR(EFRAME)` with tag `0x0000` (§C.6.4) as a diagnostic hint but does not directly signal which command was lost — the corrupted bytes are, by definition, not reliably parseable. - -## Appendix D — Glossary - -**Airtime**: The duration a packet occupies the radio channel during transmission. - -**CAD**: Channel Activity Detection. A LoRa radio primitive that briefly listens for a LoRa preamble and reports busy/clear. - -**COBS**: Consistent Overhead Byte Stuffing. A framing technique that eliminates a chosen byte value (here, `0x00`) from an encoded byte stream. - -**CR**: Coding Rate. LoRa forward-error-correction ratio: 4/5, 4/6, 4/7, or 4/8. - -**FLRC**: Fast Long Range Communication. A higher-throughput modulation supported by SX128x and LR2021. - -**LDRO**: Low Data Rate Optimization. A LoRa setting required at high SF and low BW to tolerate clock drift during long symbol times. - -**LoRa**: Long Range. A chirp-spread-spectrum modulation technique from Semtech. - -**LR-FHSS**: Long Range Frequency-Hopping Spread Spectrum. A Semtech modulation for low-throughput satellite or very-long-range use. - -**Outstanding tag**: A tag for which the host has sent a command but not yet received its final response. - -**SF**: Spreading Factor. LoRa symbol-duration parameter (5–12), trading throughput for range. - -**Tag**: The 16-bit correlation ID in the frame header, used to match responses to commands. diff --git a/README.md b/README.md index bede9b1..de38a1d 100644 --- a/README.md +++ b/README.md @@ -62,4 +62,8 @@ See [src/board/PORTING.md](src/board/PORTING.md). ## Protocol -See [PROTOCOL.md](PROTOCOL.md) for the complete wire format specification. +See [PROTOCOL.md](https://github.com/donglora/protocol-rs/blob/main/PROTOCOL.md) +in the [donglora-protocol](https://github.com/donglora/protocol-rs) repo +(crates.io: [`donglora-protocol`](https://crates.io/crates/donglora-protocol)) +for the complete wire format specification — that is the canonical home; +this firmware just consumes it. diff --git a/BLE_MUX_DESIGN_DOC.md b/docs/ble_mux_design.md similarity index 100% rename from BLE_MUX_DESIGN_DOC.md rename to docs/ble_mux_design.md