Hopefully once and for all, use smarter pathing data on all direct contacts

This commit is contained in:
Jack Kingsman
2026-01-14 16:05:53 -08:00
parent ece9f8f2cf
commit e272be88ca
8 changed files with 59 additions and 29 deletions
+4 -1
View File
@@ -68,6 +68,7 @@ class PacketInfo:
payload_type: PayloadType
payload_version: int
path_length: int
path: bytes # The routing path (empty if path_length is 0)
payload: bytes
@@ -149,9 +150,10 @@ def parse_packet(raw_packet: bytes) -> PacketInfo | None:
path_length = raw_packet[offset]
offset += 1
# Skip path data
# Extract path data
if len(raw_packet) < offset + path_length:
return None
path = raw_packet[offset:offset + path_length]
offset += path_length
# Rest is payload
@@ -162,6 +164,7 @@ def parse_packet(raw_packet: bytes) -> PacketInfo | None:
payload_type=payload_type,
payload_version=payload_version,
path_length=path_length,
path=path,
payload=payload,
)
except (ValueError, IndexError):
+22 -8
View File
@@ -19,8 +19,8 @@ import time
from app.decoder import (
PayloadType,
parse_packet,
parse_advertisement,
try_decrypt_packet_with_channel_key,
try_parse_advertisement,
)
from app.models import CONTACT_TYPE_REPEATER, RawPacketBroadcast, RawPacketDecryptedInfo
from app.repository import ChannelRepository, ContactRepository, MessageRepository, RawPacketRepository
@@ -176,7 +176,7 @@ async def process_raw_packet(
result.update(decrypt_result)
elif payload_type == PayloadType.ADVERT:
await _process_advertisement(raw_bytes, ts)
await _process_advertisement(raw_bytes, ts, packet_info)
# TODO: Add TEXT_MESSAGE (direct message) decryption when private key is available
# elif payload_type == PayloadType.TEXT_MESSAGE:
@@ -288,6 +288,7 @@ async def _process_group_text(
async def _process_advertisement(
raw_bytes: bytes,
timestamp: int,
packet_info=None,
) -> None:
"""
Process an advertisement packet.
@@ -295,14 +296,25 @@ async def _process_advertisement(
Extracts contact info and updates the database/broadcasts to clients.
For non-repeater contacts, triggers sync of recent contacts to radio for DM ACK support.
"""
advert = try_parse_advertisement(raw_bytes)
if not advert:
# Parse packet to get path info if not already provided
if packet_info is None:
packet_info = parse_packet(raw_bytes)
if packet_info is None:
logger.debug("Failed to parse advertisement packet")
return
advert = parse_advertisement(packet_info.payload)
if not advert:
logger.debug("Failed to parse advertisement payload")
return
# Extract path info from packet
path_len = packet_info.path_length
path_hex = packet_info.path.hex() if packet_info.path else ""
logger.debug(
"Parsed advertisement from %s: %s (role=%d, lat=%s, lon=%s)",
advert.public_key[:12], advert.name, advert.device_role, advert.lat, advert.lon
"Parsed advertisement from %s: %s (role=%d, lat=%s, lon=%s, path_len=%d)",
advert.public_key[:12], advert.name, advert.device_role, advert.lat, advert.lon, path_len
)
# Try to find existing contact
@@ -320,6 +332,8 @@ async def _process_advertisement(
"lon": advert.lon,
"last_advert": advert.timestamp if advert.timestamp > 0 else timestamp,
"last_seen": timestamp,
"last_path": path_hex,
"last_path_len": path_len,
}
await ContactRepository.upsert(contact_data)
@@ -330,8 +344,8 @@ async def _process_advertisement(
"name": advert.name,
"type": contact_type,
"flags": existing.flags if existing else 0,
"last_path": existing.last_path if existing else None,
"last_path_len": existing.last_path_len if existing else -1,
"last_path": path_hex,
"last_path_len": path_len,
"last_advert": advert.timestamp if advert.timestamp > 0 else timestamp,
"lat": advert.lat,
"lon": advert.lon,
+12 -8
View File
@@ -45,15 +45,17 @@ async def prepare_repeater_connection(mc, contact: Contact, password: str) -> No
Raises:
HTTPException: If login fails
"""
# Add contact to radio with flood mode
logger.info("Adding repeater %s to radio", contact.public_key[:12])
# Add contact to radio with path from DB (or flood if no path known)
path_len = contact.last_path_len if contact.last_path_len >= 0 else -1
path_str = "direct" if path_len == 0 else f"{path_len} hops" if path_len > 0 else "flood"
logger.info("Adding repeater %s to radio (%s)", contact.public_key[:12], path_str)
contact_data = {
"public_key": contact.public_key,
"adv_name": contact.name or "",
"type": contact.type,
"flags": contact.flags,
"out_path": "",
"out_path_len": -1, # Flood mode
"out_path": contact.last_path or "",
"out_path_len": path_len,
"adv_lat": contact.lat or 0.0,
"adv_lon": contact.lon or 0.0,
"last_advert": contact.last_advert or 0,
@@ -380,15 +382,17 @@ async def send_repeater_command(public_key: str, request: CommandRequest) -> Com
# Pause message polling to prevent it from stealing our response
async with pause_polling():
# Add contact to radio with flood mode
logger.info("Adding repeater %s to radio", contact.public_key[:12])
# Add contact to radio with path from DB (or flood if no path known)
path_len = contact.last_path_len if contact.last_path_len >= 0 else -1
path_str = "direct" if path_len == 0 else f"{path_len} hops" if path_len > 0 else "flood"
logger.info("Adding repeater %s to radio (%s)", contact.public_key[:12], path_str)
contact_data = {
"public_key": contact.public_key,
"adv_name": contact.name or "",
"type": contact.type,
"flags": contact.flags,
"out_path": "",
"out_path_len": -1, # Flood mode
"out_path": contact.last_path or "",
"out_path_len": path_len,
"adv_lat": contact.lat or 0.0,
"adv_lon": contact.lon or 0.0,
"last_advert": contact.last_advert or 0,
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -13,7 +13,7 @@
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="manifest" href="/site.webmanifest" />
<script type="module" crossorigin src="/assets/index-BqCjcbH8.js"></script>
<script type="module" crossorigin src="/assets/index-B6Dj5zg2.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DZ67iE5i.css">
</head>
<body>
+9
View File
@@ -204,6 +204,10 @@ class TestAdvertisementPipeline:
assert contact.lon is not None
assert abs(contact.lat - expected["lat"]) < 0.001
assert abs(contact.lon - expected["lon"]) < 0.001
# This advertisement has path_len=6 (6 hops through repeaters)
assert contact.last_path_len == 6
assert contact.last_path is not None
assert len(contact.last_path) == 12 # 6 bytes = 12 hex chars
# Verify WebSocket broadcast
contact_broadcasts = [b for b in broadcasts if b["type"] == "contact"]
@@ -213,6 +217,7 @@ class TestAdvertisementPipeline:
assert broadcast["data"]["public_key"] == expected["public_key"]
assert broadcast["data"]["name"] == expected["name"]
assert broadcast["data"]["type"] == expected["type"]
assert broadcast["data"]["last_path_len"] == 6
@pytest.mark.asyncio
async def test_advertisement_updates_existing_contact(self, test_db, captured_broadcasts):
@@ -244,6 +249,10 @@ class TestAdvertisementPipeline:
assert contact.type == expected["type"] # Type updated
assert contact.lat is not None # GPS added
assert contact.lon is not None
# This advertisement has path_len=0 (direct neighbor)
assert contact.last_path_len == 0
# Empty path stored as None or ""
assert contact.last_path in (None, "")
class TestAckPipeline: