parked-controlsd mode: shut down heavy stack while ignition+park

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) <noreply@anthropic.com>
This commit is contained in:
2026-04-26 09:13:41 -05:00
parent f7e602c00b
commit 887b9c9e12
5 changed files with 105 additions and 0 deletions
+36
View File
@@ -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: