diff --git a/app/routers/contacts.py b/app/routers/contacts.py index a4289df..3e628b8 100644 --- a/app/routers/contacts.py +++ b/app/routers/contacts.py @@ -40,6 +40,10 @@ logger = logging.getLogger(__name__) router = APIRouter(prefix="/contacts", tags=["contacts"]) +TRACE_HASH_BYTES = 4 +TRACE_FLAGS_4BYTE = 2 + + def _ambiguous_contact_detail(err: AmbiguousPublicKeyPrefixError) -> str: sample = ", ".join(key[:12] for key in err.matches[:2]) return ( @@ -373,17 +377,17 @@ async def delete_contact(public_key: str) -> dict: async def request_trace(public_key: str) -> TraceResponse: """Send a single-hop trace to a contact and wait for the result. - The trace path contains the contact's 1-byte pubkey hash as the sole hop - (no intermediate repeaters). The radio firmware requires at least one - node in the path. + The trace path contains the contact's 4-byte pubkey hash as the sole hop + (no intermediate repeaters). This uses TRACE's dedicated width flags rather + than the radio's normal path_hash_mode setting. """ require_connected() contact = await _resolve_contact_or_404(public_key) tag = random.randint(1, 0xFFFFFFFF) - # First 2 hex chars of pubkey = 1-byte hash used by the trace protocol - contact_hash = contact.public_key[:2] + # Use a 4-byte contact hash for low-collision direct trace targeting. + contact_hash = contact.public_key[: TRACE_HASH_BYTES * 2] # Trace does not need auto-fetch suspension: response arrives as TRACE_DATA # from the reader loop, not via get_msg(). @@ -394,7 +398,11 @@ async def request_trace(public_key: str) -> TraceResponse: logger.info( "Sending trace to %s (tag=%d, hash=%s)", contact.public_key[:12], tag, contact_hash ) - result = await mc.commands.send_trace(path=contact_hash, tag=tag) + result = await mc.commands.send_trace( + path=contact_hash, + tag=tag, + flags=TRACE_FLAGS_4BYTE, + ) if result.type == EventType.ERROR: raise HTTPException(status_code=500, detail=f"Failed to send trace: {result.payload}") diff --git a/frontend/src/components/ChatHeader.tsx b/frontend/src/components/ChatHeader.tsx index d92c4b3..aa6fba9 100644 --- a/frontend/src/components/ChatHeader.tsx +++ b/frontend/src/components/ChatHeader.tsx @@ -268,7 +268,7 @@ export function ChatHeader({ title={ activeContactIsPrefixOnly ? 'Direct Trace unavailable until the full contact key is known' - : 'Direct Trace. Send a zero-hop packet to this contact and display out and back SNR' + : 'Direct Trace. Send a direct trace probe to this contact and display out and back SNR' } aria-label="Direct Trace" disabled={activeContactIsPrefixOnly} diff --git a/frontend/src/components/PathModal.tsx b/frontend/src/components/PathModal.tsx index aef5ba1..61d30d3 100644 --- a/frontend/src/components/PathModal.tsx +++ b/frontend/src/components/PathModal.tsx @@ -81,13 +81,14 @@ export function PathModal({ ) : hasSinglePath ? ( <> This shows one route that this message traveled through the mesh network. - Repeaters may be incorrectly identified due to prefix collisions between heard and - non-heard repeater advertisements. + Repeater identities are inferred from locally known advert and path data, so some + hops may be missing or misidentified when that data is incomplete. ) : ( <> This message was received via {paths.length} different routes. - Repeaters may be incorrectly identified due to prefix collisions. + Repeater identities are inferred from locally known advert and path data, so some + hops may be missing or misidentified when that data is incomplete. )} diff --git a/tests/test_repeater_routes.py b/tests/test_repeater_routes.py index 7f113fb..8d27f64 100644 --- a/tests/test_repeater_routes.py +++ b/tests/test_repeater_routes.py @@ -483,6 +483,11 @@ class TestTraceRoute: await request_trace(KEY_A) assert exc.value.status_code == 500 + mc.commands.send_trace.assert_awaited_once_with( + path=KEY_A[:8], + tag=1234, + flags=2, + ) @pytest.mark.asyncio async def test_wait_timeout_returns_504(self, test_db): @@ -500,6 +505,11 @@ class TestTraceRoute: await request_trace(KEY_A) assert exc.value.status_code == 504 + mc.commands.send_trace.assert_awaited_once_with( + path=KEY_A[:8], + tag=1234, + flags=2, + ) @pytest.mark.asyncio async def test_success_returns_remote_and_local_snr(self, test_db): @@ -520,6 +530,11 @@ class TestTraceRoute: assert response.remote_snr == 5.5 assert response.local_snr == 3.2 assert response.path_len == 2 + mc.commands.send_trace.assert_awaited_once_with( + path=KEY_A[:8], + tag=1234, + flags=2, + ) # ---------------------------------------------------------------------------