diff --git a/CLAUDE.md b/CLAUDE.md index d5d0067..310fa21 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -57,17 +57,25 @@ chown -R comma:comma /data/openpilot ### 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 -# 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 -# Remove prebuilt to force recompilation (it is recreated after each successful build) -rm -f /data/openpilot/prebuilt +# 2. Build (kills running manager, removes prebuilt, compiles, exits) +# 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" + +# 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 @@ -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) - `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) ### Architecture diff --git a/build_only.sh b/build_only.sh new file mode 100755 index 0000000..25b0ebb --- /dev/null +++ b/build_only.sh @@ -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 diff --git a/launch_chffrplus.sh b/launch_chffrplus.sh index 52689a8..cd27ef5 100755 --- a/launch_chffrplus.sh +++ b/launch_chffrplus.sh @@ -79,6 +79,9 @@ function launch { agnos_init fi + # CLEARPILOT: kill stale error display from previous build/run + pkill -f "selfdrive/ui/text" 2>/dev/null + # write tmux scrollback to a file tmux capture-pane -pq -S-1000 > /tmp/launch_log diff --git a/launch_openpilot.sh b/launch_openpilot.sh index 875b3aa..4e634fb 100755 --- a/launch_openpilot.sh +++ b/launch_openpilot.sh @@ -12,4 +12,5 @@ sleep 1 bash /data/openpilot/system/clearpilot/on_start.sh +cd /data/openpilot exec ./launch_chffrplus.sh diff --git a/selfdrive/manager/build.py b/selfdrive/manager/build.py index e46f20c..e83def1 100755 --- a/selfdrive/manager/build.py +++ b/selfdrive/manager/build.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 import os +import sys import subprocess from pathlib import Path @@ -54,6 +55,14 @@ def build(spinner: Spinner, dirty: bool = False, minimal: bool = False) -> None: if scons.returncode == 0: 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 if scons.returncode != 0: @@ -69,8 +78,13 @@ def build(spinner: Spinner, dirty: bool = False, minimal: bool = False) -> None: # Show TextWindow spinner.close() 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.close() exit(1) # enforce max cache size diff --git a/selfdrive/manager/manager.py b/selfdrive/manager/manager.py index 8ee32e6..90bffb8 100755 --- a/selfdrive/manager/manager.py +++ b/selfdrive/manager/manager.py @@ -16,7 +16,7 @@ from openpilot.common.text_window import TextWindow from openpilot.common.time import system_time_valid from openpilot.system.hardware import HARDWARE, PC 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.athena.registration import register, UNREGISTERED_DONGLE_ID from openpilot.common.swaglog import cloudlog, add_file_handler @@ -51,7 +51,26 @@ def frogpilot_boot_functions(frogpilot_functions): except Exception as 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: + cleanup_old_logs() + frogpilot_boot = threading.Thread(target=frogpilot_boot_functions, args=(frogpilot_functions,)) frogpilot_boot.start() @@ -367,6 +386,7 @@ def manager_thread(frogpilot_functions) -> None: cloudlog.bind(daemon="manager") cloudlog.info("manager start") cloudlog.info({"environ": os.environ}) + session_log.info("manager starting") params = Params() params_memory = Params("/dev/shm/params") @@ -397,6 +417,7 @@ def manager_thread(frogpilot_functions) -> None: if started and not started_prev: params.clear_all(ParamKeyType.CLEAR_ON_ONROAD_TRANSITION) + session_log.info("onroad transition") if openpilot_crashed: 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: params.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 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) + # 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) for p in managed_processes.values() if p.proc) print(running) @@ -430,6 +455,7 @@ def manager_thread(frogpilot_functions) -> None: shutdown = True params.put("LastManagerExitReason", f"{param} {datetime.datetime.now()}") cloudlog.warning(f"Shutting down manager - {param} set") + session_log.info("manager shutting down: %s", param) if shutdown: break @@ -481,6 +507,7 @@ if __name__ == "__main__": except Exception: add_file_handler(cloudlog) cloudlog.exception("Manager failed to start") + session_log.critical("manager failed to start: %s", traceback.format_exc()) try: managed_processes['ui'].stop() diff --git a/selfdrive/manager/process.py b/selfdrive/manager/process.py index 2d3f0c4..59dc61a 100755 --- a/selfdrive/manager/process.py +++ b/selfdrive/manager/process.py @@ -2,6 +2,7 @@ import importlib import os import signal import struct +import sys import datetime import time import subprocess @@ -17,17 +18,62 @@ import openpilot.selfdrive.sentry as sentry from openpilot.common.basedir import BASEDIR from openpilot.common.params import Params from openpilot.common.swaglog import cloudlog +from openpilot.common.time import system_time_valid WATCHDOG_FN = "/dev/shm/wd_" ENABLE_WATCHDOG = os.getenv("NO_WATCHDOG") is None -timestamp = datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S") -_log_dir = f"/data/log2/{timestamp}" +# CLEARPILOT: time-safe log directory — use temporary name if clock is invalid (1970), +# 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) +# 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: # import the process mod = importlib.import_module(proc) @@ -53,9 +99,16 @@ def launcher(proc: str, name: str) -> None: 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 + # 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 os.chdir(cwd) os.execvp(pargs[0], pargs) @@ -110,6 +163,7 @@ class ManagerProcess(ABC): if dt > self.watchdog_max_dt: 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=})") + session_log.warning("watchdog timeout for %s (exitcode %s), restarting", self.name, self.proc.exitcode) self.restart() else: self.watchdog_seen = True @@ -139,6 +193,10 @@ class ManagerProcess(ABC): ret = self.proc.exitcode 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: self.shutting_down = False @@ -200,7 +258,8 @@ class NativeProcess(ManagerProcess): cwd = os.path.join(BASEDIR, self.cwd) 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.watchdog_seen = False self.shutting_down = False @@ -232,7 +291,8 @@ class PythonProcess(ManagerProcess): global _log_dir log_path = _log_dir+"/"+self.name+".log" 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.watchdog_seen = False self.shutting_down = False @@ -276,6 +336,7 @@ class DaemonProcess(ManagerProcess): pass cloudlog.info(f"starting daemon {self.name}") + session_log.info("starting daemon %s", self.name) proc = subprocess.Popen(['python', '-m', self.module], stdin=open('/dev/null'), stdout=open(log_path, 'a'), diff --git a/selfdrive/ui/qt/home.cc b/selfdrive/ui/qt/home.cc index 81226a9..d1ecc2d 100755 --- a/selfdrive/ui/qt/home.cc +++ b/selfdrive/ui/qt/home.cc @@ -61,19 +61,42 @@ void HomeWindow::showSidebar(bool show) { } void HomeWindow::updateState(const UIState &s) { - // const SubMaster &sm = *(s.sm); if (s.scene.started) { showDriverView(s.scene.driver_camera_timer >= 10, true); + + // CLEARPILOT: show splash screen when onroad but in park + bool parked = s.scene.parked; + 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) { + sidebar->setVisible(false); if (offroad) { - sidebar->setVisible(false); + was_parked_onroad = false; slayout->setCurrentWidget(ready); } else { - sidebar->setVisible(false); - slayout->setCurrentWidget(onroad); + // CLEARPILOT: start onroad in splash — updateState will switch to + // camera view once the car shifts out of park + was_parked_onroad = true; + slayout->setCurrentWidget(ready); } } diff --git a/selfdrive/ui/qt/home.h b/selfdrive/ui/qt/home.h index 6f929eb..673530f 100755 --- a/selfdrive/ui/qt/home.h +++ b/selfdrive/ui/qt/home.h @@ -56,10 +56,11 @@ private: // FrogPilot variables Params params; + Params paramsMemory{"/dev/shm/params"}; // CLEARPILOT - // bool show_ready; ReadyWindow *ready; + bool was_parked_onroad = false; private slots: void updateState(const UIState &s); diff --git a/selfdrive/ui/qt/spinner b/selfdrive/ui/qt/spinner index 645bc44..b635b3c 100755 Binary files a/selfdrive/ui/qt/spinner and b/selfdrive/ui/qt/spinner differ diff --git a/selfdrive/ui/qt/spinner.cc b/selfdrive/ui/qt/spinner.cc index 7ca2f1a..143e920 100755 --- a/selfdrive/ui/qt/spinner.cc +++ b/selfdrive/ui/qt/spinner.cc @@ -15,48 +15,24 @@ #include "selfdrive/ui/qt/qt_window.h" #include "selfdrive/ui/qt/util.h" -TrackWidget::TrackWidget(QWidget *parent) : QWidget(parent) { - 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 +// CLEARPILOT: full-screen boot logo background with progress bar overlay 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); 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->setWordWrap(true); @@ -69,7 +45,10 @@ Spinner::Spinner(QWidget *parent) : QWidget(parent) { progress_bar->setTextVisible(false); progress_bar->setVisible(false); 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"( Spinner { @@ -88,7 +67,7 @@ Spinner::Spinner(QWidget *parent) : QWidget(parent) { } QProgressBar::chunk { 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); } +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) { std::string line; std::getline(std::cin, line); diff --git a/selfdrive/ui/qt/spinner.h b/selfdrive/ui/qt/spinner.h index 43d90a7..b279a17 100755 --- a/selfdrive/ui/qt/spinner.h +++ b/selfdrive/ui/qt/spinner.h @@ -1,36 +1,23 @@ -#include - #include #include #include #include -#include #include -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 track_imgs; - QVariantAnimation m_anim; -}; - class Spinner : public QWidget { Q_OBJECT public: explicit Spinner(QWidget *parent = 0); +protected: + void paintEvent(QPaintEvent *event) override; + private: QLabel *text; QProgressBar *progress_bar; QSocketNotifier *notifier; + QPixmap bg_img; public slots: void update(int n); diff --git a/selfdrive/ui/spinner b/selfdrive/ui/spinner index 35feab3..1e04f7a 100755 --- a/selfdrive/ui/spinner +++ b/selfdrive/ui/spinner @@ -1,5 +1,10 @@ #!/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 cp qt/spinner_larch64 qt/spinner fi diff --git a/system/clearpilot/startup_logo/bg.jpg b/system/clearpilot/startup_logo/bg.jpg index 911630f..55c651f 100755 Binary files a/system/clearpilot/startup_logo/bg.jpg and b/system/clearpilot/startup_logo/bg.jpg differ diff --git a/system/clearpilot/startup_logo/generate_logo.sh b/system/clearpilot/startup_logo/generate_logo.sh index b6d0b17..6a6b742 100755 --- a/system/clearpilot/startup_logo/generate_logo.sh +++ b/system/clearpilot/startup_logo/generate_logo.sh @@ -3,8 +3,9 @@ # Create a 2160x1080 true color bitmap canvas with black background convert -size 2160x1080 canvas:black /tmp/black_canvas.png -# Place the image in the center of the canvas, blending the transparent background -composite -gravity center /data/openpilot/selfdrive/clearpilot/theme/clearpilot/images/boot_logo.png /tmp/black_canvas.png /tmp/centered_image.png +# Scale logo 140% then center on canvas +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 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 # 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 diff --git a/system/clearpilot/startup_logo/set_logo.sh b/system/clearpilot/startup_logo/set_logo.sh index 4b1533a..0b19f51 100755 --- a/system/clearpilot/startup_logo/set_logo.sh +++ b/system/clearpilot/startup_logo/set_logo.sh @@ -3,6 +3,13 @@ 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 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