From f5f846f5b6b7199165265797979a2da6de968e89 Mon Sep 17 00:00:00 2001 From: firestar5683 <168790843+firestar5683@users.noreply.github.com> Date: Fri, 24 Jul 2026 00:08:26 -0500 Subject: [PATCH] Pause vision inference while parked Co-Authored-By: Prabhaav Pillai <143428353+prabhaavp@users.noreply.github.com> --- starpilot/system/adj_spot_monitor_vision.py | 13 ++++++--- starpilot/system/speed_limit_vision.py | 28 ++++++++++++++++++- .../system/tests/test_speed_limit_vision.py | 27 ++++++++++++++++++ .../system/the_galaxy/tests/test_vasm.py | 20 ++++++++++++- starpilot/system/the_galaxy/the_galaxy.py | 15 ++++++++-- 5 files changed, 95 insertions(+), 8 deletions(-) diff --git a/starpilot/system/adj_spot_monitor_vision.py b/starpilot/system/adj_spot_monitor_vision.py index 7ad7f984e..19840d8cb 100644 --- a/starpilot/system/adj_spot_monitor_vision.py +++ b/starpilot/system/adj_spot_monitor_vision.py @@ -52,6 +52,7 @@ class VASMDaemon: self.followup_until = 0.0 self.onroad_prev = False + self.parked_prev = False self._last_pub_left = False self._last_pub_right = False @@ -179,12 +180,15 @@ class VASMDaemon: onroad = self.sm["deviceState"].started if self.sm.valid.get("deviceState", False) else False parked = self.sm["starpilotCarState"].isParked if self.sm.valid.get("starpilotCarState", False) else False - if not onroad or not self._enabled or not self.inference.valid or not self._annotation_loaded: - self._update_inactive(reset_inference=(onroad != self.onroad_prev)) - if onroad != self.onroad_prev: + if not onroad or parked or not self._enabled or not self.inference.valid or not self._annotation_loaded: + state_changed = onroad != self.onroad_prev or parked != self.parked_prev + self._update_inactive(reset_inference=state_changed) + if state_changed: self.last_inference_at = 0.0 + self.followup_until = 0.0 self.onroad_prev = onroad - if now - self._last_status_log >= STATUS_LOG_INTERVAL and not onroad: + self.parked_prev = parked + if now - self._last_status_log >= STATUS_LOG_INTERVAL and (not onroad or parked): cpu = list(self.sm["deviceState"].cpuUsagePercent) if self.sm.valid.get("deviceState", False) else [] cpu_str = f"avg={sum(cpu)/len(cpu):.0f}% cores={','.join(f'{c:.0f}' for c in cpu)}" if cpu else "?" status = f"[VASM] idle | {cpu_str} | onroad={onroad} parked={parked}" @@ -195,6 +199,7 @@ class VASMDaemon: continue self.onroad_prev = onroad + self.parked_prev = parked if not self._connect_camera(): self._update_inactive(reset_inference=True) diff --git a/starpilot/system/speed_limit_vision.py b/starpilot/system/speed_limit_vision.py index 058bc620a..35f5c017c 100644 --- a/starpilot/system/speed_limit_vision.py +++ b/starpilot/system/speed_limit_vision.py @@ -320,7 +320,7 @@ class SpeedLimitVisionDaemon: self.Ratekeeper = Ratekeeper self.VisionIpcClient = VisionIpcClient self.VisionStreamType = VisionStreamType - self.sm = messaging.SubMaster(["deviceState", "mapdOut", "userBookmark", "livePose"]) + self.sm = messaging.SubMaster(["deviceState", "mapdOut", "userBookmark", "livePose", "starpilotCarState"]) self.client = None self.stream_name = "" @@ -344,6 +344,7 @@ class SpeedLimitVisionDaemon: self.track_start_count = 0 self.max_track_proposal_confidence = 0.0 self.started_prev = False + self.parked_prev = False self.history: deque[HistoryEntry] = deque() self.published_speed_limit_mph = 0 @@ -2316,6 +2317,16 @@ class SpeedLimitVisionDaemon: self.params_memory.remove("VisionSpeedLimitSupportCount") self.params_memory.remove("VisionSpeedLimitSupportSpeed") + def _enter_parked(self, now): + self.current_frame_bgr = None + self.latest_detector_proposal = None + self._clear_proposal_track() + self.pending_auto_bookmark = None + self.pending_training_capture = None + self.followup_until = 0.0 + self._publish_status("Idle - parked", clear_speed=False) + self._publish_runtime_telemetry(now, "parked", force=True) + def _publish_detection_support(self, speed_limit_mph, support_count): if self.params_memory is None: return @@ -2563,6 +2574,7 @@ class SpeedLimitVisionDaemon: self._disconnect_camera() self.last_road_name = "" self.started_prev = False + self.parked_prev = False self.current_frame_bgr = None self.pending_auto_bookmark = None self._publish_status("Idle - offroad", clear_speed=True) @@ -2582,6 +2594,20 @@ class SpeedLimitVisionDaemon: self._write_debug_event("session_recovered", reason="missing_debug_session_while_onroad") self._publish_runtime_telemetry(now, "session_recovered", force=True) + parked = self.sm["starpilotCarState"].isParked if self.sm.valid.get("starpilotCarState", False) else False + if parked: + if not self.parked_prev: + self._enter_parked(now) + else: + self._publish_runtime_telemetry(now, "parked") + self.parked_prev = True + ratekeeper.keep_time() + continue + if self.parked_prev: + self.parked_prev = False + self.last_inference_at = -float("inf") + self._publish_runtime_telemetry(now, "parked_exit", force=True) + road_name = self.sm["mapdOut"].roadName if self.last_road_name and road_name and road_name != self.last_road_name: self._write_debug_event("road_change", previousRoadName=self.last_road_name, roadName=road_name) diff --git a/starpilot/system/tests/test_speed_limit_vision.py b/starpilot/system/tests/test_speed_limit_vision.py index 2e6cc665e..fe8255f56 100644 --- a/starpilot/system/tests/test_speed_limit_vision.py +++ b/starpilot/system/tests/test_speed_limit_vision.py @@ -111,6 +111,33 @@ def test_vasm_coexistence_mode_is_conditional(monkeypatch): assert daemon.detector_classifier_expansions == slv.DETECTOR_CLASSIFIER_EXPANSIONS +def test_enter_parked_preserves_published_limit_and_clears_transient_work(): + daemon = SpeedLimitVisionDaemon.__new__(SpeedLimitVisionDaemon) + daemon.current_frame_bgr = np.ones((2, 2, 3), dtype=np.uint8) + daemon.latest_detector_proposal = object() + daemon.proposal_track = object() + daemon.pending_auto_bookmark = object() + daemon.pending_training_capture = object() + daemon.followup_until = 100.0 + daemon.published_speed_limit_mph = 55 + published = [] + telemetry = [] + daemon._publish_status = lambda status, clear_speed=False: published.append((status, clear_speed)) + daemon._publish_runtime_telemetry = lambda now, phase, force=False: telemetry.append((now, phase, force)) + + daemon._enter_parked(10.0) + + assert daemon.current_frame_bgr is None + assert daemon.latest_detector_proposal is None + assert daemon.proposal_track is None + assert daemon.pending_auto_bookmark is None + assert daemon.pending_training_capture is None + assert daemon.followup_until == 0.0 + assert daemon.published_speed_limit_mph == 55 + assert published == [("Idle - parked", False)] + assert telemetry == [(10.0, "parked", True)] + + def test_receive_frame_does_not_retain_vision_buffer(monkeypatch): buffer_refs = [] diff --git a/starpilot/system/the_galaxy/tests/test_vasm.py b/starpilot/system/the_galaxy/tests/test_vasm.py index 0cbf76826..389eae9ad 100644 --- a/starpilot/system/the_galaxy/tests/test_vasm.py +++ b/starpilot/system/the_galaxy/tests/test_vasm.py @@ -1,6 +1,6 @@ import pytest -from openpilot.starpilot.system.the_galaxy.the_galaxy import _normalize_vasm_config +from openpilot.starpilot.system.the_galaxy.the_galaxy import _decode_json_object, _normalize_vasm_config def test_normalize_vasm_config_accepts_bounded_polygons(): @@ -28,3 +28,21 @@ def test_normalize_vasm_config_accepts_bounded_polygons(): def test_normalize_vasm_config_rejects_unsafe_config(config): with pytest.raises(ValueError): _normalize_vasm_config(config) + + +@pytest.mark.parametrize("raw", ( + '{"width":1928,"height":1208,"poly_left":[],"poly_right":[]}', + b'{"width":1928,"height":1208,"poly_left":[],"poly_right":[]}', +)) +def test_decode_vasm_config_from_legacy_params_payload(raw): + assert _decode_json_object(raw) == { + "width": 1928, + "height": 1208, + "poly_left": [], + "poly_right": [], + } + + +@pytest.mark.parametrize("raw", (None, "", "invalid", [], 1)) +def test_decode_vasm_config_rejects_non_objects(raw): + assert _decode_json_object(raw) == {} diff --git a/starpilot/system/the_galaxy/the_galaxy.py b/starpilot/system/the_galaxy/the_galaxy.py index 1f43f72bb..aeedda037 100644 --- a/starpilot/system/the_galaxy/the_galaxy.py +++ b/starpilot/system/the_galaxy/the_galaxy.py @@ -2596,6 +2596,18 @@ def _normalize_vasm_config(data): raise ValueError("At least one window polygon is required.") return config + +def _decode_json_object(value): + if isinstance(value, bytes): + value = value.decode("utf-8", errors="replace") + if isinstance(value, str): + try: + value = json.loads(value) + except json.JSONDecodeError: + return {} + return value if isinstance(value, dict) else {} + + def _is_blank_param_raw(raw_value): if raw_value is None: return True @@ -7462,8 +7474,7 @@ def setup(app): @app.route("/api/v_asm/config", methods=["GET"]) def v_asm_get_config(): - config = params.get("VASMAnnotationConfig") - return jsonify(config if isinstance(config, dict) else {}) + return jsonify(_decode_json_object(params.get("VASMAnnotationConfig"))) @app.route("/api/v_asm/config", methods=["POST"]) def v_asm_save_config():