mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-05 20:13:47 +08:00
a82935f326
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>
133 lines
5.9 KiB
Python
133 lines
5.9 KiB
Python
"""Read-only scalar settings, resolved by the same owners as the settings UI."""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import math
|
|
import re
|
|
from collections.abc import Iterable
|
|
|
|
MAX_SETTINGS = 512
|
|
MAX_BYTES = 128 * 1024
|
|
MAX_FIELDS = 2048
|
|
MAX_ENUM = 80
|
|
KEY = re.compile(r"[A-Za-z][A-Za-z0-9_:/.-]{0,95}\Z")
|
|
NUMBER = re.compile(r"-?(?:\d+(?:\.\d*)?|\.\d+)\Z")
|
|
EXCLUDED_KEYS = frozenset(("SoundPack", "torque_prepare_firestar"))
|
|
EXCLUDED_PREFIXES = ("pip:mask:", "pip:format:")
|
|
COMMAND = re.compile(r"(?:^|[:_])(?:reset|repair|adopt|editor|annotation|auto)(?:_|:|$)")
|
|
Scalar = bool | float | str
|
|
|
|
|
|
class _ReadOnlyParams:
|
|
def __init__(self, params):
|
|
self._params = params
|
|
|
|
def __getattr__(self, name):
|
|
if name not in ("get", "get_bool", "get_int", "get_float", "get_type", "get_default_value", "get_param_path", "get_params_path"):
|
|
raise AttributeError(f"Analytics cannot use Params.{name}")
|
|
return getattr(self._params, name)
|
|
|
|
|
|
def _number(value, low, high):
|
|
if not isinstance(value, str) or NUMBER.fullmatch(value) is None:
|
|
return None
|
|
number = float(value)
|
|
return number if math.isfinite(number) and low <= number <= high else None
|
|
|
|
|
|
def _value(row, value):
|
|
if not isinstance(value, str) or len(value) > MAX_ENUM:
|
|
return None
|
|
if row.choices == ("Off", "On") or not row.choices and row.default_value in ("Off", "On"):
|
|
return {"Off": False, "On": True}.get(value)
|
|
if (math.isfinite(row.minimum) and math.isfinite(row.maximum) and row.maximum > row.minimum and
|
|
(row.step > 0 or row.default_value is not None)):
|
|
numeric = _number(value, row.minimum, row.maximum)
|
|
if numeric is not None:
|
|
return numeric
|
|
if isinstance(value, str) and value in row.choices and len(value) <= MAX_ENUM:
|
|
return value
|
|
return None
|
|
|
|
|
|
def _eligible(row):
|
|
return (isinstance(row.key, str) and KEY.fullmatch(row.key) is not None and
|
|
not row.page and row.key not in EXCLUDED_KEYS and COMMAND.search(row.key) is None and
|
|
not row.key.startswith(EXCLUDED_PREFIXES) and
|
|
not row.actions and bool(row.choices or row.default_value in ("Off", "On") or row.step > 0 or
|
|
row.default_value is not None and row.maximum > row.minimum))
|
|
|
|
|
|
def _fields(rows: Iterable[tuple[str, object]]) -> dict[str, Scalar]:
|
|
records = {}
|
|
identities = {}
|
|
collisions = set()
|
|
for page, row in rows:
|
|
if not _eligible(row):
|
|
continue
|
|
identity = (page, row.key)
|
|
suffix = re.sub(r"[^A-Za-z0-9_]", "_", f"{page}_{row.key}")
|
|
if suffix in identities and identities[suffix] != identity:
|
|
collisions.add(suffix)
|
|
continue
|
|
identities[suffix] = identity
|
|
value, default = _value(row, row.value), _value(row, row.default_value)
|
|
fields = {f"valid_{suffix}": value is not None, f"default_known_{suffix}": default is not None}
|
|
if value is not None:
|
|
fields[f"setting_{suffix}"] = value
|
|
if default is not None:
|
|
fields[f"default_{suffix}"] = default
|
|
records[suffix] = fields
|
|
result: dict[str, Scalar] = {}
|
|
truncated = False
|
|
count = 0
|
|
for suffix in sorted(records):
|
|
fields = {f"valid_{suffix}": False, f"default_known_{suffix}": False} if suffix in collisions else records[suffix]
|
|
candidate = {**result, **fields}
|
|
if (count >= MAX_SETTINGS or len(candidate) > MAX_FIELDS or
|
|
len(json.dumps(candidate, allow_nan=False).encode()) > MAX_BYTES - 128):
|
|
truncated = True
|
|
break
|
|
result = candidate
|
|
count += 1
|
|
return {**result, "settings_schema_version": 1.0, "settings_count": float(count), "settings_truncated": truncated}
|
|
|
|
|
|
def _owner_rows(params, CP, profile):
|
|
from openpilot.common.hardware import HARDWARE
|
|
from openpilot.starpilot.galaxy.settings import PAGES, AuthorityContext, _qualified
|
|
from openpilot.starpilot.ui.appearance_owner import AppearanceOwner
|
|
from openpilot.starpilot.ui.display_owner import DisplayOwner
|
|
from openpilot.starpilot.ui.feature_settings_owner import FeatureSettingsOwner
|
|
from openpilot.starpilot.ui.feature_settings_state import FeaturePage
|
|
from openpilot.starpilot.ui.presentation import Profile
|
|
from openpilot.starpilot.ui.pip_owner import PiPOwner
|
|
from openpilot.starpilot.ui.sentry_owner import SentryOwner
|
|
from openpilot.starpilot.ui.sounds_owner import SoundsOwner
|
|
from openpilot.starpilot.ui.vasm_owner import VASMOwner
|
|
|
|
params = _ReadOnlyParams(params)
|
|
parked = params.get("IsOffroad") is True
|
|
profile = Profile(profile) if profile is not None else Profile.COMPACT if HARDWARE.get_device_type() == "mici" else Profile.LARGE
|
|
context = AuthorityContext(parked, CP, None)
|
|
owner = FeatureSettingsOwner(params, lambda group: _qualified(context, group),
|
|
vehicle_fingerprint=lambda: str(CP.carFingerprint) if CP is not None else None,
|
|
vehicle_params=lambda: CP, show_cruise_intervals=True)
|
|
system_long = bool(CP is not None and CP.openpilotLongitudinalControl)
|
|
lateral_context = _qualified(context, "lane")
|
|
separate = {"hub", "appearance", "display", "sounds", "pip", "sentry", "vasm"}
|
|
pages = sorted(({str(page) for page in FeaturePage} | PAGES) - separate)
|
|
for page in pages:
|
|
state = owner.snapshot(page, parked=parked, system_long=system_long, lateral_context=lateral_context, metric=False)
|
|
yield from ((page, row) for row in state.rows)
|
|
for state in (AppearanceOwner(params, lambda: parked).snapshot(profile),
|
|
DisplayOwner(params, lambda: parked).snapshot(profile), SoundsOwner(params, lambda: parked).snapshot(),
|
|
PiPOwner(params, lambda: parked, lambda: False).snapshot(), SentryOwner(params, lambda: parked).snapshot(),
|
|
VASMOwner(params, lambda: parked).snapshot()):
|
|
yield from ((state.page, row) for row in state.rows)
|
|
|
|
|
|
def collect_settings(params, CP, *, profile=None) -> dict[str, Scalar]:
|
|
"""Saved/default choices only; no live actuation, source blobs, or mutations."""
|
|
return _fields(_owner_rows(params, CP, profile))
|