mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-08 05:23:41 +08:00
169 lines
7.6 KiB
Python
169 lines
7.6 KiB
Python
"""Authenticated Galaxy owner for a separate, negotiated AA layout document."""
|
|
import hashlib
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
|
|
from openpilot.starpilot.galaxy.onroad_layout import LayoutChanged
|
|
from openpilot.starpilot.saved_document import commit_exact
|
|
from openpilot.starpilot.saved_source import read_saved
|
|
from openpilot.starpilot.system.starport.display_profile import (
|
|
SCREEN_PATH, MAX_BYTES as SCREEN_MAX_BYTES, record_screen, validate_screen,
|
|
)
|
|
from openpilot.starpilot.system.starport.projection_layout import (
|
|
DOCUMENT_KEY, JETLINK_WIDGET, MAX_BYTES, ProjectionLayoutSource, comma_display, decode_layout, default_layout, layout_metadata, validate_layout,
|
|
)
|
|
from openpilot.starpilot.ui.appearance_preferences import onroad_appearance
|
|
from openpilot.starpilot.ui.onroad_customization import read_customization
|
|
|
|
|
|
class _ScreenSource:
|
|
def __init__(self, path):
|
|
self.path = Path(path)
|
|
|
|
def get_param_path(self, _key):
|
|
return str(self.path)
|
|
|
|
|
|
def _revision(screen, document):
|
|
digest = hashlib.sha256(b'StarPilot projection layout v1\0')
|
|
for raw in (screen, document):
|
|
digest.update(b'absent\0' if raw is None else b'present\0' + len(raw).to_bytes(8, 'big') + raw)
|
|
return digest.hexdigest()
|
|
|
|
|
|
def _unique(pairs):
|
|
result = {}
|
|
for key, value in pairs:
|
|
if key in result:
|
|
raise ValueError('Duplicate screen field')
|
|
result[key] = value
|
|
return result
|
|
|
|
|
|
class ProjectionLayoutOwner:
|
|
def __init__(self, params, parked, source=None, screen_path=None, require_enabled=True):
|
|
self.params, self.parked, self.require_enabled = params, parked, require_enabled
|
|
self.source = source if source is not None else ProjectionLayoutSource()
|
|
self.screen_source = _ScreenSource(screen_path if screen_path is not None else SCREEN_PATH)
|
|
|
|
def _enabled(self):
|
|
from openpilot.starpilot.system.starport.settings import enabled
|
|
return not self.require_enabled or enabled(self.params)
|
|
|
|
def _screen(self):
|
|
raw, readable = read_saved(self.screen_source, 'screen', SCREEN_MAX_BYTES)
|
|
screen = None
|
|
if readable and raw is not None:
|
|
try:
|
|
screen = validate_screen(json.loads(raw, object_pairs_hook=_unique))
|
|
except (ValueError, TypeError, UnicodeError, RecursionError):
|
|
pass
|
|
return raw, readable, screen
|
|
|
|
def snapshot(self):
|
|
screen_raw, screen_readable, screen = self._screen()
|
|
raw, readable = read_saved(self.source, DOCUMENT_KEY, MAX_BYTES)
|
|
defaults = default_layout(screen) if screen is not None else None
|
|
document = defaults
|
|
if readable and raw is None and screen is not None:
|
|
# A first AA layout starts from the comma screen's display choices.
|
|
document = default_layout(screen, comma_display(onroad_appearance(self.params)))
|
|
valid = False
|
|
if readable and raw is not None and screen is not None:
|
|
try:
|
|
document = decode_layout(raw, screen)
|
|
valid = True
|
|
except (ValueError, TypeError, UnicodeError, RecursionError):
|
|
pass
|
|
enabled = self._enabled()
|
|
available = bool(enabled and screen is not None and screen_readable and readable)
|
|
reason = ('Enable StarPort to edit its layout' if not enabled else
|
|
'Connect StarPort once to obtain the actual screen' if screen is None else
|
|
'Saved projection layout is unavailable' if not readable else None)
|
|
base = read_customization(self.params)
|
|
metadata = layout_metadata(screen) if screen is not None else None
|
|
if metadata is not None:
|
|
mode = self.params.get('JetlinkMode')
|
|
metadata['widgets'][JETLINK_WIDGET]['available'] = type(mode) is int and mode in (1, 2)
|
|
return {'version': 1, 'document': document, 'defaults': defaults,
|
|
'metadata': metadata,
|
|
'screen': screen, 'revision': _revision(screen_raw, raw),
|
|
'available': available, 'editable': bool(available and self.parked()), 'valid': valid,
|
|
'reason': reason, 'retainedLayout': raw is not None,
|
|
'colors': {'palette': base['palette'], 'widgetColors': base['widgetColors']['large'],
|
|
'roadColors': base['roadColors']['large']}}
|
|
|
|
def save(self, payload, *, session_valid):
|
|
if type(payload) is not dict or set(payload) != {'revision', 'document'} or type(payload['revision']) is not str:
|
|
raise ValueError('Invalid projection layout request')
|
|
screen_raw, screen_readable, screen = self._screen()
|
|
raw, readable = read_saved(self.source, DOCUMENT_KEY, MAX_BYTES)
|
|
if not readable or not screen_readable or screen is None:
|
|
raise LayoutChanged('Projection screen or layout is unavailable; reconnect and reload')
|
|
if payload['revision'] != _revision(screen_raw, raw):
|
|
raise LayoutChanged('Projection screen or layout changed; reload before saving')
|
|
document = validate_layout(payload['document'], screen)
|
|
encoded = json.dumps(document, separators=(',', ':'), allow_nan=False).encode()
|
|
|
|
def authorized():
|
|
if not session_valid() or not self.parked() or not self._enabled():
|
|
return False
|
|
current, current_readable, current_screen = self._screen()
|
|
return current_readable and current_screen is not None and current == screen_raw
|
|
|
|
if not authorized():
|
|
raise LayoutChanged('Projection layout requires a current session, parked device and unchanged screen')
|
|
self.source.prepare()
|
|
result = commit_exact(self.source, key=DOCUMENT_KEY, max_bytes=MAX_BYTES, raw=encoded, expected=raw,
|
|
authorized=authorized, temp_prefix='.projection-layout-')
|
|
if not result.committed or not result.verified:
|
|
raise LayoutChanged('Projection layout could not be saved; reload while parked')
|
|
return self.snapshot()
|
|
|
|
def disable_map(self, *, session_valid):
|
|
"""Turn the Android Auto map off when the Big screen has taken it; True when it was on."""
|
|
raw, readable = read_saved(self.source, DOCUMENT_KEY, MAX_BYTES)
|
|
if not readable or raw is None:
|
|
return False
|
|
try:
|
|
document = json.loads(raw)
|
|
placed = document['widgets']['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
|
|
screen_raw, screen_readable, _ = self._screen()
|
|
|
|
def authorized():
|
|
if not session_valid() or not self.parked():
|
|
return False
|
|
current, current_readable, _ = self._screen()
|
|
return current_readable and current == screen_raw
|
|
|
|
self.source.prepare()
|
|
result = commit_exact(self.source, key=DOCUMENT_KEY, max_bytes=MAX_BYTES,
|
|
raw=json.dumps(document, separators=(',', ':'), allow_nan=False).encode(), expected=raw,
|
|
authorized=authorized, temp_prefix='.projection-layout-')
|
|
return bool(result.committed and result.verified)
|
|
|
|
|
|
# ./dev galaxy has no car and no writable /data: give it a private screen and edit it with Android Auto off.
|
|
HOST_SCREEN = {'width': 1920, 'height': 1080, 'margin_width': 0, 'margin_height': 0, 'fps': 60, 'config_index': 0}
|
|
|
|
|
|
def host_owner(params, parked, root=None, environ=os.environ):
|
|
"""Return a host-runtime owner, or None on a device where the negotiated screen is authoritative."""
|
|
if environ.get('SP_HOST_RUNTIME') != '1':
|
|
return None
|
|
if root is None:
|
|
from openpilot.starpilot.system.starport.identity import DATA_DIR
|
|
root = DATA_DIR
|
|
root = Path(root)
|
|
screen_path = root / 'screen.json'
|
|
if not screen_path.exists():
|
|
record_screen(HOST_SCREEN, screen_path)
|
|
return ProjectionLayoutOwner(params, parked, ProjectionLayoutSource(root / 'layouts/document.json'), screen_path,
|
|
require_enabled=False)
|