Skip to content

fix: decode trailing wire-format fields the reader drops (AUTOADD_CONFIG, LOGIN_SUCCESS, ACK, DEFAULT_FLOOD_SCOPE)#87

Merged
fdlamotte merged 1 commit into
meshcore-dev:mainfrom
mwolter805:fix/wire-format-parity-bundle
Jun 15, 2026
Merged

fix: decode trailing wire-format fields the reader drops (AUTOADD_CONFIG, LOGIN_SUCCESS, ACK, DEFAULT_FLOOD_SCOPE)#87
fdlamotte merged 1 commit into
meshcore-dev:mainfrom
mwolter805:fix/wire-format-parity-bundle

Conversation

@mwolter805

@mwolter805 mwolter805 commented May 30, 2026

Copy link
Copy Markdown
Contributor

Background

This started while wiring the AUTOADD_CONFIG response into a Home Assistant MeshCore integration built on this SDK — the auto-add config dialog was missing max_hops because the reader stopped before that trailing byte. Rather than patch the one field downstream, I diffed every frame the companion-radio firmware emits against what reader.py actually reads, which surfaced several more frames with the same class of gap.

Rebased onto current main

Rebased after #86 and #88 merged — thanks for those. Two consequences:

Summary

Four wire-format parity fixes in reader.py for trailing fields the companion-radio firmware emits but the SDK decoder drops, plus an over-read guard. All use the existing BATT_AND_STORAGE defensive-read pattern (up-front minimum-length check + per-field cumulative-length gates keyed on len(data) >= N), so older firmware that doesn't emit the trailing fields keeps decoding without raising. Each ships a legacy-frame + modern-frame test pair, making the firmware-version dependency explicit.

Fixes

AUTOADD_CONFIGmax_hops trailing byte. The firmware grew this response from 1 byte to 2 (config + max_hops) in companion-v1.14.0+ (00566741). The reader read only config. Adds a length-gated 1-byte max_hops read; the legacy 1-byte frame decodes to just config with no exception.

LOGIN_SUCCESS — three trailing fields. The new-style RESP_SERVER_LOGIN_OK payload carries server_timestamp (4B, 0e90b731), acl_permissions (1B, 7947e8a2), and fw_ver_level (1B, 418ae08b) — all first shipped in companion-v1.10.0 — but the reader stopped after pubkey_prefix. Adds per-field gated reads. The legacy 8-byte "OK" frame is unaffected.

ACKtrip_time trailing field. The firmware emits PUSH_CODE_SEND_CONFIRMED as 9 bytes (ack_hash 4B + trip_time 4B, the round-trip latency in ms) since d9dc76f1 (companion-v1.0.0a, ~16 months on the wire), but the reader read only ack_hash. Adds a length-gated 4-byte read. The key is trip_time, matching the firmware variable name; happy to rename to trip_time_ms if you prefer the unit suffix.

DEFAULT_FLOOD_SCOPE — over-read guard. On the 1-byte sentinel frame the handler read 47 bytes past the end. Gates the read on len(data) >= 48; the sentinel frame now dispatches an empty payload. A consumer treating payload["scope_name"] == "" as a sentinel should switch to a key-presence check ("scope_name" in payload).

Tests

tests/unit/test_protocol_surface_gaps.py — 10 tests, a legacy-frame + modern-frame pair per fix (and the RAW_DATA regression test covering #86's fix). All pass on the rebased base.

@fdlamotte

Copy link
Copy Markdown
Collaborator

Hi @mwolter805
Just merged another PR and it induced a conflict with yours ... could you please update it ?
Thanks

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 meshcore-dev#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.
@mwolter805 mwolter805 force-pushed the fix/wire-format-parity-bundle branch from 8052908 to 46288e4 Compare June 15, 2026 15:58
@mwolter805 mwolter805 changed the title fix: decode wire-format fields the firmware emits but the reader drops (AUTOADD_CONFIG, LOGIN_SUCCESS, ACK, RAW_DATA, DEFAULT_FLOOD_SCOPE) fix: decode trailing wire-format fields the reader drops (AUTOADD_CONFIG, LOGIN_SUCCESS, ACK, DEFAULT_FLOOD_SCOPE) Jun 15, 2026
@mwolter805

Copy link
Copy Markdown
Contributor Author

Rebased onto current main and pushed — conflict resolved. 👍

The conflict was entirely from #86: your 44b21be ("fix raw_data issues") landed the same RAW_DATA payload-framing fix this PR carried, with identical decode logic. I dropped my redundant code change and kept only the regression test for it (test_raw_data_realistic_frame), since #86 shipped without one. The CHANNEL_DATA_RECV test-file overlap from #88 is resolved with both test sets retained. So this PR is now four trailing-field fixes (AUTOADD_CONFIG / LOGIN_SUCCESS / ACK / DEFAULT_FLOOD_SCOPE) plus tests — title and description updated to match.

One unrelated heads-up while I was here: 44b21be also added a payload must be at least 4 bytes guard to send_raw_data, but the existing test_send_raw_data_wrapper still sends a 2-byte payload, so it now fails on main (independent of this PR). I'm happy to send a one-liner — either relax the guard or bump the test's payload to ≥4 bytes — whichever matches your intent for send_raw_data.

@fdlamotte

Copy link
Copy Markdown
Collaborator

Thanks, I'll merge

For the payload size, I just checked on the firmware code and it drops payloads < 4 bytes.
So I would rather change the test to send 4 bytes

Thanks for pointing this and go ahead for the fix ;)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants