diff --git a/app/decoder.py b/app/decoder.py index e17b8a2..7a46838 100644 --- a/app/decoder.py +++ b/app/decoder.py @@ -299,8 +299,11 @@ def parse_advertisement( timestamp = int.from_bytes(payload[32:36], byteorder="little") flags = payload[100] - # Parse flags + # Parse flags — clamp device_role to valid range (0-4); corrupted + # advertisements can have junk in the lower nibble. device_role = flags & 0x0F + if device_role > 4: + device_role = 0 has_location = bool(flags & 0x10) has_feature1 = bool(flags & 0x20) has_feature2 = bool(flags & 0x40) diff --git a/app/models.py b/app/models.py index 52fa989..96615c2 100644 --- a/app/models.py +++ b/app/models.py @@ -4,6 +4,10 @@ from pydantic import BaseModel, Field from app.path_utils import normalize_contact_route, normalize_route_override +# Valid MeshCore contact types: 0=unknown, 1=client, 2=repeater, 3=room, 4=sensor. +# Corrupted radio data can produce values outside this range. +_VALID_CONTACT_TYPES = frozenset({0, 1, 2, 3, 4}) + class ContactRoute(BaseModel): """A normalized contact route.""" @@ -59,16 +63,30 @@ class ContactUpsert(BaseModel): -1 if radio_data.get("out_path_len", -1) == -1 else 0, ), ) + # Clamp invalid contact types to 0 (unknown) — corrupted radio data + # can produce values like 111 or 240 that break downstream branching. + raw_type = radio_data.get("type", 0) + contact_type = raw_type if raw_type in _VALID_CONTACT_TYPES else 0 + + # Null out impossible coordinates — the contact is still ingested, + # but garbage lat/lon (e.g. 1953.7) is discarded rather than stored. + lat = radio_data.get("adv_lat") + lon = radio_data.get("adv_lon") + if lat is not None and not (-90 <= lat <= 90): + lat = None + if lon is not None and not (-180 <= lon <= 180): + lon = None + return cls( public_key=public_key, name=radio_data.get("adv_name"), - type=radio_data.get("type", 0), + type=contact_type, flags=radio_data.get("flags", 0), direct_path=direct_path, direct_path_len=direct_path_len, direct_path_hash_mode=direct_path_hash_mode, - lat=radio_data.get("adv_lat"), - lon=radio_data.get("adv_lon"), + lat=lat, + lon=lon, last_advert=radio_data.get("last_advert"), on_radio=on_radio, ) diff --git a/app/radio_sync.py b/app/radio_sync.py index cdedc31..4d769a3 100644 --- a/app/radio_sync.py +++ b/app/radio_sync.py @@ -21,7 +21,7 @@ from meshcore import EventType, MeshCore from app.channel_constants import PUBLIC_CHANNEL_KEY, PUBLIC_CHANNEL_NAME from app.config import settings from app.event_handlers import cleanup_expired_acks, on_contact_message -from app.models import Contact, ContactUpsert +from app.models import Contact, ContactUpsert, _VALID_CONTACT_TYPES from app.radio import RadioOperationBusyError from app.repository import ( AmbiguousPublicKeyPrefixError, @@ -1073,7 +1073,6 @@ async def sync_contacts_from_radio(mc: MeshCore) -> dict: # Import radio-favorited contacts into app favorites. # Only trust the favorite bit on contacts with a valid type (0-4); # garbled radio data can have junk flags with bit 0 set. - _VALID_CONTACT_TYPES = {0, 1, 2, 3, 4} radio_fav_keys = [ pk for pk, data in contacts.items()