Files
StarPilot/starpilot/system/the_galaxy/utilities.py
T
firestar5683 9910651a9d Carl The Witch
2026-06-20 14:38:56 -05:00

2738 lines
96 KiB
Python

#!/usr/bin/env python3
import base64
import copy
import hashlib
import json
import os
import re
import secrets
import shutil
import subprocess
import sys
import threading
import time
from datetime import datetime, timedelta
from pathlib import Path
from typing import List
from urllib.parse import quote
from openpilot.common.constants import CV
from openpilot.common.params import Params
from openpilot.system.loggerd.config import get_available_bytes, get_used_bytes
from openpilot.system.loggerd.deleter import PRESERVE_ATTR_NAME, PRESERVE_ATTR_VALUE
from openpilot.system.loggerd.uploader import listdir_by_creation
from openpilot.tools.lib.route import SegmentName
from openpilot.starpilot.assets.model_manager import canonical_model_key
from openpilot.starpilot.common.starpilot_variables import THEME_SAVE_PATH, VIDEO_CACHE_PATH
from openpilot.starpilot.assets.theme_manager import HOLIDAY_THEME_PATH
LOG_CANDIDATES = [
"qlog",
"qlog.zst",
"qlog.bz2",
"rlog",
"rlog.zst",
"rlog.bz2",
"raw_log.zst",
"raw_log.bz2",
]
ROUTE_TIME_LOG_CANDIDATES = [
"qlog.zst",
"qlog.bz2",
"qlog",
"rlog.zst",
"rlog.bz2",
"rlog",
"raw_log.zst",
"raw_log.bz2",
]
SEGMENT_RE = re.compile(r"^[0-9a-fA-F]{8}--[0-9a-fA-F]{10}--\d+$")
ROUTE_RE = re.compile(r"^[0-9a-fA-F]{8}--[0-9a-fA-F]{10}$")
TARGET_LOUDNESS = -15.0
METER_TO_MILE = 1.0 / 1609.344
METER_TO_KILOMETER = 0.001
MILE_TO_KILOMETER = CV.MPH_TO_KPH
METER_PER_SECOND_TO_MPH = CV.MS_TO_KPH * CV.KPH_TO_MPH
DASHBOARD_CACHE_TTL_SECONDS = 5.0
DASHBOARD_ROUTE_SCAN_LIMIT = 96
DASHBOARD_ROUTE_ANALYSIS_LIMIT = 0
DASHBOARD_ANALYSIS_TIME_BUDGET_SECONDS = 0.0
DASHBOARD_BACKGROUND_ROUTE_ANALYSIS_LIMIT = 5
DASHBOARD_RECENT_DRIVE_LIMIT = 5
DASHBOARD_ROUTE_SEGMENT_SAMPLE_LIMIT = 2
DASHBOARD_PERSISTED_ROUTE_LIMIT = 5000
DASHBOARD_PERSIST_MIN_ROUTE_AGE_SECONDS = 120
DASHBOARD_PERSISTENT_STATS_PARAM = "GalaxyDashboardStats"
DASHBOARD_ROUTE_ANALYSIS_VERSION = 3
DASHBOARD_PARAMS_DIR = Path("/data/params/d")
DASHBOARD_ANALYZER_LOG_PATH = "/tmp/galaxy_dashboard_analyzer.log"
DASHBOARD_ANALYZER_STATUS_PATH = Path("/tmp/galaxy_dashboard_analyzer_status.json")
DASHBOARD_ANALYZER_STATUS_MAX_AGE_SECONDS = 30 * 60
DASHBOARD_TOP_MODEL_LIMIT = 3
DASHBOARD_EVENT_DISTRACTED = "promptDriverDistracted"
DASHBOARD_EVENT_UNRESPONSIVE = "driverUnresponsive"
DASHBOARD_TIME_SOURCE_LOG = "log"
DASHBOARD_TIME_SOURCE_FILESYSTEM = "filesystem"
DASHBOARD_MIN_VALID_ROUTE_TIME = datetime(2026, 1, 1)
DASHBOARD_ROUTE_FUTURE_GRACE_SECONDS = 6 * 60 * 60
DASHBOARD_LOCAL_ROUTE_MAX_AGE_SECONDS = 45 * 24 * 60 * 60
XOR_KEY = "s8#pL3*Xj!aZ@dWq"
MAX_FILE_SIZE = 5 * 1024 * 1024
_FILENAME_SANITIZE_RE = re.compile(r"[^A-Za-z0-9_.-]+")
_GALAXY_DEPS_PATH = "/data/galaxy_deps"
_LEGACY_GALAXY_DEPS_PATH = "/data/" + "".join(chr(code) for code in (112, 111, 110, 100)) + "_deps"
for deps_path in (_GALAXY_DEPS_PATH, _LEGACY_GALAXY_DEPS_PATH):
if os.path.isdir(deps_path) and deps_path not in sys.path:
sys.path.insert(0, deps_path)
_REPO_THIRD_PARTY_PATH = Path(__file__).resolve().parents[2] / "third_party"
if _REPO_THIRD_PARTY_PATH.is_dir() and str(_REPO_THIRD_PARTY_PATH) not in sys.path:
sys.path.insert(0, str(_REPO_THIRD_PARTY_PATH))
_PIL_IMAGE = None
_PYDUB_AUDIOSEGMENT = None
_DASHBOARD_CACHE = {
"key": None,
"updated_at": 0.0,
"value": None,
}
_DASHBOARD_ANALYZER_LOCK = threading.Lock()
_DASHBOARD_ANALYZER_PROCESS = None
params = Params(return_defaults=True)
def secure_filename(filename):
safe = os.path.basename(str(filename or ""))
safe = safe.replace(" ", "_").strip()
safe = _FILENAME_SANITIZE_RE.sub("_", safe)
safe = safe.strip("._")
return safe or "file"
def _decode_json_param(value, default):
if value is None:
return default
if isinstance(value, (dict, list)):
return value
if isinstance(value, bytes):
value = value.decode("utf-8", errors="replace")
if isinstance(value, str):
text = value.strip()
if not text:
return default
try:
parsed = json.loads(text)
except json.JSONDecodeError:
return default
if isinstance(default, dict) and isinstance(parsed, dict):
return parsed
if isinstance(default, list) and isinstance(parsed, list):
return parsed
return default
return default
def _get_pillow_image():
global _PIL_IMAGE
if _PIL_IMAGE is None:
from PIL import Image as pil_image
_PIL_IMAGE = pil_image
return _PIL_IMAGE
def _get_pydub_audio_segment():
global _PYDUB_AUDIOSEGMENT
if _PYDUB_AUDIOSEGMENT is None:
from pydub import AudioSegment as pydub_audio_segment
_PYDUB_AUDIOSEGMENT = pydub_audio_segment
return _PYDUB_AUDIOSEGMENT
def check_theme_components(theme_path):
components = {
"hasColors": False,
"hasIcons": False,
"hasSounds": False,
"hasTurnSignals": False,
"hasDistanceIcons": False,
"hasSteeringWheel": False
}
colors_path = theme_path / "colors" / "colors.json"
if colors_path.exists():
components["hasColors"] = True
icons_path = theme_path / "icons"
if icons_path.exists() and any(icons_path.iterdir()):
components["hasIcons"] = True
sounds_path = theme_path / "sounds"
if sounds_path.exists() and any(sounds_path.iterdir()):
components["hasSounds"] = True
signals_path = theme_path / "signals"
if signals_path.exists() and any(signals_path.iterdir()):
components["hasTurnSignals"] = True
distance_icons_path = theme_path / "distance_icons"
if distance_icons_path.exists() and any(distance_icons_path.iterdir()):
components["hasDistanceIcons"] = True
is_holiday_theme = str(HOLIDAY_THEME_PATH) in str(theme_path)
if is_holiday_theme:
wheel_path = theme_path / "steering_wheel"
if wheel_path.exists() and any(f.name.startswith("wheel.") for f in wheel_path.iterdir()):
components["hasSteeringWheel"] = True
else:
wheel_path = THEME_SAVE_PATH / "steering_wheels"
if wheel_path.exists():
theme_name = theme_path.name.replace('-user_created', '')
if any(wheel_path.glob(f"{theme_name}-user_created.*")):
components["hasSteeringWheel"] = True
return components
def covert_audio(input_file):
sound = _get_pydub_audio_segment().from_file(input_file)
sound = sound.set_frame_rate(48000)
sound = sound.set_channels(1)
output_filename = os.path.splitext(input_file)[0] + ".wav"
sound.export(output_filename, format="wav", parameters=["-acodec", "pcm_s16le"])
if input_file != output_filename:
os.remove(input_file)
def create_theme(form_data, files, temporary=False):
theme_name = form_data.get("themeName")
if not theme_name:
return None, "Theme name is required."
sane_theme_name = secure_filename(theme_name.replace(" ", "_"))
save_checklist_str = form_data.get("saveChecklist", "{}")
save_checklist = json.loads(save_checklist_str)
needs_theme_pack = any([
save_checklist.get("colors"),
save_checklist.get("icons"),
save_checklist.get("sounds"),
save_checklist.get("turn_signals"),
save_checklist.get("distance_icons"),
])
if temporary:
base_path = Path(f"/tmp/{sane_theme_name}_{secrets.token_hex(8)}")
else:
base_path = THEME_SAVE_PATH / "theme_packs" if needs_theme_pack else None
theme_path = (base_path / f"{sane_theme_name}-user_created") if base_path else None
if theme_path:
theme_path.mkdir(parents=True, exist_ok=True)
if save_checklist.get("colors"):
(theme_path / "colors").mkdir(exist_ok=True)
colors_str = form_data.get("colors")
if colors_str:
color_data = json.loads(colors_str)
for key, values in color_data.items():
if "alpha" in values:
values["alpha"] = values.pop("alpha")
colors_file = theme_path / "colors" / "colors.json"
with open(colors_file, "w") as f:
json.dump(color_data, f, indent=2)
if save_checklist.get("turn_signals"):
signals_path = theme_path / "signals"
signals_path.mkdir(exist_ok=True)
if turn_signal_length := form_data.get("turnSignalLength"):
style = form_data.get("turnSignalStyle", "Traditional").lower()
(signals_path / f"{style}_{turn_signal_length}").touch()
turn_signal_type = form_data.get("turnSignalType", "Single Image").lower()
if turn_signal_type == "single image":
for f in signals_path.glob("turn_signal.*"):
f.unlink()
for f in signals_path.glob("turn_signal_blindspot.*"):
f.unlink()
file = files.get("turnSignal")
if file and file.filename:
if file.content_length > MAX_FILE_SIZE:
return None, f"File {file.filename} exceeds 1MB limit."
ext = Path(file.filename).suffix
file.save(signals_path / f"turn_signal{ext}")
file = files.get("turnSignalBlindspot")
if file and file.filename:
if file.content_length > MAX_FILE_SIZE:
return None, f"File {file.filename} exceeds 1MB limit."
ext = Path(file.filename).suffix
file.save(signals_path / f"turn_signal_blindspot{ext}")
elif turn_signal_type == "sequential":
for f in signals_path.glob("turn_signal_*"):
f.unlink()
signal_map = {
"turnSignal": "turn_signal",
"turnSignalBlindspot": "turn_signal_blindspot",
}
for field, base_name in signal_map.items():
file = files.get(field)
if file and file.filename:
if file.content_length > MAX_FILE_SIZE:
return None, f"File {file.filename} exceeds 1MB limit."
for f in signals_path.glob(f"{base_name}.*"):
f.unlink()
ext = Path(file.filename).suffix.lower()
file.save(signals_path / f"{base_name}{ext}")
for f in signals_path.glob("turn_signal.*"):
f.unlink()
for f in signals_path.glob("turn_signal_blindspot.*"):
f.unlink()
sequential_keys = sorted(
[k for k in files if k.startswith("turn_signal_")],
key=lambda name: int(name.split("_")[-1])
)
for key in sequential_keys:
file = files.get(key)
if file and file.filename:
if file.content_length > MAX_FILE_SIZE:
return None, f"File {file.filename} exceeds 1MB limit."
idx = key.split("_")[-1]
ext = Path(file.filename).suffix
file.save(signals_path / f"turn_signal_{idx}{ext}")
if save_checklist.get("icons"):
(theme_path / "icons").mkdir(exist_ok=True)
icon_map = {
"settingsButton": (theme_path / "icons", "button_settings", (169, 104)),
"homeButton": (theme_path / "icons", "button_home", (250, 250)),
}
for field, (dest_path, base_name, resize_dims) in icon_map.items():
file = files.get(field)
if file and file.filename:
if file.content_length > MAX_FILE_SIZE:
return None, f"File {file.filename} exceeds 1MB limit."
for f in dest_path.glob(f"{base_name}.*"):
f.unlink()
ext = Path(file.filename).suffix.lower()
save_path = dest_path / f"{base_name}{ext}"
file.save(save_path)
if resize_dims:
if ext == ".gif":
width, height = resize_dims
palette_path = save_path.with_suffix(".palette.png")
temp_output_path = save_path.with_suffix(".resized.gif")
subprocess.run(["ffmpeg", "-i", str(save_path), "-vf", "palettegen", "-y", str(palette_path)], check=True)
subprocess.run(["ffmpeg", "-i", str(save_path), "-i", str(palette_path), "-lavfi", f"fps=20,scale={width}:{height}:flags=lanczos[x];[x][1:v]paletteuse", "-y", str(temp_output_path)], check=True)
palette_path.unlink()
temp_output_path.rename(save_path)
else:
pil_image = _get_pillow_image()
img = pil_image.open(save_path).resize(resize_dims, pil_image.Resampling.LANCZOS)
if ext != ".png":
save_path.unlink()
save_path = save_path.with_suffix(".png")
img.save(save_path, "PNG")
if save_checklist.get("steering_wheel"):
wheels_dir = THEME_SAVE_PATH / "steering_wheels"
wheels_dir.mkdir(parents=True, exist_ok=True)
file = files.get("steeringWheel")
saved_wheel_path = None
if file and file.filename:
if file.content_length > MAX_FILE_SIZE:
return None, f"File {file.filename} exceeds 1MB limit."
for f in wheels_dir.glob(f"{sane_theme_name}-user_created.*"):
f.unlink()
ext = Path(file.filename).suffix.lower()
saved_wheel_path = wheels_dir / f"{sane_theme_name}-user_created{ext}"
file.save(saved_wheel_path)
if ext == ".gif":
width, height = (250, 250)
palette_path = saved_wheel_path.with_suffix(".palette.png")
temp_output_path = saved_wheel_path.with_suffix(".resized.gif")
subprocess.run(["ffmpeg", "-i", str(saved_wheel_path), "-vf", "palettegen", "-y", str(palette_path)], check=True)
subprocess.run(["ffmpeg", "-i", str(saved_wheel_path), "-i", str(palette_path), "-lavfi", f"fps=20,scale={width}:{height}:flags=lanczos[x];[x][1:v]paletteuse", "-y", str(temp_output_path)], check=True)
palette_path.unlink()
temp_output_path.rename(saved_wheel_path)
else:
pil_image = _get_pillow_image()
img = pil_image.open(saved_wheel_path).resize((250, 250), pil_image.Resampling.LANCZOS)
if ext != ".png":
saved_wheel_path.unlink()
saved_wheel_path = saved_wheel_path.with_suffix(".png")
img.save(saved_wheel_path, "PNG")
if temporary and (theme_path is not None):
existing = saved_wheel_path if saved_wheel_path is not None else next(wheels_dir.glob(f"{sane_theme_name}-user_created.*"), None)
if existing:
wheel_icon_dir = theme_path / "WheelIcon"
wheel_icon_dir.mkdir(parents=True, exist_ok=True)
dest = wheel_icon_dir / f"wheel{existing.suffix.lower()}"
if dest.exists():
dest.unlink()
dest.symlink_to(existing)
if save_checklist.get("distance_icons"):
dist_path = theme_path / "distance_icons"
dist_path.mkdir(exist_ok=True)
for name in ["traffic", "aggressive", "standard", "relaxed"]:
file = files.get(f"distanceIcons_{name}")
if file and file.filename:
if file.content_length > MAX_FILE_SIZE:
return None, f"File {file.filename} exceeds 1MB limit."
for f in dist_path.glob(f"{name}.*"):
f.unlink()
ext = Path(file.filename).suffix.lower()
save_path = dist_path / f"{name}{ext}"
file.save(save_path)
if ext == ".gif":
width, height = (250, 250)
palette_path = save_path.with_suffix(".palette.png")
temp_output_path = save_path.with_suffix(".resized.gif")
subprocess.run(["ffmpeg", "-i", str(save_path), "-vf", "palettegen", "-y", str(palette_path)], check=True)
subprocess.run(["ffmpeg", "-i", str(save_path), "-i", str(palette_path), "-lavfi", f"fps=20,scale={width}:{height}:flags=lanczos[x];[x][1:v]paletteuse", "-y", str(temp_output_path)], check=True)
palette_path.unlink()
temp_output_path.rename(save_path)
else:
pil_image = _get_pillow_image()
img = pil_image.open(save_path).resize((250, 250), pil_image.Resampling.LANCZOS)
if ext != ".png":
save_path.unlink()
save_path = save_path.with_suffix(".png")
img.save(save_path, "PNG")
if save_checklist.get("sounds"):
sounds_path = theme_path / "sounds"
sounds_path.mkdir(exist_ok=True)
for name in ["engage", "disengage", "prompt", "startup"]:
file = files.get(name)
if file and file.filename:
if file.content_length > MAX_FILE_SIZE:
return None, f"File {file.filename} exceeds 1MB limit."
save_path = sounds_path / f"{name}{Path(file.filename).suffix}"
file.save(save_path)
covert_audio(str(save_path))
return theme_path, None
def decode_parameters(encoded_string):
obfuscated_data = base64.b64decode(encoded_string.encode("utf-8")).decode("utf-8")
decrypted_data = xor_encrypt_decrypt(obfuscated_data, XOR_KEY)
return json.loads(decrypted_data)
def encode_parameters(params_dict):
serialized_data = json.dumps(params_dict)
obfuscated_data = xor_encrypt_decrypt(serialized_data, XOR_KEY)
encoded_data = base64.b64encode(obfuscated_data.encode("utf-8")).decode("utf-8")
return encoded_data
def ffmpeg_concat_segments_to_mp4(input_files, cache_key=None):
if not input_files:
raise ValueError("No input files provided for concatenation")
VIDEO_CACHE_PATH.mkdir(exist_ok=True)
key_str = "|".join(str(p) for p in input_files)
if cache_key:
key_str = f"{cache_key}|{key_str}"
file_hash = hashlib.md5(key_str.encode()).hexdigest()
cache_path = VIDEO_CACHE_PATH / f"{file_hash}.mp4"
if cache_path.exists() and all(cache_path.stat().st_mtime > Path(f).stat().st_mtime for f in input_files):
return open(cache_path, "rb")
list_file = VIDEO_CACHE_PATH / f"{file_hash}.txt"
with open(list_file, "w") as f:
for seg in input_files:
f.write(f"file '{Path(seg)}'\n")
try:
subprocess.run(
["ffmpeg", "-hide_banner", "-loglevel", "error", "-f", "concat", "-safe", "0",
"-i", str(list_file), "-c", "copy", "-movflags", "faststart", "-y", str(cache_path)],
check=True
)
except subprocess.CalledProcessError:
try:
subprocess.run(
["ffmpeg", "-hide_banner", "-loglevel", "error", "-f", "concat", "-safe", "0",
"-i", str(list_file), "-c:v", "libx264", "-movflags", "faststart", "-y", str(cache_path)],
check=True
)
except subprocess.CalledProcessError:
if cache_path.exists():
cache_path.unlink()
raise ValueError(f"Cannot process concatenated video segments: {input_files}")
finally:
if list_file.exists():
list_file.unlink()
return open(cache_path, "rb")
def ffmpeg_mp4_wrap_process_builder(filename):
input_path = Path(filename)
if not input_path.exists():
raise FileNotFoundError(f"Input file does not exist: {input_path}")
if input_path.stat().st_size == 0:
raise ValueError(f"Input file is empty: {input_path}")
lock_file = input_path.parent / "rlog.lock"
if lock_file.exists():
raise ValueError(f"File is still being recorded: {input_path}")
VIDEO_CACHE_PATH.mkdir(exist_ok=True)
total, used, free = shutil.disk_usage(VIDEO_CACHE_PATH)
if free < 500 * 1024 * 1024:
for cache_file in VIDEO_CACHE_PATH.glob("*.mp4"):
try:
cache_file.unlink()
except:
pass
file_hash = hashlib.md5(str(input_path).encode()).hexdigest()
cache_path = VIDEO_CACHE_PATH / f"{file_hash}.mp4"
if cache_path.exists() and cache_path.stat().st_mtime > input_path.stat().st_mtime:
return open(cache_path, "rb")
try:
subprocess.run(["ffmpeg", "-hide_banner", "-loglevel", "error", "-i", str(input_path), "-c", "copy", "-movflags", "faststart", "-y", str(cache_path)], check=True)
except subprocess.CalledProcessError:
try:
subprocess.run(["ffmpeg", "-hide_banner", "-loglevel", "error", "-i", str(input_path), "-c:v", "libx264", "-movflags", "faststart", "-y", str(cache_path)], check=True)
except subprocess.CalledProcessError:
if cache_path.exists():
cache_path.unlink()
raise ValueError(f"Cannot process video file: {input_path}")
return open(cache_path, "rb")
def format_git_date(raw_date: str):
date_object = datetime.strptime(raw_date.split()[1], "%Y-%m-%d")
day = date_object.day
suffix = "th" if 11 <= day <= 13 else {1: "st", 2: "nd", 3: "rd"}.get(day % 10, "th")
return date_object.strftime(f"%B {day}{suffix}, %Y")
def get_all_segment_names(footage_path):
entries = listdir_by_creation(footage_path)
segment_names = []
for entry in entries:
if not SEGMENT_RE.fullmatch(entry):
continue
segment_names.append(segment_to_segment_name(footage_path, entry))
return segment_names
def get_available_cameras(segment_path):
segment_path = Path(segment_path)
return [
name for name, file in {
"driver": "dcamera.hevc",
"forward": "fcamera.hevc",
"wide": "ecamera.hevc"
}.items() if (segment_path / file).exists()
]
def get_disk_usage():
free = get_available_bytes(default=0)
used = get_used_bytes(default=0)
total = used + free
def to_gb(b):
return f"{b // (2**30)} GB"
return [{
"free": to_gb(free),
"size": to_gb(total),
"used": to_gb(used),
"usedPercentage": f"{(used / total) * 100:.2f}%" if total > 0 else "0.00%"
}]
def get_drive_stats():
stats = _decode_json_param(params.get("ApiCache_DriveStats"), {})
starpilot_stats = _decode_json_param(params.get("StarPilotStats"), {})
is_metric = params.get_bool("IsMetric")
unit = "kilometers" if is_metric else "miles"
def numeric(value, default=0.0):
try:
parsed = float(value)
except (TypeError, ValueError):
return default
return parsed if parsed == parsed else default
def process(timeframe):
data = stats.get(timeframe, {})
distance_miles = numeric(data.get("distance", 0))
return {
"distance": distance_miles * (MILE_TO_KILOMETER if is_metric else 1),
"drives": numeric(data.get("routes", 0)),
"hours": numeric(data.get("minutes", 0)) / 60,
"unit": unit
}
stats["all"] = process("all")
stats["week"] = process("week")
stats["starpilot"] = {
"distance": numeric(starpilot_stats.get("StarPilotMeters", 0)) * (METER_TO_KILOMETER if is_metric else METER_TO_MILE),
"hours": numeric(starpilot_stats.get("StarPilotSeconds", 0)) / (60 * 60),
"drives": numeric(starpilot_stats.get("StarPilotDrives", 0)),
"unit": unit
}
return stats
def _params_get_value(params_obj, key, default=None):
if params_obj is None:
return default
try:
value = params_obj.get(key, encoding="utf-8")
except TypeError:
try:
value = params_obj.get(key)
except Exception:
return default
except Exception:
return default
if value is None:
return default
return value
def _params_get_text(params_obj, key, default=""):
value = _params_get_value(params_obj, key, default)
if value is None:
return default
if isinstance(value, bytes):
return value.decode("utf-8", errors="replace")
if isinstance(value, (dict, list)):
return json.dumps(value, separators=(",", ":"))
return str(value)
def _params_get_bool(params_obj, key):
if params_obj is None:
return False
try:
return bool(params_obj.get_bool(key))
except Exception:
value = _params_get_text(params_obj, key, "")
return value.strip().lower() in ("1", "true", "yes", "on")
def _dashboard_param_file_path(key):
if key != DASHBOARD_PERSISTENT_STATS_PARAM:
return None
return DASHBOARD_PARAMS_DIR / key
def _read_dashboard_param_file(key):
path = _dashboard_param_file_path(key)
if path is None or not path.is_file():
return None
try:
return path.read_text(encoding="utf-8")
except Exception:
return None
def _write_dashboard_param_file(key, value):
path = _dashboard_param_file_path(key)
if path is None:
return False
try:
path.parent.mkdir(parents=True, exist_ok=True)
tmp_path = path.with_name(f".{path.name}.{os.getpid()}.tmp")
tmp_path.write_text(str(value), encoding="utf-8")
os.replace(tmp_path, path)
return True
except Exception:
return False
def _params_put_text(params_obj, key, value):
if params_obj is None:
return _write_dashboard_param_file(key, value)
try:
params_obj.put(key, value)
return True
except Exception:
return _write_dashboard_param_file(key, value)
def _split_csv(value):
return [entry.strip() for entry in str(value or "").split(",") if entry.strip()]
def _safe_int(value, default=0):
try:
return int(float(value))
except (TypeError, ValueError):
return default
def _safe_float(value, default=0.0):
try:
parsed = float(value)
except (TypeError, ValueError):
return default
return parsed if parsed == parsed else default
def _clean_model_label(value):
clean = re.sub(r"[\U0001f5fa\ufe0f\U0001f440\U0001f4e1]", "", str(value or ""))
return clean.replace("(Default)", "").strip()
def _jsonable_time(value):
if isinstance(value, datetime):
return value.isoformat()
return ""
def _coerce_dashboard_time(value):
if isinstance(value, datetime):
return value
if isinstance(value, str):
try:
return datetime.fromisoformat(value)
except ValueError:
return None
return None
def _dashboard_time_is_valid(value, now=None, require_recent=False):
parsed = _coerce_dashboard_time(value)
if parsed is None:
return False
if parsed < DASHBOARD_MIN_VALID_ROUTE_TIME:
return False
now = now or datetime.now()
if now < DASHBOARD_MIN_VALID_ROUTE_TIME:
return True
if parsed > now + timedelta(seconds=DASHBOARD_ROUTE_FUTURE_GRACE_SECONDS):
return False
if require_recent and parsed < now - timedelta(seconds=DASHBOARD_LOCAL_ROUTE_MAX_AGE_SECONDS):
return False
return True
def _timestamp_to_dashboard_time(timestamp, require_recent=False):
timestamp = _safe_float(timestamp, 0.0)
if timestamp <= 0.0:
return None
try:
parsed = datetime.fromtimestamp(timestamp)
except (OSError, OverflowError, ValueError):
return None
return parsed if _dashboard_time_is_valid(parsed, require_recent=require_recent) else None
def _parse_segment_dir_name(name):
if not SEGMENT_RE.fullmatch(name):
return None
route_name, segment_num_text = name.rsplit("--", 1)
try:
segment_num = int(segment_num_text)
except ValueError:
return None
return route_name, segment_num
def _segment_mtime(segment_path):
try:
return Path(segment_path).stat().st_mtime
except OSError:
return 0.0
def _segment_has_dashboard_log(segment_path):
path = Path(segment_path)
if not path.is_dir():
return False
return any((path / candidate).is_file() for candidate in ROUTE_TIME_LOG_CANDIDATES)
def _select_dashboard_segment_candidate(candidates):
if not candidates:
return None
return next((candidate for candidate in candidates if _segment_has_dashboard_log(candidate)), candidates[0])
def _list_dashboard_routes(footage_paths, limit=DASHBOARD_ROUTE_SCAN_LIMIT):
routes = {}
for footage_path in footage_paths or []:
root = Path(footage_path)
if not root.is_dir():
continue
try:
entries = list(root.iterdir())
except OSError:
continue
for entry in entries:
if not entry.is_dir():
continue
parsed = _parse_segment_dir_name(entry.name)
if parsed is None:
continue
route_name, segment_num = parsed
route = routes.setdefault(route_name, {
"name": route_name,
"segments_by_num": {},
"modified_at": 0.0,
"started_at": None,
})
route["segments_by_num"].setdefault(segment_num, []).append(entry)
route["modified_at"] = max(route["modified_at"], _segment_mtime(entry))
route_infos = []
for route in routes.values():
segments = []
for segment_num, candidates in sorted(route["segments_by_num"].items()):
selected = _select_dashboard_segment_candidate(candidates)
if selected is not None:
segments.append({"num": segment_num, "path": selected})
if not segments:
continue
first_segment = next((segment for segment in segments if segment["num"] == 0), segments[0])
try:
started_at = _timestamp_to_dashboard_time(first_segment["path"].stat().st_mtime, require_recent=True)
except OSError:
started_at = None
route_infos.append({
"name": route["name"],
"segments": segments,
"segmentCount": len(segments),
"startedAt": started_at,
"modifiedAt": route["modified_at"],
"timeSource": DASHBOARD_TIME_SOURCE_FILESYSTEM if started_at is not None else "",
})
route_infos.sort(key=lambda route: (
route["startedAt"].timestamp() if isinstance(route["startedAt"], datetime) else 0.0,
route["modifiedAt"],
route["name"],
), reverse=True)
return route_infos[:limit]
def _dashboard_cache_key(route_infos, params_obj):
param_keys = (
"IsMetric",
"Model",
"DrivingModel",
"DrivingModelName",
)
route_sig = tuple(
(route["name"], route["segmentCount"], round(route["modifiedAt"], 3))
for route in route_infos
)
param_sig = tuple(_params_get_text(params_obj, key, "") for key in param_keys)
return route_sig, param_sig
def _model_lookup(params_obj):
model_keys = _split_csv(_params_get_text(params_obj, "AvailableModels", ""))
model_names = _split_csv(_params_get_text(params_obj, "AvailableModelNames", ""))
model_series = _split_csv(_params_get_text(params_obj, "AvailableModelSeries", ""))
lookup = {}
for idx, key in enumerate(model_keys):
canonical_key = canonical_model_key(key)
if not canonical_key:
continue
name = model_names[idx] if idx < len(model_names) else key
series = model_series[idx] if idx < len(model_series) else ""
lookup[canonical_key] = {
"key": canonical_key,
"name": _clean_model_label(name) or canonical_key,
"series": series or "Custom Series",
}
return lookup
def _decode_init_param_value(value):
if value is None:
return ""
if isinstance(value, bytes):
return value.decode("utf-8", errors="replace").strip()
if isinstance(value, bytearray):
return bytes(value).decode("utf-8", errors="replace").strip()
try:
if not isinstance(value, str):
value = bytes(value)
return value.decode("utf-8", errors="replace").strip()
except Exception:
pass
return str(value).strip()
def _init_params_items(init_params):
if init_params is None:
return []
if hasattr(init_params, "items"):
return list(init_params.items())
entries = getattr(init_params, "entries", None)
if entries is not None:
items = []
try:
for entry in entries:
key = getattr(entry, "key", None)
value = getattr(entry, "value", None)
if key is not None:
items.append((key, value))
except Exception:
return []
return items
items = []
try:
for item in init_params:
key = getattr(item, "key", None)
value = getattr(item, "value", None)
if key is not None:
items.append((key, value))
except Exception:
return []
return items
def _route_model_from_init_data(init_data, model_names):
values = {}
for key, value in _init_params_items(getattr(init_data, "params", None)):
values[str(key)] = _decode_init_param_value(value)
display_name = _clean_model_label(values.get("DrivingModelName", ""))
if display_name:
return display_name
for key in ("DrivingModel", "Model"):
model_key = canonical_model_key(values.get(key, ""))
if model_key:
return model_names.get(model_key, {}).get("name", model_key)
return ""
def _event_name_text(value):
return str(value or "").split(".")[-1]
def _message_type(message):
try:
return message.which()
except Exception:
return getattr(message, "type", "")
def _message_payload(message, message_type):
return getattr(message, message_type, None)
def _deadline_reached(deadline):
return deadline is not None and time.monotonic() >= deadline
def _numeric_attr(value, attr):
try:
return getattr(value, attr)
except Exception:
return None
def _wall_time_seconds_from_payload(payload):
if payload is None:
return None
for attr in ("wallTimeNanos", "wallTimeNs", "unixTimestampNanos"):
value = _safe_float(_numeric_attr(payload, attr), 0.0)
if value > 1e12:
return value / 1e9
for attr in ("wallTimeMillis", "unixTimestampMillis"):
value = _safe_float(_numeric_attr(payload, attr), 0.0)
if value > 1e9:
return value / 1000.0
for attr in ("wallTime", "unixTimestamp"):
value = _safe_float(_numeric_attr(payload, attr), 0.0)
if value > 1e8:
return value
return None
def _log_wall_time_range(first_time, last_time, wall_time_offset, duration_seconds):
if first_time is None or wall_time_offset is None:
return None
start_seconds = first_time + wall_time_offset
end_seconds = (last_time + wall_time_offset) if last_time is not None else start_seconds
if end_seconds <= start_seconds and duration_seconds > 0:
end_seconds = start_seconds + duration_seconds
try:
start_time = datetime.fromtimestamp(start_seconds)
end_time = datetime.fromtimestamp(end_seconds)
except (OSError, OverflowError, ValueError):
return None
if not _dashboard_time_is_valid(start_time) or not _dashboard_time_is_valid(end_time):
return None
return _jsonable_time(start_time), _jsonable_time(end_time)
def _sample_route_info(route_info, limit=DASHBOARD_ROUTE_SEGMENT_SAMPLE_LIMIT):
segments = route_info.get("segments", [])
segment_count = len(segments)
if segment_count <= limit:
sampled = dict(route_info)
sampled["analysisSegmentCount"] = segment_count
return sampled
indices = []
if limit <= 1:
indices = [segment_count - 1]
else:
for idx in range(limit):
candidate = round(idx * (segment_count - 1) / (limit - 1))
if candidate not in indices:
indices.append(candidate)
if len(indices) < limit:
for candidate in range(segment_count - 1, -1, -1):
if candidate not in indices:
indices.append(candidate)
if len(indices) >= limit:
break
sampled = dict(route_info)
sampled["segments"] = [segments[idx] for idx in sorted(indices[:limit])]
sampled["analysisSegmentCount"] = len(sampled["segments"])
return sampled
def _analyze_route_messages(messages, route_info, model_names, is_metric, deadline=None):
first_time = None
last_time = None
previous_car_time = None
previous_speed = 0.0
previous_state_time = None
previous_enabled = False
previous_events = set()
distance_m = 0.0
engaged_seconds = 0.0
distracted_moments = 0
unresponsive_moments = 0
model = ""
wall_time_offset = None
for message in messages:
if _deadline_reached(deadline):
break
mono_time = getattr(message, "logMonoTime", None)
seconds = (mono_time / 1e9) if isinstance(mono_time, (int, float)) else None
if seconds is not None:
first_time = seconds if first_time is None else min(first_time, seconds)
last_time = seconds if last_time is None else max(last_time, seconds)
message_type = _message_type(message)
payload = _message_payload(message, message_type)
if seconds is not None and wall_time_offset is None:
wall_seconds = _wall_time_seconds_from_payload(payload)
if wall_seconds is None:
wall_seconds = _wall_time_seconds_from_payload(message)
if wall_seconds is not None:
wall_time_offset = wall_seconds - seconds
if message_type == "initData" and payload is not None and not model:
model = _route_model_from_init_data(payload, model_names)
elif message_type == "carState" and payload is not None and seconds is not None:
if previous_car_time is not None and seconds > previous_car_time:
distance_m += max(previous_speed, 0.0) * min(seconds - previous_car_time, 10.0)
previous_speed = _safe_float(getattr(payload, "vEgo", 0.0), 0.0)
previous_car_time = seconds
elif message_type == "selfdriveState" and payload is not None and seconds is not None:
if previous_state_time is not None and seconds > previous_state_time and previous_enabled:
engaged_seconds += min(seconds - previous_state_time, 10.0)
previous_enabled = bool(getattr(payload, "enabled", False))
previous_state_time = seconds
elif message_type == "onroadEvents":
current_events = {
_event_name_text(getattr(event, "name", ""))
for event in (payload or [])
}
if DASHBOARD_EVENT_DISTRACTED in current_events and DASHBOARD_EVENT_DISTRACTED not in previous_events:
distracted_moments += 1
if DASHBOARD_EVENT_UNRESPONSIVE in current_events and DASHBOARD_EVENT_UNRESPONSIVE not in previous_events:
unresponsive_moments += 1
previous_events = current_events
if previous_state_time is not None and last_time is not None and last_time > previous_state_time and previous_enabled:
engaged_seconds += min(last_time - previous_state_time, 10.0)
segment_count = max(0, int(route_info.get("segmentCount", 0)))
analysis_segment_count = max(0, int(route_info.get("analysisSegmentCount", segment_count)))
scale = (segment_count / analysis_segment_count) if analysis_segment_count > 0 and analysis_segment_count < segment_count else 1.0
fallback_duration = segment_count * 60
log_duration = max(0.0, (last_time - first_time) if first_time is not None and last_time is not None else 0.0)
if first_time is None or last_time is None:
duration_seconds = fallback_duration
elif scale > 1.0:
duration_seconds = max(log_duration, fallback_duration)
else:
duration_seconds = log_duration
if scale > 1.0:
distance_m *= scale
engaged_seconds = min(engaged_seconds * scale, duration_seconds)
engaged_percent = round((engaged_seconds / duration_seconds) * 100) if duration_seconds > 0 else 0
distance = distance_m * (METER_TO_KILOMETER if is_metric else METER_TO_MILE)
avg_speed = (distance_m / duration_seconds) * (CV.MS_TO_KPH if is_metric else METER_PER_SECOND_TO_MPH) if duration_seconds > 0 else 0.0
time_range = _log_wall_time_range(first_time, last_time, wall_time_offset, duration_seconds)
start_date, end_date = time_range if time_range is not None else _route_time_range(route_info, duration_seconds)
time_source = DASHBOARD_TIME_SOURCE_LOG if time_range is not None else (DASHBOARD_TIME_SOURCE_FILESYSTEM if start_date else "")
return {
"name": route_info.get("name", ""),
"routeNames": [route_info.get("name", "")],
"date": start_date,
"endDate": end_date,
"distance": round(distance, 1),
"distanceMeters": round(distance_m, 1),
"duration": int(round(duration_seconds)),
"avgSpeed": int(round(avg_speed)),
"engagedPercent": max(0, min(100, engaged_percent)),
"engagedSeconds": round(engaged_seconds, 1),
"model": model or "Unknown model",
"segmentCount": int(route_info.get("segmentCount", 0)),
"distractedMoments": distracted_moments,
"unresponsiveMoments": unresponsive_moments,
"routeModifiedAt": _safe_float(route_info.get("modifiedAt", 0.0), 0.0),
"timeSource": time_source,
"attentionKnown": True,
"analysisComplete": analysis_segment_count >= segment_count,
"analysisVersion": DASHBOARD_ROUTE_ANALYSIS_VERSION,
}
def _iter_route_log_messages(route_info, deadline=None):
try:
from openpilot.tools.lib.logreader import LogReader
except Exception:
return
for segment in route_info.get("segments", []):
if _deadline_reached(deadline):
return
log_path = get_route_log_path(segment.get("path"))
if log_path is None:
continue
try:
for message in LogReader(str(log_path), sort_by_time=False):
if _deadline_reached(deadline):
return
yield message
except Exception:
continue
def _empty_drive(is_metric):
return {
"name": "",
"routeNames": [],
"ignored": False,
"date": "",
"endDate": "",
"distance": 0,
"duration": 0,
"avgSpeed": 0,
"engagedPercent": 0,
"model": "Unknown model",
"segmentCount": 0,
"distractedMoments": 0,
"unresponsiveMoments": 0,
"attentionKnown": True,
"analysisComplete": False,
"distanceUnit": "kilometers" if is_metric else "miles",
"speedUnit": "kph" if is_metric else "mph",
}
def _public_drive(drive, is_metric):
public = _empty_drive(is_metric)
for key in public:
if key in drive:
public[key] = drive[key]
return public
def _route_time_range(route_info, duration_seconds):
modified_at = _safe_float(route_info.get("modifiedAt", 0.0), 0.0)
duration_seconds = max(0.0, _safe_float(duration_seconds, 0.0))
modified_time = _timestamp_to_dashboard_time(modified_at, require_recent=True)
if modified_time is not None and duration_seconds > 0.0:
end_time = modified_time
start_time = end_time - timedelta(seconds=duration_seconds)
return _jsonable_time(start_time), _jsonable_time(end_time)
started_at = route_info.get("startedAt")
if _dashboard_time_is_valid(started_at, require_recent=True):
return _jsonable_time(started_at), _jsonable_time(modified_time) if modified_time is not None else ""
return "", ""
def _distance_from_meters(distance_m, is_metric):
return distance_m * (METER_TO_KILOMETER if is_metric else METER_TO_MILE)
def _current_model_name(params_obj, model_names):
for key in ("DrivingModelName", "DrivingModel", "Model"):
value = _params_get_text(params_obj, key, "")
model_key = canonical_model_key(value)
if model_key and model_key in model_names:
return model_names[model_key]["name"]
label = _clean_model_label(value)
if label:
return label
return ""
def _route_shell_drive(route_info, params_obj, model_names, is_metric):
segment_count = max(0, _safe_int(route_info.get("segmentCount", 0), 0))
duration_seconds = segment_count * 60
start_date, end_date = _route_time_range(route_info, duration_seconds)
return {
"name": route_info.get("name", ""),
"routeNames": [route_info.get("name", "")],
"ignored": False,
"date": start_date,
"endDate": end_date,
"distance": 0,
"distanceMeters": 0.0,
"duration": duration_seconds,
"avgSpeed": 0,
"engagedPercent": 0,
"engagedSeconds": 0.0,
"model": _current_model_name(params_obj, model_names) or "Unknown model",
"segmentCount": segment_count,
"distractedMoments": 0,
"unresponsiveMoments": 0,
"routeModifiedAt": _safe_float(route_info.get("modifiedAt", 0.0), 0.0),
"timeSource": DASHBOARD_TIME_SOURCE_FILESYSTEM if start_date else "",
"attentionKnown": False,
"analysisComplete": False,
"analysisVersion": 0,
}
def _drive_from_persistent_route(route_name, entry, is_metric):
duration = max(0, _safe_int(entry.get("duration", 0), 0))
distance_m = max(0.0, _safe_float(entry.get("distanceMeters", 0.0), 0.0))
engaged_seconds = max(0.0, _safe_float(entry.get("engagedSeconds", 0.0), 0.0))
engaged_percent = round((engaged_seconds / duration) * 100) if duration > 0 else 0
avg_speed = (distance_m / duration) * (CV.MS_TO_KPH if is_metric else METER_PER_SECOND_TO_MPH) if duration > 0 else 0.0
return {
"name": route_name,
"routeNames": [route_name],
"ignored": False,
"date": entry.get("date", ""),
"endDate": entry.get("endDate", ""),
"distance": round(_distance_from_meters(distance_m, is_metric), 1),
"distanceMeters": round(distance_m, 1),
"duration": duration,
"avgSpeed": int(round(avg_speed)),
"engagedPercent": max(0, min(100, engaged_percent)),
"engagedSeconds": round(engaged_seconds, 1),
"model": _clean_model_label(entry.get("model", "")) or "Unknown model",
"segmentCount": max(0, _safe_int(entry.get("segmentCount", 0), 0)),
"distractedMoments": max(0, _safe_int(entry.get("distractedMoments", 0), 0)),
"unresponsiveMoments": max(0, _safe_int(entry.get("unresponsiveMoments", 0), 0)),
"routeModifiedAt": _safe_float(entry.get("modifiedAt", 0.0), 0.0),
"timeSource": str(entry.get("timeSource", "") or ""),
"attentionKnown": bool(entry.get("attentionKnown", True)),
"analysisComplete": bool(entry.get("analysisComplete", False)),
"analysisVersion": max(0, _safe_int(entry.get("analysisVersion", 0), 0)),
}
def _persistent_drives(stats, is_metric):
routes = stats.get("routes", {}) if isinstance(stats, dict) else {}
if not isinstance(routes, dict):
return []
ignored_routes = set(stats.get("ignoredRoutes", [])) if isinstance(stats.get("ignoredRoutes", []), list) else set()
drives = [
_drive_from_persistent_route(route_name, entry, is_metric)
for route_name, entry in routes.items()
if isinstance(entry, dict) and _dashboard_time_is_valid(entry.get("date", ""))
]
for drive in drives:
drive["ignored"] = drive["name"] in ignored_routes
return drives
def _merge_dashboard_drives(*drive_lists):
merged = {}
for drives in drive_lists:
for drive in drives or []:
route_name = str(drive.get("name", "")).strip()
if not route_name:
continue
existing = merged.get(route_name)
if existing is None:
merged[route_name] = dict(drive)
continue
existing_distance = _safe_float(existing.get("distanceMeters", existing.get("distance", 0.0)), 0.0)
drive_distance = _safe_float(drive.get("distanceMeters", drive.get("distance", 0.0)), 0.0)
existing_attention = bool(existing.get("attentionKnown", False))
drive_attention = bool(drive.get("attentionKnown", False))
if drive_distance > existing_distance or (drive_attention and not existing_attention):
merged[route_name] = dict(drive)
return sorted(merged.values(), key=_drive_sort_time, reverse=True)
def _drive_display_group_key(drive):
start_text = str(drive.get("date", "")).strip()
end_text = str(drive.get("endDate", "")).strip()
model_key = _model_usage_key(drive.get("model", ""))
if not start_text or not end_text or not model_key:
return None
return start_text, end_text, model_key
def _coalesced_drive_group(group, is_metric):
if len(group) == 1:
return dict(group[0])
ordered = sorted(group, key=_drive_sort_time, reverse=True)
primary = dict(ordered[0])
total_distance_m = sum(max(0.0, _safe_float(drive.get("distanceMeters", 0.0), 0.0)) for drive in ordered)
total_duration = sum(max(0, _safe_int(drive.get("duration", 0), 0)) for drive in ordered)
total_engaged = sum(max(0.0, _safe_float(drive.get("engagedSeconds", 0.0), 0.0)) for drive in ordered)
if total_distance_m > 0.0:
primary["distanceMeters"] = round(total_distance_m, 1)
primary["distance"] = round(_distance_from_meters(total_distance_m, is_metric), 1)
else:
primary["distance"] = round(sum(max(0.0, _safe_float(drive.get("distance", 0.0), 0.0)) for drive in ordered), 1)
primary["duration"] = total_duration
primary["engagedSeconds"] = round(total_engaged, 1)
primary["engagedPercent"] = max(0, min(100, round((total_engaged / total_duration) * 100))) if total_duration > 0 else 0
primary["avgSpeed"] = int(round((total_distance_m / total_duration) * (CV.MS_TO_KPH if is_metric else METER_PER_SECOND_TO_MPH))) if total_distance_m > 0.0 and total_duration > 0 else 0
primary["segmentCount"] = sum(max(0, _safe_int(drive.get("segmentCount", 0), 0)) for drive in ordered)
primary["distractedMoments"] = sum(max(0, _safe_int(drive.get("distractedMoments", 0), 0)) for drive in ordered)
primary["unresponsiveMoments"] = sum(max(0, _safe_int(drive.get("unresponsiveMoments", 0), 0)) for drive in ordered)
primary["routeModifiedAt"] = max(_safe_float(drive.get("routeModifiedAt", 0.0), 0.0) for drive in ordered)
primary["attentionKnown"] = any(bool(drive.get("attentionKnown", True)) for drive in ordered)
primary["analysisComplete"] = all(bool(drive.get("analysisComplete", False)) for drive in ordered)
route_names = []
for drive in ordered:
for route_name in drive.get("routeNames", [drive.get("name", "")]):
route_name = str(route_name or "").strip()
if route_name and route_name not in route_names:
route_names.append(route_name)
primary["routeNames"] = route_names
primary["name"] = ",".join(route_names)
primary["ignored"] = bool(route_names) and all(bool(drive.get("ignored", False)) for drive in ordered)
return primary
def _coalesce_display_drives(drives, is_metric):
groups = {}
fallback = []
for drive in drives or []:
group_key = _drive_display_group_key(drive)
if group_key is None:
fallback.append(dict(drive))
continue
groups.setdefault(group_key, []).append(drive)
coalesced = [_coalesced_drive_group(group, is_metric) for group in groups.values()]
coalesced.extend(fallback)
return sorted(coalesced, key=_drive_sort_time, reverse=True)
def _drive_has_stale_analysis(drive):
if not bool(drive.get("attentionKnown", True)) or not bool(drive.get("analysisComplete", False)):
return False
return _safe_int(drive.get("analysisVersion", 0), 0) < DASHBOARD_ROUTE_ANALYSIS_VERSION
def _week_summary_drives(drives, pending_route_names=None):
pending_route_names = pending_route_names or set()
return [
drive for drive in drives or []
if not (_drive_has_stale_analysis(drive) and str(drive.get("name", "")).strip() in pending_route_names)
]
def _persistent_route_needs_time_refresh(entry):
source = str(entry.get("timeSource", "") or "")
version = _safe_int(entry.get("analysisVersion", 0), 0)
if not source:
return version < DASHBOARD_ROUTE_ANALYSIS_VERSION
if source == DASHBOARD_TIME_SOURCE_FILESYSTEM:
return not _dashboard_time_is_valid(entry.get("date", ""), require_recent=True)
if source == DASHBOARD_TIME_SOURCE_LOG:
return not _dashboard_time_is_valid(entry.get("date", ""))
return True
def _analysis_candidates(route_infos, persistent_stats):
routes = persistent_stats.get("routes", {}) if isinstance(persistent_stats, dict) else {}
routes = routes if isinstance(routes, dict) else {}
ignored_routes = set(persistent_stats.get("ignoredRoutes", [])) if isinstance(persistent_stats, dict) else set()
def needs_analysis(route_info):
route_name = route_info.get("name", "")
entry = routes.get(route_name, {})
if not isinstance(entry, dict):
return True
if _safe_float(entry.get("modifiedAt", 0.0), 0.0) < _safe_float(route_info.get("modifiedAt", 0.0), 0.0):
return True
if _persistent_route_needs_time_refresh(entry):
return True
if _safe_int(entry.get("analysisVersion", 0), 0) < DASHBOARD_ROUTE_ANALYSIS_VERSION:
return True
return not bool(entry.get("attentionKnown", True)) or not bool(entry.get("analysisComplete", False))
missing = [
route_info for route_info in route_infos
if route_info.get("name", "") not in ignored_routes and needs_analysis(route_info)
]
return missing
def _mark_ignored_drives(drives, persistent_stats):
ignored_routes = set(persistent_stats.get("ignoredRoutes", [])) if isinstance(persistent_stats, dict) else set()
for drive in drives or []:
route_names = drive.get("routeNames", [drive.get("name", "")])
route_names = [str(route_name or "").strip() for route_name in route_names if str(route_name or "").strip()]
drive["routeNames"] = route_names
drive["ignored"] = bool(route_names) and all(route_name in ignored_routes for route_name in route_names)
return drives
def _invalidate_dashboard_cache():
_DASHBOARD_CACHE.update({
"key": None,
"updated_at": 0.0,
"value": None,
})
def warm_dashboard_stats(footage_paths=None):
params_obj = params
route_infos = _list_dashboard_routes(footage_paths or [])
if not route_infos:
return
is_metric = _params_get_bool(params_obj, "IsMetric")
model_names = _model_lookup(params_obj)
shell_drives = [
_route_shell_drive(route_info, params_obj, model_names, is_metric)
for route_info in route_infos
]
if shell_drives:
_update_dashboard_persistent_stats(params_obj, shell_drives, time.time())
persistent_stats = _load_dashboard_persistent_stats(params_obj)
candidates = _analysis_candidates(route_infos, persistent_stats)[:DASHBOARD_BACKGROUND_ROUTE_ANALYSIS_LIMIT]
for route_info in candidates:
full_route_info = dict(route_info)
full_route_info["analysisSegmentCount"] = max(0, _safe_int(route_info.get("segmentCount", 0), 0))
messages = _iter_route_log_messages(full_route_info)
drive = _analyze_route_messages(messages, full_route_info, model_names, is_metric)
_update_dashboard_persistent_stats(params_obj, [drive], time.time())
def _dashboard_worker_env(repo_root):
env = os.environ.copy()
pythonpath = [
"/usr/local/venv/lib/python3.12/site-packages",
str(repo_root / "starpilot" / "third_party"),
str(repo_root),
]
if env.get("PYTHONPATH"):
pythonpath.append(env["PYTHONPATH"])
env["PYTHONPATH"] = os.pathsep.join(pythonpath)
env.setdefault("OPENBLAS_NUM_THREADS", "1")
env.setdefault("OMP_NUM_THREADS", "1")
env.setdefault("MKL_NUM_THREADS", "1")
env.setdefault("NUMEXPR_NUM_THREADS", "1")
return env
def _dashboard_analyzer_running():
process = _DASHBOARD_ANALYZER_PROCESS
if process is not None and process.poll() is None:
return True
status = _read_dashboard_analyzer_status()
pid = _safe_int(status.get("pid", 0), 0)
started_at = _safe_float(status.get("startedAt", 0.0), 0.0)
if pid <= 0 or started_at <= 0:
return False
if (time.time() - started_at) > DASHBOARD_ANALYZER_STATUS_MAX_AGE_SECONDS:
_clear_dashboard_analyzer_status()
return False
try:
os.kill(pid, 0)
except ProcessLookupError:
_clear_dashboard_analyzer_status()
return False
except PermissionError:
return True
except OSError:
return False
return True
def _read_dashboard_analyzer_status():
try:
status = json.loads(DASHBOARD_ANALYZER_STATUS_PATH.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
return {}
return status if isinstance(status, dict) else {}
def _write_dashboard_analyzer_status(process, pending_count):
status = {
"pid": process.pid,
"startedAt": time.time(),
"pendingRoutes": max(0, _safe_int(pending_count, 0)),
"batchSize": min(DASHBOARD_BACKGROUND_ROUTE_ANALYSIS_LIMIT, max(0, _safe_int(pending_count, 0))),
}
tmp_path = DASHBOARD_ANALYZER_STATUS_PATH.with_suffix(".tmp")
try:
tmp_path.write_text(json.dumps(status, separators=(",", ":")), encoding="utf-8")
tmp_path.replace(DASHBOARD_ANALYZER_STATUS_PATH)
except OSError:
pass
def _clear_dashboard_analyzer_status():
try:
DASHBOARD_ANALYZER_STATUS_PATH.unlink()
except FileNotFoundError:
pass
except OSError:
pass
def _dashboard_analysis_status(candidates):
pending_count = len(candidates or [])
return {
"pendingRoutes": pending_count,
"running": _dashboard_analyzer_running(),
"batchSize": min(DASHBOARD_BACKGROUND_ROUTE_ANALYSIS_LIMIT, pending_count),
}
def _start_dashboard_background_analysis(footage_paths, route_infos, persistent_stats, candidates=None):
global _DASHBOARD_ANALYZER_PROCESS
candidates = candidates if candidates is not None else _analysis_candidates(route_infos, persistent_stats)
if not route_infos or not candidates:
return False
with _DASHBOARD_ANALYZER_LOCK:
if _dashboard_analyzer_running():
return True
repo_root = Path(__file__).resolve().parents[3]
worker_code = (
"import json, sys;"
"from openpilot.starpilot.system.the_galaxy import utilities;"
"utilities.warm_dashboard_stats(json.loads(sys.argv[1]))"
)
command = [
"nice",
"-n",
"19",
sys.executable or "python3",
"-c",
worker_code,
json.dumps([str(path) for path in (footage_paths or [])]),
]
log_file = None
try:
log_file = open(DASHBOARD_ANALYZER_LOG_PATH, "ab")
_DASHBOARD_ANALYZER_PROCESS = subprocess.Popen(
command,
cwd=str(repo_root),
env=_dashboard_worker_env(repo_root),
stdout=log_file,
stderr=log_file,
start_new_session=True,
)
_write_dashboard_analyzer_status(_DASHBOARD_ANALYZER_PROCESS, len(candidates))
except Exception:
_DASHBOARD_ANALYZER_PROCESS = None
finally:
if log_file is not None:
log_file.close()
return _dashboard_analyzer_running()
def _start_of_week(now):
return datetime(now.year, now.month, now.day) - timedelta(days=now.weekday())
def _build_week_summary(drives, now, is_metric):
week_start = _start_of_week(now)
week_end = week_start + timedelta(days=7)
day_buckets = [
{
"date": (week_start + timedelta(days=idx)).date().isoformat(),
"label": (week_start + timedelta(days=idx)).strftime("%a"),
"distance": 0.0,
}
for idx in range(7)
]
total_distance = 0.0
total_duration = 0
total_engaged = 0.0
total_drives = 0
for drive in drives:
try:
drive_date = datetime.fromisoformat(drive.get("date", ""))
except ValueError:
continue
if not (week_start <= drive_date < week_end):
continue
day_index = (drive_date.date() - week_start.date()).days
if 0 <= day_index < len(day_buckets):
day_buckets[day_index]["distance"] += _safe_float(drive.get("distance", 0.0), 0.0)
total_distance += _safe_float(drive.get("distance", 0.0), 0.0)
total_duration += _safe_int(drive.get("duration", 0), 0)
total_engaged += _safe_float(drive.get("engagedSeconds", 0.0), 0.0)
total_drives += 1
for bucket in day_buckets:
bucket["distance"] = round(bucket["distance"], 1)
return {
"distance": round(total_distance, 1),
"duration": total_duration,
"hours": round(total_duration / 3600, 1),
"drives": total_drives,
"engagedPercent": round((total_engaged / total_duration) * 100) if total_duration > 0 else 0,
"dailyDistance": day_buckets,
"distanceUnit": "kilometers" if is_metric else "miles",
}
def _format_record_date(date_text):
try:
parsed = datetime.fromisoformat(date_text)
if datetime.now().year == parsed.year:
return parsed.strftime("%b %-d")
return parsed.strftime("%b %-d, %Y")
except ValueError:
return "Unknown date"
def _format_record_hours(seconds):
hours = max(0.0, _safe_float(seconds, 0.0) / 3600.0)
return f"{hours:.1f} hour" if round(hours, 1) == 1.0 else f"{hours:.1f} hours"
def _drive_is_clean(drive):
return _safe_int(drive.get("unresponsiveMoments", 0), 0) == 0
def _drive_is_undistracted(drive):
return _safe_int(drive.get("distractedMoments", 0), 0) == 0
def _drive_sort_time(drive):
try:
return datetime.fromisoformat(drive.get("date", ""))
except ValueError:
return datetime.min
def _display_attention_records(stats):
records = {}
attention = stats.get("attentionRecords", {}) if isinstance(stats, dict) else {}
longest = attention.get("longestUndistractedDrive", {}) if isinstance(attention, dict) else {}
clean_streak = attention.get("cleanDriveStreak", {}) if isinstance(attention, dict) else {}
longest_duration = _safe_int(longest.get("duration", 0), 0) if isinstance(longest, dict) else 0
if longest_duration > 0:
records["longestUndistractedDrive"] = {
"value": _format_record_hours(longest_duration),
"detail": _format_record_date(longest.get("date", "")),
}
streak_drives = _safe_int(clean_streak.get("drives", 0), 0) if isinstance(clean_streak, dict) else 0
if streak_drives > 0:
start = _format_record_date(clean_streak.get("startDate", ""))
end = _format_record_date(clean_streak.get("endDate", ""))
detail = start if start == end else f"{start} - {end}"
records["cleanDriveStreak"] = {
"value": f"{streak_drives} drive" if streak_drives == 1 else f"{streak_drives} drives",
"detail": detail,
}
return records
def _display_distance_record_value(distance_m, is_metric):
distance = max(0.0, _safe_float(distance_m, 0.0)) * (METER_TO_KILOMETER if is_metric else METER_TO_MILE)
return f"{distance:.1f}"
def _display_personal_records(stats, is_metric):
records = _build_records([], is_metric)
if not isinstance(stats, dict):
return records
raw_records = stats.get("personalRecords", {})
if not isinstance(raw_records, dict) or not raw_records:
raw_records = _merge_personal_records(_build_personal_records_raw(stats.get("routes", {})), {}, stats.get("attentionRecords", {}))
unit = "kilometers" if is_metric else "miles"
longest_drive = raw_records.get("longestDrive", {}) if isinstance(raw_records, dict) else {}
longest_distance_m = _safe_float(longest_drive.get("distanceMeters", 0.0), 0.0) if isinstance(longest_drive, dict) else 0.0
if longest_distance_m > 0.0:
records["longestDrive"] = {
"value": _display_distance_record_value(longest_distance_m, is_metric),
"detail": f"{unit} - {_format_record_date(longest_drive.get('date', ''))}",
}
most_engaged = raw_records.get("mostEngagedDay", {}) if isinstance(raw_records, dict) else {}
most_engaged_percent = _safe_int(most_engaged.get("percent", 0), 0) if isinstance(most_engaged, dict) else 0
if most_engaged_percent > 0:
records["mostEngagedDay"] = {
"value": f"{most_engaged_percent}%",
"detail": _format_record_date(most_engaged.get("date", "")),
}
best_week = raw_records.get("bestWeek", {}) if isinstance(raw_records, dict) else {}
best_week_distance_m = _safe_float(best_week.get("distanceMeters", 0.0), 0.0) if isinstance(best_week, dict) else 0.0
if best_week_distance_m > 0.0:
records["bestWeek"] = {
"value": _display_distance_record_value(best_week_distance_m, is_metric),
"detail": f"{unit} - week of {_format_record_date(best_week.get('weekDate', ''))}",
}
highest_streak = raw_records.get("highestStreak", {}) if isinstance(raw_records, dict) else {}
highest_days = _safe_int(highest_streak.get("days", 0), 0) if isinstance(highest_streak, dict) else 0
if highest_days > 0:
records["highestStreak"] = {
"value": f"{highest_days} day" if highest_days == 1 else f"{highest_days} days",
"detail": "Consecutive drive days",
}
longest_undistracted = raw_records.get("longestUndistractedDrive", {}) if isinstance(raw_records, dict) else {}
longest_undistracted_duration = _safe_int(longest_undistracted.get("duration", 0), 0) if isinstance(longest_undistracted, dict) else 0
if longest_undistracted_duration > 0:
records["longestUndistractedDrive"] = {
"value": _format_record_hours(longest_undistracted_duration),
"detail": _format_record_date(longest_undistracted.get("date", "")),
}
clean_streak = raw_records.get("cleanDriveStreak", {}) if isinstance(raw_records, dict) else {}
clean_drives = _safe_int(clean_streak.get("drives", 0), 0) if isinstance(clean_streak, dict) else 0
if clean_drives > 0:
start = _format_record_date(clean_streak.get("startDate", ""))
end = _format_record_date(clean_streak.get("endDate", ""))
detail = start if start == end else f"{start} - {end}"
records["cleanDriveStreak"] = {
"value": f"{clean_drives} drive" if clean_drives == 1 else f"{clean_drives} drives",
"detail": detail,
}
return records
def _build_records(drives, is_metric):
unit = "kilometers" if is_metric else "miles"
empty = {"value": "0", "detail": unit}
if not drives:
return {
"longestDrive": empty,
"mostEngagedDay": {"value": "0%", "detail": "No drives"},
"bestWeek": empty,
"highestStreak": {"value": "0 days", "detail": "No drives"},
"longestUndistractedDrive": {"value": "0.0 hours", "detail": "No clean drives"},
"cleanDriveStreak": {"value": "0 drives", "detail": "No clean drives"},
}
longest = max(drives, key=lambda drive: _safe_float(drive.get("distance", 0), 0))
days = {}
weeks = {}
for drive in drives:
try:
drive_date = datetime.fromisoformat(drive.get("date", ""))
except ValueError:
continue
day_key = drive_date.date()
week_key = _start_of_week(drive_date).date()
days.setdefault(day_key, {"duration": 0, "engaged": 0.0})
days[day_key]["duration"] += _safe_int(drive.get("duration", 0), 0)
days[day_key]["engaged"] += _safe_float(drive.get("engagedSeconds", 0.0), 0.0)
weeks[week_key] = weeks.get(week_key, 0.0) + _safe_float(drive.get("distance", 0.0), 0.0)
if days:
most_engaged_date, most_engaged_data = max(
days.items(),
key=lambda item: (item[1]["engaged"] / item[1]["duration"]) if item[1]["duration"] > 0 else 0,
)
most_engaged_percent = round((most_engaged_data["engaged"] / most_engaged_data["duration"]) * 100) if most_engaged_data["duration"] > 0 else 0
else:
most_engaged_date = None
most_engaged_percent = 0
if weeks:
best_week_date, best_week_distance = max(weeks.items(), key=lambda item: item[1])
else:
best_week_date, best_week_distance = None, 0.0
longest_streak = 0
current_streak = 0
previous_day = None
for day in sorted(days):
if previous_day is not None and (day - previous_day).days == 1:
current_streak += 1
else:
current_streak = 1
longest_streak = max(longest_streak, current_streak)
previous_day = day
undistracted_drives = [drive for drive in drives if _drive_is_undistracted(drive)]
longest_undistracted = max(undistracted_drives, key=lambda drive: _safe_int(drive.get("duration", 0), 0)) if undistracted_drives else None
longest_undistracted_duration = _safe_int(longest_undistracted.get("duration", 0), 0) if longest_undistracted else 0
longest_clean_streak = 0
current_clean_streak = 0
for drive in sorted(drives, key=_drive_sort_time):
if _drive_is_clean(drive):
current_clean_streak += 1
else:
current_clean_streak = 0
longest_clean_streak = max(longest_clean_streak, current_clean_streak)
return {
"longestDrive": {
"value": f"{_safe_float(longest.get('distance'), 0):.1f}",
"detail": f"{unit} - {_format_record_date(longest.get('date', ''))}",
},
"mostEngagedDay": {
"value": f"{most_engaged_percent}%",
"detail": most_engaged_date.strftime("%b %-d") if most_engaged_date else "No drives",
},
"bestWeek": {
"value": f"{best_week_distance:.1f}",
"detail": f"{unit} - week of {best_week_date.strftime('%b %-d')}" if best_week_date else unit,
},
"highestStreak": {
"value": f"{longest_streak} day" if longest_streak == 1 else f"{longest_streak} days",
"detail": "Consecutive drive days",
},
"longestUndistractedDrive": {
"value": _format_record_hours(longest_undistracted_duration),
"detail": _format_record_date(longest_undistracted.get("date", "")) if longest_undistracted else "No undistracted drives",
},
"cleanDriveStreak": {
"value": f"{longest_clean_streak} drive" if longest_clean_streak == 1 else f"{longest_clean_streak} drives",
"detail": "No attention warnings",
},
}
def _normalize_persistent_routes(raw_routes):
if not isinstance(raw_routes, dict):
return {}
routes = {}
for route_name, entry in raw_routes.items():
if not isinstance(entry, dict):
continue
name = str(route_name or entry.get("name", "")).strip()
date = str(entry.get("date", "")).strip()
if not name or not date or not _dashboard_time_is_valid(date):
continue
routes[name] = {
"date": date,
"endDate": str(entry.get("endDate", "")).strip(),
"distanceMeters": max(0.0, _safe_float(entry.get("distanceMeters", 0.0), 0.0)),
"duration": max(0, _safe_int(entry.get("duration", 0), 0)),
"clean": bool(entry.get("clean", False)),
"undistracted": bool(entry.get("undistracted", entry.get("clean", False))),
"engagedSeconds": max(0.0, _safe_float(entry.get("engagedSeconds", 0.0), 0.0)),
"distractedMoments": max(0, _safe_int(entry.get("distractedMoments", 0), 0)),
"unresponsiveMoments": max(0, _safe_int(entry.get("unresponsiveMoments", 0), 0)),
"model": _clean_model_label(entry.get("model", "")),
"modelKey": canonical_model_key(entry.get("modelKey", "")),
"segmentCount": max(0, _safe_int(entry.get("segmentCount", 0), 0)),
"modifiedAt": _safe_float(entry.get("modifiedAt", 0.0), 0.0),
"timeSource": str(entry.get("timeSource", "") or ""),
"attentionKnown": bool(entry.get("attentionKnown", True)),
"analysisComplete": bool(entry.get("analysisComplete", False)),
"analysisVersion": max(0, _safe_int(entry.get("analysisVersion", 0), 0)),
}
return routes
def _load_dashboard_persistent_stats(params_obj):
raw_data = _read_dashboard_param_file(DASHBOARD_PERSISTENT_STATS_PARAM)
if raw_data is None:
raw_data = _params_get_value(params_obj, DASHBOARD_PERSISTENT_STATS_PARAM, None)
data = _decode_json_param(raw_data, {})
if not isinstance(data, dict):
data = {}
data["version"] = 1
data["routes"] = _normalize_persistent_routes(data.get("routes", {}))
ignored_routes = data.get("ignoredRoutes", [])
data["ignoredRoutes"] = sorted({
str(route_name).strip()
for route_name in ignored_routes
if ROUTE_RE.fullmatch(str(route_name).strip())
}) if isinstance(ignored_routes, list) else []
attention = data.get("attentionRecords", {})
data["attentionRecords"] = attention if isinstance(attention, dict) else {}
personal_records = data.get("personalRecords", {})
data["personalRecords"] = personal_records if isinstance(personal_records, dict) else {}
model_usage = data.get("modelUsage", {})
data["modelUsage"] = model_usage if isinstance(model_usage, dict) else {}
return data
def _route_entry_sort_key(item):
route_name, entry = item
try:
route_time = datetime.fromisoformat(entry.get("date", ""))
except ValueError:
route_time = datetime.min
return route_time, route_name
def _model_usage_key(model_name):
clean_name = _clean_model_label(model_name)
if not clean_name or clean_name == "Unknown model":
return ""
return canonical_model_key(clean_name) or clean_name.lower()
def _better_record(current, previous, metric_key):
if not isinstance(previous, dict):
return current
if _safe_float(previous.get(metric_key, 0.0), 0.0) > _safe_float(current.get(metric_key, 0.0), 0.0):
return dict(previous)
return current
def _record_with_valid_dates(record, metric_key, *date_keys):
if not isinstance(record, dict):
return None
if _safe_float(record.get(metric_key, 0.0), 0.0) <= 0.0:
return record
for key in date_keys:
if not _dashboard_time_is_valid(record.get(key, "")):
return None
return record
def _build_personal_records_raw(routes):
ordered_routes = sorted((routes or {}).items(), key=_route_entry_sort_key)
records = {
"longestDrive": {"distanceMeters": 0.0, "date": ""},
"mostEngagedDay": {"percent": 0, "date": ""},
"bestWeek": {"distanceMeters": 0.0, "weekDate": ""},
"highestStreak": {"days": 0},
"longestUndistractedDrive": {"duration": 0, "date": ""},
"cleanDriveStreak": {"drives": 0, "startDate": "", "endDate": ""},
}
if not ordered_routes:
return records
days = {}
weeks = {}
current_clean_streak = 0
current_clean_start = ""
for _, entry in ordered_routes:
date_text = str(entry.get("date", "")).strip()
if not _dashboard_time_is_valid(date_text):
continue
try:
drive_date = datetime.fromisoformat(date_text)
except ValueError:
drive_date = None
distance_m = max(0.0, _safe_float(entry.get("distanceMeters", 0.0), 0.0))
duration = max(0, _safe_int(entry.get("duration", 0), 0))
engaged_seconds = max(0.0, _safe_float(entry.get("engagedSeconds", 0.0), 0.0))
if distance_m > _safe_float(records["longestDrive"].get("distanceMeters", 0.0), 0.0):
records["longestDrive"] = {"distanceMeters": distance_m, "date": date_text}
if drive_date is not None:
day_key = drive_date.date()
week_key = _start_of_week(drive_date).date()
days.setdefault(day_key, {"duration": 0, "engaged": 0.0})
days[day_key]["duration"] += duration
days[day_key]["engaged"] += engaged_seconds
weeks[week_key] = weeks.get(week_key, 0.0) + distance_m
attention_known = bool(entry.get("attentionKnown", True))
if attention_known and bool(entry.get("undistracted", False)) and duration > _safe_int(records["longestUndistractedDrive"].get("duration", 0), 0):
records["longestUndistractedDrive"] = {"duration": duration, "date": date_text}
if bool(entry.get("clean", False)) and attention_known:
if current_clean_streak == 0:
current_clean_start = date_text
current_clean_streak += 1
if current_clean_streak > _safe_int(records["cleanDriveStreak"].get("drives", 0), 0):
records["cleanDriveStreak"] = {
"drives": current_clean_streak,
"startDate": current_clean_start,
"endDate": date_text,
}
elif attention_known:
current_clean_streak = 0
current_clean_start = ""
for day, data in days.items():
duration = _safe_int(data.get("duration", 0), 0)
if duration <= 0:
continue
percent = round((_safe_float(data.get("engaged", 0.0), 0.0) / duration) * 100)
if percent > _safe_int(records["mostEngagedDay"].get("percent", 0), 0):
records["mostEngagedDay"] = {"percent": percent, "date": day.isoformat()}
if weeks:
best_week_date, best_week_distance = max(weeks.items(), key=lambda item: item[1])
records["bestWeek"] = {
"distanceMeters": max(0.0, _safe_float(best_week_distance, 0.0)),
"weekDate": best_week_date.isoformat(),
}
current_day_streak = 0
previous_day = None
for day in sorted(days):
if previous_day is not None and (day - previous_day).days == 1:
current_day_streak += 1
else:
current_day_streak = 1
records["highestStreak"]["days"] = max(_safe_int(records["highestStreak"].get("days", 0), 0), current_day_streak)
previous_day = day
return records
def _merge_personal_records(current, previous, legacy_attention=None):
previous = previous if isinstance(previous, dict) else {}
merged = {
"longestDrive": _better_record(
current.get("longestDrive", {}),
_record_with_valid_dates(previous.get("longestDrive"), "distanceMeters", "date"),
"distanceMeters",
),
"mostEngagedDay": _better_record(
current.get("mostEngagedDay", {}),
_record_with_valid_dates(previous.get("mostEngagedDay"), "percent", "date"),
"percent",
),
"bestWeek": _better_record(
current.get("bestWeek", {}),
_record_with_valid_dates(previous.get("bestWeek"), "distanceMeters", "weekDate"),
"distanceMeters",
),
"highestStreak": _better_record(current.get("highestStreak", {}), previous.get("highestStreak"), "days"),
"longestUndistractedDrive": _better_record(
current.get("longestUndistractedDrive", {}),
_record_with_valid_dates(previous.get("longestUndistractedDrive"), "duration", "date"),
"duration",
),
"cleanDriveStreak": _better_record(
current.get("cleanDriveStreak", {}),
_record_with_valid_dates(previous.get("cleanDriveStreak"), "drives", "startDate", "endDate"),
"drives",
),
}
if isinstance(legacy_attention, dict):
merged["longestUndistractedDrive"] = _better_record(
merged["longestUndistractedDrive"],
_record_with_valid_dates(legacy_attention.get("longestUndistractedDrive"), "duration", "date"),
"duration",
)
merged["cleanDriveStreak"] = _better_record(
merged["cleanDriveStreak"],
_record_with_valid_dates(legacy_attention.get("cleanDriveStreak"), "drives", "startDate", "endDate"),
"drives",
)
return merged
def _recalculate_persistent_stats(stats, reset_personal_records=False):
routes = stats.get("routes", {})
ordered_routes = sorted(routes.items(), key=_route_entry_sort_key)
if len(ordered_routes) > DASHBOARD_PERSISTED_ROUTE_LIMIT:
ordered_routes = ordered_routes[-DASHBOARD_PERSISTED_ROUTE_LIMIT:]
routes = dict(ordered_routes)
stats["routes"] = routes
ignored_routes = set(stats.get("ignoredRoutes", []))
included_routes = {
route_name: entry
for route_name, entry in routes.items()
if route_name not in ignored_routes
}
included_ordered_routes = [
(route_name, entry)
for route_name, entry in ordered_routes
if route_name not in ignored_routes
]
model_usage = {}
for _, entry in included_ordered_routes:
if not _dashboard_time_is_valid(entry.get("date", "")):
continue
if not bool(entry.get("analysisComplete", False)):
continue
model_name = _clean_model_label(entry.get("model", ""))
model_key = canonical_model_key(entry.get("modelKey", "")) or _model_usage_key(model_name)
if model_key:
usage = model_usage.setdefault(model_key, {
"key": model_key,
"name": model_name or model_key,
"drives": 0,
"lastUsed": "",
})
usage["drives"] += 1
usage["lastUsed"] = entry.get("date", "") or usage["lastUsed"]
if model_name:
usage["name"] = model_name
previous_attention = stats.get("attentionRecords", {}) if isinstance(stats.get("attentionRecords", {}), dict) else {}
current_records = _build_personal_records_raw(included_routes)
if reset_personal_records:
stats["personalRecords"] = current_records
else:
stats["personalRecords"] = _merge_personal_records(current_records, stats.get("personalRecords", {}), previous_attention)
stats["attentionRecords"] = {
"longestUndistractedDrive": stats["personalRecords"]["longestUndistractedDrive"],
"cleanDriveStreak": stats["personalRecords"]["cleanDriveStreak"],
}
stats["modelUsage"] = model_usage
return stats
def set_dashboard_routes_ignored(params_obj, route_names, ignored):
normalized_names = {
str(route_name or "").strip()
for route_name in (route_names or [])
if ROUTE_RE.fullmatch(str(route_name or "").strip())
}
if not normalized_names:
raise ValueError("No valid dashboard routes were provided.")
stats = _load_dashboard_persistent_stats(params_obj)
ignored_routes = set(stats.get("ignoredRoutes", []))
if ignored:
ignored_routes.update(normalized_names)
else:
ignored_routes.difference_update(normalized_names)
stats["ignoredRoutes"] = sorted(ignored_routes)
stats = _recalculate_persistent_stats(stats, reset_personal_records=True)
_params_put_text(params_obj, DASHBOARD_PERSISTENT_STATS_PARAM, json.dumps(stats, separators=(",", ":")))
_invalidate_dashboard_cache()
return sorted(normalized_names)
def ignore_dashboard_routes(params_obj, route_names):
return set_dashboard_routes_ignored(params_obj, route_names, True)
def include_dashboard_routes(params_obj, route_names):
return set_dashboard_routes_ignored(params_obj, route_names, False)
def _drive_stable_for_persistence(drive, wall_now):
modified_at = _safe_float(drive.get("routeModifiedAt", 0.0), 0.0)
return modified_at <= 0.0 or wall_now - modified_at >= DASHBOARD_PERSIST_MIN_ROUTE_AGE_SECONDS
def _drive_time_reliable_for_persistence(drive):
source = str(drive.get("timeSource", "") or "")
date_text = str(drive.get("date", "")).strip()
if source == DASHBOARD_TIME_SOURCE_FILESYSTEM:
return _dashboard_time_is_valid(date_text, require_recent=True)
if source == DASHBOARD_TIME_SOURCE_LOG:
return _dashboard_time_is_valid(date_text)
return bool(date_text)
def _update_dashboard_persistent_stats(params_obj, drives, wall_now):
stats = _load_dashboard_persistent_stats(params_obj)
before = json.dumps(stats, sort_keys=True, separators=(",", ":"))
routes = stats.setdefault("routes", {})
changed = False
for drive in sorted(drives, key=_drive_sort_time):
route_name = str(drive.get("name", "")).strip()
route_date = str(drive.get("date", "")).strip()
if not route_name or not route_date or not _drive_stable_for_persistence(drive, wall_now) or not _drive_time_reliable_for_persistence(drive):
continue
model_name = _clean_model_label(drive.get("model", ""))
model_key = _model_usage_key(model_name)
attention_known = bool(drive.get("attentionKnown", True))
time_source = str(drive.get("timeSource", "") or "")
next_entry = {
"date": route_date,
"endDate": str(drive.get("endDate", "")).strip(),
"distanceMeters": max(0.0, _safe_float(drive.get("distanceMeters", 0.0), 0.0)),
"duration": max(0, _safe_int(drive.get("duration", 0), 0)),
"clean": attention_known and _drive_is_clean(drive),
"undistracted": attention_known and _drive_is_undistracted(drive),
"engagedSeconds": max(0.0, _safe_float(drive.get("engagedSeconds", 0.0), 0.0)),
"distractedMoments": max(0, _safe_int(drive.get("distractedMoments", 0), 0)),
"unresponsiveMoments": max(0, _safe_int(drive.get("unresponsiveMoments", 0), 0)),
"model": model_name,
"modelKey": model_key,
"segmentCount": max(0, _safe_int(drive.get("segmentCount", 0), 0)),
"modifiedAt": _safe_float(drive.get("routeModifiedAt", 0.0), 0.0),
"timeSource": time_source,
"attentionKnown": attention_known,
"analysisComplete": bool(drive.get("analysisComplete", False)),
"analysisVersion": _safe_int(drive.get("analysisVersion", 0), 0),
}
if attention_known and next_entry["analysisComplete"]:
next_entry["analysisVersion"] = DASHBOARD_ROUTE_ANALYSIS_VERSION
existing_entry = routes.get(route_name)
if isinstance(existing_entry, dict):
existing_distance = max(0.0, _safe_float(existing_entry.get("distanceMeters", 0.0), 0.0))
next_distance = max(0.0, _safe_float(next_entry.get("distanceMeters", 0.0), 0.0))
existing_current = _safe_float(existing_entry.get("modifiedAt", 0.0), 0.0) >= _safe_float(next_entry.get("modifiedAt", 0.0), 0.0)
existing_attention_known = bool(existing_entry.get("attentionKnown", True))
if not attention_known and existing_current and existing_attention_known:
next_entry["clean"] = bool(existing_entry.get("clean", False))
next_entry["undistracted"] = bool(existing_entry.get("undistracted", existing_entry.get("clean", False)))
next_entry["attentionKnown"] = True
next_entry["analysisComplete"] = bool(existing_entry.get("analysisComplete", False))
next_entry["analysisVersion"] = max(0, _safe_int(existing_entry.get("analysisVersion", 0), 0))
if not attention_known and existing_current and existing_distance >= next_distance:
next_entry["distanceMeters"] = existing_distance
existing_duration = _safe_int(existing_entry.get("duration", 0), 0)
next_entry["duration"] = existing_duration if existing_attention_known else max(existing_duration, next_entry["duration"])
if existing_attention_known and str(existing_entry.get("date", "")).strip():
next_entry["date"] = str(existing_entry.get("date", "")).strip()
next_entry["timeSource"] = str(existing_entry.get("timeSource", "") or next_entry["timeSource"])
if str(existing_entry.get("endDate", "")).strip():
next_entry["endDate"] = str(existing_entry.get("endDate", "")).strip()
next_entry["engagedSeconds"] = max(0.0, _safe_float(existing_entry.get("engagedSeconds", 0.0), 0.0))
next_entry["distractedMoments"] = max(0, _safe_int(existing_entry.get("distractedMoments", 0), 0))
next_entry["unresponsiveMoments"] = max(0, _safe_int(existing_entry.get("unresponsiveMoments", 0), 0))
next_entry["analysisComplete"] = bool(existing_entry.get("analysisComplete", False))
next_entry["analysisVersion"] = max(0, _safe_int(existing_entry.get("analysisVersion", 0), 0))
if (not model_name or model_name == "Unknown model") and _clean_model_label(existing_entry.get("model", "")):
next_entry["model"] = _clean_model_label(existing_entry.get("model", ""))
next_entry["modelKey"] = canonical_model_key(existing_entry.get("modelKey", "")) or _model_usage_key(next_entry["model"])
if routes.get(route_name) != next_entry:
routes[route_name] = next_entry
changed = True
stats = _recalculate_persistent_stats(stats)
after = json.dumps(stats, sort_keys=True, separators=(",", ":"))
if changed or after != before:
_params_put_text(params_obj, DASHBOARD_PERSISTENT_STATS_PARAM, json.dumps(stats, separators=(",", ":")))
return stats
def _storage_category(path):
path_text = str(path)
if "realdata_HD" in path_text:
return "highResolution"
if "realdata_konik" in path_text:
return "alternate"
return "standard"
def _build_storage_summary(footage_paths):
free = get_available_bytes(default=0)
used = get_used_bytes(default=0)
total = free + used
counts = {"standard": 0, "highResolution": 0, "alternate": 0}
for footage_path in footage_paths or []:
root = Path(footage_path)
if not root.is_dir():
continue
category = _storage_category(root)
try:
counts[category] += sum(1 for entry in root.iterdir() if entry.is_dir() and _parse_segment_dir_name(entry.name))
except OSError:
continue
return {
"freeBytes": free,
"usedBytes": used,
"totalBytes": total,
"usedPercent": round((used / total) * 100) if total > 0 else 0,
"segmentCounts": counts,
}
def _read_uptime_seconds():
try:
with open("/proc/uptime", encoding="utf-8") as f:
return int(float(f.read().split()[0]))
except Exception:
return None
def _normalize_temp_c(value):
try:
raw = float(value)
except (TypeError, ValueError):
return None
if raw > 1000:
raw /= 1000.0
return raw if 0 < raw < 150 else None
def _read_hardware_cpu_temps():
try:
from openpilot.system.hardware import HARDWARE
thermal_config = HARDWARE.get_thermal_config()
thermal_msg = thermal_config.get_msg()
except Exception:
return []
cpu_temps = thermal_msg.get("cpuTempC", [])
if not isinstance(cpu_temps, (list, tuple)):
cpu_temps = [cpu_temps]
return [
temp for temp in (_normalize_temp_c(value) for value in cpu_temps)
if temp is not None
]
def _read_cpu_temp_c(thermal_root=None):
if thermal_root is None:
hardware_temps = _read_hardware_cpu_temps()
if hardware_temps:
return round(max(hardware_temps))
thermal_root = Path("/sys/class/thermal")
try:
zones = sorted(thermal_root.glob("thermal_zone*/temp"))
except Exception:
return None
values = []
for temp_path in zones:
try:
zone_type = temp_path.with_name("type").read_text(encoding="utf-8").strip().lower()
except Exception:
zone_type = ""
if "cpu" not in zone_type:
continue
try:
raw = temp_path.read_text().strip()
except Exception:
continue
temp = _normalize_temp_c(raw)
if temp is not None:
values.append(temp)
return round(max(values)) if values else None
def _build_device_summary(params_obj):
is_onroad = _params_get_bool(params_obj, "IsOnroad")
uptime_seconds = _read_uptime_seconds()
cpu_temp_c = _read_cpu_temp_c()
return {
"status": "Driving" if is_onroad else "Parked",
"online": True,
"uptimeSeconds": uptime_seconds,
"cpuTempC": cpu_temp_c,
}
def _build_favorite_models(params_obj, persistent_stats=None):
lookup = _model_lookup(params_obj)
user_favorites = {canonical_model_key(entry) for entry in _split_csv(_params_get_text(params_obj, "UserFavorites", ""))}
listed_favorites = {canonical_model_key(entry) for entry in _split_csv(_params_get_text(params_obj, "CommunityFavorites", ""))}
usage = (persistent_stats or {}).get("modelUsage", {})
usage = usage if isinstance(usage, dict) else {}
top_models = []
for raw_key, usage_entry in usage.items():
if not isinstance(usage_entry, dict):
continue
drives = _safe_int(usage_entry.get("drives", 0), 0)
if drives <= 0:
continue
key = canonical_model_key(usage_entry.get("key", "")) or canonical_model_key(raw_key)
name = _clean_model_label(usage_entry.get("name", ""))
if not key:
key = _model_usage_key(name)
if not key:
continue
model = lookup.get(key, {"key": key, "name": name or key, "series": "Custom Series"})
top_models.append({
"key": key,
"name": model["name"],
"series": model["series"],
"userFavorite": key in user_favorites,
"listedFavorite": key in listed_favorites,
"drives": drives,
"weight": drives,
})
top_models.sort(key=lambda model: (
-model["drives"],
model["name"].lower(),
))
return top_models[:DASHBOARD_TOP_MODEL_LIMIT]
def _dashboard_empty(is_metric, now, footage_paths, params_obj, persistent_stats=None):
records = _display_personal_records(persistent_stats or {}, is_metric)
return {
"lastDrive": _empty_drive(is_metric),
"recentDrives": [],
"week": _build_week_summary([], now, is_metric),
"records": records,
"device": _build_device_summary(params_obj),
"storage": _build_storage_summary(footage_paths),
"favoriteModels": _build_favorite_models(params_obj, persistent_stats),
}
def get_dashboard_stats(footage_paths, params_obj=None, now=None):
params_obj = params_obj or params
now = now or datetime.now()
route_infos = _list_dashboard_routes(footage_paths)
cache_key = _dashboard_cache_key(route_infos, params_obj)
cache_now = time.monotonic()
if (
_DASHBOARD_CACHE["key"] == cache_key
and _DASHBOARD_CACHE["value"] is not None
and cache_now - _DASHBOARD_CACHE["updated_at"] < DASHBOARD_CACHE_TTL_SECONDS
):
cached_dashboard = copy.deepcopy(_DASHBOARD_CACHE["value"])
cached_analysis = cached_dashboard.get("analysis", {}) if isinstance(cached_dashboard, dict) else {}
if isinstance(cached_analysis, dict):
cached_analysis["running"] = _dashboard_analyzer_running()
cached_dashboard["analysis"] = cached_analysis
return cached_dashboard
is_metric = _params_get_bool(params_obj, "IsMetric")
model_names = _model_lookup(params_obj)
analysis_deadline = cache_now + DASHBOARD_ANALYSIS_TIME_BUDGET_SECONDS
persistent_stats = _load_dashboard_persistent_stats(params_obj)
shell_drives = [
_route_shell_drive(route_info, params_obj, model_names, is_metric)
for route_info in route_infos
]
if shell_drives:
persistent_stats = _update_dashboard_persistent_stats(params_obj, shell_drives, time.time())
analyzed_drives = []
if DASHBOARD_ROUTE_ANALYSIS_LIMIT > 0 and DASHBOARD_ANALYSIS_TIME_BUDGET_SECONDS > 0:
for route_info in _analysis_candidates(route_infos, persistent_stats)[:DASHBOARD_ROUTE_ANALYSIS_LIMIT]:
if _deadline_reached(analysis_deadline):
break
sampled_route_info = _sample_route_info(route_info)
messages = _iter_route_log_messages(sampled_route_info, analysis_deadline)
analyzed_drives.append(_analyze_route_messages(messages, sampled_route_info, model_names, is_metric, analysis_deadline))
if analyzed_drives:
persistent_stats = _update_dashboard_persistent_stats(params_obj, analyzed_drives, time.time())
persisted_drives = _persistent_drives(persistent_stats, is_metric)
combined_drives = _merge_dashboard_drives(shell_drives, persisted_drives, analyzed_drives)
_mark_ignored_drives(combined_drives, persistent_stats)
display_drives = _coalesce_display_drives(combined_drives, is_metric)
included_display_drives = [drive for drive in display_drives if not bool(drive.get("ignored", False))]
pending_candidates = _analysis_candidates(route_infos, persistent_stats)
pending_route_names = {str(route.get("name", "")).strip() for route in pending_candidates}
included_drives = [drive for drive in combined_drives if not bool(drive.get("ignored", False))]
week_drives = _week_summary_drives(included_drives, pending_route_names)
_start_dashboard_background_analysis(footage_paths, route_infos, persistent_stats, pending_candidates)
analysis_status = _dashboard_analysis_status(pending_candidates)
if not display_drives:
dashboard = _dashboard_empty(is_metric, now, footage_paths, params_obj, persistent_stats)
else:
records = _display_personal_records(persistent_stats, is_metric)
dashboard = {
"lastDrive": _public_drive(included_display_drives[0], is_metric) if included_display_drives else _empty_drive(is_metric),
"recentDrives": [_public_drive(drive, is_metric) for drive in display_drives[:DASHBOARD_RECENT_DRIVE_LIMIT]],
"week": _build_week_summary(week_drives, now, is_metric),
"records": records,
"device": _build_device_summary(params_obj),
"storage": _build_storage_summary(footage_paths),
"favoriteModels": _build_favorite_models(params_obj, persistent_stats),
}
dashboard["analysis"] = analysis_status
_DASHBOARD_CACHE.update({
"key": cache_key,
"updated_at": cache_now,
"value": copy.deepcopy(dashboard),
})
return dashboard
def get_repo_owner(git_normalized_origin):
parts = git_normalized_origin.split("/")
return parts[1] if len(parts) >= 2 else "unknown"
def normalize_github_remote(remote):
remote = str(remote or "").strip()
if remote.startswith("git@github.com:"):
remote = "https://github.com/" + remote.split(":", 1)[1]
elif remote.startswith("ssh://git@github.com/"):
remote = "https://github.com/" + remote.split("ssh://git@github.com/", 1)[1]
elif remote.startswith("github.com/"):
remote = "https://" + remote
elif remote.startswith("http://github.com/"):
remote = "https://github.com/" + remote.split("http://github.com/", 1)[1]
elif re.fullmatch(r"[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+", remote):
remote = f"https://github.com/{remote}"
if not remote.startswith("https://github.com/"):
return ""
remote = remote.rstrip("/")
if remote.endswith(".git"):
remote = remote[:-4]
return remote
def get_github_changelog_url(git_normalized_origin, branch):
remote = normalize_github_remote(git_normalized_origin)
if not remote or not branch:
return ""
return f"{remote}/commits/{quote(str(branch), safe='')}/"
def get_github_commit_url(git_normalized_origin, commit):
remote = normalize_github_remote(git_normalized_origin)
if not remote or not commit:
return ""
return f"{remote}/commit/{quote(str(commit), safe='')}"
def get_route_log_path(path):
target = Path(path)
if target.is_dir():
for candidate in ROUTE_TIME_LOG_CANDIDATES:
candidate_path = target / candidate
if candidate_path.exists():
return candidate_path
return None
if target.exists():
return target
if target.parent.is_dir():
for candidate in ROUTE_TIME_LOG_CANDIDATES:
candidate_path = target.parent / candidate
if candidate_path.exists():
return candidate_path
return None
def get_route_start_time(path):
log_path = get_route_log_path(path)
if log_path is None:
target = Path(path)
if not target.exists():
return None
log_path = target
try:
modified_time = log_path.stat().st_mtime
except OSError:
return None
if modified_time <= 0:
return None
return datetime.fromtimestamp(modified_time)
def get_routes_names(footage_path):
segments = get_all_segment_names(footage_path)
route_times = {segment.route_name.time_str for segment in segments}
return sorted(route_times, reverse=True)
def get_segments_in_route(route_time_str, footage_path):
return [
f"{segment.time_str}--{segment.segment_num}"
for segment in get_all_segment_names(footage_path)
if segment.time_str == route_time_str
]
def get_video_duration(input_path):
try:
result = subprocess.run([
"ffprobe", "-v", "error", "-show_entries", "format=duration",
"-of", "default=noprint_wrappers=1:nokey=1", str(input_path)
], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, check=True)
return float(result.stdout)
except (ValueError, subprocess.CalledProcessError):
return 60
def has_preserve_attr(path: str):
return PRESERVE_ATTR_NAME in os.listxattr(path) and os.getxattr(path, PRESERVE_ATTR_NAME) == PRESERVE_ATTR_VALUE
def list_file(path):
return sorted(os.listdir(path), reverse=True)
def normalize_theme_name(name, for_path=False):
name = name.replace("-user_created", "")
if for_path:
return name.lower().replace(" (", "-").replace(")", "").replace(" ", "-").replace("'", "").replace(".", "")
parts = re.split(r'[-_]', name)
normalized_parts = [part.capitalize() for part in parts]
if '-' in name and len(normalized_parts) > 1:
return f"{normalized_parts[0]} ({' '.join(normalized_parts[1:])})".replace(" Week", "")
return ' '.join(normalized_parts).replace(" Week", "")
def process_route(footage_path, route_name):
segment_path = f"{footage_path}{route_name}--0"
qcamera_path = f"{segment_path}/qcamera.ts"
png_output_path = os.path.join(segment_path, "preview.png")
if not os.path.exists(png_output_path):
video_to_png(qcamera_path, png_output_path)
custom_name = None
if os.path.isdir(segment_path):
for item in os.listdir(segment_path):
if not item.endswith((".hevc", ".ts", ".png", ".gif")) and item not in LOG_CANDIDATES:
custom_name = item
break
route_timestamp_str = custom_name
if not custom_name:
route_timestamp_dt = get_route_start_time(segment_path)
route_timestamp_str = route_timestamp_dt.isoformat() if route_timestamp_dt else None
return {
"name": route_name,
"png": f"/thumbnails/{route_name}--0/preview.png",
"timestamp": route_timestamp_str,
"is_preserved": has_preserve_attr(segment_path)
}
def process_screen_recording(mp4):
stem = mp4.with_suffix("")
png_path = stem.with_suffix(".png")
if not png_path.exists():
video_to_png(mp4, png_path)
is_custom_name = False
try:
datetime.strptime(stem.name, "%B_%d_%Y-%I-%M%p")
except ValueError:
is_custom_name = True
return {
"filename": mp4.name,
"png": f"/screen_recordings/{png_path.name}",
"timestamp": datetime.fromtimestamp(mp4.stat().st_mtime).isoformat(),
"is_custom_name": is_custom_name
}
def segment_to_segment_name(data_dir, segment):
full_path = os.path.join(data_dir, f"FakeDongleID1337|{segment}")
return SegmentName(full_path)
def video_to_png(input_path, output_path):
try:
subprocess.run([
"ffmpeg", "-hide_banner", "-loglevel", "error",
"-ss", "1",
"-i", str(input_path),
"-frames:v", "1",
"-y",
str(output_path)
], capture_output=True, check=True, text=True)
except subprocess.CalledProcessError as e:
print(f"Failed to generate PNG for {input_path}")
if e.stderr:
print(e.stderr)
def xor_encrypt_decrypt(data, key):
return "".join(chr(ord(c) ^ ord(key[i % len(key)])) for i, c in enumerate(data))