park mode: pause self-driving processes, keep car ECU heartbeat

When the car is in park (ignition still on), pause everything that
isn't strictly needed and keep controlsd in a minimal-cycle keepalive
mode so the car's steering ECU keeps seeing the LFA/LKAS CAN-FD
messages and stays in tester mode (no steering fault on shift to drive).

controlsd:
- Detects park gear, writes ParkMode to /dev/shm/params.
- park_mode_tick(): publishes a do-nothing CarControl through
  self.card (CarController.update unconditionally appends the
  steering messages every cycle, which is the actual heartbeat).
  Still runs clearpilot_state_control so the LFA/debug button +
  ScreenDisplayMode keep working in park.
- After park→drive, stay in keepalive-tick mode until SubMaster
  reports all services healthy (an 8s hard cap as safety net).
  Avoids the burst of commIssue alerts that would otherwise fire
  while modeld/plannerd/paramsd/torqued/dmonitoringd/calibrationd/
  frogpilot_process spin back up from cold.

manager / process_config:
- New gating helpers _park_mode(), only_onroad_active,
  driverview_active, always_run_unless_parked.
- Re-gated to only_onroad_active: modeld, sensord, soundd, locationd,
  calibrationd, torqued, paramsd, plannerd, radard, speed_logicd.
- Re-gated to driverview_active: dmonitoringmodeld, dmonitoringd.
- frogpilot_process → always_run_unless_parked (preserves offroad
  behavior, only pauses when ignition+parked).
- controlsd stays plain only_onroad — it's the writer + heartbeat.
- ParkMode registered in params.cc, defaulted to "0" in manager_init.

thermald + fan_controller (broken-tree rule, ported):
- thermald subscribes to carState; passes standstill, is_parked,
  cruise_engaged into fan_controller.update.
- New fan range rules:
    parked        → 0-100% (no floor, full cooling)
    cruise on     → 30-100%
    standstill    → 10-100%
    moving        → 30-100%
    ignition off  → 0-30% (existing)

dashcamd: already park-aware via gear-driven trip lifecycle — no
change needed.

Verified: build clean. Launched on bench: park mode kicks in on
startup (gearShifter=unknown initially → eventually park),
modeld/plannerd/paramsd/torqued/calibrationd/frogpilot_process
correctly disappear from the manager process list, gpsd/dashcamd/
ui/controlsd/pandad stay alive, ParkMode=1 in /dev/shm/params.
This commit is contained in:
2026-05-04 19:36:38 -05:00
parent 0d1ceddad2
commit 5d18ad1e72
8 changed files with 131 additions and 20 deletions
+18 -3
View File
@@ -10,7 +10,7 @@ from pathlib import Path
import psutil
import cereal.messaging as messaging
from cereal import log
from cereal import car, log
from cereal.services import SERVICE_LIST
from openpilot.common.dict_helpers import strip_deprecated_keys
from openpilot.common.filter_simple import FirstOrderFilter
@@ -164,7 +164,8 @@ def hw_state_thread(end_event, hw_queue):
def thermald_thread(end_event, hw_queue) -> None:
pm = messaging.PubMaster(['deviceState', 'frogpilotDeviceState'])
sm = messaging.SubMaster(["peripheralState", "gpsLocationExternal", "controlsState", "pandaStates"], poll="pandaStates")
# CLEARPILOT: subscribe to carState for the fan-range gear/cruise gating
sm = messaging.SubMaster(["peripheralState", "gpsLocationExternal", "controlsState", "pandaStates", "carState"], poll="pandaStates")
count = 0
@@ -290,7 +291,21 @@ def thermald_thread(end_event, hw_queue) -> None:
msg.deviceState.maxTempC = all_comp_temp
if fan_controller is not None:
msg.deviceState.fanSpeedPercentDesired = fan_controller.update(all_comp_temp, onroad_conditions["ignition"])
# CLEARPILOT: feed gear + cruise state to widen the fan range when parked
# (0-100%, no floor) so the device can fully cool the moment it can't be
# heard over road noise. carState may not be alive when ignition is off
# or before pandad/card hand it through — default safe to "parked".
if sm.seen['carState']:
cs = sm['carState']
standstill = cs.standstill
is_parked = cs.gearShifter == car.CarState.GearShifter.park
else:
standstill = False
is_parked = True
cruise_engaged = sm['controlsState'].enabled if sm.seen['controlsState'] else False
msg.deviceState.fanSpeedPercentDesired = fan_controller.update(
all_comp_temp, onroad_conditions["ignition"], standstill,
is_parked=is_parked, cruise_engaged=cruise_engaged)
is_offroad_for_5_min = (started_ts is None) and ((not started_seen) or (off_ts is None) or (time.monotonic() - off_ts > 60 * 5))
if is_offroad_for_5_min and offroad_comp_temp > OFFROAD_DANGER_TEMP: