Commit Graph

382 Commits

Author SHA1 Message Date
fdlamotte
80c3afe633 Merge pull request #90 from mwolter805/fix/raw-data-test-payload
test: fix send_raw_data_wrapper for the 4-byte guard and path_len framing
2026-06-16 12:31:11 -04:00
Matthew Wolter
1b1d66053c test: fix send_raw_data wrapper for 4-byte guard and path_len framing
Why: PR #86 (44b21be) reworked send_raw_data to frame as
0x19 | path_len(1) | path | payload and to reject payloads under 4
bytes, but left test_send_raw_data_wrapper unchanged. The test sent a
2-byte payload (now raising ValueError before any assertion) and
asserted captured_data[1:] == payload, which no longer holds because of
the inserted path_len byte. The firmware drops payloads under 4 bytes,
so the guard is correct; this fixes the test, not the guard.

Bumps the payload to 4 bytes and asserts the zero path_len byte plus the
payload at offset 2, validating the wire format #86 introduced.

Tests: tests/unit/test_protocol_surface_gaps.py -- full suite 156 passed
(test_send_raw_data_wrapper was the lone failure on current main).
2026-06-15 16:55:41 -07:00
fdlamotte
c208839a06 Merge pull request #87 from mwolter805/fix/wire-format-parity-bundle
fix: decode trailing wire-format fields the reader drops (AUTOADD_CONFIG, LOGIN_SUCCESS, ACK, DEFAULT_FLOOD_SCOPE)
2026-06-15 14:32:33 -04:00
Matthew Wolter
46288e4bef 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.
2026-06-15 08:56:39 -07:00
fdlamotte
28ee333352 Merge pull request #88 from mwolter805/feature/channel-data-recv
feat: add CHANNEL_DATA_RECV (RESP_CODE 27) packet type and handler
2026-06-14 10:47:25 -04:00
fdlamotte
febd03e00b Merge pull request #86 from JohannesFriedrich/main
Sending and receiving raw_data not working as expected
2026-06-14 10:45:59 -04:00
Matthew Wolter
3ed4ec3eab feat: add CHANNEL_DATA_RECV packet type and handler
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.
2026-05-28 11:28:05 -07:00
JohannesFriedrich
44b21be20e fix raw_data issues 2026-05-19 18:56:22 +02:00
Florent
4d0be8788a v2.3.7 2026-04-25 17:10:29 +02:00
Florent
0e453f7c5d implementing default_flood_scope 2026-04-25 17:10:00 +02:00
Florent
f538062546 small indent issue 2026-04-25 15:48:45 +02:00
fdlamotte
2e178e83e7 Merge pull request #82 from jkingsman/add-repeater-error-count
Add repeater error count delivery in telemetry
2026-04-25 15:23:03 +02:00
fdlamotte
ff58e1c30d Merge pull request #79 from mwolter805/fix/standalone-bugs-and-cleanup
fix: remove broken req_mma, bump DEFAULT_TIMEOUT, guard TypeError, pre-register binary requests
2026-04-25 15:21:33 +02:00
fdlamotte
fda191d0a4 Merge branch 'main' into fix/standalone-bugs-and-cleanup 2026-04-25 15:21:16 +02:00
fdlamotte
5032f810c1 Merge pull request #74 from mwolter805/fix/reader-parser-crash-safety
fix: add umbrella crash protection and length guards to reader/parser dispatch
2026-04-25 15:18:05 +02:00
fdlamotte
b040656892 Merge branch 'main' into fix/reader-parser-crash-safety 2026-04-25 15:17:48 +02:00
fdlamotte
173bba5a82 Merge pull request #80 from mwolter805/fix/protocol-surface-gaps
feat: add missing protocol handlers (CONTACT_DELETED, CONTACTS_FULL, TUNING_PARAMS) and command wrappers
2026-04-25 15:07:43 +02:00
fdlamotte
5728463f21 Merge pull request #77 from mwolter805/fix/transport-symmetry
fix: symmetric disconnect signaling, serial timeout, BLE callback, oversize-frame recovery
2026-04-25 15:05:32 +02:00
fdlamotte
2aff27e725 Merge pull request #76 from mwolter805/fix/error-response-handling
fix: add EventType.ERROR to command expected_events and guard error payloads
2026-04-25 15:04:30 +02:00
fdlamotte
1c08569f07 Merge pull request #75 from mwolter805/fix/reconnect-path
fix: resolve reconnect storm — TCP Future return, missing appstart, task overwrite race
2026-04-25 15:02:30 +02:00
fdlamotte
df6cec1d0b Merge pull request #78 from mwolter805/fix/asyncio-lifecycle
fix: track background tasks, defer Queue/Lock construction, use get_running_loop
2026-04-25 15:00:44 +02:00
fdlamotte
ba6dcd459e Merge pull request #73 from mwolter805/fix/test-timeout-waste
test: resolve mock_dispatcher futures to drop suite runtime from ~8 min to <1s
2026-04-25 14:53:27 +02:00
Jack Kingsman
5cf8150146 Add repeater error count delivery in telemetry 2026-04-22 17:31:18 -07:00
Florent
ac7b035b9e solve issue when login fails 2026-04-21 08:03:33 +02:00
Matthew Wolter
af886466d5 Remove finding IDs from test_standalone_fixes.py
Strip finding IDs (F13, F09, M03, M05, M07, R03, R05) from module
docstring, section comments, function names, and docstrings.
2026-04-12 07:58:44 -07:00
Matthew Wolter
aa7f584ca0 Remove internal references from protocol surface gaps tests
Rename test_g6_protocol_surface_gaps.py to test_protocol_surface_gaps.py.
Strip G6 from module docstring, and finding IDs (N01, N02, N03, N05,
N09, R04) from docstrings and section comments.
2026-04-12 07:58:07 -07:00
Matthew Wolter
4fddbffa3d Remove internal references from asyncio lifecycle tests
Rename test_g5_asyncio_lifecycle.py to test_asyncio_lifecycle.py.
Strip G5 from module docstring, finding IDs (F05, F07, F08, F19)
from class names and docstrings.
2026-04-12 07:57:23 -07:00
Matthew Wolter
b4b40718e9 Remove internal references from transport symmetry tests
Rename test_g4_transport_symmetry.py to test_transport_symmetry.py.
Strip G4 from module docstring, _g4_ from function names, and finding
IDs (F04, NEW-A, F18, M06, N04) from docstrings and section comments.
2026-04-12 07:56:48 -07:00
Matthew Wolter
578ac36ccd Remove internal references from error handling tests
Rename test_g2_error_handling.py to test_error_handling.py. Strip G2
prefix from module docstring, _g2_ from function names, and finding
IDs (F22, F21/M01, M02, M04, N06, F14) from docstrings and section
comments. Proposal cross-references removed.
2026-04-12 07:56:06 -07:00
Matthew Wolter
5a4960b268 Remove finding IDs from test_reader.py
Strip internal forensics finding references (F06, N07, NEW-C, R02)
from docstrings, comments, and assertion messages. Descriptive text
is preserved — only the ID prefixes are removed.
2026-04-12 07:55:03 -07:00
Matthew Wolter
f3aa131019 Remove finding IDs from test_connection_manager.py
Strip internal forensics finding references (F01, F02, F03, N11)
from docstrings and section comments. The descriptive text is
preserved — only the ID prefixes are removed.
2026-04-12 07:54:02 -07:00
Matthew Wolter
9e2fc0d63e Remove internal G-numbering from test_connection_manager.py
Strip G3 from module docstring and _g3_ from function names.
Finding IDs (F01, F02, F03, N11) are preserved.
2026-04-12 07:53:10 -07:00
Matthew Wolter
66b4a532a1 Remove internal G-numbering from test_reader.py
Strip G1/ prefix from docstrings, _g1_ from function names,
and G1 from section comments. Finding IDs (F06, N07, etc.)
are preserved as they are meaningful standalone references.
2026-04-12 07:52:43 -07:00
Matthew Wolter
83cf65ec3c Rename test_g7_standalone_bugs_and_cleanup.py to test_standalone_fixes.py
Remove internal G-numbering from test filename and docstring
before upstream submission.
2026-04-12 07:44:33 -07:00
Matthew Wolter
74e349be97 Fix test_r03 resolving mock to include expected_ack payload
send_binary_req reads result.payload['expected_ack'] from MSG_SENT
events. The resolving subscribe must provide that key for MSG_SENT
event types to avoid KeyError.
2026-04-12 07:00:10 -07:00
Matthew Wolter
4c1e5f4fe2 Fix test_r03 mock to resolve events immediately
The test_r03_placeholder_registered_before_send test used a bare
MagicMock dispatcher whose subscribe never resolved event futures,
causing send() to block for DEFAULT_TIMEOUT (15s). Add a resolving
subscribe mock matching the pattern from the fixture fix on
fix/test-timeout-waste.
2026-04-12 06:58:30 -07:00
Matthew Wolter
75c4a58841 Fix test fixture to resolve events immediately instead of blocking
The mock_dispatcher fixture's fake_subscribe recorded event handlers
but never called them, causing asyncio.wait() to block for the full
DEFAULT_TIMEOUT (15s) on every test that passes expected_events to
send(). With 28 affected tests, the suite wasted ~8 minutes on dead
waits and required an undocumented pytest-timeout plugin to complete.

Add call_soon to the default fake_subscribe so futures resolve on the
next event loop iteration, matching the pattern already used by
setup_event_response(). Override with a non-resolving mock in
test_send_timeout to preserve timeout path coverage.

Suite now completes in <1 second with no --timeout flag.
2026-04-12 06:57:53 -07:00
Matthew Wolter
1a017709c5 G7: add verification tests for F13, F09, M03, M05, M07, R03, R05
Why: 15 tests covering all G7 findings — F13 req_mma removal confirmed,
F09 DEFAULT_TIMEOUT value and inheritance, M03 TypeError for bad scope types
plus regression checks for None/str/bytes, M05 dead shift removal via source
inspection, M07 timeout returns Error Event not None, R03 placeholder
registration before send, R05 annotation parity with EventDispatcher.

Refs: Forensics report findings F13, F09, M03, M05, M07, R03, R05
2026-04-12 04:52:38 -07:00
Matthew Wolter
eb2598400a G7: R05 — widen MeshCore.subscribe callback annotation to match dispatcher
Why: MeshCore.subscribe typed callbacks as Callable[[Event], Coroutine[...]]
(async only), while EventDispatcher.subscribe typed them as
Callable[[Event], Union[None, asyncio.Future]] (sync or async). Type-checkers
flag any sync handler passed through MeshCore.subscribe. Fix: align the
annotation to match EventDispatcher's union type; remove unused Coroutine import.

Refs: Forensics report finding R05
2026-04-12 04:52:28 -07:00
Matthew Wolter
168e613ed7 G7: R03 — pre-register binary request before send() to close race window
Why: send_binary_req() registered the pending request with the reader AFTER
send() returned. If a BINARY_RESPONSE arrives between send() returning and
registration (reachable for TCP-companion proxies), the reader logs "No tracked
request found" and the caller's wait_for_event times out. Fix: pre-register a
placeholder keyed by object id before send(), then swap it for the real tag
from MSG_SENT. On send() failure, the placeholder is cleaned up.

Refs: Forensics report finding R03
2026-04-12 04:52:19 -07:00
Matthew Wolter
aed7db21b3 G7: M03+M05+M07 — cleanup: TypeError guard, dead code removal, None normalization
Why: Three minor cleanup fixes. M03 adds an else branch in set_flood_scope so
unsupported scope types raise TypeError instead of UnboundLocalError. M05 removes
the dead `out_path_len >> 6` shift in update_contact (high bits always zero due
to reader masking) and initializes path_hash_mode=0 explicitly. M07 normalizes
three `return None` paths in get_contacts to return Event(EventType.ERROR, ...)
so callers can rely on the return type always being Event.

Refs: Forensics report findings M03, M05, M07
2026-04-12 04:52:07 -07:00
Matthew Wolter
4204bf090c G7: F09 — bump DEFAULT_TIMEOUT from 5s to 15s
Why: 5 seconds is too short for slow-path mesh operations (path-resolving
messaging, long binary responses, remote auth). Also the root cause of tests
that appeared to "hang" — they were falling through to the 5s timeout because
their mock dispatchers don't wire matching responses. Landed as a separate
commit so reviewers can drop it independently if they push back.

Refs: Forensics report finding F09
2026-04-12 04:51:54 -07:00
Matthew Wolter
0709b9f650 G7: F13 — remove broken deprecated req_mma method
Why: req_mma() references undefined variables `start` and `end`, causing a
NameError on every call. The logger.error migration warning confirms the method
is intentionally deprecated in favor of req_mma_sync. Since it is broken as
shipped, removing it cannot break any working caller.

Refs: Forensics report finding F13
2026-04-12 04:51:06 -07:00
Matthew Wolter
baa5494758 G6: add verification tests for N01, N02, N03, N05, N09, R04
Why: 16 tests covering all G6 findings — reader dispatch for
CONTACT_DELETED/CONTACTS_FULL/TUNING_PARAMS (including short-frame
guards), send_trace() padding behavior, all 5 new command wrappers
(send_raw_data, has_connection, get_tuning, get_contact_by_key,
factory_reset two-step), and GET_STATS enum existence + usage.

Refs: Forensics report findings N01, N02, N03, N05, N09, R04
2026-04-12 04:14:48 -07:00
Matthew Wolter
6f1b48cc50 G6: N09 — add command wrappers for orphaned CommandType entries
Why: Five CommandType enum entries had no user-facing SDK method:
SEND_RAW_DATA (25), HAS_CONNECTION (28), GET_CONTACT_BY_KEY (30),
GET_TUNING_PARAMS (43), FACTORY_RESET (51). Added send_raw_data() in
MessagingCommands, has_connection()/get_tuning()/request_factory_reset()/
confirm_factory_reset() in DeviceCommands, get_contact_by_key() in
ContactCommands. FACTORY_RESET uses a two-step token pattern as a
Python-side safety measure against accidental invocation.

Refs: Forensics report finding N09 (also N03 for get_tuning)
2026-04-12 04:14:31 -07:00
Matthew Wolter
1f319159b6 G6: N05 — pad send_trace() to 11 bytes when path is empty
Why: Firmware requires strict len > 10 (MyMesh.cpp:1620). When path is
empty, send_trace() builds exactly 10 bytes (cmd+tag+auth+flags), which
is silently rejected. Appending one zero byte when the packet is <= 10
bytes satisfies the firmware gate without changing the semantic content.

Refs: Forensics report finding N05
2026-04-12 04:14:20 -07:00
Matthew Wolter
8400995600 G6: N01+N02+N03 — add reader branches for CONTACT_DELETED, CONTACTS_FULL, TUNING_PARAMS
Why: Three firmware push/response codes had no SDK handler — frames fell
through to the "Unhandled" debug log. CONTACT_DELETED (0x8F) carries a
32-byte pubkey from onContactOverwrite(); CONTACTS_FULL (0x90) is a
1-byte push from onContactsFull(); TUNING_PARAMS (23) is the 9-byte
response to CMD_GET_TUNING_PARAMS carrying rx_delay and airtime_factor.
All three now dispatch typed events. Short frames are guarded.

Refs: Forensics report findings N01, N02, N03
2026-04-12 04:14:11 -07:00
Matthew Wolter
df388e494e G6: N02+R04 — add CONTACTS_FULL and GET_STATS enum entries
Why: PacketType was missing CONTACTS_FULL (0x90), emitted by
MyMesh::onContactsFull(). CommandType was missing GET_STATS (56),
used by get_stats_core/radio/packets but referenced only as literal
b"\\x38". Adding both enum entries prepares for handler and wrapper
implementations in subsequent commits.

Refs: Forensics report findings N02, R04
2026-04-12 04:14:01 -07:00
Matthew Wolter
7b459aa6a5 G5: add verification tests for F05, F07, F08, F19
10 new tests in tests/unit/test_g5_asyncio_lifecycle.py:

- TestF05: _spawn_background retains tasks in TCP, Serial, and
  EventDispatcher; tracked tasks survive gc.collect(); TCP handle_rx
  and connection_lost use tracked dispatch.
- TestF07: stop() waits for in-flight async callbacks to complete.
- TestF08: EventDispatcher.queue is None before start(), created on
  start(), dispatch() before start() raises RuntimeError;
  CommandHandlerBase lock is None before access, created lazily.
- TestF19: send() calls get_running_loop (not get_event_loop).

Refs: Forensics report findings F05, F07, F08, F19
2026-04-12 03:57:35 -07:00
Matthew Wolter
1b404221a2 G5: F19 — replace deprecated get_event_loop with get_running_loop
Why: asyncio.get_event_loop() inside an async function emits
DeprecationWarning since Python 3.10 and raises in some contexts on
Python 3.12+. The call in CommandHandlerBase.send() is always inside
a running async context where get_running_loop() is the correct API.

Refs: Forensics report finding F19
2026-04-12 03:57:20 -07:00