# DongLoRa USB Protocol DongLoRa communicates over USB CDC-ACM (virtual serial port). No baud rate configuration needed. **USB identifiers:** VID `1209`, PID `5741` ## Framing Each message is [COBS](https://en.wikipedia.org/wiki/Consistent_Overhead_Byte_Stuffing)-encoded and terminated with a `0x00` sentinel byte: ``` [COBS-encoded bytes] [0x00] ``` COBS guarantees `0x00` never appears in the encoded data, so the sentinel unambiguously marks frame boundaries. USB packets are chunked at 64 bytes. ## Encoding All integers are **fixed-size little-endian**. No variable-length encoding. Every field has a known size at a known offset. | Type | Size | Encoding | |------|------|----------| | `u8` | 1 | Raw byte | | `i8` | 1 | Raw byte (two's complement) | | `u16` | 2 | Little-endian | | `i16` | 2 | Little-endian (two's complement) | | `u32` | 4 | Little-endian | ## RadioConfig (13 bytes) | Offset | Size | Field | Type | Valid range | |--------|------|-------|------|-------------| | 0 | 4 | `freq_hz` | u32 LE | 150,000,000 – 960,000,000 | | 4 | 1 | `bw` | u8 | 0–9 (see bandwidth table) | | 5 | 1 | `sf` | u8 | 5–12 | | 6 | 1 | `cr` | u8 | 5–8 (coding rate denominator) | | 7 | 2 | `sync_word` | u16 LE | any | | 9 | 1 | `tx_power_dbm` | i8 | board-dependent, or `-128` for max | | 10 | 2 | `preamble_len` | u16 LE | 6–65535, or `0` for default (16) | | 12 | 1 | `cad` | u8 | 0 = disabled, non-zero = enabled | Set `tx_power_dbm` to `-128` (0x80) to use the board's maximum TX power. The firmware resolves this to the actual hardware max. Set `preamble_len` to `0` to use the firmware default of 16 symbols. Longer preambles improve reliability in mesh/noisy environments at the cost of slightly longer air time. Set `cad` to `1` (or any non-zero value) to enable Channel Activity Detection (listen-before-talk) before each transmission. The radio checks for channel activity and waits for a clear channel before transmitting. Set to `0` to disable. Default: enabled. ## Commands (Host → Firmware) Each command is a tag byte followed by fixed-size fields. | Tag | Command | Payload | |-----|---------|---------| | 0 | Ping | — | | 1 | GetConfig | — | | 2 | SetConfig | RadioConfig (10 bytes) | | 3 | StartRx | — | | 4 | StopRx | — | | 5 | Transmit | has_config (1) + [RadioConfig if 1] + len (u16 LE) + payload | | 6 | DisplayOn | — | | 7 | DisplayOff | — | | 8 | GetMac | — | ## Responses (Firmware → Host) | Tag | Response | Payload | |-----|----------|---------| | 0 | Pong | — | | 1 | Config | RadioConfig (10 bytes) | | 2 | RxPacket | rssi (i16 LE) + snr (i16 LE) + len (u16 LE) + payload | | 3 | TxDone | — | | 4 | Ok | — | | 5 | Error | code (u8) | | 6 | MacAddress | 6 raw bytes (board's MAC/device address) | ## Bandwidth | Value | Bandwidth | |-------|-----------| | 0 | 7.8 kHz | | 1 | 10.4 kHz | | 2 | 15.6 kHz | | 3 | 20.8 kHz | | 4 | 31.25 kHz | | 5 | 41.7 kHz | | 6 | 62.5 kHz | | 7 | 125 kHz | | 8 | 250 kHz | | 9 | 500 kHz | ## Error Codes | Value | Error | Meaning | |-------|-------|---------| | 0 | InvalidConfig | Radio config validation failed | | 1 | RadioBusy | Radio busy (e.g. RX restart failed) | | 2 | TxTimeout | Transmission timed out | | 3 | CrcError | CRC error on received packet | | 4 | NotConfigured | SetConfig required first | | 5 | NoDisplay | No display attached | ## Worked Example Configure for 915 MHz, 125 kHz BW, SF7, CR 4/5, max TX power, default preamble, CAD on: ### 1. Build the bytes ```python import struct config = struct.pack(" 32: return INVALID # bad metadata, discard or flag # Step 2: check against demodulation floor min_snr = -2.5 * (sf - 4) margin = snr - min_snr # dB above the floor if margin < 0: return UNRELIABLE # below floor, likely corrupt if margin < 3: return MARGINAL # near floor, may contain errors return GOOD # comfortable margin ``` **What to do with each grade:** - **GOOD** — process normally. - **MARGINAL** — process, but prefer application-layer integrity checks (checksums, sequence numbers, known-format validation) before acting on the payload. Log for diagnostics. - **UNRELIABLE** — log for diagnostics but do not trust the payload for routing decisions, command execution, or retransmission. Applications may still display the data with a warning. - **INVALID** — discard. The SNR metadata itself is corrupt. ### Application-layer defenses For safety-critical or command-and-control applications, do not rely solely on the radio CRC. Add your own integrity checks: - An application-layer CRC or hash over the payload - Sequence numbers to detect duplicates and missing packets - Known-format validation (magic bytes, expected lengths) - Require acknowledgement before acting on commands ### Radio config impact Wider bandwidth and lower spreading factor increase throughput but reduce sensitivity, making low-SNR packets more common in marginal links. If your application sees frequent MARGINAL/UNRELIABLE packets, consider increasing SF or decreasing BW to improve link margin at the cost of air time. ## Host Implementation Checklist 1. Open USB serial port (find by VID:PID `1209:5741`) 2. Implement COBS encode/decode (libraries exist for every language) 3. Pack commands with `struct.pack` or equivalent — all fields are fixed-size LE 4. Optionally send `GetMac`, read back `MacAddress` (useful with multiple dongles) 5. Send `SetConfig`, read back `Ok` 6. Send `StartRx`, read back `Ok` 7. Loop: read frames, decode `RxPacket` (rssi, snr, payload at known offsets) 8. To transmit: send `Transmit`, read back `TxDone` See `examples/` for working Python implementations.