Add parked manual editing for thirty steering coordinates

This commit is contained in:
Dom
2026-11-04 02:14:49 +00:00
parent 465db1bc61
commit 2929a25afe
32 changed files with 1676 additions and 1114 deletions
+15 -1
View File
@@ -73,7 +73,7 @@ class Controls:
self.lateral_controller_selection = read_selection(self.params, self.CP)
self.vehicle_startup_preferences = VehicleStartupPreferences.read(
self.params, enabled=self.params.get_bool("OpenpilotEnabledToggle"))
self.torque_learning_allowed = learning_allowed(self.params, self.CP, selection=self.lateral_controller_selection)
manual_effects, manual_status = self._configure_manual_tune()
self.aol_replay = feature_enabled(self.params, self.CP, 'aol', os.environ)
self.aol_transition_diagnostic = TransitionDiagnostic() if self.aol_replay and self.CP.brand == 'gm' else None
self.ordinary_axis_ack_required = axis_policy_for(self.CP).ordinary_axis_ack_required
@@ -123,11 +123,16 @@ class Controls:
self.LaC = create_lateral_controller(self.CP, self.CI, DT_CTRL,
mode=self.lateral_controller_selection.mode, turn_assist=self.vehicle_startup_preferences.turn_assist)
if isinstance(self.LaC, LatControlTorque):
# Only the actual startup owner reads configuration; replay clones stay pure.
if manual_effects is not None and manual_effects.profile.context[1] != str(self.LaC.controller_mode):
manual_effects, manual_status = None, 'context_changed'
self.LaC.manual_flm_effects, self.LaC.manual_flm_status = manual_effects, manual_status
self.lateral_gain_owner = create_gain_owner(self.params, self.CP, self.LaC)
cloudlog.info({"event": "torque controller selected", "controller": self.LaC.controller_mode.value,
"source": self.lateral_controller_selection.source,
"vehicle": self.CP.carFingerprint, "policy": self.LaC.controller_policy,
"automaticLearning": self.torque_learning_allowed, "startupParameterSource": "vehicle",
"manualCoordinateStatus": self.LaC.manual_flm_status,
"startupTorque": {"factor": self.LaC.torque_params.latAccelFactor,
"offset": self.LaC.torque_params.latAccelOffset,
"friction": self.LaC.torque_params.friction},
@@ -152,6 +157,15 @@ class Controls:
assist_enabled = self.vehicle_startup_preferences.turn_assist and model_turn_assist_supported(self.CP)
self.turn_assist_enabled = lambda: assist_enabled
def _configure_manual_tune(self):
manual_effects, manual_status = None, 'not_configured'
if self.CP.lateralTuning.which() == 'torque' and str(self.CP.steerControlType) == 'torque':
from openpilot.starpilot.flm.manual_coordinates import load as load_manual_coordinates
manual_effects, manual_status = load_manual_coordinates(self.CP, self.lateral_controller_selection.mode, self.params)
self.torque_learning_allowed = bool(manual_status == 'not_configured' and
learning_allowed(self.params, self.CP, selection=self.lateral_controller_selection))
return manual_effects, manual_status
def update(self, timeout: int = 15):
self.sm.update(timeout)
if self.sm.updated["extrinsicsCalibration"]:
@@ -67,6 +67,8 @@ class LatControlTorque(LatControl):
self.pid._k_p = [[0.0], [STARPILOT_KP]]
self.flm_source = flm.GmLiveSource(CP, self.controller_mode)
self.flm_surface = None
self.manual_flm_effects = None
self.manual_flm_status = 'not_configured'
from openpilot.starpilot.flm.runtime_adapter_registry import baseline_values
self.native_learning_baseline = baseline_values(self)
self.native_learning_refinement = None
@@ -96,6 +98,8 @@ class LatControlTorque(LatControl):
def update(self, active, CS, VM, params, steer_limited_by_safety, desired_curvature, curvature_limited, lat_delay):
self.flm_surface = self.flm_source.sample(active=active, speed=CS.vEgo)
if self.manual_flm_effects is not None:
self.manual_flm_effects.begin_frame()
if self.starpilot_extension is not None:
result = self.starpilot_extension.update(active, CS, VM, params, steer_limited_by_safety,
desired_curvature, curvature_limited, lat_delay)
@@ -206,24 +206,24 @@ class ClockEditor(Widget):
self._dragged = False
self._keyboard_focus = False
def _handle_mouse_event(self, event):
def _handle_mouse_event(self, mouse_event):
if not isinstance(self._pressed, int):
return
if event.left_pressed:
self._last_t = self._last_motion = event.t
if mouse_event.left_pressed:
self._last_t = self._last_motion = mouse_event.t
return
delta = event.pos.y - self._last_y
self._dragged |= abs(event.pos.y - self._start_y) > 12 * self._scale
delta = mouse_event.pos.y - self._last_y
self._dragged |= abs(mouse_event.pos.y - self._start_y) > 12 * self._scale
if delta:
self._scroll(self._pressed, delta)
elapsed = event.t - self._last_t
elapsed = mouse_event.t - self._last_t
if 0 < elapsed < 0.15:
self._velocity = max(-1600 * self._scale, min(1600 * self._scale, delta / elapsed))
self._last_motion = event.t
self._last_y, self._last_t = event.pos.y, event.t
if event.left_released and event.t - self._last_motion > 0.12:
self._last_motion = mouse_event.t
self._last_y, self._last_t = mouse_event.pos.y, mouse_event.t
if mouse_event.left_released and mouse_event.t - self._last_motion > 0.12:
self._velocity = 0.0
if event.left_released and not rl.check_collision_point_rec(event.pos, self._rect):
if mouse_event.left_released and not rl.check_collision_point_rec(mouse_event.pos, self._rect):
self._pressed = None
self._offset = self._velocity = 0.0
+3 -1
View File
@@ -220,7 +220,9 @@ class GlobalRuntime:
self.stage_effect = False
self.centre_ack = None
policy = getattr(getattr(self.controller, 'starpilot_extension', None), 'policy', None)
self.live_manual = bool(manual_current or getattr(self.controller, 'flm_surface', None) is not None or
self.live_manual = bool(manual_current or getattr(self.controller, 'manual_flm_effects', None) is not None or
getattr(self.controller, 'manual_flm_status', 'not_configured') not in ('configured', 'not_configured') or
getattr(self.controller, 'flm_surface', None) is not None or
getattr(policy, 'surface', None) is not None)
self.native_required = bool(native_required)
self.native_witness = native_witness
@@ -105,6 +105,7 @@ def identity_for(CP, controller, params, conversion_version):
return None, False, False
manual = profile is not None and (profile.factor.mode == 'custom' or profile.friction.mode == 'custom' or
profile.proportional_gain.mode == 'custom' or profile.flm is not None or
profile.manual_coordinates is not None and any(v for _, v in profile.manual_coordinates.values) or
profile.geometry is not None and (profile.geometry.ratio.mode == 'custom' or
profile.geometry.full_delay.mode == 'custom'))
baseline = serialize_document({str(CP.carFingerprint): profile}).hex() if profile is not None else None
@@ -0,0 +1,111 @@
"""Explicit parked manual coordinates, separate from learned history and authority."""
from dataclasses import dataclass
import hashlib
import json
import math
import time
from openpilot.starpilot.flm.canonical_surface_adapter import NAMES, MANIFEST_SHA256, coordinate_bounds
def context(cp, controller):
from openpilot.starpilot.flm.runtime_adapter_registry import car_params_digest
from openpilot.starpilot.lateral.controller_selection import ControllerMode
if (str(cp.steerControlType) != 'torque' or cp.lateralTuning.which() != 'torque'
or cp.passive or cp.notCar or cp.dashcamOnly):
raise ValueError('Manual coordinates require an active torque controller')
return car_params_digest(cp), str(ControllerMode(controller)), MANIFEST_SHA256
@dataclass(frozen=True)
class ManualCoordinates:
context: tuple[str, str, str]
values: tuple[tuple[str, float], ...]
def validate(self):
if (type(self.context) is not tuple or len(self.context) != 3
or any(type(v) is not str for v in self.context)
or any(len(self.context[i]) != 64 or self.context[i].strip('0123456789abcdef') for i in (0, 2))
or not self.context[1] or len(self.context[1]) > 32 or not self.context[1].isascii()
or type(self.values) is not tuple or len(self.values) != len(NAMES)):
raise ValueError('Invalid manual coordinate context')
from openpilot.starpilot.lateral.controller_selection import ControllerMode
ControllerMode(self.context[1])
for name, row in zip(NAMES, self.values, strict=True):
if type(row) is not tuple or len(row) != 2 or row[0] != name:
raise ValueError('Exactly thirty ordered manual coordinates required')
low, high = coordinate_bounds(name)
if type(row[1]) not in (int, float) or not math.isfinite(row[1]) or not low <= row[1] <= high:
raise ValueError('Manual coordinate outside the selected envelope')
# Validate the complete composed surface as well as each individual value.
from openpilot.starpilot.flm.canonical_surface_adapter import CanonicalSurfaceEffects
CanonicalSurfaceEffects(dict(self.values[:1]), self.digest(), accumulated=dict(self.values))
return self
def digest(self):
return hashlib.sha256(json.dumps((self.context, self.values), separators=(',', ':'), allow_nan=False).encode()).hexdigest()
def document(self):
self.validate()
return {'context': list(self.context), 'values': dict(self.values)}
@classmethod
def from_document(cls, value):
if (type(value) is not dict or set(value) != {'context', 'values'} or type(value['context']) is not list
or type(value['values']) is not dict or set(value['values']) != set(NAMES)):
raise ValueError('Invalid manual coordinate document')
return cls(tuple(value['context']), tuple((n, value['values'][n]) for n in NAMES)).validate()
class ManualEffects:
"""Human-selected bounded math; never a live trial or a native permission."""
def __init__(self, profile):
from openpilot.starpilot.flm.canonical_surface_adapter import CanonicalSurfaceEffects
if type(profile) is not ManualCoordinates:
raise ValueError('Typed manual coordinates required')
profile.validate()
self.profile = profile
self.source = CanonicalSurfaceEffects(dict(profile.values[:1]), profile.digest(), accumulated=dict(profile.values))
self.original_effect_anchor = self.source.original_effect_anchor
def begin_frame(self):
self.source.begin_frame(time.monotonic_ns())
def selected_stage(self, *args, **kwargs):
return self.source.selected_stage(*args, **kwargs)
def selected_friction(self, *args, **kwargs):
return self.source.selected_friction(*args, **kwargs)
def selected_deadzone(self, *args, **kwargs):
return self.source.selected_deadzone(*args, **kwargs)
def selected_output(self, *args, **kwargs):
return self.source.selected_output(*args, **kwargs)
def load(cp, controller, params=None):
"""Load once at controller startup. Unreadable is distinct from absent."""
from openpilot.common.params import Params
from openpilot.starpilot.saved_source import read_saved
from openpilot.starpilot.lateral.torque_settings import DOCUMENT_KEY, MAX_DOCUMENT_BYTES, parse_document
params = Params() if params is None else params
raw, readable = read_saved(params, DOCUMENT_KEY, MAX_DOCUMENT_BYTES)
if not readable:
return None, 'unreadable'
if raw is None:
return None, 'not_configured'
try:
profile = parse_document(raw).get(str(cp.carFingerprint))
manual = None if profile is None else profile.manual_coordinates
if manual is None or not any(v for _, v in manual.values):
return None, 'not_configured'
tune = cp.lateralTuning.torque
if (profile.basis != (float(tune.latAccelFactor), float(tune.latAccelOffset), float(tune.friction))
or profile.flm is not None or manual.context != context(cp, controller)):
return None, 'context_changed'
return ManualEffects(manual), 'configured'
except (AttributeError, KeyError, TypeError, ValueError, OverflowError, UnicodeError):
return None, 'invalid'
+24 -9
View File
@@ -337,12 +337,26 @@ class EffectReplaySource:
return self.source.selected_output(*args, **kwargs)
def _selected_source(parent):
from openpilot.starpilot.flm.manual_coordinates import ManualEffects
source = getattr(parent, 'manual_flm_effects', None)
if source is not None:
policy = getattr(getattr(parent, 'starpilot_extension', None), 'policy', None)
if getattr(parent, 'flm_surface', None) is not None or getattr(policy, 'surface', None) is not None:
return None
return source if type(source) is ManualEffects else None
return getattr(parent, 'automatic_flm_effects', None)
def manual(parent):
policy = getattr(getattr(parent, 'starpilot_extension', None), 'policy', None)
return getattr(parent, 'flm_surface', None) is not None or getattr(policy, 'surface', None) is not None
return (getattr(parent, 'manual_flm_effects', None) is not None
or getattr(parent, 'flm_surface', None) is not None or getattr(policy, 'surface', None) is not None)
def bounded_profile_on_limited_frame(parent):
if getattr(parent, 'manual_flm_effects', None) is not None:
return False
# This is parameter shaping, not a native grant. The live owner must already
# hold current source/native permission; replay uses the same bounded math.
# Legacy residual owners and arbitrary callback objects remain inhibited.
@@ -357,14 +371,14 @@ def bounded_profile_on_limited_frame(parent):
def apply_selected_stage(parent, frame):
source = getattr(parent, 'automatic_flm_effects', None)
source = _selected_source(parent)
if (
source is None
or not frame.valid()
or not frame.active
or frame.driver_override
or (frame.limited and (frame.curvature_limited or not bounded_profile_on_limited_frame(parent)))
or manual(parent)
or (manual(parent) and source is not getattr(parent, 'manual_flm_effects', None))
):
return frame.baseline_ff, frame.baseline_threshold
try:
@@ -379,13 +393,13 @@ def apply_selected_stage(parent, frame):
def apply_selected_friction(parent, cs, setpoint, baseline, *, active, limited, curvature_limited=False):
source = getattr(parent, 'automatic_flm_effects', None)
source = _selected_source(parent)
if (
source is None
or not active
or (limited and (curvature_limited or not bounded_profile_on_limited_frame(parent)))
or cs.steeringPressed
or manual(parent)
or (manual(parent) and source is not getattr(parent, 'manual_flm_effects', None))
or not finite(baseline)
):
return baseline
@@ -399,8 +413,9 @@ def apply_selected_friction(parent, cs, setpoint, baseline, *, active, limited,
def apply_selected_deadzone(parent, cs, vm, baseline_accel, requested_curvature, *, active, limited, curvature_limited=False):
source = getattr(parent, 'automatic_flm_effects', None)
if source is None or not active or (limited and (curvature_limited or not bounded_profile_on_limited_frame(parent))) or cs.steeringPressed or manual(parent):
source = _selected_source(parent)
if (source is None or not active or (limited and (curvature_limited or not bounded_profile_on_limited_frame(parent)))
or cs.steeringPressed or (manual(parent) and source is not getattr(parent, 'manual_flm_effects', None))):
return baseline_accel
try:
speed = float(cs.vEgo)
@@ -415,13 +430,13 @@ def apply_selected_deadzone(parent, cs, vm, baseline_accel, requested_curvature,
def apply_selected_output(parent, cs, vm, params, requested_curvature, result, *, active, limited, curvature_limited=False):
source = getattr(parent, 'automatic_flm_effects', None)
source = _selected_source(parent)
if (
source is None
or not active
or (limited and (curvature_limited or not bounded_profile_on_limited_frame(parent)))
or cs.steeringPressed
or manual(parent)
or (manual(parent) and source is not getattr(parent, 'manual_flm_effects', None))
or cs.vEgo < 0.1 * 0.44704
):
return result
@@ -0,0 +1,306 @@
"""Manual edits are explicit bounded configuration, never accepted learning."""
from dataclasses import replace
from types import SimpleNamespace
import pytest
from opendbc.car import structs
from opendbc.car.hyundai.values import CAR
from openpilot.starpilot.flm.canonical_surface_adapter import NAMES
from openpilot.starpilot.flm.manual_coordinates import ManualCoordinates, ManualEffects, context, load
from openpilot.starpilot.flm.selected_effects import apply_selected_stage
from openpilot.starpilot.flm.torque_stages import StageFrame
from openpilot.starpilot.lateral.controller_selection import default_selection
from openpilot.starpilot.lateral.torque_settings import DOCUMENT_KEY, FieldChoice, PlatformProfile, parse_document, serialize_document
from openpilot.starpilot.saved_source import read_saved
from openpilot.starpilot.ui.feature_settings_state import FeatureSettingsRequest
from openpilot.starpilot.ui.torque_feature import TorqueFeature
@pytest.fixture
def cp():
value = structs.CarParams.new_message()
value.carFingerprint = str(CAR.HYUNDAI_IONIQ_6)
value.brand = 'hyundai'
value.steerControlType = 'torque'
tune = value.lateralTuning.init('torque')
tune.latAccelFactor, tune.latAccelOffset, tune.friction = 2.5, 0., .1
return value
class PrivateParams:
def __init__(self, root):
self.root = root
(root / 'd').mkdir()
def get_param_path(self, key):
return str(self.root / 'd' / key)
def put(self, *args, **kwargs):
raise AssertionError('Manual editing must use the exact-source document writer')
def basis(cp):
t = cp.lateralTuning.torque
return float(t.latAccelFactor), float(t.latAccelOffset), float(t.friction)
def manual(cp, **changes):
values = dict.fromkeys(NAMES, 0.)
values.update(changes)
return ManualCoordinates(context(cp, default_selection(cp).mode), tuple(values.items())).validate()
def save(params, cp, value):
profile = PlatformProfile(basis(cp), FieldChoice(), FieldChoice(), manual_coordinates=value)
raw = serialize_document({str(cp.carFingerprint): profile})
from pathlib import Path
Path(params.get_param_path(DOCUMENT_KEY)).write_bytes(raw)
return raw
def saved_profile(params, cp):
raw, readable = read_saved(params, DOCUMENT_KEY, 16384)
assert readable and raw is not None
return parse_document(raw)[str(cp.carFingerprint)]
@pytest.mark.parametrize('name', NAMES)
def test_each_of_thirty_values_roundtrips_without_learning_history(cp, name):
value = manual(cp, **{name: .01})
restored = ManualCoordinates.from_document(value.document())
assert restored == value
assert dict(restored.values)[name] == .01
assert sum(v != 0 for _, v in restored.values) == 1
@pytest.mark.parametrize('bad', [True, float('nan'), float('inf'), -.001, .251])
def test_deadband_refuses_wrong_types_nonfinite_and_bounds(cp, bad):
with pytest.raises((ValueError, OverflowError)):
manual(cp, center_deadband_low_deg=bad)
def test_full_profile_is_required_and_ordered(cp):
value = manual(cp)
with pytest.raises(ValueError):
replace(value, values=value.values[:-1]).validate()
with pytest.raises(ValueError):
replace(value, values=tuple(reversed(value.values))).validate()
def test_opposite_speed_window_edges_stay_ordered_inside_original_limits(cp):
value = manual(cp, curvy_turn_in_trim_speed_min=.25, curvy_turn_in_trim_speed_max=-.25)
assert dict(value.values)['curvy_turn_in_trim_speed_min'] == .25
assert dict(value.values)['curvy_turn_in_trim_speed_max'] == -.25
def test_same_actual_context_loads_once_and_does_not_write_history(tmp_path, cp):
params = PrivateParams(tmp_path)
raw = save(params, cp, manual(cp, center_taper_max=.01))
owner, status = load(cp, default_selection(cp).mode, params)
assert type(owner) is ManualEffects and status == 'configured'
assert dict(owner.profile.values)['center_taper_max'] == .01
assert read_saved(params, DOCUMENT_KEY, 16384) == (raw, True)
assert read_saved(params, 'AutomaticSteeringLiveTrials', 65536) == (None, True)
def test_changed_vehicle_context_refuses_instead_of_retagging(tmp_path, cp):
params = PrivateParams(tmp_path)
raw = save(params, cp, manual(cp, center_taper_max=.01))
cp.mass = 1800.
assert load(cp, default_selection(cp).mode, params) == (None, 'context_changed')
assert read_saved(params, DOCUMENT_KEY, 16384) == (raw, True)
def test_unreadable_is_not_absence(tmp_path, cp):
params = PrivateParams(tmp_path)
(tmp_path / 'd' / DOCUMENT_KEY).mkdir()
assert load(cp, default_selection(cp).mode, params) == (None, 'unreadable')
def test_neutral_profile_does_not_activate_manual_override(tmp_path, cp):
params = PrivateParams(tmp_path)
save(params, cp, manual(cp))
assert load(cp, default_selection(cp).mode, params) == (None, 'not_configured')
@pytest.mark.parametrize('active,override,limited,curvature_limited', [
(False, False, False, False), (True, True, False, False),
(True, False, True, False), (True, False, True, True),
])
def test_manual_does_not_bypass_inactive_override_or_limiter(cp, active, override, limited, curvature_limited):
source = ManualEffects(manual(cp, center_taper_max=.01))
source.begin_frame()
parent = SimpleNamespace(manual_flm_effects=source, flm_surface=None, starpilot_extension=None, automatic_flm_effects=None)
frame = StageFrame(15., 0., 0., .5, .3, active, override, limited, curvature_limited)
assert apply_selected_stage(parent, frame) == (.5, .3)
def test_manual_uses_existing_selected_stage_with_no_auto_owner(cp):
source = ManualEffects(manual(cp, center_taper_max=.01))
source.begin_frame()
parent = SimpleNamespace(manual_flm_effects=source, flm_surface=None, starpilot_extension=None, automatic_flm_effects=None)
frame = StageFrame(15., 0., 0., .5, .3, True, False, False)
ff, threshold = apply_selected_stage(parent, frame)
assert 0 < ff < .5
assert .3 <= threshold <= .375
assert parent.automatic_flm_effects is None
def feature(cp, params, state):
capability = (str(cp.carFingerprint), 'hyundai', 'torque', 'torque', False, *basis(cp), None)
def raw(key):
return read_saved(params, key, 16384)[0]
owner = SimpleNamespace(params=params, authority=lambda group: state['parked'] if group == 'parked_preferences' else True,
vehicle_fingerprint=lambda: str(cp.carFingerprint), vehicle_params=lambda: cp,
_capability=lambda group: capability, _raw=raw, _readable=lambda key: read_saved(params, key, 16384)[1],
_dependents=lambda *keys: tuple((key, raw(key)) for key in keys))
return TorqueFeature(owner), capability
def request(row, value):
return FeatureSettingsRequest(row.key, row.source, str(value), capability=row.capability,
related_source=row.related_source, dependencies=row.dependencies, vehicle_fingerprint=row.vehicle_fingerprint)
def test_parked_edit_exact_readback_and_single_value_reset(tmp_path, cp):
params = PrivateParams(tmp_path)
state = {'parked': True}
editor, capability = feature(cp, params, state)
rows = editor.manual_rows(capability, True)
assert len(rows) == 30 and all(row.available for row in rows)
row = next(r for r in rows if r.key.endswith(':center_taper_max'))
row = replace(row, vehicle_fingerprint=str(cp.carFingerprint))
assert editor.apply(request(row, 1.))
profile = saved_profile(params, cp)
assert profile.manual_coordinates is not None
assert dict(profile.manual_coordinates.values)['center_taper_max'] == .01
row = next(r for r in editor.manual_rows(capability, True) if r.key.endswith(':center_taper_max'))
assert editor.apply(request(replace(row, vehicle_fingerprint=str(cp.carFingerprint)), 0.))
assert saved_profile(params, cp).manual_coordinates is None
def test_stale_source_and_onroad_writes_are_refused(tmp_path, cp):
params = PrivateParams(tmp_path)
state = {'parked': True}
editor, capability = feature(cp, params, state)
row = replace(editor.manual_rows(capability, True)[0], vehicle_fingerprint=str(cp.carFingerprint))
assert editor.apply(request(row, 1.))
assert not editor.apply(request(row, 2.))
current = replace(editor.manual_rows(capability, True)[0], vehicle_fingerprint=str(cp.carFingerprint))
state['parked'] = False
assert not editor.apply(request(current, 2.))
profile = saved_profile(params, cp)
assert profile.manual_coordinates is not None
assert dict(profile.manual_coordinates.values)[NAMES[0]] == .01
def test_invalid_saved_document_is_not_replaced_by_a_new_manual_tune(tmp_path, cp):
params = PrivateParams(tmp_path)
(tmp_path / 'd' / DOCUMENT_KEY).write_bytes(b'{broken')
editor, capability = feature(cp, params, {'parked': True})
assert not any(row.available for row in editor.manual_rows(capability, True))
assert (tmp_path / 'd' / DOCUMENT_KEY).read_bytes() == b'{broken'
def test_authority_loss_under_native_lock_refuses_and_removes_stage(tmp_path, cp, monkeypatch):
from openpilot.starpilot import saved_document
params = PrivateParams(tmp_path)
state = {'parked': True}
editor, capability = feature(cp, params, state)
row = replace(editor.manual_rows(capability, True)[0], vehicle_fingerprint=str(cp.carFingerprint))
original = saved_document.acquire_native_lock
def acquire(fd):
original(fd)
state['parked'] = False
monkeypatch.setattr(saved_document, 'acquire_native_lock', acquire)
assert not editor.apply(request(row, 1.))
assert read_saved(params, DOCUMENT_KEY, 16384) == (None, True)
assert not list(tmp_path.glob('.manual-steering-*'))
def test_source_change_under_native_lock_is_not_overwritten(tmp_path, cp, monkeypatch):
from openpilot.starpilot import saved_document
from pathlib import Path
params = PrivateParams(tmp_path)
editor, capability = feature(cp, params, {'parked': True})
row = replace(editor.manual_rows(capability, True)[0], vehicle_fingerprint=str(cp.carFingerprint))
other = serialize_document({str(cp.carFingerprint): PlatformProfile(basis(cp), FieldChoice(), FieldChoice('custom', .12))})
original = saved_document.acquire_native_lock
def acquire(fd):
original(fd)
Path(params.get_param_path(DOCUMENT_KEY)).write_bytes(other)
monkeypatch.setattr(saved_document, 'acquire_native_lock', acquire)
assert not editor.apply(request(row, 1.))
assert read_saved(params, DOCUMENT_KEY, 16384) == (other, True)
assert not list(tmp_path.glob('.manual-steering-*'))
def test_reset_all_uses_original_row_request_and_requires_confirmation(tmp_path, cp):
from openpilot.starpilot.ui.feature_settings_state import row_change
params = PrivateParams(tmp_path)
save(params, cp, manual(cp, ff_gain_left=.01, center_taper_max=.01))
editor, capability = feature(cp, params, {'parked': True})
reset = replace(editor.manual_rows(capability, True)[-1], vehicle_fingerprint=str(cp.carFingerprint))
unconfirmed = row_change(reset)
assert unconfirmed is not None and unconfirmed.value == 'Reset'
assert not editor.apply(unconfirmed)
assert editor.apply(replace(unconfirmed, confirmation=True))
assert saved_profile(params, cp).manual_coordinates is None
def test_replay_controller_construction_does_not_read_manual_preferences(cp, monkeypatch):
from openpilot.selfdrive.controls.lib.latcontrol_torque import LatControlTorque
from openpilot.starpilot.flm import manual_coordinates
def forbidden(*args, **kwargs):
raise AssertionError('Only the actual Controls owner may load manual configuration')
monkeypatch.setattr(manual_coordinates, 'load', forbidden)
ci = SimpleNamespace(
torque_from_lateral_accel=lambda: lambda value, tune: value / tune.latAccelFactor,
lateral_accel_from_torque=lambda: lambda value, tune: value * tune.latAccelFactor)
controller = LatControlTorque(cp.as_reader(), ci, .01, controller_mode=default_selection(cp).mode)
assert controller.manual_flm_effects is None
assert controller.manual_flm_status == 'not_configured'
def test_next_startup_loads_new_saved_values_without_mutating_running_profile(tmp_path, cp):
params = PrivateParams(tmp_path)
save(params, cp, manual(cp, center_taper_max=.01))
original, status = load(cp, default_selection(cp).mode, params)
assert status == 'configured'
save(params, cp, manual(cp, center_taper_max=.02))
following, status = load(cp, default_selection(cp).mode, params)
assert status == 'configured'
assert dict(original.profile.values)['center_taper_max'] == .01
assert dict(following.profile.values)['center_taper_max'] == .02
@pytest.mark.parametrize('saved,expected', [(False, True), (True, False)])
def test_actual_controls_startup_holds_native_learning_only_for_manual_tune(tmp_path, cp, monkeypatch, saved, expected):
from openpilot.selfdrive.controls import controlsd
params = PrivateParams(tmp_path)
if saved:
save(params, cp, manual(cp, center_taper_max=.01))
(tmp_path / 'd' / 'ForceAutoTuneOff').write_bytes(b'0')
monkeypatch.setattr(controlsd, 'learning_allowed', lambda *args, **kwargs: True)
owner = object.__new__(controlsd.Controls)
owner.CP = cp
monkeypatch.setattr(owner, 'params', params, raising=False)
owner.lateral_controller_selection = default_selection(cp)
effects, status = owner._configure_manual_tune()
assert owner.torque_learning_allowed is expected
assert (effects is not None) is saved
assert status == ('configured' if saved else 'not_configured')
assert (tmp_path / 'd' / 'ForceAutoTuneOff').read_bytes() == b'0'
def test_actual_controls_startup_does_not_treat_unreadable_as_automatic_permission(tmp_path, cp, monkeypatch):
from openpilot.selfdrive.controls import controlsd
params = PrivateParams(tmp_path)
(tmp_path / 'd' / DOCUMENT_KEY).mkdir()
monkeypatch.setattr(controlsd, 'learning_allowed', lambda *args, **kwargs: True)
owner = object.__new__(controlsd.Controls)
owner.CP = cp
monkeypatch.setattr(owner, 'params', params, raising=False)
owner.lateral_controller_selection = default_selection(cp)
assert owner._configure_manual_tune() == (None, 'unreadable')
assert owner.torque_learning_allowed is False
@@ -10,6 +10,18 @@ from openpilot.starpilot.flm.tests.test_live_tracking import episode
from openpilot.starpilot.flm.tests.test_tracking_source_context import context, refresh
def offer_state(owner):
state = owner.offer_state
assert state is not None
return state
def selector_hint(owner):
hint = owner.selector_hint
assert hint is not None
return hint
class Drive:
"""Declared whole episodes and typed ACK operands, never vehicle evidence."""
def __init__(self, initial=.0):
@@ -41,7 +53,7 @@ class Drive:
profile_sha256=profile or o._hash(o.accepted), goal_rms=goal, normalization_accel=max(.1, goal),
goal_shape=shape or (goal / max(.1, goal),) * 8)
refresh(o, self.now)
if renew and o.offer_state is not None and o.offer_state['phase'] == 'rollback':
if renew and o.offer_state is not None and offer_state(o)['phase'] == 'rollback':
assert self.ack()
return row, o.observe_episode(row, now_ns=self.now)
@@ -49,11 +61,11 @@ class Drive:
o = self.owner
for _ in range(20):
self.row(name, **row_values)
if o.offer_state is not None and o.offer_state['phase'] == 'trial':
if o.offer_state is not None and offer_state(o)['phase'] == 'trial':
offer = o.offer(self.now)
assert offer is not None
return offer
pytest.fail('No bounded source trial selected')
raise AssertionError('No bounded source trial selected')
def batch(self, offer, name='ff_gain_left', **row_values):
rows = []
@@ -68,9 +80,9 @@ def return_phase(d, name='ff_gain_left', **values):
assert d.ack(trial)
d.batch(trial, name, error=.03, goal=values.get('goal', .09), shape=values.get('shape'))
o = d.owner
assert o.offer_state['phase'] == 'rollback', 'A first positive batch must return to A, not confirm B'
assert offer_state(o)['phase'] == 'rollback', 'A first positive batch must return to A, not confirm B'
assert o.source_recheck is not None
assert not o.offer_state['accepted'] and o.source_recheck.first_ack_ns == 0
assert not offer_state(o)['accepted'] and o.source_recheck.first_ack_ns == 0
return trial
@@ -95,14 +107,14 @@ def test_each_coordinate_requires_return_baseline_and_fresh_reapplied_performanc
initial = dict(o.accepted)
first, second = reapply_phase(d, name)
assert o.accepted == initial and o.settled_ns is None and not o.future
assert len(o.before) == 3 and len(o.offer_state['source_rechecked_before']) == 3
assert len(o.before) == 3 and len(offer_state(o)['source_rechecked_before']) == 3
assert not d.ack(first) # Old B1 trial/sequence cannot acknowledge B2.
early, admitted = d.row(name, profile=second.target_sha256, error=.03)
assert not admitted and not o.future
assert d.ack(second)
for index in range(3):
d.row(name, profile=second.target_sha256, error=.03)
assert o.offer_state['phase'] == ('confirmed' if index == 2 else 'trial')
assert offer_state(o)['phase'] == ('confirmed' if index == 2 else 'trial')
assert o.accepted == initial
confirmed = o.offer(d.now)
assert confirmed is not None and confirmed.phase == 'confirmed'
@@ -149,12 +161,12 @@ def test_null_response_cannot_confirm_when_fresh_baseline_has_same_apparent_bene
assert d.ack(trial)
d.batch(trial, **new)
o = d.owner
assert o.offer_state['phase'] == 'rollback', 'An easier input cannot immediately confirm an unchanged-response B'
assert offer_state(o)['phase'] == 'rollback', 'An easier input cannot immediately confirm an unchanged-response B'
assert o.source_recheck is not None
assert not any(o.accepted.values()) and d.ack()
for _ in range(3):
d.row(**new)
assert o.source_recheck is None and o.offer_state['phase'] == 'rollback' and o.offer_state['accepted']
assert o.source_recheck is None and offer_state(o)['phase'] == 'rollback' and offer_state(o)['accepted']
assert o.reason == 'return_baseline_did_not_confirm_improvement'
assert not any(o.accepted.values())
@@ -190,7 +202,7 @@ def test_original_deadline_ends_every_recheck_transition(phase):
if phase == 'confirmed_ack_pending':
d.row(profile=target.target_sha256, error=.03)
d.row(profile=target.target_sha256, error=.03)
assert o.offer_state['phase'] == 'confirmed'
assert offer_state(o)['phase'] == 'confirmed'
d.now = first.deadline_ns + 1
refresh(o,d.now)
offer = o.offer(d.now)
@@ -206,7 +218,7 @@ def test_actual_gross_oscillation_rejection_remains_terminal(stage):
offer = d.trial() if stage == 'first_trial' else reapply_phase(d)[1]
assert d.ack(offer)
d.row(profile=offer.target_sha256, error=.03, reversal=10.)
assert o.reason == 'trial_tracking_or_oscillation_worse' and o.offer_state['phase'] == 'rollback'
assert o.reason == 'trial_tracking_or_oscillation_worse' and offer_state(o)['phase'] == 'rollback'
assert getattr(o,'source_recheck',None) is None and o.paused_intent is None and not any(o.accepted.values())
@@ -217,7 +229,7 @@ def test_interrupted_return_baseline_never_retains_provisional_candidate(reason,
assert d.ack()
d.row(error=.045)
d.owner.interrupt(reason,context_changed=changed)
assert d.owner.source_recheck is None and d.owner.offer_state['phase'] == 'rollback'
assert d.owner.source_recheck is None and offer_state(d.owner)['phase'] == 'rollback'
assert all(value == .002 for value in d.owner.accepted.values())
@@ -230,17 +242,17 @@ def test_pretrial_numbers_wait_for_original_demand_without_an_offer_or_expiry_re
break
assert o.discovery is not None
discovery = o.discovery
selected,expires = o.selector_hint['selected_ns'],o.selector_hint['expires_ns']
selected,expires = selector_hint(o)['selected_ns'],selector_hint(o)['expires_ns']
d.row() # First whole independent baseline.
ids = tuple(v.episode_id for v in o.comparison)
assert len(ids) == 1
d.row(goal=.4,kind='large')
assert o.discovery is discovery and tuple(v.episode_id for v in o.comparison) == ids
assert o.selector_hint['selected_ns'] == selected and o.selector_hint['expires_ns'] == expires
assert selector_hint(o)['selected_ns'] == selected and selector_hint(o)['expires_ns'] == expires
assert o.offer(d.now) is None
for _ in range(2):
d.row()
assert o.offer_state is not None and o.offer_state['phase'] == 'trial'
assert o.offer_state is not None and offer_state(o)['phase'] == 'trial'
assert {v.episode_id for v in o.before}.issuperset(ids)
@@ -254,7 +266,7 @@ def test_return_baseline_requires_its_own_current_exact_ack(bad):
'wrong_hash': {'target_sha256': first.target_sha256},
'stale': {'control_ns': d.now-100_000_001}}[bad]
assert not d.ack(offer, **changes)
assert o.source_recheck.first_ack_ns == 0 and not o.offer_state['accepted']
assert o.source_recheck.first_ack_ns == 0 and not offer_state(o)['accepted']
row, admitted = d.row(error=.045, renew=False)
assert not admitted and not o.source_recheck.baseline
assert row.episode_id not in {v.episode_id for v in o.source_recheck.before}
@@ -297,7 +309,7 @@ def test_recheck_numeric_state_does_not_survive_identity_model_or_baseline_chang
o.accepted['ff_gain_left'] = .002
o.offer(d.now)
assert o.source_recheck is None
assert o.offer_state['phase'] == 'rollback'
assert offer_state(o)['phase'] == 'rollback'
def test_zero_error_return_baseline_is_not_censored_to_keep_candidate():
@@ -308,7 +320,7 @@ def test_zero_error_return_baseline_is_not_censored_to_keep_candidate():
d.row(error=0.)
o = d.owner
assert o.source_recheck is None and not any(o.accepted.values())
assert o.offer_state['phase'] == 'rollback' and o.offer_state['accepted']
assert offer_state(o)['phase'] == 'rollback' and offer_state(o)['accepted']
assert o.reason == 'return_baseline_did_not_confirm_improvement'
@@ -332,7 +344,7 @@ def test_missing_current_response_witness_cannot_advance_any_recheck_transition(
d.now += 100_000_001
assert o.offer(d.now) is None
assert o.source_recheck is None and not any(o.accepted.values())
assert o.offer_state is None or o.offer_state['phase'] == 'rollback'
assert o.offer_state is None or offer_state(o)['phase'] == 'rollback'
@pytest.mark.parametrize('cause',['expiry','context'])
@@ -349,7 +361,7 @@ def test_waiting_pretrial_comparison_cannot_renew_or_survive_changed_context(cau
old_ids = {v.episode_id for v in o.comparison}
assert old_ids
if cause == 'expiry':
d.now = o.selector_hint['expires_ns']+1
d.now = selector_hint(o)['expires_ns']+1
d.row()
else:
o.interrupt('changed_context',context_changed=True)
+2
View File
@@ -52,6 +52,8 @@ def apply_stages(parent, cs, setpoint, jerk, ff, threshold, *, active, limited,
observer = getattr(parent, 'automatic_flm_observation', None)
if observer is not None and frame.valid():
observer.observe_stage(frame, ff, (0.0, 0.0))
if getattr(parent, 'manual_flm_effects', None) is not None:
return apply_selected_stage(parent, frame)
if source is None:
return ff, threshold
if not frame.valid() or not frame.active or frame.driver_override:
@@ -252,6 +252,10 @@ def purpose(row):
return 'Selects camera crop dimensions matching the source camera image.'
if key.startswith('pip:mask:'):
return 'Moves the camera-image crop center along the indicated axis.'
if key.startswith('torque:surface:'):
return ('Removes the thirty manual steering overrides while keeping saved Auto Learn history.'
if key == 'torque:surface:reset' else
'Adjusts this manual steering correction.')
if key.startswith('torque:'):
field = key.split(':')[1]
return {'factor': 'Sets the lateral-acceleration-to-torque conversion for compatible steering controllers.',
@@ -279,6 +283,8 @@ def description(row):
suffix = INITIALIZATION.get(row.key, '')
if row.key.startswith('lane_change:') and row.key not in ('lane_change:close_gap', 'lane_change:close_gap_seconds', 'lane_change:reset'):
suffix = 'Changes apply when the current signal and maneuver end; turning assistance off cancels it after the preference refresh.'
if row.key.startswith('torque:surface:'):
suffix = row.reason.strip()
if row.key.startswith('torque:') and row.key.split(':')[1] in ('factor', 'friction', 'gain'):
suffix = ('First activation of a manual tuning owner requires a driving-process restart. An ' +
'already active owner refreshes saved adjustments during the drive.')
+6 -1
View File
@@ -41,7 +41,7 @@ from openpilot.starpilot.ui.vasm_owner import ANNOTATION as VASM_ANNOTATION, RES
PAGES = frozenset(("hub", "localization", "developer", "vehicle", "slc", "lane", "lane_change", "profiles", "personalities",
"traffic", "aggressive", "standard", "relaxed",
"curve", "torque", "torque/bluepilot", "aol", "wheel", "appearance", "display", "data", "power", "sounds", "pip", "sentry", "vasm",
"curve", "torque", "tuning", "torque/bluepilot", "aol", "wheel", "appearance", "display", "data", "power", "sounds", "pip", "sentry", "vasm",
"conditional", "conditional/cem", "conditional/ccm",
*(f"{name}/{category}" for name in ("traffic", "aggressive", "standard", "relaxed")
for category in ("acceleration", "braking", "following"))))
@@ -393,6 +393,7 @@ def _projection(row: FeatureRow) -> dict:
row.key in (PIP_RESET, PIP_EDITOR, VASM_RESET, VASM_ANNOTATION) or row.key.startswith(PIP_FORMAT_PREFIX)),
"repairValue": row.repair_value, "defaultValue": row.default_value, "resetAvailable": row_default(row) is not None,
"displayScale": "lane-strength" if row.key == "LaneCenteringStrength" else None,
**({"coordinate": row.key.removeprefix("torque:surface:")} if row.key.startswith("torque:surface:") and row.key != "torque:surface:reset" else {}),
"heading": is_heading(row.key)}
@@ -457,6 +458,8 @@ def _question(row: FeatureRow, request: FeatureSettingsRequest) -> str:
if key in ("profile:global_acceleration", "profile:global_braking"):
return (f"Save {row.label.lower()} as {request.value}? Personalities set to Selected Profile follow this choice; " +
"explicit personality overrides stay unchanged.")
if key == "torque:surface:reset":
return "Remove the thirty manual overrides for this vehicle? Saved Auto Learn history is kept."
if key == "torque_gain_rebase":
return "Apply the saved steering response to the selected controller and current vehicle tune? Friction and other models stay unchanged."
if key == "torque_rebase":
@@ -788,6 +791,8 @@ class SettingsGateway:
if direction != 0:
raise SettingsChanged("Invalid action")
value = ("Off" if row.value == "On" else "On") if row.key == SETUP_ACTION else "confirm"
if row.key == "torque:surface:reset":
value = "Reset"
if row.key == VASM_ANNOTATION:
if draft is None:
raise SettingsChanged("Camera regions are required")
@@ -0,0 +1,72 @@
"""Shared declared-context HTTP fixture; no physical vehicle evidence."""
from pathlib import Path
from types import SimpleNamespace
import http.client
import json
import tempfile
import threading
import unittest
from opendbc.car.structs import car
from opendbc.car.toyota.interface import CarInterface as ToyotaInterface
from opendbc.car.toyota.values import CAR as ToyotaCar
from openpilot.common.params import Params
from openpilot.starpilot.galaxy.settings import AuthorityContext, SettingsGateway
from openpilot.starpilot.galaxy.access import GalaxyAccessOwner
from openpilot.starpilot.galaxy.server import make_server
class MutableContext:
def __init__(self, cp):
self.value = AuthorityContext(True, cp, b"verified-cp")
def sample(self):
return self.value
class SettingsHttpFixture(unittest.TestCase):
def setUp(self):
temporary = tempfile.TemporaryDirectory()
self.addCleanup(temporary.cleanup)
self.params = Params(temporary.name)
native = ToyotaInterface.get_non_essential_params(ToyotaCar.TOYOTA_COROLLA_TSS2)
cp = SimpleNamespace(carFingerprint="TOYOTA_COROLLA_TSS2", openpilotLongitudinalControl=True,
pcmCruise=False, notCar=False, dashcamOnly=False, passive=False,
brand="toyota", steerControlType=native.steerControlType, carVin="",
lateralTuning=native.lateralTuning, minSteerSpeed=native.minSteerSpeed, safetyConfigs=native.safetyConfigs,
transmissionType=car.CarParams.TransmissionType.automatic)
self.context = MutableContext(cp)
self.gateway = SettingsGateway(self.params, self.context)
self.access = GalaxyAccessOwner(Path(temporary.name) / "access")
self.assertTrue(self.access.configure("password123", lambda: True))
self.server = make_server(port=0, owner=self.access, settings=self.gateway,
parked=lambda: self.context.sample().parked)
self.worker = threading.Thread(target=self.server.serve_forever, kwargs={"poll_interval": 0.01}, daemon=True)
self.worker.start()
self.addCleanup(self.stop)
def stop(self):
self.server.shutdown()
self.worker.join(timeout=2)
self.server.server_close()
def request(self, path, *, payload=None, cookie="", origin=None):
connection = http.client.HTTPConnection("127.0.0.1", self.server.server_port, timeout=3)
headers = {"Cookie": cookie}
if payload is not None:
headers.update({"Content-Type": "application/json",
"Origin": origin if origin is not None else f"http://127.0.0.1:{self.server.server_port}"})
try:
connection.request("POST" if payload is not None else "GET", path,
body=json.dumps(payload) if payload is not None else None, headers=headers)
result = connection.getresponse()
return result.status, json.loads(result.read()), dict(result.getheaders())
finally:
connection.close()
def login(self):
status, _, headers = self.request("/api/auth/login", payload={"password": "password123"})
self.assertEqual(status, 200)
return headers["Set-Cookie"].split(";", 1)[0]
@@ -0,0 +1,55 @@
"""Manual reset uses the real HTTP preview/confirm path in private settings."""
from pathlib import Path
from openpilot.starpilot.galaxy.settings import AuthorityContext
from openpilot.starpilot.galaxy.tests.settings_http_fixture import SettingsHttpFixture
from openpilot.starpilot.saved_source import read_saved
class ManualResetHttpTest(SettingsHttpFixture):
def test_manual_reset_http_uses_row_choice_and_preserves_history(self):
from opendbc.car.hyundai.interface import CarInterface
from opendbc.car.hyundai.values import CAR
from openpilot.starpilot.lateral.torque_settings import DOCUMENT_KEY, parse_document
cp = CarInterface.get_non_essential_params(CAR.HYUNDAI_IONIQ_6).as_reader()
self.context.value = AuthorityContext(True, cp, cp.as_builder().to_bytes())
history = Path(self.params.get_param_path("AutomaticSteeringLiveTrials"))
history.write_bytes(b"unit-only inert history preservation sentinel")
cookie = self.login()
status, page, _ = self.request("/api/settings/pages/tuning", cookie=cookie)
self.assertEqual(status, 200)
index = next(i for i, row in enumerate(page["rows"]) if row.get("coordinate") == "center_taper_max")
status, intent, _ = self.request("/api/settings/preview", cookie=cookie,
payload={"view": page["view"], "row": index, "value": 1})
self.assertEqual(status, 200)
self.assertEqual(self.request("/api/settings/confirm", cookie=cookie,
payload={"intent": intent["intent"], "confirmed": True})[:2], (200, {"saved": True}))
raw, readable = read_saved(self.params, DOCUMENT_KEY, 16384)
self.assertTrue(readable)
assert raw is not None
manual = parse_document(raw)[str(cp.carFingerprint)].manual_coordinates
assert manual is not None
self.assertEqual(dict(manual.values)["center_taper_max"], .01)
status, page, _ = self.request("/api/settings/pages/tuning", cookie=cookie)
self.assertEqual(status, 200)
index = next(i for i, row in enumerate(page["rows"]) if row["label"] == "Reset manual tune")
status, intent, _ = self.request("/api/settings/preview", cookie=cookie,
payload={"view": page["view"], "row": index, "direction": 0})
self.assertEqual(status, 200)
self.assertEqual(intent["proposed"], "Reset")
self.assertEqual(self.request("/api/settings/confirm", cookie=cookie,
payload={"intent": intent["intent"], "confirmed": True})[:2], (200, {"saved": True}))
raw, readable = read_saved(self.params, DOCUMENT_KEY, 16384)
self.assertTrue(readable)
assert raw is not None
self.assertIsNone(parse_document(raw)[str(cp.carFingerprint)].manual_coordinates)
self.assertEqual(history.read_bytes(), b"unit-only inert history preservation sentinel")
status, page, _ = self.request("/api/settings/pages/tuning", cookie=cookie)
self.assertEqual(status, 200)
self.assertFalse(any(row["label"] == "Reset manual tune" for row in page["rows"]))
coordinates = [row for row in page["rows"] if row.get("coordinate")]
self.assertEqual(len(coordinates), 30)
self.assertTrue(all(float(row["value"]) == 0 for row in coordinates))
@@ -43,6 +43,19 @@ let history = []
for (let i = 1; i <= 260; i++) history = appendPlotSample(history, payload(i), i * 750)
assert.equal(history.length, MAX_POINTS)
assert.equal(history[0].index, 21)
// Optional source ages must not move the shared observation clock backwards.
let variedAge = []
for (const [i, age] of [.1, 1.2, .1, .1, .1].entries()) {
variedAge = appendPlotSample(variedAge, validatePlotPayload(payload(i + 1, { sampleAgeSeconds: age })), 1000 + i * 750)
}
assert.deepEqual(variedAge.map(sample => sample.at), [1000, 1750, 2500, 3250, 4000])
assert.deepEqual(chartGeometry(variedAge, ['desiredLateralAccel', 'actualLateralAccel']).paths.map(paths => paths.length), [1, 1])
const missingObservedInterval = variedAge.map((sample, index) => ({ ...sample, at: sample.at + (index >= 2 ? 2000 : 0) }))
assert.deepEqual(chartGeometry(missingObservedInterval, ['desiredLateralAccel', 'actualLateralAccel']).paths.map(paths => paths.length), [2, 2])
const unavailableSignal = variedAge.map((sample, index) => index === 2 ? { ...sample, actualLateralAccel: null } : sample)
assert.deepEqual(chartGeometry(unavailableSignal, ['desiredLateralAccel', 'actualLateralAccel']).paths.map(paths => paths.length), [1, 2])
assert.equal(appendPlotSample(history, payload(260), 10000).length, MAX_POINTS)
const restarted = appendPlotSample(history, payload(260, { sessionId: 'cd'.repeat(8) }), 10000)
assert.equal(restarted.length, 1)
@@ -141,7 +154,7 @@ aged.feed.start()
aged.advance(90)
await aged.reply(0, payload(1, { sampleAgeSeconds: 1.4 }))
assert.equal(aged.states.at(-1).status, 'current')
assert.equal(aged.states.at(-1).history[0].at, -400)
assert.equal(aged.states.at(-1).history[0].at, 1090) // Receipt time, independent of source age.
aged.advance(10)
assert.equal(aged.states.at(-1).status, 'stale')
aged.feed.stop()
@@ -19,7 +19,7 @@ class PreviewContractTest(unittest.TestCase):
self.assertEqual([tool["path"] for tool in catalog["tools"] if tool["availability"] == "partial-preview"], ["/logs"])
self.assertEqual([tool["path"] for tool in catalog["tools"] if tool["availability"] == "local-only"],
["/bluetooth", "/cameras", "/galaxy", "/manage_models", "/model_laboratory", "/navigation", "/system",
"/theme_maker", "/tuning/plots", "/vehicle", "/starport"])
"/theme_maker", "/tuning", "/vehicle", "/starport"])
self.assertEqual([tool["path"] for tool in catalog["tools"] if tool["availability"] == "always"], [])
self.assertTrue(all(tool["availability"] in {"always", "unavailable", "partial-preview", "local-only"} for tool in catalog["tools"]))
self.assertTrue(all(tool["name"] and tool["description"] and tool["icon"] for tool in catalog["tools"]))
@@ -114,3 +114,16 @@ def test_actual_owner_projections_have_no_uncovered_setting(tmp_path):
for page, owner in (('sounds', SoundsOwner(params, lambda: True)), ('pip', PiPOwner(params, lambda: True, lambda: False)),
('vasm', VASMOwner(params, lambda: True)), ('sentry', SentryOwner(params, lambda: True))):
check(owner.snapshot().rows, page)
def test_manual_coordinate_purposes_preserve_startup_and_context_warnings():
from openpilot.starpilot.flm.canonical_surface_adapter import NAMES
startup = "Parked only. Saved for the next drive; zero restores this stage's original baseline. Manual overrides pause Auto Learn."
changed = "Vehicle tune, controller or effect definition changed. Reset this manual tune before editing."
for name in NAMES:
row = FeatureRow('torque:surface:' + name, 'Manual correction', '0', reason=startup)
assert purpose(row) == 'Adjusts this manual steering correction.'
assert description(row).endswith(startup)
assert description(replace(row, available=False, reason=changed)).endswith(changed)
reset = FeatureRow('torque:surface:reset', 'Reset manual tune', 'Reset')
assert purpose(reset) == 'Removes the thirty manual steering overrides while keeping saved Auto Learn history.'
@@ -4,7 +4,6 @@ from dataclasses import replace
from pathlib import Path
from types import SimpleNamespace
from concurrent.futures import ThreadPoolExecutor
import http.client
import json
import os
import tempfile
@@ -16,25 +15,16 @@ from opendbc.car.structs import car
from opendbc.car.toyota.interface import CarInterface as ToyotaInterface
from opendbc.car.toyota.values import CAR as ToyotaCar
from openpilot.common.params import Params
from openpilot.starpilot.galaxy.tests.settings_http_fixture import MutableContext, SettingsHttpFixture
from openpilot.starpilot.galaxy.settings import (
AuthorityContext, LiveContextSource, PAGES, SettingsChanged, SettingsGateway,
)
from openpilot.starpilot.galaxy.access import GalaxyAccessOwner
from openpilot.starpilot.galaxy.server import make_server
from openpilot.starpilot.saved_source import read_saved
from openpilot.starpilot.lateral.controller_selection import learning_allowed
from openpilot.starpilot.lateral.learning_preferences import KEY as LEARNING_KEY, read_preference, replace_preference
from openpilot.starpilot import saved_document
class MutableContext:
def __init__(self, cp):
self.value = AuthorityContext(True, cp, b"verified-cp")
def sample(self):
return self.value
class SettingsGatewayTest(unittest.TestCase):
def setUp(self):
temporary = tempfile.TemporaryDirectory()
@@ -1512,50 +1502,7 @@ class LiveContextTest(unittest.TestCase):
self.assertEqual(path.read_bytes(), b"not a versioned cache")
class SettingsHttpTest(unittest.TestCase):
def setUp(self):
temporary = tempfile.TemporaryDirectory()
self.addCleanup(temporary.cleanup)
self.params = Params(temporary.name)
native = ToyotaInterface.get_non_essential_params(ToyotaCar.TOYOTA_COROLLA_TSS2)
cp = SimpleNamespace(carFingerprint="TOYOTA_COROLLA_TSS2", openpilotLongitudinalControl=True,
pcmCruise=False, notCar=False, dashcamOnly=False, passive=False,
brand="toyota", steerControlType=native.steerControlType, carVin="",
lateralTuning=native.lateralTuning, minSteerSpeed=native.minSteerSpeed, safetyConfigs=native.safetyConfigs,
transmissionType=car.CarParams.TransmissionType.automatic)
self.context = MutableContext(cp)
self.gateway = SettingsGateway(self.params, self.context)
self.access = GalaxyAccessOwner(Path(temporary.name) / "access")
self.assertTrue(self.access.configure("password123", lambda: True))
self.server = make_server(port=0, owner=self.access, settings=self.gateway)
self.worker = threading.Thread(target=self.server.serve_forever, kwargs={"poll_interval": 0.01}, daemon=True)
self.worker.start()
self.addCleanup(self.stop)
def stop(self):
self.server.shutdown()
self.worker.join(timeout=2)
self.server.server_close()
def request(self, path, *, payload=None, cookie="", origin=None):
connection = http.client.HTTPConnection("127.0.0.1", self.server.server_port, timeout=3)
headers = {"Cookie": cookie}
if payload is not None:
headers.update({"Content-Type": "application/json",
"Origin": origin if origin is not None else f"http://127.0.0.1:{self.server.server_port}"})
try:
connection.request("POST" if payload is not None else "GET", path,
body=json.dumps(payload) if payload is not None else None, headers=headers)
result = connection.getresponse()
return result.status, json.loads(result.read()), dict(result.getheaders())
finally:
connection.close()
def login(self):
status, _, headers = self.request("/api/auth/login", payload={"password": "password123"})
self.assertEqual(status, 200)
return headers["Set-Cookie"].split(";", 1)[0]
class SettingsHttpTest(SettingsHttpFixture):
def test_force_stop_disable_http_while_onroad(self):
self.context.value = AuthorityContext(False, self.context.value.cp, b"verified-cp")
self.params.put_bool("ForceStops", True, block=True)
@@ -2,47 +2,61 @@ import assert from 'node:assert/strict'
import { FlmPage } from '../web/js/flm.js'
import { PlotsPage } from '../web/js/plots.js'
import { SettingsFeed } from '../web/js/settings.js'
import { SURFACE_GROUPS } from '../web/js/flm-training-results.js'
const vm = { ...FlmPage.data(), activePage: true, mode: 'local', unauthorized() {} }
for (const [name, method] of Object.entries(FlmPage.methods)) vm[name] = method.bind(vm)
for (const [name, method] of Object.entries(FlmPage.computed)) Object.defineProperty(vm, name, { get: () => method.call(vm) })
const calls = []
const snapshot = { page: 'torque', view: 'original-view', subtitle: 'Saved steering tune', rows: [
{ label: 'Use StarPilot Auto Learn', value: 'On', choices: ['Off', 'On'], available: true, action: true },
{ label: 'Lat Accel', value: '2', minimum: 1, maximum: 3, step: .05, available: true, action: true },
{ label: 'Friction', value: '.1', minimum: 0, maximum: .2, step: .01, available: false, action: true },
] }
const timers = new Map()
const names = [...new Set(SURFACE_GROUPS.flatMap(group => group.names))]
assert.equal(names.length, 30)
const selected = names.indexOf(vm.selectedGroup.names[0])
const locked = names.indexOf(vm.selectedGroup.names[1])
const snapshot = { page: 'tuning', view: 'original-view', subtitle: 'Parked edits apply next drive', parked: true,
rows: names.map((coordinate, index) => ({ label: coordinate, coordinate, value: '0',
minimum: 0, maximum: 25, step: .25, available: index !== locked, action: true })) }
const calls = [], timers = new Map()
let edited = null
vm.manualFeed = new SettingsFeed({ publish: update => Object.assign(vm.manualState, update),
later: fn => { const key = timers.size + 1; timers.set(key, fn); return key }, cancelTimer: id => timers.delete(id),
fetcher: async (path, options) => {
calls.push({ path, body: options.body ? JSON.parse(options.body) : null })
if (path.endsWith('/torque')) return { ok: true, json: async () => structuredClone(snapshot) }
if (path.endsWith('/preview')) return { ok: true, json: async () => ({ intent: 'actual-settings-intent' }) }
const body = options.body ? JSON.parse(options.body) : null
calls.push({ path, body })
if (path.endsWith('/tuning')) return { ok: true, json: async () => structuredClone(snapshot) }
assert(!path.endsWith('/torque'), 'Tuning does not fetch the duplicate scalar controls')
if (path.endsWith('/preview')) { edited = body; return { ok: true, json: async () => ({ intent: 'manual-coordinate-intent' }) } }
assert(path.endsWith('/confirm'))
return { ok: true, json: async () => ({ saved: true, page: { ...structuredClone(snapshot), view: 'saved-view',
rows: snapshot.rows.map((row, index) => index === 1 ? { ...row, value: '2.1' } : row) } }) }
rows: snapshot.rows.map((row, index) => index === edited.row ? { ...row, value: String(edited.value) } : row) } }) }
} })
await vm.selectView('overview')
assert.equal(calls.length, 0, 'Numeric settings do not poll outside Your tune')
assert.equal(calls.length, 0)
await vm.selectView('tune')
assert.deepEqual(vm.manualRows.map(item => [item.row.label, item.index]), [['Lat Accel', 1], ['Friction', 2]])
assert.equal(vm.manualRows[1].row.available, false, 'Backend unavailable control stays unavailable')
assert.equal(await vm.saveManual(2, .12), false, 'Unavailable controls cannot dispatch edits')
assert.equal(await vm.saveManual(0, 'Off'), false, 'Only numeric rows are editable here')
assert(await vm.saveManual(1, 2.1))
assert.deepEqual(calls[1].body, { view: 'original-view', row: 1, value: 2.1 })
assert.deepEqual(calls[2].body, { intent: 'actual-settings-intent', confirmed: true, page: 'torque' })
assert.equal(vm.manualRows[0].row.value, '2.1', 'Display changes only after saved readback')
assert.equal(calls.length, 3, 'One edit uses existing preview then confirm')
assert.equal(calls[0].path, './api/settings/pages/tuning')
assert.equal(vm.manualRows.length, vm.selectedGroup.names.length, 'Only the selected group is shown')
assert.equal(await vm.saveManual(locked, 1), false, 'Unavailable controls cannot dispatch edits')
assert.equal(await vm.saveManual(selected, 1), true)
assert.deepEqual(calls[1].body, { view: 'original-view', row: selected, value: 1 })
assert.deepEqual(calls[2].body, { intent: 'manual-coordinate-intent', confirmed: true, page: 'tuning' })
assert.equal(vm.manualState.data.rows[selected].value, '1', 'Only confirmed saved readback changes the displayed setting')
assert.equal(vm.manualResetRequested, false, 'Numeric save does not open the reset dialog')
vm.manualState.error = 'Saved settings changed'
assert.equal(await vm.saveManual(1, 2.2), false)
assert.equal(calls.length, 3, 'No writes with stale or failed settings')
assert.equal(await vm.saveManual(selected, 2), false)
assert.equal(calls.length, 3)
vm.manualState.error = ''
const resetIndex = vm.manualState.data.rows.length
vm.manualState.data.rows.push({ label: 'Reset manual tune', value: 'Reset', choices: ['Reset'], confirm: true, available: true, action: true })
await vm.reviewManualReset(resetIndex, 1)
assert.equal(vm.manualResetRequested, true)
assert(vm.manualState.pending, 'Reset is held for explicit confirmation')
assert.deepEqual(calls.at(-1).body, { view: 'saved-view', row: resetIndex, direction: 1 })
vm.cancelManualReset()
assert.equal(vm.manualState.pending, null)
assert.equal(vm.manualResetRequested, false)
assert.equal(vm.manualState.data.rows[selected].value, '1', 'Cancel preserves the saved manual value')
await vm.selectView('plots')
assert.equal(vm.manualFeed.active, false)
assert.equal(timers.size, 0, 'Leaving Your tune stops settings timers')
assert.equal(await vm.saveManual(1, 2.3), false)
assert.equal(timers.size, 0)
assert.equal(await vm.saveManual(selected, 2), false)
const oldDocument = globalThis.document
let started = 0, stopped = 0
@@ -66,4 +80,4 @@ try {
PlotsPage.beforeUnmount.call(page)
assert.equal(stopped, 1)
} finally { globalThis.document = oldDocument }
console.log('Tuning numeric save/readback, stale write refusal, tab polling and retained plot lifecycle passed')
console.log('Thirty-coordinate editor, saved readback, stale-write refusal and retained plot lifecycle passed')
File diff suppressed because it is too large Load Diff
+30 -19
View File
@@ -1,5 +1,6 @@
import { GxState } from "./state.js"
import { GxNotice } from "./notice.js"
import { GxDialog } from "./dialog.js"
import { SettingsFeed, settingsBusy } from "./settings.js"
import { GalaxySettingRow, settingControl } from "./galaxy-setting-row.js"
import { PollTimer, connectionError } from "./polling.js"
@@ -29,18 +30,17 @@ const PARAMETER_LABELS = {
export const FlmPage = {
name: "FlmPage",
components: { GxState, GxNotice, PlotGraph, PlotsPage, GalaxySettingRow },
components: { GxState, GxNotice, GxDialog, PlotGraph, PlotsPage, GalaxySettingRow },
props: { initialView: { type: String, default: "overview" }, mode: { type: String, required: true }, unauthorized: { type: Function, required: true },
go: { type: Function, default: path => { window.location.hash = path } } },
data: () => ({ report: null, error: "", loading: false, request: null, receivedAt: 0, now: 0,
manualState: { status: "idle", data: null, pending: null, error: "" },
manualState: { status: "idle", data: null, pending: null, error: "" }, manualResetRequested: false,
view: "overview", group: "balance", showAllHistory: false, groups: SURFACE_GROUPS }),
watch: { initialView(view) { this.selectView(view) }, mode() { this.syncManualControls() } },
mounted() {
this.activePage = true
this.manualFeed = new SettingsFeed({ publish: update => Object.assign(this.manualState, update), unauthorized: this.unauthorized })
this.selectView(this.initialView)
this.syncManualControls()
this.poller = new PollTimer({ read: () => this.load(), interval: 1000 })
this.visibility = () => {
this.now = Date.now()
@@ -55,7 +55,8 @@ export const FlmPage = {
document.removeEventListener("visibilitychange", this.visibility)
},
computed: {
manualRows() { return (this.manualState.data?.rows || []).map((row, index) => ({ row, index })).filter(({ row }) => settingControl(row) === "slider") },
manualRows() { return (this.manualState.data?.rows || []).map((row, index) => ({ row, index })).filter(({ row }) => this.selectedGroup.names.includes(row.coordinate)) },
manualActions() { return (this.manualState.data?.rows || []).map((row, index) => ({ row, index })).filter(({ row }) => row.confirm) },
manualBusy() { return settingsBusy(this.manualState) },
current() { return this.report?.current },
preference() { return validLearningPreference(this.report?.preference) ? this.report.preference : null },
@@ -108,13 +109,22 @@ export const FlmPage = {
syncManualControls() {
if (!this.manualFeed) return
if (this.activePage && this.mode === "local" && this.view === "tune") {
if (!this.manualFeed.active) return this.manualFeed.start("torque")
} else this.manualFeed.stop()
if (!this.manualFeed.active) return this.manualFeed.start("tuning")
} else { this.manualResetRequested = false; this.manualFeed.stop() }
},
async reviewManualReset(index, direction) {
const row = this.manualState.data?.rows?.[index]
if (this.view !== "tune" || this.manualBusy || this.manualState.error || !row?.confirm || !row.available) return
this.manualResetRequested = true
await this.manualFeed.preview(index, direction)
if (!this.manualState.pending) this.manualResetRequested = false
},
cancelManualReset() { this.manualResetRequested = false; this.manualFeed.cancel() },
async confirmManualReset() { await this.manualFeed.confirm(); this.manualResetRequested = false },
async saveManual(index, value) {
const row = this.manualState.data?.rows?.[index]
if (this.view !== "tune" || !this.manualFeed?.active || this.manualBusy || this.manualState.error ||
!row?.available || !row.action || settingControl(row) !== "slider") return false
!this.selectedGroup.names.includes(row?.coordinate) || !row.available || !row.action || settingControl(row) !== "slider") return false
await this.manualFeed.previewValue(index, value)
return !this.manualState.error && this.manualState.status === "ready"
},
@@ -124,7 +134,7 @@ export const FlmPage = {
event.preventDefault()
const index = VIEWS.indexOf(this.view)
const next = event.key === "Home" ? 0 : event.key === "End" ? VIEWS.length - 1 : (index + delta + VIEWS.length) % VIEWS.length
this.view = VIEWS[next]
this.selectView(VIEWS[next])
event.currentTarget.parentElement.querySelectorAll('[role="tab"]')[next]?.focus()
},
async load() {
@@ -153,8 +163,8 @@ export const FlmPage = {
<header class="gx-page-header gx-settings__header gx-tuner__heading"><div><p class="gx-tuner__eyebrow">STEERING · AUTO LEARN</p><h2>Tuning and Analysis</h2><p>{{ vehicle }}</p></div></header>
<GxState v-if="mode !== 'local'">Connect to your device to see its steering tune.</GxState>
<template v-else>
<GxNotice v-if="error" tone="danger">{{ error }}</GxNotice>
<section class="gx-card gx-tuner__hero" :data-tone="state.tone" aria-label="Auto Learn status">
<GxNotice v-if="error && view !== 'plots'" tone="danger">{{ error }}</GxNotice>
<section v-if="view !== 'plots'" class="gx-card gx-tuner__hero" :data-tone="state.tone" aria-label="Auto Learn status">
<div class="gx-tuner__hero-icon" aria-hidden="true"><i class="bi bi-sliders2"></i></div>
<div class="gx-tuner__hero-copy"><div class="gx-tuner__eyebrow"><span class="gx-tuner__status-dot"></span>{{ loading && !report ? 'CONNECTING' : state.key === 'off' ? 'DISABLED' : choiceLive || live ? 'DEVICE CONNECTED' : 'WAITING FOR DATA' }}</div>
<h3 role="status">{{ loading && !report ? 'Reading your tuner…' : state.title }}</h3><p>{{ state.description }}</p>
@@ -162,6 +172,7 @@ export const FlmPage = {
</div>
<div class="gx-tuner__hero-meta"><span>{{ currentApplied ? 'Controller acknowledged' : live ? 'No correction acknowledged' : 'Current application unknown' }}</span><small>{{ reportAge === null ? 'No learner report yet' : 'Learner report ' + reportAge.toFixed(1) + 's old' }}</small></div>
</section>
<GxDialog v-if="manualResetRequested && manualState.pending" labelledby="manual-tune-confirm-title" @close="cancelManualReset"><h3 id="manual-tune-confirm-title">Reset manual tune?</h3><p>{{ manualState.pending.question }}</p><div class="gx-dialog__actions"><button type="button" class="gx-btn gx-btn--tonal" @click="cancelManualReset">Cancel</button><button type="button" class="gx-btn" @click="confirmManualReset">Reset</button></div></GxDialog>
<nav class="gx-tuner__tabs" role="tablist" aria-label="Tuner views">
<button v-for="tab in ['overview', 'tune', 'activity', 'plots']" :key="tab" type="button" role="tab" :id="'tuner-tab-' + tab" :aria-controls="'tuner-panel-' + tab" :aria-selected="view === tab" :tabindex="view === tab ? 0 : -1" @click="selectView(tab)" @keydown="tabKey">{{ tab === 'overview' ? 'Overview' : tab === 'tune' ? 'Your tune' : tab === 'activity' ? 'Activity' : 'Live plots' }}<span v-if="tab === 'tune'" class="gx-tuner__count">30</span></button>
</nav>
@@ -182,17 +193,17 @@ export const FlmPage = {
</section></div>
</section>
<section v-if="view === 'tune'" id="tuner-panel-tune" role="tabpanel" aria-labelledby="tuner-tab-tune" class="gx-tuner__panel">
<section class="gx-card gx-tuner__section gx-settings__section gx-tuner__manual"><div class="gx-tuner__section-heading"><div><h3>Steering controls</h3><p>Adjust your saved vehicle tune.</p></div></div>
<GxNotice v-if="manualState.error" tone="danger">{{ manualState.error }}</GxNotice>
<p v-if="!manualState.data" class="gx-tuner__caption">{{ manualState.status === 'loading' ? 'Loading steering controls…' : 'Steering controls unavailable. Reconnecting…' }}</p>
<template v-else><p class="gx-tuner__caption">{{ manualState.data.subtitle }}</p>
<GalaxySettingRow v-for="{ row, index } in manualRows" :key="index" :row="row" :index="index" :busy="manualBusy" :disabled="!!manualState.error" :save-value="saveManual" @reset-default="index => manualFeed.resetDefault(index)" />
<p v-if="!manualRows.length" class="gx-tuner__caption">No manual numeric controls are available for this vehicle.</p>
</template>
</section>
<div class="gx-tuner__section-heading"><div><h3>Auto Learn corrections</h3><p>Thirty automatic correction values, grouped by steering behavior.</p></div><span class="gx-tuner__pill">Reported corrections</span></div>
<div class="gx-tuner__section-heading"><div><h3>Your tune</h3><p>Thirty steering parameters, grouped by behavior.</p></div><span class="gx-tuner__pill">Manual + Auto Learn</span></div>
<div class="gx-tuner__tune-layout"><nav class="gx-tuner__groups" aria-label="Steering parameter groups"><button v-for="item in groups" :key="item.id" type="button" :aria-pressed="group === item.id" @click="group = item.id"><i :class="['bi', item.icon]" aria-hidden="true"></i><span>{{ item.label }}<small>{{ item.names.length }} parameters · {{ groupChanges(item) }}</small></span><i class="bi bi-chevron-right" aria-hidden="true"></i></button></nav>
<section class="gx-card gx-tuner__section gx-tuner__parameters"><div class="gx-tuner__section-heading"><div><h3>{{ selectedGroup.label }}</h3><p>{{ selectedGroup.description }}</p></div></div>
<section class="gx-tuner__manual" aria-label="Manual tuning"><h4>Manual tune</h4><p class="gx-tuner__caption">Parked edits apply to the next drive. Auto Learn pauses while manual overrides are set. Zero removes an override.</p>
<GxNotice v-if="manualState.error" tone="danger">{{ manualState.error }}</GxNotice>
<p v-if="!manualState.data" class="gx-tuner__caption">{{ manualState.status === 'loading' ? 'Loading saved manual tune…' : 'Manual tuning unavailable. Reconnecting…' }}</p>
<p v-else-if="!manualRows.length" class="gx-tuner__caption">{{ manualState.data.rows[0]?.reason || 'Manual tuning is unavailable for this controller.' }}</p>
<GalaxySettingRow v-for="{ row, index } in manualRows" :key="row.coordinate" :row="{ ...row, label: parameterLabel(row.coordinate), reason: row.available ? '' : row.reason }" :index="index" :busy="manualBusy" :disabled="!!manualState.error" :save-value="saveManual" @reset-default="index => manualFeed.resetDefault(index)" />
<GalaxySettingRow v-for="{ row, index } in manualActions" :key="index" :row="row" :index="index" :busy="manualBusy" :disabled="!!manualState.error" :save-value="saveManual" @review="reviewManualReset" />
</section>
<h4>Auto Learn evidence</h4><p class="gx-tuner__caption">These reported results are separate from your manual settings.</p>
<div class="gx-tuner__parameter-head" aria-hidden="true"><span>Parameter</span><span>{{ trackingValues ? 'Accepted' : 'Proposal' }}</span><span>Trial</span><span>Applied now</span></div>
<div v-for="row in groupRows" :key="row.name" class="gx-tuner__parameter"><div><h4>{{ parameterLabel(row.name) }}</h4><small v-if="!row.pidSupported">Torque only · PID unsupported</small></div><div><span class="gx-tuner__mobile-label">{{ trackingValues ? 'Accepted' : 'Proposal' }}</span><strong>{{ coordinateText(trackingValues ? row.accepted : row.learned) }}</strong></div><div><span class="gx-tuner__mobile-label">Trial</span><strong :class="{ 'gx-tuner__pending': row.pending !== null && row.pending !== row.accepted }">{{ coordinateText(row.pending) }}</strong></div><div><span class="gx-tuner__mobile-label">Applied now</span><strong :class="{ 'gx-tuner__applied': row.applied !== null && row.applied !== 0 }">{{ coordinateText(row.applied) }}</strong></div></div>
<details class="gx-tuner__disclosure"><summary>How to read these values</summary><p>Zero is the original baseline. These are correction coordinates, not raw degrees or torque. Speed-window coordinates scale the original speed range; deadband and angle assistance use the controller's physical normalizer. A trial is an offer, not proof of application. All 30 stages exist for torque; 26 exist for torque-actuated PID.</p></details>
+12 -11
View File
@@ -43,7 +43,8 @@ export function appendPlotSample(history, payload, observedAt) {
if (payload.sampleIndex === (history.at(-1)?.index ?? 0)) return history
if (payload.sampleIndex < (history.at(-1)?.index ?? 0)) history = []
const sample = { ...payload.values, sessionId: payload.sessionId, index: payload.sampleIndex,
at: observedAt - payload.sampleAgeSeconds * 1000 }
// Chart client observations; source age remains a separate freshness check.
at: observedAt }
return [...history, sample].slice(-MAX_POINTS)
}
@@ -146,7 +147,7 @@ export class PlotsFeed {
const displayGeneration = ++this.displayGeneration
const remaining = data.sampleAgeSeconds === null ? 0 : 1500 - data.sampleAgeSeconds * 1000 - elapsed
if (data.state === 'current' && remaining > 0) {
this.history = appendPlotSample(this.history, data, this.now() - elapsed)
this.history = appendPlotSample(this.history, data, this.now())
this.lastData = data
this.publish({ status: 'current', data, history: this.history, error: '' })
this.expiry = this.later(() => {
@@ -232,18 +233,18 @@ export const PlotsPage = {
<span class="gx-plots__status" role="status">{{ paused ? 'Paused' : data?.bootStabilizing ? 'Starting vehicle systems' : status === 'current' ? 'Live' : status === 'stale' ? 'Reconnecting · last received values' : status === 'unavailable' ? 'Unavailable' : 'Connecting' }}</span></div>
<p class="gx-note gx-plots__hint" role="status">{{ !paused && error ? error : 'Missing samples leave a gap in the graph.' }}</p>
<div class="gx-plots__grid">
<section class="gx-card gx-plots__panel"><div class="gx-section__header"><i class="bi bi-graph-up" aria-hidden="true"></i><span class="gx-section__title">Lateral Acceleration</span></div><p class="gx-note">Desired / actual · m/s² · {{ source(data?.values?.lateralSource) }}</p>
<section class="gx-card gx-plots__panel"><div class="gx-section__header"><i class="bi bi-graph-up" aria-hidden="true"></i><span class="gx-section__title">Lateral Acceleration</span></div><p class="gx-note gx-plots__source">Desired / actual · m/s² · {{ source(data?.values?.lateralSource) }}</p>
<plot-graph :chart="lateralChart" title="Lateral acceleration history" />
<p>Desired: {{ label(data?.values?.desiredLateralAccel) }} · Actual: {{ label(data?.values?.actualLateralAccel) }}</p>
<p class="gx-note">Speed source: {{ source(data?.values?.speedSource) }}</p>
<p>30 s match: <strong>{{ !data || data?.bootStabilizing ? 'N/A' : lateralQuality.label }}</strong> · {{ data?.bootStabilizing ? 'Waiting for startup to settle' : status !== 'current' ? 'Last received window' : lateralQuality.detail }}</p></section>
<section class="gx-card gx-plots__panel"><div class="gx-section__header"><i class="bi bi-graph-up-arrow" aria-hidden="true"></i><span class="gx-section__title">Longitudinal Acceleration</span></div><p class="gx-note">Target / measured · m/s² · {{ source(data?.values?.longitudinalSource) }}; measured from {{ data?.values?.poseFresh ? 'device motion' : 'unavailable motion source' }}</p>
<dl class="gx-plots__readout"><div><dt>Desired</dt><dd>{{ label(data?.values?.desiredLateralAccel) }}</dd></div><div><dt>Actual</dt><dd>{{ label(data?.values?.actualLateralAccel) }}</dd></div></dl>
<p class="gx-note gx-plots__source">Speed source: {{ source(data?.values?.speedSource) }}</p>
<p class="gx-plots__match">30 s match: <strong>{{ !data || data?.bootStabilizing ? 'N/A' : lateralQuality.label }}</strong> · {{ data?.bootStabilizing ? 'Waiting for startup to settle' : status !== 'current' ? 'Last received window' : lateralQuality.detail }}</p></section>
<section class="gx-card gx-plots__panel"><div class="gx-section__header"><i class="bi bi-graph-up-arrow" aria-hidden="true"></i><span class="gx-section__title">Longitudinal Acceleration</span></div><p class="gx-note gx-plots__source">Target / measured · m/s² · {{ source(data?.values?.longitudinalSource) }}; measured from {{ data?.values?.poseFresh ? 'device motion' : 'unavailable motion source' }}</p>
<plot-graph :chart="longChart" title="Longitudinal acceleration history" />
<p>Target: {{ label(data?.values?.desiredLongitudinalAccel) }} · Measured: {{ label(data?.values?.actualLongitudinalAccel) }}</p>
<p>30 s match: <strong>{{ !data || data?.bootStabilizing ? 'N/A' : longQuality.label }}</strong> · {{ data?.bootStabilizing ? 'Waiting for startup to settle' : status !== 'current' ? 'Last received window' : longQuality.detail }}</p></section>
<section v-if="advanced" class="gx-card gx-plots__panel"><div class="gx-section__header"><i class="bi bi-sliders" aria-hidden="true"></i><span class="gx-section__title">Lateral Controller Terms</span></div><p class="gx-note">P / I / D / F · controller output units · {{ source(data?.values?.lateralTermsSource) }}</p>
<dl class="gx-plots__readout"><div><dt>Target</dt><dd>{{ label(data?.values?.desiredLongitudinalAccel) }}</dd></div><div><dt>Measured</dt><dd>{{ label(data?.values?.actualLongitudinalAccel) }}</dd></div></dl>
<p class="gx-plots__match">30 s match: <strong>{{ !data || data?.bootStabilizing ? 'N/A' : longQuality.label }}</strong> · {{ data?.bootStabilizing ? 'Waiting for startup to settle' : status !== 'current' ? 'Last received window' : longQuality.detail }}</p></section>
<section v-if="advanced" class="gx-card gx-plots__panel"><div class="gx-section__header"><i class="bi bi-sliders" aria-hidden="true"></i><span class="gx-section__title">Lateral Controller Terms</span></div><p class="gx-note gx-plots__source">P / I / D / F · controller output units · {{ source(data?.values?.lateralTermsSource) }}</p>
<plot-graph :chart="lateralTermsChart" title="Lateral controller terms" :terms="true" /></section>
<section v-if="advanced" class="gx-card gx-plots__panel"><div class="gx-section__header"><i class="bi bi-sliders" aria-hidden="true"></i><span class="gx-section__title">Longitudinal Controller Terms</span></div><p class="gx-note">P / I / F · m/s² · {{ source(data?.values?.longitudinalTermsSource) }}</p>
<section v-if="advanced" class="gx-card gx-plots__panel"><div class="gx-section__header"><i class="bi bi-sliders" aria-hidden="true"></i><span class="gx-section__title">Longitudinal Controller Terms</span></div><p class="gx-note gx-plots__source">P / I / F · m/s² · {{ source(data?.values?.longitudinalTermsSource) }}</p>
<plot-graph :chart="longTermsChart" title="Longitudinal controller terms" :terms="true" /></section>
</div><p class="gx-note">Blue is target, orange is observed. Missing or stale sources are never plotted as zero.</p>
</template>
+24 -5
View File
@@ -6,7 +6,7 @@ from dataclasses import dataclass, replace
from enum import StrEnum
import json
import math
from typing import cast
from typing import TYPE_CHECKING, cast
from openpilot.starpilot.lateral.torque_supported import BOLT_VEHICLES, GAIN_VEHICLES, GM_VEHICLES
@@ -17,6 +17,9 @@ MAX_DOCUMENT_BYTES = 16_384
from openpilot.starpilot.flm.torque_surface import GmFlmBinding
from opendbc.car.values import PLATFORMS
if TYPE_CHECKING:
from openpilot.starpilot.flm.manual_coordinates import ManualCoordinates
@dataclass(frozen=True)
class FieldChoice:
@@ -49,6 +52,7 @@ class PlatformProfile:
gain_basis: GainBasis | None = None
geometry: GeometryProfile | None = None
flm: GmFlmBinding | None = None
manual_coordinates: ManualCoordinates | None = None
class LegacyMode(StrEnum):
@@ -163,7 +167,7 @@ def parse_document(raw: bytes) -> dict[str, PlatformProfile]:
except (RecursionError, OverflowError) as exc:
raise ValueError("Malformed torque document") from exc
if not isinstance(document, dict) or set(document) != {"schemaVersion", "vehicles"} or \
type(document["schemaVersion"]) is not int or document["schemaVersion"] not in (1, 2, 3, 4) or \
type(document["schemaVersion"]) is not int or document["schemaVersion"] not in (1, 2, 3, 4, 5) or \
not isinstance(document["vehicles"], dict) or len(document["vehicles"]) > len(PLATFORMS):
raise ValueError("Malformed torque document")
profiles: dict[str, PlatformProfile] = {}
@@ -173,8 +177,10 @@ def parse_document(raw: bytes) -> dict[str, PlatformProfile]:
expected |= {"proportionalGain", "gainBasis"}
if document["schemaVersion"] >= 3 and isinstance(value, dict) and "geometry" in value:
expected |= {"geometry"}
if document["schemaVersion"] == 4 and isinstance(value, dict) and "flm" in value:
if document["schemaVersion"] >= 4 and isinstance(value, dict) and "flm" in value:
expected |= {"flm"}
if document["schemaVersion"] == 5 and isinstance(value, dict) and "manualCoordinates" in value:
expected |= {"manualCoordinates"}
if fingerprint not in PLATFORMS or not isinstance(value, dict) or set(value) != expected:
raise ValueError("Malformed torque profile")
basis_obj = value["basis"]
@@ -204,13 +210,21 @@ def parse_document(raw: bytes) -> dict[str, PlatformProfile]:
raise ValueError("Unsupported GM FLM profile")
flm = GmFlmBinding.from_document(value["flm"])
flm.validate_manual(fingerprint)
profiles[fingerprint] = PlatformProfile(basis, _choice(value["factor"]), _choice(value["friction"]), gain, gain_basis, geometry, flm)
manual = None
if "manualCoordinates" in value:
from openpilot.starpilot.flm.manual_coordinates import ManualCoordinates
manual = ManualCoordinates.from_document(value["manualCoordinates"])
if flm is not None:
raise ValueError("Only one manual surface may be selected")
profiles[fingerprint] = PlatformProfile(basis, _choice(value["factor"]), _choice(value["friction"]), gain, gain_basis, geometry, flm, manual)
return profiles
def serialize_document(profiles: dict[str, PlatformProfile]) -> bytes:
vehicles: dict[str, dict] = {}
version = (4 if any(p.flm is not None for p in profiles.values()) else 3 if any(p.geometry is not None for p in profiles.values())
version = (5 if any(p.manual_coordinates is not None for p in profiles.values())
else 4 if any(p.flm is not None for p in profiles.values())
else 3 if any(p.geometry is not None for p in profiles.values())
else 2 if any(p.gain_basis is not None for p in profiles.values()) else 1)
for fingerprint, profile in profiles.items():
vehicles[fingerprint] = {
@@ -235,6 +249,11 @@ def serialize_document(profiles: dict[str, PlatformProfile]) -> bytes:
}
if profile.flm is not None:
vehicles[fingerprint]["flm"] = profile.flm.document()
if profile.manual_coordinates is not None:
from openpilot.starpilot.flm.manual_coordinates import ManualCoordinates
if type(profile.manual_coordinates) is not ManualCoordinates:
raise ValueError("Typed manual coordinates required")
vehicles[fingerprint]["manualCoordinates"] = profile.manual_coordinates.document()
raw = json.dumps({"schemaVersion": version, "vehicles": vehicles}, sort_keys=True, separators=(",", ":"), allow_nan=False).encode()
parse_document(raw)
return raw
@@ -2,6 +2,7 @@
from unittest.mock import Mock
from openpilot.cereal.messaging import PubMaster
from openpilot.starpilot.lateral.controller_selection import default_selection
from openpilot.starpilot.longitudinal.extension import LongitudinalContext
from openpilot.starpilot.longitudinal.tests.extension_helpers import extension_state
import os
@@ -159,7 +160,7 @@ class TestEuvLongitudinal(unittest.TestCase):
with patch('openpilot.selfdrive.controls.controlsd.Params'), \
patch('openpilot.selfdrive.controls.controlsd.messaging.log_from_bytes', return_value=cp), \
patch('openpilot.selfdrive.controls.controlsd.read_selection', return_value=NS(policy=None)), \
patch('openpilot.selfdrive.controls.controlsd.read_selection', return_value=NS(policy=None, mode=default_selection(cp).mode)), \
patch('openpilot.selfdrive.controls.controlsd.learning_allowed', return_value=False), \
patch('openpilot.selfdrive.controls.controlsd.feature_enabled', return_value=False), \
patch('openpilot.starpilot.longitudinal.inputs.toyota_development_enabled', return_value=False), \
@@ -3,6 +3,7 @@
from unittest.mock import Mock
from unittest.mock import patch
from openpilot.starpilot.lateral.controller_selection import default_selection
from openpilot.starpilot.longitudinal.extension import LongitudinalContext
from openpilot.starpilot.longitudinal.tests.extension_helpers import extension_state
import os
@@ -150,7 +151,7 @@ class TestVoltCc(unittest.TestCase):
for alpha in (False, True):
with patch('openpilot.selfdrive.controls.controlsd.Params'), \
patch('openpilot.selfdrive.controls.controlsd.messaging.log_from_bytes', return_value=params(alpha)), \
patch('openpilot.selfdrive.controls.controlsd.read_selection', return_value=NS(policy=None)), \
patch('openpilot.selfdrive.controls.controlsd.read_selection', return_value=NS(policy=None, mode=default_selection(params(alpha)).mode)), \
patch('openpilot.selfdrive.controls.controlsd.learning_allowed', return_value=False), \
patch('openpilot.selfdrive.controls.controlsd.feature_enabled', return_value=False), \
patch('openpilot.starpilot.longitudinal.inputs.toyota_development_enabled', return_value=False), \
@@ -1,5 +1,6 @@
"""Default Volt STOP ownership, qualified release, and parsed Controls-to-CAN recurrence."""
from openpilot.starpilot.lateral.controller_selection import default_selection
from openpilot.starpilot.longitudinal.extension import LongitudinalContext
from types import SimpleNamespace as NS
import unittest
@@ -101,7 +102,7 @@ class TestVoltStop(unittest.TestCase):
raise Registered
with patch('openpilot.selfdrive.controls.controlsd.Params'), \
patch('openpilot.selfdrive.controls.controlsd.messaging.log_from_bytes', return_value=cp), \
patch('openpilot.selfdrive.controls.controlsd.read_selection', return_value=NS(policy=None)), \
patch('openpilot.selfdrive.controls.controlsd.read_selection', return_value=NS(policy=None, mode=default_selection(cp).mode)), \
patch('openpilot.selfdrive.controls.controlsd.learning_allowed', return_value=False), \
patch('openpilot.selfdrive.controls.controlsd.feature_enabled', return_value=False), \
patch('openpilot.starpilot.longitudinal.inputs.toyota_development_enabled', return_value=False), \
@@ -1029,6 +1029,11 @@ class FeatureSettingsOwner:
title = "BluePilot Controller Options"
rows.extend(FordBluePilotFeature(self).rows(parked, options=True))
rows.extend(FordClusterFeature(self).rows(parked))
elif page == FeaturePage.TUNING:
title = "Manual steering tune"
capability = self._capability("torque")
rows.extend(self.torque.manual_rows(capability, parked and self.authority("torque") and
self.authority("parked_preferences") and capability is not None))
elif page == FeaturePage.TORQUE:
title = "Steering and Torque"
rows.extend(FordBluePilotFeature(self).rows(parked))
@@ -1319,6 +1324,8 @@ class FeatureSettingsOwner:
row.key.startswith('conditional:') and not row.key.startswith(BUTTON_PREFIX) else fingerprint)
for row in rows]
subtitle = (
"Parked changes apply after the next drive starts. These are explicit manual overrides, separate from Auto Learn."
if page == FeaturePage.TUNING else
"Configure features supported by this vehicle. Restart requirements are shown with each setting."
if page == FeaturePage.VEHICLE else
"Configure automatic Chill and Experimental switching."
@@ -57,6 +57,7 @@ class FeaturePage(StrEnum):
TRAFFIC = "traffic"
CURVE = "curve"
TORQUE = "torque"
TUNING = "tuning"
BLUEPILOT = "torque/bluepilot"
AOL = "aol"
WHEEL = "wheel"
@@ -65,7 +66,8 @@ class FeaturePage(StrEnum):
CONDITIONAL_CCM = "conditional/ccm"
TORQUE_CONFIRM_ACTIONS = frozenset(("torque_adopt", "torque_reset", "torque_reset_profile", "torque_rebase", "torque_prepare_firestar", "torque_gain_rebase"))
TORQUE_CONFIRM_ACTIONS = frozenset(("torque_adopt", "torque_reset", "torque_reset_profile", "torque_rebase",
"torque_prepare_firestar", "torque_gain_rebase", "torque:surface:reset"))
SLC_CONFIRM_ACTIONS = frozenset(("slc_reset",))
SLC_OFFSET_KEYS = tuple(f"slc:offset:{index}" for index in range(1, 8))
LANE_CHANGE_CONFIRM_ACTIONS = frozenset(("lane_change:reset",))
@@ -7,7 +7,7 @@ from pathlib import Path
from tempfile import TemporaryDirectory
import threading
import unittest
from unittest.mock import patch
from unittest.mock import Mock, patch
from openpilot.starpilot.ui.home_stats_source import HomeStatsSource, read_home_stats
@@ -55,7 +55,7 @@ class TestHomeStatsSource(unittest.TestCase):
self.assertIsNone(read_home_stats(False, store=path))
def test_future_read_never_blocks_render_and_changes_units_without_mixing_results(self):
pool = unittest.mock.Mock()
pool = Mock()
first, second = Future(), Future()
pool.submit.side_effect = [first, second]
with patch('openpilot.starpilot.ui.home_stats_source.ThreadPoolExecutor', return_value=pool):
@@ -76,7 +76,7 @@ class TestHomeStatsSource(unittest.TestCase):
def test_failed_read_retries_and_recovers_instead_of_sticking_unavailable(self):
now = [10.]
pool = unittest.mock.Mock()
pool = Mock()
failed, recovered = Future(), Future()
failed.set_exception(OSError('cache temporarily unavailable'))
recovered.set_result('recovered')
+83
View File
@@ -28,6 +28,74 @@ class TorqueFeature:
self._readable = owner._readable
self._dependents = owner._dependents
def _manual_context(self):
from openpilot.starpilot.flm.manual_coordinates import context
cp = self.owner.vehicle_params()
if cp is None or not self._readable(SELECTION_KEY):
raise ValueError("Current controller unavailable")
selection = selection_from_bytes(cp, self._raw(SELECTION_KEY))
if selection.source == "invalid":
raise ValueError("Saved controller selection unavailable")
return context(cp, selection.mode)
def _manual_token(self):
return json.dumps(self._manual_context(), separators=(",", ":")).encode()
def manual_rows(self, capability, allowed):
from openpilot.starpilot.flm.canonical_surface_adapter import NAMES, coordinate_bounds
raw = self._raw(DOCUMENT_KEY)
if capability is None or not self._readable(DOCUMENT_KEY):
return (FeatureRow("", "Manual tune", "Unavailable", reason="Current vehicle and readable saved settings are required"),)
try:
profiles = {} if raw is None else parse_document(raw)
profile = profiles.get(capability[0])
basis = tuple(capability[5:8])
token = self._manual_token()
current = self._manual_context()
saved = None if profile is None else profile.manual_coordinates
matches = profile is None or (profile.basis == basis and profile.flm is None
and (saved is None or saved.context == current))
values = dict(saved.values) if saved is not None else dict.fromkeys(NAMES, 0.)
common = {"source": raw, "capability": capability, "related_source": token,
"dependencies": self._dependents(SELECTION_KEY, *TORQUE_NUMBERS, "ForceAutoTuneOff")}
reason = ("Parked only. Saved for the next drive; zero restores this stage's original baseline. " +
"Manual overrides pause Auto Learn and are not learned results.") if matches else (
"Vehicle tune, controller or effect definition changed. Reset this manual tune before editing.")
rows = []
for name in NAMES:
low, high = coordinate_bounds(name)
rows.append(FeatureRow("torque:surface:" + name, name.replace("_", " ").capitalize(),
f"{100 * values[name]:.4f}", step=.25, minimum=100 * low, maximum=100 * high, unit="%",
available=allowed and matches, reason=reason, default_value="0", **common))
if saved is not None:
rows.append(FeatureRow("torque:surface:reset", "Reset manual tune", "Reset", choices=("Reset",),
available=allowed, reason="Remove only the thirty manual overrides. Accepted Auto Learn history is preserved.", **common))
return tuple(rows)
except (AttributeError, KeyError, TypeError, ValueError, OverflowError, UnicodeError):
return (FeatureRow("", "Manual tune", "Unavailable", reason="Saved tuning or controller context is invalid; repair it in Toggles"),)
def _edit_manual(self, profiles, request, fingerprint, basis):
from openpilot.starpilot.flm.canonical_surface_adapter import NAMES
from openpilot.starpilot.flm.manual_coordinates import ManualCoordinates
if not self.authority("parked_preferences") or request.related_source != self._manual_token():
raise ValueError("Manual tuning requires the same parked vehicle and controller")
prior = profiles.get(fingerprint, PlatformProfile(basis, FieldChoice(), FieldChoice()))
name = request.key.removeprefix("torque:surface:")
if name == "reset":
if not request.confirmation or request.value != "Reset":
raise ValueError("Explicit reset confirmation required")
return {**profiles, fingerprint: replace(prior, manual_coordinates=None)}
current = self._manual_context()
saved = prior.manual_coordinates
if (name not in NAMES or prior.basis != basis or prior.flm is not None
or saved is not None and saved.context != current):
raise ValueError("Manual tune context changed")
values = dict(saved.values) if saved is not None else dict.fromkeys(NAMES, 0.)
values[name] = float(request.value) / 100
manual = ManualCoordinates(current, tuple((n, values[n]) for n in NAMES)).validate()
return {**profiles, fingerprint: replace(prior, manual_coordinates=manual if any(values.values()) else None)}
def _factor_editable(self, fingerprint: str) -> bool:
if fingerprint not in GM_VEHICLES:
return True
@@ -289,6 +357,8 @@ class TorqueFeature:
amended[fingerprint] = updated
resolve_document(amended, fingerprint, basis)
profiles = amended
elif request.key.startswith("torque:surface:"):
profiles = self._edit_manual(profiles, request, fingerprint, basis)
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:"):
@@ -330,6 +400,7 @@ class TorqueFeature:
else:
return False
encoded = serialize_document(profiles)
manual_request = request.key.startswith("torque:surface:")
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.
@@ -337,9 +408,21 @@ class TorqueFeature:
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
manual_request and (not self.authority("parked_preferences") or request.related_source != self._manual_token()) 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
if manual_request:
from openpilot.starpilot.saved_document import commit_exact
from openpilot.starpilot.lateral.torque_settings import MAX_DOCUMENT_BYTES
def authorized():
return (self.authority("torque") and self.authority("parked_preferences") and
self.vehicle_fingerprint() == request.vehicle_fingerprint and self._capability("torque") == capability and
request.related_source == self._manual_token() and
all(self._readable(name) and self._raw(name) == raw for name, raw in request.dependencies))
result = commit_exact(self.params, key=DOCUMENT_KEY, max_bytes=MAX_DOCUMENT_BYTES, raw=encoded,
expected=request.expected, authorized=authorized, temp_prefix=".manual-steering-")
return result.committed and result.verified
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
@@ -29,7 +29,9 @@ def test_corrupt_cached_package_is_rejected(tmp_path):
def test_shipped_offline_payload_matches_the_pinned_bundle():
offline = Path(setup.__file__).with_name('offline')
setup_file = setup.__file__
assert setup_file is not None
offline = Path(setup_file).with_name('offline')
expected = set()
for package in setup.approved()['packages']:
digest = package.get('sha512', package.get('sha256'))
+1 -1
View File
@@ -1 +1 @@
{"protected_inventory_sha256": "803e3f48381fb605404075f5d247a5830c23bd3ccf6abf12ec3aadd420608976", "schema_version": 1}
{"protected_inventory_sha256": "dc0fc34a78b6ecec51ba5a4ecfd601088ed237535b321ef878019980d3b8b465", "schema_version": 1}