bench mode, ClearPilot menu, status window, UI introspection RPC

Bench mode (--bench flag):
- bench_onroad.py publishes fake vehicle state as managed process
- manager blocks real car processes (pandad, thermald, controlsd, etc.)
- bench_cmd.py for setting params and querying UI state
- BLOCKER: UI segfaults (SIGSEGV) when OnroadWindow becomes visible
  without camera frames — need to make CameraWidget handle missing
  VisionIPC gracefully before bench drive mode works

ClearPilot menu:
- Replaced grid launcher with sidebar settings panel (ClearPilotPanel)
- Sidebar: Home, Dashcam, Debug
- Home panel: Status and System Settings buttons
- Debug panel: telemetry logging toggle (ParamControl)
- Tap from any view (splash, onroad) opens ClearPilotPanel
- Back button returns to splash/onroad

Status window:
- Live system stats refreshing every 1 second
- Storage, RAM, load, IP, WiFi, VPN, GPS, time, telemetry status
- Tap anywhere to close, returns to previous view
- Honors interactive timeout

UI introspection RPC:
- ZMQ REP server at ipc:///tmp/clearpilot_ui_rpc
- Dumps full widget tree with visibility, geometry, stacked indices
- bench_cmd dump queries it, detects crash loops via process uptime
- ui_dump.py standalone tool

Other:
- Telemetry toggle wired to TelemetryEnabled param with disk space guard
- Telemetry disabled on every manager start
- Blinking red circle on onroad UI when telemetry recording
- Fixed showDriverView overriding park/drive transitions every frame
- Fixed offroadTransition sidebar visibility race in MainWindow
- launch_openpilot.sh: cd to /data/openpilot, --bench flag support

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-13 02:33:33 +00:00
parent e3bdae8b5e
commit 4b0d0bb708
15 changed files with 863 additions and 94 deletions

View File

@@ -57,7 +57,7 @@ chown -R comma:comma /data/openpilot
### Testing Changes ### Testing Changes
Always use `build_only.sh` to compile, then start the manager separately. Never compile individual targets with scons directly — always use the full build script. Always use full paths with `su - comma` — the login shell drops into `/home/comma` (volatile tmpfs), not `/data/openpilot`. Always use `build_only.sh` to compile, then start the manager separately. Never compile individual targets with scons directly — always use the full build script. Always use full paths with `su - comma` — the login shell drops into `/home/comma` (volatile tmpfs), not `/data/openpilot`. **Always start the manager after a successful build** — don't wait for the user to ask.
```bash ```bash
# 1. Fix ownership (we edit as root, openpilot runs as comma) # 1. Fix ownership (we edit as root, openpilot runs as comma)
@@ -93,6 +93,65 @@ The params system uses a C++ whitelist. Adding a new param name in `manager.py`
- OMX encoder object (`omx_encoder.o`) is compiled by the UI build — reference the pre-built `.o` file rather than recompiling (avoids "two environments" scons error) - OMX encoder object (`omx_encoder.o`) is compiled by the UI build — reference the pre-built `.o` file rather than recompiling (avoids "two environments" scons error)
- `prebuilt` is recreated after every successful build — always remove it before rebuilding - `prebuilt` is recreated after every successful build — always remove it before rebuilding
## Bench Mode (Onroad UI Testing)
Bench mode allows testing the onroad UI without a car connected. It runs a fake vehicle simulator (`bench_onroad.py`) as a managed process and disables real car processes (pandad, thermald, controlsd, etc.).
### Usage
**IMPORTANT**: Do NOT use `echo` to write bench params — `echo` appends a newline which causes param parsing to fail silently (e.g. gear stays in park). Always use the `bench_cmd.py` tool.
```bash
# 1. Start in bench mode
su - comma -c "bash /data/openpilot/launch_openpilot.sh --bench"
# 2. Wait for UI to be ready (polls RPC, up to 10s)
su - comma -c "PYTHONPATH=/data/openpilot python3 -m selfdrive.clearpilot.bench_cmd wait_ready"
# 3. Control vehicle state
su - comma -c "PYTHONPATH=/data/openpilot python3 -m selfdrive.clearpilot.bench_cmd gear D"
su - comma -c "PYTHONPATH=/data/openpilot python3 -m selfdrive.clearpilot.bench_cmd speed 20"
su - comma -c "PYTHONPATH=/data/openpilot python3 -m selfdrive.clearpilot.bench_cmd speedlimit 45"
su - comma -c "PYTHONPATH=/data/openpilot python3 -m selfdrive.clearpilot.bench_cmd cruise 55"
su - comma -c "PYTHONPATH=/data/openpilot python3 -m selfdrive.clearpilot.bench_cmd engaged 1"
# 4. Inspect UI widget tree (RPC call, instant response)
su - comma -c "PYTHONPATH=/data/openpilot python3 -m selfdrive.clearpilot.bench_cmd dump"
```
### UI Introspection RPC
The UI process runs a ZMQ REP server at `ipc:///tmp/clearpilot_ui_rpc`. Send `"dump"` to get a recursive widget tree showing class name, visibility, geometry, and stacked layout current indices. This is the primary debugging tool for understanding what the UI is displaying.
- `bench_cmd dump` — queries the RPC and prints the widget tree
- `bench_cmd wait_ready` — polls the RPC every second until `ReadyWindow` is visible (up to 10s)
- `ui_dump.py` — standalone dump tool (same as `bench_cmd dump`)
### Architecture
- `launch_openpilot.sh --bench` sets `BENCH_MODE=1` env var
- `manager.py` reads `BENCH_MODE`, blocks real car processes, enables `bench_onroad` process
- `bench_onroad.py` publishes fake `deviceState`, `pandaStates`, `carState`, `controlsState` at correct frequencies
- The UI receives these messages identically to real car data
- Blocked processes in bench mode: pandad, thermald, controlsd, radard, plannerd, calibrationd, torqued, paramsd, locationd, sensord, ubloxd, pigeond, dmonitoringmodeld, dmonitoringd, modeld, soundd, camerad, loggerd, micd, dashcamd
### Key Files
| File | Role |
|------|------|
| `selfdrive/clearpilot/bench_onroad.py` | Fake vehicle state publisher |
| `selfdrive/clearpilot/bench_cmd.py` | Command tool for setting bench params and querying UI |
| `selfdrive/clearpilot/ui_dump.py` | Standalone UI widget tree dump |
| `selfdrive/manager/process_config.py` | Registers bench_onroad as managed process (enabled=BENCH_MODE) |
| `selfdrive/manager/manager.py` | Blocks conflicting processes in bench mode |
| `launch_openpilot.sh` | Accepts `--bench` flag, exports BENCH_MODE env var |
| `selfdrive/ui/qt/window.cc` | UI RPC server (`ipc:///tmp/clearpilot_ui_rpc`), widget tree dump |
### Known Issues
- The UI segfaults (exit code -11) when switching to the onroad camera view without a camera feed. The `CameraWidget` / `AnnotatedCameraWidget` crash on empty VisionIPC buffers. camerad is blocked in bench mode, but the `OnroadWindow` still contains a `CameraWidget` that crashes when made visible. Need to either provide fake VisionIPC frames or make the camera widget gracefully handle missing data.
- The `showDriverView` function in `home.cc` was previously overriding park/drive transitions every frame. Fixed to only act when not in started+parked state, but transitions to onroad (drive) view still trigger the camera crash.
## Session Logging ## Session Logging
Per-process stderr and an aggregate event log are captured in `/data/log2/{session}/`. Per-process stderr and an aggregate event log are captured in `/data/log2/{session}/`.

View File

@@ -193,6 +193,7 @@ std::unordered_map<std::string, uint32_t> keys = {
{"SnoozeUpdate", CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION}, {"SnoozeUpdate", CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION},
{"SshEnabled", PERSISTENT}, {"SshEnabled", PERSISTENT},
{"TermsVersion", PERSISTENT}, {"TermsVersion", PERSISTENT},
{"TelemetryEnabled", PERSISTENT},
{"Timezone", PERSISTENT}, {"Timezone", PERSISTENT},
{"TrainingVersion", PERSISTENT}, {"TrainingVersion", PERSISTENT},
{"UbloxAvailable", PERSISTENT}, {"UbloxAvailable", PERSISTENT},
@@ -211,6 +212,12 @@ std::unordered_map<std::string, uint32_t> keys = {
// FrogPilot parameters // FrogPilot parameters
{"AccelerationPath", PERSISTENT}, {"AccelerationPath", PERSISTENT},
{"BenchCruiseSpeed", CLEAR_ON_MANAGER_START},
{"ClpUiState", CLEAR_ON_MANAGER_START},
{"BenchEngaged", CLEAR_ON_MANAGER_START},
{"BenchGear", CLEAR_ON_MANAGER_START},
{"BenchSpeed", CLEAR_ON_MANAGER_START},
{"BenchSpeedLimit", CLEAR_ON_MANAGER_START},
{"AccelerationProfile", PERSISTENT}, {"AccelerationProfile", PERSISTENT},
{"AdjacentPath", PERSISTENT}, {"AdjacentPath", PERSISTENT},
{"AdjacentPathMetrics", PERSISTENT}, {"AdjacentPathMetrics", PERSISTENT},

View File

@@ -12,5 +12,10 @@ sleep 1
bash /data/openpilot/system/clearpilot/on_start.sh bash /data/openpilot/system/clearpilot/on_start.sh
# CLEARPILOT: pass --bench flag through to manager via env var
if [ "$1" = "--bench" ]; then
export BENCH_MODE=1
fi
cd /data/openpilot cd /data/openpilot
exec ./launch_chffrplus.sh exec ./launch_chffrplus.sh

View File

@@ -0,0 +1,122 @@
#!/usr/bin/env python3
"""
ClearPilot bench command tool. Sets bench params and queries UI state.
Usage:
python3 -m selfdrive.clearpilot.bench_cmd gear D
python3 -m selfdrive.clearpilot.bench_cmd speed 20
python3 -m selfdrive.clearpilot.bench_cmd speedlimit 45
python3 -m selfdrive.clearpilot.bench_cmd cruise 55
python3 -m selfdrive.clearpilot.bench_cmd engaged 1
python3 -m selfdrive.clearpilot.bench_cmd dump
python3 -m selfdrive.clearpilot.bench_cmd wait_ready
"""
import os
import subprocess
import sys
import time
import zmq
from openpilot.common.params import Params
def check_ui_process():
"""Check if UI process is running and healthy. Returns error string or None if OK."""
try:
result = subprocess.run(["pgrep", "-a", "-f", "./ui"], capture_output=True, text=True)
if result.returncode != 0:
return "ERROR: UI process not running"
# Get the PID and check its uptime
for line in result.stdout.strip().split("\n"):
parts = line.split(None, 1)
if len(parts) >= 2 and "./ui" in parts[1]:
pid = parts[0]
try:
stat = os.stat(f"/proc/{pid}")
uptime = time.time() - stat.st_mtime
if uptime < 5:
return f"ERROR: UI process (pid {pid}) uptime {uptime:.1f}s — likely crash looping. Check: tail /data/log2/$(ls -t /data/log2/ | head -1)/session.log"
except FileNotFoundError:
return "ERROR: UI process disappeared"
except Exception:
pass
return None
def ui_dump():
ctx = zmq.Context()
sock = ctx.socket(zmq.REQ)
sock.setsockopt(zmq.RCVTIMEO, 2000)
sock.connect("ipc:///tmp/clearpilot_ui_rpc")
sock.send_string("dump")
try:
return sock.recv_string()
except zmq.Again:
return None
finally:
sock.close()
ctx.term()
def wait_ready(timeout=10):
"""Wait until the UI shows ReadyWindow, up to timeout seconds."""
start = time.time()
while time.time() - start < timeout:
dump = ui_dump()
if dump and "ReadyWindow" in dump:
# Check it's actually visible
for line in dump.split("\n"):
if "ReadyWindow" in line and "vis=Y" in line:
print("UI ready (ReadyWindow visible)")
return True
time.sleep(1)
print(f"ERROR: UI not ready after {timeout}s")
if dump:
print(dump)
return False
def main():
if len(sys.argv) < 2:
print(__doc__)
return
cmd = sys.argv[1].lower()
params = Params("/dev/shm/params")
param_map = {
"gear": "BenchGear",
"speed": "BenchSpeed",
"speedlimit": "BenchSpeedLimit",
"cruise": "BenchCruiseSpeed",
"engaged": "BenchEngaged",
}
if cmd == "dump":
ui_status = check_ui_process()
if ui_status:
print(ui_status)
else:
result = ui_dump()
if result:
print(result)
else:
print("ERROR: UI not responding")
elif cmd == "wait_ready":
wait_ready()
elif cmd in param_map:
if len(sys.argv) < 3:
print(f"Usage: bench_cmd {cmd} <value>")
return
value = sys.argv[2]
params.put(param_map[cmd], value)
print(f"{param_map[cmd]} = {value}")
else:
print(f"Unknown command: {cmd}")
print(__doc__)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,129 @@
#!/usr/bin/env python3
"""
ClearPilot bench onroad simulator.
Publishes fake messages to make the UI go onroad and display
configurable vehicle state. Control values via params in /dev/shm/params:
BenchSpeed - vehicle speed in mph (default: 0)
BenchSpeedLimit - speed limit in mph (default: 0, 0=hidden)
BenchCruiseSpeed - cruise set speed in mph (default: 0, 0=not set)
BenchGear - P, D, R, N (default: P)
BenchEngaged - 0 or 1, cruise engaged (default: 0)
Usage:
su - comma -c "PYTHONPATH=/data/openpilot python3 -m selfdrive.clearpilot.bench_onroad"
To stop: Ctrl+C or kill the process. UI returns to offroad.
"""
import time
import cereal.messaging as messaging
from cereal import log, car
from openpilot.common.params import Params
MS_PER_MPH = 0.44704
def main():
params = Params()
params_mem = Params("/dev/shm/params")
# Set defaults
params_mem.put("BenchSpeed", "0")
params_mem.put("BenchSpeedLimit", "0")
params_mem.put("BenchCruiseSpeed", "0")
params_mem.put("BenchGear", "P")
params_mem.put("BenchEngaged", "0")
pm = messaging.PubMaster([
"deviceState", "pandaStates", "carState", "controlsState",
"driverMonitoringState", "liveCalibration",
])
print("Bench onroad simulator started")
print("Set values in /dev/shm/params/d/Bench*")
print(" BenchSpeed=0 BenchSpeedLimit=0 BenchCruiseSpeed=0 BenchGear=P BenchEngaged=0")
gear_map = {
"P": car.CarState.GearShifter.park,
"D": car.CarState.GearShifter.drive,
"R": car.CarState.GearShifter.reverse,
"N": car.CarState.GearShifter.neutral,
}
frame = 0
try:
while True:
# Read bench params
speed_mph = float((params_mem.get("BenchSpeed", encoding="utf-8") or "0").strip())
speed_limit_mph = float((params_mem.get("BenchSpeedLimit", encoding="utf-8") or "0").strip())
cruise_mph = float((params_mem.get("BenchCruiseSpeed", encoding="utf-8") or "0").strip())
gear_str = (params_mem.get("BenchGear", encoding="utf-8") or "P").strip().upper()
engaged = (params_mem.get("BenchEngaged", encoding="utf-8") or "0").strip() == "1"
speed_ms = speed_mph * MS_PER_MPH
gear = gear_map.get(gear_str, car.CarState.GearShifter.park)
# deviceState — 2 Hz
if frame % 5 == 0:
dat = messaging.new_message("deviceState")
dat.deviceState.started = True
dat.deviceState.freeSpacePercent = 80
dat.deviceState.memoryUsagePercent = 30
dat.deviceState.cpuTempC = [40.0] * 3
dat.deviceState.gpuTempC = [40.0] * 3
dat.deviceState.cpuUsagePercent = [20] * 8
dat.deviceState.networkType = log.DeviceState.NetworkType.cell4G
pm.send("deviceState", dat)
# pandaStates — 10 Hz
if frame % 1 == 0:
dat = messaging.new_message("pandaStates", 1)
dat.pandaStates[0].ignitionLine = True
dat.pandaStates[0].pandaType = log.PandaState.PandaType.tres
pm.send("pandaStates", dat)
# carState — 10 Hz
dat = messaging.new_message("carState")
cs = dat.carState
cs.vEgo = speed_ms
cs.vEgoCluster = speed_ms
cs.gearShifter = gear
cs.standstill = speed_ms < 0.1
cs.cruiseState.available = True
cs.cruiseState.enabled = engaged
cs.cruiseState.speed = cruise_mph * MS_PER_MPH if cruise_mph > 0 else 0
pm.send("carState", dat)
# controlsState — 10 Hz
dat = messaging.new_message("controlsState")
ctl = dat.controlsState
ctl.vCruise = cruise_mph * 1.60934 if cruise_mph > 0 else 255 # km/h or 255=not set
ctl.vCruiseCluster = ctl.vCruise
ctl.enabled = engaged
ctl.active = engaged
pm.send("controlsState", dat)
# driverMonitoringState — low freq
if frame % 10 == 0:
dat = messaging.new_message("driverMonitoringState")
dat.driverMonitoringState.isActiveMode = True
pm.send("driverMonitoringState", dat)
# liveCalibration — low freq
if frame % 50 == 0:
dat = messaging.new_message("liveCalibration")
dat.liveCalibration.calStatus = log.LiveCalibrationData.Status.calibrated
dat.liveCalibration.rpyCalib = [0.0, 0.0, 0.0]
pm.send("liveCalibration", dat)
frame += 1
time.sleep(0.1) # 10 Hz base loop
except KeyboardInterrupt:
print("\nBench simulator stopped")
if __name__ == "__main__":
main()

View File

@@ -5,18 +5,28 @@ ClearPilot telemetry collector.
Receives telemetry packets from any process via ZMQ, diffs against previous Receives telemetry packets from any process via ZMQ, diffs against previous
state per group, and writes only changed values to CSV. state per group, and writes only changed values to CSV.
Controlled by TelemetryEnabled param — when toggled on, the first packet
from each group writes all values (full dump). When toggled off, stops writing.
CSV format: timestamp,group,key,value CSV format: timestamp,group,key,value
""" """
import csv import csv
import json import json
import os import os
import shutil
import time
import zmq import zmq
from openpilot.common.params import Params
from openpilot.selfdrive.clearpilot.telemetry import TELEMETRY_SOCK from openpilot.selfdrive.clearpilot.telemetry import TELEMETRY_SOCK
from openpilot.selfdrive.manager.process import _log_dir from openpilot.selfdrive.manager.process import _log_dir, session_log
MIN_DISK_FREE_GB = 5
DISK_CHECK_INTERVAL = 10 # seconds
def main(): def main():
params = Params()
ctx = zmq.Context.instance() ctx = zmq.Context.instance()
sock = ctx.socket(zmq.PULL) sock = ctx.socket(zmq.PULL)
sock.setsockopt(zmq.RCVHWM, 1000) sock.setsockopt(zmq.RCVHWM, 1000)
@@ -24,16 +34,54 @@ def main():
csv_path = os.path.join(_log_dir, "telemetry.csv") csv_path = os.path.join(_log_dir, "telemetry.csv")
state: dict[str, dict] = {} # group -> {key: last_value} state: dict[str, dict] = {} # group -> {key: last_value}
was_enabled = False
with open(csv_path, "w", newline="") as f: writer = None
writer = csv.writer(f) f = None
writer.writerow(["timestamp", "group", "key", "value"]) last_disk_check = 0
f.flush()
while True: while True:
# Check enable state every iteration (cheap param read)
enabled = params.get("TelemetryEnabled") == b"1"
# Check disk space every 10 seconds while enabled
if enabled and (time.monotonic() - last_disk_check) > DISK_CHECK_INTERVAL:
last_disk_check = time.monotonic()
disk = shutil.disk_usage("/data")
free_gb = disk.free / (1024 ** 3)
if free_gb < MIN_DISK_FREE_GB:
session_log.warning("telemetry disabled: disk free %.1f GB < %d GB", free_gb, MIN_DISK_FREE_GB)
params.put("TelemetryEnabled", "0")
enabled = False
# Toggled on: open CSV, clear state so first packet is a full dump
if enabled and not was_enabled:
f = open(csv_path, "a", newline="")
writer = csv.writer(f)
if os.path.getsize(csv_path) == 0:
writer.writerow(["timestamp", "group", "key", "value"])
state.clear() # force full dump on first packet per group
f.flush()
# Toggled off: close file
if not enabled and was_enabled:
if f:
f.close()
f = None
writer = None
was_enabled = enabled
# Always drain the socket (even when disabled) to avoid backpressure
try: try:
raw = sock.recv_string() raw = sock.recv_string(zmq.NOBLOCK)
except zmq.Again:
time.sleep(0.01)
continue
except zmq.ZMQError: except zmq.ZMQError:
time.sleep(0.01)
continue
if not enabled or writer is None:
continue continue
try: try:
@@ -52,7 +100,6 @@ def main():
changed = False changed = False
for key, value in data.items(): for key, value in data.items():
# Convert to string for comparison so floats/ints/bools all diff correctly
str_val = str(value) str_val = str(value)
if key not in prev or prev[key] != str_val: if key not in prev or prev[key] != str_val:
writer.writerow([f"{ts:.6f}", group, key, str_val]) writer.writerow([f"{ts:.6f}", group, key, str_val])

View File

@@ -0,0 +1,12 @@
#!/usr/bin/env python3
"""Query the UI process for its current widget tree. Usage: python3 -m selfdrive.clearpilot.ui_dump"""
import zmq
ctx = zmq.Context()
sock = ctx.socket(zmq.REQ)
sock.setsockopt(zmq.RCVTIMEO, 2000)
sock.connect("ipc:///tmp/clearpilot_ui_rpc")
sock.send_string("dump")
try:
print(sock.recv_string())
except zmq.Again:
print("ERROR: UI not responding (timeout)")

View File

@@ -79,6 +79,9 @@ def manager_init(frogpilot_functions) -> None:
params = Params() params = Params()
params_storage = Params("/persist/params") params_storage = Params("/persist/params")
params.clear_all(ParamKeyType.CLEAR_ON_MANAGER_START) params.clear_all(ParamKeyType.CLEAR_ON_MANAGER_START)
# CLEARPILOT: always start with telemetry disabled
params.put("TelemetryEnabled", "0")
params.clear_all(ParamKeyType.CLEAR_ON_ONROAD_TRANSITION) params.clear_all(ParamKeyType.CLEAR_ON_ONROAD_TRANSITION)
params.clear_all(ParamKeyType.CLEAR_ON_OFFROAD_TRANSITION) params.clear_all(ParamKeyType.CLEAR_ON_OFFROAD_TRANSITION)
if is_release_branch(): if is_release_branch():
@@ -155,6 +158,7 @@ def manager_init(frogpilot_functions) -> None:
("DisableVTSCSmoothing", "0"), ("DisableVTSCSmoothing", "0"),
("DisengageVolume", "100"), ("DisengageVolume", "100"),
("DashcamDebug", "1"), ("DashcamDebug", "1"),
("TelemetryEnabled", "0"),
("DragonPilotTune", "0"), ("DragonPilotTune", "0"),
("DriverCamera", "0"), ("DriverCamera", "0"),
("DynamicPathWidth", "0"), ("DynamicPathWidth", "0"),
@@ -396,6 +400,14 @@ def manager_thread(frogpilot_functions) -> None:
ignore += ["manage_athenad", "uploader"] ignore += ["manage_athenad", "uploader"]
if os.getenv("NOBOARD") is not None: if os.getenv("NOBOARD") is not None:
ignore.append("pandad") ignore.append("pandad")
# CLEARPILOT: bench mode — disable real car processes, enable bench simulator
if os.getenv("BENCH_MODE") is not None:
ignore += ["pandad", "thermald", "controlsd", "radard", "plannerd",
"calibrationd", "torqued", "paramsd", "locationd", "sensord",
"ubloxd", "pigeond", "dmonitoringmodeld", "dmonitoringd",
"modeld", "soundd", "camerad", "loggerd", "micd",
"dashcamd"]
session_log.info("bench mode enabled")
ignore += [x for x in os.getenv("BLOCK", "").split(",") if len(x) > 0] ignore += [x for x in os.getenv("BLOCK", "").split(",") if len(x) > 0]
sm = messaging.SubMaster(['deviceState', 'carParams'], poll='deviceState') sm = messaging.SubMaster(['deviceState', 'carParams'], poll='deviceState')

View File

@@ -7,6 +7,7 @@ from openpilot.selfdrive.manager.process import PythonProcess, NativeProcess, Da
WEBCAM = os.getenv("USE_WEBCAM") is not None WEBCAM = os.getenv("USE_WEBCAM") is not None
BENCH_MODE = os.getenv("BENCH_MODE") is not None
def driverview(started: bool, params: Params, CP: car.CarParams) -> bool: def driverview(started: bool, params: Params, CP: car.CarParams) -> bool:
return started or params.get_bool("IsDriverViewEnabled") return started or params.get_bool("IsDriverViewEnabled")
@@ -110,6 +111,7 @@ procs = [
# ClearPilot processes # ClearPilot processes
NativeProcess("dashcamd", "selfdrive/clearpilot", ["./dashcamd"], dashcam_should_run), NativeProcess("dashcamd", "selfdrive/clearpilot", ["./dashcamd"], dashcam_should_run),
PythonProcess("telemetryd", "selfdrive.clearpilot.telemetryd", always_run), PythonProcess("telemetryd", "selfdrive.clearpilot.telemetryd", always_run),
PythonProcess("bench_onroad", "selfdrive.clearpilot.bench_onroad", always_run, enabled=BENCH_MODE),
] ]
managed_processes = {p.name: p for p in procs} managed_processes = {p.name: p for p in procs}

View File

@@ -5,7 +5,9 @@
#include <QStackedWidget> #include <QStackedWidget>
#include <QVBoxLayout> #include <QVBoxLayout>
#include "common/swaglog.h"
#include "selfdrive/ui/qt/util.h" #include "selfdrive/ui/qt/util.h"
#include "selfdrive/ui/qt/widgets/scrollview.h"
// HomeWindow: the container for the offroad and onroad UIs // HomeWindow: the container for the offroad and onroad UIs
@@ -25,8 +27,17 @@ HomeWindow::HomeWindow(QWidget* parent) : QWidget(parent) {
slayout = new QStackedLayout(); slayout = new QStackedLayout();
main_layout->addLayout(slayout); main_layout->addLayout(slayout);
home = new OffroadHome(this); home = new ClearPilotPanel(this);
QObject::connect(home, &OffroadHome::openSettings, this, &HomeWindow::openSettings); 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); slayout->addWidget(home);
onroad = new OnroadWindow(this); onroad = new OnroadWindow(this);
@@ -67,10 +78,10 @@ void HomeWindow::updateState(const UIState &s) {
// CLEARPILOT: show splash screen when onroad but in park // CLEARPILOT: show splash screen when onroad but in park
bool parked = s.scene.parked; bool parked = s.scene.parked;
if (parked && !was_parked_onroad) { if (parked && !was_parked_onroad) {
// just shifted into park — show splash LOGW("CLP UI: park transition -> showing splash");
slayout->setCurrentWidget(ready); slayout->setCurrentWidget(ready);
} else if (!parked && was_parked_onroad) { } else if (!parked && was_parked_onroad) {
// just shifted out of park — show onroad camera LOGW("CLP UI: drive transition -> showing onroad");
slayout->setCurrentWidget(onroad); slayout->setCurrentWidget(onroad);
} }
was_parked_onroad = parked; was_parked_onroad = parked;
@@ -90,11 +101,13 @@ void HomeWindow::updateState(const UIState &s) {
void HomeWindow::offroadTransition(bool offroad) { void HomeWindow::offroadTransition(bool offroad) {
sidebar->setVisible(false); sidebar->setVisible(false);
if (offroad) { if (offroad) {
LOGW("CLP UI: offroad transition -> showing splash");
was_parked_onroad = false; was_parked_onroad = false;
slayout->setCurrentWidget(ready); slayout->setCurrentWidget(ready);
} else { } else {
// CLEARPILOT: start onroad in splash — updateState will switch to // CLEARPILOT: start onroad in splash — updateState will switch to
// camera view once the car shifts out of park // camera view once the car shifts out of park
LOGW("CLP UI: onroad transition -> showing splash (parked)");
was_parked_onroad = true; was_parked_onroad = true;
slayout->setCurrentWidget(ready); slayout->setCurrentWidget(ready);
} }
@@ -102,34 +115,26 @@ void HomeWindow::offroadTransition(bool offroad) {
void HomeWindow::showDriverView(bool show, bool started) { void HomeWindow::showDriverView(bool show, bool started) {
if (show) { if (show) {
LOGW("CLP UI: showDriverView(true) -> driver_view");
emit closeSettings(); emit closeSettings();
slayout->setCurrentWidget(driver_view); slayout->setCurrentWidget(driver_view);
sidebar->setVisible(show == false); sidebar->setVisible(false);
} else { } else if (!started) {
if (started) { // Offroad, not started — show home menu
slayout->setCurrentWidget(onroad);
sidebar->setVisible(params.getBool("Sidebar"));
} else {
slayout->setCurrentWidget(home); 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) { void HomeWindow::mousePressEvent(QMouseEvent* e) {
// CLEARPILOT todo - tap on main goes straight to settings // CLEARPILOT: tap from any view goes to ClearPilotPanel
// Unless we click a debug widget. if (ready->isVisible() || onroad->isVisible()) {
LOGW("CLP UI: tap -> showing ClearPilotPanel");
// CLEARPILOT - click ready shows home (no sidebar)
if (!onroad->isVisible() && ready->isVisible()) {
sidebar->setVisible(false); sidebar->setVisible(false);
slayout->setCurrentWidget(home); slayout->setCurrentWidget(home);
} }
// Todo: widgets
if (onroad->isVisible()) {
emit openSettings();
}
} }
void HomeWindow::mouseDoubleClickEvent(QMouseEvent* e) { void HomeWindow::mouseDoubleClickEvent(QMouseEvent* e) {
@@ -137,59 +142,149 @@ void HomeWindow::mouseDoubleClickEvent(QMouseEvent* e) {
// const SubMaster &sm = *(uiState()->sm); // const SubMaster &sm = *(uiState()->sm);
} }
// CLEARPILOT: OffroadHome — clean grid launcher // CLEARPILOT: ClearPilotPanel — settings-style sidebar menu
OffroadHome::OffroadHome(QWidget* parent) : QFrame(parent) { static const char *clpSidebarBtnStyle = R"(
QVBoxLayout* main_layout = new QVBoxLayout(this);
main_layout->setContentsMargins(80, 80, 80, 80);
main_layout->setSpacing(0);
// grid of launcher buttons
QGridLayout *grid = new QGridLayout();
grid->setSpacing(40);
// Dashcam viewer button
QPushButton *dashcam_btn = new QPushButton("Dashcam");
dashcam_btn->setFixedSize(400, 300);
dashcam_btn->setStyleSheet(R"(
QPushButton { QPushButton {
background-color: #333333; color: grey;
border: none;
background: none;
font-size: 65px;
font-weight: 500;
}
QPushButton:checked {
color: white; color: white;
border-radius: 20px;
font-size: 48px;
font-weight: 600;
} }
QPushButton:pressed { QPushButton:pressed {
background-color: #555555; color: #ADADAD;
} }
)"); )";
grid->addWidget(dashcam_btn, 0, 0);
// Settings button ClearPilotPanel::ClearPilotPanel(QWidget* parent) : QFrame(parent) {
QPushButton *settings_btn = new QPushButton("Settings"); // Sidebar
settings_btn->setFixedSize(400, 300); QWidget *sidebar_widget = new QWidget;
settings_btn->setStyleSheet(R"( 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 { QPushButton {
background-color: #333333;
color: white; color: white;
border-radius: 20px; border-radius: 25px;
font-size: 48px; background: #292929;
font-weight: 600; font-size: 50px;
font-weight: 500;
} }
QPushButton:pressed { QPushButton:pressed {
background-color: #555555; color: #ADADAD;
} }
)"); )");
QObject::connect(settings_btn, &QPushButton::clicked, [=]() { emit openSettings(); }); close_btn->setFixedSize(300, 125);
grid->addWidget(settings_btn, 0, 1); sidebar_layout->addSpacing(10);
sidebar_layout->addWidget(close_btn, 0, Qt::AlignRight);
QObject::connect(close_btn, &QPushButton::clicked, [=]() { emit closePanel(); });
main_layout->addStretch(); // Panel content area
main_layout->addLayout(grid); panel_widget = new QStackedWidget();
main_layout->addStretch();
// Define panels: sidebar label -> content widget
// Home panel: buttons for Status and System Settings
QWidget *home_panel = new QWidget(this);
QVBoxLayout *home_layout = new QVBoxLayout(home_panel);
home_layout->setContentsMargins(50, 25, 50, 25);
home_layout->setSpacing(20);
QPushButton *status_btn = new QPushButton("Status");
status_btn->setFixedHeight(120);
status_btn->setStyleSheet(R"(
QPushButton {
background-color: #393939;
color: white;
border-radius: 15px;
font-size: 50px;
font-weight: 500;
text-align: left;
padding-left: 30px;
}
QPushButton:pressed { background-color: #4a4a4a; }
)");
QObject::connect(status_btn, &QPushButton::clicked, [=]() { emit openStatus(); });
home_layout->addWidget(status_btn);
QPushButton *sysset_btn = new QPushButton("System Settings");
sysset_btn->setFixedHeight(120);
sysset_btn->setStyleSheet(status_btn->styleSheet());
QObject::connect(sysset_btn, &QPushButton::clicked, [=]() { emit openSettings(); });
home_layout->addWidget(sysset_btn);
home_layout->addStretch();
// Dashcam panel: placeholder
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 ParamControl("TelemetryEnabled", "Telemetry Logging",
"Record telemetry data to CSV in the session log directory. "
"Captures only changed values for efficiency.", "", this);
debug_panel->addItem(telemetry_toggle);
// Register panels with sidebar buttons
QList<QPair<QString, QWidget *>> panels = {
{"Home", home_panel},
{"Dashcam", dashcam_panel},
{"Debug", debug_panel},
};
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);
ScrollView *panel_frame = new ScrollView(panel, this);
panel_widget->addWidget(panel_frame);
QObject::connect(btn, &QPushButton::clicked, [=, w = panel_frame]() {
btn->setChecked(true);
panel_widget->setCurrentWidget(w);
});
}
// Select Home by default
if (auto *first_btn = sidebar_widget->findChild<QPushButton *>(QString(), Qt::FindDirectChildrenOnly)) {
// Skip close_btn, find first sidebar btn
}
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"( setStyleSheet(R"(
OffroadHome { * {
color: white;
font-size: 50px;
}
ClearPilotPanel {
background-color: black; background-color: black;
} }
QStackedWidget, ScrollView {
background-color: #292929;
border-radius: 30px;
}
)"); )");
} }

View File

@@ -4,6 +4,7 @@
#include <QLabel> #include <QLabel>
#include <QPushButton> #include <QPushButton>
#include <QStackedLayout> #include <QStackedLayout>
#include <QStackedWidget>
#include <QTimer> #include <QTimer>
#include <QWidget> #include <QWidget>
@@ -16,14 +17,19 @@
#include "selfdrive/ui/qt/widgets/offroad_alerts.h" #include "selfdrive/ui/qt/widgets/offroad_alerts.h"
#include "selfdrive/ui/ui.h" #include "selfdrive/ui/ui.h"
class OffroadHome : public QFrame { class ClearPilotPanel : public QFrame {
Q_OBJECT Q_OBJECT
public: public:
explicit OffroadHome(QWidget* parent = 0); explicit ClearPilotPanel(QWidget* parent = 0);
signals: signals:
void openSettings(int index = 0, const QString &param = ""); void openSettings(int index = 0, const QString &param = "");
void openStatus();
void closePanel();
private:
QStackedWidget *panel_widget;
}; };
class HomeWindow : public QWidget { class HomeWindow : public QWidget {
@@ -35,6 +41,7 @@ public:
signals: signals:
void openSettings(int index = 0, const QString &param = ""); void openSettings(int index = 0, const QString &param = "");
void openStatus();
void closeSettings(); void closeSettings();
public slots: public slots:
@@ -49,7 +56,7 @@ protected:
private: private:
Sidebar *sidebar; Sidebar *sidebar;
OffroadHome *home; ClearPilotPanel *home;
OnroadWindow *onroad; OnroadWindow *onroad;
DriverViewWindow *driver_view; DriverViewWindow *driver_view;
QStackedLayout *slayout; QStackedLayout *slayout;

View File

@@ -7,6 +7,7 @@
#include <sstream> #include <sstream>
#include <QApplication> #include <QApplication>
#include <QDateTime>
#include <QDebug> #include <QDebug>
#include <QMouseEvent> #include <QMouseEvent>
@@ -402,6 +403,17 @@ void AnnotatedCameraWidget::drawHud(QPainter &p) {
Hardware::set_display_power(true); Hardware::set_display_power(true);
} }
// CLEARPILOT: blinking red circle when telemetry is recording
if (Params().get("TelemetryEnabled") == "1") {
// Blink: visible for 500ms, hidden for 500ms
int phase = (QDateTime::currentMSecsSinceEpoch() / 500) % 2;
if (phase == 0) {
p.setPen(Qt::NoPen);
p.setBrush(QColor(220, 30, 30));
p.drawEllipse(width() - 150, 50, 100, 100);
}
}
// Header gradient // Header gradient
QLinearGradient bg(0, UI_HEADER_HEIGHT - (UI_HEADER_HEIGHT / 2.5), 0, UI_HEADER_HEIGHT); 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)); bg.setColorAt(0, QColor::fromRgbF(0, 0, 0, 0.45));

Binary file not shown.

View File

@@ -1,6 +1,9 @@
#include "selfdrive/ui/qt/window.h" #include "selfdrive/ui/qt/window.h"
#include <QFontDatabase> #include <QFontDatabase>
#include <QMouseEvent>
#include <zmq.h>
#include "system/hardware/hw.h" #include "system/hardware/hw.h"
@@ -11,6 +14,7 @@ MainWindow::MainWindow(QWidget *parent) : QWidget(parent) {
homeWindow = new HomeWindow(this); homeWindow = new HomeWindow(this);
main_layout->addWidget(homeWindow); main_layout->addWidget(homeWindow);
QObject::connect(homeWindow, &HomeWindow::openSettings, this, &MainWindow::openSettings); QObject::connect(homeWindow, &HomeWindow::openSettings, this, &MainWindow::openSettings);
QObject::connect(homeWindow, &HomeWindow::openStatus, this, &MainWindow::openStatus);
QObject::connect(homeWindow, &HomeWindow::closeSettings, this, &MainWindow::closeSettings); QObject::connect(homeWindow, &HomeWindow::closeSettings, this, &MainWindow::closeSettings);
settingsWindow = new SettingsWindow(this); settingsWindow = new SettingsWindow(this);
@@ -24,6 +28,11 @@ MainWindow::MainWindow(QWidget *parent) : QWidget(parent) {
homeWindow->showDriverView(true); 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); onboardingWindow = new OnboardingWindow(this);
main_layout->addWidget(onboardingWindow); main_layout->addWidget(onboardingWindow);
QObject::connect(onboardingWindow, &OnboardingWindow::onboardingDone, [=]() { QObject::connect(onboardingWindow, &OnboardingWindow::onboardingDone, [=]() {
@@ -35,11 +44,14 @@ MainWindow::MainWindow(QWidget *parent) : QWidget(parent) {
QObject::connect(uiState(), &UIState::offroadTransition, [=](bool offroad) { QObject::connect(uiState(), &UIState::offroadTransition, [=](bool offroad) {
if (!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, [=]() { QObject::connect(device(), &Device::interactiveTimeout, [=]() {
if (main_layout->currentWidget() == settingsWindow) { if (main_layout->currentWidget() == settingsWindow ||
main_layout->currentWidget() == statusWindow) {
closeSettings(); closeSettings();
} }
}); });
@@ -63,6 +75,74 @@ MainWindow::MainWindow(QWidget *parent) : QWidget(parent) {
} }
)"); )");
setAttribute(Qt::WA_NoSystemBackground); setAttribute(Qt::WA_NoSystemBackground);
// CLEARPILOT: UI introspection RPC server
zmq_ctx = zmq_ctx_new();
zmq_sock = zmq_socket(zmq_ctx, ZMQ_REP);
zmq_bind(zmq_sock, "ipc:///tmp/clearpilot_ui_rpc");
int fd;
size_t fd_sz = sizeof(fd);
zmq_getsockopt(zmq_sock, ZMQ_FD, &fd, &fd_sz);
rpc_notifier = new QSocketNotifier(fd, QSocketNotifier::Read, this);
connect(rpc_notifier, &QSocketNotifier::activated, this, &MainWindow::handleRpcRequest);
}
void MainWindow::handleRpcRequest() {
int events = 0;
size_t events_sz = sizeof(events);
zmq_getsockopt(zmq_sock, ZMQ_EVENTS, &events, &events_sz);
if (!(events & ZMQ_POLLIN)) return;
char buf[256];
int rc = zmq_recv(zmq_sock, buf, sizeof(buf) - 1, ZMQ_DONTWAIT);
if (rc < 0) return;
buf[rc] = 0;
QString response;
if (strcmp(buf, "dump") == 0) {
response = dumpWidgetTree(this);
} else {
response = "unknown command";
}
QByteArray resp = response.toUtf8();
zmq_send(zmq_sock, resp.data(), resp.size(), 0);
}
QString MainWindow::dumpWidgetTree(QWidget *w, int depth) {
QString result;
QString indent(depth * 2, ' ');
QString className = w->metaObject()->className();
QString name = w->objectName().isEmpty() ? "(no name)" : w->objectName();
bool visible = w->isVisible();
QRect geo = w->geometry();
result += QString("%1%2 [%3] vis=%4 geo=%5,%6 %7x%8")
.arg(indent, className, name)
.arg(visible ? "Y" : "N")
.arg(geo.x()).arg(geo.y()).arg(geo.width()).arg(geo.height());
// Show stacked layout/widget current index
if (auto *sl = w->findChild<QStackedLayout *>(QString(), Qt::FindDirectChildrenOnly)) {
QWidget *cur = sl->currentWidget();
QString curClass = cur ? cur->metaObject()->className() : "null";
result += QString(" stack_cur=%1/%2(%3)").arg(sl->currentIndex()).arg(sl->count()).arg(curClass);
}
if (auto *sw = qobject_cast<QStackedWidget *>(w)) {
QWidget *cur = sw->currentWidget();
QString curClass = cur ? cur->metaObject()->className() : "null";
result += QString(" stack_cur=%1/%2(%3)").arg(sw->currentIndex()).arg(sw->count()).arg(curClass);
}
result += "\n";
for (QObject *child : w->children()) {
QWidget *cw = qobject_cast<QWidget *>(child);
if (cw && depth < 4) {
result += dumpWidgetTree(cw, depth + 1);
}
}
return result;
} }
void MainWindow::openSettings(int index, const QString &param) { void MainWindow::openSettings(int index, const QString &param) {
@@ -70,6 +150,10 @@ void MainWindow::openSettings(int index, const QString &param) {
settingsWindow->setCurrentPanel(index, param); settingsWindow->setCurrentPanel(index, param);
} }
void MainWindow::openStatus() {
main_layout->setCurrentWidget(statusWindow);
}
void MainWindow::closeSettings() { void MainWindow::closeSettings() {
main_layout->setCurrentWidget(homeWindow); main_layout->setCurrentWidget(homeWindow);
@@ -96,3 +180,138 @@ bool MainWindow::eventFilter(QObject *obj, QEvent *event) {
} }
return ignore; return ignore;
} }
// CLEARPILOT: Status window — live system stats, refreshed every second
#include <QFile>
#include <QProcess>
#include <QDateTime>
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();
}
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");
ip_label = makeRow("IP Address");
wifi_label = makeRow("WiFi");
vpn_label = makeRow("VPN");
gps_label = makeRow("GPS");
telemetry_label = makeRow("Telemetry");
layout->addStretch();
setStyleSheet("StatusWindow { background-color: black; }");
// Refresh every second
QTimer *timer = new QTimer(this);
connect(timer, &QTimer::timeout, this, &StatusWindow::refresh);
timer->start(1000);
refresh();
}
void StatusWindow::refresh() {
// Time
time_label->setText(QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss"));
// Storage
QString df = shellCmd("df -h /data | tail -1 | awk '{print $3 \" / \" $2 \" (\" $5 \" used)\"}'");
storage_label->setText(df);
// RAM
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;
ram_label->setText(QString("%1 / %2 MB").arg(used / 1024).arg(total / 1024));
}
// Load
QString loadavg = readFile("/proc/loadavg");
QStringList parts = loadavg.split(' ');
if (parts.size() >= 3) {
load_label->setText(QString("%1 %2 %3").arg(parts[0], parts[1], parts[2]));
}
// IP + WiFi
QString ip = shellCmd("ip route get 1.1.1.1 2>/dev/null | head -1 | awk '{print $7}'");
ip_label->setText(ip.isEmpty() ? "No connection" : ip);
QString essid = shellCmd("iwconfig wlan0 2>/dev/null | grep -oP 'ESSID:\"\\K[^\"]*'");
wifi_label->setText(essid.isEmpty() ? "Not connected" : essid);
// VPN
QString tun = shellCmd("ip link show tun0 2>/dev/null | head -1");
if (tun.contains("UP")) {
QString vpn_ip = shellCmd("ip addr show tun0 2>/dev/null | grep 'inet ' | awk '{print $2}'");
vpn_label->setText("Connected (" + vpn_ip + ")");
vpn_label->setStyleSheet("color: #17c44d; font-size: 38px;");
} else {
vpn_label->setText("Not connected");
vpn_label->setStyleSheet("color: #ff4444; font-size: 38px;");
}
// GPS
QString gps_raw = shellCmd("cat /data/params/d/LastGPSPosition 2>/dev/null");
if (gps_raw.isEmpty()) {
gps_label->setText("No fix");
gps_label->setStyleSheet("color: #ff4444; font-size: 38px;");
} else {
gps_label->setText(gps_raw);
gps_label->setStyleSheet("color: white; font-size: 38px;");
}
// Telemetry
QString telem = shellCmd("cat /data/params/d/TelemetryEnabled 2>/dev/null");
if (telem == "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;");
}
}
void StatusWindow::mousePressEvent(QMouseEvent *e) {
emit closeStatus();
}

View File

@@ -1,12 +1,42 @@
#pragma once #pragma once
#include <QStackedLayout> #include <QStackedLayout>
#include <QLabel>
#include <QSocketNotifier>
#include <QTimer>
#include <QWidget> #include <QWidget>
#include "selfdrive/ui/qt/home.h" #include "selfdrive/ui/qt/home.h"
#include "selfdrive/ui/qt/offroad/onboarding.h" #include "selfdrive/ui/qt/offroad/onboarding.h"
#include "selfdrive/ui/qt/offroad/settings.h" #include "selfdrive/ui/qt/offroad/settings.h"
class StatusWindow : public QFrame {
Q_OBJECT
public:
explicit StatusWindow(QWidget *parent = 0);
protected:
void mousePressEvent(QMouseEvent *e) override;
signals:
void closeStatus();
private slots:
void refresh();
private:
QLabel *storage_label;
QLabel *ram_label;
QLabel *load_label;
QLabel *ip_label;
QLabel *wifi_label;
QLabel *vpn_label;
QLabel *gps_label;
QLabel *time_label;
QLabel *telemetry_label;
};
class MainWindow : public QWidget { class MainWindow : public QWidget {
Q_OBJECT Q_OBJECT
@@ -16,13 +46,24 @@ public:
private: private:
bool eventFilter(QObject *obj, QEvent *event) override; bool eventFilter(QObject *obj, QEvent *event) override;
void openSettings(int index = 0, const QString &param = ""); void openSettings(int index = 0, const QString &param = "");
void openStatus();
void closeSettings(); void closeSettings();
QString dumpWidgetTree(QWidget *w, int depth = 0);
QStackedLayout *main_layout; QStackedLayout *main_layout;
HomeWindow *homeWindow; HomeWindow *homeWindow;
SettingsWindow *settingsWindow; SettingsWindow *settingsWindow;
StatusWindow *statusWindow;
OnboardingWindow *onboardingWindow; OnboardingWindow *onboardingWindow;
// CLEARPILOT: UI introspection RPC
void *zmq_ctx = nullptr;
void *zmq_sock = nullptr;
QSocketNotifier *rpc_notifier = nullptr;
// FrogPilot variables // FrogPilot variables
Params params; Params params;
private slots:
void handleRpcRequest();
}; };