From 40e0f1f2a906c7ca46e7cd482c2ac62830087639 Mon Sep 17 00:00:00 2001 From: firestar5683 <168790843+firestar5683@users.noreply.github.com> Date: Fri, 16 Oct 2026 08:13:10 -0500 Subject: [PATCH] Controls Loop Optimizations --- openpilot/selfdrive/controls/controlsd.py | 6 +- .../tests/test_controlsd_scheduling.py | 32 +++- openpilot/selfdrive/selfdrived/selfdrived.py | 2 +- .../starpilot/controllers/mode_actions.py | 13 ++ .../controllers/tests/test_mode_actions.py | 43 ++++++ openpilot/starpilot/favorites/actions.py | 17 ++- openpilot/starpilot/favorites/owner.py | 12 +- .../tests/test_radar_lead_projection_cache.py | 97 ++++++++++++ .../longitudinal/toyota_output_policy.py | 48 +++++- openpilot/starpilot/ui/runtime_app.py | 15 +- openpilot/starpilot/ui/runtime_snapshot.py | 19 ++- .../tests/test_favorite_render_selection.py | 141 ++++++++++++++++++ .../ui/tests/test_runtime_onroad_leaf.py | 110 ++++++++++++++ panda/board/obj/body_h7.bin.signed | Bin 210540 -> 210540 bytes panda/board/obj/bootstub.body_h7.bin | Bin 12040 -> 12040 bytes panda/board/obj/bootstub.panda.bin | Bin 11176 -> 11176 bytes .../obj/bootstub.panda_can_ignition_only.bin | Bin 11176 -> 11176 bytes panda/board/obj/bootstub.panda_h7.bin | Bin 18824 -> 18824 bytes .../bootstub.panda_h7_can_ignition_only.bin | Bin 18824 -> 18824 bytes .../obj/bootstub.panda_h7_hkg_remote.bin | Bin 18824 -> 18824 bytes ....panda_h7_hkg_remote_can_ignition_only.bin | Bin 18824 -> 18824 bytes panda/board/obj/bootstub.panda_h7_remote.bin | Bin 18824 -> 18824 bytes ...stub.panda_h7_remote_can_ignition_only.bin | Bin 18824 -> 18824 bytes .../obj/bootstub.panda_h7_tesla_wake.bin | Bin 18824 -> 18824 bytes ....panda_h7_tesla_wake_can_ignition_only.bin | Bin 18824 -> 18824 bytes panda/board/obj/bootstub.panda_hkg_remote.bin | Bin 11176 -> 11176 bytes ...tub.panda_hkg_remote_can_ignition_only.bin | Bin 11176 -> 11176 bytes panda/board/obj/bootstub.panda_remote.bin | Bin 11176 -> 11176 bytes ...ootstub.panda_remote_can_ignition_only.bin | Bin 11176 -> 11176 bytes panda/board/obj/bootstub.panda_tesla_wake.bin | Bin 11176 -> 11176 bytes ...tub.panda_tesla_wake_can_ignition_only.bin | Bin 11176 -> 11176 bytes panda/board/obj/gitversion.h | 2 +- panda/board/obj/panda.bin.signed | Bin 201952 -> 201952 bytes .../obj/panda_can_ignition_only.bin.signed | Bin 201928 -> 201928 bytes panda/board/obj/panda_h7.bin.signed | Bin 211268 -> 211268 bytes .../obj/panda_h7_can_ignition_only.bin.signed | Bin 211244 -> 211244 bytes .../board/obj/panda_h7_hkg_remote.bin.signed | Bin 211352 -> 211352 bytes ...h7_hkg_remote_can_ignition_only.bin.signed | Bin 211336 -> 211336 bytes panda/board/obj/panda_h7_remote.bin.signed | Bin 211268 -> 211268 bytes ...nda_h7_remote_can_ignition_only.bin.signed | Bin 211244 -> 211244 bytes .../board/obj/panda_h7_tesla_wake.bin.signed | Bin 211404 -> 211404 bytes ...h7_tesla_wake_can_ignition_only.bin.signed | Bin 211380 -> 211380 bytes panda/board/obj/panda_hkg_remote.bin.signed | Bin 202036 -> 202036 bytes ...da_hkg_remote_can_ignition_only.bin.signed | Bin 202020 -> 202020 bytes panda/board/obj/panda_remote.bin.signed | Bin 201948 -> 201948 bytes .../panda_remote_can_ignition_only.bin.signed | Bin 201924 -> 201924 bytes panda/board/obj/panda_tesla_wake.bin.signed | Bin 202084 -> 202084 bytes ...da_tesla_wake_can_ignition_only.bin.signed | Bin 202060 -> 202060 bytes panda/board/obj/version | 2 +- prebuilt.json | 2 +- 50 files changed, 532 insertions(+), 29 deletions(-) create mode 100644 openpilot/starpilot/longitudinal/tests/test_radar_lead_projection_cache.py create mode 100644 openpilot/starpilot/ui/tests/test_favorite_render_selection.py create mode 100644 openpilot/starpilot/ui/tests/test_runtime_onroad_leaf.py 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 da287397916ff60005d38d217db479a333f40d30..576e6e4de7a2a9bb1cb87653a18f0e9ca5387529 100644 GIT binary patch delta 160 zcmV;R0AK&?>G{U_BXwDMy z9UbDG87SU;p9TN~Q3sTO=#j1d{y_I`Saj9Z^u~QPNY9`q_y7FqTd1ZOD9VY~nLM9G z@A0oiN6ZZGg?ycR;{IL9ueC==V8K-YjG;Ts&e(CV0GC#dV0PWGDUl0FQk+yS}3S5RUA delta 160 zcmV;R0AK&?>xkQsti_RPX<=k(VQ#oNTWc;008xhq+yDRo delta 21 ccmeB(>xkQsti@q$X=Y)WoMN^)TWc;008!Kj3jhEB diff --git a/panda/board/obj/bootstub.panda.bin b/panda/board/obj/bootstub.panda.bin index 70af21b11a9a09d1ede9ef6122c5bc356b2494ae..8dbc5cba8344315e992317237cc82210b0672aa1 100755 GIT binary patch delta 21 dcmZ1xz9M|XMm3I9OA8}Q3v*Wv&QO delta 23 fcmeB}%-AuRal=k$4r5C*3(Mpbv&{#ckFx>*W%~$# 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 f564b6f226e717a0b5828fc1aba61dcd9378829a..b5742480d3f202df26aa7b2e6126fb9156ab2d12 100755 GIT binary patch delta 23 fcmeB}%-AuRal=k$j#NtvBTEZ&!_5bskFx>*Wv&QO delta 23 fcmeB}%-AuRal=k$4r5C*3(Mpbv&{#ckFx>*W%~$# diff --git a/panda/board/obj/bootstub.panda_h7_hkg_remote.bin b/panda/board/obj/bootstub.panda_h7_hkg_remote.bin index f564b6f226e717a0b5828fc1aba61dcd9378829a..b5742480d3f202df26aa7b2e6126fb9156ab2d12 100755 GIT binary patch delta 23 fcmeB}%-AuRal=k$j#NtvBTEZ&!_5bskFx>*Wv&QO delta 23 fcmeB}%-AuRal=k$4r5C*3(Mpbv&{#ckFx>*W%~$# 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 f564b6f226e717a0b5828fc1aba61dcd9378829a..b5742480d3f202df26aa7b2e6126fb9156ab2d12 100755 GIT binary patch delta 23 fcmeB}%-AuRal=k$j#NtvBTEZ&!_5bskFx>*Wv&QO delta 23 fcmeB}%-AuRal=k$4r5C*3(Mpbv&{#ckFx>*W%~$# diff --git a/panda/board/obj/bootstub.panda_h7_remote.bin b/panda/board/obj/bootstub.panda_h7_remote.bin index f564b6f226e717a0b5828fc1aba61dcd9378829a..b5742480d3f202df26aa7b2e6126fb9156ab2d12 100755 GIT binary patch delta 23 fcmeB}%-AuRal=k$j#NtvBTEZ&!_5bskFx>*Wv&QO delta 23 fcmeB}%-AuRal=k$4r5C*3(Mpbv&{#ckFx>*W%~$# 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 f564b6f226e717a0b5828fc1aba61dcd9378829a..b5742480d3f202df26aa7b2e6126fb9156ab2d12 100755 GIT binary patch delta 23 fcmeB}%-AuRal=k$j#NtvBTEZ&!_5bskFx>*Wv&QO delta 23 fcmeB}%-AuRal=k$4r5C*3(Mpbv&{#ckFx>*W%~$# diff --git a/panda/board/obj/bootstub.panda_h7_tesla_wake.bin b/panda/board/obj/bootstub.panda_h7_tesla_wake.bin index f564b6f226e717a0b5828fc1aba61dcd9378829a..b5742480d3f202df26aa7b2e6126fb9156ab2d12 100755 GIT binary patch delta 23 fcmeB}%-AuRal=k$j#NtvBTEZ&!_5bskFx>*Wv&QO delta 23 fcmeB}%-AuRal=k$4r5C*3(Mpbv&{#ckFx>*W%~$# 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 f564b6f226e717a0b5828fc1aba61dcd9378829a..b5742480d3f202df26aa7b2e6126fb9156ab2d12 100755 GIT binary patch delta 23 fcmeB}%-AuRal=k$j#NtvBTEZ&!_5bskFx>*Wv&QO delta 23 fcmeB}%-AuRal=k$4r5C*3(Mpbv&{#ckFx>*W%~$# diff --git a/panda/board/obj/bootstub.panda_hkg_remote.bin b/panda/board/obj/bootstub.panda_hkg_remote.bin index 70af21b11a9a09d1ede9ef6122c5bc356b2494ae..8dbc5cba8344315e992317237cc82210b0672aa1 100755 GIT binary patch delta 21 dcmZ1xz9M|XMm3I9OA8}Q3v4^+0PJ*-K;+ z)d+W));c&*0rQ+wgVpQrrWn(A=}O~u<66qq7Z~z=xMoND+h`)OxI9B!&Nngh6P;;# z+ht8-j80+sk=MCy0(aIXE&oI5#ne9QOgY9QOh&9e-Z^(RBYMpqRepT+y?l z7>pKXa%a)LPp>4ZOPv+*kjz$hLhx)cYpB~^7jgFGOB?{ZL-N=VRSDPIc)k2u;Csnk zZVtp&=IqAcd(-}7*k^JhyZx6Fb9GcUiUblkBW#Eaz3-owwM19077&iy};NfB7rMd=*{T9_nX z10c3nGA(w<({R5k7es+6SO*Ecw;2r6Gwzf+l!j=<6P~-lG7=F84YbCS?k4m}nr})p9<-tXl>$ z9I{zJ6*e87x1eB|u;|$^vWh${)Qx&Z>}wUx{of~{4c4qJO*^pNVfRrHFu(JQ;kvRU QnC$3-%LM+C;KcO-!KX`2T>t<8 delta 162 zcmV;T0A2sY@(jfC41k0Iv;s&P2s1f0I5}fvHiu9e0=G~a19TpL0qzryPB-qjM0m;c zdQ0%R`WuXC6Fq+}(+Cp)4l}HnWwO%is8~w9f?coKr~<@Y0NPCLciYNAIukp)n>w?=oYdq3Js{e1@` QcR;?e3flfL|74{BSNG4PpmJx&QzG diff --git a/panda/board/obj/panda_h7_hkg_remote.bin.signed b/panda/board/obj/panda_h7_hkg_remote.bin.signed index 6f73d5c00ff73e752126515c1915dc398a876a4d..2e205bf7e4a27855e05734a56b1fd8ec411c5326 100644 GIT binary patch delta 162 zcmV;T0A2r>^9-2t41k0Iv;v$O2xU1qGC4RmF^8lY0=J|Z19TpLhZ}kzAOuFQz9WjK zH52Pg9ziP|hi4A$+aBOgd6~~QeN&u3owe4?=?!VfWgiR&ez^bV`NQQq)aa)rQl2s< zo@(Hcx5mqNI95a7U&w9@OJ^*8g(LvIsvr}U!VJA-2DD@S!;FtiYm52J?zky&+d$?a QZFU@WD0in`9}uMhjI1|Dy8r+H delta 162 zcmV;T0A2r>^9-2t41k0Iv;v$O2s1f0I5}fvHix7d0=J|Z19TpLFdpempik^D_A1dJ zgwrHgJ5)8waZvTCEPfOEFIJM8wp0y$mbxT|In9l@q(E!K@MUEtlLt&V$8~pZR(LLf zJ}Sp1@0SBEdWjk!1IM)hH=mby9Rfb8D^?A2r)DWe)`r$V&)M~oVYreU#VKtHzrBuv Q5OIfdi+Y-0P9j4AOG6t&+5i9m 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 9bce2fd5c458b838f6f4d42d9b88d42a6dc098f5..17cf18e575766b928df5c099a9d6a668d9ba0443 100644 GIT binary patch delta 162 zcmV;T0A2rx^9+dd41k0Iv;vG82xU1qGC4RmF^7~I0=JYJ19TpLl!%L62e{6>#)6Tyd#oncjfkQV@$5)XnaG{lS=qf@5;*{x>!fX#(^v?36l3{E*#H0l delta 162 zcmV;T0A2rx^9+dd41k0Iv;vG82s1f0I5}fvHiwiN0=JYJ19TpLOF=bJMtcOFk|!=e z)(PmhhF|)_j}t>i%7^8emQ6hqiUjd$sslAhx)dLXOy`1DVmk{iu}}|`ALDK|xbw{F z$^6jb!Vs$vuOq)b4+JATsTNI2yH?vnGE>_TMoK+B&G0OmxYh&Ug_tT7C*-l{)NJp( QvTm0|R%$l$N((9h!sM1kegFUf diff --git a/panda/board/obj/panda_h7_remote.bin.signed b/panda/board/obj/panda_h7_remote.bin.signed index 6097c0dc888a90deb6f454998f0bdfec875a7fcd..b9d27e1dfcef78eb4277573d9f8811b653704582 100644 GIT binary patch delta 162 zcmV;T0A2sY@(jfC41k0Iv;s&P2xU1qGC4RmF^5nZ0=G~a19TpL2SV_(O*!_9`A{4O z#G;BdHkCMOYZNCgsZ5Uj z66sxNLySAKxwnjOf({x@ULU{G4Q;aHGoLvn|HsJ6pJ$wULX%Z|&@}qav*m zo)+G=c>!iQyJ5d-qhZ$=I>t8q*|dS02(A@{rA7L|5vK0qCK6TFXGCZ`$dv+tjx{($ zXRw3q&ihIbHI-gbrPKfK Qid{T0$rotnOyTz>nd}$s;yjaIm$y`7iox!GmAAp{L0;^;2|9rU1!=Fe23= zTsW9dl)Wf^UeJ2L`O_b{06xVzT#)9Oo3CU*ZdxC;^kA}^HWtw*)p>ZWVPF(+CVt6W Q_I_e-j9wKy(LjX(E08QnYybcN delta 162 zcmV;T0A2sA@(ir<41k0Iv;r_12s1f0I5}fvHitMG0=GCC19TpLJu!poB2G;!^Dx1h zjwn_oMs27-kj(?hRWC{WWIY!po$UM(nkd8~B&Au9l45B69=qw9gP$1;-7>kS_L#w; zp!@z(!m%qOgblmT5Wn}VYy08S!9`J17Yg+`V`Jms3a;7%P7#HZRswV$fG1+QT+I0c QRL2KO0O@jZC8gK_4~}U^EC2ui diff --git a/panda/board/obj/panda_h7_tesla_wake.bin.signed b/panda/board/obj/panda_h7_tesla_wake.bin.signed index 4f255c3d59578f74bcc3523f87c6dfc0283ef710..eb252a11ef7e1d3ee2d34fb49766bf3e6c544113 100644 GIT binary patch delta 162 zcmV;T0A2sg^9;=M41k0Iv;xc;2xU1qGC4RmF^AL|0=Lu}19%>PcWpY>jwoehnv`{S zgg9+rM=V{7P#R#*!`MV(j8lkR9tpET3 delta 162 zcmV;T0A2sg^9;=M41k0Iv;xc;2s1f0I5}fvHiy(20=Lu}19%>PNlzmvwa;#+C?W4k z3zX@jS+>oC0ep(*HiG&W22OOoAh?~p>7J>~rDhYagknLIj%qZ)-2Q3Zt;ojU!n9rV zB8KA_TbQc-{DhDeTN7<_4qw65${1P_W62vQsOT~CxJf+DjB?oHCJKPNBFDwcsI8AEPDZ2 zsUKV2(E*Om@N+eYgC;#F8L|RBnnYD4#Yla+?jIxhX&{i9vnLS;=1o$RS|Uwp7pRtX zm*VM9AYq0%yz`L!saJQ<;%2N+cU##8$LlGg+6GxG2nw$r8X5*PL-rhI#tgsr%;`V$ QERi@6m}}7NSDBCj2nSb34*&oF delta 162 zcmV;T0A2sI^9;1}41k0Iv;wpm2s1f0I5}fvHix_#0=K*x19%>Pjmxz()aRzRxaLPB zmAv&qVSn;p@PgX|5Shfi4p(#$UsB~IP!S|K3f8;nFMs$xD6(W!Y$kSXFPOkD{z{IL#7em QE5^|MjL+n9L0B9C020kfaR2}S diff --git a/panda/board/obj/panda_hkg_remote.bin.signed b/panda/board/obj/panda_hkg_remote.bin.signed index 077a98fdf8ea8b05aace144a1428e03b17c40848..e68d08333ae89d5007d619793459476d1312e46c 100644 GIT binary patch delta 160 zcmV;R0AK&KstmNM41lx&fcFSxIXE&oI5#nei1z`vi1z|49e<5TWqab~wOd7^SJA~H zh%^u6n(kf17>O7c$8GpYU`o;4Zs0mxK8N3FJO=6YkYG(V!KzEqcVt68d#F-ydN^Hk zpl4Bn$f3xPFkqa{^^~syGZukqBaA}}h}JZkcJ@|X$1-Z2srkS zU2&4(_|q>7TVCS4OqM9JD%KGcrAR+?ux!|SIHFLCBB|~ABzgAWm!K$eZi1&Hh&U#Wy?J%B@lH6JK}jaz}o Ot@|puntxnw=K*6#GD~s* 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 d799c0d4616e924d78d003ad0ab917dc126396cb..d0fc51f8bdf2b9e6b8ff36332b05d695eacb438b 100644 GIT binary patch delta 160 zcmV;R0AK&4stly641lx&aQ6shIXE&oI5#nec=rLfc=rM<9enn(=* delta 160 zcmV;R0AK&4stly641lx&aQ6r^IW{;sV`Mgmc=rLfc=rM<9e;;NO2yKxYtjipvsZv@ z0wcy7K@qzrawO$BYQd O8BP7TZD+y#fB_RtF-A-P diff --git a/panda/board/obj/panda_remote.bin.signed b/panda/board/obj/panda_remote.bin.signed index 1a51f88ebd5e711ae9df21f7051b94bda08b8a50..2327fd232f87d0f8b02d6419854baba9bfcd5879 100644 GIT binary patch delta 160 zcmV;R0AK&ysSMny41lx&DEA0uIXE&oI5#neF!uqsF!us19e)M10*{O_jZJfoIwEmY zHjw}m_-uNY9Ie9Q_fur5zWAt>#V>|Z1Cs@E{v}&w2I6pwB#1nax8(n=B?Xu%i;Tb?E1c~hU7)Ma-6CzqV9MB`>4BoV5>WUTS O|HxQ>H8xpk$^rRh-A2Oz delta 160 zcmV;R0AK&ysSMny41lx&DEA06IW{;sV`MgmF!uqsF!us19e;N>?HkpRh<-65U^}pK zkZpyvAN39;LUhO;i&EZ771gqOe6(h-COZys4@)hcdkQC#_`Vn|QtSVD0+?UzRe-V+ zf}!g$UQN&K|G@!DxW_y-Os+E71$?pICP6fco50+p1p8|2dXei6oz|m`@6A+09JFW7 O7xCBp@ol2)f&mBtK}%o& 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 8072fa74d2cfc178605fe7e875f0f07d48b4c394..91e6f5d6c08efce7ed04cb36ec31e0c940ae2b04 100644 GIT binary patch delta 160 zcmV;R0AK&asSL!a41lx&5cddWIXE&oI5#ne8216U8217!9e;FTdxLG2)S-BtP_^X` zi+=|m^;6Xp)kZbhk*-^G)}!f1WmG3j`U$FGmxy$baTGy}@ns*qKhmn99gA6>)0rs)$> OS8qi@-F}s$Z~=2*`c2XR delta 160 zcmV;R0AK&asSL!a41lx&5cdc(IW{;sV`Mgm8216U8217!9e=&bQIx62OF0Rq)81F= zxW{bMAQZ@w2~t2mnRe)!6&)J6Jeeh0Xew1?RRTvZO%~-8FcTNRW?U}jr=mo|I1l8` zmGHQBVUE6ilXtqNfI-pxvUW@Bn OFX>$j_42X_UI8w<)ki`A diff --git a/panda/board/obj/panda_tesla_wake.bin.signed b/panda/board/obj/panda_tesla_wake.bin.signed index 1df032b3ba6102ee237b2a35c034aa04888325db..1a5b5d7ce8b429353d2b068c3f7c2ab016600c9c 100644 GIT binary patch delta 160 zcmV;R0AK&)stn|+41lx&toI0IIXE&oI5#newD$qGwD$rq9e;m2fijU&;jp4aGacKR zMrtoib@*t{$BhDMZ%*Zzxrn&;j|o0|IDxu2K{8E^l# zb=9+(tW}DtAt2?!q^hJE`QE-g1-Dbb{kESHsg-d5fh9oS0di93SagK}8~B*m&WL#P O*&sEHODDDOT>;2w15m2~ delta 160 zcmV;R0AK&)stn|+41lx&toH~rIW{;sV`MgmwD$qGwD$rq9e-X};>=Cg61SDGmC5D0 zNWhL!ObkEh1I`XTFKfqhC_j@~rW38nI4f%5D18YZb6a`J%ppeW6KcFwxDs&)>;$t+ zTuJZ=;&ES-cb3@FA3-3<>VU^)%DN(NEQajRUr*v}X!r`*GzKGEPl8u*kruR$KXD~g O5;-44EO_xLTmk9_dPlAR 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 dd71b17d6ceab0a0bd471a597eef82d462cd06f9..c4101125fade9987b70e229b4f4961f7058fdec6 100644 GIT binary patch delta 160 zcmV;R0AK&istnAk41lx&l=lc_IXE&oI5#neoc95@oc97S9e;C}X49$AWs+`rgBqxp zyo7Y6kv;DB2>-vTpF%E(;%!%b6Zrh2>a=F2d;s%`wtuA+tV`F#GRg0Z?oNE6sQG&~0eR~VcedzegNNLoj<<0|j&G%(;`5yE(*--0hz z-)|Js-du<|A8EwyKH84HB`v5Q-=L4QuD>Zi76))F=s@cc|us OuHVPqXx%d&-vM3^Axk3w 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}