diff --git a/common/params.cc b/common/params.cc index cce31fb..8e31dba 100755 --- a/common/params.cc +++ b/common/params.cc @@ -261,6 +261,7 @@ std::unordered_map keys = { {"DashcamState", PERSISTENT}, {"IsDaylight", PERSISTENT}, {"ModelFps", PERSISTENT}, + {"ParkMode", PERSISTENT}, {"ModelStandby", PERSISTENT}, {"ModelStandbyTs", PERSISTENT}, {"ScreenRecorderDebug", PERSISTENT}, diff --git a/selfdrive/clearpilot/dashcamd b/selfdrive/clearpilot/dashcamd index da98ac4..d664a93 100755 Binary files a/selfdrive/clearpilot/dashcamd and b/selfdrive/clearpilot/dashcamd differ diff --git a/selfdrive/controls/controlsd.py b/selfdrive/controls/controlsd.py index b7b8d2e..5e91138 100755 --- a/selfdrive/controls/controlsd.py +++ b/selfdrive/controls/controlsd.py @@ -78,8 +78,19 @@ class Controls: self.params_memory.put_bool("CPTLkasButtonAction", False) self.params_memory.put_int("ScreenDisplayMode", 0) + self.params_memory.put_bool("ParkMode", False) # CLEARPILOT: edge tracking for park→drive auto-wake of screen self.was_driving_gear = False + # CLEARPILOT: park-mode bookkeeping. While ParkMode is true, controlsd + # only does a minimal tick (carControl heartbeat through card so the + # hyundai CAN-FD steering messages keep the car ECU's tester mode alive) + # and writes ParkMode so manager pauses modeld/locationd/etc. After + # exiting park, stay in keepalive-tick mode until SubMaster reports + # everything healthy again, with an 8-second hard cap so we never get + # stuck if some service is permanently broken. + self.park_mode = False + self.park_exit_frame = -1 + self.PARK_GRACE_MAX_FRAMES = int(8.0 / DT_CTRL) self.radarless_model = self.params.get("Model", encoding='utf-8') in RADARLESS_MODELS @@ -237,6 +248,16 @@ class Controls: CS = self.data_sample() cloudlog.timestamp("Data sampled") + # CLEARPILOT: park mode — short-circuit to a minimal tick (just publish + # the carControl heartbeat to keep the car's ECU in tester mode). Also + # covers the post-park grace window so commIssue events don't flash + # while modeld / locationd / paramsd / etc. spin back up. + self._update_park_mode(CS) + if self.park_mode or self._in_park_exit_grace(): + self._park_mode_tick(CS) + self.CS_prev = CS + return + self.update_events(CS) self.update_frogpilot_events(CS) self.update_clearpilot_events(CS) @@ -1238,6 +1259,38 @@ class Controls: if (len(CS.buttonEvents) > 0): print (CS.buttonEvents) + def _update_park_mode(self, CS): + # CLEARPILOT: track the gear-park flag and write ParkMode for manager. + # On park→drive transition, capture the frame so the grace window starts. + in_park = CS.gearShifter == GearShifter.park + if in_park != self.park_mode: + self.park_mode = in_park + self.params_memory.put_bool("ParkMode", in_park) + if not in_park: + self.park_exit_frame = self.sm.frame + + def _in_park_exit_grace(self): + # CLEARPILOT: after exiting park, keep doing the minimal tick until the + # restarting modeld/locationd/etc. are publishing again, so they don't + # trip a burst of commIssue alerts. Exit grace as soon as SubMaster + # reports everything healthy; otherwise hold for up to 8 seconds as a + # safety cap (avoids getting stuck if a service is permanently broken). + if self.park_exit_frame < 0: + return False + if (self.sm.frame - self.park_exit_frame) >= self.PARK_GRACE_MAX_FRAMES: + return False + return not (self.sm.all_checks() and not self.card.can_rcv_timeout) + + def _park_mode_tick(self, CS): + # CLEARPILOT: minimal-cycle keepalive. Publishes a do-nothing carControl + # via card → CarController.update → sendcan, which sends the LFA/LKAS + # CAN-FD messages every cycle. Those send unconditionally regardless of + # CC.enabled / latActive, so the car's steering ECU keeps seeing the + # heartbeat and stays in tester mode. + CC = car.CarControl.new_message() + self.clearpilot_state_control(CC, CS) + self.card.controls_update(CC, self.frogpilot_variables) + def clearpilot_state_control(self, CC, CS): # CLEARPILOT: pure UI plumbing — does not modify CC/actuators. Maintains # ScreenDisplayMode (5-state machine driven by the LFA/debug button + gear diff --git a/selfdrive/manager/manager.py b/selfdrive/manager/manager.py index 101f65d..6e5e50d 100755 --- a/selfdrive/manager/manager.py +++ b/selfdrive/manager/manager.py @@ -321,6 +321,7 @@ def manager_init(frogpilot_functions) -> None: ("ModelFps", "20"), ("ModelStandby", "0"), ("ModelStandbyTs", "0"), + ("ParkMode", "0"), ("ShutdownTouchReset", "0"), ("TelemetryEnabled", "0"), ("VpnEnabled", "1"), diff --git a/selfdrive/manager/process_config.py b/selfdrive/manager/process_config.py index eb8cacc..085f6d3 100755 --- a/selfdrive/manager/process_config.py +++ b/selfdrive/manager/process_config.py @@ -39,6 +39,25 @@ def always_run(started, params, CP: car.CarParams) -> bool: def only_onroad(started: bool, params, CP: car.CarParams) -> bool: return started +# CLEARPILOT: park-mode gating. controlsd writes ParkMode to /dev/shm/params +# when the car is parked (ignition still on). Manager's ensure_running loop +# reads these gates every tick and stops/starts processes naturally as the +# flag flips. controlsd itself stays on plain `started` (it's the one +# writing the flag and emitting the steering keepalive to the car's ECU). +def _park_mode() -> bool: + return Params("/dev/shm/params").get_bool("ParkMode") + +def only_onroad_active(started: bool, params, CP: car.CarParams) -> bool: + return started and not _park_mode() + +def driverview_active(started: bool, params, CP: car.CarParams) -> bool: + return driverview(started, params, CP) and not _park_mode() + +def always_run_unless_parked(started, params, CP: car.CarParams) -> bool: + # Same as always_run, but pauses while ignition is on and the car is parked. + # Preserves offroad behavior; only changes the started+parked case. + return not (started and _park_mode()) + def only_offroad(started, params, CP: car.CarParams) -> bool: return not started @@ -63,26 +82,26 @@ procs = [ PythonProcess("micd", "system.micd", iscar), PythonProcess("timed", "system.timed", always_run, enabled=not PC), - PythonProcess("dmonitoringmodeld", "selfdrive.modeld.dmonitoringmodeld", driverview, enabled=(not PC or WEBCAM)), + PythonProcess("dmonitoringmodeld", "selfdrive.modeld.dmonitoringmodeld", driverview_active, enabled=(not PC or WEBCAM)), # CLEARPILOT: disabled segment + camera logging — no rlog/qlog or .hevc # files written to /data/media/0/realdata. We don't use comma's upload/ # replay pipeline. Keep deleter running for any leftover cleanup. # NativeProcess("encoderd", "system/loggerd", ["./encoderd"], allow_logging), # NativeProcess("stream_encoderd", "system/loggerd", ["./encoderd", "--stream"], notcar), # NativeProcess("loggerd", "system/loggerd", ["./loggerd"], allow_logging), - NativeProcess("modeld", "selfdrive/modeld", ["./modeld"], only_onroad), + NativeProcess("modeld", "selfdrive/modeld", ["./modeld"], only_onroad_active), #NativeProcess("mapsd", "selfdrive/navd", ["./mapsd"], only_onroad), #PythonProcess("navmodeld", "selfdrive.modeld.navmodeld", only_onroad), - NativeProcess("sensord", "system/sensord", ["./sensord"], only_onroad, enabled=not PC), + NativeProcess("sensord", "system/sensord", ["./sensord"], only_onroad_active, enabled=not PC), NativeProcess("ui", "selfdrive/ui", ["./ui"], always_run, watchdog_max_dt=(5 if not PC else None), always_watchdog=only_offroad), - PythonProcess("soundd", "selfdrive.ui.soundd", only_onroad), - NativeProcess("locationd", "selfdrive/locationd", ["./locationd"], only_onroad), + PythonProcess("soundd", "selfdrive.ui.soundd", only_onroad_active), + NativeProcess("locationd", "selfdrive/locationd", ["./locationd"], only_onroad_active), NativeProcess("boardd", "selfdrive/boardd", ["./boardd"], always_run, enabled=False), - PythonProcess("calibrationd", "selfdrive.locationd.calibrationd", only_onroad), - PythonProcess("torqued", "selfdrive.locationd.torqued", only_onroad), + PythonProcess("calibrationd", "selfdrive.locationd.calibrationd", only_onroad_active), + PythonProcess("torqued", "selfdrive.locationd.torqued", only_onroad_active), PythonProcess("controlsd", "selfdrive.controls.controlsd", only_onroad), PythonProcess("deleter", "system.loggerd.deleter", always_run), - PythonProcess("dmonitoringd", "selfdrive.monitoring.dmonitoringd", driverview, enabled=(not PC or WEBCAM)), + PythonProcess("dmonitoringd", "selfdrive.monitoring.dmonitoringd", driverview_active, enabled=(not PC or WEBCAM)), # PythonProcess("qcomgpsd", "system.qcomgpsd.qcomgpsd", qcomgps, enabled=TICI), # Fixme # CLEARPILOT: replacement for qcomgpsd (whose diag interface is broken on this device). # Uses Quectel modem AT commands via mmcli. Self-driving does NOT consume this; locationd @@ -90,18 +109,18 @@ procs = [ PythonProcess("gpsd", "system.clearpilot.gpsd", qcomgps, enabled=TICI), # CLEARPILOT: speed/cruise overlay daemon. Reads gpsLocation + carState, writes display # params for the onroad UI; asserts ClearpilotPlayDing on speed-limit warning transitions. - PythonProcess("speed_logicd", "selfdrive.clearpilot.speed_logicd", only_onroad), + PythonProcess("speed_logicd", "selfdrive.clearpilot.speed_logicd", only_onroad_active), # CLEARPILOT: dashcam — VisionIPC frames → OMX H.264 → 3-min MP4 segments + SRT # GPS subtitles in /data/media/0/videos/. Manages its own trip lifecycle. NativeProcess("dashcamd", "selfdrive/clearpilot", ["./dashcamd"], always_run), # PythonProcess("ugpsd", "system.ugpsd", only_onroad, enabled=TICI), #PythonProcess("navd", "selfdrive.navd.navd", only_onroad), PythonProcess("pandad", "selfdrive.boardd.pandad", always_run), - PythonProcess("paramsd", "selfdrive.locationd.paramsd", only_onroad), + PythonProcess("paramsd", "selfdrive.locationd.paramsd", only_onroad_active), NativeProcess("ubloxd", "system/ubloxd", ["./ubloxd"], ublox, enabled=TICI), PythonProcess("pigeond", "system.ubloxd.pigeond", ublox, enabled=TICI), - PythonProcess("plannerd", "selfdrive.controls.plannerd", only_onroad), - PythonProcess("radard", "selfdrive.controls.radard", only_onroad), + PythonProcess("plannerd", "selfdrive.controls.plannerd", only_onroad_active), + PythonProcess("radard", "selfdrive.controls.radard", only_onroad_active), PythonProcess("thermald", "selfdrive.thermald.thermald", always_run), PythonProcess("tombstoned", "selfdrive.tombstoned", always_run, enabled=not PC), # PythonProcess("updated", "selfdrive.updated.updated", always_run, enabled=not PC), @@ -115,7 +134,7 @@ procs = [ # FrogPilot processes PythonProcess("fleet_manager", "selfdrive.frogpilot.fleetmanager.fleet_manager", always_run), - PythonProcess("frogpilot_process", "selfdrive.frogpilot.frogpilot_process", always_run), + PythonProcess("frogpilot_process", "selfdrive.frogpilot.frogpilot_process", always_run_unless_parked), ] managed_processes = {p.name: p for p in procs} \ No newline at end of file diff --git a/selfdrive/thermald/fan_controller.py b/selfdrive/thermald/fan_controller.py index 19c3292..6b26eb5 100755 --- a/selfdrive/thermald/fan_controller.py +++ b/selfdrive/thermald/fan_controller.py @@ -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() diff --git a/selfdrive/thermald/thermald.py b/selfdrive/thermald/thermald.py index ddebd87..b8bb9c4 100755 --- a/selfdrive/thermald/thermald.py +++ b/selfdrive/thermald/thermald.py @@ -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: diff --git a/selfdrive/ui/qt/spinner b/selfdrive/ui/qt/spinner index aa5dbb7..c45eb09 100755 Binary files a/selfdrive/ui/qt/spinner and b/selfdrive/ui/qt/spinner differ