diff --git a/static/js/modes/gps.js b/static/js/modes/gps.js index 1e1c7b2..f343995 100644 --- a/static/js/modes/gps.js +++ b/static/js/modes/gps.js @@ -8,6 +8,7 @@ const GPS = (function() { let connected = false; let lastPosition = null; let lastSky = null; + let skyPollTimer = null; // Constellation color map const CONST_COLORS = { @@ -41,6 +42,7 @@ const GPS = (function() { updateSkyUI(data.sky); } subscribeToStream(); + startSkyPolling(); // Ensure the global GPS stream is running if (typeof startGpsStream === 'function' && !gpsEventSource) { startGpsStream(); @@ -58,6 +60,7 @@ const GPS = (function() { function disconnect() { unsubscribeFromStream(); + stopSkyPolling(); fetch('/gps/stop', { method: 'POST' }) .then(() => { connected = false; @@ -77,6 +80,34 @@ const GPS = (function() { } } + function startSkyPolling() { + stopSkyPolling(); + // Poll satellite data every 5 seconds as a reliable fallback + // SSE stream may miss sky updates due to queue contention with position messages + pollSatellites(); + skyPollTimer = setInterval(pollSatellites, 5000); + } + + function stopSkyPolling() { + if (skyPollTimer) { + clearInterval(skyPollTimer); + skyPollTimer = null; + } + } + + function pollSatellites() { + if (!connected) return; + fetch('/gps/satellites') + .then(r => r.json()) + .then(data => { + if (data.status === 'ok' && data.sky) { + lastSky = data.sky; + updateSkyUI(data.sky); + } + }) + .catch(() => {}); + } + function subscribeToStream() { // Subscribe to the global GPS stream instead of opening a separate SSE connection if (typeof addGpsStreamSubscriber === 'function') { @@ -395,6 +426,7 @@ const GPS = (function() { function destroy() { unsubscribeFromStream(); + stopSkyPolling(); } return { diff --git a/utils/gps.py b/utils/gps.py index 88fc7af..c50d426 100644 --- a/utils/gps.py +++ b/utils/gps.py @@ -318,6 +318,8 @@ class GPSDClient: except json.JSONDecodeError: logger.debug(f"Invalid JSON from gpsd: {line[:50]}") + except Exception as parse_err: + logger.error(f"Error handling gpsd {msg_class} message: {parse_err}") except socket.timeout: continue @@ -371,19 +373,33 @@ class GPSDClient: self._update_position(position) def _handle_sky(self, msg: dict) -> None: - """Handle SKY (satellite sky view) message from gpsd.""" - sats = [] - for sat in msg.get('satellites', []): - prn = sat.get('PRN', 0) - gnssid = sat.get('gnssid') - sats.append(GPSSatellite( - prn=prn, - elevation=sat.get('el'), - azimuth=sat.get('az'), - snr=sat.get('ss'), - used=sat.get('used', False), - constellation=_classify_constellation(prn, gnssid), - )) + """Handle SKY (satellite sky view) message from gpsd. + + gpsd sends multiple SKY messages per cycle: some contain only DOP + values while others include the full satellites array. When a + DOP-only SKY arrives, preserve the most recent satellite list + instead of overwriting it with an empty one. + """ + raw_sats = msg.get('satellites', []) + has_satellites = len(raw_sats) > 0 + + if has_satellites: + sats = [] + for sat in raw_sats: + prn = sat.get('PRN', 0) + gnssid = sat.get('gnssid') + sats.append(GPSSatellite( + prn=prn, + elevation=sat.get('el'), + azimuth=sat.get('az'), + snr=sat.get('ss'), + used=sat.get('used', False), + constellation=_classify_constellation(prn, gnssid), + )) + else: + # DOP-only SKY message — keep existing satellites + with self._lock: + sats = list(self._sky.satellites) if self._sky else [] sky_data = GPSSkyData( satellites=sats,