Skip to content

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
PartMeaning
:Start sentinel
LLLLExactly 4 hex digits — byte length of payload (0000FFFF; values > 00FF require all four digits)
<PAYLOAD_HEX>Twice the length in hex digits (upper or lower case accepted)
\nLF line terminator (CR before LF optional on RX)

Example: an 8-byte payload A1 B2 C3 D4 E5 F6 07 08:

:0008A1B2C3D4E5F60708

Implementations 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:

VerbHexDirectionMeaning
FRAME_INJECT0x01Host→DeviceInject raw LowMesh mesh PDU bytes (test harness, operator tools, and host-originated mesh traffic such as FRAG_NACK recovery — see Fragmentation)
FRAME_CAPTURE0x02Device→HostForward 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_SET0x10Host→DeviceSet mute / buzzer profile
NOTIFY_POLICY_GET0x11Device→HostRead policy
CHAN_KEY_SET0x20Host→DeviceProvision per-channel AES-256 key (wrapped)
NETWORK_KEY_SET0x21Host→DeviceSet Network Master Key (32 B); firmware derives per-channel keys per Crypto keys
GPS_INJECT0x28Host→DevicePush a WGS84 fix from phone / PC (lat_e7, lon_e7, ts); firmware may emit GPS_FIX or attach to NODE_ADVERT
GPS_REQUEST0x29Device→HostRadio has no GNSS: ask companion app for a location (see GPS)
LOG_LEVEL0x30Host→DeviceAdjust debug

NOTIFY_POLICY_SET body

FieldSize
channelSlot1
mute1
buzzerProfileId1

Example session (annotated)

Host sends (hex payload only):

10 05 00 03
│  │  │  └── buzzer profile 3
│  │  └── mute off (0)
│  └── channel slot 5
└── NOTIFY_POLICY_SET

Device ACK:

11 05 00 03

Security

  • 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 (FragmentationHost-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_CAPTURE record.
  • The host parses channelHash, srcId, sessionId, messageId, fragment, and PAYLOAD_BODY_V2 chunk 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_NACK mesh frame in FRAME_INJECT so 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.

LowMeshOS — always open-source mesh protocol documentation