Files
firestar5683 a82935f326 User interfaces and Android Auto
Integrate onroad views, configurable widgets, local settings, steering-profile contracts, Bluetooth and Android Auto with their device and runtime services.

Co-authored-by: Adeeb Shihadeh <adeebshihadeh@gmail.com>
Co-authored-by: Daniel Koepping <elkoled@gmail.com>
Co-authored-by: Danny <dannyirwin31@yahoo.com>
Co-authored-by: DimitriChrysafis <Dimitri.chrysafis@gmail.com>
Co-authored-by: Dom <168790843+firestar5683@users.noreply.github.com>
Co-authored-by: Harald Schäfer <harald.the.engineer@gmail.com>
Co-authored-by: Prabhaav Pillai <prabhaav@msu.edu>
Co-authored-by: Shane Smiskol <shane@smiskol.com>
Co-authored-by: dirwin31 <83434411+dirwin31@users.noreply.github.com>
Co-authored-by: firestarsdog <229254897+firestarsdog@users.noreply.github.com>
Co-authored-by: stef <19478336+stefpi@users.noreply.github.com>
Co-authored-by: whoisdomi <dcantu83@gmail.com>

Co-authored-by: Intelli <contact@intelli.software>
Co-authored-by: pharmacomaniac <blittle65@gmail.com>

Co-authored-by: prabhaavp <prabhaav@msu.edu>
2026-10-14 20:02:39 -05:00

141 lines
7.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Driving-model receipt and shared native catalog action rules."""
import unicodedata
from openpilot.starpilot.models.catalog import BUNDLED_CURRENT, model_entries
from openpilot.starpilot.models.status import ModelHealth, ModelStatus, ModelVariant
from openpilot.starpilot.ui.feature_settings_state import FeatureRow, FeatureSettingsState
def model_page(status: ModelStatus, manager: dict | None = None) -> FeatureSettingsState:
entries = model_entries()
if status.health is ModelHealth.ACTIVE:
health = "Active"
elif status.health is ModelHealth.LOADING:
health = "Starting"
elif status.health is ModelHealth.STALE:
health = "Model output stale"
elif status.health is ModelHealth.FAILED:
health = "Model status mismatch"
elif status.health is ModelHealth.IDENTITY_UNAVAILABLE:
health = "Identity unavailable"
else:
health = "Model not running"
variant = ("Chestnut big" if status.variant is ModelVariant.CHESTNUT else "Small") if status.loaded_id else "Unavailable"
rows = (
FeatureRow("", "Requested model", entries[status.requested_id].name if status.requested_id in entries else "Unavailable",
reason="Saved selection for the next start" if status.pending_next_start else "Current request"),
FeatureRow("", "Runtime", health, reason="Updates while driving"),
FeatureRow("", "Loaded variant", variant),
FeatureRow("", "Compiled artifact", status.artifact_sha256[:16] if status.artifact_sha256 else "Unavailable",
reason="SHA-256 prefix" if status.artifact_sha256 else "No verified load receipt"),
)
if status.loaded_id:
rows += (FeatureRow("", "Loaded model", entries[status.loaded_id].name if status.loaded_id in entries else status.loaded_id),)
if status.fallback_reason == "chestnut-run-stalled":
rows += (FeatureRow("", "Fallback", entries[status.loaded_id].name if status.loaded_id in entries else "Active Small",
reason="Chestnut output stopped; Small restarted for this drive"),)
elif status.fallback_reason == "chestnut-load-failed":
rows += (FeatureRow("", "Fallback", entries[status.loaded_id].name if status.loaded_id in entries else "Active Small",
reason="Chestnut load or run failed"),)
elif status.fallback_reason == "selected-load-failed":
rows += (FeatureRow("", "Fallback", "Bundled driving model", reason="Selected model load or run failed"),)
if manager is not None:
rows = model_manager_rows(manager) + rows
return FeatureSettingsState(title="Driving Model", subtitle="Selections apply at next start; runtime shows the loaded model.",
rows=rows)
def model_fits_profile(model: dict, profile: str) -> bool:
profiles = model.get("profiles")
return profile in profiles if isinstance(profiles, list) else model.get("requiresGpu") is (profile == "big")
def model_action_allowed(data: dict, action: str, model: dict | None = None, profile: str = "") -> bool:
capability = {"active": "select", "preferences": "favorites", "download_all": "downloadAll", "refresh_manifest": "refresh",
"exclusion": "exclusions"}.get(action, action)
if data.get("capabilities", {}).get(capability) is not True:
return False
if data.get("isOnroad") is not False and action not in ("preferences", "cancel"):
return False
if action == "active":
return (not data.get("randomizer") and not data.get("downloading") and profile in ("small", "big") and
((model is None and profile == "big") or
bool(model and model.get("installed") is True and model.get("selectable") is True and model_fits_profile(model, profile))))
if action == "download":
return not data.get("downloading") and bool(model and not model.get("installed") and model.get("downloadAvailable") is True)
if action == "download_all":
return not data.get("downloading") and any(not m.get("installed") and m.get("downloadAvailable") is True for m in data.get("models", ()))
if action == "cancel":
return data.get("downloading") is True
if action == "delete":
return bool(not data.get("downloading") and model and not model.get("builtin") and model.get("deletable") is not False and
(model.get("installed") or model.get("partial")) and
model.get("value") not in (data.get("currentModel"), data.get("activeSmallModel"), data.get("activeBigModel")))
if action == "randomizer":
return True
if action == "exclusion":
return model is not None
return action == "preferences" or action == "refresh_manifest" and not data.get("downloading")
def model_catalog_rows(data: dict, sort_mode: str = "date", profile: str = "", installed_only: bool = False) -> list[dict]:
rows = [m for m in data.get("models", ()) if isinstance(m, dict) and isinstance(m.get("value"), str) and
(not profile or model_fits_profile(m, profile)) and
(not installed_only or m.get("installed") is True and m.get("selectable") is True)]
if sort_mode == "favorites":
rows = [m for m in rows if m.get("userFavorite")]
elif sort_mode == "community":
rows = [m for m in rows if m.get("communityFavorite")]
rows.sort(key=lambda m: str(m.get("label", m["value"])).casefold())
if sort_mode in ("date", "date_oldest"):
rows.sort(key=lambda m: str(m.get("released", "")), reverse=sort_mode == "date")
return rows
def model_profile_choices(data: dict, profile: str) -> tuple[tuple[str, str], ...]:
rows = model_catalog_rows(data, "name", profile, installed_only=True)
choices = tuple((m.get("label", m["value"]), m["value"]) for m in rows)
return (("None — always use Active Small", ""),) + choices if profile == "big" else choices
def model_profile_request(data: dict, profile: str, model_id: str) -> dict | None:
model = next((m for m in data.get("models", ()) if m.get("value") == model_id), None)
if model_id and model is None or not model_action_allowed(data, "active", model, profile):
return None
return {"profile": profile, "model": model_id}
def model_manager_rows(data: dict) -> tuple[FeatureRow, ...]:
summary = data.get("summary", {})
rows = [FeatureRow("", "Model catalog", f"{summary.get('installed', 0)} installed / {summary.get('total', 0)} total")]
for profile, field in (("small", "activeSmallModel"), ("big", "activeBigModel")):
mid = data.get(field, "")
label = next((m.get("label", mid) for m in data.get("models", ()) if m.get("value") == mid), mid or "None — always use Active Small")
available = (data.get("capabilities", {}).get("select") is True and data.get("isOnroad") is False and
not data.get("downloading") and not data.get("randomizer"))
rows.append(FeatureRow("", f"Active {profile.title()}", label, source=mid.encode(), available=available,
reason="Randomizer selects at the next start" if data.get("randomizer") else
"Change for next start" if available else "Turn the vehicle off to change models", page=f"models:{profile}"))
return tuple(rows)
def model_display_text(value: str) -> str:
# The native Inter atlas has bullets and en dashes, but no emoji glyphs.
text = value.replace("·", "•").replace("—", "–")
return " ".join("".join(char for char in text if ord(char) <= 0xffff and
unicodedata.category(char) != "So" and char not in "\ufe0e\ufe0f\u200d").split())
def home_model_label(status: ModelStatus, commit: str = "") -> str:
entry = model_entries().get(status.loaded_id or status.requested_id)
if entry is None:
return "Driving model unavailable"
commit = commit.strip().lower()
revision = commit[:7] if 7 <= len(commit) <= 64 and all(char in "0123456789abcdef" for char in commit) else ""
name = entry.name
if entry.model_id == BUNDLED_CURRENT:
name = "Cinque v2" if status.variant is ModelVariant.CHESTNUT else "CD210"
return model_display_text(name + (f" · {revision}" if revision else ""))