Files
StarPilot/openpilot/starpilot/galaxy/onroad_layout.py
T

133 lines
6.7 KiB
Python

import copy
import hashlib
import json
from collections.abc import Callable
from openpilot.starpilot.saved_document import commit_exact
from openpilot.starpilot.saved_source import read_saved
from openpilot.starpilot.ui.appearance_owner import AppearanceOwner
from openpilot.starpilot.ui.appearance_preferences import read_lead_info
from openpilot.starpilot.ui.feature_settings_state import FeatureSettingsRequest
from openpilot.starpilot.ui.onroad_customization import (
MAX_BYTES, PARAM_KEY, customization_metadata, decode_document, default_document, validate_document,
)
from openpilot.starpilot.ui.presentation import Profile
from openpilot.starpilot.galaxy.setting_descriptions import description
# Display preferences edited with the layout but saved in their own params. Groups order the editor.
DISPLAY_GROUPS = {
"DeveloperSidebar": "Big layout",
"ShowBrakeStatus": "widget", "StockConfidenceBallWidget": "widget",
"HideLeadMarker": "Lead car (Small layout)", "LeadInfo": "Lead car (Small layout)",
"CameraView": "Camera (Small layout)", "DriverCamera": "Camera (Small layout)",
"HidePersonalityAlerts": "Alerts and borders", "SignalMetrics": "Alerts and borders", "BlindSpotMetrics": "Alerts and borders",
}
class LayoutChanged(Exception):
pass
def _revision(raw: bytes | None, rows=()) -> str:
digest = hashlib.sha256(b"\x00" if raw is None else b"\x01" + raw)
for row in rows:
digest.update(repr((row.key, row.source, row.related_source, row.dependencies)).encode())
return digest.hexdigest()
class OnroadLayoutOwner:
def __init__(self, params, parked: Callable[[], bool]):
self.params = params
self.parked = parked
def _display_rows(self):
rows = AppearanceOwner(self.params, self.parked).snapshot(Profile.COMPACT, include_large=True).rows
order = list(DISPLAY_GROUPS)
return tuple(sorted((row for row in rows if row.key in DISPLAY_GROUPS), key=lambda row: order.index(row.key)))
def snapshot(self) -> dict:
raw, readable = read_saved(self.params, PARAM_KEY, MAX_BYTES)
valid = readable
document = default_document()
if raw is not None:
try:
document = decode_document(raw)
except (ValueError, TypeError, UnicodeError, RecursionError):
valid = False
parked = self.parked()
rows = self._display_rows()
lead_readable = read_lead_info(self.params).readable
# The editor merges these into its draft as document["display"]; the saved layout never holds them.
display = {
"rows": [{"key": row.key, "label": row.label, "group": DISPLAY_GROUPS[row.key], "choices": list(row.choices),
# Lead info follows the draft's Lead indicator, not the saved one.
"available": row.available or row.key == "LeadInfo" and parked and lead_readable and bool(row.choices),
"reason": description(row), "requires": "HideLeadMarker" if row.key == "LeadInfo" else ""} for row in rows],
"values": {row.key: row.value for row in rows},
"defaults": {row.key: row.default_value for row in rows if row.default_value is not None},
}
return {"document": document, "defaults": default_document(), "revision": _revision(raw, rows),
"metadata": customization_metadata(), "editable": readable and parked, "valid": valid,
"activeProfile": None, "display": display, "failed": []}
def save(self, payload: dict, *, session_valid: Callable[[], bool]) -> dict:
if (type(payload) is not dict or set(payload) != {"revision", "document"} or type(payload["revision"]) is not str or
type(payload["document"]) is not dict):
raise ValueError("Invalid layout request")
document = copy.copy(payload["document"])
display = document.pop("display", {})
rows = {row.key: row for row in self._display_rows()}
if (type(display) is not dict or not display.keys() <= rows.keys() or
any(type(value) is not str or value != rows[key].value and value not in rows[key].choices
for key, value in display.items())):
raise ValueError("Invalid display preferences")
document = validate_document(document)
encoded = json.dumps(document, separators=(",", ":"), allow_nan=False).encode()
if len(encoded) > MAX_BYTES:
raise ValueError("Layout is too large")
source, readable = read_saved(self.params, PARAM_KEY, MAX_BYTES)
if not readable:
raise OSError("Saved layout is unavailable")
if payload["revision"] != _revision(source, tuple(rows.values())):
raise LayoutChanged("Saved layout changed; reload before saving")
result = commit_exact(self.params, key=PARAM_KEY, max_bytes=MAX_BYTES, raw=encoded, expected=source,
authorized=lambda: session_valid() and self.parked(), temp_prefix=".onroad-layout-")
if not result.committed or not result.verified:
raise LayoutChanged("Layout could not be saved; check that the device is parked and reload")
failed = self._save_display(display, rows, session_valid)
return {**self.snapshot(), "failed": failed}
def disable_map(self, session_valid: Callable[[], bool]) -> bool:
"""Turn the Big-screen map off when Android Auto has taken it; True when it was on."""
raw, readable = read_saved(self.params, PARAM_KEY, MAX_BYTES)
if not readable or raw is None:
return False
try:
document = json.loads(raw)
placed = document["layouts"]["large"]["nav_map"]
except (ValueError, KeyError, TypeError):
return False
if type(placed) is not dict or placed.get("enabled") is not True:
return False
placed["enabled"] = False
result = commit_exact(self.params, key=PARAM_KEY, max_bytes=MAX_BYTES,
raw=json.dumps(document, separators=(",", ":"), allow_nan=False).encode(), expected=raw,
authorized=lambda: session_valid() and self.parked(), temp_prefix=".onroad-layout-")
return bool(result.committed and result.verified)
def _save_display(self, display, rows, session_valid) -> list[str]:
changes = [key for key, value in display.items() if value != rows[key].value]
# Lead info can change only while the Lead indicator is shown.
if "HideLeadMarker" in changes and "LeadInfo" in changes:
changes.remove("LeadInfo")
changes.insert(changes.index("HideLeadMarker") + (display["HideLeadMarker"] == "On"), "LeadInfo")
owner = AppearanceOwner(self.params, lambda: session_valid() and self.parked())
failed = []
for key in changes:
row = {row.key: row for row in self._display_rows()}[key]
request = FeatureSettingsRequest(key, row.source, display[key], related_source=row.related_source,
dependencies=row.dependencies)
if not row.available or not owner.apply(request):
failed.append(row.label)
return failed