22 Commits

Author SHA1 Message Date
brianhansonxyz cea422b075 locationd: ignore GPS as Kalman input; expand park-cached-output to onroad consumers
locationd: ignore gpsLocation observations entirely (clearpilot_disable_gps
const at the top of handle_gps; falls through to determine_gps_mode's
no-GPS path). gpsd.py keeps publishing real GPS for UI / dashcam / clock
/ night-mode — only locationd ignores it. The previous gpsd.py path was
hard-coding vNED=[0,0,0] while the car was moving 28 m/s, feeding the
Kalman contradictory GPS-vs-IMU velocity observations that propagated into
latcontrol_torque through liveLocationKalman.angularVelocityCalibrated and
caused a real "drift right on straight roads" symptom.

controlsd: short-circuit state_control() while parked. Skips LaC/LoC PID,
MPC, model_v2 reads, lane-change logic — all wasted work when the car
isn't moving. Returns the same enabled=False/latActive=False/longActive=False
CC the original code would have produced, so publish_logs runs unchanged
and card.controls_update keeps the sendcan / tester-present heartbeat
flowing at 100Hz. controlsd CPU dropped from ~59% to ~3% in park.

controlsd: also wire self.FPCC.noLatLaneChange = True/False in the
existing lane-change suppression branch. The Hyundai carcontroller already
reads off frogpilot_variables.no_lat_lane_change for the no-steer signal,
but the UI's distinctive yellow CHANGE_LANE_PATH_COLOR (onroad.cc:901)
reads from frogpilotCarControl.NoLatLaneChange — which nobody was setting,
so the yellow line never appeared during lane-change suppression.

plannerd: skip longitudinal_planner.update + publish + publish_ui_plan
while parked. Downstream controlsd already short-circuits in park, so
longitudinalPlan / uiPlan staleness is fine.

frogpilot_process: same idea — skip frogpilot_planner.update + publish
while parked.

dmonitoringmodeld: mirror the cached-output trick from modeld. Add carState
to the SubMaster, cache the last (model_output, dsp_execution_time) tuple,
and serve it on every frame while gear is in park instead of running DSP
inference on the driver camera.

Together with the existing modeld park-cached-output, the heavy onroad
processing pipeline (modeld → plannerd → controlsd → carcontroller) now
all idles in park and only fires when the car leaves park.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 12:12:38 -05:00
brianhansonxyz dc7e0a2db7 controlsd+calibrationd: suppress commIssue from valid=False cascade
Two fixes that reinstate the pre-revert defenses against the "TAKE CONTROL
IMMEDIATELY / Communication Issue" banner that fires when self-driving
on the baseline modelrevert stack:

calibrationd: publish valid based on calStatus == calibrated, not
sm.all_checks(). Original gate cascaded upstream freq glitches into
liveCalibration.valid=False, which kept locationd.filterInitialized
False, which fed garbage into paramsd, which corrupted steerRatio
(erratic steering). "valid" here is a question about convergence, not
input freshness.

controlsd: narrow the commIssue trigger to genuine comm failures —
not_alive OR can_rcv_timeout. The `not sm.all_checks()` branch also
picked up valid=False, but paramsd / torqued / plannerd / frogpilot_planner
/ dmonitoringd all propagate their sm.all_checks() into msg.valid via
a polling-pattern artifact (freq_ok inside poll='...' subscribers
tracks gaps between drain bursts rather than the publish rate), so
the whole stack flaps valid and trips the banner during normal
driving. Content and rate are fine; just the flag.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 11:00:45 -05:00
brianhansonxyz f896dfe25a modeld: cached-output standby while gear is in park
Narrow re-introduction of the power-save mode: when carState reports
gearShifter == park, serve the last rendered model_output instead of
running GPU inference. First cycle (or before carState is flowing)
still runs one real inference so we have something to cache and serve.
Out-of-park returns to per-frame inference immediately.

Avoids the pitfall of the earlier variable-rate path: this doesn't
change the publish rate on modelV2/cameraOdometry (we still publish
every frame), so downstream freq_ok / valid checks in calibrationd /
locationd / paramsd stay happy. Only the GPU inference is skipped.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 10:49:44 -05:00
brianhansonxyz 2ce6e8fe0c modelrevert: boot fixes + strip remaining variable-rate cruft
controlsd:
- stop writing the removed 'no_lat_lane_change' persistent param
  (key isn't in the whitelist anymore). Write to
  frogpilot_variables.no_lat_lane_change instead so the carcontroller
  read still works and the lane-change-disengage feature is preserved.
- initialize self.driving_gear = False in __init__; it's set at the
  end of step() in update_frogpilot_variables but clearpilot_state_control
  reads it on the first iteration before that's run.

modeld:
- remove the remaining CLEARPILOT variable-rate / standby code path
  that was baked into the initial-import commit (the one our first
  commit captured). Constant 20fps, no ModelStandby/ModelStandbyTs
  writes, no params_memory reads for 'no_lat_lane_change' (which
  isn't registered anymore).
- drop now-unused locals (model_standby, last_standby_ts_write,
  params_memory).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 10:42:11 -05:00
brianhansonxyz 3bd0c942e8 controlsd: re-apply QoL hooks on top of reverted baseline
Re-adds the non-self-drive controlsd integrations that the UI and
memory-param pipeline need:

- import SpeedState from clearpilot.speed_logic
- self.speed_state, speed_state_frame, was_driving_gear init
- subscribe to gpsLocation (ignored in alive/freq_ok/valid gates — OK if
  the GPS daemon isn't publishing)
- clearpilot_state_control:
  - auto-reset ScreenDisplayMode 3→0 on park→drive transition
  - ~2Hz speed-state update driving the speed-limit sign and cruise
    over/under warning sign via ClearpilotCruiseWarning /
    ClearpilotSpeedLimitDisplay memory params

The debug-button (LFA) ScreenDisplayMode cycling already lived in the
reverted baseline (it was in the first commit), so it's preserved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 13:31:51 -05:00
brianhansonxyz 12da9acfdd revert modeld/controlsd/plannerd/locationd to first-commit baseline
Starting point for rebuilding self-drive from a known-good baseline.
Reverts the following to their state at f46339c:
- selfdrive/modeld/modeld.py (constant 20fps, no variable-rate / standby skip)
- selfdrive/modeld/dmonitoringmodeld.py (no carState sub, no standstill skip)
- selfdrive/controls/controlsd.py (no parked-cycle skip, no FPCC hoisting, no MDPS split)
- selfdrive/controls/lib/longitudinal_planner.py
- selfdrive/locationd/calibrationd.py (valid = sm.all_checks again)
- selfdrive/locationd/paramsd.py
- selfdrive/locationd/torqued.py

All non-self-drive features (thermald fan control, speed limit controller,
cruise warning signs, UI state transitions, GPS fixes, ClearPilot menu,
dashcamd, telemetry, etc.) remain as-is on this branch — only the 4 core
self-drive processes are reverted.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 13:26:36 -05:00
brianhansonxyz 62a403d0f1 UI: shift speed-limit/cruise numbers down; modeld: keep 20fps at standstill-in-drive; health: FPS instead of LAG
prebuilt / build prebuilt (push) Has been cancelled
badges / create badges (push) Has been cancelled
- onroad: speed-limit sign and cruise over/under sign — shift the number
  ~10% further down inside the inner box (adjusted top inset 42→86).
- modeld: narrow the standby condition. Previously 0fps when
  standstill-or-parked; now 0fps only when parked. Standstill in drive
  (red light) continues to run at 20fps so lateral can engage/stay
  responsive and liveCalibration/paramsd keep seeing observations.
  Ignition-off still stops modeld at the manager level.
- Health overlay: replace LAG row with FPS (modeld framerate read from
  ModelFps memory param, which modeld already writes only on standby
  transition — no per-frame writes).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 12:54:56 -05:00
brianhansonxyz 7ee923b0e6 calibrationd: publish valid based on calStatus, not sm.all_checks
prebuilt / build prebuilt (push) Has been cancelled
badges / create badges (push) Has been cancelled
stale / stale (push) Has been cancelled
release / build master-ci (push) Has been cancelled
Previous behavior gated liveCalibration.valid on calibrationd's own
sm.all_checks(). Upstream freq glitches (e.g. carState polling-pattern
artifacts) flapped liveCalibration.valid to False, which cascaded into
locationd: its filterInitialized check requires sm.allAliveAndValid(),
so flapped valid kept locationd uninitialized. While uninitialized,
locationd still published liveLocationKalman but with empty/garbage
angularVelocityCalibrated fields. paramsd's Kalman drank the garbage
and converged to steerRatio ≈ 0, stiffnessFactor ≈ 0 — which
controlsd clamped to 0.1 each and fed into VM.calc_curvature,
producing nonsense curvature commands and visibly jerky steering.

"valid" semantically asks whether the calibration data is
trustworthy — that's a question about convergence (calStatus ==
calibrated), not about input freshness. Switching the gate removes
the cascade: once calibration completes, liveCalibration.valid stays
True stably, locationd initializes, paramsd gets clean observations,
steerRatio converges to the real value.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 14:40:02 -05:00
brianhansonxyz 6a79996a14 park CPU savings + fix early shutdown on virtual battery capacity
prebuilt / build prebuilt (push) Has been cancelled
badges / create badges (push) Has been cancelled
controlsd: in park+ignition-on, run full step() only every 10th cycle.
data_sample still runs every cycle (CAN parse, button detection) and
card.controls_update still runs every cycle (CAN TX heartbeat, counter
increments). Skipped cycles re-send cached controlsState/carControl so
downstream freq_ok stays OK. Button edge handling + display-mode
transitions extracted to handle_screen_mode() and called every cycle so
debug-button presses aren't dropped. controlsd: 55% → 30% CPU in park.

dmonitoringmodeld: subscribe to carState; at standstill, skip model.run
and re-publish last inference. driverStateV2 continues flowing at 10Hz
with known-good last face data (driver can't become distracted relative
to a stopped car). ~5% CPU saved.

power_monitoring: remove the `car_battery_capacity_uWh <= 0` shutdown
trigger. That virtual capacity counter floor-limits to 3e6 µWh on boot
and drains in ~12 min at typical device power, so a short drive (that
doesn't fully recharge the 30e6 µWh virtual cap) followed by a quick
store stop would trip shutdown well before the 30-min idle timer. The
real car-battery-voltage protection (low_voltage_shutdown at 11.8V with
60s debounce) is kept.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 13:46:33 -05:00
brianhansonxyz 426382960a dmonitoringd: narrow update_states gate to fix stuck face_detected
Original gate was sm.all_checks() which required modelV2 fresh and
liveCalibration.valid. Both fail spuriously in our setup:
- modelV2 stops at standstill/parked (two-state modeld)
- calibrationd propagates its own freq_ok glitches into liveCalibration.valid

Either condition froze DM pose updates — face_detected stuck False →
maybe_distracted True → awareness decayed to 0 within ~6s of engagement
("TAKE OVER" driver-distraction alert the moment controlsd engaged).

Narrow the gate to only the subs update_states actually consumes
(driverStateV2, carState, controlsState, liveCalibration), check only
alive+valid (skip freq_ok), and skip liveCalibration.valid — rpyCalib
presence is sufficient proof that calibration has produced output.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 13:21:07 -05:00
brianhansonxyz 54a2a3afc5 add ghost_frame1/2 sprites for manual editing
prebuilt / build prebuilt (push) Has been cancelled
badges / create badges (push) Has been cancelled
Generated from xscreensaver left-facing wag pair (pacman.c frames 5 & 6)
and recolored blue at 2x our bg.jpg sprite size. Saved in source
orientation (portrait, same as bg.jpg) for hand-editing later.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 11:47:01 -05:00
brianhansonxyz 4c8ef93b2b modeld two-state; UI: immediate blank on ignition off; READY splash on wake
- modeld: simplified to 0fps (standstill or parked) or 20fps. Removed
  4/10fps reduced-rate path, republish caching, FPCC/liveCalibration
  reads, and ModelFps per-cycle param writes.
- ui.cc updateWakefulness: ignition on→off now resets interactive_timeout
  to 0 for immediate screen blank. Tap still wakes via existing handler.
- home.cc offroadTransition(false): reset ready->has_driven to false so
  the READY text appears on fresh ignition, not the textless post-drive
  splash.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 11:20:48 -05:00
brianhansonxyz ba4176ffd0 controlsd: suppress freq-only cascade; modeld: variable rate w/ republish cache
prebuilt / build prebuilt (push) Has been cancelled
badges / create badges (push) Has been cancelled
release / build master-ci (push) Has been cancelled
- commIssue now fires only on real comms failure (not_alive or CAN RX
  timeout), not on self-declared valid=False cascades from MSGQ-conflate +
  slow-polling freq_ok artifacts. Car drives fine through these and the
  banner was false-positive.
- Add 5-cycle hysteresis (50ms) on commIssue / posenetInvalid /
  locationdTemporaryError / paramsdTemporaryError.
- Cascade-aware suppression: skip posenet/locationd/paramsd temporary
  errors when the only problem is freq_only_cascade (all alive, just
  freq/valid tripped).
- Remove debug _dbg_dump_alert_triggers helper and EVENTS/EVENT_NAME
  imports.
- Re-enable variable-rate modeld (4/10fps idle, 20fps when lat_active /
  lane_changing / calibrating) with republish caching so consumers get
  constant publish rate.
- Split lat_requested (modeld signal) from lat_engaged (actuator gate).
  Momentary steerFaultTemporary no longer drops modeld rate, preventing
  stale-prediction feedback loop on MDPS recovery. CC.latActive still
  respects the fault.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 21:11:52 -05:00
brianhansonxyz e86eafde15 diag: per-publisher valid=False logging; 30min shutdown; daylight fix; UI tweaks
prebuilt / build prebuilt (push) Has been cancelled
badges / create badges (push) Has been cancelled
stale / stale (push) Has been cancelled
CLAUDE.md: added a "Logging" rule — never use cloudlog (upstream cloud
pipeline, effectively a black hole for us), always use
print(..., file=sys.stderr, flush=True). Manager redirects each process's
stderr to /data/log2/current/{proc}.log. Prefix our lines with "CLP ".

Diagnostic logging — when a publisher sets its own msg.valid=False, log
which specific subscriber tripped the check. Only fires on transition
(True→False) so we don't spam. Covers the services whose cascades we've
been chasing:
  - frogpilot_planner (frogpilotPlan)
  - longitudinal_planner (longitudinalPlan)
  - paramsd (liveParameters)
  - calibrationd (liveCalibration)
  - torqued (liveTorqueParameters)
  - dmonitoringd (driverMonitoringState)

gpsd.is_daylight: fixed a day-boundary bug where the function would flip
to "night" at UTC midnight regardless of actual local sunset. At 85W
sunset is ~00:20 UTC next day, so between local 8pm and actual sunset
the function used *tomorrow's* sunrise/sunset and said night. Now checks
yesterday/today/tomorrow windows with UTC-day offsets.

ui/onroad.cc: nightrider tire-path outline is now light blue (#99CCFF)
at 3px (was white/status-tinted at 6px); lane lines 5% thinner (float
pen width).

thermald/power_monitoring: auto-shutdown timer 10min → 30min.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-17 19:31:09 -05:00
brianhansonxyz cf2b3fc637 modeld: revert to constant 20fps (except standby at standstill)
prebuilt / build prebuilt (push) Has been cancelled
badges / create badges (push) Has been cancelled
Disabled the variable-rate (4/10/20fps) logic — it caused persistent
downstream freq/valid cascades that broke paramsd's angle-offset
learner, calibrationd, and locationd's one-shot filterInitialized latch.
Symptom: user's "TAKE CONTROL IMMEDIATELY / Communication Issue"
banner during engaged driving, plus subtle right-pull (paramsd's fast
angle-offset adaptation was frozen all session).

Simpler model now:
  standstill → standby (0fps, paused)
  otherwise  → 20fps (no variable rate)

Republish-caching and FPCC.latRequested/noLatLaneChange plumbing left in
place so re-enabling variable rate later is a one-line change
(full_rate = True → the original full_rate expression).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-17 18:21:46 -05:00
brianhansonxyz 5b67d4798b fix: update services.py rates to match new thermald/gpsd publish rates
prebuilt / build prebuilt (push) Has been cancelled
badges / create badges (push) Has been cancelled
Root cause of "TAKE CONTROL IMMEDIATELY / Communication Issue Between
Processes" during driving: our 4Hz thermald and 2Hz gpsd changes pushed
deviceState, managerState, and gpsLocation above their declared rate
ceilings in cereal/services.py. SubMaster's freq_ok check requires
observed rate within [0.8*min, 1.2*max]; publishing faster than max also
fails the check — which trips all_freq_ok(), raises EventName.commIssue,
and fires the banner.

- deviceState: 2Hz → 4Hz  (matches thermald DT_TRML=0.25)
- gpsLocation: 1Hz → 2Hz  (matches gpsd 2Hz polling)
- managerState: 2Hz → 4Hz (gated on deviceState arrival, inherits rate)

Also:
- Re-enabled dashcamd in process_config.py (was disabled for the
  diagnostic test — dashcamd was innocent).
- Added a CLAUDE.md section documenting this class of bug so the next
  rate change updates services.py too.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-17 16:28:55 -05:00
brianhansonxyz d64a0f6420 fix: thermald crash on startup — CarState is in cereal.car, not cereal.log
The new fan control plumbing referenced log.CarState.GearShifter.park,
but CarState is defined in car.capnp and only DeviceState/PandaState
live in log.capnp. Thermald crashed immediately on first carState tick,
which meant deviceState stopped publishing entirely. The UI then read
stale/default values — freeSpacePercent=0 → "100% full" → "Out of Storage"
alert despite 65 GB actually free. The fan also stopped updating.

from cereal import car, log  (was just log)
car.CarState.GearShifter.park (was log.CarState...)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-17 16:12:35 -05:00
brianhansonxyz 4dae5804ab feat: 4Hz fan control with gear/cruise-aware clamps; move hot signals to cereal
Fan control rework (thermald → 4Hz):
- DT_TRML 0.5s → 0.25s (thermald loop + fan PID now at 4Hz)
- New clamp rules based on (gear, cruise_engaged, standstill):
    parked                                → 0-100%
    in drive + cruise engaged (any speed) → 30-100%
    in drive + cruise off + standstill    → 10-100%
    in drive + cruise off + moving        → 30-100%
- thermald now reads gearShifter (via carState) and controlsState.enabled,
  passes them to fan_controller.update()
- Removed BENCH_MODE special case — new rules cover bench automatically
- Removed ignition-based branches — gear is the correct signal

System health overlay:
- Subscribed UI to peripheralState so we can read fanSpeedRpm
- Added FAN row: actual fan% (RPM / 65) to sit alongside LAG/DROP/TEMP/CPU/MEM.
  Shows the real fan output vs. what the PID is asking for.

Migrate hot signals from paramsMemory to cereal (frogpilotCarControl):
- Added latRequested @3 and noLatLaneChange @4 to FrogPilotCarControl schema
- controlsd sets FPCC.latRequested / FPCC.noLatLaneChange (send-on-change
  already gates the IPC)
- modeld reads from sm['frogpilotCarControl'] (added to its subscribers)
  instead of paramsMemory (saves ~20 file-read syscalls/sec)
- carcontroller reads from frogpilot_variables (set in-process by controlsd)
  instead of paramsMemory (saves ~100 file-read syscalls/sec in 100Hz path).
  Dropped carcontroller's now-unused Params instance and import.
- UI (ui.cc, onroad.cc) reads from sm['frogpilotCarControl'].noLatLaneChange
- Removed LatRequested and no_lat_lane_change param registrations + defaults

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-17 16:01:39 -05:00
brianhansonxyz 25b4672fda feat: raise controlsdLagging alert threshold to 20ms avg
prebuilt / build prebuilt (push) Has been cancelled
badges / create badges (push) Has been cancelled
release / build master-ci (push) Has been cancelled
Stock rk.lagging fires at 11.1ms (90% of 10ms interval), which the
Hyundai CAN load routinely crosses during normal driving as carstate
parses 50-200 msgs/sec. That's normal, not a fault — the same code
behaved the same way at the fork baseline; we just made it visible with
the LAG overlay.

50Hz effective control is still tighter than any human reaction loop.
rk.lagging remains wired to the defensive skip paths (secondary camera
and radar checks) at the original tighter threshold, so we still avoid
false-alarming dependent events when briefly overloaded.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 22:46:07 -05:00
brianhansonxyz 7a0854387e ui: shift health overlay labels 50px left
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 22:35:22 -05:00
brianhansonxyz 83aed16a35 perf: suppress controlsdLagging on engage + gate frogpilotCarControl send
Two related fixes for the post-engage lag spike:

1. controlsdLagging suppressed during lat_engage_suppress window.
   Ratekeeper.lagging triggers when avg cycle duration over 100 cycles
   exceeds 11.1ms (90% of 10ms budget). The modeld 10→20fps ramp causes
   a legitimate transient where downstream services (plannerd, locationd,
   calibrationd, paramsd) each drain 2x the message rate, briefly pushing
   avg cycle time past the threshold. The underlying system isn't broken —
   it's correctly absorbing a scheduled workload transition.

2. frogpilotCarControl now sends only on change (+ 1Hz keepalive) instead
   of every 10ms. The message has 3 bool fields, of which speedLimitChanged
   code is entirely commented out, trafficModeActive flips only on UI
   button press, and alwaysOnLateral changes only on cruise/gear/brake
   edges. plannerd doesn't include frogpilotCarControl in its all_checks
   list so stale-freq detection isn't a concern. Saves ~7ms/sec of
   capnp build + zmq send work.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 22:35:16 -05:00
brianhansonxyz 6dede984dc perf: pin dashcamd to cores 0-3 (little cluster)
Previously ran unpinned (affinity mask 0xff) across all 8 cores. When it
landed on core 4 (controlsd) or 5 (plannerd/radard) or 7 (modeld), its
70MB/s frame copies and MP4 muxing caused cache/memory-bandwidth
contention with the RT-pinned processes. SCHED_FIFO prevented direct
preemption but not the cache thrash.

OMX offloads actual H.264 work to hardware so the main thread is
lightweight — fine on the little cluster.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 22:35:02 -05:00
29 changed files with 344 additions and 228 deletions
+42 -4
View File
@@ -37,6 +37,20 @@ This is self-driving software. All changes must be deliberate and well-understoo
- **Test everything thoroughly** — Brian must always be in the loop
- **Do not refactor, clean up, or "improve" code beyond the specific request**
### Logging
**NEVER use `cloudlog`.** It's comma.ai's cloud telemetry pipeline, not ours — writes go to a publisher that's effectively a black hole for us (and the only thing it could do if ever reachable is bother the upstream FrogPilot developer). Our changes must always use **file logging** instead.
Use `print(..., file=sys.stderr, flush=True)`. `manager.py` redirects each managed process's stderr to `/data/log2/current/{process}.log`, so these lines land in the per-process log we already grep. Prefix custom log lines with `CLP ` so they're easy to filter out from upstream noise.
Example:
```python
import sys
print(f"CLP frogpilotPlan valid=False: carState_freq_ok={sm.freq_ok['carState']}", file=sys.stderr, flush=True)
```
Do not use `cloudlog.warning`, `cloudlog.info`, `cloudlog.error`, `cloudlog.event`, or `cloudlog.exception` in any CLEARPILOT-added code. Existing upstream/FrogPilot `cloudlog` calls can stay untouched.
### File Ownership
We operate as `root` on this device, but openpilot runs as the `comma` user (uid=1000, gid=1000). After any code changes that touch multiple files or before testing:
@@ -84,6 +98,31 @@ ls /data/log2/current/
cat /data/log2/current/gpsd.log
```
### Changing a Service's Publish Rate
SubMaster's `freq_ok` check requires observed rate to fall within
`[0.8 × min_freq, 1.2 × max_freq]` of the value declared in
`cereal/services.py`. Publishing *faster* than declared fails the check
just as surely as publishing too slowly — it trips `all_freq_ok()`
`EventName.commIssue` → "TAKE CONTROL IMMEDIATELY / Communication Issue
Between Processes".
If you change how often a process publishes, you **must** update the rate
in `cereal/services.py` to match. Example (real bug we hit):
- Bumped thermald from 2Hz → 4Hz (DT_TRML 0.5→0.25) for faster fan control
- Bumped gpsd from 1Hz → 2Hz
- manager publishes `managerState` gated by `sm.update()` returning on
`deviceState`, so it picks up thermald's rate automatically
`services.py` still declared `deviceState` and `managerState` as 2Hz
(ceiling 2.4Hz) and `gpsLocation` as 1Hz (ceiling 1.2Hz), so controlsd
fired `commIssue` on every cycle the moment any of these arrived at the
new faster rate.
Fix: update the second tuple element (Hz) in `cereal/services.py` for the
affected service. Cereal will use the updated rate for all consumers.
### Adding New Params
The params system uses a C++ whitelist. Adding a new param name in `manager.py` alone will crash with `UnknownKeyName`. You must:
@@ -101,7 +140,7 @@ ClearPilot uses memory params (`/dev/shm/params/d/`) for UI toggles that should
- **Python access**: Use `Params("/dev/shm/params")`
- **Defaults**: Set in `manager_init()` via `Params("/dev/shm/params").put(key, value)`
- **UI toggles**: Use `ToggleControl` with manual `toggleFlipped` lambda that writes via `Params("/dev/shm/params")`. Do NOT use `ParamControl` for memory params — it reads/writes persistent params only
- **Current memory params**: `TelemetryEnabled` (default "0"), `VpnEnabled` (default "1"), `ModelStandby` (default "0"), `ScreenDisplayMode`, `DashcamState` (default "stopped"), `DashcamFrames` (default "0")
- **Current memory params**: `TelemetryEnabled` (default "0"), `VpnEnabled` (default "1"), `ModelStandby` (default "0"), `ScreenDisplayMode`, `DashcamState` (default "stopped"), `DashcamFrames` (default "0"), `DashcamShutdown` (default "0")
- **IMPORTANT — method names differ between C++ and Python**: C++ uses camelCase (`putBool`, `getBool`, `getInt`), Python uses snake_case (`put_bool`, `get_bool`, `get_int`). This is a common source of silent failures — the wrong casing compiles/runs but doesn't work.
### Building Native (C++) Processes
@@ -311,8 +350,7 @@ The OMX encoder (`selfdrive/frogpilot/screenrecorder/omx_encoder.cc`) was ported
### Params
- `DashcamDebug` — when `"1"`, dashcamd runs even without car connected (for bench testing)
- `DashcamShutdown` — set by thermald before power-off, dashcamd acks by clearing it
- `DashcamShutdown` (memory param) — set by thermald before power-off, dashcamd acks by clearing it
- `DashcamState` (memory param) — "stopped", "waiting", or "recording" — published every 5s
- `DashcamFrames` (memory param) — per-trip encoded frame count, resets each trip — published every 5s
@@ -524,7 +562,7 @@ Power On
### ClearPilot Processes
- `dashcamd` — raw camera dashcam recording (runs onroad or with DashcamDebug flag)
- `dashcamd` — raw camera dashcam recording (always runs; manages its own trip lifecycle)
### GPS
+3
View File
@@ -12,6 +12,9 @@ struct FrogPilotCarControl @0x81c2f05a394cf4af {
alwaysOnLateral @0 :Bool;
speedLimitChanged @1 :Bool;
trafficModeActive @2 :Bool;
# CLEARPILOT: migrated from paramsMemory to avoid file-read syscalls in modeld/UI/carcontroller
latRequested @3 :Bool; # controlsd's request to enable lat before ramp-up delay
noLatLaneChange @4 :Bool; # lat is temporarily suppressed during a lane change
}
struct FrogPilotCarState @0xaedffd8f31e7b55d {
+3 -3
View File
@@ -30,7 +30,7 @@ services: dict[str, tuple] = {
"temperatureSensor": (True, 2., 200),
"temperatureSensor2": (True, 2., 200),
"gpsNMEA": (True, 9.),
"deviceState": (True, 2., 1),
"deviceState": (True, 5., 1), # CLEARPILOT: 5Hz — thermald decimates pandaStates (10Hz) every 2 ticks
"can": (True, 100., 1223), # decimation gives ~5 msgs in a full segment
"controlsState": (True, 100., 10),
"pandaStates": (True, 10., 1),
@@ -50,7 +50,7 @@ services: dict[str, tuple] = {
"longitudinalPlan": (True, 20., 5),
"procLog": (True, 0.5, 15),
"gpsLocationExternal": (True, 10., 10),
"gpsLocation": (True, 1., 1),
"gpsLocation": (True, 2., 1), # CLEARPILOT: 2Hz (was 1Hz) — gpsd polls at 2Hz
"ubloxGnss": (True, 10.),
"qcomGnss": (True, 2.),
"gnssMeasurements": (True, 10., 10),
@@ -70,7 +70,7 @@ services: dict[str, tuple] = {
"wideRoadEncodeIdx": (False, 20., 1),
"wideRoadCameraState": (True, 20., 20),
"modelV2": (True, 20., 40),
"managerState": (True, 2., 1),
"managerState": (True, 5., 1), # CLEARPILOT: 5Hz — gated by deviceState arrival (see thermald)
"uploaderState": (True, 0., 1),
"navInstruction": (True, 1., 10),
"navRoute": (True, 0.),
-4
View File
@@ -109,7 +109,6 @@ std::unordered_map<std::string, uint32_t> keys = {
{"ControlsReady", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION},
{"CurrentBootlog", PERSISTENT},
{"CurrentRoute", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION},
{"DashcamDebug", PERSISTENT},
{"DashcamFrames", PERSISTENT},
{"DashcamState", PERSISTENT},
{"DashcamShutdown", CLEAR_ON_MANAGER_START},
@@ -201,7 +200,6 @@ std::unordered_map<std::string, uint32_t> keys = {
{"TelemetryEnabled", PERSISTENT},
{"Timezone", PERSISTENT},
{"TrainingVersion", PERSISTENT},
{"LatRequested", PERSISTENT},
{"ModelFps", PERSISTENT},
{"ModelStandby", PERSISTENT},
{"ModelStandbyTs", PERSISTENT},
@@ -510,8 +508,6 @@ std::unordered_map<std::string, uint32_t> keys = {
{"WheelIcon", PERSISTENT},
{"WheelSpeed", PERSISTENT},
// Clearpilot
{"no_lat_lane_change", PERSISTENT},
};
} // namespace
+1 -1
View File
@@ -12,7 +12,7 @@ from openpilot.system.hardware import PC
# time step for each process
DT_CTRL = 0.01 # controlsd
DT_MDL = 0.05 # model
DT_TRML = 0.5 # thermald and manager
DT_TRML = 0.25 # thermald and manager — 4 Hz
DT_DMON = 0.05 # driver monitoring
+2 -5
View File
@@ -8,7 +8,6 @@ from openpilot.selfdrive.car.hyundai import hyundaicanfd, hyundaican
from openpilot.selfdrive.car.hyundai.hyundaicanfd import CanBus
from openpilot.selfdrive.car.hyundai.values import HyundaiFlags, Buttons, CarControllerParams, CANFD_CAR, CAR
from openpilot.selfdrive.car.interfaces import CarControllerBase
from openpilot.common.params import Params
VisualAlert = car.CarControl.HUDControl.VisualAlert
LongCtrlState = car.CarControl.Actuators.LongControlState
@@ -57,9 +56,6 @@ class CarController(CarControllerBase):
self.car_fingerprint = CP.carFingerprint
self.last_button_frame = 0
# CLEARPILOT: cache Params instance — create_steering_messages was building
# a new one every 10ms (100Hz), causing needless allocation + file opens
self.params_memory = Params("/dev/shm/params")
def update(self, CC, CS, now_nanos, frogpilot_variables):
actuators = CC.actuators
@@ -116,7 +112,8 @@ class CarController(CarControllerBase):
hda2_long = hda2 and self.CP.openpilotLongitudinalControl
# steering control
no_lat_lane_change = self.params_memory.get_int("no_lat_lane_change", 1)
# CLEARPILOT: no_lat_lane_change comes via frogpilot_variables (set by controlsd in-process)
no_lat_lane_change = int(getattr(frogpilot_variables, 'no_lat_lane_change', False))
can_sends.extend(hyundaicanfd.create_steering_messages(self.packer, self.CP, self.CAN, CC.enabled, apply_steer_req, apply_steer, no_lat_lane_change))
# prevent LFA from activating on HDA2 by sending "no lane lines detected" to ADAS ECU
Binary file not shown.
+18 -2
View File
@@ -38,7 +38,9 @@
#include <cstdio>
#include <ctime>
#include <string>
#include <errno.h>
#include <signal.h>
#include <sched.h>
#include <execinfo.h>
#include <sys/stat.h>
#include <sys/resource.h>
@@ -121,6 +123,20 @@ int main(int argc, char *argv[]) {
signal(SIGABRT, crash_handler);
setpriority(PRIO_PROCESS, 0, -10);
// CLEARPILOT: pin to cores 0-3 (little cluster). Avoids cache/memory-bandwidth
// contention with the RT-pinned processes on the big cluster:
// core 4 = controlsd, core 5 = plannerd/radard, core 7 = modeld.
// OMX offloads actual H.264 work to hardware, so the main thread just copies
// frames and muxes MP4 — fine on the little cluster.
cpu_set_t mask;
CPU_ZERO(&mask);
for (int i = 0; i < 4; i++) CPU_SET(i, &mask);
if (sched_setaffinity(0, sizeof(mask), &mask) != 0) {
LOGW("dashcamd: sched_setaffinity failed (%d), continuing unpinned", errno);
} else {
LOGW("dashcamd: pinned to cores 0-3");
}
// Ensure base output directory exists
mkdir(VIDEOS_BASE.c_str(), 0755);
@@ -263,12 +279,12 @@ int main(int argc, char *argv[]) {
// Check for graceful shutdown request (every ~1 second)
if (++param_check_counter >= CAMERA_FPS) {
param_check_counter = 0;
if (params.getBool("DashcamShutdown")) {
if (params_memory.getBool("DashcamShutdown")) {
LOGW("dashcamd: shutdown requested");
if (state == RECORDING || state == IDLE_TIMEOUT) {
close_trip();
}
params.putBool("DashcamShutdown", false);
params_memory.putBool("DashcamShutdown", false);
LOGW("dashcamd: shutdown ack sent, exiting");
break;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

+61 -56
View File
@@ -81,9 +81,11 @@ class Controls:
self.params_memory.put_bool("CPTLkasButtonAction", False)
self.params_memory.put_int("ScreenDisplayMode", 0)
# ClearPilot speed processing
# CLEARPILOT: speed-limit/cruise-warning state machine + park→drive auto-reset
self.speed_state = SpeedState()
self.speed_state_frame = 0
self.was_driving_gear = False
self.driving_gear = False
self.radarless_model = self.params.get("Model", encoding='utf-8') in RADARLESS_MODELS
@@ -114,8 +116,7 @@ class Controls:
'carOutput', 'driverMonitoringState', 'longitudinalPlan', 'liveLocationKalman',
'managerState', 'liveParameters', 'radarState', 'liveTorqueParameters',
'testJoystick', 'frogpilotPlan', 'gpsLocation'] + self.camera_packets + self.sensor_packets,
ignore_alive=ignore+['gpsLocation'], ignore_avg_freq=ignore+['radarState', 'testJoystick', 'gpsLocation'],
ignore_valid=['testJoystick', 'gpsLocation'],
ignore_alive=ignore + ['gpsLocation'], ignore_avg_freq=ignore+['radarState', 'testJoystick', 'gpsLocation'], ignore_valid=['testJoystick', 'gpsLocation'],
frequency=int(1/DT_CTRL))
self.joystick_mode = self.params.get_bool("JoystickDebugMode")
@@ -170,9 +171,6 @@ class Controls:
self.current_alert_types = [ET.PERMANENT]
self.logged_comm_issue = None
self.not_running_prev = None
self.lat_requested_ts = 0 # CLEARPILOT: timestamp when lat was first requested (ramp-up delay + comm issue suppression)
self.prev_lat_requested = False
self.prev_no_lat_lane_change = None # CLEARPILOT: gate no_lat_lane_change write on change
self.steer_limited = False
self.desired_curvature = 0.0
self.experimental_mode = False
@@ -207,8 +205,6 @@ class Controls:
self.always_on_lateral_main = self.always_on_lateral and self.params.get_bool("AlwaysOnLateralMain")
self.drive_added = False
self.driving_gear = False
self.was_driving_gear = False
self.fcw_random_event_triggered = False
self.holiday_theme_alerted = False
self.onroad_distance_pressed = False
@@ -458,13 +454,7 @@ class Controls:
standby_ts = float(self.params_memory.get("ModelStandbyTs") or "0")
except (ValueError, TypeError):
standby_ts = 0
now = time.monotonic()
model_suppress = (now - standby_ts) < 2.0
# CLEARPILOT: suppress commIssue/location/params errors for 2s after lat was requested
# Model FPS jumps from 4 to 20 on engage, causing transient freq check failures
# lat_requested_ts is set in state_control on the False->True transition of LatRequested
lat_engage_suppress = (now - self.lat_requested_ts) < 2.0
model_suppress = (time.monotonic() - standby_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):
@@ -495,13 +485,16 @@ 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 and not lat_engage_suppress:
# CLEARPILOT: fire commIssue ONLY when messages actually aren't flowing (not_alive)
# or CAN RX is timing out. Don't fire on self-declared valid=False — that's the
# polling-pattern / all_checks cascade that paramsd/torqued/plannerd/frogpilot
# propagate even while their publish rate and content are fine.
comms_really_broken = (not self.sm.all_alive()) or self.card.can_rcv_timeout
if comms_really_broken and no_system_errors and not model_suppress:
if not self.sm.all_alive():
self.events.add(EventName.commIssue)
elif not self.sm.all_freq_ok():
self.events.add(EventName.commIssueAvgFreq)
else: # invalid or can_rcv_timeout.
self.events.add(EventName.commIssue)
else:
self.events.add(EventName.commIssue) # can_rcv_timeout path
logs = {
'invalid': [s for s, valid in self.sm.valid.items() if not valid],
@@ -516,15 +509,13 @@ class Controls:
self.logged_comm_issue = None
if not (self.CP.notCar and self.joystick_mode):
# CLEARPILOT: also suppress on lat engage — modeld 4fps→20fps transition causes
# transient inputsOK/valid drops in locationd/paramsd downstream
if not self.sm['liveLocationKalman'].posenetOK and not model_suppress and not lat_engage_suppress:
if not self.sm['liveLocationKalman'].posenetOK and not model_suppress:
self.events.add(EventName.posenetInvalid)
if not self.sm['liveLocationKalman'].deviceStable:
self.events.add(EventName.deviceFalling)
if not self.sm['liveLocationKalman'].inputsOK and not model_suppress and not lat_engage_suppress:
if not self.sm['liveLocationKalman'].inputsOK and not model_suppress:
self.events.add(EventName.locationdTemporaryError)
if not self.sm['liveParameters'].valid and not TESTING_CLOSET and (not SIMULATION or REPLAY) and not model_suppress and not lat_engage_suppress:
if not self.sm['liveParameters'].valid and not TESTING_CLOSET and (not SIMULATION or REPLAY) and not model_suppress:
self.events.add(EventName.paramsdTemporaryError)
# conservative HW alert. if the data or frequency are off, locationd will throw an error
@@ -538,8 +529,16 @@ class Controls:
if self.cruise_mismatch_counter > int(6. / DT_CTRL):
self.events.add(EventName.cruiseMismatch)
# CLEARPILOT: FCW disabled — car's own radar AEB works better and triggers reliably.
# The comma FCW was producing false positives and adds nothing over the stock system.
# Check for FCW
stock_long_is_braking = self.enabled and not self.CP.openpilotLongitudinalControl and CS.aEgo < -1.25
model_fcw = self.sm['modelV2'].meta.hardBrakePredicted and not CS.brakePressed and not stock_long_is_braking
planner_fcw = self.sm['longitudinalPlan'].fcw and self.enabled
if planner_fcw or model_fcw:
self.events.add(EventName.fcw)
# self.fcw_random_event_triggered = True
# elif self.fcw_random_event_triggered and self.random_events:
# self.events.add(EventName.yourFrogTriedToKillMe)
# self.fcw_random_event_triggered = False
for m in messaging.drain_sock(self.log_sock, wait_for_one=False):
try:
@@ -666,6 +665,25 @@ class Controls:
def state_control(self, CS):
"""Given the state, this function returns a CarControl packet"""
# CLEARPILOT: short-circuit while parked. Skip LaC/LoC PID, MPC, model_v2
# reads, lane-change logic — none of it matters when the car isn't moving.
# publish_logs still runs and still triggers carcontroller.apply via
# card.controls_update, so the sendcan heartbeats / tester-present messages
# keep flowing at 100Hz and the car doesn't fault. Saves ~30% controlsd CPU
# in park.
if CS.gearShifter == car.CarState.GearShifter.park:
CC = car.CarControl.new_message()
CC.enabled = False
CC.latActive = False
CC.longActive = False
CC.actuators.longControlState = self.LoC.long_control_state
self.LaC.reset()
self.LoC.reset(v_pid=CS.vEgo)
self.frogpilot_variables.no_lat_lane_change = False
self.FPCC.noLatLaneChange = False
lac_log = log.ControlsState.LateralDebugState.new_message()
return CC, lac_log
# Update VehicleModel
lp = self.sm['liveParameters']
x = max(lp.stiffnessFactor, 0.1)
@@ -687,18 +705,8 @@ class Controls:
# Check which actuators can be enabled
standstill = CS.vEgo <= max(self.CP.minSteerSpeed, MIN_LATERAL_CONTROL_SPEED) or CS.standstill
lat_requested = (self.active or self.FPCC.alwaysOnLateral) and self.speed_check and not CS.steerFaultTemporary and not CS.steerFaultPermanent and \
(not standstill or self.joystick_mode)
# CLEARPILOT: tell modeld early so it can ramp to 20fps, then delay latActive by
# 250ms (5 frames at 20fps) so downstream services stabilize before steering engages.
now_ts = time.monotonic()
if lat_requested != self.prev_lat_requested:
self.params_memory.put_bool("LatRequested", lat_requested)
if lat_requested:
self.lat_requested_ts = now_ts
self.prev_lat_requested = lat_requested
CC.latActive = lat_requested and (now_ts - self.lat_requested_ts) >= 0.25
CC.latActive = (self.active or self.FPCC.alwaysOnLateral) and self.speed_check and not CS.steerFaultTemporary and not CS.steerFaultPermanent and \
(not standstill or self.joystick_mode)
CC.longActive = self.enabled and not self.events.contains(ET.OVERRIDE_LONGITUDINAL) and self.CP.openpilotLongitudinalControl
actuators = CC.actuators
@@ -713,13 +721,13 @@ class Controls:
CC.leftBlinker = model_v2.meta.laneChangeDirection == LaneChangeDirection.left
CC.rightBlinker = model_v2.meta.laneChangeDirection == LaneChangeDirection.right
no_lat_lane_change = model_v2.meta.laneChangeState == LaneChangeState.laneChangeStarting and clearpilot_disable_lat_on_lane_change
if no_lat_lane_change:
if model_v2.meta.laneChangeState == LaneChangeState.laneChangeStarting and clearpilot_disable_lat_on_lane_change:
CC.latActive = False
# CLEARPILOT: only write on change — per-cycle atomic writes were eating ~15% CPU
if no_lat_lane_change != self.prev_no_lat_lane_change:
self.params_memory.put_bool("no_lat_lane_change", no_lat_lane_change)
self.prev_no_lat_lane_change = no_lat_lane_change
self.frogpilot_variables.no_lat_lane_change = True
self.FPCC.noLatLaneChange = True
else:
self.frogpilot_variables.no_lat_lane_change = False
self.FPCC.noLatLaneChange = False
if CS.leftBlinker or CS.rightBlinker:
self.last_blinker_frame = self.sm.frame
@@ -999,11 +1007,6 @@ class Controls:
while True:
self.step()
self.rk.monitor_time()
# CLEARPILOT: accumulated debt from startup/blocking calls never recovers.
# Reset the rate keeper when catastrophically behind — prevents a one-time
# ~8s fingerprinting stall from poisoning the lag metric for the whole session.
if self.rk.remaining < -1.0:
self.rk._next_frame_time = time.monotonic() + self.rk._interval
except SystemExit:
e.set()
t.join()
@@ -1074,11 +1077,13 @@ class Controls:
elif self.sm['modelV2'].meta.turnDirection == Desire.turnRight:
self.events.add(EventName.turningRight)
# CLEARPILOT: check crash file once per second, not every cycle (stat syscall at 100Hz hurts)
if self.sm.frame % 100 == 0:
self._crashed_file_exists = os.path.isfile(os.path.join(sentry.CRASHES_DIR, 'error.txt'))
if getattr(self, '_crashed_file_exists', False) and self.crashed_timer < 10:
if os.path.isfile(os.path.join(sentry.CRASHES_DIR, 'error.txt')) and self.crashed_timer < 10:
self.events.add(EventName.openpilotCrashed)
# if self.random_events and not self.openpilot_crashed_triggered:
# self.events.add(EventName.openpilotCrashedRandomEvents)
# self.openpilot_crashed_triggered = True
self.crashed_timer += DT_CTRL
# if self.speed_limit_alert or self.speed_limit_confirmation:
@@ -1302,7 +1307,8 @@ class Controls:
self.params_memory.put_int("ScreenDisplayMode", new_mode)
# ClearPilot speed processing (~2 Hz at 100 Hz loop)
# ClearPilot speed processing (~2 Hz at 100 Hz loop) — drives speed-limit sign
# and cruise over/under warning sign via memory params read by the UI.
self.speed_state_frame += 1
if self.speed_state_frame % 50 == 0:
gps = self.sm['gpsLocation']
@@ -1315,7 +1321,6 @@ class Controls:
cruise_standstill = CS.cruiseState.standstill
self.speed_state.update(speed_ms, has_gps, speed_limit_ms, is_metric,
cruise_speed_ms, cruise_active, cruise_standstill)
return CC
def main():
+1 -20
View File
@@ -143,9 +143,6 @@ 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"
@@ -176,19 +173,6 @@ 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
@@ -255,10 +239,7 @@ 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
# 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
self.fcw = self.mpc.crash_cnt > 2 and not sm['carState'].standstill
if self.fcw:
cloudlog.info("FCW triggered")
+4
View File
@@ -34,6 +34,10 @@ def plannerd_thread():
while True:
sm.update()
if sm.updated['modelV2']:
# CLEARPILOT: skip planning while parked. The downstream consumer (controlsd)
# already short-circuits in park, so longitudinalPlan/uiPlan staleness is fine.
if sm['carState'].gearShifter == car.CarState.GearShifter.park:
continue
longitudinal_planner.update(sm)
longitudinal_planner.publish(sm, pm)
publish_ui_plan(sm, pm, longitudinal_planner)
@@ -58,6 +58,9 @@ class FrogPilotPlanner:
self.params = Params()
self.params_memory = Params("/dev/shm/params")
# CLEARPILOT: track valid transitions so we only log when it flips, not every cycle
self._dbg_prev_valid = True
self.cem = ConditionalExperimentalMode()
self.lead_one = Lead()
self.mtsc = MapTurnSpeedController()
@@ -242,7 +245,18 @@ class FrogPilotPlanner:
def publish(self, sm, pm):
frogpilot_plan_send = messaging.new_message('frogpilotPlan')
frogpilot_plan_send.valid = sm.all_checks(service_list=['carState', 'controlsState'])
valid = sm.all_checks(service_list=['carState', 'controlsState'])
# CLEARPILOT: log on transition into invalid — stderr goes to our plannerd.log
if valid != self._dbg_prev_valid and not valid:
import sys
print(
"CLP frogpilotPlan valid=False: "
f"carState(a={sm.alive['carState']},v={sm.valid['carState']},f={sm.freq_ok['carState']}) "
f"controlsState(a={sm.alive['controlsState']},v={sm.valid['controlsState']},f={sm.freq_ok['controlsState']})",
file=sys.stderr, flush=True
)
self._dbg_prev_valid = valid
frogpilot_plan_send.valid = valid
frogpilotPlan = frogpilot_plan_send.frogpilotPlan
frogpilotPlan.accelerationJerk = A_CHANGE_COST * (float(self.jerk) if self.lead_one.status else 1)
+3 -1
View File
@@ -85,7 +85,9 @@ def frogpilot_thread():
frogpilot_planner = FrogPilotPlanner(CP)
frogpilot_planner.update_frogpilot_params()
if sm.updated['modelV2']:
# CLEARPILOT: skip planner work while parked.
parked = sm['carState'].gearShifter == car.CarState.GearShifter.park
if sm.updated['modelV2'] and not parked:
frogpilot_planner.update(sm['carState'], sm['controlsState'], sm['frogpilotCarControl'], sm['frogpilotNavigation'],
sm['liveLocationKalman'], sm['modelV2'], sm['radarState'])
frogpilot_planner.publish(sm, pm)
+8 -1
View File
@@ -284,7 +284,14 @@ def main() -> NoReturn:
# 4Hz driven by cameraOdometry
if sm.frame % 5 == 0:
calibrator.send_data(pm, sm.all_checks())
# CLEARPILOT: publish valid based on calibration status, not upstream sm.all_checks().
# The original gate cascaded upstream freq glitches into liveCalibration.valid=False,
# which kept locationd.filterInitialized False, which fed garbage into paramsd, which
# corrupted steerRatio and caused erratic steering (and controlsd commIssue banners).
# "valid" here semantically means "the calibration data is trustworthy" — a question
# about convergence, not input freshness.
cal_valid = calibrator.cal_status == log.LiveCalibrationData.Status.calibrated
calibrator.send_data(pm, cal_valid)
if __name__ == "__main__":
+7 -1
View File
@@ -308,12 +308,18 @@ void Localizer::input_fake_gps_observations(double current_time) {
}
void Localizer::handle_gps(double current_time, const cereal::GpsLocationData::Reader& log, const double sensor_time_offset) {
// CLEARPILOT: GPS disabled as a Kalman input. gpsd still publishes real GPS for
// UI / dashcam / clock / night-mode; only locationd ignores it. Falls through to
// determine_gps_mode() which is openpilot's existing no-GPS path (fake observations
// to bound position uncertainty). To re-enable, set this to false.
const bool clearpilot_disable_gps = true;
bool gps_unreasonable = (Vector2d(log.getHorizontalAccuracy(), log.getVerticalAccuracy()).norm() >= SANE_GPS_UNCERTAINTY);
bool gps_accuracy_insane = ((log.getVerticalAccuracy() <= 0) || (log.getSpeedAccuracy() <= 0) || (log.getBearingAccuracyDeg() <= 0));
bool gps_lat_lng_alt_insane = ((std::abs(log.getLatitude()) > 90) || (std::abs(log.getLongitude()) > 180) || (std::abs(log.getAltitude()) > ALTITUDE_SANITY_CHECK));
bool gps_vel_insane = (floatlist2vector(log.getVNED()).norm() > TRANS_SANITY_CHECK);
if (!log.getHasFix() || gps_unreasonable || gps_accuracy_insane || gps_lat_lng_alt_insane || gps_vel_insane) {
if (clearpilot_disable_gps || !log.getHasFix() || gps_unreasonable || gps_accuracy_insane || gps_lat_lng_alt_insane || gps_vel_insane) {
//this->gps_valid = false;
this->determine_gps_mode(current_time);
return;
+1 -2
View File
@@ -89,7 +89,7 @@ def manager_init(frogpilot_functions) -> None:
params_memory.put("VpnEnabled", "1")
params_memory.put("DashcamFrames", "0")
params_memory.put("DashcamState", "stopped")
params_memory.put("LatRequested", "0")
params_memory.put("DashcamShutdown", "0")
params_memory.put("ModelFps", "20")
params_memory.put("ModelStandby", "0")
params_memory.put("ShutdownTouchReset", "0")
@@ -178,7 +178,6 @@ def manager_init(frogpilot_functions) -> None:
("DisableOpenpilotLongitudinal", "0"),
("DisableVTSCSmoothing", "0"),
("DisengageVolume", "100"),
("DashcamDebug", "1"),
("TelemetryEnabled", "0"),
("DragonPilotTune", "0"),
("DriverCamera", "0"),
+13 -3
View File
@@ -7,7 +7,7 @@ import ctypes
import numpy as np
from pathlib import Path
from cereal import messaging
from cereal import car, messaging
from cereal.messaging import PubMaster, SubMaster
from cereal.visionipc import VisionIpcClient, VisionStreamType, VisionBuf
from openpilot.common.swaglog import cloudlog
@@ -128,10 +128,15 @@ def main():
assert vipc_client.is_connected()
cloudlog.warning(f"connected with buffer size: {vipc_client.buffer_len}")
sm = SubMaster(["liveCalibration"])
sm = SubMaster(["liveCalibration", "carState"])
pm = PubMaster(["driverStateV2"])
calib = np.zeros(CALIB_LEN, dtype=np.float32)
# CLEARPILOT: cache last model output to serve while gear is in park —
# mirrors the same trick modeld uses. Skips DSP inference on the driver
# camera when the car is stationary; downstream dmonitoringd still gets
# a fresh publish each frame.
last_model_output = None
# last = 0
while True:
@@ -143,8 +148,13 @@ def main():
if sm.updated["liveCalibration"]:
calib[:] = np.array(sm["liveCalibration"].rpyCalib)
parked = sm["carState"].gearShifter == car.CarState.GearShifter.park
t1 = time.perf_counter()
model_output, dsp_execution_time = model.run(buf, calib)
if parked and last_model_output is not None:
model_output, dsp_execution_time = last_model_output
else:
model_output, dsp_execution_time = model.run(buf, calib)
last_model_output = (model_output, dsp_execution_time)
t2 = time.perf_counter()
pm.send("driverStateV2", get_driverstate_packet(model_output, vipc_client.frame_id, vipc_client.timestamp_sof, t2 - t1, dsp_execution_time))
+21 -60
View File
@@ -183,9 +183,10 @@ def main(demo=False):
model_transform_main = np.zeros((3, 3), dtype=np.float32)
model_transform_extra = np.zeros((3, 3), dtype=np.float32)
live_calib_seen = False
model_standby = False
last_standby_ts_write = 0
params_memory = Params("/dev/shm/params")
# CLEARPILOT: cache last model output to serve while gear is in park — saves
# GPU inference cost while still giving downstream a constant publish rate so
# freq_ok / valid checks don't cascade.
last_model_output = None
nav_features = np.zeros(ModelConstants.NAV_FEATURE_LEN, dtype=np.float32)
nav_instructions = np.zeros(ModelConstants.NAV_INSTRUCTION_LEN, dtype=np.float32)
buf_main, buf_extra = None, None
@@ -241,59 +242,10 @@ def main(demo=False):
sm.update(0)
# CLEARPILOT: power saving — three modes based on driving state
# Full 20fps: lat requested or lane changing
# Reduced 4fps: not lat requested, not standstill (driving without cruise)
# Standby 0fps: standstill
# Uses LatRequested (not carControl.latActive) so modeld ramps to 20fps BEFORE
# controlsd actually engages steering — gives downstream services time to stabilize.
lat_active = params_memory.get_bool("LatRequested")
lane_changing = params_memory.get_bool("no_lat_lane_change")
standstill = sm['carState'].standstill
calibrating = sm['liveCalibration'].calStatus != log.LiveCalibrationData.Status.calibrated
full_rate = lat_active or lane_changing or calibrating
# Standby transitions (standstill only)
should_standby = standstill and not full_rate
if should_standby and not model_standby:
params_memory.put_bool("ModelStandby", True)
model_standby = True
cloudlog.warning("modeld: standby ON (standstill)")
elif not should_standby and model_standby:
params_memory.put_bool("ModelStandby", False)
model_standby = False
run_count = 0
frame_dropped_filter.x = 0.
cloudlog.warning("modeld: standby OFF")
if model_standby:
now = _time.monotonic()
if now - last_standby_ts_write > 1.0:
params_memory.put("ModelStandbyTs", str(now))
last_standby_ts_write = now
last_vipc_frame_id = meta_main.frame_id
continue
# Reduced framerate: daylight 10fps (skip 1/2), night 4fps (skip 4/5)
# Daytime runs twice as fast — better model responsiveness when lighting is good
# and the neural net has more signal. Night stays conservative for power.
# Write standby timestamp so controlsd suppresses transient errors
if not full_rate:
is_daylight = params_memory.get_bool("IsDaylight")
skip_interval = 2 if is_daylight else 5
target_fps = b"10" if is_daylight else b"4"
if params_memory.get("ModelFps") != target_fps:
params_memory.put("ModelFps", target_fps.decode())
now = _time.monotonic()
if now - last_standby_ts_write > 1.0:
params_memory.put("ModelStandbyTs", str(now))
last_standby_ts_write = now
if run_count % skip_interval != 0:
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")
# CLEARPILOT: constant 20fps. Variable-rate + standby logic removed — the
# variable-rate path caused freq_ok cascades in downstream consumers
# (calibrationd/locationd/paramsd). Running at the camera's native rate is
# simpler and keeps the full-stack localization chain happy.
desire = DH.desire
is_rhd = sm["driverMonitoringState"].isRHD
@@ -366,12 +318,21 @@ def main(demo=False):
**({'radar_tracks': radar_tracks,} if DISABLE_RADAR else {}),
}
mt1 = time.perf_counter()
model_output = model.run(buf_main, buf_extra, model_transform_main, model_transform_extra, inputs, prepare_only)
mt2 = time.perf_counter()
model_execution_time = mt2 - mt1
# CLEARPILOT: in park, serve the cached last model output instead of running
# GPU inference. First cycle (no cache yet) still runs once so we have
# something to serve. Out-of-park resumes fresh inference every frame.
parked = sm['carState'].gearShifter == car.CarState.GearShifter.park
if parked and last_model_output is not None:
model_output = last_model_output
model_execution_time = 0.0
else:
mt1 = time.perf_counter()
model_output = model.run(buf_main, buf_extra, model_transform_main, model_transform_extra, inputs, prepare_only)
mt2 = time.perf_counter()
model_execution_time = mt2 - mt1
if model_output is not None:
last_model_output = model_output
modelv2_send = messaging.new_message('modelV2')
posenet_send = messaging.new_message('cameraOdometry')
fill_model_msg(modelv2_send, model_output, publish_state, meta_main.frame_id, meta_extra.frame_id, frame_id, frame_drop_ratio,
+22 -2
View File
@@ -21,6 +21,7 @@ def dmonitoringd_thread():
v_cruise_last = 0
driver_engaged = False
dbg_prev_valid = True # CLEARPILOT: track valid transitions
# 10Hz <- dmonitoringmodeld
while True:
@@ -43,7 +44,18 @@ def dmonitoringd_thread():
# Get data from dmonitoringmodeld
events = Events()
if sm.all_checks() and len(sm['liveCalibration'].rpyCalib):
# CLEARPILOT: narrow update_states gate. The original sm.all_checks() also
# required modelV2 fresh (stops at standstill in two-state modeld) and
# liveCalibration.valid (calibrationd cascades its own freq_ok to valid, which
# flaps). Both made DM freeze pose → face_detected stuck False → awareness
# decayed to 0 within 6s of engagement. Narrow the gate to the subs
# update_states actually reads, and only to alive+valid (skip freq_ok and
# skip liveCalibration.valid). rpyCalib presence is sufficient to know
# calibration has produced output.
if (sm.alive['driverStateV2'] and sm.valid['driverStateV2'] and
sm.alive['carState'] and sm.valid['carState'] and
sm.alive['controlsState'] and sm.valid['controlsState'] and
sm.alive['liveCalibration'] and len(sm['liveCalibration'].rpyCalib) > 0):
driver_status.update_states(sm['driverStateV2'], sm['liveCalibration'].rpyCalib, sm['carState'].vEgo, sm['controlsState'].enabled)
# Block engaging after max number of distrations
@@ -54,8 +66,16 @@ def dmonitoringd_thread():
# Update events from driver state
driver_status.update_events(events, driver_engaged, sm['controlsState'].enabled, sm['carState'].standstill)
# CLEARPILOT: log on transition to invalid so we can see which sub caused the cascade
dm_valid = sm.all_checks()
if dm_valid != dbg_prev_valid and not dm_valid:
import sys
bad = [s for s in sm.alive if not (sm.alive[s] and sm.valid[s] and sm.freq_ok.get(s, True))]
details = [f"{s}(a={sm.alive[s]},v={sm.valid[s]},f={sm.freq_ok[s]})" for s in bad]
print(f"CLP driverMonitoringState valid=False: {' '.join(details)}", file=sys.stderr, flush=True)
dbg_prev_valid = dm_valid
# build driverMonitoringState packet
dat = messaging.new_message('driverMonitoringState', valid=sm.all_checks())
dat = messaging.new_message('driverMonitoringState', valid=dm_valid)
dat.driverMonitoringState = {
"events": events.to_msg(),
"faceDetected": driver_status.face_detected,
+19 -22
View File
@@ -1,5 +1,4 @@
#!/usr/bin/env python3
import os
from abc import ABC, abstractmethod
from openpilot.common.realtime import DT_TRML
@@ -7,11 +6,11 @@ from openpilot.common.numpy_fast import interp
from openpilot.common.swaglog import cloudlog
from openpilot.selfdrive.controls.lib.pid import PIDController
BENCH_MODE = os.environ.get("BENCH_MODE") == "1"
class BaseFanController(ABC):
@abstractmethod
def update(self, cur_temp: float, ignition: bool, standstill: bool = False) -> int:
def update(self, cur_temp: float, ignition: bool, standstill: bool = False,
is_parked: bool = True, cruise_engaged: bool = False) -> int:
pass
@@ -23,29 +22,27 @@ 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, standstill: bool = False) -> int:
# CLEARPILOT: bench mode always uses normal onroad fan range (30-100%)
if BENCH_MODE and ignition:
def update(self, cur_temp: float, ignition: bool, standstill: bool = False,
is_parked: bool = True, cruise_engaged: bool = False) -> int:
# CLEARPILOT fan range rules:
# parked → 0-100% (full, no floor)
# in drive + cruise engaged (any speed, inc standstill) → 30-100%
# in drive + cruise off + standstill → 10-100%
# in drive + cruise off + moving → 30-100%
# In the PID output, neg_limit is how negative it can go (= max fan as %),
# pos_limit is how positive (= negative of min fan %).
if 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
# CLEARPILOT: at standstill below 74°C, clamp to 0-30% (quiet)
# at standstill above 74°C, allow full 0-100% range
elif ignition and standstill and cur_temp < 74:
self.controller.neg_limit = -30
self.controller.pos_limit = 0
elif ignition and standstill:
elif standstill:
self.controller.neg_limit = -100
self.controller.pos_limit = 0
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
self.controller.pos_limit = -10
else:
self.controller.neg_limit = -30
self.controller.pos_limit = 0
self.controller.neg_limit = -100
self.controller.pos_limit = -30
if ignition != self.last_ignition:
self.controller.reset()
+9 -3
View File
@@ -36,8 +36,8 @@ class PowerMonitoring:
# Reset capacity if it's low
self.car_battery_capacity_uWh = max((CAR_BATTERY_CAPACITY_uWh / 10), int(car_battery_capacity_uWh))
# CLEARPILOT: hardcoded 10 minute shutdown timer (not user-configurable)
self.device_shutdown_time = 600
# CLEARPILOT: hardcoded 30 minute shutdown timer (not user-configurable)
self.device_shutdown_time = 1800
self.low_voltage_shutdown = VBATT_PAUSE_CHARGING
# Calculation tick
@@ -122,7 +122,13 @@ class PowerMonitoring:
offroad_time > VOLTAGE_SHUTDOWN_MIN_OFFROAD_TIME_S)
should_shutdown |= offroad_time > self.device_shutdown_time
should_shutdown |= low_voltage_shutdown
should_shutdown |= (self.car_battery_capacity_uWh <= 0)
# CLEARPILOT: removed `car_battery_capacity_uWh <= 0` trigger. That's a virtual
# capacity counter floor-limited to 3e6 µWh on boot which drains in ~12 min at
# typical device power. With a short drive that doesn't fully recharge (charges
# at 45W, cap 30e6 µWh = 36 min to full), a quick store stop could trip shutdown
# well before the intended 30-min idle timer. The real protection we want here
# is the car battery voltage check (kept above) — the virtual counter is now
# retained only for telemetry.
should_shutdown &= not ignition
should_shutdown &= (not self.params.get_bool("DisablePowerDown"))
should_shutdown &= in_car
+15 -5
View File
@@ -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
@@ -292,8 +292,18 @@ def thermald_thread(end_event, hw_queue) -> None:
msg.deviceState.maxTempC = all_comp_temp
if fan_controller is not None:
standstill = sm['carState'].standstill if sm.seen['carState'] else False
msg.deviceState.fanSpeedPercentDesired = fan_controller.update(all_comp_temp, onroad_conditions["ignition"], standstill)
# CLEARPILOT: fan rules based on gear (parked vs drive) and cruise-engaged state
if sm.seen['carState']:
cs = sm['carState']
standstill = cs.standstill
is_parked = cs.gearShifter == car.CarState.GearShifter.park
else:
standstill = False
is_parked = True # default safe: assume parked, no fan floor
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:
@@ -422,10 +432,10 @@ def thermald_thread(end_event, hw_queue) -> None:
if power_monitor.should_shutdown(onroad_conditions["ignition"], in_car, off_ts, started_seen):
cloudlog.warning(f"shutting device down, offroad since {off_ts}")
# CLEARPILOT: signal dashcamd to close recording gracefully before power-off
params.put_bool("DashcamShutdown", True)
params_memory.put_bool("DashcamShutdown", True)
deadline = time.monotonic() + 15.0
while time.monotonic() < deadline:
if not params.get_bool("DashcamShutdown"):
if not params_memory.get_bool("DashcamShutdown"):
cloudlog.info("dashcamd shutdown ack received")
break
time.sleep(0.5)
+3 -1
View File
@@ -122,9 +122,11 @@ void HomeWindow::offroadTransition(bool offroad) {
slayout->setCurrentWidget(ready);
} else {
// CLEARPILOT: start onroad in splash — updateState will switch to
// camera view once the car shifts out of park
// camera view once the car shifts out of park. Reset has_driven so
// fresh ignition shows the READY text (not the post-drive textless splash).
LOGW("CLP UI: onroad transition -> showing splash (parked)");
was_parked_onroad = true;
ready->has_driven = false;
slayout->setCurrentWidget(ready);
}
}
+31 -20
View File
@@ -79,9 +79,10 @@ void OnroadWindow::updateState(const UIState &s) {
nvg->update(); // CLEARPILOT: force repaint every frame for HUD elements
QColor bgColor = bg_colors[s.status];
if (paramsMemory.getBool("no_lat_lane_change") == 1) {
// CLEARPILOT: read from frogpilotCarControl cereal message (was paramsMemory)
if ((*s.sm)["frogpilotCarControl"].getFrogpilotCarControl().getNoLatLaneChange()) {
bgColor = bg_colors[STATUS_DISENGAGED];
}
}
if (bg != bgColor) {
bg = bgColor;
@@ -606,9 +607,9 @@ void AnnotatedCameraWidget::drawSpeedLimitSign(QPainter &p) {
p.setFont(InterFont(30, QFont::Bold));
p.drawText(innerRect.adjusted(0, 48, 0, 0), Qt::AlignTop | Qt::AlignHCenter, "LIMIT");
// Speed limit number
// Speed limit number — shifted down ~10% of innerRect height via extra top inset
p.setFont(InterFont(90, QFont::Bold));
p.drawText(innerRect.adjusted(0, 42, 0, 0), Qt::AlignCenter, clpSpeedLimitDisplay);
p.drawText(innerRect.adjusted(0, 86, 0, 0), Qt::AlignCenter, clpSpeedLimitDisplay);
} else {
// Normal: white background, black border and text
QColor borderColor(0, 0, 0);
@@ -634,9 +635,9 @@ void AnnotatedCameraWidget::drawSpeedLimitSign(QPainter &p) {
p.setFont(InterFont(30, QFont::Bold));
p.drawText(innerRect.adjusted(0, 48, 0, 0), Qt::AlignTop | Qt::AlignHCenter, "LIMIT");
// Speed limit number
// Speed limit number — shifted down ~10% of innerRect height via extra top inset
p.setFont(InterFont(90, QFont::Bold));
p.drawText(innerRect.adjusted(0, 42, 0, 0), Qt::AlignCenter, clpSpeedLimitDisplay);
p.drawText(innerRect.adjusted(0, 86, 0, 0), Qt::AlignCenter, clpSpeedLimitDisplay);
}
p.restore();
@@ -690,7 +691,7 @@ void AnnotatedCameraWidget::drawCruiseWarningSign(QPainter &p) {
// Cruise speed number
p.setFont(InterFont(90, QFont::Bold));
p.drawText(innerRect.adjusted(0, 42, 0, 0), Qt::AlignCenter, clpCruiseWarningSpeed);
p.drawText(innerRect.adjusted(0, 86, 0, 0), Qt::AlignCenter, clpCruiseWarningSpeed);
} else {
// Normal: colored background with white border/text
QColor bgColor = isOver ? QColor(200, 30, 30, 240) : QColor(40, 160, 60, 240);
@@ -716,7 +717,7 @@ void AnnotatedCameraWidget::drawCruiseWarningSign(QPainter &p) {
// Cruise speed number
p.setFont(InterFont(90, QFont::Bold));
p.drawText(innerRect.adjusted(0, 42, 0, 0), Qt::AlignCenter, clpCruiseWarningSpeed);
p.drawText(innerRect.adjusted(0, 86, 0, 0), Qt::AlignCenter, clpCruiseWarningSpeed);
}
p.restore();
@@ -732,12 +733,14 @@ void AnnotatedCameraWidget::drawText(QPainter &p, int x, int y, const QString &t
// CLEARPILOT: System health overlay — shows metrics that indicate the system
// is overburdened or behind. Toggled via ClearpilotShowHealthMetrics param.
// Metrics (top→bottom): LAG, DROP, TEMP, CPU, MEM
// LAG (ms): controlsd cumLagMs — most direct "is the loop keeping up" indicator
// Metrics (top→bottom): FPS, DROP, TEMP, CPU, MEM, FAN
// FPS: modeld framerate — 20 normally, 0 in park. Read from ModelFps memory
// param which modeld writes only on transition.
// DROP (%): modelV2 frameDropPerc — modeld losing frames; controlsd errors >20%
// TEMP (°C): deviceState.maxTempC — thermal throttling starts ~75, serious >88
// CPU (%): max core from deviceState.cpuUsagePercent
// MEM (%): deviceState.memoryUsagePercent
// FAN (%): actual fan duty from peripheralState RPM (scaled to 6500 RPM = 100%)
// Each value color-codes green/yellow/red by severity.
void AnnotatedCameraWidget::drawHealthMetrics(QPainter &p) {
static bool enabled = Params().getBool("ClearpilotShowHealthMetrics");
@@ -750,16 +753,18 @@ void AnnotatedCameraWidget::drawHealthMetrics(QPainter &p) {
if (!enabled) return;
SubMaster &sm = *(uiState()->sm);
auto cs = sm["controlsState"].getControlsState();
auto ds = sm["deviceState"].getDeviceState();
auto mv = sm["modelV2"].getModelV2();
auto ps = sm["peripheralState"].getPeripheralState();
float lag_ms = cs.getCumLagMs();
int model_fps = paramsMemory.getInt("ModelFps");
float drop_pct = mv.getFrameDropPerc();
float temp_c = ds.getMaxTempC();
int mem_pct = ds.getMemoryUsagePercent();
int cpu_pct = 0;
for (auto v : ds.getCpuUsagePercent()) cpu_pct = std::max(cpu_pct, (int)v);
// Actual fan (not commanded): scale RPM to 0-100 using 6500 RPM as full scale
int fan_pct = std::min(100, (int)(ps.getFanSpeedRpm() * 100 / 6500));
auto color_for = [](float v, float warn, float crit) {
if (v >= crit) return QColor(0xff, 0x50, 0x50); // red
@@ -769,11 +774,12 @@ void AnnotatedCameraWidget::drawHealthMetrics(QPainter &p) {
struct Row { QString label; QString value; QColor color; };
Row rows[] = {
{"LAG", QString::number((int)lag_ms), color_for(lag_ms, 50.f, 200.f)},
{"FPS", QString::number(model_fps), QColor(0xff, 0xff, 0xff)},
{"DROP", QString::number((int)drop_pct),color_for(drop_pct, 5.f, 15.f)},
{"TEMP", QString::number((int)temp_c), color_for(temp_c, 75.f, 88.f)},
{"CPU", QString::number(cpu_pct), color_for((float)cpu_pct, 75.f, 90.f)},
{"MEM", QString::number(mem_pct), color_for((float)mem_pct, 70.f, 85.f)},
{"FAN", QString::number(fan_pct), QColor(0xff, 0xff, 0xff)},
};
p.save();
@@ -797,8 +803,8 @@ void AnnotatedCameraWidget::drawHealthMetrics(QPainter &p) {
int text_y = y + margin + fm.ascent();
for (int i = 0; i < n; i++) {
p.setPen(rows[i].color);
// label left, value right
p.drawText(x + margin, text_y, rows[i].label);
// label left (shifted -50px per user request), value right
p.drawText(x + margin - 50, text_y, rows[i].label);
QRect vrect = fm.boundingRect(rows[i].value);
p.drawText(x + panel_w - margin - vrect.width(), text_y, rows[i].value);
text_y += row_h + gap;
@@ -853,12 +859,14 @@ void AnnotatedCameraWidget::drawLaneLines(QPainter &painter, const UIState *s) {
// CLEARPILOT: nightrider lines are 1px wider (3 instead of 2)
int outlineWidth = outlineOnly ? 3 : 2;
// CLEARPILOT: lane lines 5% thinner than the generic outline (QPen accepts float width)
float laneLineWidth = outlineOnly ? (float)outlineWidth * 0.95f : (float)outlineWidth;
// lanelines
for (int i = 0; i < std::size(scene.lane_line_vertices); ++i) {
QColor lineColor = QColor::fromRgbF(1.0, 1.0, 1.0, std::clamp<float>(scene.lane_line_probs[i], 0.0, 0.7));
if (outlineOnly) {
painter.setPen(QPen(lineColor, outlineWidth));
painter.setPen(QPen(lineColor, laneLineWidth));
painter.setBrush(Qt::NoBrush);
} else {
painter.setPen(Qt::NoPen);
@@ -885,7 +893,8 @@ void AnnotatedCameraWidget::drawLaneLines(QPainter &painter, const UIState *s) {
// paint center lane path
// QColor bg_colors[CHANGE_LANE_PATH_COLOR];
bool is_no_lat_lane_change = paramsMemory.getBool("no_lat_lane_change");
// CLEARPILOT: read from frogpilotCarControl cereal message (was paramsMemory)
bool is_no_lat_lane_change = sm["frogpilotCarControl"].getFrogpilotCarControl().getNoLatLaneChange();
QColor center_lane_color;
@@ -946,9 +955,11 @@ void AnnotatedCameraWidget::drawLaneLines(QPainter &painter, const UIState *s) {
}
if (outlineOnly) {
// CLEARPILOT: center path (tire track) is 2x wider than other lines in nightrider
painter.setPen(QPen(QColor(center_lane_color.red(), center_lane_color.green(),
center_lane_color.blue(), 180), outlineWidth * 2));
// CLEARPILOT: in nightrider, the tire path outline is light blue at 3px.
// Uses a fixed light-blue instead of center_lane_color (which is status-tinted) so
// the path reads as a neutral guide, not as engagement/status feedback.
QColor lightBlue(153, 204, 255, 220); // #99CCFF light blue, mostly opaque
painter.setPen(QPen(lightBlue, 3));
painter.setBrush(Qt::NoBrush);
} else {
painter.setPen(Qt::NoPen);
Binary file not shown.
+8 -3
View File
@@ -118,8 +118,10 @@ void update_model(UIState *s,
}
// update path
// CLEARPILOT: read from frogpilotCarControl cereal message (was paramsMemory)
bool no_lat_lane_change = (*s->sm)["frogpilotCarControl"].getFrogpilotCarControl().getNoLatLaneChange();
float path;
if (paramsMemory.getBool("no_lat_lane_change")) {
if (no_lat_lane_change) {
path = (float)LANE_CHANGE_NO_LAT_PATH_WIDTH / 20; // Release: better calc for EU users
} else {
path = (float)CENTER_LANE_WIDTH / 20; // Release: better calc for EU users
@@ -439,7 +441,7 @@ void UIState::updateStatus() {
UIState::UIState(QObject *parent) : QObject(parent) {
sm = std::make_unique<SubMaster, const std::initializer_list<const char *>>({
"modelV2", "controlsState", "liveCalibration", "radarState", "deviceState",
"pandaStates", "carParams", "driverMonitoringState", "carState", "liveLocationKalman", "driverStateV2",
"pandaStates", "peripheralState", "carParams", "driverMonitoringState", "carState", "liveLocationKalman", "driverStateV2",
"wideRoadCameraState", "managerState", "uiPlan", "liveTorqueParameters",
"frogpilotCarControl", "frogpilotDeviceState", "frogpilotPlan",
"gpsLocation",
@@ -556,7 +558,10 @@ void Device::updateWakefulness(const UIState &s) {
}
if (ignition_state_changed) {
if (ignition_on && s.scene.screen_brightness_onroad == 0 && !s.scene.standby_mode) {
if (!ignition_on) {
// CLEARPILOT: ignition on→off blanks the screen immediately (tap still wakes).
resetInteractiveTimeout(0, 0);
} else if (s.scene.screen_brightness_onroad == 0 && !s.scene.standby_mode) {
resetInteractiveTimeout(0, 0);
} else {
resetInteractiveTimeout(s.scene.screen_timeout, s.scene.screen_timeout_onroad);
+34 -8
View File
@@ -20,17 +20,16 @@ from openpilot.common.time import system_time_valid
from openpilot.system.hardware.tici.pins import GPIO
def is_daylight(lat: float, lon: float, utc_dt: datetime.datetime) -> bool:
"""Return True if it's between sunrise and sunset at the given location.
Uses NOAA simplified solar position equations. Pure math, no external libs."""
def _sunrise_sunset_min(lat: float, lon: float, utc_dt: datetime.datetime):
"""Compute (sunrise_min, sunset_min) in UTC minutes since midnight of utc_dt's day.
Values can be negative or >1440 for western/eastern longitudes. Returns
(None, None) for polar night, ('always', 'always') for midnight sun."""
n = utc_dt.timetuple().tm_yday
gamma = 2 * math.pi / 365 * (n - 1 + (utc_dt.hour - 12) / 24)
# Equation of time (minutes)
eqtime = 229.18 * (0.000075 + 0.001868 * math.cos(gamma)
- 0.032077 * math.sin(gamma)
- 0.014615 * math.cos(2 * gamma)
- 0.040849 * math.sin(2 * gamma))
# Solar declination (radians)
decl = (0.006918 - 0.399912 * math.cos(gamma)
+ 0.070257 * math.sin(gamma)
- 0.006758 * math.cos(2 * gamma)
@@ -42,14 +41,41 @@ def is_daylight(lat: float, lon: float, utc_dt: datetime.datetime) -> bool:
cos_ha = (math.cos(zenith) / (math.cos(lat_rad) * math.cos(decl))
- math.tan(lat_rad) * math.tan(decl))
if cos_ha < -1:
return True # midnight sun
return ('always', 'always') # midnight sun
if cos_ha > 1:
return False # polar night
return (None, None) # polar night
ha = math.degrees(math.acos(cos_ha))
sunrise_min = 720 - 4 * (lon + ha) - eqtime
sunset_min = 720 - 4 * (lon - ha) - eqtime
return (sunrise_min, sunset_min)
def is_daylight(lat: float, lon: float, utc_dt: datetime.datetime) -> bool:
"""Return True if the sun is currently above the horizon at (lat, lon).
Handles west-of-Greenwich correctly: at UTC midnight it may still be
evening local time, and the relevant sunset is the PREVIOUS UTC day's
value (which is >1440 min if we re-ref to that day, i.e. it's past
midnight UTC). Symmetric case for east-of-Greenwich at the other end.
Strategy: compute sunrise/sunset for yesterday, today, and tomorrow (each
relative to its own UTC midnight), shift them all onto today's clock
(yesterday = -1440, tomorrow = +1440), and check if now_min falls inside
any of the three [sunrise, sunset] intervals.
"""
now_min = utc_dt.hour * 60 + utc_dt.minute + utc_dt.second / 60
return sunrise_min <= now_min <= sunset_min
for day_offset in (-1, 0, 1):
d = utc_dt + datetime.timedelta(days=day_offset)
sr, ss = _sunrise_sunset_min(lat, lon, d)
if sr == 'always':
return True
if sr is None:
continue # polar night this day
sr += day_offset * 1440
ss += day_offset * 1440
if sr <= now_min <= ss:
return True
return False
def at_cmd(cmd: str) -> str: