15 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
47 changed files with 3079 additions and 779 deletions
+266
View File
@@ -0,0 +1,266 @@
# ClearPilot — CLAUDE.md
## Project Overview
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.
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.
## Current State
`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
### CRITICAL: Change Control
This is self-driving software. All changes must be deliberate and well-understood.
- **NEVER make changes outside of what is explicitly requested**
- **Always explain proposed changes first** — describe the change, the logic, and the architecture; let Brian review and approve before writing any code
- **Brian is an expert on this software** — do not override his judgment or assume responsibility for changes he doesn't understand
- **Every line must be understood** — work slowly and deliberately
- **Test everything thoroughly** — Brian must always be in the loop
- **Do not refactor, clean up, or "improve" code beyond the specific request**
### Logging
**NEVER use `cloudlog`.** It's comma.ai's cloud telemetry pipeline, not ours — writes go to a publisher that's effectively a black hole for us (and the only thing it could do if ever reachable is bother the upstream FrogPilot developer). Our changes must always use **file logging** instead.
Use `print(..., file=sys.stderr, flush=True)`. 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
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:
```bash
chown -R comma:comma /data/openpilot
```
### Git
- Remote: `git@git.hanson.xyz:brianhansonxyz/clearpilot.git`
- 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)
- 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
- Share name: `openpilot` (e.g. `\\comma-3889765b\openpilot`)
- Path: `/data/openpilot`
- Username: `comma`
- Password: `i-like-to-drive-cars`
- Runs as `comma:comma` via force user/group — files created over SMB are owned correctly
- Enabled at boot (`smbd` + `nmbd`)
### Testing Changes
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
# 1. Fix ownership
chown -R comma:comma /data/openpilot
# 2. Build (kills running manager, removes prebuilt, compiles, exits)
# build_only.sh tees output to /tmp/build.log and propagates the build's
# 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"
# 3. If build succeeded ($? == 0), start openpilot
su - comma -c "bash /data/openpilot/launch_openpilot.sh"
# 4. Inspect logs
ls /data/log2/current/
cat /data/log2/current/session.log
tail /tmp/build.log # last build's output
```
### Adding New Params
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)
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
### Memory Params (paramsMemory)
ClearPilot uses memory params (`/dev/shm/params/d/`) for transient state that should reset on boot. Conventions:
- **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.
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.
### Changing a Service's Publish Rate
SubMaster's `freq_ok` check requires observed rate to fall within `[0.8 × min_freq, 1.2 × max_freq]` of the value declared in `cereal/services.py`. Publishing *faster* than declared 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.
## Device: comma 3x
- Qualcomm Snapdragon SoC (aarch64), serial `comma-3889765b`
- Storage: WDC SDINDDH4-128G, 128 GB UFS 2.1
- 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
### Filesystem mount quirks
| Mount | Device | Type | Notes |
|---|---|---|---|
| `/` | /dev/sda7 | ext4 | rw |
| `/data` | /dev/sda12 | ext4 | **persistent** — openpilot lives here |
| `/home` | overlay | overlayfs | **volatile** (upper on tmpfs) — changes lost on reboot |
| `/tmp` | tmpfs | tmpfs | volatile |
| `/persist` | /dev/sda2 | ext4 | persistent config/certs, noexec |
| `/dsp` | /dev/sde26 | ext4 | **read-only** Qualcomm DSP firmware |
| `/firmware` | /dev/sde4 | vfat | **read-only** firmware blobs |
### GPS
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.
### Sidebar / Process Notes
- `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
```
Power On
→ systemd: comma.service (runs as comma user)
→ /usr/comma/comma.sh (waits for Weston, handles factory reset)
→ /data/continue.sh
→ /data/openpilot/launch_openpilot.sh
→ kill stale instances (launch_openpilot, launch_chffrplus, manager.py, ./ui, _text by comm)
→ bash system/clearpilot/on_start.sh (SSH, WiFi, run provision.sh)
→ background system/clearpilot/vpn-monitor.sh
→ background system/clearpilot/nice-monitor.sh
→ exec ./launch_chffrplus.sh
→ source launch_env.sh
→ run agnos_init
→ set PYTHONPATH
→ if no `prebuilt`: run build.py (spinner + scons)
→ exec selfdrive/manager/manager.py
→ manager_init() sets default params (persistent + memory)
→ ensure_running() loop starts managed processes
```
+4 -1
View File
@@ -10,8 +10,11 @@ BASEDIR="/data/openpilot"
# Fix ownership — we edit as root, openpilot builds/runs as comma
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 -x _text 2>/dev/null
pkill -9 -f 'launch_openpilot.sh' 2>/dev/null
pkill -9 -f 'launch_chffrplus.sh' 2>/dev/null
pkill -9 -f 'python.*manager.py' 2>/dev/null
+24
View File
@@ -243,6 +243,30 @@ std::unordered_map<std::string, uint32_t> keys = {
{"CPTLkasButtonAction", PERSISTENT},
{"ScreenDisplayMode", PERSISTENT},
// 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},
{"ModelDist", PERSISTENT},
+4 -1
View File
@@ -79,8 +79,11 @@ function launch {
agnos_init
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 -x _text 2>/dev/null
# write tmux scrollback to a file
tmux capture-pane -pq -S-1000 > /tmp/launch_log
+3
View File
@@ -13,6 +13,9 @@ pkill -9 -f 'selfdrive\.' 2>/dev/null
pkill -9 -f 'system\.' 2>/dev/null
pkill -9 -f './ui' 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
# CLEARPILOT: ensure params persistence dir is owned by comma:comma. Editing
+2 -1
View File
@@ -3,4 +3,5 @@ SConscript(['controls/lib/lateral_mpc_lib/SConscript'])
SConscript(['controls/lib/longitudinal_mpc_lib/SConscript'])
SConscript(['locationd/SConscript'])
SConscript(['modeld/SConscript'])
SConscript(['ui/SConscript'])
SConscript(['ui/SConscript'])
SConscript(['clearpilot/SConscript'])
+16
View File
@@ -0,0 +1,16 @@
Import('env', 'arch', 'common', 'messaging', 'visionipc', 'cereal')
clearpilot_env = env.Clone()
clearpilot_env['CPPPATH'] += ['#selfdrive/frogpilot/screenrecorder/openmax/include/']
# Disable --as-needed so static lib ordering doesn't matter
clearpilot_env['LINKFLAGS'] = [f for f in clearpilot_env.get('LINKFLAGS', []) if f != '-Wl,--as-needed']
if arch == "larch64":
omx_obj = File('#selfdrive/frogpilot/screenrecorder/omx_encoder.o')
clearpilot_env.Program(
'dashcamd',
['dashcamd.cc', omx_obj],
LIBS=[common, 'json11', cereal, visionipc, messaging,
'zmq', 'capnp', 'kj', 'm', 'OpenCL', 'ssl', 'crypto', 'pthread',
'OmxCore', 'avformat', 'avcodec', 'avutil', 'yuv']
)
BIN
View File
Binary file not shown.
+409
View File
@@ -0,0 +1,409 @@
/*
* CLEARPILOT dashcamd — records raw camera footage to MP4 using OMX H.264 hardware encoder.
*
* Connects to camerad via VisionIPC, receives NV12 frames, and feeds them directly
* to the Qualcomm OMX encoder. Produces 3-minute MP4 segments organized by trip.
*
* Trip directory structure:
* /data/media/0/videos/YYYYMMDD-HHMMSS/ (trip directory, named at trip start)
* YYYYMMDD-HHMMSS.mp4 (3-minute segments)
* YYYYMMDD-HHMMSS.srt (GPS subtitle sidecar)
*
* Trip lifecycle state machine:
*
* WAITING:
* - Process starts in this state
* - Waits for valid system time (year >= 2024) AND car in drive gear
* - Transitions to RECORDING when both conditions met
*
* RECORDING:
* - Actively encoding frames, car is in drive
* - Car leaves drive → start 10-min idle timer → IDLE_TIMEOUT
*
* IDLE_TIMEOUT:
* - Car left drive, still recording with timer running
* - Car re-enters drive → cancel timer → RECORDING
* - Timer expires → close trip → WAITING
* - Ignition off → close trip → WAITING
*
* Graceful shutdown (DashcamShutdown param):
* - thermald sets DashcamShutdown="1" before device power-off
* - dashcamd closes current segment, acks, exits
*
* Published params (memory, every 5s):
* - DashcamState: "waiting" or "recording"
* - DashcamFrames: per-trip encoded frame count (resets each trip)
*/
#include <cstdio>
#include <ctime>
#include <string>
#include <errno.h>
#include <signal.h>
#include <sched.h>
#include <execinfo.h>
#include <sys/stat.h>
#include <sys/resource.h>
#include <unistd.h>
#include "cereal/messaging/messaging.h"
#include "cereal/visionipc/visionipc_client.h"
#include "common/params.h"
#include "common/timing.h"
#include "common/swaglog.h"
#include "common/util.h"
#include "selfdrive/frogpilot/screenrecorder/omx_encoder.h"
const std::string VIDEOS_BASE = "/data/media/0/videos";
const int SEGMENT_SECONDS = 180; // 3 minutes
const int SOURCE_FPS = 20;
const int CAMERA_FPS = 10;
const int FRAMES_PER_SEGMENT = SEGMENT_SECONDS * CAMERA_FPS;
const int BITRATE = 2500 * 1024; // 2500 kbps
const double IDLE_TIMEOUT_SECONDS = 600.0; // 10 minutes
ExitHandler do_exit;
enum TripState {
WAITING, // no trip, waiting for valid time + drive gear
RECORDING, // actively recording, car in drive
IDLE_TIMEOUT, // car left drive, recording with 10-min timer
};
static std::string make_timestamp() {
char buf[64];
time_t t = time(NULL);
struct tm tm = *localtime(&t);
snprintf(buf, sizeof(buf), "%04d%02d%02d-%02d%02d%02d",
tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday,
tm.tm_hour, tm.tm_min, tm.tm_sec);
return std::string(buf);
}
static bool system_time_valid() {
time_t t = time(NULL);
struct tm tm = *gmtime(&t);
return (tm.tm_year + 1900) >= 2024;
}
static std::string make_utc_timestamp() {
char buf[32];
time_t t = time(NULL);
struct tm tm = *gmtime(&t);
snprintf(buf, sizeof(buf), "%04d-%02d-%02d %02d:%02d:%02d UTC",
tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday,
tm.tm_hour, tm.tm_min, tm.tm_sec);
return std::string(buf);
}
// Format SRT timestamp: HH:MM:SS,mmm
static std::string srt_time(int seconds) {
int h = seconds / 3600;
int m = (seconds % 3600) / 60;
int s = seconds % 60;
char buf[16];
snprintf(buf, sizeof(buf), "%02d:%02d:%02d,000", h, m, s);
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[]) {
signal(SIGSEGV, crash_handler);
signal(SIGABRT, crash_handler);
setpriority(PRIO_PROCESS, 0, -10);
// CLEARPILOT: pin to cores 0-3 (little cluster). Avoids cache/memory-bandwidth
// contention with the RT-pinned processes on the big cluster:
// core 4 = controlsd, core 5 = plannerd/radard, core 7 = modeld.
// OMX offloads actual H.264 work to hardware, so the main thread just copies
// frames and muxes MP4 — fine on the little cluster.
cpu_set_t mask;
CPU_ZERO(&mask);
for (int i = 0; i < 4; i++) CPU_SET(i, &mask);
if (sched_setaffinity(0, sizeof(mask), &mask) != 0) {
LOGW("dashcamd: sched_setaffinity failed (%d), continuing unpinned", errno);
} else {
LOGW("dashcamd: pinned to cores 0-3");
}
// Ensure base output directory exists
mkdir(VIDEOS_BASE.c_str(), 0755);
LOGW("dashcamd: started, connecting to camerad road stream");
VisionIpcClient vipc("camerad", VISION_STREAM_ROAD, false);
while (!do_exit && !vipc.connect(false)) {
usleep(100000);
}
if (do_exit) return 0;
LOGW("dashcamd: vipc connected, waiting for valid frame");
// Wait for a frame with valid dimensions (camerad may still be initializing)
VisionBuf *init_buf = nullptr;
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;
LOGW("dashcamd: first valid frame %dx%d stride=%d", width, height, y_stride);
// Subscribe to carState (gear), deviceState (ignition), gpsLocation (subtitles)
SubMaster sm({"carState", "deviceState", "gpsLocation"});
Params params;
Params params_memory("/dev/shm/params");
// Trip state
TripState state = WAITING;
OmxEncoder *encoder = nullptr;
std::string trip_dir;
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;
double idle_timer_start = 0.0;
// SRT subtitle state
FILE *srt_file = nullptr;
int srt_index = 0;
int srt_segment_sec = 0;
double last_srt_write = 0;
// Ignition tracking
bool prev_started = false;
bool started_initialized = false;
// Param publish throttle
int param_check_counter = 0;
double last_param_write = 0;
// 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 = [&]() {
trip_dir = VIDEOS_BASE + "/" + make_timestamp();
mkdir(trip_dir.c_str(), 0755);
LOGW("dashcamd: new trip %s", trip_dir.c_str());
encoder = new OmxEncoder(trip_dir.c_str(), width, height, CAMERA_FPS, BITRATE);
std::string seg_name = make_timestamp();
LOGW("dashcamd: opening segment %s", seg_name.c_str());
encoder->encoder_open((seg_name + ".mp4").c_str());
std::string srt_path = trip_dir + "/" + seg_name + ".srt";
srt_file = fopen(srt_path.c_str(), "w");
srt_index = 0;
srt_segment_sec = 0;
last_srt_write = 0;
frame_count = 0;
trip_frames = 0;
segment_start_ts = nanos_since_boot();
state = RECORDING;
params_memory.put("DashcamState", "recording");
params_memory.put("DashcamFrames", "0");
};
// Helper: close current trip
auto close_trip = [&]() {
if (srt_file) { fclose(srt_file); srt_file = nullptr; }
if (encoder) {
encoder->encoder_close();
LOGW("dashcamd: segment closed");
delete encoder;
encoder = nullptr;
}
state = WAITING;
frame_count = 0;
trip_frames = 0;
idle_timer_start = 0.0;
LOGW("dashcamd: trip ended, returning to WAITING");
params_memory.put("DashcamState", "waiting");
params_memory.put("DashcamFrames", "0");
};
while (!do_exit) {
VisionBuf *buf = vipc.recv();
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);
double now = nanos_since_boot() / 1e9;
// Read vehicle state
bool started = sm.valid("deviceState") &&
sm["deviceState"].getDeviceState().getStarted();
auto gear = sm.valid("carState") ?
sm["carState"].getCarState().getGearShifter() :
cereal::CarState::GearShifter::UNKNOWN;
bool in_drive = (gear == cereal::CarState::GearShifter::DRIVE ||
gear == cereal::CarState::GearShifter::SPORT ||
gear == cereal::CarState::GearShifter::LOW ||
gear == cereal::CarState::GearShifter::MANUMATIC);
// Detect ignition off → close any active trip
if (started_initialized && prev_started && !started) {
LOGW("dashcamd: ignition off");
if (state == RECORDING || state == IDLE_TIMEOUT) {
close_trip();
}
}
prev_started = started;
started_initialized = true;
// Check for graceful shutdown request (every ~1 second)
if (++param_check_counter >= CAMERA_FPS) {
param_check_counter = 0;
if (params_memory.getBool("DashcamShutdown")) {
LOGW("dashcamd: shutdown requested");
if (state == RECORDING || state == IDLE_TIMEOUT) {
close_trip();
}
params_memory.putBool("DashcamShutdown", false);
LOGW("dashcamd: shutdown ack sent, exiting");
break;
}
}
// State machine
switch (state) {
case WAITING: {
bool has_gps = sm.valid("gpsLocation") && sm["gpsLocation"].getGpsLocation().getHasFix();
if (in_drive && system_time_valid() && has_gps) {
start_new_trip();
}
break;
}
case RECORDING:
if (!in_drive) {
idle_timer_start = now;
state = IDLE_TIMEOUT;
LOGW("dashcamd: car left drive, starting 10-min idle timer");
}
break;
case IDLE_TIMEOUT:
if (in_drive) {
idle_timer_start = 0.0;
state = RECORDING;
LOGW("dashcamd: back in drive, resuming trip");
} else if ((now - idle_timer_start) >= IDLE_TIMEOUT_SECONDS) {
LOGW("dashcamd: idle timeout expired");
close_trip();
}
break;
}
// Only encode frames when we have an active recording
if (state != RECORDING && state != IDLE_TIMEOUT) continue;
// Segment rotation
if (frame_count >= FRAMES_PER_SEGMENT) {
if (srt_file) { fclose(srt_file); srt_file = nullptr; }
encoder->encoder_close();
std::string seg_name = make_timestamp();
LOGW("dashcamd: opening segment %s", seg_name.c_str());
encoder->encoder_open((seg_name + ".mp4").c_str());
std::string srt_path = trip_dir + "/" + seg_name + ".srt";
srt_file = fopen(srt_path.c_str(), "w");
srt_index = 0;
srt_segment_sec = 0;
last_srt_write = 0;
frame_count = 0;
segment_start_ts = nanos_since_boot();
}
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
encoder->encode_frame_nv12(buf->y, y_stride, buf->uv, uv_stride, width, height, ts);
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
if (srt_file && (now - last_srt_write) >= 1.0) {
last_srt_write = now;
srt_index++;
double lat = 0, lon = 0, speed_ms = 0;
bool has_gps = sm.valid("gpsLocation") && sm["gpsLocation"].getGpsLocation().getHasFix();
if (has_gps) {
auto gps = sm["gpsLocation"].getGpsLocation();
lat = gps.getLatitude();
lon = gps.getLongitude();
speed_ms = gps.getSpeed();
}
double speed_mph = speed_ms * 2.23694;
std::string utc = make_utc_timestamp();
std::string t_start = srt_time(srt_segment_sec);
std::string t_end = srt_time(srt_segment_sec + 1);
srt_segment_sec++;
if (has_gps) {
const char *deg = "\xC2\xB0"; // UTF-8 degree sign
fprintf(srt_file, "%d\n%s --> %s\n%.0f MPH | %.4f%s%c %.4f%s%c | %s\n\n",
srt_index, t_start.c_str(), t_end.c_str(),
speed_mph,
std::abs(lat), deg, lat >= 0 ? 'N' : 'S',
std::abs(lon), deg, lon >= 0 ? 'E' : 'W',
utc.c_str());
} else {
fprintf(srt_file, "%d\n%s --> %s\nNo GPS | %s\n\n",
srt_index, t_start.c_str(), t_end.c_str(), utc.c_str());
}
fflush(srt_file);
}
}
// Clean exit
if (state == RECORDING || state == IDLE_TIMEOUT) {
close_trip();
}
params_memory.put("DashcamState", "stopped");
params_memory.put("DashcamFrames", "0");
LOGW("dashcamd: stopped");
return 0;
}
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()
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

+28 -23
View File
@@ -77,7 +77,9 @@ class Controls:
self.params_storage = Params("/persist/params")
self.params_memory.put_bool("CPTLkasButtonAction", False)
self.params_memory.put_bool("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
@@ -1233,32 +1235,35 @@ class Controls:
self.frogpilot_variables.use_ev_tables = self.params.get_bool("EVTable")
def update_clearpilot_events(self, CS):
if (len(CS.buttonEvents) > 0):
if (len(CS.buttonEvents) > 0):
print (CS.buttonEvents)
# Uncomment to alert when lkas button pressed
# 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):
if any(be.pressed and be.type == FrogPilotButtonType.lkas for be in CS.buttonEvents):
# Uncomment for debug testing.
# if self.params_memory.get_bool("CPTLkasButtonAction"):
# self.params_memory.put_bool("CPTLkasButtonAction", False)
# else:
# self.params_memory.put_bool("CPTLkasButtonAction", True)
# Rotate display mode. These are mostly used in the frontend ui app.
max_display_mode = 2
current_display_mode = self.params_memory.get_int("ScreenDisplayMode")
current_display_mode = current_display_mode + 1
if current_display_mode > max_display_mode:
current_display_mode = 0
self.params_memory.put_int("ScreenDisplayMode", current_display_mode)
# 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):
current = self.params_memory.get_int("ScreenDisplayMode")
if driving_gear:
# 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}
new_mode = transitions.get(current, 0)
else:
# Not in drive: anything except 3 → 3 (screen off), state 3 → 0 (auto)
new_mode = 0 if current == 3 else 3
self.params_memory.put_int("ScreenDisplayMode", new_mode)
# Leftovers
# self.params_memory.put_int("SpeedLimitLatDesired", CC.actuators.speed * CV.MS_TO_MPH )
return CC
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,
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:
# Clearpilot todo:
@@ -778,10 +768,6 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
ET.SOFT_DISABLE: soft_disable_alert("Sensor Data Invalid"),
},
EventName.clpDebug: {
ET.PERMANENT: clp_debug_notice,
},
EventName.noGps: {
ET.PERMANENT: no_gps_alert,
},
+341 -400
View File
@@ -35,120 +35,19 @@ int ABGRToNV12(const uint8_t* src_abgr,
int halfwidth = (width + 1) >> 1;
void (*ABGRToUVRow)(const uint8_t* src_abgr0, int src_stride_abgr,
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) =
ABGRToYRow_C;
void (*MergeUVRow_)(const uint8_t* src_u, const uint8_t* src_v, uint8_t* dst_uv, int width) = MergeUVRow_C;
ABGRToYRow_NEON;
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) {
return -1;
}
if (height < 0) { // Negative height means invert the image.
height = -height;
src_abgr = src_abgr + (height - 1) * 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;
}
if (height < 0) {
height = -height;
src_abgr = src_abgr + (height - 1) * src_stride_abgr;
src_stride_abgr = -src_stride_abgr;
}
#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);
uint8_t* row_v = row_u + ((halfwidth + 31) & ~31);
@@ -182,9 +81,9 @@ extern ExitHandler do_exit;
// ***** OMX callback functions *****
void OmxEncoder::wait_for_state(OMX_STATETYPE state_) {
std::unique_lock lk(this->state_lock);
while (this->state != state_) {
this->state_cv.wait(lk);
std::unique_lock lk(state_lock);
while (state != state_) {
state_cv.wait(lk);
}
}
@@ -236,270 +135,203 @@ static const char* omx_color_fomat_name(uint32_t format) __attribute__((unused))
static const char* omx_color_fomat_name(uint32_t format) {
switch (format) {
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_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 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_Format32bitRGBA8888Compressed: return "QOMX_COLOR_Format32bitRGBA8888Compressed";
default:
return "unkn";
default: return "unkn";
}
}
// ***** 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->width = width;
this->height = height;
this->fps = fps;
this->remuxing = !h265;
this->downscale = downscale;
if (this->downscale) {
this->y_ptr2 = (uint8_t *)malloc(this->width*this->height);
this->u_ptr2 = (uint8_t *)malloc(this->width*this->height/4);
this->v_ptr2 = (uint8_t *)malloc(this->width*this->height/4);
OMX_ERRORTYPE err = OMX_Init();
if (err != OMX_ErrorNone) {
LOGE("OMX_Init failed: %x", err);
return;
}
auto component = (OMX_STRING)(h265 ? "OMX.qcom.video.encoder.hevc" : "OMX.qcom.video.encoder.avc");
int err = OMX_GetHandle(&this->handle, component, this, &omx_callbacks);
OMX_STRING component = (OMX_STRING)("OMX.qcom.video.encoder.avc");
err = OMX_GetHandle(&handle, component, this, &omx_callbacks);
if (err != OMX_ErrorNone) {
LOGE("error getting codec: %x", err);
LOGE("Error getting codec: %x", err);
OMX_Deinit();
return;
}
// setup input port
OMX_PARAM_PORTDEFINITIONTYPE in_port = {0};
in_port.nSize = sizeof(in_port);
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.nFrameHeight = this->height;
in_port.format.video.nStride = VENUS_Y_STRIDE(COLOR_FMT_NV12, this->width);
in_port.format.video.nSliceHeight = this->height;
in_port.nBufferSize = VENUS_BUFFER_SIZE(COLOR_FMT_NV12, this->width, this->height);
in_port.format.video.xFramerate = (this->fps * 65536);
in_port.format.video.nFrameWidth = width;
in_port.format.video.nFrameHeight = height;
in_port.format.video.nStride = VENUS_Y_STRIDE(COLOR_FMT_NV12, width);
in_port.format.video.nSliceHeight = height;
in_port.nBufferSize = VENUS_BUFFER_SIZE(COLOR_FMT_NV12, width, height);
in_port.format.video.xFramerate = (fps * 65536);
in_port.format.video.eCompressionFormat = OMX_VIDEO_CodingUnused;
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_GetParameter(this->handle, OMX_IndexParamPortDefinition, (OMX_PTR) &in_port));
this->in_buf_headers.resize(in_port.nBufferCountActual);
OMX_CHECK(OMX_SetParameter(handle, OMX_IndexParamPortDefinition, (OMX_PTR) &in_port));
OMX_CHECK(OMX_GetParameter(handle, OMX_IndexParamPortDefinition, (OMX_PTR) &in_port));
in_buf_headers.resize(in_port.nBufferCountActual);
// setup output port
OMX_PARAM_PORTDEFINITIONTYPE out_port = {0};
OMX_PARAM_PORTDEFINITIONTYPE out_port;
memset(&out_port, 0, sizeof(OMX_PARAM_PORTDEFINITIONTYPE));
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;
OMX_CHECK(OMX_GetParameter(this->handle, OMX_IndexParamPortDefinition, (OMX_PTR)&out_port));
out_port.format.video.nFrameWidth = this->width;
out_port.format.video.nFrameHeight = this->height;
OMX_ERRORTYPE error = OMX_GetParameter(handle, OMX_IndexParamPortDefinition, (OMX_PTR)&out_port);
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.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;
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));
this->out_buf_headers.resize(out_port.nBufferCountActual);
error = OMX_GetParameter(handle, OMX_IndexParamPortDefinition, (OMX_PTR) &out_port);
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};
bitrate_type.nSize = sizeof(bitrate_type);
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.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));
// setup h264
OMX_VIDEO_PARAM_AVCTYPE avc = {0};
avc.nSize = sizeof(avc);
avc.nPortIndex = (OMX_U32) PORT_INDEX_OUT;
OMX_CHECK(OMX_GetParameter(handle, OMX_IndexParamVideoAvc, &avc));
hevc_type.eProfile = OMX_VIDEO_HEVCProfileMain;
hevc_type.eLevel = OMX_VIDEO_HEVCHighTierLevel5;
avc.nBFrames = 0;
avc.nPFrames = 15;
OMX_CHECK(OMX_SetParameter(this->handle, index_type, (OMX_PTR) &hevc_type));
} else {
// setup h264
OMX_VIDEO_PARAM_AVCTYPE avc = { 0 };
avc.nSize = sizeof(avc);
avc.nPortIndex = (OMX_U32) PORT_INDEX_OUT;
OMX_CHECK(OMX_GetParameter(this->handle, OMX_IndexParamVideoAvc, &avc));
avc.eProfile = OMX_VIDEO_AVCProfileHigh;
avc.eLevel = OMX_VIDEO_AVCLevel31;
avc.nBFrames = 0;
avc.nPFrames = 15;
avc.nAllowedPictureTypes |= OMX_VIDEO_PictureTypeB;
avc.eLoopFilterMode = OMX_VIDEO_AVCLoopFilterEnable;
avc.eProfile = OMX_VIDEO_AVCProfileHigh;
avc.eLevel = OMX_VIDEO_AVCLevel31;
avc.nRefFrames = 1;
avc.bUseHadamard = OMX_TRUE;
avc.bEntropyCodingCABAC = OMX_TRUE;
avc.bWeightedPPrediction = OMX_TRUE;
avc.bconstIpred = OMX_TRUE;
avc.nAllowedPictureTypes |= OMX_VIDEO_PictureTypeB;
avc.eLoopFilterMode = OMX_VIDEO_AVCLoopFilterEnable;
OMX_CHECK(OMX_SetParameter(handle, OMX_IndexParamVideoAvc, &avc));
avc.nRefFrames = 1;
avc.bUseHadamard = OMX_TRUE;
avc.bEntropyCodingCABAC = OMX_TRUE;
avc.bWeightedPPrediction = OMX_TRUE;
avc.bconstIpred = OMX_TRUE;
OMX_CHECK(OMX_SendCommand(handle, OMX_CommandStateSet, OMX_StateIdle, NULL));
OMX_CHECK(OMX_SetParameter(this->handle, OMX_IndexParamVideoAvc, &avc));
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 (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));
for (OMX_BUFFERHEADERTYPE* &buf : out_buf_headers) {
OMX_CHECK(OMX_AllocateBuffer(handle, &buf, PORT_INDEX_OUT, this, out_port.nBufferSize));
}
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);
// give omx all the output buffers
for (auto &buf : this->out_buf_headers) {
OMX_CHECK(OMX_FillThisBuffer(this->handle, buf));
for (OMX_BUFFERHEADERTYPE* &buf : out_buf_headers) {
OMX_CHECK(OMX_FillThisBuffer(handle, buf));
}
// fill the input free queue
for (auto &buf : this->in_buf_headers) {
this->free_in.push(buf);
for (OMX_BUFFERHEADERTYPE* &buf : in_buf_headers) {
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;
uint8_t *buf_data = out_buf->pBuffer + out_buf->nOffset;
if (out_buf->nFlags & OMX_BUFFERFLAG_CODECCONFIG) {
if (e->codec_config_len < out_buf->nFilledLen) {
e->codec_config = (uint8_t *)realloc(e->codec_config, out_buf->nFilledLen);
if (encoder->codec_config_len < out_buf->nFilledLen) {
encoder->codec_config = (uint8_t *)realloc(encoder->codec_config, out_buf->nFilledLen);
}
e->codec_config_len = out_buf->nFilledLen;
memcpy(e->codec_config, buf_data, out_buf->nFilledLen);
encoder->codec_config_len = out_buf->nFilledLen;
memcpy(encoder->codec_config, buf_data, out_buf->nFilledLen);
#ifdef QCOM2
out_buf->nTimeStamp = 0;
#endif
}
if (e->of) {
fwrite(buf_data, out_buf->nFilledLen, 1, e->of);
if (encoder->of) {
fwrite(buf_data, out_buf->nFilledLen, 1, encoder->of);
}
if (e->remuxing) {
if (!e->wrote_codec_config && e->codec_config_len > 0) {
// 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);
e->codec_ctx->extradata_size = e->codec_config_len;
memcpy(e->codec_ctx->extradata, e->codec_config, e->codec_config_len);
if (!encoder->wrote_codec_config && encoder->codec_config_len > 0) {
// extradata will be freed by av_free() in avcodec_free_context()
encoder->out_stream->codecpar->extradata = (uint8_t*)av_mallocz(encoder->codec_config_len + AV_INPUT_BUFFER_PADDING_SIZE);
encoder->out_stream->codecpar->extradata_size = encoder->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);
assert(err >= 0);
err = avformat_write_header(e->ofmt_ctx, NULL);
assert(err >= 0);
err = avformat_write_header(encoder->ofmt_ctx, NULL);
assert(err >= 0);
e->wrote_codec_config = true;
encoder->wrote_codec_config = true;
}
if (out_buf->nTimeStamp > 0) {
// input timestamps are in microseconds
AVRational in_timebase = {1, 1000000};
AVPacket pkt;
av_init_packet(&pkt);
pkt.data = buf_data;
pkt.size = out_buf->nFilledLen;
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, encoder->out_stream->time_base, rnd);
pkt.duration = av_rescale_q(1, AVRational{1, encoder->fps}, encoder->out_stream->time_base);
if (out_buf->nFlags & OMX_BUFFERFLAG_SYNCFRAME) {
pkt.flags |= AV_PKT_FLAG_KEY;
}
if (out_buf->nTimeStamp > 0) {
// input timestamps are in microseconds
AVRational in_timebase = {1, 1000000};
err = av_write_frame(encoder->ofmt_ctx, &pkt);
if (err < 0) { LOGW("ts encoder write issue"); }
AVPacket pkt;
av_init_packet(&pkt);
pkt.data = buf_data;
pkt.size = out_buf->nFilledLen;
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.duration = av_rescale_q(50*1000, in_timebase, e->ofmt_ctx->streams[0]->time_base);
if (out_buf->nFlags & OMX_BUFFERFLAG_SYNCFRAME) {
pkt.flags |= AV_PKT_FLAG_KEY;
}
err = av_write_frame(e->ofmt_ctx, &pkt);
if (err < 0) { LOGW("ts encoder write issue"); }
av_free_packet(&pkt);
}
av_packet_unref(&pkt);
}
// give omx back the buffer
@@ -508,134 +340,186 @@ void OmxEncoder::handle_out_buf(OmxEncoder *e, OMX_BUFFERHEADERTYPE *out_buf) {
out_buf->nTimeStamp = 0;
}
#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 err;
if (!this->is_open) {
if (!is_open) {
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;
while (!this->free_in.try_pop(in_buf, 20)) {
while (!free_in.try_pop(in_buf, 20)) {
if (do_exit) {
return -1;
}
}
int ret = this->counter;
int ret = counter;
uint8_t *in_buf_ptr = in_buf->pBuffer;
uint8_t *in_y_ptr = in_buf_ptr;
int in_y_stride = VENUS_Y_STRIDE(COLOR_FMT_NV12, this->width);
int in_uv_stride = VENUS_UV_STRIDE(COLOR_FMT_NV12, this->width);
uint8_t *in_uv_ptr = in_buf_ptr + (in_y_stride * VENUS_Y_SCANLINES(COLOR_FMT_NV12, this->height));
int in_y_stride = VENUS_Y_STRIDE(COLOR_FMT_NV12, 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, height));
err = ABGRToNV12(ptr, this->width*4,
in_y_ptr, in_y_stride,
in_uv_ptr, in_uv_stride,
this->width, this->height);
int err = ABGRToNV12(ptr, width * 4, in_y_ptr, in_y_stride, in_uv_ptr, in_uv_stride, width, height);
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->nOffset = 0;
in_buf->nTimeStamp = ts/1000LL; // OMX_TICKS, in microseconds
this->last_t = in_buf->nTimeStamp;
in_buf->nTimeStamp = ts / 1000LL; // OMX_TICKS, in microseconds
last_t = in_buf->nTimeStamp;
OMX_CHECK(OMX_EmptyThisBuffer(this->handle, in_buf));
OMX_CHECK(OMX_EmptyThisBuffer(handle, in_buf));
// pump output
while (true) {
OMX_BUFFERHEADERTYPE *out_buf;
if (!this->done_out.try_pop(out_buf)) {
if (!done_out.try_pop(out_buf)) {
break;
}
handle_out_buf(this, out_buf);
}
this->dirty = true;
dirty = true;
this->counter++;
counter++;
return ret;
}
// 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 in_width, int in_height, uint64_t ts) {
if (!is_open) {
return -1;
}
OMX_BUFFERHEADERTYPE* in_buf = nullptr;
while (!free_in.try_pop(in_buf, 20)) {
if (do_exit) {
return -1;
}
}
int ret = counter;
uint8_t *in_buf_ptr = in_buf->pBuffer;
int venus_y_stride = VENUS_Y_STRIDE(COLOR_FMT_NV12, width);
int venus_uv_stride = VENUS_UV_STRIDE(COLOR_FMT_NV12, width);
uint8_t *dst_y = in_buf_ptr;
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)
for (int row = 0; row < in_height; row++) {
memcpy(dst_y + row * venus_y_stride, y_ptr + row * y_stride, in_width);
}
// Copy UV plane row by row
int uv_height = in_height / 2;
for (int row = 0; row < uv_height; row++) {
memcpy(dst_uv + row * venus_uv_stride, uv_ptr + row * uv_stride, in_width);
}
in_buf->nFilledLen = VENUS_BUFFER_SIZE(COLOR_FMT_NV12, width, height);
in_buf->nFlags = OMX_BUFFERFLAG_ENDOFFRAME;
in_buf->nOffset = 0;
in_buf->nTimeStamp = ts / 1000LL;
last_t = in_buf->nTimeStamp;
OMX_CHECK(OMX_EmptyThisBuffer(handle, in_buf));
while (true) {
OMX_BUFFERHEADERTYPE *out_buf;
if (!done_out.try_pop(out_buf)) {
break;
}
handle_out_buf(this, out_buf);
}
dirty = true;
counter++;
return ret;
}
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};
if (stat(this->path.c_str(), &st) == -1) {
mkdir(this->path.c_str(), 0755);
}
snprintf(this->vid_path, sizeof(this->vid_path), "%s/%s", this->path.c_str(), filename);
printf("encoder_open %s remuxing:%d\n", this->vid_path, this->remuxing);
if (this->remuxing) {
avformat_alloc_output_context2(&this->ofmt_ctx, NULL, NULL, this->vid_path);
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);
if (stat(path.c_str(), &st) == -1) {
if (mkdir(path.c_str(), 0755) == -1) {
return;
}
#endif
}
// create camera lock file
snprintf(this->lock_path, sizeof(this->lock_path), "%s/%s.lock", this->path.c_str(), filename);
int lock_fd = HANDLE_EINTR(open(this->lock_path, O_RDWR | O_CREAT, 0664));
assert(lock_fd >= 0);
snprintf(vid_path, sizeof(vid_path), "%s/%s", path.c_str(), filename);
if (avformat_alloc_output_context2(&ofmt_ctx, NULL, NULL, vid_path) < 0 || !ofmt_ctx) {
return;
}
out_stream = avformat_new_stream(ofmt_ctx, NULL);
if (!out_stream) {
avformat_free_context(ofmt_ctx);
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);
this->is_open = true;
this->counter = 0;
is_open = true;
counter = 0;
}
void OmxEncoder::encoder_close() {
if (this->is_open) {
if (this->dirty) {
// drain output only if there could be frames in the encoder
if (!is_open) return;
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->nOffset = 0;
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) {
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);
@@ -643,55 +527,112 @@ void OmxEncoder::encoder_close() {
break;
}
}
this->dirty = false;
}
if (this->remuxing) {
av_write_trailer(this->ofmt_ctx);
avcodec_free_context(&this->codec_ctx);
avio_closep(&this->ofmt_ctx->pb);
avformat_free_context(this->ofmt_ctx);
} else {
fclose(this->of);
this->of = nullptr;
}
unlink(this->lock_path);
dirty = false;
}
if (out_stream) {
out_stream->nb_frames = counter;
out_stream->duration = av_rescale_q(counter, AVRational{1, fps}, out_stream->time_base);
}
if (ofmt_ctx) {
av_write_trailer(ofmt_ctx);
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);
}
}
this->is_open = false;
}
OmxEncoder::~OmxEncoder() {
assert(!this->is_open);
OMX_CHECK(OMX_SendCommand(this->handle, OMX_CommandStateSet, OMX_StateIdle, NULL));
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));
if (is_open) {
LOGE("OmxEncoder closed with is_open=true, calling encoder_close()");
encoder_close();
}
for (auto &buf : this->out_buf_headers) {
OMX_CHECK(OMX_FreeBuffer(this->handle, PORT_INDEX_OUT, buf));
if (!handle) {
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);
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;
while (this->free_in.try_pop(out_buf));
while (this->done_out.try_pop(out_buf));
while (free_in.try_pop(out_buf));
while (done_out.try_pop(out_buf));
if (this->codec_config) {
free(this->codec_config);
if (codec_config) {
free(codec_config);
codec_config = nullptr;
}
if (this->downscale) {
free(this->y_ptr2);
free(this->u_ptr2);
free(this->v_ptr2);
}
in_buf_headers.clear();
out_buf_headers.clear();
}
@@ -12,13 +12,15 @@ extern "C" {
#include "common/queue.h"
// OmxEncoder, lossey codec using hardware hevc
// OmxEncoder, lossey codec using hardware H.264
class OmxEncoder {
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();
int encode_frame_rgba(const uint8_t *ptr, int in_width, int in_height, uint64_t ts);
int 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);
void encoder_open(const char* filename);
void encoder_close();
@@ -42,31 +44,26 @@ private:
int counter = 0;
std::string path;
FILE *of;
FILE *of = nullptr;
size_t codec_config_len;
uint8_t *codec_config = NULL;
bool wrote_codec_config;
size_t codec_config_len = 0;
uint8_t *codec_config = nullptr;
bool wrote_codec_config = false;
std::mutex state_lock;
std::condition_variable state_cv;
OMX_STATETYPE state = OMX_StateLoaded;
OMX_HANDLETYPE handle;
OMX_HANDLETYPE handle = nullptr;
std::vector<OMX_BUFFERHEADERTYPE *> in_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 *> done_out;
AVFormatContext *ofmt_ctx;
AVCodecContext *codec_ctx;
AVStream *out_stream;
bool remuxing;
bool downscale;
uint8_t *y_ptr2, *u_ptr2, *v_ptr2;
AVFormatContext *ofmt_ctx = nullptr;
AVStream *out_stream = nullptr;
};
+25 -24
View File
@@ -594,10 +594,15 @@ void Localizer::handle_msg(const cereal::Event::Reader& log) {
this->handle_sensor(t, log.getAccelerometer());
} else if (log.isGyroscope()) {
this->handle_sensor(t, log.getGyroscope());
} 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);
// CLEARPILOT: GPS branches removed — locationd no longer subscribes to
// gpsLocation/gpsLocationExternal, so these would be dead code regardless.
// Self-driving treats GPS as not present: gpsOK stays false (last_gps_msg
// 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()) {
// this->handle_gnss(t, log.getGnssMeasurements());
} else if (log.isCarState()) {
@@ -676,22 +681,16 @@ void Localizer::configure_gnss_source(const LocalizerGnssSource &source) {
}
int Localizer::locationd_thread() {
Params params;
LocalizerGnssSource source;
const char* gps_location_socket;
if (params.getBool("UbloxAvailable")) {
source = LocalizerGnssSource::UBLOX;
gps_location_socket = "gpsLocationExternal";
} else {
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",
// CLEARPILOT: do not subscribe to GPS. Our gpsd publishes gpsLocation for
// UI/clock/dashcam, but feeding it to the kalman filter screws up the
// self-driving math. liveLocationKalman.gpsOK stays false; downstream
// self-driving consumers (controlsd, paramsd, torqued, frogpilot_planner)
// already handle that case.
this->configure_gnss_source(LocalizerGnssSource::QCOM);
const std::initializer_list<const char *> service_list = {"cameraOdometry", "liveCalibration",
"carState", "accelerometer", "gyroscope"};
SubMaster sm(service_list, {}, nullptr, {gps_location_socket});
SubMaster sm(service_list, {}, nullptr, {});
PubMaster pm({"liveLocationKalman"});
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);
pm.send("liveLocationKalman", bytes.begin(), bytes.size());
if (cnt % 1200 == 0 && gpsOK) { // once a minute
VectorXd posGeo = this->get_position_geodetic();
std::string lastGPSPosJSON = util::string_format(
"{\"latitude\": %.15f, \"longitude\": %.15f, \"altitude\": %.15f}", posGeo(0), posGeo(1), posGeo(2));
params.putNonBlocking("LastGPSPosition", lastGPSPosJSON);
}
// CLEARPILOT: dead code now that gpsOK is permanently false (we don't
// subscribe to gpsLocation). Was: write LastGPSPosition once a minute.
// if (cnt % 1200 == 0 && gpsOK) {
// VectorXd posGeo = this->get_position_geodetic();
// std::string lastGPSPosJSON = util::string_format(
// "{\"latitude\": %.15f, \"longitude\": %.15f, \"altitude\": %.15f}", posGeo(0), posGeo(1), posGeo(2));
// params.putNonBlocking("LastGPSPosition", lastGPSPosJSON);
// }
cnt++;
}
}
+28 -1
View File
@@ -55,7 +55,8 @@ def manager_init(frogpilot_functions) -> None:
frogpilot_boot = threading.Thread(target=frogpilot_boot_functions, args=(frogpilot_functions,))
frogpilot_boot.start()
save_bootlog()
# CLEARPILOT: skip writing boot logs to /data/media/0/realdata/boot/
# save_bootlog()
params = Params()
params_storage = Params("/persist/params")
@@ -300,6 +301,32 @@ def manager_init(frogpilot_functions) -> None:
else:
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
try:
os.mkdir("/dev/shm")
+19 -4
View File
@@ -54,7 +54,9 @@ def allow_uploads(started, params, CP: car.CarParams) -> bool:
procs = [
DaemonProcess("manage_athenad", "selfdrive.athena.manage_athenad", "AthenadPid"),
NativeProcess("camerad", "system/camerad", ["./camerad"], driverview),
# 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("logcatd", "system/logcatd", ["./logcatd"], allow_logging),
NativeProcess("proclogd", "system/proclogd", ["./proclogd"], allow_logging),
PythonProcess("logmessaged", "system.logmessaged", allow_logging),
@@ -62,9 +64,12 @@ procs = [
PythonProcess("timed", "system.timed", always_run, enabled=not PC),
PythonProcess("dmonitoringmodeld", "selfdrive.modeld.dmonitoringmodeld", driverview, enabled=(not PC or WEBCAM)),
NativeProcess("encoderd", "system/loggerd", ["./encoderd"], allow_logging),
NativeProcess("stream_encoderd", "system/loggerd", ["./encoderd", "--stream"], notcar),
NativeProcess("loggerd", "system/loggerd", ["./loggerd"], allow_logging),
# CLEARPILOT: disabled segment + camera logging — no rlog/qlog or .hevc
# files written to /data/media/0/realdata. We don't use comma's upload/
# replay pipeline. Keep deleter running for any leftover cleanup.
# NativeProcess("encoderd", "system/loggerd", ["./encoderd"], allow_logging),
# NativeProcess("stream_encoderd", "system/loggerd", ["./encoderd", "--stream"], notcar),
# NativeProcess("loggerd", "system/loggerd", ["./loggerd"], allow_logging),
NativeProcess("modeld", "selfdrive/modeld", ["./modeld"], only_onroad),
#NativeProcess("mapsd", "selfdrive/navd", ["./mapsd"], only_onroad),
#PythonProcess("navmodeld", "selfdrive.modeld.navmodeld", only_onroad),
@@ -79,6 +84,16 @@ procs = [
PythonProcess("deleter", "system.loggerd.deleter", always_run),
PythonProcess("dmonitoringd", "selfdrive.monitoring.dmonitoringd", driverview, enabled=(not PC or WEBCAM)),
# PythonProcess("qcomgpsd", "system.qcomgpsd.qcomgpsd", qcomgps, enabled=TICI), # Fixme
# CLEARPILOT: replacement for qcomgpsd (whose diag interface is broken on this device).
# Uses Quectel modem AT commands via mmcli. Self-driving does NOT consume this; locationd
# 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("navd", "selfdrive.navd.navd", only_onroad),
PythonProcess("pandad", "selfdrive.boardd.pandad", always_run),
+2 -3
View File
@@ -39,7 +39,7 @@ qt_src = ["main.cc", "qt/sidebar.cc", "qt/onroad.cc", "qt/body.cc",
"qt/window.cc", "qt/home.cc", "qt/offroad/settings.cc",
"qt/offroad/software_settings.cc", "qt/offroad/onboarding.cc",
"qt/offroad/driverview.cc", "qt/offroad/experimental_mode.cc",
"../frogpilot/screenrecorder/omx_encoder.cc", "../frogpilot/screenrecorder/screenrecorder.cc",
"../frogpilot/screenrecorder/omx_encoder.cc", # kept for dashcamd .o dependency
"qt/ready.cc"]
# build translation files
@@ -77,8 +77,7 @@ qt_env.Program("_text", ["qt/text.cc"], LIBS=qt_libs)
qt_env.Program("_spinner", ["qt/spinner.cc"], LIBS=qt_libs)
# Clearpilot
# Create clearpilot tools
# Clearpilot tools
qt_env.Program("/data/openpilot/system/clearpilot/tools/qt_shell", ["/data/openpilot/system/clearpilot/tools/qt_shell.cc"], LIBS=qt_libs)
# build main UI
+25
View File
@@ -1,4 +1,9 @@
#include <sys/resource.h>
#include <csignal>
#include <cstdio>
#include <cstdlib>
#include <execinfo.h>
#include <unistd.h>
#include <QApplication>
#include <QTranslator>
@@ -8,7 +13,27 @@
#include "selfdrive/ui/qt/util.h"
#include "selfdrive/ui/qt/window.h"
// CLEARPILOT: crash handler — prints stack trace to stderr on SIGSEGV/SIGABRT
static void crash_handler(int sig) {
const char *sig_name = (sig == SIGSEGV) ? "SIGSEGV" : (sig == SIGABRT) ? "SIGABRT" : "SIGNAL";
fprintf(stderr, "\n=== CRASH: %s (signal %d) ===\n", sig_name, sig);
void *frames[64];
int count = backtrace(frames, 64);
fprintf(stderr, "Backtrace (%d frames):\n", count);
backtrace_symbols_fd(frames, count, STDERR_FILENO);
fprintf(stderr, "=== END CRASH ===\n");
fflush(stderr);
// Re-raise to get default behavior (core dump / exit)
signal(sig, SIG_DFL);
raise(sig);
}
int main(int argc, char *argv[]) {
signal(SIGSEGV, crash_handler);
signal(SIGABRT, crash_handler);
setpriority(PRIO_PROCESS, 0, -20);
qInstallMessageHandler(swagLogMessageHandler);
+328 -142
View File
@@ -1,14 +1,20 @@
#include "selfdrive/ui/qt/home.h"
#include <cstdlib>
#include <QHBoxLayout>
#include <QMouseEvent>
#include <QStackedWidget>
#include <QVBoxLayout>
#include "selfdrive/ui/qt/offroad/experimental_mode.h"
#include "selfdrive/ui/qt/util.h"
#include "selfdrive/ui/qt/widgets/drive_stats.h"
#include "common/swaglog.h"
#include "common/params.h"
#include "system/hardware/hw.h"
#include "selfdrive/ui/qt/util.h"
#include "selfdrive/ui/qt/widgets/controls.h"
#include "selfdrive/ui/qt/widgets/input.h"
#include "selfdrive/ui/qt/widgets/scrollview.h"
#include "selfdrive/ui/qt/network/networking.h"
#include "cereal/messaging/messaging.h"
// HomeWindow: the container for the offroad and onroad UIs
@@ -21,6 +27,7 @@ HomeWindow::HomeWindow(QWidget* parent) : QWidget(parent) {
main_layout->setSpacing(0);
sidebar = new Sidebar(this);
sidebar->setVisible(false);
main_layout->addWidget(sidebar);
QObject::connect(sidebar, &Sidebar::openSettings, this, &HomeWindow::openSettings);
QObject::connect(sidebar, &Sidebar::openOnroad, this, &HomeWindow::showOnroad);
@@ -28,8 +35,17 @@ HomeWindow::HomeWindow(QWidget* parent) : QWidget(parent) {
slayout = new QStackedLayout();
main_layout->addLayout(slayout);
home = new OffroadHome(this);
QObject::connect(home, &OffroadHome::openSettings, this, &HomeWindow::openSettings);
home = new ClearPilotPanel(this);
QObject::connect(home, &ClearPilotPanel::openSettings, this, &HomeWindow::openSettings);
QObject::connect(home, &ClearPilotPanel::openStatus, this, &HomeWindow::openStatus);
QObject::connect(home, &ClearPilotPanel::closePanel, [=]() {
// Return to splash or onroad depending on state
if (uiState()->scene.started) {
slayout->setCurrentWidget(onroad);
} else {
slayout->setCurrentWidget(ready);
}
});
slayout->addWidget(home);
onroad = new OnroadWindow(this);
@@ -38,7 +54,8 @@ HomeWindow::HomeWindow(QWidget* parent) : QWidget(parent) {
// CLEARPILOT
ready = new ReadyWindow(this);
slayout->addWidget(ready);
slayout->setCurrentWidget(ready); // show splash immediately, not ClearPilotPanel
driver_view = new DriverViewWindow(this);
connect(driver_view, &DriverViewWindow::done, [=] {
showDriverView(false);
@@ -64,52 +81,79 @@ void HomeWindow::showSidebar(bool show) {
}
void HomeWindow::updateState(const UIState &s) {
// const SubMaster &sm = *(s.sm);
if (s.scene.started) {
showDriverView(s.scene.driver_camera_timer >= 10, true);
// CLEARPILOT: show splash screen when onroad but in park
bool parked = s.scene.parked;
int screenMode = paramsMemory.getInt("ScreenDisplayMode");
bool nightrider = (screenMode == 1 || screenMode == 4);
if (parked && !was_parked_onroad) {
LOGW("CLP UI: park transition -> showing splash");
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) {
LOGW("CLP UI: drive transition -> showing onroad");
slayout->setCurrentWidget(onroad);
ready->has_driven = true;
}
was_parked_onroad = parked;
// CLEARPILOT: honor display on/off while showing splash in park (normal mode only)
if (parked && ready->isVisible()) {
if (screenMode == 3) {
Hardware::set_display_power(false);
} else {
Hardware::set_display_power(true);
}
}
}
}
void HomeWindow::offroadTransition(bool offroad) {
sidebar->setVisible(false);
if (offroad) {
sidebar->setVisible(false);
LOGW("CLP UI: offroad transition -> showing splash");
was_parked_onroad = false;
slayout->setCurrentWidget(ready);
} else {
sidebar->setVisible(false);
slayout->setCurrentWidget(onroad);
// CLEARPILOT: start onroad in splash — updateState will switch to
// camera view once the car shifts out of park. Reset has_driven so
// fresh ignition shows the READY text (not the post-drive textless splash).
LOGW("CLP UI: onroad transition -> showing splash (parked)");
was_parked_onroad = true;
ready->has_driven = false;
slayout->setCurrentWidget(ready);
}
}
void HomeWindow::showDriverView(bool show, bool started) {
if (show) {
LOGW("CLP UI: showDriverView(true) -> driver_view");
emit closeSettings();
slayout->setCurrentWidget(driver_view);
sidebar->setVisible(show == false);
} else {
if (started) {
slayout->setCurrentWidget(onroad);
sidebar->setVisible(params.getBool("Sidebar"));
} else {
slayout->setCurrentWidget(home);
sidebar->setVisible(show == false);
}
sidebar->setVisible(false);
} else if (!started) {
// Offroad, not started — show home menu
slayout->setCurrentWidget(home);
sidebar->setVisible(false);
}
// CLEARPILOT: when started, don't touch slayout here —
// updateState handles park->splash and drive->onroad transitions
}
void HomeWindow::mousePressEvent(QMouseEvent* e) {
// CLEARPILOT todo - tap on main goes straight to settings
// Unless we click a debug widget.
// CLEARPILOT - click ready shows home
if (!onroad->isVisible() && ready->isVisible()) {
sidebar->setVisible(true);
// CLEARPILOT: tap from any view goes to ClearPilotPanel
if (ready->isVisible() || onroad->isVisible()) {
LOGW("CLP UI: tap -> showing ClearPilotPanel");
sidebar->setVisible(false);
home->resetToGeneral();
slayout->setCurrentWidget(home);
}
// Todo: widgets
if (onroad->isVisible()) {
emit openSettings();
}
}
void HomeWindow::mouseDoubleClickEvent(QMouseEvent* e) {
@@ -117,132 +161,274 @@ void HomeWindow::mouseDoubleClickEvent(QMouseEvent* e) {
// const SubMaster &sm = *(uiState()->sm);
}
// OffroadHome: the offroad home page
// CLEARPILOT: ClearPilotPanel — settings-style sidebar menu
OffroadHome::OffroadHome(QWidget* parent) : QFrame(parent) {
QVBoxLayout* main_layout = new QVBoxLayout(this);
main_layout->setContentsMargins(40, 40, 40, 40);
// top header
QHBoxLayout* header_layout = new QHBoxLayout();
header_layout->setContentsMargins(0, 0, 0, 0);
header_layout->setSpacing(16);
update_notif = new QPushButton(tr("UPDATE"));
update_notif->setVisible(false);
update_notif->setStyleSheet("background-color: #364DEF;");
QObject::connect(update_notif, &QPushButton::clicked, [=]() { center_layout->setCurrentIndex(1); });
header_layout->addWidget(update_notif, 0, Qt::AlignHCenter | Qt::AlignLeft);
alert_notif = new QPushButton();
alert_notif->setVisible(false);
alert_notif->setStyleSheet("background-color: #E22C2C;");
QObject::connect(alert_notif, &QPushButton::clicked, [=] { center_layout->setCurrentIndex(2); });
header_layout->addWidget(alert_notif, 0, Qt::AlignHCenter | Qt::AlignLeft);
date = new ElidedLabel();
header_layout->addWidget(date, 0, Qt::AlignHCenter | Qt::AlignLeft);
version = new ElidedLabel();
header_layout->addWidget(version, 0, Qt::AlignHCenter | Qt::AlignRight);
main_layout->addLayout(header_layout);
// main content
main_layout->addSpacing(25);
center_layout = new QStackedLayout();
QWidget *home_widget = new QWidget(this);
{
QHBoxLayout *home_layout = new QHBoxLayout(home_widget);
home_layout->setContentsMargins(0, 0, 0, 0);
home_layout->setSpacing(30);
// // // Create a QWebEngineView
// QWebEngineView *web_view = new QWebEngineView();
// web_view->load(QUrl("http://fark.com"));
// // Add the QWebEngineView to the layout
// home_layout->addWidget(web_view);
static const char *clpSidebarBtnStyle = R"(
QPushButton {
color: grey;
border: none;
background: none;
font-size: 65px;
font-weight: 500;
}
center_layout->addWidget(home_widget);
QPushButton:checked {
color: white;
}
QPushButton:pressed {
color: #ADADAD;
}
)";
// add update & alerts widgets
update_widget = new UpdateAlert();
QObject::connect(update_widget, &UpdateAlert::dismiss, [=]() { center_layout->setCurrentIndex(0); });
center_layout->addWidget(update_widget);
alerts_widget = new OffroadAlert();
QObject::connect(alerts_widget, &OffroadAlert::dismiss, [=]() { center_layout->setCurrentIndex(0); });
center_layout->addWidget(alerts_widget);
// clpActionBtnStyle removed — no longer used
main_layout->addLayout(center_layout, 1);
// set up refresh timer
timer = new QTimer(this);
timer->callOnTimeout(this, &OffroadHome::refresh);
ClearPilotPanel::ClearPilotPanel(QWidget* parent) : QFrame(parent) {
// Sidebar
QWidget *sidebar_widget = new QWidget;
QVBoxLayout *sidebar_layout = new QVBoxLayout(sidebar_widget);
sidebar_layout->setContentsMargins(50, 50, 100, 50);
// Close button
QPushButton *close_btn = new QPushButton("← Back");
close_btn->setStyleSheet(R"(
QPushButton {
color: white;
border-radius: 25px;
background: #292929;
font-size: 50px;
font-weight: 500;
}
QPushButton:pressed {
color: #ADADAD;
}
)");
close_btn->setFixedSize(300, 125);
sidebar_layout->addSpacing(10);
sidebar_layout->addWidget(close_btn, 0, Qt::AlignRight);
QObject::connect(close_btn, &QPushButton::clicked, [=]() { emit closePanel(); });
// Panel content area
panel_widget = new QStackedWidget();
// ── General panel ──
ListWidget *general_panel = new ListWidget(this);
general_panel->setContentsMargins(50, 25, 50, 25);
// Status button
auto *status_btn = new ButtonControl("System Status", "VIEW", "");
connect(status_btn, &ButtonControl::clicked, [=]() { emit openStatus(); });
general_panel->addItem(status_btn);
// Reset Calibration
auto resetCalibBtn = new ButtonControl("Reset Calibration", "RESET", "");
connect(resetCalibBtn, &ButtonControl::showDescriptionEvent, [=]() {
QString desc = "openpilot requires the device to be mounted within 4° left or right and "
"within 5° up or 9° down. openpilot is continuously calibrating, resetting is rarely required.";
std::string calib_bytes = Params().get("CalibrationParams");
if (!calib_bytes.empty()) {
try {
AlignedBuffer aligned_buf;
capnp::FlatArrayMessageReader cmsg(aligned_buf.align(calib_bytes.data(), calib_bytes.size()));
auto calib = cmsg.getRoot<cereal::Event>().getLiveCalibration();
if (calib.getCalStatus() != cereal::LiveCalibrationData::Status::UNCALIBRATED) {
double pitch = calib.getRpyCalib()[1] * (180 / M_PI);
double yaw = calib.getRpyCalib()[2] * (180 / M_PI);
desc += QString(" Your device is pointed %1° %2 and %3° %4.")
.arg(QString::number(std::abs(pitch), 'g', 1), pitch > 0 ? "down" : "up",
QString::number(std::abs(yaw), 'g', 1), yaw > 0 ? "left" : "right");
}
} catch (...) {
qInfo() << "invalid CalibrationParams";
}
}
qobject_cast<ButtonControl *>(sender())->setDescription(desc);
});
connect(resetCalibBtn, &ButtonControl::clicked, [=]() {
if (ConfirmationDialog::confirm("Are you sure you want to reset calibration?", "Reset", this)) {
Params().remove("CalibrationParams");
Params().remove("LiveTorqueParameters");
}
});
general_panel->addItem(resetCalibBtn);
// Power buttons
QHBoxLayout *power_layout = new QHBoxLayout();
power_layout->setSpacing(30);
QPushButton *reboot_btn = new QPushButton("Reboot");
reboot_btn->setObjectName("reboot_btn");
power_layout->addWidget(reboot_btn);
QPushButton *softreboot_btn = new QPushButton("Soft Reboot");
softreboot_btn->setObjectName("softreboot_btn");
power_layout->addWidget(softreboot_btn);
QPushButton *poweroff_btn = new QPushButton("Power Off");
poweroff_btn->setObjectName("poweroff_btn");
power_layout->addWidget(poweroff_btn);
QObject::connect(reboot_btn, &QPushButton::clicked, [=]() {
if (!uiState()->engaged()) {
if (ConfirmationDialog::confirm("Are you sure you want to reboot?", "Reboot", this)) {
if (!uiState()->engaged()) {
Params().putBool("DoReboot", true);
}
}
} else {
ConfirmationDialog::alert("Disengage to Reboot", this);
}
});
QObject::connect(softreboot_btn, &QPushButton::clicked, [=]() {
if (!uiState()->engaged()) {
if (ConfirmationDialog::confirm("Are you sure you want to soft reboot?", "Soft Reboot", this)) {
if (!uiState()->engaged()) {
Params().putBool("DoSoftReboot", true);
}
}
} else {
ConfirmationDialog::alert("Disengage to Soft Reboot", this);
}
});
QObject::connect(poweroff_btn, &QPushButton::clicked, [=]() {
if (!uiState()->engaged()) {
if (ConfirmationDialog::confirm("Are you sure you want to power off?", "Power Off", this)) {
if (!uiState()->engaged()) {
Params().putBool("DoShutdown", true);
}
}
} else {
ConfirmationDialog::alert("Disengage to Power Off", this);
}
});
general_panel->addItem(power_layout);
// ── Network panel ──
Networking *network_panel = new Networking(this);
// Hide APN button — find by searching QPushButton labels inside AdvancedNetworking
for (auto *btn : network_panel->findChildren<QPushButton *>()) {
if (btn->text().contains("APN", Qt::CaseInsensitive)) {
// Hide the parent AbstractControl frame, not just the button
if (auto *frame = qobject_cast<QFrame *>(btn->parentWidget())) {
frame->setVisible(false);
}
}
}
// ── Dashcam panel ──
QWidget *dashcam_panel = new QWidget(this);
QVBoxLayout *dash_layout = new QVBoxLayout(dashcam_panel);
dash_layout->setContentsMargins(50, 25, 50, 25);
QLabel *dash_label = new QLabel("Dashcam viewer coming soon");
dash_label->setStyleSheet("color: grey; font-size: 40px;");
dash_label->setAlignment(Qt::AlignCenter);
dash_layout->addWidget(dash_label);
dash_layout->addStretch();
// ── Debug panel ──
ListWidget *debug_panel = new ListWidget(this);
debug_panel->setContentsMargins(50, 25, 50, 25);
auto *telemetry_toggle = new ToggleControl("Telemetry Logging",
"Record telemetry data to CSV in the session log directory. "
"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);
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. "
"Disabling kills the active tunnel and stops reconnection attempts.", "",
Params("/dev/shm/params").getBool("VpnEnabled"), this);
QObject::connect(vpn_toggle, &ToggleControl::toggleFlipped, [](bool on) {
Params("/dev/shm/params").putBool("VpnEnabled", on);
if (on) {
std::system("sudo bash -c 'nohup /data/openpilot/system/clearpilot/vpn-monitor.sh >> /tmp/vpn-monitor.log 2>&1 &'");
} else {
std::system("sudo pkill -TERM -f vpn-monitor.sh; sudo killall openvpn");
}
});
debug_panel->addItem(vpn_toggle);
// ── Register panels with sidebar buttons ──
QList<QPair<QString, QWidget *>> panels = {
{"General", general_panel},
{"Network", network_panel},
{"Dashcam", dashcam_panel},
{"Debug", debug_panel},
};
nav_group = new QButtonGroup(this);
nav_group->setExclusive(true);
for (auto &[name, panel] : panels) {
QPushButton *btn = new QPushButton(name);
btn->setCheckable(true);
btn->setStyleSheet(clpSidebarBtnStyle);
btn->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
sidebar_layout->addWidget(btn, 0, Qt::AlignRight);
nav_group->addButton(btn);
// Network panel handles its own scrolling/margins
if (name == "Network") {
panel_widget->addWidget(panel);
QObject::connect(btn, &QPushButton::clicked, [=, w = panel]() {
panel_widget->setCurrentWidget(w);
});
} else {
ScrollView *panel_frame = new ScrollView(panel, this);
panel_widget->addWidget(panel_frame);
QObject::connect(btn, &QPushButton::clicked, [=, w = panel_frame]() {
panel_widget->setCurrentWidget(w);
});
}
}
// Select General by default
nav_group->buttons().first()->setChecked(true);
panel_widget->setCurrentIndex(0);
// Main layout: sidebar + panels
QHBoxLayout *main_layout = new QHBoxLayout(this);
sidebar_widget->setFixedWidth(500);
main_layout->addWidget(sidebar_widget);
main_layout->addWidget(panel_widget);
setStyleSheet(R"(
* {
color: white;
font-size: 50px;
}
OffroadHome {
ClearPilotPanel {
background-color: black;
}
OffroadHome > QPushButton {
padding: 15px 30px;
border-radius: 5px;
font-size: 40px;
font-weight: 500;
}
OffroadHome > QLabel {
font-size: 55px;
QStackedWidget, ScrollView, Networking {
background-color: #292929;
border-radius: 30px;
}
#softreboot_btn { height: 120px; border-radius: 15px; background-color: #e2e22c; }
#softreboot_btn:pressed { background-color: #ffe224; }
#reboot_btn { height: 120px; border-radius: 15px; background-color: #393939; }
#reboot_btn:pressed { background-color: #4a4a4a; }
#poweroff_btn { height: 120px; border-radius: 15px; background-color: #E22C2C; }
#poweroff_btn:pressed { background-color: #FF2424; }
)");
}
/* Refresh data on screen every 5 seconds. */
void OffroadHome::showEvent(QShowEvent *event) {
refresh();
timer->start(5 * 1000);
}
void OffroadHome::hideEvent(QHideEvent *event) {
timer->stop();
}
void OffroadHome::refresh() {
QString model = QString::fromStdString(params.get("ModelName"));
date->setText(QLocale(uiState()->language.mid(5)).toString(QDateTime::currentDateTime(), "dddd, MMMM d"));
version->setText(getBrand() + " v" + getVersion().left(14).trimmed() + " - " + model);
// bool updateAvailable = update_widget->refresh();
int alerts = alerts_widget->refresh();
if (alerts > 0 && !alerts_widget->isVisible()) {
alerts_widget->setVisible(true);
} else if (alerts == 0 && alerts_widget->isVisible()) {
alerts_widget->setVisible(false);
}
// pop-up new notification
// CLEARPILOT temp disabled update notifications
// int idx = center_layout->currentIndex();
// if (!updateAvailable && !alerts && false) {
// idx = 0;
// } else if (updateAvailable && (!update_notif->isVisible() || (!alerts && idx == 2))) {
// idx = 1;
// } else if (alerts && (!alert_notif->isVisible() || (!updateAvailable && idx == 1))) {
// idx = 2;
// }
// center_layout->setCurrentIndex(idx);
// CLEARPILOT temp disabled update notifications
// update_notif->setVisible(updateAvailable);
// alert_notif->setVisible(alerts);
alert_notif->setVisible(false);
if (alerts) {
alert_notif->setText(QString::number(alerts) + (alerts > 1 ? tr(" ALERTS") : tr(" ALERT")));
}
void ClearPilotPanel::resetToGeneral() {
panel_widget->setCurrentIndex(0);
nav_group->buttons().first()->setChecked(true);
}
+15 -20
View File
@@ -1,9 +1,11 @@
#pragma once
#include <QButtonGroup>
#include <QFrame>
#include <QLabel>
#include <QPushButton>
#include <QStackedLayout>
#include <QStackedWidget>
#include <QTimer>
#include <QWidget>
@@ -16,32 +18,23 @@
#include "selfdrive/ui/qt/widgets/offroad_alerts.h"
#include "selfdrive/ui/ui.h"
class OffroadHome : public QFrame {
class ClearPilotPanel : public QFrame {
Q_OBJECT
public:
explicit OffroadHome(QWidget* parent = 0);
explicit ClearPilotPanel(QWidget* parent = 0);
signals:
void openSettings(int index = 0, const QString &param = "");
void openStatus();
void closePanel();
public:
void resetToGeneral();
private:
void showEvent(QShowEvent *event) override;
void hideEvent(QHideEvent *event) override;
void refresh();
Params params;
QTimer* timer;
ElidedLabel* version;
QStackedLayout* center_layout;
UpdateAlert *update_widget;
OffroadAlert* alerts_widget;
QPushButton* alert_notif;
QPushButton* update_notif;
// FrogPilot variables
ElidedLabel* date;
QStackedWidget *panel_widget;
QButtonGroup *nav_group;
};
class HomeWindow : public QWidget {
@@ -53,6 +46,7 @@ public:
signals:
void openSettings(int index = 0, const QString &param = "");
void openStatus();
void closeSettings();
public slots:
@@ -67,17 +61,18 @@ protected:
private:
Sidebar *sidebar;
OffroadHome *home;
ClearPilotPanel *home;
OnroadWindow *onroad;
DriverViewWindow *driver_view;
QStackedLayout *slayout;
// FrogPilot variables
Params params;
Params paramsMemory{"/dev/shm/params"};
// CLEARPILOT
// bool show_ready;
ReadyWindow *ready;
bool was_parked_onroad = false;
private slots:
void updateState(const UIState &s);
+430 -72
View File
@@ -7,6 +7,7 @@
#include <sstream>
#include <QApplication>
#include <QDateTime>
#include <QDebug>
#include <QMouseEvent>
@@ -75,12 +76,13 @@ void OnroadWindow::updateState(const UIState &s) {
alerts->updateAlert(alert);
nvg->updateState(s);
nvg->update(); // CLEARPILOT: force repaint every frame for HUD elements
QColor bgColor = bg_colors[s.status];
if (paramsMemory.getBool("no_lat_lane_change") == 1 || nvg->screenDisplayMode == 2) {
// CLEARPILOT: read from paramsMemory; controlsd writes "no_lat_lane_change".
if (paramsMemory.getBool("no_lat_lane_change")) {
bgColor = bg_colors[STATUS_DISENGAGED];
}
}
if (bg != bgColor) {
bg = bgColor;
@@ -176,7 +178,14 @@ void OnroadWindow::offroadTransition(bool offroad) {
void OnroadWindow::paintEvent(QPaintEvent *event) {
QPainter p(this);
p.fillRect(rect(), QColor(bg.red(), bg.green(), bg.blue(), 255));
// 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));
}
QString logicsDisplayString = QString();
if (scene.show_jerk) {
@@ -216,6 +225,13 @@ void OnroadWindow::paintEvent(QPaintEvent *event) {
}
}
// void OnroadWindow::update_screen_on_off() {
// int screenDisaplayMode = paramsMemory.getInt("ScreenDisaplayMode");
// if (screenDisaplayMode == 1) {
// // Conditionally off
// }
// }
// ***** onroad widgets *****
@@ -308,10 +324,7 @@ AnnotatedCameraWidget::AnnotatedCameraWidget(VisionStreamType type, QWidget* par
QHBoxLayout *buttons_layout = new QHBoxLayout();
buttons_layout->setSpacing(0);
// Neokii screen recorder
recorder_btn = new ScreenRecorder(this);
recorder_btn->setVisible(false);
// buttons_layout->addWidget(recorder_btn);
// Neokii screen recorder — DISABLED: using dashcamd instead
QVBoxLayout *top_right_layout = new QVBoxLayout();
top_right_layout->setSpacing(0);
@@ -327,24 +340,13 @@ AnnotatedCameraWidget::AnnotatedCameraWidget(VisionStreamType type, QWidget* par
}
void AnnotatedCameraWidget::updateState(const UIState &s) {
screenDisplayMode = paramsMemory.getInt("ScreenDisplayMode");
if (screenDisplayMode == 2 && !alert_is_visible) {
// Draw black, filled, full-size rectangle to blank the screen
// p.fillRect(0, 0, width(), height(), Qt::black);
// p.restore();
Hardware::set_display_power(false);
return;
} else {
Hardware::set_display_power(true);
}
const int SET_SPEED_NA = 255;
const SubMaster &sm = *(s.sm);
const bool cs_alive = sm.alive("controlsState");
const auto cs = sm["controlsState"].getControlsState();
const auto car_state = sm["carState"].getCarState();
(void)car_state; // CLEARPILOT: suppress unused warning, will use later
// Handle older routes where vCruiseCluster is not set
float v_cruise = cs.getVCruiseCluster() == 0.0 ? cs.getVCruise() : cs.getVCruiseCluster();
@@ -354,11 +356,16 @@ void AnnotatedCameraWidget::updateState(const UIState &s) {
setSpeed *= KM_TO_MILE;
}
// Handle older routes where vEgoCluster is not set
v_ego_cluster_seen = v_ego_cluster_seen || car_state.getVEgoCluster() != 0.0;
float v_ego = v_ego_cluster_seen && !scene.wheel_speed ? car_state.getVEgoCluster() : car_state.getVEgo();
speed = cs_alive ? std::max<float>(0.0, v_ego) : 0.0;
speed *= s.scene.is_metric ? MS_TO_KPH : MS_TO_MPH;
// CLEARPILOT: read speed and speed limit from params (written by speed_logic.py ~2Hz)
clpParamFrame++;
if (clpParamFrame % 10 == 0) { // ~2Hz at 20Hz UI update rate
clpHasSpeed = paramsMemory.get("ClearpilotHasSpeed") == "1";
clpSpeedDisplay = QString::fromStdString(paramsMemory.get("ClearpilotSpeedDisplay"));
clpSpeedLimitDisplay = QString::fromStdString(paramsMemory.get("ClearpilotSpeedLimitDisplay"));
clpSpeedUnit = QString::fromStdString(paramsMemory.get("ClearpilotSpeedUnit"));
clpCruiseWarning = QString::fromStdString(paramsMemory.get("ClearpilotCruiseWarning"));
clpCruiseWarningSpeed = QString::fromStdString(paramsMemory.get("ClearpilotCruiseWarningSpeed"));
}
// auto speed_limit_sign = nav_instruction.getSpeedLimitSign();
// speedLimit = slcOverridden ? scene.speed_limit_overridden_speed : speedLimitController ? scene.speed_limit : nav_alive ? nav_instruction.getSpeedLimit() : 0.0;
@@ -394,9 +401,28 @@ if (edgeColor != bgColor) {
}
void AnnotatedCameraWidget::drawHud(QPainter &p) {
// Blank when screenDisplayMode=1
// CLEARPILOT: display power control based on ScreenDisplayMode
p.save();
if (displayMode == 3 && !alert_is_visible) {
Hardware::set_display_power(false);
p.restore();
return;
} else {
Hardware::set_display_power(true);
}
// CLEARPILOT: blinking blue circle when telemetry is recording
if (paramsMemory.getBool("TelemetryEnabled")) {
// Blink: visible for 500ms, hidden for 500ms
int phase = (QDateTime::currentMSecsSinceEpoch() / 500) % 2;
if (phase == 0) {
p.setPen(Qt::NoPen);
p.setBrush(QColor(30, 100, 220));
p.drawEllipse(width() - 150, 50, 100, 100);
}
}
// Header gradient
QLinearGradient bg(0, UI_HEADER_HEIGHT - (UI_HEADER_HEIGHT / 2.5), 0, UI_HEADER_HEIGHT);
bg.setColorAt(0, QColor::fromRgbF(0, 0, 0, 0.45));
@@ -407,7 +433,7 @@ void AnnotatedCameraWidget::drawHud(QPainter &p) {
// QString speedLimitStr = (speedLimit > 1) ? QString::number(std::nearbyint(speedLimit)) : "";
// 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)) : "";
p.restore();
@@ -431,17 +457,21 @@ void AnnotatedCameraWidget::drawHud(QPainter &p) {
// Todo: lead speed
// Todo: Experimental speed
// // current speed
if (!(scene.hide_speed)) {
// CLEARPILOT changes to 120 from ~176
// Maybe we want to hide this?
// CLEARPILOT: show speed from speed_logic params, hide when no speed or speed=0
if (clpHasSpeed && !clpSpeedDisplay.isEmpty() && !scene.hide_speed) {
p.setFont(InterFont(140, QFont::Bold));
drawText(p, rect().center().x(), 210, speedStr);
// CLEARPILOT changes to 40 from 66
drawText(p, rect().center().x(), 210, clpSpeedDisplay);
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
paintFrogPilotWidgets(p);
}
@@ -535,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) {
QRect real_rect = p.fontMetrics().boundingRect(text);
real_rect.moveCenter({x, y - real_rect.height() / 2});
@@ -543,6 +731,87 @@ void AnnotatedCameraWidget::drawText(QPainter &p, int x, int y, const QString &t
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() {
CameraWidget::initializeGL();
qInfo() << "OpenGL version:" << QString((const char*)glGetString(GL_VERSION));
@@ -562,13 +831,6 @@ void AnnotatedCameraWidget::updateFrameMat() {
s->fb_w = w;
s->fb_h = h;
if (screenDisplayMode == 1 || screenDisplayMode == 2) {
// Render a black box instead of the video feed
QPainter painter(this);
painter.fillRect(0, 0, w, h, Qt::black);
return;
}
// Apply transformation such that video pixel coordinates match video
// 1) Put (0, 0) in the middle of the video
// 2) Apply same scaling as video
@@ -580,32 +842,65 @@ void AnnotatedCameraWidget::updateFrameMat() {
}
void AnnotatedCameraWidget::drawLaneLines(QPainter &painter, const UIState *s) {
if (screenDisplayMode == 2) {
return;
}
painter.save();
// CLEARPILOT: color channel code rewriten to allow custom colors
SubMaster &sm = *(s->sm);
// CLEARPILOT: nightrider mode — outline only, no fill
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
for (int i = 0; i < std::size(scene.lane_line_vertices); ++i) {
painter.setBrush(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) {
painter.setPen(QPen(lineColor, laneLineWidth));
painter.setBrush(Qt::NoBrush);
} else {
painter.setPen(Qt::NoPen);
painter.setBrush(lineColor);
}
painter.drawPolygon(scene.lane_line_vertices[i]);
}
if (outlineOnly) painter.setPen(Qt::NoPen);
// road edges
for (int i = 0; i < std::size(scene.road_edge_vertices); ++i) {
painter.setBrush(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) {
painter.setPen(QPen(edgeCol, outlineWidth));
painter.setBrush(Qt::NoBrush);
} else {
painter.setPen(Qt::NoPen);
painter.setBrush(edgeCol);
}
painter.drawPolygon(scene.road_edge_vertices[i]);
}
if (outlineOnly) painter.setPen(Qt::NoPen);
// paint center lane path
// 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;
@@ -665,12 +960,29 @@ void AnnotatedCameraWidget::drawLaneLines(QPainter &painter, const UIState *s) {
path_gradient.setColorAt(1.0, QColor(center_lane_color.red(), center_lane_color.green(), center_lane_color.blue(), static_cast<int>(CENTER_LANE_ALPHA * 255 * 0.0)));
}
painter.setBrush(path_gradient);
if (outlineOnly) {
// CLEARPILOT: in nightrider, the tire path is rendered as an outline.
// - 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);
} else {
painter.setPen(Qt::NoPen);
painter.setBrush(path_gradient);
}
painter.drawPolygon(scene.track_vertices);
if (outlineOnly) painter.setPen(Qt::NoPen);
}
// Paint path edges ,Use current background color
if (edgeColor != bg_colors[STATUS_DISENGAGED]) {
if (edgeColor != bg_colors[STATUS_DISENGAGED] && !outlineOnly) {
QLinearGradient edge_gradient;
edge_gradient.setColorAt(0.0, QColor(edgeColor.red(), edgeColor.green(), edgeColor.blue(), static_cast<int>(255)));
edge_gradient.setColorAt(0.5, QColor(edgeColor.red(), edgeColor.green(), edgeColor.blue(), static_cast<int>(255 * 0.8) ));
@@ -686,18 +998,24 @@ void AnnotatedCameraWidget::drawLaneLines(QPainter &painter, const UIState *s) {
// Paint blindspot path
if (scene.blind_spot_path) {
QLinearGradient bs(0, height(), 0, 0);
bs.setColorAt(0.0, QColor::fromHslF(0 / 360., 0.75, 0.50, 0.6));
bs.setColorAt(0.5, QColor::fromHslF(0 / 360., 0.75, 0.50, 0.4));
bs.setColorAt(1.0, QColor::fromHslF(0 / 360., 0.75, 0.50, 0.2));
painter.setBrush(bs);
QColor bsColor = QColor::fromHslF(0 / 360., 0.75, 0.50, 0.6);
if (outlineOnly) {
painter.setPen(QPen(bsColor, outlineWidth));
painter.setBrush(Qt::NoBrush);
} else {
QLinearGradient bs(0, height(), 0, 0);
bs.setColorAt(0.0, QColor::fromHslF(0 / 360., 0.75, 0.50, 0.6));
bs.setColorAt(0.5, QColor::fromHslF(0 / 360., 0.75, 0.50, 0.4));
bs.setColorAt(1.0, QColor::fromHslF(0 / 360., 0.75, 0.50, 0.2));
painter.setBrush(bs);
}
if (blindSpotLeft) {
painter.drawPolygon(scene.track_adjacent_vertices[4]);
}
if (blindSpotRight) {
painter.drawPolygon(scene.track_adjacent_vertices[5]);
}
if (outlineOnly) painter.setPen(Qt::NoPen);
}
// Paint adjacent lane paths
@@ -708,16 +1026,19 @@ void AnnotatedCameraWidget::drawLaneLines(QPainter &painter, const UIState *s) {
float maxLaneWidth = laneDetectionWidth * 1.5;
auto paintLane = [=](QPainter &painter, const QPolygonF &lane, float laneWidth, bool blindspot) {
QLinearGradient al(0, height(), 0, 0);
bool redPath = laneWidth < minLaneWidth || laneWidth > maxLaneWidth || blindspot;
float hue = redPath ? 0.0 : 120.0 * (laneWidth - minLaneWidth) / (maxLaneWidth - minLaneWidth);
al.setColorAt(0.0, QColor::fromHslF(hue / 360.0, 0.75, 0.50, 0.6));
al.setColorAt(0.5, QColor::fromHslF(hue / 360.0, 0.75, 0.50, 0.4));
al.setColorAt(1.0, QColor::fromHslF(hue / 360.0, 0.75, 0.50, 0.2));
painter.setBrush(al);
if (outlineOnly) {
painter.setPen(QPen(QColor::fromHslF(hue / 360.0, 0.75, 0.50, 0.6), 2));
painter.setBrush(Qt::NoBrush);
} else {
QLinearGradient al(0, height(), 0, 0);
al.setColorAt(0.0, QColor::fromHslF(hue / 360.0, 0.75, 0.50, 0.6));
al.setColorAt(0.5, QColor::fromHslF(hue / 360.0, 0.75, 0.50, 0.4));
al.setColorAt(1.0, QColor::fromHslF(hue / 360.0, 0.75, 0.50, 0.2));
painter.setBrush(al);
}
painter.drawPolygon(lane);
painter.setFont(InterFont(30, QFont::DemiBold));
@@ -805,6 +1126,10 @@ void AnnotatedCameraWidget::paintEvent(QPaintEvent *event) {
const cereal::ModelDataV2::Reader &model = sm["modelV2"].getModelV2();
const float v_ego = sm["carState"].getCarState().getVEgo();
// CLEARPILOT: read display mode early — needed for camera suppression
displayMode = paramsMemory.getInt("ScreenDisplayMode");
nightriderMode = (displayMode == 1 || displayMode == 4);
// draw camera frame
{
std::lock_guard lk(frame_lock);
@@ -845,9 +1170,19 @@ void AnnotatedCameraWidget::paintEvent(QPaintEvent *event) {
} else {
CameraWidget::updateCalibration(DEFAULT_CALIBRATION);
}
// CLEARPILOT: force CameraWidget bg to black in nightrider to prevent color bleed
if (nightriderMode) {
CameraWidget::setBackgroundColor(Qt::black);
}
painter.beginNativePainting();
CameraWidget::setFrameId(model.getFrameId());
CameraWidget::paintGL();
if (nightriderMode) {
// CLEARPILOT: black background, no camera feed
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
} else {
CameraWidget::setFrameId(model.getFrameId());
CameraWidget::paintGL();
}
painter.endNativePainting();
}
@@ -925,7 +1260,7 @@ void AnnotatedCameraWidget::initializeFrogPilotWidgets() {
animationFrameIndex = (animationFrameIndex + 1) % totalFrames;
});
// Initialize the timer for the screen recorder
// CLEARPILOT: screen recorder disabled — replaced by dedicated dashcamd process
// QTimer *record_timer = new QTimer(this);
// connect(record_timer, &QTimer::timeout, this, [this]() {
// if (recorder_btn) {
@@ -1010,7 +1345,8 @@ void AnnotatedCameraWidget::updateFrogPilotWidgets() {
}
void AnnotatedCameraWidget::paintFrogPilotWidgets(QPainter &p) {
if ((showAlwaysOnLateralStatusBar || showConditionalExperimentalStatusBar || roadNameUI)) {
// CLEARPILOT: only show status bar when telemetry is enabled
if (paramsMemory.getBool("TelemetryEnabled")) {
drawStatusBar(p);
}
@@ -1022,8 +1358,7 @@ void AnnotatedCameraWidget::paintFrogPilotWidgets(QPainter &p) {
drawSLCConfirmation(p);
}
// recorder_btn->setVisible(scene.screen_recorder && !mapOpen);
recorder_btn->setVisible(false);
// CLEARPILOT: screen recorder disabled, using dashcamd instead
}
void AnnotatedCameraWidget::drawLeadInfo(QPainter &p) {
@@ -1212,7 +1547,30 @@ void AnnotatedCameraWidget::drawStatusBar(QPainter &p) {
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"));
} else if (showConditionalExperimentalStatusBar) {
newStatus = conditionalStatusMap[status != STATUS_DISENGAGED ? conditionalStatus : 0];
+17 -4
View File
@@ -12,7 +12,7 @@
#include "selfdrive/ui/ui.h"
#include "selfdrive/ui/qt/widgets/cameraview.h"
#include "selfdrive/frogpilot/screenrecorder/screenrecorder.h"
// #include "selfdrive/frogpilot/screenrecorder/screenrecorder.h" // DISABLED: using dashcamd instead
const int btn_size = 192;
const int img_size = (btn_size / 4) * 3;
@@ -47,16 +47,31 @@ public:
explicit AnnotatedCameraWidget(VisionStreamType type, QWidget* parent = 0);
void updateState(const UIState &s);
void updateLaneEdgeColor(QColor &bgColor);
int screenDisplayMode = 0;
private:
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 drawSpeedLimitSign(QPainter &p);
void drawCruiseWarningSign(QPainter &p);
void drawHealthMetrics(QPainter &p);
QVBoxLayout *main_layout;
QPixmap dm_img;
float speed;
bool has_gps_speed = false;
bool nightriderMode = false;
int displayMode = 0;
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 speedLimit;
bool is_cruise_set = false;
@@ -89,8 +104,6 @@ private:
Params paramsMemory{"/dev/shm/params"};
UIScene &scene;
ScreenRecorder *recorder_btn;
QHBoxLayout *bottom_layout;
bool alwaysOnLateralActive;
+74 -13
View File
@@ -30,11 +30,12 @@ ReadyWindow::ReadyWindow(QWidget *parent) : QWidget(parent) {
timer = new QTimer(this);
timer->callOnTimeout(this, &ReadyWindow::refresh);
uptime.start();
}
void ReadyWindow::showEvent(QShowEvent *event) {
refresh();
timer->start(5 * 1000);
timer->start(2 * 1000);
}
void ReadyWindow::hideEvent(QHideEvent *event) {
@@ -43,34 +44,94 @@ void ReadyWindow::hideEvent(QHideEvent *event) {
void ReadyWindow::paintEvent(QPaintEvent *event) {
QPainter painter(this);
QPixmap *img_shown = nullptr;
painter.fillRect(rect(), Qt::black);
if (is_hot) {
if (img_hot.isNull()) {
img_hot.load("/data/openpilot/selfdrive/clearpilot/theme/clearpilot/images/hot.png");
}
img_shown = &img_hot;
int x = (width() - img_hot.width()) / 2;
int y = (height() - img_hot.height()) / 2;
painter.drawPixmap(x, y, img_hot);
} else {
if (img_ready.isNull()) {
img_ready.load("/data/openpilot/selfdrive/clearpilot/theme/clearpilot/images/ready.png");
// Boot logo — same rotation as spinner (bg.jpg is pre-rotated 90° CW for framebuffer)
if (img_bg.isNull()) {
QPixmap raw("/usr/comma/bg.jpg");
if (!raw.isNull()) {
QTransform rot;
rot.rotate(-90);
img_bg = raw.transformed(rot);
}
}
if (!img_bg.isNull()) {
QPixmap scaled = img_bg.scaled(size(), Qt::KeepAspectRatio, Qt::SmoothTransformation);
int x = (width() - scaled.width()) / 2;
int y = (height() - scaled.height()) / 2;
painter.drawPixmap(x, y, scaled);
}
img_shown = &img_ready;
}
int x = (width() - img_shown->width()) / 2;
int y = (height() - img_shown->height()) / 2;
painter.drawPixmap(x, y, *img_shown);
if (error_msg.isEmpty() && !has_driven) {
// "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");
if (!ready_text.isNull()) {
int tx = (width() - ready_text.width()) / 2;
int ty = height() / 2 + height() * 15 / 100;
painter.drawPixmap(tx, ty, ready_text);
}
} else {
// Error state: red text at 25% below center
QFont font("Inter", 50, QFont::Bold);
painter.setFont(font);
painter.setPen(QColor(0xFF, 0x44, 0x44));
int ty = height() / 2 + height() * 25 / 100;
QRect text_rect(0, ty, width(), 100);
painter.drawText(text_rect, Qt::AlignHCenter | Qt::AlignTop, error_msg);
}
}
}
void ReadyWindow::refresh() {
bool changed = false;
// Temperature check
std::string bytes = params.get("Offroad_TemperatureTooHigh");
bool was_hot = is_hot;
if (!bytes.empty()) {
auto doc = QJsonDocument::fromJson(bytes.data());
is_hot = true;
cur_temp = doc["extra"].toString();
update();
} else if (is_hot) {
} else {
is_hot = false;
update();
}
if (is_hot != was_hot) changed = true;
// Error state checks (only when not hot — hot has its own display)
if (!is_hot) {
QString prev_error = error_msg;
// Panda check — same logic as sidebar, with 10s grace period on startup
if (uptime.elapsed() > 10000 &&
uiState()->scene.pandaType == cereal::PandaState::PandaType::UNKNOWN) {
error_msg = "PANDA NOT CONNECTED";
}
// Car unrecognized check
else if (!params.get("Offroad_CarUnrecognized").empty()) {
error_msg = "CAR NOT RECOGNIZED";
}
else {
error_msg = "";
}
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();
}
+7 -2
View File
@@ -8,8 +8,9 @@
#include <QSocketNotifier>
#include <QVariantAnimation>
#include <QWidget>
#include <QElapsedTimer>
#include <QTimer>
#include "common/util.h"
#include "selfdrive/ui/ui.h"
@@ -17,6 +18,7 @@ class ReadyWindow : public QWidget {
Q_OBJECT
public:
ReadyWindow(QWidget* parent = nullptr);
bool has_driven = false;
private:
void showEvent(QShowEvent *event) override;
void hideEvent(QHideEvent *event) override;
@@ -26,6 +28,9 @@ private:
QTimer* timer;
bool is_hot = false;
QString cur_temp;
QPixmap img_ready;
QString error_msg; // non-empty = show red error instead of READY!
QElapsedTimer uptime;
bool last_started = false;
QPixmap img_bg;
QPixmap img_hot;
};
Binary file not shown.
+310 -3
View File
@@ -1,6 +1,9 @@
#include "selfdrive/ui/qt/window.h"
#include <QFontDatabase>
#include <QMouseEvent>
#include <zmq.h>
#include "system/hardware/hw.h"
@@ -11,6 +14,7 @@ MainWindow::MainWindow(QWidget *parent) : QWidget(parent) {
homeWindow = new HomeWindow(this);
main_layout->addWidget(homeWindow);
QObject::connect(homeWindow, &HomeWindow::openSettings, this, &MainWindow::openSettings);
QObject::connect(homeWindow, &HomeWindow::openStatus, this, &MainWindow::openStatus);
QObject::connect(homeWindow, &HomeWindow::closeSettings, this, &MainWindow::closeSettings);
settingsWindow = new SettingsWindow(this);
@@ -24,6 +28,11 @@ MainWindow::MainWindow(QWidget *parent) : QWidget(parent) {
homeWindow->showDriverView(true);
});
// CLEARPILOT: Status window
statusWindow = new StatusWindow(this);
main_layout->addWidget(statusWindow);
QObject::connect(statusWindow, &StatusWindow::closeStatus, this, &MainWindow::closeSettings);
onboardingWindow = new OnboardingWindow(this);
main_layout->addWidget(onboardingWindow);
QObject::connect(onboardingWindow, &OnboardingWindow::onboardingDone, [=]() {
@@ -35,13 +44,17 @@ MainWindow::MainWindow(QWidget *parent) : QWidget(parent) {
QObject::connect(uiState(), &UIState::offroadTransition, [=](bool offroad) {
if (!offroad) {
closeSettings();
// CLEARPILOT: just switch to homeWindow, don't show sidebar
// HomeWindow::offroadTransition handles the internal view
main_layout->setCurrentWidget(homeWindow);
}
});
QObject::connect(device(), &Device::interactiveTimeout, [=]() {
if (main_layout->currentWidget() == settingsWindow) {
closeSettings();
// CLEARPILOT: on timeout, return to splash/onroad (not ClearPilotPanel)
if (main_layout->currentWidget() != homeWindow) {
main_layout->setCurrentWidget(homeWindow);
}
homeWindow->offroadTransition(!uiState()->scene.started);
});
// load fonts
@@ -63,6 +76,74 @@ MainWindow::MainWindow(QWidget *parent) : QWidget(parent) {
}
)");
setAttribute(Qt::WA_NoSystemBackground);
// CLEARPILOT: UI introspection RPC server
zmq_ctx = zmq_ctx_new();
zmq_sock = zmq_socket(zmq_ctx, ZMQ_REP);
zmq_bind(zmq_sock, "ipc:///tmp/clearpilot_ui_rpc");
int fd;
size_t fd_sz = sizeof(fd);
zmq_getsockopt(zmq_sock, ZMQ_FD, &fd, &fd_sz);
rpc_notifier = new QSocketNotifier(fd, QSocketNotifier::Read, this);
connect(rpc_notifier, &QSocketNotifier::activated, this, &MainWindow::handleRpcRequest);
}
void MainWindow::handleRpcRequest() {
int events = 0;
size_t events_sz = sizeof(events);
zmq_getsockopt(zmq_sock, ZMQ_EVENTS, &events, &events_sz);
if (!(events & ZMQ_POLLIN)) return;
char buf[256];
int rc = zmq_recv(zmq_sock, buf, sizeof(buf) - 1, ZMQ_DONTWAIT);
if (rc < 0) return;
buf[rc] = 0;
QString response;
if (strcmp(buf, "dump") == 0) {
response = dumpWidgetTree(this);
} else {
response = "unknown command";
}
QByteArray resp = response.toUtf8();
zmq_send(zmq_sock, resp.data(), resp.size(), 0);
}
QString MainWindow::dumpWidgetTree(QWidget *w, int depth) {
QString result;
QString indent(depth * 2, ' ');
QString className = w->metaObject()->className();
QString name = w->objectName().isEmpty() ? "(no name)" : w->objectName();
bool visible = w->isVisible();
QRect geo = w->geometry();
result += QString("%1%2 [%3] vis=%4 geo=%5,%6 %7x%8")
.arg(indent, className, name)
.arg(visible ? "Y" : "N")
.arg(geo.x()).arg(geo.y()).arg(geo.width()).arg(geo.height());
// Show stacked layout/widget current index
if (auto *sl = w->findChild<QStackedLayout *>(QString(), Qt::FindDirectChildrenOnly)) {
QWidget *cur = sl->currentWidget();
QString curClass = cur ? cur->metaObject()->className() : "null";
result += QString(" stack_cur=%1/%2(%3)").arg(sl->currentIndex()).arg(sl->count()).arg(curClass);
}
if (auto *sw = qobject_cast<QStackedWidget *>(w)) {
QWidget *cur = sw->currentWidget();
QString curClass = cur ? cur->metaObject()->className() : "null";
result += QString(" stack_cur=%1/%2(%3)").arg(sw->currentIndex()).arg(sw->count()).arg(curClass);
}
result += "\n";
for (QObject *child : w->children()) {
QWidget *cw = qobject_cast<QWidget *>(child);
if (cw && depth < 4) {
result += dumpWidgetTree(cw, depth + 1);
}
}
return result;
}
void MainWindow::openSettings(int index, const QString &param) {
@@ -70,6 +151,10 @@ void MainWindow::openSettings(int index, const QString &param) {
settingsWindow->setCurrentPanel(index, param);
}
void MainWindow::openStatus() {
main_layout->setCurrentWidget(statusWindow);
}
void MainWindow::closeSettings() {
main_layout->setCurrentWidget(homeWindow);
@@ -86,6 +171,16 @@ bool MainWindow::eventFilter(QObject *obj, QEvent *event) {
case QEvent::TouchEnd:
case QEvent::MouseButtonPress:
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 = !device()->isAwake();
device()->resetInteractiveTimeout(uiState()->scene.screen_timeout, uiState()->scene.screen_timeout_onroad);
@@ -96,3 +191,215 @@ bool MainWindow::eventFilter(QObject *obj, QEvent *event) {
}
return ignore;
}
// CLEARPILOT: Status window — live system stats, collected on background thread
#include <QFile>
#include <QProcess>
#include <QDateTime>
#include <QtConcurrent>
static QString readFile(const QString &path) {
QFile f(path);
if (f.open(QIODevice::ReadOnly)) return QString(f.readAll()).trimmed();
return "";
}
static QString shellCmd(const QString &cmd) {
QProcess p;
p.start("bash", QStringList() << "-c" << cmd);
p.waitForFinished(1000);
return QString(p.readAllStandardOutput()).trimmed();
}
static StatusWindow::StatusData collectStatus() {
StatusWindow::StatusData d;
d.time = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss");
d.storage = shellCmd("df -h /data | tail -1 | awk '{print $3 \" / \" $2 \" (\" $5 \" used)\"}'");
// RAM from /proc/meminfo (no subprocess needed)
QString meminfo = readFile("/proc/meminfo");
long total = 0, avail = 0;
for (const QString &line : meminfo.split('\n')) {
if (line.startsWith("MemTotal:")) total = line.split(QRegExp("\\s+"))[1].toLong();
if (line.startsWith("MemAvailable:")) avail = line.split(QRegExp("\\s+"))[1].toLong();
}
if (total > 0) {
long used = total - avail;
d.ram = QString("%1 / %2 MB").arg(used / 1024).arg(total / 1024);
}
// Load from /proc/loadavg
QString loadavg = readFile("/proc/loadavg");
QStringList parts = loadavg.split(' ');
if (parts.size() >= 3) {
d.load = QString("%1 %2 %3").arg(parts[0], parts[1], parts[2]);
}
// Temperature
QString temps = shellCmd("cat /sys/class/thermal/thermal_zone*/temp 2>/dev/null | sort -rn | head -1");
if (!temps.isEmpty()) {
d.temp_c = temps.toLong() / 1000.0f;
d.temp = QString("%1\u00B0C").arg(d.temp_c, 0, 'f', 1);
}
// Fan
QString fan = shellCmd("cat /sys/class/hwmon/hwmon*/fan1_input 2>/dev/null | head -1");
if (fan.isEmpty()) fan = readFile("/dev/shm/params/d/LastFanSpeed");
d.fan = fan.isEmpty() ? QString::fromUtf8("\u2014") : fan + " RPM";
// Network
d.ip = shellCmd("ip route get 1.1.1.1 2>/dev/null | head -1 | awk '{print $7}'");
d.wifi = shellCmd("iwconfig wlan0 2>/dev/null | grep -oP 'ESSID:\"\\K[^\"]*'");
// VPN
QString tun = shellCmd("ip link show tun0 2>/dev/null | head -1");
if (tun.contains("UP")) {
d.vpn_status = "up";
d.vpn_ip = shellCmd("ip addr show tun0 2>/dev/null | grep 'inet ' | awk '{print $2}'");
}
// GPS
d.gps = readFile("/data/params/d/LastGPSPosition");
// Telemetry
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
return d;
}
StatusWindow::StatusWindow(QWidget *parent) : QFrame(parent) {
QVBoxLayout *layout = new QVBoxLayout(this);
layout->setContentsMargins(50, 60, 50, 40);
layout->setSpacing(0);
// Status rows
auto makeRow = [&](const QString &label) -> QLabel* {
QHBoxLayout *row = new QHBoxLayout();
row->setContentsMargins(20, 0, 20, 0);
QLabel *name = new QLabel(label);
name->setStyleSheet("color: grey; font-size: 38px;");
name->setFixedWidth(350);
row->addWidget(name);
QLabel *value = new QLabel("");
value->setStyleSheet("color: white; font-size: 38px;");
row->addWidget(value);
row->addStretch();
layout->addLayout(row);
layout->addSpacing(12);
return value;
};
time_label = makeRow("Time");
storage_label = makeRow("Storage");
ram_label = makeRow("Memory");
load_label = makeRow("Load");
temp_label = makeRow("Temperature");
fan_label = makeRow("Fan Speed");
panda_label = makeRow("Panda");
ip_label = makeRow("IP Address");
wifi_label = makeRow("WiFi");
vpn_label = makeRow("VPN");
gps_label = makeRow("GPS");
telemetry_label = makeRow("Telemetry");
dashcam_label = makeRow("Dashcam");
layout->addStretch();
setStyleSheet("StatusWindow { background-color: black; }");
connect(&watcher, &QFutureWatcher<StatusData>::finished, this, &StatusWindow::applyResults);
QTimer *timer = new QTimer(this);
connect(timer, &QTimer::timeout, this, &StatusWindow::kickRefresh);
timer->start(1000);
}
void StatusWindow::kickRefresh() {
if (!isVisible() || collecting) return;
collecting = true;
watcher.setFuture(QtConcurrent::run(collectStatus));
}
void StatusWindow::applyResults() {
collecting = false;
StatusData d = watcher.result();
time_label->setText(d.time);
storage_label->setText(d.storage);
if (!d.ram.isEmpty()) ram_label->setText(d.ram);
if (!d.load.isEmpty()) load_label->setText(d.load);
if (!d.temp.isEmpty()) {
temp_label->setText(d.temp);
temp_label->setStyleSheet(d.temp_c > 70 ? "color: #ff4444; font-size: 38px;" :
d.temp_c > 55 ? "color: #ffaa00; font-size: 38px;" :
"color: white; font-size: 38px;");
}
fan_label->setText(d.fan);
// Panda: same check as sidebar — read scene.pandaType on UI thread
if (uiState()->scene.pandaType != cereal::PandaState::PandaType::UNKNOWN) {
panda_label->setText("Connected");
panda_label->setStyleSheet("color: #17c44d; font-size: 38px;");
} else {
panda_label->setText("Not connected");
panda_label->setStyleSheet("color: #ff4444; font-size: 38px;");
}
ip_label->setText(d.ip.isEmpty() ? "No connection" : d.ip);
wifi_label->setText(d.wifi.isEmpty() ? "Not connected" : d.wifi);
if (d.vpn_status == "up") {
vpn_label->setText("Connected (" + d.vpn_ip + ")");
vpn_label->setStyleSheet("color: #17c44d; font-size: 38px;");
} else {
vpn_label->setText("Not connected");
vpn_label->setStyleSheet("color: #ff4444; font-size: 38px;");
}
if (d.gps.isEmpty()) {
gps_label->setText("No fix");
gps_label->setStyleSheet("color: #ff4444; font-size: 38px;");
} else {
gps_label->setText(d.gps);
gps_label->setStyleSheet("color: white; font-size: 38px;");
}
if (d.telemetry == "1") {
telemetry_label->setText("Enabled");
telemetry_label->setStyleSheet("color: #17c44d; font-size: 38px;");
} else {
telemetry_label->setText("Disabled");
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) {
emit closeStatus();
}
+58
View File
@@ -1,12 +1,59 @@
#pragma once
#include <QFutureWatcher>
#include <QStackedLayout>
#include <QLabel>
#include <QSocketNotifier>
#include <QTimer>
#include <QWidget>
#include "selfdrive/ui/qt/home.h"
#include "selfdrive/ui/qt/offroad/onboarding.h"
#include "selfdrive/ui/qt/offroad/settings.h"
class StatusWindow : public QFrame {
Q_OBJECT
public:
explicit StatusWindow(QWidget *parent = 0);
struct StatusData {
QString time, storage, ram, load, temp, fan, ip, wifi;
QString vpn_status, vpn_ip, gps, telemetry;
QString dashcam_state, dashcam_frames;
float temp_c = 0;
};
protected:
void mousePressEvent(QMouseEvent *e) override;
signals:
void closeStatus();
private slots:
void kickRefresh();
void applyResults();
private:
QFutureWatcher<StatusData> watcher;
bool collecting = false;
QLabel *storage_label;
QLabel *ram_label;
QLabel *load_label;
QLabel *temp_label;
QLabel *fan_label;
QLabel *ip_label;
QLabel *wifi_label;
QLabel *vpn_label;
QLabel *gps_label;
QLabel *time_label;
QLabel *telemetry_label;
QLabel *dashcam_label;
QLabel *panda_label;
};
class MainWindow : public QWidget {
Q_OBJECT
@@ -16,13 +63,24 @@ public:
private:
bool eventFilter(QObject *obj, QEvent *event) override;
void openSettings(int index = 0, const QString &param = "");
void openStatus();
void closeSettings();
QString dumpWidgetTree(QWidget *w, int depth = 0);
QStackedLayout *main_layout;
HomeWindow *homeWindow;
SettingsWindow *settingsWindow;
StatusWindow *statusWindow;
OnboardingWindow *onboardingWindow;
// CLEARPILOT: UI introspection RPC
void *zmq_ctx = nullptr;
void *zmq_sock = nullptr;
QSocketNotifier *rpc_notifier = nullptr;
// FrogPilot variables
Params params;
private slots:
void handleRpcRequest();
};
+45 -1
View File
@@ -93,6 +93,29 @@ class Soundd:
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):
self.loaded_sounds: dict[int, np.ndarray] = {}
@@ -137,7 +160,20 @@ class Soundd:
written_frames += 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:
if status:
@@ -205,6 +241,14 @@ class Soundd:
if self.params_memory.get_bool("FrogPilotTogglesUpdated"):
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):
self.alert_volume_control = self.params.get_bool("AlertVolumeControl")
+3 -3
View File
@@ -818,15 +818,15 @@
<name>OffroadHome</name>
<message>
<source>UPDATE</source>
<translation>تحديث</translation>
<translation type="vanished">تحديث</translation>
</message>
<message>
<source> ALERTS</source>
<translation> التنبهات</translation>
<translation type="vanished"> التنبهات</translation>
</message>
<message>
<source> ALERT</source>
<translation> تنبيه</translation>
<translation type="vanished"> تنبيه</translation>
</message>
</context>
<context>
+3 -3
View File
@@ -771,15 +771,15 @@
<name>OffroadHome</name>
<message>
<source>UPDATE</source>
<translation>Aktualisieren</translation>
<translation type="vanished">Aktualisieren</translation>
</message>
<message>
<source> ALERTS</source>
<translation> HINWEISE</translation>
<translation type="vanished"> HINWEISE</translation>
</message>
<message>
<source> ALERT</source>
<translation> HINWEIS</translation>
<translation type="vanished"> HINWEIS</translation>
</message>
</context>
<context>
+3 -3
View File
@@ -814,15 +814,15 @@
<name>OffroadHome</name>
<message>
<source>UPDATE</source>
<translation>MISE À JOUR</translation>
<translation type="vanished">MISE À JOUR</translation>
</message>
<message>
<source> ALERTS</source>
<translation> ALERTES</translation>
<translation type="vanished"> ALERTES</translation>
</message>
<message>
<source> ALERT</source>
<translation> ALERTE</translation>
<translation type="vanished"> ALERTE</translation>
</message>
</context>
<context>
+3 -3
View File
@@ -770,15 +770,15 @@
<name>OffroadHome</name>
<message>
<source>UPDATE</source>
<translation></translation>
<translation type="vanished"></translation>
</message>
<message>
<source> ALERTS</source>
<translation> </translation>
<translation type="vanished"> </translation>
</message>
<message>
<source> ALERT</source>
<translation> </translation>
<translation type="vanished"> </translation>
</message>
</context>
<context>
+3 -3
View File
@@ -813,15 +813,15 @@
<name>OffroadHome</name>
<message>
<source>UPDATE</source>
<translation></translation>
<translation type="vanished"></translation>
</message>
<message>
<source> ALERTS</source>
<translation> </translation>
<translation type="vanished"> </translation>
</message>
<message>
<source> ALERT</source>
<translation> </translation>
<translation type="vanished"> </translation>
</message>
</context>
<context>
+3 -3
View File
@@ -814,15 +814,15 @@
<name>OffroadHome</name>
<message>
<source>UPDATE</source>
<translation>ATUALIZAÇÃO</translation>
<translation type="vanished">ATUALIZAÇÃO</translation>
</message>
<message>
<source> ALERTS</source>
<translation> ALERTAS</translation>
<translation type="vanished"> ALERTAS</translation>
</message>
<message>
<source> ALERT</source>
<translation> ALERTA</translation>
<translation type="vanished"> ALERTA</translation>
</message>
</context>
<context>
+3 -3
View File
@@ -813,15 +813,15 @@
<name>OffroadHome</name>
<message>
<source>UPDATE</source>
<translation></translation>
<translation type="vanished"></translation>
</message>
<message>
<source> ALERTS</source>
<translation> </translation>
<translation type="vanished"> </translation>
</message>
<message>
<source> ALERT</source>
<translation> </translation>
<translation type="vanished"> </translation>
</message>
</context>
<context>
+3 -3
View File
@@ -770,15 +770,15 @@
<name>OffroadHome</name>
<message>
<source>UPDATE</source>
<translation>GÜNCELLE</translation>
<translation type="vanished">GÜNCELLE</translation>
</message>
<message>
<source> ALERTS</source>
<translation> UYARILAR</translation>
<translation type="vanished"> UYARILAR</translation>
</message>
<message>
<source> ALERT</source>
<translation> UYARI</translation>
<translation type="vanished"> UYARI</translation>
</message>
</context>
<context>
+3 -3
View File
@@ -813,15 +813,15 @@
<name>OffroadHome</name>
<message>
<source>UPDATE</source>
<translation></translation>
<translation type="vanished"></translation>
</message>
<message>
<source> ALERTS</source>
<translation> </translation>
<translation type="vanished"> </translation>
</message>
<message>
<source> ALERT</source>
<translation> </translation>
<translation type="vanished"> </translation>
</message>
</context>
<context>
+3 -3
View File
@@ -813,15 +813,15 @@
<name>OffroadHome</name>
<message>
<source>UPDATE</source>
<translation></translation>
<translation type="vanished"></translation>
</message>
<message>
<source> ALERTS</source>
<translation> </translation>
<translation type="vanished"> </translation>
</message>
<message>
<source> ALERT</source>
<translation> </translation>
<translation type="vanished"> </translation>
</message>
</context>
<context>
+18 -4
View File
@@ -93,6 +93,9 @@ void update_model(UIState *s,
if (plan_position.getX().size() < model.getPosition().getX().size()) {
plan_position = model.getPosition();
}
// CLEARPILOT: guard against empty model data (bench mode, no modeld running)
if (plan_position.getX().size() == 0) return;
float max_distance = scene.unlimited_road_ui_length ? *(plan_position.getX().end() - 1) :
std::clamp(*(plan_position.getX().end() - 1),
MIN_DRAW_DISTANCE, MAX_DRAW_DISTANCE);
@@ -115,8 +118,11 @@ void update_model(UIState *s,
}
// 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;
if (paramsMemory.getBool("no_lat_lane_change")) {
if (no_lat_lane_change) {
path = (float)LANE_CHANGE_NO_LAT_PATH_WIDTH / 20; // Release: better calc for EU users
} else {
path = (float)CENTER_LANE_WIDTH / 20; // Release: better calc for EU users
@@ -391,6 +397,7 @@ void ui_update_frogpilot_params(UIState *s) {
scene.screen_brightness = screen_management ? params.getInt("ScreenBrightness") : 101;
scene.screen_brightness_onroad = screen_management ? params.getInt("ScreenBrightnessOnroad") : 101;
scene.screen_recorder = screen_management && params.getBool("ScreenRecorder");
scene.screen_recorder_debug = params.getBool("ScreenRecorderDebug");
scene.screen_timeout = screen_management ? params.getInt("ScreenTimeout") : 120;
scene.screen_timeout_onroad = screen_management ? params.getInt("ScreenTimeoutOnroad") : 10;
scene.standby_mode = screen_management && params.getBool("StandbyMode");
@@ -435,9 +442,10 @@ void UIState::updateStatus() {
UIState::UIState(QObject *parent) : QObject(parent) {
sm = std::make_unique<SubMaster, const std::initializer_list<const char *>>({
"modelV2", "controlsState", "liveCalibration", "radarState", "deviceState",
"pandaStates", "carParams", "driverMonitoringState", "carState", "liveLocationKalman", "driverStateV2",
"pandaStates", "peripheralState", "carParams", "driverMonitoringState", "carState", "liveLocationKalman", "driverStateV2",
"wideRoadCameraState", "managerState", "uiPlan", "liveTorqueParameters",
"frogpilotCarControl", "frogpilotDeviceState", "frogpilotPlan",
"gpsLocation",
});
Params params;
@@ -551,7 +559,10 @@ void Device::updateWakefulness(const UIState &s) {
}
if (ignition_state_changed) {
if (ignition_on && s.scene.screen_brightness_onroad == 0 && !s.scene.standby_mode) {
if (!ignition_on) {
// CLEARPILOT: ignition on→off blanks the screen immediately (tap still wakes).
resetInteractiveTimeout(0, 0);
} else if (s.scene.screen_brightness_onroad == 0 && !s.scene.standby_mode) {
resetInteractiveTimeout(0, 0);
} else {
resetInteractiveTimeout(s.scene.screen_timeout, s.scene.screen_timeout_onroad);
@@ -560,7 +571,10 @@ void Device::updateWakefulness(const UIState &s) {
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);
} else {
setAwake(interactive_timeout > 0);
+1
View File
@@ -231,6 +231,7 @@ typedef struct UIScene {
bool road_name_ui;
bool rotating_wheel;
bool screen_recorder;
bool screen_recorder_debug;
bool show_aol_status_bar;
bool show_cem_status_bar;
bool show_jerk;
View File
+261
View File
@@ -0,0 +1,261 @@
#!/usr/bin/env python3
"""
ClearPilot GPS daemon reads GPS from Quectel EC25 modem via AT commands
and publishes gpsLocation messages.
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 os
import subprocess
import sys
import time
import datetime
from cereal import log
import cereal.messaging as messaging
from openpilot.common.gpio import gpio_init, gpio_set
from openpilot.common.params import Params
from openpilot.common.time import system_time_valid
from openpilot.system.hardware.tici.pins import GPIO
def _sunrise_sunset_min(lat: float, lon: float, utc_dt: datetime.datetime):
"""Compute (sunrise_min, sunset_min) in UTC minutes since midnight of utc_dt's day.
Values can be negative or >1440 for western/eastern longitudes. Returns
(None, None) for polar night, ('always', 'always') for midnight sun."""
n = utc_dt.timetuple().tm_yday
gamma = 2 * math.pi / 365 * (n - 1 + (utc_dt.hour - 12) / 24)
eqtime = 229.18 * (0.000075 + 0.001868 * math.cos(gamma)
- 0.032077 * math.sin(gamma)
- 0.014615 * math.cos(2 * gamma)
- 0.040849 * math.sin(2 * gamma))
decl = (0.006918 - 0.399912 * math.cos(gamma)
+ 0.070257 * math.sin(gamma)
- 0.006758 * math.cos(2 * gamma)
+ 0.000907 * math.sin(2 * gamma)
- 0.002697 * math.cos(3 * gamma)
+ 0.00148 * math.sin(3 * gamma))
lat_rad = math.radians(lat)
zenith = math.radians(90.833)
cos_ha = (math.cos(zenith) / (math.cos(lat_rad) * math.cos(decl))
- math.tan(lat_rad) * math.tan(decl))
if cos_ha < -1:
return ('always', 'always') # midnight sun
if cos_ha > 1:
return (None, None) # polar night
ha = math.degrees(math.acos(cos_ha))
sunrise_min = 720 - 4 * (lon + ha) - eqtime
sunset_min = 720 - 4 * (lon - ha) - eqtime
return (sunrise_min, sunset_min)
def is_daylight(lat: float, lon: float, utc_dt: datetime.datetime) -> bool:
"""Return True if the sun is currently above the horizon at (lat, lon).
Handles west-of-Greenwich correctly: at UTC midnight it may still be
evening local time, and the relevant sunset is the PREVIOUS UTC day's
value (which is >1440 min if we re-ref to that day, i.e. it's past
midnight UTC). Symmetric case for east-of-Greenwich at the other end.
Strategy: compute sunrise/sunset for yesterday, today, and tomorrow (each
relative to its own UTC midnight), shift them all onto today's clock
(yesterday = -1440, tomorrow = +1440), and check if now_min falls inside
any of the three [sunrise, sunset] intervals.
"""
now_min = utc_dt.hour * 60 + utc_dt.minute + utc_dt.second / 60
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:
try:
result = subprocess.check_output(
f"mmcli -m any --timeout 10 --command='{cmd}'",
shell=True, encoding='utf8', stderr=subprocess.DEVNULL
).strip()
# mmcli wraps AT responses: response: '+QGPSLOC: ...' — strip the wrapper
if result.startswith("response: '") and result.endswith("'"):
result = result[len("response: '"):-1]
return result
except subprocess.CalledProcessError:
return ""
def wait_for_modem():
print("CLP gpsd: waiting for modem", file=sys.stderr, flush=True)
while True:
ret = subprocess.call(
"mmcli -m any --timeout 10 --command='AT+QGPS?'",
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, shell=True
)
if ret == 0:
return
time.sleep(0.5)
def parse_qgpsloc(response: str):
"""Parse AT+QGPSLOC=2 response into a dict.
Format: +QGPSLOC: UTC,lat,lon,hdop,alt,fix,cog,spkm,spkn,date,nsat
"""
if "+QGPSLOC:" not in response:
return None
try:
data = response.split("+QGPSLOC:")[1].strip()
fields = data.split(",")
if len(fields) < 11:
return None
utc = fields[0] # HHMMSS.S
lat = float(fields[1])
lon = float(fields[2])
hdop = float(fields[3])
alt = float(fields[4])
fix = int(fields[5]) # 2=2D, 3=3D
cog = float(fields[6]) # course over ground
spkm = float(fields[7]) # speed km/h
date = fields[9] # DDMMYY
nsat = int(fields[10])
# Build unix timestamp from UTC + date
hh, mm = int(utc[0:2]), int(utc[2:4])
ss = float(utc[4:])
dd, mo, yy = int(date[0:2]), int(date[2:4]), 2000 + int(date[4:6])
dt = datetime.datetime(yy, mo, dd, hh, mm, int(ss),
int((ss % 1) * 1e6), datetime.timezone.utc)
return {
"latitude": lat,
"longitude": lon,
"altitude": alt,
"speed": spkm / 3.6, # km/h -> m/s
"bearing": cog,
"accuracy": hdop * 5, # rough HDOP -> meters
"timestamp_ms": dt.timestamp() * 1e3,
"satellites": nsat,
"fix": fix,
}
except (ValueError, IndexError) as e:
print(f"CLP gpsd: parse error: {e}", file=sys.stderr, flush=True)
return None
def main():
print("CLP gpsd: starting", file=sys.stderr, flush=True)
# Kill system gpsd which may respawn and interfere with modem access
subprocess.run("pkill -f /usr/sbin/gpsd", shell=True,
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
wait_for_modem()
print("CLP gpsd: modem ready", file=sys.stderr, flush=True)
# Enable GPS antenna power
gpio_init(GPIO.GNSS_PWR_EN, True)
gpio_set(GPIO.GNSS_PWR_EN, True)
print("CLP gpsd: GPIO power enabled", file=sys.stderr, flush=True)
# Don't restart GPS if already running (preserve existing fix)
resp = at_cmd("AT+QGPS?")
print(f"CLP gpsd: QGPS status: {resp}", file=sys.stderr, flush=True)
if "QGPS: 1" not in resp:
at_cmd('AT+QGPSCFG="dpoenable",0')
at_cmd('AT+QGPSCFG="outport","none"')
at_cmd("AT+QGPS=1")
print("CLP gpsd: GPS started fresh", file=sys.stderr, flush=True)
else:
print("CLP gpsd: GPS already running, keeping fix", file=sys.stderr, flush=True)
pm = messaging.PubMaster(["gpsLocation"])
clock_set = system_time_valid()
params_memory = Params("/dev/shm/params")
last_daylight_check = 0.0
daylight_computed = False
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:
resp = at_cmd("AT+QGPSLOC=2")
fix = parse_qgpsloc(resp)
if fix:
# Set system clock from GPS on first valid fix if clock is invalid
if not clock_set:
gps_dt = datetime.datetime.utcfromtimestamp(fix["timestamp_ms"] / 1000)
ret = subprocess.run(["date", "-s", gps_dt.strftime("%Y-%m-%d %H:%M:%S")],
env={**os.environ, "TZ": "UTC"},
capture_output=True)
if ret.returncode == 0:
clock_set = True
print(f"CLP gpsd: system clock set from GPS: {gps_dt}", file=sys.stderr, flush=True)
else:
print(f"CLP gpsd: failed to set clock: {ret.stderr.decode().strip()}", file=sys.stderr, flush=True)
msg = messaging.new_message("gpsLocation", valid=True)
gps = msg.gpsLocation
gps.latitude = fix["latitude"]
gps.longitude = fix["longitude"]
gps.altitude = fix["altitude"]
gps.speed = fix["speed"]
gps.bearingDeg = fix["bearing"]
gps.horizontalAccuracy = fix["accuracy"]
gps.unixTimestampMillis = int(fix["timestamp_ms"])
gps.source = log.GpsLocationData.SensorSource.qcomdiag
gps.hasFix = fix["fix"] >= 2
gps.flags = 1
gps.vNED = [0.0, 0.0, 0.0]
gps.verticalAccuracy = fix["accuracy"]
gps.bearingAccuracyDeg = 10.0
gps.speedAccuracy = 1.0
pm.send("gpsLocation", msg)
# 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:
now_mono = time.monotonic()
interval = 1.0 if not daylight_computed else 30.0
if (now_mono - last_daylight_check) >= interval:
last_daylight_check = now_mono
utc_now = datetime.datetime.utcfromtimestamp(fix["timestamp_ms"] / 1000)
daylight = is_daylight(fix["latitude"], fix["longitude"], utc_now)
if daylight != prev_daylight:
params_memory.put_bool("IsDaylight", daylight)
prev_daylight = daylight
if not daylight_computed:
daylight_computed = True
print(f"CLP gpsd: initial daylight calc: {'day' if daylight else 'night'}",
file=sys.stderr, flush=True)
# Auto-transition: only touch states 0 and 1 (manual modes 2/3/4 stay)
current_mode = params_memory.get_int("ScreenDisplayMode")
if current_mode == 0 and not daylight:
params_memory.put_int("ScreenDisplayMode", 1)
print("CLP gpsd: auto-switch to nightrider (sunset)", file=sys.stderr, flush=True)
elif current_mode == 1 and daylight:
params_memory.put_int("ScreenDisplayMode", 0)
print("CLP gpsd: auto-switch to normal (sunrise)", file=sys.stderr, flush=True)
time.sleep(0.5) # 2 Hz polling
if __name__ == "__main__":
main()
+103 -1
View File
@@ -1,6 +1,7 @@
#!/usr/bin/env python3
import os
import shutil
import sys
import threading
from openpilot.system.hardware.hw import Paths
from openpilot.common.swaglog import cloudlog
@@ -8,11 +9,17 @@ 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.xattr_cache import getxattr
MIN_BYTES = 5 * 1024 * 1024 * 1024
# CLEARPILOT: bumped from 5 GB to 9 GB so dashcam footage has headroom
MIN_BYTES = 9 * 1024 * 1024 * 1024
MIN_PERCENT = 10
DELETE_LAST = ['boot', 'crash']
# CLEARPILOT: dashcam footage directory (trip dirs YYYYMMDD-HHMMSS/ with .mp4 segments)
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_VALUE = b'1'
PRESERVE_COUNT = 5
@@ -44,12 +51,105 @@ def get_preserved_segments(dirs_by_creation: list[str]) -> list[str]:
return preserved
def delete_oldest_video():
"""CLEARPILOT: prune dashcam footage when disk space is low.
Trip directories are /data/media/0/videos/YYYYMMDD-HHMMSS/ containing .mp4
segments. Deletes entire oldest trip directory first. If only one trip
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:
if not os.path.isdir(VIDEOS_DIR):
return False
legacy_files = []
trip_dirs = []
for entry in os.listdir(VIDEOS_DIR):
path = os.path.join(VIDEOS_DIR, entry)
if os.path.isfile(path) and entry.endswith('.mp4'):
legacy_files.append(entry)
elif os.path.isdir(path):
trip_dirs.append(entry)
if legacy_files:
legacy_files.sort()
delete_path = os.path.join(VIDEOS_DIR, legacy_files[0])
print(f"CLP deleter: deleting legacy video {delete_path}", file=sys.stderr, flush=True)
os.remove(delete_path)
return True
if not trip_dirs:
return False
trip_dirs.sort() # timestamp names = chronological order
if len(trip_dirs) > 1:
delete_path = os.path.join(VIDEOS_DIR, trip_dirs[0])
print(f"CLP deleter: deleting trip {delete_path}", file=sys.stderr, flush=True)
shutil.rmtree(delete_path)
return True
# Only one trip left (likely active) — drop its oldest segment
trip_path = os.path.join(VIDEOS_DIR, trip_dirs[0])
segments = sorted(f for f in os.listdir(trip_path) if f.endswith('.mp4'))
if not segments:
return False
delete_path = os.path.join(trip_path, segments[0])
print(f"CLP deleter: deleting segment {delete_path}", file=sys.stderr, flush=True)
os.remove(delete_path)
return True
except OSError as e:
print(f"CLP deleter: issue deleting video from {VIDEOS_DIR}: {e}", file=sys.stderr, flush=True)
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):
while not exit_event.is_set():
out_of_bytes = get_available_bytes(default=MIN_BYTES + 1) < MIN_BYTES
out_of_percent = get_available_percent(default=MIN_PERCENT + 1) < MIN_PERCENT
if out_of_percent or out_of_bytes:
# CLEARPILOT: drop oldest dashcam footage first, fall back to log segments
if delete_oldest_video():
exit_event.wait(.1)
continue
dirs = listdir_by_creation(Paths.log_root())
# skip deleting most recent N preserved segments (and their prior segment)
@@ -73,6 +173,8 @@ def deleter_thread(exit_event):
cloudlog.exception(f"issue deleting {delete_path}")
exit_event.wait(.1)
else:
# CLEARPILOT: keep /data/log2 quota even when disk space is fine
cleanup_log2()
exit_event.wait(30)