Add multibyte trace output. Closes #127.

This commit is contained in:
Jack Kingsman
2026-03-30 12:52:01 -07:00
parent db248302e9
commit d4bbb8a542
4 changed files with 34 additions and 10 deletions

View File

@@ -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}")

View File

@@ -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}

View File

@@ -81,13 +81,14 @@ export function PathModal({
) : hasSinglePath ? (
<>
This shows <em>one route</em> 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 <strong>{paths.length} different routes</strong>.
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.
</>
)}
</DialogDescription>

View File

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