Files
Prabhaav Pillai c532223fa0 Restore V-Adj Spot Monitoring warnings and polish Galaxy settings, cameras, and updates
Bundle the verified VASM model, enable it at startup, and split VASM and vision speed-sign inference across little cores when both are active.
Restore VASM warnings in PiP activation, red outlines, and onroad blind-spot borders, including expanded Android Auto views.
Improve cabin snapshots with longer warm-up, adjusted capture timeouts, a fresh preview frame, and clearer capture buttons and save prompts.
Polish Galaxy settings with dimmed gated controls and icons, clearer vehicle fingerprinting and steering availability descriptions, rounded speed-offset ranges, and immediate refresh after saving.
Improve updater startup messages and preserve update/reconnect screens across service interruptions and restarts.
Add regression tests for the updated behavior.
2026-10-19 23:07:19 -04:00

348 lines
21 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Native Torque editor adapter; strict numeric policy lives in the lateral domain."""
from __future__ import annotations
from dataclasses import replace
import json
from openpilot.starpilot.lateral.torque_supported import BOLT_VEHICLES, GM_VEHICLES
from openpilot.starpilot.lateral.controller_selection import DOCUMENT_KEY as SELECTION_KEY, selection_from_bytes
from openpilot.starpilot.lateral.torque_runtime import factor_edit_supported
from openpilot.starpilot.lateral.torque_settings import (
DOCUMENT_KEY, LEGACY_KEYS, FieldChoice, LegacyMode, PlatformProfile, bounds as torque_bounds,
interpret_legacy, parse_document, replace_field, resolve_document, serialize_document,
)
from openpilot.starpilot.ui.feature_settings_state import FeatureRow, FeatureSettingsRequest, TORQUE_CONFIRM_ACTIONS
TORQUE_NUMBERS = LEGACY_KEYS
class TorqueFeature:
def __init__(self, owner):
self.owner = owner
self.params = owner.params
self.authority = owner.authority
self.vehicle_fingerprint = owner.vehicle_fingerprint
self._capability = owner._capability
self._raw = owner._raw
self._readable = owner._readable
self._dependents = owner._dependents
def _factor_editable(self, fingerprint: str) -> bool:
if fingerprint not in GM_VEHICLES:
return True
cp = self.owner.vehicle_params()
if cp is None or not self._readable(SELECTION_KEY):
return False
selection = selection_from_bytes(cp, self._raw(SELECTION_KEY))
return selection.source != "invalid" and factor_edit_supported(cp, selection.mode)
def _torque_dependencies(self, fingerprint: str, field: str):
keys = ("ForceAutoTuneOff", *TORQUE_NUMBERS)
return self._dependents(*keys, *((SELECTION_KEY,) if field == "factor" and fingerprint in GM_VEHICLES and fingerprint not in BOLT_VEHICLES else ()))
@staticmethod
def _legacy_label(mode: LegacyMode, saved: float | None) -> str:
if mode == LegacyMode.ABSENT:
return "No saved override"
if saved is None:
raise ValueError("Missing saved torque value")
return f"Stock tracked ({saved:.2f})" if mode == LegacyMode.STOCK else f"Saved custom ({saved:.2f})"
def _torque_values(self, capability: tuple | None) -> tuple[bool, tuple[FeatureRow, ...]]:
readings = {key: self._raw(key) for key in TORQUE_NUMBERS}
try:
if capability is None:
raise ValueError("Vehicle unavailable")
legacy = interpret_legacy(readings, (capability[5], capability[6], capability[7]), fingerprint=capability[0])
statuses = (self._legacy_label(legacy.factor_mode, legacy.factor_saved),
self._legacy_label(legacy.friction_mode, legacy.friction_saved))
valid = True
except (ValueError, OverflowError):
statuses = tuple("Invalid saved value" if readings[key] is not None else "No saved override"
for key in TORQUE_NUMBERS[:2])
valid = False
return valid, tuple(FeatureRow(key, "Lateral acceleration" if key == "SteerLatAccel" else "Friction",
status, readings[key], reason="Legacy saved value; adopt to edit")
for key, status in zip(TORQUE_NUMBERS[:2], statuses, strict=True))
def rows(self, capability: tuple | None, allowed: bool, *, repair_allowed: bool | None = None) -> tuple[bool, tuple[FeatureRow, ...]]:
repair_allowed = allowed if repair_allowed is None else repair_allowed
raw = self._raw(DOCUMENT_KEY)
bolt = capability is not None and capability[0] in BOLT_VEHICLES
try:
if capability is None:
cp = self.owner.vehicle_params()
identified = bool(cp and getattr(cp, "carFingerprint", None))
return False, (FeatureRow("", "Vehicle", "Steering torque tuning is unavailable for this vehicle" if identified else
"Turn on the car and wait for vehicle identification before configuring steering"),)
fingerprint = capability[0]
basis = (capability[5], capability[6], capability[7])
if raw is None:
legacy = interpret_legacy({key: None if fingerprint in GM_VEHICLES else self._raw(key) for key in TORQUE_NUMBERS}, basis, fingerprint=fingerprint)
profiles = {fingerprint: PlatformProfile(basis,
FieldChoice("custom", legacy.factor) if legacy.factor is not None and fingerprint not in BOLT_VEHICLES else FieldChoice(),
FieldChoice("custom", legacy.friction) if legacy.friction is not None and fingerprint not in BOLT_VEHICLES else FieldChoice())}
else:
profiles = parse_document(raw)
factor, friction, review = resolve_document(profiles, fingerprint, basis)
except (ValueError, UnicodeError, OverflowError):
reset = FeatureRow("torque_reset", "Reset invalid torque profiles", "", raw,
available=repair_allowed and raw is not None and self._readable(DOCUMENT_KEY), capability=capability,
dependencies=self._dependents("ForceAutoTuneOff", *TORQUE_NUMBERS))
return False, (FeatureRow("", "Torque profile", "Invalid saved values; supplied tune in use"), reset)
if review:
profile = profiles[fingerprint]
if bolt and profile.factor.mode == "custom":
reset = FeatureRow("torque_reset_profile", "Reset this model's tuning", "Reset to configure friction", raw,
available=repair_allowed, capability=capability,
dependencies=self._dependents("AdvancedLateralTune", "ForceAutoTuneOff"))
return False, (FeatureRow("", "Friction tuning paused",
"This saved tune includes a lateral-acceleration adjustment that this Bolt editor cannot apply. " +
"Reset this model’s tune to configure friction."), reset)
details = []
summary = []
for field, label, current, choice in (("factor", "Lateral acceleration", basis[0], profile.factor),
("friction", "Friction", basis[2], profile.friction)):
low, high = torque_bounds(basis, field, fingerprint=fingerprint)
saved = f"{choice.custom_value:.4f}" if choice.mode == "custom" and choice.custom_value is not None else "Vehicle/learned"
details.append(FeatureRow("", label, f"Saved {saved} · vehicle {current:.4f} · range {low:.4f}–{high:.4f}"))
summary.append(f"{label} {saved}")
rebase = FeatureRow("torque_rebase", "Review changed vehicle tune", ", ".join(summary), raw,
available=repair_allowed, capability=capability,
dependencies=self._dependents("ForceAutoTuneOff", *TORQUE_NUMBERS))
reset = FeatureRow("torque_reset_profile", "Use vehicle values for this model", "", raw,
available=repair_allowed, capability=capability,
dependencies=self._dependents("ForceAutoTuneOff", *TORQUE_NUMBERS))
return False, (FeatureRow("", "Torque profile", "Vehicle tune changed; custom values paused"), *details, rebase, reset)
profile = profiles.get(fingerprint, PlatformProfile(basis, FieldChoice(), FieldChoice()))
from openpilot.starpilot.lateral.gm_geometry_runtime import read_geometry
cp = self.owner.vehicle_params()
force_auto = cp is not None and read_geometry(self.params, cp).force_auto
rows: list[FeatureRow] = []
for field, label, active in (("factor", "Lateral acceleration", factor), ("friction", "Friction", friction)):
if bolt and field == "factor":
continue
choice = profile.factor if field == "factor" else profile.friction
low, high = torque_bounds(basis, field, fingerprint=fingerprint)
dependencies = self._torque_dependencies(fingerprint, field)
supplied = basis[0 if field == "factor" else 2]
value = active if active is not None else supplied
displayed = str(round(value, 8))
editable = field != "factor" or self._factor_editable(fingerprint)
rows.append(FeatureRow(f"torque:{field}:value", "Lat Accel" if field == "factor" else "Friction", displayed, raw,
step=0.05 if field == "factor" else 0.01, minimum=low, maximum=high,
available=allowed and editable, capability=capability, dependencies=dependencies,
default_value="Vehicle/learned", default_key=f"torque:{field}:mode",
reason=("Selected controller uses a fixed torque conversion; factor does not change its output" if not editable else
"Saved choice temporarily ignored while Force auto is ON" if force_auto else
"Uses the supplied vehicle tune" if active is None else "Uses your custom value")))
rows.append(FeatureRow(f"torque:{field}:reset", label + " — Reset to Default", "", raw,
available=allowed and active is not None, repair_value="Reset",
capability=capability, dependencies=dependencies))
if bolt:
rows.insert(0, FeatureRow("", "Manual friction", "Compensates steering friction. First manual activation requires " +
"a driving-process restart; an active tuning owner refreshes changes during the drive."))
if fingerprint in GM_VEHICLES:
from openpilot.starpilot.ui.gain_feature import GainFeature
rows.extend(GainFeature(self.owner).rows(capability, profiles, raw, allowed, repair_allowed))
rows.extend(self._geometry_rows(capability, profiles, raw, allowed))
return True, tuple(rows)
@staticmethod
def _geometry_token(cp):
from openpilot.starpilot.lateral.gm_geometry_runtime import geometry_basis
return json.dumps(geometry_basis(cp), separators=(",", ":"), allow_nan=False).encode()
def _geometry_rows(self, capability, profiles, raw, allowed):
from openpilot.starpilot.lateral.gm_geometry_runtime import geometry_basis, read_geometry
from openpilot.starpilot.lateral.torque_runtime import gm_manual_supported_cp
from openpilot.starpilot.lateral.torque_settings import GeometryProfile
cp = self.owner.vehicle_params()
if cp is None or not gm_manual_supported_cp(cp):
return []
fingerprint = capability[0]
profile = profiles.get(fingerprint)
basis = geometry_basis(cp)
geometry = profile.geometry if profile is not None else None
if geometry is not None and geometry.basis != basis:
return [FeatureRow("", "Vehicle model tuning", "Vehicle basis changed; saved geometry paused"),
FeatureRow("torque:geometry_reset:value", "Reset saved geometry", "Reset", raw,
choices=("Reset",), available=allowed, capability=capability,
related_source=self._geometry_token(cp), dependencies=self._dependents("ForceAutoTuneOff", SELECTION_KEY))]
geometry = geometry or GeometryProfile(basis)
token = self._geometry_token(cp)
dependencies = self._dependents("ForceAutoTuneOff", SELECTION_KEY)
selected = read_geometry(self.params, cp)
rows = []
learning_labels = {"source": "Selected controller", "force_auto": "Force auto", "force_off": "Force off"}
rows.append(FeatureRow("torque:learning:value", "Automatic torque learning", learning_labels[geometry.learning], raw,
choices=tuple(learning_labels.values()), available=allowed, capability=capability,
related_source=token, dependencies=dependencies,
reason="Chooses vehicle learning or a manual override. Force off takes priority; " +
"Force auto temporarily ignores saved factor, friction and ratio."))
rows.append(FeatureRow("torque:automatic_delay:value", "Steering delay source", "Learned" if geometry.automatic_delay else "Vehicle/custom", raw,
choices=("Learned", "Vehicle/custom"), available=allowed, capability=capability,
related_source=token, dependencies=dependencies,
reason="Full delay includes the vehicle actuator delay plus 0.2 seconds; estimator continues independently."))
for field, label, choice, supplied, low, high, step in (
("ratio", "Steering ratio", geometry.ratio, basis[0], .5*basis[0], 1.5*basis[0], .1),
("full_delay", "Full steering delay", geometry.full_delay, basis[1], .01, 1., .01),
):
value = choice.custom_value if choice.mode == "custom" else supplied
assert value is not None
suppressed = selected.force_auto if field == "ratio" else geometry.automatic_delay
rows.append(FeatureRow(f"torque:{field}:value", label, str(round(value, 8)), raw,
step=step, minimum=low, maximum=high, available=allowed,
related_source=token, capability=capability, dependencies=dependencies,
default_value="Vehicle/learned" if field == "ratio" else "Vehicle full delay",
default_key=f"torque:{field}:mode", unit="s" if field == "full_delay" else "",
reason="Saved choice temporarily ignored by automatic source" if suppressed else "Custom value; estimator remains unchanged"))
return rows
def _edit_geometry(self, profiles, request, fingerprint, torque_basis):
from openpilot.starpilot.lateral.gm_geometry_runtime import geometry_basis
from openpilot.starpilot.lateral.torque_runtime import gm_manual_supported_cp
from openpilot.starpilot.lateral.torque_settings import GeometryProfile, replace_geometry
cp = self.owner.vehicle_params()
if cp is None or not gm_manual_supported_cp(cp) or request.related_source != self._geometry_token(cp):
raise ValueError("Changed geometry source")
if not {"ForceAutoTuneOff", SELECTION_KEY} <= dict(request.dependencies).keys():
raise ValueError("Missing geometry source evidence")
basis = geometry_basis(cp)
prior = profiles.get(fingerprint)
geometry = prior.geometry if prior is not None else None
geometry = geometry or GeometryProfile(basis)
_, field, part = request.key.split(":")
if field == "geometry_reset" and part == "value" and request.value == "Reset":
if prior is None:
raise ValueError("No saved geometry")
return {**profiles, fingerprint: replace(prior, geometry=None)}
if field == "learning" and part == "value":
value = {"Selected controller": "source", "Force auto": "force_auto", "Force off": "force_off"}[request.value]
elif field == "automatic_delay" and part == "value":
value = {"Learned": True, "Vehicle/custom": False}[request.value]
elif field in ("ratio", "full_delay"):
choice = getattr(geometry, field)
if part == "mode" and request.value in ("Vehicle/learned", "Vehicle full delay"):
value = FieldChoice("source", choice.custom_value)
elif part == "value":
value = FieldChoice("custom", float(request.value))
else:
raise ValueError("Unknown geometry edit")
else:
raise ValueError("Unknown geometry edit")
result = replace_geometry(profiles, fingerprint, torque_basis, basis, field, value)
if field == "full_delay":
result = replace_geometry(result, fingerprint, torque_basis, basis, "automatic_delay", False)
return result
def apply(self, request: FeatureSettingsRequest) -> bool:
"""One guarded document replacement; the presentation never writes Params."""
if request.key == "torque_gain_rebase" or request.key.startswith("torque:gain:"):
from openpilot.starpilot.ui.gain_feature import GainFeature
return GainFeature(self.owner).apply(request)
if request.key in TORQUE_CONFIRM_ACTIONS and not self.authority("parked_preferences"):
return False
if (request.vehicle_fingerprint != self.vehicle_fingerprint() or not self.authority("torque") or
request.capability is None or request.capability != self._capability("torque") or
self._raw(DOCUMENT_KEY) != request.expected or not self._readable(DOCUMENT_KEY) or
any(self._raw(name) != raw or not self._readable(name) for name, raw in request.dependencies)):
return False
capability = request.capability
fingerprint = capability[0]
basis = (capability[5], capability[6], capability[7])
try:
if request.key == "torque_adopt":
if not request.confirmation or request.expected is not None:
return False
profiles: dict[str, PlatformProfile] = {fingerprint: PlatformProfile(basis, FieldChoice(), FieldChoice())}
elif request.key == "torque_reset":
if not request.confirmation or request.expected is None:
return False
try:
parse_document(request.expected)
return False
except (ValueError, UnicodeError, OverflowError):
profiles = {}
else:
if request.expected is None:
legacy = interpret_legacy({key: None if fingerprint in GM_VEHICLES else self._raw(key) for key in TORQUE_NUMBERS}, basis, fingerprint=fingerprint)
profiles = {fingerprint: PlatformProfile(basis,
FieldChoice("custom", legacy.factor) if legacy.factor is not None and fingerprint not in BOLT_VEHICLES else FieldChoice(),
FieldChoice("custom", legacy.friction) if legacy.friction is not None and fingerprint not in BOLT_VEHICLES else FieldChoice())}
else:
profiles = parse_document(request.expected)
_, _, review = resolve_document(profiles, fingerprint, basis)
if request.key == "torque_reset_profile":
if not request.confirmation or not review:
return False
amended = dict(profiles)
amended[fingerprint] = PlatformProfile(basis, FieldChoice(), FieldChoice())
profiles = amended
elif request.key == "torque_rebase":
if not request.confirmation or not review:
return False
prior = profiles[fingerprint]
updated = replace(prior, basis=basis)
amended = dict(profiles)
amended[fingerprint] = updated
resolve_document(amended, fingerprint, basis)
profiles = amended
elif request.key.startswith(("torque:ratio:", "torque:full_delay:", "torque:automatic_delay:", "torque:learning:", "torque:geometry_reset:")):
profiles = self._edit_geometry(profiles, request, fingerprint, basis)
elif request.key.startswith("torque:"):
if review:
return False
_, field, part = request.key.split(":")
if fingerprint in BOLT_VEHICLES and field == "factor":
return False
if fingerprint in GM_VEHICLES and field == "factor":
if SELECTION_KEY not in dict(request.dependencies):
return False
withdrawing = part == "reset" or part == "mode" and request.value == "Vehicle/learned"
if not withdrawing and not self._factor_editable(fingerprint):
return False
if field not in ("factor", "friction") or part not in ("mode", "value", "reset"):
return False
profile = profiles.get(fingerprint)
choice = getattr(profile, field) if profile else FieldChoice()
if part == "mode":
if request.value == "Vehicle/learned":
profiles = replace_field(profiles, fingerprint, basis, field, "source", None)
elif request.value == "Custom":
candidate = choice.custom_value if choice.custom_value is not None else basis[0 if field == "factor" else 2]
low, high = torque_bounds(basis, field, fingerprint=fingerprint)
if not low <= candidate <= high:
candidate = basis[0 if field == "factor" else 2]
profiles = replace_field(profiles, fingerprint, basis, field, "custom", candidate)
else:
return False
elif part == "reset" and request.value == "Reset":
profiles = replace_field(profiles, fingerprint, basis, field, "source", None)
elif part == "value":
value = float(request.value)
supplied = basis[0 if field == "factor" else 2]
mode = "source" if round(value, 2) == round(supplied, 2) else "custom"
profiles = replace_field(profiles, fingerprint, basis, field, mode, value if mode == "custom" else None)
else:
return False
else:
return False
encoded = serialize_document(profiles)
geometry_request = request.key.startswith(("torque:ratio:", "torque:full_delay:", "torque:automatic_delay:",
"torque:learning:", "torque:geometry_reset:"))
# Fresh authority and source evidence immediately before the one Params replacement.
if (not self.authority("torque") or self.vehicle_fingerprint() != request.vehicle_fingerprint or
self._capability("torque") != capability or not self._readable(DOCUMENT_KEY) or
self._raw(DOCUMENT_KEY) != request.expected or
geometry_request and request.related_source != self._geometry_token(self.owner.vehicle_params()) or
request.key in TORQUE_CONFIRM_ACTIONS and not self.authority("parked_preferences") or
any(not self._readable(name) or self._raw(name) != raw for name, raw in request.dependencies)):
return False
self.params.put(DOCUMENT_KEY, json.loads(encoded), block=True)
saved = self._raw(DOCUMENT_KEY)
return saved is not None and parse_document(saved) == profiles
except (OSError, ValueError, TypeError, KeyError, OverflowError, UnicodeError):
return False