mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-07 04:53:47 +08:00
330 lines
20 KiB
Python
330 lines
20 KiB
Python
"""Shared native and Galaxy presentation of saved conditional modes."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import fields, replace
|
|
import hashlib
|
|
|
|
from openpilot.starpilot.conditional_mode.button_actions import (
|
|
BUTTON_PREFIX, MEDIA_KEYS, DISTANCE_KEYS, capture_sources, commit_assignment, display_action, media_capability,
|
|
distance_capability, assignment_capability, runtime_map_ready,
|
|
)
|
|
|
|
from openpilot.starpilot.conditional_mode.actions import (
|
|
BOOLEAN_FIELDS, DOCUMENT_KEY, MANUAL_RESET, MODE, NUMBER_FIELDS, PREFIX, RESET, commit, commit_manual, commit_mode,
|
|
display_number, edit, field_limit, stock_document,
|
|
)
|
|
from openpilot.starpilot.conditional_mode.preferences import (
|
|
MAX_DOCUMENT_BYTES, PreferenceError, SavedPreferences, decode_preferences,
|
|
)
|
|
from openpilot.starpilot.saved_source import read_saved
|
|
from openpilot.starpilot.conditional_mode.manual_saved import KEY as MANUAL_KEY, read_codes
|
|
from openpilot.starpilot.conditional_mode.preferences import encode_preferences
|
|
from openpilot.starpilot.conditional_mode.runtime_settings import experimental_available, read_mode_sources
|
|
from openpilot.starpilot.conditional_mode.policy import ModeChoice
|
|
from openpilot.starpilot.ui.feature_settings_state import FeatureRow, FeatureSettingsRequest
|
|
|
|
|
|
LABELS = {
|
|
"speed_mps": "Speed threshold", "speed_with_lead_mps": "Speed with lead", "signal_speed_mps": "Signal speed",
|
|
"open_road": "Open road", "curves": "Curves", "curves_with_lead": "Curves with lead",
|
|
"lead": "Lead vehicle", "slower_lead": "Slower lead", "stopped_lead": "Stopped lead",
|
|
"stop_lights": "Stop lights", "model_stop_s": "Model stop time",
|
|
"signal_lane_detection": "Signal lane detection", "signal_lane_width_m": "Signal lane width",
|
|
"set_speed_margin_mps": "Set speed margin", "launch_assist": "Launch assist",
|
|
}
|
|
MODES = ("Chill", "Experimental", "Conditional Experimental", "Conditional Chill")
|
|
SIGNAL_LANE_FIELDS = frozenset(("signal_lane_detection", "signal_lane_width_m"))
|
|
SIGNAL_LANE_HELP = {
|
|
"signal_lane_detection": "Distinguish a turn from a lane change using the space beside your car. With this on, a turn signal requests " +
|
|
"Experimental only when there is no adjacent lane.",
|
|
"signal_lane_width_m": "Minimum width of the space beside your car that counts as a lane, rather than a turn.",
|
|
}
|
|
MEDIA_LABELS = ("MODE press", "MODE long press", "MODE very long press",
|
|
"Star button", "Star button long press", "Star button very long press")
|
|
|
|
|
|
|
|
def _mode_capability(cp) -> tuple | None:
|
|
if cp is None:
|
|
return None
|
|
try:
|
|
reader = cp.as_reader() if hasattr(cp, "as_reader") else cp
|
|
encoded = reader.as_builder().to_bytes()
|
|
return (str(cp.carFingerprint), hashlib.sha256(encoded).hexdigest())
|
|
except (AttributeError, TypeError, ValueError, OverflowError):
|
|
return None
|
|
|
|
def _display(number: float) -> str:
|
|
return f"{number:.2f}".rstrip("0").rstrip(".") if number else "0"
|
|
|
|
|
|
def _unit(field: str, metric: bool) -> str:
|
|
if field == "model_stop_s":
|
|
return "s"
|
|
if field == "signal_lane_width_m":
|
|
return "m" if metric else "ft"
|
|
return "km/h" if metric else "mph"
|
|
|
|
|
|
def confirmation_question(request: FeatureSettingsRequest, label: str = "", unit: str = "") -> str:
|
|
if request.key == RESET:
|
|
return "Replace the invalid conditional mode settings with Chill defaults?"
|
|
if request.key == MANUAL_RESET:
|
|
return "Reset both remembered manual choices to Automatic?"
|
|
value = f"{request.value} {unit}".rstrip()
|
|
target = f"{label.lower()} to" if label else "to"
|
|
tail = " This resets the mode's remembered choice to Automatic." if request.key.endswith(":persist_manual") else ""
|
|
return f"Set {target} {value}?{tail}"
|
|
|
|
|
|
class ConditionalFeature:
|
|
def __init__(self, owner):
|
|
self.owner = owner
|
|
|
|
def _source(self):
|
|
document, readable = read_saved(self.owner.params, DOCUMENT_KEY, MAX_DOCUMENT_BYTES)
|
|
units, unit_readable = read_saved(self.owner.params, "IsMetric", 8)
|
|
metric = units is None or units == b"0" or units == b"1"
|
|
return document, units, readable and unit_readable, metric, units == b"1"
|
|
|
|
def _media_rows(self, document: bytes | None, safe: bytes | None, safe_readable: bool,
|
|
*, config_valid: bool) -> list[FeatureRow]:
|
|
cp = self.owner.vehicle_params()
|
|
gm = distance_capability(cp)
|
|
switchback_only = not gm and cp is not None and not cp.openpilotLongitudinalControl
|
|
capability = gm or media_capability(cp, switchback_only=switchback_only)
|
|
keys = DISTANCE_KEYS if gm else MEDIA_KEYS
|
|
labels = ("Distance press", "Distance long press", "Distance very long press") if gm else MEDIA_LABELS
|
|
if capability is None:
|
|
return []
|
|
sources = capture_sources(self.owner.params, document, safe)
|
|
enabled = (self.owner.authority("switchback_wheel" if switchback_only else "conditional_wheel") and config_valid and
|
|
safe_readable and safe in (None, b"0", b"1") and sources.readable)
|
|
ready = runtime_map_ready(sources, cp=cp) and all(display_action(sources.raw(key))[0] != "Unsupported saved action"
|
|
for key in keys)
|
|
rows = [] if ready else [FeatureRow("", "Button assignments",
|
|
"Review saved actions",
|
|
reason="Some saved actions are not supported. Choose Off for those buttons before assigning a new action.")]
|
|
for key, label in zip(keys, labels, strict=True):
|
|
raw = sources.raw(key)
|
|
value, choices, repair = display_action(raw)
|
|
if gm:
|
|
if raw not in (None, b"0", b"6"):
|
|
value, choices, repair = "Unsupported saved action", (), "Off"
|
|
else:
|
|
choices = ("Off", "Toggle traffic mode")
|
|
if switchback_only:
|
|
choices = ("Off", "Switchback Mode") if sources.readable else ()
|
|
rows.append(FeatureRow(BUTTON_PREFIX + key, label, value if sources.readable else "Unavailable saved action",
|
|
raw, choices if sources.readable else (), available=enabled,
|
|
reason=("Toggle Traffic Mode from this button. May take effect this drive." if gm else
|
|
"Choose this button action. May take effect this drive.") if enabled else
|
|
"Reload to read saved button actions",
|
|
vehicle_fingerprint=self.owner.vehicle_fingerprint(), capability=capability,
|
|
dependencies=sources.dependencies, repair_value=repair if sources.readable else "", default_value="Off"))
|
|
return rows
|
|
|
|
def wheel_rows(self) -> list[FeatureRow]:
|
|
document, _, readable, _, _ = self._source()
|
|
safe, safe_readable = read_saved(self.owner.params, "SafeMode", 8)
|
|
try:
|
|
if document is not None:
|
|
decode_preferences(document)
|
|
valid = readable
|
|
except PreferenceError:
|
|
valid = False
|
|
return self._media_rows(document, safe, safe_readable, config_valid=valid)
|
|
|
|
def rows(self, page: str, parked: bool) -> list[FeatureRow]:
|
|
document, units, readable, valid_units, metric = self._source()
|
|
allowed = self.owner.authority("preferences")
|
|
repair_allowed = parked and self.owner.authority("parked_preferences")
|
|
unavailable = "Saved settings are unavailable"
|
|
dependencies = (("IsMetric", units),)
|
|
safe, safe_readable = read_saved(self.owner.params, "SafeMode", 8) if page == "conditional" else (None, True)
|
|
if not readable:
|
|
return [FeatureRow("", "Saved choices", "Unavailable", reason="Saved source cannot be read; no changes allowed")]
|
|
try:
|
|
preferences = SavedPreferences() if document is None else decode_preferences(document)
|
|
except PreferenceError:
|
|
return [FeatureRow("", "Saved choices", "Invalid", reason="Existing bytes remain unchanged until explicit reset"),
|
|
FeatureRow(RESET, "Restore Chill defaults", "", document,
|
|
available=repair_allowed, reason="Replaces the invalid document after review",
|
|
dependencies=dependencies)]
|
|
rows = []
|
|
manual = read_codes(self.owner.params)
|
|
if page != "conditional":
|
|
safe, safe_readable = read_saved(self.owner.params, "SafeMode", 8)
|
|
manual_dependencies = dependencies + ((MANUAL_KEY, manual.raw), ("SafeMode", safe))
|
|
manual_available = repair_allowed and safe_readable and safe in (None, b"0", b"1")
|
|
if page == "conditional":
|
|
(mode_document, mode_readable), (baseline, baseline_readable), (mode_safe, mode_safe_readable) = read_mode_sources(self.owner.params)
|
|
mode_valid = (mode_readable and mode_document == document and baseline_readable and baseline in (None, b"0", b"1") and
|
|
mode_safe_readable and mode_safe in (None, b"0", b"1"))
|
|
choice = {"stock": "Experimental" if baseline == b"1" else "Chill",
|
|
"conditional_experimental": "Conditional Experimental", "conditional_chill": "Conditional Chill"}[preferences.mode.value]
|
|
cp = self.owner.vehicle_params()
|
|
choices = MODES if choice == "Experimental" or experimental_available(cp) and mode_safe in (None, b"0") else tuple(
|
|
mode for mode in MODES if mode != "Experimental")
|
|
rows.append(FeatureRow(MODE, "Saved driving mode", choice if mode_valid else "Unavailable", document,
|
|
choices if mode_valid else (), default_value=MODES[0], available=allowed and mode_valid,
|
|
reason="Choose Chill, Experimental, or automatic switching from your saved conditions." if allowed and mode_valid else unavailable,
|
|
vehicle_fingerprint=self.owner.vehicle_fingerprint(), capability=_mode_capability(cp),
|
|
dependencies=dependencies + (("ExperimentalMode", baseline), ("SafeMode", mode_safe))))
|
|
rows.append(FeatureRow("", "Experimental conditions", "Saved options", page="conditional/cem", available=True))
|
|
rows.append(FeatureRow("", "Chill conditions", "Saved options", page="conditional/ccm", available=True))
|
|
if manual.status in ("invalid", "read_error"):
|
|
rows.append(FeatureRow("", "Remembered manual choices", "Invalid" if manual.status == "invalid" else "Unavailable"))
|
|
if manual.status == "invalid":
|
|
rows.append(FeatureRow(MANUAL_RESET, "Reset remembered manual choices", "Both modes to Automatic", document,
|
|
available=manual_available,
|
|
dependencies=manual_dependencies))
|
|
return rows
|
|
lane_page = page == "lane_change"
|
|
section = "cem" if page in ("conditional/cem", "lane_change") else "ccm"
|
|
option = preferences.cem if section == "cem" else preferences.ccm
|
|
for field in fields(option):
|
|
name = field.name
|
|
if (name in SIGNAL_LANE_FIELDS) != lane_page:
|
|
continue
|
|
if name == "persist_manual":
|
|
rows.append(FeatureRow(PREFIX + section + ":persist_manual", "Remember manual choice",
|
|
"On" if getattr(option, name) else "Off", document, ("Off", "On"),
|
|
default_value="On" if getattr(getattr(decode_preferences(stock_document()), section), name) else "Off",
|
|
available=manual_available and manual.status in ("valid", "absent"),
|
|
reason="Keeps your manual choice across drives. Changing this setting first resets the choice to Automatic."
|
|
if manual_available and
|
|
manual.status in ("valid", "absent") else
|
|
"Saved manual choices need repair" if manual_available else unavailable,
|
|
dependencies=manual_dependencies))
|
|
continue
|
|
value = getattr(option, name)
|
|
key = PREFIX + section + ":" + name
|
|
if name in BOOLEAN_FIELDS[section]:
|
|
rows.append(FeatureRow(key, LABELS[name], "On" if value else "Off", document, ("Off", "On"),
|
|
default_value="On" if getattr(getattr(decode_preferences(stock_document()), section), name) else "Off",
|
|
available=allowed, reason=SIGNAL_LANE_HELP.get(name, "") if allowed else unavailable,
|
|
dependencies=dependencies))
|
|
elif name in NUMBER_FIELDS[section]:
|
|
displayed = display_number(name, value, metric)
|
|
unit = _unit(name, metric)
|
|
rows.append(FeatureRow(key, LABELS[name], _display(displayed) if valid_units else "Invalid saved units", document,
|
|
default_value=_display(display_number(name, getattr(getattr(decode_preferences(stock_document()), section), name), metric)),
|
|
step=0.1 if name == "model_stop_s" else 1.0 if valid_units else 0.0,
|
|
minimum=0.0, maximum=max(field_limit(section, name, metric), float(_display(displayed))),
|
|
unit=unit if valid_units else "",
|
|
available=allowed and valid_units, reason=SIGNAL_LANE_HELP.get(name, "") if allowed and valid_units else
|
|
"Invalid saved units" if not valid_units else unavailable,
|
|
dependencies=dependencies, display_unit=unit if valid_units else ""))
|
|
return rows
|
|
|
|
def apply(self, request: FeatureSettingsRequest) -> bool:
|
|
if request.key.startswith(BUTTON_PREFIX):
|
|
key = request.key.removeprefix(BUTTON_PREFIX)
|
|
if key not in (*MEDIA_KEYS, *DISTANCE_KEYS) or not request.vehicle_fingerprint or request.capability is None:
|
|
return False
|
|
cp = self.owner.vehicle_params()
|
|
switchback_only = key in MEDIA_KEYS and cp is not None and not cp.openpilotLongitudinalControl
|
|
if switchback_only and request.value not in ("Off", "Switchback Mode"):
|
|
return False
|
|
def authorized_button() -> bool:
|
|
return bool(self.owner.vehicle_fingerprint() == request.vehicle_fingerprint and
|
|
(media_capability(self.owner.vehicle_params(), switchback_only=True) if switchback_only else
|
|
assignment_capability(self.owner.vehicle_params(), key)) == request.capability and
|
|
self.owner.authority("switchback_wheel" if switchback_only else "conditional_wheel"))
|
|
result = commit_assignment(self.owner.params, key=key, choice=request.value,
|
|
expected=request.expected, dependencies=request.dependencies,
|
|
authorized=authorized_button)
|
|
return result.committed and result.verified
|
|
if request.key == MODE:
|
|
(document, readable), (baseline, baseline_readable), (safe, safe_readable) = read_mode_sources(self.owner.params)
|
|
units, units_readable = read_saved(self.owner.params, "IsMetric", 8)
|
|
dependencies = (("IsMetric", units), ("ExperimentalMode", baseline), ("SafeMode", safe))
|
|
if (not readable or not baseline_readable or not safe_readable or not units_readable or
|
|
document != request.expected or request.dependencies != dependencies or
|
|
baseline not in (None, b"0", b"1") or safe not in (None, b"0", b"1") or request.value not in MODES):
|
|
return False
|
|
|
|
def authorized_mode() -> bool:
|
|
cp = self.owner.vehicle_params()
|
|
return bool(self.owner.authority("preferences") and
|
|
(request.value != "Experimental" or request.capability is not None and
|
|
_mode_capability(cp) == request.capability and self.owner.vehicle_fingerprint() == request.capability[0] and
|
|
experimental_available(cp) and safe in (None, b"0")))
|
|
|
|
if not authorized_mode():
|
|
return False
|
|
try:
|
|
preferences = SavedPreferences() if document is None else decode_preferences(document)
|
|
value = "Stock" if request.value in ("Chill", "Experimental") else request.value
|
|
raw = edit(preferences, MODE, value, metric=units == b"1")
|
|
except PreferenceError:
|
|
return False
|
|
next_baseline = b"1" if request.value == "Experimental" else b"0" if request.value == "Chill" else baseline or b"0"
|
|
result = commit_mode(self.owner.params, raw, next_baseline, expected=document, expected_baseline=baseline,
|
|
expected_units=units, expected_safe=safe, authorized=authorized_mode)
|
|
return result.committed and result.verified
|
|
if (request.key in (RESET, MANUAL_RESET) and not request.confirmation) or \
|
|
request.capability is not None or request.vehicle_fingerprint is not None:
|
|
return False
|
|
manual_action = request.key == MANUAL_RESET or request.key in (
|
|
PREFIX + "cem:persist_manual", PREFIX + "ccm:persist_manual")
|
|
def authorized() -> bool:
|
|
group = "parked_preferences" if manual_action or request.key == RESET else "preferences"
|
|
return self.owner.authority(group)
|
|
if not authorized():
|
|
return False
|
|
document, units, readable, valid_units, metric = self._source()
|
|
if not readable or document != request.expected:
|
|
return False
|
|
manual = read_codes(self.owner.params) if manual_action else None
|
|
safe, safe_readable = read_saved(self.owner.params, "SafeMode", 8) if manual_action else (None, True)
|
|
dependencies = (("IsMetric", units),) + (((MANUAL_KEY, manual.raw), ("SafeMode", safe)) if manual is not None else ())
|
|
if request.dependencies != dependencies or not safe_readable or safe not in (None, b"0", b"1"):
|
|
return False
|
|
try:
|
|
if request.key == MANUAL_RESET:
|
|
if request.value != "confirm" or manual is None or manual.status != "invalid":
|
|
return False
|
|
result = commit_manual(self.owner.params, expected_config=document, expected_units=units,
|
|
expected_safe=safe, expected_manual=manual.raw, authorized=authorized,
|
|
choice=None, config_raw=None)
|
|
return result.committed and result.verified
|
|
if request.key == RESET:
|
|
if request.value != "confirm" or document is None:
|
|
return False
|
|
try:
|
|
decode_preferences(document)
|
|
return False
|
|
except PreferenceError:
|
|
raw = stock_document()
|
|
else:
|
|
preferences = SavedPreferences() if document is None else decode_preferences(document)
|
|
if request.key in (PREFIX + "cem:persist_manual", PREFIX + "ccm:persist_manual"):
|
|
if request.value not in ("On", "Off") or manual is None or manual.status not in ("valid", "absent"):
|
|
return False
|
|
section = request.key.split(":")[1]
|
|
option = preferences.cem if section == "cem" else preferences.ccm
|
|
raw = encode_preferences(replace(preferences, **{section: replace(option, persist_manual=request.value == "On")}))
|
|
choice = ModeChoice.CEM if section == "cem" else ModeChoice.CCM
|
|
result = commit_manual(self.owner.params, expected_config=document, expected_units=units,
|
|
expected_safe=safe, expected_manual=manual.raw, authorized=authorized,
|
|
choice=choice, config_raw=raw)
|
|
return result.committed and result.verified
|
|
if request.key.startswith(PREFIX) and request.key != MODE and not valid_units:
|
|
return False
|
|
if request.key.startswith(PREFIX) and request.key != MODE:
|
|
field = request.key.split(":")[-1]
|
|
expected_unit = (_unit(field, metric) if field in
|
|
("model_stop_s", "signal_lane_width_m", "speed_mps", "speed_with_lead_mps",
|
|
"signal_speed_mps", "set_speed_margin_mps") else "")
|
|
if request.display_unit != expected_unit:
|
|
return False
|
|
value = "Stock" if request.key == MODE and request.value == "Chill" else request.value
|
|
raw = edit(preferences, request.key, value, metric=metric)
|
|
except (PreferenceError, ValueError, TypeError, OverflowError):
|
|
return False
|
|
result = commit(self.owner.params, raw, document, units, authorized)
|
|
return result.committed and result.verified
|