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

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)