From c201bef873ae3e2f34044246e0cf8958456432ce Mon Sep 17 00:00:00 2001 From: firestarsdog <229254897+firestarsdog@users.noreply.github.com> Date: Tue, 18 Aug 2026 18:59:44 -0400 Subject: [PATCH] Offroad Start Navigation Guards --- selfdrive/ui/tests/test_home_info_card.py | 49 +++++++++++++++- selfdrive/ui/widgets/home_info_card.py | 70 ++++++++++++++++++++++- 2 files changed, 115 insertions(+), 4 deletions(-) diff --git a/selfdrive/ui/tests/test_home_info_card.py b/selfdrive/ui/tests/test_home_info_card.py index 767a1d12c..eeea96cdc 100644 --- a/selfdrive/ui/tests/test_home_info_card.py +++ b/selfdrive/ui/tests/test_home_info_card.py @@ -39,8 +39,8 @@ def _favorite(name, latitude, longitude, **flags): return {"name": name, "latitude": latitude, "longitude": longitude, **flags} -def _card(params): - card = HomeInfoCard(params, FakeDriveStats()) +def _card(params, *, online_provider=lambda: True, gps_provider=lambda: True): + card = HomeInfoCard(params, FakeDriveStats(), online_provider=online_provider, gps_provider=gps_provider) card.set_rect(rl.Rectangle(100, 200, 750, 745)) card.refresh() return card @@ -127,3 +127,48 @@ def test_refresh_falls_back_without_clearing_an_active_non_favorite_destination( assert not card.quick_start_available assert card.show_records assert card.active_destination["name"] == "Old destination" + + +def test_quick_start_requires_online_and_gps(): + params = FakeParams({ + "MapboxSecretKey": "secret", + FAVORITE_DESTINATIONS_KEY: json.dumps([_favorite("Home", 1, 2)]), + }) + + # Both online and GPS available + card = _card(params, online_provider=lambda: True, gps_provider=lambda: True) + assert card.quick_start_available + assert not card.show_records + + # Offline + card_offline = _card(params, online_provider=lambda: False, gps_provider=lambda: True) + assert not card_offline.quick_start_available + assert card_offline.show_records + + # No GPS fix + card_no_gps = _card(params, online_provider=lambda: True, gps_provider=lambda: False) + assert not card_no_gps.quick_start_available + assert card_no_gps.show_records + + # Fallback GPS check via LastGPSPosition in params + params_with_gps = FakeParams({ + "MapboxSecretKey": "secret", + FAVORITE_DESTINATIONS_KEY: json.dumps([_favorite("Home", 1, 2)]), + "LastGPSPosition": json.dumps({"latitude": 37.77, "longitude": -122.41, "hasFix": True}), + }) + card_param_gps = HomeInfoCard(params_with_gps, FakeDriveStats(), online_provider=lambda: True) + card_param_gps.set_rect(rl.Rectangle(100, 200, 750, 745)) + card_param_gps.refresh() + assert card_param_gps.quick_start_available + + # LastGPSPosition with hasFix=False or Null Island -> unavailable + params_bad_gps = FakeParams({ + "MapboxSecretKey": "secret", + FAVORITE_DESTINATIONS_KEY: json.dumps([_favorite("Home", 1, 2)]), + "LastGPSPosition": json.dumps({"latitude": 0.0, "longitude": 0.0, "hasFix": False}), + }) + card_bad_gps = HomeInfoCard(params_bad_gps, FakeDriveStats(), online_provider=lambda: True) + card_bad_gps.set_rect(rl.Rectangle(100, 200, 750, 745)) + card_bad_gps.refresh() + assert not card_bad_gps.quick_start_available + diff --git a/selfdrive/ui/widgets/home_info_card.py b/selfdrive/ui/widgets/home_info_card.py index 67498fce8..ecd544d59 100644 --- a/selfdrive/ui/widgets/home_info_card.py +++ b/selfdrive/ui/widgets/home_info_card.py @@ -1,7 +1,11 @@ from __future__ import annotations +import json +import math +from collections.abc import Callable from typing import Any +from cereal import log import pyray as rl from openpilot.selfdrive.ui.widgets.drive_stats import ( @@ -33,14 +37,38 @@ TEXT_LEFT_INSET = 30.0 TEXT_RIGHT_INSET = 34.0 +def _has_valid_gps_payload(raw: Any) -> bool: + if not raw: + return False + try: + data = json.loads(raw) if isinstance(raw, (str, bytes)) else raw + if isinstance(data, dict): + if str(data.get("hasFix")).strip().lower() in ("0", "false", "no", "off", "", "none"): + return False + lat, lon = float(data.get("latitude", 0.0)), float(data.get("longitude", 0.0)) + return math.isfinite(lat) and math.isfinite(lon) and (abs(lat) > 1e-6 or abs(lon) > 1e-6) + except Exception: + pass + return False + + class HomeInfoCard(Widget): """Paired offroad Home card for Quick Start and Personal Records.""" - def __init__(self, params: Any, drive_stats: Any): + def __init__( + self, + params: Any, + drive_stats: Any, + *, + online_provider: Callable[[], bool] | None = None, + gps_provider: Callable[[], bool] | None = None, + ): super().__init__() self._params = params self._store = NavigationDestinationStore(params) self._drive_stats = drive_stats + self._online_provider = online_provider + self._gps_provider = gps_provider self._show_records = False self._quick_start_available = False @@ -73,6 +101,39 @@ class HomeInfoCard(Widget): self.refresh() super().show_event() + def _is_online(self) -> bool: + if self._online_provider is not None: + return bool(self._online_provider()) + try: + from openpilot.selfdrive.ui.ui_state import ui_state + if hasattr(ui_state, "sm") and ui_state.sm.valid.get("deviceState", False): + return ui_state.sm["deviceState"].networkType != log.DeviceState.NetworkType.none + except Exception: + pass + return False + + def _has_gps(self) -> bool: + if self._gps_provider is not None: + return bool(self._gps_provider()) + + if _has_valid_gps_payload(self._params.get("LastGPSPosition", encoding="utf-8")): + return True + + try: + from openpilot.selfdrive.ui.ui_state import ui_state + if hasattr(ui_state, "sm") and ui_state.sm.valid.get("gpsLocationExternal", False): + gps = ui_state.sm["gpsLocationExternal"] + if getattr(gps, "hasFix", False): + lat, lon = float(getattr(gps, "latitude", 0.0)), float(getattr(gps, "longitude", 0.0)) + if math.isfinite(lat) and math.isfinite(lon) and (abs(lat) > 1e-6 or abs(lon) > 1e-6): + return True + + if hasattr(ui_state, "params_memory"): + return _has_valid_gps_payload(ui_state.params_memory.get("LastGPSPosition", encoding="utf-8")) + except Exception: + pass + return False + def refresh(self) -> None: was_available = self._quick_start_available self._active_destination = self._store.active_destination() @@ -80,7 +141,12 @@ class HomeInfoCard(Widget): raw_favorites = self._params.get(FAVORITE_DESTINATIONS_KEY, encoding="utf-8", default="[]") favorites = load_favorite_destinations(raw_favorites) self._favorites = ordered_favorite_destinations(favorites, limit=ROW_COUNT) - self._quick_start_available = routing_configured(self._params) and bool(self._favorites) + self._quick_start_available = ( + routing_configured(self._params) + and bool(self._favorites) + and self._is_online() + and self._has_gps() + ) if not self._quick_start_available: self._show_records = True