Ditch garbage data ingest for lat/lon and extend map. Closes #63

This commit is contained in:
Jack Kingsman
2026-03-16 14:24:58 -07:00
parent 8d7d926762
commit 749fb43fd0
39 changed files with 79 additions and 9 deletions
+17 -1
View File
@@ -85,6 +85,10 @@ class PacketInfo:
path_hash_size: int = 1 # Bytes per hop: 1, 2, or 3
def _is_valid_advert_location(lat: float, lon: float) -> bool:
return -90 <= lat <= 90 and -180 <= lon <= 180
def extract_payload(raw_packet: bytes) -> bytes | None:
"""
Extract just the payload from a raw packet, skipping header and path.
@@ -243,7 +247,9 @@ def get_packet_payload_type(raw_packet: bytes) -> PayloadType | None:
return None
def parse_advertisement(payload: bytes) -> ParsedAdvertisement | None:
def parse_advertisement(
payload: bytes, raw_packet: bytes | None = None
) -> ParsedAdvertisement | None:
"""
Parse an advertisement payload.
@@ -299,6 +305,16 @@ def parse_advertisement(payload: bytes) -> ParsedAdvertisement | None:
lon_raw = int.from_bytes(payload[offset + 4 : offset + 8], byteorder="little", signed=True)
lat = lat_raw / 1_000_000
lon = lon_raw / 1_000_000
if not _is_valid_advert_location(lat, lon):
packet_hex = (raw_packet if raw_packet is not None else payload).hex().upper()
logger.warning(
"Dropping location data for nonsensical packet -- packet %s implies lat/lon %s/%s. Outta this world!",
packet_hex,
lat,
lon,
)
lat = None
lon = None
offset += 8
# Skip feature fields if present
+1 -1
View File
@@ -425,7 +425,7 @@ async def _process_advertisement(
logger.debug("Failed to parse advertisement packet")
return
advert = parse_advertisement(packet_info.payload)
advert = parse_advertisement(packet_info.payload, raw_packet=raw_bytes)
if not advert:
logger.debug("Failed to parse advertisement payload")
return
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -225,7 +225,7 @@ export function RepeaterDashboard({
) : (
<div className="space-y-4">
{/* Top row: Telemetry + Radio Settings | Node Info + Neighbors */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 md:items-stretch">
<div className="flex flex-col gap-4">
<NodeInfoPane
data={paneData.nodeInfo}
@@ -255,7 +255,7 @@ export function RepeaterDashboard({
disabled={anyLoading}
/>
</div>
<div className="flex flex-col gap-4">
<div className="flex min-h-0 flex-col gap-4">
<NeighborsPane
data={paneData.neighbors}
state={paneStates.neighbors}
@@ -99,16 +99,16 @@ export function NeighborsPane({
state={state}
onRefresh={onRefresh}
disabled={disabled}
className="flex flex-col"
contentClassName="flex-1 flex flex-col"
className="flex min-h-0 flex-1 flex-col"
contentClassName="flex min-h-0 flex-1 flex-col"
>
{!data ? (
<NotFetched />
) : sorted.length === 0 ? (
<p className="text-sm text-muted-foreground">No neighbors reported</p>
) : (
<div className="flex-1 flex flex-col gap-2">
<div className="overflow-x-auto">
<div className="flex min-h-0 flex-1 flex-col gap-2">
<div className="shrink-0 overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="text-left text-muted-foreground text-xs">
@@ -145,7 +145,7 @@ export function NeighborsPane({
{hasValidRepeaterGps && (neighborsWithCoords.length > 0 || hasValidRepeaterGps) ? (
<Suspense
fallback={
<div className="h-48 flex items-center justify-center text-xs text-muted-foreground">
<div className="flex min-h-48 flex-1 items-center justify-center text-xs text-muted-foreground">
Loading map...
</div>
}
+19
View File
@@ -7,6 +7,7 @@ import {
formatRouteLabel,
formatRoutingOverrideInput,
getEffectiveContactRoute,
isValidLocation,
resolvePath,
formatDistance,
formatHopCounts,
@@ -665,6 +666,24 @@ describe('resolvePath', () => {
});
});
describe('isValidLocation', () => {
it('rejects null and unset coordinates', () => {
expect(isValidLocation(null, -122.3)).toBe(false);
expect(isValidLocation(47.6, null)).toBe(false);
expect(isValidLocation(0, 0)).toBe(false);
});
it('rejects out-of-range coordinates', () => {
expect(isValidLocation(-593.497573, -1659.939204)).toBe(false);
expect(isValidLocation(91, 0)).toBe(false);
expect(isValidLocation(0, 181)).toBe(false);
});
it('accepts sane coordinates', () => {
expect(isValidLocation(47.6062, -122.3321)).toBe(true);
});
});
describe('formatDistance', () => {
it('formats distances under 1km in meters', () => {
expect(formatDistance(0.5)).toBe('500m');
+3
View File
@@ -273,6 +273,9 @@ export function isValidLocation(lat: number | null, lon: number | null): boolean
if (lat === null || lon === null) {
return false;
}
if (lat < -90 || lat > 90 || lon < -180 || lon > 180) {
return false;
}
// (0, 0) is in the Atlantic Ocean - treat as unset
if (lat === 0 && lon === 0) {
return false;
+32
View File
@@ -448,6 +448,38 @@ class TestAdvertisementParsing:
assert result.lat is None
assert result.lon is None
def test_parse_advertisement_discards_out_of_range_gps(self, caplog):
"""Out-of-range advert coordinates are treated as missing."""
from app.decoder import parse_advertisement
payload = bytearray()
payload.extend(
bytes.fromhex("f29fdc7c560f9d813d1593a8587fa46a9e7efe2f5506d38c0af41307bf9e517a")
)
payload.extend((1718749967).to_bytes(4, byteorder="little"))
payload.extend(bytes(64))
payload.append(0x92)
payload.extend((-593497573).to_bytes(4, byteorder="little", signed=True))
payload.extend((-1659939204).to_bytes(4, byteorder="little", signed=True))
payload.extend(b"Tacompton")
raw_packet = bytes.fromhex("11") + bytes(payload)
with caplog.at_level("WARNING"):
result = parse_advertisement(bytes(payload), raw_packet=raw_packet)
assert result is not None
assert (
result.public_key == "f29fdc7c560f9d813d1593a8587fa46a9e7efe2f5506d38c0af41307bf9e517a"
)
assert result.name == "Tacompton"
assert result.device_role == 2
assert result.lat is None
assert result.lon is None
assert "Dropping location data for nonsensical packet -- packet" in caplog.text
assert raw_packet.hex().upper() in caplog.text
assert "-593.497573/-1659.939204" in caplog.text
assert "Outta this world!" in caplog.text
def test_parse_advertisement_extracts_public_key(self):
"""Advertisement parsing extracts the public key correctly."""
from app.decoder import parse_advertisement, parse_packet