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:
50
CLAUDE.md
50
CLAUDE.md
@@ -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
29
build_only.sh
Executable 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
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
|||||||
@@ -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) {
|
||||||
if (offroad) {
|
|
||||||
sidebar->setVisible(false);
|
sidebar->setVisible(false);
|
||||||
|
if (offroad) {
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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.
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 |
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user