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