mirror of
https://github.com/meshcore-dev/meshcore_py.git
synced 2026-07-29 03:18:12 +00:00
fix: decode trailing fields in AUTOADD_CONFIG/LOGIN_SUCCESS/ACK
Wire-format parity fixes in reader.py for trailing fields the
companion-radio firmware emits but the SDK decoder was dropping, plus an
over-read guard on DEFAULT_FLOOD_SCOPE. Each fix uses the existing
BATT_AND_STORAGE defensive-read pattern (up-front minimum-length check
plus per-field cumulative-length gates), so older firmware that doesn't
emit the trailing fields keeps decoding without raising. Every fix ships
a legacy-frame and modern-frame unit test pair.
AUTOADD_CONFIG (PacketType 25): companion-v1.14.0 firmware emits a
trailing max_hops byte (firmware commit 00566741); the SDK dropped it.
Adds a defensive `if len(data) >= 3` read. Pre-v1.14.0 frames decode to
`{"config": ...}` with no max_hops key.
LOGIN_SUCCESS (PacketType 0x85): the new-style RESP_SERVER_LOGIN_OK path
emits three trailing fields the SDK was dropping — server_timestamp (4B,
firmware commit 0e90b731), acl_permissions (1B, 7947e8a2), and
fw_ver_level (1B, 418ae08b), all first shipped in companion-v1.10.0.
Adds per-field length gates (>=12, >=13, >=14). Legacy 8-byte "OK"-path
frames decode unchanged.
ACK (PacketType 0x82): firmware emits a trailing 4-byte trip_time
(round-trip latency in ms) since companion-v1.0.0a (firmware commit
d9dc76f1, Jan 2025) — on the wire ~16 months but never surfaced by the
SDK. Adds an `if len(data) >= 9` read. Legacy 5-byte ACK frames decode
unchanged.
DEFAULT_FLOOD_SCOPE (PacketType 28): firmware emits a 48-byte populated
frame or a 1-byte sentinel when no scope is set. The SDK unconditionally
read 31+16 bytes, over-reading 47 bytes past the end of the sentinel
frame and dispatching `{"scope_name": "", "scope_key": ""}`. Adds an
`if len(data) >= 48` guard so the sentinel dispatches `{}`. Consumers
detecting "no scope" via `payload["scope_name"] == ""` should switch to
a key-presence check.
RAW_DATA: the payload-framing fix this branch originally carried landed
upstream first in PR #86 (commit 44b21be), with identical decode logic
(discard the reserved 0xFF byte, then read the remaining variable-length
payload). That redundant change is dropped here; this commit retains only
its regression test (test_raw_data_realistic_frame), which exercises the
upstream fix end-to-end — the upstream change shipped without a test.
CHANGELOG:
- Decode max_hops trailing byte in AUTOADD_CONFIG (companion-v1.14.0+).
- Decode server_timestamp / acl_permissions / fw_ver_level trailing
fields in LOGIN_SUCCESS (companion-v1.10.0+).
- Decode trip_time trailing field in ACK (companion-v1.0.0a+).
- DEFAULT_FLOOD_SCOPE dispatches `{}` on the 1-byte sentinel frame
instead of `{scope_name: "", scope_key: ""}`. Detect "no scope" via
key presence.
Tests: 10 tests in tests/unit/test_protocol_surface_gaps.py (legacy +
modern frame pair per finding, including a RAW_DATA regression test for
the upstream fix); all 10 pass on the rebased upstream base.
Why: companion-radio firmware has been emitting these fields on the wire
for between 2 and 16 months; SDK consumers (integrations, bots,
dashboards) lose access to data that is on the wire today.
This commit is contained in:
@@ -184,8 +184,13 @@ class MessageReader:
|
||||
|
||||
elif packet_type_value == PacketType.DEFAULT_FLOOD_SCOPE.value:
|
||||
res = {}
|
||||
res["scope_name"] = dbuf.read(31).decode("utf-8", "ignore").replace("\0", "")
|
||||
res["scope_key"] = dbuf.read(16).hex()
|
||||
# Firmware emits a 48-byte frame when a scope is configured,
|
||||
# or a 1-byte sentinel frame (just the response code) when no
|
||||
# scope is set. Gate the 31+16 byte read so the sentinel
|
||||
# dispatches an empty payload instead of empty-string fields.
|
||||
if len(data) >= 48:
|
||||
res["scope_name"] = dbuf.read(31).decode("utf-8", "ignore").replace("\0", "")
|
||||
res["scope_key"] = dbuf.read(16).hex()
|
||||
await self.dispatcher.dispatch(Event(EventType.DEFAULT_FLOOD_SCOPE, res))
|
||||
|
||||
elif packet_type_value == PacketType.MSG_SENT.value:
|
||||
@@ -526,7 +531,12 @@ class MessageReader:
|
||||
|
||||
res = {}
|
||||
res["config"] = dbuf.read(1)[0]
|
||||
await self.dispatcher.dispatch(Event(EventType.AUTOADD_CONFIG, res, res))
|
||||
# `max_hops` trailing byte added in companion-v1.14.0
|
||||
# (firmware commit 00566741). Pre-v1.14.0 firmware emits a
|
||||
# 1-byte response; read defensively to remain compatible.
|
||||
if len(data) >= 3:
|
||||
res["max_hops"] = dbuf.read(1)[0]
|
||||
await self.dispatcher.dispatch(Event(EventType.AUTOADD_CONFIG, res, res))
|
||||
|
||||
elif packet_type_value == PacketType.CHANNEL_INFO.value:
|
||||
logger.debug(f"received channel info response: {data.hex()}")
|
||||
@@ -568,6 +578,12 @@ class MessageReader:
|
||||
|
||||
if len(data) >= 5:
|
||||
ack_data["code"] = dbuf.read(4).hex()
|
||||
# `trip_time` (round-trip latency in ms) has been on the wire
|
||||
# since companion-v1.0.0a (firmware commit d9dc76f1, Jan 2025).
|
||||
# Read defensively so legacy frames without this field still
|
||||
# dispatch correctly.
|
||||
if len(data) >= 9:
|
||||
ack_data["trip_time"] = int.from_bytes(dbuf.read(4), "little")
|
||||
|
||||
attributes = {"code": ack_data.get("code", "")}
|
||||
|
||||
@@ -596,8 +612,19 @@ class MessageReader:
|
||||
res["is_admin"] = (perms & 1) == 1 # Check if admin bit is set
|
||||
if len(data) > 7:
|
||||
res["pubkey_prefix"] = dbuf.read(6).hex()
|
||||
|
||||
|
||||
attributes = {"pubkey_prefix": res.get("pubkey_prefix")}
|
||||
# The following trailing fields are emitted only by the new-
|
||||
# style RESP_SERVER_LOGIN_OK path. server_timestamp landed in
|
||||
# firmware commit 0e90b731 (companion-v1.10.0); acl_permissions
|
||||
# in 7947e8a2; fw_ver_level in 418ae08b (also companion-v1.10.0).
|
||||
# Per-field length gates handle every firmware-version tier.
|
||||
if len(data) >= 12:
|
||||
res["server_timestamp"] = int.from_bytes(dbuf.read(4), "little")
|
||||
if len(data) >= 13:
|
||||
res["acl_permissions"] = dbuf.read(1)[0]
|
||||
if len(data) >= 14:
|
||||
res["fw_ver_level"] = dbuf.read(1)[0]
|
||||
|
||||
await self.dispatcher.dispatch(
|
||||
Event(EventType.LOGIN_SUCCESS, res, attributes)
|
||||
|
||||
Reference in New Issue
Block a user