Controls Loop Optimizations

This commit is contained in:
firestar5683
2026-10-16 08:13:10 -05:00
parent e982987197
commit 40e0f1f2a9
50 changed files with 532 additions and 29 deletions
+4 -2
View File
@@ -495,9 +495,11 @@ class Controls:
def main():
config_realtime_process(4, Priority.CTRL_HIGH)
controls = Controls()
set_core_affinity([4, 5])
if HARDWARE.get_device_type() in ("tici", "tizi", "mici"):
os.sched_setscheduler(0, os.SCHED_FIFO, os.sched_param(Priority.CTRL_HIGH - 1))
set_core_affinity([6])
os.sched_setscheduler(0, os.SCHED_OTHER, os.sched_param(0))
else:
set_core_affinity([4, 5])
controls.run()
@@ -18,24 +18,28 @@ def test_controls_main_priority_is_applied_after_initialization_and_affinity(dev
mock.patch.object(controlsd, "Controls", side_effect=lambda: calls.append(("construct",)) or controls),
mock.patch.object(controlsd, "set_core_affinity", side_effect=lambda cores: calls.append(("affinity", cores))),
mock.patch.object(controlsd.HARDWARE, "get_device_type", return_value=device),
mock.patch.object(controlsd.os, "SCHED_FIFO", getattr(controlsd.os, "SCHED_FIFO", 1), create=True),
mock.patch.object(controlsd.os, "SCHED_OTHER", getattr(controlsd.os, "SCHED_OTHER", 0), create=True),
mock.patch.object(controlsd.os, "sched_param", getattr(controlsd.os, "sched_param",
lambda priority: SimpleNamespace(sched_priority=priority)), create=True),
mock.patch.object(controlsd.os, "sched_setscheduler", create=True, side_effect=priority_changed)):
controlsd.main()
fifo = controlsd.os.SCHED_FIFO
expected = [("configure", 4, controlsd.Priority.CTRL_HIGH), ("construct",), ("affinity", [4, 5])]
other = controlsd.os.SCHED_OTHER
expected = [("configure", 4, controlsd.Priority.CTRL_HIGH), ("construct",)]
if device in ("tici", "tizi", "mici"):
expected.append(("priority", 0, fifo, 52))
expected.extend([("affinity", [6]), ("priority", 0, other, 0)])
else:
expected.append(("affinity", [4, 5]))
assert calls == [*expected, ("run",)]
def test_constructor_failure_does_not_reprioritize_existing_threads():
with (mock.patch.object(controlsd, "config_realtime_process"),
mock.patch.object(controlsd, "Controls", side_effect=RuntimeError("initialization failed")),
mock.patch.object(controlsd, "set_core_affinity") as affinity,
mock.patch.object(controlsd.os, "sched_setscheduler", create=True) as priority,
pytest.raises(RuntimeError, match="initialization failed")):
controlsd.main()
affinity.assert_not_called()
priority.assert_not_called()
@@ -43,12 +47,28 @@ def test_scheduler_failure_does_not_start_controls():
controls = mock.Mock()
with (mock.patch.object(controlsd, "config_realtime_process"),
mock.patch.object(controlsd, "Controls", return_value=controls),
mock.patch.object(controlsd, "set_core_affinity"),
mock.patch.object(controlsd, "set_core_affinity") as affinity,
mock.patch.object(controlsd.HARDWARE, "get_device_type", return_value="mici"),
mock.patch.object(controlsd.os, "SCHED_FIFO", getattr(controlsd.os, "SCHED_FIFO", 1), create=True),
mock.patch.object(controlsd.os, "SCHED_OTHER", getattr(controlsd.os, "SCHED_OTHER", 0), create=True),
mock.patch.object(controlsd.os, "sched_param", getattr(controlsd.os, "sched_param",
lambda priority: SimpleNamespace(sched_priority=priority)), create=True),
mock.patch.object(controlsd.os, "sched_setscheduler", create=True, side_effect=PermissionError("priority denied")),
pytest.raises(PermissionError, match="priority denied")):
controlsd.main()
affinity.assert_called_once_with([6])
controls.run.assert_not_called()
@pytest.mark.parametrize("device", ("mici", "tizi", "tici", "pc", "unknown"))
def test_affinity_failure_does_not_reprioritize_or_start_controls(device):
controls = mock.Mock()
with (mock.patch.object(controlsd, "config_realtime_process"),
mock.patch.object(controlsd, "Controls", return_value=controls),
mock.patch.object(controlsd.HARDWARE, "get_device_type", return_value=device),
mock.patch.object(controlsd, "set_core_affinity", side_effect=PermissionError("affinity denied")) as affinity,
mock.patch.object(controlsd.os, "sched_setscheduler", create=True) as priority,
pytest.raises(PermissionError, match="affinity denied")):
controlsd.main()
affinity.assert_called_once_with([6] if device in ("tici", "tizi", "mici") else [4, 5])
priority.assert_not_called()
controls.run.assert_not_called()
+1 -1
View File
@@ -1029,7 +1029,7 @@ class SelfdriveD:
drive = int(self.sm['deviceState'].startedMonoTime) if self.sm['deviceState'].started else 0
switchback = bool(self.switchback_capable and self.sm.seen['slcState'] and self.sm.alive['slcState'] and
self.sm.valid['slcState'] and self.sm['carControl'].latActive and
self.switchback_status.sample(self.sm['slcState'], drive_id=drive, now_ns=now_ns,
self.switchback_status.sample_state(self.sm['slcState'], drive_id=drive, now_ns=now_ns,
event_ns=int(self.sm.logMonoTime['slcState'])))
if now_ns - self.switchback_setting_ns >= 1_000_000_000:
self.switchback_setting_ns = now_ns
@@ -166,12 +166,25 @@ def switchback_observation(event, *, drive_id: int, now_ns: int, event_ns: int |
return False
@dataclass(frozen=True, slots=True)
class _SwitchbackStateEnvelope:
# Explicit payload entry avoids probing nonexistent fields on a Capnp reader.
slcState: object
logMonoTime: int
valid: bool
class SwitchbackStatusOwner:
def __init__(self):
self.drive = 0
self.last = None
self.retired = ()
def sample_state(self, state, *, drive_id: int, now_ns: int, event_ns: int, valid: bool = True) -> bool:
"""Explicit SlcState payload + original envelope metadata; no retained cache."""
return self.sample(_SwitchbackStateEnvelope(state, event_ns, valid),
drive_id=drive_id, now_ns=now_ns, event_ns=event_ns)
def sample(self, event, *, drive_id: int, now_ns: int, event_ns: int | None = None) -> bool:
if drive_id != self.drive or drive_id <= 0:
self.drive, self.last, self.retired = drive_id, None, ()
@@ -177,6 +177,49 @@ def test_leased_switchback_status_rejects_expiry_reorder_and_retired_session():
assert not status.sample(first, drive_id=DRIVE, now_ns=NOW + 1)
def test_switchback_payload_reader_matches_generic_envelope_metadata():
from openpilot.cereal import log
from openpilot.starpilot.controllers.mode_actions import ModeIntent, publish_switchback, SwitchbackStatusOwner
# Actual SubMaster payload shape; compare to the existing public envelope API.
for valid, age, stamp_delta, drive, expected in (
(True, 0, 0, DRIVE, True), (False, 0, 0, DRIVE, False),
(True, 100_000_000, 0, DRIVE, True), (True, 100_000_001, 0, DRIVE, False),
(True, 0, -1, DRIVE, False), (True, 0, 0, DRIVE + 1, False)):
message = publish_switchback(None, ModeIntent(DRIVE, True, True), session='a' * 32,
sequence=2, now_ns=NOW, source_car_control_ns=NOW - 1)
message.valid, message.logMonoTime = valid, NOW + stamp_delta
with log.Event.from_bytes(message.to_bytes()) as event:
generic, payload = SwitchbackStatusOwner(), SwitchbackStatusOwner()
assert generic.sample(event, drive_id=drive, now_ns=NOW + age) is expected
assert payload.sample_state(event.slcState, drive_id=drive, now_ns=NOW + age,
event_ns=event.logMonoTime, valid=event.valid) is expected
assert (payload.drive, payload.last, payload.retired) == (generic.drive, generic.last, generic.retired)
if valid:
bare = SwitchbackStatusOwner()
assert bare.sample(event.slcState, drive_id=drive, now_ns=NOW + age, event_ns=event.logMonoTime) is expected
assert (bare.drive, bare.last, bare.retired) == (payload.drive, payload.last, payload.retired)
def test_switchback_payload_reader_preserves_duplicates_retirement_and_drive_reset():
from openpilot.cereal import log
from openpilot.starpilot.controllers.mode_actions import ModeIntent, publish_switchback, SwitchbackStatusOwner
generic, payload = SwitchbackStatusOwner(), SwitchbackStatusOwner()
for session, sequence, observed, drive, expected in (
('a', 2, NOW, DRIVE, True), ('a', 2, NOW, DRIVE, True),
('a', 1, NOW, DRIVE, False), ('b', 1, NOW + 1, DRIVE, False),
('b', 1, NOW + 1, DRIVE, True), ('a', 2, NOW, DRIVE, False),
('a', 2, NOW, 0, False), ('a', 2, NOW, DRIVE, True)):
message = publish_switchback(None, ModeIntent(DRIVE, True, True), session=session * 32,
sequence=sequence, now_ns=observed, source_car_control_ns=observed - 1)
with log.Event.from_bytes(message.to_bytes()) as event:
assert generic.sample(event, drive_id=drive, now_ns=NOW + 2) is expected
assert payload.sample_state(event.slcState, drive_id=drive, now_ns=NOW + 2,
event_ns=event.logMonoTime) is expected
assert (payload.drive, payload.last, payload.retired) == (generic.drive, generic.last, generic.retired)
def test_controller_dispatch_requires_live_producer_and_binds_current_control_stamp():
from unittest.mock import Mock
from openpilot.starpilot.controllers.mode_actions import ModeActionPublisher, ModeIntent, publish_switchback
+10 -7
View File
@@ -42,17 +42,20 @@ def _row_action(row, apply, section):
token, (lambda: apply(request)) if allowed else None, section)
def mapped_actions(feature_snapshot, feature_apply, appearance_snapshot, appearance_apply):
def mapped_actions(feature_snapshot, feature_apply, appearance_snapshot, appearance_apply, *, requested_keys=None):
actions = {}
for page, keys in FEATURE_KEYS.items():
if requested_keys is not None and not requested_keys.intersection(keys):
continue
state = feature_snapshot(page)
for row in state.rows:
if row.key in keys:
if row.key in keys and (requested_keys is None or row.key in requested_keys):
actions[row.key] = _row_action(row, feature_apply, state.title)
appearance = appearance_snapshot()
for row in appearance.rows:
if row.key in APPEARANCE_KEYS:
actions[row.key] = _row_action(row, appearance_apply, "Visual")
if requested_keys is None or requested_keys.intersection(APPEARANCE_KEYS):
appearance = appearance_snapshot()
for row in appearance.rows:
if row.key in APPEARANCE_KEYS and (requested_keys is None or row.key in requested_keys):
actions[row.key] = _row_action(row, appearance_apply, "Visual")
actions[BOOKMARK] = FavoriteAction(BOOKMARK, "Bookmark", reason="Use this control on the device")
actions[EXPERIMENTAL] = FavoriteAction(EXPERIMENTAL, "Experimental Mode", kind="toggle", state_label="On device",
reason="Use this control on the device")
@@ -64,4 +67,4 @@ def mapped_actions(feature_snapshot, feature_apply, appearance_snapshot, appeara
(TRAFFIC, "Traffic Mode"), (SWITCHBACK, "Switchback Mode"), (SCREEN_OFF, "Toggle Screen Off"), (SELFIE, "Selfie")):
actions[key] = FavoriteAction(key, label, kind="toggle" if key in (TRAFFIC, SWITCHBACK, SCREEN_OFF) else "action",
reason="Use the qualified control on the device", section="Driving")
return actions
return actions if requested_keys is None else {key: action for key, action in actions.items() if key in requested_keys}
+10 -2
View File
@@ -75,13 +75,21 @@ def read_slots(params):
class FavoritesOwner:
def __init__(self, params, actions: Callable[[], Mapping[str, FavoriteAction]], configurable: Callable[[], bool]):
def __init__(self, params, actions: Callable[[], Mapping[str, FavoriteAction]], configurable: Callable[[], bool],
render_actions: Callable[[frozenset[str]], Mapping[str, FavoriteAction]] | None = None):
self.params, self.actions, self.configurable = params, actions, configurable
self.render_actions = render_actions
def snapshot(self, *, include_options=True):
saved, raw, readable, valid = read_slots(self.params)
# Empty render slots need no action metadata until the picker is opened.
options = self.actions() if include_options or any(slot["key"] is not None for slot in saved) else {}
keys = frozenset(slot["key"] for slot in saved if slot["key"] is not None)
if include_options:
options = self.actions()
elif keys:
options = self.render_actions(keys) if self.render_actions is not None else self.actions()
else:
options = {}
token = revision(raw)
slots = []
for index, slot in enumerate(saved):
@@ -0,0 +1,97 @@
"""Native pure-projection cache ordering/concurrency, complementary to prior parity proof."""
from collections import OrderedDict
from concurrent.futures import ThreadPoolExecutor
import threading
import unittest
from unittest.mock import patch
from openpilot.cereal import log
from openpilot.starpilot.longitudinal import toyota_output_policy as policy
class TestRadarLeadProjectionCache(unittest.TestCase):
def setUp(self):
with policy._RADAR_LEAD_LOCK:
policy._RADAR_LEAD_ROWS.clear()
self.kwargs = {'message_ns': 1_000_000_000, 'receipt_ns': 1_001_000_000,
'now_ns': 1_002_000_000, 'drive_id': 900_000_000, 'valid': True}
def tearDown(self):
with policy._RADAR_LEAD_LOCK:
policy._RADAR_LEAD_ROWS.clear()
@staticmethod
def radar(stamp, distance=10.):
radar = log.RadarState.new_message()
radar.mdMonoTime = stamp
radar.leadOne.present = True
radar.leadOne.dRel = distance
return radar
@staticmethod
def key(radar):
return radar.as_reader().as_builder().to_bytes()
def test_exact_native_bytes_and_lru_recency(self):
# All rows are equal; an unrelated native field still changes the exact key.
radars = [self.radar(stamp) for stamp in range(9)]
keys = [self.key(radar) for radar in radars]
self.assertEqual(len(set(keys)), 9)
for radar in radars[:8]:
self.assertIsNotNone(policy.leads_from_radar(radar, **self.kwargs))
policy.leads_from_radar(radars[0], **self.kwargs)
policy.leads_from_radar(radars[8], **self.kwargs)
with policy._RADAR_LEAD_LOCK:
self.assertEqual(list(policy._RADAR_LEAD_ROWS), keys[2:8] + [keys[0], keys[8]])
self.assertNotIn(keys[1], policy._RADAR_LEAD_ROWS)
self.assertEqual(len(set(policy._RADAR_LEAD_ROWS.values())), 1)
def test_concurrent_native_projection_retains_locked_bounded_rows(self):
lock = policy._RADAR_LEAD_LOCK
class GuardedRows(OrderedDict):
# Assert every compound-operation primitive is inside the shared mutex.
# The worker exercise also verifies concurrent output and retention bounds.
def get(self, key, default=None):
assert lock.locked()
return super().get(key, default)
def move_to_end(self, key, last=True):
assert lock.locked()
return super().move_to_end(key, last)
def __setitem__(self, key, value):
assert lock.locked()
super().__setitem__(key, value)
def popitem(self, last=True):
assert lock.locked()
return super().popitem(last)
rows = GuardedRows()
start = threading.Barrier(6)
def project(worker):
# Each thread owns its native builder; mutable builders are never shared.
radar = self.radar(worker, worker + 1.)
start.wait(timeout=10)
for sequence in range(16):
radar.mdMonoTime = worker * 100 + sequence
for _ in range(5):
leads = policy.leads_from_radar(radar, **self.kwargs)
self.assertIsNotNone(leads)
self.assertEqual(leads[0], policy.Lead(True, worker + 1., 0., 0., 0.))
return True
with patch.object(policy, '_RADAR_LEAD_ROWS', rows):
with ThreadPoolExecutor(max_workers=6) as workers:
futures = [workers.submit(project, worker) for worker in range(6)]
self.assertTrue(all(future.result(timeout=30) for future in futures))
with lock:
self.assertEqual(len(rows), 8)
self.assertTrue(all(type(key) is bytes and len(key) <= 4096 for key in rows))
self.assertTrue(all(type(value) is tuple and all(type(row) is tuple for row in value) for value in rows.values()))
if __name__ == '__main__':
unittest.main()
@@ -5,14 +5,19 @@ validated lead data for Sienna and reset it whenever longitudinal control or
source authority is lost. It does not select a planner profile or grant control.
"""
from collections import OrderedDict
from dataclasses import dataclass
import capnp
import math
import os
import time
import threading
import numpy as np
from opendbc.car import structs
from openpilot.cereal import log
from opendbc.car.toyota.values import CAR, EPS_SCALE, ToyotaFlags, ToyotaSafetyFlags
from openpilot.common.realtime import DT_CTRL
@@ -54,6 +59,18 @@ def leads_from_radar(radar, *, message_ns: int, receipt_ns: int, now_ns: int,
not 0 < drive_id < message_ns <= receipt_ns <= now_ns or
now_ns - message_ns > RADAR_MAX_AGE_NS or now_ns - receipt_ns > RADAR_MAX_AGE_NS):
return None
rows = _radar_lead_rows(radar)
return tuple(Lead(*row) for row in rows) if rows is not None else None
# Only immutable pure projection values are retained, never transport authority.
_RADAR_LEAD_ROWS = OrderedDict()
_RADAR_LEAD_LOCK = threading.Lock()
_RADAR_LEAD_CACHE_MAX = 8
_RADAR_LEAD_KEY_MAX_BYTES = 4096
def _decode_radar_lead_rows(radar):
try:
if any(type(item.present) is not bool for item in (radar.leadOne, radar.leadTwo)):
return None
@@ -63,7 +80,36 @@ def leads_from_radar(radar, *, message_ns: int, receipt_ns: int, now_ns: int,
return None
if len(leads) != 2 or not ToyotaOutputPolicy._leads_valid(leads):
return None
return leads[0], leads[1]
return tuple((lead.present, lead.distance_m, lead.lateral_m, lead.speed_mps, lead.accel_mps2) for lead in leads)
def _radar_lead_rows(radar):
# Custom fixtures retain their original decoding behavior. A builder is copied
# on every call so mutation changes the key and cannot poison prior rows.
if type(radar) not in (capnp._DynamicStructReader, capnp._DynamicStructBuilder):
return _decode_radar_lead_rows(radar)
try:
if radar.schema != log.RadarState.schema:
return _decode_radar_lead_rows(radar)
snapshot = radar.as_builder() if type(radar) is capnp._DynamicStructReader else radar.as_reader().as_builder()
key = snapshot.to_bytes()
except (AttributeError, TypeError, ValueError, OverflowError, capnp.KjException):
return _decode_radar_lead_rows(radar)
if len(key) > _RADAR_LEAD_KEY_MAX_BYTES:
return _decode_radar_lead_rows(snapshot)
with _RADAR_LEAD_LOCK:
rows = _RADAR_LEAD_ROWS.get(key)
if rows is not None:
_RADAR_LEAD_ROWS.move_to_end(key)
return rows
rows = _decode_radar_lead_rows(snapshot)
if rows is not None:
with _RADAR_LEAD_LOCK:
_RADAR_LEAD_ROWS[key] = rows
_RADAR_LEAD_ROWS.move_to_end(key)
if len(_RADAR_LEAD_ROWS) > _RADAR_LEAD_CACHE_MAX:
_RADAR_LEAD_ROWS.popitem(last=False)
return rows
def eligible(cp) -> bool:
+11 -4
View File
@@ -270,7 +270,7 @@ class StarShellSession:
self._wheel_personality = None
self.conditional_actions: ConditionalUiActionOwner | None = None
self.cruise_actions = CruiseActionPublisher()
self.favorites_owner = FavoritesOwner(ui_state.params, self._favorite_actions, self._favorite_authority)
self.favorites_owner = FavoritesOwner(ui_state.params, self._favorite_actions, self._favorite_authority, self._favorite_render_actions)
self.favorites = OnroadFavorites(self.fonts, self._favorite_request)
self._favorite_snapshot = None
self._favorite_data = None
@@ -317,13 +317,19 @@ class StarShellSession:
longitudinal_active=state.cruise_active, traffic_mode=state.traffic_mode)
def _favorite_actions(self):
return self._resolve_favorite_actions()
def _favorite_render_actions(self, keys):
return self._resolve_favorite_actions(keys)
def _resolve_favorite_actions(self, keys=None):
actions = mapped_actions(lambda page: self.feature_snapshot(page, favorite=True), self.feature_request,
lambda: self.appearance_owner.snapshot(self.profile), self.appearance_owner.apply)
lambda: self.appearance_owner.snapshot(self.profile), self.appearance_owner.apply, requested_keys=keys)
actions.update(layout_actions(ui_state.params, str(self.profile),
lambda: self._settings_preference_authority() or self._favorite_authority(),
lambda: self.adapter.invalidate_appearance()))
actions.update(self._native_favorite_actions())
return actions
return actions if keys is None else {key: action for key, action in actions.items() if key in keys}
def native_favorite_actions(self, bookmark, personality, experimental):
state = self.snapshot(ShellMode.ONROAD).onroad
@@ -1216,7 +1222,8 @@ class StarShellSession:
return self._snapshot_cache[1]
snapshot = self.adapter.build(mode, self.selected, compact_y=self.compact_y,
compact_scroll_x=self.compact_scroll_x, sidebar_expanded=self.sidebar_expanded,
menu_only=self.profile == Profile.COMPACT and mode == ShellMode.SETTINGS)
menu_only=self.profile == Profile.COMPACT and mode == ShellMode.SETTINGS,
onroad_view_only=mode == ShellMode.ONROAD)
if mode == ShellMode.ONROAD and self.profile == Profile.LARGE:
width = getattr(self, '_onroad_page_width', 2160.0) - (SIDEBAR_WIDTH if snapshot.onroad.appearance.show_developer_sidebar else 0)
if snapshot.onroad.viewport_width != width:
+16 -3
View File
@@ -299,6 +299,7 @@ class RuntimeSnapshotAdapter:
self.appearance_override: Callable[[OnroadAppearance], OnroadAppearance] | None = None
self._last_mode: ShellMode | None = None
self._onroad_ancillary: tuple[int, tuple[bool, int, bool], ShellSnapshot] | None = None
self._onroad_view_ancillary: tuple[int, tuple[bool, int, bool], ShellSnapshot] | None = None
self._conditional = ConditionalDisplayProjector()
self._conditional_perception = ConditionalPerceptionProjector()
self._conditional_choice: ModeChoice | None = None
@@ -371,7 +372,8 @@ class RuntimeSnapshotAdapter:
def build(self, mode: ShellMode, selected: Destination = Destination.STAR, *, compact_y: float = 0,
compact_scroll_x: float = 0, sidebar_expanded: bool = True, now_ns: int | None = None,
menu_only: bool = False, stopped_timer_enabled: bool | None = None) -> ShellSnapshot:
menu_only: bool = False, stopped_timer_enabled: bool | None = None,
onroad_view_only: bool = False) -> ShellSnapshot:
replay = getattr(self.ui_state, "replay_sample", None)
if getattr(self.ui_state, "replay_clock", None) is not None:
if replay is not None and replay.epoch != self._replay_epoch:
@@ -383,6 +385,7 @@ class RuntimeSnapshotAdapter:
now_ns = time.monotonic_ns() if now_ns is None else now_ns
if mode != ShellMode.ONROAD or self._last_mode != ShellMode.ONROAD:
self._onroad_ancillary = None
self._onroad_view_ancillary = None
if mode == ShellMode.ONROAD and self._last_mode == ShellMode.SETTINGS:
self.invalidate_appearance()
self._conditional_read_ns = None
@@ -653,8 +656,9 @@ class RuntimeSnapshotAdapter:
lead_indicator_source_fresh=display_model is not None and display_radar is not None)
ancillary_key = (bool(ui.started), int(ui.started_frame), confirmed_offroad)
if mode == ShellMode.ONROAD and self._onroad_ancillary is not None:
stamp, key, previous = self._onroad_ancillary
ancillary = self._onroad_view_ancillary if onroad_view_only else self._onroad_ancillary
if mode == ShellMode.ONROAD and ancillary is not None:
stamp, key, previous = ancillary
if key == ancillary_key and 0 <= now_ns - stamp < 1_000_000_000:
return replace(previous, onroad=onroad)
@@ -708,6 +712,15 @@ class RuntimeSnapshotAdapter:
if mode == ShellMode.HOME or menu_only and mode == ShellMode.SETTINGS:
return ShellSnapshot(mode, home, settings, onroad, DeviceState(offroad=confirmed_offroad), selected=selected)
# The shell's onroad view/input consume onroad state, not settings leaf panels.
# Keep current home/settings presentation and offroad evidence without querying
# Galaxy credentials or constructing unconsumed device/software/toggle panels.
if mode == ShellMode.ONROAD and onroad_view_only:
snapshot = ShellSnapshot(mode, home, settings, onroad,
DeviceState(offroad=confirmed_offroad, available_actions=frozenset()), selected=selected)
self._onroad_view_ancillary = (now_ns, ancillary_key, snapshot)
return snapshot
device_actions = {DeviceRequest.PREVIEW_DRIVER_CAMERA, DeviceRequest.RESET_CALIBRATION} if confirmed_offroad else set()
galaxy_status = self.galaxy_access.status().status if self.galaxy_access is not None else AccessStatus.UNAVAILABLE
if self.galaxy_access is not None:
@@ -0,0 +1,141 @@
import json
from types import SimpleNamespace
from unittest.mock import Mock
import pytest
from openpilot.starpilot.favorites.actions import APPEARANCE_KEYS, BOOKMARK, FEATURE_KEYS, mapped_actions
from openpilot.starpilot.favorites.owner import FAVORITE_SLOTS_PARAM, FavoritesOwner, default_slots
from openpilot.starpilot.favorites.state import FavoriteAction
from openpilot.starpilot.ui.feature_settings_state import FeatureRow
@pytest.fixture
def selected_owner(tmp_path):
path = tmp_path / FAVORITE_SLOTS_PARAM
params = SimpleNamespace(get_param_path=lambda key: str(path))
invoke = Mock(return_value=True)
action = FavoriteAction(BOOKMARK, "Bookmark", available=True, token="fresh", invoke=invoke)
catalog = Mock(return_value={BOOKMARK: action})
selected = Mock(side_effect=lambda keys: {key: action for key, action in catalog.return_value.items() if key in keys})
owner = FavoritesOwner(params, catalog, lambda: True, selected)
return owner, catalog, selected, path, invoke
@pytest.mark.parametrize("keys", [(BOOKMARK,), (BOOKMARK, BOOKMARK), ("UnknownSavedControl",),
(BOOKMARK, "UnknownSavedControl")])
@pytest.mark.parametrize("enabled", [False, True])
def test_selected_render_preserves_saved_slots_and_current_metadata(selected_owner, keys, enabled):
owner, catalog, selected, path, invoke = selected_owner
slots = default_slots()
for index, key in enumerate(keys):
slots[index].update(key=key, enabled=enabled, show_onroad=enabled, label="Saved label")
raw = json.dumps(slots).encode()
path.write_bytes(raw)
full = owner.snapshot()
rendered = owner.snapshot(include_options=False)
assert rendered.slots == full.slots
assert rendered.revision == full.revision
assert (rendered.valid, rendered.configurable) == (full.valid, full.configurable)
assert {action.key for action in rendered.options} == {action.key for action in full.options}.intersection(keys)
selected.assert_called_once_with(frozenset(keys))
assert catalog.call_count == 1
assert path.read_bytes() == raw
invoke.assert_not_called()
@pytest.mark.parametrize("raw", [None, b"[]", b"bad-json", b'{"slots":[]}'])
def test_empty_or_invalid_render_does_not_resolve_actions(selected_owner, raw):
owner, catalog, selected, path, invoke = selected_owner
if raw is not None:
path.write_bytes(raw)
rendered = owner.snapshot(include_options=False)
assert rendered.options == ()
catalog.assert_not_called()
selected.assert_not_called()
invoke.assert_not_called()
assert not path.exists() if raw is None else path.read_bytes() == raw
def test_unreadable_render_keeps_refusal(selected_owner):
owner, catalog, selected, path, invoke = selected_owner
path.mkdir()
rendered = owner.snapshot(include_options=False)
assert not rendered.valid and not rendered.configurable
catalog.assert_not_called()
selected.assert_not_called()
invoke.assert_not_called()
assert path.is_dir()
def test_default_and_picker_snapshots_keep_full_catalog(selected_owner):
owner, catalog, selected, _, _ = selected_owner
assert owner.snapshot() == owner.snapshot(include_options=True)
assert catalog.call_count == 2
selected.assert_not_called()
def test_owner_without_selected_resolver_retains_full_render_options(selected_owner):
owner, catalog, _, path, _ = selected_owner
owner.render_actions = None
slots = default_slots()
slots[0].update(key=BOOKMARK)
path.write_text(json.dumps(slots))
assert owner.snapshot(include_options=False) == owner.snapshot()
assert catalog.call_count == 2
def test_selected_render_request_rechecks_full_live_authority_token_and_revision(selected_owner):
owner, catalog, selected, path, invoke = selected_owner
slots = default_slots()
slots[0].update(key=BOOKMARK, enabled=True, show_onroad=True)
path.write_text(json.dumps(slots))
request = owner.snapshot(include_options=False).slots[0].request
assert request is not None
catalog.return_value = {BOOKMARK: FavoriteAction(BOOKMARK, "Bookmark", available=False, token="fresh", invoke=invoke)}
assert not owner.invoke(request).success
catalog.return_value = {BOOKMARK: FavoriteAction(BOOKMARK, "Bookmark", available=True, token="changed", invoke=invoke)}
assert not owner.invoke(request).success
catalog.return_value = {BOOKMARK: FavoriteAction(BOOKMARK, "Bookmark", available=True, token="fresh", invoke=invoke)}
path.write_bytes(path.read_bytes() + b"\n")
assert not owner.invoke(request).success
assert selected.call_count == 1
assert catalog.call_count == 2
invoke.assert_not_called()
def test_selected_resolver_errors_are_not_hidden(selected_owner):
owner, catalog, selected, path, _ = selected_owner
slots = default_slots()
slots[0].update(key=BOOKMARK)
path.write_text(json.dumps(slots))
selected.side_effect = OSError("saved source unavailable")
with pytest.raises(OSError, match="saved source unavailable"):
owner.snapshot(include_options=False)
catalog.assert_not_called()
@pytest.mark.parametrize("keys", [frozenset([BOOKMARK]), frozenset(["LaneCentering"]),
frozenset(["CameraView"]), frozenset(["UnknownSavedControl"]),
frozenset(["LaneCentering", "CameraView"])])
def test_mapped_selection_preserves_current_actions_and_skips_unrelated_pages(keys):
def row(key):
return FeatureRow(key, key, "Off", choices=("Off", "On"), available=True)
pages = {page: SimpleNamespace(title=page, rows=tuple(row(key) for key in names))
for page, names in FEATURE_KEYS.items()}
feature = Mock(side_effect=lambda page: pages[page])
appearance = Mock(return_value=SimpleNamespace(rows=tuple(row(key) for key in APPEARANCE_KEYS)))
apply = Mock()
full = mapped_actions(feature, apply, appearance, apply)
feature.reset_mock()
appearance.reset_mock()
rendered = mapped_actions(feature, apply, appearance, apply, requested_keys=keys)
assert set(rendered) == set(full).intersection(keys)
for key, action in rendered.items():
expected = full[key]
assert (action.label, action.available, action.token, action.reason, action.state_label) == (
expected.label, expected.available, expected.token, expected.reason, expected.state_label)
assert (action.invoke is None) == (expected.invoke is None)
assert feature.call_count == sum(bool(keys.intersection(names)) for names in FEATURE_KEYS.values())
assert appearance.call_count == int(bool(keys.intersection(APPEARANCE_KEYS)))
apply.assert_not_called()
@@ -0,0 +1,110 @@
from types import SimpleNamespace
import unittest
from unittest.mock import Mock
from openpilot.starpilot.ui.settings_state import Destination
from openpilot.starpilot.ui.shell import ShellMode
from openpilot.starpilot.ui.tests.test_runtime_snapshot import NOW, RuntimeSnapshotAdapter, ui_fake
from openpilot.tools.replay.display_clock import ClockSample
class TestOnroadLeafScope(unittest.TestCase):
def test_opt_in_preserves_consumed_state_and_skips_leaf_queries(self):
full_ui, view_ui = ui_fake(), ui_fake()
full = RuntimeSnapshotAdapter(full_ui)
view = RuntimeSnapshotAdapter(view_ui, galaxy_access=SimpleNamespace(status=Mock(side_effect=AssertionError("unused Galaxy query"))))
forbidden = {"DongleId", "HardwareSerial", "UpdaterState", "UpdaterFetchAvailable", "DisableUpdates",
"UpdaterTargetBranch", "OpenpilotEnabledToggle", "DisengageOnAccelerator", "IsLdwEnabled", "AlwaysOnDM", "RecordFront", "RecordAudio"}
get, get_bool = view_ui.params.get, view_ui.params.get_bool
def checked_get(key):
self.assertNotIn(key, forbidden)
return get(key)
def checked_bool(key):
self.assertNotIn(key, forbidden)
return get_bool(key)
view_ui.params.get, view_ui.params.get_bool = checked_get, checked_bool
for frame in range(5):
now = NOW + frame * 600_000_000
for ui in (full_ui, view_ui):
ui.sm.frame += 1
ui.params.values["ShowCSCStatus"] = str(frame % 2)
expected = full.build(ShellMode.ONROAD, now_ns=now)
actual = view.build(ShellMode.ONROAD, now_ns=now, onroad_view_only=True)
self.assertEqual(actual.onroad, expected.onroad)
self.assertEqual(actual.home, expected.home)
self.assertEqual(actual.settings, expected.settings)
self.assertEqual(actual.device.offroad, expected.device.offroad)
self.assertEqual(actual.selected, expected.selected)
view.galaxy_access.status.assert_not_called()
def test_narrow_cache_cannot_replace_default_full_snapshot(self):
ui = ui_fake()
ui.params.values["DongleId"] = "actual-device"
adapter = RuntimeSnapshotAdapter(ui)
narrow = adapter.build(ShellMode.ONROAD, now_ns=NOW, onroad_view_only=True)
full = adapter.build(ShellMode.ONROAD, now_ns=NOW)
self.assertNotEqual(narrow.device.dongle_id, full.device.dongle_id)
self.assertEqual(full.device.dongle_id, "actual-device")
self.assertEqual(narrow.onroad, full.onroad)
live = adapter.build(ShellMode.ONROAD, now_ns=NOW + 1, onroad_view_only=True)
self.assertEqual(live.onroad.observed_ns, NOW + 1)
self.assertEqual(narrow.onroad.observed_ns, NOW)
self.assertIsNot(live.onroad, narrow.onroad)
self.assertFalse(narrow.device.available_actions)
self.assertIsNotNone(adapter._onroad_ancillary)
self.assertIsNotNone(adapter._onroad_view_ancillary)
def test_settings_and_home_rebuild_immediately_after_onroad(self):
ui = ui_fake()
adapter = RuntimeSnapshotAdapter(ui)
adapter.build(ShellMode.ONROAD, now_ns=NOW, onroad_view_only=True)
ui.params.values.update(DongleId="new-device", UpdaterState="downloading", GitCommit="new-commit")
settings = adapter.build(ShellMode.SETTINGS, Destination.DEVICE, now_ns=NOW + 1, onroad_view_only=True)
self.assertEqual(settings.device.dongle_id, "new-device")
self.assertEqual(settings.software.download_status, "downloading")
self.assertIsNone(adapter._onroad_view_ancillary)
home = adapter.build(ShellMode.HOME, now_ns=NOW + 2, onroad_view_only=True)
self.assertEqual(home.home.commit, "new-commit")
adapter.build(ShellMode.ONROAD, now_ns=NOW + 3, onroad_view_only=True)
self.assertIsNotNone(adapter._onroad_view_ancillary)
def test_onroad_key_change_recomputes_fresh_parked_evidence(self):
ui = ui_fake()
adapter = RuntimeSnapshotAdapter(ui)
first = adapter.build(ShellMode.ONROAD, now_ns=NOW, onroad_view_only=True)
cached = adapter._onroad_view_ancillary
ui.started_frame += 1
ui.sm.frame += 1
second = adapter.build(ShellMode.ONROAD, now_ns=NOW + 1, onroad_view_only=True)
self.assertIsNot(adapter._onroad_view_ancillary, cached)
self.assertFalse(first.device.offroad)
self.assertFalse(second.device.offroad)
self.assertFalse(second.device.available_actions)
def test_replay_epoch_reinitializes_narrow_cache_and_temporal_projectors(self):
ui = ui_fake()
ui.replay_clock = object()
ui.replay_sample = ClockSample(NOW, None, NOW * 1000, epoch=1, valid=True)
ui.sm.replay_sample = ui.replay_sample
adapter = RuntimeSnapshotAdapter(ui)
first = adapter.build(ShellMode.ONROAD, onroad_view_only=True)
cache, projector = adapter._onroad_view_ancillary, adapter._conditional
ui.replay_sample = ClockSample(NOW - 1_000_000_000, None, NOW * 1000, epoch=2, valid=True)
ui.sm.replay_sample = ui.replay_sample
second = adapter.build(ShellMode.ONROAD, onroad_view_only=True)
self.assertIsNot(adapter._onroad_view_ancillary, cache)
self.assertIsNot(adapter._conditional, projector)
self.assertFalse(first.device.offroad)
self.assertFalse(second.device.offroad)
self.assertFalse(second.onroad.engaged)
def test_full_onroad_cache_does_not_suppress_narrow_refresh_or_settings(self):
ui = ui_fake()
adapter = RuntimeSnapshotAdapter(ui)
adapter.build(ShellMode.ONROAD, now_ns=NOW)
adapter.build(ShellMode.ONROAD, now_ns=NOW + 1, onroad_view_only=True)
ui.params.values["UpdaterState"] = "downloading"
full = adapter.build(ShellMode.SETTINGS, Destination.SOFTWARE, now_ns=NOW + 2)
self.assertEqual(full.software.download_status, "downloading")
self.assertIsNone(adapter._onroad_view_ancillary)
self.assertIsNone(adapter._onroad_ancillary)
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -1,4 +1,4 @@
#pragma once
extern const uint8_t gitversion[29];
const uint8_t gitversion[29] = "DEV-39689cd6-P92eb5651-DEBUG";
const uint8_t gitversion[29] = "DEV-e9829871-P92eb5651-DEBUG";
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
DEV-39689cd6-P92eb5651-DEBUG
DEV-e9829871-P92eb5651-DEBUG
+1 -1
View File
@@ -1 +1 @@
{"protected_inventory_sha256": "d2d6d6688177e81187cdf55c32b9fe2bbf198f72c77e66c8cc1d5561ee5dbd4d", "schema_version": 1}
{"protected_inventory_sha256": "57bf4bf680d75776c53c5e64a459500d3743fd8c850d14e89bd9ab2f432d5f0c", "schema_version": 1}