fix: correct postcard encoding for u8/i8 fields (was using varint/zigzag)

ROOT CAUSE: postcard encodes u8 and i8 as raw bytes, NOT varint/zigzag.
The Python encoders used varint() for u8 and zigzag() for i8. For small
values this was silently correct (varint(7) == raw byte 7), but for
tx_power_dbm: zigzag(14) = 28, which the new validation correctly
rejected as out of range (-9..22). This was a pre-existing bug masked
by the absence of validation.

Fix all RadioConfig field encoders in both donglora.py and meshcore_rx.py.
Fix docs/PROTOCOL.md to correctly document i8 as raw byte, not zigzag.
Restore full config validation.
This commit is contained in:
swaits committed 2026-03-29 20:42:58 -06:00
1 parent a0d9b3b4f0
commit 0b4eb04591
3 files changed
+22 -15

No files matched your search

+2 -1
View File
@@ -26,8 +26,9 @@ Postcard is a compact binary format. The subset used by DongLoRa:
| Type | Encoding | | Type | Encoding |
|------|----------| |------|----------|
| `u8` | Raw byte | | `u8` | Raw byte |
| `i8` | Raw byte (two's complement) |
| `u16`, `u32` | Varint (LEB128: 7 data bits per byte, MSB = continuation) | | `u16`, `u32` | Varint (LEB128: 7 data bits per byte, MSB = continuation) |
| `i8`, `i16` | Zigzag then varint (`(n << 1) ^ (n >> bits-1)`) | | `i16`, `i32` | Zigzag then varint (`(n << 1) ^ (n >> bits-1)`) |
| Enum variant | Varint of 0-based variant index, then variant fields | | Enum variant | Varint of 0-based variant index, then variant fields |
| `Option<T>` | `0x00` = None, `0x01` + T = Some | | `Option<T>` | `0x00` = None, `0x01` + T = Some |
| `Vec<u8, N>` | Varint length, then raw bytes | | `Vec<u8, N>` | Varint length, then raw bytes |
+12 -7
View File
@@ -12,6 +12,7 @@ See docs/PROTOCOL.md for the full specification.
import glob import glob
import struct import struct
import subprocess import subprocess
import serial import serial
import time import time
from cobs import cobs from cobs import cobs
@@ -112,13 +113,17 @@ def decode_zigzag(data: bytes) -> tuple[int, bytes]:
# ── Command encoding ───────────────────────────────────────────── # ── Command encoding ─────────────────────────────────────────────
def encode_config(cfg: dict) -> bytes: def encode_config(cfg: dict) -> bytes:
"""Encode a RadioConfig dict to postcard bytes.""" """Encode a RadioConfig dict to postcard bytes.
out = varint(cfg["freq_hz"])
out += varint(cfg["bw"]) Postcard wire format: u8/i8 are raw bytes, u16/u32 are varint,
out += varint(cfg["sf"]) i16/i32 are zigzag+varint, enums are varint variant index.
out += varint(cfg["cr"]) """
out += varint(cfg["sync_word"]) out = varint(cfg["freq_hz"]) # u32: varint
out += zigzag(cfg["tx_power_dbm"]) out += varint(cfg["bw"]) # enum: varint variant index
out += struct.pack("B", cfg["sf"]) # u8: raw byte
out += struct.pack("B", cfg["cr"]) # u8: raw byte
out += varint(cfg["sync_word"]) # u16: varint
out += struct.pack("b", cfg["tx_power_dbm"]) # i8: raw signed byte
return out return out
+8 -7
View File
@@ -426,12 +426,12 @@ def zigzag(n: int) -> bytes:
def encode_radio_config(cfg: dict) -> bytes: def encode_radio_config(cfg: dict) -> bytes:
out = varint(cfg["freq_hz"]) out = varint(cfg["freq_hz"]) # u32: varint
out += varint(cfg["bw"]) out += varint(cfg["bw"]) # enum: varint variant index
out += varint(cfg["sf"]) out += struct.pack("B", cfg["sf"]) # u8: raw byte
out += varint(cfg["cr"]) out += struct.pack("B", cfg["cr"]) # u8: raw byte
out += varint(cfg["sync_word"]) out += varint(cfg["sync_word"]) # u16: varint
out += zigzag(cfg["tx_power_dbm"]) out += struct.pack("b", cfg["tx_power_dbm"]) # i8: raw byte (signed)
return out return out
@@ -931,7 +931,8 @@ def _format_rssi_snr(rssi: int, snr: int) -> str:
def configure_and_listen(ser: serial.Serial): def configure_and_listen(ser: serial.Serial):
send_cmd(ser, {"type": "SetConfig", "config": RADIO_CONFIG}, "SetConfig 910.525/62.5k/SF7/CR4_5") send_cmd(ser, {"type": "Ping"}, "Ping")
send_cmd(ser, {"type": "SetConfig", "config": RADIO_CONFIG}, "SetConfig 910.525/62.5k/SF7/CR4/5")
send_cmd(ser, {"type": "StartRx"}, "StartRx") send_cmd(ser, {"type": "StartRx"}, "StartRx")
print(f"\n{BWHT}Listening for packets{RST} {DIM}(Ctrl+C to stop){RST}\n") print(f"\n{BWHT}Listening for packets{RST} {DIM}(Ctrl+C to stop){RST}\n")