Appearance
USB and BLE hex API
Applications and test harnesses interact with firmware through framed hex lines. This unifies USB CDC and BLE notifications.
Line format (normative)
Each record is exactly:
:LLLL<PAYLOAD_HEX>\n| Part | Meaning |
|---|---|
: | Start sentinel |
LLLL | Exactly 4 hex digits — byte length of payload (0000–FFFF; values > 00FF require all four digits) |
<PAYLOAD_HEX> | Twice the length in hex digits (upper or lower case accepted) |
\n | LF line terminator (CR before LF optional on RX) |
Example: an 8-byte payload A1 B2 C3 D4 E5 F6 07 08:
:0008A1B2C3D4E5F60708Implementations MUST emit LLLL as four hex characters on TX. Parsers MUST read four length digits after :. Alternate styles (LEN=4;…, 2-digit lengths, binary HDLC) are not part of the normative USB/BLE API.
USB (CDC)
- Baud rate is ignored for native USB CDC.
- The normative on-wire format remains
:LLLL<PAYLOAD_HEX>\n— do not rely on undocumented binary shortcuts for interoperability tests.
BLE (GATT)
- Notify from device → client: hex lines as UTF-8 text.
- Write client → device: same hex line format.
- Increase MTU to 247 where possible to reduce fragmentation of hex strings.
Command verbs (payload prefix)
First payload byte = verb:
| Verb | Hex | Direction | Meaning |
|---|---|---|---|
FRAME_INJECT | 0x01 | Host→Device | Inject raw LowMesh mesh PDU bytes (test harness, operator tools, and host-originated mesh traffic such as FRAG_NACK recovery — see Fragmentation) |
FRAME_CAPTURE | 0x02 | Device→Host | Forward a captured mesh PDU (or documented slice) received over RF — used for sniffing, debug, and streaming fragmented payloads to a companion in Host-Tethered / proxy mode (Fragmentation) |
NOTIFY_POLICY_SET | 0x10 | Host→Device | Set mute / buzzer profile |
NOTIFY_POLICY_GET | 0x11 | Device→Host | Read policy |
CHAN_KEY_SET | 0x20 | Host→Device | Provision per-channel AES-256 key (wrapped) |
NETWORK_KEY_SET | 0x21 | Host→Device | Set Network Master Key (32 B); firmware derives per-channel keys per Crypto keys |
GPS_INJECT | 0x28 | Host→Device | Push a WGS84 fix from phone / PC (lat_e7, lon_e7, ts); firmware may emit GPS_FIX or attach to NODE_ADVERT |
GPS_REQUEST | 0x29 | Device→Host | Radio has no GNSS: ask companion app for a location (see GPS) |
LOG_LEVEL | 0x30 | Host→Device | Adjust debug |
NOTIFY_POLICY_SET body
| Field | Size |
|---|---|
channelSlot | 1 |
mute | 1 |
buzzerProfileId | 1 |
Example session (annotated)
Host sends (hex payload only):
10 05 00 03
│ │ │ └── buzzer profile 3
│ │ └── mute off (0)
│ └── channel slot 5
└── NOTIFY_POLICY_SETDevice ACK:
11 05 00 03Security
- Channel key set over USB/BLE MUST require physical button confirmation or already unlocked session.
- Never echo full keys in log unless user explicitly enables key debug mode.
Reference parser (pseudo-TypeScript)
ts
function parseHexLine(line: string): Uint8Array {
const s = line.trim()
if (!s.startsWith(':')) throw new Error('bad frame')
const len = parseInt(s.slice(1, 5), 16)
const hex = s.slice(5, 5 + len * 2)
const out = new Uint8Array(len)
for (let i = 0; i < len; i++) {
out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16)
}
return out
}Fragment streaming and recovery (proxy Clients)
When a Client device is acting as a transparent pipe for large fragmented transfers (Fragmentation — Host-Tethered Streaming):
FRAME_CAPTURE (0x02) — live fragments to the host
- Firmware SHOULD push each accepted file / multi-fragment frame (typically the full Mesh PDU bytes the stack would have handed to a local reassembler) to the companion as a
FRAME_CAPTURErecord. - The host parses
channelHash,srcId,sessionId,messageId,fragment, andPAYLOAD_BODY_V2chunk fields from each forwarded frame and runs sliding-window reassembly in RAM.
FRAME_INJECT (0x01) — host-injected FRAG_NACK (and peers)
- When the host detects holes in its reassembly window, it MUST wrap the corresponding
FRAG_NACKmesh frame inFRAME_INJECTso the firmware transmits it on RF like any other injected mesh PDU. - The MCU routes that frame; it does not need a local full-file buffer for the missing pieces.
Backpressure: drop RF, do not exhaust RAM
Normative for proxy-class firmware: If the USB CDC pipe, BLE ATT queue, or an internal host-egress ring shows sustained backpressure (pending FRAME_CAPTURE deliveries above a fixed, documented threshold), the node MUST drop newly received RF fragments that would otherwise be queued for the host — rather than growing unbounded RAM queues.
Rationale: A dropped fragment is recoverable via the host-driven FRAG_NACK / retry path (Fragmentation). Crashing or watchdog-resetting the node is worse than losing a chunk. Companion apps SHOULD assume lossy streaming and SHOULD NACK gaps on a window / timeout basis, not only after the sender stops.
Throughput note
BLE notifications and USB CDC bulk throughput vary by OS, cable, and negotiated MTU. Firmware SHOULD coalesce hex lines only when it does not violate the drop rather than queue rule above; hosts SHOULD request large ATT MTU (e.g. 247 where supported) and avoid synchronous per-fragment UI work on the hot path.
Alignment with mesh frames
The binary layout inside FRAME_INJECT MUST match the Overview / Packet layout Mesh PDU so that what you inject on USB/BLE is what would have been transmitted on RF (minus PHY preamble). The same applies to the inner bytes reported by FRAME_CAPTURE.