Files
clearpilot/selfdrive/controls/controlsd_parked.py
T
brianhansonxyz 74e7c9e627 parked-controlsd: tester-present heartbeat; cruise-set keeps full stack; dashcam idle on park
Three independent changes for the parked-controlsd architecture.

controlsd_parked: send the Hyundai HDA2 tester-present heartbeat
("\x02\x3E\x80\x00\x00\x00\x00\x00") at 1 Hz to 0x730 on E-CAN while we're
the active controlsd variant. The full carcontroller normally sends this
to keep the ADAS ECU held in its disabled diagnostic session — when full
controlsd hands off to parked-controlsd, the heartbeat used to stop, the
ECU would time out (~5 s default-session timeout) and snap back to stock,
lighting up the LKAS / blind-spot warning icons on the cluster. Continuing
the heartbeat from the parked variant keeps the ECU disabled across the
swap. The panda safety filter only allows tester-present on 0x730 so this
is the only "graceful release" mechanism available to us.

thermald: cruise-set override on the parked check. If carState.cruiseState
.speed > 0 (engaged OR paused-with-speed-set), stay in not_parked even if
gear is in P. The user can shift to park at a stop, glance at the cluster
to verify cruise is still set, and roll forward without waiting for full
controlsd to spin up. PARKED_HYSTERESIS_S still applies for the
gear-in-park-no-cruise → parked transition.

dashcamd: close the trip immediately on gear shift to PARK (was: 10-min
idle timer before close). User wants the dashcam idle in park and a fresh
trip on every drive engagement; brief drive-thru / fuel-stop across-trip
continuity isn't valued.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 14:41:44 -05:00

79 lines
3.1 KiB
Python

#!/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.
We also keep the Hyundai HDA2 tester-present heartbeat alive while parked
so the ADAS ECU doesn't snap back to default-session and light up LKAS /
blind-spot warning icons on the cluster during the swap.
"""
from types import SimpleNamespace
from openpilot.common.realtime import Priority, config_realtime_process
from openpilot.selfdrive.boardd.boardd import can_list_to_can_capnp
from openpilot.selfdrive.car.card import CarD
from openpilot.selfdrive.car.hyundai.hyundaicanfd import CanBus as HyundaiCanBus
from openpilot.selfdrive.car.hyundai.values import HyundaiFlags
# UDS Tester Present, suppressPositiveResponse — same bytes the full
# carcontroller sends every 100 frames to 0x730 on E-CAN to keep the ADAS
# ECU held in its disabled diagnostic session.
TESTER_PRESENT = b"\x02\x3E\x80\x00\x00\x00\x00\x00"
TESTER_PRESENT_PERIOD_FRAMES = 100 # ~1 Hz at the CAN-paced loop rate
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()
# Determine if this car wants the Hyundai HDA2 tester-present heartbeat,
# and which bus E-CAN is on for this panda configuration.
is_hda2 = card.CP.carName == "hyundai" and bool(card.CP.flags & HyundaiFlags.CANFD_HDA2.value)
ecan = HyundaiCanBus(card.CP).ECAN if is_hda2 else None
# 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.
frame = 0
while True:
card.state_update(fv)
if is_hda2 and frame % TESTER_PRESENT_PERIOD_FRAMES == 0:
can_sends = [[0x730, 0, TESTER_PRESENT, ecan]]
card.pm.send('sendcan', can_list_to_can_capnp(can_sends, msgtype='sendcan', valid=True))
frame += 1
if __name__ == "__main__":
main()