Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 119d412c24 | |||
| 0d1ceddad2 | |||
| 20ea43f317 | |||
| f6516eb4cc | |||
| 8a7a776f9b | |||
| 3f5172b58b | |||
| b5a0b3221c | |||
| 41c154014a | |||
| 5d2fe5248b | |||
| f687d712e7 | |||
| 837c14e081 | |||
| 3ebfc29d35 | |||
| 54c566c68f | |||
| f28ba340f2 | |||
| 6ec4c7bdac | |||
| b57f2d8d70 |
@@ -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
@@ -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
|
||||
|
||||
@@ -244,6 +244,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},
|
||||
{"RadarSpeed", PERSISTENT},
|
||||
|
||||
@@ -0,0 +1,358 @@
|
||||
# Park-mode power savings — design notes & retro
|
||||
|
||||
## Goal
|
||||
|
||||
While ignition is on but the car is in **park**, reduce CPU/power draw by
|
||||
shutting down processes that aren't needed for "watching the gear lever."
|
||||
The constraint: the car's steering ECU must keep seeing the openpilot
|
||||
heartbeat (LFA / LKAS / LKAS_ALT CAN-FD messages, every controlsd cycle)
|
||||
or it drops out of tester mode and throws a steering fault on the next
|
||||
shift to drive.
|
||||
|
||||
Behavior should approximate "ignition off" except:
|
||||
- controlsd stays alive (it's the heartbeat source and the gear watcher)
|
||||
- pandad / boardd / camerad / dashcamd / gpsd / ui / thermald keep running
|
||||
- everything else (modeld, locationd, paramsd, torqued, calibrationd,
|
||||
plannerd, radard, dmonitoringmodeld, dmonitoringd, soundd, sensord,
|
||||
speed_logicd, frogpilot_process) gets paused
|
||||
|
||||
When the car shifts back to drive, all the paused processes spin up cold
|
||||
and openpilot resumes normal operation.
|
||||
|
||||
## Status: reverted
|
||||
|
||||
We attempted this on **2026-05-04** (commits `5d18ad1` → `1e4e95c`,
|
||||
force-pushed away). It built and launched fine on the bench but produced
|
||||
a cascade of false alerts during the park→drive transition in the car —
|
||||
"controlsd unresponsive", commIssue with longitudinalPlan/frogpilotPlan,
|
||||
locationd temporaryError, sensorDataInvalid. Reverted to tag
|
||||
`stable_5_4_26` (commit `0d1cedd`).
|
||||
|
||||
This document captures the design we tried and the problems we ran into
|
||||
so the next attempt doesn't relearn the same mistakes.
|
||||
|
||||
## Design we tried
|
||||
|
||||
### 1. The flag
|
||||
|
||||
A new memory param `ParkMode` (registered in `common/params.cc`,
|
||||
defaulted to `"0"` in `manager_init`'s memory-params loop). Lives at
|
||||
`/dev/shm/params/d/ParkMode`. Written by controlsd, read by
|
||||
process_config gating callbacks.
|
||||
|
||||
### 2. controlsd writes the flag and runs a minimal cycle in park
|
||||
|
||||
In `__init__`:
|
||||
```python
|
||||
self.park_mode = False
|
||||
self.park_exit_frame = -1
|
||||
self.startup_complete_frame = -1
|
||||
self.PARK_GRACE_MAX_FRAMES = int(15.0 / DT_CTRL) # see "Park-grace cap" below
|
||||
self.PARK_STARTUP_DELAY_FRAMES = int(10.0 / DT_CTRL) # see "Startup delay" below
|
||||
self.last_engaged_frame = -1
|
||||
self.last_engage_attempt_frame = -1
|
||||
self.POST_ENGAGE_LAG_GRACE_FRAMES = int(3.0 / DT_CTRL)
|
||||
```
|
||||
|
||||
In `step()` after `data_sample()`:
|
||||
```python
|
||||
self._update_park_mode(CS)
|
||||
if self.park_mode or self._in_park_exit_grace():
|
||||
self._park_mode_tick(CS)
|
||||
self.CS_prev = CS
|
||||
return
|
||||
# ... normal flow
|
||||
```
|
||||
|
||||
Helpers:
|
||||
```python
|
||||
def _park_mode_allowed(self):
|
||||
# Don't allow park mode until init has completed AND we've run at
|
||||
# least PARK_STARTUP_DELAY_FRAMES of normal step() after init.
|
||||
if not self.initialized:
|
||||
return False
|
||||
if self.startup_complete_frame < 0:
|
||||
self.startup_complete_frame = self.sm.frame
|
||||
return (self.sm.frame - self.startup_complete_frame) >= self.PARK_STARTUP_DELAY_FRAMES
|
||||
|
||||
def _update_park_mode(self, CS):
|
||||
if not self._park_mode_allowed():
|
||||
return
|
||||
in_park = CS.gearShifter == GearShifter.park
|
||||
if in_park != self.park_mode:
|
||||
self.park_mode = in_park
|
||||
self.params_memory.put_bool("ParkMode", in_park)
|
||||
if not in_park:
|
||||
self.park_exit_frame = self.sm.frame
|
||||
|
||||
def _in_park_exit_grace(self):
|
||||
if self.park_exit_frame < 0:
|
||||
return False
|
||||
if (self.sm.frame - self.park_exit_frame) >= self.PARK_GRACE_MAX_FRAMES:
|
||||
return False
|
||||
return not (self.sm.all_checks() and not self.card.can_rcv_timeout)
|
||||
|
||||
def _park_mode_tick(self, CS):
|
||||
# Build a do-nothing CarControl; card.controls_update -> CI.apply ->
|
||||
# CarController.update appends create_steering_messages()
|
||||
# unconditionally, so the LFA/LKAS heartbeat keeps flowing.
|
||||
CC = car.CarControl.new_message()
|
||||
self.clearpilot_state_control(CC, CS)
|
||||
self.card.controls_update(CC, self.frogpilot_variables)
|
||||
```
|
||||
|
||||
Why every piece matters:
|
||||
- **`_park_mode_allowed` (10 s startup delay)**: `card.initialize()` is
|
||||
only called inside `data_sample`'s `if not self.initialized` branch.
|
||||
That call wires `CarInterface` up so `controls_update` actually
|
||||
produces CAN sends. If we entered park-mode before init completed,
|
||||
the heartbeat was a silent no-op. The 10 s buffer also lets all
|
||||
`only_onroad_active` processes complete their cold-spawn the very
|
||||
first time, before manager starts pausing them.
|
||||
- **`_in_park_exit_grace`**: when ParkMode flips off, the cold-spawn
|
||||
chain takes time. Stay in keepalive-tick mode until SubMaster reports
|
||||
everything healthy, with a hard cap as a safety net (we used 15 s,
|
||||
saw it still wasn't enough — see "What broke" below).
|
||||
- **`_park_mode_tick` calls `card.controls_update`**: don't call
|
||||
`state_update` or `data_sample` — `step()` already did that. Just
|
||||
publish the empty CC to push the heartbeat CAN messages.
|
||||
|
||||
### 3. Manager gating
|
||||
|
||||
In `selfdrive/manager/process_config.py`:
|
||||
|
||||
```python
|
||||
def _park_mode() -> bool:
|
||||
return Params("/dev/shm/params").get_bool("ParkMode")
|
||||
|
||||
def only_onroad_active(started, params, CP):
|
||||
return started and not _park_mode()
|
||||
|
||||
def driverview_active(started, params, CP):
|
||||
return driverview(started, params, CP) and not _park_mode()
|
||||
|
||||
def always_run_unless_parked(started, params, CP):
|
||||
# Same as always_run, but pauses while ignition is on and parked.
|
||||
# Preserves offroad behavior.
|
||||
return not (started and _park_mode())
|
||||
```
|
||||
|
||||
Re-gated:
|
||||
- `only_onroad` → `only_onroad_active`: modeld, sensord, soundd,
|
||||
locationd, calibrationd, torqued, paramsd, plannerd, radard,
|
||||
speed_logicd
|
||||
- `driverview` → `driverview_active`: dmonitoringmodeld, dmonitoringd
|
||||
- `always_run` → `always_run_unless_parked`: frogpilot_process
|
||||
|
||||
`controlsd` stays plain `only_onroad` — it's the writer + heartbeat source.
|
||||
`camerad`, `pandad`, `thermald`, `ui`, `dashcamd`, `gpsd`, `deleter`,
|
||||
`fleet_manager`, `tombstoned`, `timed`, manager internals stay
|
||||
`always_run` (or whatever they were).
|
||||
|
||||
### 4. Engage-attempt grace (separate from park-grace)
|
||||
|
||||
The state transition into engaged briefly bumps the controlsd loop time
|
||||
over budget. `self.rk.lagging` flips True for a cycle, `update_events`
|
||||
adds `EventName.controlsdLagging`, which has an `ET.NO_ENTRY` alert
|
||||
("Controls Process Lagging: Reboot Your Device"). That alert fires
|
||||
right as the user is taking their hands off the wheel after pressing
|
||||
SET. Same goes for `commIssue` if any service is briefly stale.
|
||||
|
||||
Solution: track two engagement edges — the actual transition into
|
||||
ENABLED (post-success) and the cruise.enabled rising edge (pre-success,
|
||||
covers blocked attempts):
|
||||
|
||||
```python
|
||||
# In __init__:
|
||||
self.last_engaged_frame = -1
|
||||
self.last_engage_attempt_frame = -1
|
||||
|
||||
# In update_events, BEFORE the lag/comm checks:
|
||||
if CS.cruiseState.enabled and not self.CS_prev.cruiseState.enabled:
|
||||
self.last_engage_attempt_frame = self.sm.frame
|
||||
|
||||
# At the engaged-state transition (state_transition):
|
||||
enabled_prev = self.enabled
|
||||
self.enabled = self.state in ENABLED_STATES
|
||||
if self.enabled and not enabled_prev:
|
||||
self.last_engaged_frame = self.sm.frame
|
||||
|
||||
# Gate logic in update_events:
|
||||
in_engage_grace = (
|
||||
(self.last_engaged_frame >= 0
|
||||
and (self.sm.frame - self.last_engaged_frame) < self.POST_ENGAGE_LAG_GRACE_FRAMES)
|
||||
or
|
||||
(self.last_engage_attempt_frame >= 0
|
||||
and (self.sm.frame - self.last_engage_attempt_frame) < self.POST_ENGAGE_LAG_GRACE_FRAMES)
|
||||
)
|
||||
if not REPLAY and self.rk.lagging and not in_engage_grace:
|
||||
self.events.add(EventName.controlsdLagging)
|
||||
# ... and similarly suppress commIssue when in_engage_grace
|
||||
```
|
||||
|
||||
### 5. Fan range rules (port from broken tree)
|
||||
|
||||
Independent of park-mode plumbing. Widen the fan PID range when the car
|
||||
is parked so the device can fully cool while the user can't hear road
|
||||
noise.
|
||||
|
||||
`selfdrive/thermald/fan_controller.py`:
|
||||
```python
|
||||
def update(self, cur_temp, ignition, standstill=False, is_parked=True, cruise_engaged=False):
|
||||
# neg_limit = -max_fan_pct, pos_limit = -min_fan_pct
|
||||
if not ignition:
|
||||
self.controller.neg_limit = -30
|
||||
self.controller.pos_limit = 0
|
||||
elif is_parked:
|
||||
self.controller.neg_limit = -100
|
||||
self.controller.pos_limit = 0 # 0-100% (full range)
|
||||
elif cruise_engaged:
|
||||
self.controller.neg_limit = -100
|
||||
self.controller.pos_limit = -30 # 30-100%
|
||||
elif standstill:
|
||||
self.controller.neg_limit = -100
|
||||
self.controller.pos_limit = -10 # 10-100%
|
||||
else:
|
||||
self.controller.neg_limit = -100
|
||||
self.controller.pos_limit = -30 # 30-100%
|
||||
```
|
||||
|
||||
`selfdrive/thermald/thermald.py`:
|
||||
```python
|
||||
# Add carState to the SubMaster service list
|
||||
sm = messaging.SubMaster([..., "carState"], poll="pandaStates")
|
||||
|
||||
# At the fan_controller.update call site:
|
||||
if sm.seen['carState']:
|
||||
cs = sm['carState']
|
||||
standstill = cs.standstill
|
||||
is_parked = cs.gearShifter == car.CarState.GearShifter.park
|
||||
else:
|
||||
standstill = False
|
||||
is_parked = True # default safe: assume parked, no fan floor
|
||||
cruise_engaged = sm['controlsState'].enabled if sm.seen['controlsState'] else False
|
||||
msg.deviceState.fanSpeedPercentDesired = fan_controller.update(
|
||||
all_comp_temp, onroad_conditions["ignition"], standstill,
|
||||
is_parked=is_parked, cruise_engaged=cruise_engaged)
|
||||
```
|
||||
|
||||
This piece worked fine standalone — it's safe to land on its own without
|
||||
the rest of the park-mode plumbing. (It does require the thermald
|
||||
carState subscription to actually receive carState — which is fine in
|
||||
normal operation.)
|
||||
|
||||
## What broke
|
||||
|
||||
### A. card.initialize() not called → silent CAN
|
||||
|
||||
First attempt entered park-mode immediately whenever `gearShifter ==
|
||||
PARK`, including before controlsd had ever passed through its
|
||||
`if not self.initialized` branch. Symptoms:
|
||||
- UI stuck on splash even after shift to drive
|
||||
- carState never published to cereal (tested by subscribing externally)
|
||||
- presumably the steering ECU was getting no heartbeat, though we
|
||||
hadn't yet tried to engage
|
||||
|
||||
Fix: `_park_mode_allowed` gate, requires `self.initialized` AND a 10 s
|
||||
post-init buffer. **Critical.** Don't skip this in v2.
|
||||
|
||||
### B. Park-grace cap (8 s) too short
|
||||
|
||||
Cold-spawn chain length:
|
||||
1. modeld load thneed weights, init GPU (~3-5 s)
|
||||
2. modeld publishes modelV2
|
||||
3. plannerd consumes modelV2, publishes longitudinalPlan
|
||||
4. frogpilot_process consumes modelV2, publishes frogpilotPlan
|
||||
5. paramsd, torqued, calibrationd estimators accumulate enough samples
|
||||
to set valid=True
|
||||
|
||||
In testing this cumulatively exceeded 8 s. We bumped the cap to 15 s.
|
||||
**Even 15 s wasn't enough in some test runs** — got commIssue alerts
|
||||
with longitudinalPlan / frogpilotPlan still invalid past the cap.
|
||||
|
||||
Possible fixes for v2:
|
||||
- Even longer cap (30 s)
|
||||
- Or condition-only with no cap (live with the risk that a genuinely
|
||||
broken service strands controlsd in keepalive forever)
|
||||
- Or **a different approach**: don't actually pause modeld; throttle it
|
||||
to 1 fps when stopped (this is what the broken tree's CLAUDE.md
|
||||
pending-features list described as "modeld throttled to 1fps when
|
||||
stopped"). Avoids the cold-spawn entirely.
|
||||
|
||||
### C. NO_ENTRY alerts fire on engage attempt, not engage success
|
||||
|
||||
`controlsdLagging` and `commIssue` both have `ET.NO_ENTRY`. NO_ENTRY
|
||||
alerts fire when the user *tries* to engage but the event blocks them.
|
||||
At that moment `self.enabled` hasn't flipped yet, so a grace keyed on
|
||||
`last_engaged_frame` is inert.
|
||||
|
||||
Fix: also track `CS.cruiseState.enabled` rising edge as
|
||||
`last_engage_attempt_frame`. See "Engage-attempt grace" above.
|
||||
|
||||
### D. Cascade of related alerts
|
||||
|
||||
After all the above were patched, in-car testing still produced
|
||||
"locationd temporaryError" and "sensorDataInvalid" alerts after the
|
||||
post-engage grace window expired. We didn't have time to chase these
|
||||
down before reverting. Hypotheses:
|
||||
- locationd's kalman filter needs more than ~15 s to converge after
|
||||
cold spawn, especially without GPS feeding it (we explicitly skip
|
||||
GPS in locationd).
|
||||
- sensord might lose its IMU sample lock and need re-init when
|
||||
killed/restarted.
|
||||
|
||||
These are an argument for the "throttle, don't kill" approach in v2.
|
||||
|
||||
### E. Sensor data invalid
|
||||
|
||||
`sensorDataInvalid` likely stems from sensord being killed and
|
||||
restarted. The IMU init handshake takes time, and during that window
|
||||
`accelerometer` / `gyroscope` services are alive but their data hasn't
|
||||
stabilized — locationd reports invalid, controlsd alerts.
|
||||
|
||||
Same fix family as (D).
|
||||
|
||||
## Recommended approach for v2
|
||||
|
||||
The "kill processes" approach is architecturally clean but creates a
|
||||
big cold-spawn cliff every time the user shifts to drive. The v2 plan
|
||||
should probably look more like:
|
||||
|
||||
1. **Throttle, don't kill.** Send a "standby" memory param that modeld
|
||||
reads and reduces its tick rate to 1 fps. dmonitoringmodeld likewise.
|
||||
Plannerd/radard naturally adapt to slower modelV2 input. paramsd /
|
||||
torqued / calibrationd / locationd keep accumulating slowly.
|
||||
2. **Keep controlsd's main loop running normally** — no park_mode_tick.
|
||||
The state machine already handles "not engaged" → no actuators →
|
||||
passive. The heartbeat flows naturally because `state_control` /
|
||||
`publish_logs` run every cycle.
|
||||
3. **Apply the fan-range widening (Section 5 of "Design")** — that
|
||||
piece is independently valuable and doesn't depend on the rest.
|
||||
4. **Skip dashcamd touchpoints.** dashcamd is already gear-aware via
|
||||
its own state machine and pauses recording in park naturally.
|
||||
5. **Keep the `_park_mode_allowed` startup-delay gate concept** if any
|
||||
form of conditional-shutdown is reintroduced — this guards against
|
||||
`card.initialize()` being skipped.
|
||||
|
||||
### Files involved (for reference)
|
||||
|
||||
| File | Why |
|
||||
|---|---|
|
||||
| `common/params.cc` | register `ParkMode` (or `ModelStandby` etc.) |
|
||||
| `selfdrive/manager/manager.py` | `manager_init` memory-param defaults |
|
||||
| `selfdrive/manager/process_config.py` | gating helpers |
|
||||
| `selfdrive/controls/controlsd.py` | the park branch and engage-grace |
|
||||
| `selfdrive/thermald/fan_controller.py` | fan range rules |
|
||||
| `selfdrive/thermald/thermald.py` | carState sub for fan rules |
|
||||
| `selfdrive/modeld/modeld.py` (v2) | throttle on standby flag |
|
||||
| `selfdrive/modeld/dmonitoringmodeld.py` (v2) | throttle on standby flag |
|
||||
|
||||
### Reference commits (force-pushed away from origin/clearpilot, but the
|
||||
text of this document captures the substance)
|
||||
|
||||
- park-mode initial: bring up flag + gating + controlsd keepalive
|
||||
- park-mode startup gate: 10 s post-init delay before allowing park
|
||||
- engage-grace: 3 s suppression on engage edge for controlsdLagging
|
||||
- engage-attempt grace + grace cap bump to 15 s
|
||||
|
||||
These were all on top of `stable_5_4_26` (`0d1cedd`).
|
||||
+4
-1
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -4,3 +4,4 @@ SConscript(['controls/lib/longitudinal_mpc_lib/SConscript'])
|
||||
SConscript(['locationd/SConscript'])
|
||||
SConscript(['modeld/SConscript'])
|
||||
SConscript(['ui/SConscript'])
|
||||
SConscript(['clearpilot/SConscript'])
|
||||
@@ -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']
|
||||
)
|
||||
Executable
BIN
Binary file not shown.
@@ -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.
@@ -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
|
||||
@@ -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 |
@@ -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
|
||||
|
||||
@@ -1236,29 +1238,32 @@ class Controls:
|
||||
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):
|
||||
# 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)
|
||||
|
||||
# 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)
|
||||
|
||||
# Leftovers
|
||||
# self.params_memory.put_int("SpeedLimitLatDesired", CC.actuators.speed * CV.MS_TO_MPH )
|
||||
return CC
|
||||
|
||||
def main():
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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.
|
||||
if (height < 0) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
#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,166 +135,106 @@ static const char* omx_color_fomat_name(uint32_t format) __attribute__((unused))
|
||||
static const char* omx_color_fomat_name(uint32_t format) {
|
||||
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.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));
|
||||
|
||||
hevc_type.eProfile = OMX_VIDEO_HEVCProfileMain;
|
||||
hevc_type.eLevel = OMX_VIDEO_HEVCHighTierLevel5;
|
||||
|
||||
OMX_CHECK(OMX_SetParameter(this->handle, index_type, (OMX_PTR) &hevc_type));
|
||||
} else {
|
||||
// setup h264
|
||||
OMX_VIDEO_PARAM_AVCTYPE avc = { 0 };
|
||||
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));
|
||||
OMX_CHECK(OMX_GetParameter(handle, OMX_IndexParamVideoAvc, &avc));
|
||||
|
||||
avc.nBFrames = 0;
|
||||
avc.nPFrames = 15;
|
||||
@@ -412,70 +251,64 @@ OmxEncoder::OmxEncoder(const char* path, int width, int height, int fps, int bit
|
||||
avc.bWeightedPPrediction = OMX_TRUE;
|
||||
avc.bconstIpred = OMX_TRUE;
|
||||
|
||||
OMX_CHECK(OMX_SetParameter(this->handle, OMX_IndexParamVideoAvc, &avc));
|
||||
OMX_CHECK(OMX_SetParameter(handle, OMX_IndexParamVideoAvc, &avc));
|
||||
|
||||
OMX_CHECK(OMX_SendCommand(handle, OMX_CommandStateSet, OMX_StateIdle, NULL));
|
||||
|
||||
for (OMX_BUFFERHEADERTYPE* &buf : in_buf_headers) {
|
||||
OMX_CHECK(OMX_AllocateBuffer(handle, &buf, PORT_INDEX_IN, this, in_port.nBufferSize));
|
||||
}
|
||||
|
||||
OMX_CHECK(OMX_SendCommand(this->handle, OMX_CommandStateSet, OMX_StateIdle, NULL));
|
||||
|
||||
for (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) {
|
||||
if (!encoder->wrote_codec_config && encoder->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);
|
||||
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);
|
||||
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) {
|
||||
@@ -488,18 +321,17 @@ void OmxEncoder::handle_out_buf(OmxEncoder *e, OMX_BUFFERHEADERTYPE *out_buf) {
|
||||
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);
|
||||
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;
|
||||
}
|
||||
|
||||
err = av_write_frame(e->ofmt_ctx, &pkt);
|
||||
err = av_write_frame(encoder->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);
|
||||
if (stat(path.c_str(), &st) == -1) {
|
||||
if (mkdir(path.c_str(), 0755) == -1) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
snprintf(vid_path, sizeof(vid_path), "%s/%s", path.c_str(), filename);
|
||||
|
||||
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);
|
||||
}
|
||||
#endif
|
||||
if (avformat_alloc_output_context2(&ofmt_ctx, NULL, NULL, vid_path) < 0 || !ofmt_ctx) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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);
|
||||
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;
|
||||
}
|
||||
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;
|
||||
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);
|
||||
}
|
||||
unlink(this->lock_path);
|
||||
}
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -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++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
+326
-140
@@ -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,6 +54,7 @@ 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, [=] {
|
||||
@@ -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) {
|
||||
if (offroad) {
|
||||
sidebar->setVisible(false);
|
||||
if (offroad) {
|
||||
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 {
|
||||
sidebar->setVisible(false);
|
||||
} else if (!started) {
|
||||
// Offroad, not started — show home menu
|
||||
slayout->setCurrentWidget(home);
|
||||
sidebar->setVisible(show == false);
|
||||
}
|
||||
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
@@ -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 ¶m = "");
|
||||
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 ¶m = "");
|
||||
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);
|
||||
|
||||
+414
-56
@@ -7,6 +7,7 @@
|
||||
#include <sstream>
|
||||
|
||||
#include <QApplication>
|
||||
#include <QDateTime>
|
||||
#include <QDebug>
|
||||
#include <QMouseEvent>
|
||||
|
||||
@@ -75,10 +76,11 @@ 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];
|
||||
}
|
||||
|
||||
@@ -176,7 +178,14 @@ void OnroadWindow::offroadTransition(bool offroad) {
|
||||
|
||||
void OnroadWindow::paintEvent(QPaintEvent *event) {
|
||||
QPainter p(this);
|
||||
// CLEARPILOT: hide engagement border in nightrider mode
|
||||
int dm = paramsMemory.getInt("ScreenDisplayMode");
|
||||
bool nightrider = (dm == 1 || dm == 4);
|
||||
if (nightrider) {
|
||||
p.fillRect(rect(), Qt::black);
|
||||
} else {
|
||||
p.fillRect(rect(), QColor(bg.red(), bg.green(), bg.blue(), 255));
|
||||
}
|
||||
|
||||
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)));
|
||||
}
|
||||
|
||||
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) {
|
||||
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);
|
||||
|
||||
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();
|
||||
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];
|
||||
|
||||
@@ -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;
|
||||
|
||||
+73
-12
@@ -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);
|
||||
}
|
||||
img_shown = &img_ready;
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <QSocketNotifier>
|
||||
#include <QVariantAnimation>
|
||||
#include <QWidget>
|
||||
#include <QElapsedTimer>
|
||||
#include <QTimer>
|
||||
|
||||
#include "common/util.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
@@ -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 ¶m) {
|
||||
@@ -70,6 +151,10 @@ void MainWindow::openSettings(int index, const QString ¶m) {
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -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 ¶m = "");
|
||||
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
@@ -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")
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user