diff --git a/openpilot/selfdrive/controls/controlsd.py b/openpilot/selfdrive/controls/controlsd.py index ddaae98411..4d532d46e7 100755 --- a/openpilot/selfdrive/controls/controlsd.py +++ b/openpilot/selfdrive/controls/controlsd.py @@ -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() diff --git a/openpilot/selfdrive/controls/tests/test_controlsd_scheduling.py b/openpilot/selfdrive/controls/tests/test_controlsd_scheduling.py index 143e51c439..88c53a398b 100644 --- a/openpilot/selfdrive/controls/tests/test_controlsd_scheduling.py +++ b/openpilot/selfdrive/controls/tests/test_controlsd_scheduling.py @@ -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() diff --git a/openpilot/selfdrive/selfdrived/selfdrived.py b/openpilot/selfdrive/selfdrived/selfdrived.py index 18b71e5820..0c4c2ce1f6 100755 --- a/openpilot/selfdrive/selfdrived/selfdrived.py +++ b/openpilot/selfdrive/selfdrived/selfdrived.py @@ -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 diff --git a/openpilot/starpilot/controllers/mode_actions.py b/openpilot/starpilot/controllers/mode_actions.py index 19e1858a01..ca5de1c384 100644 --- a/openpilot/starpilot/controllers/mode_actions.py +++ b/openpilot/starpilot/controllers/mode_actions.py @@ -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, () diff --git a/openpilot/starpilot/controllers/tests/test_mode_actions.py b/openpilot/starpilot/controllers/tests/test_mode_actions.py index 7385718284..cbb6dfdda3 100644 --- a/openpilot/starpilot/controllers/tests/test_mode_actions.py +++ b/openpilot/starpilot/controllers/tests/test_mode_actions.py @@ -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 diff --git a/openpilot/starpilot/favorites/actions.py b/openpilot/starpilot/favorites/actions.py index 673c882688..de725def83 100644 --- a/openpilot/starpilot/favorites/actions.py +++ b/openpilot/starpilot/favorites/actions.py @@ -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} diff --git a/openpilot/starpilot/favorites/owner.py b/openpilot/starpilot/favorites/owner.py index 25225fefbb..4a710ae230 100644 --- a/openpilot/starpilot/favorites/owner.py +++ b/openpilot/starpilot/favorites/owner.py @@ -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): diff --git a/openpilot/starpilot/longitudinal/tests/test_radar_lead_projection_cache.py b/openpilot/starpilot/longitudinal/tests/test_radar_lead_projection_cache.py new file mode 100644 index 0000000000..42735a26ee --- /dev/null +++ b/openpilot/starpilot/longitudinal/tests/test_radar_lead_projection_cache.py @@ -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() diff --git a/openpilot/starpilot/longitudinal/toyota_output_policy.py b/openpilot/starpilot/longitudinal/toyota_output_policy.py index 9cdb69301c..2906f26032 100644 --- a/openpilot/starpilot/longitudinal/toyota_output_policy.py +++ b/openpilot/starpilot/longitudinal/toyota_output_policy.py @@ -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: diff --git a/openpilot/starpilot/ui/runtime_app.py b/openpilot/starpilot/ui/runtime_app.py index c8f9e15ba8..ed17a8206b 100644 --- a/openpilot/starpilot/ui/runtime_app.py +++ b/openpilot/starpilot/ui/runtime_app.py @@ -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: diff --git a/openpilot/starpilot/ui/runtime_snapshot.py b/openpilot/starpilot/ui/runtime_snapshot.py index 399893c02d..dd7fd4dcd4 100644 --- a/openpilot/starpilot/ui/runtime_snapshot.py +++ b/openpilot/starpilot/ui/runtime_snapshot.py @@ -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: diff --git a/openpilot/starpilot/ui/tests/test_favorite_render_selection.py b/openpilot/starpilot/ui/tests/test_favorite_render_selection.py new file mode 100644 index 0000000000..8753583617 --- /dev/null +++ b/openpilot/starpilot/ui/tests/test_favorite_render_selection.py @@ -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() diff --git a/openpilot/starpilot/ui/tests/test_runtime_onroad_leaf.py b/openpilot/starpilot/ui/tests/test_runtime_onroad_leaf.py new file mode 100644 index 0000000000..7099a32f61 --- /dev/null +++ b/openpilot/starpilot/ui/tests/test_runtime_onroad_leaf.py @@ -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) diff --git a/panda/board/obj/body_h7.bin.signed b/panda/board/obj/body_h7.bin.signed index da28739791..576e6e4de7 100644 Binary files a/panda/board/obj/body_h7.bin.signed and b/panda/board/obj/body_h7.bin.signed differ diff --git a/panda/board/obj/bootstub.body_h7.bin b/panda/board/obj/bootstub.body_h7.bin index 73c5810449..31b3ae146b 100755 Binary files a/panda/board/obj/bootstub.body_h7.bin and b/panda/board/obj/bootstub.body_h7.bin differ diff --git a/panda/board/obj/bootstub.panda.bin b/panda/board/obj/bootstub.panda.bin index 70af21b11a..8dbc5cba83 100755 Binary files a/panda/board/obj/bootstub.panda.bin and b/panda/board/obj/bootstub.panda.bin differ diff --git a/panda/board/obj/bootstub.panda_can_ignition_only.bin b/panda/board/obj/bootstub.panda_can_ignition_only.bin index 70af21b11a..8dbc5cba83 100755 Binary files a/panda/board/obj/bootstub.panda_can_ignition_only.bin and b/panda/board/obj/bootstub.panda_can_ignition_only.bin differ diff --git a/panda/board/obj/bootstub.panda_h7.bin b/panda/board/obj/bootstub.panda_h7.bin index f564b6f226..b5742480d3 100755 Binary files a/panda/board/obj/bootstub.panda_h7.bin and b/panda/board/obj/bootstub.panda_h7.bin differ diff --git a/panda/board/obj/bootstub.panda_h7_can_ignition_only.bin b/panda/board/obj/bootstub.panda_h7_can_ignition_only.bin index f564b6f226..b5742480d3 100755 Binary files a/panda/board/obj/bootstub.panda_h7_can_ignition_only.bin and b/panda/board/obj/bootstub.panda_h7_can_ignition_only.bin differ diff --git a/panda/board/obj/bootstub.panda_h7_hkg_remote.bin b/panda/board/obj/bootstub.panda_h7_hkg_remote.bin index f564b6f226..b5742480d3 100755 Binary files a/panda/board/obj/bootstub.panda_h7_hkg_remote.bin and b/panda/board/obj/bootstub.panda_h7_hkg_remote.bin differ diff --git a/panda/board/obj/bootstub.panda_h7_hkg_remote_can_ignition_only.bin b/panda/board/obj/bootstub.panda_h7_hkg_remote_can_ignition_only.bin index f564b6f226..b5742480d3 100755 Binary files a/panda/board/obj/bootstub.panda_h7_hkg_remote_can_ignition_only.bin and b/panda/board/obj/bootstub.panda_h7_hkg_remote_can_ignition_only.bin differ diff --git a/panda/board/obj/bootstub.panda_h7_remote.bin b/panda/board/obj/bootstub.panda_h7_remote.bin index f564b6f226..b5742480d3 100755 Binary files a/panda/board/obj/bootstub.panda_h7_remote.bin and b/panda/board/obj/bootstub.panda_h7_remote.bin differ diff --git a/panda/board/obj/bootstub.panda_h7_remote_can_ignition_only.bin b/panda/board/obj/bootstub.panda_h7_remote_can_ignition_only.bin index f564b6f226..b5742480d3 100755 Binary files a/panda/board/obj/bootstub.panda_h7_remote_can_ignition_only.bin and b/panda/board/obj/bootstub.panda_h7_remote_can_ignition_only.bin differ diff --git a/panda/board/obj/bootstub.panda_h7_tesla_wake.bin b/panda/board/obj/bootstub.panda_h7_tesla_wake.bin index f564b6f226..b5742480d3 100755 Binary files a/panda/board/obj/bootstub.panda_h7_tesla_wake.bin and b/panda/board/obj/bootstub.panda_h7_tesla_wake.bin differ diff --git a/panda/board/obj/bootstub.panda_h7_tesla_wake_can_ignition_only.bin b/panda/board/obj/bootstub.panda_h7_tesla_wake_can_ignition_only.bin index f564b6f226..b5742480d3 100755 Binary files a/panda/board/obj/bootstub.panda_h7_tesla_wake_can_ignition_only.bin and b/panda/board/obj/bootstub.panda_h7_tesla_wake_can_ignition_only.bin differ diff --git a/panda/board/obj/bootstub.panda_hkg_remote.bin b/panda/board/obj/bootstub.panda_hkg_remote.bin index 70af21b11a..8dbc5cba83 100755 Binary files a/panda/board/obj/bootstub.panda_hkg_remote.bin and b/panda/board/obj/bootstub.panda_hkg_remote.bin differ diff --git a/panda/board/obj/bootstub.panda_hkg_remote_can_ignition_only.bin b/panda/board/obj/bootstub.panda_hkg_remote_can_ignition_only.bin index 70af21b11a..8dbc5cba83 100755 Binary files a/panda/board/obj/bootstub.panda_hkg_remote_can_ignition_only.bin and b/panda/board/obj/bootstub.panda_hkg_remote_can_ignition_only.bin differ diff --git a/panda/board/obj/bootstub.panda_remote.bin b/panda/board/obj/bootstub.panda_remote.bin index 70af21b11a..8dbc5cba83 100755 Binary files a/panda/board/obj/bootstub.panda_remote.bin and b/panda/board/obj/bootstub.panda_remote.bin differ diff --git a/panda/board/obj/bootstub.panda_remote_can_ignition_only.bin b/panda/board/obj/bootstub.panda_remote_can_ignition_only.bin index 70af21b11a..8dbc5cba83 100755 Binary files a/panda/board/obj/bootstub.panda_remote_can_ignition_only.bin and b/panda/board/obj/bootstub.panda_remote_can_ignition_only.bin differ diff --git a/panda/board/obj/bootstub.panda_tesla_wake.bin b/panda/board/obj/bootstub.panda_tesla_wake.bin index 70af21b11a..8dbc5cba83 100755 Binary files a/panda/board/obj/bootstub.panda_tesla_wake.bin and b/panda/board/obj/bootstub.panda_tesla_wake.bin differ diff --git a/panda/board/obj/bootstub.panda_tesla_wake_can_ignition_only.bin b/panda/board/obj/bootstub.panda_tesla_wake_can_ignition_only.bin index 70af21b11a..8dbc5cba83 100755 Binary files a/panda/board/obj/bootstub.panda_tesla_wake_can_ignition_only.bin and b/panda/board/obj/bootstub.panda_tesla_wake_can_ignition_only.bin differ diff --git a/panda/board/obj/gitversion.h b/panda/board/obj/gitversion.h index f379662f84..d027a03126 100644 --- a/panda/board/obj/gitversion.h +++ b/panda/board/obj/gitversion.h @@ -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"; diff --git a/panda/board/obj/panda.bin.signed b/panda/board/obj/panda.bin.signed index f2676646a0..47bcb6e837 100644 Binary files a/panda/board/obj/panda.bin.signed and b/panda/board/obj/panda.bin.signed differ diff --git a/panda/board/obj/panda_can_ignition_only.bin.signed b/panda/board/obj/panda_can_ignition_only.bin.signed index 3a9eb45567..16318d4f67 100644 Binary files a/panda/board/obj/panda_can_ignition_only.bin.signed and b/panda/board/obj/panda_can_ignition_only.bin.signed differ diff --git a/panda/board/obj/panda_h7.bin.signed b/panda/board/obj/panda_h7.bin.signed index 267bc2af37..0a0c35b0d8 100644 Binary files a/panda/board/obj/panda_h7.bin.signed and b/panda/board/obj/panda_h7.bin.signed differ diff --git a/panda/board/obj/panda_h7_can_ignition_only.bin.signed b/panda/board/obj/panda_h7_can_ignition_only.bin.signed index 6e722fb169..da7a80f9ec 100644 Binary files a/panda/board/obj/panda_h7_can_ignition_only.bin.signed and b/panda/board/obj/panda_h7_can_ignition_only.bin.signed differ diff --git a/panda/board/obj/panda_h7_hkg_remote.bin.signed b/panda/board/obj/panda_h7_hkg_remote.bin.signed index 6f73d5c00f..2e205bf7e4 100644 Binary files a/panda/board/obj/panda_h7_hkg_remote.bin.signed and b/panda/board/obj/panda_h7_hkg_remote.bin.signed differ diff --git a/panda/board/obj/panda_h7_hkg_remote_can_ignition_only.bin.signed b/panda/board/obj/panda_h7_hkg_remote_can_ignition_only.bin.signed index 9bce2fd5c4..17cf18e575 100644 Binary files a/panda/board/obj/panda_h7_hkg_remote_can_ignition_only.bin.signed and b/panda/board/obj/panda_h7_hkg_remote_can_ignition_only.bin.signed differ diff --git a/panda/board/obj/panda_h7_remote.bin.signed b/panda/board/obj/panda_h7_remote.bin.signed index 6097c0dc88..b9d27e1dfc 100644 Binary files a/panda/board/obj/panda_h7_remote.bin.signed and b/panda/board/obj/panda_h7_remote.bin.signed differ diff --git a/panda/board/obj/panda_h7_remote_can_ignition_only.bin.signed b/panda/board/obj/panda_h7_remote_can_ignition_only.bin.signed index 2383a3e613..c72f6f5639 100644 Binary files a/panda/board/obj/panda_h7_remote_can_ignition_only.bin.signed and b/panda/board/obj/panda_h7_remote_can_ignition_only.bin.signed differ diff --git a/panda/board/obj/panda_h7_tesla_wake.bin.signed b/panda/board/obj/panda_h7_tesla_wake.bin.signed index 4f255c3d59..eb252a11ef 100644 Binary files a/panda/board/obj/panda_h7_tesla_wake.bin.signed and b/panda/board/obj/panda_h7_tesla_wake.bin.signed differ diff --git a/panda/board/obj/panda_h7_tesla_wake_can_ignition_only.bin.signed b/panda/board/obj/panda_h7_tesla_wake_can_ignition_only.bin.signed index 7385e7c86f..bc3db1a1be 100644 Binary files a/panda/board/obj/panda_h7_tesla_wake_can_ignition_only.bin.signed and b/panda/board/obj/panda_h7_tesla_wake_can_ignition_only.bin.signed differ diff --git a/panda/board/obj/panda_hkg_remote.bin.signed b/panda/board/obj/panda_hkg_remote.bin.signed index 077a98fdf8..e68d08333a 100644 Binary files a/panda/board/obj/panda_hkg_remote.bin.signed and b/panda/board/obj/panda_hkg_remote.bin.signed differ diff --git a/panda/board/obj/panda_hkg_remote_can_ignition_only.bin.signed b/panda/board/obj/panda_hkg_remote_can_ignition_only.bin.signed index d799c0d461..d0fc51f8bd 100644 Binary files a/panda/board/obj/panda_hkg_remote_can_ignition_only.bin.signed and b/panda/board/obj/panda_hkg_remote_can_ignition_only.bin.signed differ diff --git a/panda/board/obj/panda_remote.bin.signed b/panda/board/obj/panda_remote.bin.signed index 1a51f88ebd..2327fd232f 100644 Binary files a/panda/board/obj/panda_remote.bin.signed and b/panda/board/obj/panda_remote.bin.signed differ diff --git a/panda/board/obj/panda_remote_can_ignition_only.bin.signed b/panda/board/obj/panda_remote_can_ignition_only.bin.signed index 8072fa74d2..91e6f5d6c0 100644 Binary files a/panda/board/obj/panda_remote_can_ignition_only.bin.signed and b/panda/board/obj/panda_remote_can_ignition_only.bin.signed differ diff --git a/panda/board/obj/panda_tesla_wake.bin.signed b/panda/board/obj/panda_tesla_wake.bin.signed index 1df032b3ba..1a5b5d7ce8 100644 Binary files a/panda/board/obj/panda_tesla_wake.bin.signed and b/panda/board/obj/panda_tesla_wake.bin.signed differ diff --git a/panda/board/obj/panda_tesla_wake_can_ignition_only.bin.signed b/panda/board/obj/panda_tesla_wake_can_ignition_only.bin.signed index dd71b17d6c..c4101125fa 100644 Binary files a/panda/board/obj/panda_tesla_wake_can_ignition_only.bin.signed and b/panda/board/obj/panda_tesla_wake_can_ignition_only.bin.signed differ diff --git a/panda/board/obj/version b/panda/board/obj/version index 6790467b69..4471752520 100644 --- a/panda/board/obj/version +++ b/panda/board/obj/version @@ -1 +1 @@ -DEV-39689cd6-P92eb5651-DEBUG \ No newline at end of file +DEV-e9829871-P92eb5651-DEBUG \ No newline at end of file diff --git a/prebuilt.json b/prebuilt.json index cf5b1e92fb..723f593940 100644 --- a/prebuilt.json +++ b/prebuilt.json @@ -1 +1 @@ -{"protected_inventory_sha256": "d2d6d6688177e81187cdf55c32b9fe2bbf198f72c77e66c8cc1d5561ee5dbd4d", "schema_version": 1} +{"protected_inventory_sha256": "57bf4bf680d75776c53c5e64a459500d3743fd8c850d14e89bd9ab2f432d5f0c", "schema_version": 1}