Files
StarPilot/openpilot/starpilot/galaxy/vehicle_selection.py
T
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

142 lines
6.0 KiB
Python

"""Authenticated projection of the existing parked vehicle-selection owner."""
from dataclasses import dataclass
import hashlib
import secrets
import threading
import time
from openpilot.starpilot.vehicle_selection import VehicleSelectionOwner
class VehicleSelectionChanged(Exception):
pass
class VehicleSelectionUnverified(Exception):
pass
@dataclass(frozen=True)
class _View:
session: bytes
generation: bytes
raw: bytes | None
valid: bool
expires: float
@dataclass(frozen=True)
class _Intent:
session: bytes
generation: bytes
raw: bytes | None
platform: str | None
expires: float
class VehicleSelectionGateway:
MAX_PENDING = 64
TTL_SECONDS = 60.0
def __init__(self, params, context, *, clock=time.monotonic):
self.params = params
self.context = context
self.clock = clock
self.lock = threading.Lock()
self.owner = VehicleSelectionOwner(params, context.parked)
self.catalog = self.owner.choices()
self.by_platform = {str(choice.platform): choice for choice in self.catalog}
self.views: dict[str, _View] = {}
self.intents: dict[str, _Intent] = {}
def close(self) -> None:
close = getattr(self.context, 'close', None)
if callable(close):
close()
def _clean(self) -> None:
now = self.clock()
self.views = {key: item for key, item in self.views.items() if item.expires > now}
self.intents = {key: item for key, item in self.intents.items() if item.expires > now}
@staticmethod
def _session(token: str) -> bytes:
return hashlib.sha256(token.encode('ascii')).digest()
def page(self, token: str, generation: bytes) -> dict:
saved = self.owner.snapshot()
parked = self.context.parked()
reported = None
try:
runtime_vehicle = getattr(self.context, 'runtime_vehicle', None)
cp = runtime_vehicle()[0] if callable(runtime_vehicle) else self.context.sample().cp
platform = str(cp.carFingerprint) if cp is not None else ''
if not cp.notCar and platform in self.by_platform:
choice = self.by_platform[platform]
origin = str(getattr(cp, 'fingerprintSource', 'unknown'))
reported = {'platform': platform, 'make': str(choice.make), 'label': str(choice.label),
'origin': 'manual' if origin == 'fixed' else 'detected' if origin in ('can', 'fw') else 'unknown'}
except (AttributeError, TypeError, ValueError, OSError, RuntimeError):
pass
view = None
if saved.readable:
with self.lock:
self._clean()
if len(self.views) >= self.MAX_PENDING:
self.views.pop(next(iter(self.views)))
view = secrets.token_urlsafe(24)
self.views[view] = _View(self._session(token), generation, saved.raw, saved.valid, self.clock() + self.TTL_SECONDS)
selected = self.by_platform.get(str(saved.platform)) if saved.valid and saved.platform is not None else None
return {'version': 1, 'parked': parked, 'readable': saved.readable, 'valid': saved.valid,
'selected': str(saved.platform) if selected is not None else None,
'selectedLabel': str(selected.label) if selected is not None else 'Auto detection' if saved.valid else
'Needs review' if saved.readable else 'Unavailable',
'selectionMode': 'manual' if selected is not None else 'auto' if saved.valid else
'invalid' if saved.readable else 'unavailable',
'reported': reported, 'view': view,
'choices': [{'platform': str(choice.platform), 'make': str(choice.make), 'label': str(choice.label)}
for choice in self.catalog]}
def preview(self, view_id: str, platform: str | None, token: str, generation: bytes) -> dict:
if platform is not None and (type(platform) is not str or platform not in self.by_platform):
raise ValueError('Invalid vehicle')
with self.lock:
self._clean()
view = self.views.get(view_id)
if view is None or view.session != self._session(token) or view.generation != generation:
raise VehicleSelectionChanged('Refresh vehicle selection')
saved = self.owner.snapshot()
if not saved.readable or saved.raw != view.raw or saved.valid != view.valid or not self.context.parked():
raise VehicleSelectionChanged('Vehicle selection changed')
if not saved.valid and platform is not None:
raise VehicleSelectionChanged('Restore Auto detection first')
with self.lock:
self._clean()
if len(self.intents) >= self.MAX_PENDING:
self.intents.pop(next(iter(self.intents)))
intent = secrets.token_urlsafe(24)
self.intents[intent] = _Intent(view.session, generation, view.raw, platform, self.clock() + self.TTL_SECONDS)
label = self.by_platform[platform].label if platform is not None else 'Auto detection'
return {'intent': intent, 'question': f'Save {label} for the next start?', 'selected': platform, 'label': str(label)}
def confirm(self, intent_id: str, token: str, generation: bytes, *, session_valid=lambda: True) -> bool:
with self.lock:
self._clean()
intent = self.intents.pop(intent_id, None)
if intent is None or intent.session != self._session(token) or intent.generation != generation or not session_valid():
raise VehicleSelectionChanged('Refresh vehicle selection')
current = self.owner.snapshot()
if not current.readable or current.raw != intent.raw or not self.context.parked():
raise VehicleSelectionChanged('Vehicle selection changed')
owner = VehicleSelectionOwner(self.params, lambda: self.context.parked() and session_valid())
result = owner.choose(intent.raw, intent.platform)
if not result.verified:
if result.committed:
raise VehicleSelectionUnverified('Vehicle selection may have been saved')
raise VehicleSelectionChanged('Vehicle selection could not be confirmed')
saved = self.owner.snapshot()
if not saved.readable or not saved.valid or saved.platform != intent.platform:
raise VehicleSelectionChanged('Vehicle selection could not be confirmed')
return True