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: