mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-09-01 16:48:29 +00:00
Ditch garbage data ingest for lat/lon and extend map. Closes #63
This commit is contained in:
+17
-1
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
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>
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user