Harden APRS station plotting across payload variants

This commit is contained in:
Smittix
2026-02-25 10:19:22 +00:00
parent 6ec15461af
commit 2d92243341
3 changed files with 100 additions and 1 deletions

View File

@@ -429,7 +429,7 @@ def parse_position(data: str) -> Optional[dict]:
data
)
if pos_match:
if pos_match:
lat_deg = int(pos_match.group(1))
lat_min = float(pos_match.group(2))
lat_dir = pos_match.group(3)
@@ -469,6 +469,49 @@ def parse_position(data: str) -> Optional[dict]:
return result
# Legacy/no-decimal variant occasionally seen in degraded decodes:
# DDMMN/DDDMMW (symbol chars still present between/after coords).
nodot_match = re.match(
r'^(\d{2})(\d{2})([NS])(.)(\d{3})(\d{2})([EW])(.)?',
data
)
if nodot_match:
lat_deg = int(nodot_match.group(1))
lat_min = float(nodot_match.group(2))
lat_dir = nodot_match.group(3)
symbol_table = nodot_match.group(4)
lon_deg = int(nodot_match.group(5))
lon_min = float(nodot_match.group(6))
lon_dir = nodot_match.group(7)
symbol_code = nodot_match.group(8) or ''
lat = lat_deg + lat_min / 60.0
if lat_dir == 'S':
lat = -lat
lon = lon_deg + lon_min / 60.0
if lon_dir == 'W':
lon = -lon
result = {
'lat': round(lat, 6),
'lon': round(lon, 6),
'symbol': symbol_table + symbol_code,
}
remaining = data[13:] if len(data) > 13 else ''
cs_match = re.search(r'(\d{3})/(\d{3})', remaining)
if cs_match:
result['course'] = int(cs_match.group(1))
result['speed'] = int(cs_match.group(2))
alt_match = re.search(r'/A=(-?\d+)', remaining)
if alt_match:
result['altitude'] = int(alt_match.group(1))
return result
# Fallback: tolerate APRS ambiguity spaces in minute fields.
# Example: 4903. N/07201. W
if len(data) >= 18:

View File

@@ -9488,8 +9488,13 @@
if (Array.isArray(payload)) return payload;
if (Array.isArray(payload.stations)) return payload.stations;
if (Array.isArray(payload.data)) return payload.data;
if (payload.data && Array.isArray(payload.data.stations)) return payload.data.stations;
if (payload.data && Array.isArray(payload.data.data)) return payload.data.data;
if (payload.result && Array.isArray(payload.result.stations)) return payload.result.stations;
if (payload.result && Array.isArray(payload.result.data)) return payload.result.data;
if (payload.data && payload.data.result && Array.isArray(payload.data.result.stations)) {
return payload.data.result.stations;
}
return [];
}
@@ -9508,6 +9513,8 @@
stations.forEach((station) => {
const callsign = String(station && station.callsign ? station.callsign : '').trim();
if (!callsign) return;
const lat = station.lat ?? station.latitude ?? null;
const lon = station.lon ?? station.longitude ?? null;
const signature = getAprsStationSignature(station);
if (aprsAgentStationSignatures.get(callsign) === signature) return;
@@ -9525,12 +9532,50 @@
processAprsPacket({
type: 'aprs',
...station,
lat,
lon,
callsign,
agent_name: station.agent_name || agentName || 'Remote Agent'
});
});
}
async function loadAprsStationSnapshot(isAgentMode = false) {
try {
const endpoint = (isAgentMode && aprsCurrentAgent)
? `/controller/agents/${aprsCurrentAgent}/aprs/data`
: '/aprs/stations';
const response = await fetch(endpoint);
if (!response.ok) return;
const payload = await response.json();
const stations = extractAprsStationsFromPayload(payload);
if (!Array.isArray(stations) || stations.length === 0) return;
if (isAgentMode) {
processAprsAgentStations(stations, payload.agent_name);
return;
}
stations.forEach((station) => {
const callsign = String(station && station.callsign ? station.callsign : '').trim();
if (!callsign) return;
const packet = {
type: 'aprs',
...station,
callsign,
lat: station.lat ?? station.latitude ?? null,
lon: station.lon ?? station.longitude ?? null,
packet_type: station.packet_type || 'position',
};
if (aprsHasValidCoordinates(packet.lat, packet.lon) && aprsMap) {
updateAprsMarker(packet);
}
updateAprsStationList(packet);
});
} catch (err) {
console.debug('APRS snapshot load failed:', err);
}
}
function startAprs() {
// Get values from function bar controls
const region = document.getElementById('aprsStripRegion').value;
@@ -9627,6 +9672,9 @@
if (customFreqInput) customFreqInput.disabled = true;
startAprsMeterCheck();
startAprsStream(isAgentMode);
// Backfill current stations in case position packets arrived before
// map initialization or SSE attachment.
loadAprsStationSnapshot(isAgentMode);
} else {
alert('APRS Error: ' + (scanResult.message || scanResult.error || 'Failed to start'));
updateAprsStatus('error');

View File

@@ -41,3 +41,11 @@ def test_parse_aprs_packet_handles_ambiguous_uncompressed_position() -> None:
assert packet["packet_type"] == "position"
assert packet["lat"] == pytest.approx(49.05, rel=0, abs=1e-6)
assert packet["lon"] == pytest.approx(-72.016667, rel=0, abs=1e-6)
def test_parse_aprs_packet_handles_no_decimal_position_variant() -> None:
packet = parse_aprs_packet("KJ7ABC-7>APRS,WIDE1-1:!4903N/07201W-Test")
assert packet is not None
assert packet["packet_type"] == "position"
assert packet["lat"] == pytest.approx(49.05, rel=0, abs=1e-6)
assert packet["lon"] == pytest.approx(-72.016667, rel=0, abs=1e-6)