controlsd: gate park mode behind init + 10s post-init delay

Park mode is now suppressed until two things happen:
1. self.initialized flips True (controlsd's normal init path runs and
   calls self.card.initialize() — which is what wires CarInterface up
   so controls_update actually generates CAN sends).
2. 10 s of normal step() execution after init completes.

Without (1), park_mode_tick's controls_update was a silent no-op — the
"keepalive heartbeat" never reached the car's ECU, and downstream
publishes (carState, carParams, carOutput) never went out either,
which is why the UI was stuck on the splash even after a gear shift
to drive (scene.parked reads the same carState that wasn't being
published).

The 10 s buffer (2) also gives all only_onroad_active processes a
clean cold-start window before manager starts pausing them — no
risk of catching them mid-init when ParkMode flips on.
This commit is contained in:
2026-05-04 19:48:14 -05:00
parent 5d18ad1e72
commit aac5f9d264
+24
View File
@@ -88,9 +88,18 @@ class Controls:
# exiting park, stay in keepalive-tick mode until SubMaster reports # exiting park, stay in keepalive-tick mode until SubMaster reports
# everything healthy again, with an 8-second hard cap so we never get # everything healthy again, with an 8-second hard cap so we never get
# stuck if some service is permanently broken. # stuck if some service is permanently broken.
#
# Park mode is suppressed for the first 10 seconds after init completes:
# card.initialize() runs in data_sample's not-initialized branch and is
# what hooks up CarInterface so controls_update actually generates CAN
# messages. If we entered park mode before that, the "heartbeat" would
# be silent — and only_onroad_active processes would never get a chance
# to come up cleanly the first time.
self.park_mode = False self.park_mode = False
self.park_exit_frame = -1 self.park_exit_frame = -1
self.startup_complete_frame = -1
self.PARK_GRACE_MAX_FRAMES = int(8.0 / DT_CTRL) self.PARK_GRACE_MAX_FRAMES = int(8.0 / DT_CTRL)
self.PARK_STARTUP_DELAY_FRAMES = int(10.0 / DT_CTRL)
self.radarless_model = self.params.get("Model", encoding='utf-8') in RADARLESS_MODELS self.radarless_model = self.params.get("Model", encoding='utf-8') in RADARLESS_MODELS
@@ -1259,9 +1268,24 @@ class Controls:
if (len(CS.buttonEvents) > 0): if (len(CS.buttonEvents) > 0):
print (CS.buttonEvents) print (CS.buttonEvents)
def _park_mode_allowed(self):
# Don't allow park mode until init has completed AND we've run at least
# 10s of normal step() after init. card.initialize() (which hooks
# CarInterface up to send CAN) only runs on the init path, so we need
# to take that path at least once before short-circuiting into the
# minimal tick. The 10s buffer also lets only_onroad_active processes
# come up cleanly the first time before we ask manager to pause them.
if not self.initialized:
return False
if self.startup_complete_frame < 0:
self.startup_complete_frame = self.sm.frame
return (self.sm.frame - self.startup_complete_frame) >= self.PARK_STARTUP_DELAY_FRAMES
def _update_park_mode(self, CS): def _update_park_mode(self, CS):
# CLEARPILOT: track the gear-park flag and write ParkMode for manager. # CLEARPILOT: track the gear-park flag and write ParkMode for manager.
# On park→drive transition, capture the frame so the grace window starts. # On park→drive transition, capture the frame so the grace window starts.
if not self._park_mode_allowed():
return
in_park = CS.gearShifter == GearShifter.park in_park = CS.gearShifter == GearShifter.park
if in_park != self.park_mode: if in_park != self.park_mode:
self.park_mode = in_park self.park_mode = in_park