fix: Preserve GPS satellites across DOP-only SKY messages

gpsd sends multiple SKY messages per cycle — some contain only DOP
values with an empty satellites array. Previously this would overwrite
the satellite list, causing the sky view to flicker empty. Now DOP-only
SKY messages preserve the existing satellite list. Also adds a 5-second
polling fallback for satellite data since SSE can miss sky updates due
to queue contention.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Smittix
2026-02-16 19:48:46 +00:00
parent 2b3f351ff0
commit 5605ae0359
2 changed files with 61 additions and 13 deletions
+32
View File
@@ -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 {
+29 -13
View File
@@ -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,