fix: fan 100% on overheat, FCW fps-aware, commIssue suppress, 10min shutdown

- Fan controller: allow full 100% fan when offroad temp >= 75°C (startup cooling)
- ModelFps memory param: modeld publishes actual FPS (20 or 4) so downstream
  consumers can adjust frame-rate-dependent logic
- Longitudinal planner: dynamically adjusts dt and v_desired_filter based on
  ModelFps; FCW crash_cnt threshold scales with FPS to maintain consistent
  0.15s trigger window at both 20fps and 4fps
- controlsd: suppress commIssue alerts for 2s after lateral control engages
  (FPS transition from 4->20 causes transient freq check failures)
- Shutdown timer: hardcoded to 10 minutes (was 45min via FrogPilot param),
  screen taps reset the countdown via ShutdownTouchReset memory param,
  removed Shutdown Timer UI selector from ClearPilot menu

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-16 16:29:42 -05:00
parent 2331aa00a0
commit 1eb8d41454
12 changed files with 62 additions and 38 deletions

View File

@@ -194,12 +194,14 @@ std::unordered_map<std::string, uint32_t> keys = {
{"RecordFrontLock", PERSISTENT}, // for the internal fleet
{"ReplayControlsState", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION},
{"RouteCount", PERSISTENT},
{"ShutdownTouchReset", PERSISTENT},
{"SnoozeUpdate", CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION},
{"SshEnabled", PERSISTENT},
{"TermsVersion", PERSISTENT},
{"TelemetryEnabled", PERSISTENT},
{"Timezone", PERSISTENT},
{"TrainingVersion", PERSISTENT},
{"ModelFps", PERSISTENT},
{"ModelStandby", PERSISTENT},
{"ModelStandbyTs", PERSISTENT},
{"UbloxAvailable", PERSISTENT},

Binary file not shown.

View File

@@ -170,6 +170,8 @@ class Controls:
self.current_alert_types = [ET.PERMANENT]
self.logged_comm_issue = None
self.not_running_prev = None
self.last_lat_engage_ts = 0 # CLEARPILOT: timestamp of most recent lat engage transition
self.prev_lat_active = False
self.steer_limited = False
self.desired_curvature = 0.0
self.experimental_mode = False
@@ -455,7 +457,16 @@ class Controls:
standby_ts = float(self.params_memory.get("ModelStandbyTs") or "0")
except (ValueError, TypeError):
standby_ts = 0
model_suppress = (time.monotonic() - standby_ts) < 2.0
now = time.monotonic()
model_suppress = (now - standby_ts) < 2.0
# CLEARPILOT: detect lat engage rising edge and suppress commIssue for 2s after
# Model FPS jumps from 4 to 20 on engage, causing transient freq check failures
lat_active_now = self.CC.latActive
if lat_active_now and not self.prev_lat_active:
self.last_lat_engage_ts = now
self.prev_lat_active = lat_active_now
lat_engage_suppress = (now - self.last_lat_engage_ts) < 2.0
not_running = {p.name for p in self.sm['managerState'].processes if not p.running and p.shouldBeRunning}
if self.sm.recv_frame['managerState'] and (not_running - IGNORE_PROCESSES):
@@ -486,7 +497,7 @@ class Controls:
# generic catch-all. ideally, a more specific event should be added above instead
has_disable_events = self.events.contains(ET.NO_ENTRY) and (self.events.contains(ET.SOFT_DISABLE) or self.events.contains(ET.IMMEDIATE_DISABLE))
no_system_errors = (not has_disable_events) or (len(self.events) == num_events)
if (not self.sm.all_checks() or self.card.can_rcv_timeout) and no_system_errors and not model_suppress:
if (not self.sm.all_checks() or self.card.can_rcv_timeout) and no_system_errors and not model_suppress and not lat_engage_suppress:
if not self.sm.all_alive():
self.events.add(EventName.commIssue)
elif not self.sm.all_freq_ok():

View File

@@ -143,6 +143,9 @@ class LongitudinalPlanner:
self.params = Params()
self.params_memory = Params("/dev/shm/params")
# CLEARPILOT: track model FPS for dynamic dt adjustment
self.model_fps = 20
self.radarless_model = self.params.get("Model", encoding='utf-8') in RADARLESS_MODELS
self.release = get_short_branch() == "clearpilot"
@@ -173,6 +176,19 @@ class LongitudinalPlanner:
return x, v, a, j
def update(self, sm):
# CLEARPILOT: read actual model FPS and adjust dt accordingly
model_fps_raw = self.params_memory.get("ModelFps")
if model_fps_raw is not None:
try:
fps = int(model_fps_raw)
if fps > 0 and fps != self.model_fps:
self.model_fps = fps
self.dt = 1.0 / fps
self.v_desired_filter.dt = self.dt
self.v_desired_filter.update_alpha(2.0) # rc=2.0, same as __init__
except (ValueError, ZeroDivisionError):
pass
self.mpc.mode = 'blended' if sm['controlsState'].experimentalMode else 'acc'
v_ego = sm['carState'].vEgo
@@ -239,7 +255,10 @@ class LongitudinalPlanner:
self.j_desired_trajectory = np.interp(ModelConstants.T_IDXS[:CONTROL_N], T_IDXS_MPC[:-1], self.mpc.j_solution)
# TODO counter is only needed because radar is glitchy, remove once radar is gone
self.fcw = self.mpc.crash_cnt > 2 and not sm['carState'].standstill
# CLEARPILOT: scale crash_cnt threshold by FPS — at 20fps need 3 frames (0.15s),
# at 4fps need 1 frame (0.25s already exceeds 0.15s window)
fcw_threshold = max(0, int(0.15 * self.model_fps) - 1) # 20fps->2, 4fps->0
self.fcw = self.mpc.crash_cnt > fcw_threshold and not sm['carState'].standstill
if self.fcw:
cloudlog.info("FCW triggered")

View File

@@ -89,7 +89,9 @@ def manager_init(frogpilot_functions) -> None:
params_memory.put("VpnEnabled", "1")
params_memory.put("DashcamFrames", "0")
params_memory.put("DashcamState", "stopped")
params_memory.put("ModelFps", "20")
params_memory.put("ModelStandby", "0")
params_memory.put("ShutdownTouchReset", "0")
params_memory.put("ModelStandbyTs", "0")
params_memory.put("CarIsMetric", "0")
params_memory.put("ClearpilotSpeedDisplay", "")

View File

@@ -275,6 +275,8 @@ def main(demo=False):
# Skip 4 out of every 5 frames (20fps -> 4fps)
# Write standby timestamp so controlsd suppresses transient errors
if not full_rate:
if params_memory.get("ModelFps") != b"4":
params_memory.put("ModelFps", "4")
now = _time.monotonic()
if now - last_standby_ts_write > 1.0:
params_memory.put("ModelStandbyTs", str(now))
@@ -283,6 +285,9 @@ def main(demo=False):
last_vipc_frame_id = meta_main.frame_id
run_count += 1
continue
else:
if params_memory.get("ModelFps") != b"20":
params_memory.put("ModelFps", "20")
desire = DH.desire
is_rhd = sm["driverMonitoringState"].isRHD

View File

@@ -39,6 +39,10 @@ class TiciFanController(BaseFanController):
elif ignition:
self.controller.neg_limit = -100
self.controller.pos_limit = -15
# CLEARPILOT: offroad but overheating (startup cooling) — full fan range
elif cur_temp >= 75:
self.controller.neg_limit = -100
self.controller.pos_limit = 0
else:
self.controller.neg_limit = -30
self.controller.pos_limit = 0

View File

@@ -36,12 +36,9 @@ class PowerMonitoring:
# Reset capacity if it's low
self.car_battery_capacity_uWh = max((CAR_BATTERY_CAPACITY_uWh / 10), int(car_battery_capacity_uWh))
# FrogPilot variables
device_management = self.params.get_bool("DeviceManagement")
device_shutdown_setting = self.params.get_int("DeviceShutdown") if device_management else 33
# If the toggle is set for < 1 hour, configure by 15 minute increments
self.device_shutdown_time = (device_shutdown_setting - 3) * 3600 if device_shutdown_setting >= 4 else device_shutdown_setting * (60 * 15)
self.low_voltage_shutdown = self.params.get_float("LowVoltageShutdown") if device_management else VBATT_PAUSE_CHARGING
# CLEARPILOT: hardcoded 10 minute shutdown timer (not user-configurable)
self.device_shutdown_time = 600
self.low_voltage_shutdown = VBATT_PAUSE_CHARGING
# Calculation tick
def calculate(self, voltage: int | None, ignition: bool):

View File

@@ -197,7 +197,9 @@ def thermald_thread(end_event, hw_queue) -> None:
engaged_prev = False
params = Params()
params_memory = Params("/dev/shm/params")
power_monitor = PowerMonitoring()
last_touch_reset = "0" # CLEARPILOT: track last seen touch reset value
HARDWARE.initialize_hardware()
thermal_config = HARDWARE.get_thermal_config()
@@ -409,6 +411,13 @@ def thermald_thread(end_event, hw_queue) -> None:
statlog.sample("som_power_draw", som_power_draw)
msg.deviceState.somPowerDrawW = som_power_draw
# CLEARPILOT: screen tap resets shutdown timer (off_ts) while offroad
touch_reset = params_memory.get("ShutdownTouchReset")
if touch_reset is not None and touch_reset != last_touch_reset and off_ts is not None:
off_ts = time.monotonic()
cloudlog.info("shutdown timer reset by screen touch")
last_touch_reset = touch_reset
# Check if we need to shut down
if power_monitor.should_shutdown(onroad_conditions["ignition"], in_car, off_ts, started_seen):
cloudlog.warning(f"shutting device down, offroad since {off_ts}")

View File

@@ -179,13 +179,6 @@ static const char *clpSidebarBtnStyle = R"(
// clpActionBtnStyle removed — no longer used
// Shutdown timer: param value -> display label
static QString shutdownLabel(int val) {
if (val == 0) return "5 mins";
if (val <= 3) return QString::number(val * 15) + " mins";
int hours = val - 3;
return QString::number(hours) + (hours == 1 ? " hour" : " hours");
}
ClearPilotPanel::ClearPilotPanel(QWidget* parent) : QFrame(parent) {
// Sidebar
@@ -256,27 +249,6 @@ ClearPilotPanel::ClearPilotPanel(QWidget* parent) : QFrame(parent) {
});
general_panel->addItem(resetCalibBtn);
// Shutdown Timer
int cur_shutdown = Params().getInt("DeviceShutdown");
auto shutdownBtn = new ButtonControl("Shutdown Timer", shutdownLabel(cur_shutdown),
"How long the device stays on after the car is turned off.");
connect(shutdownBtn, &ButtonControl::clicked, [=]() {
QStringList options;
for (int i = 0; i <= 33; i++) {
options << shutdownLabel(i);
}
int current = Params().getInt("DeviceShutdown");
QString sel = MultiOptionDialog::getSelection("Shutdown Timer", options, shutdownLabel(current), this);
if (!sel.isEmpty()) {
int idx = options.indexOf(sel);
if (idx >= 0) {
Params().putInt("DeviceShutdown", idx);
shutdownBtn->setValue(shutdownLabel(idx));
}
}
});
general_panel->addItem(shutdownBtn);
// Power buttons
QHBoxLayout *power_layout = new QHBoxLayout();
power_layout->setSpacing(30);

Binary file not shown.

View File

@@ -172,12 +172,15 @@ bool MainWindow::eventFilter(QObject *obj, QEvent *event) {
case QEvent::MouseButtonPress:
case QEvent::MouseMove: {
// CLEARPILOT: tap while screen-off (mode 3) -> wake to auto-normal (mode 0)
if (!device()->isAwake()) {
Params pmem{"/dev/shm/params"};
if (!device()->isAwake()) {
if (pmem.getInt("ScreenDisplayMode") == 3) {
pmem.putInt("ScreenDisplayMode", 0);
}
}
// CLEARPILOT: reset shutdown timer on any screen touch
static int touch_counter = 0;
pmem.put("ShutdownTouchReset", std::to_string(++touch_counter));
// ignore events when device is awakened by resetInteractiveTimeout
ignore = !device()->isAwake();
device()->resetInteractiveTimeout(uiState()->scene.screen_timeout, uiState()->scene.screen_timeout_onroad);