diff --git a/starpilot/common/starpilot_variables.py b/starpilot/common/starpilot_variables.py index 21d98d92f..77fedca07 100644 --- a/starpilot/common/starpilot_variables.py +++ b/starpilot/common/starpilot_variables.py @@ -583,7 +583,7 @@ class StarPilotVariables: self.params.put_float("SteerDelay", full_stock_delay) self.params.put_bool(STEER_DELAY_MODE_MIGRATION_KEY, True) - def update(self, holiday_theme="stock", started=False): + def update(self, holiday_theme="stock", started=False, clear_update_flag=True): toggle = self.starpilot_toggles toggle.tuning_level = self.params.get("TuningLevel") if self.params.get_bool("TuningLevelConfirmed") else TUNING_LEVELS["ADVANCED"] # CarParams uses this value to select the matching Panda safety configuration. @@ -1446,4 +1446,5 @@ class StarPilotVariables: toggle.volt_sng = self.get_value("VoltSNG", condition=toggle.car_model in LEGACY_VOLT_STOCK_ACC_CARS) process_starpilot_toggles.cache_clear() - self.params_memory.remove("StarPilotTogglesUpdated") + if clear_update_flag: + self.params_memory.remove("StarPilotTogglesUpdated") diff --git a/starpilot/common/tests/test_starpilot_process.py b/starpilot/common/tests/test_starpilot_process.py index 91bfb02b6..d74af13ea 100644 --- a/starpilot/common/tests/test_starpilot_process.py +++ b/starpilot/common/tests/test_starpilot_process.py @@ -27,6 +27,40 @@ class FakeModelManager: return None +def test_background_toggle_update_does_not_mutate_active_toggles(): + active_toggles = SimpleNamespace(holiday_themes=False, random_themes=False, enabled=False) + + class FakeVariables: + def __init__(self): + self.starpilot_toggles = active_toggles + self.clear_update_flag = None + + def update(self, holiday_theme, started, clear_update_flag=True): + self.clear_update_flag = clear_update_flag + self.starpilot_toggles.enabled = True + + theme_manager = FakeThemeManager() + theme_manager.holiday_theme = "stock" + theme_manager.theme_updated = False + result = {} + + starpilot_process.update_toggles_in_background( + result, + FakeVariables(), + True, + theme_manager, + FakeThreadManager(), + False, + FakeParams(), + active_toggles, + ) + + updated_variables, updated_toggles = result["update"] + assert active_toggles.enabled is False + assert updated_toggles.enabled is True + assert updated_variables.clear_update_flag is False + + def test_transition_offroad_skips_invalid_gps_persist(): params = FakeParams() planner = SimpleNamespace(gps_position={ diff --git a/starpilot/starpilot_process.py b/starpilot/starpilot_process.py index 370a24d08..935bd9b03 100644 --- a/starpilot/starpilot_process.py +++ b/starpilot/starpilot_process.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +import copy import datetime import hashlib import importlib @@ -209,11 +210,12 @@ def update_checks(now, model_manager, theme_manager, thread_manager, params, par time.sleep(1) -def update_toggles(starpilot_variables, started, theme_manager, thread_manager, time_validated, params, starpilot_toggles): +def update_toggles(starpilot_variables, started, theme_manager, thread_manager, time_validated, params, starpilot_toggles, + clear_update_flag=True): previous_holiday_themes = starpilot_toggles.holiday_themes previous_random_themes = starpilot_toggles.random_themes - starpilot_variables.update(theme_manager.holiday_theme, started) + starpilot_variables.update(theme_manager.holiday_theme, started, clear_update_flag=clear_update_flag) starpilot_toggles = starpilot_variables.starpilot_toggles randomize_theme = starpilot_toggles.holiday_themes != previous_holiday_themes @@ -224,6 +226,20 @@ def update_toggles(starpilot_variables, started, theme_manager, thread_manager, return starpilot_toggles + +def update_toggles_in_background(result, starpilot_variables, started, theme_manager, thread_manager, time_validated, params, + starpilot_toggles): + """Reload toggles without mutating the values used by the planner mid-update.""" + try: + updated_variables = copy.copy(starpilot_variables) + updated_variables.starpilot_toggles = copy.copy(starpilot_toggles) + updated_toggles = update_toggles(updated_variables, started, theme_manager, thread_manager, time_validated, params, + updated_variables.starpilot_toggles, clear_update_flag=False) + result["update"] = (updated_variables, updated_toggles) + except Exception: + result["failed"] = True + raise + def starpilot_thread(): rate_keeper = Ratekeeper(1 / DT_MDL, None) @@ -249,6 +265,7 @@ def starpilot_thread(): starpilot_toggles = starpilot_variables.starpilot_toggles serialized_starpilot_toggles = serialize_starpilot_toggles(starpilot_toggles) toggle_broadcast_pending = True + toggle_update_result = {} drive_stats_session = requests.Session() next_dashboard_analysis_refresh = 0.0 @@ -355,8 +372,9 @@ def starpilot_thread(): elif current_safe_mode and (params_memory.get_bool("StarPilotTogglesUpdated") or rate_keeper.frame % SAFE_MODE_ENFORCE_FRAMES == 0): apply_safe_mode(params, params_raw, params_memory, ensure_backup=False) - if params_memory.get_bool("StarPilotTogglesUpdated") or theme_manager.theme_updated: - starpilot_toggles = update_toggles(starpilot_variables, started, theme_manager, thread_manager, time_validated, params, starpilot_toggles) + completed_toggle_update = toggle_update_result.pop("update", None) + if completed_toggle_update is not None: + starpilot_variables, starpilot_toggles = completed_toggle_update serialized_starpilot_toggles = serialize_starpilot_toggles(starpilot_toggles) toggle_broadcast_pending = True @@ -365,6 +383,22 @@ def starpilot_thread(): model_manager.randomize_selected_model() model_randomizer_previously = model_randomizer_enabled + toggle_update_result.pop("failed", None) + toggle_refresh_requested = params_memory.get_bool("StarPilotTogglesUpdated") or theme_manager.theme_updated + toggle_update_running = thread_manager.is_thread_alive("update_toggles_in_background") + if started and toggle_refresh_requested and not toggle_update_running and not toggle_update_result: + # StarPilotVariables.update performs hundreds of param reads and can exceed + # the starpilotPlan liveness timeout. Keep that work off the planner loop. + params_memory.remove("StarPilotTogglesUpdated") + thread_manager.run_with_lock( + update_toggles_in_background, + (toggle_update_result, starpilot_variables, started, theme_manager, thread_manager, time_validated, params, starpilot_toggles), + ) + elif not started and toggle_refresh_requested: + starpilot_toggles = update_toggles(starpilot_variables, started, theme_manager, thread_manager, time_validated, params, starpilot_toggles) + serialized_starpilot_toggles = serialize_starpilot_toggles(starpilot_toggles) + toggle_broadcast_pending = True + periodic_update_due = monotonic_now >= next_periodic_update_check if periodic_update_due: next_periodic_update_check = get_next_periodic_update_check(monotonic_now, periodic_update_phase)