mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-08 13:33:42 +08:00
223 lines
13 KiB
Python
223 lines
13 KiB
Python
"""Shared saved controller preference for Galaxy and native driving settings."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import replace
|
|
import json
|
|
|
|
from openpilot.starpilot.lateral.controller_selection import (
|
|
DOCUMENT_KEY, LEARNING_OFF_KEY, MAX_DOCUMENT_BYTES, ControllerMode, policy_for, replace_mode, selection_from_bytes,
|
|
)
|
|
from openpilot.starpilot.saved_document import commit_exact
|
|
from openpilot.starpilot.saved_source import read_saved
|
|
from openpilot.starpilot.lateral.lane_change_preferences import (
|
|
KEY as LANE_CHANGE_KEY, MAX_BYTES as LANE_CHANGE_LIMIT, LaneChangePolicy, decode as decode_lane_change, to_value,
|
|
)
|
|
from openpilot.starpilot.ui.feature_settings_state import FeatureRow, FeatureSettingsRequest
|
|
|
|
|
|
CHOICES = ('Stock Controller', 'StarPilot Controller')
|
|
MODES = dict(zip(CHOICES, (ControllerMode.STANDARD, ControllerMode.STARPILOT), strict=True))
|
|
TUNING_GUIDANCE = ("StarPilot's torque controller can provide higher low-speed steering accuracy, but may require additional tuning. " +
|
|
"Join the StarPilot Discord and open a tuning request: https://firestar.link/discord")
|
|
SETUP_ACTION = 'torque_prepare_firestar'
|
|
SETUP_QUESTION = 'Change tuning preparation? Turning it off restores your previous steering preferences.'
|
|
SETUP_DEFAULTS = {'TurnAssist': b'0', LEARNING_OFF_KEY: b'1', 'LaneCentering': b'0',
|
|
'LaneCenterOffset': b'0.0', 'LaneCenteringE2EAuthority': b'1.0', 'LaneCenteringPauseOnSignal': b'1'}
|
|
SETUP_LIMITS = {**dict.fromkeys(SETUP_DEFAULTS, 128), LANE_CHANGE_KEY: LANE_CHANGE_LIMIT, DOCUMENT_KEY: MAX_DOCUMENT_BYTES}
|
|
|
|
|
|
class ControllerFeature:
|
|
def __init__(self, owner):
|
|
self.params = owner.params
|
|
self.authority = owner.authority
|
|
self.vehicle_fingerprint = owner.vehicle_fingerprint
|
|
self.vehicle_params = owner.vehicle_params
|
|
|
|
def capability(self, CP=None) -> tuple | None:
|
|
CP = self.vehicle_params() if CP is None else CP
|
|
if CP is None or CP.notCar:
|
|
return None
|
|
try:
|
|
policy = policy_for(CP)
|
|
if policy is None:
|
|
return None
|
|
tune = CP.lateralTuning.torque
|
|
firmware = tuple((str(fw.ecu), bytes(fw.fwVersion)) for fw in CP.carFw)
|
|
return (str(CP.brand), str(CP.carFingerprint), policy, str(CP.steerControlType),
|
|
str(CP.lateralTuning.which()), bool(CP.passive), bool(CP.dashcamOnly), bool(CP.notCar),
|
|
int(CP.flags), int(CP.alternativeExperience), float(tune.latAccelFactor),
|
|
float(tune.latAccelOffset), float(tune.friction), float(tune.steeringAngleDeadzoneDeg),
|
|
float(CP.steerActuatorDelay), float(CP.steerLimitTimer), str(CP.carVin), firmware)
|
|
except (AttributeError, TypeError, ValueError, OverflowError):
|
|
return None
|
|
|
|
def _setup_sources(self) -> dict[str, bytes | None] | None:
|
|
sources = {}
|
|
for key, limit in SETUP_LIMITS.items():
|
|
raw, readable = read_saved(self.params, key, limit)
|
|
if not readable:
|
|
return None
|
|
sources[key] = raw
|
|
return sources
|
|
|
|
@staticmethod
|
|
def _setup_values(CP, sources: dict[str, bytes | None]) -> dict[str, bytes]:
|
|
lane_raw = sources[LANE_CHANGE_KEY]
|
|
lane = LaneChangePolicy() if lane_raw is None else decode_lane_change(lane_raw)
|
|
if lane is None:
|
|
raise ValueError('Invalid lane-change preferences')
|
|
# Gap reduction is longitudinal; retain it while resetting lateral behavior.
|
|
lane = replace(LaneChangePolicy(), close_gap=lane.close_gap, close_gap_seconds=lane.close_gap_seconds)
|
|
return {**SETUP_DEFAULTS, LANE_CHANGE_KEY: json.dumps(to_value(lane), sort_keys=True, separators=(',', ':')).encode(),
|
|
DOCUMENT_KEY: replace_mode(sources[DOCUMENT_KEY], CP, ControllerMode.STARPILOT)}
|
|
|
|
def setup_row(self) -> FeatureRow | None:
|
|
CP = self.vehicle_params()
|
|
capability = self.capability(CP)
|
|
if capability is None:
|
|
return None
|
|
sources = self._setup_sources()
|
|
valid = sources is not None
|
|
if sources is not None:
|
|
try:
|
|
self._setup_values(CP, sources)
|
|
except (ValueError, TypeError, UnicodeError, OverflowError):
|
|
valid = False
|
|
from openpilot.starpilot.ui.tuning_preparation import KEY, LIMIT, decode
|
|
journal_raw, readable = read_saved(self.params, KEY, LIMIT)
|
|
journal = decode(journal_raw, SETUP_LIMITS)
|
|
active = isinstance(journal, dict) and journal['vehicle'] == str(CP.carFingerprint)
|
|
complete = active and sources == journal['prepared']
|
|
conflict = isinstance(journal, dict) and (journal['vehicle'] != str(CP.carFingerprint) or
|
|
any(raw not in (journal['prior'][key], journal['prepared'][key]) for key, raw in (sources or {}).items()))
|
|
return FeatureRow(SETUP_ACTION, 'Prep My Vehicle for Tuning', 'On' if active else 'Off',
|
|
journal_raw, choices=('Off', 'On'), default_value='Off',
|
|
available=valid and readable and journal is not False and not conflict and
|
|
self.authority('parked_preferences') and self.authority('preferences'),
|
|
reason=('Enable before recording a route for a tuning request on the StarPilot Discord. ' +
|
|
'Turn off to restore your previous tune.' if not conflict and (not active or complete) else
|
|
'Preparation was interrupted. Turn off to restore your previous tune.' if not conflict else
|
|
'Settings changed during preparation; your saved tune has been kept.'),
|
|
capability=capability, dependencies=tuple((key, raw) for key, raw in (sources or {}).items()))
|
|
|
|
def apply_setup(self, request: FeatureSettingsRequest) -> bool:
|
|
from openpilot.starpilot.ui.tuning_preparation import transition
|
|
row = self.setup_row()
|
|
if (row is None or not row.available or request.key != SETUP_ACTION or request.value not in row.choices or
|
|
request.expected != row.source or request.capability != row.capability or request.dependencies != row.dependencies or
|
|
request.vehicle_fingerprint != self.vehicle_fingerprint()):
|
|
return False
|
|
CP = self.vehicle_params()
|
|
sources = dict(row.dependencies)
|
|
try:
|
|
desired = self._setup_values(CP, sources)
|
|
except (ValueError, TypeError, UnicodeError, OverflowError):
|
|
return False
|
|
capability = row.capability
|
|
def authorized():
|
|
return (self.authority('parked_preferences') and self.authority('preferences') and
|
|
self.vehicle_fingerprint() == request.vehicle_fingerprint and self.capability() == capability)
|
|
return transition(self.params, str(CP.carFingerprint), sources, desired, request.expected,
|
|
request.value == 'On', authorized)
|
|
|
|
def row(self) -> FeatureRow | None:
|
|
CP = self.vehicle_params()
|
|
capability = self.capability(CP)
|
|
if capability is None:
|
|
return None
|
|
raw, readable = read_saved(self.params, DOCUMENT_KEY, MAX_DOCUMENT_BYTES)
|
|
selected = selection_from_bytes(CP, raw) if readable else None
|
|
valid = readable and selected is not None and selected.source != 'invalid'
|
|
value = CHOICES[0] if valid and selected.mode == ControllerMode.STANDARD else \
|
|
CHOICES[1] if valid else 'Invalid saved controller choice'
|
|
learning, learning_readable = read_saved(self.params, LEARNING_OFF_KEY, 1)
|
|
learning_valid = learning_readable and learning in (None, b'0', b'1')
|
|
allowed = valid and learning_valid and self.authority('preferences')
|
|
reason = ('Choose the steering controller for your next drive. ' + TUNING_GUIDANCE if valid and learning_valid else
|
|
'Saved steering preferences are unreadable or invalid; no change was made.')
|
|
return FeatureRow(DOCUMENT_KEY, 'Steering Controller', value, raw, CHOICES if valid else (),
|
|
available=allowed, reason=reason, capability=capability, dependencies=((LEARNING_OFF_KEY, learning),),
|
|
default_value=CHOICES[0] if selection_from_bytes(CP, None).mode == ControllerMode.STANDARD else CHOICES[1])
|
|
|
|
def learning_row(self) -> FeatureRow | None:
|
|
CP = self.vehicle_params()
|
|
capability = self.capability(CP)
|
|
if capability is None:
|
|
return None
|
|
controller, controller_readable = read_saved(self.params, DOCUMENT_KEY, MAX_DOCUMENT_BYTES)
|
|
selection = selection_from_bytes(CP, controller)
|
|
raw, readable = read_saved(self.params, LEARNING_OFF_KEY, 1)
|
|
valid = readable and raw in (None, b'0', b'1') and controller_readable and selection.source != 'invalid'
|
|
starpilot = selection.mode == ControllerMode.STARPILOT
|
|
value = ('Off' if starpilot or raw == b'1' else 'On') if valid else 'Invalid saved preference'
|
|
reason = ('StarPilot uses the vehicle tune. ' + TUNING_GUIDANCE
|
|
if starpilot else 'Saved for the next drive. Turn on only if you want Stock Controller to learn torque values.')
|
|
return FeatureRow(LEARNING_OFF_KEY, 'Automatic Steering Learning', value, raw, ('Off', 'On') if valid else (),
|
|
available=valid and not starpilot and self.authority('preferences'),
|
|
reason=reason if valid else 'Saved controller or learning preference is unreadable or invalid.',
|
|
capability=capability, dependencies=((DOCUMENT_KEY, controller),), default_value="On")
|
|
|
|
def apply_learning(self, request: FeatureSettingsRequest) -> bool:
|
|
row = self.learning_row()
|
|
if (row is None or not row.available or request.key != LEARNING_OFF_KEY or request.value not in row.choices or
|
|
request.capability != row.capability or request.expected != row.source or request.dependencies != row.dependencies or
|
|
request.related_source is not None or request.display_unit or request.direction or
|
|
not request.vehicle_fingerprint or request.vehicle_fingerprint != self.vehicle_fingerprint()):
|
|
return False
|
|
|
|
def authorized() -> bool:
|
|
current = self.learning_row()
|
|
return (self.vehicle_fingerprint() == request.vehicle_fingerprint and current is not None and current.available and
|
|
current.capability == request.capability and current.dependencies == request.dependencies)
|
|
|
|
result = commit_exact(self.params, key=LEARNING_OFF_KEY, max_bytes=1,
|
|
raw=b'0' if request.value == 'On' else b'1', expected=request.expected,
|
|
authorized=authorized, temp_prefix='.torque-learning-')
|
|
return result.verified
|
|
|
|
def apply(self, request: FeatureSettingsRequest) -> bool:
|
|
if (request.key != DOCUMENT_KEY or request.value not in MODES or request.capability is None or
|
|
request.related_source is not None or request.display_unit or request.direction or
|
|
not request.vehicle_fingerprint or request.vehicle_fingerprint != self.vehicle_fingerprint()):
|
|
return False
|
|
CP = self.vehicle_params()
|
|
if CP is None or request.capability != self.capability(CP):
|
|
return False
|
|
raw, readable = read_saved(self.params, DOCUMENT_KEY, MAX_DOCUMENT_BYTES)
|
|
learning, learning_readable = read_saved(self.params, LEARNING_OFF_KEY, 1)
|
|
if (not readable or raw != request.expected or not self.authority('preferences') or
|
|
not learning_readable or learning not in (None, b'0', b'1') or request.dependencies != ((LEARNING_OFF_KEY, learning),)):
|
|
return False
|
|
try:
|
|
selected = selection_from_bytes(CP, raw)
|
|
if selected.source == 'invalid':
|
|
return False
|
|
encoded = replace_mode(raw, CP, MODES[request.value])
|
|
except (UnicodeError, ValueError, TypeError, OverflowError):
|
|
return False
|
|
|
|
def authorized() -> bool:
|
|
current, readable = read_saved(self.params, LEARNING_OFF_KEY, 1)
|
|
return (self.authority('preferences') and self.vehicle_fingerprint() == request.vehicle_fingerprint and
|
|
self.capability() == request.capability and readable and current == learning)
|
|
|
|
if learning != b'1' and (MODES[request.value] == ControllerMode.STARPILOT or selected.mode == ControllerMode.STARPILOT):
|
|
def authorize_learning_off() -> bool:
|
|
current, readable = read_saved(self.params, DOCUMENT_KEY, MAX_DOCUMENT_BYTES)
|
|
return authorized() and readable and current == request.expected
|
|
|
|
# Persist the conservative side first, including a return from a default
|
|
# StarPilot choice. An interrupted save can leave learning off, but never
|
|
# enables it or saves StarPilot with an enabled learning preference.
|
|
result = commit_exact(self.params, key=LEARNING_OFF_KEY, max_bytes=1, raw=b'1', expected=learning,
|
|
authorized=authorize_learning_off, temp_prefix='.torque-learning-')
|
|
if not result.verified:
|
|
return False
|
|
learning = b'1'
|
|
|
|
result = commit_exact(self.params, key=DOCUMENT_KEY, max_bytes=MAX_DOCUMENT_BYTES,
|
|
raw=encoded, expected=request.expected, authorized=authorized,
|
|
temp_prefix='.lateral-controller-')
|
|
return result.verified
|