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,
+ )
# ---------------------------------------------------------------------------