From 838c7fdc9ba5f0ed9713abc6a43c87459bec32b9 Mon Sep 17 00:00:00 2001 From: firestar5683 <168790843+firestar5683@users.noreply.github.com> Date: Mon, 15 Jun 2026 11:30:41 -0500 Subject: [PATCH] fixes --- .../common/tests/test_starpilot_process.py | 82 +++++++++++++++++++ starpilot/starpilot_process.py | 3 +- .../the_pond/tests/test_navigation_params.py | 76 +++++++++++++++++ starpilot/system/the_pond/the_pond.py | 40 ++++++--- 4 files changed, 189 insertions(+), 12 deletions(-) create mode 100644 starpilot/common/tests/test_starpilot_process.py create mode 100644 starpilot/system/the_pond/tests/test_navigation_params.py diff --git a/starpilot/common/tests/test_starpilot_process.py b/starpilot/common/tests/test_starpilot_process.py new file mode 100644 index 000000000..d2cd0d6ce --- /dev/null +++ b/starpilot/common/tests/test_starpilot_process.py @@ -0,0 +1,82 @@ +import json +from types import SimpleNamespace + +from openpilot.starpilot import starpilot_process + + +class FakeParams: + def __init__(self): + self.writes = [] + + def put(self, key, value): + self.writes.append((key, value)) + + +class FakeThreadManager: + def run_with_lock(self, *args, **kwargs): + return None + + +class FakeThemeManager: + def update_active_theme(self, *args, **kwargs): + return None + + +class FakeModelManager: + def randomize_selected_model(self): + return None + + +def test_transition_offroad_skips_invalid_gps_persist(): + params = FakeParams() + planner = SimpleNamespace(gps_position={ + "latitude": 0.0, + "longitude": 0.0, + "bearing": 0.0, + "speed": 0.0, + "hasFix": False, + "updatedAtMonotonic": 1.0, + "updatedAtSec": 1.0, + }) + toggles = SimpleNamespace(lock_doors_timer=0, random_themes=False) + + starpilot_process.transition_offroad( + planner, + FakeModelManager(), + FakeThemeManager(), + FakeThreadManager(), + False, + None, + params, + toggles, + ) + + assert params.writes == [] + + +def test_transition_offroad_persists_valid_gps(): + params = FakeParams() + gps_position = { + "latitude": 41.0, + "longitude": -87.0, + "bearing": 90.0, + "speed": 12.0, + "hasFix": True, + "updatedAtMonotonic": 1.0, + "updatedAtSec": 2.0, + } + planner = SimpleNamespace(gps_position=gps_position) + toggles = SimpleNamespace(lock_doors_timer=0, random_themes=False) + + starpilot_process.transition_offroad( + planner, + FakeModelManager(), + FakeThemeManager(), + FakeThreadManager(), + False, + None, + params, + toggles, + ) + + assert params.writes == [("LastGPSPosition", json.dumps(gps_position))] diff --git a/starpilot/starpilot_process.py b/starpilot/starpilot_process.py index 63fa7f0fb..05aac114a 100644 --- a/starpilot/starpilot_process.py +++ b/starpilot/starpilot_process.py @@ -144,7 +144,8 @@ def sync_drive_stats(params, session): print(f"Failed to sync drive stats: {exception}") def transition_offroad(starpilot_planner, model_manager, theme_manager, thread_manager, time_validated, sm, params, starpilot_toggles): - params.put("LastGPSPosition", json.dumps(starpilot_planner.gps_position)) + if gps_position_valid(starpilot_planner.gps_position): + params.put("LastGPSPosition", json.dumps(starpilot_planner.gps_position)) if starpilot_toggles.lock_doors_timer != 0: thread_manager.run_with_lock(lock_doors, (starpilot_toggles.lock_doors_timer, sm, params), report=False) diff --git a/starpilot/system/the_pond/tests/test_navigation_params.py b/starpilot/system/the_pond/tests/test_navigation_params.py new file mode 100644 index 000000000..cc574a40c --- /dev/null +++ b/starpilot/system/the_pond/tests/test_navigation_params.py @@ -0,0 +1,76 @@ +import json + +from openpilot.common.params import ParamKeyType +from openpilot.starpilot.system.the_pond import the_pond + + +class FakeParamsBackend: + def __init__(self, key_types=None, default_values=None, values=None): + self.key_types = key_types or {} + self.default_values = default_values or {} + self.values = values or {} + self.writes = [] + + def get_key_type(self, key): + return self.key_types[key] + + def get_default_value(self, key): + return self.default_values.get(key) + + def put(self, key, value): + self.writes.append((key, value)) + self.values[key] = value + + def get(self, key, block=False): + return self.values.get(key) + + +def test_params_compat_accepts_json_strings_for_json_keys(): + backend = FakeParamsBackend( + key_types={"FavoriteDestinations": ParamKeyType.JSON}, + default_values={"FavoriteDestinations": []}, + ) + compat = the_pond.ParamsCompat(backend) + + compat.put("FavoriteDestinations", json.dumps([{"name": "Home"}])) + + assert backend.writes == [("FavoriteDestinations", [{"name": "Home"}])] + + +def test_navigation_last_position_uses_recent_persisted_fix(monkeypatch): + recent_payload = json.dumps({ + "latitude": 41.0, + "longitude": -87.0, + "hasFix": True, + "updatedAtSec": 10_000.0, + }) + memory_backend = FakeParamsBackend(values={"LastGPSPosition": ""}) + persisted_backend = FakeParamsBackend(values={"LastGPSPosition": recent_payload}) + + monkeypatch.setattr(the_pond, "params_memory", the_pond.ParamsCompat(memory_backend)) + monkeypatch.setattr(the_pond, "params", the_pond.ParamsCompat(persisted_backend)) + monkeypatch.setattr(the_pond.time, "time", lambda: 10_300.0) + monkeypatch.setattr(the_pond, "system_time_valid", lambda: True) + + position = the_pond._get_navigation_last_position() + + assert position["latitude"] == 41.0 + assert position["longitude"] == -87.0 + + +def test_navigation_last_position_rejects_stale_persisted_fix(monkeypatch): + stale_payload = json.dumps({ + "latitude": 41.0, + "longitude": -87.0, + "hasFix": True, + "updatedAtSec": 10_000.0, + }) + memory_backend = FakeParamsBackend(values={"LastGPSPosition": ""}) + persisted_backend = FakeParamsBackend(values={"LastGPSPosition": stale_payload}) + + monkeypatch.setattr(the_pond, "params_memory", the_pond.ParamsCompat(memory_backend)) + monkeypatch.setattr(the_pond, "params", the_pond.ParamsCompat(persisted_backend)) + monkeypatch.setattr(the_pond.time, "time", lambda: 10_000.0 + the_pond.NAVIGATION_PERSISTED_LOCATION_MAX_AGE_SECONDS + 1.0) + monkeypatch.setattr(the_pond, "system_time_valid", lambda: True) + + assert the_pond._get_navigation_last_position() is None diff --git a/starpilot/system/the_pond/the_pond.py b/starpilot/system/the_pond/the_pond.py index 1e1a587a9..9e4649ac9 100644 --- a/starpilot/system/the_pond/the_pond.py +++ b/starpilot/system/the_pond/the_pond.py @@ -313,6 +313,20 @@ class ParamsCompat: typed_value = "" else: typed_value = str(value) + elif expected_type == ParamKeyType.JSON: + if isinstance(value, bytes): + value = value.decode("utf-8", errors="replace") + + if isinstance(value, str): + stripped = value.strip() + if not stripped: + typed_value = self._params.get_default_value(key) + else: + typed_value = json.loads(stripped) + elif isinstance(value, tuple): + typed_value = list(value) + else: + typed_value = value self._params.put(key, typed_value) @@ -466,7 +480,8 @@ KEYS = { } NAVIGATION_MEMORY_LOCATION_STALE_SECONDS = 10.0 -NAVIGATION_PERSISTED_LOCATION_BOOT_SKEW_SECONDS = 5.0 +NAVIGATION_PERSISTED_LOCATION_FUTURE_SKEW_SECONDS = 60.0 +NAVIGATION_PERSISTED_LOCATION_MAX_AGE_SECONDS = 24 * 60 * 60 TMUX_LOGS_PATH = Path("/data/tmux_logs") @@ -524,7 +539,7 @@ def _last_gps_position_is_live(payload): return (time.monotonic() - updated_at_monotonic) <= NAVIGATION_MEMORY_LOCATION_STALE_SECONDS -def _last_gps_position_is_current_boot(payload): +def _last_gps_position_is_recent(payload): if not isinstance(payload, dict): return False @@ -537,11 +552,14 @@ def _last_gps_position_is_current_boot(payload): return False now_sec = time.time() - boot_started_at_sec = now_sec - time.monotonic() - if updated_at_sec > (now_sec + 60.0): + if updated_at_sec > (now_sec + NAVIGATION_PERSISTED_LOCATION_FUTURE_SKEW_SECONDS): return False - return updated_at_sec >= (boot_started_at_sec - NAVIGATION_PERSISTED_LOCATION_BOOT_SKEW_SECONDS) + if not system_time_valid(): + return True + + age_sec = now_sec - updated_at_sec + return 0.0 <= age_sec <= NAVIGATION_PERSISTED_LOCATION_MAX_AGE_SECONDS def _get_navigation_last_position(): @@ -550,7 +568,7 @@ def _get_navigation_last_position(): return memory_position persisted_position = _parse_last_gps_position(params.get("LastGPSPosition", encoding="utf8") or "") - if _last_gps_position_is_current_boot(persisted_position): + if _last_gps_position_is_recent(persisted_position): return persisted_position return None @@ -3640,7 +3658,7 @@ def setup(app): destination, ) params.put("NavDestination", json.dumps(destination)) - params.put("ApiCache_NavDestinations", json.dumps(recent_destinations)) + params.put("ApiCache_NavDestinations", recent_destinations) return {"message": "Destination set"} @app.route("/api/navigation/favorite", methods=["DELETE"]) @@ -3662,7 +3680,7 @@ def setup(app): ) ] - params.put("FavoriteDestinations", json.dumps(favorites)) + params.put("FavoriteDestinations", favorites) return jsonify(message="Destination removed from favorites!") @app.route("/api/navigation/favorite", methods=["GET"]) @@ -3675,7 +3693,7 @@ def setup(app): f["id"] = hashlib.sha1(raw.encode()).hexdigest() changed = True if changed: - params.put("FavoriteDestinations", json.dumps(favorites)) + params.put("FavoriteDestinations", favorites) return jsonify(favorites=favorites) @app.route("/api/navigation/favorite", methods=["POST"]) @@ -3690,7 +3708,7 @@ def setup(app): if not any(f.get("id") == new_fav["id"] for f in existing): existing.append(new_fav) - params.put("FavoriteDestinations", json.dumps(existing)) + params.put("FavoriteDestinations", existing) return {"message": "Destination added to favorites!"} @app.route("/api/navigation/favorite/rename", methods=["POST"]) @@ -3740,7 +3758,7 @@ def setup(app): if not found: return jsonify({"error": "Favorite not found"}), 404 - params.put("FavoriteDestinations", json.dumps(existing_favorites)) + params.put("FavoriteDestinations", existing_favorites) return jsonify(message="Favorite updated successfully!") @app.route("/api/navigation_key", methods=["DELETE"])