session logging, build-only mode, park splash, boot logo spinner

- Per-process stderr logging to /data/log2/{session}/ with time-safe
  directory naming (boot-xxx renamed once GPS/NTP sets clock)
- Aggregate session.log with major events (start/stop, transitions)
- 30-day log rotation cleanup on manager startup
- build_only.sh: compile without starting manager, non-blocking error
  display, kills stale processes
- build.py: copies updated spinner binary after successful build
- Onroad park mode: show splash screen when car is on but in park,
  switch to camera view when shifted to drive, honor screen on/off
- Spinner: full-screen boot logo background from /usr/comma/bg.jpg
  with white progress bar overlay
- Boot logo: auto-regenerate when boot_logo.png changes, 140% scale
- launch_openpilot.sh: cd to /data/openpilot for reliable execution
- launch_chffrplus.sh: kill stale text error displays on startup
- spinner shell wrapper: prefer freshly built _spinner over prebuilt

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-12 23:19:07 +00:00
parent 4f3bfb4e8c
commit 578277ab9c
16 changed files with 266 additions and 77 deletions

View File

@@ -57,17 +57,25 @@ chown -R comma:comma /data/openpilot
### Testing Changes ### Testing Changes
The launch script now self-cleans — it kills other instances of itself, `launch_chffrplus.sh`, and `manager.py` before starting. No need to manually kill first. 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`.
```bash ```bash
# Fix ownership (we edit as root, openpilot runs as comma) # 1. Fix ownership (we edit as root, openpilot runs as comma)
chown -R comma:comma /data/openpilot chown -R comma:comma /data/openpilot
# Remove prebuilt to force recompilation (it is recreated after each successful build) # 2. Build (kills running manager, removes prebuilt, compiles, exits)
rm -f /data/openpilot/prebuilt # Shows progress spinner on screen. On failure, shows error on screen
# and prints to stderr. Does NOT start the manager.
su - comma -c "bash /data/openpilot/build_only.sh"
# Must use a login shell as comma — sudo -u won't set up the right Python/env (3.11 via pyenv) # 3. If build succeeded ($? == 0), start openpilot
su - comma -c "bash /data/openpilot/launch_openpilot.sh" su - comma -c "bash /data/openpilot/launch_openpilot.sh"
# 4. Review the aggregate session log for errors
cat /data/log2/$(ls -t /data/log2/ | head -1)/session.log
# 5. Check per-process stderr logs if needed
ls /data/log2/$(ls -t /data/log2/ | head -1)/
``` ```
### Adding New Params ### Adding New Params
@@ -85,6 +93,38 @@ 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
## Session Logging
Per-process stderr and an aggregate event log are captured in `/data/log2/{session}/`.
### Log Directory
- Created at manager import time with timestamp: `/data/log2/YYYY-MM-DD-HH-MM-SS/`
- If system clock is invalid (cold boot, no WiFi, RTC stuck at 1970): uses `/data/log2/boot-{monotonic}/`, renamed to real timestamp once GPS/NTP resolves the time
- Session directories older than 30 days are deleted on manager startup
### Per-Process Logs
- Every `PythonProcess` and `NativeProcess` has stderr redirected to `{name}.log` in the session directory
- `DaemonProcess` (athenad) redirects both stdout+stderr (existing behavior)
- Stderr is redirected via `os.dup2` inside the forked child process
### Aggregate Session Log (`session.log`)
A single `session.log` in each session directory records major events:
- Manager start/stop/crash
- Process starts, deaths (with exit codes), watchdog restarts
- Onroad/offroad transitions
### Key Files
| File | Role |
|------|------|
| `selfdrive/manager/process.py` | Log directory creation, stderr redirection, session_log logger |
| `selfdrive/manager/manager.py` | Log rotation cleanup, session event logging |
| `build_only.sh` | Build-only script (no manager start) |
## Dashcam (dashcamd) ## Dashcam (dashcamd)
### Architecture ### Architecture

29
build_only.sh Executable file
View File

@@ -0,0 +1,29 @@
#!/usr/bin/bash
# CLEARPILOT: build-only mode — compile without starting manager.
# On failure: shows error on screen (non-blocking) and exits nonzero with stderr output.
# On success: exits 0, does not start manager.
#
# Usage: su - comma -c "bash /data/openpilot/build_only.sh"
BASEDIR="/data/openpilot"
# Kill stale error displays and any running manager/launch processes
pkill -f "selfdrive/ui/text" 2>/dev/null
pkill -f 'launch_openpilot.sh' 2>/dev/null
pkill -f 'launch_chffrplus.sh' 2>/dev/null
pkill -f 'python.*manager.py' 2>/dev/null
sleep 1
source "$BASEDIR/launch_env.sh"
ln -sfn "$BASEDIR" /data/pythonpath
export PYTHONPATH="$BASEDIR"
# Hardware init (GPU perms)
sudo chgrp gpu /dev/adsprpc-smd /dev/ion /dev/kgsl-3d0 2>/dev/null
sudo chmod 660 /dev/adsprpc-smd /dev/ion /dev/kgsl-3d0 2>/dev/null
cd "$BASEDIR/selfdrive/manager"
rm -f "$BASEDIR/prebuilt"
BUILD_ONLY=1 exec ./build.py

View File

@@ -79,6 +79,9 @@ function launch {
agnos_init agnos_init
fi fi
# CLEARPILOT: kill stale error display from previous build/run
pkill -f "selfdrive/ui/text" 2>/dev/null
# write tmux scrollback to a file # write tmux scrollback to a file
tmux capture-pane -pq -S-1000 > /tmp/launch_log tmux capture-pane -pq -S-1000 > /tmp/launch_log

View File

@@ -12,4 +12,5 @@ sleep 1
bash /data/openpilot/system/clearpilot/on_start.sh bash /data/openpilot/system/clearpilot/on_start.sh
cd /data/openpilot
exec ./launch_chffrplus.sh exec ./launch_chffrplus.sh

View File

@@ -1,5 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import os import os
import sys
import subprocess import subprocess
from pathlib import Path from pathlib import Path
@@ -54,6 +55,14 @@ def build(spinner: Spinner, dirty: bool = False, minimal: bool = False) -> None:
if scons.returncode == 0: if scons.returncode == 0:
Path('/data/openpilot/prebuilt').touch() Path('/data/openpilot/prebuilt').touch()
# CLEARPILOT: update prebuilt spinner if the new build is newer
new_spinner = Path(BASEDIR) / "selfdrive/ui/_spinner"
old_spinner = Path(BASEDIR) / "selfdrive/ui/qt/spinner"
if new_spinner.exists() and (not old_spinner.exists() or new_spinner.stat().st_mtime > old_spinner.stat().st_mtime):
import shutil
shutil.copy2(str(new_spinner), str(old_spinner))
break break
if scons.returncode != 0: if scons.returncode != 0:
@@ -69,8 +78,13 @@ def build(spinner: Spinner, dirty: bool = False, minimal: bool = False) -> None:
# Show TextWindow # Show TextWindow
spinner.close() spinner.close()
if not os.getenv("CI"): if not os.getenv("CI"):
with TextWindow("openpilot failed to build\n \n" + error_s) as t: # CLEARPILOT: BUILD_ONLY mode shows error on screen but doesn't block
t = TextWindow("openpilot failed to build\n \n" + error_s)
if os.getenv("BUILD_ONLY"):
print(error_s, file=sys.stderr)
else:
t.wait_for_exit() t.wait_for_exit()
t.close()
exit(1) exit(1)
# enforce max cache size # enforce max cache size

View File

@@ -16,7 +16,7 @@ from openpilot.common.text_window import TextWindow
from openpilot.common.time import system_time_valid from openpilot.common.time import system_time_valid
from openpilot.system.hardware import HARDWARE, PC from openpilot.system.hardware import HARDWARE, PC
from openpilot.selfdrive.manager.helpers import unblock_stdout, write_onroad_params, save_bootlog from openpilot.selfdrive.manager.helpers import unblock_stdout, write_onroad_params, save_bootlog
from openpilot.selfdrive.manager.process import ensure_running from openpilot.selfdrive.manager.process import ensure_running, update_log_dir_timestamp, session_log
from openpilot.selfdrive.manager.process_config import managed_processes from openpilot.selfdrive.manager.process_config import managed_processes
from openpilot.selfdrive.athena.registration import register, UNREGISTERED_DONGLE_ID from openpilot.selfdrive.athena.registration import register, UNREGISTERED_DONGLE_ID
from openpilot.common.swaglog import cloudlog, add_file_handler from openpilot.common.swaglog import cloudlog, add_file_handler
@@ -51,7 +51,26 @@ def frogpilot_boot_functions(frogpilot_functions):
except Exception as e: except Exception as e:
print(f"An unexpected error occurred: {e}") print(f"An unexpected error occurred: {e}")
def cleanup_old_logs(max_age_days=30):
"""CLEARPILOT: delete session log directories older than max_age_days."""
import shutil
log_base = "/data/log2"
if not os.path.exists(log_base):
return
cutoff = time.time() - (max_age_days * 86400)
for entry in os.listdir(log_base):
path = os.path.join(log_base, entry)
if os.path.isdir(path):
if os.path.getmtime(path) < cutoff:
try:
shutil.rmtree(path)
except OSError:
pass
def manager_init(frogpilot_functions) -> None: def manager_init(frogpilot_functions) -> None:
cleanup_old_logs()
frogpilot_boot = threading.Thread(target=frogpilot_boot_functions, args=(frogpilot_functions,)) frogpilot_boot = threading.Thread(target=frogpilot_boot_functions, args=(frogpilot_functions,))
frogpilot_boot.start() frogpilot_boot.start()
@@ -367,6 +386,7 @@ def manager_thread(frogpilot_functions) -> None:
cloudlog.bind(daemon="manager") cloudlog.bind(daemon="manager")
cloudlog.info("manager start") cloudlog.info("manager start")
cloudlog.info({"environ": os.environ}) cloudlog.info({"environ": os.environ})
session_log.info("manager starting")
params = Params() params = Params()
params_memory = Params("/dev/shm/params") params_memory = Params("/dev/shm/params")
@@ -397,6 +417,7 @@ def manager_thread(frogpilot_functions) -> None:
if started and not started_prev: if started and not started_prev:
params.clear_all(ParamKeyType.CLEAR_ON_ONROAD_TRANSITION) params.clear_all(ParamKeyType.CLEAR_ON_ONROAD_TRANSITION)
session_log.info("onroad transition")
if openpilot_crashed: if openpilot_crashed:
os.remove(os.path.join(sentry.CRASHES_DIR, 'error.txt')) os.remove(os.path.join(sentry.CRASHES_DIR, 'error.txt'))
@@ -404,6 +425,7 @@ def manager_thread(frogpilot_functions) -> None:
elif not started and started_prev: elif not started and started_prev:
params.clear_all(ParamKeyType.CLEAR_ON_OFFROAD_TRANSITION) params.clear_all(ParamKeyType.CLEAR_ON_OFFROAD_TRANSITION)
params_memory.clear_all(ParamKeyType.CLEAR_ON_OFFROAD_TRANSITION) params_memory.clear_all(ParamKeyType.CLEAR_ON_OFFROAD_TRANSITION)
session_log.info("offroad transition")
# update onroad params, which drives boardd's safety setter thread # update onroad params, which drives boardd's safety setter thread
if started != started_prev: if started != started_prev:
@@ -413,6 +435,9 @@ def manager_thread(frogpilot_functions) -> None:
ensure_running(managed_processes.values(), started, params=params, CP=sm['carParams'], not_run=ignore) ensure_running(managed_processes.values(), started, params=params, CP=sm['carParams'], not_run=ignore)
# CLEARPILOT: rename log directory once system time is valid
update_log_dir_timestamp()
running = ' '.join("{}{}\u001b[0m".format("\u001b[32m" if p.proc.is_alive() else "\u001b[31m", p.name) running = ' '.join("{}{}\u001b[0m".format("\u001b[32m" if p.proc.is_alive() else "\u001b[31m", p.name)
for p in managed_processes.values() if p.proc) for p in managed_processes.values() if p.proc)
print(running) print(running)
@@ -430,6 +455,7 @@ def manager_thread(frogpilot_functions) -> None:
shutdown = True shutdown = True
params.put("LastManagerExitReason", f"{param} {datetime.datetime.now()}") params.put("LastManagerExitReason", f"{param} {datetime.datetime.now()}")
cloudlog.warning(f"Shutting down manager - {param} set") cloudlog.warning(f"Shutting down manager - {param} set")
session_log.info("manager shutting down: %s", param)
if shutdown: if shutdown:
break break
@@ -481,6 +507,7 @@ if __name__ == "__main__":
except Exception: except Exception:
add_file_handler(cloudlog) add_file_handler(cloudlog)
cloudlog.exception("Manager failed to start") cloudlog.exception("Manager failed to start")
session_log.critical("manager failed to start: %s", traceback.format_exc())
try: try:
managed_processes['ui'].stop() managed_processes['ui'].stop()

View File

@@ -2,6 +2,7 @@ import importlib
import os import os
import signal import signal
import struct import struct
import sys
import datetime import datetime
import time import time
import subprocess import subprocess
@@ -17,17 +18,62 @@ import openpilot.selfdrive.sentry as sentry
from openpilot.common.basedir import BASEDIR from openpilot.common.basedir import BASEDIR
from openpilot.common.params import Params from openpilot.common.params import Params
from openpilot.common.swaglog import cloudlog from openpilot.common.swaglog import cloudlog
from openpilot.common.time import system_time_valid
WATCHDOG_FN = "/dev/shm/wd_" WATCHDOG_FN = "/dev/shm/wd_"
ENABLE_WATCHDOG = os.getenv("NO_WATCHDOG") is None ENABLE_WATCHDOG = os.getenv("NO_WATCHDOG") is None
timestamp = datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S") # CLEARPILOT: time-safe log directory — use temporary name if clock is invalid (1970),
_log_dir = f"/data/log2/{timestamp}" # rename to real timestamp once GPS/NTP resolves the time
if system_time_valid():
_log_dir = f"/data/log2/{datetime.datetime.now().strftime('%Y-%m-%d-%H-%M-%S')}"
_time_resolved = True
else:
_log_dir = f"/data/log2/boot-{int(time.monotonic())}"
_time_resolved = False
os.makedirs(_log_dir, exist_ok=True) os.makedirs(_log_dir, exist_ok=True)
# CLEARPILOT: aggregate session log for major events
import logging
session_log = logging.getLogger("clearpilot.session")
session_log.setLevel(logging.DEBUG)
_session_handler = logging.FileHandler(os.path.join(_log_dir, "session.log"))
_session_handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(message)s"))
session_log.addHandler(_session_handler)
def launcher(proc: str, name: str) -> None: def update_log_dir_timestamp():
"""Rename boot-xxx log dir to real timestamp once system time is valid."""
global _log_dir, _time_resolved, _session_handler
if _time_resolved:
return
if not system_time_valid():
return
new_dir = f"/data/log2/{datetime.datetime.now().strftime('%Y-%m-%d-%H-%M-%S')}"
try:
os.rename(_log_dir, new_dir)
_log_dir = new_dir
_time_resolved = True
# Re-point session log handler to renamed directory
session_log.removeHandler(_session_handler)
_session_handler.close()
_session_handler = logging.FileHandler(os.path.join(_log_dir, "session.log"))
_session_handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(message)s"))
session_log.addHandler(_session_handler)
session_log.info("log directory renamed to %s", _log_dir)
except OSError:
pass
def launcher(proc: str, name: str, log_path: str) -> None:
# CLEARPILOT: redirect stderr to per-process log file
try:
log_file = open(log_path, 'a')
os.dup2(log_file.fileno(), sys.stderr.fileno())
except Exception:
pass
try: try:
# import the process # import the process
mod = importlib.import_module(proc) mod = importlib.import_module(proc)
@@ -53,9 +99,16 @@ def launcher(proc: str, name: str) -> None:
raise raise
def nativelauncher(pargs: list[str], cwd: str, name: str) -> None: def nativelauncher(pargs: list[str], cwd: str, name: str, log_path: str) -> None:
os.environ['MANAGER_DAEMON'] = name os.environ['MANAGER_DAEMON'] = name
# CLEARPILOT: redirect stderr to per-process log file
try:
log_file = open(log_path, 'a')
os.dup2(log_file.fileno(), sys.stderr.fileno())
except Exception:
pass
# exec the process # exec the process
os.chdir(cwd) os.chdir(cwd)
os.execvp(pargs[0], pargs) os.execvp(pargs[0], pargs)
@@ -110,6 +163,7 @@ class ManagerProcess(ABC):
if dt > self.watchdog_max_dt: if dt > self.watchdog_max_dt:
if (self.watchdog_seen or self.always_watchdog and self.proc.exitcode is not None) and ENABLE_WATCHDOG: if (self.watchdog_seen or self.always_watchdog and self.proc.exitcode is not None) and ENABLE_WATCHDOG:
cloudlog.error(f"Watchdog timeout for {self.name} (exitcode {self.proc.exitcode}) restarting ({started=})") cloudlog.error(f"Watchdog timeout for {self.name} (exitcode {self.proc.exitcode}) restarting ({started=})")
session_log.warning("watchdog timeout for %s (exitcode %s), restarting", self.name, self.proc.exitcode)
self.restart() self.restart()
else: else:
self.watchdog_seen = True self.watchdog_seen = True
@@ -139,6 +193,10 @@ class ManagerProcess(ABC):
ret = self.proc.exitcode ret = self.proc.exitcode
cloudlog.info(f"{self.name} is dead with {ret}") cloudlog.info(f"{self.name} is dead with {ret}")
if ret is not None and ret != 0:
session_log.error("process %s died with exit code %s", self.name, ret)
elif ret == 0:
session_log.info("process %s stopped (exit 0)", self.name)
if self.proc.exitcode is not None: if self.proc.exitcode is not None:
self.shutting_down = False self.shutting_down = False
@@ -200,7 +258,8 @@ class NativeProcess(ManagerProcess):
cwd = os.path.join(BASEDIR, self.cwd) cwd = os.path.join(BASEDIR, self.cwd)
cloudlog.info(f"starting process {self.name}") cloudlog.info(f"starting process {self.name}")
self.proc = Process(name=self.name, target=self.launcher, args=(self.cmdline, cwd, self.name)) session_log.info("starting %s", self.name)
self.proc = Process(name=self.name, target=self.launcher, args=(self.cmdline, cwd, self.name, log_path))
self.proc.start() self.proc.start()
self.watchdog_seen = False self.watchdog_seen = False
self.shutting_down = False self.shutting_down = False
@@ -232,7 +291,8 @@ class PythonProcess(ManagerProcess):
global _log_dir global _log_dir
log_path = _log_dir+"/"+self.name+".log" log_path = _log_dir+"/"+self.name+".log"
cloudlog.info(f"starting python {self.module}") cloudlog.info(f"starting python {self.module}")
self.proc = Process(name=self.name, target=self.launcher, args=(self.module, self.name)) session_log.info("starting %s", self.name)
self.proc = Process(name=self.name, target=self.launcher, args=(self.module, self.name, log_path))
self.proc.start() self.proc.start()
self.watchdog_seen = False self.watchdog_seen = False
self.shutting_down = False self.shutting_down = False
@@ -276,6 +336,7 @@ class DaemonProcess(ManagerProcess):
pass pass
cloudlog.info(f"starting daemon {self.name}") cloudlog.info(f"starting daemon {self.name}")
session_log.info("starting daemon %s", self.name)
proc = subprocess.Popen(['python', '-m', self.module], proc = subprocess.Popen(['python', '-m', self.module],
stdin=open('/dev/null'), stdin=open('/dev/null'),
stdout=open(log_path, 'a'), stdout=open(log_path, 'a'),

View File

@@ -61,19 +61,42 @@ void HomeWindow::showSidebar(bool show) {
} }
void HomeWindow::updateState(const UIState &s) { void HomeWindow::updateState(const UIState &s) {
// const SubMaster &sm = *(s.sm);
if (s.scene.started) { if (s.scene.started) {
showDriverView(s.scene.driver_camera_timer >= 10, true); showDriverView(s.scene.driver_camera_timer >= 10, true);
// CLEARPILOT: show splash screen when onroad but in park
bool parked = s.scene.parked;
if (parked && !was_parked_onroad) {
// just shifted into park — show splash
slayout->setCurrentWidget(ready);
} else if (!parked && was_parked_onroad) {
// just shifted out of park — show onroad camera
slayout->setCurrentWidget(onroad);
}
was_parked_onroad = parked;
// CLEARPILOT: honor display on/off while showing splash in park
if (parked && ready->isVisible()) {
int screenMode = paramsMemory.getInt("ScreenDisaplayMode");
if (screenMode == 1) {
Hardware::set_display_power(false);
} else {
Hardware::set_display_power(true);
}
}
} }
} }
void HomeWindow::offroadTransition(bool offroad) { void HomeWindow::offroadTransition(bool offroad) {
sidebar->setVisible(false);
if (offroad) { if (offroad) {
sidebar->setVisible(false); was_parked_onroad = false;
slayout->setCurrentWidget(ready); slayout->setCurrentWidget(ready);
} else { } else {
sidebar->setVisible(false); // CLEARPILOT: start onroad in splash — updateState will switch to
slayout->setCurrentWidget(onroad); // camera view once the car shifts out of park
was_parked_onroad = true;
slayout->setCurrentWidget(ready);
} }
} }

View File

@@ -56,10 +56,11 @@ private:
// FrogPilot variables // FrogPilot variables
Params params; Params params;
Params paramsMemory{"/dev/shm/params"};
// CLEARPILOT // CLEARPILOT
// bool show_ready;
ReadyWindow *ready; ReadyWindow *ready;
bool was_parked_onroad = false;
private slots: private slots:
void updateState(const UIState &s); void updateState(const UIState &s);

Binary file not shown.

View File

@@ -15,48 +15,24 @@
#include "selfdrive/ui/qt/qt_window.h" #include "selfdrive/ui/qt/qt_window.h"
#include "selfdrive/ui/qt/util.h" #include "selfdrive/ui/qt/util.h"
TrackWidget::TrackWidget(QWidget *parent) : QWidget(parent) { // CLEARPILOT: full-screen boot logo background with progress bar overlay
setAttribute(Qt::WA_OpaquePaintEvent);
setFixedSize(spinner_size);
// pre-compute all the track imgs. make this a gif instead?
QPixmap comma_img = loadPixmap("../assets/img_spinner_comma.png", spinner_size);
QPixmap track_img = loadPixmap("../assets/img_spinner_track.png", spinner_size);
QTransform transform(1, 0, 0, 1, width() / 2, height() / 2);
QPixmap pm(spinner_size);
QPainter p(&pm);
p.setRenderHint(QPainter::SmoothPixmapTransform);
for (int i = 0; i < track_imgs.size(); ++i) {
p.resetTransform();
p.fillRect(0, 0, spinner_size.width(), spinner_size.height(), Qt::black);
p.drawPixmap(0, 0, comma_img);
p.setTransform(transform.rotate(360 / spinner_fps));
p.drawPixmap(-width() / 2, -height() / 2, track_img);
track_imgs[i] = pm.copy();
}
m_anim.setDuration(1000);
m_anim.setStartValue(0);
m_anim.setEndValue(int(track_imgs.size() -1));
m_anim.setLoopCount(-1);
m_anim.start();
connect(&m_anim, SIGNAL(valueChanged(QVariant)), SLOT(update()));
}
void TrackWidget::paintEvent(QPaintEvent *event) {
QPainter painter(this);
painter.drawPixmap(0, 0, track_imgs[m_anim.currentValue().toInt()]);
}
// Spinner
Spinner::Spinner(QWidget *parent) : QWidget(parent) { Spinner::Spinner(QWidget *parent) : QWidget(parent) {
// Load boot logo as full-screen background, rotated 90° CCW
// (bg.jpg is pre-rotated 90° CW for the raw framebuffer)
QPixmap boot_logo("/usr/comma/bg.jpg");
if (!boot_logo.isNull()) {
QTransform rot;
rot.rotate(-90);
bg_img = boot_logo.transformed(rot);
}
QGridLayout *main_layout = new QGridLayout(this); QGridLayout *main_layout = new QGridLayout(this);
main_layout->setSpacing(0); main_layout->setSpacing(0);
main_layout->setMargin(200); main_layout->setMargin(0);
main_layout->addWidget(new TrackWidget(this), 0, 0, Qt::AlignHCenter | Qt::AlignVCenter); // Spacer to push progress bar toward bottom
main_layout->setRowStretch(0, 1);
text = new QLabel(); text = new QLabel();
text->setWordWrap(true); text->setWordWrap(true);
@@ -69,7 +45,10 @@ Spinner::Spinner(QWidget *parent) : QWidget(parent) {
progress_bar->setTextVisible(false); progress_bar->setTextVisible(false);
progress_bar->setVisible(false); progress_bar->setVisible(false);
progress_bar->setFixedHeight(20); progress_bar->setFixedHeight(20);
main_layout->addWidget(progress_bar, 1, 0, Qt::AlignHCenter); main_layout->addWidget(progress_bar, 2, 0, Qt::AlignHCenter | Qt::AlignBottom);
// Bottom margin for progress bar
main_layout->setContentsMargins(0, 0, 0, 80);
setStyleSheet(R"( setStyleSheet(R"(
Spinner { Spinner {
@@ -88,7 +67,7 @@ Spinner::Spinner(QWidget *parent) : QWidget(parent) {
} }
QProgressBar::chunk { QProgressBar::chunk {
border-radius: 10px; border-radius: 10px;
background-color: rgba(23, 134, 68, 255); background-color: white;
} }
)"); )");
@@ -96,6 +75,17 @@ Spinner::Spinner(QWidget *parent) : QWidget(parent) {
QObject::connect(notifier, &QSocketNotifier::activated, this, &Spinner::update); QObject::connect(notifier, &QSocketNotifier::activated, this, &Spinner::update);
} }
void Spinner::paintEvent(QPaintEvent *event) {
QPainter p(this);
p.fillRect(rect(), Qt::black);
if (!bg_img.isNull()) {
QPixmap scaled = bg_img.scaled(size(), Qt::KeepAspectRatio, Qt::SmoothTransformation);
int x = (width() - scaled.width()) / 2;
int y = (height() - scaled.height()) / 2;
p.drawPixmap(x, y, scaled);
}
}
void Spinner::update(int n) { void Spinner::update(int n) {
std::string line; std::string line;
std::getline(std::cin, line); std::getline(std::cin, line);

View File

@@ -1,36 +1,23 @@
#include <array>
#include <QLabel> #include <QLabel>
#include <QPixmap> #include <QPixmap>
#include <QProgressBar> #include <QProgressBar>
#include <QSocketNotifier> #include <QSocketNotifier>
#include <QVariantAnimation>
#include <QWidget> #include <QWidget>
constexpr int spinner_fps = 30;
constexpr QSize spinner_size = QSize(360, 360);
class TrackWidget : public QWidget {
Q_OBJECT
public:
TrackWidget(QWidget *parent = nullptr);
private:
void paintEvent(QPaintEvent *event) override;
std::array<QPixmap, spinner_fps> track_imgs;
QVariantAnimation m_anim;
};
class Spinner : public QWidget { class Spinner : public QWidget {
Q_OBJECT Q_OBJECT
public: public:
explicit Spinner(QWidget *parent = 0); explicit Spinner(QWidget *parent = 0);
protected:
void paintEvent(QPaintEvent *event) override;
private: private:
QLabel *text; QLabel *text;
QProgressBar *progress_bar; QProgressBar *progress_bar;
QSocketNotifier *notifier; QSocketNotifier *notifier;
QPixmap bg_img;
public slots: public slots:
void update(int n); void update(int n);

View File

@@ -1,5 +1,10 @@
#!/bin/sh #!/bin/sh
# CLEARPILOT: prefer freshly built _spinner over prebuilt qt/spinner
if [ -f ./_spinner ]; then
exec ./_spinner "$1"
fi
if [ -f /TICI ] && [ ! -f qt/spinner ]; then if [ -f /TICI ] && [ ! -f qt/spinner ]; then
cp qt/spinner_larch64 qt/spinner cp qt/spinner_larch64 qt/spinner
fi fi

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View File

@@ -3,8 +3,9 @@
# Create a 2160x1080 true color bitmap canvas with black background # Create a 2160x1080 true color bitmap canvas with black background
convert -size 2160x1080 canvas:black /tmp/black_canvas.png convert -size 2160x1080 canvas:black /tmp/black_canvas.png
# Place the image in the center of the canvas, blending the transparent background # Scale logo 140% then center on canvas
composite -gravity center /data/openpilot/selfdrive/clearpilot/theme/clearpilot/images/boot_logo.png /tmp/black_canvas.png /tmp/centered_image.png convert /data/openpilot/selfdrive/clearpilot/theme/clearpilot/images/boot_logo.png -resize 140% /tmp/scaled_logo.png
composite -gravity center /tmp/scaled_logo.png /tmp/black_canvas.png /tmp/centered_image.png
# Rotate the image clockwise 90 degrees # Rotate the image clockwise 90 degrees
convert /tmp/centered_image.png -rotate 90 /tmp/rotated_image.png convert /tmp/centered_image.png -rotate 90 /tmp/rotated_image.png
@@ -13,4 +14,4 @@ convert /tmp/centered_image.png -rotate 90 /tmp/rotated_image.png
convert /tmp/rotated_image.png -quality 95 /data/openpilot/system/clearpilot/startup_logo/bg.jpg convert /tmp/rotated_image.png -quality 95 /data/openpilot/system/clearpilot/startup_logo/bg.jpg
# Clean up temporary files # Clean up temporary files
rm /tmp/black_canvas.png /tmp/centered_image.png /tmp/rotated_image.png rm /tmp/black_canvas.png /tmp/scaled_logo.png /tmp/centered_image.png /tmp/rotated_image.png

View File

@@ -3,6 +3,13 @@
set -x set -x
# CLEARPILOT: regenerate bg.jpg if boot_logo.png is newer (handles logo changes)
BOOT_LOGO="/data/openpilot/selfdrive/clearpilot/theme/clearpilot/images/boot_logo.png"
GENERATED_BG="/data/openpilot/system/clearpilot/startup_logo/bg.jpg"
if [ "$BOOT_LOGO" -nt "$GENERATED_BG" ] 2>/dev/null; then
bash /data/openpilot/system/clearpilot/startup_logo/generate_logo.sh
fi
# Check if md5sum of /usr/comma/bg.jpg is not equal to md5sum of /data/openpilot/system/clearpilot/startup_logo/bg.jpg # Check if md5sum of /usr/comma/bg.jpg is not equal to md5sum of /data/openpilot/system/clearpilot/startup_logo/bg.jpg
if [ "$(md5sum /usr/comma/bg.jpg | awk '{print $1}')" != "$(md5sum /data/openpilot/system/clearpilot/startup_logo/bg.jpg | awk '{print $1}')" ]; then if [ "$(md5sum /usr/comma/bg.jpg | awk '{print $1}')" != "$(md5sum /data/openpilot/system/clearpilot/startup_logo/bg.jpg | awk '{print $1}')" ]; then
bash /data/openpilot/system/clearpilot/startup_logo/generate_logo.sh bash /data/openpilot/system/clearpilot/startup_logo/generate_logo.sh