From 947c6f6b1018f4aa16d4654375b35d19f453a7f2 Mon Sep 17 00:00:00 2001 From: whoisdomi Date: Thu, 17 Sep 2026 11:50:18 -0500 Subject: [PATCH] see sats --- selfdrive/ui/onroad/starpilot/gnss_health.py | 113 ++++++++++++++++++ .../onroad/starpilot/starpilot_onroad_view.py | 6 + selfdrive/ui/ui_state.py | 1 + 3 files changed, 120 insertions(+) create mode 100644 selfdrive/ui/onroad/starpilot/gnss_health.py diff --git a/selfdrive/ui/onroad/starpilot/gnss_health.py b/selfdrive/ui/onroad/starpilot/gnss_health.py new file mode 100644 index 0000000000..829b478b40 --- /dev/null +++ b/selfdrive/ui/onroad/starpilot/gnss_health.py @@ -0,0 +1,113 @@ +"""Onroad GNSS health readout. + +Built to measure RF desense from the external GPU's USB-C link: with the eGPU unplugged the +receiver demodulates satellite time from ~93% of tracked satellites and fixes immediately, and +with it plugged in that collapses to 0% while the tracked satellite count actually rises. Raw +satellite counts are therefore misleading on their own - the demodulation rate and C/No are what +show whether a cable, ferrite or antenna placement change helped. +""" +import pyray as rl + +from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.system.ui.lib.application import gui_app, FontWeight + +WIDTH = 300 +HEIGHT = 150 +MARGIN = 30 +PADDING = 14 +TITLE_SIZE = 26 +ROW_SIZE = 32 + +_BG = rl.Color(0, 0, 0, 180) +_LABEL = rl.Color(255, 255, 255, 140) +_GOOD = rl.Color(34, 197, 94, 255) +_WARN = rl.Color(234, 179, 8, 255) +_BAD = rl.Color(239, 68, 68, 255) + +# Demodulation rate thresholds. Anything under ~40% has never produced a fix in logged drives. +SAT_TIME_GOOD = 60.0 +SAT_TIME_WARN = 25.0 + +# Carrier-to-noise, in the modem's raw units (~2550 average with the eGPU unplugged and a clean +# fix; the desensed drives report 0 for every satellite, so any non-zero reading is progress). +CNO_GOOD = 2000.0 +CNO_WARN = 800.0 + + +def _grade(value: float, good: float, warn: float) -> rl.Color: + if value >= good: + return _GOOD + if value >= warn: + return _WARN + return _BAD + + +class GnssHealth: + """Bottom-right live GNSS quality readout.""" + + def __init__(self): + self._font = gui_app.font(FontWeight.SEMI_BOLD) + self._sat_time_pct = 0.0 + self._cno = 0.0 + self._gps_sv = 0 + self._glonass_sv = 0 + self._has_fix = False + + def _update(self) -> None: + sm = ui_state.sm + if not sm.valid.get("qcomGnss", False): + return + + gnss = sm["qcomGnss"] + if gnss.which() != "measurementReport": + return + + report = gnss.measurementReport + svs = list(report.sv) + source = str(report.source) + + if "glonass" in source: + self._glonass_sv = len(svs) + else: + self._gps_sv = len(svs) + + if svs: + known = sum(1 for sv in svs if sv.measurementStatus.satelliteTimeIsKnown) + self._sat_time_pct = 100.0 * known / len(svs) + noise = [sv.carrierNoise for sv in svs if sv.carrierNoise > 0] + self._cno = sum(noise) / len(noise) if noise else 0.0 + + if sm.valid.get("gpsLocationExternal", False): + self._has_fix = sm["gpsLocationExternal"].hasFix + + def render(self, bounds: rl.Rectangle) -> None: + self._update() + + x = bounds.x + bounds.width - WIDTH - MARGIN + y = bounds.y + bounds.height - HEIGHT - MARGIN + rect = rl.Rectangle(x, y, WIDTH, HEIGHT) + rl.draw_rectangle_rounded(rect, 0.12, 10, _BG) + + tx = int(x + PADDING) + ty = int(y + PADDING) + + rl.draw_text_ex(self._font, "GNSS", rl.Vector2(tx, ty), TITLE_SIZE, 0, _LABEL) + fix_text = "FIX" if self._has_fix else "NO FIX" + fix_color = _GOOD if self._has_fix else _BAD + rl.draw_text_ex(self._font, fix_text, rl.Vector2(int(x + WIDTH - PADDING - 90), ty), + TITLE_SIZE, 0, fix_color) + + ty += 34 + rl.draw_text_ex(self._font, "time", rl.Vector2(tx, ty), ROW_SIZE, 0, _LABEL) + rl.draw_text_ex(self._font, f"{self._sat_time_pct:.0f}%", rl.Vector2(tx + 120, ty), + ROW_SIZE, 0, _grade(self._sat_time_pct, SAT_TIME_GOOD, SAT_TIME_WARN)) + + ty += 36 + rl.draw_text_ex(self._font, "C/No", rl.Vector2(tx, ty), ROW_SIZE, 0, _LABEL) + rl.draw_text_ex(self._font, f"{self._cno:.0f}", rl.Vector2(tx + 120, ty), + ROW_SIZE, 0, _grade(self._cno, CNO_GOOD, CNO_WARN)) + + ty += 36 + rl.draw_text_ex(self._font, "sats", rl.Vector2(tx, ty), ROW_SIZE, 0, _LABEL) + rl.draw_text_ex(self._font, f"{self._gps_sv}G {self._glonass_sv}R", + rl.Vector2(tx + 120, ty), ROW_SIZE, 0, rl.WHITE) diff --git a/selfdrive/ui/onroad/starpilot/starpilot_onroad_view.py b/selfdrive/ui/onroad/starpilot/starpilot_onroad_view.py index 5fd40bb8ac..840f316174 100644 --- a/selfdrive/ui/onroad/starpilot/starpilot_onroad_view.py +++ b/selfdrive/ui/onroad/starpilot/starpilot_onroad_view.py @@ -16,6 +16,7 @@ from openpilot.selfdrive.ui.onroad.starpilot.stopping_point import render_stoppi from openpilot.selfdrive.ui.onroad.starpilot.pause_indicators import render_lateral_paused, render_longitudinal_paused from openpilot.selfdrive.ui.onroad.starpilot.pulse_glide import get_pulse_glide_border_color, render_pulse_glide from openpilot.selfdrive.ui.onroad.starpilot.pip_sidecam import PipSideCamera +from openpilot.selfdrive.ui.onroad.starpilot.gnss_health import GnssHealth from openpilot.selfdrive.ui.onroad.starpilot.favorite_radial_menu import FavoriteRadialMenu from openpilot.selfdrive.ui.onroad.starpilot.weather_icon import render_weather_icon from openpilot.selfdrive.ui.lib.starpilot_status import ( @@ -49,6 +50,7 @@ class StarPilotOnroadView(AugmentedRoadView): self._avg_fps = 0.0 self._pip_sidecam = self._child(PipSideCamera()) + self._gnss_health = GnssHealth() self._favorite_radial_menu = FavoriteRadialMenu( ui_state.ui_params, ui_state.params_memory, @@ -143,6 +145,10 @@ class StarPilotOnroadView(AugmentedRoadView): self._pip_sidecam.render(self._content_rect) + # Always on: this is a temporary instrument for the eGPU RF-desense testing, and gating it + # behind a param would mean registering a new key in params_keys.h, which needs a rebuild. + self._gnss_health.render(self._content_rect) + # The picker is an app-drawer modal, so it intentionally draws above # PiP and other on-road overlays while active. if self._draw_hud_controls and not self._full_alert_showing(): diff --git a/selfdrive/ui/ui_state.py b/selfdrive/ui/ui_state.py index 2905a87ac9..4f4497ef8d 100644 --- a/selfdrive/ui/ui_state.py +++ b/selfdrive/ui/ui_state.py @@ -64,6 +64,7 @@ class UIState: "selfdriveState", "longitudinalPlan", "gpsLocationExternal", + "qcomGnss", "mapdOut", "carOutput", "carControl",