diff --git a/routes/aprs.py b/routes/aprs.py index eb14c97..8611f37 100644 --- a/routes/aprs.py +++ b/routes/aprs.py @@ -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: diff --git a/templates/index.html b/templates/index.html index ccaaaa8..9274460 100644 --- a/templates/index.html +++ b/templates/index.html @@ -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'); diff --git a/tests/test_aprs_parser.py b/tests/test_aprs_parser.py index 21651f8..bbefe2e 100644 --- a/tests/test_aprs_parser.py +++ b/tests/test_aprs_parser.py @@ -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)