From 887b9c9e12d91bba0ce7e8574f52eef42f45d9cc Mon Sep 17 00:00:00 2001 From: brian Date: Sun, 26 Apr 2026 09:13:41 -0500 Subject: [PATCH] parked-controlsd mode: shut down heavy stack while ignition+park MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a second controlsd variant that runs while ignition is on but the car is in Park. It only listens to CAN and publishes carState — no model, no planner, no lateral/long control, no actuator commands — so modeld, locationd, calibrationd, plannerd, radard, paramsd, torqued, dmonitoring*, soundd, loggerd all stay stopped while parked. Manager swaps between the two via mutually-exclusive predicates: - controlsd_parked: ignition AND not started - controlsd (full): started (= ignition AND not_parked) Thermald owns the swap. It already subscribes to carState; we add a new onroad condition `not_parked` derived from gearShifter, with a 1.5s hysteresis on going into parked (R/P/D thrash protection) and zero hysteresis on going out (instant wake on shift to D/R/N). At boot we assume parked so the heavy stack waits for carState to confirm gear has actually left Park. Manager predicates can only see persistent Params, not pandaStates, so thermald exposes ignition as a new IgnitionOn param (edge-written). Reverse is treated as not-parked — driver is moving. Files: - selfdrive/controls/controlsd_parked.py (new, ~50 lines) - selfdrive/thermald/thermald.py: not_parked condition + IgnitionOn - selfdrive/manager/process_config.py: parked_only predicate + entry - selfdrive/manager/manager.py: seed IgnitionOn=False - common/params.cc: register IgnitionOn The full controlsd is unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) --- common/params.cc | 1 + selfdrive/controls/controlsd_parked.py | 54 ++++++++++++++++++++++++++ selfdrive/manager/manager.py | 3 ++ selfdrive/manager/process_config.py | 11 ++++++ selfdrive/thermald/thermald.py | 36 +++++++++++++++++ 5 files changed, 105 insertions(+) create mode 100644 selfdrive/controls/controlsd_parked.py diff --git a/common/params.cc b/common/params.cc index 6b1c8d1..3e236bc 100755 --- a/common/params.cc +++ b/common/params.cc @@ -138,6 +138,7 @@ std::unordered_map keys = { {"GsmRoaming", PERSISTENT}, {"HardwareSerial", PERSISTENT}, {"HasAcceptedTerms", PERSISTENT}, + {"IgnitionOn", CLEAR_ON_MANAGER_START}, {"IMEI", PERSISTENT}, {"InstallDate", PERSISTENT}, {"IsDriverViewEnabled", CLEAR_ON_MANAGER_START}, diff --git a/selfdrive/controls/controlsd_parked.py b/selfdrive/controls/controlsd_parked.py new file mode 100644 index 0000000..d70e259 --- /dev/null +++ b/selfdrive/controls/controlsd_parked.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +""" +CLEARPILOT: minimal controlsd variant that runs while ignition is on but the +car is in Park. Keeps CAN parsing and carState publishing alive (so thermald +can see gearShifter and decide when to swap us out for the full controlsd), +but skips all of the heavy onroad work — no model, no planner, no lateral or +longitudinal control, no actuator commands. + +Manager swaps between this and the full controlsd via predicate flips: + - this runs when: ignition AND not started + - full runs when: started (which requires ignition AND not_parked) + +The two are mutually exclusive — only one publishes carState at a time. +""" +from types import SimpleNamespace + +from openpilot.common.realtime import Priority, config_realtime_process +from openpilot.selfdrive.car.card import CarD + + +def _make_default_frogpilot_variables() -> SimpleNamespace: + """Safe defaults for fields read inside CarInterface.update / CarState.update. + + We're not actuating anything here; these only need to keep the update path + from raising AttributeError. False/0 across the board is the safe baseline.""" + fv = SimpleNamespace() + fv.conditional_experimental_mode = False + fv.experimental_mode_via_distance = False + fv.traffic_mode = False + fv.sport_plus = False + fv.long_pitch = False + fv.no_lat_lane_change = False + return fv + + +def main(): + config_realtime_process(4, Priority.CTRL_HIGH) + + # CarD's __init__ blocks until it sees CAN + a pandaState, then calls get_car + # to fingerprint and write CarParams. Same path the full controlsd takes. + card = CarD() + card.initialize() + + fv = _make_default_frogpilot_variables() + + # state_update drains CAN, parses carState, publishes carState/carOutput/carParams. + # Internally blocks via drain_sock_raw(wait_for_one=True), so the loop is + # naturally paced by CAN traffic — no extra sleep needed. + while True: + card.state_update(fv) + + +if __name__ == "__main__": + main() diff --git a/selfdrive/manager/manager.py b/selfdrive/manager/manager.py index 94710fe..74584b9 100755 --- a/selfdrive/manager/manager.py +++ b/selfdrive/manager/manager.py @@ -363,6 +363,9 @@ def manager_init(frogpilot_functions) -> None: params.put("GitRemote", get_origin()) params.put_bool("IsTestedBranch", is_tested_branch()) params.put_bool("IsReleaseBranch", is_release_branch()) + # CLEARPILOT: thermald is the source of truth for IgnitionOn; seed False so + # the parked-controlsd predicate evaluates to False before thermald's first tick. + params.put_bool("IgnitionOn", False) # set dongle id reg_res = register(show_spinner=True) diff --git a/selfdrive/manager/process_config.py b/selfdrive/manager/process_config.py index 0302fb5..97ba0bc 100755 --- a/selfdrive/manager/process_config.py +++ b/selfdrive/manager/process_config.py @@ -43,6 +43,12 @@ def only_onroad(started: bool, params, CP: car.CarParams) -> bool: def only_offroad(started, params, CP: car.CarParams) -> bool: return not started +# CLEARPILOT: predicate for the parked controlsd variant. Runs while ignition +# is on but the car is in Park (so started=False because thermald has gated it +# off). Mutually exclusive with the full controlsd, which uses only_onroad. +def parked_only(started, params, CP: car.CarParams) -> bool: + return params.get_bool("IgnitionOn") and not started + # FrogPilot functions def allow_logging(started, params, CP: car.CarParams) -> bool: allow_logging = not (params.get_bool("DeviceManagement") and params.get_bool("NoLogging")) @@ -82,6 +88,11 @@ procs = [ PythonProcess("calibrationd", "selfdrive.locationd.calibrationd", only_onroad), PythonProcess("torqued", "selfdrive.locationd.torqued", only_onroad), PythonProcess("controlsd", "selfdrive.controls.controlsd", only_onroad), + # CLEARPILOT: lightweight CAN listener that runs while ignition is on and the + # car is parked. Publishes carState (so thermald can see gear); does no model, + # planner, or actuator work. Manager swaps it out for the full controlsd as + # soon as gear leaves Park. + PythonProcess("controlsd_parked", "selfdrive.controls.controlsd_parked", parked_only), PythonProcess("deleter", "system.loggerd.deleter", always_run), PythonProcess("dmonitoringd", "selfdrive.monitoring.dmonitoringd", driverview, enabled=(not PC or WEBCAM)), # PythonProcess("qcomgpsd", "system.qcomgpsd.qcomgpsd", qcomgps, enabled=TICI), diff --git a/selfdrive/thermald/thermald.py b/selfdrive/thermald/thermald.py index ac2f845..c9dd499 100755 --- a/selfdrive/thermald/thermald.py +++ b/selfdrive/thermald/thermald.py @@ -170,7 +170,17 @@ def thermald_thread(end_event, hw_queue) -> None: onroad_conditions: dict[str, bool] = { "ignition": False, + # CLEARPILOT: park-aware gating. When False, manager runs controlsd_parked + # (CAN listener only) instead of the full onroad stack. Latched + hysteresis + # on going-into-parked to avoid R↔P↔D thrash; flips out of parked instantly. + # Initialized False (assume parked) so the full stack waits for carState + # to confirm gear has actually left Park before spinning up. + "not_parked": False, } + is_parked = True + parked_since: float | None = None # monotonic ts when gear first read as Park + PARKED_HYSTERESIS_S = 1.5 + ignition_param_prev: bool | None = None startup_conditions: dict[str, bool] = {} startup_conditions_prev: dict[str, bool] = {} @@ -247,6 +257,32 @@ def thermald_thread(end_event, hw_queue) -> None: onroad_conditions["ignition"] = False cloudlog.error("panda timed out onroad") + # CLEARPILOT: derive is_parked from carState gearShifter. Whichever controlsd + # variant is currently running publishes carState; we just read the gear. + # Going INTO parked has hysteresis (PARKED_HYSTERESIS_S) so brief P touches + # during low-speed parking don't kick the heavy stack off. Going OUT of + # parked is instant so the full stack starts spinning up the moment the + # driver shifts to D/R/N. + if sm.updated['carState']: + gear = sm['carState'].gearShifter + gear_is_park = gear == car.CarState.GearShifter.park + now_mono = time.monotonic() + if gear_is_park: + if parked_since is None: + parked_since = now_mono + if (not is_parked) and (now_mono - parked_since) >= PARKED_HYSTERESIS_S: + is_parked = True + else: + parked_since = None + is_parked = False + onroad_conditions["not_parked"] = not is_parked + + # CLEARPILOT: expose ignition as a Param so manager predicates (which only + # see persistent Params, not pandaStates) can gate controlsd_parked. + if ignition_param_prev != onroad_conditions["ignition"]: + params.put_bool("IgnitionOn", onroad_conditions["ignition"]) + ignition_param_prev = onroad_conditions["ignition"] + try: last_hw_state = hw_queue.get_nowait() except queue.Empty: