Skip to content

feat: add CHANNEL_DATA_RECV (RESP_CODE 27) packet type and handler#88

Open
mwolter805 wants to merge 1 commit into
meshcore-dev:mainfrom
mwolter805:feature/channel-data-recv
Open

feat: add CHANNEL_DATA_RECV (RESP_CODE 27) packet type and handler#88
mwolter805 wants to merge 1 commit into
meshcore-dev:mainfrom
mwolter805:feature/channel-data-recv

Conversation

@mwolter805
Copy link
Copy Markdown

Summary

Adds SDK support for the CHANNEL_DATA_RECV push frame (RESP_CODE_CHANNEL_DATA_RECV = 27), which the companion-radio firmware emits for group-channel binary data (PAYLOAD_TYPE_GRP_DATA — e.g. LPP-encoded telemetry or other binary blobs delivered over a shared channel). Before this change the SDK had no PacketType value for 27 (the enum jumped from ALLOWED_REPEAT_FREQ = 26 straight to DEFAULT_FLOOD_SCOPE = 28), no EventType, and no reader handler, so these frames hit the unknown-packet-type fallthrough in handle_rx and the payload was silently dropped.

What changed

  • packets.py: add PacketType.CHANNEL_DATA_RECV = 27 in the previously-skipped enum slot.
  • events.py: add EventType.CHANNEL_DATA_RECV = "channel_data".
  • reader.py: add a handler that decodes the fixed 9-byte header (including the code byte) plus the variable payload, then dispatches an EventType.CHANNEL_DATA_RECV event.
  • tests/unit/test_protocol_surface_gaps.py: 5 new tests.

Wire format

The firmware emits the frame from MyMesh::onChannelDataRecv():

Offset Width Field
0 1B RESP_CODE_CHANNEL_DATA_RECV (27)
1 1B SNR × 4 (signed int8)
2–3 2B reserved (0)
4 1B channel_idx
5 1B path_len (route-flood) or 0xFF (direct)
6–7 2B data_type (uint16, little-endian)
8 1B data_len
9+ N B payload (data_len bytes)

The first six post-code bytes (SNR, reserved, channel_idx, path_len with its sentinel/hash-mode encoding) are byte-for-byte identical in shape to CHANNEL_MSG_RECV_V3, so the decode of that prefix reuses the existing convention directly. The only genuinely new decode is the typed data_type / data_len / payload tail.

Firmware history

The feature shipped across three firmware commits, all first contained in release companion-v1.15.0:

  • 9b842786 (2026-03-05) — feat: Add support for PAYLOAD_TYPE_GRP_DATA — initial feature.
  • f25d7a88fix: Align channel data framing — framing alignment.
  • 2f687691 (2026-03-19) — fix: Widen grp data type — widened data_type from uint8_t to uint16_t, which is why data_type is decoded as a 2-byte little-endian field (offsets 6–7) rather than a single byte.

Design choices

These default to the SDK's existing conventions rather than introducing new patterns; happy to adjust any of them on review:

  • data_type exposed as an int — mirrors the txt_type convention in CHANNEL_MSG_RECV_V3. The raw integer preserves the on-wire value for any consumer that wants to dispatch on it directly; a parsed enum could be layered on later without a breaking change.
  • data_len exposed as an int — lets consumers validate payload integrity independent of len(payload) // 2.
  • payload exposed as a hex string — mirrors RAW_DATA's convention for binary data of unknown encoding. It is read as exactly data_len bytes (the firmware treats data_len as authoritative) rather than "read remainder", so any trailer bytes are not folded into the payload.
  • attributes surfaces channel_idx and data_type — mirrors how CHANNEL_MSG_RECV_V3 surfaces channel_idx and txt_type, so subscribers can filter without unpacking the payload.
  • Up-front length gate (len(data) < 9 → debug-log and return) — matches the defensive-read pattern used by the other handlers; protects against truncated frames.

Tests

tests/unit/test_protocol_surface_gaps.py adds:

  1. test_channel_data_recv_enum_existsPacketType.CHANNEL_DATA_RECV == 27.
  2. test_channel_data_recv_direct_path_frame — realistic direct-path frame (path_len = 0xFF, data_type = 0x0123, 4-byte payload); asserts every decoded field including payload == "deadbeef".
  3. test_channel_data_recv_route_flood_path_len_bitspath_len = 0x42 splits into hash_mode = 1, length = 2.
  4. test_channel_data_recv_under_minimum_frame_ignored — 8-byte frame returns early, no dispatch.
  5. test_channel_data_recv_widened_data_typedata_type = 0x0201 confirms the high byte survives the 2-byte read; empty-payload tail.

Full unit suite passes (no regressions).

Notes for the maintainer

  • An SDK convenience method (e.g. subscribe_channel_data(channel_idx, callback)) was deliberately left out — consumers can use the event dispatcher directly. Happy to add it if you'd prefer the helper.
  • The field names and types above follow existing SDK conventions, but this is the first new public packet type on the channel-receive surface, so naming/shape feedback is welcome — see the design-choices section for the rationale behind each.
  • Submitted alongside sibling PR fix: decode wire-format fields the firmware emits but the reader drops (AUTOADD_CONFIG, LOGIN_SUCCESS, ACK, RAW_DATA, DEFAULT_FLOOD_SCOPE) #87 (wire-format parity fixes). Both branch off v2.3.7 and both touch reader.py, so whichever merges second will need a trivial rebase.

Why: companion-radio firmware emits RESP_CODE_CHANNEL_DATA_RECV (27) for
group-channel binary data (PAYLOAD_TYPE_GRP_DATA), shipped in
companion-v1.15.0. The SDK had no PacketType value 27, no EventType, and
no reader handler, so these frames hit the unknown-packet-type
fallthrough and the payload was silently dropped.

This adds PacketType.CHANNEL_DATA_RECV = 27 (the previously-skipped enum
slot), EventType.CHANNEL_DATA_RECV, and a reader handler. The fixed
9-byte header (snr, reserved, channel_idx, path_len with the
sentinel/hash-mode encoding) reuses CHANNEL_MSG_RECV_V3's framing; the
typed tail decodes data_type (uint16 little-endian, widened from uint8
in firmware), data_len, and payload (hex string). An up-front length
gate mirrors the defensive-read pattern used by the other handlers.

data_type is exposed as an int (matching the txt_type convention) and
payload as a hex string (matching RAW_DATA's convention for binary data
of unknown encoding); attributes surface channel_idx and data_type so
subscribers can filter without unpacking the payload.

Tests: tests/unit/test_protocol_surface_gaps.py — 5 new tests (enum
slot present, direct-path frame, route-flood path_len bit-split,
under-minimum frame dropped, widened data_type round-trip). Full unit
suite: 146 passed.
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.

1 participant