Reorganized 'Current State' to enumerate what's actually active in the tree now (boot/build infra, suppressed logging, gpsd, UI port, display modes, speed_logicd, sound, dashcamd) instead of the brief summary of just the early commits. Trimmed 'Pending' to remove items that are now ported (UI, gpsd, sound, speed_logic, display modes, dashcamd, memory-param scaffolding). Kept the still-pending items: HDA2 driving-behavior tweaks, CarSpeedLimit publisher, telemetry, bench mode, power/thermal, session logging. Notes added: - Memory-param keys currently registered (so the next port doesn't re-register existing keys by accident) - camerad gating change to always_run (and the power tradeoff) - Branch policy: clearpilot is the canonical/main branch; do not abandon it for another - pkill _text by comm in the launch chain - speed-limit widget shows 0 until carstate.py CAN-FD writes CarSpeedLimit
17 KiB
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_textby comm),on_start.shSSH/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.pyatomically swaps the prebuilt spinner binary after each successful build BUILD_ONLY=1env path inbuild.pyspawns the failure TextWindow fully detached (own session, /dev/null stdio) sobuild_only.shexits with a non-blocking error window
Logging suppressed
loggerd,encoderd,stream_encoderddisabled inprocess_config.py— norlog/qlogsegments or.hevcfiles written to/data/media/0/realdata/save_bootlog()skipped inmanager_init()— no boot logs
GPS (Quectel modem replacement)
system/clearpilot/gpsd.pypolls Quectel EC25 modem viammcliAT commands at 2 Hz, publishesgpsLocation, sets system clock on first fix- NOAA solar-position calc →
IsDaylightmemory param + autoScreenDisplayMode0↔1 switch (sunrise/sunset) locationdpatched to NOT subscribe to GPS —liveLocationKalman.gpsOKstays 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
gpsLocationviaspeed_logicd), speed-limit, cruise-vs-limit warning sign - Nightrider mode: camera suppressed, lane lines/path drawn as 2px outlines (
ScreenDisplayMode1 or 4) - Display power:
ScreenDisplayMode3 → screen off; ignition off blanks immediately - Qt RPC widget-tree dump server at
ipc:///tmp/clearpilot_ui_rpc - Crash handler in
main.ccwith stack-trace dump for SIGSEGV/SIGABRT screenDisplayModeis a member ofAnnotatedCameraWidget, accessed fromOnroadWindow::updateStateasnvg->screenDisplayMode- QtWebEngine dependency removed entirely
Display modes (LFA debug button)
- 5-state
ScreenDisplayModemachine incontrolsd.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_noticeand its event registration deleted fromevents.py)
Speed / cruise
speed_logicdstandalone managed process (selfdrive/clearpilot/speed_logicd.pywrappingspeed_logic.SpeedState) — subscribes togpsLocation+carState, ticks at 2 Hz, writesClearpilot{Speed,SpeedLimit,CruiseWarning,...}Displaymemory 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
controlsdso self-driving timing is unaffected - Note: speed-limit widget shows 0 until
CarSpeedLimitpublisher inhyundai/carstate.pyCAN-FD decode is ported (not done yet). Speed widget works.
Sound
soundd.pymixes a single-shot ding alongside the alert stream (MAX_VOLUME, independent of alert volume map). PollsClearpilotPlayDingat ~2 Hz, clears on read.selfdrive/clearpilot/sounds/ding.wavis 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") andDashcamFrames(per-trip count) every 5 s omx_encoder.ccports broken's version (addsencode_frame_nv12(), NEON-only libyuv paths)cameradgating changed toalways_runso the dashcam can record the moment ignition+drive arrives without waiting for camera startupsystem/loggerd/deleter.pytrip-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_changeinfrastructure already wired throughcontrolsd→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 inselfdrive/clearpilot/models/; selection logic not ported
Speed-limit publisher
hyundai/carstate.py update_canfd()writesCarSpeedLimitfrom 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
TelemetryEnabledmemory param from Debug panel - Auto-disabled if
/datafree < 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)
--benchflag →BENCH_MODE=1inlaunch_openpilot.sh(already wired) → enablesbench_onroad.py, blocks real car processesbench_cmd.pyfor 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:
modeldanddmonitoringmodeldthrottled 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; aggregatesession.logof major events- Time-resolved log dir rename via GPS/NTP; 30-day rotation
- See
selfdrive/manager/process.pyandmanager.pychanges in the broken tree LogDirInitializedparam 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.
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:
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
clearpilotbranch was force-pushed on 2026-05-03 as part of the reset; the prior history is reachable via thepre-reset-2026-05-03tag.
Samba Share
- Share name:
openpilot(e.g.\\comma-3889765b\openpilot) - Path:
/data/openpilot - Username:
comma - Password:
i-like-to-drive-cars - Runs as
comma:commavia 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.
# 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:
- Register the key in
common/params.cc(alphabetically, withPERSISTENTorCLEAR_ON_*flag) - For memory params, add a default in
manager_init()'s memory-params loop (after the persistent default loop) - Remove
prebuilt,common/params.o, andcommon/libcommon.ato 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.ccasPERSISTENT(the registration flag does NOT control which path the param lives at —/dev/shmis 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
ToggleControlwith manualtoggleFlippedlambda, notParamControl(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
cameradrunsalways_run(wasdriverview) — this is needed for dashcamd. Costs steady-state camera power even when ignition is off.loggerd,encoderd,stream_encoderdare commented out inprocess_config.py— no segment or video loggingqcomgpsdis commented out;system.clearpilot.gpsdis registered in its place (gated byqcomgpscallback =not ublox_available(), which is always true on this device)speed_logicdisonly_onroad;dashcamdisalways_run(manages own trip lifecycle)uploaderalready 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