From 744e021fb055e531a863a99164e2bd2f29bc6343 Mon Sep 17 00:00:00 2001 From: Jean-Pierre Martineau Date: Wed, 12 Aug 2026 22:56:29 -0400 Subject: [PATCH] Bridge: push TRACE_DATA for over-the-air trace replies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pymc_core never forwards a received trace to the companion client: its frame server registers every push callback except on_trace_received, and nothing in the library ever fires the trace_received event, so the whole path is dead code — the only push_trace_data call is the self-hop shortcut in _cmd_send_trace_path. A trace sent from MeshTerm therefore returns RESP_CODE_SENT and then times out, because the node parses the reply and drops it. Shim it at the dispatcher's packet-received callback (where CompanionRadio already watches every RX packet with its measured SNR): wrap it, and for a TRACE payload re-slice the on-air bytes into the hash/SNR blocks push_trace_data wants, emitting the standard 0x89 frame the meshcore client decodes. Verified byte-exact end to end. Handles both the SNR-per-hop layout (final SNR from our reception) and firmware that trails the final SNR in the payload. Upstream fix (no local pymc_core checkout to carry it): mirror every other push — fire trace_received from _on_packet_received and register on_trace_received in frame_server._setup_push_callbacks. Co-Authored-By: Claude Opus 4.8 --- scripts/meshterm-spi-bridge | 91 +++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/scripts/meshterm-spi-bridge b/scripts/meshterm-spi-bridge index ff32e18..9f6e901 100755 --- a/scripts/meshterm-spi-bridge +++ b/scripts/meshterm-spi-bridge @@ -661,6 +661,96 @@ def _wire_contact_persistence(companion, log) -> None: log.info("contact persistence at %s", path) +def _wire_trace_push(companion, server, log) -> None: + """Forward over-the-air trace replies to the client as PUSH_CODE_TRACE_DATA. + + pymc_core parses received TRACE packets but never hands them to the + companion client: its frame server registers every push callback *except* + ``on_trace_received``, and nothing in the library ever fires the + ``trace_received`` event — so the whole path is dead code. The only + ``push_trace_data`` call is the self-hop shortcut in ``_cmd_send_trace_path``. + The upshot is that MeshTerm's ``run_trace`` sends the trace, gets + ``RESP_CODE_SENT``, then waits on ``TRACE_DATA`` and times out while the node + quietly drops the reply. + + Bridge it at the one seam that sees every received packet with its measured + SNR — the dispatcher's packet-received callback (``CompanionRadio`` already + installs ``_on_packet_received`` there and only acts on group data). Wrap it: + run the original, then, for a TRACE payload, re-slice the on-air bytes into + the hash/SNR blocks ``push_trace_data`` wants and push the standard 0x89 + frame — exactly what the ``meshcore`` client on MeshTerm's side decodes. + + Upstream, the clean fix is the mirror of every other push: have + ``_on_packet_received`` fire ``trace_received`` for a TRACE packet and have + ``frame_server._setup_push_callbacks`` register ``on_trace_received`` → + ``push_trace_data``. Until a pymc_core release carries that, this shim stands + (same spirit as :func:`_patch_pymc_ack_compat`). + """ + import struct + + try: + from pymc_core.protocol.constants import PAYLOAD_TYPE_TRACE + except Exception as exc: # package layout changed — leave it alone + log.warning("trace push not wired (%s)", exc) + return + + dispatcher = getattr(getattr(companion, "node", None), "dispatcher", None) + if dispatcher is None or not hasattr(dispatcher, "set_packet_received_callback"): + log.warning("trace push not wired (no dispatcher)") + return + + prev = getattr(companion, "_on_packet_received", None) + + def push_reply(pkt) -> None: + payload = bytes(pkt.payload or b"") + if len(payload) < 9: + log.debug("trace reply too short (%d bytes)", len(payload)) + return + tag, auth_code, flags = struct.unpack_from(" None: + if prev is not None: + await prev(pkt) + try: + if pkt.get_payload_type() != PAYLOAD_TYPE_TRACE: + return + except Exception: + return + try: + push_reply(pkt) + except Exception as exc: + log.warning("trace push failed: %s", exc) + + dispatcher.set_packet_received_callback(on_packet) + log.info("compat: TRACE replies now push PUSH_CODE_TRACE_DATA to the client") + + def run_bridge(host: str, port: int, node_name: str) -> int: import asyncio import logging @@ -781,6 +871,7 @@ def run_bridge(host: str, port: int, node_name: str) -> int: device_model="pyMC-spi-bridge", ) await server.start() + _wire_trace_push(companion, server, log) log.info("READY — connect with: meshterm --tcp %s:%d", host, port) import signal