83 Commits

Author SHA1 Message Date
brianhansonxyz 0d1ceddad2 ui: restore ready_text.png splash sprite
ready.cc:75 loads /data/openpilot/selfdrive/clearpilot/theme/clearpilot/images/ready_text.png
to render the 'ready' text on the splash, but the asset never got copied
during the C++ UI port (b5a0b32). isNull() check meant it just silently drew
nothing. Copied verbatim from the broken tree.
2026-05-04 19:07:54 -05:00
brianhansonxyz 20ea43f317 nightrider: render lane-change UI (was hidden by disengaged-skip)
During a lane change controlsd writes no_lat_lane_change=true; OnroadWindow
forces edgeColor to STATUS_DISENGAGED so the bg color flips to the disengaged
hue. drawLaneLines's nightrider early-return ("hide all when disengaged")
was matching that and skipping the entire frame, so nightrider showed nothing
during the lane change.

Fixes:
- Hoist is_no_lat_lane_change above the early-return.
- Skip the early-return during lane change, so lane lines + road edges +
  blind-spot path still render in nightrider while the maneuver is active
  (matching how nightrider looks when normally engaged).
- In the outline-only track-polygon branch: when in lane change, draw the
  track as a 4 px outline of CHANGE_LANE_PATH_COLOR (the same yellow used
  to fill the polygon in normal mode) instead of the usual 3 px light-blue
  guide. Hollow shape, thicker stroke — matches Brian's spec.

Normal-mode rendering is unchanged.
2026-05-04 19:07:54 -05:00
brianhansonxyz f6516eb4cc CLAUDE.md: bring up to date with current state
Reorganized 'Current State' to enumerate what's actually active in the
tree now (boot/build infra, suppressed logging, gpsd, UI port, display
modes, speed_logicd, sound, dashcamd) instead of the brief summary of
just the early commits.

Trimmed 'Pending' to remove items that are now ported (UI, gpsd, sound,
speed_logic, display modes, dashcamd, memory-param scaffolding). Kept
the still-pending items: HDA2 driving-behavior tweaks, CarSpeedLimit
publisher, telemetry, bench mode, power/thermal, session logging.

Notes added:
- Memory-param keys currently registered (so the next port doesn't
  re-register existing keys by accident)
- camerad gating change to always_run (and the power tradeoff)
- Branch policy: clearpilot is the canonical/main branch; do not
  abandon it for another
- pkill _text by comm in the launch chain
- speed-limit widget shows 0 until carstate.py CAN-FD writes
  CarSpeedLimit
2026-05-03 23:19:41 -05:00
brianhansonxyz 8a7a776f9b port dashcamd: hardware-encoded MP4 dashcam
VisionIPC frames from camerad → OMX H.264 hardware encoder → 3-min MP4
segments + SRT GPS subtitles in /data/media/0/videos/<trip>/. Manages
its own trip lifecycle (WAITING/RECORDING/IDLE_TIMEOUT) and writes
DashcamState/DashcamFrames memory params for the UI's Status window.
Honors DashcamShutdown for graceful close before power-off.

Files added:
- selfdrive/clearpilot/dashcamd.cc + SConscript

Files modified:
- selfdrive/frogpilot/screenrecorder/omx_encoder.{cc,h}: ported broken's
  version, which adds encode_frame_nv12() (direct NV12 input from camerad,
  alongside the existing encode_frame_rgba used by the disabled screen
  recorder) and simplifies the libyuv conversion paths to NEON-only since
  this device is aarch64.
- selfdrive/SConscript: register selfdrive/clearpilot/SConscript so the
  dashcamd binary is part of the build.
- selfdrive/manager/process_config.py:
  - camerad gating driverview → always_run so dashcamd can record the
    moment ignition+drive arrives without waiting for camera startup.
  - Register dashcamd as NativeProcess gated always_run.
- system/loggerd/deleter.py:
  - MIN_BYTES 5 GB → 9 GB to leave headroom for dashcam footage.
  - delete_oldest_video(): trip-aware cleanup. Drops entire oldest trip
    dir first; if only the active trip remains, drops oldest segment
    inside it; cleans up legacy flat .mp4s too.
  - cleanup_log2(): keeps /data/log2 session logs under 4 GB total.
  - Hooked into deleter_thread: video first when out of bytes/percent;
    log2 quota check on the idle path. New code uses print(stderr) per
    the no-cloudlog rule.

Verified: built clean, manager started, dashcamd in WAITING state
(DashcamState=waiting, DashcamFrames=0), camerad running, no errors.
2026-05-03 23:14:23 -05:00
brianhansonxyz 3f5172b58b controlsd: 5-state ScreenDisplayMode machine + park->drive auto-wake
Replaces the 3-state cycle in clearpilot_state_control with broken's
5-state machine:
- Onroad (drive gear): 0→4, 1→2, 2→3, 3→4, 4→2 (auto modes never
  reached via the button — gpsd's day/night calc is the only path back)
- Not in drive: anything except 3 → 3 (screen off), state 3 → 0 (auto)

Also tracks park→drive edge to auto-wake from screen-off (mode 3 → 0)
when shifting into drive, regardless of how the screen got turned off.

Speed/cruise overlay state intentionally left out — owned by
speed_logicd, not controlsd.

events.py: remove clp_debug_notice + EventName.clpDebug binding
(no more "Clearpilot Debug Function Executed (N)" on-screen message).
The dead commented-out events.add(EventName.clpDebug) and stray print
of buttonEvents in controlsd are also cleaned up.
2026-05-03 23:06:57 -05:00
brianhansonxyz b5a0b3221c port C++ UI from broken: ready/splash, ClearPilot menu, onroad widgets, nightrider
Wholesale port of the 10 UI files (main.cc, ui.{cc,h}, onroad.{cc,h},
home.{cc,h}, window.{cc,h}, ready.{cc,h}, SConscript). sidebar.{cc,h}
unchanged.

Brings in:
- splash/ready screen rendered via Qt directly (replaces stock home)
- ready page shown when started but gear=park
- ClearPilot offroad menu (Home/Dashcam/Debug panels) replacing stock
- onroad UI: speed widget reading ClearpilotSpeedDisplay (gpsLocation
  via speed_logicd), speed-limit widget, cruise-vs-limit warning sign
- nightrider mode: camera suppressed, lane lines/path drawn as 2px
  outlines only (ScreenDisplayMode 1 or 4)
- screen power: ScreenDisplayMode 3 forces setAwake(false)
- ignition off blanks screen immediately (tap still wakes)
- Qt-based RPC widget-tree dump server at ipc:///tmp/clearpilot_ui_rpc
- crash handler in main.cc with stack trace

Deviation from broken (3 sites): no_lat_lane_change is read from
paramsMemory (controlsd writes it there in baseline) instead of broken's
custom cereal field frogpilotCarControl.noLatLaneChange. Keeps the
existing self-driving wiring intact.

SConscript drops screenrecorder.cc from qt_src (broken did the same;
omx_encoder.cc still built for dashcamd's future link).

Build clean. UI hasn't been launched yet — that comes after the
controlsd button-handler commit lands.

Translation .ts files are auto-regenerated by Qt's lupdate; included.
2026-05-03 23:03:42 -05:00
brianhansonxyz 41c154014a add speed_logic daemon: write speed + cruise warning display params
Standalone managed process (selfdrive.clearpilot.speed_logicd) that
ticks SpeedState at 2 Hz from cereal subscriptions. Decoupled from
controlsd so self-driving timing isn't affected.

Inputs:
- gpsLocation.speed + hasFix (vehicle speed)
- carState.cruiseState (cruise speed, enabled, standstill)
- CarSpeedLimit memory param (will be populated when carstate.py CAN-FD
  decode is ported; 0 until then — speed-limit + warning logic
  naturally short-circuits)
- IsMetric persistent param (read once at startup)

Outputs (memory params, gated on change to avoid 2 Hz tmpfs churn):
- ClearpilotSpeedDisplay, ClearpilotHasSpeed, ClearpilotSpeedUnit
- ClearpilotSpeedLimitDisplay, ClearpilotIsMetric
- ClearpilotCruiseWarning, ClearpilotCruiseWarningSpeed
- ClearpilotPlayDing (one-shot trigger consumed by soundd)

speed_logic.py copied verbatim from broken; speed_logicd.py is new
glue. Registered as PythonProcess gated on only_onroad.
2026-05-03 22:52:39 -05:00
brianhansonxyz 5d2fe5248b soundd: mix ClearPilot ding alongside alerts; add ding.wav
Adds the ding sound effect that fires on speed-limit changes / cruise-
warning transitions (writers will set ClearpilotPlayDing="1" in
/dev/shm/params; soundd polls at ~2Hz, clears the flag, plays once).

The ding plays at MAX_VOLUME independent of the alert volume map and
mixes into the same output stream. Single-shot — no looping. Loaded
once at startup; failure to load just disables the ding (rest of soundd
keeps working).

print-to-stderr, not cloudlog (CLAUDE.md rule).
2026-05-03 22:50:46 -05:00
brianhansonxyz f687d712e7 gpsd: re-add day/night auto display-mode switch
Restores the IsDaylight + ScreenDisplayMode auto-switch logic that was
stripped during the initial gpsd port (since the display-modes feature
hadn't been touched yet).

NOAA solar-position calc against the current GPS fix:
- First calc immediately when system clock is valid + GPS has fix
- Every 30s thereafter
- Writes IsDaylight memory param on change (twice/day, gated)
- ScreenDisplayMode 0 + sunset → 1 (auto-nightrider)
- ScreenDisplayMode 1 + sunrise → 0 (auto-normal)
- Manual modes 2/3/4 are never touched

Print-to-stderr instead of cloudlog per CLAUDE.md.
2026-05-03 22:47:28 -05:00
brianhansonxyz 837c14e081 register clearpilot memory params and seed defaults in manager_init
Scaffolding for the upcoming UI port. Registers the keys the new C++
UI and SpeedState daemon will read/write, and seeds /dev/shm/params
defaults at manager start so first-readers don't see missing keys
before writers spin up.

Keys registered: CarIsMetric, ClearpilotCruiseWarning,
ClearpilotCruiseWarningSpeed, ClearpilotHasSpeed, ClearpilotIsMetric,
ClearpilotPlayDing, ClearpilotShowHealthMetrics,
ClearpilotSpeedDisplay, ClearpilotSpeedLimitDisplay,
ClearpilotSpeedUnit, DashcamFrames, DashcamShutdown, DashcamState,
IsDaylight, ModelFps, ModelStandby, ModelStandbyTs,
ScreenRecorderDebug, ShutdownTouchReset, TelemetryEnabled, VpnEnabled.

PERSISTENT registration here is purely a no-op placeholder: the actual
storage is /dev/shm/params (tmpfs), which clears on every reboot
regardless. Bench-mode keys (Bench*, ClpUiState, LogDirInitialized)
not registered yet — they'll come with their respective features.
2026-05-03 22:45:04 -05:00
brianhansonxyz 3ebfc29d35 port clearpilot gpsd; decouple self-driving from GPS
Adds the AT-command-based GPS daemon for the Quectel EC25 modem (the
device has no u-blox chip and qcomgpsd's diag interface hangs on this
hardware). Trimmed from broken's version: dropped cloudlog calls and
the IsDaylight / ScreenDisplayMode auto-switching (those belong to the
display-modes feature, port later).

Used solely for system clock initialization, on-screen UI speed, and
per-segment dashcam GPS metadata.

Self-driving must NOT consume gpsLocation — feeding it to locationd's
kalman filter screws up the math. Patch locationd to skip GPS:
- locationd_thread() no longer subscribes to gpsLocation/gpsLocationExternal
- handle_msg's GPS branches commented (dead code without subscription)
- the "save LastGPSPosition once a minute when gpsOK" block commented
  (dead because gpsOK is now permanently false)

Result: liveLocationKalman.gpsOK = false for all self-driving consumers
(controlsd, paramsd, torqued, frogpilot_planner). They already handle
that case. Other liveLocationKalman fields still publish from the
camera-odometry + IMU + calibration kalman state.

system/clearpilot/__init__.py added so system.clearpilot.gpsd is a
valid Python module.
2026-05-03 22:29:07 -05:00
brianhansonxyz 54c566c68f kill stale text-error window by comm too, not just path
selfdrive/ui/text is a shell wrapper that execs ./_text. After exec,
the running process's argv has no path component, so
pkill -f 'selfdrive/ui/text' silently misses it. Add pkill -x _text
alongside the existing path-pattern kills in build_only.sh,
launch_openpilot.sh, and launch_chffrplus.sh.
2026-05-03 22:28:33 -05:00
brianhansonxyz f28ba340f2 disable segment + camera + boot data logging
We don't use comma's upload/replay pipeline, and the segment recorder
was silently filling /data with 30 MB rlog files per minute (the 64 GB
we just cleared). Disable the writers entirely.

- process_config.py: comment out loggerd, encoderd, stream_encoderd
- manager.py: skip save_bootlog() at manager init (was writing
  boot info to /data/media/0/realdata/boot/<bootid>/)

deleter still runs for cleanup of any leftover data; logmessaged still
runs (in-memory log routing IPC). uploader was already disabled in
baseline. No process consumes loggerd/encoderd output onroad, so this
is purely a sink removal.
2026-05-03 22:13:06 -05:00
brianhansonxyz 6ec4c7bdac controlsd: ScreenDisplayMode init uses put_int, not put_bool
Every other reference treats ScreenDisplayMode as an int (range 0-4).
The init line was using put_bool(0) which happened to write the same
"0" string and read back fine via get_int, but the type mismatch was
misleading.
2026-05-03 22:08:08 -05:00
brianhansonxyz b57f2d8d70 add CLAUDE.md documenting current state, snapshot locations, and port roadmap
Records: where the broken-but-feature-complete tree lives
(/data/openpilot-broken-2026-05-03 + tag pre-reset-2026-05-03), where
the baseline source still sits (/data/clearpilot-baseline), the
working-baseline-2 tag, and a categorized roadmap of features in the
broken tree that still need to be ported (driving behavior, onroad UI,
speed/cruise logic, dashcamd, gpsd, telemetry, bench mode, power/
thermal, memory params, session logging).

Carries forward the operational guardrails from the previous CLAUDE.md
(no cloudlog, chown to comma, samba/git/test workflow, params/cereal
gotchas, device specifics) so future sessions don't re-learn them.
2026-05-03 21:58:42 -05:00
brianhansonxyz b287fd094e make baseline build & launch cleanly
- onroad.cc: fix screenDisplayMode reference to nvg->screenDisplayMode
  (baseline had a stale reference to the variable after it moved into
  AnnotatedCameraWidget). Restores the original conditional intent.
- ui/SConscript + home.cc: drop QtWebEngine include/link entirely
  (no longer used by any active code).
- build.py: BUILD_ONLY env var spawns the failure TextWindow fully
  detached (own session, /dev/null stdio) so build_only.sh exits and
  caller can capture stderr; spinner binary update uses temp+os.replace
  so a running spinner doesn't ETXTBSY the build.
- build_only.sh: tee build output to /tmp/build.log and propagate
  build.py's exit code via PIPESTATUS.

Verified: build_only.sh completes cleanly, launch_openpilot.sh boots
the manager and spawns the standard process set.
2026-05-03 21:55:48 -05:00
brianhansonxyz 5624898a92 port startup-related changes from broken tree
Restores the boot/launch chain customizations on top of the freshly-reset
baseline. Driving-model and per-feature changes (dashcamd, telemetry, gpsd,
bench mode, manager wiring) are deliberately left out and will be ported
piecemeal.

Brought in:
- launch_openpilot.sh: kill stale instances, run on_start.sh, background
  vpn-monitor + nice-monitor, BENCH_MODE pass-through
- launch_chffrplus.sh: source build_preflight.sh, kill stale text-error UI
- build_only.sh, build_preflight.sh
- system/clearpilot/on_start.sh: SSH keys, ssh.service enable, git.hanson.xyz
  Host config, WiFi radio on, run provision.sh
- system/clearpilot/provision.sh + provision_wrapper.sh: connectivity wait,
  apt install (openvpn, curl, ccrypt, nodejs), Claude Code installer, git
  remote fix, fast-forward origin/clearpilot, /data/quick_boot
- system/clearpilot/vpn-monitor.sh + vpn.ovpn: OpenVPN tunnel auto-connect
- system/clearpilot/nice-monitor.sh: keep claude processes at nice 19
- system/clearpilot/dev: id_ed25519.{cpt,pub.cpt}, GithubSshKeys, encrypt.sh
  (DongleId-keyed instead of hardware-serial)
- system/clearpilot/tools/{decrypt,encrypt}: switch key source to DongleId
- system/clearpilot/startup_logo/{bg.jpg, generate_logo.sh, set_logo.sh}
- selfdrive/ui/qt/spinner{,.cc,.h}: new spinner with logo

Removed (baseline-only flow superseded by broken's on_start.sh+provision.sh):
- system/clearpilot/dev/on_start.sh
- system/clearpilot/dev/on_start_brian.sh.cpt
- system/clearpilot/dev/provision.sh
2026-05-03 21:07:14 -05:00
brianhansonxyz c2ab0fa662 reset to pre-modification baseline; restart feature work from clean state
Restoring the working tree to the pristine pre-Claude baseline previously
preserved at /data/clearpilot (now /data/clearpilot-baseline). The prior
modified-but-broken tree is snapshotted at /data/openpilot-broken-2026-05-03
and tagged here as pre-reset-2026-05-03 for reference.

From here, features (UI changes, dashcam, telemetry, GPS, display modes,
speed logic, standstill power saving, etc.) will be re-introduced one at
a time with proper testing.
2026-05-03 20:53:51 -05:00
brianhansonxyz f7e602c00b controlsd: re-wire UI hooks on top of restored baseline
Adds back the UI plumbing that the baseline controlsd doesn't have, so
the existing UI features keep working without changing driving logic:

- ScreenDisplayMode 5-state machine (auto-normal, auto-nightrider,
  manual normal, screen-off, manual nightrider) driven by the LFA
  debug button + gear edges. Replaces baseline's simple 0..2 cycle.
  Pure params write — no actuator effect.
- Speed/cruise-warning overlay tick at ~2Hz feeding SpeedState, which
  writes ClearpilotSpeedDisplay / ClearpilotSpeedLimitDisplay /
  ClearpilotCruiseWarning for the onroad UI. Reads gpsLocation,
  CarSpeedLimit, and CS.cruiseState — no actuator effect.
- frogpilot_variables.no_lat_lane_change is now populated alongside
  the existing Params write, so the perf-optimized carcontroller (which
  takes the bit as an argument instead of re-reading Params on the
  100Hz hot path) still sees the lane-change suppression signal.
- ScreenDisplayMode init switched from put_bool to put_int (UI reads
  it as int).
- gpsLocation added to SubMaster (ignore_alive/avg_freq/valid set,
  since gpsd is a ClearPilot addition not present everywhere).

No changes to controlsd's lateral or longitudinal control paths.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 08:44:52 -05:00
brianhansonxyz 47321e3867 restore driving logic to pre-variable-fps baseline
Wholesale revert of driving-relevant files to the snapshot in
/projects/openpilot/archive/clearpilot (HEAD 980f0aa). Goal: get
known-good driving behavior back, then re-introduce optimizations
slowly to track down a "feels like the wheel pulls right" regression.

Files restored from baseline:
- selfdrive/controls/controlsd.py
- selfdrive/controls/lib/events.py
- selfdrive/controls/lib/longitudinal_planner.py
- selfdrive/modeld/modeld.py
- selfdrive/modeld/dmonitoringmodeld.py
- selfdrive/locationd/calibrationd.py
- selfdrive/locationd/paramsd.py
- selfdrive/locationd/torqued.py
- selfdrive/car/interfaces.py
- selfdrive/car/hyundai/carstate.py (CAN-FD telemetry preserved as a
  commented block for future re-enable)
- selfdrive/monitoring/dmonitoringd.py
- selfdrive/frogpilot/controls/frogpilot_planner.py
- common/realtime.py

Intentionally NOT restored (kept as current):
- selfdrive/thermald/* (fan/power tuning kept)
- selfdrive/car/hyundai/carcontroller.py + hyundaicanfd.py (perf-only
  hoist of no_lat_lane_change Params read; behavior-equivalent)
- cereal/services.py, cereal/custom.capnp (additive only)
- selfdrive/manager/*, common/params.cc (heavy ClearPilot
  infrastructure: bench mode, log dir, dashcamd, gpsd, params)
- All selfdrive/ui/, selfdrive/clearpilot/, system/clearpilot/

UI features will be re-wired in a follow-up commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 08:40:02 -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
brianhansonxyz 2ddb7fc764 feat: onroad health overlay + 2x tire path in nightrider
System health overlay:
- Lower-right 5-metric panel: LAG (controlsState.cumLagMs), DROP
  (modelV2.frameDropPerc), TEMP (deviceState.maxTempC), CPU (max core of
  deviceState.cpuUsagePercent), MEM (deviceState.memoryUsagePercent)
- Color-coded white→yellow→red by severity (LAG: 50/200ms, DROP: 5/15%,
  TEMP: 75/88°C, CPU: 75/90%, MEM: 70/85%)
- Toggle in ClearPilot → Debug → "System Health Overlay"
- New param ClearpilotShowHealthMetrics, PERSISTENT (disk, survives
  reboots), default false — re-polled every ~2s so toggle takes effect
  without process restart
- InterFont(90, Bold) to match speed limit numeric styling, 30px margin,
  40px between rows, black rounded background

Nightrider center lane path (the "tire track" polygon from
scene.track_vertices) is now drawn at 2x the width of other lines —
highlights the planned path distinctly against the otherwise stark
outline-only rendering.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 22:03:30 -05:00
brianhansonxyz 22ced0c558 cleanup: remove dead CarCruiseDisplayActual param
Audit of post-fork param additions found CarCruiseDisplayActual was
written every CAN cycle (gated) but only consumed by hyundaicanfd.py::
create_buttons_alt, which has `return` on line 1 and no active callers.
Write was pure waste. Removed registration, write path, cache field,
and the dead read.

Also dropped the now-unused `from openpilot.common.params import Params`
in hyundaicanfd.py.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 22:03:01 -05:00
brianhansonxyz ef4e02e354 feat: 10fps daytime / 4fps night modeld + 2Hz GPS
Reduced-rate modeld path now branches on IsDaylight:
- daylight: skip 1/2 frames → 10fps (better model responsiveness when
  lighting gives the net more signal)
- night: skip 4/5 frames → 4fps (unchanged, conservative for power)

IsDaylight is already in /dev/shm (memory) via gpsd.py. Gated the
IsDaylight write on change — it flips twice a day, no reason to rewrite
every 30s. GPS polling bumped from 1Hz → 2Hz.

ModelFps publishes "10" / "4" / "20" so longitudinal_planner's dt and
FCW-threshold scaling (if re-enabled) still track actual rate.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 21:41:29 -05:00
brianhansonxyz 02f25f83c4 perf: hoist Params read out of create_steering_messages 100Hz path
prebuilt / build prebuilt (push) Has been cancelled
badges / create badges (push) Has been cancelled
create_steering_messages was constructing a new Params("/dev/shm/params")
object and reading no_lat_lane_change on every CAN steering message build
— i.e. 100 allocations + 100 file reads per second. Now the Params
instance lives on CarController, and the value is read once per update()
cycle and passed as a parameter.

Audited all other hyundai CAN-FD integration code for similar patterns:
- carstate.py — already fixed (previous commit)
- carcontroller.py — other Params references are all in commented-out code
- hyundaicanfd.py::create_buttons_alt — dead code (early return), so the
  Params read there never executes; left as-is

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 21:34:52 -05:00
brianhansonxyz ceb3481cdc feat: add low-speed tier to cruise warning, gate param writes on change
Speed limit ≤25 mph now allows +8 over before warning (e.g. limit 25 →
33 is ok, warn at 34). Existing tiers unchanged: ≥50 → +9 ok warn at +10,
26-49 → +6 ok warn at +7.

Also gate all 7 speed_logic param writes on change (same pattern as the
earlier carstate/controlsd perf fix). Called at 2Hz so not as hot, but
unit/is_metric never change mid-drive and the cruise warning rarely
flips — no reason to thrash /dev/shm on every update.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 21:28:05 -05:00
brianhansonxyz bae022db43 fix: thermald crash blocking shutdown (getBool → get_bool)
Python Params uses snake_case; the C++ camelCase call raised
AttributeError, killing thermald_thread at the exact moment of shutdown.
Result: DoShutdown never got set, the 10-minute timer "worked" once (set
DashcamShutdown=True) and then thermald died silently. Device kept
draining the battery instead of powering down.

Caught because CLAUDE.md specifically flags this pattern as a common
source of silent failures between C++ and Python.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 21:20:53 -05:00
brianhansonxyz 1e36d7ec23 feat: disable FCW — stock AEB handles it better
Tucson's radar-based collision warning is more reliable than the comma
model/planner FCW and was producing false positives. Single-user fork in
a single car, so no need to keep both.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 21:11:39 -05:00
brianhansonxyz 6f3b1b1d2f docs: add py-spy profiling recipe to CLAUDE.md
prebuilt / build prebuilt (push) Has been cancelled
badges / create badges (push) Has been cancelled
stale / stale (push) Has been cancelled
Captures how we found the hot Params writes in carstate.py so the same
technique can be repeated. Includes the awk aggregators for per-callsite
and per-file-line breakdowns.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 17:35:03 -05:00
brianhansonxyz 7a1e157c9c perf: gate hot /dev/shm writes on change — controlsd 69%→28% CPU
py-spy showed per-cycle atomic param writes were the dominant cost. Each
put() is mkstemp+fsync+flock+rename+fsync_dir — fine when rare, ruinous at
100Hz. At park with no state changes, these writes were running anyway and
the flock contention was poisoning the whole system.

carstate.py (update + update_canfd): CarSpeedLimit, CarIsMetric,
  CarCruiseDisplayActual were written every CAN update. Now cached and
  written only on change.
controlsd.py: same fix for LatRequested and no_lat_lane_change. Also
  throttle the sentry crash-file stat() from 100Hz to 1Hz.

Also: suppress locationdTemporaryError/paramsdTemporaryError/posenetInvalid
on lat engage (same 2s window as commIssue), and tie suppression to the
LatRequested edge instead of CC.latActive (fires immediately, not after
the 250ms ramp-up delay).

Also: reset Ratekeeper when it falls >1s behind — the ~6s fingerprinting
stall at startup was poisoning the lag metric for the entire session.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 17:34:07 -05:00
brianhansonxyz 2d819c784b feat: ramp-up delay on lat engagement to prevent commIssue flash
prebuilt / build prebuilt (push) Has been cancelled
badges / create badges (push) Has been cancelled
Decouples "tell modeld to go fast" from "steering actually active":
- New LatRequested memory param — controlsd writes when lat would be active
- modeld reads LatRequested (not carControl.latActive) for FPS decision,
  so it switches to 20fps immediately on engage request
- controlsd delays CC.latActive becoming true by 250ms (5 frames @ 20fps)
  after LatRequested goes true, giving downstream services
  (longitudinalPlan, liveCalibration, etc.) time to stabilize at the new rate

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 16:46:37 -05:00
brianhansonxyz 1eb8d41454 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>
2026-04-16 16:29:42 -05:00
brianhansonxyz 2331aa00a0 feat: dashcamd trip lifecycle, status indicator, CLAUDE.md updates
prebuilt / build prebuilt (push) Has been cancelled
badges / create badges (push) Has been cancelled
release / build master-ci (push) Has been cancelled
dashcamd now waits for valid system time + GPS fix + drive gear before
starting a trip. Returns to waiting state on 10-min park timeout or
ignition off. Publishes DashcamState and per-trip DashcamFrames to
memory params. Status window shows stopped/waiting/recording states.

Updated CLAUDE.md with current display mode behavior, OmxEncoder port
details, speed limit warning thresholds, and dashcam param docs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 01:26:58 -05:00
brianhansonxyz dfb7b7404f fix: port OmxEncoder safety fixes from upstream FrogPilot
- OMX_Init/OMX_Deinit managed per encoder instance lifecycle
- Proper error handling in constructor, encoder_open, encoder_close
- Null guards on done_out.pop() and handle in destructor
- Codec config written directly to codecpar (no codec_ctx)
- ffmpeg faststart remux on segment close
- Crash handler in dashcamd for diagnostics
- DashcamFrames param for live frame count in status window

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 01:13:41 -05:00
brianhansonxyz 9ac334b7cf fix: dashcamd OMX crash on restart, add dashcam status indicator
prebuilt / build prebuilt (push) Has been cancelled
badges / create badges (push) Has been cancelled
- Reset OMX subsystem (Deinit/Init) on dashcamd startup to clear stale
  encoder state from previous unclean exits
- Validate OMX output buffers before memcpy to prevent segfault
- Validate VisionBuf frame data before encoding
- Add dashcam row to status window showing recording state and disk usage

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 00:44:13 -05:00
brianhansonxyz 3cbb81f9f1 fix: always show splash in park, nightrider transitions to screen off
Removed nightrider exception that kept onroad UI visible in park.
Shifting to park from nightrider mode now auto-switches to screen off.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 00:12:01 -05:00
brianhansonxyz 3b47412100 feat: tap screen to wake from screen-off mode
prebuilt / build prebuilt (push) Has been cancelled
badges / create badges (push) Has been cancelled
Tapping the touchscreen while in display mode 3 (screen off) resets
ScreenDisplayMode to 0 (auto-normal) and wakes the display.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 23:59:52 -05:00
brianhansonxyz 5e7c8bed52 fix: speed limit rounding, controlsd crashes, modeld calibration rate
- Speed limit display uses round() instead of floor() to fix off-by-one
- Initialize driving_gear in controlsd __init__ to prevent AttributeError
- Fix gpsLocation capnp access (attribute style, not getter methods)
- Fix sm.valid dict access (brackets not parentheses)
- modeld runs full 20fps during calibration instead of throttled 4fps
- Over-speed warning threshold below 50mph changed from 5 to 7

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 23:55:19 -05:00
brianhansonxyz 6cf21a3698 fix: debug button screen-off mode now works in park state
updateWakefulness was overriding display power every frame when ignition
was on, fighting the screen-off set by home.cc. Now respects
ScreenDisplayMode 3 unconditionally. Also auto-resets to mode 0 when
shifting into drive from screen-off.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 04:21:37 +00:00
brianhansonxyz adcffad276 feat: speed limit ding sound when cruise warning sign appears
Plays a ding via soundd when the cruise warning sign becomes visible
(cruise set speed out of range vs speed limit) or when the speed limit
changes while the warning sign is already showing. Max 1 ding per 30s.

Ding is mixed independently into soundd output at max volume without
interrupting alert sounds. bench_cmd ding available for manual trigger.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 03:49:02 +00:00
brianhansonxyz 6e7117b177 feat: cruise warning signs and speed limit sign sizing
Cruise warning sign appears above speed limit sign when cruise set
speed is too far from the speed limit:
- Red (over): cruise >= limit + 10 (if limit >= 50) or + 5 (if < 50)
- Green (under): cruise <= limit - 5
- Only when cruise active (not paused/disabled) and limit >= 20
- Nightrider mode: colored text/border on black background

Speed limit sign enlarged 5%. 20px gap between signs. Bench mode
gains cruiseactive command (0=disabled, 1=active, 2=paused).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 03:33:27 +00:00
brianhansonxyz 8ccdb47c88 feat: speed limit sign UI and speed processing pipeline
New speed_logic.py module converts raw CAN speed limit and GPS speed
into display-ready params. Called from controlsd (live) and
bench_onroad (bench) at ~2Hz. UI reads params to render:
- Current speed (top center, hidden when 0 or no GPS)
- MUTCD speed limit sign (lower-left, normal + nightrider styles)
- Unit-aware display (mph/kph based on CAN DISTANCE_UNIT)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 03:15:07 +00:00
brianhansonxyz b84c268b6e fix: standstill fan clamp 0-30% and bench mode always 30-100%
prebuilt / build prebuilt (push) Has been cancelled
badges / create badges (push) Has been cancelled
release / build master-ci (push) Has been cancelled
stale / stale (push) Has been cancelled
Standstill quiet mode was clamped to 0-10% which is dangerously low
under sustained load. Raised to 0-30%. Bench mode now forces 30-100%
onroad fan range regardless of standstill to prevent overheating
during bench testing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 03:09:41 +00:00
brianhansonxyz ffa9da2f97 add root SSH config for git.hanson.xyz to on_start.sh
Ensures git push works without GIT_SSH_COMMAND override. Idempotent —
skips if Host entry already exists in /root/.ssh/config.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 03:00:55 +00:00
brianhansonxyz 5e7911e599 move SSH key decryption from provision.sh to on_start.sh
prebuilt / build prebuilt (push) Has been cancelled
badges / create badges (push) Has been cancelled
Keys now install to /root/.ssh/ (for root git operations) instead of
/data/ssh/.ssh/. Runs every boot via on_start.sh so keys are available
even without a full provision cycle.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 02:59:34 +00:00
brianhansonxyz 6fcd4b37ba fix: use wrapper script for claude instead of PATH modification
Create /usr/local/bin/claude wrapper that remounts / read-write before
calling the real binary. Removes PATH append to ~/.bashrc which was
duplicating on every provision run.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 02:57:40 +00:00
brianhansonxyz 8d5903b945 fix: pass log_path to athenad launcher to fix crash loop
manage_athenad was calling launcher() with only 2 args but the
per-process logging changes added a required log_path parameter.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 02:48:47 +00:00
brianhansonxyz cea8926604 fix: correct git remote repo name to clearpilot.git
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 02:44:55 +00:00
brianhansonxyz e98ae2f9d1 fix git remote: use SSH URL, add remote fixup step to provision.sh
Provision script now checks and corrects the git origin URL to the
SSH remote before fetching updates. Also fixed CLAUDE.md to reflect
the correct hostname (git.hanson.xyz, not git.internal.hanson.xyz).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 02:44:11 +00:00
brianhansonxyz 531b3edcd2 fix: decrypt SSH keys to tmpdir instead of repo, gitignore ed25519 keys
The decrypt step in provision.sh was writing decrypted private keys
directly into the source tree (system/clearpilot/dev/), leaving them
as untracked files in the repo. Now decrypts to a mktemp dir, copies
to the SSH dir, and cleans up. Also added ed25519 key paths to
.gitignore to match the existing id_rsa entries.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 02:42:52 +00:00
brianhansonxyz f46339c949 switch SSH keys to ed25519, encrypt with hardware serial instead of DongleId
prebuilt / build prebuilt (push) Has been cancelled
badges / create badges (push) Has been cancelled
- Generate new ed25519 keypair (replaces old RSA keys)
- Encrypt with device serial from /proc/cmdline (always available, no manager needed)
- Update decrypt/encrypt tools and provision.sh to use serial
- Remove dependency on DongleId param for SSH key provisioning

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 01:32:51 +00:00
brianhansonxyz 21f71cbc37 Merge branch 'clearpilot' of https://git.hanson.xyz/brianhansonxyz/clearpilot into clearpilot 2026-04-15 01:26:46 +00:00
brianhansonxyz 9d5c838fe3 fix fresh-install build: add preflight script, restore third_party binaries, remove WebEngine dep
- Add build_preflight.sh to create obj/ dirs that git can't track (body/board/obj, panda/board/obj)
- Wire preflight into build_only.sh and launch_chffrplus.sh
- Restore missing third_party binaries (libyuv, snpe, acados, maplibre) that were text pointers
- Remove dead Qt5WebEngineWidgets dependency from UI SConscript

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 01:26:40 +00:00
brianhansonxyz 4283a3d3f7 provision: add ccrypt, nodejs, claude, ssh identity keys, fix scons obj dirs
stale / stale (push) Has been cancelled
- Install ccrypt, nodejs 18, npm, claude code in provision
- Decrypt id_rsa/id_rsa.pub via dongle ID and install to /data/ssh/.ssh/
- Run provision directly instead of through qt_shell wrapper
- Fix panda and body SConscripts to mkdir obj/ before writing gitversion.h
- Add sudo to su - comma build call
- Remount / rw at top of provision

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 20:18:36 -05:00
brianhansonxyz 7221c8e216 move SSH setup to on_start.sh, runs unconditionally before provision
prebuilt / build prebuilt (push) Has been cancelled
badges / create badges (push) Has been cancelled
SSH keys and sshd start immediately on every boot, not gated behind
quick_boot or dongle check. Provision script only handles packages,
git pull, and build.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 19:13:28 -05:00
brianhansonxyz 1e150bc487 provision: show output on screen via qt_shell, capture stderr
prebuilt / build prebuilt (push) Has been cancelled
badges / create badges (push) Has been cancelled
- on_start.sh runs provision through qt_shell for on-screen display
- provision_wrapper.sh redirects stderr to stdout so errors are visible
- provision.sh: SSH setup before WiFi wait, verbose echo output,
  sleep on failure so messages are readable

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 18:46:20 -05:00
brianhansonxyz 2b481b6656 provision: simplify boot with quick_boot flag, auto-update from git
- on_start.sh: always enables WiFi, waits 30s for connectivity if
  no /data/quick_boot, then runs provision.sh
- New provision.sh: sets up SSH keys, installs openvpn, pulls latest
  code from remote (hard reset, remote wins), runs build_only.sh,
  touches /data/quick_boot on success
- Delete old dev/on_start.sh, dev/provision.sh, dev/on_start_brian.sh.cpt
  (encrypted key decryption no longer needed)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 18:41:06 -05:00
brianhansonxyz 4d0e4efd6f add root@concordia SSH keys for new device provisioning
prebuilt / build prebuilt (push) Has been cancelled
badges / create badges (push) Has been cancelled
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 15:15:12 -05:00
brianhansonxyz 55f73fd421 ready screen: hide READY! text after first drive, reset on ignition cycle
prebuilt / build prebuilt (push) Has been cancelled
badges / create badges (push) Has been cancelled
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 02:51:13 -05:00
brianhansonxyz 2b522021d5 modeld 3-mode power saving, tlog disabled, dashcamd boot fix
- modeld: full 20fps (lat active), 4fps (driving no lateral), 0fps
  (standstill). Standby timestamp written in both standby and 4fps modes
  to suppress transient errors on engagement transition
- tlog: all calls commented out (was causing 100Hz CPU load in controlsd
  and carstate). tlog client now checks TelemetryEnabled param before
  sending — zero cost when disabled
- dashcamd: wait for valid frame dimensions on startup (fix SIGSEGV),
  always_run (manages own trip lifecycle)
- Fan: driving range 15-100%

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 02:40:18 -05:00
brianhansonxyz 4756d8e32c disable tlog calls, add param gate to tlog client
- Comment out all tlog() calls in controlsd (100Hz) and carstate (100Hz)
  — was causing controlsd to lag from JSON serialization + ZMQ overhead
- tlog() now checks TelemetryEnabled memory param (1/sec file read),
  returns immediately when disabled — zero cost when telemetry is off

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 02:28:19 -05:00
brianhansonxyz 73472e5fab docs: cruise control desync investigation notes
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 02:19:31 -05:00
brianhansonxyz b85ce8176d modeld standby on lat inactive, dashcamd always-on, alert suppression
- modeld: enter standby when latActive=false (not just standstill),
  exception for lane changes (no_lat_lane_change). Fix Python capnp
  property access (.latActive not getLatActive())
- controlsd: move model_suppress computation early, suppress radarFault,
  posenetInvalid, locationdTemporaryError, paramsdTemporaryError during
  model standby + 2s grace period. All cascade from modeld not publishing
- dashcamd: always_run (manages own trip lifecycle), wait for valid frame
  dimensions before encoding (fix SIGSEGV on early start)
- Fan: driving range 15-100% (was 30-100%)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 02:13:40 -05:00
brianhansonxyz 24f29dcfb7 dashcam: reduce recording to 10fps (skip every other frame)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 01:40:06 -05:00
brianhansonxyz 6de3a8c68f disable reverse SSH — using VPN for remote access
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 01:36:57 -05:00
brianhansonxyz bf030db9fe modeld standby, fan standstill clamping, log rotation, diagnostic logging
- modeld standby: skip inference at standstill (model stays loaded in GPU),
  ModelStandby param + ModelStandbyTs heartbeat for race-free suppression
- controlsd: suppress commIssue/modeldLagging when ModelStandbyTs < 2s old,
  ignore telemetryd/dashcamd in process_not_running check
- Fan controller: standstill below 74°C clamps to 0-10% (near silent),
  standstill above 74°C allows 0-100%, thermald reads carState.standstill
- Deleter: enforce 4GB quota on /data/log2 session logs, oldest-first cleanup
- Diagnostic logging: steerTempUnavailable and controlsdLagging log to stderr
  with full context (steering angle, torque, speed, remaining time)
- CLAUDE.md: document memory params method name difference (C++ camelCase
  vs Python snake_case)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 01:35:14 -05:00
brianhansonxyz 3d9143c41b telemetry overlay, memory params fix, nightrider border, GPS logging
- Telemetry status bar in onroad UI: temp, fan %, model FPS, standstill
- Fix paramsMemory usage: Params("/dev/shm/params") not "/dev/shm/params/d"
- Telemetry/VPN toggles use ToggleControl with manual paramsMemory writes
- TelemetryEnabled/VpnEnabled registered PERSISTENT, written to memory path
- GPS telemetry: telemetryd subscribes to gpsLocation at 1Hz via cereal
- Nightrider: force CameraWidget bg black to eliminate color bleed border
- Suppress "Always On Lateral active" status bar message
- Re-enable gpsd and dashcamd
- CLAUDE.md: document memory params pattern, speed_limit.calculated usage

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 00:36:29 -05:00
brianhansonxyz 698a1647a0 nightrider mode improvements, nice monitor, ready text 1x
- Nightrider: lines 1px wider (3px outline), engagement border hidden,
  planner lines hidden when disengaged, stay on onroad view in park
- Normal mode only: return to ready splash on park
- Ready text sprite at native 1x size
- Nice monitor: keeps claude processes at nice 19, runs every 30s

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 23:24:37 -05:00
93 changed files with 1783 additions and 2160 deletions
+181 -414
View File
@@ -2,27 +2,115 @@
## Project Overview ## Project Overview
ClearPilot is a custom fork of **FrogPilot** (itself a fork of comma.ai's openpilot), based on a 2024 release. It is purpose-built for Brian Hanson's **Hyundai Tucson** (HDA2 equipped). The vehicle's HDA2 system has specific quirks around how it synchronizes driving state with openpilot that require careful handling. ClearPilot is a custom fork of **FrogPilot** (itself a fork of comma.ai's openpilot), purpose-built for Brian Hanson's **Hyundai Tucson** (HDA2 equipped). The vehicle's HDA2 system has specific quirks around how it synchronizes driving state with openpilot that require careful handling.
### Key Customizations in This Fork The fork was previously in a state where many features were layered on top but the driving model behavior had regressed in ways that couldn't be traced. On **2026-05-03** the working tree was reset back to a known-clean baseline so features can be re-introduced one at a time with proper testing. Most non-driving-math features have since been ported back.
- **UI changes** to the onroad driving interface ## Current State
- **Lane change behavior**: brief disengage when turn signal is active during lane changes
- **Lateral control disabled**: the car's own radar cruise control handles lateral; openpilot handles longitudinal only
- **Driver monitoring timeouts**: modified safety timeouts for the driver monitoring system
- **Custom driving models**: `duck-amigo.thneed`, `farmville.onnx`, `wd-40.thneed` in `selfdrive/clearpilot/models/`
- **ClearPilot service**: Node.js service at `selfdrive/clearpilot/` with behavior scripts for lane change and longitudinal control
- **Native dashcamd**: C++ process capturing raw camera frames via VisionIPC with OMX H.264 hardware encoding
- **Standstill power saving**: model inference throttled to 1fps and fan quieted when car is stopped
- **ClearPilot menu**: sidebar settings panel replacing stock home screen (Home, Dashcam, Debug panels)
- **Status window**: live system stats (temp, fan, storage, RAM, WiFi, VPN, GPS, telemetry status)
- **Debug button (LFA)**: steering wheel button repurposed for screen toggle and future UI actions
- **Telemetry system**: diff-based CSV logger via ZMQ IPC, toggleable from Debug panel
- **Bench mode**: `--bench` flag for onroad UI testing without car connected
- **GPS**: custom AT-command based GPS daemon (`system/clearpilot/gpsd.py`) replacing broken qcomgpsd diag interface
- **OpenVPN tunnel**: auto-connecting VPN to expose device on remote network for SSH access
See `GOALS.md` for feature roadmap. `build_only.sh` succeeds and `launch_openpilot.sh` boots the manager. The following customizations are active in this tree:
**Boot / build / device infra**
- launch chain (`launch_openpilot.sh`, `launch_chffrplus.sh`) with stale-process kill (including `_text` by comm), `on_start.sh` SSH/WiFi setup, OpenVPN auto-connect (`vpn-monitor.sh` + `vpn.ovpn`), `nice-monitor.sh`, `build_only.sh` + `build_preflight.sh`
- DongleId-keyed dev SSH identity (`system/clearpilot/dev/id_ed25519.{cpt,pub.cpt}` + `tools/{encrypt,decrypt}`)
- Custom startup logo (`bg.jpg`) + custom Qt spinner (`selfdrive/ui/qt/spinner{,.cc,.h}`) — `build.py` atomically swaps the prebuilt spinner binary after each successful build
- `BUILD_ONLY=1` env path in `build.py` spawns the failure TextWindow fully detached (own session, /dev/null stdio) so `build_only.sh` exits with a non-blocking error window
**Logging suppressed**
- `loggerd`, `encoderd`, `stream_encoderd` disabled in `process_config.py` — no `rlog`/`qlog` segments or `.hevc` files written to `/data/media/0/realdata/`
- `save_bootlog()` skipped in `manager_init()` — no boot logs
**GPS (Quectel modem replacement)**
- `system/clearpilot/gpsd.py` polls Quectel EC25 modem via `mmcli` AT commands at 2 Hz, publishes `gpsLocation`, sets system clock on first fix
- NOAA solar-position calc → `IsDaylight` memory param + auto `ScreenDisplayMode` 0↔1 switch (sunrise/sunset)
- **`locationd` patched to NOT subscribe to GPS** — `liveLocationKalman.gpsOK` stays false permanently. Self-driving sees GPS as not-present; downstream consumers (controlsd, paramsd, torqued, frogpilot_planner) handle that case naturally. GPS data is still published and used by speed_logicd / UI / dashcamd, just kept out of the kalman filter.
**UI (full C++ port)**
- New ready/splash screen rendered by Qt directly; shown when started but gear=park
- ClearPilot offroad menu (Home / Dashcam / Debug panels) replacing the stock home — Status window with live system stats (temp, fan, storage, RAM, WiFi, VPN, GPS, telemetry, dashcam)
- Onroad widgets: speed (from `gpsLocation` via `speed_logicd`), speed-limit, cruise-vs-limit warning sign
- Nightrider mode: camera suppressed, lane lines/path drawn as 2px outlines (`ScreenDisplayMode` 1 or 4)
- Display power: `ScreenDisplayMode` 3 → screen off; ignition off blanks immediately
- Qt RPC widget-tree dump server at `ipc:///tmp/clearpilot_ui_rpc`
- Crash handler in `main.cc` with stack-trace dump for SIGSEGV/SIGABRT
- `screenDisplayMode` is a member of `AnnotatedCameraWidget`, accessed from `OnroadWindow::updateState` as `nvg->screenDisplayMode`
- QtWebEngine dependency removed entirely
**Display modes (LFA debug button)**
- 5-state `ScreenDisplayMode` machine in `controlsd.clearpilot_state_control()`:
- Drive: 0→4, 1→2, 2→3, 3→4, 4→2 (button never goes back to auto)
- Not drive: anything except 3 → 3 (off), 3 → 0 (auto)
- Auto-wake from screen-off (mode 3 → 0) on park→drive edge
- "Clearpilot Debug Function Executed" alert removed (`clp_debug_notice` and its event registration deleted from `events.py`)
**Speed / cruise**
- `speed_logicd` standalone managed process (`selfdrive/clearpilot/speed_logicd.py` wrapping `speed_logic.SpeedState`) — subscribes to `gpsLocation` + `carState`, ticks at 2 Hz, writes `Clearpilot{Speed,SpeedLimit,CruiseWarning,...}Display` memory params
- Cruise-vs-limit warning thresholds: +10 over (limit ≥ 50), +7 over (26-49), +9 over (≤ 25); -5 under
- On warning transitions / speed-limit change: writes `ClearpilotPlayDing="1"` (consumed by soundd)
- Decoupled from `controlsd` so self-driving timing is unaffected
- **Note**: speed-limit widget shows 0 until `CarSpeedLimit` publisher in `hyundai/carstate.py` CAN-FD decode is ported (not done yet). Speed widget works.
**Sound**
- `soundd.py` mixes a single-shot ding alongside the alert stream (`MAX_VOLUME`, independent of alert volume map). Polls `ClearpilotPlayDing` at ~2 Hz, clears on read. `selfdrive/clearpilot/sounds/ding.wav` is the asset.
**Dashcam**
- `selfdrive/clearpilot/dashcamd` (native C++) — VisionIPC frames from camerad → OMX H.264 hardware encoder → 3-min MP4 segments + SRT GPS subtitle sidecars in `/data/media/0/videos/<trip>/`
- Trip lifecycle (WAITING → RECORDING → IDLE_TIMEOUT). 10-min idle close from drive→park; immediate close on ignition off; graceful close on `DashcamShutdown`
- Publishes `DashcamState` ("waiting"/"recording"/"stopped") and `DashcamFrames` (per-trip count) every 5 s
- `omx_encoder.cc` ports broken's version (adds `encode_frame_nv12()`, NEON-only libyuv paths)
- **`camerad` gating changed to `always_run`** so the dashcam can record the moment ignition+drive arrives without waiting for camera startup
- `system/loggerd/deleter.py` trip-aware cleanup: drops oldest entire trip dir first, falls back to oldest segment within active trip; also enforces 4 GB cap on `/data/log2`
## Where the Old Code Lives
| Location | What it is |
|---|---|
| `/data/openpilot/` | This repo. **Active.** |
| `/data/openpilot-broken-2026-05-03/` | Full snapshot (with `.git`) of the prior modified-but-broken tree. Reference for porting features. |
| `/data/clearpilot-baseline/` | The original baseline source we copied in. Kept for safety; do not modify. |
| `/data/openpilot-features-broken/` | Pre-existing snapshot from an earlier reset attempt — **unverified**, leave alone. |
| Tag | Commit | What |
|---|---|---|
| `pre-reset-2026-05-03` | `f7e602c` | Last commit of the pre-reset broken-but-feature-complete tree. |
| `working-baseline-2` | `b287fd0` | First commit after the reset where the bare baseline built and launched. |
Both tags are pushed to `origin/clearpilot`.
## Pending Feature Port Roadmap
Items still in `/data/openpilot-broken-2026-05-03/` that haven't been ported. Port in small, testable batches.
**Driving behavior (HDA2 specifics)** — these touch self-driving code and warrant extra scrutiny:
- Lateral disabled (car's radar cruise handles steering; openpilot longitudinal only)
- Brief disengage when turn signal is active during lane changes (note: `no_lat_lane_change` infrastructure already wired through `controlsd``paramsMemory` → UI/canfd, but the actual disengage trigger may need work)
- Driver-monitoring timeout adjustments
- Custom driving models — model files (`duck-amigo.thneed`, `farmville.onnx`, `wd-40.thneed`) live in `selfdrive/clearpilot/models/`; selection logic not ported
**Speed-limit publisher**
- `hyundai/carstate.py update_canfd()` writes `CarSpeedLimit` from CAN — until ported, the speed-limit widget shows 0 and cruise warnings never trigger
**Telemetry**
- `selfdrive/clearpilot/telemetry.py` (client) + `telemetryd.py` (collector) — diff-based CSV logger over ZMQ
- Toggleable via `TelemetryEnabled` memory param from Debug panel
- Auto-disabled if `/data` free < 5 GB; auto-disabled on every manager start
- Hyundai CAN-FD data logged from `update_canfd()` groups (car/cruise/speed_limit/buttons)
**Bench mode (UI testing without a car)**
- `--bench` flag → `BENCH_MODE=1` in `launch_openpilot.sh` (already wired) → enables `bench_onroad.py`, blocks real car processes
- `bench_cmd.py` for setting fake vehicle state via params; UI dump RPC wired (`bench_cmd dump`)
- Param keys (`Bench*`, `ClpUiState`) not yet registered
**Power/thermal**
- Standstill power saving: `modeld` and `dmonitoringmodeld` throttled to 1 fps when stopped
- Fan controller uses offroad clamps at standstill
- Park CPU savings + virtual battery shutdown fix
**Session logging**
- `/data/log2/current/` per-process stderr capture; aggregate `session.log` of major events
- Time-resolved log dir rename via GPS/NTP; 30-day rotation
- See `selfdrive/manager/process.py` and `manager.py` changes in the broken tree
- `LogDirInitialized` param not yet registered
## Working Rules ## Working Rules
@@ -37,6 +125,19 @@ This is self-driving software. All changes must be deliberate and well-understoo
- **Test everything thoroughly** — Brian must always be in the loop - **Test everything thoroughly** — Brian must always be in the loop
- **Do not refactor, clean up, or "improve" code beyond the specific request** - **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)`. Once session-logging is re-ported, manager will redirect each managed process's stderr to `/data/log2/current/{process}.log`. Prefix custom log lines with `CLP ` so they're easy to filter from upstream noise.
```python
import sys
print(f"CLP gpsd: ...", 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 ### 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: 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:
@@ -47,9 +148,10 @@ chown -R comma:comma /data/openpilot
### Git ### Git
- Remote: `git@git.internal.hanson.xyz:brianhansonxyz/comma.git` - Remote: `git@git.hanson.xyz:brianhansonxyz/clearpilot.git`
- Branch: `clearpilot` - Branch: `clearpilot` — this is the canonical/main branch and is what fresh device flashes provision from. **Do not abandon it for another branch.**
- Large model files are tracked in git (intentional — this is a backup) - Large model files are tracked in git (intentional — this is a backup)
- The `clearpilot` branch was force-pushed on 2026-05-03 as part of the reset; the prior history is reachable via the `pre-reset-2026-05-03` tag.
### Samba Share ### Samba Share
@@ -62,438 +164,103 @@ chown -R comma:comma /data/openpilot
### Testing Changes ### Testing Changes
Always use `build_only.sh` to compile, then start the manager separately. Never compile individual targets with scons directly — always use the full build script. Always use full paths with `su - comma` — the login shell drops into `/home/comma` (volatile tmpfs), not `/data/openpilot`. **Always start the manager after a successful build** — don't wait for the user to ask. Use `build_only.sh` to compile, then start the manager separately. Never compile individual targets with scons directly — always use the full build script. Always start the manager after a successful build — don't wait for the user to ask.
```bash ```bash
# 1. Fix ownership (we edit as root, openpilot runs as comma) # 1. Fix ownership
chown -R comma:comma /data/openpilot chown -R comma:comma /data/openpilot
# 2. Build (kills running manager, removes prebuilt, compiles, exits) # 2. Build (kills running manager, removes prebuilt, compiles, exits)
# Shows progress spinner on screen. On failure, shows error on screen # build_only.sh tees output to /tmp/build.log and propagates the build's
# and prints to stderr. Does NOT start the manager. # exit code via PIPESTATUS. On failure: error text window stays on screen
# fully detached; the script exits non-zero and stderr has the compile error.
su - comma -c "bash /data/openpilot/build_only.sh" su - comma -c "bash /data/openpilot/build_only.sh"
# 3. If build succeeded ($? == 0), start openpilot # 3. If build succeeded ($? == 0), start openpilot
su - comma -c "bash /data/openpilot/launch_openpilot.sh" su - comma -c "bash /data/openpilot/launch_openpilot.sh"
# 4. Review the aggregate session log for errors # 4. Inspect logs
cat /data/log2/current/session.log
# 5. Check per-process stdout/stderr logs if needed
ls /data/log2/current/ ls /data/log2/current/
cat /data/log2/current/gpsd.log cat /data/log2/current/session.log
tail /tmp/build.log # last build's output
``` ```
### Adding New Params ### 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: The params system uses a C++ whitelist. Adding a new param name without registering it will crash with `UnknownKeyName`. To add one:
1. Register the key in `common/params.cc` (alphabetically, with `PERSISTENT` or `CLEAR_ON_*` flag) 1. Register the key in `common/params.cc` (alphabetically, with `PERSISTENT` or `CLEAR_ON_*` flag)
2. Add the default value in `selfdrive/manager/manager.py` in `manager_init()` 2. For memory params, add a default in `manager_init()`'s memory-params loop (after the persistent default loop)
3. Remove `prebuilt`, `common/params.o`, and `common/libcommon.a` to force rebuild 3. Remove `prebuilt`, `common/params.o`, and `common/libcommon.a` to force rebuild
### Building Native (C++) Processes ### Memory Params (paramsMemory)
- SCons is the build system. Static libraries (`common`, `messaging`, `cereal`, `visionipc`) must be imported as SCons objects, not `-l` flags ClearPilot uses memory params (`/dev/shm/params/d/`) for transient state that should reset on boot. Conventions:
- The `--as-needed` linker flag can cause link order issues with static libs — disable it in your SConscript if needed
- OMX encoder object (`omx_encoder.o`) is compiled by the UI build — reference the pre-built `.o` file rather than recompiling (avoids "two environments" scons error)
- `prebuilt` is recreated after every successful build — always remove it before rebuilding
## Bench Mode (Onroad UI Testing) - **Registration**: register in `common/params.cc` as `PERSISTENT` (the registration flag does NOT control which path the param lives at — `/dev/shm` is tmpfs and clears on reboot regardless)
- **C++ access**: `Params{"/dev/shm/params"}` (the Params class appends `/d/` internally — `Params("/dev/shm/params/d")` would resolve to `/dev/shm/params/d/d/`)
- **Python access**: `Params("/dev/shm/params")`
- **UI toggles**: use `ToggleControl` with manual `toggleFlipped` lambda, not `ParamControl` (which only handles persistent params)
- **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.
Bench mode allows testing the onroad UI without a car connected. It runs a fake vehicle simulator (`bench_onroad.py`) as a managed process and disables real car processes (pandad, thermald, controlsd, etc.). Currently registered memory-param keys (in `params.cc`): `ScreenDisplayMode`, `CPTLkasButtonAction`, `CarIsMetric`, `Clearpilot{CruiseWarning,CruiseWarningSpeed,HasSpeed,IsMetric,PlayDing,ShowHealthMetrics,SpeedDisplay,SpeedLimitDisplay,SpeedUnit}`, `Dashcam{Frames,Shutdown,State}`, `IsDaylight`, `ModelFps`, `ModelStandby`, `ModelStandbyTs`, `ScreenRecorderDebug`, `ShutdownTouchReset`, `TelemetryEnabled`, `VpnEnabled`, `no_lat_lane_change`. Bench-mode and `LogDirInitialized` keys not registered yet.
### Usage ### Changing a Service's Publish Rate
**IMPORTANT**: Do NOT use `echo` to write bench params — `echo` appends a newline which causes param parsing to fail silently (e.g. gear stays in park). Always use the `bench_cmd.py` tool. 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 trips `commIssue` just as surely as too slow. If you change how often a process publishes, update the rate in `cereal/services.py` to match.
```bash
# 1. Build first
chown -R comma:comma /data/openpilot
su - comma -c "bash /data/openpilot/build_only.sh"
# 2. Start in bench mode
su - comma -c "bash /data/openpilot/launch_openpilot.sh --bench"
# 3. Wait for UI to be ready (polls RPC every 1s, up to 20s)
su - comma -c "PYTHONPATH=/data/openpilot python3 -m selfdrive.clearpilot.bench_cmd wait_ready"
# 4. Control vehicle state
su - comma -c "PYTHONPATH=/data/openpilot python3 -m selfdrive.clearpilot.bench_cmd gear D"
su - comma -c "PYTHONPATH=/data/openpilot python3 -m selfdrive.clearpilot.bench_cmd speed 20"
su - comma -c "PYTHONPATH=/data/openpilot python3 -m selfdrive.clearpilot.bench_cmd speedlimit 45"
su - comma -c "PYTHONPATH=/data/openpilot python3 -m selfdrive.clearpilot.bench_cmd cruise 55"
su - comma -c "PYTHONPATH=/data/openpilot python3 -m selfdrive.clearpilot.bench_cmd engaged 1"
# 5. Inspect UI widget tree (RPC call, instant response)
su - comma -c "PYTHONPATH=/data/openpilot python3 -m selfdrive.clearpilot.bench_cmd dump"
```
### Debugging Crashes
The UI has a SIGSEGV/SIGABRT crash handler (`selfdrive/ui/main.cc`) that prints a stack trace to stderr, captured in the per-process log:
```bash
# Check for crash traces (use /data/log2/current which is always the active session)
grep -A 30 "CRASH" /data/log2/current/ui.log
# Resolve addresses to source lines
addr2line -e /data/openpilot/selfdrive/ui/ui -f 0xADDRESS
# bench_cmd dump detects crash loops automatically:
# if UI process uptime < 5s, it reports "likely crash looping"
# Check per-process logs
ls /data/log2/current/
cat /data/log2/current/session.log
cat /data/log2/current/gpsd.log
```
### UI Introspection RPC
The UI process runs a ZMQ REP server at `ipc:///tmp/clearpilot_ui_rpc`. Send `"dump"` to get a recursive widget tree showing class name, visibility, geometry, and stacked layout current indices. This is the primary debugging tool for understanding what the UI is displaying.
- `bench_cmd dump` — queries the RPC and prints the widget tree
- `bench_cmd wait_ready` — polls the RPC every second until `ReadyWindow` is visible (up to 10s)
- `ui_dump.py` — standalone dump tool (same as `bench_cmd dump`)
### Architecture
- `launch_openpilot.sh --bench` sets `BENCH_MODE=1` env var
- `manager.py` reads `BENCH_MODE`, blocks real car processes, enables `bench_onroad` process
- `bench_onroad.py` publishes fake `pandaStates` (ignition=true), `carState`, `controlsState` — thermald reads the fake pandaStates to determine ignition and publishes `deviceState.started=true` on its own
- thermald and camerad run normally in bench mode (thermald manages CPU cores needed for camerad)
- Blocked processes in bench mode: pandad, controlsd, radard, plannerd, calibrationd, torqued, paramsd, locationd, sensord, ubloxd, pigeond, dmonitoringmodeld, dmonitoringd, modeld, soundd, loggerd, micd, dashcamd
### Key Files
| File | Role |
|------|------|
| `selfdrive/clearpilot/bench_onroad.py` | Fake vehicle state publisher |
| `selfdrive/clearpilot/bench_cmd.py` | Command tool for setting bench params and querying UI |
| `selfdrive/clearpilot/ui_dump.py` | Standalone UI widget tree dump |
| `selfdrive/manager/process_config.py` | Registers bench_onroad as managed process (enabled=BENCH_MODE) |
| `selfdrive/manager/manager.py` | Blocks conflicting processes in bench mode |
| `launch_openpilot.sh` | Accepts `--bench` flag, exports BENCH_MODE env var |
| `selfdrive/ui/qt/window.cc` | UI RPC server (`ipc:///tmp/clearpilot_ui_rpc`), widget tree dump |
### Resolved Issues
- **SIGSEGV in onroad view (fixed)**: `update_model()` in `ui.cc` dereferenced empty model position data when modeld wasn't running. Fixed by guarding against empty `plan_position.getX()`. The root cause was found using the crash handler + `addr2line`.
- **`showDriverView` overriding transitions (fixed)**: was forcing `slayout` to onroad/home every frame at 20Hz, overriding park/drive logic. Fixed to only act when not in started state.
- **Sidebar appearing during onroad transition (fixed)**: `MainWindow::closeSettings()` was re-enabling the sidebar. Fixed by not calling `closeSettings` during `offroadTransition`.
## Session Logging
Per-process stderr and an aggregate event log are captured in `/data/log2/current/`.
### Log Directory
- `/data/log2/current/` is always the active session directory
- `init_log_dir()` is called once from `manager_init()` — creates a fresh `/data/log2/current/` real directory
- If a previous `current/` real directory exists (unresolved session), it's renamed using its mtime timestamp
- If a previous `current` symlink exists, it's removed
- Once system time is valid (GPS/NTP), the real directory is renamed to `/data/log2/YYYY-MM-DD-HH-MM-SS/` and `current` becomes a symlink to it
- `LogDirInitialized` param: `"0"` until time resolves, then `"1"`
- Open file handles survive the rename (same inode, same filesystem)
- Session directories older than 30 days are deleted on manager startup
### Per-Process Logs
- Every `PythonProcess` and `NativeProcess` has stderr redirected to `{name}.log` in the session directory
- `DaemonProcess` (athenad) redirects both stdout+stderr (existing behavior)
- Stderr is redirected via `os.dup2` inside the forked child process
### Aggregate Session Log (`session.log`)
A single `session.log` in each session directory records major events:
- Manager start/stop/crash
- Process starts, deaths (with exit codes), watchdog restarts
- Onroad/offroad transitions
### Key Files
| File | Role |
|------|------|
| `selfdrive/manager/process.py` | Log directory creation, stderr redirection, session_log logger |
| `selfdrive/manager/manager.py` | Log rotation cleanup, session event logging |
| `build_only.sh` | Build-only script (no manager start) |
## Dashcam (dashcamd)
### Architecture
`dashcamd` is a native C++ process that captures raw camera frames directly from `camerad` via VisionIPC and encodes to MP4 using the Qualcomm OMX H.264 hardware encoder. This replaces the earlier FrogPilot screen recorder approach (`QWidget::grab()` -> OMX).
- **Codec**: H.264 AVC (hardware accelerated via `OMX.qcom.video.encoder.avc`)
- **Resolution**: 1928x1208 (full camera resolution, no downscaling)
- **Bitrate**: 2500 kbps
- **Container**: MP4 (remuxed via libavformat) + SRT subtitle sidecar
- **Segment length**: 3 minutes per file
- **Save path**: `/data/media/0/videos/YYYYMMDD-HHMMSS/YYYYMMDD-HHMMSS.mp4` (trip directories)
- **GPS subtitles**: companion `.srt` file per segment with 1Hz entries (speed MPH, lat/lon, UTC timestamp)
- **Trip lifecycle**: starts recording on launch with 10-min idle timer; car in drive cancels timer; park/off restarts timer; ignition cycle = new trip
- **Graceful shutdown**: thermald sets `DashcamShutdown` param, dashcamd closes segment and acks within 15s
- **Storage**: ~56 MB per 3-minute segment at 2500 kbps
- **Storage device**: WDC SDINDDH4-128G UFS 2.1 — automotive grade, ~384 TB write endurance, no concern for continuous writes
### Key Differences from Old Screen Recorder
| | Old (screen recorder) | New (dashcamd) |
|---|---|---|
| Source | `QWidget::grab()` screen capture | Raw NV12 from VisionIPC |
| Resolution | 1440x720 | 1928x1208 |
| Works with screen off | No (needs visible widget) | Yes (independent of UI) |
| Process type | Part of UI process | Standalone native process |
| Encoder input | RGBA -> NV12 conversion | NV12 direct (added `encode_frame_nv12`) |
### Key Files
| File | Role |
|------|------|
| `selfdrive/clearpilot/dashcamd.cc` | Main dashcam process — VisionIPC -> OMX encoder |
| `selfdrive/clearpilot/SConscript` | Build config for dashcamd |
| `selfdrive/frogpilot/screenrecorder/omx_encoder.cc` | OMX encoder (added `encode_frame_nv12` method) |
| `selfdrive/frogpilot/screenrecorder/omx_encoder.h` | Encoder header |
| `selfdrive/manager/process_config.py` | dashcamd registered as NativeProcess, camerad always_run, encoderd disabled |
| `system/loggerd/deleter.py` | Trip-aware storage rotation (oldest trip first, then segments within active trip) |
### 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
## Standstill Power Saving
When `carState.standstill` is true:
- **modeld**: skips GPU inference on 19/20 frames (1fps vs 20fps), reports 0 frame drops to avoid triggering `modeldLagging` in controlsd
- **dmonitoringmodeld**: same 1fps throttle, added `carState` subscription
- **Fan controller**: uses offroad clamps (0-30%) instead of onroad (30-100%) at standstill; thermal protection still active via feedforward if temp > 60°C
### Key Files
| File | Role |
|------|------|
| `selfdrive/modeld/modeld.py` | Standstill frame skip logic |
| `selfdrive/modeld/dmonitoringmodeld.py` | Standstill frame skip logic |
| `selfdrive/thermald/fan_controller.py` | Standstill-aware fan clamps |
| `selfdrive/thermald/thermald.py` | Passes standstill to fan controller via carState |
## Display Modes (LFA/LKAS Debug Button)
The Hyundai Tucson's LFA steering wheel button cycles through 5 display modes via `ScreenDisplayMode` param (`/dev/shm/params`, CLEAR_ON_MANAGER_START, default 0).
### Display States
| State | Name | Display | Camera | Path Drawing |
|-------|------|---------|--------|-------------|
| 0 | auto-normal | on | yes | filled (normal) |
| 1 | auto-nightrider | on | black | 2px outline only |
| 2 | normal (manual) | on | yes | filled (normal) |
| 3 | screen off | GPIO off | n/a | n/a |
| 4 | nightrider (manual) | on | black | 2px outline only |
### Auto Day/Night Switching
- `gpsd.py` computes `is_daylight(lat, lon, utc_dt)` using NOAA solar position algorithm
- First calculation immediately on GPS fix + valid clock, then every 30 seconds
- State 0 + sunset → auto-switch to 1; State 1 + sunrise → auto-switch to 0
- States 2/3/4 are never touched by auto logic
- `IsDaylight` param written to `/dev/shm/params` for reference
### Button Behavior
**Onroad (car in drive gear):** 0→4, 1→2, 2→3, 3→4, 4→2 (never back to auto via button)
**Not in drive (parked/off):** any except 3 → 3 (screen off), state 3 → 0 (auto-normal)
### Nightrider Mode
- Camera feed suppressed (OpenGL clears to black instead of rendering camera texture)
- All HUD elements (speed, alerts, telemetry indicator) still visible
- Path/lane polygons drawn as 2px outlines only (no gradient fill)
- Lane lines, road edges, blind spot paths, adjacent paths all use outline rendering
### Signal Chain
```
Steering wheel LFA button press
-> CAN-FD: cruise_btns_msg_canfd["LFA_BTN"]
-> Edge detection → ButtonEvent(type=FrogPilotButtonType.lkas)
-> controlsd.clearpilot_state_control()
-> Reads ScreenDisplayMode, applies transition table based on driving_gear
-> Writes new ScreenDisplayMode to /dev/shm/params
-> UI reads ScreenDisplayMode in paintEvent() (for camera/nightrider)
and drawHud() (for display power on/off)
```
### Key Files
| File | Role |
|------|------|
| `selfdrive/controls/controlsd.py` | Button state machine, writes ScreenDisplayMode |
| `selfdrive/ui/qt/onroad.cc` | Nightrider rendering, display power control |
| `selfdrive/ui/qt/home.cc` | Display power for park/splash state |
| `system/clearpilot/gpsd.py` | Sunset/sunrise calc, auto day/night transitions |
| `selfdrive/clearpilot/bench_cmd.py` | `debugbutton` command simulates button press |
## Screen Timeout / Display Power
Display power is managed by `Device::updateWakefulness()` in `selfdrive/ui/ui.cc`.
- **Ignition off (offroad)**: screen blanks after `ScreenTimeout` seconds (default 120) of no touch. Tapping wakes it.
- **Ignition on (onroad)**: screen stays on unconditionally — ignition=true short-circuits the timeout check.
- **Debug button (LFA)**: cycles through display modes including screen off (state 3). Only state 3 calls `Hardware::set_display_power(false)`.
## Offroad UI (ClearPilot Menu)
The offroad home screen (`selfdrive/ui/qt/home.cc`) is a sidebar settings panel replacing the stock home screen. The original system settings are no longer accessible — the ClearPilot menu is the only settings interface.
### Panels
- **General**: Status window, Reset Calibration, Shutdown Timer, Reboot/Soft Reboot/Power Off
- **Network**: WiFi management, tethering, roaming, hidden networks (APN settings removed)
- **Dashcam**: placeholder for future dashcam footage viewer
- **Debug**: Telemetry logging toggle
### Navigation
- Tapping the splash screen (ReadyWindow) opens the ClearPilot menu
- "← Back" button returns to splash or onroad view
- Sidebar with stock metrics (TEMP, VEHICLE, CONNECT) is hidden
## Device: comma 3x ## Device: comma 3x
### Hardware - Qualcomm Snapdragon SoC (aarch64), serial `comma-3889765b`
- Qualcomm Snapdragon SoC (aarch64)
- Serial: comma-3889765b
- Storage: WDC SDINDDH4-128G, 128 GB UFS 2.1 - Storage: WDC SDINDDH4-128G, 128 GB UFS 2.1
- Connects to the car via comma panda (CAN bus interface) - Ubuntu 20.04.6 LTS on AGNOS 9.7
- Kernel 4.9.103+ (custom comma.ai PREEMPT build, vendor-patched Qualcomm)
- Python 3.11.4 via pyenv at `/usr/local/pyenv/versions/3.11.4/` (system python 3.8 — do not use)
- Display: Weston (Wayland) on tty1
- Hardware encoding: OMX (`OMX.qcom.video.encoder.avc` / `.hevc`); V4L2 VIDC exists but is not usable from ffmpeg subprocess
### Operating System ### Filesystem mount quirks
- **Ubuntu 20.04.6 LTS (Focal Fossa)** on aarch64
- **Kernel**: 4.9.103+ (custom comma.ai PREEMPT build, Feb 2024) — very old, vendor-patched Qualcomm kernel
- **Python**: 3.11.4 via pyenv at `/usr/local/pyenv/versions/3.11.4/` (system python is 3.8, do not use)
- **AGNOS version**: 9.7 (comma's custom OS layer on top of Ubuntu)
- **Display server**: Weston (Wayland compositor) on tty1
- **SELinux**: mounted (enforcement status varies)
### Users
- `comma` (uid=1000) — the user openpilot runs as; member of root, sudo, disk, gpu, gpio groups
- `root` — what we SSH in as; files must be chowned back to comma before running openpilot
### Filesystem / Mount Quirks
| Mount | Device | Type | Notes | | Mount | Device | Type | Notes |
|-------------|-------------|---------|-------| |---|---|---|---|
| `/` | /dev/sda7 | ext4 | Root filesystem, read-write | | `/` | /dev/sda7 | ext4 | rw |
| `/data` | /dev/sda12 | ext4 | **Persistent**. Openpilot lives here. Survives reboots. | | `/data` | /dev/sda12 | ext4 | **persistent** — openpilot lives here |
| `/home` | overlay | overlayfs | **VOLATILE** upperdir on tmpfs, changes lost on reboot | | `/home` | overlay | overlayfs | **volatile** (upper on tmpfs) — changes lost on reboot |
| `/tmp` | tmpfs | tmpfs | Volatile, 150 MB | | `/tmp` | tmpfs | tmpfs | volatile |
| `/var` | tmpfs | tmpfs | Volatile, 128 MB (fstab) / 1.5 GB (actual) | | `/persist` | /dev/sda2 | ext4 | persistent config/certs, noexec |
| `/systemrw` | /dev/sda10 | ext4 | Writable system overlay, noexec | | `/dsp` | /dev/sde26 | ext4 | **read-only** Qualcomm DSP firmware |
| `/persist` | /dev/sda2 | ext4 | Persistent config/certs, noexec | | `/firmware` | /dev/sde4 | vfat | **read-only** firmware blobs |
| `/cache` | /dev/sda11 | ext4 | Android-style cache partition |
| `/dsp` | /dev/sde26 | ext4 | **Read-only** Qualcomm DSP firmware |
| `/firmware` | /dev/sde4 | vfat | **Read-only** firmware blobs |
### Hardware Encoding ### GPS
- **OMX**: `OMX.qcom.video.encoder.avc` (H.264) and `OMX.qcom.video.encoder.hevc` — used by dashcamd and screen recorder The device has **no u-blox chip** (`/dev/ttyHS0` does not exist). GPS is the **Quectel EC25 LTE modem**'s built-in GPS, accessed via AT commands through `mmcli`. The original `qcomgpsd` is broken on this device because the diag interface hangs after setup. `system/clearpilot/gpsd.py` is the replacement; locationd is patched not to consume `gpsLocation` so the kalman filter isn't tainted.
- **V4L2**: Qualcomm VIDC at `/dev/v4l/by-path/platform-aa00000.qcom_vidc-video-index1` — used by encoderd (now disabled). Not accessible from ffmpeg due to permission/driver issues
- **ffmpeg**: v4.2.2, has `h264_v4l2m2m` and `h264_omx` listed but neither works from ffmpeg subprocess (OMX port issues, V4L2 device not found). Use OMX directly via the C++ encoder
### Fan Control ### Sidebar / Process Notes
Software-controlled via `thermald` -> `fan_controller.py` -> panda USB -> PWM. Target temp 70°C, PI+feedforward controller. See Standstill Power Saving section for standstill-aware clamps. - `camerad` runs `always_run` (was `driverview`) — this is needed for dashcamd. Costs steady-state camera power even when ignition is off.
- `loggerd`, `encoderd`, `stream_encoderd` are commented out in `process_config.py` — no segment or video logging
- `qcomgpsd` is commented out; `system.clearpilot.gpsd` is registered in its place (gated by `qcomgps` callback = `not ublox_available()`, which is always true on this device)
- `speed_logicd` is `only_onroad`; `dashcamd` is `always_run` (manages own trip lifecycle)
- `uploader` already commented out in baseline
## Boot Sequence ## Boot Sequence
``` ```
Power On Power On
-> systemd: comma.service (runs as comma user) systemd: comma.service (runs as comma user)
-> /usr/comma/comma.sh (waits for Weston, handles factory reset) /usr/comma/comma.sh (waits for Weston, handles factory reset)
-> /data/continue.sh (exec bridge to openpilot) /data/continue.sh
-> /data/openpilot/launch_openpilot.sh /data/openpilot/launch_openpilot.sh
-> Kills other instances of itself and manager.py → kill stale instances (launch_openpilot, launch_chffrplus, manager.py, ./ui, _text by comm)
-> Runs on_start.sh (logo, reverse SSH) → bash system/clearpilot/on_start.sh (SSH, WiFi, run provision.sh)
-> exec launch_chffrplus.sh → background system/clearpilot/vpn-monitor.sh
-> Sources launch_env.sh (thread counts, AGNOS_VERSION) → background system/clearpilot/nice-monitor.sh
-> Runs agnos_init (marks boot slot, GPU perms, checks OS update) → exec ./launch_chffrplus.sh
-> Sets PYTHONPATH, symlinks /data/pythonpath → source launch_env.sh
-> Runs build.py if no `prebuilt` marker → run agnos_init
-> Launches selfdrive/manager/manager.py → set PYTHONPATH
-> manager_init() sets default params → if no `prebuilt`: run build.py (spinner + scons)
-> ensure_running() loop starts all managed processes → exec selfdrive/manager/manager.py
→ manager_init() sets default params (persistent + memory)
→ ensure_running() loop starts managed processes
``` ```
## Openpilot Architecture
### Process Manager
`selfdrive/manager/manager.py` orchestrates all processes defined in `selfdrive/manager/process_config.py`.
### Always-Running Processes (offroad + onroad)
- `thermald` — thermal management and fan control
- `pandad` — panda CAN bus interface
- `ui` — Qt-based onroad/offroad UI
- `deleter` — storage cleanup (9 GB threshold)
- `statsd`, `timed`, `logmessaged`, `tombstoned` — telemetry/logging
- `manage_athenad` — comma cloud connectivity
- `fleet_manager`, `frogpilot_process` — FrogPilot additions
### Onroad-Only Processes (when driving)
- `controlsd` — main vehicle control loop
- `plannerd` — path planning
- `radard` — radar processing
- `modeld` — driving model inference (throttled to 1fps at standstill)
- `dmonitoringmodeld` — driver monitoring model (throttled to 1fps at standstill)
- `locationd`, `calibrationd`, `paramsd`, `torqued` — localization and calibration
- `sensord` — IMU/sensor data
- `soundd` — alert sounds
- `camerad` — camera capture
- `loggerd` — CAN/sensor log recording (video encoding disabled)
### ClearPilot Processes
- `dashcamd` — raw camera dashcam recording (runs onroad or with DashcamDebug flag)
### GPS
- Device has **no u-blox chip** (`/dev/ttyHS0` does not exist) — `ubloxd`/`pigeond` never start
- GPS hardware is a **Quectel EC25 LTE modem** (USB, `lsusb: 2c7c:0125`) with built-in GPS
- GPS is accessed via AT commands through `mmcli`: `mmcli -m any --command='AT+QGPSLOC=2'`
- **`qcomgpsd`** (original openpilot process) uses the modem's diag interface which is broken on this device — the diag recv loop blocks forever after setup. Commented out.
- **`system/clearpilot/gpsd.py`** is the replacement — polls GPS via AT commands at 1Hz, publishes `gpsLocation` cereal messages
- GPS data flows: `gpsd``gpsLocation``locationd``liveLocationKalman``timed` (sets system clock)
- `locationd` checks `UbloxAvailable` param (false on this device) to subscribe to `gpsLocation` instead of `gpsLocationExternal`
- `mmcli` returns `response: '...'` wrapper — `at_cmd()` strips it before parsing (fixed)
- GPS antenna power must be enabled via GPIO: `gpio_set(GPIO.GNSS_PWR_EN, True)`
- System `/usr/sbin/gpsd` daemon may respawn and interfere — should be disabled or killed
### Telemetry
- **Client**: `selfdrive/clearpilot/telemetry.py``tlog(group, data)` sends JSON over ZMQ PUSH
- **Collector**: `selfdrive/clearpilot/telemetryd.py` — diffs against previous state, writes changed values to CSV
- **Toggle**: `TelemetryEnabled` param, controlled from Debug panel in ClearPilot menu
- **Auto-disable**: disabled on every manager start; disabled if `/data` free < 5GB
- **Hyundai CAN-FD data**: logged from `selfdrive/car/hyundai/carstate.py` `update_canfd()` — groups: `car`, `cruise`, `speed_limit`, `buttons`
- **CSV location**: `/data/log2/current/telemetry.csv` (or session directory)
### Key Dependencies
- **Python 3.11** (via pyenv) with: numpy, casadi, onnx/onnxruntime, pycapnp, pyzmq, sentry-sdk, sympy, Cython
- **capnp (Cap'n Proto)** — IPC message serialization between all processes
- **ZeroMQ** — IPC transport layer
- **Qt 5** — UI framework (with WebEngine available but not used for rotation reasons)
- **OpenMAX (OMX)** — hardware video encoding
- **libavformat** — MP4 container muxing
- **libyuv** — color space conversion
- **SCons** — build system for native C++ components
-49
View File
@@ -1,49 +0,0 @@
# Dashcam Project
## Status
### Completed (2026-04-11)
- Disabled comma training data video encoding (`encoderd`) — CAN/sensor logs still recorded
- Re-enabled FrogPilot OMX screen recorder (H.264 MP4, 1440x720, 2Mbps, hardware encoded)
- Auto-start/stop recording tied to car ignition (`scene.started`)
- `ScreenRecorderDebug` param for bench testing without car connected
- Hidden all recorder UI elements — invisible to driver
- Videos saved to `/data/media/0/videos/YYYYMMDD-HHMMSS.mp4`, 3-minute segments
- Deleter updated: 9 GB free space threshold, rotates oldest videos first
- Cleaned up offroad UI: replaced stock home screen with grid launcher (Settings + Dashcam buttons)
### Next: Dashcam Footage Viewer
Build a native Qt widget accessible from the offroad home screen "Dashcam" button.
**Requirements:**
- List MP4 files from `/data/media/0/videos/` sorted newest first
- Tap a file to play it back using Qt Multimedia (`QMediaPlayer` + `QVideoWidget`)
- Only accessible when offroad (car in park or off)
- Back button to return to offroad home
- No rotation hacks needed — native Qt widget in existing UI tree handles rotation correctly
**Architecture:**
- New widget class (e.g. `DashcamViewer`) added to `selfdrive/ui/qt/`
- Wire into `HomeWindow`'s `QStackedLayout` alongside `home`, `onroad`, `ready`, `driver_view`
- Dashcam button in `OffroadHome` switches `slayout` to the viewer
- Viewer has a file list view and a playback view (sub-stacked layout)
- Back button returns to `OffroadHome`
- Build: add to `qt_src` list in `selfdrive/ui/SConscript`, link `Qt5Multimedia`
**Previous webview attempts (abandoned):**
- Brian tried QWebEngineView for browser-based playback but the WebEngine subprocess renders independently of Qt's widget tree
- Screen rotation (`view.rotate(90)`, Wayland `wl_surface_set_buffer_transform`) did not work for WebEngine content
- Native Qt widget approach avoids this problem entirely
## Key Files
| File | Role |
|------|------|
| `selfdrive/frogpilot/screenrecorder/screenrecorder.cc` | Recording logic, auto-start/stop |
| `selfdrive/frogpilot/screenrecorder/omx_encoder.cc` | OMX H.264 hardware encoder |
| `selfdrive/ui/qt/onroad.cc` | Timer driving frame capture |
| `selfdrive/ui/qt/home.cc` | Offroad home with Dashcam button |
| `system/loggerd/deleter.py` | Storage rotation |
| `/data/media/0/videos/` | Video output directory |
-46
View File
@@ -1,46 +0,0 @@
# ClearPilot — Feature Goals
Each goal below will be discussed in detail before implementation. Background and specifics to be provided by Brian when we get to each item.
## Dashcam
- [ ] **Dashcam viewer** — on-screen Qt widget for browsing and playing back recorded footage from the offroad home screen (Dashcam button already placed). See `DASHCAM_PROJECT.md` for architecture notes.
- [ ] **Dashcam uploader** — upload footage to a remote server or cloud storage
- [ ] **GPS + speed overlay on dashcam footage** — embed GPS coordinates and vehicle speed into the video stream or as metadata
- [ ] **Dashcam footage archive** — option to mark a trip's footage as archived (kept in storage but hidden from the main UI)
- [ ] **Suspend dashcam / route recording** — on-screen setting to pause recording for a trip and delete footage of the current trip so far (privacy mode)
## Driving Alerts & Safety
- [ ] **Chirp on speed change and over speed limit** — audible alert when speed limit changes or vehicle exceeds the limit
- [ ] **Warn on traffic standstill ahead** — alert when approaching cars ahead too quickly (closing distance warning)
- [ ] **On-screen current speed limit** — display the speed limit as reported by the car's CAN data
- [ ] **Fix on-screen speed value for MPH** — correct the speed display to use the proper value for MPH conversion
## Cruise Control & Driving State
- [ ] **Auto-set speed via CAN bus** — simulate pressing speed up/down buttons on CAN bus to automatically set cruise to the correct speed via UI interaction (stretch goal)
- [ ] **Fix engaged-while-braking state sync** — if openpilot is engaged while brakes are pressed, the system thinks it's active but the car isn't actually in cruise control, causing a sync error that should be recoverable
- [ ] **Fix resume-cruise lateral mode bug** — pressing resume cruise control (rather than enable) causes the car to enter a state where it thinks it's in always-on lateral mode and disables driver monitoring
- [ ] **Lateral assist in turn lanes** — keep lateral assist active when using turn signal in a turn lane (at low/turning speeds) as opposed to cruising speeds, where lateral currently disengages
## Driver Monitoring
- [ ] **Disable driver monitoring on demand** — add a technique to disable DM for 2 minutes via a button or gesture
- [ ] **Hands-on-wheel mode** — add a setting requiring the driver to keep hands on the wheel (configurable via settings)
## UI Modes & Display
- [ ] **Curves-only UI mode** — show only on-screen curve/path depictions without the camera feed (reduces visual clutter, saves processing)
- [ ] **Night mode auto-display-off** — turn off display if device starts at night (determine sunset for current location/datetime or use ambient light levels from car sensors)
- [ ] **Update boot/offroad splash logos** — replace the red pac-man ghost with a blue pac-man ghost for boot and offroad full-screen splash
- [ ] **Dashcam-only reboot mode** — on-screen option to reboot into a mode that provides no driver assist (reverts to stock OEM lane assist) but keeps dashcam running. For lending the car to friends who shouldn't use comma's driving features.
## Connectivity & Data
- [ ] **Offroad weather report screen** — weather display accessible from the offroad home grid (depends on internet connection)
- [ ] **Fix GPS tracking** — GPS tracking feature currently broken, processes commented out in process_config
## Vehicle-Specific (Hyundai Tucson HDA2)
- [ ] **Warn if panoramic roof is open** — if possible to intercept window/roof state from CAN bus, warn when the top roof is left open when the car is turned off
+10 -2
View File
@@ -10,8 +10,11 @@ BASEDIR="/data/openpilot"
# Fix ownership — we edit as root, openpilot builds/runs as comma # Fix ownership — we edit as root, openpilot builds/runs as comma
sudo chown -R comma:comma "$BASEDIR" sudo chown -R comma:comma "$BASEDIR"
# Kill stale error displays and any running manager/launch/managed processes # Kill stale error displays and any running manager/launch/managed processes.
# `text` is a shell wrapper that execs `./_text` — after exec the process is named
# `_text` (no path), so we kill by exact comm in addition to the path pattern.
pkill -9 -f "selfdrive/ui/text" 2>/dev/null pkill -9 -f "selfdrive/ui/text" 2>/dev/null
pkill -9 -x _text 2>/dev/null
pkill -9 -f 'launch_openpilot.sh' 2>/dev/null pkill -9 -f 'launch_openpilot.sh' 2>/dev/null
pkill -9 -f 'launch_chffrplus.sh' 2>/dev/null pkill -9 -f 'launch_chffrplus.sh' 2>/dev/null
pkill -9 -f 'python.*manager.py' 2>/dev/null pkill -9 -f 'python.*manager.py' 2>/dev/null
@@ -29,7 +32,12 @@ export PYTHONPATH="$BASEDIR"
sudo chgrp gpu /dev/adsprpc-smd /dev/ion /dev/kgsl-3d0 2>/dev/null sudo chgrp gpu /dev/adsprpc-smd /dev/ion /dev/kgsl-3d0 2>/dev/null
sudo chmod 660 /dev/adsprpc-smd /dev/ion /dev/kgsl-3d0 2>/dev/null sudo chmod 660 /dev/adsprpc-smd /dev/ion /dev/kgsl-3d0 2>/dev/null
# Preflight: create dirs git can't track
source "$BASEDIR/build_preflight.sh"
cd "$BASEDIR/selfdrive/manager" cd "$BASEDIR/selfdrive/manager"
rm -f "$BASEDIR/prebuilt" rm -f "$BASEDIR/prebuilt"
BUILD_ONLY=1 exec ./build.py # Tee output to /tmp/build.log for inspection after the script exits
BUILD_ONLY=1 ./build.py 2>&1 | tee /tmp/build.log
exit "${PIPESTATUS[0]}"
+11
View File
@@ -0,0 +1,11 @@
#!/usr/bin/bash
# CLEARPILOT: build preflight — create directories and fix state that
# git cannot track but the build requires. Called by build_only.sh and
# launch_chffrplus.sh before scons runs.
BASEDIR="${BASEDIR:-/data/openpilot}"
# SConscript files write generated headers into obj/ directories at
# parse time — these must exist before scons starts.
mkdir -p "$BASEDIR/body/board/obj"
mkdir -p "$BASEDIR/panda/board/obj"
+25 -14
View File
@@ -109,8 +109,6 @@ std::unordered_map<std::string, uint32_t> keys = {
{"ControlsReady", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION}, {"ControlsReady", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION},
{"CurrentBootlog", PERSISTENT}, {"CurrentBootlog", PERSISTENT},
{"CurrentRoute", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION}, {"CurrentRoute", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION},
{"DashcamDebug", PERSISTENT},
{"DashcamShutdown", CLEAR_ON_MANAGER_START},
{"DisableLogging", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION}, {"DisableLogging", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION},
{"DisablePowerDown", PERSISTENT}, {"DisablePowerDown", PERSISTENT},
{"DisableUpdates", PERSISTENT}, {"DisableUpdates", PERSISTENT},
@@ -160,7 +158,6 @@ std::unordered_map<std::string, uint32_t> keys = {
{"LastUpdateTime", PERSISTENT}, {"LastUpdateTime", PERSISTENT},
{"LiveParameters", PERSISTENT}, {"LiveParameters", PERSISTENT},
{"LiveTorqueParameters", PERSISTENT | DONT_LOG}, {"LiveTorqueParameters", PERSISTENT | DONT_LOG},
{"LogDirInitialized", CLEAR_ON_MANAGER_START},
{"LongitudinalPersonality", PERSISTENT}, {"LongitudinalPersonality", PERSISTENT},
{"NavDestination", CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION}, {"NavDestination", CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION},
{"NavDestinationWaypoints", CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION}, {"NavDestinationWaypoints", CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION},
@@ -195,11 +192,9 @@ std::unordered_map<std::string, uint32_t> keys = {
{"SnoozeUpdate", CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION}, {"SnoozeUpdate", CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION},
{"SshEnabled", PERSISTENT}, {"SshEnabled", PERSISTENT},
{"TermsVersion", PERSISTENT}, {"TermsVersion", PERSISTENT},
{"TelemetryEnabled", PERSISTENT},
{"Timezone", PERSISTENT}, {"Timezone", PERSISTENT},
{"TrainingVersion", PERSISTENT}, {"TrainingVersion", PERSISTENT},
{"UbloxAvailable", PERSISTENT}, {"UbloxAvailable", PERSISTENT},
{"VpnEnabled", CLEAR_ON_MANAGER_START},
{"UpdateAvailable", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION}, {"UpdateAvailable", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION},
{"UpdateFailedCount", CLEAR_ON_MANAGER_START}, {"UpdateFailedCount", CLEAR_ON_MANAGER_START},
{"UpdaterAvailableBranches", PERSISTENT}, {"UpdaterAvailableBranches", PERSISTENT},
@@ -215,12 +210,6 @@ std::unordered_map<std::string, uint32_t> keys = {
// FrogPilot parameters // FrogPilot parameters
{"AccelerationPath", PERSISTENT}, {"AccelerationPath", PERSISTENT},
{"BenchCruiseSpeed", CLEAR_ON_MANAGER_START},
{"ClpUiState", CLEAR_ON_MANAGER_START},
{"BenchEngaged", CLEAR_ON_MANAGER_START},
{"BenchGear", CLEAR_ON_MANAGER_START},
{"BenchSpeed", CLEAR_ON_MANAGER_START},
{"BenchSpeedLimit", CLEAR_ON_MANAGER_START},
{"AccelerationProfile", PERSISTENT}, {"AccelerationProfile", PERSISTENT},
{"AdjacentPath", PERSISTENT}, {"AdjacentPath", PERSISTENT},
{"AdjacentPathMetrics", PERSISTENT}, {"AdjacentPathMetrics", PERSISTENT},
@@ -253,8 +242,31 @@ std::unordered_map<std::string, uint32_t> keys = {
// {"SpeedLimitVTSC", PERSISTENT}, // {"SpeedLimitVTSC", PERSISTENT},
{"CPTLkasButtonAction", PERSISTENT}, {"CPTLkasButtonAction", PERSISTENT},
{"IsDaylight", CLEAR_ON_MANAGER_START}, {"ScreenDisplayMode", PERSISTENT},
{"ScreenDisplayMode", CLEAR_ON_MANAGER_START},
// CLEARPILOT memory params (live at /dev/shm/params; tmpfs is volatile so
// these reset on reboot regardless of registration flag).
{"CarIsMetric", PERSISTENT},
{"ClearpilotCruiseWarning", PERSISTENT},
{"ClearpilotCruiseWarningSpeed", PERSISTENT},
{"ClearpilotHasSpeed", PERSISTENT},
{"ClearpilotIsMetric", PERSISTENT},
{"ClearpilotPlayDing", PERSISTENT},
{"ClearpilotShowHealthMetrics", PERSISTENT},
{"ClearpilotSpeedDisplay", PERSISTENT},
{"ClearpilotSpeedLimitDisplay", PERSISTENT},
{"ClearpilotSpeedUnit", PERSISTENT},
{"DashcamFrames", PERSISTENT},
{"DashcamShutdown", PERSISTENT},
{"DashcamState", PERSISTENT},
{"IsDaylight", PERSISTENT},
{"ModelFps", PERSISTENT},
{"ModelStandby", PERSISTENT},
{"ModelStandbyTs", PERSISTENT},
{"ScreenRecorderDebug", PERSISTENT},
{"ShutdownTouchReset", PERSISTENT},
{"TelemetryEnabled", PERSISTENT},
{"VpnEnabled", PERSISTENT},
{"RadarDist", PERSISTENT}, {"RadarDist", PERSISTENT},
{"ModelDist", PERSISTENT}, {"ModelDist", PERSISTENT},
@@ -428,7 +440,6 @@ std::unordered_map<std::string, uint32_t> keys = {
{"ScreenBrightnessOnroad", PERSISTENT}, {"ScreenBrightnessOnroad", PERSISTENT},
{"ScreenManagement", PERSISTENT}, {"ScreenManagement", PERSISTENT},
{"ScreenRecorder", PERSISTENT}, {"ScreenRecorder", PERSISTENT},
{"ScreenRecorderDebug", PERSISTENT},
{"ScreenTimeout", PERSISTENT}, {"ScreenTimeout", PERSISTENT},
{"ScreenTimeoutOnroad", PERSISTENT}, {"ScreenTimeoutOnroad", PERSISTENT},
{"SearchInput", PERSISTENT}, {"SearchInput", PERSISTENT},
+7 -1
View File
@@ -79,12 +79,18 @@ function launch {
agnos_init agnos_init
fi fi
# CLEARPILOT: kill stale error display from previous build/run # CLEARPILOT: kill stale error display from previous build/run.
# `text` is a wrapper that execs ./_text — running process is named _text
# with no path, so kill by exact comm too.
pkill -f "selfdrive/ui/text" 2>/dev/null pkill -f "selfdrive/ui/text" 2>/dev/null
pkill -x _text 2>/dev/null
# write tmux scrollback to a file # write tmux scrollback to a file
tmux capture-pane -pq -S-1000 > /tmp/launch_log tmux capture-pane -pq -S-1000 > /tmp/launch_log
# Preflight: create dirs git can't track
source "$DIR/build_preflight.sh"
# start manager # start manager
cd selfdrive/manager cd selfdrive/manager
if [ ! -f $DIR/prebuilt ]; then if [ ! -f $DIR/prebuilt ]; then
+12
View File
@@ -13,13 +13,25 @@ pkill -9 -f 'selfdrive\.' 2>/dev/null
pkill -9 -f 'system\.' 2>/dev/null pkill -9 -f 'system\.' 2>/dev/null
pkill -9 -f './ui' 2>/dev/null pkill -9 -f './ui' 2>/dev/null
pkill -9 -f 'selfdrive/ui/text' 2>/dev/null pkill -9 -f 'selfdrive/ui/text' 2>/dev/null
# `text` is a shell wrapper that execs `./_text` — after exec the running
# process's argv has no path, so kill by exact comm too.
pkill -9 -x _text 2>/dev/null
sleep 1 sleep 1
# CLEARPILOT: ensure params persistence dir is owned by comma:comma. Editing
# the tree as root leaves files owned by root in /data/params/d_tmp/, and
# Params writes done as comma will then EACCES on rename. Reset on every
# launch so this never silently breaks again.
sudo chown -R comma:comma /data/params
bash /data/openpilot/system/clearpilot/on_start.sh bash /data/openpilot/system/clearpilot/on_start.sh
# CLEARPILOT: start VPN monitor (kills previous instances, runs as root) # CLEARPILOT: start VPN monitor (kills previous instances, runs as root)
sudo bash -c 'nohup /data/openpilot/system/clearpilot/vpn-monitor.sh >> /tmp/vpn-monitor.log 2>&1 &' sudo bash -c 'nohup /data/openpilot/system/clearpilot/vpn-monitor.sh >> /tmp/vpn-monitor.log 2>&1 &'
# CLEARPILOT: start nice monitor (keeps claude at nice 19)
sudo bash -c 'nohup /data/openpilot/system/clearpilot/nice-monitor.sh > /dev/null 2>&1 &'
# CLEARPILOT: pass --bench flag through to manager via env var # CLEARPILOT: pass --bench flag through to manager via env var
if [ "$1" = "--bench" ]; then if [ "$1" = "--bench" ]; then
export BENCH_MODE=1 export BENCH_MODE=1
+12 -1
View File
@@ -1,7 +1,17 @@
----- -----
- screen off on lkas - fix debug notice message
- screen mode 1 = no UI, screen mode 2 = off
- speed limit change chirp on cruise - speed limit change chirp on cruise
- fix always on detection, change message
-----
- Better debugger and data recorder
- Represent distance to lead car on primary UI
- Start testing for alert when stopped traffic and approaching > 31 mph
- or not enough time to slow at 31
- special experimental mode override?
----- -----
@@ -11,6 +21,7 @@
- better turning logic - better turning logic
- new settings UI - new settings UI
- experimental mode - experimental mode
- resume from stop
------------ ------------
-55
View File
@@ -13,7 +13,6 @@ from openpilot.selfdrive.car.hyundai.hyundaicanfd import CanBus
from openpilot.selfdrive.car.hyundai.values import HyundaiFlags, CAR, DBC, CAN_GEARS, CAMERA_SCC_CAR, \ from openpilot.selfdrive.car.hyundai.values import HyundaiFlags, CAR, DBC, CAN_GEARS, CAMERA_SCC_CAR, \
CANFD_CAR, Buttons, CarControllerParams CANFD_CAR, Buttons, CarControllerParams
from openpilot.selfdrive.car.interfaces import CarStateBase from openpilot.selfdrive.car.interfaces import CarStateBase
from openpilot.selfdrive.clearpilot.telemetry import tlog
PREV_BUTTON_SAMPLES = 8 PREV_BUTTON_SAMPLES = 8
CLUSTER_SAMPLE_RATE = 20 # frames CLUSTER_SAMPLE_RATE = 20 # frames
@@ -421,60 +420,6 @@ class CarState(CarStateBase):
# self.params_memory.put_float("CarSpeedLimitLiteral", self.calculate_speed_limit(cp, cp_cam)) # self.params_memory.put_float("CarSpeedLimitLiteral", self.calculate_speed_limit(cp, cp_cam))
self.params_memory.put_float("CarSpeedLimit", self.calculate_speed_limit(cp, cp_cam) * speed_factor) self.params_memory.put_float("CarSpeedLimit", self.calculate_speed_limit(cp, cp_cam) * speed_factor)
# CLEARPILOT: telemetry logging
speed_limit_bus = cp if self.CP.flags & HyundaiFlags.CANFD_HDA2 else cp_cam
scc = cp_cam.vl["SCC_CONTROL"] if self.CP.flags & HyundaiFlags.CANFD_CAMERA_SCC else cp.vl["SCC_CONTROL"]
cluster = speed_limit_bus.vl["CLUSTER_SPEED_LIMIT"]
tlog("car", {
"vEgo": round(ret.vEgo, 3),
"vEgoRaw": round(ret.vEgoRaw, 3),
"aEgo": round(ret.aEgo, 3),
"steeringAngleDeg": round(ret.steeringAngleDeg, 1),
"gear": str(ret.gearShifter),
"brakePressed": ret.brakePressed,
"gasPressed": ret.gasPressed,
"standstill": ret.standstill,
"leftBlinker": ret.leftBlinker,
"rightBlinker": ret.rightBlinker,
})
tlog("cruise", {
"enabled": ret.cruiseState.enabled,
"available": ret.cruiseState.available,
"speed": round(ret.cruiseState.speed, 3),
"standstill": ret.cruiseState.standstill,
"accFaulted": ret.accFaulted,
"brakePressed": ret.brakePressed,
"ACCMode": scc.get("ACCMode", 0),
"VSetDis": scc.get("VSetDis", 0),
"aReqRaw": round(scc.get("aReqRaw", 0), 3),
"aReqValue": round(scc.get("aReqValue", 0), 3),
"DISTANCE_SETTING": scc.get("DISTANCE_SETTING", 0),
"ACC_ObjDist": round(scc.get("ACC_ObjDist", 0), 1),
})
tlog("speed_limit", {
"SPEED_LIMIT_1": cluster.get("SPEED_LIMIT_1", 0),
"SPEED_LIMIT_2": cluster.get("SPEED_LIMIT_2", 0),
"SPEED_LIMIT_3": cluster.get("SPEED_LIMIT_3", 0),
"SCHOOL_ZONE": cluster.get("SCHOOL_ZONE", 0),
"CHIME_1": cluster.get("CHIME_1", 0),
"CHIME_2": cluster.get("CHIME_2", 0),
"SPEED_CHANGE_BLINKING": cluster.get("SPEED_CHANGE_BLINKING", 0),
"calculated": self.calculate_speed_limit(cp, cp_cam),
"is_metric": self.is_metric,
})
tlog("buttons", {
"cruise_button": self.cruise_buttons[-1],
"prev_cruise_button": self.prev_cruise_buttons,
"main_button": self.main_buttons[-1],
"prev_main_button": self.prev_main_buttons,
"lkas_enabled": self.lkas_enabled,
"main_enabled": self.main_enabled,
})
return ret return ret
def get_can_parser(self, CP): def get_can_parser(self, CP):
-140
View File
@@ -1,140 +0,0 @@
#!/usr/bin/env python3
"""
ClearPilot bench command tool. Sets bench params and queries UI state.
Usage:
python3 -m selfdrive.clearpilot.bench_cmd gear D
python3 -m selfdrive.clearpilot.bench_cmd speed 20
python3 -m selfdrive.clearpilot.bench_cmd speedlimit 45
python3 -m selfdrive.clearpilot.bench_cmd cruise 55
python3 -m selfdrive.clearpilot.bench_cmd engaged 1
python3 -m selfdrive.clearpilot.bench_cmd debugbutton (simulate LKAS debug button press)
python3 -m selfdrive.clearpilot.bench_cmd dump
python3 -m selfdrive.clearpilot.bench_cmd wait_ready
"""
import os
import subprocess
import sys
import time
import zmq
from openpilot.common.params import Params
def check_ui_process():
"""Check if UI process is running and healthy. Returns error string or None if OK."""
try:
result = subprocess.run(["pgrep", "-a", "-f", "./ui"], capture_output=True, text=True)
if result.returncode != 0:
return "ERROR: UI process not running"
# Get the PID and check its uptime
for line in result.stdout.strip().split("\n"):
parts = line.split(None, 1)
if len(parts) >= 2 and "./ui" in parts[1]:
pid = parts[0]
try:
stat = os.stat(f"/proc/{pid}")
uptime = time.time() - stat.st_mtime
if uptime < 5:
return f"ERROR: UI process (pid {pid}) uptime {uptime:.1f}s — likely crash looping. Check: tail /data/log2/$(ls -t /data/log2/ | head -1)/session.log"
except FileNotFoundError:
return "ERROR: UI process disappeared"
except Exception:
pass
return None
def ui_dump():
ctx = zmq.Context()
sock = ctx.socket(zmq.REQ)
sock.setsockopt(zmq.RCVTIMEO, 2000)
sock.connect("ipc:///tmp/clearpilot_ui_rpc")
sock.send_string("dump")
try:
return sock.recv_string()
except zmq.Again:
return None
finally:
sock.close()
ctx.term()
def wait_ready(timeout=20):
"""Wait until the UI shows ReadyWindow, up to timeout seconds."""
start = time.time()
while time.time() - start < timeout:
dump = ui_dump()
if dump and "ReadyWindow" in dump:
# Check it's actually visible
for line in dump.split("\n"):
if "ReadyWindow" in line and "vis=Y" in line:
print("UI ready (ReadyWindow visible)")
return True
time.sleep(1)
print(f"ERROR: UI not ready after {timeout}s")
if dump:
print(dump)
return False
def main():
if len(sys.argv) < 2:
print(__doc__)
return
cmd = sys.argv[1].lower()
params = Params("/dev/shm/params")
param_map = {
"gear": "BenchGear",
"speed": "BenchSpeed",
"speedlimit": "BenchSpeedLimit",
"cruise": "BenchCruiseSpeed",
"engaged": "BenchEngaged",
}
if cmd == "dump":
ui_status = check_ui_process()
if ui_status:
print(ui_status)
else:
result = ui_dump()
if result:
print(result)
else:
print("ERROR: UI not responding")
elif cmd == "wait_ready":
wait_ready()
elif cmd == "debugbutton":
# Simulate LKAS debug button — same state machine as controlsd.clearpilot_state_control()
current = params.get_int("ScreenDisplayMode")
gear = (params.get("BenchGear") or b"P").decode().strip().upper()
in_drive = gear in ("D", "S", "L")
if in_drive:
transitions = {0: 4, 1: 2, 2: 3, 3: 4, 4: 2}
new_mode = transitions.get(current, 0)
else:
new_mode = 0 if current == 3 else 3
params.put_int("ScreenDisplayMode", new_mode)
mode_names = {0: "auto-normal", 1: "auto-nightrider", 2: "normal", 3: "screen-off", 4: "nightrider"}
print(f"ScreenDisplayMode: {current} ({mode_names.get(current, '?')}) → {new_mode} ({mode_names.get(new_mode, '?')})"
f" [gear={gear}, in_drive={in_drive}]")
elif cmd in param_map:
if len(sys.argv) < 3:
print(f"Usage: bench_cmd {cmd} <value>")
return
value = sys.argv[2]
params.put(param_map[cmd], value)
print(f"{param_map[cmd]} = {value}")
else:
print(f"Unknown command: {cmd}")
print(__doc__)
if __name__ == "__main__":
main()
-118
View File
@@ -1,118 +0,0 @@
#!/usr/bin/env python3
"""
ClearPilot bench onroad simulator.
Publishes fake messages to make the UI go onroad and display
configurable vehicle state. Control values via params in /dev/shm/params:
BenchSpeed - vehicle speed in mph (default: 0)
BenchSpeedLimit - speed limit in mph (default: 0, 0=hidden)
BenchCruiseSpeed - cruise set speed in mph (default: 0, 0=not set)
BenchGear - P, D, R, N (default: P)
BenchEngaged - 0 or 1, cruise engaged (default: 0)
Usage:
su - comma -c "PYTHONPATH=/data/openpilot python3 -m selfdrive.clearpilot.bench_onroad"
To stop: Ctrl+C or kill the process. UI returns to offroad.
"""
import time
import cereal.messaging as messaging
from cereal import log, car
from openpilot.common.params import Params
MS_PER_MPH = 0.44704
def main():
params = Params()
params_mem = Params("/dev/shm/params")
# Set defaults
params_mem.put("BenchSpeed", "0")
params_mem.put("BenchSpeedLimit", "0")
params_mem.put("BenchCruiseSpeed", "0")
params_mem.put("BenchGear", "P")
params_mem.put("BenchEngaged", "0")
# thermald handles deviceState (reads our fake pandaStates for ignition)
pm = messaging.PubMaster([
"pandaStates", "carState", "controlsState",
"driverMonitoringState", "liveCalibration",
])
print("Bench onroad simulator started")
print("Set values in /dev/shm/params/d/Bench*")
print(" BenchSpeed=0 BenchSpeedLimit=0 BenchCruiseSpeed=0 BenchGear=P BenchEngaged=0")
gear_map = {
"P": car.CarState.GearShifter.park,
"D": car.CarState.GearShifter.drive,
"R": car.CarState.GearShifter.reverse,
"N": car.CarState.GearShifter.neutral,
}
frame = 0
try:
while True:
# Read bench params
speed_mph = float((params_mem.get("BenchSpeed", encoding="utf-8") or "0").strip())
speed_limit_mph = float((params_mem.get("BenchSpeedLimit", encoding="utf-8") or "0").strip())
cruise_mph = float((params_mem.get("BenchCruiseSpeed", encoding="utf-8") or "0").strip())
gear_str = (params_mem.get("BenchGear", encoding="utf-8") or "P").strip().upper()
engaged = (params_mem.get("BenchEngaged", encoding="utf-8") or "0").strip() == "1"
speed_ms = speed_mph * MS_PER_MPH
gear = gear_map.get(gear_str, car.CarState.GearShifter.park)
# pandaStates — 10 Hz (thermald reads ignition from this)
if frame % 1 == 0:
dat = messaging.new_message("pandaStates", 1)
dat.pandaStates[0].ignitionLine = True
dat.pandaStates[0].pandaType = log.PandaState.PandaType.tres
pm.send("pandaStates", dat)
# carState — 10 Hz
dat = messaging.new_message("carState")
cs = dat.carState
cs.vEgo = speed_ms
cs.vEgoCluster = speed_ms
cs.gearShifter = gear
cs.standstill = speed_ms < 0.1
cs.cruiseState.available = True
cs.cruiseState.enabled = engaged
cs.cruiseState.speed = cruise_mph * MS_PER_MPH if cruise_mph > 0 else 0
pm.send("carState", dat)
# controlsState — 10 Hz
dat = messaging.new_message("controlsState")
ctl = dat.controlsState
ctl.vCruise = cruise_mph * 1.60934 if cruise_mph > 0 else 255 # km/h or 255=not set
ctl.vCruiseCluster = ctl.vCruise
ctl.enabled = engaged
ctl.active = engaged
pm.send("controlsState", dat)
# driverMonitoringState — low freq
if frame % 10 == 0:
dat = messaging.new_message("driverMonitoringState")
dat.driverMonitoringState.isActiveMode = True
pm.send("driverMonitoringState", dat)
# liveCalibration — low freq
if frame % 50 == 0:
dat = messaging.new_message("liveCalibration")
dat.liveCalibration.calStatus = log.LiveCalibrationData.Status.calibrated
dat.liveCalibration.rpyCalib = [0.0, 0.0, 0.0]
pm.send("liveCalibration", dat)
frame += 1
time.sleep(0.1) # 10 Hz base loop
except KeyboardInterrupt:
print("\nBench simulator stopped")
if __name__ == "__main__":
main()
Binary file not shown.
+131 -102
View File
@@ -7,49 +7,41 @@
* Trip directory structure: * Trip directory structure:
* /data/media/0/videos/YYYYMMDD-HHMMSS/ (trip directory, named at trip start) * /data/media/0/videos/YYYYMMDD-HHMMSS/ (trip directory, named at trip start)
* YYYYMMDD-HHMMSS.mp4 (3-minute segments) * YYYYMMDD-HHMMSS.mp4 (3-minute segments)
* YYYYMMDD-HHMMSS.srt (GPS subtitle sidecar)
* *
* Trip lifecycle state machine: * Trip lifecycle state machine:
* *
* On process start (after time-valid wait): * WAITING:
* - Create trip directory, start recording immediately with 10-min idle timer * - Process starts in this state
* - If car is already in drive, timer is cancelled and recording continues * - Waits for valid system time (year >= 2024) AND car in drive gear
* - If car stays parked/off for 10 minutes, trip ends * - Transitions to RECORDING when both conditions met
* *
* IDLE_TIMEOUT → RECORDING: * RECORDING:
* - Car enters drive gear before timer expires → cancel timer, resume recording * - Actively encoding frames, car is in drive
* in the same trip (no new trip directory) * - Car leaves drive → start 10-min idle timer → IDLE_TIMEOUT
* *
* RECORDING → IDLE_TIMEOUT: * IDLE_TIMEOUT:
* - Car leaves drive gear (park, off, neutral) → start 10-minute idle timer, * - Car left drive, still recording with timer running
* continue recording frames during the timeout period * - Car re-enters drive → cancel timer → RECORDING
* * - Timer expires → close trip → WAITING
* IDLE_TIMEOUT → TRIP_ENDED: * - Ignition off → close trip → WAITING
* - 10-minute timer expires → close segment, trip is over
*
* TRIP_ENDED → RECORDING (new trip):
* - Car enters drive gear → create new trip directory, start recording
*
* Any state → (new trip) on ignition off→on:
* - Ignition transitions off→on → close current trip if active, create new trip
* directory, start recording with 10-min idle timer. This applies even from
* TRIP_ENDED — turning the car on always starts a new trip. If the car is in
* park after ignition on, the idle timer runs; entering drive cancels it.
* *
* Graceful shutdown (DashcamShutdown param): * Graceful shutdown (DashcamShutdown param):
* - thermald sets DashcamShutdown="1" before device power-off * - thermald sets DashcamShutdown="1" before device power-off
* - dashcamd closes current segment, sets DashcamShutdown="0" (ack), exits * - dashcamd closes current segment, acks, exits
* - thermald waits up to 15s for ack, then proceeds with shutdown
* *
* GPS subtitle track: * Published params (memory, every 5s):
* - Each .mp4 segment has a companion .srt subtitle file * - DashcamState: "waiting" or "recording"
* - Updated at most once per second from gpsLocation cereal messages * - DashcamFrames: per-trip encoded frame count (resets each trip)
* - Format: "35 MPH | 44.9216°N 93.3260°W | 2026-04-13 05:19:00 UTC"
* - Most players auto-detect .srt files alongside .mp4 files
*/ */
#include <cstdio> #include <cstdio>
#include <ctime> #include <ctime>
#include <string> #include <string>
#include <errno.h>
#include <signal.h>
#include <sched.h>
#include <execinfo.h>
#include <sys/stat.h> #include <sys/stat.h>
#include <sys/resource.h> #include <sys/resource.h>
#include <unistd.h> #include <unistd.h>
@@ -64,7 +56,8 @@
const std::string VIDEOS_BASE = "/data/media/0/videos"; const std::string VIDEOS_BASE = "/data/media/0/videos";
const int SEGMENT_SECONDS = 180; // 3 minutes const int SEGMENT_SECONDS = 180; // 3 minutes
const int CAMERA_FPS = 20; const int SOURCE_FPS = 20;
const int CAMERA_FPS = 10;
const int FRAMES_PER_SEGMENT = SEGMENT_SECONDS * CAMERA_FPS; const int FRAMES_PER_SEGMENT = SEGMENT_SECONDS * CAMERA_FPS;
const int BITRATE = 2500 * 1024; // 2500 kbps const int BITRATE = 2500 * 1024; // 2500 kbps
const double IDLE_TIMEOUT_SECONDS = 600.0; // 10 minutes const double IDLE_TIMEOUT_SECONDS = 600.0; // 10 minutes
@@ -72,10 +65,9 @@ const double IDLE_TIMEOUT_SECONDS = 600.0; // 10 minutes
ExitHandler do_exit; ExitHandler do_exit;
enum TripState { enum TripState {
IDLE, // no trip active, waiting for drive WAITING, // no trip, waiting for valid time + drive gear
RECORDING, // actively recording, car in drive RECORDING, // actively recording, car in drive
IDLE_TIMEOUT, // car parked/off, recording with 10-min timer IDLE_TIMEOUT, // car left drive, recording with 10-min timer
TRIP_ENDED, // trip closed, waiting for next drive
}; };
static std::string make_timestamp() { static std::string make_timestamp() {
@@ -114,81 +106,110 @@ static std::string srt_time(int seconds) {
return std::string(buf); return std::string(buf);
} }
static void crash_handler(int sig) {
FILE *f = fopen("/tmp/dashcamd_crash.log", "w");
if (f) {
fprintf(f, "CRASH: signal %d\n", sig);
void *bt[30];
int n = backtrace(bt, 30);
backtrace_symbols_fd(bt, n, fileno(f));
fclose(f);
}
_exit(1);
}
int main(int argc, char *argv[]) { int main(int argc, char *argv[]) {
signal(SIGSEGV, crash_handler);
signal(SIGABRT, crash_handler);
setpriority(PRIO_PROCESS, 0, -10); 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 // Ensure base output directory exists
mkdir(VIDEOS_BASE.c_str(), 0755); mkdir(VIDEOS_BASE.c_str(), 0755);
// Wait for valid system time so trip/segment names have real timestamps LOGW("dashcamd: started, connecting to camerad road stream");
LOGW("dashcamd: waiting for system time");
while (!do_exit) {
if (system_time_valid()) break;
usleep(1000000); // 1 Hz poll
}
if (do_exit) return 0;
LOGW("dashcamd: system time valid");
LOGW("dashcamd: connecting to camerad road stream");
VisionIpcClient vipc("camerad", VISION_STREAM_ROAD, false); VisionIpcClient vipc("camerad", VISION_STREAM_ROAD, false);
while (!do_exit && !vipc.connect(false)) { while (!do_exit && !vipc.connect(false)) {
usleep(100000); usleep(100000);
} }
if (do_exit) return 0; if (do_exit) return 0;
LOGW("dashcamd: vipc connected, waiting for valid frame");
int width = vipc.buffers[0].width; // Wait for a frame with valid dimensions (camerad may still be initializing)
int height = vipc.buffers[0].height; VisionBuf *init_buf = nullptr;
int y_stride = vipc.buffers[0].stride; while (!do_exit) {
init_buf = vipc.recv();
if (init_buf != nullptr && init_buf->width > 0 && init_buf->height > 0) break;
usleep(100000);
}
if (do_exit) return 0;
int width = init_buf->width;
int height = init_buf->height;
int y_stride = init_buf->stride;
int uv_stride = y_stride; int uv_stride = y_stride;
LOGW("dashcamd: connected %dx%d, stride=%d", width, height, y_stride); LOGW("dashcamd: first valid frame %dx%d stride=%d", width, height, y_stride);
// Subscribe to carState (gear), deviceState (ignition), gpsLocation (subtitles) // Subscribe to carState (gear), deviceState (ignition), gpsLocation (subtitles)
SubMaster sm({"carState", "deviceState", "gpsLocation"}); SubMaster sm({"carState", "deviceState", "gpsLocation"});
Params params; Params params;
Params params_memory("/dev/shm/params");
// Trip state // Trip state
TripState state = IDLE; TripState state = WAITING;
OmxEncoder *encoder = nullptr; OmxEncoder *encoder = nullptr;
std::string trip_dir; std::string trip_dir;
int frame_count = 0; int frame_count = 0; // per-segment (for rotation)
int trip_frames = 0; // per-trip (published to params)
int recv_count = 0;
uint64_t segment_start_ts = 0; uint64_t segment_start_ts = 0;
double idle_timer_start = 0.0; double idle_timer_start = 0.0;
// SRT subtitle state // SRT subtitle state
FILE *srt_file = nullptr; FILE *srt_file = nullptr;
int srt_index = 0; // subtitle entry counter (1-based) int srt_index = 0;
int srt_segment_sec = 0; // seconds elapsed in current segment int srt_segment_sec = 0;
double last_srt_write = 0; // monotonic time of last SRT write double last_srt_write = 0;
// Ignition tracking for off→on detection // Ignition tracking
bool prev_started = false; bool prev_started = false;
bool started_initialized = false; bool started_initialized = false;
// Param check throttle (don't hit filesystem every frame) // Param publish throttle
int param_check_counter = 0; int param_check_counter = 0;
double last_param_write = 0;
// Helper: start a new trip with recording + optional idle timer // Publish initial state
params_memory.put("DashcamState", "waiting");
params_memory.put("DashcamFrames", "0");
LOGW("dashcamd: entering main loop in WAITING state");
// Helper: start a new trip
auto start_new_trip = [&]() { auto start_new_trip = [&]() {
// Close existing encoder if any
if (encoder) {
if (state == RECORDING || state == IDLE_TIMEOUT) {
encoder->encoder_close();
}
delete encoder;
encoder = nullptr;
}
trip_dir = VIDEOS_BASE + "/" + make_timestamp(); trip_dir = VIDEOS_BASE + "/" + make_timestamp();
mkdir(trip_dir.c_str(), 0755); mkdir(trip_dir.c_str(), 0755);
LOGW("dashcamd: new trip %s", trip_dir.c_str()); LOGW("dashcamd: new trip %s", trip_dir.c_str());
encoder = new OmxEncoder(trip_dir.c_str(), width, height, CAMERA_FPS, BITRATE, false, false); encoder = new OmxEncoder(trip_dir.c_str(), width, height, CAMERA_FPS, BITRATE);
std::string seg_name = make_timestamp(); std::string seg_name = make_timestamp();
LOGW("dashcamd: opening segment %s", seg_name.c_str()); LOGW("dashcamd: opening segment %s", seg_name.c_str());
encoder->encoder_open((seg_name + ".mp4").c_str()); encoder->encoder_open((seg_name + ".mp4").c_str());
// Open companion SRT file
std::string srt_path = trip_dir + "/" + seg_name + ".srt"; std::string srt_path = trip_dir + "/" + seg_name + ".srt";
srt_file = fopen(srt_path.c_str(), "w"); srt_file = fopen(srt_path.c_str(), "w");
srt_index = 0; srt_index = 0;
@@ -196,37 +217,41 @@ int main(int argc, char *argv[]) {
last_srt_write = 0; last_srt_write = 0;
frame_count = 0; frame_count = 0;
trip_frames = 0;
segment_start_ts = nanos_since_boot(); segment_start_ts = nanos_since_boot();
state = RECORDING; state = RECORDING;
params_memory.put("DashcamState", "recording");
params_memory.put("DashcamFrames", "0");
}; };
// Helper: close current trip
auto close_trip = [&]() { auto close_trip = [&]() {
if (srt_file) { fclose(srt_file); srt_file = nullptr; } if (srt_file) { fclose(srt_file); srt_file = nullptr; }
if (encoder) { if (encoder) {
if (state == RECORDING || state == IDLE_TIMEOUT) {
encoder->encoder_close(); encoder->encoder_close();
LOGW("dashcamd: segment closed"); LOGW("dashcamd: segment closed");
}
delete encoder; delete encoder;
encoder = nullptr; encoder = nullptr;
} }
state = TRIP_ENDED; state = WAITING;
frame_count = 0; frame_count = 0;
trip_frames = 0;
idle_timer_start = 0.0; idle_timer_start = 0.0;
LOGW("dashcamd: trip ended"); LOGW("dashcamd: trip ended, returning to WAITING");
};
// Start recording immediately — if the car is in drive, great; if parked/off, params_memory.put("DashcamState", "waiting");
// the 10-min idle timer will stop the trip if drive is never detected. params_memory.put("DashcamFrames", "0");
start_new_trip(); };
idle_timer_start = nanos_since_boot() / 1e9;
state = IDLE_TIMEOUT;
LOGW("dashcamd: recording started, waiting for drive (10-min idle timer active)");
while (!do_exit) { while (!do_exit) {
VisionBuf *buf = vipc.recv(); VisionBuf *buf = vipc.recv();
if (buf == nullptr) continue; if (buf == nullptr) continue;
// Skip frames to match target FPS (SOURCE_FPS -> CAMERA_FPS)
recv_count++;
if (SOURCE_FPS > CAMERA_FPS && (recv_count % (SOURCE_FPS / CAMERA_FPS)) != 0) continue;
sm.update(0); sm.update(0);
double now = nanos_since_boot() / 1e9; double now = nanos_since_boot() / 1e9;
@@ -241,60 +266,54 @@ int main(int argc, char *argv[]) {
gear == cereal::CarState::GearShifter::LOW || gear == cereal::CarState::GearShifter::LOW ||
gear == cereal::CarState::GearShifter::MANUMATIC); gear == cereal::CarState::GearShifter::MANUMATIC);
// Detect ignition off→on transition (new ignition cycle = new trip) // Detect ignition off → close any active trip
if (started_initialized && !prev_started && started) { if (started_initialized && prev_started && !started) {
LOGW("dashcamd: ignition on — new cycle"); LOGW("dashcamd: ignition off");
if (state == RECORDING || state == IDLE_TIMEOUT) { if (state == RECORDING || state == IDLE_TIMEOUT) {
close_trip(); close_trip();
} }
// Start recording immediately, idle timer until drive is detected
start_new_trip();
idle_timer_start = now;
state = IDLE_TIMEOUT;
} }
prev_started = started; prev_started = started;
started_initialized = true; started_initialized = true;
// Check for graceful shutdown request (every ~1 second = 20 frames) // Check for graceful shutdown request (every ~1 second)
if (++param_check_counter >= CAMERA_FPS) { if (++param_check_counter >= CAMERA_FPS) {
param_check_counter = 0; param_check_counter = 0;
if (params.getBool("DashcamShutdown")) { if (params_memory.getBool("DashcamShutdown")) {
LOGW("dashcamd: shutdown requested, closing trip"); LOGW("dashcamd: shutdown requested");
if (state == RECORDING || state == IDLE_TIMEOUT) { if (state == RECORDING || state == IDLE_TIMEOUT) {
close_trip(); close_trip();
} }
params.putBool("DashcamShutdown", false); params_memory.putBool("DashcamShutdown", false);
LOGW("dashcamd: shutdown ack sent, exiting"); LOGW("dashcamd: shutdown ack sent, exiting");
break; break;
} }
} }
// State machine transitions // State machine
switch (state) { switch (state) {
case IDLE: case WAITING: {
case TRIP_ENDED: bool has_gps = sm.valid("gpsLocation") && sm["gpsLocation"].getGpsLocation().getHasFix();
if (in_drive) { if (in_drive && system_time_valid() && has_gps) {
start_new_trip(); start_new_trip();
} }
break; break;
}
case RECORDING: case RECORDING:
if (!in_drive) { if (!in_drive) {
// Car left drive — start idle timeout
idle_timer_start = now; idle_timer_start = now;
state = IDLE_TIMEOUT; state = IDLE_TIMEOUT;
LOGW("dashcamd: car not in drive, starting 10-min idle timer"); LOGW("dashcamd: car left drive, starting 10-min idle timer");
} }
break; break;
case IDLE_TIMEOUT: case IDLE_TIMEOUT:
if (in_drive) { if (in_drive) {
// Back in drive — cancel timer, resume recording same trip
idle_timer_start = 0.0; idle_timer_start = 0.0;
state = RECORDING; state = RECORDING;
LOGW("dashcamd: back in drive, resuming trip"); LOGW("dashcamd: back in drive, resuming trip");
} else if ((now - idle_timer_start) >= IDLE_TIMEOUT_SECONDS) { } else if ((now - idle_timer_start) >= IDLE_TIMEOUT_SECONDS) {
// Timer expired — end trip
LOGW("dashcamd: idle timeout expired"); LOGW("dashcamd: idle timeout expired");
close_trip(); close_trip();
} }
@@ -325,16 +344,28 @@ int main(int argc, char *argv[]) {
uint64_t ts = nanos_since_boot() - segment_start_ts; uint64_t ts = nanos_since_boot() - segment_start_ts;
// Validate buffer before encoding
if (buf->y == nullptr || buf->uv == nullptr || buf->width == 0 || buf->height == 0) {
LOGE("dashcamd: invalid frame buf y=%p uv=%p %zux%zu, skipping", buf->y, buf->uv, buf->width, buf->height);
continue;
}
// Feed NV12 frame directly to OMX encoder // Feed NV12 frame directly to OMX encoder
encoder->encode_frame_nv12(buf->y, y_stride, buf->uv, uv_stride, width, height, ts); encoder->encode_frame_nv12(buf->y, y_stride, buf->uv, uv_stride, width, height, ts);
frame_count++; frame_count++;
trip_frames++;
// Publish state every 5 seconds
if (now - last_param_write >= 5.0) {
params_memory.put("DashcamFrames", std::to_string(trip_frames));
last_param_write = now;
}
// Write GPS subtitle at most once per second // Write GPS subtitle at most once per second
if (srt_file && (now - last_srt_write) >= 1.0) { if (srt_file && (now - last_srt_write) >= 1.0) {
last_srt_write = now; last_srt_write = now;
srt_index++; srt_index++;
// Read GPS data
double lat = 0, lon = 0, speed_ms = 0; double lat = 0, lon = 0, speed_ms = 0;
bool has_gps = sm.valid("gpsLocation") && sm["gpsLocation"].getGpsLocation().getHasFix(); bool has_gps = sm.valid("gpsLocation") && sm["gpsLocation"].getGpsLocation().getHasFix();
if (has_gps) { if (has_gps) {
@@ -367,13 +398,11 @@ int main(int argc, char *argv[]) {
} }
// Clean exit // Clean exit
if (srt_file) { fclose(srt_file); srt_file = nullptr; }
if (encoder) {
if (state == RECORDING || state == IDLE_TIMEOUT) { if (state == RECORDING || state == IDLE_TIMEOUT) {
encoder->encoder_close(); close_trip();
}
delete encoder;
} }
params_memory.put("DashcamState", "stopped");
params_memory.put("DashcamFrames", "0");
LOGW("dashcamd: stopped"); LOGW("dashcamd: stopped");
return 0; return 0;
-121
View File
@@ -1,121 +0,0 @@
#!/usr/bin/env python3
"""
CLEARPILOT dashcamd — records raw camera footage to MP4 using hardware H.264 encoder.
Connects directly to camerad via VisionIPC, receives NV12 frames, and pipes them
to ffmpeg's h264_v4l2m2m encoder. Produces 3-minute MP4 segments in /data/media/0/videos/.
This replaces the FrogPilot screen recorder approach (QWidget::grab -> OMX) with a
direct camera capture that works regardless of UI state (screen off, alternate modes, etc).
"""
import os
import time
import subprocess
import signal
from pathlib import Path
from datetime import datetime
from cereal.visionipc import VisionIpcClient, VisionStreamType
from openpilot.common.params import Params
from openpilot.common.swaglog import cloudlog
from openpilot.selfdrive import sentry
PROCESS_NAME = "selfdrive.clearpilot.dashcamd"
VIDEOS_DIR = "/data/media/0/videos"
SEGMENT_SECONDS = 180 # 3 minutes
CAMERA_FPS = 20
FRAMES_PER_SEGMENT = SEGMENT_SECONDS * CAMERA_FPS
def make_filename():
return datetime.now().strftime("%Y%m%d-%H%M%S") + ".mp4"
def open_encoder(width, height, filepath):
"""Start an ffmpeg subprocess that accepts raw NV12 on stdin and writes MP4."""
cmd = [
"ffmpeg", "-y", "-nostdin", "-loglevel", "error",
"-f", "rawvideo",
"-pix_fmt", "nv12",
"-s", f"{width}x{height}",
"-r", str(CAMERA_FPS),
"-i", "pipe:0",
"-c:v", "h264_v4l2m2m",
"-b:v", "4M",
"-f", "mp4",
"-movflags", "+faststart",
filepath,
]
return subprocess.Popen(cmd, stdin=subprocess.PIPE)
def main():
sentry.set_tag("daemon", PROCESS_NAME)
cloudlog.bind(daemon=PROCESS_NAME)
os.makedirs(VIDEOS_DIR, exist_ok=True)
params = Params()
# Connect to camerad road stream
cloudlog.info("dashcamd: connecting to camerad road stream")
vipc = VisionIpcClient("camerad", VisionStreamType.VISION_STREAM_ROAD, False)
while not vipc.connect(False):
time.sleep(0.1)
width, height = vipc.width, vipc.height
# NV12 frame: Y plane (w*h) + UV plane (w*h/2)
frame_size = width * height * 3 // 2
cloudlog.info(f"dashcamd: connected, {width}x{height}, frame_size={frame_size}")
frame_count = 0
encoder = None
lock_path = None
try:
while True:
buf = vipc.recv()
if buf is None:
continue
# Start new segment if needed
if encoder is None or frame_count >= FRAMES_PER_SEGMENT:
# Close previous segment
if encoder is not None:
encoder.stdin.close()
encoder.wait()
if lock_path and os.path.exists(lock_path):
os.remove(lock_path)
cloudlog.info(f"dashcamd: closed segment, {frame_count} frames")
# Open new segment
filename = make_filename()
filepath = os.path.join(VIDEOS_DIR, filename)
lock_path = filepath + ".lock"
Path(lock_path).touch()
cloudlog.info(f"dashcamd: opening segment {filename}")
encoder = open_encoder(width, height, filepath)
frame_count = 0
# Write raw NV12 frame to ffmpeg stdin
try:
encoder.stdin.write(buf.data[:frame_size])
frame_count += 1
except BrokenPipeError:
cloudlog.error("dashcamd: encoder pipe broken, restarting segment")
encoder = None
except (KeyboardInterrupt, SystemExit):
pass
finally:
if encoder is not None:
encoder.stdin.close()
encoder.wait()
if lock_path and os.path.exists(lock_path):
os.remove(lock_path)
cloudlog.info("dashcamd: stopped")
if __name__ == "__main__":
main()
Binary file not shown.
+114
View File
@@ -0,0 +1,114 @@
"""
ClearPilot speed processing module.
Shared logic for converting raw speed and speed limit data into display-ready
values. Called from controlsd (live mode) and bench_onroad (bench mode).
Reads raw inputs, converts to display units (mph or kph based on car's CAN
unit setting), detects speed limit changes, and writes results to params_memory
for the onroad UI to read.
"""
import math
import time
from openpilot.common.params import Params
from openpilot.common.conversions import Conversions as CV
class SpeedState:
def __init__(self):
self.params_memory = Params("/dev/shm/params")
self.prev_speed_limit = 0
# Ding state tracking
self.last_ding_time = 0.0
self.prev_warning = ""
self.prev_warning_speed_limit = 0
# Cache last-written param values — each put() is mkstemp+fsync+flock+rename.
# Sentinel None so the first call always writes.
self._w_has_speed = None
self._w_speed_display = None
self._w_speed_limit_display = None
self._w_speed_unit = None
self._w_is_metric = None
self._w_cruise_warning = None
self._w_cruise_warning_speed = None
def _put_if_changed(self, key, value, attr):
if getattr(self, attr) != value:
self.params_memory.put(key, value)
setattr(self, attr, value)
def update(self, speed_ms: float, has_speed: bool, speed_limit_ms: float, is_metric: bool,
cruise_speed_ms: float = 0.0, cruise_active: bool = False, cruise_standstill: bool = False):
"""
Convert raw m/s values to display-ready strings and write to params_memory.
"""
now = time.monotonic()
if is_metric:
speed_display = speed_ms * CV.MS_TO_KPH
speed_limit_display = speed_limit_ms * CV.MS_TO_KPH
cruise_display = cruise_speed_ms * CV.MS_TO_KPH
unit = "km/h"
else:
speed_display = speed_ms * CV.MS_TO_MPH
speed_limit_display = speed_limit_ms * CV.MS_TO_MPH
cruise_display = cruise_speed_ms * CV.MS_TO_MPH
unit = "mph"
speed_int = int(math.floor(speed_display))
speed_limit_int = int(round(speed_limit_display))
cruise_int = int(round(cruise_display))
self.prev_speed_limit = speed_limit_int
# Write display-ready values to params_memory (gated on change)
self._put_if_changed("ClearpilotHasSpeed", "1" if has_speed and speed_int > 0 else "0", "_w_has_speed")
self._put_if_changed("ClearpilotSpeedDisplay", str(speed_int) if has_speed and speed_int > 0 else "", "_w_speed_display")
self._put_if_changed("ClearpilotSpeedLimitDisplay", str(speed_limit_int) if speed_limit_int > 0 else "0", "_w_speed_limit_display")
self._put_if_changed("ClearpilotSpeedUnit", unit, "_w_speed_unit")
self._put_if_changed("ClearpilotIsMetric", "1" if is_metric else "0", "_w_is_metric")
# Cruise warning logic
warning = ""
warning_speed = ""
cruise_engaged = cruise_active and not cruise_standstill
if speed_limit_int >= 20 and cruise_engaged and cruise_int > 0:
# Tiers (warning fires at >= limit + threshold):
# limit >= 50: +9 over ok, warn at +10 (e.g. 60 → warn at 70)
# limit 26-49: +6 over ok, warn at +7 (e.g. 35 → warn at 42)
# limit <= 25: +8 over ok, warn at +9 (e.g. 25 → warn at 34, so 33 is ok)
if speed_limit_int >= 50:
over_threshold = 10
elif speed_limit_int <= 25:
over_threshold = 9
else:
over_threshold = 7
if cruise_int >= speed_limit_int + over_threshold:
warning = "over"
warning_speed = str(cruise_int)
elif cruise_int <= speed_limit_int - 5:
warning = "under"
warning_speed = str(cruise_int)
self._put_if_changed("ClearpilotCruiseWarning", warning, "_w_cruise_warning")
self._put_if_changed("ClearpilotCruiseWarningSpeed", warning_speed, "_w_cruise_warning_speed")
# Ding logic: play when warning sign appears or speed limit changes while visible
should_ding = False
if warning:
if not self.prev_warning:
# Warning sign just appeared
should_ding = True
elif speed_limit_int != self.prev_warning_speed_limit:
# Speed limit changed while warning sign is visible
should_ding = True
if should_ding and now - self.last_ding_time >= 30:
self.params_memory.put("ClearpilotPlayDing", "1")
self.last_ding_time = now
self.prev_warning = warning
self.prev_warning_speed_limit = speed_limit_int if warning else 0
+60
View File
@@ -0,0 +1,60 @@
#!/usr/bin/env python3
"""
ClearPilot speed/cruise daemon.
Subscribes to gpsLocation (for vehicle speed display) and carState (for
cruise state), reads CarSpeedLimit from /dev/shm/params, and ticks
SpeedState at ~2 Hz. SpeedState writes the display params the onroad UI
reads, and asserts ClearpilotPlayDing="1" on speed-limit warning
transitions (soundd consumes that flag).
Decoupled from controlsd so self-driving timing isn't affected.
"""
import sys
import cereal.messaging as messaging
from openpilot.common.params import Params
from openpilot.common.realtime import Ratekeeper
from openpilot.selfdrive.clearpilot.speed_logic import SpeedState
def main():
print("CLP speed_logicd: starting", file=sys.stderr, flush=True)
params = Params()
params_memory = Params("/dev/shm/params")
speed_state = SpeedState()
is_metric = params.get_bool("IsMetric")
sm = messaging.SubMaster(['gpsLocation', 'carState'])
rk = Ratekeeper(2.0, print_delay_threshold=None)
while True:
sm.update(0)
gps = sm['gpsLocation']
has_gps = sm.valid['gpsLocation'] and gps.hasFix
speed_ms = float(gps.speed) if has_gps else 0.0
cs = sm['carState']
cruise_speed_ms = float(cs.cruiseState.speed)
cruise_active = bool(cs.cruiseState.enabled)
cruise_standstill = bool(cs.cruiseState.standstill)
# CarSpeedLimit is published by hyundai/carstate.py CAN-FD decode (when
# ported). Until then it's 0 / empty and the speed-limit + warning logic
# naturally short-circuits.
try:
speed_limit_ms = float(params_memory.get("CarSpeedLimit") or 0.0)
except (TypeError, ValueError):
speed_limit_ms = 0.0
speed_state.update(speed_ms, has_gps, speed_limit_ms, is_metric,
cruise_speed_ms, cruise_active, cruise_standstill)
rk.keep_time()
if __name__ == "__main__":
main()
-34
View File
@@ -1,34 +0,0 @@
"""
ClearPilot telemetry client library.
Usage from any process:
from openpilot.selfdrive.clearpilot.telemetry import tlog
tlog("canbus", {"speed": 45.2, "speed_limit": 60})
Sends JSON packets over ZMQ PUSH to telemetryd, which diffs and writes CSV.
"""
import json
import time
import zmq
TELEMETRY_SOCK = "ipc:///tmp/clearpilot_telemetry"
_ctx = None
_sock = None
def tlog(group: str, data: dict):
"""Log a telemetry packet. Only changed values will be written to CSV by telemetryd."""
global _ctx, _sock
if _sock is None:
_ctx = zmq.Context.instance()
_sock = _ctx.socket(zmq.PUSH)
_sock.setsockopt(zmq.LINGER, 0)
_sock.setsockopt(zmq.SNDHWM, 100)
_sock.connect(TELEMETRY_SOCK)
msg = json.dumps({"ts": time.time(), "group": group, "data": data})
try:
_sock.send_string(msg, zmq.NOBLOCK)
except zmq.Again:
pass # drop if collector isn't running or queue full
-114
View File
@@ -1,114 +0,0 @@
#!/usr/bin/env python3
"""
ClearPilot telemetry collector.
Receives telemetry packets from any process via ZMQ, diffs against previous
state per group, and writes only changed values to CSV.
Controlled by TelemetryEnabled param — when toggled on, the first packet
from each group writes all values (full dump). When toggled off, stops writing.
CSV format: timestamp,group,key,value
"""
import csv
import json
import os
import shutil
import time
import zmq
from openpilot.common.params import Params
from openpilot.selfdrive.clearpilot.telemetry import TELEMETRY_SOCK
from openpilot.selfdrive.manager.process import _log_dir, session_log
MIN_DISK_FREE_GB = 5
DISK_CHECK_INTERVAL = 10 # seconds
def main():
params = Params()
ctx = zmq.Context.instance()
sock = ctx.socket(zmq.PULL)
sock.setsockopt(zmq.RCVHWM, 1000)
sock.bind(TELEMETRY_SOCK)
csv_path = os.path.join(_log_dir, "telemetry.csv")
state: dict[str, dict] = {} # group -> {key: last_value}
was_enabled = False
writer = None
f = None
last_disk_check = 0
while True:
# Check enable state every iteration (cheap param read)
enabled = params.get("TelemetryEnabled") == b"1"
# Check disk space every 10 seconds while enabled
if enabled and (time.monotonic() - last_disk_check) > DISK_CHECK_INTERVAL:
last_disk_check = time.monotonic()
disk = shutil.disk_usage("/data")
free_gb = disk.free / (1024 ** 3)
if free_gb < MIN_DISK_FREE_GB:
session_log.warning("telemetry disabled: disk free %.1f GB < %d GB", free_gb, MIN_DISK_FREE_GB)
params.put("TelemetryEnabled", "0")
enabled = False
# Toggled on: open CSV, clear state so first packet is a full dump
if enabled and not was_enabled:
f = open(csv_path, "a", newline="")
writer = csv.writer(f)
if os.path.getsize(csv_path) == 0:
writer.writerow(["timestamp", "group", "key", "value"])
state.clear() # force full dump on first packet per group
f.flush()
# Toggled off: close file
if not enabled and was_enabled:
if f:
f.close()
f = None
writer = None
was_enabled = enabled
# Always drain the socket (even when disabled) to avoid backpressure
try:
raw = sock.recv_string(zmq.NOBLOCK)
except zmq.Again:
time.sleep(0.01)
continue
except zmq.ZMQError:
time.sleep(0.01)
continue
if not enabled or writer is None:
continue
try:
pkt = json.loads(raw)
except json.JSONDecodeError:
continue
ts = pkt.get("ts", 0)
group = pkt.get("group", "")
data = pkt.get("data", {})
if group not in state:
state[group] = {}
prev = state[group]
changed = False
for key, value in data.items():
str_val = str(value)
if key not in prev or prev[key] != str_val:
writer.writerow([f"{ts:.6f}", group, key, str_val])
prev[key] = str_val
changed = True
if changed:
f.flush()
if __name__ == "__main__":
main()
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 53 KiB

-12
View File
@@ -1,12 +0,0 @@
#!/usr/bin/env python3
"""Query the UI process for its current widget tree. Usage: python3 -m selfdrive.clearpilot.ui_dump"""
import zmq
ctx = zmq.Context()
sock = ctx.socket(zmq.REQ)
sock.setsockopt(zmq.RCVTIMEO, 2000)
sock.connect("ipc:///tmp/clearpilot_ui_rpc")
sock.send_string("dump")
try:
print(sock.recv_string())
except zmq.Again:
print("ERROR: UI not responding (timeout)")
+19 -17
View File
@@ -37,7 +37,6 @@ from openpilot.system.version import get_short_branch
from openpilot.selfdrive.frogpilot.controls.lib.frogpilot_functions import CRUISING_SPEED, PROBABILITY, MovingAverageCalculator from openpilot.selfdrive.frogpilot.controls.lib.frogpilot_functions import CRUISING_SPEED, PROBABILITY, MovingAverageCalculator
from openpilot.selfdrive.frogpilot.controls.lib.model_manager import RADARLESS_MODELS from openpilot.selfdrive.frogpilot.controls.lib.model_manager import RADARLESS_MODELS
from openpilot.selfdrive.clearpilot.telemetry import tlog
from openpilot.selfdrive.frogpilot.controls.lib.speed_limit_controller import SpeedLimitController from openpilot.selfdrive.frogpilot.controls.lib.speed_limit_controller import SpeedLimitController
SOFT_DISABLE_TIME = 3 # seconds SOFT_DISABLE_TIME = 3 # seconds
@@ -79,6 +78,8 @@ class Controls:
self.params_memory.put_bool("CPTLkasButtonAction", False) self.params_memory.put_bool("CPTLkasButtonAction", False)
self.params_memory.put_int("ScreenDisplayMode", 0) self.params_memory.put_int("ScreenDisplayMode", 0)
# CLEARPILOT: edge tracking for park→drive auto-wake of screen
self.was_driving_gear = False
self.radarless_model = self.params.get("Model", encoding='utf-8') in RADARLESS_MODELS self.radarless_model = self.params.get("Model", encoding='utf-8') in RADARLESS_MODELS
@@ -629,16 +630,6 @@ class Controls:
# Check if openpilot is engaged and actuators are enabled # Check if openpilot is engaged and actuators are enabled
self.enabled = self.state in ENABLED_STATES self.enabled = self.state in ENABLED_STATES
self.active = self.state in ACTIVE_STATES self.active = self.state in ACTIVE_STATES
# CLEARPILOT: log engagement state for debugging cruise desync issues
tlog("engage", {
"state": self.state.name if hasattr(self.state, 'name') else str(self.state),
"enabled": self.enabled,
"active": self.active,
"cruise_enabled": CS.cruiseState.enabled,
"cruise_available": CS.cruiseState.available,
"brakePressed": CS.brakePressed,
})
if self.active: if self.active:
self.current_alert_types.append(ET.WARNING) self.current_alert_types.append(ET.WARNING)
@@ -1246,22 +1237,33 @@ class Controls:
def update_clearpilot_events(self, CS): def update_clearpilot_events(self, CS):
if (len(CS.buttonEvents) > 0): if (len(CS.buttonEvents) > 0):
print (CS.buttonEvents) print (CS.buttonEvents)
if any(be.pressed and be.type == FrogPilotButtonType.lkas for be in CS.buttonEvents):
self.events.add(EventName.clpDebug)
def clearpilot_state_control(self, CC, CS): 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
# edges). Speed/cruise overlay state is owned by speed_logicd, not here.
driving_gear = CS.gearShifter not in (GearShifter.neutral, GearShifter.park,
GearShifter.reverse, GearShifter.unknown)
# Auto-wake screen when shifting into drive from screen-off
if driving_gear and not self.was_driving_gear:
if self.params_memory.get_int("ScreenDisplayMode") == 3:
self.params_memory.put_int("ScreenDisplayMode", 0)
self.was_driving_gear = driving_gear
# LFA/debug button cycles ScreenDisplayMode. Onroad and offroad use
# different transition tables.
if any(be.pressed and be.type == FrogPilotButtonType.lkas for be in CS.buttonEvents): if any(be.pressed and be.type == FrogPilotButtonType.lkas for be in CS.buttonEvents):
current = self.params_memory.get_int("ScreenDisplayMode") current = self.params_memory.get_int("ScreenDisplayMode")
if driving_gear:
if self.driving_gear:
# Onroad: 0→4, 1→2, 2→3, 3→4, 4→2 (never back to auto via button) # Onroad: 0→4, 1→2, 2→3, 3→4, 4→2 (never back to auto via button)
transitions = {0: 4, 1: 2, 2: 3, 3: 4, 4: 2} transitions = {0: 4, 1: 2, 2: 3, 3: 4, 4: 2}
new_mode = transitions.get(current, 0) new_mode = transitions.get(current, 0)
else: else:
# Not in drive: any except 3 → 3, state 3 → 0 # Not in drive: anything except 3 → 3 (screen off), state 3 → 0 (auto)
new_mode = 0 if current == 3 else 3 new_mode = 0 if current == 3 else 3
self.params_memory.put_int("ScreenDisplayMode", new_mode) self.params_memory.put_int("ScreenDisplayMode", new_mode)
return CC return CC
def main(): def main():
-14
View File
@@ -262,16 +262,6 @@ def calibration_incomplete_alert(CP: car.CarParams, CS: car.CarState, sm: messag
AlertStatus.normal, AlertSize.mid, AlertStatus.normal, AlertSize.mid,
Priority.LOWEST, VisualAlert.none, AudibleAlert.none, .2) Priority.LOWEST, VisualAlert.none, AudibleAlert.none, .2)
def clp_debug_notice(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int) -> Alert:
if not hasattr(clp_debug_notice, "counter"):
clp_debug_notice.counter = 0
clp_debug_notice.counter += 1
return Alert(
f"Clearpilot Debug Function Executed ({clp_debug_notice.counter})",
"",
AlertStatus.normal, AlertSize.small,
Priority.LOWER, VisualAlert.none, AudibleAlert.none, 3.0)
def no_gps_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int) -> Alert: def no_gps_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int) -> Alert:
# Clearpilot todo: # Clearpilot todo:
@@ -778,10 +768,6 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
ET.SOFT_DISABLE: soft_disable_alert("Sensor Data Invalid"), ET.SOFT_DISABLE: soft_disable_alert("Sensor Data Invalid"),
}, },
# CLEARPILOT: alert suppressed — event still fires for screen toggle and future actions
EventName.clpDebug: {
},
EventName.noGps: { EventName.noGps: {
ET.PERMANENT: no_gps_alert, ET.PERMANENT: no_gps_alert,
}, },
+259 -372
View File
@@ -35,120 +35,19 @@ int ABGRToNV12(const uint8_t* src_abgr,
int halfwidth = (width + 1) >> 1; int halfwidth = (width + 1) >> 1;
void (*ABGRToUVRow)(const uint8_t* src_abgr0, int src_stride_abgr, void (*ABGRToUVRow)(const uint8_t* src_abgr0, int src_stride_abgr,
uint8_t* dst_u, uint8_t* dst_v, int width) = uint8_t* dst_u, uint8_t* dst_v, int width) =
ABGRToUVRow_C; ABGRToUVRow_NEON;
void (*ABGRToYRow)(const uint8_t* src_abgr, uint8_t* dst_y, int width) = void (*ABGRToYRow)(const uint8_t* src_abgr, uint8_t* dst_y, int width) =
ABGRToYRow_C; ABGRToYRow_NEON;
void (*MergeUVRow_)(const uint8_t* src_u, const uint8_t* src_v, uint8_t* dst_uv, int width) = MergeUVRow_C; void (*MergeUVRow_)(const uint8_t* src_u, const uint8_t* src_v, uint8_t* dst_uv, int width) = MergeUVRow_NEON;
if (!src_abgr || !dst_y || !dst_uv || width <= 0 || height == 0) { if (!src_abgr || !dst_y || !dst_uv || width <= 0 || height == 0) {
return -1; return -1;
} }
if (height < 0) { // Negative height means invert the image. if (height < 0) {
height = -height; height = -height;
src_abgr = src_abgr + (height - 1) * src_stride_abgr; src_abgr = src_abgr + (height - 1) * src_stride_abgr;
src_stride_abgr = -src_stride_abgr; src_stride_abgr = -src_stride_abgr;
} }
#if defined(HAS_ABGRTOYROW_SSSE3) && defined(HAS_ABGRTOUVROW_SSSE3)
if (TestCpuFlag(kCpuHasSSSE3)) {
ABGRToUVRow = ABGRToUVRow_Any_SSSE3;
ABGRToYRow = ABGRToYRow_Any_SSSE3;
if (IS_ALIGNED(width, 16)) {
ABGRToUVRow = ABGRToUVRow_SSSE3;
ABGRToYRow = ABGRToYRow_SSSE3;
}
}
#endif
#if defined(HAS_ABGRTOYROW_AVX2) && defined(HAS_ABGRTOUVROW_AVX2)
if (TestCpuFlag(kCpuHasAVX2)) {
ABGRToUVRow = ABGRToUVRow_Any_AVX2;
ABGRToYRow = ABGRToYRow_Any_AVX2;
if (IS_ALIGNED(width, 32)) {
ABGRToUVRow = ABGRToUVRow_AVX2;
ABGRToYRow = ABGRToYRow_AVX2;
}
}
#endif
#if defined(HAS_ABGRTOYROW_NEON)
if (TestCpuFlag(kCpuHasNEON)) {
ABGRToYRow = ABGRToYRow_Any_NEON;
if (IS_ALIGNED(width, 8)) {
ABGRToYRow = ABGRToYRow_NEON;
}
}
#endif
#if defined(HAS_ABGRTOUVROW_NEON)
if (TestCpuFlag(kCpuHasNEON)) {
ABGRToUVRow = ABGRToUVRow_Any_NEON;
if (IS_ALIGNED(width, 16)) {
ABGRToUVRow = ABGRToUVRow_NEON;
}
}
#endif
#if defined(HAS_ABGRTOYROW_MMI) && defined(HAS_ABGRTOUVROW_MMI)
if (TestCpuFlag(kCpuHasMMI)) {
ABGRToYRow = ABGRToYRow_Any_MMI;
ABGRToUVRow = ABGRToUVRow_Any_MMI;
if (IS_ALIGNED(width, 8)) {
ABGRToYRow = ABGRToYRow_MMI;
}
if (IS_ALIGNED(width, 16)) {
ABGRToUVRow = ABGRToUVRow_MMI;
}
}
#endif
#if defined(HAS_ABGRTOYROW_MSA) && defined(HAS_ABGRTOUVROW_MSA)
if (TestCpuFlag(kCpuHasMSA)) {
ABGRToYRow = ABGRToYRow_Any_MSA;
ABGRToUVRow = ABGRToUVRow_Any_MSA;
if (IS_ALIGNED(width, 16)) {
ABGRToYRow = ABGRToYRow_MSA;
}
if (IS_ALIGNED(width, 32)) {
ABGRToUVRow = ABGRToUVRow_MSA;
}
}
#endif
#if defined(HAS_MERGEUVROW_SSE2)
if (TestCpuFlag(kCpuHasSSE2)) {
MergeUVRow_ = MergeUVRow_Any_SSE2;
if (IS_ALIGNED(halfwidth, 16)) {
MergeUVRow_ = MergeUVRow_SSE2;
}
}
#endif
#if defined(HAS_MERGEUVROW_AVX2)
if (TestCpuFlag(kCpuHasAVX2)) {
MergeUVRow_ = MergeUVRow_Any_AVX2;
if (IS_ALIGNED(halfwidth, 32)) {
MergeUVRow_ = MergeUVRow_AVX2;
}
}
#endif
#if defined(HAS_MERGEUVROW_NEON)
if (TestCpuFlag(kCpuHasNEON)) {
MergeUVRow_ = MergeUVRow_Any_NEON;
if (IS_ALIGNED(halfwidth, 16)) {
MergeUVRow_ = MergeUVRow_NEON;
}
}
#endif
#if defined(HAS_MERGEUVROW_MMI)
if (TestCpuFlag(kCpuHasMMI)) {
MergeUVRow_ = MergeUVRow_Any_MMI;
if (IS_ALIGNED(halfwidth, 8)) {
MergeUVRow_ = MergeUVRow_MMI;
}
}
#endif
#if defined(HAS_MERGEUVROW_MSA)
if (TestCpuFlag(kCpuHasMSA)) {
MergeUVRow_ = MergeUVRow_Any_MSA;
if (IS_ALIGNED(halfwidth, 16)) {
MergeUVRow_ = MergeUVRow_MSA;
}
}
#endif
{ {
// Allocate a rows of uv.
align_buffer_64(row_u, ((halfwidth + 31) & ~31) * 2); align_buffer_64(row_u, ((halfwidth + 31) & ~31) * 2);
uint8_t* row_v = row_u + ((halfwidth + 31) & ~31); uint8_t* row_v = row_u + ((halfwidth + 31) & ~31);
@@ -182,9 +81,9 @@ extern ExitHandler do_exit;
// ***** OMX callback functions ***** // ***** OMX callback functions *****
void OmxEncoder::wait_for_state(OMX_STATETYPE state_) { void OmxEncoder::wait_for_state(OMX_STATETYPE state_) {
std::unique_lock lk(this->state_lock); std::unique_lock lk(state_lock);
while (this->state != state_) { while (state != state_) {
this->state_cv.wait(lk); state_cv.wait(lk);
} }
} }
@@ -236,166 +135,106 @@ static const char* omx_color_fomat_name(uint32_t format) __attribute__((unused))
static const char* omx_color_fomat_name(uint32_t format) { static const char* omx_color_fomat_name(uint32_t format) {
switch (format) { switch (format) {
case OMX_COLOR_FormatUnused: return "OMX_COLOR_FormatUnused"; case OMX_COLOR_FormatUnused: return "OMX_COLOR_FormatUnused";
case OMX_COLOR_FormatMonochrome: return "OMX_COLOR_FormatMonochrome";
case OMX_COLOR_Format8bitRGB332: return "OMX_COLOR_Format8bitRGB332";
case OMX_COLOR_Format12bitRGB444: return "OMX_COLOR_Format12bitRGB444";
case OMX_COLOR_Format16bitARGB4444: return "OMX_COLOR_Format16bitARGB4444";
case OMX_COLOR_Format16bitARGB1555: return "OMX_COLOR_Format16bitARGB1555";
case OMX_COLOR_Format16bitRGB565: return "OMX_COLOR_Format16bitRGB565";
case OMX_COLOR_Format16bitBGR565: return "OMX_COLOR_Format16bitBGR565";
case OMX_COLOR_Format18bitRGB666: return "OMX_COLOR_Format18bitRGB666";
case OMX_COLOR_Format18bitARGB1665: return "OMX_COLOR_Format18bitARGB1665";
case OMX_COLOR_Format19bitARGB1666: return "OMX_COLOR_Format19bitARGB1666";
case OMX_COLOR_Format24bitRGB888: return "OMX_COLOR_Format24bitRGB888";
case OMX_COLOR_Format24bitBGR888: return "OMX_COLOR_Format24bitBGR888";
case OMX_COLOR_Format24bitARGB1887: return "OMX_COLOR_Format24bitARGB1887";
case OMX_COLOR_Format25bitARGB1888: return "OMX_COLOR_Format25bitARGB1888";
case OMX_COLOR_Format32bitBGRA8888: return "OMX_COLOR_Format32bitBGRA8888";
case OMX_COLOR_Format32bitARGB8888: return "OMX_COLOR_Format32bitARGB8888";
case OMX_COLOR_FormatYUV411Planar: return "OMX_COLOR_FormatYUV411Planar";
case OMX_COLOR_FormatYUV411PackedPlanar: return "OMX_COLOR_FormatYUV411PackedPlanar";
case OMX_COLOR_FormatYUV420Planar: return "OMX_COLOR_FormatYUV420Planar";
case OMX_COLOR_FormatYUV420PackedPlanar: return "OMX_COLOR_FormatYUV420PackedPlanar";
case OMX_COLOR_FormatYUV420SemiPlanar: return "OMX_COLOR_FormatYUV420SemiPlanar"; case OMX_COLOR_FormatYUV420SemiPlanar: return "OMX_COLOR_FormatYUV420SemiPlanar";
case OMX_COLOR_FormatYUV422Planar: return "OMX_COLOR_FormatYUV422Planar";
case OMX_COLOR_FormatYUV422PackedPlanar: return "OMX_COLOR_FormatYUV422PackedPlanar";
case OMX_COLOR_FormatYUV422SemiPlanar: return "OMX_COLOR_FormatYUV422SemiPlanar";
case OMX_COLOR_FormatYCbYCr: return "OMX_COLOR_FormatYCbYCr";
case OMX_COLOR_FormatYCrYCb: return "OMX_COLOR_FormatYCrYCb";
case OMX_COLOR_FormatCbYCrY: return "OMX_COLOR_FormatCbYCrY";
case OMX_COLOR_FormatCrYCbY: return "OMX_COLOR_FormatCrYCbY";
case OMX_COLOR_FormatYUV444Interleaved: return "OMX_COLOR_FormatYUV444Interleaved";
case OMX_COLOR_FormatRawBayer8bit: return "OMX_COLOR_FormatRawBayer8bit";
case OMX_COLOR_FormatRawBayer10bit: return "OMX_COLOR_FormatRawBayer10bit";
case OMX_COLOR_FormatRawBayer8bitcompressed: return "OMX_COLOR_FormatRawBayer8bitcompressed";
case OMX_COLOR_FormatL2: return "OMX_COLOR_FormatL2";
case OMX_COLOR_FormatL4: return "OMX_COLOR_FormatL4";
case OMX_COLOR_FormatL8: return "OMX_COLOR_FormatL8";
case OMX_COLOR_FormatL16: return "OMX_COLOR_FormatL16";
case OMX_COLOR_FormatL24: return "OMX_COLOR_FormatL24";
case OMX_COLOR_FormatL32: return "OMX_COLOR_FormatL32";
case OMX_COLOR_FormatYUV420PackedSemiPlanar: return "OMX_COLOR_FormatYUV420PackedSemiPlanar";
case OMX_COLOR_FormatYUV422PackedSemiPlanar: return "OMX_COLOR_FormatYUV422PackedSemiPlanar";
case OMX_COLOR_Format18BitBGR666: return "OMX_COLOR_Format18BitBGR666";
case OMX_COLOR_Format24BitARGB6666: return "OMX_COLOR_Format24BitARGB6666";
case OMX_COLOR_Format24BitABGR6666: return "OMX_COLOR_Format24BitABGR6666";
case OMX_COLOR_FormatAndroidOpaque: return "OMX_COLOR_FormatAndroidOpaque";
case OMX_TI_COLOR_FormatYUV420PackedSemiPlanar: return "OMX_TI_COLOR_FormatYUV420PackedSemiPlanar";
case OMX_QCOM_COLOR_FormatYVU420SemiPlanar: return "OMX_QCOM_COLOR_FormatYVU420SemiPlanar";
case OMX_QCOM_COLOR_FormatYUV420PackedSemiPlanar64x32Tile2m8ka: return "OMX_QCOM_COLOR_FormatYUV420PackedSemiPlanar64x32Tile2m8ka";
case OMX_SEC_COLOR_FormatNV12Tiled: return "OMX_SEC_COLOR_FormatNV12Tiled";
case OMX_QCOM_COLOR_FormatYUV420PackedSemiPlanar32m: return "OMX_QCOM_COLOR_FormatYUV420PackedSemiPlanar32m"; case OMX_QCOM_COLOR_FormatYUV420PackedSemiPlanar32m: return "OMX_QCOM_COLOR_FormatYUV420PackedSemiPlanar32m";
case QOMX_COLOR_FormatYVU420PackedSemiPlanar32m4ka: return "QOMX_COLOR_FormatYVU420PackedSemiPlanar32m4ka";
case QOMX_COLOR_FormatYUV420PackedSemiPlanar16m2ka: return "QOMX_COLOR_FormatYUV420PackedSemiPlanar16m2ka";
case QOMX_COLOR_FORMATYUV420PackedSemiPlanar32mMultiView: return "QOMX_COLOR_FORMATYUV420PackedSemiPlanar32mMultiView";
case QOMX_COLOR_FORMATYUV420PackedSemiPlanar32mCompressed: return "QOMX_COLOR_FORMATYUV420PackedSemiPlanar32mCompressed";
case QOMX_COLOR_Format32bitRGBA8888: return "QOMX_COLOR_Format32bitRGBA8888"; case QOMX_COLOR_Format32bitRGBA8888: return "QOMX_COLOR_Format32bitRGBA8888";
case QOMX_COLOR_Format32bitRGBA8888Compressed: return "QOMX_COLOR_Format32bitRGBA8888Compressed"; default: return "unkn";
default:
return "unkn";
} }
} }
// ***** encoder functions ***** // ***** encoder functions *****
OmxEncoder::OmxEncoder(const char* path, int width, int height, int fps, int bitrate, bool h265, bool downscale) { OmxEncoder::OmxEncoder(const char* path, int width, int height, int fps, int bitrate) {
this->path = path; this->path = path;
this->width = width; this->width = width;
this->height = height; this->height = height;
this->fps = fps; this->fps = fps;
this->remuxing = !h265;
this->downscale = downscale; OMX_ERRORTYPE err = OMX_Init();
if (this->downscale) { if (err != OMX_ErrorNone) {
this->y_ptr2 = (uint8_t *)malloc(this->width*this->height); LOGE("OMX_Init failed: %x", err);
this->u_ptr2 = (uint8_t *)malloc(this->width*this->height/4); return;
this->v_ptr2 = (uint8_t *)malloc(this->width*this->height/4);
} }
auto component = (OMX_STRING)(h265 ? "OMX.qcom.video.encoder.hevc" : "OMX.qcom.video.encoder.avc"); OMX_STRING component = (OMX_STRING)("OMX.qcom.video.encoder.avc");
int err = OMX_GetHandle(&this->handle, component, this, &omx_callbacks); err = OMX_GetHandle(&handle, component, this, &omx_callbacks);
if (err != OMX_ErrorNone) { if (err != OMX_ErrorNone) {
LOGE("error getting codec: %x", err); LOGE("Error getting codec: %x", err);
OMX_Deinit();
return;
} }
// setup input port // setup input port
OMX_PARAM_PORTDEFINITIONTYPE in_port = {0}; OMX_PARAM_PORTDEFINITIONTYPE in_port = {0};
in_port.nSize = sizeof(in_port); in_port.nSize = sizeof(in_port);
in_port.nPortIndex = (OMX_U32) PORT_INDEX_IN; in_port.nPortIndex = (OMX_U32) PORT_INDEX_IN;
OMX_CHECK(OMX_GetParameter(this->handle, OMX_IndexParamPortDefinition, (OMX_PTR) &in_port)); OMX_CHECK(OMX_GetParameter(handle, OMX_IndexParamPortDefinition, (OMX_PTR) &in_port));
in_port.format.video.nFrameWidth = this->width; in_port.format.video.nFrameWidth = width;
in_port.format.video.nFrameHeight = this->height; in_port.format.video.nFrameHeight = height;
in_port.format.video.nStride = VENUS_Y_STRIDE(COLOR_FMT_NV12, this->width); in_port.format.video.nStride = VENUS_Y_STRIDE(COLOR_FMT_NV12, width);
in_port.format.video.nSliceHeight = this->height; in_port.format.video.nSliceHeight = height;
in_port.nBufferSize = VENUS_BUFFER_SIZE(COLOR_FMT_NV12, this->width, this->height); in_port.nBufferSize = VENUS_BUFFER_SIZE(COLOR_FMT_NV12, width, height);
in_port.format.video.xFramerate = (this->fps * 65536); in_port.format.video.xFramerate = (fps * 65536);
in_port.format.video.eCompressionFormat = OMX_VIDEO_CodingUnused; in_port.format.video.eCompressionFormat = OMX_VIDEO_CodingUnused;
in_port.format.video.eColorFormat = (OMX_COLOR_FORMATTYPE)QOMX_COLOR_FORMATYUV420PackedSemiPlanar32m; in_port.format.video.eColorFormat = (OMX_COLOR_FORMATTYPE)QOMX_COLOR_FORMATYUV420PackedSemiPlanar32m;
OMX_CHECK(OMX_SetParameter(this->handle, OMX_IndexParamPortDefinition, (OMX_PTR) &in_port)); OMX_CHECK(OMX_SetParameter(handle, OMX_IndexParamPortDefinition, (OMX_PTR) &in_port));
OMX_CHECK(OMX_GetParameter(this->handle, OMX_IndexParamPortDefinition, (OMX_PTR) &in_port)); OMX_CHECK(OMX_GetParameter(handle, OMX_IndexParamPortDefinition, (OMX_PTR) &in_port));
this->in_buf_headers.resize(in_port.nBufferCountActual); in_buf_headers.resize(in_port.nBufferCountActual);
// setup output port // setup output port
OMX_PARAM_PORTDEFINITIONTYPE out_port;
OMX_PARAM_PORTDEFINITIONTYPE out_port = {0}; memset(&out_port, 0, sizeof(OMX_PARAM_PORTDEFINITIONTYPE));
out_port.nSize = sizeof(out_port); out_port.nSize = sizeof(out_port);
out_port.nVersion.s.nVersionMajor = 1;
out_port.nVersion.s.nVersionMinor = 0;
out_port.nVersion.s.nRevision = 0;
out_port.nVersion.s.nStep = 0;
out_port.nPortIndex = (OMX_U32) PORT_INDEX_OUT; out_port.nPortIndex = (OMX_U32) PORT_INDEX_OUT;
OMX_CHECK(OMX_GetParameter(this->handle, OMX_IndexParamPortDefinition, (OMX_PTR)&out_port));
out_port.format.video.nFrameWidth = this->width; OMX_ERRORTYPE error = OMX_GetParameter(handle, OMX_IndexParamPortDefinition, (OMX_PTR)&out_port);
out_port.format.video.nFrameHeight = this->height; if (error != OMX_ErrorNone) {
LOGE("Error getting output port parameters: 0x%08x", error);
return;
}
out_port.format.video.nFrameWidth = width;
out_port.format.video.nFrameHeight = height;
out_port.format.video.xFramerate = 0; out_port.format.video.xFramerate = 0;
out_port.format.video.nBitrate = bitrate; out_port.format.video.nBitrate = bitrate;
if (h265) {
out_port.format.video.eCompressionFormat = OMX_VIDEO_CodingHEVC;
} else {
out_port.format.video.eCompressionFormat = OMX_VIDEO_CodingAVC; out_port.format.video.eCompressionFormat = OMX_VIDEO_CodingAVC;
}
out_port.format.video.eColorFormat = OMX_COLOR_FormatUnused; out_port.format.video.eColorFormat = OMX_COLOR_FormatUnused;
OMX_CHECK(OMX_SetParameter(this->handle, OMX_IndexParamPortDefinition, (OMX_PTR) &out_port)); error = OMX_SetParameter(handle, OMX_IndexParamPortDefinition, (OMX_PTR) &out_port);
if (error != OMX_ErrorNone) {
LOGE("Error setting output port parameters: 0x%08x", error);
return;
}
OMX_CHECK(OMX_GetParameter(this->handle, OMX_IndexParamPortDefinition, (OMX_PTR) &out_port)); error = OMX_GetParameter(handle, OMX_IndexParamPortDefinition, (OMX_PTR) &out_port);
this->out_buf_headers.resize(out_port.nBufferCountActual); if (error != OMX_ErrorNone) {
LOGE("Error getting updated output port parameters: 0x%08x", error);
return;
}
out_buf_headers.resize(out_port.nBufferCountActual);
OMX_VIDEO_PARAM_BITRATETYPE bitrate_type = {0}; OMX_VIDEO_PARAM_BITRATETYPE bitrate_type = {0};
bitrate_type.nSize = sizeof(bitrate_type); bitrate_type.nSize = sizeof(bitrate_type);
bitrate_type.nPortIndex = (OMX_U32) PORT_INDEX_OUT; bitrate_type.nPortIndex = (OMX_U32) PORT_INDEX_OUT;
OMX_CHECK(OMX_GetParameter(this->handle, OMX_IndexParamVideoBitrate, (OMX_PTR) &bitrate_type)); OMX_CHECK(OMX_GetParameter(handle, OMX_IndexParamVideoBitrate, (OMX_PTR) &bitrate_type));
bitrate_type.eControlRate = OMX_Video_ControlRateVariable; bitrate_type.eControlRate = OMX_Video_ControlRateVariable;
bitrate_type.nTargetBitrate = bitrate; bitrate_type.nTargetBitrate = bitrate;
OMX_CHECK(OMX_SetParameter(this->handle, OMX_IndexParamVideoBitrate, (OMX_PTR) &bitrate_type)); OMX_CHECK(OMX_SetParameter(handle, OMX_IndexParamVideoBitrate, (OMX_PTR) &bitrate_type));
if (h265) {
// setup HEVC
#ifndef QCOM2
OMX_VIDEO_PARAM_HEVCTYPE hevc_type = {0};
OMX_INDEXTYPE index_type = (OMX_INDEXTYPE) OMX_IndexParamVideoHevc;
#else
OMX_VIDEO_PARAM_PROFILELEVELTYPE hevc_type = {0};
OMX_INDEXTYPE index_type = OMX_IndexParamVideoProfileLevelCurrent;
#endif
hevc_type.nSize = sizeof(hevc_type);
hevc_type.nPortIndex = (OMX_U32) PORT_INDEX_OUT;
OMX_CHECK(OMX_GetParameter(this->handle, index_type, (OMX_PTR) &hevc_type));
hevc_type.eProfile = OMX_VIDEO_HEVCProfileMain;
hevc_type.eLevel = OMX_VIDEO_HEVCHighTierLevel5;
OMX_CHECK(OMX_SetParameter(this->handle, index_type, (OMX_PTR) &hevc_type));
} else {
// setup h264 // setup h264
OMX_VIDEO_PARAM_AVCTYPE avc = {0}; OMX_VIDEO_PARAM_AVCTYPE avc = {0};
avc.nSize = sizeof(avc); avc.nSize = sizeof(avc);
avc.nPortIndex = (OMX_U32) PORT_INDEX_OUT; avc.nPortIndex = (OMX_U32) PORT_INDEX_OUT;
OMX_CHECK(OMX_GetParameter(this->handle, OMX_IndexParamVideoAvc, &avc)); OMX_CHECK(OMX_GetParameter(handle, OMX_IndexParamVideoAvc, &avc));
avc.nBFrames = 0; avc.nBFrames = 0;
avc.nPFrames = 15; avc.nPFrames = 15;
@@ -412,70 +251,64 @@ OmxEncoder::OmxEncoder(const char* path, int width, int height, int fps, int bit
avc.bWeightedPPrediction = OMX_TRUE; avc.bWeightedPPrediction = OMX_TRUE;
avc.bconstIpred = OMX_TRUE; avc.bconstIpred = OMX_TRUE;
OMX_CHECK(OMX_SetParameter(this->handle, OMX_IndexParamVideoAvc, &avc)); OMX_CHECK(OMX_SetParameter(handle, OMX_IndexParamVideoAvc, &avc));
OMX_CHECK(OMX_SendCommand(handle, OMX_CommandStateSet, OMX_StateIdle, NULL));
for (OMX_BUFFERHEADERTYPE* &buf : in_buf_headers) {
OMX_CHECK(OMX_AllocateBuffer(handle, &buf, PORT_INDEX_IN, this, in_port.nBufferSize));
} }
OMX_CHECK(OMX_SendCommand(this->handle, OMX_CommandStateSet, OMX_StateIdle, NULL)); for (OMX_BUFFERHEADERTYPE* &buf : out_buf_headers) {
OMX_CHECK(OMX_AllocateBuffer(handle, &buf, PORT_INDEX_OUT, this, out_port.nBufferSize));
for (auto &buf : this->in_buf_headers) {
OMX_CHECK(OMX_AllocateBuffer(this->handle, &buf, PORT_INDEX_IN, this,
in_port.nBufferSize));
}
for (auto &buf : this->out_buf_headers) {
OMX_CHECK(OMX_AllocateBuffer(this->handle, &buf, PORT_INDEX_OUT, this,
out_port.nBufferSize));
} }
wait_for_state(OMX_StateIdle); wait_for_state(OMX_StateIdle);
OMX_CHECK(OMX_SendCommand(this->handle, OMX_CommandStateSet, OMX_StateExecuting, NULL)); OMX_CHECK(OMX_SendCommand(handle, OMX_CommandStateSet, OMX_StateExecuting, NULL));
wait_for_state(OMX_StateExecuting); wait_for_state(OMX_StateExecuting);
// give omx all the output buffers // give omx all the output buffers
for (auto &buf : this->out_buf_headers) { for (OMX_BUFFERHEADERTYPE* &buf : out_buf_headers) {
OMX_CHECK(OMX_FillThisBuffer(this->handle, buf)); OMX_CHECK(OMX_FillThisBuffer(handle, buf));
} }
// fill the input free queue // fill the input free queue
for (auto &buf : this->in_buf_headers) { for (OMX_BUFFERHEADERTYPE* &buf : in_buf_headers) {
this->free_in.push(buf); free_in.push(buf);
} }
} }
void OmxEncoder::handle_out_buf(OmxEncoder *e, OMX_BUFFERHEADERTYPE *out_buf) { void OmxEncoder::handle_out_buf(OmxEncoder *encoder, OMX_BUFFERHEADERTYPE *out_buf) {
int err; int err;
uint8_t *buf_data = out_buf->pBuffer + out_buf->nOffset; uint8_t *buf_data = out_buf->pBuffer + out_buf->nOffset;
if (out_buf->nFlags & OMX_BUFFERFLAG_CODECCONFIG) { if (out_buf->nFlags & OMX_BUFFERFLAG_CODECCONFIG) {
if (e->codec_config_len < out_buf->nFilledLen) { if (encoder->codec_config_len < out_buf->nFilledLen) {
e->codec_config = (uint8_t *)realloc(e->codec_config, out_buf->nFilledLen); encoder->codec_config = (uint8_t *)realloc(encoder->codec_config, out_buf->nFilledLen);
} }
e->codec_config_len = out_buf->nFilledLen; encoder->codec_config_len = out_buf->nFilledLen;
memcpy(e->codec_config, buf_data, out_buf->nFilledLen); memcpy(encoder->codec_config, buf_data, out_buf->nFilledLen);
#ifdef QCOM2 #ifdef QCOM2
out_buf->nTimeStamp = 0; out_buf->nTimeStamp = 0;
#endif #endif
} }
if (e->of) { if (encoder->of) {
fwrite(buf_data, out_buf->nFilledLen, 1, e->of); fwrite(buf_data, out_buf->nFilledLen, 1, encoder->of);
} }
if (e->remuxing) { if (!encoder->wrote_codec_config && encoder->codec_config_len > 0) {
if (!e->wrote_codec_config && e->codec_config_len > 0) {
// extradata will be freed by av_free() in avcodec_free_context() // extradata will be freed by av_free() in avcodec_free_context()
e->codec_ctx->extradata = (uint8_t*)av_mallocz(e->codec_config_len + AV_INPUT_BUFFER_PADDING_SIZE); encoder->out_stream->codecpar->extradata = (uint8_t*)av_mallocz(encoder->codec_config_len + AV_INPUT_BUFFER_PADDING_SIZE);
e->codec_ctx->extradata_size = e->codec_config_len; encoder->out_stream->codecpar->extradata_size = encoder->codec_config_len;
memcpy(e->codec_ctx->extradata, e->codec_config, e->codec_config_len); memcpy(encoder->out_stream->codecpar->extradata, encoder->codec_config, encoder->codec_config_len);
err = avcodec_parameters_from_context(e->out_stream->codecpar, e->codec_ctx); err = avformat_write_header(encoder->ofmt_ctx, NULL);
assert(err >= 0);
err = avformat_write_header(e->ofmt_ctx, NULL);
assert(err >= 0); assert(err >= 0);
e->wrote_codec_config = true; encoder->wrote_codec_config = true;
} }
if (out_buf->nTimeStamp > 0) { if (out_buf->nTimeStamp > 0) {
@@ -488,18 +321,17 @@ void OmxEncoder::handle_out_buf(OmxEncoder *e, OMX_BUFFERHEADERTYPE *out_buf) {
pkt.size = out_buf->nFilledLen; pkt.size = out_buf->nFilledLen;
enum AVRounding rnd = static_cast<enum AVRounding>(AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX); enum AVRounding rnd = static_cast<enum AVRounding>(AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX);
pkt.pts = pkt.dts = av_rescale_q_rnd(out_buf->nTimeStamp, in_timebase, e->ofmt_ctx->streams[0]->time_base, rnd); pkt.pts = pkt.dts = av_rescale_q_rnd(out_buf->nTimeStamp, in_timebase, encoder->out_stream->time_base, rnd);
pkt.duration = av_rescale_q(50*1000, in_timebase, e->ofmt_ctx->streams[0]->time_base); pkt.duration = av_rescale_q(1, AVRational{1, encoder->fps}, encoder->out_stream->time_base);
if (out_buf->nFlags & OMX_BUFFERFLAG_SYNCFRAME) { if (out_buf->nFlags & OMX_BUFFERFLAG_SYNCFRAME) {
pkt.flags |= AV_PKT_FLAG_KEY; pkt.flags |= AV_PKT_FLAG_KEY;
} }
err = av_write_frame(e->ofmt_ctx, &pkt); err = av_write_frame(encoder->ofmt_ctx, &pkt);
if (err < 0) { LOGW("ts encoder write issue"); } if (err < 0) { LOGW("ts encoder write issue"); }
av_free_packet(&pkt); av_packet_unref(&pkt);
}
} }
// give omx back the buffer // give omx back the buffer
@@ -508,59 +340,53 @@ void OmxEncoder::handle_out_buf(OmxEncoder *e, OMX_BUFFERHEADERTYPE *out_buf) {
out_buf->nTimeStamp = 0; out_buf->nTimeStamp = 0;
} }
#endif #endif
OMX_CHECK(OMX_FillThisBuffer(e->handle, out_buf)); OMX_CHECK(OMX_FillThisBuffer(encoder->handle, out_buf));
} }
int OmxEncoder::encode_frame_rgba(const uint8_t *ptr, int in_width, int in_height, uint64_t ts) { int OmxEncoder::encode_frame_rgba(const uint8_t *ptr, int in_width, int in_height, uint64_t ts) {
int err; if (!is_open) {
if (!this->is_open) {
return -1; return -1;
} }
// this sometimes freezes... put it outside the encoder lock so we can still trigger rotates...
// THIS IS A REALLY BAD IDEA, but apparently the race has to happen 30 times to trigger this
OMX_BUFFERHEADERTYPE* in_buf = nullptr; OMX_BUFFERHEADERTYPE* in_buf = nullptr;
while (!this->free_in.try_pop(in_buf, 20)) { while (!free_in.try_pop(in_buf, 20)) {
if (do_exit) { if (do_exit) {
return -1; return -1;
} }
} }
int ret = this->counter; int ret = counter;
uint8_t *in_buf_ptr = in_buf->pBuffer; uint8_t *in_buf_ptr = in_buf->pBuffer;
uint8_t *in_y_ptr = in_buf_ptr; uint8_t *in_y_ptr = in_buf_ptr;
int in_y_stride = VENUS_Y_STRIDE(COLOR_FMT_NV12, this->width); int in_y_stride = VENUS_Y_STRIDE(COLOR_FMT_NV12, width);
int in_uv_stride = VENUS_UV_STRIDE(COLOR_FMT_NV12, this->width); int in_uv_stride = VENUS_UV_STRIDE(COLOR_FMT_NV12, width);
uint8_t *in_uv_ptr = in_buf_ptr + (in_y_stride * VENUS_Y_SCANLINES(COLOR_FMT_NV12, this->height)); uint8_t *in_uv_ptr = in_buf_ptr + (in_y_stride * VENUS_Y_SCANLINES(COLOR_FMT_NV12, height));
err = ABGRToNV12(ptr, this->width*4, int err = ABGRToNV12(ptr, width * 4, in_y_ptr, in_y_stride, in_uv_ptr, in_uv_stride, width, height);
in_y_ptr, in_y_stride,
in_uv_ptr, in_uv_stride,
this->width, this->height);
assert(err == 0); assert(err == 0);
in_buf->nFilledLen = VENUS_BUFFER_SIZE(COLOR_FMT_NV12, this->width, this->height); in_buf->nFilledLen = VENUS_BUFFER_SIZE(COLOR_FMT_NV12, width, height);
in_buf->nFlags = OMX_BUFFERFLAG_ENDOFFRAME; in_buf->nFlags = OMX_BUFFERFLAG_ENDOFFRAME;
in_buf->nOffset = 0; in_buf->nOffset = 0;
in_buf->nTimeStamp = ts / 1000LL; // OMX_TICKS, in microseconds in_buf->nTimeStamp = ts / 1000LL; // OMX_TICKS, in microseconds
this->last_t = in_buf->nTimeStamp; last_t = in_buf->nTimeStamp;
OMX_CHECK(OMX_EmptyThisBuffer(this->handle, in_buf)); OMX_CHECK(OMX_EmptyThisBuffer(handle, in_buf));
// pump output // pump output
while (true) { while (true) {
OMX_BUFFERHEADERTYPE *out_buf; OMX_BUFFERHEADERTYPE *out_buf;
if (!this->done_out.try_pop(out_buf)) { if (!done_out.try_pop(out_buf)) {
break; break;
} }
handle_out_buf(this, out_buf); handle_out_buf(this, out_buf);
} }
this->dirty = true; dirty = true;
this->counter++; counter++;
return ret; return ret;
} }
@@ -568,24 +394,24 @@ int OmxEncoder::encode_frame_rgba(const uint8_t *ptr, int in_width, int in_heigh
// CLEARPILOT: encode raw NV12 frames directly (no RGBA conversion needed) // CLEARPILOT: encode raw NV12 frames directly (no RGBA conversion needed)
int OmxEncoder::encode_frame_nv12(const uint8_t *y_ptr, int y_stride, const uint8_t *uv_ptr, int uv_stride, int OmxEncoder::encode_frame_nv12(const uint8_t *y_ptr, int y_stride, const uint8_t *uv_ptr, int uv_stride,
int in_width, int in_height, uint64_t ts) { int in_width, int in_height, uint64_t ts) {
if (!this->is_open) { if (!is_open) {
return -1; return -1;
} }
OMX_BUFFERHEADERTYPE* in_buf = nullptr; OMX_BUFFERHEADERTYPE* in_buf = nullptr;
while (!this->free_in.try_pop(in_buf, 20)) { while (!free_in.try_pop(in_buf, 20)) {
if (do_exit) { if (do_exit) {
return -1; return -1;
} }
} }
int ret = this->counter; int ret = counter;
uint8_t *in_buf_ptr = in_buf->pBuffer; uint8_t *in_buf_ptr = in_buf->pBuffer;
int venus_y_stride = VENUS_Y_STRIDE(COLOR_FMT_NV12, this->width); int venus_y_stride = VENUS_Y_STRIDE(COLOR_FMT_NV12, width);
int venus_uv_stride = VENUS_UV_STRIDE(COLOR_FMT_NV12, this->width); int venus_uv_stride = VENUS_UV_STRIDE(COLOR_FMT_NV12, width);
uint8_t *dst_y = in_buf_ptr; uint8_t *dst_y = in_buf_ptr;
uint8_t *dst_uv = in_buf_ptr + (venus_y_stride * VENUS_Y_SCANLINES(COLOR_FMT_NV12, this->height)); uint8_t *dst_uv = in_buf_ptr + (venus_y_stride * VENUS_Y_SCANLINES(COLOR_FMT_NV12, height));
// Copy Y plane row by row (source stride may differ from VENUS stride) // Copy Y plane row by row (source stride may differ from VENUS stride)
for (int row = 0; row < in_height; row++) { for (int row = 0; row < in_height; row++) {
@@ -597,99 +423,103 @@ int OmxEncoder::encode_frame_nv12(const uint8_t *y_ptr, int y_stride, const uint
memcpy(dst_uv + row * venus_uv_stride, uv_ptr + row * uv_stride, in_width); memcpy(dst_uv + row * venus_uv_stride, uv_ptr + row * uv_stride, in_width);
} }
in_buf->nFilledLen = VENUS_BUFFER_SIZE(COLOR_FMT_NV12, this->width, this->height); in_buf->nFilledLen = VENUS_BUFFER_SIZE(COLOR_FMT_NV12, width, height);
in_buf->nFlags = OMX_BUFFERFLAG_ENDOFFRAME; in_buf->nFlags = OMX_BUFFERFLAG_ENDOFFRAME;
in_buf->nOffset = 0; in_buf->nOffset = 0;
in_buf->nTimeStamp = ts / 1000LL; in_buf->nTimeStamp = ts / 1000LL;
this->last_t = in_buf->nTimeStamp; last_t = in_buf->nTimeStamp;
OMX_CHECK(OMX_EmptyThisBuffer(this->handle, in_buf)); OMX_CHECK(OMX_EmptyThisBuffer(handle, in_buf));
while (true) { while (true) {
OMX_BUFFERHEADERTYPE *out_buf; OMX_BUFFERHEADERTYPE *out_buf;
if (!this->done_out.try_pop(out_buf)) { if (!done_out.try_pop(out_buf)) {
break; break;
} }
handle_out_buf(this, out_buf); handle_out_buf(this, out_buf);
} }
this->dirty = true; dirty = true;
this->counter++; counter++;
return ret; return ret;
} }
void OmxEncoder::encoder_open(const char* filename) { void OmxEncoder::encoder_open(const char* filename) {
int err; if (!filename || strlen(filename) == 0) {
return;
}
if (strlen(filename) + path.size() + 2 > sizeof(vid_path)) {
return;
}
struct stat st = {0}; struct stat st = {0};
if (stat(this->path.c_str(), &st) == -1) { if (stat(path.c_str(), &st) == -1) {
mkdir(this->path.c_str(), 0755); if (mkdir(path.c_str(), 0755) == -1) {
return;
}
} }
snprintf(this->vid_path, sizeof(this->vid_path), "%s/%s", this->path.c_str(), filename); snprintf(vid_path, sizeof(vid_path), "%s/%s", path.c_str(), filename);
printf("encoder_open %s remuxing:%d\n", this->vid_path, this->remuxing);
if (this->remuxing) { if (avformat_alloc_output_context2(&ofmt_ctx, NULL, NULL, vid_path) < 0 || !ofmt_ctx) {
avformat_alloc_output_context2(&this->ofmt_ctx, NULL, NULL, this->vid_path); return;
assert(this->ofmt_ctx);
this->out_stream = avformat_new_stream(this->ofmt_ctx, NULL);
assert(this->out_stream);
// set codec correctly
av_register_all();
AVCodec *codec = NULL;
codec = avcodec_find_encoder(AV_CODEC_ID_H264);
assert(codec);
this->codec_ctx = avcodec_alloc_context3(codec);
assert(this->codec_ctx);
this->codec_ctx->width = this->width;
this->codec_ctx->height = this->height;
this->codec_ctx->pix_fmt = AV_PIX_FMT_YUV420P;
this->codec_ctx->time_base = (AVRational){ 1, this->fps };
err = avio_open(&this->ofmt_ctx->pb, this->vid_path, AVIO_FLAG_WRITE);
assert(err >= 0);
this->wrote_codec_config = false;
} else {
this->of = fopen(this->vid_path, "wb");
assert(this->of);
#ifndef QCOM2
if (this->codec_config_len > 0) {
fwrite(this->codec_config, this->codec_config_len, 1, this->of);
}
#endif
} }
// create camera lock file out_stream = avformat_new_stream(ofmt_ctx, NULL);
snprintf(this->lock_path, sizeof(this->lock_path), "%s/%s.lock", this->path.c_str(), filename); if (!out_stream) {
int lock_fd = HANDLE_EINTR(open(this->lock_path, O_RDWR | O_CREAT, 0664)); avformat_free_context(ofmt_ctx);
assert(lock_fd >= 0); ofmt_ctx = nullptr;
return;
}
out_stream->time_base = AVRational{1, fps};
out_stream->codecpar->codec_id = AV_CODEC_ID_H264;
out_stream->codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
out_stream->codecpar->width = width;
out_stream->codecpar->height = height;
int err = avio_open(&ofmt_ctx->pb, vid_path, AVIO_FLAG_WRITE);
if (err < 0) {
avformat_free_context(ofmt_ctx);
ofmt_ctx = nullptr;
return;
}
wrote_codec_config = false;
snprintf(lock_path, sizeof(lock_path), "%s/%s.lock", path.c_str(), filename);
int lock_fd = HANDLE_EINTR(open(lock_path, O_RDWR | O_CREAT, 0664));
if (lock_fd < 0) {
avio_closep(&ofmt_ctx->pb);
avformat_free_context(ofmt_ctx);
ofmt_ctx = nullptr;
return;
}
close(lock_fd); close(lock_fd);
this->is_open = true; is_open = true;
this->counter = 0; counter = 0;
} }
void OmxEncoder::encoder_close() { void OmxEncoder::encoder_close() {
if (this->is_open) { if (!is_open) return;
if (this->dirty) {
// drain output only if there could be frames in the encoder
OMX_BUFFERHEADERTYPE* in_buf = this->free_in.pop(); if (dirty) {
OMX_BUFFERHEADERTYPE* in_buf = free_in.pop();
if (in_buf) {
in_buf->nFilledLen = 0; in_buf->nFilledLen = 0;
in_buf->nOffset = 0; in_buf->nOffset = 0;
in_buf->nFlags = OMX_BUFFERFLAG_EOS; in_buf->nFlags = OMX_BUFFERFLAG_EOS;
in_buf->nTimeStamp = this->last_t + 1000000LL/this->fps; in_buf->nTimeStamp = last_t + 1000000LL / fps;
OMX_CHECK(OMX_EmptyThisBuffer(this->handle, in_buf)); OMX_CHECK(OMX_EmptyThisBuffer(handle, in_buf));
while (true) { while (true) {
OMX_BUFFERHEADERTYPE *out_buf = this->done_out.pop(); OMX_BUFFERHEADERTYPE *out_buf = done_out.pop();
if (!out_buf) break;
handle_out_buf(this, out_buf); handle_out_buf(this, out_buf);
@@ -697,55 +527,112 @@ void OmxEncoder::encoder_close() {
break; break;
} }
} }
this->dirty = false; }
dirty = false;
} }
if (this->remuxing) { if (out_stream) {
av_write_trailer(this->ofmt_ctx); out_stream->nb_frames = counter;
avcodec_free_context(&this->codec_ctx); out_stream->duration = av_rescale_q(counter, AVRational{1, fps}, out_stream->time_base);
avio_closep(&this->ofmt_ctx->pb); }
avformat_free_context(this->ofmt_ctx);
} else { if (ofmt_ctx) {
fclose(this->of); av_write_trailer(ofmt_ctx);
this->of = nullptr; ofmt_ctx->duration = out_stream ? out_stream->duration : 0;
avio_closep(&ofmt_ctx->pb);
avformat_free_context(ofmt_ctx);
ofmt_ctx = nullptr;
out_stream = nullptr;
}
if (lock_path[0] != '\0') {
unlink(lock_path);
}
is_open = false;
// Remux with faststart for streaming/seeking support
if (strlen(vid_path) > 0) {
char fixed_path[1024];
snprintf(fixed_path, sizeof(fixed_path), "%s.fixed.mp4", vid_path);
char cmd[2048];
snprintf(cmd, sizeof(cmd), "ffmpeg -y -i \"%s\" -c copy -movflags +faststart \"%s\" && mv \"%s\" \"%s\"",
vid_path, fixed_path, fixed_path, vid_path);
int ret = system(cmd);
if (ret != 0) {
LOGW("ffmpeg faststart remux failed with exit code %d", ret);
} }
unlink(this->lock_path);
} }
this->is_open = false;
} }
OmxEncoder::~OmxEncoder() { OmxEncoder::~OmxEncoder() {
assert(!this->is_open); if (is_open) {
LOGE("OmxEncoder closed with is_open=true, calling encoder_close()");
OMX_CHECK(OMX_SendCommand(this->handle, OMX_CommandStateSet, OMX_StateIdle, NULL)); encoder_close();
wait_for_state(OMX_StateIdle);
OMX_CHECK(OMX_SendCommand(this->handle, OMX_CommandStateSet, OMX_StateLoaded, NULL));
for (auto &buf : this->in_buf_headers) {
OMX_CHECK(OMX_FreeBuffer(this->handle, PORT_INDEX_IN, buf));
} }
for (auto &buf : this->out_buf_headers) { if (!handle) {
OMX_CHECK(OMX_FreeBuffer(this->handle, PORT_INDEX_OUT, buf)); LOGE("OMX handle is null in destructor, skipping teardown.");
return;
}
OMX_ERRORTYPE err;
err = OMX_SendCommand(handle, OMX_CommandStateSet, OMX_StateIdle, NULL);
if (err != OMX_ErrorNone) {
LOGE("Failed to set OMX state to Idle: %x", err);
} else {
wait_for_state(OMX_StateIdle);
}
err = OMX_SendCommand(handle, OMX_CommandStateSet, OMX_StateLoaded, NULL);
if (err != OMX_ErrorNone) {
LOGE("Failed to set OMX state to Loaded: %x", err);
}
for (OMX_BUFFERHEADERTYPE *buf : in_buf_headers) {
if (buf) {
err = OMX_FreeBuffer(handle, PORT_INDEX_IN, buf);
if (err != OMX_ErrorNone) {
LOGE("Failed to free input buffer: %x", err);
}
}
}
for (OMX_BUFFERHEADERTYPE *buf : out_buf_headers) {
if (buf) {
err = OMX_FreeBuffer(handle, PORT_INDEX_OUT, buf);
if (err != OMX_ErrorNone) {
LOGE("Failed to free output buffer: %x", err);
}
}
} }
wait_for_state(OMX_StateLoaded); wait_for_state(OMX_StateLoaded);
OMX_CHECK(OMX_FreeHandle(this->handle)); err = OMX_FreeHandle(handle);
if (err != OMX_ErrorNone) {
LOGE("Failed to free OMX handle: %x", err);
}
handle = nullptr;
err = OMX_Deinit();
if (err != OMX_ErrorNone) {
LOGE("OMX_Deinit failed: %x", err);
}
OMX_BUFFERHEADERTYPE *out_buf; OMX_BUFFERHEADERTYPE *out_buf;
while (this->free_in.try_pop(out_buf)); while (free_in.try_pop(out_buf));
while (this->done_out.try_pop(out_buf)); while (done_out.try_pop(out_buf));
if (this->codec_config) { if (codec_config) {
free(this->codec_config); free(codec_config);
codec_config = nullptr;
} }
if (this->downscale) { in_buf_headers.clear();
free(this->y_ptr2); out_buf_headers.clear();
free(this->u_ptr2);
free(this->v_ptr2);
}
} }
@@ -12,10 +12,10 @@ extern "C" {
#include "common/queue.h" #include "common/queue.h"
// OmxEncoder, lossey codec using hardware hevc // OmxEncoder, lossey codec using hardware H.264
class OmxEncoder { class OmxEncoder {
public: public:
OmxEncoder(const char* path, int width, int height, int fps, int bitrate, bool h265, bool downscale); OmxEncoder(const char* path, int width, int height, int fps, int bitrate);
~OmxEncoder(); ~OmxEncoder();
int encode_frame_rgba(const uint8_t *ptr, int in_width, int in_height, uint64_t ts); int encode_frame_rgba(const uint8_t *ptr, int in_width, int in_height, uint64_t ts);
@@ -44,31 +44,26 @@ private:
int counter = 0; int counter = 0;
std::string path; std::string path;
FILE *of; FILE *of = nullptr;
size_t codec_config_len; size_t codec_config_len = 0;
uint8_t *codec_config = NULL; uint8_t *codec_config = nullptr;
bool wrote_codec_config; bool wrote_codec_config = false;
std::mutex state_lock; std::mutex state_lock;
std::condition_variable state_cv; std::condition_variable state_cv;
OMX_STATETYPE state = OMX_StateLoaded; OMX_STATETYPE state = OMX_StateLoaded;
OMX_HANDLETYPE handle; OMX_HANDLETYPE handle = nullptr;
std::vector<OMX_BUFFERHEADERTYPE *> in_buf_headers; std::vector<OMX_BUFFERHEADERTYPE *> in_buf_headers;
std::vector<OMX_BUFFERHEADERTYPE *> out_buf_headers; std::vector<OMX_BUFFERHEADERTYPE *> out_buf_headers;
uint64_t last_t; uint64_t last_t = 0;
SafeQueue<OMX_BUFFERHEADERTYPE *> free_in; SafeQueue<OMX_BUFFERHEADERTYPE *> free_in;
SafeQueue<OMX_BUFFERHEADERTYPE *> done_out; SafeQueue<OMX_BUFFERHEADERTYPE *> done_out;
AVFormatContext *ofmt_ctx; AVFormatContext *ofmt_ctx = nullptr;
AVCodecContext *codec_ctx; AVStream *out_stream = nullptr;
AVStream *out_stream;
bool remuxing;
bool downscale;
uint8_t *y_ptr2, *u_ptr2, *v_ptr2;
}; };
@@ -132,20 +132,13 @@ void ScreenRecorder::stop() {
} }
void ScreenRecorder::update_screen() { void ScreenRecorder::update_screen() {
bool car_on = uiState()->scene.started || uiState()->scene.screen_recorder_debug; if (!uiState()->scene.started) {
if (!car_on) {
if (recording) { if (recording) {
stop(); stop();
} }
return; return;
} }
if (!recording) return;
// CLEARPILOT: auto-start recording when car is on (or debug flag set)
if (!recording) {
start();
return;
}
if (milliseconds() - started > 1000 * 60 * 3) { if (milliseconds() - started > 1000 * 60 * 3) {
stop(); stop();
+25 -24
View File
@@ -594,10 +594,15 @@ void Localizer::handle_msg(const cereal::Event::Reader& log) {
this->handle_sensor(t, log.getAccelerometer()); this->handle_sensor(t, log.getAccelerometer());
} else if (log.isGyroscope()) { } else if (log.isGyroscope()) {
this->handle_sensor(t, log.getGyroscope()); this->handle_sensor(t, log.getGyroscope());
} else if (log.isGpsLocation()) { // CLEARPILOT: GPS branches removed — locationd no longer subscribes to
this->handle_gps(t, log.getGpsLocation(), GPS_QUECTEL_SENSOR_TIME_OFFSET); // gpsLocation/gpsLocationExternal, so these would be dead code regardless.
} else if (log.isGpsLocationExternal()) { // Self-driving treats GPS as not present: gpsOK stays false (last_gps_msg
this->handle_gps(t, log.getGpsLocationExternal(), GPS_UBLOX_SENSOR_TIME_OFFSET); // never updates), and other liveLocationKalman fields stay at the kalman
// filter's IMU+camera-odometry-only state.
// } else if (log.isGpsLocation()) {
// this->handle_gps(t, log.getGpsLocation(), GPS_QUECTEL_SENSOR_TIME_OFFSET);
// } else if (log.isGpsLocationExternal()) {
// this->handle_gps(t, log.getGpsLocationExternal(), GPS_UBLOX_SENSOR_TIME_OFFSET);
//} else if (log.isGnssMeasurements()) { //} else if (log.isGnssMeasurements()) {
// this->handle_gnss(t, log.getGnssMeasurements()); // this->handle_gnss(t, log.getGnssMeasurements());
} else if (log.isCarState()) { } else if (log.isCarState()) {
@@ -676,22 +681,16 @@ void Localizer::configure_gnss_source(const LocalizerGnssSource &source) {
} }
int Localizer::locationd_thread() { int Localizer::locationd_thread() {
Params params; // CLEARPILOT: do not subscribe to GPS. Our gpsd publishes gpsLocation for
LocalizerGnssSource source; // UI/clock/dashcam, but feeding it to the kalman filter screws up the
const char* gps_location_socket; // self-driving math. liveLocationKalman.gpsOK stays false; downstream
if (params.getBool("UbloxAvailable")) { // self-driving consumers (controlsd, paramsd, torqued, frogpilot_planner)
source = LocalizerGnssSource::UBLOX; // already handle that case.
gps_location_socket = "gpsLocationExternal"; this->configure_gnss_source(LocalizerGnssSource::QCOM);
} else { const std::initializer_list<const char *> service_list = {"cameraOdometry", "liveCalibration",
source = LocalizerGnssSource::QCOM;
gps_location_socket = "gpsLocation";
}
this->configure_gnss_source(source);
const std::initializer_list<const char *> service_list = {gps_location_socket, "cameraOdometry", "liveCalibration",
"carState", "accelerometer", "gyroscope"}; "carState", "accelerometer", "gyroscope"};
SubMaster sm(service_list, {}, nullptr, {gps_location_socket}); SubMaster sm(service_list, {}, nullptr, {});
PubMaster pm({"liveLocationKalman"}); PubMaster pm({"liveLocationKalman"});
uint64_t cnt = 0; uint64_t cnt = 0;
@@ -730,12 +729,14 @@ int Localizer::locationd_thread() {
kj::ArrayPtr<capnp::byte> bytes = this->get_message_bytes(msg_builder, inputsOK, sensorsOK, gpsOK, filterInitialized); kj::ArrayPtr<capnp::byte> bytes = this->get_message_bytes(msg_builder, inputsOK, sensorsOK, gpsOK, filterInitialized);
pm.send("liveLocationKalman", bytes.begin(), bytes.size()); pm.send("liveLocationKalman", bytes.begin(), bytes.size());
if (cnt % 1200 == 0 && gpsOK) { // once a minute // CLEARPILOT: dead code now that gpsOK is permanently false (we don't
VectorXd posGeo = this->get_position_geodetic(); // subscribe to gpsLocation). Was: write LastGPSPosition once a minute.
std::string lastGPSPosJSON = util::string_format( // if (cnt % 1200 == 0 && gpsOK) {
"{\"latitude\": %.15f, \"longitude\": %.15f, \"altitude\": %.15f}", posGeo(0), posGeo(1), posGeo(2)); // VectorXd posGeo = this->get_position_geodetic();
params.putNonBlocking("LastGPSPosition", lastGPSPosJSON); // std::string lastGPSPosJSON = util::string_format(
} // "{\"latitude\": %.15f, \"longitude\": %.15f, \"altitude\": %.15f}", posGeo(0), posGeo(1), posGeo(2));
// params.putNonBlocking("LastGPSPosition", lastGPSPosJSON);
// }
cnt++; cnt++;
} }
} }
+22 -5
View File
@@ -56,12 +56,19 @@ def build(spinner: Spinner, dirty: bool = False, minimal: bool = False) -> None:
if scons.returncode == 0: if scons.returncode == 0:
Path('/data/openpilot/prebuilt').touch() Path('/data/openpilot/prebuilt').touch()
# CLEARPILOT: update prebuilt spinner if the new build is newer # CLEARPILOT: update prebuilt spinner if the new build is newer.
# Write to a temp path then os.replace so we can swap a binary that's
# currently executing (the in-process spinner holds the old one open).
new_spinner = Path(BASEDIR) / "selfdrive/ui/_spinner" new_spinner = Path(BASEDIR) / "selfdrive/ui/_spinner"
old_spinner = Path(BASEDIR) / "selfdrive/ui/qt/spinner" old_spinner = Path(BASEDIR) / "selfdrive/ui/qt/spinner"
if new_spinner.exists() and (not old_spinner.exists() or new_spinner.stat().st_mtime > old_spinner.stat().st_mtime): if new_spinner.exists() and (not old_spinner.exists() or new_spinner.stat().st_mtime > old_spinner.stat().st_mtime):
import shutil import shutil
shutil.copy2(str(new_spinner), str(old_spinner)) try:
tmp_spinner = old_spinner.with_name(old_spinner.name + ".new")
shutil.copy2(str(new_spinner), str(tmp_spinner))
os.replace(str(tmp_spinner), str(old_spinner))
except OSError as e:
print(f"CLP failed to update prebuilt spinner: {e}", file=sys.stderr)
break break
@@ -78,13 +85,23 @@ def build(spinner: Spinner, dirty: bool = False, minimal: bool = False) -> None:
# Show TextWindow # Show TextWindow
spinner.close() spinner.close()
if not os.getenv("CI"): if not os.getenv("CI"):
# CLEARPILOT: BUILD_ONLY mode shows error on screen but doesn't block msg = "openpilot failed to build\n \n" + error_s
t = TextWindow("openpilot failed to build\n \n" + error_s)
if os.getenv("BUILD_ONLY"): if os.getenv("BUILD_ONLY"):
# CLEARPILOT: BUILD_ONLY mode — spawn the text window fully detached
# (own session, /dev/null stdio) so it stays on screen after this
# script exits and doesn't hold our stdout/stderr pipes open.
print(error_s, file=sys.stderr) print(error_s, file=sys.stderr)
devnull = open(os.devnull, 'r+b')
subprocess.Popen(
["./text", msg],
cwd=os.path.join(BASEDIR, "selfdrive", "ui"),
stdin=devnull, stdout=devnull, stderr=devnull,
start_new_session=True,
close_fds=True,
)
else: else:
with TextWindow(msg) as t:
t.wait_for_exit() t.wait_for_exit()
t.close()
exit(1) exit(1)
# enforce max cache size # enforce max cache size
+29 -47
View File
@@ -16,7 +16,7 @@ from openpilot.common.text_window import TextWindow
from openpilot.common.time import system_time_valid from openpilot.common.time import system_time_valid
from openpilot.system.hardware import HARDWARE, PC from openpilot.system.hardware import HARDWARE, PC
from openpilot.selfdrive.manager.helpers import unblock_stdout, write_onroad_params, save_bootlog from openpilot.selfdrive.manager.helpers import unblock_stdout, write_onroad_params, save_bootlog
from openpilot.selfdrive.manager.process import ensure_running, init_log_dir, update_log_dir_timestamp, session_log from openpilot.selfdrive.manager.process import ensure_running
from openpilot.selfdrive.manager.process_config import managed_processes from openpilot.selfdrive.manager.process_config import managed_processes
from openpilot.selfdrive.athena.registration import register, UNREGISTERED_DONGLE_ID from openpilot.selfdrive.athena.registration import register, UNREGISTERED_DONGLE_ID
from openpilot.common.swaglog import cloudlog, add_file_handler from openpilot.common.swaglog import cloudlog, add_file_handler
@@ -51,41 +51,16 @@ def frogpilot_boot_functions(frogpilot_functions):
except Exception as e: except Exception as e:
print(f"An unexpected error occurred: {e}") print(f"An unexpected error occurred: {e}")
def cleanup_old_logs(max_age_days=30):
"""CLEARPILOT: delete session log directories older than max_age_days."""
import shutil
log_base = "/data/log2"
if not os.path.exists(log_base):
return
cutoff = time.time() - (max_age_days * 86400)
for entry in os.listdir(log_base):
if entry == "current":
continue
path = os.path.join(log_base, entry)
if os.path.isdir(path):
if os.path.getmtime(path) < cutoff:
try:
shutil.rmtree(path)
except OSError:
pass
def manager_init(frogpilot_functions) -> None: def manager_init(frogpilot_functions) -> None:
init_log_dir()
cleanup_old_logs()
frogpilot_boot = threading.Thread(target=frogpilot_boot_functions, args=(frogpilot_functions,)) frogpilot_boot = threading.Thread(target=frogpilot_boot_functions, args=(frogpilot_functions,))
frogpilot_boot.start() frogpilot_boot.start()
save_bootlog() # CLEARPILOT: skip writing boot logs to /data/media/0/realdata/boot/
# save_bootlog()
params = Params() params = Params()
params_storage = Params("/persist/params") params_storage = Params("/persist/params")
params.clear_all(ParamKeyType.CLEAR_ON_MANAGER_START) params.clear_all(ParamKeyType.CLEAR_ON_MANAGER_START)
# CLEARPILOT: always start with telemetry disabled, VPN enabled
params.put("TelemetryEnabled", "0")
params.put("VpnEnabled", "1")
params.clear_all(ParamKeyType.CLEAR_ON_ONROAD_TRANSITION) params.clear_all(ParamKeyType.CLEAR_ON_ONROAD_TRANSITION)
params.clear_all(ParamKeyType.CLEAR_ON_OFFROAD_TRANSITION) params.clear_all(ParamKeyType.CLEAR_ON_OFFROAD_TRANSITION)
if is_release_branch(): if is_release_branch():
@@ -161,8 +136,6 @@ def manager_init(frogpilot_functions) -> None:
("DisableOpenpilotLongitudinal", "0"), ("DisableOpenpilotLongitudinal", "0"),
("DisableVTSCSmoothing", "0"), ("DisableVTSCSmoothing", "0"),
("DisengageVolume", "100"), ("DisengageVolume", "100"),
("DashcamDebug", "1"),
("TelemetryEnabled", "0"),
("DragonPilotTune", "0"), ("DragonPilotTune", "0"),
("DriverCamera", "0"), ("DriverCamera", "0"),
("DynamicPathWidth", "0"), ("DynamicPathWidth", "0"),
@@ -257,7 +230,6 @@ def manager_init(frogpilot_functions) -> None:
("ScreenBrightnessOnroad", "101"), ("ScreenBrightnessOnroad", "101"),
("ScreenManagement", "1"), ("ScreenManagement", "1"),
("ScreenRecorder", "1"), ("ScreenRecorder", "1"),
("ScreenRecorderDebug", "1"),
("ScreenTimeout", "30"), ("ScreenTimeout", "30"),
("ScreenTimeoutOnroad", "30"), ("ScreenTimeoutOnroad", "30"),
("SearchInput", "0"), ("SearchInput", "0"),
@@ -329,6 +301,32 @@ def manager_init(frogpilot_functions) -> None:
else: else:
params_storage.put(k, params.get(k)) params_storage.put(k, params.get(k))
# CLEARPILOT memory-param defaults. /dev/shm/params is on tmpfs so these
# reset on every boot anyway; we still set them so first-readers don't see
# missing keys before the writer process has spun up.
params_memory = Params("/dev/shm/params")
for k, v in [
("CarIsMetric", "0"),
("ClearpilotCruiseWarning", ""),
("ClearpilotCruiseWarningSpeed", ""),
("ClearpilotHasSpeed", "0"),
("ClearpilotIsMetric", "0"),
("ClearpilotPlayDing", "0"),
("ClearpilotSpeedDisplay", ""),
("ClearpilotSpeedLimitDisplay", "0"),
("ClearpilotSpeedUnit", "mph"),
("DashcamFrames", "0"),
("DashcamShutdown", "0"),
("DashcamState", "stopped"),
("ModelFps", "20"),
("ModelStandby", "0"),
("ModelStandbyTs", "0"),
("ShutdownTouchReset", "0"),
("TelemetryEnabled", "0"),
("VpnEnabled", "1"),
]:
params_memory.put(k, v)
# Create folders needed for msgq # Create folders needed for msgq
try: try:
os.mkdir("/dev/shm") os.mkdir("/dev/shm")
@@ -394,7 +392,6 @@ def manager_thread(frogpilot_functions) -> None:
cloudlog.bind(daemon="manager") cloudlog.bind(daemon="manager")
cloudlog.info("manager start") cloudlog.info("manager start")
cloudlog.info({"environ": os.environ}) cloudlog.info({"environ": os.environ})
session_log.info("manager starting")
params = Params() params = Params()
params_memory = Params("/dev/shm/params") params_memory = Params("/dev/shm/params")
@@ -404,14 +401,6 @@ def manager_thread(frogpilot_functions) -> None:
ignore += ["manage_athenad", "uploader"] ignore += ["manage_athenad", "uploader"]
if os.getenv("NOBOARD") is not None: if os.getenv("NOBOARD") is not None:
ignore.append("pandad") ignore.append("pandad")
# CLEARPILOT: bench mode — disable real car processes, enable bench simulator
if os.getenv("BENCH_MODE") is not None:
ignore += ["pandad", "controlsd", "radard", "plannerd",
"calibrationd", "torqued", "paramsd", "locationd", "sensord",
"ubloxd", "pigeond", "dmonitoringmodeld", "dmonitoringd",
"modeld", "soundd", "loggerd", "micd",
"dashcamd"]
session_log.info("bench mode enabled")
ignore += [x for x in os.getenv("BLOCK", "").split(",") if len(x) > 0] ignore += [x for x in os.getenv("BLOCK", "").split(",") if len(x) > 0]
sm = messaging.SubMaster(['deviceState', 'carParams'], poll='deviceState') sm = messaging.SubMaster(['deviceState', 'carParams'], poll='deviceState')
@@ -433,7 +422,6 @@ def manager_thread(frogpilot_functions) -> None:
if started and not started_prev: if started and not started_prev:
params.clear_all(ParamKeyType.CLEAR_ON_ONROAD_TRANSITION) params.clear_all(ParamKeyType.CLEAR_ON_ONROAD_TRANSITION)
session_log.info("onroad transition")
if openpilot_crashed: if openpilot_crashed:
os.remove(os.path.join(sentry.CRASHES_DIR, 'error.txt')) os.remove(os.path.join(sentry.CRASHES_DIR, 'error.txt'))
@@ -441,7 +429,6 @@ def manager_thread(frogpilot_functions) -> None:
elif not started and started_prev: elif not started and started_prev:
params.clear_all(ParamKeyType.CLEAR_ON_OFFROAD_TRANSITION) params.clear_all(ParamKeyType.CLEAR_ON_OFFROAD_TRANSITION)
params_memory.clear_all(ParamKeyType.CLEAR_ON_OFFROAD_TRANSITION) params_memory.clear_all(ParamKeyType.CLEAR_ON_OFFROAD_TRANSITION)
session_log.info("offroad transition")
# update onroad params, which drives boardd's safety setter thread # update onroad params, which drives boardd's safety setter thread
if started != started_prev: if started != started_prev:
@@ -451,9 +438,6 @@ def manager_thread(frogpilot_functions) -> None:
ensure_running(managed_processes.values(), started, params=params, CP=sm['carParams'], not_run=ignore) ensure_running(managed_processes.values(), started, params=params, CP=sm['carParams'], not_run=ignore)
# CLEARPILOT: rename log directory once system time is valid
update_log_dir_timestamp()
running = ' '.join("{}{}\u001b[0m".format("\u001b[32m" if p.proc.is_alive() else "\u001b[31m", p.name) running = ' '.join("{}{}\u001b[0m".format("\u001b[32m" if p.proc.is_alive() else "\u001b[31m", p.name)
for p in managed_processes.values() if p.proc) for p in managed_processes.values() if p.proc)
print(running) print(running)
@@ -471,7 +455,6 @@ def manager_thread(frogpilot_functions) -> None:
shutdown = True shutdown = True
params.put("LastManagerExitReason", f"{param} {datetime.datetime.now()}") params.put("LastManagerExitReason", f"{param} {datetime.datetime.now()}")
cloudlog.warning(f"Shutting down manager - {param} set") cloudlog.warning(f"Shutting down manager - {param} set")
session_log.info("manager shutting down: %s", param)
if shutdown: if shutdown:
break break
@@ -523,7 +506,6 @@ if __name__ == "__main__":
except Exception: except Exception:
add_file_handler(cloudlog) add_file_handler(cloudlog)
cloudlog.exception("Manager failed to start") cloudlog.exception("Manager failed to start")
session_log.critical("manager failed to start: %s", traceback.format_exc())
try: try:
managed_processes['ui'].stop() managed_processes['ui'].stop()
+7 -130
View File
@@ -2,7 +2,6 @@ import importlib
import os import os
import signal import signal
import struct import struct
import sys
import datetime import datetime
import time import time
import subprocess import subprocess
@@ -18,117 +17,17 @@ import openpilot.selfdrive.sentry as sentry
from openpilot.common.basedir import BASEDIR from openpilot.common.basedir import BASEDIR
from openpilot.common.params import Params from openpilot.common.params import Params
from openpilot.common.swaglog import cloudlog from openpilot.common.swaglog import cloudlog
from openpilot.common.time import system_time_valid
WATCHDOG_FN = "/dev/shm/wd_" WATCHDOG_FN = "/dev/shm/wd_"
ENABLE_WATCHDOG = os.getenv("NO_WATCHDOG") is None ENABLE_WATCHDOG = os.getenv("NO_WATCHDOG") is None
# CLEARPILOT: logging directory and session log timestamp = datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S")
# init_log_dir() must be called once from manager_init() before any process starts. _log_dir = f"/data/log2/{timestamp}"
# Until then, _log_dir and session_log are usable but write to a NullHandler. os.makedirs(_log_dir, exist_ok=True)
import logging
_log_dir = "/data/log2/current"
_time_resolved = False
_session_handler = None
session_log = logging.getLogger("clearpilot.session")
session_log.setLevel(logging.DEBUG)
session_log.addHandler(logging.NullHandler())
def init_log_dir():
"""Create /data/log2/current as a real directory for this session.
Called once from manager_init(). Previous current (if a real dir) is
renamed to a timestamp or boot-monotonic name before we create a fresh one."""
global _log_dir, _time_resolved, _session_handler
log_base = "/data/log2"
current = os.path.join(log_base, "current")
os.makedirs(log_base, exist_ok=True)
# If 'current' is a symlink, just remove the symlink
if os.path.islink(current):
os.unlink(current)
# If 'current' is a real directory (leftover from previous session that
# never got time-resolved), rename it out of the way
elif os.path.isdir(current):
# Use mtime of session.log (or the dir itself) for the rename
session_file = os.path.join(current, "session.log")
mtime = os.path.getmtime(session_file) if os.path.exists(session_file) else os.path.getmtime(current)
ts = datetime.datetime.fromtimestamp(mtime).strftime('%Y-%m-%d-%H-%M-%S')
dest = os.path.join(log_base, ts)
# Avoid collision
if os.path.exists(dest):
dest = dest + f"-{int(time.monotonic())}"
try:
os.rename(current, dest)
except OSError:
pass
os.makedirs(current, exist_ok=True)
_log_dir = current
_time_resolved = False
# Set up session log file handler
_session_handler = logging.FileHandler(os.path.join(_log_dir, "session.log"))
_session_handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(message)s"))
# Remove NullHandler and add file handler
session_log.handlers.clear()
session_log.addHandler(_session_handler)
session_log.info("session started, log dir: %s", _log_dir)
def update_log_dir_timestamp():
"""Rename /data/log2/current to a real timestamp and replace with a symlink
once system time is valid."""
global _log_dir, _time_resolved, _session_handler
if _time_resolved:
return
if not system_time_valid():
return
log_base = "/data/log2"
current = os.path.join(log_base, "current")
ts_name = datetime.datetime.now().strftime('%Y-%m-%d-%H-%M-%S')
new_dir = os.path.join(log_base, ts_name)
try:
os.rename(current, new_dir)
# Create symlink: current -> YYYY-MM-DD-HH-MM-SS
os.symlink(ts_name, current)
_log_dir = new_dir
_time_resolved = True
# Re-point session log handler (open files follow the inode, but
# new opens should go through the symlink — update handler for clarity)
session_log.removeHandler(_session_handler)
_session_handler.close()
_session_handler = logging.FileHandler(os.path.join(_log_dir, "session.log"))
_session_handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(message)s"))
session_log.addHandler(_session_handler)
session_log.info("log directory renamed to %s", _log_dir)
# Signal via param that the directory has been time-resolved
try:
from openpilot.common.params import Params
Params().put("LogDirInitialized", "1")
except Exception:
pass
except OSError:
pass
def launcher(proc: str, name: str, log_path: str) -> None: def launcher(proc: str, name: str) -> None:
# CLEARPILOT: redirect stderr to per-process log file
try:
log_file = open(log_path, 'a')
os.dup2(log_file.fileno(), sys.stderr.fileno())
os.dup2(log_file.fileno(), sys.stdout.fileno())
except Exception as e:
print(f"CLEARPILOT: stderr redirect failed for {name}: {e}", file=sys.stderr)
try: try:
# import the process # import the process
mod = importlib.import_module(proc) mod = importlib.import_module(proc)
@@ -154,17 +53,9 @@ def launcher(proc: str, name: str, log_path: str) -> None:
raise raise
def nativelauncher(pargs: list[str], cwd: str, name: str, log_path: str) -> None: def nativelauncher(pargs: list[str], cwd: str, name: str) -> None:
os.environ['MANAGER_DAEMON'] = name os.environ['MANAGER_DAEMON'] = name
# CLEARPILOT: redirect stderr and stdout to per-process log file
try:
log_file = open(log_path, 'a')
os.dup2(log_file.fileno(), sys.stderr.fileno())
os.dup2(log_file.fileno(), sys.stdout.fileno())
except Exception as e:
print(f"CLEARPILOT: stderr redirect failed for {name}: {e}", file=sys.stderr)
# exec the process # exec the process
os.chdir(cwd) os.chdir(cwd)
os.execvp(pargs[0], pargs) os.execvp(pargs[0], pargs)
@@ -219,7 +110,6 @@ class ManagerProcess(ABC):
if dt > self.watchdog_max_dt: if dt > self.watchdog_max_dt:
if (self.watchdog_seen or self.always_watchdog and self.proc.exitcode is not None) and ENABLE_WATCHDOG: if (self.watchdog_seen or self.always_watchdog and self.proc.exitcode is not None) and ENABLE_WATCHDOG:
cloudlog.error(f"Watchdog timeout for {self.name} (exitcode {self.proc.exitcode}) restarting ({started=})") cloudlog.error(f"Watchdog timeout for {self.name} (exitcode {self.proc.exitcode}) restarting ({started=})")
session_log.warning("watchdog timeout for %s (exitcode %s), restarting", self.name, self.proc.exitcode)
self.restart() self.restart()
else: else:
self.watchdog_seen = True self.watchdog_seen = True
@@ -249,10 +139,6 @@ class ManagerProcess(ABC):
ret = self.proc.exitcode ret = self.proc.exitcode
cloudlog.info(f"{self.name} is dead with {ret}") cloudlog.info(f"{self.name} is dead with {ret}")
if ret is not None and ret != 0:
session_log.error("process %s died with exit code %s", self.name, ret)
elif ret == 0:
session_log.info("process %s stopped (exit 0)", self.name)
if self.proc.exitcode is not None: if self.proc.exitcode is not None:
self.shutting_down = False self.shutting_down = False
@@ -312,13 +198,9 @@ class NativeProcess(ManagerProcess):
global _log_dir global _log_dir
log_path = _log_dir+"/"+self.name+".log" log_path = _log_dir+"/"+self.name+".log"
# CLEARPILOT: ensure log file exists even if child redirect fails
open(log_path, 'a').close()
cwd = os.path.join(BASEDIR, self.cwd) cwd = os.path.join(BASEDIR, self.cwd)
cloudlog.info(f"starting process {self.name}") cloudlog.info(f"starting process {self.name}")
session_log.info("starting %s", self.name) self.proc = Process(name=self.name, target=self.launcher, args=(self.cmdline, cwd, self.name))
self.proc = Process(name=self.name, target=self.launcher, args=(self.cmdline, cwd, self.name, log_path))
self.proc.start() self.proc.start()
self.watchdog_seen = False self.watchdog_seen = False
self.shutting_down = False self.shutting_down = False
@@ -349,12 +231,8 @@ class PythonProcess(ManagerProcess):
global _log_dir global _log_dir
log_path = _log_dir+"/"+self.name+".log" log_path = _log_dir+"/"+self.name+".log"
# CLEARPILOT: ensure log file exists even if child redirect fails
open(log_path, 'a').close()
cloudlog.info(f"starting python {self.module}") cloudlog.info(f"starting python {self.module}")
session_log.info("starting %s", self.name) self.proc = Process(name=self.name, target=self.launcher, args=(self.module, self.name))
self.proc = Process(name=self.name, target=self.launcher, args=(self.module, self.name, log_path))
self.proc.start() self.proc.start()
self.watchdog_seen = False self.watchdog_seen = False
self.shutting_down = False self.shutting_down = False
@@ -398,7 +276,6 @@ class DaemonProcess(ManagerProcess):
pass pass
cloudlog.info(f"starting daemon {self.name}") cloudlog.info(f"starting daemon {self.name}")
session_log.info("starting daemon %s", self.name)
proc = subprocess.Popen(['python', '-m', self.module], proc = subprocess.Popen(['python', '-m', self.module],
stdin=open('/dev/null'), stdin=open('/dev/null'),
stdout=open(log_path, 'a'), stdout=open(log_path, 'a'),
+17 -14
View File
@@ -7,7 +7,6 @@ from openpilot.selfdrive.manager.process import PythonProcess, NativeProcess, Da
WEBCAM = os.getenv("USE_WEBCAM") is not None WEBCAM = os.getenv("USE_WEBCAM") is not None
BENCH_MODE = os.getenv("BENCH_MODE") is not None
def driverview(started: bool, params: Params, CP: car.CarParams) -> bool: def driverview(started: bool, params: Params, CP: car.CarParams) -> bool:
return started or params.get_bool("IsDriverViewEnabled") return started or params.get_bool("IsDriverViewEnabled")
@@ -52,13 +51,11 @@ def allow_uploads(started, params, CP: car.CarParams) -> bool:
allow_uploads = not (params.get_bool("DeviceManagement") and params.get_bool("NoUploads")) allow_uploads = not (params.get_bool("DeviceManagement") and params.get_bool("NoUploads"))
return allow_uploads return allow_uploads
# ClearPilot functions
def dashcam_should_run(started, params, CP: car.CarParams) -> bool:
return started or params.get_bool("DashcamDebug")
procs = [ procs = [
DaemonProcess("manage_athenad", "selfdrive.athena.manage_athenad", "AthenadPid"), DaemonProcess("manage_athenad", "selfdrive.athena.manage_athenad", "AthenadPid"),
# CLEARPILOT: camerad runs always (was driverview) so dashcamd can record
# the moment ignition+drive-gear arrives without waiting for camera startup.
NativeProcess("camerad", "system/camerad", ["./camerad"], always_run), NativeProcess("camerad", "system/camerad", ["./camerad"], always_run),
NativeProcess("logcatd", "system/logcatd", ["./logcatd"], allow_logging), NativeProcess("logcatd", "system/logcatd", ["./logcatd"], allow_logging),
NativeProcess("proclogd", "system/proclogd", ["./proclogd"], allow_logging), NativeProcess("proclogd", "system/proclogd", ["./proclogd"], allow_logging),
@@ -67,10 +64,12 @@ procs = [
PythonProcess("timed", "system.timed", always_run, enabled=not PC), 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, enabled=(not PC or WEBCAM)),
# CLEARPILOT: disabled video encoding (camera .hevc files) — CAN/sensor logs still recorded via loggerd # 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("encoderd", "system/loggerd", ["./encoderd"], allow_logging),
# NativeProcess("stream_encoderd", "system/loggerd", ["./encoderd", "--stream"], notcar), # NativeProcess("stream_encoderd", "system/loggerd", ["./encoderd", "--stream"], notcar),
NativeProcess("loggerd", "system/loggerd", ["./loggerd"], allow_logging), # NativeProcess("loggerd", "system/loggerd", ["./loggerd"], allow_logging),
NativeProcess("modeld", "selfdrive/modeld", ["./modeld"], only_onroad), NativeProcess("modeld", "selfdrive/modeld", ["./modeld"], only_onroad),
#NativeProcess("mapsd", "selfdrive/navd", ["./mapsd"], only_onroad), #NativeProcess("mapsd", "selfdrive/navd", ["./mapsd"], only_onroad),
#PythonProcess("navmodeld", "selfdrive.modeld.navmodeld", only_onroad), #PythonProcess("navmodeld", "selfdrive.modeld.navmodeld", only_onroad),
@@ -84,8 +83,17 @@ procs = [
PythonProcess("controlsd", "selfdrive.controls.controlsd", only_onroad), PythonProcess("controlsd", "selfdrive.controls.controlsd", only_onroad),
PythonProcess("deleter", "system.loggerd.deleter", always_run), PythonProcess("deleter", "system.loggerd.deleter", always_run),
PythonProcess("dmonitoringd", "selfdrive.monitoring.dmonitoringd", driverview, enabled=(not PC or WEBCAM)), PythonProcess("dmonitoringd", "selfdrive.monitoring.dmonitoringd", driverview, enabled=(not PC or WEBCAM)),
# PythonProcess("qcomgpsd", "system.qcomgpsd.qcomgpsd", qcomgps, enabled=TICI), # PythonProcess("qcomgpsd", "system.qcomgpsd.qcomgpsd", qcomgps, enabled=TICI), # Fixme
# PythonProcess("gpsd", "system.clearpilot.gpsd", qcomgps, enabled=TICI), # DISABLED: testing perf # 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
# is patched to skip gpsLocation. Used only for system clock + UI speed + dashcam metadata.
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),
# 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("ugpsd", "system.ugpsd", only_onroad, enabled=TICI),
#PythonProcess("navd", "selfdrive.navd.navd", only_onroad), #PythonProcess("navd", "selfdrive.navd.navd", only_onroad),
PythonProcess("pandad", "selfdrive.boardd.pandad", always_run), PythonProcess("pandad", "selfdrive.boardd.pandad", always_run),
@@ -108,11 +116,6 @@ procs = [
# FrogPilot processes # FrogPilot processes
PythonProcess("fleet_manager", "selfdrive.frogpilot.fleetmanager.fleet_manager", always_run), 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),
# ClearPilot processes
# NativeProcess("dashcamd", "selfdrive/clearpilot", ["./dashcamd"], dashcam_should_run), # DISABLED: testing perf
PythonProcess("telemetryd", "selfdrive.clearpilot.telemetryd", always_run),
PythonProcess("bench_onroad", "selfdrive.clearpilot.bench_onroad", always_run, enabled=BENCH_MODE),
] ]
managed_processes = {p.name: p for p in procs} managed_processes = {p.name: p for p in procs}
-10
View File
@@ -411,16 +411,6 @@ def thermald_thread(end_event, hw_queue) -> None:
# Check if we need to shut down # Check if we need to shut down
if power_monitor.should_shutdown(onroad_conditions["ignition"], in_car, off_ts, started_seen): if power_monitor.should_shutdown(onroad_conditions["ignition"], in_car, off_ts, started_seen):
cloudlog.warning(f"shutting device down, offroad since {off_ts}") 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)
deadline = time.monotonic() + 15.0
while time.monotonic() < deadline:
if not params.getBool("DashcamShutdown"):
cloudlog.info("dashcamd shutdown ack received")
break
time.sleep(0.5)
else:
cloudlog.warning("dashcamd shutdown ack timeout, proceeding")
params.put_bool("DoShutdown", True) params.put_bool("DoShutdown", True)
msg.deviceState.started = started_ts is not None msg.deviceState.started = started_ts is not None
+2 -11
View File
@@ -77,20 +77,11 @@ qt_env.Program("_text", ["qt/text.cc"], LIBS=qt_libs)
qt_env.Program("_spinner", ["qt/spinner.cc"], LIBS=qt_libs) qt_env.Program("_spinner", ["qt/spinner.cc"], LIBS=qt_libs)
# Clearpilot # Clearpilot tools
# Add qtwebengine to build paths
qt_env['CXXFLAGS'] += ["-I/usr/include/aarch64-linux-gnu/qt5/QtWebEngine"]
qt_env['CXXFLAGS'] += ["-I/usr/include/aarch64-linux-gnu/qt5/QtWebEngineCore"]
qt_env['CXXFLAGS'] += ["-I/usr/include/aarch64-linux-gnu/qt5/QtWebEngineWidgets"]
qt_env['CXXFLAGS'] += ["-I/usr/include/aarch64-linux-gnu/qt5/QtWebChannel"]
qt_webengine_libs = qt_libs + ['Qt5WebEngineWidgets']
# Create clearpilot tools
qt_env.Program("/data/openpilot/system/clearpilot/tools/qt_shell", ["/data/openpilot/system/clearpilot/tools/qt_shell.cc"], LIBS=qt_libs) qt_env.Program("/data/openpilot/system/clearpilot/tools/qt_shell", ["/data/openpilot/system/clearpilot/tools/qt_shell.cc"], LIBS=qt_libs)
# qt_env.Program("/data/openpilot/system/clearpilot/tools/qt_webview", ["/data/openpilot/system/clearpilot/tools/qt_webview.cc"], LIBS=qt_webengine_libs)
# build main UI # build main UI
qt_env.Program("ui", qt_src + [asset_obj], LIBS=qt_webengine_libs) qt_env.Program("ui", qt_src + [asset_obj], LIBS=qt_libs)
if GetOption('extras'): if GetOption('extras'):
qt_src.remove("main.cc") # replaced by test_runner qt_src.remove("main.cc") # replaced by test_runner
qt_env.Program('tests/test_translations', [asset_obj, 'tests/test_runner.cc', 'tests/test_translations.cc'] + qt_src, LIBS=qt_libs) qt_env.Program('tests/test_translations', [asset_obj, 'tests/test_runner.cc', 'tests/test_translations.cc'] + qt_src, LIBS=qt_libs)
+31 -35
View File
@@ -86,18 +86,25 @@ void HomeWindow::updateState(const UIState &s) {
// CLEARPILOT: show splash screen when onroad but in park // CLEARPILOT: show splash screen when onroad but in park
bool parked = s.scene.parked; bool parked = s.scene.parked;
int screenMode = paramsMemory.getInt("ScreenDisplayMode");
bool nightrider = (screenMode == 1 || screenMode == 4);
if (parked && !was_parked_onroad) { if (parked && !was_parked_onroad) {
LOGW("CLP UI: park transition -> showing splash"); LOGW("CLP UI: park transition -> showing splash");
slayout->setCurrentWidget(ready); slayout->setCurrentWidget(ready);
// If we were in nightrider mode, switch to screen off
if (nightrider) {
paramsMemory.putInt("ScreenDisplayMode", 3);
}
} else if (!parked && was_parked_onroad) { } else if (!parked && was_parked_onroad) {
LOGW("CLP UI: drive transition -> showing onroad"); LOGW("CLP UI: drive transition -> showing onroad");
slayout->setCurrentWidget(onroad); slayout->setCurrentWidget(onroad);
ready->has_driven = true;
} }
was_parked_onroad = parked; was_parked_onroad = parked;
// CLEARPILOT: honor display on/off while showing splash in park // CLEARPILOT: honor display on/off while showing splash in park (normal mode only)
if (parked && ready->isVisible()) { if (parked && ready->isVisible()) {
int screenMode = paramsMemory.getInt("ScreenDisplayMode");
if (screenMode == 3) { if (screenMode == 3) {
Hardware::set_display_power(false); Hardware::set_display_power(false);
} else { } else {
@@ -115,9 +122,11 @@ void HomeWindow::offroadTransition(bool offroad) {
slayout->setCurrentWidget(ready); slayout->setCurrentWidget(ready);
} else { } else {
// CLEARPILOT: start onroad in splash — updateState will switch to // 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)"); LOGW("CLP UI: onroad transition -> showing splash (parked)");
was_parked_onroad = true; was_parked_onroad = true;
ready->has_driven = false;
slayout->setCurrentWidget(ready); slayout->setCurrentWidget(ready);
} }
} }
@@ -172,13 +181,6 @@ static const char *clpSidebarBtnStyle = R"(
// clpActionBtnStyle removed — no longer used // 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) { ClearPilotPanel::ClearPilotPanel(QWidget* parent) : QFrame(parent) {
// Sidebar // Sidebar
@@ -249,27 +251,6 @@ ClearPilotPanel::ClearPilotPanel(QWidget* parent) : QFrame(parent) {
}); });
general_panel->addItem(resetCalibBtn); 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 // Power buttons
QHBoxLayout *power_layout = new QHBoxLayout(); QHBoxLayout *power_layout = new QHBoxLayout();
power_layout->setSpacing(30); power_layout->setSpacing(30);
@@ -350,15 +331,30 @@ ClearPilotPanel::ClearPilotPanel(QWidget* parent) : QFrame(parent) {
ListWidget *debug_panel = new ListWidget(this); ListWidget *debug_panel = new ListWidget(this);
debug_panel->setContentsMargins(50, 25, 50, 25); debug_panel->setContentsMargins(50, 25, 50, 25);
auto *telemetry_toggle = new ParamControl("TelemetryEnabled", "Telemetry Logging", auto *telemetry_toggle = new ToggleControl("Telemetry Logging",
"Record telemetry data to CSV in the session log directory. " "Record telemetry data to CSV in the session log directory. "
"Captures only changed values for efficiency.", "", this); "Captures only changed values for efficiency.", "",
Params("/dev/shm/params").getBool("TelemetryEnabled"), this);
QObject::connect(telemetry_toggle, &ToggleControl::toggleFlipped, [](bool on) {
Params("/dev/shm/params").putBool("TelemetryEnabled", on);
});
debug_panel->addItem(telemetry_toggle); debug_panel->addItem(telemetry_toggle);
auto *vpn_toggle = new ParamControl("VpnEnabled", "VPN", auto *health_metrics_toggle = new ToggleControl("System Health Overlay",
"Show controls lag, model frame drops, temperature, CPU, and memory usage "
"in the lower-right of the onroad UI. For diagnosing slowdown issues.", "",
Params().getBool("ClearpilotShowHealthMetrics"), this);
QObject::connect(health_metrics_toggle, &ToggleControl::toggleFlipped, [](bool on) {
Params().putBool("ClearpilotShowHealthMetrics", on);
});
debug_panel->addItem(health_metrics_toggle);
auto *vpn_toggle = new ToggleControl("VPN",
"Connect to vpn.hanson.xyz for remote SSH access. " "Connect to vpn.hanson.xyz for remote SSH access. "
"Disabling kills the active tunnel and stops reconnection attempts.", "", this); "Disabling kills the active tunnel and stops reconnection attempts.", "",
Params("/dev/shm/params").getBool("VpnEnabled"), this);
QObject::connect(vpn_toggle, &ToggleControl::toggleFlipped, [](bool on) { QObject::connect(vpn_toggle, &ToggleControl::toggleFlipped, [](bool on) {
Params("/dev/shm/params").putBool("VpnEnabled", on);
if (on) { if (on) {
std::system("sudo bash -c 'nohup /data/openpilot/system/clearpilot/vpn-monitor.sh >> /tmp/vpn-monitor.log 2>&1 &'"); std::system("sudo bash -c 'nohup /data/openpilot/system/clearpilot/vpn-monitor.sh >> /tmp/vpn-monitor.log 2>&1 &'");
} else { } else {
+333 -23
View File
@@ -79,7 +79,8 @@ void OnroadWindow::updateState(const UIState &s) {
nvg->update(); // CLEARPILOT: force repaint every frame for HUD elements nvg->update(); // CLEARPILOT: force repaint every frame for HUD elements
QColor bgColor = bg_colors[s.status]; QColor bgColor = bg_colors[s.status];
if (paramsMemory.getBool("no_lat_lane_change") == 1) { // CLEARPILOT: read from paramsMemory; controlsd writes "no_lat_lane_change".
if (paramsMemory.getBool("no_lat_lane_change")) {
bgColor = bg_colors[STATUS_DISENGAGED]; bgColor = bg_colors[STATUS_DISENGAGED];
} }
@@ -177,7 +178,14 @@ void OnroadWindow::offroadTransition(bool offroad) {
void OnroadWindow::paintEvent(QPaintEvent *event) { void OnroadWindow::paintEvent(QPaintEvent *event) {
QPainter p(this); QPainter p(this);
// CLEARPILOT: hide engagement border in nightrider mode
int dm = paramsMemory.getInt("ScreenDisplayMode");
bool nightrider = (dm == 1 || dm == 4);
if (nightrider) {
p.fillRect(rect(), Qt::black);
} else {
p.fillRect(rect(), QColor(bg.red(), bg.green(), bg.blue(), 255)); p.fillRect(rect(), QColor(bg.red(), bg.green(), bg.blue(), 255));
}
QString logicsDisplayString = QString(); QString logicsDisplayString = QString();
if (scene.show_jerk) { if (scene.show_jerk) {
@@ -348,14 +356,15 @@ void AnnotatedCameraWidget::updateState(const UIState &s) {
setSpeed *= KM_TO_MILE; setSpeed *= KM_TO_MILE;
} }
// CLEARPILOT: use GPS speed if available, hide speed display if no GPS fix // CLEARPILOT: read speed and speed limit from params (written by speed_logic.py ~2Hz)
has_gps_speed = sm.valid("gpsLocation") && sm["gpsLocation"].getGpsLocation().getHasFix(); clpParamFrame++;
if (has_gps_speed) { if (clpParamFrame % 10 == 0) { // ~2Hz at 20Hz UI update rate
float gps_speed_ms = sm["gpsLocation"].getGpsLocation().getSpeed(); clpHasSpeed = paramsMemory.get("ClearpilotHasSpeed") == "1";
speed = std::max<float>(0.0, gps_speed_ms); clpSpeedDisplay = QString::fromStdString(paramsMemory.get("ClearpilotSpeedDisplay"));
speed *= s.scene.is_metric ? MS_TO_KPH : MS_TO_MPH; clpSpeedLimitDisplay = QString::fromStdString(paramsMemory.get("ClearpilotSpeedLimitDisplay"));
} else { clpSpeedUnit = QString::fromStdString(paramsMemory.get("ClearpilotSpeedUnit"));
speed = 0.0; clpCruiseWarning = QString::fromStdString(paramsMemory.get("ClearpilotCruiseWarning"));
clpCruiseWarningSpeed = QString::fromStdString(paramsMemory.get("ClearpilotCruiseWarningSpeed"));
} }
// auto speed_limit_sign = nav_instruction.getSpeedLimitSign(); // auto speed_limit_sign = nav_instruction.getSpeedLimitSign();
@@ -404,7 +413,7 @@ void AnnotatedCameraWidget::drawHud(QPainter &p) {
} }
// CLEARPILOT: blinking blue circle when telemetry is recording // CLEARPILOT: blinking blue circle when telemetry is recording
if (Params().getBool("TelemetryEnabled")) { if (paramsMemory.getBool("TelemetryEnabled")) {
// Blink: visible for 500ms, hidden for 500ms // Blink: visible for 500ms, hidden for 500ms
int phase = (QDateTime::currentMSecsSinceEpoch() / 500) % 2; int phase = (QDateTime::currentMSecsSinceEpoch() / 500) % 2;
if (phase == 0) { if (phase == 0) {
@@ -424,7 +433,7 @@ void AnnotatedCameraWidget::drawHud(QPainter &p) {
// QString speedLimitStr = (speedLimit > 1) ? QString::number(std::nearbyint(speedLimit)) : ""; // QString speedLimitStr = (speedLimit > 1) ? QString::number(std::nearbyint(speedLimit)) : "";
// QString speedLimitOffsetStr = slcSpeedLimitOffset == 0 ? "" : QString::number(slcSpeedLimitOffset, 'f', 0).prepend(slcSpeedLimitOffset > 0 ? "+" : ""); // QString speedLimitOffsetStr = slcSpeedLimitOffset == 0 ? "" : QString::number(slcSpeedLimitOffset, 'f', 0).prepend(slcSpeedLimitOffset > 0 ? "+" : "");
QString speedStr = QString::number(std::nearbyint(speed)); // QString speedStr = QString::number(std::nearbyint(speed));
QString setSpeedStr = is_cruise_set ? QString::number(std::nearbyint(setSpeed - cruiseAdjustment)) : ""; QString setSpeedStr = is_cruise_set ? QString::number(std::nearbyint(setSpeed - cruiseAdjustment)) : "";
p.restore(); p.restore();
@@ -448,14 +457,21 @@ void AnnotatedCameraWidget::drawHud(QPainter &p) {
// Todo: lead speed // Todo: lead speed
// Todo: Experimental speed // Todo: Experimental speed
// CLEARPILOT: show GPS speed, hide when no fix // CLEARPILOT: show speed from speed_logic params, hide when no speed or speed=0
if (has_gps_speed && !scene.hide_speed) { if (clpHasSpeed && !clpSpeedDisplay.isEmpty() && !scene.hide_speed) {
p.setFont(InterFont(140, QFont::Bold)); p.setFont(InterFont(140, QFont::Bold));
drawText(p, rect().center().x(), 210, speedStr); drawText(p, rect().center().x(), 210, clpSpeedDisplay);
p.setFont(InterFont(50)); p.setFont(InterFont(50));
drawText(p, rect().center().x(), 290, speedUnit, 200); drawText(p, rect().center().x(), 290, clpSpeedUnit, 200);
} }
// CLEARPILOT: speed limit sign in lower-left, cruise warning above it
drawSpeedLimitSign(p);
drawCruiseWarningSign(p);
// CLEARPILOT: system health metrics in lower-right (debug overlay)
drawHealthMetrics(p);
// Draw FrogPilot widgets // Draw FrogPilot widgets
paintFrogPilotWidgets(p); paintFrogPilotWidgets(p);
} }
@@ -549,6 +565,164 @@ void AnnotatedCameraWidget::drawSpeedWidget(QPainter &p, int x, int y, const QSt
} }
void AnnotatedCameraWidget::drawSpeedLimitSign(QPainter &p) {
// Hide when no speed limit or speed limit is 0
if (clpSpeedLimitDisplay.isEmpty() || clpSpeedLimitDisplay == "0") return;
p.save();
const int signW = 189;
const int signH = 239;
const int margin = 20;
const int borderW = 6;
const int innerBorderW = 4;
const int innerMargin = 10;
const int cornerR = 15;
// Position: 20px from lower-left corner
QRect signRect(margin, height() - signH - margin, signW, signH);
if (nightriderMode) {
// Nightrider: black background, light gray-blue border and text
QColor borderColor(160, 180, 210);
QColor textColor(160, 180, 210);
// Outer border
p.setPen(QPen(borderColor, borderW));
p.setBrush(QColor(0, 0, 0, 220));
p.drawRoundedRect(signRect, cornerR, cornerR);
// Inner border
QRect innerRect = signRect.adjusted(innerMargin, innerMargin, -innerMargin, -innerMargin);
p.setPen(QPen(borderColor, innerBorderW));
p.setBrush(Qt::NoBrush);
p.drawRoundedRect(innerRect, cornerR - 4, cornerR - 4);
// "SPEED" text
p.setPen(textColor);
p.setFont(InterFont(30, QFont::Bold));
p.drawText(innerRect.adjusted(0, 15, 0, 0), Qt::AlignTop | Qt::AlignHCenter, "SPEED");
// "LIMIT" text
p.setFont(InterFont(30, QFont::Bold));
p.drawText(innerRect.adjusted(0, 48, 0, 0), Qt::AlignTop | Qt::AlignHCenter, "LIMIT");
// Speed limit number — shifted down ~10% of innerRect height via extra top inset
p.setFont(InterFont(90, QFont::Bold));
p.drawText(innerRect.adjusted(0, 86, 0, 0), Qt::AlignCenter, clpSpeedLimitDisplay);
} else {
// Normal: white background, black border and text
QColor borderColor(0, 0, 0);
QColor textColor(0, 0, 0);
// Outer border
p.setPen(QPen(borderColor, borderW));
p.setBrush(QColor(255, 255, 255, 240));
p.drawRoundedRect(signRect, cornerR, cornerR);
// Inner border
QRect innerRect = signRect.adjusted(innerMargin, innerMargin, -innerMargin, -innerMargin);
p.setPen(QPen(borderColor, innerBorderW));
p.setBrush(Qt::NoBrush);
p.drawRoundedRect(innerRect, cornerR - 4, cornerR - 4);
// "SPEED" text
p.setPen(textColor);
p.setFont(InterFont(30, QFont::Bold));
p.drawText(innerRect.adjusted(0, 15, 0, 0), Qt::AlignTop | Qt::AlignHCenter, "SPEED");
// "LIMIT" text
p.setFont(InterFont(30, QFont::Bold));
p.drawText(innerRect.adjusted(0, 48, 0, 0), Qt::AlignTop | Qt::AlignHCenter, "LIMIT");
// Speed limit number — shifted down ~10% of innerRect height via extra top inset
p.setFont(InterFont(90, QFont::Bold));
p.drawText(innerRect.adjusted(0, 86, 0, 0), Qt::AlignCenter, clpSpeedLimitDisplay);
}
p.restore();
}
void AnnotatedCameraWidget::drawCruiseWarningSign(QPainter &p) {
// Only show when there's an active warning and the speed limit sign is visible
if (clpCruiseWarning.isEmpty() || clpCruiseWarningSpeed.isEmpty()) return;
if (clpSpeedLimitDisplay.isEmpty() || clpSpeedLimitDisplay == "0") return;
bool isOver = (clpCruiseWarning == "over");
if (!isOver && clpCruiseWarning != "under") return;
p.save();
// Same dimensions as speed limit sign
const int signW = 189;
const int signH = 239;
const int margin = 20;
const int borderW = 6;
const int innerBorderW = 4;
const int innerMargin = 10;
const int cornerR = 15;
const int gap = 20;
// Position: directly above the speed limit sign
int speedLimitY = height() - signH - margin;
QRect signRect(margin, speedLimitY - signH - gap, signW, signH);
if (nightriderMode) {
// Nightrider: black background with colored border/text
QColor accentColor = isOver ? QColor(220, 50, 50) : QColor(50, 180, 80);
p.setPen(QPen(accentColor, borderW));
p.setBrush(QColor(0, 0, 0, 220));
p.drawRoundedRect(signRect, cornerR, cornerR);
QRect innerRect = signRect.adjusted(innerMargin, innerMargin, -innerMargin, -innerMargin);
p.setPen(QPen(accentColor, innerBorderW));
p.setBrush(Qt::NoBrush);
p.drawRoundedRect(innerRect, cornerR - 4, cornerR - 4);
// "CRUISE" text
p.setPen(accentColor);
p.setFont(InterFont(26, QFont::Bold));
p.drawText(innerRect.adjusted(0, 15, 0, 0), Qt::AlignTop | Qt::AlignHCenter, "CRUISE");
// "SET" text
p.setFont(InterFont(26, QFont::Bold));
p.drawText(innerRect.adjusted(0, 45, 0, 0), Qt::AlignTop | Qt::AlignHCenter, "SET");
// Cruise speed number
p.setFont(InterFont(90, QFont::Bold));
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);
QColor fgColor(255, 255, 255);
p.setPen(QPen(fgColor, borderW));
p.setBrush(bgColor);
p.drawRoundedRect(signRect, cornerR, cornerR);
QRect innerRect = signRect.adjusted(innerMargin, innerMargin, -innerMargin, -innerMargin);
p.setPen(QPen(fgColor, innerBorderW));
p.setBrush(Qt::NoBrush);
p.drawRoundedRect(innerRect, cornerR - 4, cornerR - 4);
// "CRUISE" text
p.setPen(fgColor);
p.setFont(InterFont(26, QFont::Bold));
p.drawText(innerRect.adjusted(0, 15, 0, 0), Qt::AlignTop | Qt::AlignHCenter, "CRUISE");
// "SET" text
p.setFont(InterFont(26, QFont::Bold));
p.drawText(innerRect.adjusted(0, 45, 0, 0), Qt::AlignTop | Qt::AlignHCenter, "SET");
// Cruise speed number
p.setFont(InterFont(90, QFont::Bold));
p.drawText(innerRect.adjusted(0, 86, 0, 0), Qt::AlignCenter, clpCruiseWarningSpeed);
}
p.restore();
}
void AnnotatedCameraWidget::drawText(QPainter &p, int x, int y, const QString &text, int alpha) { void AnnotatedCameraWidget::drawText(QPainter &p, int x, int y, const QString &text, int alpha) {
QRect real_rect = p.fontMetrics().boundingRect(text); QRect real_rect = p.fontMetrics().boundingRect(text);
real_rect.moveCenter({x, y - real_rect.height() / 2}); real_rect.moveCenter({x, y - real_rect.height() / 2});
@@ -557,6 +731,87 @@ void AnnotatedCameraWidget::drawText(QPainter &p, int x, int y, const QString &t
p.drawText(real_rect.x(), real_rect.bottom(), text); p.drawText(real_rect.x(), real_rect.bottom(), text);
} }
// CLEARPILOT: System health overlay — shows metrics that indicate the system
// is overburdened or behind. Toggled via ClearpilotShowHealthMetrics param.
// 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");
static int check_counter = 0;
// re-check the param every ~2s without a toggle signal path
if (++check_counter >= 40) {
check_counter = 0;
enabled = Params().getBool("ClearpilotShowHealthMetrics");
}
if (!enabled) return;
SubMaster &sm = *(uiState()->sm);
auto ds = sm["deviceState"].getDeviceState();
auto mv = sm["modelV2"].getModelV2();
auto ps = sm["peripheralState"].getPeripheralState();
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
if (v >= warn) return QColor(0xff, 0xd0, 0x40); // yellow
return QColor(0xff, 0xff, 0xff); // white (ok)
};
struct Row { QString label; QString value; QColor color; };
Row rows[] = {
{"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();
p.setFont(InterFont(90, QFont::Bold));
QFontMetrics fm = p.fontMetrics();
int row_h = fm.height(); // natural line height at 90pt bold
int gap = 40; // requested 40px between values
int margin = 30; // requested 30px margin
int panel_w = 360; // fixed width — fits "TEMP 99"
int n = sizeof(rows) / sizeof(rows[0]);
int panel_h = n * row_h + (n - 1) * gap + 2 * margin;
int x = width() - panel_w - margin;
int y = height() - panel_h - margin;
// black background
p.setPen(Qt::NoPen);
p.setBrush(QColor(0, 0, 0, 200));
p.drawRoundedRect(QRect(x, y, panel_w, panel_h), 20, 20);
// rows
int text_y = y + margin + fm.ascent();
for (int i = 0; i < n; i++) {
p.setPen(rows[i].color);
// 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;
}
p.restore();
}
void AnnotatedCameraWidget::initializeGL() { void AnnotatedCameraWidget::initializeGL() {
CameraWidget::initializeGL(); CameraWidget::initializeGL();
qInfo() << "OpenGL version:" << QString((const char*)glGetString(GL_VERSION)); qInfo() << "OpenGL version:" << QString((const char*)glGetString(GL_VERSION));
@@ -596,11 +851,29 @@ void AnnotatedCameraWidget::drawLaneLines(QPainter &painter, const UIState *s) {
// CLEARPILOT: nightrider mode — outline only, no fill // CLEARPILOT: nightrider mode — outline only, no fill
bool outlineOnly = nightriderMode; bool outlineOnly = nightriderMode;
// CLEARPILOT: read here so the nightrider hide-when-disengaged check below
// can let lane-change frames through (controlsd forces edgeColor to
// STATUS_DISENGAGED while no_lat_lane_change is true).
bool is_no_lat_lane_change = paramsMemory.getBool("no_lat_lane_change");
// CLEARPILOT: in nightrider mode, hide all lines when not engaged — except
// during a lane change, where we still want lane lines + road edges drawn
// alongside the yellow lane-change outline.
if (outlineOnly && edgeColor == bg_colors[STATUS_DISENGAGED] && !is_no_lat_lane_change) {
painter.restore();
return;
}
// 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 // lanelines
for (int i = 0; i < std::size(scene.lane_line_vertices); ++i) { 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)); QColor lineColor = QColor::fromRgbF(1.0, 1.0, 1.0, std::clamp<float>(scene.lane_line_probs[i], 0.0, 0.7));
if (outlineOnly) { if (outlineOnly) {
painter.setPen(QPen(lineColor, 2)); painter.setPen(QPen(lineColor, laneLineWidth));
painter.setBrush(Qt::NoBrush); painter.setBrush(Qt::NoBrush);
} else { } else {
painter.setPen(Qt::NoPen); painter.setPen(Qt::NoPen);
@@ -614,7 +887,7 @@ void AnnotatedCameraWidget::drawLaneLines(QPainter &painter, const UIState *s) {
for (int i = 0; i < std::size(scene.road_edge_vertices); ++i) { for (int i = 0; i < std::size(scene.road_edge_vertices); ++i) {
QColor edgeCol = QColor::fromRgbF(1.0, 0, 0, std::clamp<float>(1.0 - scene.road_edge_stds[i], 0.0, 1.0)); QColor edgeCol = QColor::fromRgbF(1.0, 0, 0, std::clamp<float>(1.0 - scene.road_edge_stds[i], 0.0, 1.0));
if (outlineOnly) { if (outlineOnly) {
painter.setPen(QPen(edgeCol, 2)); painter.setPen(QPen(edgeCol, outlineWidth));
painter.setBrush(Qt::NoBrush); painter.setBrush(Qt::NoBrush);
} else { } else {
painter.setPen(Qt::NoPen); painter.setPen(Qt::NoPen);
@@ -627,7 +900,7 @@ void AnnotatedCameraWidget::drawLaneLines(QPainter &painter, const UIState *s) {
// paint center lane path // paint center lane path
// QColor bg_colors[CHANGE_LANE_PATH_COLOR]; // QColor bg_colors[CHANGE_LANE_PATH_COLOR];
bool is_no_lat_lane_change = paramsMemory.getBool("no_lat_lane_change"); // is_no_lat_lane_change was read at the top of this function.
QColor center_lane_color; QColor center_lane_color;
@@ -688,8 +961,17 @@ void AnnotatedCameraWidget::drawLaneLines(QPainter &painter, const UIState *s) {
} }
if (outlineOnly) { if (outlineOnly) {
painter.setPen(QPen(QColor(center_lane_color.red(), center_lane_color.green(), // CLEARPILOT: in nightrider, the tire path is rendered as an outline.
center_lane_color.blue(), 180), 2)); // - Normal: light blue 3px (status-neutral guide)
// - Lane change: 4px outline of CHANGE_LANE_PATH_COLOR (the same yellow
// used to fill the polygon in normal mode), so the nightrider lane
// change reads as the same visual cue, just hollow.
if (is_no_lat_lane_change) {
painter.setPen(QPen(bg_colors[CHANGE_LANE_PATH_COLOR], 4));
} else {
QColor lightBlue(153, 204, 255, 220); // #99CCFF light blue, mostly opaque
painter.setPen(QPen(lightBlue, 3));
}
painter.setBrush(Qt::NoBrush); painter.setBrush(Qt::NoBrush);
} else { } else {
painter.setPen(Qt::NoPen); painter.setPen(Qt::NoPen);
@@ -718,7 +1000,7 @@ void AnnotatedCameraWidget::drawLaneLines(QPainter &painter, const UIState *s) {
if (scene.blind_spot_path) { if (scene.blind_spot_path) {
QColor bsColor = QColor::fromHslF(0 / 360., 0.75, 0.50, 0.6); QColor bsColor = QColor::fromHslF(0 / 360., 0.75, 0.50, 0.6);
if (outlineOnly) { if (outlineOnly) {
painter.setPen(QPen(bsColor, 2)); painter.setPen(QPen(bsColor, outlineWidth));
painter.setBrush(Qt::NoBrush); painter.setBrush(Qt::NoBrush);
} else { } else {
QLinearGradient bs(0, height(), 0, 0); QLinearGradient bs(0, height(), 0, 0);
@@ -888,6 +1170,10 @@ void AnnotatedCameraWidget::paintEvent(QPaintEvent *event) {
} else { } else {
CameraWidget::updateCalibration(DEFAULT_CALIBRATION); CameraWidget::updateCalibration(DEFAULT_CALIBRATION);
} }
// CLEARPILOT: force CameraWidget bg to black in nightrider to prevent color bleed
if (nightriderMode) {
CameraWidget::setBackgroundColor(Qt::black);
}
painter.beginNativePainting(); painter.beginNativePainting();
if (nightriderMode) { if (nightriderMode) {
// CLEARPILOT: black background, no camera feed // CLEARPILOT: black background, no camera feed
@@ -1059,7 +1345,8 @@ void AnnotatedCameraWidget::updateFrogPilotWidgets() {
} }
void AnnotatedCameraWidget::paintFrogPilotWidgets(QPainter &p) { void AnnotatedCameraWidget::paintFrogPilotWidgets(QPainter &p) {
if ((showAlwaysOnLateralStatusBar || showConditionalExperimentalStatusBar || roadNameUI)) { // CLEARPILOT: only show status bar when telemetry is enabled
if (paramsMemory.getBool("TelemetryEnabled")) {
drawStatusBar(p); drawStatusBar(p);
} }
@@ -1260,7 +1547,30 @@ void AnnotatedCameraWidget::drawStatusBar(QPainter &p) {
QString roadName = roadNameUI ? QString::fromStdString(paramsMemory.get("RoadName")) : QString(); QString roadName = roadNameUI ? QString::fromStdString(paramsMemory.get("RoadName")) : QString();
if (alwaysOnLateralActive && showAlwaysOnLateralStatusBar) { // CLEARPILOT: telemetry status bar — show live stats when telemetry enabled
if (paramsMemory.getBool("TelemetryEnabled")) {
SubMaster &sm = *(uiState()->sm);
auto deviceState = sm["deviceState"].getDeviceState();
int maxTempC = deviceState.getMaxTempC();
int fanPct = deviceState.getFanSpeedPercentDesired();
bool standstill = sm["carState"].getCarState().getStandstill();
static double last_model_status_t = 0;
static float model_status_fps = 0;
if (sm.updated("modelV2")) {
double now = millis_since_boot();
if (last_model_status_t > 0) {
double dt = now - last_model_status_t;
if (dt > 0) model_status_fps = model_status_fps * 0.8 + (1000.0 / dt) * 0.2;
}
last_model_status_t = now;
}
newStatus = QString("%1\u00B0C FAN %2% MDL %3")
.arg(maxTempC).arg(fanPct).arg(model_status_fps, 0, 'f', 0);
if (standstill) newStatus += " STANDSTILL";
// CLEARPILOT: suppress "Always On Lateral active" status bar message
} else if (false && alwaysOnLateralActive && showAlwaysOnLateralStatusBar) {
newStatus = tr("Always On Lateral active") + (tr(". Press the \"Cruise Control\" button to disable")); newStatus = tr("Always On Lateral active") + (tr(". Press the \"Cruise Control\" button to disable"));
} else if (showConditionalExperimentalStatusBar) { } else if (showConditionalExperimentalStatusBar) {
newStatus = conditionalStatusMap[status != STATUS_DISENGAGED ? conditionalStatus : 0]; newStatus = conditionalStatusMap[status != STATUS_DISENGAGED ? conditionalStatus : 0];
+13
View File
@@ -51,6 +51,9 @@ public:
private: private:
void drawText(QPainter &p, int x, int y, const QString &text, int alpha = 255); void drawText(QPainter &p, int x, int y, const QString &text, int alpha = 255);
void drawSpeedWidget(QPainter &p, int x, int y, const QString &title, const QString &speedLimitStr, QColor colorSpeed, int width = 176); void drawSpeedWidget(QPainter &p, int x, int y, const QString &title, const QString &speedLimitStr, QColor colorSpeed, int width = 176);
void drawSpeedLimitSign(QPainter &p);
void drawCruiseWarningSign(QPainter &p);
void drawHealthMetrics(QPainter &p);
QVBoxLayout *main_layout; QVBoxLayout *main_layout;
QPixmap dm_img; QPixmap dm_img;
@@ -59,6 +62,16 @@ private:
bool nightriderMode = false; bool nightriderMode = false;
int displayMode = 0; int displayMode = 0;
QString speedUnit; QString speedUnit;
// ClearPilot speed state (from params_memory, updated ~2Hz)
bool clpHasSpeed = false;
QString clpSpeedDisplay;
QString clpSpeedLimitDisplay;
QString clpSpeedUnit;
QString clpCruiseWarning;
QString clpCruiseWarningSpeed;
int clpParamFrame = 0;
float setSpeed; float setSpeed;
float speedLimit; float speedLimit;
bool is_cruise_set = false; bool is_cruise_set = false;
+12 -5
View File
@@ -70,14 +70,13 @@ void ReadyWindow::paintEvent(QPaintEvent *event) {
painter.drawPixmap(x, y, scaled); painter.drawPixmap(x, y, scaled);
} }
if (error_msg.isEmpty()) { if (error_msg.isEmpty() && !has_driven) {
// "READY!" 8-bit text sprite, 2x size, 15% below center // "READY!" 8-bit text sprite, 15% below center — only before first drive
static QPixmap ready_text("/data/openpilot/selfdrive/clearpilot/theme/clearpilot/images/ready_text.png"); static QPixmap ready_text("/data/openpilot/selfdrive/clearpilot/theme/clearpilot/images/ready_text.png");
if (!ready_text.isNull()) { if (!ready_text.isNull()) {
QPixmap scaled = ready_text.scaled(ready_text.width() * 3 / 2, ready_text.height() * 3 / 2, Qt::KeepAspectRatio, Qt::FastTransformation); int tx = (width() - ready_text.width()) / 2;
int tx = (width() - scaled.width()) / 2;
int ty = height() / 2 + height() * 15 / 100; int ty = height() / 2 + height() * 15 / 100;
painter.drawPixmap(tx, ty, scaled); painter.drawPixmap(tx, ty, ready_text);
} }
} else { } else {
// Error state: red text at 25% below center // Error state: red text at 25% below center
@@ -126,5 +125,13 @@ void ReadyWindow::refresh() {
if (error_msg != prev_error) changed = true; if (error_msg != prev_error) changed = true;
} }
// Reset has_driven on ignition off→on (power cycle)
bool started = uiState()->scene.started;
if (!last_started && started) {
has_driven = false;
changed = true;
}
last_started = started;
if (changed) update(); if (changed) update();
} }
+2
View File
@@ -18,6 +18,7 @@ class ReadyWindow : public QWidget {
Q_OBJECT Q_OBJECT
public: public:
ReadyWindow(QWidget* parent = nullptr); ReadyWindow(QWidget* parent = nullptr);
bool has_driven = false;
private: private:
void showEvent(QShowEvent *event) override; void showEvent(QShowEvent *event) override;
void hideEvent(QHideEvent *event) override; void hideEvent(QHideEvent *event) override;
@@ -29,6 +30,7 @@ private:
QString cur_temp; QString cur_temp;
QString error_msg; // non-empty = show red error instead of READY! QString error_msg; // non-empty = show red error instead of READY!
QElapsedTimer uptime; QElapsedTimer uptime;
bool last_started = false;
QPixmap img_bg; QPixmap img_bg;
QPixmap img_hot; QPixmap img_hot;
}; };
Binary file not shown.
+29
View File
@@ -171,6 +171,16 @@ bool MainWindow::eventFilter(QObject *obj, QEvent *event) {
case QEvent::TouchEnd: case QEvent::TouchEnd:
case QEvent::MouseButtonPress: case QEvent::MouseButtonPress:
case QEvent::MouseMove: { case QEvent::MouseMove: {
// CLEARPILOT: tap while screen-off (mode 3) -> wake to auto-normal (mode 0)
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 events when device is awakened by resetInteractiveTimeout
ignore = !device()->isAwake(); ignore = !device()->isAwake();
device()->resetInteractiveTimeout(uiState()->scene.screen_timeout, uiState()->scene.screen_timeout_onroad); device()->resetInteractiveTimeout(uiState()->scene.screen_timeout, uiState()->scene.screen_timeout_onroad);
@@ -256,6 +266,11 @@ static StatusWindow::StatusData collectStatus() {
// Telemetry // Telemetry
d.telemetry = readFile("/data/params/d/TelemetryEnabled"); d.telemetry = readFile("/data/params/d/TelemetryEnabled");
// Dashcam
d.dashcam_state = readFile("/dev/shm/params/d/DashcamState");
if (d.dashcam_state.isEmpty()) d.dashcam_state = "stopped";
d.dashcam_frames = readFile("/dev/shm/params/d/DashcamFrames");
// Panda: checked on UI thread in applyResults() via scene.pandaType // Panda: checked on UI thread in applyResults() via scene.pandaType
return d; return d;
@@ -298,6 +313,7 @@ StatusWindow::StatusWindow(QWidget *parent) : QFrame(parent) {
vpn_label = makeRow("VPN"); vpn_label = makeRow("VPN");
gps_label = makeRow("GPS"); gps_label = makeRow("GPS");
telemetry_label = makeRow("Telemetry"); telemetry_label = makeRow("Telemetry");
dashcam_label = makeRow("Dashcam");
layout->addStretch(); layout->addStretch();
@@ -369,6 +385,19 @@ void StatusWindow::applyResults() {
telemetry_label->setText("Disabled"); telemetry_label->setText("Disabled");
telemetry_label->setStyleSheet("color: grey; font-size: 38px;"); telemetry_label->setStyleSheet("color: grey; font-size: 38px;");
} }
if (d.dashcam_state == "recording") {
QString text = "Recording";
if (!d.dashcam_frames.isEmpty() && d.dashcam_frames != "0") text += " (" + d.dashcam_frames + " frames)";
dashcam_label->setText(text);
dashcam_label->setStyleSheet("color: #17c44d; font-size: 38px;");
} else if (d.dashcam_state == "waiting") {
dashcam_label->setText("Waiting");
dashcam_label->setStyleSheet("color: #ffaa00; font-size: 38px;");
} else {
dashcam_label->setText("Stopped");
dashcam_label->setStyleSheet("color: #ff4444; font-size: 38px;");
}
} }
void StatusWindow::mousePressEvent(QMouseEvent *e) { void StatusWindow::mousePressEvent(QMouseEvent *e) {
+2
View File
@@ -20,6 +20,7 @@ public:
struct StatusData { struct StatusData {
QString time, storage, ram, load, temp, fan, ip, wifi; QString time, storage, ram, load, temp, fan, ip, wifi;
QString vpn_status, vpn_ip, gps, telemetry; QString vpn_status, vpn_ip, gps, telemetry;
QString dashcam_state, dashcam_frames;
float temp_c = 0; float temp_c = 0;
}; };
@@ -49,6 +50,7 @@ private:
QLabel *gps_label; QLabel *gps_label;
QLabel *time_label; QLabel *time_label;
QLabel *telemetry_label; QLabel *telemetry_label;
QLabel *dashcam_label;
QLabel *panda_label; QLabel *panda_label;
}; };
+45 -1
View File
@@ -93,6 +93,29 @@ class Soundd:
self.spl_filter_weighted = FirstOrderFilter(0, 2.5, FILTER_DT, initialized=False) self.spl_filter_weighted = FirstOrderFilter(0, 2.5, FILTER_DT, initialized=False)
# CLEARPILOT ding (plays independently of alerts; triggered by writers
# setting ClearpilotPlayDing="1" in /dev/shm/params).
self.ding_sound = None
self.ding_frame = 0
self.ding_playing = False
self.ding_check_counter = 0
self._load_ding()
def _load_ding(self):
import sys
ding_path = BASEDIR + "/selfdrive/clearpilot/sounds/ding.wav"
try:
wavefile = wave.open(ding_path, 'r')
assert wavefile.getnchannels() == 1
assert wavefile.getsampwidth() == 2
assert wavefile.getframerate() == SAMPLE_RATE
length = wavefile.getnframes()
self.ding_sound = np.frombuffer(wavefile.readframes(length), dtype=np.int16).astype(np.float32) / (2**16/2)
print(f"CLP soundd: ClearPilot ding loaded: {length} frames", file=sys.stderr, flush=True)
except Exception as e:
print(f"CLP soundd: failed to load ding sound: {e}", file=sys.stderr, flush=True)
self.ding_sound = None
def load_sounds(self): def load_sounds(self):
self.loaded_sounds: dict[int, np.ndarray] = {} self.loaded_sounds: dict[int, np.ndarray] = {}
@@ -137,7 +160,20 @@ class Soundd:
written_frames += frames_to_write written_frames += frames_to_write
self.current_sound_frame += frames_to_write self.current_sound_frame += frames_to_write
return ret * self.current_volume ret = ret * self.current_volume
# CLEARPILOT: mix in ding (independent of alerts, always max volume)
if self.ding_playing and self.ding_sound is not None:
ding_remaining = len(self.ding_sound) - self.ding_frame
if ding_remaining > 0:
frames_to_write = min(ding_remaining, frames)
ret[:frames_to_write] += self.ding_sound[self.ding_frame:self.ding_frame + frames_to_write] * MAX_VOLUME
self.ding_frame += frames_to_write
else:
self.ding_playing = False
self.ding_frame = 0
return ret
def callback(self, data_out: np.ndarray, frames: int, time, status) -> None: def callback(self, data_out: np.ndarray, frames: int, time, status) -> None:
if status: if status:
@@ -205,6 +241,14 @@ class Soundd:
if self.params_memory.get_bool("FrogPilotTogglesUpdated"): if self.params_memory.get_bool("FrogPilotTogglesUpdated"):
self.update_frogpilot_params() self.update_frogpilot_params()
# CLEARPILOT: poll ClearpilotPlayDing at ~2Hz; clear it on read.
self.ding_check_counter += 1
if self.ding_check_counter % 10 == 0 and self.ding_sound is not None:
if self.params_memory.get("ClearpilotPlayDing") == b"1":
self.params_memory.put("ClearpilotPlayDing", "0")
self.ding_playing = True
self.ding_frame = 0
def update_frogpilot_params(self): def update_frogpilot_params(self):
self.alert_volume_control = self.params.get_bool("AlertVolumeControl") self.alert_volume_control = self.params.get_bool("AlertVolumeControl")
-5
View File
@@ -1,10 +1,5 @@
#!/bin/sh #!/bin/sh
# CLEARPILOT: prefer freshly built _spinner over prebuilt qt/spinner
if [ -f ./_spinner ]; then
exec ./_spinner "$1"
fi
if [ -f /TICI ] && [ ! -f qt/spinner ]; then if [ -f /TICI ] && [ ! -f qt/spinner ]; then
cp qt/spinner_larch64 qt/spinner cp qt/spinner_larch64 qt/spinner
fi fi
+13 -4
View File
@@ -118,8 +118,11 @@ void update_model(UIState *s,
} }
// update path // update path
// CLEARPILOT: read from paramsMemory; controlsd writes "no_lat_lane_change"
// there. (Broken used a custom cereal field; we keep the param-based wiring.)
bool no_lat_lane_change = paramsMemory.getBool("no_lat_lane_change");
float path; 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 path = (float)LANE_CHANGE_NO_LAT_PATH_WIDTH / 20; // Release: better calc for EU users
} else { } else {
path = (float)CENTER_LANE_WIDTH / 20; // Release: better calc for EU users path = (float)CENTER_LANE_WIDTH / 20; // Release: better calc for EU users
@@ -439,7 +442,7 @@ void UIState::updateStatus() {
UIState::UIState(QObject *parent) : QObject(parent) { UIState::UIState(QObject *parent) : QObject(parent) {
sm = std::make_unique<SubMaster, const std::initializer_list<const char *>>({ sm = std::make_unique<SubMaster, const std::initializer_list<const char *>>({
"modelV2", "controlsState", "liveCalibration", "radarState", "deviceState", "modelV2", "controlsState", "liveCalibration", "radarState", "deviceState",
"pandaStates", "carParams", "driverMonitoringState", "carState", "liveLocationKalman", "driverStateV2", "pandaStates", "peripheralState", "carParams", "driverMonitoringState", "carState", "liveLocationKalman", "driverStateV2",
"wideRoadCameraState", "managerState", "uiPlan", "liveTorqueParameters", "wideRoadCameraState", "managerState", "uiPlan", "liveTorqueParameters",
"frogpilotCarControl", "frogpilotDeviceState", "frogpilotPlan", "frogpilotCarControl", "frogpilotDeviceState", "frogpilotPlan",
"gpsLocation", "gpsLocation",
@@ -556,7 +559,10 @@ void Device::updateWakefulness(const UIState &s) {
} }
if (ignition_state_changed) { 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); resetInteractiveTimeout(0, 0);
} else { } else {
resetInteractiveTimeout(s.scene.screen_timeout, s.scene.screen_timeout_onroad); resetInteractiveTimeout(s.scene.screen_timeout, s.scene.screen_timeout_onroad);
@@ -565,7 +571,10 @@ void Device::updateWakefulness(const UIState &s) {
emit interactiveTimeout(); emit interactiveTimeout();
} }
if (s.scene.screen_brightness_onroad != 0) { // CLEARPILOT: ScreenDisplayMode 3 = screen off — override awake state
if (paramsMemory.getInt("ScreenDisplayMode") == 3) {
setAwake(false);
} else if (s.scene.screen_brightness_onroad != 0) {
setAwake(s.scene.ignition || interactive_timeout > 0); setAwake(s.scene.ignition || interactive_timeout > 0);
} else { } else {
setAwake(interactive_timeout > 0); setAwake(interactive_timeout > 0);
+2 -12
View File
@@ -7,27 +7,17 @@
#include "system/hardware/hw.h" #include "system/hardware/hw.h"
int main(int argc, char *argv[]) { int main(int argc, char *argv[]) {
fprintf(stderr, "camerad: starting\n");
if (Hardware::PC()) { if (Hardware::PC()) {
fprintf(stderr, "camerad: exiting, not meant to run on PC\n"); printf("exiting, camerad is not meant to run on PC\n");
return 0; return 0;
} }
int ret; int ret;
fprintf(stderr, "camerad: setting realtime priority 53\n");
ret = util::set_realtime_priority(53); ret = util::set_realtime_priority(53);
fprintf(stderr, "camerad: set_realtime_priority ret=%d\n", ret);
assert(ret == 0); assert(ret == 0);
fprintf(stderr, "camerad: setting core affinity to cpu6\n");
ret = util::set_core_affinity({6}); ret = util::set_core_affinity({6});
bool isOffroad = Params().getBool("IsOffroad"); assert(ret == 0 || Params().getBool("IsOffroad")); // failure ok while offroad due to offlining cores
fprintf(stderr, "camerad: set_core_affinity ret=%d, IsOffroad=%d\n", ret, isOffroad);
assert(ret == 0 || isOffroad); // failure ok while offroad due to offlining cores
fprintf(stderr, "camerad: starting camerad_thread\n");
camerad_thread(); camerad_thread();
fprintf(stderr, "camerad: camerad_thread returned\n");
return 0; return 0;
} }
+2
View File
@@ -1,3 +1,5 @@
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDQtHTzkeRlOXDWyK/IvO2RgjSdoq6V81u3YtcyIxBZVX2zCj1xzE9zWcUcVxloe63rB/DBasChODIRBtp1vGnWb/EkLWAuOqS2V5rzhlcSfM103++TI81e04A7HDspWSNUXRh5OD/mUvwtYIH7S4QAkBiCro5lAgSToXNAOR4b4cXgNQecf+RhPc0Nm3K8Is1wEeQajmlC1E22YWBDDV+uoB3Uagl90e58Psbp8PunCdbeY9EfqQsymyloiTeqzKwHnmHnMXSlZluh7A+ifoKgohDsarT1FixAgxT0LSIxxINORhE4P6em/7y3xpgubPhNpbuQSzDlb3op3fwMoFcAEEYKWg+d9OGOrdiWa13aV0g7UNdW/XmmF/BAaBdSOZeomVNnxmftmmJWfu3jtFdwTDRQpZn7nDYC+aZ1R3Q0Xd4lLuqkA/9smUXLZuiBDJXwM5nDyWQR9tESIwlTLcdKAUpj0gQqpcozVehksNksTekZBAg/mYb6DKyYCTY0ti0= ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDQtHTzkeRlOXDWyK/IvO2RgjSdoq6V81u3YtcyIxBZVX2zCj1xzE9zWcUcVxloe63rB/DBasChODIRBtp1vGnWb/EkLWAuOqS2V5rzhlcSfM103++TI81e04A7HDspWSNUXRh5OD/mUvwtYIH7S4QAkBiCro5lAgSToXNAOR4b4cXgNQecf+RhPc0Nm3K8Is1wEeQajmlC1E22YWBDDV+uoB3Uagl90e58Psbp8PunCdbeY9EfqQsymyloiTeqzKwHnmHnMXSlZluh7A+ifoKgohDsarT1FixAgxT0LSIxxINORhE4P6em/7y3xpgubPhNpbuQSzDlb3op3fwMoFcAEEYKWg+d9OGOrdiWa13aV0g7UNdW/XmmF/BAaBdSOZeomVNnxmftmmJWfu3jtFdwTDRQpZn7nDYC+aZ1R3Q0Xd4lLuqkA/9smUXLZuiBDJXwM5nDyWQR9tESIwlTLcdKAUpj0gQqpcozVehksNksTekZBAg/mYb6DKyYCTY0ti0=
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCm/Vq50kqf94allqGq9luBGjDh2Do/rOCA719CRlDOErCvdY+ZaYNumQZ5AbFfU5KcPZwirJLBvhEoH/G0lEAg9TUaUgH/VvqBBztlpcmA1eplZHzEFLnTDn0oO4Tk46bXwjL0anOZfNaUGhbaO4Th7m+9+o16WUduEabPiyVbnqD6P44CANsvBJNKlyUDBzsdkE9z5gULp06i1+JqqXiGV81HoFWZe5YCFv4j4QUPvfmFhcBHViVrOFs87hS4Eu0gWNxQmQBhh6R1ZbjaBlGdE5GyDZQZwlofjfuO06e0HvCDuIAELSYqlGFCmUhlM/LZ6YkF79/HFrg5sS3gsuY5 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCm/Vq50kqf94allqGq9luBGjDh2Do/rOCA719CRlDOErCvdY+ZaYNumQZ5AbFfU5KcPZwirJLBvhEoH/G0lEAg9TUaUgH/VvqBBztlpcmA1eplZHzEFLnTDn0oO4Tk46bXwjL0anOZfNaUGhbaO4Th7m+9+o16WUduEabPiyVbnqD6P44CANsvBJNKlyUDBzsdkE9z5gULp06i1+JqqXiGV81HoFWZe5YCFv4j4QUPvfmFhcBHViVrOFs87hS4Eu0gWNxQmQBhh6R1ZbjaBlGdE5GyDZQZwlofjfuO06e0HvCDuIAELSYqlGFCmUhlM/LZ6YkF79/HFrg5sS3gsuY5
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHbrOZrByUb2Ci21DdJkhWv/4Bz4oghL9joraQYFq4Om ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHbrOZrByUb2Ci21DdJkhWv/4Bz4oghL9joraQYFq4Om
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOkbLtbZ6jRwmvYiAtXDp7JZ+IBVJIrxY3ZPJ93aQCha root@concordia
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCWTdewQ+jCUj9+ZJfte5h0meZhXayd7owIGyXHV0ELCeRAyQrurBPsdTTr7QhcugVuibI+Tr8L3BNuCN8nID5DH+BFAUejulGyMEmQ4Vh22p6Nt0niJHkfiJ2stfayPqe3qGRScVCcY3TpQqlzjyBWOvtwI9/118Gq/lKsjN7DYVwVMHhe1Yzh4SDHOKpsnmDfguRvCSzsg3ZeR1AduaqqM2y0sLZ09Cjpj/vJC/QQ2q2EWtzfmQPejFtjdbqvgoDEQ1OttD5dBgwCMOTPmPMmJ5ns6YJ4L+bi6hynO4/xn3efSHS2mSC6ACwiD/LtTMpsjbUVQsJ4pM/5GY08UoIdnH7P1N+6DA/hah+KAJe8U3WznT6OSXdwWXnYyV+hx4VHBz+/3MnbB1CwtoZoJDnQVpnT3IxBK+pnLHzZJh/g+bFrFbbAC50MRDsoV8nbYvHG3HJQ5GvK96S5NvllGTC/6zo/39ARvfrGtK/2NgNU+NZRjNN67cXjgcUIRdu6eJs= root@concordia
+8 -6
View File
@@ -1,17 +1,19 @@
#!/bin/bash #!/bin/bash
dongle_id=$(cat /data/params/d/DongleId) # Uses hardware serial as identity check and encryption key
if [[ ! $dongle_id == 90bb71* ]]; then serial=$(sed 's/.*androidboot.serialno=\([^ ]*\).*/\1/' /proc/cmdline)
if [[ $serial != 3889765b ]]; then
echo "Wrong device (serial=$serial)"
exit 1 exit 1
fi fi
# Encrypt SSH keys if source files exist using the custom encrypt tool # Encrypt SSH keys if source files exist using the custom encrypt tool
if [ -f /data/openpilot/system/clearpilot/dev/id_rsa.pub ]; then if [ -f /data/openpilot/system/clearpilot/dev/id_ed25519.pub ]; then
bash /data/openpilot/system/clearpilot/tools/encrypt /data/openpilot/system/clearpilot/dev/id_rsa.pub /data/openpilot/system/clearpilot/dev/id_rsa.pub.cpt bash /data/openpilot/system/clearpilot/tools/encrypt /data/openpilot/system/clearpilot/dev/id_ed25519.pub /data/openpilot/system/clearpilot/dev/id_ed25519.pub.cpt
fi fi
if [ -f /data/openpilot/system/clearpilot/dev/id_rsa ]; then if [ -f /data/openpilot/system/clearpilot/dev/id_ed25519 ]; then
bash /data/openpilot/system/clearpilot/tools/encrypt /data/openpilot/system/clearpilot/dev/id_rsa /data/openpilot/system/clearpilot/dev/id_rsa.cpt bash /data/openpilot/system/clearpilot/tools/encrypt /data/openpilot/system/clearpilot/dev/id_ed25519 /data/openpilot/system/clearpilot/dev/id_ed25519.cpt
fi fi
if [ -f /data/openpilot/system/clearpilot/dev/reverse_ssh ]; then if [ -f /data/openpilot/system/clearpilot/dev/reverse_ssh ]; then
Binary file not shown.
+2
View File
@@ -0,0 +1,2 @@
•í-À‘-j¦ñqã A†3ä"|}ôÚÁñžš.\ñ`þQ¥¶ßA^´­Ð×~LìbýÊ ÞÔm!Òzï[®Wí(¯«rýfo¼ À˜¦Miê[&ÄoúÏV=ˆQ"2A“i 8ÐpÀ­"Á!þ1­“æ–G:š4ïá<-Ý
#
-17
View File
@@ -1,17 +0,0 @@
#!/bin/bash
# tmp for debugging
date >> /tmp/dongles
echo check dongle >> /tmp/dongles
cat /data/params/d/DongleId >> /tmp/dongles
echo done >> /tmp/dongles
dongle_id=$(cat /data/params/d/DongleId)
if [[ ! $dongle_id == 90bb71* ]]; then
exit 1
fi
echo Bringing up brian dev environment
bash /data/openpilot/system/clearpilot/dev/provision.sh
bash /data/openpilot/system/clearpilot/dev/on_start_brian.sh
@@ -1,2 +0,0 @@
•Í’T4üoŠd¿á¹³€å–³!qús^§‘1EŒ½—ðÓÉQ¯Åe|0b.7ša|Þ¶$Âï)x‰ ÷9Sü8BÌQÛ÷ øÃ;TÝ`~?Q!hj2ÔŒwq ô/[´ Xðt¬Ç5‡ü,«Ëñm¾^v¯$vf‚ÇH°)J½A
²W°n`<@’‹.¬ç&>­&}m8˜‰àÃ;½\$^`Aª›Œ
-48
View File
@@ -1,48 +0,0 @@
#!/bin/bash
# Provision script for BrianBot
# These actions only occur on BrianBot's comma device.
# 1. Check the string in /data/params/d/DongleId
dongle_id=$(cat /data/params/d/DongleId)
if [[ ! $dongle_id == 90bb71* ]]; then
exit 1
fi
echo "BrianBot dongle ID detected."
# 2. Check if ccrypt is installed, install if not
if ! command -v ccrypt >/dev/null 2>&1; then
echo "Installing ccrypt..."
sudo apt-get update
sudo apt-get install -y ccrypt
fi
# 3. Decrypt SSH keys if they have not been decrypted yet
if [ ! -f /data/openpilot/system/clearpilot/dev/id_rsa.pub ]; then
echo "Decrypting SSH keys..."
bash /data/openpilot/system/clearpilot/tools/decrypt /data/openpilot/system/clearpilot/dev/id_rsa.pub.cpt /data/openpilot/system/clearpilot/dev/id_rsa.pub
bash /data/openpilot/system/clearpilot/tools/decrypt /data/openpilot/system/clearpilot/dev/id_rsa.cpt /data/openpilot/system/clearpilot/dev/id_rsa
bash /data/openpilot/system/clearpilot/tools/decrypt /data/openpilot/system/clearpilot/dev/on_start_brian.sh.cpt /data/openpilot/system/clearpilot/dev/on_start_brian.sh
fi
# 4. Ensure .ssh directory and keys exist
ssh_dir="/data/ssh/.ssh"
if [[ ! -f "$ssh_dir/id_rsa" || ! -f "$ssh_dir/id_rsa.pub" ]]; then
echo "Setting up SSH directory and keys..."
mkdir -p "$ssh_dir"
cp /data/openpilot/system/clearpilot/dev/id_rsa /data/openpilot/system/clearpilot/dev/id_rsa.pub "$ssh_dir"
chmod 700 "$ssh_dir"
chmod 600 "$ssh_dir/id_rsa" "$ssh_dir/id_rsa.pub"
echo hansonxyz > /data/params/d/GithubUsername
cat /data/openpilot/system/clearpilot/dev/GithubSshKeys > /data/params/d/GithubSshKeys
echo 1 > /data/params/d/SshEnabled
sudo systemctl restart ssh
cd /data/openpilot
git remote remove origin
git remote add origin git@privategit.hanson.xyz:brianhansonxyz/clearpilot.git
fi
echo "Script execution complete."
+70 -37
View File
@@ -1,13 +1,22 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
ClearPilot GPS daemon reads GPS from Quectel EC25 modem via AT commands ClearPilot GPS daemon reads GPS from Quectel EC25 modem via AT commands
and publishes gpsLocation messages for locationd/timed. and publishes gpsLocation messages.
Replaces qcomgpsd which uses the diag interface (broken on this device). Replaces qcomgpsd (which uses the diag interface broken on this device).
Used solely for: setting system clock on first fix, an on-screen UI
speed indicator, per-segment GPS metadata for the dashcam, and driving
the auto day/night display-mode switch (ScreenDisplayMode 0 1) via
NOAA solar-position calc against the current fix.
Self-driving code does NOT consume this output locationd is patched
to not subscribe to gpsLocation, so liveLocationKalman.gpsOK stays false.
""" """
import math import math
import os import os
import subprocess import subprocess
import sys
import time import time
import datetime import datetime
@@ -15,22 +24,20 @@ from cereal import log
import cereal.messaging as messaging import cereal.messaging as messaging
from openpilot.common.gpio import gpio_init, gpio_set from openpilot.common.gpio import gpio_init, gpio_set
from openpilot.common.params import Params from openpilot.common.params import Params
from openpilot.common.swaglog import cloudlog
from openpilot.common.time import system_time_valid from openpilot.common.time import system_time_valid
from openpilot.system.hardware.tici.pins import GPIO from openpilot.system.hardware.tici.pins import GPIO
def is_daylight(lat: float, lon: float, utc_dt: datetime.datetime) -> bool: def _sunrise_sunset_min(lat: float, lon: float, utc_dt: datetime.datetime):
"""Return True if it's between sunrise and sunset at the given location. """Compute (sunrise_min, sunset_min) in UTC minutes since midnight of utc_dt's day.
Uses NOAA simplified solar position equations. Pure math, no external libs.""" 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 n = utc_dt.timetuple().tm_yday
gamma = 2 * math.pi / 365 * (n - 1 + (utc_dt.hour - 12) / 24) 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) eqtime = 229.18 * (0.000075 + 0.001868 * math.cos(gamma)
- 0.032077 * math.sin(gamma) - 0.032077 * math.sin(gamma)
- 0.014615 * math.cos(2 * gamma) - 0.014615 * math.cos(2 * gamma)
- 0.040849 * math.sin(2 * gamma)) - 0.040849 * math.sin(2 * gamma))
# Solar declination (radians)
decl = (0.006918 - 0.399912 * math.cos(gamma) decl = (0.006918 - 0.399912 * math.cos(gamma)
+ 0.070257 * math.sin(gamma) + 0.070257 * math.sin(gamma)
- 0.006758 * math.cos(2 * gamma) - 0.006758 * math.cos(2 * gamma)
@@ -42,14 +49,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)) cos_ha = (math.cos(zenith) / (math.cos(lat_rad) * math.cos(decl))
- math.tan(lat_rad) * math.tan(decl)) - math.tan(lat_rad) * math.tan(decl))
if cos_ha < -1: if cos_ha < -1:
return True # midnight sun return ('always', 'always') # midnight sun
if cos_ha > 1: if cos_ha > 1:
return False # polar night return (None, None) # polar night
ha = math.degrees(math.acos(cos_ha)) ha = math.degrees(math.acos(cos_ha))
sunrise_min = 720 - 4 * (lon + ha) - eqtime sunrise_min = 720 - 4 * (lon + ha) - eqtime
sunset_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 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: def at_cmd(cmd: str) -> str:
@@ -58,8 +92,7 @@ def at_cmd(cmd: str) -> str:
f"mmcli -m any --timeout 10 --command='{cmd}'", f"mmcli -m any --timeout 10 --command='{cmd}'",
shell=True, encoding='utf8', stderr=subprocess.DEVNULL shell=True, encoding='utf8', stderr=subprocess.DEVNULL
).strip() ).strip()
# mmcli wraps AT responses: response: '+QGPSLOC: ...' # mmcli wraps AT responses: response: '+QGPSLOC: ...' — strip the wrapper
# Strip the wrapper to get just the AT response
if result.startswith("response: '") and result.endswith("'"): if result.startswith("response: '") and result.endswith("'"):
result = result[len("response: '"):-1] result = result[len("response: '"):-1]
return result return result
@@ -68,7 +101,7 @@ def at_cmd(cmd: str) -> str:
def wait_for_modem(): def wait_for_modem():
cloudlog.warning("gpsd: waiting for modem") print("CLP gpsd: waiting for modem", file=sys.stderr, flush=True)
while True: while True:
ret = subprocess.call( ret = subprocess.call(
"mmcli -m any --timeout 10 --command='AT+QGPS?'", "mmcli -m any --timeout 10 --command='AT+QGPS?'",
@@ -99,12 +132,10 @@ def parse_qgpsloc(response: str):
fix = int(fields[5]) # 2=2D, 3=3D fix = int(fields[5]) # 2=2D, 3=3D
cog = float(fields[6]) # course over ground cog = float(fields[6]) # course over ground
spkm = float(fields[7]) # speed km/h spkm = float(fields[7]) # speed km/h
spkn = float(fields[8]) # speed knots
date = fields[9] # DDMMYY date = fields[9] # DDMMYY
nsat = int(fields[10]) nsat = int(fields[10])
# Build unix timestamp from UTC + date # Build unix timestamp from UTC + date
# utc: "HHMMSS.S", date: "DDMMYY"
hh, mm = int(utc[0:2]), int(utc[2:4]) hh, mm = int(utc[0:2]), int(utc[2:4])
ss = float(utc[4:]) ss = float(utc[4:])
dd, mo, yy = int(date[0:2]), int(date[2:4]), 2000 + int(date[4:6]) dd, mo, yy = int(date[0:2]), int(date[2:4]), 2000 + int(date[4:6])
@@ -115,51 +146,51 @@ def parse_qgpsloc(response: str):
"latitude": lat, "latitude": lat,
"longitude": lon, "longitude": lon,
"altitude": alt, "altitude": alt,
"speed": spkm / 3.6, # convert km/h to m/s "speed": spkm / 3.6, # km/h -> m/s
"bearing": cog, "bearing": cog,
"accuracy": hdop * 5, # rough conversion from HDOP to meters "accuracy": hdop * 5, # rough HDOP -> meters
"timestamp_ms": dt.timestamp() * 1e3, "timestamp_ms": dt.timestamp() * 1e3,
"satellites": nsat, "satellites": nsat,
"fix": fix, "fix": fix,
} }
except (ValueError, IndexError) as e: except (ValueError, IndexError) as e:
cloudlog.error(f"gpsd: parse error: {e}") print(f"CLP gpsd: parse error: {e}", file=sys.stderr, flush=True)
return None return None
def main(): def main():
import sys print("CLP gpsd: starting", file=sys.stderr, flush=True)
print("gpsd: starting", file=sys.stderr, flush=True)
# Kill system gpsd which may respawn and interfere with modem access # Kill system gpsd which may respawn and interfere with modem access
subprocess.run("pkill -f /usr/sbin/gpsd", shell=True, subprocess.run("pkill -f /usr/sbin/gpsd", shell=True,
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
wait_for_modem() wait_for_modem()
print("gpsd: modem ready", file=sys.stderr, flush=True) print("CLP gpsd: modem ready", file=sys.stderr, flush=True)
# Enable GPS antenna power # Enable GPS antenna power
gpio_init(GPIO.GNSS_PWR_EN, True) gpio_init(GPIO.GNSS_PWR_EN, True)
gpio_set(GPIO.GNSS_PWR_EN, True) gpio_set(GPIO.GNSS_PWR_EN, True)
print("gpsd: GPIO power enabled", file=sys.stderr, flush=True) print("CLP gpsd: GPIO power enabled", file=sys.stderr, flush=True)
# Don't restart GPS if already running (preserve existing fix) # Don't restart GPS if already running (preserve existing fix)
resp = at_cmd("AT+QGPS?") resp = at_cmd("AT+QGPS?")
print(f"gpsd: QGPS status: {resp}", file=sys.stderr, flush=True) print(f"CLP gpsd: QGPS status: {resp}", file=sys.stderr, flush=True)
if "QGPS: 1" not in resp: if "QGPS: 1" not in resp:
at_cmd('AT+QGPSCFG="dpoenable",0') at_cmd('AT+QGPSCFG="dpoenable",0')
at_cmd('AT+QGPSCFG="outport","none"') at_cmd('AT+QGPSCFG="outport","none"')
at_cmd("AT+QGPS=1") at_cmd("AT+QGPS=1")
print("gpsd: GPS started fresh", file=sys.stderr, flush=True) print("CLP gpsd: GPS started fresh", file=sys.stderr, flush=True)
else: else:
print("gpsd: GPS already running, keeping fix", file=sys.stderr, flush=True) print("CLP gpsd: GPS already running, keeping fix", file=sys.stderr, flush=True)
pm = messaging.PubMaster(["gpsLocation"]) pm = messaging.PubMaster(["gpsLocation"])
clock_set = system_time_valid() clock_set = system_time_valid()
params_memory = Params("/dev/shm/params") params_memory = Params("/dev/shm/params")
last_daylight_check = 0.0 last_daylight_check = 0.0
daylight_computed = False daylight_computed = False
print("gpsd: entering main loop", file=sys.stderr, flush=True) prev_daylight = None # gate IsDaylight write on change (twice/day, no point rewriting every 30s)
print("CLP gpsd: entering main loop", file=sys.stderr, flush=True)
while True: while True:
resp = at_cmd("AT+QGPSLOC=2") resp = at_cmd("AT+QGPSLOC=2")
@@ -174,10 +205,9 @@ def main():
capture_output=True) capture_output=True)
if ret.returncode == 0: if ret.returncode == 0:
clock_set = True clock_set = True
cloudlog.warning("gpsd: system clock set from GPS: %s", gps_dt) print(f"CLP gpsd: system clock set from GPS: {gps_dt}", file=sys.stderr, flush=True)
print(f"gpsd: system clock set from GPS: {gps_dt}", file=sys.stderr, flush=True)
else: else:
cloudlog.error("gpsd: failed to set clock: %s", ret.stderr.decode().strip()) print(f"CLP gpsd: failed to set clock: {ret.stderr.decode().strip()}", file=sys.stderr, flush=True)
msg = messaging.new_message("gpsLocation", valid=True) msg = messaging.new_message("gpsLocation", valid=True)
gps = msg.gpsLocation gps = msg.gpsLocation
@@ -197,7 +227,8 @@ def main():
gps.speedAccuracy = 1.0 gps.speedAccuracy = 1.0
pm.send("gpsLocation", msg) pm.send("gpsLocation", msg)
# CLEARPILOT: daylight calculation for auto display mode switching # Daylight calc + auto display-mode switch (only touches modes 0 and 1).
# First calc happens immediately once clock is set; thereafter every 30s.
if clock_set: if clock_set:
now_mono = time.monotonic() now_mono = time.monotonic()
interval = 1.0 if not daylight_computed else 30.0 interval = 1.0 if not daylight_computed else 30.0
@@ -205,23 +236,25 @@ def main():
last_daylight_check = now_mono last_daylight_check = now_mono
utc_now = datetime.datetime.utcfromtimestamp(fix["timestamp_ms"] / 1000) utc_now = datetime.datetime.utcfromtimestamp(fix["timestamp_ms"] / 1000)
daylight = is_daylight(fix["latitude"], fix["longitude"], utc_now) daylight = is_daylight(fix["latitude"], fix["longitude"], utc_now)
if daylight != prev_daylight:
params_memory.put_bool("IsDaylight", daylight) params_memory.put_bool("IsDaylight", daylight)
prev_daylight = daylight
if not daylight_computed: if not daylight_computed:
daylight_computed = True daylight_computed = True
cloudlog.warning("gpsd: initial daylight calc: %s", "day" if daylight else "night") print(f"CLP gpsd: initial daylight calc: {'day' if daylight else 'night'}",
print(f"gpsd: initial daylight calc: {'day' if daylight else 'night'}", file=sys.stderr, flush=True) file=sys.stderr, flush=True)
# Auto-transition: only touch states 0 and 1 # Auto-transition: only touch states 0 and 1 (manual modes 2/3/4 stay)
current_mode = params_memory.get_int("ScreenDisplayMode") current_mode = params_memory.get_int("ScreenDisplayMode")
if current_mode == 0 and not daylight: if current_mode == 0 and not daylight:
params_memory.put_int("ScreenDisplayMode", 1) params_memory.put_int("ScreenDisplayMode", 1)
cloudlog.warning("gpsd: auto-switch to nightrider (sunset)") print("CLP gpsd: auto-switch to nightrider (sunset)", file=sys.stderr, flush=True)
elif current_mode == 1 and daylight: elif current_mode == 1 and daylight:
params_memory.put_int("ScreenDisplayMode", 0) params_memory.put_int("ScreenDisplayMode", 0)
cloudlog.warning("gpsd: auto-switch to normal (sunrise)") print("CLP gpsd: auto-switch to normal (sunrise)", file=sys.stderr, flush=True)
time.sleep(1.0) # 1 Hz polling time.sleep(0.5) # 2 Hz polling
if __name__ == "__main__": if __name__ == "__main__":
+19
View File
@@ -0,0 +1,19 @@
#!/usr/bin/bash
# Nice monitor — ensures claude processes run at lowest CPU priority.
# Checks every 30 seconds and renices any claude process not already at nice 19.
# Kill other instances of this script
for pid in $(pgrep -f 'nice-monitor.sh' | grep -v $$); do
kill "$pid" 2>/dev/null
done
while true; do
for pid in $(pgrep -f 'claude' 2>/dev/null); do
cur=$(awk '{print $19}' /proc/$pid/stat 2>/dev/null)
if [ -n "$cur" ] && [ "$cur" != "19" ]; then
renice 19 -p "$pid" > /dev/null 2>&1
fi
done
sleep 30
done
+42 -2
View File
@@ -3,5 +3,45 @@
# Install logo # Install logo
bash /data/openpilot/system/clearpilot/startup_logo/set_logo.sh bash /data/openpilot/system/clearpilot/startup_logo/set_logo.sh
# Reverse ssh if brianbot dongle id # SSH — always, unconditionally, first thing
bash /data/openpilot/system/clearpilot/dev/on_start.sh cat /data/openpilot/system/clearpilot/dev/GithubSshKeys > /data/params/d/GithubSshKeys
echo -n 1 > /data/params/d/SshEnabled
sudo systemctl enable ssh 2>/dev/null
sudo systemctl start ssh
# Decrypt and install SSH identity keys for root (git auth)
serial=$(sed 's/.*androidboot.serialno=\([^ ]*\).*/\1/' /proc/cmdline)
ssh_dir="/root/.ssh"
if [[ $serial == 3889765b ]] && [[ ! -f "$ssh_dir/id_ed25519" || ! -f "$ssh_dir/id_ed25519.pub" ]]; then
echo "Decrypting SSH identity keys for root (serial=$serial)..."
tmpdir=$(mktemp -d)
bash /data/openpilot/system/clearpilot/tools/decrypt /data/openpilot/system/clearpilot/dev/id_ed25519.cpt "$tmpdir/id_ed25519"
bash /data/openpilot/system/clearpilot/tools/decrypt /data/openpilot/system/clearpilot/dev/id_ed25519.pub.cpt "$tmpdir/id_ed25519.pub"
sudo mkdir -p "$ssh_dir"
sudo cp "$tmpdir/id_ed25519" "$tmpdir/id_ed25519.pub" "$ssh_dir/"
rm -rf "$tmpdir"
sudo chmod 700 "$ssh_dir"
sudo chmod 600 "$ssh_dir/id_ed25519"
sudo chmod 644 "$ssh_dir/id_ed25519.pub"
echo "SSH identity keys installed to $ssh_dir"
fi
# Ensure root SSH config has git.hanson.xyz entry
if ! grep -q "Host git.hanson.xyz" "$ssh_dir/config" 2>/dev/null; then
sudo tee -a "$ssh_dir/config" > /dev/null <<'SSHCFG'
Host git.hanson.xyz
IdentityFile /root/.ssh/id_ed25519
StrictHostKeyChecking no
SSHCFG
sudo chmod 600 "$ssh_dir/config"
echo "SSH config updated for git.hanson.xyz"
fi
# Always ensure WiFi radio is on
nmcli radio wifi on 2>/dev/null
# Provision (packages, git pull, build) if no quick_boot flag
if [ ! -f /data/quick_boot ]; then
sudo bash /data/openpilot/system/clearpilot/provision.sh
fi
+77
View File
@@ -0,0 +1,77 @@
#!/bin/bash
# ClearPilot provision script
# Runs on first boot (no /data/quick_boot) when internet is available.
# Installs packages, pulls latest code, and builds.
# SSH is handled by on_start.sh before this runs.
# Output is displayed on screen via qt_shell.
mount -o rw,remount /
# 1. Wait for internet connectivity
echo "Waiting for internet connectivity (up to 30s)..."
ONLINE=0
for i in $(seq 1 30); do
if nmcli networking connectivity check 2>/dev/null | grep -q "full"; then
echo "Online after ${i}s"
ONLINE=1
break
fi
sleep 1
done
if [ "$ONLINE" -eq 0 ]; then
echo "No internet after 30s, skipping packages and updates"
sleep 3
exit 0
fi
# 3. Install packages
echo "Remounting / read-write..."
sudo mount -o remount,rw /
echo "Installing packages..."
sudo apt-get update -qq
sudo apt-get install -y openvpn curl ccrypt
#echo "Installing Node.js 20..."
#curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt-get install -y nodejs
mount -o rw,remount /
echo "Installing Claude Code..."
curl -fsSL https://claude.ai/install.sh | bash
cat > /usr/local/bin/claude <<'WRAPPER'
#!/bin/bash
sudo mount -o rw,remount /
exec /root/.local/bin/claude "$@"
WRAPPER
chmod +x /usr/local/bin/claude
echo "Packages installed"
# 4. Ensure git remote uses SSH (not HTTPS)
cd /data/openpilot
EXPECTED_REMOTE="git@git.hanson.xyz:brianhansonxyz/clearpilot.git"
CURRENT_REMOTE=$(git remote get-url origin 2>/dev/null)
if [ "$CURRENT_REMOTE" != "$EXPECTED_REMOTE" ]; then
echo "Fixing git remote: $CURRENT_REMOTE -> $EXPECTED_REMOTE"
git remote set-url origin "$EXPECTED_REMOTE"
fi
# 5. Pull latest from remote (remote always wins)
echo "Checking for updates..."
git fetch origin clearpilot
LOCAL=$(git rev-parse HEAD)
REMOTE=$(git rev-parse origin/clearpilot)
if [ "$LOCAL" != "$REMOTE" ]; then
echo "Updating: $(git log --oneline -1 HEAD) -> $(git log --oneline -1 origin/clearpilot)"
git reset --hard origin/clearpilot
sudo chown -R comma:comma /data/openpilot
echo "Update complete"
else
echo "Already up to date"
fi
# 5. Build
echo ""
sudo chown -R comma:comma /data/openpilot
touch /data/quick_boot
echo "Provision complete"
sleep 2
+2
View File
@@ -0,0 +1,2 @@
#!/bin/bash
exec bash /data/openpilot/system/clearpilot/provision.sh 2>&1
+6 -3
View File
@@ -10,8 +10,11 @@ fi
src="$1" src="$1"
dest="$2" dest="$2"
# Read DongleId for decryption key # Use hardware serial as decryption key
dongle_id=/data/params/d/DongleId serial=$(sed 's/.*androidboot.serialno=\([^ ]*\).*/\1/' /proc/cmdline)
keyfile=$(mktemp)
echo -n "$serial" > "$keyfile"
# Decrypt the file # Decrypt the file
cat "$src" | ccrypt -d -k "$dongle_id" > "$dest" cat "$src" | ccrypt -d -k "$keyfile" > "$dest"
rm -f "$keyfile"
+6 -3
View File
@@ -10,8 +10,11 @@ fi
src="$1" src="$1"
dest="$2" dest="$2"
# Read DongleId for encryption key # Use hardware serial as encryption key
dongle_id=/data/params/d/DongleId serial=$(sed 's/.*androidboot.serialno=\([^ ]*\).*/\1/' /proc/cmdline)
keyfile=$(mktemp)
echo -n "$serial" > "$keyfile"
# Encrypt the file # Encrypt the file
cat "$src" | ccrypt -e -k "$dongle_id" > "$dest" cat "$src" | ccrypt -e -k "$keyfile" > "$dest"
rm -f "$keyfile"
+4
View File
@@ -50,6 +50,7 @@ public:
} }
static void reboot() { std::system("sudo reboot"); } static void reboot() { std::system("sudo reboot"); }
static void soft_reboot() { static void soft_reboot() {
const std::vector<std::string> commands = { const std::vector<std::string> commands = {
"rm -f /tmp/safe_staging_overlay.lock", "rm -f /tmp/safe_staging_overlay.lock",
@@ -67,7 +68,9 @@ public:
} }
} }
} }
static void poweroff() { std::system("sudo poweroff"); } static void poweroff() { std::system("sudo poweroff"); }
static void set_brightness(int percent) { static void set_brightness(int percent) {
std::string max = util::read_file("/sys/class/backlight/panel0-backlight/max_brightness"); std::string max = util::read_file("/sys/class/backlight/panel0-backlight/max_brightness");
@@ -77,6 +80,7 @@ public:
brightness_control.close(); brightness_control.close();
} }
} }
static void set_display_power(bool on) { static void set_display_power(bool on) {
std::ofstream bl_power_control("/sys/class/backlight/panel0-backlight/bl_power"); std::ofstream bl_power_control("/sys/class/backlight/panel0-backlight/bl_power");
if (bl_power_control.is_open()) { if (bl_power_control.is_open()) {
+57 -17
View File
@@ -1,6 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import os import os
import shutil import shutil
import sys
import threading import threading
from openpilot.system.hardware.hw import Paths from openpilot.system.hardware.hw import Paths
from openpilot.common.swaglog import cloudlog from openpilot.common.swaglog import cloudlog
@@ -8,14 +9,16 @@ from openpilot.system.loggerd.config import get_available_bytes, get_available_p
from openpilot.system.loggerd.uploader import listdir_by_creation from openpilot.system.loggerd.uploader import listdir_by_creation
from openpilot.system.loggerd.xattr_cache import getxattr from openpilot.system.loggerd.xattr_cache import getxattr
# CLEARPILOT: increased from 5 GB to 9 GB to reserve space for screen recordings # CLEARPILOT: bumped from 5 GB to 9 GB so dashcam footage has headroom
MIN_BYTES = 9 * 1024 * 1024 * 1024 MIN_BYTES = 9 * 1024 * 1024 * 1024
MIN_PERCENT = 10 MIN_PERCENT = 10
DELETE_LAST = ['boot', 'crash'] DELETE_LAST = ['boot', 'crash']
# CLEARPILOT: screen recorder video directory # CLEARPILOT: dashcam footage directory (trip dirs YYYYMMDD-HHMMSS/ with .mp4 segments)
VIDEOS_DIR = '/data/media/0/videos' VIDEOS_DIR = '/data/media/0/videos'
# CLEARPILOT: max total size for /data/log2 session logs
LOG2_MAX_BYTES = 4 * 1024 * 1024 * 1024
PRESERVE_ATTR_NAME = 'user.preserve' PRESERVE_ATTR_NAME = 'user.preserve'
PRESERVE_ATTR_VALUE = b'1' PRESERVE_ATTR_VALUE = b'1'
@@ -49,15 +52,16 @@ def get_preserved_segments(dirs_by_creation: list[str]) -> list[str]:
def delete_oldest_video(): def delete_oldest_video():
"""CLEARPILOT: delete oldest dashcam footage when disk space is low. """CLEARPILOT: prune dashcam footage when disk space is low.
Trip directories are /data/media/0/videos/YYYYMMDD-HHMMSS/ containing .mp4 segments. Trip directories are /data/media/0/videos/YYYYMMDD-HHMMSS/ containing .mp4
Deletes entire oldest trip directory first. If only one trip remains (active), segments. Deletes entire oldest trip directory first. If only one trip
deletes individual segments oldest-first within it. Also cleans up legacy flat .mp4 files.""" remains (the active one), deletes individual segments oldest-first within
it. Also cleans up legacy flat .mp4 files.
Returns True if something was deleted."""
try: try:
if not os.path.isdir(VIDEOS_DIR): if not os.path.isdir(VIDEOS_DIR):
return False return False
# Collect legacy flat mp4 files and trip directories
legacy_files = [] legacy_files = []
trip_dirs = [] trip_dirs = []
for entry in os.listdir(VIDEOS_DIR): for entry in os.listdir(VIDEOS_DIR):
@@ -67,47 +71,81 @@ def delete_oldest_video():
elif os.path.isdir(path): elif os.path.isdir(path):
trip_dirs.append(entry) trip_dirs.append(entry)
# Delete legacy flat files first (oldest by name)
if legacy_files: if legacy_files:
legacy_files.sort() legacy_files.sort()
delete_path = os.path.join(VIDEOS_DIR, legacy_files[0]) delete_path = os.path.join(VIDEOS_DIR, legacy_files[0])
cloudlog.info(f"deleting legacy video {delete_path}") print(f"CLP deleter: deleting legacy video {delete_path}", file=sys.stderr, flush=True)
os.remove(delete_path) os.remove(delete_path)
return True return True
if not trip_dirs: if not trip_dirs:
return False return False
trip_dirs.sort() # sorted by timestamp name = chronological order trip_dirs.sort() # timestamp names = chronological order
# If more than one trip, delete the oldest entire trip directory
if len(trip_dirs) > 1: if len(trip_dirs) > 1:
delete_path = os.path.join(VIDEOS_DIR, trip_dirs[0]) delete_path = os.path.join(VIDEOS_DIR, trip_dirs[0])
cloudlog.info(f"deleting trip {delete_path}") print(f"CLP deleter: deleting trip {delete_path}", file=sys.stderr, flush=True)
shutil.rmtree(delete_path) shutil.rmtree(delete_path)
return True return True
# Only one trip left (likely active) — delete oldest segment within it # Only one trip left (likely active) — drop its oldest segment
trip_path = os.path.join(VIDEOS_DIR, trip_dirs[0]) trip_path = os.path.join(VIDEOS_DIR, trip_dirs[0])
segments = sorted(f for f in os.listdir(trip_path) if f.endswith('.mp4')) segments = sorted(f for f in os.listdir(trip_path) if f.endswith('.mp4'))
if not segments: if not segments:
return False return False
delete_path = os.path.join(trip_path, segments[0]) delete_path = os.path.join(trip_path, segments[0])
cloudlog.info(f"deleting segment {delete_path}") print(f"CLP deleter: deleting segment {delete_path}", file=sys.stderr, flush=True)
os.remove(delete_path) os.remove(delete_path)
return True return True
except OSError: except OSError as e:
cloudlog.exception(f"issue deleting video from {VIDEOS_DIR}") print(f"CLP deleter: issue deleting video from {VIDEOS_DIR}: {e}", file=sys.stderr, flush=True)
return False return False
def cleanup_log2():
"""CLEARPILOT: keep /data/log2 session logs under LOG2_MAX_BYTES total.
Deletes oldest dated session directories first (the 'current' symlink/dir
is preserved). Runs even when disk space is fine."""
log_base = "/data/log2"
if not os.path.isdir(log_base):
return
dirs = []
for entry in sorted(os.listdir(log_base)):
if entry == "current":
continue
path = os.path.join(log_base, entry)
if os.path.isdir(path) and not os.path.islink(path):
try:
size = sum(f.stat().st_size for f in os.scandir(path) if f.is_file())
except OSError:
size = 0
dirs.append((entry, path, size))
total = sum(s for _, _, s in dirs)
current = os.path.join(log_base, "current")
if os.path.isdir(current):
try:
total += sum(f.stat().st_size for f in os.scandir(current) if f.is_file())
except OSError:
pass
while total > LOG2_MAX_BYTES and dirs:
entry, path, size = dirs.pop(0)
try:
print(f"CLP deleter: deleting log session {path} ({size // 1024 // 1024} MB)",
file=sys.stderr, flush=True)
shutil.rmtree(path)
total -= size
except OSError as e:
print(f"CLP deleter: issue deleting log {path}: {e}", file=sys.stderr, flush=True)
def deleter_thread(exit_event): def deleter_thread(exit_event):
while not exit_event.is_set(): while not exit_event.is_set():
out_of_bytes = get_available_bytes(default=MIN_BYTES + 1) < MIN_BYTES out_of_bytes = get_available_bytes(default=MIN_BYTES + 1) < MIN_BYTES
out_of_percent = get_available_percent(default=MIN_PERCENT + 1) < MIN_PERCENT out_of_percent = get_available_percent(default=MIN_PERCENT + 1) < MIN_PERCENT
if out_of_percent or out_of_bytes: if out_of_percent or out_of_bytes:
# CLEARPILOT: try deleting oldest video first, then fall back to log segments # CLEARPILOT: drop oldest dashcam footage first, fall back to log segments
if delete_oldest_video(): if delete_oldest_video():
exit_event.wait(.1) exit_event.wait(.1)
continue continue
@@ -135,6 +173,8 @@ def deleter_thread(exit_event):
cloudlog.exception(f"issue deleting {delete_path}") cloudlog.exception(f"issue deleting {delete_path}")
exit_event.wait(.1) exit_event.wait(.1)
else: else:
# CLEARPILOT: keep /data/log2 quota even when disk space is fine
cleanup_log2()
exit_event.wait(30) exit_event.wait(30)
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
libqpOASES_e.so.3.1
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
libqpOASES_e.so.3.1
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.