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
+26 -4
View File
@@ -8,7 +8,8 @@ from openpilot.selfdrive.controls.lib.pid import PIDController
class BaseFanController(ABC):
@abstractmethod
def update(self, cur_temp: float, ignition: bool) -> int:
def update(self, cur_temp: float, ignition: bool, standstill: bool = False,
is_parked: bool = True, cruise_engaged: bool = False) -> int:
pass
@@ -20,9 +21,30 @@ class TiciFanController(BaseFanController):
self.last_ignition = False
self.controller = PIDController(k_p=0, k_i=4e-3, k_f=1, rate=(1 / DT_TRML))
def update(self, cur_temp: float, ignition: bool) -> int:
self.controller.neg_limit = -(100 if ignition else 30)
self.controller.pos_limit = -(30 if ignition else 0)
def update(self, cur_temp: float, ignition: bool, standstill: bool = False,
is_parked: bool = True, cruise_engaged: bool = False) -> int:
# CLEARPILOT fan range rules. neg_limit is the most-negative the PID can
# output (= max fan %); pos_limit is the inverse of the floor (= -min fan %).
# parked → 0-100% (full range, no floor)
# in drive + cruise engaged → 30-100%
# in drive + cruise off + standstill → 10-100%
# in drive + cruise off + moving → 30-100%
# ignition off → 0-30% (existing behavior)
if not ignition:
self.controller.neg_limit = -30
self.controller.pos_limit = 0
elif is_parked:
self.controller.neg_limit = -100
self.controller.pos_limit = 0
elif cruise_engaged:
self.controller.neg_limit = -100
self.controller.pos_limit = -30
elif standstill:
self.controller.neg_limit = -100
self.controller.pos_limit = -10
else:
self.controller.neg_limit = -100
self.controller.pos_limit = -30
if ignition != self.last_ignition:
self.controller.reset()