From cab53438aefe5b4d48721da4af09efacd0d19451 Mon Sep 17 00:00:00 2001 From: Daniel Koepping Date: Wed, 16 Sep 2026 19:10:00 -0700 Subject: [PATCH 01/79] AGNOS 19.8 (#38935) --- launch_env.sh | 2 +- openpilot/common/hardware/comma/agnos.json | 22 +++++++++++----------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/launch_env.sh b/launch_env.sh index 4fcb243f12..0cb06e7e5b 100755 --- a/launch_env.sh +++ b/launch_env.sh @@ -16,7 +16,7 @@ export VECLIB_MAXIMUM_THREADS=1 export QCOM_PRIORITY=12 if [ -z "$AGNOS_VERSION" ]; then - export AGNOS_VERSION="19.7" + export AGNOS_VERSION="19.8" fi export STAGING_ROOT="/data/safe_staging" diff --git a/openpilot/common/hardware/comma/agnos.json b/openpilot/common/hardware/comma/agnos.json index 4a48016ed2..92c6a4c7cb 100644 --- a/openpilot/common/hardware/comma/agnos.json +++ b/openpilot/common/hardware/comma/agnos.json @@ -56,29 +56,29 @@ }, { "name": "boot", - "url": "https://commadist.azureedge.net/agnosupdate/boot-6ecf6f987cd11968104abcccabbe268485d329cdb73012dfd3c381a6b8deb27d.img.xz", - "hash": "6ecf6f987cd11968104abcccabbe268485d329cdb73012dfd3c381a6b8deb27d", - "hash_raw": "6ecf6f987cd11968104abcccabbe268485d329cdb73012dfd3c381a6b8deb27d", + "url": "https://commadist.azureedge.net/agnosupdate/boot-335c17579879614a3b6f9b58c867aa2e66609530258e12d9174d6007494fdb11.img.xz", + "hash": "335c17579879614a3b6f9b58c867aa2e66609530258e12d9174d6007494fdb11", + "hash_raw": "335c17579879614a3b6f9b58c867aa2e66609530258e12d9174d6007494fdb11", "size": 46897152, "sparse": false, "full_check": true, "has_ab": true, - "ondevice_hash": "d12e1e5b9455b62a1464558716493b33e470d7a7e88da1c4105a3b21d0961808" + "ondevice_hash": "545d64ff38626d0aff49054cdb7abc735992178f4872fd26ac544d652af38544" }, { "name": "system", - "url": "https://commadist.azureedge.net/agnosupdate/system-3c271e2b3d20d2f0a8bf6555a1319f3efb12845490967d6151195174a01e912f.img.xz", - "hash": "74ffc9c551e1f29cda897ace8a69080fe644f8039977c6885f2b48362e39b744", - "hash_raw": "3c271e2b3d20d2f0a8bf6555a1319f3efb12845490967d6151195174a01e912f", + "url": "https://commadist.azureedge.net/agnosupdate/system-20d7a522190ba8b6351364beca9702f40303651577d7c3f6c2c1cb6b34e5db6b.img.xz", + "hash": "62c6ecab2772f48b90dd2e7f0c3bb413dd7577d1ae9aad971135ce0d9d141544", + "hash_raw": "20d7a522190ba8b6351364beca9702f40303651577d7c3f6c2c1cb6b34e5db6b", "size": 4718592000, "sparse": true, "full_check": false, "has_ab": true, - "ondevice_hash": "6a992680183685eea9db99d915219a37935f45989330d9b619e880450257f448", + "ondevice_hash": "14251b982859824ea12f5e5c63b336b6d6333779d42457aff7e43bbf9c9fd79b", "alt": { - "hash": "3c271e2b3d20d2f0a8bf6555a1319f3efb12845490967d6151195174a01e912f", - "url": "https://commadist.azureedge.net/agnosupdate/system-3c271e2b3d20d2f0a8bf6555a1319f3efb12845490967d6151195174a01e912f.img", + "hash": "20d7a522190ba8b6351364beca9702f40303651577d7c3f6c2c1cb6b34e5db6b", + "url": "https://commadist.azureedge.net/agnosupdate/system-20d7a522190ba8b6351364beca9702f40303651577d7c3f6c2c1cb6b34e5db6b.img", "size": 4718592000 } } -] \ No newline at end of file +] From 4d9d1bc34168215ba8a093b168e0073acf5336cc Mon Sep 17 00:00:00 2001 From: stef <19478336+stefpi@users.noreply.github.com> Date: Wed, 16 Sep 2026 22:34:52 -0400 Subject: [PATCH 02/79] ui: not paired bookmark alert (#38936) * check connect alert * remove test * simplify * clean diff * fit with bookmark icon out * clean --- openpilot/cereal/log.capnp | 1 + openpilot/selfdrive/selfdrived/events.py | 4 ++++ openpilot/selfdrive/selfdrived/selfdrived.py | 4 +++- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/openpilot/cereal/log.capnp b/openpilot/cereal/log.capnp index 086f6ca719..d8a761573c 100644 --- a/openpilot/cereal/log.capnp +++ b/openpilot/cereal/log.capnp @@ -128,6 +128,7 @@ struct OnroadEvent @0xc4fa6047f024e718 { personalityChanged @91; aeb @92; userBookmark @95; + userBookmarkNotPaired @104; excessiveActuation @96; bigModelLoading @100; bigModelFailed @102; diff --git a/openpilot/selfdrive/selfdrived/events.py b/openpilot/selfdrive/selfdrived/events.py index 69fa900be4..b430b91c82 100755 --- a/openpilot/selfdrive/selfdrived/events.py +++ b/openpilot/selfdrive/selfdrived/events.py @@ -1008,6 +1008,10 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = { EventName.userBookmark: { ET.PERMANENT: NormalPermanentAlert("Bookmark Saved", duration=1.5), }, + + EventName.userBookmarkNotPaired: { + ET.PERMANENT: NormalPermanentAlert("Bookmark Saved", "pair to connect\nto view", duration=1.5), + }, } diff --git a/openpilot/selfdrive/selfdrived/selfdrived.py b/openpilot/selfdrive/selfdrived/selfdrived.py index f7636ce427..0a468091a7 100755 --- a/openpilot/selfdrive/selfdrived/selfdrived.py +++ b/openpilot/selfdrive/selfdrived/selfdrived.py @@ -199,7 +199,9 @@ class SelfdriveD: # Check for user bookmark press if self.sm.updated['userBookmark']: - self.events.add(EventName.userBookmark) + prime_type = self.params.get("PrimeType") + paired = prime_type is not None and int(prime_type) >= 0 + self.events.add(EventName.userBookmark if paired else EventName.userBookmarkNotPaired) # Don't add any more events while in dashcam mode if self.CP.passive: From bd176cb6a7c9aec51bb92577b7e3a6035a6986c6 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 17 Sep 2026 00:59:12 -0700 Subject: [PATCH 03/79] ui: remove question marks (#38938) fix question marks --- .../ui/mici/layouts/settings/network/network_layout.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpilot/selfdrive/ui/mici/layouts/settings/network/network_layout.py b/openpilot/selfdrive/ui/mici/layouts/settings/network/network_layout.py index 4203336469..d38d640bdd 100644 --- a/openpilot/selfdrive/ui/mici/layouts/settings/network/network_layout.py +++ b/openpilot/selfdrive/ui/mici/layouts/settings/network/network_layout.py @@ -30,7 +30,7 @@ class NetworkLayoutMici(NavScroller): self._wifi_manager.set_tethering_active(checked) self._tethering_toggle_btn = BigToggle("enable tethering", "", toggle_callback=tethering_toggle_callback, - description="Share the device’s internet connection through a Wi-Fi hotspot.") + description="Share the device's internet connection through a Wi-Fi hotspot.") def tethering_password_callback(password: str): if password: @@ -61,7 +61,7 @@ class NetworkLayoutMici(NavScroller): # TODO: signal for current network metered type when changing networks, this is wrong until you press it once # TODO: disable when not connected self._network_metered_btn = BigMultiToggle("network usage", ["default", "metered", "unmetered"], select_callback=network_metered_callback, - description="Metered prevents large uploads on this Wi-Fi connection. Default uses the network’s detected " + + description="Metered prevents large uploads on this Wi-Fi connection. Default uses the network's detected " + "setting.") self._network_metered_btn.set_enabled(False) From cd1490af4beca7e9edcd3b21daf6737aadff2a4a Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Thu, 17 Sep 2026 14:31:42 -0700 Subject: [PATCH 04/79] modeld: 2x faster chestnut build (#38656) * not worth 1m * saves 24s * save 13s * parallelize model compilation * explain compiler affinity * full cpufreq * rm that for now * cmt --------- Co-authored-by: elkoled --- openpilot/selfdrive/modeld/SConscript | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpilot/selfdrive/modeld/SConscript b/openpilot/selfdrive/modeld/SConscript index 2aac90fe63..b6274f5cc5 100644 --- a/openpilot/selfdrive/modeld/SConscript +++ b/openpilot/selfdrive/modeld/SConscript @@ -35,8 +35,8 @@ mac_brew_string = f'HOME={os.path.expanduser("~")}' if arch == 'Darwin' else '' warp_deps = [File("#openpilot/system/camerad/cameras/nv12_info.py")] compiler = Dir('#tinygrad_repo/examples/openpilot').abspath -# CPU 7 is isolated with isolcpus on AGNOS, so explicitly pin the compiler to it. -taskset = 'taskset -c 7 ' if arch == 'comma_arm64' else '' +# build on all cores +taskset = 'taskset -c 0-7 ' if arch == 'comma_arm64' else '' def chestnut_action(command): def do_compile(target, source, env): From 7db7735598cd6a44a1c4e1d146c27b2611bbdb07 Mon Sep 17 00:00:00 2001 From: Daniel Koepping Date: Thu, 17 Sep 2026 15:58:41 -0700 Subject: [PATCH 05/79] rm chestnut power test (#38943) * test power draw with chestnut * revert chestnut power draw test * Revert "add powertest for mici (#38843)" This reverts commit f174e39d7a799d2523da0650e043973a3389451e. --- Jenkinsfile | 1 - .../common/hardware/comma/power_monitor.py | 13 +++++------- openpilot/selfdrive/test/test_power_draw.py | 21 ++++--------------- 3 files changed, 9 insertions(+), 26 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 67039d0927..6908fa6c3d 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -265,7 +265,6 @@ node { step("build", "./openpilot/selfdrive/test/chestnut.sh"), step("model replay", "openpilot/selfdrive/test/process_replay/model_replay.py --chestnut"), step("onroad tests", "./openpilot/selfdrive/test/test_onroad.py TestChestnutOnroad", [timeout: 120]), - step("test power draw", "./openpilot/selfdrive/test/test_power_draw.py"), ]) }, diff --git a/openpilot/common/hardware/comma/power_monitor.py b/openpilot/common/hardware/comma/power_monitor.py index 9ae2c9e138..296290dae8 100755 --- a/openpilot/common/hardware/comma/power_monitor.py +++ b/openpilot/common/hardware/comma/power_monitor.py @@ -9,25 +9,22 @@ from openpilot.common.realtime import Ratekeeper from openpilot.common.filter_simple import FirstOrderFilter -def read_power(panda=None): - if panda is not None and panda.get_type() == panda.HW_TYPE_CUATRO: - health = panda.health() - return health['voltage'] * health['current'] / 1e6 +def read_power(): with open("/sys/bus/i2c/devices/0-0040/hwmon/hwmon1/power1_input") as f: return int(f.read()) / 1e6 -def sample_power(seconds=5, panda=None) -> list[float]: +def sample_power(seconds=5) -> list[float]: rate = 123 rk = Ratekeeper(rate, print_delay_threshold=None) pwrs = [] for _ in range(rate*seconds): - pwrs.append(read_power(panda)) + pwrs.append(read_power()) rk.keep_time() return pwrs -def get_power(seconds=5, panda=None): - pwrs = sample_power(seconds, panda) +def get_power(seconds=5): + pwrs = sample_power(seconds) return np.mean(pwrs) def wait_for_power(min_pwr, max_pwr, min_secs_in_range, timeout): diff --git a/openpilot/selfdrive/test/test_power_draw.py b/openpilot/selfdrive/test/test_power_draw.py index d8b234fe60..1d0be24ef0 100755 --- a/openpilot/selfdrive/test/test_power_draw.py +++ b/openpilot/selfdrive/test/test_power_draw.py @@ -5,8 +5,6 @@ import time import unittest import numpy as np from dataclasses import dataclass -from panda import Panda -from openpilot.common.hardware import HARDWARE from openpilot.common.test import OpenpilotTestCase from openpilot.common.utils import tabulate @@ -16,14 +14,11 @@ from opendbc.car.car_helpers import get_demo_car_params from openpilot.common.mock import mock_messages from openpilot.common.params import Params from openpilot.common.hardware.comma.power_monitor import get_power -from openpilot.selfdrive.modeld.helpers import chestnut_present from openpilot.system.manager.process_config import managed_processes from openpilot.system.manager.manager import manager_cleanup SAMPLE_TIME = 2 # seconds to sample power MAX_WARMUP_TIME = 30 # seconds to wait for SAMPLE_TIME consecutive valid samples -MICI = HARDWARE.get_device_type() == "mici" -CHESTNUT = chestnut_present() @dataclass class Proc: @@ -38,10 +33,9 @@ class Proc: return '+'.join(self.procs) -# MICI readings exclude the separately powered Chestnut GPU. PROCS = [ - Proc(['camerad'], 0.85 if MICI else 1.65, atol=0.4, msgs=['narrowRoadCameraState', 'wideRoadCameraState', 'cabinCameraState']), - Proc(['modeld'], 0.45 if MICI and CHESTNUT else 1.5, atol=0.2, msgs=['modelV2']), + Proc(['camerad'], 1.65, atol=0.4, msgs=['narrowRoadCameraState', 'wideRoadCameraState', 'cabinCameraState']), + Proc(['modeld'], 1.5, atol=0.2, msgs=['modelV2']), Proc(['dmonitoringmodeld'], 0.65, atol=0.35, msgs=['driverStateV2']), Proc(['encoderd'], 0.23, msgs=[]), ] @@ -52,13 +46,6 @@ class TestPowerDraw(OpenpilotTestCase): def setup_method(self): Params().put("CarParams", get_demo_car_params().to_bytes(), block=True) - self.panda = None - if MICI: - HARDWARE.reset_internal_panda() - self.addCleanup(HARDWARE.reset_internal_panda) - Panda.wait_for_panda(None, 30) - self.panda = Panda(cli=False) - self.addCleanup(self.panda.close) def teardown_method(self): manager_cleanup() @@ -91,7 +78,7 @@ class TestPowerDraw(OpenpilotTestCase): start_time = time.monotonic() while (time.monotonic() - start_time) < MAX_WARMUP_TIME: - power = get_power(1, self.panda) + power = get_power(1) iteration_msg_counts = {} for msg,sock in socks.items(): iteration_msg_counts[msg] = len(messaging.drain_sock_raw(sock)) @@ -110,7 +97,7 @@ class TestPowerDraw(OpenpilotTestCase): @mock_messages(['deviceMotion']) def test_camera_procs(self, subtests): - baseline = get_power(panda=self.panda) + baseline = get_power() prev = baseline used = {} From 1328ace0684c2850b0fb195ad73d31925b8e553f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Harald=20Sch=C3=A4fer?= Date: Thu, 17 Sep 2026 16:34:52 -0700 Subject: [PATCH 06/79] Remove model chunking and use LFS for Chestnut releases (#38941) * Remove model chunking and use LFS for Chestnut releases * Check Git blob sizes once during release packaging * Download the big model for Chestnut nightly builds * Reuse published LFS models when pushing releases * Preserve LFS configuration when copying Chestnut releases * Detect release builds independently of LFS attributes --- Jenkinsfile | 2 +- SConstruct | 15 +--- openpilot/common/file_chunker.py | 75 ------------------- .../selfdrive/modeld/dmonitoringmodeld.py | 4 +- openpilot/selfdrive/modeld/helpers.py | 3 +- openpilot/selfdrive/modeld/modeld.py | 4 +- tools/release/README.md | 2 +- tools/release/build_release.sh | 8 +- tools/release/build_stripped.sh | 21 ++---- tools/release/check_file_sizes.sh | 8 ++ tools/release/release_files.py | 7 +- tools/release/setup_lfs.sh | 9 +++ 12 files changed, 39 insertions(+), 119 deletions(-) delete mode 100755 openpilot/common/file_chunker.py create mode 100644 tools/release/check_file_sizes.sh create mode 100644 tools/release/setup_lfs.sh diff --git a/Jenkinsfile b/Jenkinsfile index 6908fa6c3d..92a26d6265 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -204,7 +204,7 @@ node { } if (env.BRANCH_NAME == '__nightly-chestnut') { - deviceStage("build nightly-chestnut", "mici-chestnut-ci", [], [ + deviceStage("build nightly-chestnut", "mici-chestnut-ci", ["CHESTNUT=1"], [ step("build nightly-chestnut", "SCONSFLAGS=-j4 INCLUDE_BIG_MODEL=1 PANDA_DEBUG_BUILD=1 RELEASE_BRANCH=nightly-chestnut $SOURCE_DIR/tools/release/build_release.sh TestChestnutOnroad"), ]) } diff --git a/SConstruct b/SConstruct index d524a20e0f..00428ed435 100644 --- a/SConstruct +++ b/SConstruct @@ -20,7 +20,7 @@ SetOption('num_jobs', max(1, int(os.cpu_count()/(1 if "CI" in os.environ else 2) AddOption('--ccflags', action='store', type='string', default='', help='pass arbitrary flags over the command line') AddOption('--verbose', action='store_true', default=False, help='show full build commands') -release = not os.path.exists(File('#.gitattributes').abspath) # file absent on release branch, see release_files.py +release = not os.path.exists(File('#.gitmodules').abspath) # file absent on release branch, see release_files.py AddOption('--minimal', action='store_false', dest='extras', @@ -312,8 +312,6 @@ def count_scons_nodes(nodes): if node in seen: continue seen.add(node) - if hasattr(node, 'has_builder') and node.has_builder(): - build_product_nodes.add(node) executor = node.get_executor() if executor is not None: stack += executor.get_all_prerequisites() + executor.get_all_children() @@ -322,7 +320,6 @@ def count_scons_nodes(nodes): progress_interval = 5 progress_count = 0 -build_product_nodes = set() progress_total = max(1, count_scons_nodes(env.arg2nodes(BUILD_TARGETS or [Dir('.')], env.fs.Entry))) def progress_function(node): @@ -338,13 +335,3 @@ def progress_function(node): Progress(progress_function, interval=progress_interval) AddPostAction(BUILD_TARGETS or [Dir('.')], prune_cache_dir) - -def check_build_product_size(target, source, env): - limit = 50 * 1024 * 1024 # GitHub max size - for t in target: - if str(t).endswith('.pkl'): # chunked during release packaging - continue - if hasattr(t, 'isfile') and t.isfile() and (size := os.path.getsize(t.abspath)) > limit: - raise SCons.Errors.UserError(f"{t} is {size / (1024 * 1024):.1f} MiB, exceeding the {limit / (1024 * 1024):.1f} MiB limit") -if not GetOption('extras'): - AddPostAction(list(build_product_nodes), Action(check_build_product_size, None)) diff --git a/openpilot/common/file_chunker.py b/openpilot/common/file_chunker.py deleted file mode 100755 index 28a7e1212e..0000000000 --- a/openpilot/common/file_chunker.py +++ /dev/null @@ -1,75 +0,0 @@ -#!/usr/bin/env python3 -import io -import sys -import math -import os -from pathlib import Path - -CHUNK_SIZE = 45 * 1024 * 1024 # 45MB, under GitHub's 50MB limit - -def get_chunk_name(name, idx, num_chunks): - return f"{name}.chunk{idx+1:02d}of{num_chunks:02d}" - -def get_manifest_path(name): - return f"{name}.chunkmanifest" - -def _chunk_paths(path, num_chunks): - return [get_manifest_path(path)] + [get_chunk_name(path, i, num_chunks) for i in range(num_chunks)] - -def get_chunk_targets(path, file_size): - num_chunks = math.ceil(file_size / CHUNK_SIZE) - return _chunk_paths(path, num_chunks) - -def chunk_file(path, targets): - manifest_path, *chunk_paths = targets - actual_num_chunks = max(1, math.ceil(os.path.getsize(path) / CHUNK_SIZE)) - assert len(chunk_paths) >= actual_num_chunks, f"Allowed {len(chunk_paths)} chunks but needs at least {actual_num_chunks}, for path {path}" - Path(manifest_path).unlink(missing_ok=True) - with open(path, 'rb') as f: - for chunk_path in chunk_paths: - with open(chunk_path, 'wb') as out: - out.write(f.read(CHUNK_SIZE)) - Path(manifest_path).write_text(str(len(chunk_paths))) - os.remove(path) - -class ChunkStream(io.RawIOBase): - def __init__(self, paths): - self._paths = iter(paths) - self._f = None - - def readable(self): - return True - - def readinto(self, b): - n = 0 - view = memoryview(b) - while n < len(b): - if self._f is None: - p = next(self._paths, None) - if p is None: - break - self._f = open(p, 'rb') - count = self._f.readinto(view[n:]) - if not count: - self._f.close() - self._f = None - continue - n += count - return n - -def open_file_chunked(path): - manifest_path = get_manifest_path(path) - if os.path.isfile(path): - paths = [path] - elif os.path.isfile(manifest_path): - num_chunks = int(Path(manifest_path).read_text().strip()) - paths = [get_chunk_name(path, i, num_chunks) for i in range(num_chunks)] - else: - raise FileNotFoundError(path) - return io.BufferedReader(ChunkStream(paths)) - - -if __name__ == "__main__": - path = sys.argv[1] - chunk_paths = get_chunk_targets(path, os.path.getsize(path)) - chunk_file(path, chunk_paths) diff --git a/openpilot/selfdrive/modeld/dmonitoringmodeld.py b/openpilot/selfdrive/modeld/dmonitoringmodeld.py index 06d0d2e5e7..1ca9aab49b 100755 --- a/openpilot/selfdrive/modeld/dmonitoringmodeld.py +++ b/openpilot/selfdrive/modeld/dmonitoringmodeld.py @@ -16,7 +16,6 @@ from openpilot.common.realtime import config_realtime_process from openpilot.common.transformations.model import dmonitoringmodel_intrinsics from openpilot.common.transformations.camera import _ar_ox_fisheye, _os_fisheye from openpilot.system.camerad.cameras.nv12_info import get_nv12_info -from openpilot.common.file_chunker import open_file_chunked from openpilot.selfdrive.modeld.parse_model_outputs import sigmoid, safe_exp SEND_RAW_PRED = os.getenv('SEND_RAW_PRED') @@ -28,7 +27,8 @@ class ModelState: output: np.ndarray def __init__(self, cam_w: int, cam_h: int): - jits = load_oob(open_file_chunked(MODEL_PKL_PATH)) + with open(MODEL_PKL_PATH, 'rb') as f: + jits = load_oob(f) self.DEV = jits['input_specs']['input_img'][2] self.input_shapes = jits['metadata']['input_shapes'] self.output_slices = pickle.loads(base64.b64decode(jits['metadata']['metadata']['output_slices'])) diff --git a/openpilot/selfdrive/modeld/helpers.py b/openpilot/selfdrive/modeld/helpers.py index 5e7f413c26..8b376af380 100644 --- a/openpilot/selfdrive/modeld/helpers.py +++ b/openpilot/selfdrive/modeld/helpers.py @@ -3,7 +3,6 @@ import pickle import struct from pathlib import Path -from openpilot.common.file_chunker import get_manifest_path from openpilot.common.hardware.usb import CHESTNUT_USB_PRODUCT, USB_DEVICES_PATH, is_chestnut_usb_id MODELS_DIR = Path(__file__).resolve().parent / 'models' @@ -36,5 +35,5 @@ def chestnut_present() -> bool: def chestnut_compiled() -> bool: path = modeld_pkl_path(chestnut=True) - return (path.is_file() or Path(get_manifest_path(path)).is_file()) and all( + return path.is_file() and all( (MODELS_DIR / f'big_driving_warp_{size}_tinygrad.pkl').is_file() for size in ('1344x760', '1928x1208')) diff --git a/openpilot/selfdrive/modeld/modeld.py b/openpilot/selfdrive/modeld/modeld.py index 42187a04f3..e187ae0ce2 100755 --- a/openpilot/selfdrive/modeld/modeld.py +++ b/openpilot/selfdrive/modeld/modeld.py @@ -34,7 +34,6 @@ from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper from openpilot.selfdrive.controls.lib.drive_helpers import get_accel_from_plan, should_stop, smooth_value, get_curvature_from_plan from openpilot.selfdrive.modeld.parse_model_outputs import Parser from openpilot.selfdrive.modeld.fill_model_msg import fill_model_msg, fill_driving_model_data, fill_pose_msg, PublishState -from openpilot.common.file_chunker import open_file_chunked from openpilot.selfdrive.modeld.constants import ModelConstants, Plan from openpilot.selfdrive.modeld.helpers import MODELS_DIR, chestnut_present, chestnut_compiled, modeld_pkl_path, load_oob @@ -139,7 +138,8 @@ class ModelState: prev_desire: np.ndarray # for tracking the rising edge of the pulse def __init__(self, cam_w: int, cam_h: int, chestnut: bool): - jits = load_oob(open_file_chunked(modeld_pkl_path(chestnut))) + with open(modeld_pkl_path(chestnut), 'rb') as f: + jits = load_oob(f) self.model_device = jits['input_specs']['new_img'][2] self.input_shapes = {name: (shape, np.dtype(dtype)) for name, (shape, dtype, _) in jits['input_specs'].items()} self.state_pairs = {name: f'next_{name}' for name in self.input_shapes if f'next_{name}' in jits['metadata']['output_shapes']} diff --git a/tools/release/README.md b/tools/release/README.md index 7e2e7e2faf..25ae32f704 100644 --- a/tools/release/README.md +++ b/tools/release/README.md @@ -21,7 +21,7 @@ - [ ] update from new release -> previous release - [ ] fresh install with `openpilot-test.comma.ai` - [ ] drive on fresh install - - [ ] no submodules or LFS + - [ ] no submodules; only the Chestnut big driving model uses LFS - [ ] check MTBF, etc. - [ ] stress test passes in production - [ ] publish the blog post diff --git a/tools/release/build_release.sh b/tools/release/build_release.sh index 42417d7ace..5b3b2a3a8a 100755 --- a/tools/release/build_release.sh +++ b/tools/release/build_release.sh @@ -39,6 +39,7 @@ cd "$SOURCE_DIR" # in the directory cd "$BUILD_DIR" +source "$SOURCE_DIR/tools/release/setup_lfs.sh" # use the full CPU available for speeding up the build. # openpilot resets the CPU frequencies when test_onroad.py runs below. @@ -58,9 +59,8 @@ else scons panda/ fi -find openpilot/selfdrive/modeld/models -name '*.pkl' -size +95M -exec ./openpilot/common/file_chunker.py {} \; if [ -n "$INCLUDE_BIG_MODEL" ]; then - test -f openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl.chunkmanifest + test -f openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl fi # Ensure no submodules in release @@ -88,6 +88,7 @@ VERSION=$(cat openpilot/common/version.h | awk -F[\"-] '{print $2}') # writing larger objects is faster than compressing them on-device git -c core.compression=0 add -f . git -c core.compression=0 -c gc.auto=0 commit -m "openpilot v$VERSION" +source "$SOURCE_DIR/tools/release/check_file_sizes.sh" # Run tests cd "$BUILD_DIR" @@ -100,6 +101,7 @@ for branch in ${RELEASE_BRANCH//,/ }; do REFS+=("$BUILD_BRANCH:$branch") done # uploading the larger pack is faster than spending CPU to optimize it -git -c pack.window=0 -c pack.depth=0 -c pack.compression=0 push -f origin "${REFS[@]}" +# The big model is already published to LFS by the source branch. +GIT_LFS_SKIP_PUSH=1 git -c pack.window=0 -c pack.depth=0 -c pack.compression=0 push -f origin "${REFS[@]}" echo "[-] done T=$SECONDS" diff --git a/tools/release/build_stripped.sh b/tools/release/build_stripped.sh index 7b9fae4c98..d6aed67778 100755 --- a/tools/release/build_stripped.sh +++ b/tools/release/build_stripped.sh @@ -39,7 +39,7 @@ cd "$SOURCE_DIR" cd "$TARGET_DIR" rm -rf .git/modules/ -find openpilot/selfdrive/modeld/models -name '*.pkl' -size +95M -exec ./openpilot/common/file_chunker.py {} \; +source "$SOURCE_DIR/tools/release/setup_lfs.sh" # include source commit hash and build date in commit GIT_HASH=$(git --git-dir="$SOURCE_DIR/.git" rev-parse HEAD) @@ -60,29 +60,20 @@ date: $DATETIME master commit: $GIT_HASH " -# should be no submodules or LFS files +# should be no submodules or unexpected LFS files git submodule status -if [ ! -z "$(git lfs ls-files)" ]; then +if [ -z "$INCLUDE_BIG_MODEL" ] && [ -n "$(git lfs ls-files)" ]; then echo "LFS files detected!" exit 1 fi -# ensure files are within GitHub's limit -BIG_FILES="$(find . -type f -not -path './.git/*' -size +95M)" -if [ ! -z "$BIG_FILES" ]; then - printf '\n\n\n' - echo "Found files exceeding GitHub's 100MB limit:" - echo "$BIG_FILES" - exit 1 -fi +source "$SOURCE_DIR/tools/release/check_file_sizes.sh" if [ ! -z "$BRANCH" ]; then echo "[-] Pushing to $BRANCH T=$SECONDS" - # Reset hooks since releases exclude .venv - git config --local core.hooksPath .git/hooks - git lfs update --force # uploading the larger pack is faster than spending CPU to optimize it - git -c pack.window=0 -c pack.depth=0 -c pack.compression=0 push -f origin "tmp:$BRANCH" + # The big model is already published to LFS by the source branch. + GIT_LFS_SKIP_PUSH=1 git -c pack.window=0 -c pack.depth=0 -c pack.compression=0 push -f origin "tmp:$BRANCH" fi echo "[-] done T=$SECONDS, ready at $TARGET_DIR" diff --git a/tools/release/check_file_sizes.sh b/tools/release/check_file_sizes.sh new file mode 100644 index 0000000000..2563e78c2b --- /dev/null +++ b/tools/release/check_file_sizes.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +BIG_FILES="$(git ls-tree -rl HEAD | awk '$4 > 95 * 1024 * 1024 {print $5}')" +if [ -n "$BIG_FILES" ]; then + echo "Found Git blobs exceeding the 95 MiB release limit:" + echo "$BIG_FILES" + exit 1 +fi diff --git a/tools/release/release_files.py b/tools/release/release_files.py index dd963eaced..caa34d0f39 100755 --- a/tools/release/release_files.py +++ b/tools/release/release_files.py @@ -14,7 +14,7 @@ blacklist = [ "matlab.*.md", - # no LFS or submodules in release + # release scripts configure LFS for the big driving model ".lfsconfig", ".gitattributes", ".git$", @@ -22,8 +22,7 @@ blacklist = [ ] # gets you through the blacklist -whitelist: list[str] = [ -] +whitelist: list[str] = [r"^\.lfsconfig$"] if os.getenv("INCLUDE_BIG_MODEL") else [] if __name__ == "__main__": tracked_files = subprocess.check_output(["git", "ls-files", "-z", "--recurse-submodules"], cwd=ROOT).split(b"\0") @@ -32,7 +31,7 @@ if __name__ == "__main__": continue rf = os.fsdecode(tracked_file) - if not os.getenv("INCLUDE_BIG_MODEL") and rf.startswith("openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl"): + if not os.getenv("INCLUDE_BIG_MODEL") and rf == "openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl": continue blacklisted = any(re.search(p, rf) for p in blacklist) whitelisted = any(re.search(p, rf) for p in whitelist) diff --git a/tools/release/setup_lfs.sh b/tools/release/setup_lfs.sh new file mode 100644 index 0000000000..68464af9d6 --- /dev/null +++ b/tools/release/setup_lfs.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +if [ -n "$INCLUDE_BIG_MODEL" ]; then + echo 'openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl filter=lfs diff=lfs merge=lfs -text' > .gitattributes +fi + +# Reset filters and hooks since releases exclude .venv. +git config --local core.hooksPath "$(git rev-parse --git-common-dir)/hooks" +git lfs install --local --force From f992a60bbda401657554629ac9e6083146a12264 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Thu, 17 Sep 2026 17:43:25 -0700 Subject: [PATCH 07/79] op setup: 2x faster on Linux (warm venv) (#38945) --- tools/setup_dependencies.sh | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/tools/setup_dependencies.sh b/tools/setup_dependencies.sh index c9e635836b..6f023f7b53 100755 --- a/tools/setup_dependencies.sh +++ b/tools/setup_dependencies.sh @@ -72,7 +72,8 @@ function install_linux_deps() { fi if [[ -d "/etc/udev/rules.d/" ]]; then - "${SUDO[@]}" tee /etc/udev/rules.d/11-openpilot.rules > /dev/null <<-EOF + local udev_rules + udev_rules=$(cat <<-EOF # Panda Jungle devices SUBSYSTEM=="usb", ATTRS{idVendor}=="3801", ATTRS{idProduct}=="ddcf", MODE="0666" SUBSYSTEM=="usb", ATTRS{idVendor}=="3801", ATTRS{idProduct}=="ddef", MODE="0666" @@ -89,11 +90,17 @@ function install_linux_deps() { # comma devices over ADB SUBSYSTEM=="usb", ATTR{idVendor}=="04d8", ATTR{idProduct}=="1234", ENV{adb_user}="yes" EOF + ) - # delete the old ones - "${SUDO[@]}" rm -f /etc/udev/rules.d/11-panda.rules /etc/udev/rules.d/12-panda_jungle.rules /etc/udev/rules.d/50-comma-adb.rules + if ! cmp -s /etc/udev/rules.d/11-openpilot.rules <<< "$udev_rules" || + [[ -e /etc/udev/rules.d/11-panda.rules || -e /etc/udev/rules.d/12-panda_jungle.rules || -e /etc/udev/rules.d/50-comma-adb.rules ]]; then + "${SUDO[@]}" tee /etc/udev/rules.d/11-openpilot.rules > /dev/null <<< "$udev_rules" - "${SUDO[@]}" udevadm control --reload-rules && "${SUDO[@]}" udevadm trigger || true + # delete the old ones + "${SUDO[@]}" rm -f /etc/udev/rules.d/11-panda.rules /etc/udev/rules.d/12-panda_jungle.rules /etc/udev/rules.d/50-comma-adb.rules + + "${SUDO[@]}" udevadm control --reload-rules && "${SUDO[@]}" udevadm trigger || true + fi fi } From d643860afa2be78559e9681c0f89094891aa7ec3 Mon Sep 17 00:00:00 2001 From: stef <19478336+stefpi@users.noreply.github.com> Date: Thu, 17 Sep 2026 21:10:57 -0400 Subject: [PATCH 08/79] ui: increase info subtext size (#38946) increase to 36 --- openpilot/selfdrive/ui/mici/layouts/settings/device.py | 4 ++-- openpilot/selfdrive/ui/mici/layouts/settings/software.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openpilot/selfdrive/ui/mici/layouts/settings/device.py b/openpilot/selfdrive/ui/mici/layouts/settings/device.py index c038f5fddc..7ce13959d3 100644 --- a/openpilot/selfdrive/ui/mici/layouts/settings/device.py +++ b/openpilot/selfdrive/ui/mici/layouts/settings/device.py @@ -101,11 +101,11 @@ class DeviceInfoLayoutMici(Widget): subheader_color = rl.Color(255, 255, 255, int(255 * 0.9 * 0.65)) max_width = int(self._rect.width - 20) self._dongle_id_label = UnifiedLabel("device ID", 48, max_width=max_width, font_weight=FontWeight.DISPLAY, wrap_text=False) - self._dongle_id_text_label = UnifiedLabel(params.get("DongleId") or 'N/A', 32, max_width=max_width, text_color=subheader_color, + self._dongle_id_text_label = UnifiedLabel(params.get("DongleId") or 'N/A', 36, max_width=max_width, text_color=subheader_color, font_weight=FontWeight.ROMAN, wrap_text=False) self._serial_number_label = UnifiedLabel("serial", 48, max_width=max_width, font_weight=FontWeight.DISPLAY, wrap_text=False) - self._serial_number_text_label = UnifiedLabel(params.get("HardwareSerial") or 'N/A', 32, max_width=max_width, text_color=subheader_color, + self._serial_number_text_label = UnifiedLabel(params.get("HardwareSerial") or 'N/A', 36, max_width=max_width, text_color=subheader_color, font_weight=FontWeight.ROMAN, wrap_text=False) def _render(self, _): diff --git a/openpilot/selfdrive/ui/mici/layouts/settings/software.py b/openpilot/selfdrive/ui/mici/layouts/settings/software.py index 7bf17713a3..392c031f30 100644 --- a/openpilot/selfdrive/ui/mici/layouts/settings/software.py +++ b/openpilot/selfdrive/ui/mici/layouts/settings/software.py @@ -42,11 +42,11 @@ class SoftwareInfoLayoutMici(Widget): subheader_color = rl.Color(255, 255, 255, int(255 * 0.9 * 0.65)) max_width = int(self._rect.width - 20) self._version_label = UnifiedLabel("version", 48, max_width=max_width, font_weight=FontWeight.DISPLAY, wrap_text=False) - self._version_text_label = UnifiedLabel("", 32, max_width=max_width, text_color=subheader_color, + self._version_text_label = UnifiedLabel("", 36, max_width=max_width, text_color=subheader_color, font_weight=FontWeight.ROMAN, wrap_text=False) self._branch_label = UnifiedLabel("branch", 48, max_width=max_width, font_weight=FontWeight.DISPLAY, wrap_text=False) - self._branch_text_label = UnifiedLabel("", 32, max_width=max_width, text_color=subheader_color, + self._branch_text_label = UnifiedLabel("", 36, max_width=max_width, text_color=subheader_color, font_weight=FontWeight.ROMAN, wrap_text=False, scroll=True) def _update_state(self): From 7dcb524058f8a48df42bfdfd41b8f4edfd0fbb10 Mon Sep 17 00:00:00 2001 From: stef <19478336+stefpi@users.noreply.github.com> Date: Thu, 17 Sep 2026 22:02:45 -0400 Subject: [PATCH 09/79] ui: abstract info stack (#38949) * abstract info stack * better seperation in device py * clean --- .../ui/mici/layouts/settings/device.py | 34 ++------------ .../ui/mici/layouts/settings/software.py | 44 +++++-------------- openpilot/selfdrive/ui/mici/widgets/info.py | 31 +++++++++++++ 3 files changed, 45 insertions(+), 64 deletions(-) create mode 100644 openpilot/selfdrive/ui/mici/widgets/info.py diff --git a/openpilot/selfdrive/ui/mici/layouts/settings/device.py b/openpilot/selfdrive/ui/mici/layouts/settings/device.py index 7ce13959d3..c85e484376 100644 --- a/openpilot/selfdrive/ui/mici/layouts/settings/device.py +++ b/openpilot/selfdrive/ui/mici/layouts/settings/device.py @@ -7,16 +7,15 @@ from openpilot.common.basedir import BASEDIR from openpilot.common.params import Params from openpilot.common.time_helpers import system_time_valid from openpilot.system.ui.widgets.scroller import NavRawScrollPanel, NavScroller +from openpilot.selfdrive.ui.mici.widgets.info import InfoLayoutMici from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigCircleButton from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigConfirmationDialog from openpilot.selfdrive.ui.mici.widgets.pairing_dialog import PairingDialog from openpilot.selfdrive.ui.mici.onroad.cabin_camera_dialog import CabinCameraDialog from openpilot.selfdrive.ui.mici.layouts.onboarding import TrainingGuide, TermsPage -from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos +from openpilot.system.ui.lib.application import gui_app, MousePos from openpilot.system.ui.lib.multilang import tr -from openpilot.system.ui.widgets import Widget from openpilot.selfdrive.ui.ui_state import device, ui_state -from openpilot.system.ui.widgets.label import UnifiedLabel from openpilot.system.ui.widgets.html_render import HtmlRenderer from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID @@ -91,35 +90,10 @@ class EngagedConfirmationButton(BigButton): self.set_click_callback(lambda: _engaged_confirmation_click(callback, action_text, icon, exit_on_confirm=exit_on_confirm, red=red)) -class DeviceInfoLayoutMici(Widget): +class DeviceInfoLayoutMici(InfoLayoutMici): def __init__(self): - super().__init__() - - self.set_rect(rl.Rectangle(0, 0, 360, 180)) - params = Params() - subheader_color = rl.Color(255, 255, 255, int(255 * 0.9 * 0.65)) - max_width = int(self._rect.width - 20) - self._dongle_id_label = UnifiedLabel("device ID", 48, max_width=max_width, font_weight=FontWeight.DISPLAY, wrap_text=False) - self._dongle_id_text_label = UnifiedLabel(params.get("DongleId") or 'N/A', 36, max_width=max_width, text_color=subheader_color, - font_weight=FontWeight.ROMAN, wrap_text=False) - - self._serial_number_label = UnifiedLabel("serial", 48, max_width=max_width, font_weight=FontWeight.DISPLAY, wrap_text=False) - self._serial_number_text_label = UnifiedLabel(params.get("HardwareSerial") or 'N/A', 36, max_width=max_width, text_color=subheader_color, - font_weight=FontWeight.ROMAN, wrap_text=False) - - def _render(self, _): - self._dongle_id_label.set_position(self._rect.x + 20, self._rect.y - 10) - self._dongle_id_label.render() - - self._dongle_id_text_label.set_position(self._rect.x + 20, self._rect.y + 68 - 25) - self._dongle_id_text_label.render() - - self._serial_number_label.set_position(self._rect.x + 20, self._rect.y + 114 - 30) - self._serial_number_label.render() - - self._serial_number_text_label.set_position(self._rect.x + 20, self._rect.y + 161 - 25) - self._serial_number_text_label.render() + super().__init__("device ID", params.get("DongleId") or 'N/A', "serial", params.get("HardwareSerial") or 'N/A', width=380) class PairBigButton(BigButton): diff --git a/openpilot/selfdrive/ui/mici/layouts/settings/software.py b/openpilot/selfdrive/ui/mici/layouts/settings/software.py index 392c031f30..0c86d8e884 100644 --- a/openpilot/selfdrive/ui/mici/layouts/settings/software.py +++ b/openpilot/selfdrive/ui/mici/layouts/settings/software.py @@ -6,13 +6,12 @@ from collections.abc import Callable from openpilot.common.time_helpers import system_time_valid from openpilot.selfdrive.ui.mici.layouts.settings.device import EngagedConfirmationButton +from openpilot.selfdrive.ui.mici.widgets.info import InfoLayoutMici from openpilot.selfdrive.ui.mici.widgets.button import BigButton from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog from openpilot.selfdrive.ui.ui_state import ui_state -from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos +from openpilot.system.ui.lib.application import gui_app, MousePos from openpilot.system.ui.lib.multilang import tr -from openpilot.system.ui.widgets import Widget -from openpilot.system.ui.widgets.label import UnifiedLabel from openpilot.system.ui.widgets.scroller import NavScroller UPDATER_TIMEOUT = 10.0 # seconds to wait for updater to respond @@ -33,44 +32,21 @@ class UpdaterState(IntEnum): UPDATER_RESPONDING = 2 -class SoftwareInfoLayoutMici(Widget): +class SoftwareInfoLayoutMici(InfoLayoutMici): def __init__(self): - super().__init__() - - self.set_rect(rl.Rectangle(0, 0, 360, 180)) - - subheader_color = rl.Color(255, 255, 255, int(255 * 0.9 * 0.65)) - max_width = int(self._rect.width - 20) - self._version_label = UnifiedLabel("version", 48, max_width=max_width, font_weight=FontWeight.DISPLAY, wrap_text=False) - self._version_text_label = UnifiedLabel("", 36, max_width=max_width, text_color=subheader_color, - font_weight=FontWeight.ROMAN, wrap_text=False) - - self._branch_label = UnifiedLabel("branch", 48, max_width=max_width, font_weight=FontWeight.DISPLAY, wrap_text=False) - self._branch_text_label = UnifiedLabel("", 36, max_width=max_width, text_color=subheader_color, - font_weight=FontWeight.ROMAN, wrap_text=False, scroll=True) + super().__init__("version", "", "branch", "") def _update_state(self): + super()._update_state() + desc = _split_description(ui_state.params.get("UpdaterCurrentDescription") or "") if desc is not None: version, branch, commit, date = desc - self._version_text_label.set_text(f"{version} ({date})") - self._branch_text_label.set_text(f"{branch} ({commit})") + self.subtext1.set_text(f"{version} ({date})") + self.subtext2.set_text(f"{branch} ({commit})") else: - self._version_text_label.set_text(ui_state.params.get("Version") or "N/A") - self._branch_text_label.set_text(ui_state.params.get("GitBranch") or "N/A") - - def _render(self, _): - self._version_label.set_position(self._rect.x + 20, self._rect.y - 10) - self._version_label.render() - - self._version_text_label.set_position(self._rect.x + 20, self._rect.y + 68 - 25) - self._version_text_label.render() - - self._branch_label.set_position(self._rect.x + 20, self._rect.y + 114 - 30) - self._branch_label.render() - - self._branch_text_label.set_position(self._rect.x + 20, self._rect.y + 161 - 25) - self._branch_text_label.render() + self.subtext1.set_text(ui_state.params.get("Version") or "N/A") + self.subtext2.set_text(ui_state.params.get("GitBranch") or "N/A") class CheckUpdateButton(BigButton): diff --git a/openpilot/selfdrive/ui/mici/widgets/info.py b/openpilot/selfdrive/ui/mici/widgets/info.py new file mode 100644 index 0000000000..1b911d7a90 --- /dev/null +++ b/openpilot/selfdrive/ui/mici/widgets/info.py @@ -0,0 +1,31 @@ +import pyray as rl + +from openpilot.system.ui.lib.application import FontWeight +from openpilot.system.ui.widgets import Widget +from openpilot.system.ui.widgets.label import UnifiedLabel + + +class InfoLayoutMici(Widget): + def __init__(self, title1: str, subtext1: str, title2: str, subtext2: str, *, width: int = 360): + super().__init__() + self.set_rect(rl.Rectangle(0, 0, width, 180)) + + subheader_color = rl.Color(255, 255, 255, int(255 * 0.9 * 0.65)) + max_width = int(self._rect.width - 20) + + self.title1 = UnifiedLabel(title1, 48, max_width=max_width, font_weight=FontWeight.DISPLAY, wrap_text=False) + self.subtext1 = UnifiedLabel(subtext1, 36, max_width=max_width, text_color=subheader_color, + font_weight=FontWeight.ROMAN, wrap_text=False, scroll=True) + self.title2 = UnifiedLabel(title2, 48, max_width=max_width, font_weight=FontWeight.DISPLAY, wrap_text=False) + self.subtext2 = UnifiedLabel(subtext2, 36, max_width=max_width, text_color=subheader_color, + font_weight=FontWeight.ROMAN, wrap_text=False, scroll=True) + + def _render(self, _): + self.title1.set_position(self._rect.x + 20, self._rect.y - 10) + self.title1.render() + self.subtext1.set_position(self._rect.x + 20, self._rect.y + 68 - 25) + self.subtext1.render() + self.title2.set_position(self._rect.x + 20, self._rect.y + 114 - 30) + self.title2.render() + self.subtext2.set_position(self._rect.x + 20, self._rect.y + 161 - 25) + self.subtext2.render() From 3cd1b6e48347a57eec6e0364af46fba4a2362381 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Harald=20Sch=C3=A4fer?= Date: Thu, 17 Sep 2026 20:43:51 -0700 Subject: [PATCH 10/79] Update tinygrad and use retargetable model artifacts (#38933) * Update tinygrad to latest upstream with minimal openpilot patches * Rebase tinygrad on latest upstream with one remaining patch * Use retargetable model and warp artifacts * Pack compiled model weights to reduce startup time * Memory-map out-of-band model weights * Use unmodified upstream tinygrad master * Keep locally built model artifacts native --- .gitmodules | 4 ++-- openpilot/selfdrive/modeld/SConscript | 2 +- openpilot/selfdrive/modeld/helpers.py | 18 +++++++++++------- openpilot/selfdrive/modeld/modeld.py | 2 ++ .../modeld/models/big_driving_tinygrad.pkl | 4 ++-- tinygrad_repo | 2 +- 6 files changed, 19 insertions(+), 13 deletions(-) diff --git a/.gitmodules b/.gitmodules index 15ab89d49c..a23c39fb03 100644 --- a/.gitmodules +++ b/.gitmodules @@ -15,5 +15,5 @@ url = ../../commaai/teleoprtc [submodule "tinygrad"] path = tinygrad_repo - url = https://github.com/commaai/tinygrad.git - branch = model-warp-compile + url = https://github.com/tinygrad/tinygrad.git + branch = master diff --git a/openpilot/selfdrive/modeld/SConscript b/openpilot/selfdrive/modeld/SConscript index b6274f5cc5..3d1bae9b44 100644 --- a/openpilot/selfdrive/modeld/SConscript +++ b/openpilot/selfdrive/modeld/SConscript @@ -55,7 +55,7 @@ def chestnut_action(command): def compile_model(onnx_path, pkl_path): onnx_path, target_pkl_path = File(onnx_path).abspath, File(pkl_path).abspath cmd = (f'{tg_flags} {mac_brew_string} {taskset}python3 "{compiler}/compile_onnx.py" ' - f'"{onnx_path}" "{target_pkl_path}" --device-input "*" --out-of-band --benchmark-runs 1') + f'"{onnx_path}" "{target_pkl_path}" --out-of-band --benchmark-runs 1') lenv.Command( target_pkl_path, tinygrad_files + [onnx_path, Value(cmd)], diff --git a/openpilot/selfdrive/modeld/helpers.py b/openpilot/selfdrive/modeld/helpers.py index 8b376af380..bc5bc9e890 100644 --- a/openpilot/selfdrive/modeld/helpers.py +++ b/openpilot/selfdrive/modeld/helpers.py @@ -1,4 +1,4 @@ -import io +import mmap import pickle import struct from pathlib import Path @@ -13,14 +13,18 @@ def modeld_pkl_path(chestnut: bool): return MODELS_DIR / f'{prefix}driving_tinygrad.pkl' def load_oob(f): - opcodes = f.read(struct.unpack(' len(data): raise EOFError("incomplete model buffer") - yield pb - return pickle.load(io.BytesIO(opcodes), buffers=buffers()) + yield pickle.PickleBuffer(data[offset:offset + size]) + offset += size + return pickle.loads(data[8:8 + opcode_size], buffers=buffers()) def chestnut_present() -> bool: for d in USB_DEVICES_PATH.glob("*"): diff --git a/openpilot/selfdrive/modeld/modeld.py b/openpilot/selfdrive/modeld/modeld.py index e187ae0ce2..72e42b0307 100755 --- a/openpilot/selfdrive/modeld/modeld.py +++ b/openpilot/selfdrive/modeld/modeld.py @@ -7,6 +7,7 @@ import os os.environ['GMMU'] = '0' # for chestnut fast loading, noop for qcom from tinygrad.device import Buffer, Device from tinygrad.dtype import DType, dtypes +from tinygrad.engine.realize import lower_and_compile from tinygrad.tensor import Tensor from tinygrad.helpers import round_up from tinygrad.uop.ops import UOp @@ -155,6 +156,7 @@ class ModelState: with open(MODELS_DIR / f'{"big_" if chestnut else ""}driving_warp_{cam_w}x{cam_h}_tinygrad.pkl', 'rb') as f: self.run_warp = pickle.load(f)['run'] self.run_model = jits['run'] + self.run_model.captured._linear = lower_and_compile(self.run_model.captured._linear) self.outputs = {name: Tensor(np.zeros(shape, dtype=dtype), device=device).realize() for name, (shape, dtype, device) in jits['output_specs'].items()} for name, next_name in self.state_pairs.items(): state = self.input_queues[name] diff --git a/openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl b/openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl index 9a99ee2633..3f479d5e42 100644 --- a/openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl +++ b/openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:76cc0a9bc3af7a318889483dcbe126337f8d338f5abcbe664a8988c9b18b6639 -size 776634338 +oid sha256:e00a53cb9c64fe70a58224bc7879e5ea1b7e18baa7710f0929bf4f2e4537a468 +size 776612574 diff --git a/tinygrad_repo b/tinygrad_repo index d5e17c935d..9fc9e60cb4 160000 --- a/tinygrad_repo +++ b/tinygrad_repo @@ -1 +1 @@ -Subproject commit d5e17c935daf11f6318e45aade9528f71b8fbdcc +Subproject commit 9fc9e60cb4126b36700269c44081fec0fad62655 From 7635dec6182705b9516b400521eac447f3eb3fed Mon Sep 17 00:00:00 2001 From: stef <19478336+stefpi@users.noreply.github.com> Date: Fri, 18 Sep 2026 00:53:39 -0400 Subject: [PATCH 11/79] ui: qr widget (#38951) * rewrite pairing dialog into nice qr widget * simplify, move refresh to device pairing dialog * fix * refactor --- .../settings/device/green_settings.png | 3 + .../selfdrive/ui/mici/layouts/onboarding.py | 25 +---- .../ui/mici/layouts/settings/device.py | 54 ++++++++++- .../ui/mici/tests/test_widget_leaks.py | 2 +- .../ui/mici/widgets/pairing_dialog.py | 95 ------------------- openpilot/selfdrive/ui/mici/widgets/qr.py | 37 ++++++++ 6 files changed, 95 insertions(+), 121 deletions(-) create mode 100644 openpilot/selfdrive/assets/icons_mici/settings/device/green_settings.png delete mode 100644 openpilot/selfdrive/ui/mici/widgets/pairing_dialog.py create mode 100644 openpilot/selfdrive/ui/mici/widgets/qr.py diff --git a/openpilot/selfdrive/assets/icons_mici/settings/device/green_settings.png b/openpilot/selfdrive/assets/icons_mici/settings/device/green_settings.png new file mode 100644 index 0000000000..230beb85dd --- /dev/null +++ b/openpilot/selfdrive/assets/icons_mici/settings/device/green_settings.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:25db08a9038b5faa3b200ef5625cb216d7f4b31ea3d49fb0395f9b726bfaf8e6 +size 13135 diff --git a/openpilot/selfdrive/ui/mici/layouts/onboarding.py b/openpilot/selfdrive/ui/mici/layouts/onboarding.py index 0590440c6b..7cfba8f541 100644 --- a/openpilot/selfdrive/ui/mici/layouts/onboarding.py +++ b/openpilot/selfdrive/ui/mici/layouts/onboarding.py @@ -3,7 +3,7 @@ import numpy as np import pyray as rl from collections.abc import Callable from openpilot.common.filter_simple import FirstOrderFilter -from openpilot.common.qrcode import make_texture +from openpilot.selfdrive.ui.mici.widgets.qr import QR from openpilot.system.ui.lib.application import FontWeight, gui_app, TextAlignment from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.button import SmallCircleIconButton @@ -275,27 +275,6 @@ class TrainingGuide(NavWidget): self._steps[0].render(self._rect) -class QRCodeWidget(Widget): - def __init__(self, url: str, size: int = 170): - super().__init__() - self.set_rect(rl.Rectangle(0, 0, size, size)) - self._size = size - self._qr_texture: rl.Texture | None = None - self._generate_qr(url) - - def _generate_qr(self, url: str): - self._qr_texture = make_texture(url, inverted=True) - - def _render(self, _): - if self._qr_texture: - scale = self._size / self._qr_texture.height - rl.draw_texture_ex(self._qr_texture, rl.Vector2(round(self._rect.x), round(self._rect.y)), 0.0, scale, rl.WHITE) - - def __del__(self): - if self._qr_texture and self._qr_texture.id != 0: - rl.unload_texture(self._qr_texture) - - class TermsPage(Scroller): def __init__(self, on_accept, on_decline): super().__init__() @@ -312,7 +291,7 @@ class TermsPage(Scroller): self._terms_header, GreyBigButton("swipe for QR code", "or go to https://comma.ai/terms", gui_app.texture("icons_mici/setup/small_slider/slider_arrow.png", 64, 56, flip_x=True)), - QRCodeWidget("https://comma.ai/terms"), + QR("https://comma.ai/terms"), self._must_accept_card, self._accept_button, self._decline_button, diff --git a/openpilot/selfdrive/ui/mici/layouts/settings/device.py b/openpilot/selfdrive/ui/mici/layouts/settings/device.py index c85e484376..0a665f7a2d 100644 --- a/openpilot/selfdrive/ui/mici/layouts/settings/device.py +++ b/openpilot/selfdrive/ui/mici/layouts/settings/device.py @@ -1,16 +1,19 @@ import os import pyray as rl +import time from collections.abc import Callable from typing import Union +from openpilot.common.api import Api +from openpilot.common.swaglog import cloudlog from openpilot.common.basedir import BASEDIR from openpilot.common.params import Params from openpilot.common.time_helpers import system_time_valid from openpilot.system.ui.widgets.scroller import NavRawScrollPanel, NavScroller from openpilot.selfdrive.ui.mici.widgets.info import InfoLayoutMici -from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigCircleButton +from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigCircleButton, GreyBigButton from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigConfirmationDialog -from openpilot.selfdrive.ui.mici.widgets.pairing_dialog import PairingDialog +from openpilot.selfdrive.ui.mici.widgets.qr import QR from openpilot.selfdrive.ui.mici.onroad.cabin_camera_dialog import CabinCameraDialog from openpilot.selfdrive.ui.mici.layouts.onboarding import TrainingGuide, TermsPage from openpilot.system.ui.lib.application import gui_app, MousePos @@ -96,6 +99,53 @@ class DeviceInfoLayoutMici(InfoLayoutMici): super().__init__("device ID", params.get("DongleId") or 'N/A', "serial", params.get("HardwareSerial") or 'N/A', width=380) +class PairingDialog(NavScroller): + """Pair the device with comma connect.""" + + QR_REFRESH_INTERVAL = 300 # 5 minutes in seconds + + def __init__(self): + super().__init__() + self._params = Params() + self._last_pairing_qr_generation = float("-inf") + self._qr = QR(self._get_pairing_url()) + self._scroller._show_scroll_indicator = False + self._scroller.add_widgets([ + self._qr, + GreyBigButton("finish setup", "scan to pair device\nwith connect", + gui_app.texture("icons_mici/settings/device/green_settings.png", 64, 64)), + GreyBigButton("", "connect lets you review recent driving footage and bookmark events."), + ]) + + def _get_pairing_url(self) -> str: + try: + dongle_id = self._params.get("DongleId") or "" + token = Api(dongle_id).get_token({'pair': True}) + except Exception as e: + cloudlog.warning(f"Failed to get pairing token: {e}") + token = "" + return f"https://connect.comma.ai/?pair={token}" + + def _update_layout_rects(self): + super()._update_layout_rects() + self._qr.set_rect(rl.Rectangle(self._qr.rect.x, self._qr.rect.y, self._rect.height, self._rect.height)) + + def _render(self, rect: rl.Rectangle): + current_time = time.monotonic() + if current_time - self._last_pairing_qr_generation >= self.QR_REFRESH_INTERVAL: + self._qr._url = self._get_pairing_url() + if self._qr._texture and self._qr._texture.id != 0: + rl.unload_texture(self._qr._texture) + self._qr._texture = self._qr._generate_qr_code() + self._last_pairing_qr_generation = current_time + super()._render(rect) + + def _update_state(self): + super()._update_state() + if ui_state.prime_state.is_paired() and not self.is_dismissing: + self.dismiss() + + class PairBigButton(BigButton): def __init__(self): super().__init__("pair", "connect.comma.ai", gui_app.texture("icons_mici/settings/comma_icon.png", 33, 60)) diff --git a/openpilot/selfdrive/ui/mici/tests/test_widget_leaks.py b/openpilot/selfdrive/ui/mici/tests/test_widget_leaks.py index c0f1ac1510..a329a00e6b 100755 --- a/openpilot/selfdrive/ui/mici/tests/test_widget_leaks.py +++ b/openpilot/selfdrive/ui/mici/tests/test_widget_leaks.py @@ -52,7 +52,7 @@ class TestWidgetLeaks(OpenpilotTestCase): # mici dialogs from openpilot.selfdrive.ui.mici.layouts.onboarding import TrainingGuide as MiciTrainingGuide, OnboardingWindow as MiciOnboardingWindow from openpilot.selfdrive.ui.mici.onroad.cabin_camera_dialog import CabinCameraDialog as MiciCabinCameraDialog - from openpilot.selfdrive.ui.mici.widgets.pairing_dialog import PairingDialog as MiciPairingDialog + from openpilot.selfdrive.ui.mici.layouts.settings.device import PairingDialog as MiciPairingDialog from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigConfirmationDialog, BigInputDialog from openpilot.selfdrive.ui.mici.layouts.settings.device import MiciFccModal diff --git a/openpilot/selfdrive/ui/mici/widgets/pairing_dialog.py b/openpilot/selfdrive/ui/mici/widgets/pairing_dialog.py deleted file mode 100644 index 7e05bc2fa0..0000000000 --- a/openpilot/selfdrive/ui/mici/widgets/pairing_dialog.py +++ /dev/null @@ -1,95 +0,0 @@ -import pyray as rl -import time - -from openpilot.common.api import Api -from openpilot.common.qrcode import make_texture -from openpilot.common.swaglog import cloudlog -from openpilot.common.params import Params -from openpilot.selfdrive.ui.ui_state import ui_state -from openpilot.system.ui.widgets.nav_widget import NavWidget -from openpilot.system.ui.lib.application import FontWeight, gui_app -from openpilot.system.ui.widgets.label import UnifiedLabel - - -class PairingDialog(NavWidget): - """Dialog for device pairing with QR code.""" - - QR_REFRESH_INTERVAL = 300 # 5 minutes in seconds - - def __init__(self): - super().__init__() - self._params = Params() - self._qr_texture: rl.Texture | None = None - self._last_qr_generation = float("-inf") - - self._txt_pair = gui_app.texture("icons_mici/settings/device/pair.png", 33, 60) - self._pair_label = UnifiedLabel("pair with comma connect", font_size=48, font_weight=FontWeight.BOLD, line_height=0.8) - - def _get_pairing_url(self) -> str: - try: - dongle_id = self._params.get("DongleId") or "" - token = Api(dongle_id).get_token({'pair': True}) - except Exception as e: - cloudlog.warning(f"Failed to get pairing token: {e}") - token = "" - return f"https://connect.comma.ai/?pair={token}" - - def _generate_qr_code(self) -> None: - try: - if self._qr_texture and self._qr_texture.id != 0: - rl.unload_texture(self._qr_texture) - self._qr_texture = make_texture(self._get_pairing_url(), inverted=True) - except Exception as e: - cloudlog.warning(f"QR code generation failed: {e}") - self._qr_texture = None - - def _check_qr_refresh(self) -> None: - current_time = time.monotonic() - if current_time - self._last_qr_generation >= self.QR_REFRESH_INTERVAL: - self._generate_qr_code() - self._last_qr_generation = current_time - - def _update_state(self): - super()._update_state() - if ui_state.prime_state.is_paired() and not self.is_dismissing: - self.dismiss() - - def _render(self, rect: rl.Rectangle): - self._check_qr_refresh() - - self._render_qr_code() - - label_x = self._rect.x + 8 + self._rect.height + 24 - self._pair_label.set_max_width(int(self._rect.width - label_x)) - self._pair_label.set_position(label_x, self._rect.y + 16) - self._pair_label.render() - - rl.draw_texture_ex(self._txt_pair, rl.Vector2(label_x, self._rect.y + self._rect.height - self._txt_pair.height - 16), - 0.0, 1.0, rl.Color(255, 255, 255, int(255 * 0.35))) - - def _render_qr_code(self) -> None: - if not self._qr_texture: - error_font = gui_app.font(FontWeight.BOLD) - rl.draw_text_ex( - error_font, "QR Code Error", rl.Vector2(self._rect.x + 20, self._rect.y + self._rect.height // 2 - 15), 30, 0.0, rl.RED - ) - return - - scale = self._rect.height / self._qr_texture.height - pos = rl.Vector2(round(self._rect.x + 8), round(self._rect.y)) - rl.draw_texture_ex(self._qr_texture, pos, 0.0, scale, rl.WHITE) - - def __del__(self): - if self._qr_texture and self._qr_texture.id != 0: - rl.unload_texture(self._qr_texture) - - -if __name__ == "__main__": - gui_app.init_window("pairing device") - pairing = PairingDialog() - gui_app.push_widget(pairing) - try: - for _ in gui_app.render(): - pass - finally: - del pairing diff --git a/openpilot/selfdrive/ui/mici/widgets/qr.py b/openpilot/selfdrive/ui/mici/widgets/qr.py new file mode 100644 index 0000000000..f0ff0d0ed9 --- /dev/null +++ b/openpilot/selfdrive/ui/mici/widgets/qr.py @@ -0,0 +1,37 @@ +import pyray as rl + +from openpilot.common.swaglog import cloudlog +from openpilot.system.ui.lib.application import FontWeight +from openpilot.system.ui.widgets import Widget +from openpilot.system.ui.widgets.label import Label +from openpilot.common.qrcode import make_texture + + +class QR(Widget): + def __init__(self, url: str, width: int = 170): + super().__init__() + self._url = url + + self._texture = self._generate_qr_code() + + self.set_rect(rl.Rectangle(0, 0, width, width)) + self._error = Label("QR Code Error", font_size=30, font_weight=FontWeight.BOLD, text_color=rl.RED) + + def _generate_qr_code(self): + try: + return make_texture(self._url, inverted=True) + except Exception as e: + cloudlog.warning(f"QR code generation failed: {e}") + return None + + def _render(self, rect: rl.Rectangle): + if not self._texture: + self._error.render(rect) + return + scale = rect.height / self._texture.height + pos = rl.Vector2(round(rect.x), round(rect.y)) + rl.draw_texture_ex(self._texture, pos, 0.0, scale, rl.WHITE) + + def __del__(self): + if self._texture and self._texture.id != 0: + rl.unload_texture(self._texture) From bca3b047203dd7923c359ccebba4d025638be39f Mon Sep 17 00:00:00 2001 From: Trey Moen <50057480+greatgitsby@users.noreply.github.com> Date: Thu, 17 Sep 2026 21:56:31 -0700 Subject: [PATCH 12/79] cabana: improve dark theme contrast (#38957) --- openpilot/tools/cabana/ui/theme.cc | 24 ++++++++++++------------ openpilot/tools/cabana/ui/theme.h | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/openpilot/tools/cabana/ui/theme.cc b/openpilot/tools/cabana/ui/theme.cc index 14d0949835..6a544ef05a 100644 --- a/openpilot/tools/cabana/ui/theme.cc +++ b/openpilot/tools/cabana/ui/theme.cc @@ -12,16 +12,16 @@ namespace fs = std::filesystem; namespace { constexpr Palette DARK_PALETTE = { - .text = rgb(0xbbbbbb), .text_disabled = rgb(0x777777), .text_selected = rgb(0xbbbbbb), - .window = rgb(0x353535), .surface = rgb(0x3c3f41), - .frame = rgb(0x3c3f41), .frame_hovered = rgb(0x484b4d), .frame_active = rgb(0x505355), - .button = rgb(0x484b4d), .button_hovered = rgb(0x535658), .button_active = rgb(0x3c3f41), - .header = rgb(0x2f65ca), .header_hovered = rgb(0x414e65), .header_active = rgb(0x2f65ca), - .accent = rgb(0x2f65ca), - .border = rgb(0x282828), .separator = rgb(0x353535), .scrollbar_grab = rgb(0x484b4d), - .slider_track = rgb(0x777777), - .tab = rgb(0x353535), .tab_hovered = rgb(0x484b4d), .table_header = rgb(0x484b4d), - .grid = rgb(0xbbbbbb, 50.0f / 255.0f), .badge = rgb(0x808080), + .text = rgb(0xdddddd), .text_disabled = rgb(0x7b7b7b), .text_selected = rgb(0xdddddd), + .window = rgb(0x353535), .surface = rgb(0x2b2c2d), + .frame = rgb(0x2b2c2d), .frame_hovered = rgb(0x363738), .frame_active = rgb(0x434445), + .button = rgb(0x3f4041), .button_hovered = rgb(0x525354), .button_active = rgb(0x303132), + .header = rgb(0x2d74d2), .header_hovered = rgb(0x323944), .header_active = rgb(0x2d74d2), + .accent = rgb(0x2d74d2), + .border = rgb(0x606060), .separator = rgb(0x353535), .scrollbar_grab = rgb(0x4c4e4f), + .slider_track = rgb(0x7b7b7b), + .tab = rgb(0x2c2c2c), .tab_hovered = rgb(0x444647), .tab_selected = rgb(0x393a3b), .table_header = rgb(0x3f4041), + .grid = rgb(0xdddddd, 50.0f / 255.0f), .badge = rgb(0x808080), .bit_background = rgb(0xffffff, 20.0f / 255.0f), .heatmap_signal_alpha = 70.0, .heatmap_bit_alpha = 28.0, .heatmap_gamma = 0.6, .sparkline_saturation = 1.0f, .sparkline_value = 1.0f, @@ -36,7 +36,7 @@ constexpr Palette LIGHT_PALETTE = { .accent = rgb(0x308cc6), .border = rgb(0xb6b6b6), .separator = rgb(0xd0d0d0), .scrollbar_grab = rgb(0xb6b6b6), .slider_track = rgb(0xd0d0d0), - .tab = rgb(0xe5e5e5), .tab_hovered = rgb(0xefefef), .table_header = rgb(0xefefef), + .tab = rgb(0xe5e5e5), .tab_hovered = rgb(0xefefef), .tab_selected = rgb(0xffffff), .table_header = rgb(0xefefef), .grid = rgb(0x000000, 50.0f / 255.0f), .badge = rgb(0xa0a0a4), .bit_background = rgb(0xffffff, 0.0f), .heatmap_signal_alpha = 25.0, .heatmap_bit_alpha = 10.0, .heatmap_gamma = 1.0, @@ -142,7 +142,7 @@ void applyTheme(int theme) { c[ImGuiCol_TextSelectedBg] = alpha(p.accent, 0.35f); c[ImGuiCol_Tab] = c[ImGuiCol_TabDimmed] = p.tab; c[ImGuiCol_TabHovered] = p.tab_hovered; - c[ImGuiCol_TabSelected] = c[ImGuiCol_TabDimmedSelected] = p.surface; + c[ImGuiCol_TabSelected] = c[ImGuiCol_TabDimmedSelected] = p.tab_selected; c[ImGuiCol_TabDimmedSelectedOverline] = none; c[ImGuiCol_TableHeaderBg] = p.table_header; c[ImGuiCol_TableRowBgAlt] = none; diff --git a/openpilot/tools/cabana/ui/theme.h b/openpilot/tools/cabana/ui/theme.h index 9c8653cfe9..e7346db6a6 100644 --- a/openpilot/tools/cabana/ui/theme.h +++ b/openpilot/tools/cabana/ui/theme.h @@ -15,7 +15,7 @@ struct Palette { ImVec4 accent; ImVec4 border, separator, scrollbar_grab; ImVec4 slider_track; - ImVec4 tab, tab_hovered, table_header; + ImVec4 tab, tab_hovered, tab_selected, table_header; ImVec4 grid; ImVec4 badge; // the fill behind the time labels drawn over a chart ImVec4 bit_background; // overlay beneath heatmap bits without a signal From 60b3ccf39ef13b4f2a54538fe58aaaba45698240 Mon Sep 17 00:00:00 2001 From: Trey Moen <50057480+greatgitsby@users.noreply.github.com> Date: Thu, 17 Sep 2026 21:57:48 -0700 Subject: [PATCH 13/79] cabana: accept prefixed hex values in id filters (#38952) --- openpilot/tools/cabana/ui/widgets/messageswidget.cc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpilot/tools/cabana/ui/widgets/messageswidget.cc b/openpilot/tools/cabana/ui/widgets/messageswidget.cc index a3cacea680..635d1ee5b3 100644 --- a/openpilot/tools/cabana/ui/widgets/messageswidget.cc +++ b/openpilot/tools/cabana/ui/widgets/messageswidget.cc @@ -23,11 +23,12 @@ namespace { const char *COLUMN_TITLES[MessageList::COLUMN_COUNT] = {"Name", "Bus", "ID", "Node", "Freq", "Count", "Bytes"}; constexpr float DEFAULT_SECTION_SIZE = 100.0f; -// surrounding whitespace is ignored; no sign, no 0x prefix +// surrounding whitespace is ignored; no sign, optional 0x prefix for hex unsigned int toUInt(const std::string &s, bool *ok, int base) { const char *b = s.data(), *e = b + s.size(); while (b < e && std::isspace((unsigned char)*b)) ++b; while (e > b && std::isspace((unsigned char)e[-1])) --e; + if (base == 16 && e - b >= 2 && b[0] == '0' && (b[1] == 'x' || b[1] == 'X')) b += 2; unsigned int v = 0; auto [p, ec] = std::from_chars(b, e, v, base); *ok = b < e && p == e && ec == std::errc(); From e77f6e56f9bbc1b16e14bac15a8cd376de049f9f Mon Sep 17 00:00:00 2001 From: Trey Moen <50057480+greatgitsby@users.noreply.github.com> Date: Thu, 17 Sep 2026 22:04:45 -0700 Subject: [PATCH 14/79] cabana: show cumulative byte and bit heatmaps (#38954) --- openpilot/tools/cabana/core/heatmap.h | 28 ++++++++++ openpilot/tools/cabana/tests/test_cabana.cc | 24 +++++++++ .../tools/cabana/ui/widgets/binaryview.cc | 53 ++++++++++--------- .../tools/cabana/ui/widgets/binaryview.h | 4 +- .../tools/cabana/ui/widgets/detailwidget.cc | 5 +- 5 files changed, 86 insertions(+), 28 deletions(-) create mode 100644 openpilot/tools/cabana/core/heatmap.h diff --git a/openpilot/tools/cabana/core/heatmap.h b/openpilot/tools/cabana/core/heatmap.h new file mode 100644 index 0000000000..04ef64e884 --- /dev/null +++ b/openpilot/tools/cabana/core/heatmap.h @@ -0,0 +1,28 @@ +#pragma once + +#include +#include +#include +#include + +// Count adjacent samples, never treating a newly appearing byte as a change. +struct HeatmapCounts { + HeatmapCounts() = default; + explicit HeatmapCounts(size_t size) : bits(size), bytes(size) {} + + void add(const uint8_t *data, size_t size) { + const size_t common = std::min({size, previous.size(), bits.size()}); + for (size_t i = 0; i < common; ++i) { + const uint8_t diff = data[i] ^ previous[i]; + if (diff) ++bytes[i]; + for (int bit = 0; bit < 8; ++bit) { + if (diff & (1u << bit)) ++bits[i][7 - bit]; + } + } + previous.assign(data, data + size); + } + + std::vector> bits; + std::vector bytes; + std::vector previous; +}; diff --git a/openpilot/tools/cabana/tests/test_cabana.cc b/openpilot/tools/cabana/tests/test_cabana.cc index 268aa9a86d..5f6b8da31f 100644 --- a/openpilot/tools/cabana/tests/test_cabana.cc +++ b/openpilot/tools/cabana/tests/test_cabana.cc @@ -11,6 +11,7 @@ #include "common/tests/native_test.h" #include "tools/cabana/dbc/dbcfile.h" +#include "tools/cabana/core/heatmap.h" #include "tools/cabana/dbc/dbcmanager.h" #include "tools/cabana/routes.h" #include "tools/cabana/ui/qtstate.h" @@ -413,7 +414,30 @@ void test_pixel_envelope() { REQUIRE(chart::pixelEnvelope(points.begin(), points.begin(), 0, 1, 10).empty()); } +void test_heatmap_counts() { + HeatmapCounts counts(3); + for (int value = 0; value < 256; ++value) { + const uint8_t data[] = {static_cast(value), 42, static_cast(value / 16)}; + counts.add(data, sizeof(data)); + } + REQUIRE(counts.bytes == std::vector({255, 0, 15})); + REQUIRE(counts.bits[0] == std::array({1, 3, 7, 15, 31, 63, 127, 255})); + REQUIRE(counts.bits[1] == std::array{}); + + // Every range starts with a baseline, without a transition from outside it. + HeatmapCounts range(3); + const uint8_t first[] = {255, 42, 15}; + range.add(first, 3); + REQUIRE(range.bytes == std::vector({0, 0, 0})); + const uint8_t short_data[] = {0}; + range.add(short_data, 1); + range.add(first, 3); + REQUIRE(range.bytes == std::vector({2, 0, 0})); + REQUIRE(HeatmapCounts(0).bits.empty()); +} + void test_cabana_core() { + test_heatmap_counts(); test_pixel_envelope(); test_format_seconds(); test_to_hex(); diff --git a/openpilot/tools/cabana/ui/widgets/binaryview.cc b/openpilot/tools/cabana/ui/widgets/binaryview.cc index 84f5a29fd8..0315279948 100644 --- a/openpilot/tools/cabana/ui/widgets/binaryview.cc +++ b/openpilot/tools/cabana/ui/widgets/binaryview.cc @@ -56,6 +56,13 @@ void fillBDiagPattern(ImDrawList *p, const ImRect &r, ImU32 col) { } // namespace BinaryView::BinaryView() { + heatmap_live_mode_ = can->liveStreaming(); + connections_.push_back(can->eventsMerged.connect([this](const MessageEventsMap &events) { + if (events.count(msg_id_)) { + bit_flip_tracker_.valid = false; + if (!heatmap_live_mode_) updateState(); + } + })); connections_.push_back(dbc()->fileChanged.connect([this]() { refresh(); })); connections_.push_back(UndoStack::instance()->indexChanged.connect([this]() { refresh(); })); } @@ -63,6 +70,8 @@ BinaryView::BinaryView() { std::string BinaryView::whatsThis() const { return R"( Binary View
+ All / selected range: brighter bits and bytes changed more often; uncolored cells did not change.
+ Live: accumulated bit flips and fading byte changes during playback.
Shortcuts
Delete Signal:  x , @@ -359,6 +368,8 @@ void BinaryView::updateState() { cells_.resize(row_count_ * COLUMN_COUNT); } + for (auto &cell : cells_) cell.valid = false; + auto &bit_flips = heatmap_live_mode_ ? last_msg.bit_flip_counts : bitFlipChanges(binary.size()); uint32_t max_bit_flip_count = 1; // 1 to avoid division by zero for (const auto &row : bit_flips) { @@ -367,13 +378,16 @@ void BinaryView::updateState() { } } + uint32_t max_byte_flip_count = 1; + for (auto count : bit_flip_tracker_.counts.bytes) max_byte_flip_count = std::max(max_byte_flip_count, count); + const Palette &p = palette(); const double max_alpha = 255.0; const double min_alpha_with_signal = p.heatmap_signal_alpha; // Base alpha for small flip counts const double min_alpha_no_signal = p.heatmap_bit_alpha; // Base alpha for small flip counts for no signal bits const double alpha_gamma = p.heatmap_gamma; const double log_factor = 1.0 + 0.2; - const double log_scaler = max_alpha / log2(log_factor * max_bit_flip_count); + const double log_scaler = max_alpha / log2(1.0 + log_factor * max_bit_flip_count); for (size_t i = 0; i < binary.size(); ++i) { for (int j = 0; j < 8; ++j) { @@ -393,38 +407,27 @@ void BinaryView::updateState() { color.a = static_cast(alpha); setCell(i, j, bit_val, color); } - setCell(i, HEX_COLUMN, binary[i], last_msg.colors[i]); + auto byte_color = last_msg.colors[i]; + if (!heatmap_live_mode_) { + const auto count = bit_flip_tracker_.counts.bytes[i]; + const double intensity = std::log2(1.0 + count * log_factor) / std::log2(1.0 + max_byte_flip_count * log_factor); + byte_color = CabanaColor(102, 86, 169, static_cast(max_alpha * std::pow(intensity, alpha_gamma))); + } + setCell(i, HEX_COLUMN, binary[i], byte_color); } } const std::vector> &BinaryView::bitFlipChanges(size_t msg_size) { auto time_range = can->timeRange(); - if (bit_flip_tracker_.time_range == time_range && !bit_flip_tracker_.flip_counts.empty()) - return bit_flip_tracker_.flip_counts; + if (bit_flip_tracker_.valid && bit_flip_tracker_.time_range == time_range && + bit_flip_tracker_.counts.bits.size() == msg_size) return bit_flip_tracker_.counts.bits; bit_flip_tracker_.time_range = time_range; - bit_flip_tracker_.flip_counts.assign(msg_size, std::array{}); - + bit_flip_tracker_.counts = HeatmapCounts(msg_size); auto [first, last] = can->eventsInRange(msg_id_, time_range); - if (std::distance(first, last) <= 1) return bit_flip_tracker_.flip_counts; - - std::vector prev_values((*first)->dat, (*first)->dat + (*first)->size); - for (auto it = std::next(first); it != last; ++it) { - const CanEvent *event = *it; - int size = std::min(msg_size, event->size); - for (int i = 0; i < size; ++i) { - const uint8_t diff = event->dat[i] ^ prev_values[i]; - if (!diff) continue; - - auto &bit_flips = bit_flip_tracker_.flip_counts[i]; - for (int bit = 0; bit < 8; ++bit) { - if (diff & (1u << bit)) ++bit_flips[7 - bit]; - } - prev_values[i] = event->dat[i]; - } - } - - return bit_flip_tracker_.flip_counts; + for (auto it = first; it != last; ++it) bit_flip_tracker_.counts.add((*it)->dat, (*it)->size); + bit_flip_tracker_.valid = true; + return bit_flip_tracker_.counts.bits; } bool BinaryView::hasSignal(const BinaryIndex &index, int dx, int dy, const cabana::Signal *sig) const { diff --git a/openpilot/tools/cabana/ui/widgets/binaryview.h b/openpilot/tools/cabana/ui/widgets/binaryview.h index 1ca3c17bab..9e55b63fcd 100644 --- a/openpilot/tools/cabana/ui/widgets/binaryview.h +++ b/openpilot/tools/cabana/ui/widgets/binaryview.h @@ -9,6 +9,7 @@ #include "imgui.h" #include "imgui_internal.h" +#include "tools/cabana/core/heatmap.h" #include "tools/cabana/core/observable.h" #include "tools/cabana/dbc/dbcmanager.h" #include "tools/cabana/streams/abstractstream.h" @@ -81,7 +82,8 @@ private: bool heatmap_live_mode_ = true; struct BitFlipTracker { std::optional> time_range; - std::vector> flip_counts; + HeatmapCounts counts; + bool valid = false; } bit_flip_tracker_; BinaryIndex anchor_index_; diff --git a/openpilot/tools/cabana/ui/widgets/detailwidget.cc b/openpilot/tools/cabana/ui/widgets/detailwidget.cc index 830a51b889..28a7a5651e 100644 --- a/openpilot/tools/cabana/ui/widgets/detailwidget.cc +++ b/openpilot/tools/cabana/ui/widgets/detailwidget.cc @@ -48,6 +48,7 @@ DetailWidget::DetailWidget(ChartsWidget *charts) : charts_(charts) { })); connections_.push_back(tabbar_.tabCloseRequested.connect([this](int index) { tabbar_.removeTab(index); })); connections_.push_back(tabbar_.tabContextMenu.connect([this](int index) { showTabBarContextMenu(index); })); + heatmap_live_ = can->liveStreaming(); binary_view_ = std::make_unique(); signal_view_ = std::make_unique(charts); @@ -74,8 +75,8 @@ DetailWidget::DetailWidget(ChartsWidget *charts) : charts_(charts) { char text[64]; if (range) snprintf(text, sizeof(text), "%.3f - %.3f", range->first, range->second); heatmap_all_text_ = range ? text : "All"; - const bool live = !range; - if (std::exchange(heatmap_live_, live) != live) binary_view_->setHeatmapLiveMode(live); + heatmap_live_ = !range && can->liveStreaming(); + binary_view_->setHeatmapLiveMode(heatmap_live_); })); } From b55e37b9c92c3364f3cab37cebc89ebe09d5db8b Mon Sep 17 00:00:00 2001 From: Trey Moen <50057480+greatgitsby@users.noreply.github.com> Date: Thu, 17 Sep 2026 22:05:10 -0700 Subject: [PATCH 15/79] cabana: right-align numeric message values (#38959) --- openpilot/tools/cabana/ui/widgets/messagebytes.cc | 4 ++-- openpilot/tools/cabana/ui/widgets/messagebytes.h | 2 +- openpilot/tools/cabana/ui/widgets/messageswidget.cc | 4 +++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/openpilot/tools/cabana/ui/widgets/messagebytes.cc b/openpilot/tools/cabana/ui/widgets/messagebytes.cc index 63e31d6f83..442f5e598f 100644 --- a/openpilot/tools/cabana/ui/widgets/messagebytes.cc +++ b/openpilot/tools/cabana/ui/widgets/messagebytes.cc @@ -31,8 +31,8 @@ ImU32 cellTextColor(bool selected, bool inactive) { return ImGui::GetColorU32(inactive ? ImGuiCol_TextDisabled : ImGuiCol_Text); } -void drawTextCell(ImDrawList *dl, const ImRect &rect, const std::string &text, bool selected, bool inactive) { - drawElidedText(dl, rect, text, cellTextColor(selected, inactive)); +void drawTextCell(ImDrawList *dl, const ImRect &rect, const std::string &text, bool selected, bool inactive, bool align_right) { + drawElidedText(dl, rect, text, cellTextColor(selected, inactive), align_right); } void drawBytesCell(ImDrawList *dl, const ImRect &rect, const std::vector &bytes, const std::vector *colors, diff --git a/openpilot/tools/cabana/ui/widgets/messagebytes.h b/openpilot/tools/cabana/ui/widgets/messagebytes.h index 7c7aabc984..163a5805ae 100644 --- a/openpilot/tools/cabana/ui/widgets/messagebytes.h +++ b/openpilot/tools/cabana/ui/widgets/messagebytes.h @@ -15,6 +15,6 @@ ImVec2 byteCellSize(); // one "00 " cell ImVec2 bytesCellSize(int n, bool multiple_lines); // a cell of n bytes, the table's cell padding included ImU32 cellTextColor(bool selected, bool inactive); // inactive rows gray the text and fade the highlighted text -void drawTextCell(ImDrawList *dl, const ImRect &rect, const std::string &text, bool selected, bool inactive); +void drawTextCell(ImDrawList *dl, const ImRect &rect, const std::string &text, bool selected, bool inactive, bool align_right = false); void drawBytesCell(ImDrawList *dl, const ImRect &rect, const std::vector &bytes, const std::vector *colors, bool selected, bool inactive, bool multiple_lines); diff --git a/openpilot/tools/cabana/ui/widgets/messageswidget.cc b/openpilot/tools/cabana/ui/widgets/messageswidget.cc index 635d1ee5b3..e648113526 100644 --- a/openpilot/tools/cabana/ui/widgets/messageswidget.cc +++ b/openpilot/tools/cabana/ui/widgets/messageswidget.cc @@ -506,7 +506,9 @@ void MessagesWidget::drawRow(int row) { if (column == MessageList::DATA && seen) { drawBytesCell(ImGui::GetWindowDrawList(), rect, m.dat, &m.colors, selected, inactive, multiple_lines); } else { - drawTextCell(ImGui::GetWindowDrawList(), rect, cellText(item, column), selected, inactive); + const bool align_right = seen && (column == MessageList::SOURCE || column == MessageList::COUNT || + (column == MessageList::FREQ && m.freq > 0)); + drawTextCell(ImGui::GetWindowDrawList(), rect, cellText(item, column), selected, inactive, align_right); } // the Selectable already sized its cell if (!row_item) ImGui::Dummy(ImVec2(width, row_height)); From 1090e2824b1dd31876caba794e12363827426054 Mon Sep 17 00:00:00 2001 From: stef <19478336+stefpi@users.noreply.github.com> Date: Fri, 18 Sep 2026 01:06:22 -0400 Subject: [PATCH 16/79] ui: add pairing provider to prime state (#38950) * changes * clean and lock sensible changes * don't write to disk every loop * clean * clean diff * clean indents --- openpilot/common/params_keys.h | 2 + openpilot/selfdrive/ui/lib/prime_state.py | 91 +++++++++++++++++++++++ openpilot/system/ui/README.md | 2 + 3 files changed, 95 insertions(+) diff --git a/openpilot/common/params_keys.h b/openpilot/common/params_keys.h index 4b87356aca..b9f3ffeaa7 100644 --- a/openpilot/common/params_keys.h +++ b/openpilot/common/params_keys.h @@ -111,6 +111,8 @@ inline static std::unordered_map keys = { {"OnroadCycleRequested", {CLEAR_ON_MANAGER_START, BOOL}}, {"OpenpilotEnabledToggle", {PERSISTENT, BOOL, "1"}}, {"PandaHeartbeatLost", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}}, + {"PairingEmail", {PERSISTENT, STRING}}, + {"PairingProvider", {PERSISTENT, STRING}}, {"PrimeType", {PERSISTENT, INT}}, {"RecordAudio", {PERSISTENT, BOOL}}, {"RecordFront", {PERSISTENT, BOOL}}, diff --git a/openpilot/selfdrive/ui/lib/prime_state.py b/openpilot/selfdrive/ui/lib/prime_state.py index 1fa87e31df..df6f18671e 100644 --- a/openpilot/selfdrive/ui/lib/prime_state.py +++ b/openpilot/selfdrive/ui/lib/prime_state.py @@ -22,6 +22,10 @@ class PrimeType(IntEnum): MAGENTA_NEW = 4 PURPLE = 5 +class Provider(str): + GOOGLE = "google" + GITHUB = "github" + APPLE = "apple" class PrimeState: FETCH_INTERVAL = 5.0 # seconds between API calls @@ -33,6 +37,11 @@ class PrimeState: self._lock = threading.Lock() self._session = requests.Session() # reuse session to reduce SSL handshake overhead self.prime_type: PrimeType = self._load_initial_state() + self._prime_trial_available = False + pairing_provider = os.getenv("PAIRING_PROVIDER") or self._params.get("PairingProvider") + self._pairing_provider: Provider | None = Provider(pairing_provider) if pairing_provider is not None else None + self._pairing_email: str | None = self._params.get("PairingEmail") + self._commacare = False self._running = False self._thread = None @@ -56,12 +65,38 @@ class PrimeState: response = api_get(f"v1.1/devices/{dongle_id}", timeout=self.API_TIMEOUT, access_token=identity_token, session=self._session) if response.status_code == 200: data = response.json() + is_paired = data.get("is_paired", False) prime_type = data.get("prime_type", 0) self.set_type(PrimeType(prime_type) if is_paired else PrimeType.UNPAIRED) + if not is_paired: + self.set_provider(None, None) + + prime_trial_available = data.get("trial_claimed") is False and data.get("eligible_features", {}).get("prime", False) + self.set_prime_trial_available(prime_trial_available) + + commacare = data.get("commacare", False) + self.set_commacare(commacare) except Exception as e: cloudlog.error(f"Failed to fetch prime status: {e}") + def _fetch_pairing_provider(self) -> None: + dongle_id = self._params.get("DongleId") + if not dongle_id or dongle_id == UNREGISTERED_DONGLE_ID: + return + + try: + identity_token = get_token(dongle_id) + response = api_get(f"v1/devices/{dongle_id}/owner", timeout=self.API_TIMEOUT, access_token=identity_token, session=self._session) + if response.status_code == 200: + data = response.json() + user_id = data.get("user_id", "") + provider = Provider(user_id.partition("_")[0]) + email = data.get("email") + self.set_provider(provider, email) + except Exception as e: + cloudlog.error(f"Failed to fetch pairing provider: {e}") + def set_type(self, prime_type: PrimeType) -> None: with self._lock: if prime_type != self.prime_type: @@ -69,12 +104,48 @@ class PrimeState: self._params.put("PrimeType", int(prime_type)) cloudlog.info(f"Prime type updated to {prime_type}") + def set_provider(self, provider: Provider | None, email: str | None): + with self._lock: + if self.prime_type <= PrimeType.UNPAIRED: + provider = None + email = None + email = email or None # if data.get(email) returns "" instead of None + + if self._pairing_provider != provider: + self._pairing_provider = provider + if provider is None: + self._params.remove("PairingProvider") + else: + self._params.put("PairingProvider", str(provider)) + + if self._pairing_email != email: + self._pairing_email = email + if email is None: + self._params.remove("PairingEmail") + else: + self._params.put("PairingEmail", email) + + def set_commacare(self, has_commacare: bool): + with self._lock: + if self.prime_type <= PrimeType.UNPAIRED: + self._commacare = False + else: + self._commacare = has_commacare + + def set_prime_trial_available(self, prime_trail_available: bool): + with self._lock: + if self.prime_type <= PrimeType.UNPAIRED: + self._prime_trial_available = False + else: + self._prime_trial_available = prime_trail_available + def _worker_thread(self) -> None: drop_realtime() from openpilot.selfdrive.ui.ui_state import ui_state, device while self._running: if not ui_state.started and device._awake: self._fetch_prime_status() + self._fetch_pairing_provider() for _ in range(int(self.FETCH_INTERVAL / self.SLEEP_INTERVAL)): if not self._running: @@ -109,5 +180,25 @@ class PrimeState: with self._lock: return self.prime_type > PrimeType.UNPAIRED + def can_claim_prime_trial(self) -> bool: + with self._lock: + return self._prime_trial_available + + def has_commacare(self) -> bool: + with self._lock: + return self._commacare + + def get_pairing_provider(self) -> str | None: + with self._lock: + return self._pairing_provider + + def get_pairing_account(self) -> str: + with self._lock: + if not self._pairing_provider: + return "unknown" + elif self._pairing_provider == Provider.GITHUB or not self._pairing_email: + return f"{self._pairing_provider} account" + return self._pairing_email + def __del__(self): self.stop() diff --git a/openpilot/system/ui/README.md b/openpilot/system/ui/README.md index 3624c8d1ce..6d1502be5a 100644 --- a/openpilot/system/ui/README.md +++ b/openpilot/system/ui/README.md @@ -11,6 +11,8 @@ Quick start: * set `GRID=50` to show a 50-pixel alignment grid overlay * set `MAGIC_DEBUG=1` to show every dropped frames (only on device) * set `RECORD=1` to record the screen, output defaults to `output.mp4` but can be set with `RECORD_OUTPUT` +* set `PRIME_TYPE` to preview pairing/Prime status: `-1` unpaired, `0` paired without Prime, `2` Prime Lite, `1` Prime Full +* set `PAIRING_PROVIDER` to preview the different states based on connect auth providers `google`, `github` or `apple` * https://www.raylib.com/cheatsheet/cheatsheet.html * https://electronstudio.github.io/raylib-python-cffi/README.html#quickstart From c504b92abadf75f170c950514e71a4f0d3690b80 Mon Sep 17 00:00:00 2001 From: Trey Moen <50057480+greatgitsby@users.noreply.github.com> Date: Thu, 17 Sep 2026 22:07:01 -0700 Subject: [PATCH 17/79] cabana: clarify signal button states (#38953) * cabana: clarify plot button states * cabana: strengthen signal action hover states --- .../tools/cabana/ui/widgets/signalview.cc | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/openpilot/tools/cabana/ui/widgets/signalview.cc b/openpilot/tools/cabana/ui/widgets/signalview.cc index bc8b9c4d23..921d4b26ab 100644 --- a/openpilot/tools/cabana/ui/widgets/signalview.cc +++ b/openpilot/tools/cabana/ui/widgets/signalview.cc @@ -909,35 +909,36 @@ void SignalView::drawIndexWidget(SignalModel::Item *item, const ImRect &rect) { ImGui::SetCursorScreenPos(ImVec2(rect.Max.x - H_MARGIN - button * 2 - spacing, rect.Min.y + (rect.GetHeight() - button) * 0.5f)); - ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0)); const auto sig = item->sig; const bool checked = item->chart_opened; const bool selected = current_sig_ == sig && current_type_ == SignalModel::Item::Sig; - auto row_button = [selected](const char *id, const char *glyph) { - if (selected) { - ImGui::PushStyleColor(ImGuiCol_Text, palette().text_selected); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, palette().header_active); - ImGui::PushStyleColor(ImGuiCol_ButtonActive, palette().header_active); - } + auto row_button = [selected, button](const char *id, const char *glyph, bool plotted = false) { + const auto &p = palette(); + const ImVec2 button_min = ImGui::GetCursorScreenPos(); + const bool hovered = ImGui::IsWindowHovered() && + ImGui::IsMouseHoveringRect(button_min, ImVec2(button_min.x + button, button_min.y + button)); + // Both actions use neutral hover fills, including on selected rows. The plot keeps its outline. + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, plotted ? 1.0f : 0.0f); + ImGui::PushStyleColor(ImGuiCol_Button, plotted ? p.button : ImVec4(0, 0, 0, 0)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, settings.theme == LIGHT_THEME ? ImLerp(p.button, p.text, 0.12f) : p.button_hovered); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, settings.theme == LIGHT_THEME ? ImLerp(p.button, p.text, 0.20f) : p.button_active); + ImGui::PushStyleColor(ImGuiCol_Border, ImLerp(p.button, p.text, 0.45f)); + ImGui::PushStyleColor(ImGuiCol_Text, selected && !plotted && !hovered ? p.text_selected : p.text); const bool clicked = iconButton(id, glyph); - if (selected) ImGui::PopStyleColor(3); + ImGui::PopStyleColor(5); + ImGui::PopStyleVar(); return clicked; }; - if (checked) ImGui::PushStyleColor(ImGuiCol_Button, selected ? palette().header_active : ImGui::GetStyleColorVec4(ImGuiCol_ButtonActive)); - if (row_button("plot", icon::GRAPH_UP) && !editor_open_on_press_) { + if (row_button("plot", icon::GRAPH_UP, checked) && !editor_open_on_press_) { item->chart_opened = !checked; showChart(model_.msgId(), sig, item->chart_opened, ImGui::GetIO().KeyShift); } - if (checked) ImGui::PopStyleColor(); ImGui::SetItemTooltip("%s", checked ? "Close Plot" : "Show Plot\nShift-click to add to the previously opened plot"); ImGui::SameLine(0.0f, spacing); if (row_button("remove", icon::X_LG) && !editor_open_on_press_) { pending_action_ = [this, sig]() { UndoStack::instance()->push(new RemoveSigCommand(model_.msgId(), sig)); }; } ImGui::SetItemTooltip("Remove signal"); - ImGui::PopStyleColor(); - ImGui::PopStyleVar(); } ValueDescriptionDlg::ValueDescriptionDlg(const ValueDescription &descriptions) { From 6c69ebed7f435747dc81777d86d2427b6fc7c601 Mon Sep 17 00:00:00 2001 From: Trey Moen <50057480+greatgitsby@users.noreply.github.com> Date: Thu, 17 Sep 2026 22:09:33 -0700 Subject: [PATCH 18/79] cabana: improve heatmap readability (#38955) * cabana: improve heatmap readability * cabana: use shared colors for the heatmap * cabana: brighten dark theme text --- openpilot/tools/cabana/ui/theme.cc | 6 +--- openpilot/tools/cabana/ui/theme.h | 2 -- .../tools/cabana/ui/widgets/binaryview.cc | 31 ++++++++++--------- 3 files changed, 17 insertions(+), 22 deletions(-) diff --git a/openpilot/tools/cabana/ui/theme.cc b/openpilot/tools/cabana/ui/theme.cc index 6a544ef05a..45212cb6d7 100644 --- a/openpilot/tools/cabana/ui/theme.cc +++ b/openpilot/tools/cabana/ui/theme.cc @@ -12,7 +12,7 @@ namespace fs = std::filesystem; namespace { constexpr Palette DARK_PALETTE = { - .text = rgb(0xdddddd), .text_disabled = rgb(0x7b7b7b), .text_selected = rgb(0xdddddd), + .text = rgb(0xffffff), .text_disabled = rgb(0x7b7b7b), .text_selected = rgb(0xffffff), .window = rgb(0x353535), .surface = rgb(0x2b2c2d), .frame = rgb(0x2b2c2d), .frame_hovered = rgb(0x363738), .frame_active = rgb(0x434445), .button = rgb(0x3f4041), .button_hovered = rgb(0x525354), .button_active = rgb(0x303132), @@ -22,8 +22,6 @@ constexpr Palette DARK_PALETTE = { .slider_track = rgb(0x7b7b7b), .tab = rgb(0x2c2c2c), .tab_hovered = rgb(0x444647), .tab_selected = rgb(0x393a3b), .table_header = rgb(0x3f4041), .grid = rgb(0xdddddd, 50.0f / 255.0f), .badge = rgb(0x808080), - .bit_background = rgb(0xffffff, 20.0f / 255.0f), - .heatmap_signal_alpha = 70.0, .heatmap_bit_alpha = 28.0, .heatmap_gamma = 0.6, .sparkline_saturation = 1.0f, .sparkline_value = 1.0f, }; @@ -38,8 +36,6 @@ constexpr Palette LIGHT_PALETTE = { .slider_track = rgb(0xd0d0d0), .tab = rgb(0xe5e5e5), .tab_hovered = rgb(0xefefef), .tab_selected = rgb(0xffffff), .table_header = rgb(0xefefef), .grid = rgb(0x000000, 50.0f / 255.0f), .badge = rgb(0xa0a0a4), - .bit_background = rgb(0xffffff, 0.0f), - .heatmap_signal_alpha = 25.0, .heatmap_bit_alpha = 10.0, .heatmap_gamma = 1.0, .sparkline_saturation = 2.0f, .sparkline_value = 0.7f, }; diff --git a/openpilot/tools/cabana/ui/theme.h b/openpilot/tools/cabana/ui/theme.h index e7346db6a6..0c565b55c7 100644 --- a/openpilot/tools/cabana/ui/theme.h +++ b/openpilot/tools/cabana/ui/theme.h @@ -18,8 +18,6 @@ struct Palette { ImVec4 tab, tab_hovered, tab_selected, table_header; ImVec4 grid; ImVec4 badge; // the fill behind the time labels drawn over a chart - ImVec4 bit_background; // overlay beneath heatmap bits without a signal - double heatmap_signal_alpha, heatmap_bit_alpha, heatmap_gamma; float sparkline_saturation, sparkline_value; // HSV multipliers for signal colors }; diff --git a/openpilot/tools/cabana/ui/widgets/binaryview.cc b/openpilot/tools/cabana/ui/widgets/binaryview.cc index 0315279948..9cb1a6ad10 100644 --- a/openpilot/tools/cabana/ui/widgets/binaryview.cc +++ b/openpilot/tools/cabana/ui/widgets/binaryview.cc @@ -16,6 +16,7 @@ namespace { const int CELL_HEIGHT = 36; +const float CELL_FONT_SIZE = UI_FONT_SIZE + 2.0f; const float SMALL_FONT_SIZE = 10.0f; // Inter needs 10 px for a 7 px cap height const int VERTICAL_HEADER_WIDTH = 30; inline int get_bit_pos(const BinaryIndex &index) { return flipBitPos(index.row * 8 + index.column); } @@ -122,9 +123,9 @@ void BinaryView::addShortcuts() { } ImVec2 BinaryView::minimumSizeHint() const { - // widest fixed-font glyph plus the header margins - pushMonoFont(); - const float min_section_size = ImGui::CalcTextSize("W").x + 8.0f; + // Match the enlarged hex font when reserving space for narrow panels. + pushMonoFont(CELL_FONT_SIZE); + const float min_section_size = std::ceil(ImGui::CalcTextSize("FF").x) + 10.0f; popMonoFont(); return {(min_section_size + 1) * 9 + VERTICAL_HEADER_WIDTH + 2, static_cast(CELL_HEIGHT * std::min(row_count_, 10) + 2)}; @@ -306,17 +307,20 @@ void BinaryView::draw() { const int rows = row_count_; // Keep hex bytes readable in narrow panels by scrolling instead of shrinking further. + pushMonoFont(CELL_FONT_SIZE); const float min_column_width = std::ceil(ImGui::CalcTextSize("FF").x) + 10.0f; + popMonoFont(); const float width = std::max(ImGui::GetContentRegionAvail().x, VERTICAL_HEADER_WIDTH + min_column_width * COLUMN_COUNT); column_width_ = std::max(min_column_width, (width - VERTICAL_HEADER_WIDTH) / COLUMN_COUNT); grid_pos_ = ImGui::GetCursorScreenPos(); ImGui::InvisibleButton("##binary_view", ImVec2(std::max(width, 1.0f), std::max(static_cast(rows * CELL_HEIGHT), 1.0f))); ImDrawList *painter = ImGui::GetWindowDrawList(); + painter->AddRectFilled(grid_pos_, ImVec2(grid_pos_.x + width, grid_pos_.y + rows * CELL_HEIGHT), paletteBase()); for (int row = 0; row < rows; ++row) { const ImRect r(grid_pos_.x, grid_pos_.y + row * CELL_HEIGHT, grid_pos_.x + VERTICAL_HEADER_WIDTH, grid_pos_.y + (row + 1) * CELL_HEIGHT); painter->AddRectFilled(r.Min, r.Max, ImGui::GetColorU32(ImGuiCol_WindowBg)); // plain header background - drawText(painter, r, std::to_string(row).c_str(), ImGui::GetColorU32(ImGuiCol_Text)); + drawText(painter, r, std::to_string(row).c_str(), paletteText(true)); } for (int row = 0; row < rows; ++row) { for (int column = 0; column < COLUMN_COUNT; ++column) { @@ -381,11 +385,10 @@ void BinaryView::updateState() { uint32_t max_byte_flip_count = 1; for (auto count : bit_flip_tracker_.counts.bytes) max_byte_flip_count = std::max(max_byte_flip_count, count); - const Palette &p = palette(); + // Use the same logarithmic intensity mapping in both themes. const double max_alpha = 255.0; - const double min_alpha_with_signal = p.heatmap_signal_alpha; // Base alpha for small flip counts - const double min_alpha_no_signal = p.heatmap_bit_alpha; // Base alpha for small flip counts for no signal bits - const double alpha_gamma = p.heatmap_gamma; + const double min_alpha_with_signal = 25.0; // Base alpha for small flip counts + const double min_alpha_no_signal = 10.0; // Base alpha for small flip counts for no signal bits const double log_factor = 1.0 + 0.2; const double log_scaler = max_alpha / log2(1.0 + log_factor * max_bit_flip_count); @@ -398,7 +401,6 @@ void BinaryView::updateState() { uint32_t flip_count = bit_flips[i][j]; if (flip_count > 0) { double normalized_alpha = log2(1.0 + flip_count * log_factor) * log_scaler; - normalized_alpha = max_alpha * std::pow(std::clamp(normalized_alpha / max_alpha, 0.0, 1.0), alpha_gamma); double min_alpha = item.sigs.empty() ? min_alpha_no_signal : min_alpha_with_signal; alpha = std::clamp(normalized_alpha, min_alpha, max_alpha); } @@ -411,7 +413,7 @@ void BinaryView::updateState() { if (!heatmap_live_mode_) { const auto count = bit_flip_tracker_.counts.bytes[i]; const double intensity = std::log2(1.0 + count * log_factor) / std::log2(1.0 + max_byte_flip_count * log_factor); - byte_color = CabanaColor(102, 86, 169, static_cast(max_alpha * std::pow(intensity, alpha_gamma))); + byte_color = CabanaColor(102, 86, 169, static_cast(max_alpha * intensity)); } setCell(i, HEX_COLUMN, binary[i], byte_color); } @@ -441,12 +443,12 @@ bool BinaryView::hasSignal(const BinaryIndex &index, int dx, int dy, const caban void BinaryView::paintCell(ImDrawList *painter, const ImRect &rect, const BinaryIndex &index) const { auto item = &cellAt(index); ImFont *font = ImGui::GetFont(); - float font_size = ImGui::GetFontSize(); - ImU32 pen = paletteText(is_message_active_); + float font_size = CELL_FONT_SIZE; + ImU32 pen = paletteText(true); if (index.column == HEX_COLUMN) { if (item->valid) { - pushMonoFont(); + pushMonoFont(CELL_FONT_SIZE); font = ImGui::GetFont(); font_size = ImGui::GetFontSize(); popMonoFont(); @@ -454,7 +456,7 @@ void BinaryView::paintCell(ImDrawList *painter, const ImRect &rect, const Binary } } else if (isSelected(index)) { painter->AddRectFilled(rect.Min, rect.Max, resize_sig_ ? toImU32(resize_sig_->color) : paletteHighlight()); - if (resize_sig_) pen = IM_COL32_WHITE; + pen = IM_COL32_WHITE; } else if (!hasSelection() || std::find(item->sigs.begin(), item->sigs.end(), resize_sig_) == item->sigs.end()) { // not resizing if (item->sigs.size() > 0) { for (auto &s : item->sigs) { @@ -465,7 +467,6 @@ void BinaryView::paintCell(ImDrawList *painter, const ImRect &rect, const Binary } } } else if (item->valid) { - painter->AddRectFilled(rect.Min, rect.Max, ImGui::GetColorU32(palette().bit_background)); if (item->bg_color.alpha() > 0) painter->AddRectFilled(rect.Min, rect.Max, toImU32(item->bg_color)); } bool bright = std::find(item->sigs.begin(), item->sigs.end(), hovered_sig_) != item->sigs.end(); From df7e0e5e77a41565fb4811787a42db8937dca2f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Harald=20Sch=C3=A4fer?= Date: Thu, 17 Sep 2026 22:18:13 -0700 Subject: [PATCH 19/79] Use upstream tinygrad disk tensors for model loading (#38956) --- .../selfdrive/modeld/dmonitoringmodeld.py | 3 +-- openpilot/selfdrive/modeld/helpers.py | 24 +++++++------------ openpilot/selfdrive/modeld/modeld.py | 3 +-- .../modeld/models/big_driving_tinygrad.pkl | 4 ++-- tinygrad_repo | 2 +- 5 files changed, 13 insertions(+), 23 deletions(-) diff --git a/openpilot/selfdrive/modeld/dmonitoringmodeld.py b/openpilot/selfdrive/modeld/dmonitoringmodeld.py index 1ca9aab49b..5a6e9216ad 100755 --- a/openpilot/selfdrive/modeld/dmonitoringmodeld.py +++ b/openpilot/selfdrive/modeld/dmonitoringmodeld.py @@ -27,8 +27,7 @@ class ModelState: output: np.ndarray def __init__(self, cam_w: int, cam_h: int): - with open(MODEL_PKL_PATH, 'rb') as f: - jits = load_oob(f) + jits = load_oob(MODEL_PKL_PATH) self.DEV = jits['input_specs']['input_img'][2] self.input_shapes = jits['metadata']['input_shapes'] self.output_slices = pickle.loads(base64.b64decode(jits['metadata']['metadata']['output_slices'])) diff --git a/openpilot/selfdrive/modeld/helpers.py b/openpilot/selfdrive/modeld/helpers.py index bc5bc9e890..0d88e12a6a 100644 --- a/openpilot/selfdrive/modeld/helpers.py +++ b/openpilot/selfdrive/modeld/helpers.py @@ -1,8 +1,7 @@ -import mmap -import pickle -import struct +import sys from pathlib import Path +from openpilot.common.hardware import AGNOS from openpilot.common.hardware.usb import CHESTNUT_USB_PRODUCT, USB_DEVICES_PATH, is_chestnut_usb_id MODELS_DIR = Path(__file__).resolve().parent / 'models' @@ -12,19 +11,12 @@ def modeld_pkl_path(chestnut: bool): prefix = 'big_' if chestnut else '' return MODELS_DIR / f'{prefix}driving_tinygrad.pkl' -def load_oob(f): - data = memoryview(mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_COPY)) - opcode_size = struct.unpack_from(' len(data): - raise EOFError("incomplete model buffer") - yield pickle.PickleBuffer(data[offset:offset + size]) - offset += size - return pickle.loads(data[8:8 + opcode_size], buffers=buffers()) +def load_oob(path, chestnut=False): + from tinygrad import Context + device = 'USB+AMD:LLVM' if chestnut else 'QCOM' if AGNOS else 'METAL' if sys.platform == 'darwin' else 'CPU:LLVM' + with Context(DEV=device): + from tinygrad_repo.examples.openpilot.helpers import load_pickle + return load_pickle(path, out_of_band=True) def chestnut_present() -> bool: for d in USB_DEVICES_PATH.glob("*"): diff --git a/openpilot/selfdrive/modeld/modeld.py b/openpilot/selfdrive/modeld/modeld.py index 72e42b0307..effb511e7d 100755 --- a/openpilot/selfdrive/modeld/modeld.py +++ b/openpilot/selfdrive/modeld/modeld.py @@ -139,8 +139,7 @@ class ModelState: prev_desire: np.ndarray # for tracking the rising edge of the pulse def __init__(self, cam_w: int, cam_h: int, chestnut: bool): - with open(modeld_pkl_path(chestnut), 'rb') as f: - jits = load_oob(f) + jits = load_oob(modeld_pkl_path(chestnut), chestnut) self.model_device = jits['input_specs']['new_img'][2] self.input_shapes = {name: (shape, np.dtype(dtype)) for name, (shape, dtype, _) in jits['input_specs'].items()} self.state_pairs = {name: f'next_{name}' for name in self.input_shapes if f'next_{name}' in jits['metadata']['output_shapes']} diff --git a/openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl b/openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl index 3f479d5e42..2e3a751659 100644 --- a/openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl +++ b/openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e00a53cb9c64fe70a58224bc7879e5ea1b7e18baa7710f0929bf4f2e4537a468 -size 776612574 +oid sha256:a5fc6813f45be5673382d6571a5fc598a7cd8e47c534101fbc3a45767c169c53 +size 773082315 diff --git a/tinygrad_repo b/tinygrad_repo index 9fc9e60cb4..9d0446a4ba 160000 --- a/tinygrad_repo +++ b/tinygrad_repo @@ -1 +1 @@ -Subproject commit 9fc9e60cb4126b36700269c44081fec0fad62655 +Subproject commit 9d0446a4ba8a532c8b674fb6ad795af015cd9dcf From c8d56a1990a3b12c6eeb2c0e44b34be2dd1d0394 Mon Sep 17 00:00:00 2001 From: Trey Moen <50057480+greatgitsby@users.noreply.github.com> Date: Thu, 17 Sep 2026 22:28:30 -0700 Subject: [PATCH 20/79] cabana: filter multiplexed signals in binary grid (#38960) * cabana: filter multiplexed signals in binary grid * cabana: refresh overlap warnings on branch changes --- .../tools/cabana/ui/widgets/binarysignals.h | 32 ++++++++++ .../tools/cabana/ui/widgets/binaryview.cc | 63 +++++++++++-------- .../tools/cabana/ui/widgets/binaryview.h | 3 + .../tools/cabana/ui/widgets/detailwidget.cc | 1 + 4 files changed, 74 insertions(+), 25 deletions(-) create mode 100644 openpilot/tools/cabana/ui/widgets/binarysignals.h diff --git a/openpilot/tools/cabana/ui/widgets/binarysignals.h b/openpilot/tools/cabana/ui/widgets/binarysignals.h new file mode 100644 index 0000000000..1ccdd16c1c --- /dev/null +++ b/openpilot/tools/cabana/ui/widgets/binarysignals.h @@ -0,0 +1,32 @@ +#pragma once + +#include +#include +#include + +#include "tools/cabana/dbc/dbc.h" + +// Only the current multiplexed branch owns the payload bits. Use this same +// signal set for painting, endpoint markers, and mouse interaction. +inline std::vector binaryViewSignals(const cabana::Msg *msg, const uint8_t *data, size_t size) { + if (!msg) return {}; + std::optional multiplex_value; + if (msg->multiplexor) { + auto selector = *msg->multiplexor; + if (std::max(selector.msb, selector.lsb) / 8 < size) { + // Multiplex values are raw identifiers, independent of display scaling. + selector.factor = 1; + selector.offset = 0; + selector.is_signed = false; + multiplex_value = get_raw_value(data, size, selector); + } + } + std::vector signals; + for (auto sig : msg->getSignals()) { + if (sig->type != cabana::Signal::Type::Multiplexed || + (multiplex_value && *multiplex_value == sig->multiplex_value)) { + signals.push_back(sig); + } + } + return signals; +} diff --git a/openpilot/tools/cabana/ui/widgets/binaryview.cc b/openpilot/tools/cabana/ui/widgets/binaryview.cc index 9cb1a6ad10..df17944447 100644 --- a/openpilot/tools/cabana/ui/widgets/binaryview.cc +++ b/openpilot/tools/cabana/ui/widgets/binaryview.cc @@ -10,6 +10,7 @@ #include "tools/cabana/commands.h" #include "tools/cabana/settings.h" #include "tools/cabana/ui/util.h" +#include "tools/cabana/ui/widgets/binarysignals.h" #include "tools/cabana/utils/strings.h" #include "tools/cabana/utils/util.h" @@ -215,35 +216,46 @@ void BinaryView::refresh() { hovered_sig_ = nullptr; bit_flip_tracker_ = {}; cells_.clear(); - if (auto dbc_msg = dbc()->msg(msg_id_)) { - row_count_ = dbc_msg->size; - cells_.resize(row_count_ * COLUMN_COUNT); - for (auto sig : dbc_msg->getSignals()) { - for (int j = 0; j < sig->size; ++j) { - int pos = sig->is_little_endian ? flipBitPos(sig->start_bit + j) : flipBitPos(sig->start_bit) + j; - int idx = COLUMN_COUNT * (pos / 8) + pos % 8; - if (idx >= cells_.size()) { - fprintf(stderr, "signal %s out of bounds.start_bit: %d size: %d\n", sig->name.c_str(), sig->start_bit, sig->size); - break; - } - if (j == 0) sig->is_little_endian ? cells_[idx].is_lsb = true : cells_[idx].is_msb = true; - if (j == sig->size - 1) sig->is_little_endian ? cells_[idx].is_msb = true : cells_[idx].is_lsb = true; - - auto &sigs = cells_[idx].sigs; - sigs.push_back(sig); - if (sigs.size() > 1) { - std::sort(sigs.begin(), sigs.end(), [](auto l, auto r) { return l->size > r->size; }); - } - } - } - } else { - row_count_ = can->lastMessage(msg_id_).dat.size(); - cells_.resize(row_count_ * COLUMN_COUNT); - } + visible_signals_.clear(); + const auto *msg = dbc()->msg(msg_id_); + row_count_ = msg ? msg->size : can->lastMessage(msg_id_).dat.size(); + cells_.resize(row_count_ * COLUMN_COUNT); updateState(); if (under_mouse_) highlightPosition(last_mouse_pos_); } +void BinaryView::updateSignals() { + const auto &data = can->lastMessage(msg_id_).dat; + auto signals = binaryViewSignals(dbc()->msg(msg_id_), data.data(), data.size()); + if (signals == visible_signals_) return; + visible_signals_ = std::move(signals); + + // A branch switch invalidates the old hover and any in-progress resize. + selection_.clear(); + anchor_index_ = {}; + resize_sig_ = nullptr; + highlight(nullptr); + for (auto &cell : cells_) { + cell.sigs.clear(); + cell.is_msb = cell.is_lsb = false; + } + for (auto sig : visible_signals_) { + for (int j = 0; j < sig->size; ++j) { + int pos = sig->is_little_endian ? flipBitPos(sig->start_bit + j) : flipBitPos(sig->start_bit) + j; + int idx = COLUMN_COUNT * (pos / 8) + pos % 8; + if (idx < 0 || idx >= cells_.size()) break; + if (j == 0) sig->is_little_endian ? cells_[idx].is_lsb = true : cells_[idx].is_msb = true; + if (j == sig->size - 1) sig->is_little_endian ? cells_[idx].is_msb = true : cells_[idx].is_lsb = true; + cells_[idx].sigs.push_back(sig); + } + } + for (auto &cell : cells_) { + std::stable_sort(cell.sigs.begin(), cell.sigs.end(), [](auto l, auto r) { return l->size > r->size; }); + } + if (under_mouse_) highlightPosition(last_mouse_pos_); + signalsChanged(); +} + std::set BinaryView::getOverlappingSignals() const { std::set overlapping; @@ -373,6 +385,7 @@ void BinaryView::updateState() { } for (auto &cell : cells_) cell.valid = false; + updateSignals(); auto &bit_flips = heatmap_live_mode_ ? last_msg.bit_flip_counts : bitFlipChanges(binary.size()); uint32_t max_bit_flip_count = 1; // 1 to avoid division by zero diff --git a/openpilot/tools/cabana/ui/widgets/binaryview.h b/openpilot/tools/cabana/ui/widgets/binaryview.h index 9e55b63fcd..507d05811c 100644 --- a/openpilot/tools/cabana/ui/widgets/binaryview.h +++ b/openpilot/tools/cabana/ui/widgets/binaryview.h @@ -40,6 +40,7 @@ public: std::string whatsThis() const; Observable signalClicked; + Observable<> signalsChanged; Observable signalHovered; Observable editSignal; Observable showChart; @@ -54,6 +55,7 @@ private: bool valid = false; }; + void updateSignals(); void refresh(); // rebuilds the grid from the DBC message void setCell(int row, int col, uint8_t val, const CabanaColor &color); const std::vector> &bitFlipChanges(size_t msg_size); @@ -78,6 +80,7 @@ private: MessageId msg_id_; std::vector cells_; + std::vector visible_signals_; int row_count_ = 0; bool heatmap_live_mode_ = true; struct BitFlipTracker { diff --git a/openpilot/tools/cabana/ui/widgets/detailwidget.cc b/openpilot/tools/cabana/ui/widgets/detailwidget.cc index 28a7a5651e..abba2b607e 100644 --- a/openpilot/tools/cabana/ui/widgets/detailwidget.cc +++ b/openpilot/tools/cabana/ui/widgets/detailwidget.cc @@ -61,6 +61,7 @@ DetailWidget::DetailWidget(ChartsWidget *charts) : charts_(charts) { updateState(); })); + connections_.push_back(binary_view_->signalsChanged.connect([this]() { refresh(); })); connections_.push_back(binary_view_->signalHovered.connect([this](const cabana::Signal *s) { signal_view_->signalHovered(s); })); connections_.push_back(binary_view_->signalClicked.connect([this](const cabana::Signal *s) { signal_view_->selectSignal(s, true); })); connections_.push_back(binary_view_->editSignal.connect([this](const cabana::Signal *origin_s, cabana::Signal &s) { signal_view_->saveSignal(origin_s, s); })); From c6d13eb723e0f6eee47ea7d56e47a700fba21380 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Harald=20Sch=C3=A4fer?= Date: Thu, 17 Sep 2026 22:44:49 -0700 Subject: [PATCH 21/79] Cinque v3 (#38932) Use f78ed37d for the precompiled eGPU driving model --- openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl b/openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl index 2e3a751659..91125f3f21 100644 --- a/openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl +++ b/openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a5fc6813f45be5673382d6571a5fc598a7cd8e47c534101fbc3a45767c169c53 +oid sha256:4937fc6f04367b28328fe3bc0d1463a245725d039ddaab29406cc565fd5c8ed4 size 773082315 From 9a95fdc1a2b1ad2399923b2918f348eec12cfa90 Mon Sep 17 00:00:00 2001 From: Trey Moen <50057480+greatgitsby@users.noreply.github.com> Date: Thu, 17 Sep 2026 22:56:42 -0700 Subject: [PATCH 22/79] cabana: fix stale message size warnings (#38961) --- .../tools/cabana/ui/widgets/detailwidget.cc | 22 +++++++++++++------ .../tools/cabana/ui/widgets/detailwidget.h | 1 + 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/openpilot/tools/cabana/ui/widgets/detailwidget.cc b/openpilot/tools/cabana/ui/widgets/detailwidget.cc index abba2b607e..b73464c6e2 100644 --- a/openpilot/tools/cabana/ui/widgets/detailwidget.cc +++ b/openpilot/tools/cabana/ui/widgets/detailwidget.cc @@ -176,23 +176,30 @@ void DetailWidget::restoreTabs(const std::string &active_msg_id, const std::vect } void DetailWidget::refresh() { + auto msg = dbc()->msg(msg_id_); + std::string msg_name = msg ? msg->name + " (" + msg->transmitter + ")" : msgName(msg_id_); + name_label_.setText(msg_name); + name_label_.setToolTip(msg_name); + action_remove_msg_enabled_ = msg != nullptr; + updateWarnings(); +} + +void DetailWidget::updateWarnings() { std::vector warnings; auto msg = dbc()->msg(msg_id_); if (msg) { - if (msg_id_.source == INVALID_SOURCE) { + // A restored tab or a seek can refer to a bus with no sample at the current time. + // lastMessage() returns empty data in that case, not a received zero-byte frame. + if (msg_id_.source == INVALID_SOURCE || !can->lastMessages().count(msg_id_)) { warnings.push_back("No messages received."); } else if (msg->size != can->lastMessage(msg_id_).dat.size()) { - warnings.push_back("Message size (" + std::to_string(msg->size) + ") is incorrect."); + warnings.push_back("Message size mismatch: DBC defines " + std::to_string(msg->size) + + " bytes, received " + std::to_string(can->lastMessage(msg_id_).dat.size()) + " bytes."); } for (auto s : binary_view_->getOverlappingSignals()) { warnings.push_back(s->name + " has overlapping bits."); } } - std::string msg_name = msg ? msg->name + " (" + msg->transmitter + ")" : msgName(msg_id_); - name_label_.setText(msg_name); - name_label_.setToolTip(msg_name); - action_remove_msg_enabled_ = msg != nullptr; - if (!warnings.empty()) { warning_label_.clear(); for (size_t i = 0; i < warnings.size(); ++i) { @@ -212,6 +219,7 @@ void DetailWidget::updateState(const std::set *msgs) { binary_view_->updateState(); else history_log_->updateState(); + updateWarnings(); } void DetailWidget::editMsg(float parent_width) { diff --git a/openpilot/tools/cabana/ui/widgets/detailwidget.h b/openpilot/tools/cabana/ui/widgets/detailwidget.h index 4745ed5c84..6c64a33bf8 100644 --- a/openpilot/tools/cabana/ui/widgets/detailwidget.h +++ b/openpilot/tools/cabana/ui/widgets/detailwidget.h @@ -75,6 +75,7 @@ private: void showTabBarContextMenu(int index); void editMsg(float parent_width); void updateState(const std::set *msgs = nullptr); + void updateWarnings(); MessageId msg_id_; const char *warning_icon_ = nullptr; From d06711be19b72688c6d8a4f485df98135352355e Mon Sep 17 00:00:00 2001 From: stef <19478336+stefpi@users.noreply.github.com> Date: Fri, 18 Sep 2026 02:58:15 -0400 Subject: [PATCH 23/79] ui: prime menu (#38860) * Show pairing provider and Prime status in device settings * remove unnecessary * simplify * bespoke human code * clean * readme * prime layout * sepearte qr code stuff * move icons * move qr code into its own widget * clean * Fix prime menu lint * Update widget leak test for prime menu * fix lint * update to new theme * change names * fix font size * get rid of subtitle * adjust commacare icon * drop manage, its cleaner * edit text * clean * for github and apple always return generic * updates * go back to original * noice * better overwrite * close on unpairing if in management section * missing 20 px when icon is there * clean diff * fix lint --- .../icons_mici/settings/device/apple.png | 3 + .../icons_mici/settings/device/commacare.png | 3 + .../icons_mici/settings/device/github.png | 3 + .../icons_mici/settings/device/google.png | 3 + .../icons_mici/settings/device/green_cell.png | 3 + .../settings/device/paired_apple.png | 3 + .../settings/device/paired_github.png | 3 + .../settings/device/paired_google.png | 3 + .../icons_mici/settings/device/phone.png | 3 + .../mici/layouts/settings/device/__init__.py | 0 .../{device.py => device/device_layout.py} | 89 +++------ .../ui/mici/layouts/settings/device/prime.py | 171 ++++++++++++++++++ .../ui/mici/layouts/settings/settings.py | 2 +- .../ui/mici/layouts/settings/software.py | 2 +- .../ui/mici/tests/test_widget_leaks.py | 8 +- openpilot/selfdrive/ui/mici/widgets/button.py | 5 +- openpilot/selfdrive/ui/mici/widgets/info.py | 16 +- 17 files changed, 241 insertions(+), 79 deletions(-) create mode 100644 openpilot/selfdrive/assets/icons_mici/settings/device/apple.png create mode 100644 openpilot/selfdrive/assets/icons_mici/settings/device/commacare.png create mode 100644 openpilot/selfdrive/assets/icons_mici/settings/device/github.png create mode 100644 openpilot/selfdrive/assets/icons_mici/settings/device/google.png create mode 100644 openpilot/selfdrive/assets/icons_mici/settings/device/green_cell.png create mode 100644 openpilot/selfdrive/assets/icons_mici/settings/device/paired_apple.png create mode 100644 openpilot/selfdrive/assets/icons_mici/settings/device/paired_github.png create mode 100644 openpilot/selfdrive/assets/icons_mici/settings/device/paired_google.png create mode 100644 openpilot/selfdrive/assets/icons_mici/settings/device/phone.png create mode 100644 openpilot/selfdrive/ui/mici/layouts/settings/device/__init__.py rename openpilot/selfdrive/ui/mici/layouts/settings/{device.py => device/device_layout.py} (78%) create mode 100644 openpilot/selfdrive/ui/mici/layouts/settings/device/prime.py diff --git a/openpilot/selfdrive/assets/icons_mici/settings/device/apple.png b/openpilot/selfdrive/assets/icons_mici/settings/device/apple.png new file mode 100644 index 0000000000..04af670a48 --- /dev/null +++ b/openpilot/selfdrive/assets/icons_mici/settings/device/apple.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dfb645e59636687d61d20283bae65b68ddda6f7b2d4fda01a6e613a842e3b41f +size 823 diff --git a/openpilot/selfdrive/assets/icons_mici/settings/device/commacare.png b/openpilot/selfdrive/assets/icons_mici/settings/device/commacare.png new file mode 100644 index 0000000000..0e484035cf --- /dev/null +++ b/openpilot/selfdrive/assets/icons_mici/settings/device/commacare.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:df07383a6ddfe8cd7eeb2f22271cbd13068327ca0978873f8d10368860562e40 +size 111083 diff --git a/openpilot/selfdrive/assets/icons_mici/settings/device/github.png b/openpilot/selfdrive/assets/icons_mici/settings/device/github.png new file mode 100644 index 0000000000..136c09aac7 --- /dev/null +++ b/openpilot/selfdrive/assets/icons_mici/settings/device/github.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f7518f3409e2ff8f7e99fb7f0c06afa8bdf0b0a506147b5cf9d4931b820a1a75 +size 1107 diff --git a/openpilot/selfdrive/assets/icons_mici/settings/device/google.png b/openpilot/selfdrive/assets/icons_mici/settings/device/google.png new file mode 100644 index 0000000000..daeb574532 --- /dev/null +++ b/openpilot/selfdrive/assets/icons_mici/settings/device/google.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8a18e3de5f455b4089a824da8d5d78d5d532ef71facb05cc75c178b24d5beeb7 +size 920 diff --git a/openpilot/selfdrive/assets/icons_mici/settings/device/green_cell.png b/openpilot/selfdrive/assets/icons_mici/settings/device/green_cell.png new file mode 100644 index 0000000000..99635bcc2b --- /dev/null +++ b/openpilot/selfdrive/assets/icons_mici/settings/device/green_cell.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:710e74b526adb5bbac15cd8915a828dd673dd8ce15b2fd694c64c182eef6f56f +size 13744 diff --git a/openpilot/selfdrive/assets/icons_mici/settings/device/paired_apple.png b/openpilot/selfdrive/assets/icons_mici/settings/device/paired_apple.png new file mode 100644 index 0000000000..75708bf51c --- /dev/null +++ b/openpilot/selfdrive/assets/icons_mici/settings/device/paired_apple.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:547f466cc59d29647f3bf131d8444d7f22b63b186bcea55ceb8109c68ca74fe8 +size 2345 diff --git a/openpilot/selfdrive/assets/icons_mici/settings/device/paired_github.png b/openpilot/selfdrive/assets/icons_mici/settings/device/paired_github.png new file mode 100644 index 0000000000..8514100359 --- /dev/null +++ b/openpilot/selfdrive/assets/icons_mici/settings/device/paired_github.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:55050096cd98e7f4ea3e843d927c18815e458dfbd5e7d100b503db92075d061b +size 2373 diff --git a/openpilot/selfdrive/assets/icons_mici/settings/device/paired_google.png b/openpilot/selfdrive/assets/icons_mici/settings/device/paired_google.png new file mode 100644 index 0000000000..853de37d9d --- /dev/null +++ b/openpilot/selfdrive/assets/icons_mici/settings/device/paired_google.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f9c439ed89bc0357e86c005d5dbb6f1ad2b8d72cdc0890e3201d842d8c3928f1 +size 2443 diff --git a/openpilot/selfdrive/assets/icons_mici/settings/device/phone.png b/openpilot/selfdrive/assets/icons_mici/settings/device/phone.png new file mode 100644 index 0000000000..6114cd9124 --- /dev/null +++ b/openpilot/selfdrive/assets/icons_mici/settings/device/phone.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7aeccd3e0d8517f74eb34f7d73632c0b29b0ad5df5b9686c0f509bb6b136561e +size 1728 diff --git a/openpilot/selfdrive/ui/mici/layouts/settings/device/__init__.py b/openpilot/selfdrive/ui/mici/layouts/settings/device/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openpilot/selfdrive/ui/mici/layouts/settings/device.py b/openpilot/selfdrive/ui/mici/layouts/settings/device/device_layout.py similarity index 78% rename from openpilot/selfdrive/ui/mici/layouts/settings/device.py rename to openpilot/selfdrive/ui/mici/layouts/settings/device/device_layout.py index 0a665f7a2d..ff2e814b7d 100644 --- a/openpilot/selfdrive/ui/mici/layouts/settings/device.py +++ b/openpilot/selfdrive/ui/mici/layouts/settings/device/device_layout.py @@ -1,19 +1,16 @@ import os import pyray as rl -import time from collections.abc import Callable from typing import Union -from openpilot.common.api import Api -from openpilot.common.swaglog import cloudlog from openpilot.common.basedir import BASEDIR from openpilot.common.params import Params from openpilot.common.time_helpers import system_time_valid +from openpilot.selfdrive.ui.mici.layouts.settings.device.prime import PrimeScroller from openpilot.system.ui.widgets.scroller import NavRawScrollPanel, NavScroller from openpilot.selfdrive.ui.mici.widgets.info import InfoLayoutMici -from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigCircleButton, GreyBigButton +from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigCircleButton from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigConfirmationDialog -from openpilot.selfdrive.ui.mici.widgets.qr import QR from openpilot.selfdrive.ui.mici.onroad.cabin_camera_dialog import CabinCameraDialog from openpilot.selfdrive.ui.mici.layouts.onboarding import TrainingGuide, TermsPage from openpilot.system.ui.lib.application import gui_app, MousePos @@ -99,86 +96,50 @@ class DeviceInfoLayoutMici(InfoLayoutMici): super().__init__("device ID", params.get("DongleId") or 'N/A', "serial", params.get("HardwareSerial") or 'N/A', width=380) -class PairingDialog(NavScroller): - """Pair the device with comma connect.""" - - QR_REFRESH_INTERVAL = 300 # 5 minutes in seconds - - def __init__(self): - super().__init__() - self._params = Params() - self._last_pairing_qr_generation = float("-inf") - self._qr = QR(self._get_pairing_url()) - self._scroller._show_scroll_indicator = False - self._scroller.add_widgets([ - self._qr, - GreyBigButton("finish setup", "scan to pair device\nwith connect", - gui_app.texture("icons_mici/settings/device/green_settings.png", 64, 64)), - GreyBigButton("", "connect lets you review recent driving footage and bookmark events."), - ]) - - def _get_pairing_url(self) -> str: - try: - dongle_id = self._params.get("DongleId") or "" - token = Api(dongle_id).get_token({'pair': True}) - except Exception as e: - cloudlog.warning(f"Failed to get pairing token: {e}") - token = "" - return f"https://connect.comma.ai/?pair={token}" - - def _update_layout_rects(self): - super()._update_layout_rects() - self._qr.set_rect(rl.Rectangle(self._qr.rect.x, self._qr.rect.y, self._rect.height, self._rect.height)) - - def _render(self, rect: rl.Rectangle): - current_time = time.monotonic() - if current_time - self._last_pairing_qr_generation >= self.QR_REFRESH_INTERVAL: - self._qr._url = self._get_pairing_url() - if self._qr._texture and self._qr._texture.id != 0: - rl.unload_texture(self._qr._texture) - self._qr._texture = self._qr._generate_qr_code() - self._last_pairing_qr_generation = current_time - super()._render(rect) - - def _update_state(self): - super()._update_state() - if ui_state.prime_state.is_paired() and not self.is_dismissing: - self.dismiss() - - class PairBigButton(BigButton): def __init__(self): - super().__init__("pair", "connect.comma.ai", gui_app.texture("icons_mici/settings/comma_icon.png", 33, 60)) - - def _get_label_font_size(self): - return 64 + self._commacare_badge = gui_app.texture("icons_mici/settings/device/commacare.png", 27, 32) + self._show_commacare = False + self._comma_icon = gui_app.texture("icons_mici/settings/comma_icon.png", 33, 60) + self._provider_icons = {provider: gui_app.texture(f"icons_mici/settings/device/paired_{provider}.png", 64, 64) + for provider in ("github", "google", "apple")} + super().__init__("pair to connect", "connect.comma.ai", self._comma_icon) def _update_state(self): super()._update_state() + self._show_commacare = (ui_state.prime_state.is_paired() and ui_state.prime_state.is_prime() + and ui_state.prime_state.has_commacare()) if ui_state.prime_state.is_paired(): + self.set_icon(self._provider_icons.get(ui_state.prime_state.get_pairing_provider(), self._comma_icon)) self.set_text("paired") if ui_state.prime_state.is_prime(): - self.set_value("subscribed") + self.set_value("prime" if ui_state.prime_state.is_full_prime() else "prime lite") else: - self.set_value("upgrade to prime") + self.set_value("claim prime trial" if ui_state.prime_state.can_claim_prime_trial() else "upgrade to prime") else: - self.set_text("pair") + self.set_icon(self._comma_icon) + self.set_text("pair to connect") self.set_value("connect.comma.ai") + def _draw_subtitle(self, rect: rl.Rectangle): + if self._show_commacare: + y = rect.y + rect.height - self._commacare_badge.height + rl.draw_texture_v(self._commacare_badge, rl.Vector2(rect.x, y), rl.WHITE) + offset = self._commacare_badge.width + 14 + rect = rl.Rectangle(rect.x + offset, rect.y, rect.width - offset, rect.height) + super()._draw_subtitle(rect) + def _handle_mouse_release(self, mouse_pos: MousePos): super()._handle_mouse_release(mouse_pos) - # TODO: show ad dialog when clicked if not prime - if ui_state.prime_state.is_paired(): - return - dlg: BigDialog | PairingDialog + dlg: BigDialog | PrimeScroller if not system_time_valid(): dlg = BigDialog("", tr("Please connect to Wi-Fi to complete initial pairing.")) elif UNREGISTERED_DONGLE_ID == (ui_state.params.get("DongleId") or UNREGISTERED_DONGLE_ID): dlg = BigDialog("", tr("Device must be registered with the comma.ai backend to pair.")) else: - dlg = PairingDialog() + dlg = PrimeScroller() gui_app.push_widget(dlg) diff --git a/openpilot/selfdrive/ui/mici/layouts/settings/device/prime.py b/openpilot/selfdrive/ui/mici/layouts/settings/device/prime.py new file mode 100644 index 0000000000..2d4172a7b6 --- /dev/null +++ b/openpilot/selfdrive/ui/mici/layouts/settings/device/prime.py @@ -0,0 +1,171 @@ +import pyray as rl +import time + +from openpilot.common.api import Api +from openpilot.common.swaglog import cloudlog +from openpilot.common.params import Params +from openpilot.selfdrive.ui.mici.widgets.button import BigButton, GreyBigButton +from openpilot.selfdrive.ui.mici.widgets.info import InfoLayoutMici +from openpilot.selfdrive.ui.mici.widgets.qr import QR +from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.system.ui.lib.application import gui_app +from openpilot.system.ui.widgets.scroller import NavScroller + + +class PairingInfoLayout(InfoLayoutMici): + def __init__(self): + super().__init__("account", "", "status", "") + self._commacare_badge = gui_app.texture("icons_mici/settings/device/commacare.png", 27, 32) + self._provider_icons = {provider: gui_app.texture(f"icons_mici/settings/device/{provider}.png", 32, 32) + for provider in ("github", "google", "apple")} + + def _update_state(self): + super()._update_state() + self.subtext1.set_text(ui_state.prime_state.get_pairing_account()) + self.subtext2.set_text(self._get_prime_status()) + self._provider_icon = self._provider_icons.get(ui_state.prime_state.get_pairing_provider()) + self._show_commacare = ui_state.prime_state.has_commacare() + + @staticmethod + def _get_prime_status() -> str: + if ui_state.prime_state.is_prime(): + return "prime" if ui_state.prime_state.is_full_prime() else "prime lite" + return "not subscribed" + + def _layout(self): + super()._layout() + provider_offset = self._provider_icon.width + 14 if self._provider_icon else 0 + commacare_offset = self._commacare_badge.width + 14 if self._show_commacare else 0 + self.subtext1.set_position(self.subtext1.rect.x + provider_offset, self.subtext1.rect.y) + self.subtext1.set_max_width(int(self._rect.width - provider_offset - (40 if self._provider_icon else 20))) + self.subtext2.set_position(self.subtext2.rect.x + commacare_offset, self.subtext2.rect.y) + self.subtext2.set_max_width(int(self._rect.width - commacare_offset - (40 if self._show_commacare else 20))) + + def _render(self, rect: rl.Rectangle): + super()._render(rect) + + if self._provider_icon: + icon_pos = rl.Vector2(self._rect.x + 20, self.subtext1.rect.y + (self.subtext1.rect.height - self._provider_icon.height) / 2) + rl.draw_texture_v(self._provider_icon, icon_pos, self._subheader_color) + if self._show_commacare: + icon_pos = rl.Vector2(self._rect.x + 20, self.subtext2.rect.y + (self.subtext2.rect.height - self._commacare_badge.height) / 2 + 4) + rl.draw_texture_v(self._commacare_badge, icon_pos, rl.WHITE) + + +class PrimeManagementScroller(NavScroller): + """Prime subscription management and upgrade information.""" + + def __init__(self): + super().__init__() + self._params = Params() + + can_claim_trial = ui_state.prime_state.can_claim_prime_trial() + + self._prime_icon = gui_app.texture("icons_mici/settings/device/green_cell.png", 64, 64) + self._phone_icon = gui_app.texture("icons_mici/settings/device/phone.png", 85, 64) + self._prime_adverts = [ + QR(self._get_prime_url()), + GreyBigButton( + "try prime for\n30 days" if can_claim_trial else "upgrade to prime", + "scan to claim trial" if can_claim_trial else "scan to manage\nprime status", + self._prime_icon + ), + GreyBigButton("", "prime adds 24/7 LTE to device and 1 year of cloud storage in connect.",), + GreyBigButton("", "prime lets you view live video and GPS location remotely in connect.",), + GreyBigButton("", "prime also includes commacare extended device warranty.",), + ] + self._prime_management = [ + QR(self._get_prime_url()), + GreyBigButton( + "manage prime", + "scan to open\ndevice prime settings", + self._phone_icon + ), + ] + self._scroller.add_widgets(self._prime_management if ui_state.prime_state.is_prime() else self._prime_adverts) + + def _get_prime_url(self) -> str: + if dongle_id := self._params.get("DongleId"): + return f"https://connect.comma.ai/{dongle_id}/prime" + return "https://connect.comma.ai" + + def _update_state(self): + super()._update_state() + if not ui_state.prime_state.is_paired(): + self.dismiss() + + +class PrimeScroller(NavScroller): + """Pairing flow or paired account information with access to prime management.""" + + QR_REFRESH_INTERVAL = 300 # 5 minutes in seconds + + def __init__(self): + super().__init__() + self._params = Params() + self.initial_is_paired = ui_state.prime_state.is_paired() + self._last_pairing_qr_generation = float("-inf") + + self._manage_icon = gui_app.texture("icons_mici/settings/comma_icon.png", 33, 60) + if not ui_state.prime_state.is_prime(): + title = "claim\nprime trial" if ui_state.prime_state.can_claim_prime_trial() else "upgrade\nto prime" + self._manage_prime = BigButton(title, icon=self._manage_icon) + else: + self._manage_prime = BigButton("manage prime", icon=self._manage_icon) + self._manage_prime.set_click_callback(lambda: gui_app.push_widget(PrimeManagementScroller())) + + if self.initial_is_paired: + self._scroller.add_widgets([ + PairingInfoLayout(), + self._manage_prime, + ]) + else: + self._qr = QR(self._get_pairing_url()) + self._scroller._show_scroll_indicator = False + self._scroller.add_widgets([ + self._qr, + GreyBigButton("finish setup", "scan to pair device\nwith connect", + gui_app.texture("icons_mici/settings/device/green_settings.png", 64, 64)), + GreyBigButton("", "connect lets you review recent driving footage and bookmark events."), + ]) + + def _get_pairing_url(self) -> str: + try: + dongle_id = self._params.get("DongleId") or "" + token = Api(dongle_id).get_token({'pair': True}) + except Exception as e: + cloudlog.warning(f"Failed to get pairing token: {e}") + token = "" + return f"https://connect.comma.ai/?pair={token}" + + def _update_layout_rects(self): + super()._update_layout_rects() + if not self.initial_is_paired: + self._qr.set_rect(rl.Rectangle(self._qr.rect.x, self._qr.rect.y, self._rect.height, self._rect.height)) + + def _render(self, rect: rl.Rectangle): + if not self.initial_is_paired: + current_time = time.monotonic() + if current_time - self._last_pairing_qr_generation >= self.QR_REFRESH_INTERVAL: + self._qr._url = self._get_pairing_url() + if self._qr._texture and self._qr._texture.id != 0: + rl.unload_texture(self._qr._texture) + self._qr._texture = self._qr._generate_qr_code() + self._last_pairing_qr_generation = current_time + super()._render(rect) + + def _update_state(self): + super()._update_state() + if self.initial_is_paired != ui_state.prime_state.is_paired() and gui_app.get_active_widget() is self: + self.dismiss() + + +if __name__ == "__main__": + gui_app.init_window("pairing device") + pairing = PrimeScroller() + gui_app.push_widget(pairing) + try: + for _ in gui_app.render(): + pass + finally: + del pairing diff --git a/openpilot/selfdrive/ui/mici/layouts/settings/settings.py b/openpilot/selfdrive/ui/mici/layouts/settings/settings.py index 477bd78935..d49358297d 100644 --- a/openpilot/selfdrive/ui/mici/layouts/settings/settings.py +++ b/openpilot/selfdrive/ui/mici/layouts/settings/settings.py @@ -3,7 +3,7 @@ from openpilot.system.ui.widgets.scroller import NavScroller from openpilot.selfdrive.ui.mici.widgets.button import BigButton from openpilot.selfdrive.ui.mici.layouts.settings.toggles import TogglesLayoutMici from openpilot.selfdrive.ui.mici.layouts.settings.network.network_layout import NetworkLayoutMici -from openpilot.selfdrive.ui.mici.layouts.settings.device import DeviceLayoutMici +from openpilot.selfdrive.ui.mici.layouts.settings.device.device_layout import DeviceLayoutMici from openpilot.selfdrive.ui.mici.layouts.settings.developer import DeveloperLayoutMici from openpilot.selfdrive.ui.mici.layouts.settings.software import SoftwareLayoutMici from openpilot.selfdrive.ui.mici.layouts.settings.firehose import FirehoseLayout diff --git a/openpilot/selfdrive/ui/mici/layouts/settings/software.py b/openpilot/selfdrive/ui/mici/layouts/settings/software.py index 0c86d8e884..5c0f4cd6fe 100644 --- a/openpilot/selfdrive/ui/mici/layouts/settings/software.py +++ b/openpilot/selfdrive/ui/mici/layouts/settings/software.py @@ -5,7 +5,7 @@ from enum import IntEnum from collections.abc import Callable from openpilot.common.time_helpers import system_time_valid -from openpilot.selfdrive.ui.mici.layouts.settings.device import EngagedConfirmationButton +from openpilot.selfdrive.ui.mici.layouts.settings.device.device_layout import EngagedConfirmationButton from openpilot.selfdrive.ui.mici.widgets.info import InfoLayoutMici from openpilot.selfdrive.ui.mici.widgets.button import BigButton from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog diff --git a/openpilot/selfdrive/ui/mici/tests/test_widget_leaks.py b/openpilot/selfdrive/ui/mici/tests/test_widget_leaks.py index a329a00e6b..71530f9fd8 100755 --- a/openpilot/selfdrive/ui/mici/tests/test_widget_leaks.py +++ b/openpilot/selfdrive/ui/mici/tests/test_widget_leaks.py @@ -19,7 +19,7 @@ KNOWN_LEAKS = { "openpilot.system.ui.widgets.button.Button", "openpilot.system.ui.widgets.html_render.HtmlRenderer", "openpilot.system.ui.widgets.nav_widget.NavBar", - "openpilot.selfdrive.ui.mici.layouts.settings.device.MiciFccModal", + "openpilot.selfdrive.ui.mici.layouts.settings.device.device_layout.MiciFccModal", "openpilot.system.ui.widgets.inputbox.InputBox", "openpilot.system.ui.widgets.scroller_tici.Scroller", "openpilot.system.ui.widgets.label.UnifiedLabel", @@ -52,9 +52,9 @@ class TestWidgetLeaks(OpenpilotTestCase): # mici dialogs from openpilot.selfdrive.ui.mici.layouts.onboarding import TrainingGuide as MiciTrainingGuide, OnboardingWindow as MiciOnboardingWindow from openpilot.selfdrive.ui.mici.onroad.cabin_camera_dialog import CabinCameraDialog as MiciCabinCameraDialog - from openpilot.selfdrive.ui.mici.layouts.settings.device import PairingDialog as MiciPairingDialog + from openpilot.selfdrive.ui.mici.layouts.settings.device.prime import PrimeScroller from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigConfirmationDialog, BigInputDialog - from openpilot.selfdrive.ui.mici.layouts.settings.device import MiciFccModal + from openpilot.selfdrive.ui.mici.layouts.settings.device.device_layout import MiciFccModal # tici dialogs from openpilot.selfdrive.ui.onroad.cabin_camera_dialog import CabinCameraDialog as TiciCabinCameraDialog @@ -71,7 +71,7 @@ class TestWidgetLeaks(OpenpilotTestCase): for ctor in ( # mici - MiciCabinCameraDialog, MiciPairingDialog, + MiciCabinCameraDialog, PrimeScroller, lambda: MiciTrainingGuide(lambda: None), lambda: MiciOnboardingWindow(lambda: None), lambda: BigDialog("test", "test"), diff --git a/openpilot/selfdrive/ui/mici/widgets/button.py b/openpilot/selfdrive/ui/mici/widgets/button.py index 4f395ae9bc..c481a7707b 100644 --- a/openpilot/selfdrive/ui/mici/widgets/button.py +++ b/openpilot/selfdrive/ui/mici/widgets/button.py @@ -241,6 +241,9 @@ class BigButton(BaseButton): btn_y = self._rect.y + (self._rect.height * (1 - scale)) / 2 return txt_bg, btn_x, btn_y, scale + def _draw_subtitle(self, rect: rl.Rectangle): + self._sub_label.render(rect) + def _draw_content(self, btn_y: float): # LABEL ------------------------------------------------------------------ label_x = self._rect.x + self.LABEL_HORIZONTAL_PADDING @@ -255,7 +258,7 @@ class BigButton(BaseButton): label_y = label_rect.y + self._label.get_content_height(int(label_rect.width)) sub_label_height = btn_y + self._rect.height - self.LABEL_VERTICAL_PADDING - label_y sub_label_rect = rl.Rectangle(label_x, label_y, self._subtitle_width_hint(), sub_label_height) - self._sub_label.render(sub_label_rect) + self._draw_subtitle(sub_label_rect) # ICON ------------------------------------------------------------------- if self._txt_icon: diff --git a/openpilot/selfdrive/ui/mici/widgets/info.py b/openpilot/selfdrive/ui/mici/widgets/info.py index 1b911d7a90..2d765cd6fd 100644 --- a/openpilot/selfdrive/ui/mici/widgets/info.py +++ b/openpilot/selfdrive/ui/mici/widgets/info.py @@ -10,22 +10,22 @@ class InfoLayoutMici(Widget): super().__init__() self.set_rect(rl.Rectangle(0, 0, width, 180)) - subheader_color = rl.Color(255, 255, 255, int(255 * 0.9 * 0.65)) max_width = int(self._rect.width - 20) + self._subheader_color = rl.Color(255, 255, 255, int(255 * 0.9 * 0.65)) self.title1 = UnifiedLabel(title1, 48, max_width=max_width, font_weight=FontWeight.DISPLAY, wrap_text=False) - self.subtext1 = UnifiedLabel(subtext1, 36, max_width=max_width, text_color=subheader_color, + self.subtext1 = UnifiedLabel(subtext1, 36, max_width=max_width, text_color=self._subheader_color, font_weight=FontWeight.ROMAN, wrap_text=False, scroll=True) self.title2 = UnifiedLabel(title2, 48, max_width=max_width, font_weight=FontWeight.DISPLAY, wrap_text=False) - self.subtext2 = UnifiedLabel(subtext2, 36, max_width=max_width, text_color=subheader_color, + self.subtext2 = UnifiedLabel(subtext2, 36, max_width=max_width, text_color=self._subheader_color, font_weight=FontWeight.ROMAN, wrap_text=False, scroll=True) - def _render(self, _): + def _layout(self): self.title1.set_position(self._rect.x + 20, self._rect.y - 10) - self.title1.render() self.subtext1.set_position(self._rect.x + 20, self._rect.y + 68 - 25) - self.subtext1.render() self.title2.set_position(self._rect.x + 20, self._rect.y + 114 - 30) - self.title2.render() self.subtext2.set_position(self._rect.x + 20, self._rect.y + 161 - 25) - self.subtext2.render() + + def _render(self, rect: rl.Rectangle): + for widget in [self.title1, self.subtext1, self.title2, self.subtext2]: + widget.render() From 7f6f13997c3c9b8e27e1581583f61e3fcabc5151 Mon Sep 17 00:00:00 2001 From: Trey Moen <50057480+greatgitsby@users.noreply.github.com> Date: Fri, 18 Sep 2026 06:44:39 -0700 Subject: [PATCH 24/79] replay: fix ranges with omitted start (#38963) * replay: fix ranges with omitted start * replay: keep range tests scoped to omitted start --- openpilot/tools/replay/.gitignore | 1 + openpilot/tools/replay/SConscript | 2 ++ openpilot/tools/replay/route.cc | 4 +-- openpilot/tools/replay/tests/test_route.cc | 37 ++++++++++++++++++++++ 4 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 openpilot/tools/replay/tests/test_route.cc diff --git a/openpilot/tools/replay/.gitignore b/openpilot/tools/replay/.gitignore index aa615770a2..720992de90 100644 --- a/openpilot/tools/replay/.gitignore +++ b/openpilot/tools/replay/.gitignore @@ -1,2 +1,3 @@ replay tests/test_replay +tests/test_route diff --git a/openpilot/tools/replay/SConscript b/openpilot/tools/replay/SConscript index 9e060bc829..f6c45b3792 100644 --- a/openpilot/tools/replay/SConscript +++ b/openpilot/tools/replay/SConscript @@ -14,3 +14,5 @@ replay_lib = replay_env.Library("replay", replay_lib_src, LIBS=base_libs, FRAMEW Export('replay_lib') replay_libs = [replay_lib] + ffmpeg_libs + ['ncurses'] + base_libs replay_env.Program("replay", ["main.cc"], LIBS=replay_libs, FRAMEWORKS=base_frameworks) +if GetOption('extras'): + replay_env.Program("tests/test_route", ["tests/test_route.cc"], LIBS=replay_libs, FRAMEWORKS=base_frameworks) diff --git a/openpilot/tools/replay/route.cc b/openpilot/tools/replay/route.cc index f38c2b1971..dce5661b4c 100644 --- a/openpilot/tools/replay/route.cc +++ b/openpilot/tools/replay/route.cc @@ -27,8 +27,8 @@ RouteIdentifier Route::parseRoute(const std::string &str) { const auto range_str = match[6].str(); if (!range_str.empty()) { if (separator == "/") { - int pos = range_str.find(':'); - int begin_seg = std::stoi(range_str.substr(0, pos)); + const auto pos = range_str.find(':'); + int begin_seg = pos == 0 ? 0 : std::stoi(range_str.substr(0, pos)); identifier.begin_segment = identifier.end_segment = begin_seg; if (pos != std::string::npos) { auto end_seg_str = range_str.substr(pos + 1); diff --git a/openpilot/tools/replay/tests/test_route.cc b/openpilot/tools/replay/tests/test_route.cc new file mode 100644 index 0000000000..c1eafbd4c5 --- /dev/null +++ b/openpilot/tools/replay/tests/test_route.cc @@ -0,0 +1,37 @@ +#include "common/tests/native_test.h" +#include "tools/replay/replay.h" +#include "tools/replay/route.h" + +void test_route_ranges() { + struct Range { + const char *suffix; + int begin; + int end; + }; + const Range ranges[] = { + {"", 0, -1}, + {"/:15", 0, 15}, + {"/:0", 0, 0}, + {"/0:15", 0, 15}, + {"/5:15", 5, 15}, + {"/5:", 5, -1}, + {"/5", 5, 5}, + {"--5", 5, -1}, + }; + for (const std::string route : {DEMO_ROUTE, "64e16c4237493597/00000033--332a5f9d82"}) { + const auto full = Route::parseRoute(route); + REQUIRE(!full.str.empty()); + for (const auto &range : ranges) { + const auto parsed = Route::parseRoute(route + range.suffix); + REQUIRE(parsed.str == full.str); + REQUIRE(parsed.dongle_id == full.dongle_id); + REQUIRE(parsed.timestamp == full.timestamp); + REQUIRE(parsed.begin_segment == range.begin); + REQUIRE(parsed.end_segment == range.end); + } + } +} + +int main() { + return run_native_test(test_route_ranges); +} From e691acbf34de063171d6f27335dc0efd73a01fc2 Mon Sep 17 00:00:00 2001 From: Daniel Koepping Date: Fri, 18 Sep 2026 11:21:39 -0700 Subject: [PATCH 25/79] chestnut stresstest (#38858) chestnut stress test --- Jenkinsfile | 44 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 92a26d6265..576daba734 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -161,6 +161,14 @@ def step(String name, String cmd, Map args = [:]) { return [name, cmd, args] } +def chestnutStage(String name = "chestnut") { + deviceStage(name, "mici-chestnut-ci", ["UNSAFE=1", "CHESTNUT=1"], [ + step("build", "./openpilot/selfdrive/test/chestnut.sh"), + step("model replay", "openpilot/selfdrive/test/process_replay/model_replay.py --chestnut"), + step("onroad tests", "./openpilot/selfdrive/test/test_onroad.py TestChestnutOnroad", [timeout: 120]), + ]) +} + node { env.CI = "1" env.PYTHONWARNINGS = "error" @@ -171,6 +179,36 @@ node { env.GIT_BRANCH = checkout(scm).GIT_BRANCH env.GIT_COMMIT = checkout(scm).GIT_COMMIT + if (env.JOB_BASE_NAME == 'chestnut-stresstest') { + sh 'git fetch --no-tags origin nightly-chestnut' + env.GIT_COMMIT = sh(script: 'git show FETCH_HEAD:git_src_commit', returnStdout: true).trim() + if (!(env.GIT_COMMIT ==~ /[0-9a-f]{40}/)) { + error("invalid nightly-chestnut source commit") + } + def results = [] + try { + timeout(time: 12, unit: 'HOURS') { + for (int run = 1; run <= 100; run++) { + results << "run ${run}: INCOMPLETE" + try { + chestnutStage("chestnut ${run}/100") + results[run - 1] = "run ${run}: PASS" + } catch (hudson.AbortException e) { + results[run - 1] = "run ${run}: FAIL" + echo e.toString() + } + } + } + } finally { + writeFile file: 'chestnut_stress_report.txt', text: "commit: ${env.GIT_COMMIT}\ncompleted: ${results.count { !it.endsWith('INCOMPLETE') }}/100\n" + results.join('\n') + '\n' + archiveArtifacts artifacts: 'chestnut_stress_report.txt' + } + if (results.any { !it.endsWith('PASS') }) { + error("chestnut stress test failed") + } + return + } + def excludeBranches = ['__nightly', '__nightly-chestnut', 'devel', 'devel-staging', 'release-tizi', 'release-tizi-staging', 'release-mici', 'release-mici-staging', 'testing-closet*', 'hotfix-*'] def excludeRegex = excludeBranches.join('|').replaceAll('\\*', '.*') @@ -261,11 +299,7 @@ node { ]) }, 'chestnut': { - deviceStage("chestnut", "mici-chestnut-ci", ["UNSAFE=1", "CHESTNUT=1"], [ - step("build", "./openpilot/selfdrive/test/chestnut.sh"), - step("model replay", "openpilot/selfdrive/test/process_replay/model_replay.py --chestnut"), - step("onroad tests", "./openpilot/selfdrive/test/test_onroad.py TestChestnutOnroad", [timeout: 120]), - ]) + chestnutStage() }, ) From 8abd1336370ba1c1f69094e48316e82fa61707e6 Mon Sep 17 00:00:00 2001 From: stef <19478336+stefpi@users.noreply.github.com> Date: Fri, 18 Sep 2026 17:44:45 -0400 Subject: [PATCH 26/79] offroad alerts: pairing notification (#38948) * Show pairing provider and Prime status in device settings * remove unnecessary * simplify * bespoke human code * clean * readme * prime layout * sepearte qr code stuff * move icons * move qr code into its own widget * clean * Fix prime menu lint * Update widget leak test for prime menu * fix lint * update to new theme * change names * fix font size * get rid of subtitle * adjust commacare icon * drop manage, its cleaner * edit text * clean * for github and apple always return generic * pairing notification * update copy and remove duplicates * don't write too often * remove * make sure it doesn't show up on 3X * reorg * remove redundancies --- openpilot/common/params_keys.h | 1 + .../selfdrive/selfdrived/alerts_offroad.json | 5 ++++ openpilot/selfdrive/ui/lib/prime_state.py | 4 +++ openpilot/selfdrive/ui/mici/layouts/main.py | 2 ++ .../ui/mici/layouts/offroad_alerts.py | 6 ++++ .../layouts/settings/device/device_layout.py | 29 ++++++++++++++++++- .../ui/mici/layouts/settings/device/prime.py | 2 +- .../ui/mici/layouts/settings/settings.py | 26 ++++++++++++++--- .../selfdrive/ui/widgets/offroad_alerts.py | 2 ++ 9 files changed, 71 insertions(+), 6 deletions(-) diff --git a/openpilot/common/params_keys.h b/openpilot/common/params_keys.h index b9f3ffeaa7..96bef4daa7 100644 --- a/openpilot/common/params_keys.h +++ b/openpilot/common/params_keys.h @@ -103,6 +103,7 @@ inline static std::unordered_map keys = { {"Offroad_ConnectivityNeededPrompt", {CLEAR_ON_MANAGER_START, JSON}}, {"Offroad_ExcessiveActuation", {PERSISTENT, JSON}}, {"Offroad_NoFirmware", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}}, + {"Offroad_Pairing", {CLEAR_ON_MANAGER_START, JSON}}, {"Offroad_Recalibration", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}}, {"Offroad_TemperatureTooHigh", {CLEAR_ON_MANAGER_START, JSON}}, {"Offroad_UnregisteredHardware", {CLEAR_ON_MANAGER_START, JSON}}, diff --git a/openpilot/selfdrive/selfdrived/alerts_offroad.json b/openpilot/selfdrive/selfdrived/alerts_offroad.json index 33fec6e330..c5ee4650a3 100644 --- a/openpilot/selfdrive/selfdrived/alerts_offroad.json +++ b/openpilot/selfdrive/selfdrived/alerts_offroad.json @@ -65,5 +65,10 @@ "text": "Excessive %1 actuation detected on your last drive. Please contact support at https://comma.ai/support and share your device's Dongle ID for troubleshooting.", "severity": 1, "_comment": "Set extra field to lateral or longitudinal." + }, + "Offroad_Pairing": { + "text": "Finish setup. Pair your device with comma connect to view recent drives and bookmarked routes.", + "severity": -2, + "icon": "icons_mici/settings/device/green_settings.png" } } diff --git a/openpilot/selfdrive/ui/lib/prime_state.py b/openpilot/selfdrive/ui/lib/prime_state.py index df6f18671e..6b37675c9b 100644 --- a/openpilot/selfdrive/ui/lib/prime_state.py +++ b/openpilot/selfdrive/ui/lib/prime_state.py @@ -10,6 +10,7 @@ from openpilot.common.realtime import drop_realtime from openpilot.common.swaglog import cloudlog from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID from openpilot.selfdrive.ui.lib.api_helpers import get_token +from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert class PrimeType(IntEnum): @@ -43,6 +44,8 @@ class PrimeState: self._pairing_email: str | None = self._params.get("PairingEmail") self._commacare = False + set_offroad_alert("Offroad_Pairing", self.prime_type <= PrimeType.UNPAIRED) + self._running = False self._thread = None @@ -103,6 +106,7 @@ class PrimeState: self.prime_type = prime_type self._params.put("PrimeType", int(prime_type)) cloudlog.info(f"Prime type updated to {prime_type}") + set_offroad_alert("Offroad_Pairing", self.prime_type <= PrimeType.UNPAIRED) def set_provider(self, provider: Provider | None, email: str | None): with self._lock: diff --git a/openpilot/selfdrive/ui/mici/layouts/main.py b/openpilot/selfdrive/ui/mici/layouts/main.py index 5364e36da1..8d3ba17e95 100644 --- a/openpilot/selfdrive/ui/mici/layouts/main.py +++ b/openpilot/selfdrive/ui/mici/layouts/main.py @@ -70,6 +70,8 @@ class MiciMainLayout(Scroller): return self._body_onroad_layout if ui_state.is_body else self._car_onroad_layout def _setup_callbacks(self): + self._alerts_layout.set_enabled(lambda: self.enabled) + self._alerts_layout.set_pairing_callback(self._settings_layout.show_pairing) self._home_layout.set_callbacks( on_settings=lambda: gui_app.push_widget(self._settings_layout), on_alerts=lambda: self._scroll_to(self._alerts_layout), diff --git a/openpilot/selfdrive/ui/mici/layouts/offroad_alerts.py b/openpilot/selfdrive/ui/mici/layouts/offroad_alerts.py index 6175a753bb..9ff7b7a7ba 100644 --- a/openpilot/selfdrive/ui/mici/layouts/offroad_alerts.py +++ b/openpilot/selfdrive/ui/mici/layouts/offroad_alerts.py @@ -4,6 +4,7 @@ import pyray as rl import re import threading import time +from collections.abc import Callable from dataclasses import dataclass from enum import IntEnum from openpilot.common.params import Params @@ -232,6 +233,11 @@ class MiciOffroadAlerts(Scroller): def scrolling(self): return self._scroller.scroll_panel.is_touch_valid() + def set_pairing_callback(self, callback: Callable[[], None]): + for alert_item in self.alert_items: + if alert_item.alert_data.key == "Offroad_Pairing": + alert_item.set_click_callback(callback) + def _build_alerts(self): """Build sorted list of alerts from OFFROAD_ALERTS.""" self.sorted_alerts = [] diff --git a/openpilot/selfdrive/ui/mici/layouts/settings/device/device_layout.py b/openpilot/selfdrive/ui/mici/layouts/settings/device/device_layout.py index ff2e814b7d..20b0523444 100644 --- a/openpilot/selfdrive/ui/mici/layouts/settings/device/device_layout.py +++ b/openpilot/selfdrive/ui/mici/layouts/settings/device/device_layout.py @@ -148,6 +148,8 @@ class DeviceLayoutMici(NavScroller): super().__init__() self._fcc_dialog: MiciFccModal | None = None + self._pairing_button = PairBigButton() + self._pending_pairing_grow_animation = False def power_off_callback(): ui_state.params.put_bool("DoShutdown", True, block=True) @@ -192,7 +194,7 @@ class DeviceLayoutMici(NavScroller): self._scroller.add_widgets([ DeviceInfoLayoutMici(), - PairBigButton(), + self._pairing_button, review_training_guide_btn, cabin_cam_btn, terms_btn, @@ -202,6 +204,31 @@ class DeviceLayoutMici(NavScroller): self._power_off_btn, ]) + def _update_state(self): + super()._update_state() + if self._pending_pairing_grow_animation: + btn_right = self._pairing_button.rect.x + self._pairing_button.rect.width + visible_right = self._rect.x + self._rect.width + if btn_right < visible_right + 50: + self._pending_pairing_grow_animation = False + self._pairing_button.trigger_grow_animation() + + def show_event(self): + super().show_event() + if not ui_state.prime_state.is_paired(): + self.set_shown_callback(self._pairing_button.trigger_grow_animation) + + def hide_event(self): + super().hide_event() + self.set_shown_callback(None) + self._pending_pairing_grow_animation = False + + def scroll_to_pairing(self): + self._scroller._layout() + offset = (self._pairing_button.rect.x + self._pairing_button.rect.width / 2) - (self._rect.x + self._rect.width / 2) + self._scroller.scroll_to(offset, smooth=True, block_interrupt=True, block_widget_interaction=True) + self._pending_pairing_grow_animation = True + def _on_regulatory(self): if not self._fcc_dialog: self._fcc_dialog = MiciFccModal(os.path.join(BASEDIR, "openpilot/selfdrive/assets/offroad/mici_fcc.html")) diff --git a/openpilot/selfdrive/ui/mici/layouts/settings/device/prime.py b/openpilot/selfdrive/ui/mici/layouts/settings/device/prime.py index 2d4172a7b6..71a3984990 100644 --- a/openpilot/selfdrive/ui/mici/layouts/settings/device/prime.py +++ b/openpilot/selfdrive/ui/mici/layouts/settings/device/prime.py @@ -124,7 +124,7 @@ class PrimeScroller(NavScroller): self._scroller._show_scroll_indicator = False self._scroller.add_widgets([ self._qr, - GreyBigButton("finish setup", "scan to pair device\nwith connect", + GreyBigButton("finish setup", "scan QR code or visit connect.comma.ai", gui_app.texture("icons_mici/settings/device/green_settings.png", 64, 64)), GreyBigButton("", "connect lets you review recent driving footage and bookmark events."), ]) diff --git a/openpilot/selfdrive/ui/mici/layouts/settings/settings.py b/openpilot/selfdrive/ui/mici/layouts/settings/settings.py index d49358297d..c979fe67a5 100644 --- a/openpilot/selfdrive/ui/mici/layouts/settings/settings.py +++ b/openpilot/selfdrive/ui/mici/layouts/settings/settings.py @@ -28,9 +28,9 @@ class SettingsLayout(NavScroller): network_btn = SettingsBigButton("network", "", gui_app.texture("icons_mici/settings/network/wifi_strength_full.png", 76, 56)) network_btn.set_click_callback(lambda: gui_app.push_widget(network_panel)) - device_panel = DeviceLayoutMici() - device_btn = SettingsBigButton("device", "", gui_app.texture("icons_mici/settings/device_icon.png", 72, 58)) - device_btn.set_click_callback(lambda: gui_app.push_widget(device_panel)) + self._device_panel = DeviceLayoutMici() + self._device_button = SettingsBigButton("device", "", gui_app.texture("icons_mici/settings/device_icon.png", 72, 58)) + self._device_button.set_click_callback(lambda: gui_app.push_widget(self._device_panel)) software_panel = SoftwareLayoutMici() software_btn = SettingsBigButton("software", "", gui_app.texture("icons_mici/settings/software.png", 64, 75)) @@ -47,10 +47,28 @@ class SettingsLayout(NavScroller): self._scroller.add_widgets([ toggles_btn, network_btn, - device_btn, + self._device_button, software_btn, firehose_btn, developer_btn, ]) self._font_medium = gui_app.font(FontWeight.MEDIUM) + + def show_pairing(self): + gui_app.push_widget(self) + # add settings in the back stack without showing its entrance animation. + self._y_pos_filter.x = 0.0 + self.set_visible(lambda: self.enabled or self._device_panel.is_dismissing) + self.set_shown_callback(self._scroll_to_device) + gui_app.push_widget(self._device_panel) + self._device_panel.set_shown_callback(self._on_pairing_shown) + + def _scroll_to_device(self): + self._scroller._layout() + offset = (self._device_button.rect.x + self._device_button.rect.width / 2) - (self._rect.x + self._rect.width / 2) + self._scroller.scroll_to(offset, smooth=False) + + def _on_pairing_shown(self): + self.set_visible(True) + self._device_panel.scroll_to_pairing() diff --git a/openpilot/selfdrive/ui/widgets/offroad_alerts.py b/openpilot/selfdrive/ui/widgets/offroad_alerts.py index 88ccc5df7e..e2b62834ce 100644 --- a/openpilot/selfdrive/ui/widgets/offroad_alerts.py +++ b/openpilot/selfdrive/ui/widgets/offroad_alerts.py @@ -263,6 +263,8 @@ class OffroadAlert(AbstractAlert): def _build_alerts(self): self.sorted_alerts = [] for key, config in sorted(OFFROAD_ALERTS.items(), key=lambda x: x[1].get("severity", 0), reverse=True): + if key == "Offroad_Pairing": + continue severity = config.get("severity", 0) alert_data = AlertData(key=key, text="", severity=severity) self.sorted_alerts.append(alert_data) From a39561058f4270809c27628e37beb27ae41253ce Mon Sep 17 00:00:00 2001 From: stef <19478336+stefpi@users.noreply.github.com> Date: Fri, 18 Sep 2026 17:58:48 -0400 Subject: [PATCH 27/79] ui: allow delay of scroller start (#38958) * delay scroller after pairing QR code * clean --- .../selfdrive/ui/mici/layouts/settings/device/prime.py | 2 +- openpilot/system/ui/widgets/scroller.py | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/openpilot/selfdrive/ui/mici/layouts/settings/device/prime.py b/openpilot/selfdrive/ui/mici/layouts/settings/device/prime.py index 71a3984990..9a914dff4b 100644 --- a/openpilot/selfdrive/ui/mici/layouts/settings/device/prime.py +++ b/openpilot/selfdrive/ui/mici/layouts/settings/device/prime.py @@ -121,7 +121,7 @@ class PrimeScroller(NavScroller): ]) else: self._qr = QR(self._get_pairing_url()) - self._scroller._show_scroll_indicator = False + self._scroller.scroll_indicator_start_after = self._qr self._scroller.add_widgets([ self._qr, GreyBigButton("finish setup", "scan QR code or visit connect.comma.ai", diff --git a/openpilot/system/ui/widgets/scroller.py b/openpilot/system/ui/widgets/scroller.py index e79c4592fc..ebde01c428 100644 --- a/openpilot/system/ui/widgets/scroller.py +++ b/openpilot/system/ui/widgets/scroller.py @@ -97,6 +97,7 @@ class _Scroller(Widget): self._scroll_enabled: bool | Callable[[], bool] = True self._show_scroll_indicator = scroll_indicator and self._horizontal + self.scroll_indicator_start_after: Widget | None = None self._scroll_indicator = ScrollIndicator() self._edge_shadows = edge_shadows and self._horizontal @@ -364,7 +365,14 @@ class _Scroller(Widget): # Draw scroll indicator on top of edge shadows if self._show_scroll_indicator and len(self._visible_items) > 0: - self._scroll_indicator.update(self._scroll_offset, self._content_size, self._rect) + viewport = self._rect + if self.scroll_indicator_start_after is not None: + offset = min(self._rect.width, max(0.0, self.scroll_indicator_start_after.rect.x + + self.scroll_indicator_start_after.rect.width + + self._spacing - self._rect.x)) + viewport = rl.Rectangle(self._rect.x + offset, self._rect.y, self._rect.width - offset, self._rect.height) + + self._scroll_indicator.update(self._scroll_offset, self._content_size, viewport) self._scroll_indicator.render() def show_event(self): From 5ae0da0e465a21cb32b07eac0a987c24b2b7945e Mon Sep 17 00:00:00 2001 From: ZwX1616 Date: Fri, 18 Sep 2026 17:35:52 -0700 Subject: [PATCH 28/79] DM: Super Leicht Model (#38942) --- openpilot/cereal/log.capnp | 1 + openpilot/selfdrive/modeld/models/dmonitoring_model.onnx | 4 ++-- openpilot/selfdrive/monitoring/policy.py | 5 +++++ openpilot/selfdrive/monitoring/test_monitoring.py | 1 + openpilot/selfdrive/test/process_replay/model_replay.py | 1 + .../tools/jotpluggler/layouts/driver-monitoring-debug.json | 2 +- 6 files changed, 11 insertions(+), 3 deletions(-) diff --git a/openpilot/cereal/log.capnp b/openpilot/cereal/log.capnp index d8a761573c..ab53884d53 100644 --- a/openpilot/cereal/log.capnp +++ b/openpilot/cereal/log.capnp @@ -2235,6 +2235,7 @@ struct DriverMonitoringState { pose @0: Bool; eye @1: Bool; phone @2: Bool; + sleep @3: Bool; } struct Pose { diff --git a/openpilot/selfdrive/modeld/models/dmonitoring_model.onnx b/openpilot/selfdrive/modeld/models/dmonitoring_model.onnx index 1e592e2192..1d6527cf2c 100644 --- a/openpilot/selfdrive/modeld/models/dmonitoring_model.onnx +++ b/openpilot/selfdrive/modeld/models/dmonitoring_model.onnx @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dd299afabe7a3e0d04cbe2bd97fdb0c93bba8ad6d3cc3663a0e0ededaf243ac2 -size 7497335 +oid sha256:dee5a294e8afaacc9295ac5d100e00733ecac278e79264b96331e40a3ede1b04 +size 7844499 diff --git a/openpilot/selfdrive/monitoring/policy.py b/openpilot/selfdrive/monitoring/policy.py index 06aca55407..7e4b672d72 100644 --- a/openpilot/selfdrive/monitoring/policy.py +++ b/openpilot/selfdrive/monitoring/policy.py @@ -51,6 +51,7 @@ class DRIVER_MONITOR_SETTINGS: self._SG_THRESHOLD = 0.9 self._BLINK_THRESHOLD = 0.865 self._PHONE_THRESH = 0.5 + self._SLEEP_THRESH = 0.75 self._POSE_PITCH_THRESHOLD = 0.3133 self._POSE_PITCH_THRESHOLD_SLACK = 0.3237 self._POSE_PITCH_THRESHOLD_STRICT = self._POSE_PITCH_THRESHOLD @@ -138,6 +139,7 @@ class DriverMonitoring: self.pose = DriverPose(settings=self.settings) self.blink = DriverBlink() self.phone_prob = 0. + self.sleep_prob = 0. self.alert_level = AlertLevel.none self.always_on = always_on @@ -240,6 +242,7 @@ class DriverMonitoring: self.distracted_types['pose'] = bool((pitch_error > pitch_threshold) or (yaw_error > yaw_threshold)) self.distracted_types['eye'] = bool((self.blink.left + self.blink.right)*0.5 > self.settings._BLINK_THRESHOLD) self.distracted_types['phone'] = bool(self.phone_prob > self.settings._PHONE_THRESH) + self.distracted_types['sleep'] = bool(self.sleep_prob > self.settings._SLEEP_THRESH) def _update_states(self, driver_state, cal_rpy, car_speed, op_engaged, lowspeed, demo_mode=False, steering_angle_deg=0.): rhd_pred = driver_state.wheelOnRightProb @@ -277,6 +280,7 @@ class DriverMonitoring: self.blink.right = driver_data.rightBlinkProb * (driver_data.rightEyeProb > self.settings._EYE_THRESHOLD) \ * (driver_data.sunglassesProb < self.settings._SG_THRESHOLD) self.phone_prob = driver_data.phoneProb + self.sleep_prob = driver_data.sleepProb self._get_distracted_types() self.driver_distracted = any(self.distracted_types.values()) and driver_data.faceProb > self.settings._FACE_THRESHOLD and self.pose.low_std @@ -406,6 +410,7 @@ class DriverMonitoring: dm.visionPolicyState.distractedTypes.pose = self.distracted_types['pose'] dm.visionPolicyState.distractedTypes.eye = self.distracted_types['eye'] dm.visionPolicyState.distractedTypes.phone = self.distracted_types['phone'] + dm.visionPolicyState.distractedTypes.sleep = self.distracted_types['sleep'] dm.visionPolicyState.faceDetected = self.face_detected dm.visionPolicyState.pose.pitch = self.pose.pitch dm.visionPolicyState.pose.yaw = self.pose.yaw diff --git a/openpilot/selfdrive/monitoring/test_monitoring.py b/openpilot/selfdrive/monitoring/test_monitoring.py index 53e40f7019..18fd52f7b0 100644 --- a/openpilot/selfdrive/monitoring/test_monitoring.py +++ b/openpilot/selfdrive/monitoring/test_monitoring.py @@ -25,6 +25,7 @@ def make_msg(face_detected, distracted=False, model_uncertain=False): ds.leftDriverData.facePositionStd = [1.*model_uncertain, 1.*model_uncertain] # TODO: test both separately when e2e is used ds.leftDriverData.phoneProb = 0. + ds.leftDriverData.sleepProb = 0. return ds diff --git a/openpilot/selfdrive/test/process_replay/model_replay.py b/openpilot/selfdrive/test/process_replay/model_replay.py index fb46e83ef0..a4f9fcfb2b 100755 --- a/openpilot/selfdrive/test/process_replay/model_replay.py +++ b/openpilot/selfdrive/test/process_replay/model_replay.py @@ -80,6 +80,7 @@ def generate_report(proposed, master, tmp, commit): (lambda x: get_idx_if_non_empty(x.leftDriverData.faceOrientation, 0), "leftDriverData.faceOrientation0"), (lambda x: get_idx_if_non_empty(x.leftDriverData.leftBlinkProb), "leftDriverData.leftBlinkProb"), (lambda x: get_idx_if_non_empty(x.leftDriverData.phoneProb), "leftDriverData.phoneProb"), + (lambda x: get_idx_if_non_empty(x.leftDriverData.sleepProb), "leftDriverData.sleepProb"), (lambda x: get_idx_if_non_empty(x.rightDriverData.faceProb), "rightDriverData.faceProb"), ], "driverStateV2") diff --git a/openpilot/tools/jotpluggler/layouts/driver-monitoring-debug.json b/openpilot/tools/jotpluggler/layouts/driver-monitoring-debug.json index 3197f40ad8..e82edfbf96 100644 --- a/openpilot/tools/jotpluggler/layouts/driver-monitoring-debug.json +++ b/openpilot/tools/jotpluggler/layouts/driver-monitoring-debug.json @@ -1 +1 @@ -{"current_tab_index": 0, "tabs": [{"name": "tab1", "root": {"children": [{"children": [{"camera_view": "driver", "curves": [], "kind": "camera", "title": "Cabin Camera"}, {"curves": [{"color": "#236bb4", "name": "/driverMonitoringState/alertLevel"}], "title": "..."}], "sizes": [0.5, 0.5], "split": "vertical"}, {"children": [{"curves": [{"color": "#236bb4", "name": "/driverMonitoringState/activePolicy"}], "title": "..."}, {"curves": [{"color": "#236bb4", "name": "/driverMonitoringState/visionPolicyState/faceDetected"}], "title": "..."}, {"curves": [{"color": "#236bb4", "name": "/driverMonitoringState/visionPolicyState/distractedTypes/eye"}, {"color": "#dc5234", "name": "/driverMonitoringState/visionPolicyState/distractedTypes/phone"}, {"color": "#43a047", "name": "/driverMonitoringState/visionPolicyState/distractedTypes/pose"}], "title": "..."}, {"curves": [{"color": "#236bb4", "name": "/driverMonitoringState/visionPolicyState/awarenessPercent"}], "title": "..."}], "sizes": [0.25, 0.25, 0.25, 0.25], "split": "vertical"}], "sizes": [0.5, 0.5], "split": "horizontal"}}]} +{"current_tab_index": 0, "tabs": [{"name": "tab1", "root": {"children": [{"children": [{"camera_view": "driver", "curves": [], "kind": "camera", "title": "Cabin Camera"}, {"curves": [{"color": "#236bb4", "name": "/driverMonitoringState/alertLevel"}], "title": "..."}], "sizes": [0.5, 0.5], "split": "vertical"}, {"children": [{"curves": [{"color": "#236bb4", "name": "/driverMonitoringState/activePolicy"}], "title": "..."}, {"curves": [{"color": "#236bb4", "name": "/driverMonitoringState/visionPolicyState/faceDetected"}], "title": "..."}, {"curves": [{"color": "#236bb4", "name": "/driverMonitoringState/visionPolicyState/distractedTypes/eye"}, {"color": "#dc5234", "name": "/driverMonitoringState/visionPolicyState/distractedTypes/phone"}, {"color": "#8e44ad", "name": "/driverMonitoringState/visionPolicyState/distractedTypes/sleep"}, {"color": "#43a047", "name": "/driverMonitoringState/visionPolicyState/distractedTypes/pose"}], "title": "..."}, {"curves": [{"color": "#236bb4", "name": "/driverMonitoringState/visionPolicyState/awarenessPercent"}], "title": "..."}], "sizes": [0.25, 0.25, 0.25, 0.25], "split": "vertical"}], "sizes": [0.5, 0.5], "split": "horizontal"}}]} From 2eee697f7d3ad80c599e0d08fa6d79c346c96968 Mon Sep 17 00:00:00 2001 From: Trey Moen <50057480+greatgitsby@users.noreply.github.com> Date: Sat, 19 Sep 2026 09:47:49 -0700 Subject: [PATCH 29/79] cabana: improve theme contrast (#38965) * cabana: centralize accessible theme and data colors * cabana: remove color contrast tests * cabana: simplify theme color helpers * cabana: soften inactive message rows in dark mode * cabana: retain original signal tooltip behavior --- openpilot/tools/cabana/ui/chart/chart.cc | 14 +-- openpilot/tools/cabana/ui/chart/sparkline.cc | 5 +- openpilot/tools/cabana/ui/chart/sparkline.h | 2 +- openpilot/tools/cabana/ui/helpoverlay.cc | 4 +- openpilot/tools/cabana/ui/theme.cc | 101 +++++++++++++++--- openpilot/tools/cabana/ui/theme.h | 14 ++- openpilot/tools/cabana/ui/util.cc | 5 +- .../tools/cabana/ui/widgets/binaryview.cc | 33 +++--- .../tools/cabana/ui/widgets/detailwidget.cc | 2 +- .../tools/cabana/ui/widgets/messagebytes.cc | 13 +-- .../tools/cabana/ui/widgets/signalview.cc | 4 +- .../tools/cabana/ui/widgets/videowidget.cc | 18 ++-- 12 files changed, 150 insertions(+), 65 deletions(-) diff --git a/openpilot/tools/cabana/ui/chart/chart.cc b/openpilot/tools/cabana/ui/chart/chart.cc index 1c319b4faf..57d00a2ee2 100644 --- a/openpilot/tools/cabana/ui/chart/chart.cc +++ b/openpilot/tools/cabana/ui/chart/chart.cc @@ -475,7 +475,7 @@ void ChartView::showTip(double sec) { std::string name = s.sig->name; std::string min = s.min == std::numeric_limits::max() ? "--" : utils::toString(s.min); std::string max = s.max == std::numeric_limits::lowest() ? "--" : utils::toString(s.max); - text_list.push_back({.has_marker = true, .marker = toImU32(s.color), .name = name, .value = value, .min = min, .max = max}); + text_list.push_back({.has_marker = true, .marker = toImU32(graphicColor(s.color)), .name = name, .value = value, .min = min, .max = max}); } } if (x < 0) { @@ -620,9 +620,9 @@ void ChartView::drawLegend() { ImGui::PopID(); if (series_type_ == SeriesType::Scatter) { - painter->AddCircleFilled(r.Min + ImVec2(marker_size / 2.0f, 2.0f + marker_size / 2.0f), marker_size / 2.0f, toImU32(s.color)); + painter->AddCircleFilled(r.Min + ImVec2(marker_size / 2.0f, 2.0f + marker_size / 2.0f), marker_size / 2.0f, toImU32(graphicColor(s.color))); } else { - drawColorMarker(painter, r.Min, toImU32(s.color)); + drawColorMarker(painter, r.Min, toImU32(graphicColor(s.color))); } float x = r.Min.x + marker_size + ImGui::GetStyle().ItemInnerSpacing.x; @@ -655,7 +655,7 @@ void ChartView::drawSeries() { const std::string label = "##sig" + std::to_string(i); ImPlotSpec spec; - spec.LineColor = toImVec4(s.color); + spec.LineColor = toImVec4(graphicColor(s.color)); spec.Stride = sizeof(ImPlotPoint); if (series_type_ == SeriesType::Scatter) { float radius = std::clamp(pixels_per_point / 2.0, 2.0, 8.0) / 2.0; @@ -682,7 +682,7 @@ void ChartView::drawSeries() { // show points when zoomed in enough if ((num_points == 1 || pixels_per_point > 20) && first != last) { ImPlotSpec dots; - dots.LineColor = toImVec4(s.color); + dots.LineColor = toImVec4(graphicColor(s.color)); dots.Stride = sizeof(ImPlotPoint); dots.Marker = ImPlotMarker_Circle; dots.MarkerSize = 4; @@ -700,12 +700,12 @@ void ChartView::drawForeground() { for (auto &s : sigs_) { if (!isNull(s.track_pt) && s.visible) { ImVec2 pos(xPos(s.track_pt.x), yPos(s.track_pt.y)); - painter->AddCircleFilled(pos, 5.5f, toImU32(s.color.darker(125))); + painter->AddCircleFilled(pos, 5.5f, toImU32(graphicColor(s.color))); track_line_x = std::max(track_line_x, pos.x); } } if (track_line_x > 0) { - const ImU32 dark_gray = IM_COL32(0x80, 0x80, 0x80, 0xff); + const ImU32 dark_gray = ImGui::GetColorU32(palette().text_disabled); for (float y = layout_.plot_area.Min.y; y < layout_.plot_area.Max.y; y += 8) { painter->AddLine(ImVec2(track_line_x, y), ImVec2(track_line_x, std::min(y + 4, layout_.plot_area.Max.y)), dark_gray, 1.0f); } diff --git a/openpilot/tools/cabana/ui/chart/sparkline.cc b/openpilot/tools/cabana/ui/chart/sparkline.cc index 1e804f2765..d9e5702fb5 100644 --- a/openpilot/tools/cabana/ui/chart/sparkline.cc +++ b/openpilot/tools/cabana/ui/chart/sparkline.cc @@ -100,7 +100,7 @@ void Sparkline::render(const CabanaColor &color, int range, ImVec2 sz, double wi } size = sz; - color_ = toImU32(sparklineColor(color)); + color_ = color; draw_individual_points_ = draw_individual_points; window_end_ = window_end; xscale_ = xscale; @@ -108,7 +108,8 @@ void Sparkline::render(const CabanaColor &color, int range, ImVec2 sz, double wi void Sparkline::draw(ImDrawList *draw_list, ImVec2 pos, ImU32 color) const { if (render_points_.empty()) return; - if (color == 0) color = color_; + // Pick up theme changes while paused. + if (color == 0) color = toImU32(sparklineColor(color_)); // update() only runs when a message of this id arrives, so a slow message would hold the sparkline // still for many frames and then move it in one step. scroll the rendered polyline by the time that diff --git a/openpilot/tools/cabana/ui/chart/sparkline.h b/openpilot/tools/cabana/ui/chart/sparkline.h index 991ecb8d4c..9d5420f4b6 100644 --- a/openpilot/tools/cabana/ui/chart/sparkline.h +++ b/openpilot/tools/cabana/ui/chart/sparkline.h @@ -26,7 +26,7 @@ private: std::vector points_; std::vector render_points_; - ImU32 color_ = 0; + CabanaColor color_; double window_end_ = 0; // the time the polyline was built for, so draw() can scroll it on from there double xscale_ = 0; bool draw_individual_points_ = false; diff --git a/openpilot/tools/cabana/ui/helpoverlay.cc b/openpilot/tools/cabana/ui/helpoverlay.cc index bc64f99196..f89e8b0b7e 100644 --- a/openpilot/tools/cabana/ui/helpoverlay.cc +++ b/openpilot/tools/cabana/ui/helpoverlay.cc @@ -181,7 +181,9 @@ void HelpOverlay::draw() { float x = min.x + 8.0f; for (const auto &r : line) { const float w = run_width(r); - const ImU32 color = r.color ? r.color : ImGui::GetColorU32(ImGuiCol_Text); + const auto background = fromImVec4(r.chip ? palette().button : palette().surface); + const auto source = r.color ? fromImVec4(ImGui::ColorConvertU32ToFloat4(r.color)) : fromImVec4(palette().text); + const ImU32 color = toImU32(contrastColor(source, background, r.swatch ? 3.0 : 4.5)); if (r.swatch) { dl->AddRectFilled(ImVec2(x + 2, y + 3), ImVec2(x + font_size - 2, y + font_size - 1), color); } else { diff --git a/openpilot/tools/cabana/ui/theme.cc b/openpilot/tools/cabana/ui/theme.cc index 45212cb6d7..20cf2919ff 100644 --- a/openpilot/tools/cabana/ui/theme.cc +++ b/openpilot/tools/cabana/ui/theme.cc @@ -12,30 +12,30 @@ namespace fs = std::filesystem; namespace { constexpr Palette DARK_PALETTE = { - .text = rgb(0xffffff), .text_disabled = rgb(0x7b7b7b), .text_selected = rgb(0xffffff), + .text = rgb(0xffffff), .text_disabled = rgb(0xcccccc), .text_inactive = rgb(0xa4a4a4), .text_selected = rgb(0xffffff), .window = rgb(0x353535), .surface = rgb(0x2b2c2d), .frame = rgb(0x2b2c2d), .frame_hovered = rgb(0x363738), .frame_active = rgb(0x434445), .button = rgb(0x3f4041), .button_hovered = rgb(0x525354), .button_active = rgb(0x303132), - .header = rgb(0x2d74d2), .header_hovered = rgb(0x323944), .header_active = rgb(0x2d74d2), - .accent = rgb(0x2d74d2), - .border = rgb(0x606060), .separator = rgb(0x353535), .scrollbar_grab = rgb(0x4c4e4f), - .slider_track = rgb(0x7b7b7b), + .header = rgb(0x2d74d2), .header_hovered = rgb(0x323944), .header_active = rgb(0x434445), + .accent = rgb(0x90bfff), + .border = rgb(0xaaaaaa), .separator = rgb(0x353535), .scrollbar_grab = rgb(0xaaaaaa), + .slider_track = rgb(0xaaaaaa), .tab = rgb(0x2c2c2c), .tab_hovered = rgb(0x444647), .tab_selected = rgb(0x393a3b), .table_header = rgb(0x3f4041), - .grid = rgb(0xdddddd, 50.0f / 255.0f), .badge = rgb(0x808080), + .grid = rgb(0xdddddd, 50.0f / 255.0f), .badge = rgb(0x626262), .sparkline_saturation = 1.0f, .sparkline_value = 1.0f, }; constexpr Palette LIGHT_PALETTE = { - .text = rgb(0x000000), .text_disabled = rgb(0xbebebe), .text_selected = rgb(0xffffff), + .text = rgb(0x000000), .text_disabled = rgb(0x595959), .text_inactive = rgb(0x595959), .text_selected = rgb(0xffffff), .window = rgb(0xefefef), .surface = rgb(0xffffff), .frame = rgb(0xffffff), .frame_hovered = rgb(0xf5f9fc), .frame_active = rgb(0xe7f3fb), .button = rgb(0xefefef), .button_hovered = rgb(0xe7f3fb), .button_active = rgb(0xd4e7f4), - .header = rgb(0x308cc6), .header_hovered = rgb(0xe7f3fb), .header_active = rgb(0x308cc6), - .accent = rgb(0x308cc6), - .border = rgb(0xb6b6b6), .separator = rgb(0xd0d0d0), .scrollbar_grab = rgb(0xb6b6b6), - .slider_track = rgb(0xd0d0d0), + .header = rgb(0x226a99), .header_hovered = rgb(0xe7f3fb), .header_active = rgb(0xd4e7f4), + .accent = rgb(0x226a99), + .border = rgb(0x747474), .separator = rgb(0xd0d0d0), .scrollbar_grab = rgb(0x747474), + .slider_track = rgb(0x747474), .tab = rgb(0xe5e5e5), .tab_hovered = rgb(0xefefef), .tab_selected = rgb(0xffffff), .table_header = rgb(0xefefef), - .grid = rgb(0x000000, 50.0f / 255.0f), .badge = rgb(0xa0a0a4), + .grid = rgb(0x000000, 50.0f / 255.0f), .badge = rgb(0x626262), .sparkline_saturation = 2.0f, .sparkline_value = 0.7f, }; @@ -67,6 +67,36 @@ ImFont *addFont(const fs::path &path, float size) { return font; } +double linearChannel(double value) { + value /= 255.0; + return value <= 0.04045 ? value / 12.92 : std::pow((value + 0.055) / 1.055, 2.4); +} + +double luminance(const CabanaColor &color) { + return 0.2126 * linearChannel(color.r) + 0.7152 * linearChannel(color.g) + 0.0722 * linearChannel(color.b); +} + +// Directed rounding preserves the luminance bound in 8-bit sRGB. +CabanaColor boundLuminance(const CabanaColor &color, double bound, bool minimum) { + const double current = luminance(color); + if (minimum ? current >= bound : current <= bound) return color; + auto channel = [&](uint8_t value) { + double c = linearChannel(value); + c = minimum ? c + (1.0 - c) * (bound - current) / (1.0 - current) : c * bound / current; + const double srgb = c <= 0.0031308 ? 12.92 * c : 1.055 * std::pow(c, 1.0 / 2.4) - 0.055; + const double scaled = std::clamp(srgb * 255.0, 0.0, 255.0); + return static_cast(minimum ? std::ceil(scaled) : std::floor(scaled)); + }; + return {channel(color.r), channel(color.g), channel(color.b), color.a}; +} + +CabanaColor composite(const CabanaColor &color, const CabanaColor &base) { + auto channel = [&](int foreground, int background) { + return static_cast(std::lround((foreground * color.a + background * (255 - color.a)) / 255.0)); + }; + return {channel(color.r, base.r), channel(color.g, base.g), channel(color.b, base.b)}; +} + ImVec4 alpha(ImVec4 c, float a) { return ImVec4(c.x, c.y, c.z, a); } } // namespace @@ -123,7 +153,8 @@ void applyTheme(int theme) { c[ImGuiCol_FrameBgHovered] = p.frame_hovered; c[ImGuiCol_FrameBgActive] = p.frame_active; c[ImGuiCol_Button] = p.button; - c[ImGuiCol_ButtonHovered] = c[ImGuiCol_SliderGrab] = p.button_hovered; + c[ImGuiCol_ButtonHovered] = p.button_hovered; + c[ImGuiCol_SliderGrab] = p.accent; c[ImGuiCol_ScrollbarGrab] = p.scrollbar_grab; c[ImGuiCol_ScrollbarGrabHovered] = p.text_disabled; c[ImGuiCol_ButtonActive] = p.button_active; @@ -157,10 +188,52 @@ void applyTheme(int theme) { const Palette &palette() { return *g_palette; } +CabanaColor contrastColor(CabanaColor color, const CabanaColor &background, double target) { + color.a = 255; + const double value = luminance(color), base = luminance(background); + if ((std::max(value, base) + 0.05) / (std::min(value, base) + 0.05) >= target) return color; + const bool lighter = base < 0.179; + const double bound = lighter ? target * (base + 0.05) - 0.05 : (base + 0.05) / target - 0.05; + return boundLuminance(color, std::clamp(bound, 0.0, 1.0), lighter); +} + +CabanaColor byteColor(const CabanaColor &color) { + const bool dark = palette().text.x > 0.5f; + const auto [h, s, v] = color.hsv(); + auto fill = CabanaColor::fromHsv(h, std::min(s, 0.30f), 0.86f, color.alphaF()); + if (dark) fill = boundLuminance(fill, 0.16, false); // 5:1 with white text + fill = composite(fill, fromImVec4(palette().surface)); + return dark ? fill : composite({255, 255, 255, 40}, fill); +} + +CabanaColor signalFill(const CabanaColor &color, bool defined) { + const bool dark = palette().text.x > 0.5f; + auto fill = composite(color, fromImVec4(palette().surface)); + fill = composite(dark ? CabanaColor(0, 0, 0, 115) : CabanaColor(255, 255, 255, defined ? 20 : 40), fill); + return boundLuminance(fill, 0.18, !dark); // 4.5:1 with the theme's text +} + +CabanaColor signalHighlight(CabanaColor color) { + color.a = 255; + return boundLuminance(boundLuminance(color, 0.175, true), 0.18, false); +} + +CabanaColor signalOutline(CabanaColor color, bool hovered) { + const bool dark = palette().text.x > 0.5f; + if (dark && hovered) color = color.lighter(150); + color.a = 255; + // 3:1 against normal and highlighted fills, including neighboring signals. + return dark ? boundLuminance(color, 0.67, true) : boundLuminance(color, 0.02, false); +} + +CabanaColor graphicColor(const CabanaColor &color, const ImVec4 &background) { + return contrastColor(color, fromImVec4(background), 3.0); +} + CabanaColor sparklineColor(const CabanaColor &color) { const Palette &p = palette(); auto [h, s, v] = color.hsv(); - return CabanaColor::fromHsv(h, std::min(1.0f, s * p.sparkline_saturation), v * p.sparkline_value, color.a / 255.0f); + return graphicColor(CabanaColor::fromHsv(h, std::min(1.0f, s * p.sparkline_saturation), v * p.sparkline_value)); } ImFont *boldFont() { return g_bold_font; } diff --git a/openpilot/tools/cabana/ui/theme.h b/openpilot/tools/cabana/ui/theme.h index 0c565b55c7..4655219505 100644 --- a/openpilot/tools/cabana/ui/theme.h +++ b/openpilot/tools/cabana/ui/theme.h @@ -6,12 +6,12 @@ #include "tools/cabana/core/color.h" struct Palette { - ImVec4 text, text_disabled, text_selected; + ImVec4 text, text_disabled, text_inactive, text_selected; ImVec4 window; // the background behind panels and docked windows ImVec4 surface; // panels, popups, table bodies: what content is drawn on ImVec4 frame, frame_hovered, frame_active; ImVec4 button, button_hovered, button_active; - ImVec4 header, header_hovered, header_active; // selections + ImVec4 header, header_hovered, header_active; // selected fill; unselected hover/press fills ImVec4 accent; ImVec4 border, separator, scrollbar_grab; ImVec4 slider_track; @@ -28,6 +28,10 @@ inline ImVec4 colorRgb(int r, int g, int b, float alpha = 1.0f) { return ImVec4(r / 255.0f, g / 255.0f, b / 255.0f, alpha); } inline ImU32 toImU32(const CabanaColor &c) { return IM_COL32(c.r, c.g, c.b, c.a); } +inline CabanaColor fromImVec4(const ImVec4 &c) { + return {static_cast(std::lround(c.x * 255)), static_cast(std::lround(c.y * 255)), + static_cast(std::lround(c.z * 255)), static_cast(std::lround(c.w * 255))}; +} inline ImVec4 toImVec4(const CabanaColor &c) { return ImVec4(c.r / 255.0f, c.g / 255.0f, c.b / 255.0f, c.a / 255.0f); } inline ImU32 withAlpha(ImU32 c, int alpha) { return (c & ~IM_COL32_A_MASK) | ((ImU32)alpha << IM_COL32_A_SHIFT); } @@ -44,6 +48,12 @@ constexpr float UI_FONT_SIZE = 16.0f; void loadFonts(); void applyTheme(int theme); // Safe to call at runtime. const Palette &palette(); +CabanaColor contrastColor(CabanaColor color, const CabanaColor &background, double target = 4.5); +CabanaColor byteColor(const CabanaColor &color); +CabanaColor signalFill(const CabanaColor &color, bool defined = true); +CabanaColor signalHighlight(CabanaColor color); +CabanaColor signalOutline(CabanaColor color, bool hovered = false); +CabanaColor graphicColor(const CabanaColor &color, const ImVec4 &background = palette().surface); CabanaColor sparklineColor(const CabanaColor &color); ImFont *boldFont(); diff --git a/openpilot/tools/cabana/ui/util.cc b/openpilot/tools/cabana/ui/util.cc index b11ecf28fa..2252355144 100644 --- a/openpilot/tools/cabana/ui/util.cc +++ b/openpilot/tools/cabana/ui/util.cc @@ -94,9 +94,10 @@ bool selectable(const char *label, bool selected, ImGuiSelectableFlags flags, co if (selected) { ImGui::PushStyleColor(ImGuiCol_Text, palette().text_selected); ImGui::PushStyleColor(ImGuiCol_HeaderHovered, palette().header); + ImGui::PushStyleColor(ImGuiCol_HeaderActive, palette().header); } const bool clicked = ImGui::Selectable(label, selected, flags, size); - if (selected) ImGui::PopStyleColor(2); + if (selected) ImGui::PopStyleColor(3); return clicked; } @@ -382,7 +383,7 @@ int tableHeadersRow() { bool viewSelectable(const char *label, bool selected, ImGuiSelectableFlags flags, const ImVec2 &size) { ImGui::PushStyleColor(ImGuiCol_HeaderHovered, selected ? ImGui::GetColorU32(ImGuiCol_Header) : IM_COL32(0, 0, 0, 0)); - ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImGui::GetColorU32(ImGuiCol_Header)); + ImGui::PushStyleColor(ImGuiCol_HeaderActive, selected ? palette().header : palette().header_active); const bool clicked = selectable(label, selected, flags, size); ImGui::PopStyleColor(2); return clicked; diff --git a/openpilot/tools/cabana/ui/widgets/binaryview.cc b/openpilot/tools/cabana/ui/widgets/binaryview.cc index df17944447..9d983b918d 100644 --- a/openpilot/tools/cabana/ui/widgets/binaryview.cc +++ b/openpilot/tools/cabana/ui/widgets/binaryview.cc @@ -22,10 +22,8 @@ const float SMALL_FONT_SIZE = 10.0f; // Inter needs 10 px for a 7 px cap height const int VERTICAL_HEADER_WIDTH = 30; inline int get_bit_pos(const BinaryIndex &index) { return flipBitPos(index.row * 8 + index.column); } -inline ImU32 paletteHighlight() { return ImGui::GetColorU32(ImGuiCol_Header); } inline ImU32 paletteBase() { return ImGui::GetColorU32(ImGuiCol_ChildBg); } -inline ImU32 paletteText(bool active) { return ImGui::GetColorU32(active ? ImGuiCol_Text : ImGuiCol_TextDisabled); } -const ImU32 DARK_GRAY = IM_COL32(128, 128, 128, 255); +inline ImU32 paletteText() { return ImGui::GetColorU32(ImGuiCol_Text); } // JetBrains Mono ships no bold variant, so emulate one by drawing the glyphs again a fraction of a // pixel to the right. Keeps the monospace advance, unlike switching to the proportional bold face. @@ -332,7 +330,7 @@ void BinaryView::draw() { for (int row = 0; row < rows; ++row) { const ImRect r(grid_pos_.x, grid_pos_.y + row * CELL_HEIGHT, grid_pos_.x + VERTICAL_HEADER_WIDTH, grid_pos_.y + (row + 1) * CELL_HEIGHT); painter->AddRectFilled(r.Min, r.Max, ImGui::GetColorU32(ImGuiCol_WindowBg)); // plain header background - drawText(painter, r, std::to_string(row).c_str(), paletteText(true)); + drawText(painter, r, std::to_string(row).c_str(), paletteText()); } for (int row = 0; row < rows; ++row) { for (int column = 0; column < COLUMN_COUNT; ++column) { @@ -457,7 +455,7 @@ void BinaryView::paintCell(ImDrawList *painter, const ImRect &rect, const Binary auto item = &cellAt(index); ImFont *font = ImGui::GetFont(); float font_size = CELL_FONT_SIZE; - ImU32 pen = paletteText(true); + ImU32 pen = paletteText(); if (index.column == HEX_COLUMN) { if (item->valid) { @@ -465,31 +463,30 @@ void BinaryView::paintCell(ImDrawList *painter, const ImRect &rect, const Binary font = ImGui::GetFont(); font_size = ImGui::GetFontSize(); popMonoFont(); - painter->AddRectFilled(rect.Min, rect.Max, toImU32(item->bg_color)); + painter->AddRectFilled(rect.Min, rect.Max, toImU32(byteColor(item->bg_color))); } } else if (isSelected(index)) { - painter->AddRectFilled(rect.Min, rect.Max, resize_sig_ ? toImU32(resize_sig_->color) : paletteHighlight()); + painter->AddRectFilled(rect.Min, rect.Max, toImU32(signalHighlight(resize_sig_ ? resize_sig_->color : fromImVec4(palette().header)))); pen = IM_COL32_WHITE; } else if (!hasSelection() || std::find(item->sigs.begin(), item->sigs.end(), resize_sig_) == item->sigs.end()) { // not resizing if (item->sigs.size() > 0) { for (auto &s : item->sigs) { - if (s == hovered_sig_) { - painter->AddRectFilled(rect.Min, rect.Max, toImU32(s->color.darker(125))); // 4/5x brightness - } else { - drawSignalCell(painter, rect, index, s); - } + drawSignalCell(painter, rect, index, s); } } else if (item->valid) { - if (item->bg_color.alpha() > 0) painter->AddRectFilled(rect.Min, rect.Max, toImU32(item->bg_color)); + if (item->bg_color.alpha() > 0) painter->AddRectFilled(rect.Min, rect.Max, toImU32(signalFill(item->bg_color, false))); } bool bright = std::find(item->sigs.begin(), item->sigs.end(), hovered_sig_) != item->sigs.end(); - pen = bright ? IM_COL32_WHITE : paletteText(is_message_active_); + if (bright) painter->AddRectFilled(rect.Min, rect.Max, toImU32(signalHighlight(hovered_sig_->color))); + pen = bright ? IM_COL32_WHITE : paletteText(); } + const auto text = fromImVec4(ImGui::ColorConvertU32ToFloat4(pen)); + const ImU32 pattern = toImU32(contrastColor({128, 128, 128}, text)); if (item->sigs.size() > 1) { - fillDense7Pattern(painter, rect, DARK_GRAY); + fillDense7Pattern(painter, rect, pattern); } else if (!item->valid) { - fillBDiagPattern(painter, rect, DARK_GRAY); + fillBDiagPattern(painter, rect, pattern); } if (item->valid) { if (index.column == HEX_COLUMN) { @@ -545,13 +542,13 @@ void BinaryView::drawSignalCell(ImDrawList *painter, const ImRect &rect, const B auto item = &cellAt(index); CabanaColor color = sig->color; color.a = item->bg_color.alpha(); - const ImU32 edge = toImU32(sig->color.darker(125)); + const ImU32 edge = toImU32(signalOutline(sig->color)); for (const ImRect &clip : region) { painter->PushClipRect(clip.Min, clip.Max, true); // mix the signal color with the background to fade it painter->AddRectFilled(rc.Min, rc.Max, paletteBase()); - painter->AddRectFilled(rc.Min, rc.Max, toImU32(color)); + painter->AddRectFilled(rc.Min, rc.Max, toImU32(signalFill(color))); if (draw_left) painter->AddLine(ImVec2(rc.Min.x + 0.5f, rc.Min.y), ImVec2(rc.Min.x + 0.5f, rc.Max.y), edge, 1.0f); if (draw_right) painter->AddLine(ImVec2(rc.Max.x - 0.5f, rc.Min.y), ImVec2(rc.Max.x - 0.5f, rc.Max.y), edge, 1.0f); diff --git a/openpilot/tools/cabana/ui/widgets/detailwidget.cc b/openpilot/tools/cabana/ui/widgets/detailwidget.cc index b73464c6e2..67169e5561 100644 --- a/openpilot/tools/cabana/ui/widgets/detailwidget.cc +++ b/openpilot/tools/cabana/ui/widgets/detailwidget.cc @@ -397,7 +397,7 @@ void CenterWidget::drawWelcomeWidget() { ImGui::SetCursorPos(ImVec2(origin.x + (avail.x - size.x) * 0.5f, y)); ImGui::TextUnformatted(text); }; - ImGui::PushStyleColor(ImGuiCol_Text, colorRgb(169, 169, 169)); + ImGui::PushStyleColor(ImGuiCol_Text, palette().text_disabled); float y = origin.y + avail.y * 0.5f - 90.0f; pushLargeFont(); centered("CABANA", y); diff --git a/openpilot/tools/cabana/ui/widgets/messagebytes.cc b/openpilot/tools/cabana/ui/widgets/messagebytes.cc index 442f5e598f..fb66726b70 100644 --- a/openpilot/tools/cabana/ui/widgets/messagebytes.cc +++ b/openpilot/tools/cabana/ui/widgets/messagebytes.cc @@ -26,9 +26,9 @@ ImVec2 bytesCellSize(int n, bool multiple_lines) { ImU32 cellTextColor(bool selected, bool inactive) { if (selected) { const ImU32 text = ImGui::GetColorU32(palette().text_selected); - return inactive ? withAlpha(text, 100) : text; + return text; } - return ImGui::GetColorU32(inactive ? ImGuiCol_TextDisabled : ImGuiCol_Text); + return ImGui::GetColorU32(inactive ? palette().text_inactive : palette().text); } void drawTextCell(ImDrawList *dl, const ImRect &rect, const std::string &text, bool selected, bool inactive, bool align_right) { @@ -48,14 +48,11 @@ void drawBytesCell(ImDrawList *dl, const ImRect &rect, const std::vectorsize() && (*colors)[i].alpha() > 0) { - if (selected) { - pen = ImGui::GetColorU32(ImGuiCol_Text); - dl->AddRectFilled(r.Min, r.Max, ImGui::GetColorU32(ImGuiCol_WindowBg)); - } - dl->AddRectFilled(r.Min, r.Max, toImU32((*colors)[i])); + // Activity backgrounds use the theme's text, including stale values. + pen = ImGui::GetColorU32(ImGuiCol_Text); + dl->AddRectFilled(r.Min, r.Max, toImU32(byteColor((*colors)[i]))); } drawText(dl, r, utils::hexByte(bytes[i]), pen, font, font_size); } diff --git a/openpilot/tools/cabana/ui/widgets/signalview.cc b/openpilot/tools/cabana/ui/widgets/signalview.cc index 921d4b26ab..6562903935 100644 --- a/openpilot/tools/cabana/ui/widgets/signalview.cc +++ b/openpilot/tools/cabana/ui/widgets/signalview.cc @@ -267,7 +267,7 @@ void SignalView::paintCell(ImDrawList *painter, const ImRect &option_rect, const if (item->type == SignalModel::Item::Sig) { // color label ImRect icon_rect(rect.Min.x, rect.Min.y, rect.Min.x + COLOR_LABEL_WIDTH, rect.Max.y); - painter->AddRectFilled(icon_rect.Min, icon_rect.Max, toImU32(item->sig->color.darker(item->highlight ? 125 : 0)), ImGui::GetStyle().FrameRounding); + painter->AddRectFilled(icon_rect.Min, icon_rect.Max, toImU32(item->highlight ? signalHighlight(item->sig->color) : contrastColor(item->sig->color, {0, 0, 0})), ImGui::GetStyle().FrameRounding); drawText(painter, icon_rect, std::to_string(item->row() + 1).c_str(), item->highlight ? IM_COL32_WHITE : IM_COL32_BLACK, nullptr, LABEL_FONT); @@ -276,7 +276,7 @@ void SignalView::paintCell(ImDrawList *painter, const ImRect &option_rect, const if (item->sig->type != cabana::Signal::Type::Normal) { const std::string indicator = multiplexIndicator(item->sig); ImRect indicator_rect(rect.Min.x, rect.Min.y, rect.Min.x + ImGui::CalcTextSize(indicator.c_str()).x, rect.Max.y); - painter->AddRectFilled(indicator_rect.Min, indicator_rect.Max, IM_COL32(160, 160, 164, 255), ImGui::GetStyle().FrameRounding); + painter->AddRectFilled(indicator_rect.Min, indicator_rect.Max, ImGui::GetColorU32(palette().badge), ImGui::GetStyle().FrameRounding); drawElidedText(painter, indicator_rect, indicator, IM_COL32_WHITE, false); rect.Min.x = indicator_rect.Max.x + h_margin * 2; } diff --git a/openpilot/tools/cabana/ui/widgets/videowidget.cc b/openpilot/tools/cabana/ui/widgets/videowidget.cc index 5c8a9ede3c..37b4348fd3 100644 --- a/openpilot/tools/cabana/ui/widgets/videowidget.cc +++ b/openpilot/tools/cabana/ui/widgets/videowidget.cc @@ -425,7 +425,7 @@ void Slider::paint(double thumbnail_time) { groove_rect.Min.y = std::floor(center_y - groove_height / 2); groove_rect.Max.y = groove_rect.Min.y + groove_height; - p->AddRectFilled(groove_rect.Min, groove_rect.Max, timeline_colors[(int)TimelineType::None], groove_height * 0.5f); + p->AddRectFilled(groove_rect.Min, groove_rect.Max, toImU32(graphicColor(fromImVec4(ImGui::ColorConvertU32ToFloat4(timeline_colors[(int)TimelineType::None])), palette().window)), groove_height * 0.5f); double min = minimum() / factor; double max = maximum() / factor; @@ -444,7 +444,7 @@ void Slider::paint(double thumbnail_time) { if (auto replay = getReplay()) { for (const auto &entry : *replay->getTimeline()) { - fillRange(entry.start_time, entry.end_time, timeline_colors[(int)entry.type]); + fillRange(entry.start_time, entry.end_time, toImU32(graphicColor(fromImVec4(ImGui::ColorConvertU32ToFloat4(timeline_colors[(int)entry.type])), palette().window))); } ImU32 empty_color = ImGui::GetColorU32(ImGuiCol_WindowBg, 160 / 255.0f); @@ -531,8 +531,9 @@ void StreamCameraView::draw(const ImVec2 &size, double thumbnail_time) { const char *text = "PAUSED"; const ImVec2 text_size = font->CalcTextSizeA(POINT_16_FONT_SIZE, FLT_MAX, 0.0f, text); const ImVec2 center = rect().GetCenter(); - p->AddText(font, POINT_16_FONT_SIZE, ImVec2(center.x - text_size.x / 2, center.y - text_size.y / 2), - IM_COL32(200, 200, 200, static_cast(255 * 0.7f)), text); + const ImVec2 pos(center.x - text_size.x / 2, center.y - text_size.y / 2); + p->AddRectFilled(ImVec2(pos.x - 4, pos.y - 2), ImVec2(pos.x + text_size.x + 4, pos.y + text_size.y + 2), ImGui::GetColorU32(palette().badge), ImGui::GetStyle().FrameRounding); + p->AddText(font, POINT_16_FONT_SIZE, pos, ImGui::GetColorU32(palette().text_selected), text); } } @@ -583,13 +584,16 @@ void StreamCameraView::drawTime(ImDrawList *p, const ImRect &rect, double second ImFont *font = ImGui::GetFont(); const ImVec2 text_size = font->CalcTextSizeA(POINT_10_FONT_SIZE, FLT_MAX, 0.0f, text); // centered horizontally, above the bottom margin - p->AddText(font, POINT_10_FONT_SIZE, ImVec2(rect.GetCenter().x - text_size.x / 2, rect.Max.y - THUMBNAIL_MARGIN - text_size.y), - IM_COL32_WHITE, text); + const ImVec2 pos(rect.GetCenter().x - text_size.x / 2, rect.Max.y - THUMBNAIL_MARGIN - text_size.y); + p->AddRectFilled(ImVec2(pos.x - 4, pos.y - 2), ImVec2(pos.x + text_size.x + 4, pos.y + text_size.y + 2), ImGui::GetColorU32(palette().badge), ImGui::GetStyle().FrameRounding); + p->AddText(font, POINT_10_FONT_SIZE, pos, ImGui::GetColorU32(palette().text_selected), text); } void StreamCameraView::drawAlert(ImDrawList *p, const ImRect &rect, const Timeline::Entry &alert, float font_size, float rounding) { const ImU32 pen = IM_COL32_WHITE; - ImU32 color = withAlpha(timeline_colors[int(alert.type)], 128); + // Opaque backing keeps alert text readable over every camera frame. + const auto source = fromImVec4(ImGui::ColorConvertU32ToFloat4(timeline_colors[int(alert.type)])); + const ImU32 color = toImU32(contrastColor(source, fromImVec4(palette().text_selected))); std::string text = alert.text1; if (!alert.text2.empty()) text += "\n" + alert.text2; From d375e7ff5da7c1c3e22eac8b768186b444008b08 Mon Sep 17 00:00:00 2001 From: Trey Moen <50057480+greatgitsby@users.noreply.github.com> Date: Sat, 19 Sep 2026 14:58:18 -0700 Subject: [PATCH 30/79] cabana: brighten quiet signals in dark mode (#38969) --- openpilot/tools/cabana/ui/widgets/binaryview.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpilot/tools/cabana/ui/widgets/binaryview.cc b/openpilot/tools/cabana/ui/widgets/binaryview.cc index 9d983b918d..d1ce34ceda 100644 --- a/openpilot/tools/cabana/ui/widgets/binaryview.cc +++ b/openpilot/tools/cabana/ui/widgets/binaryview.cc @@ -541,7 +541,7 @@ void BinaryView::drawSignalCell(ImDrawList *painter, const ImRect &rect, const B auto item = &cellAt(index); CabanaColor color = sig->color; - color.a = item->bg_color.alpha(); + color.a = std::max(item->bg_color.alpha(), palette().text.x > 0.5f ? 96 : 25); const ImU32 edge = toImU32(signalOutline(sig->color)); for (const ImRect &clip : region) { From 680aa6f8681ebc2ff0bd834c62381e5ab0242218 Mon Sep 17 00:00:00 2001 From: Trey Moen <50057480+greatgitsby@users.noreply.github.com> Date: Sat, 19 Sep 2026 15:02:54 -0700 Subject: [PATCH 31/79] cabana: revert quiet signal brightness clamp (#38970) Revert "cabana: brighten quiet signals in dark mode (#38969)" This reverts commit d375e7ff5da7c1c3e22eac8b768186b444008b08. --- openpilot/tools/cabana/ui/widgets/binaryview.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpilot/tools/cabana/ui/widgets/binaryview.cc b/openpilot/tools/cabana/ui/widgets/binaryview.cc index d1ce34ceda..9d983b918d 100644 --- a/openpilot/tools/cabana/ui/widgets/binaryview.cc +++ b/openpilot/tools/cabana/ui/widgets/binaryview.cc @@ -541,7 +541,7 @@ void BinaryView::drawSignalCell(ImDrawList *painter, const ImRect &rect, const B auto item = &cellAt(index); CabanaColor color = sig->color; - color.a = std::max(item->bg_color.alpha(), palette().text.x > 0.5f ? 96 : 25); + color.a = item->bg_color.alpha(); const ImU32 edge = toImU32(signalOutline(sig->color)); for (const ImRect &clip : region) { From ea0afb67a8a82f4661d28f3d5642335912c01022 Mon Sep 17 00:00:00 2001 From: Trey Moen <50057480+greatgitsby@users.noreply.github.com> Date: Sat, 19 Sep 2026 15:24:14 -0700 Subject: [PATCH 32/79] cabana: preserve activity with a brighter baseline (#38971) * cabana: preserve activity above a brighter baseline * cabana: apply signal baseline in both themes --- openpilot/tools/cabana/ui/widgets/binaryview.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpilot/tools/cabana/ui/widgets/binaryview.cc b/openpilot/tools/cabana/ui/widgets/binaryview.cc index 9d983b918d..eb8b77b43c 100644 --- a/openpilot/tools/cabana/ui/widgets/binaryview.cc +++ b/openpilot/tools/cabana/ui/widgets/binaryview.cc @@ -541,7 +541,7 @@ void BinaryView::drawSignalCell(ImDrawList *painter, const ImRect &rect, const B auto item = &cellAt(index); CabanaColor color = sig->color; - color.a = item->bg_color.alpha(); + color.a = 96 + item->bg_color.alpha() * (255 - 96) / 255; const ImU32 edge = toImU32(signalOutline(sig->color)); for (const ImRect &clip : region) { From 95ed0213eca247730fe6c6080d4c82ced74a8db9 Mon Sep 17 00:00:00 2001 From: Trey Moen <50057480+greatgitsby@users.noreply.github.com> Date: Sat, 19 Sep 2026 16:00:59 -0700 Subject: [PATCH 33/79] cabana: refine signal heatmap grid (#38964) * cabana: refine signal heatmap grid * cabana: restore direct signal color blending * cabana: strengthen heatmap signal saturation * Revert "cabana: strengthen heatmap signal saturation" This reverts commit 07ee9664d533f260b5bbfaed1afc4391c795603c. * cabana: remove signal cell gradients --- openpilot/tools/cabana/ui/theme.cc | 4 +- .../tools/cabana/ui/widgets/binaryview.cc | 152 +++++++++--------- .../tools/cabana/ui/widgets/binaryview.h | 1 + 3 files changed, 76 insertions(+), 81 deletions(-) diff --git a/openpilot/tools/cabana/ui/theme.cc b/openpilot/tools/cabana/ui/theme.cc index 20cf2919ff..eda301a566 100644 --- a/openpilot/tools/cabana/ui/theme.cc +++ b/openpilot/tools/cabana/ui/theme.cc @@ -208,8 +208,10 @@ CabanaColor byteColor(const CabanaColor &color) { CabanaColor signalFill(const CabanaColor &color, bool defined) { const bool dark = palette().text.x > 0.5f; + // Constrain the full-strength color before blending to preserve the activity ramp. + if (defined) return composite(boundLuminance(color, 0.18, !dark), fromImVec4(palette().surface)); auto fill = composite(color, fromImVec4(palette().surface)); - fill = composite(dark ? CabanaColor(0, 0, 0, 115) : CabanaColor(255, 255, 255, defined ? 20 : 40), fill); + fill = composite(dark ? CabanaColor(0, 0, 0, 115) : CabanaColor(255, 255, 255, 40), fill); return boundLuminance(fill, 0.18, !dark); // 4.5:1 with the theme's text } diff --git a/openpilot/tools/cabana/ui/widgets/binaryview.cc b/openpilot/tools/cabana/ui/widgets/binaryview.cc index eb8b77b43c..56f67075f4 100644 --- a/openpilot/tools/cabana/ui/widgets/binaryview.cc +++ b/openpilot/tools/cabana/ui/widgets/binaryview.cc @@ -19,7 +19,7 @@ namespace { const int CELL_HEIGHT = 36; const float CELL_FONT_SIZE = UI_FONT_SIZE + 2.0f; const float SMALL_FONT_SIZE = 10.0f; // Inter needs 10 px for a 7 px cap height -const int VERTICAL_HEADER_WIDTH = 30; +const int GRID_COLUMN_COUNT = BinaryView::COLUMN_COUNT + 1; inline int get_bit_pos(const BinaryIndex &index) { return flipBitPos(index.row * 8 + index.column); } inline ImU32 paletteBase() { return ImGui::GetColorU32(ImGuiCol_ChildBg); } @@ -126,7 +126,7 @@ ImVec2 BinaryView::minimumSizeHint() const { pushMonoFont(CELL_FONT_SIZE); const float min_section_size = std::ceil(ImGui::CalcTextSize("FF").x) + 10.0f; popMonoFont(); - return {(min_section_size + 1) * 9 + VERTICAL_HEADER_WIDTH + 2, + return {min_section_size * GRID_COLUMN_COUNT + 2, static_cast(CELL_HEIGHT * std::min(row_count_, 10) + 2)}; } @@ -173,6 +173,8 @@ void BinaryView::highlightPosition(const ImVec2 &pos) { auto item = &cellAt(index); const cabana::Signal *sig = item->sigs.empty() ? nullptr : item->sigs.back(); highlight(sig); + } else { + highlight(nullptr); } } @@ -292,18 +294,18 @@ std::tuple BinaryView::getSelection(BinaryIndex index) { } BinaryIndex BinaryView::indexAt(const ImVec2 &pos) const { - if (column_width_ <= 0 || pos.x < grid_pos_.x + VERTICAL_HEADER_WIDTH || pos.y < grid_pos_.y) return {}; - int column = static_cast((pos.x - grid_pos_.x - VERTICAL_HEADER_WIDTH) / column_width_); + if (column_width_ <= 0 || pos.x < grid_pos_.x + IM_ROUND(column_width_) || pos.y < grid_pos_.y) return {}; + int column = 0; + while (column < COLUMN_COUNT && pos.x >= grid_pos_.x + IM_ROUND((column + 2) * column_width_)) ++column; int row = static_cast((pos.y - grid_pos_.y) / CELL_HEIGHT); if (column >= COLUMN_COUNT || row >= row_count_) return {}; return {row, column}; } ImRect BinaryView::visualRect(const BinaryIndex &index) const { - // sections are integral: round the edges so neighboring cells share them exactly and the cells are - // painted edge to edge with no grid line between them - const float x0 = grid_pos_.x + VERTICAL_HEADER_WIDTH + IM_ROUND(index.column * column_width_); - const float x1 = grid_pos_.x + VERTICAL_HEADER_WIDTH + IM_ROUND((index.column + 1) * column_width_); + // Round shared edges consistently across the byte index, bits, and hex column. + const float x0 = grid_pos_.x + IM_ROUND((index.column + 1) * column_width_); + const float x1 = grid_pos_.x + IM_ROUND((index.column + 2) * column_width_); const float y = grid_pos_.y + index.row * CELL_HEIGHT; return ImRect(x0, y, x1, y + CELL_HEIGHT); } @@ -320,17 +322,16 @@ void BinaryView::draw() { pushMonoFont(CELL_FONT_SIZE); const float min_column_width = std::ceil(ImGui::CalcTextSize("FF").x) + 10.0f; popMonoFont(); - const float width = std::max(ImGui::GetContentRegionAvail().x, VERTICAL_HEADER_WIDTH + min_column_width * COLUMN_COUNT); - column_width_ = std::max(min_column_width, (width - VERTICAL_HEADER_WIDTH) / COLUMN_COUNT); + const float width = std::max(ImGui::GetContentRegionAvail().x, min_column_width * GRID_COLUMN_COUNT); + column_width_ = std::max(min_column_width, width / GRID_COLUMN_COUNT); grid_pos_ = ImGui::GetCursorScreenPos(); ImGui::InvisibleButton("##binary_view", ImVec2(std::max(width, 1.0f), std::max(static_cast(rows * CELL_HEIGHT), 1.0f))); ImDrawList *painter = ImGui::GetWindowDrawList(); painter->AddRectFilled(grid_pos_, ImVec2(grid_pos_.x + width, grid_pos_.y + rows * CELL_HEIGHT), paletteBase()); for (int row = 0; row < rows; ++row) { - const ImRect r(grid_pos_.x, grid_pos_.y + row * CELL_HEIGHT, grid_pos_.x + VERTICAL_HEADER_WIDTH, grid_pos_.y + (row + 1) * CELL_HEIGHT); - painter->AddRectFilled(r.Min, r.Max, ImGui::GetColorU32(ImGuiCol_WindowBg)); // plain header background - drawText(painter, r, std::to_string(row).c_str(), paletteText()); + const ImRect r(grid_pos_.x, grid_pos_.y + row * CELL_HEIGHT, grid_pos_.x + IM_ROUND(column_width_), grid_pos_.y + (row + 1) * CELL_HEIGHT); + drawText(painter, r, std::to_string(row).c_str(), paletteText(), nullptr, CELL_FONT_SIZE); } for (int row = 0; row < rows; ++row) { for (int column = 0; column < COLUMN_COUNT; ++column) { @@ -339,6 +340,29 @@ void BinaryView::draw() { } } + // One consistent grid over every cell; hover never changes its geometry. + const ImU32 grid_color = ImGui::GetColorU32(ImGuiCol_Border, 0.35f); + const float right = grid_pos_.x + IM_ROUND(GRID_COLUMN_COUNT * column_width_); + const float bottom = grid_pos_.y + rows * CELL_HEIGHT; + for (int column = 0; column <= GRID_COLUMN_COUNT; ++column) { + const float x = std::clamp(grid_pos_.x + IM_ROUND(column * column_width_), grid_pos_.x + 0.5f, right - 0.5f); + painter->AddLine(ImVec2(x, grid_pos_.y), ImVec2(x, bottom), grid_color); + } + for (int row = 0; row <= rows; ++row) { + const float y = grid_pos_.y + row * CELL_HEIGHT; + painter->AddLine(ImVec2(grid_pos_.x, y), ImVec2(right, y), grid_color); + } + + // Signal definitions are a separate layer from heatmap activity. + for (int row = 0; row < rows; ++row) { + for (int column = 0; column < HEX_COLUMN; ++column) { + const BinaryIndex index{row, column}; + for (const auto *sig : cellAt(index).sigs) { + if (!hasSelection() || sig != resize_sig_) drawSignalOutline(painter, visualRect(index), index, sig); + } + } + } + const ImVec2 mouse = ImGui::GetMousePos(); const bool hovered = ImGui::IsItemHovered(); const bool active = ImGui::IsItemActive(); @@ -443,19 +467,12 @@ const std::vector> &BinaryView::bitFlipChanges(size_t ms return bit_flip_tracker_.counts.bits; } -bool BinaryView::hasSignal(const BinaryIndex &index, int dx, int dy, const cabana::Signal *sig) const { - if (!index.isValid()) return false; - int idx = (index.row + dy) * COLUMN_COUNT + index.column + dx; - if (idx < 0 || idx >= (int)cells_.size()) return false; - auto &s = cells_[idx].sigs; - return std::find(s.begin(), s.end(), sig) != s.end(); -} - void BinaryView::paintCell(ImDrawList *painter, const ImRect &rect, const BinaryIndex &index) const { auto item = &cellAt(index); ImFont *font = ImGui::GetFont(); float font_size = CELL_FONT_SIZE; ImU32 pen = paletteText(); + const bool hovered = hovered_sig_ && std::find(item->sigs.begin(), item->sigs.end(), hovered_sig_) != item->sigs.end(); if (index.column == HEX_COLUMN) { if (item->valid) { @@ -473,12 +490,12 @@ void BinaryView::paintCell(ImDrawList *painter, const ImRect &rect, const Binary for (auto &s : item->sigs) { drawSignalCell(painter, rect, index, s); } + // Hover covers the entire signal, including bits shared with another definition. + if (hovered) painter->AddRectFilled(rect.Min, rect.Max, toImU32(signalHighlight(hovered_sig_->color))); } else if (item->valid) { if (item->bg_color.alpha() > 0) painter->AddRectFilled(rect.Min, rect.Max, toImU32(signalFill(item->bg_color, false))); } - bool bright = std::find(item->sigs.begin(), item->sigs.end(), hovered_sig_) != item->sigs.end(); - if (bright) painter->AddRectFilled(rect.Min, rect.Max, toImU32(signalHighlight(hovered_sig_->color))); - pen = bright ? IM_COL32_WHITE : paletteText(); + pen = hovered ? IM_COL32_WHITE : paletteText(); } const auto text = fromImVec4(ImGui::ColorConvertU32ToFloat4(pen)); @@ -488,6 +505,7 @@ void BinaryView::paintCell(ImDrawList *painter, const ImRect &rect, const Binary } else if (!item->valid) { fillBDiagPattern(painter, rect, pattern); } + if (item->valid) { if (index.column == HEX_COLUMN) { drawBoldText(painter, rect, utils::hexByte(item->val), pen, font, font_size); @@ -501,64 +519,38 @@ void BinaryView::paintCell(ImDrawList *painter, const ImRect &rect, const Binary } } -// Draw border on edge of signal -void BinaryView::drawSignalCell(ImDrawList *painter, const ImRect &rect, const BinaryIndex &index, const cabana::Signal *sig) const { - bool draw_left = !hasSignal(index, -1, 0, sig); - bool draw_top = !hasSignal(index, 0, -1, sig); - bool draw_right = !hasSignal(index, 1, 0, sig); - bool draw_bottom = !hasSignal(index, 0, 1, sig); +bool BinaryView::hasSignal(const BinaryIndex &index, int dx, int dy, const cabana::Signal *sig) const { + const int row = index.row + dy; + const int column = index.column + dx; + if (row < 0 || row >= row_count_ || column < 0 || column >= HEX_COLUMN) return false; + const auto &signals = cellAt({row, column}).sigs; + return std::find(signals.begin(), signals.end(), sig) != signals.end(); +} - const int spacing = 2; - ImRect rc(rect.Min.x + draw_left * 3, rect.Min.y + draw_top * spacing, rect.Max.x - draw_right * 3, rect.Max.y - draw_bottom * spacing); - std::vector subtract; - if (!draw_top) { - if (!draw_left && !hasSignal(index, -1, -1, sig)) { - subtract.emplace_back(rc.Min.x, rc.Min.y, rc.Min.x + 3, rc.Min.y + spacing); - } else if (!draw_right && !hasSignal(index, 1, -1, sig)) { - subtract.emplace_back(rc.Max.x - 3, rc.Min.y, rc.Max.x, rc.Min.y + spacing); - } - } - if (!draw_bottom) { - if (!draw_left && !hasSignal(index, -1, 1, sig)) { - subtract.emplace_back(rc.Min.x, rc.Max.y - spacing, rc.Min.x + 3, rc.Max.y); - } else if (!draw_right && !hasSignal(index, 1, 1, sig)) { - subtract.emplace_back(rc.Max.x - 3, rc.Max.y - spacing, rc.Max.x, rc.Max.y); - } - } - // rc split into horizontal bands with the notch corners removed: at most one notch in the top band and - // one in the bottom band - const ImRect *top_notch = !subtract.empty() && subtract.front().Min.y == rc.Min.y ? &subtract.front() : nullptr; - const ImRect *bottom_notch = !subtract.empty() && subtract.back().Min.y != rc.Min.y ? &subtract.back() : nullptr; - std::vector region; - auto band = [&](const ImRect *notch, float y0, float y1) { - const float x0 = notch && notch->Min.x == rc.Min.x ? notch->Max.x : rc.Min.x; - const float x1 = notch && notch->Min.x != rc.Min.x ? notch->Min.x : rc.Max.x; - if (x1 > x0 && y1 > y0) region.emplace_back(x0, y0, x1, y1); +void BinaryView::drawSignalOutline(ImDrawList *painter, const ImRect &rect, const BinaryIndex &index, + const cabana::Signal *sig) const { + // Definition boundaries remain visible even at zero activity. Keep them above the + // fills and grid, at the same fixed position before and during hover. + const ImU32 edge = toImU32(signalOutline(sig->color, sig == hovered_sig_)); + const bool left = !hasSignal(index, -1, 0, sig); + const bool right = !hasSignal(index, 1, 0, sig); + const bool top = !hasSignal(index, 0, -1, sig); + const bool bottom = !hasSignal(index, 0, 1, sig); + // Extend one pixel into adjacent cells to join inward corners. + auto offset = [&](bool boundary, int dx, int dy) { + return boundary ? 1.0f : hasSignal(index, dx, dy, sig) ? -1.0f : 0.0f; }; - if (top_notch) band(top_notch, rc.Min.y, rc.Min.y + spacing); - band(nullptr, rc.Min.y + (top_notch ? spacing : 0), rc.Max.y - (bottom_notch ? spacing : 0)); - if (bottom_notch) band(bottom_notch, rc.Max.y - spacing, rc.Max.y); + const ImVec2 min = rect.Min, max = rect.Max; + if (left) painter->AddLine(ImVec2(min.x + 1, min.y + offset(top, -1, -1)), ImVec2(min.x + 1, max.y - offset(bottom, -1, 1)), edge); + if (right) painter->AddLine(ImVec2(max.x - 1, min.y + offset(top, 1, -1)), ImVec2(max.x - 1, max.y - offset(bottom, 1, 1)), edge); + if (top) painter->AddLine(ImVec2(min.x + offset(left, -1, -1), min.y + 1), ImVec2(max.x - offset(right, 1, -1), min.y + 1), edge); + if (bottom) painter->AddLine(ImVec2(min.x + offset(left, -1, 1), max.y - 1), ImVec2(max.x - offset(right, 1, 1), max.y - 1), edge); +} - auto item = &cellAt(index); +// Signal intensity and hover share the same full-cell geometry. +void BinaryView::drawSignalCell(ImDrawList *painter, const ImRect &rect, const BinaryIndex &index, const cabana::Signal *sig) const { CabanaColor color = sig->color; - color.a = 96 + item->bg_color.alpha() * (255 - 96) / 255; - const ImU32 edge = toImU32(signalOutline(sig->color)); - - for (const ImRect &clip : region) { - painter->PushClipRect(clip.Min, clip.Max, true); - // mix the signal color with the background to fade it - painter->AddRectFilled(rc.Min, rc.Max, paletteBase()); - painter->AddRectFilled(rc.Min, rc.Max, toImU32(signalFill(color))); - - if (draw_left) painter->AddLine(ImVec2(rc.Min.x + 0.5f, rc.Min.y), ImVec2(rc.Min.x + 0.5f, rc.Max.y), edge, 1.0f); - if (draw_right) painter->AddLine(ImVec2(rc.Max.x - 0.5f, rc.Min.y), ImVec2(rc.Max.x - 0.5f, rc.Max.y), edge, 1.0f); - if (draw_bottom) painter->AddLine(ImVec2(rc.Min.x, rc.Max.y - 0.5f), ImVec2(rc.Max.x, rc.Max.y - 0.5f), edge, 1.0f); - if (draw_top) painter->AddLine(ImVec2(rc.Min.x, rc.Min.y + 0.5f), ImVec2(rc.Max.x, rc.Min.y + 0.5f), edge, 1.0f); - - // fill gaps inside corners: the 2px stroke is clipped to the region, only the half outside the notch is painted - for (auto &r : subtract) { - painter->AddRect(r.Min, r.Max, edge, 0.0f, 0, 2.0f); - } - painter->PopClipRect(); - } + color.a = 96 + cellAt(index).bg_color.alpha() * (255 - 96) / 255; + painter->AddRectFilled(rect.Min, rect.Max, paletteBase()); + painter->AddRectFilled(rect.Min, rect.Max, toImU32(signalFill(color))); } diff --git a/openpilot/tools/cabana/ui/widgets/binaryview.h b/openpilot/tools/cabana/ui/widgets/binaryview.h index 507d05811c..4f1afa1032 100644 --- a/openpilot/tools/cabana/ui/widgets/binaryview.h +++ b/openpilot/tools/cabana/ui/widgets/binaryview.h @@ -76,6 +76,7 @@ private: void paintCell(ImDrawList *painter, const ImRect &rect, const BinaryIndex &index) const; bool hasSignal(const BinaryIndex &index, int dx, int dy, const cabana::Signal *sig) const; + void drawSignalOutline(ImDrawList *painter, const ImRect &rect, const BinaryIndex &index, const cabana::Signal *sig) const; void drawSignalCell(ImDrawList *painter, const ImRect &rect, const BinaryIndex &index, const cabana::Signal *sig) const; MessageId msg_id_; From 1b1b60ba93bf70d3a37bc306a02048fc71dd0b2c Mon Sep 17 00:00:00 2001 From: Trey Moen <50057480+greatgitsby@users.noreply.github.com> Date: Sat, 19 Sep 2026 16:54:47 -0700 Subject: [PATCH 34/79] cabana: generate dbc files during builds (#38974) --- openpilot/tools/cabana/SConscript | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/openpilot/tools/cabana/SConscript b/openpilot/tools/cabana/SConscript index 0249a019bb..b1ef7602a5 100644 --- a/openpilot/tools/cabana/SConscript +++ b/openpilot/tools/cabana/SConscript @@ -1,14 +1,23 @@ import os +from pathlib import Path import bootstrap_icons import imgui import libusb from openpilot.common.basedir import BASEDIR +from opendbc.dbc.generator.generator import generate_all Import('env', 'arch', 'common', 'messaging', 'visionipc', 'cereal', 'replay_lib', 'ffmpeg_libs') opendbc_path = '-DOPENDBC_FILE_PATH=\'"%s"\'' % (env.Dir("../../../opendbc_repo/opendbc/dbc").abspath) +dbc_dir = Path(env.Dir("#opendbc_repo/opendbc/dbc").abspath) +generated_dbcs = env.Command( + [str(dbc_dir / (name + '.dbc')) for name in sorted(generate_all())], + sorted(str(path) for path in (dbc_dir / 'generator').rglob('*') if path.suffix in ('.py', '.dbc')), + 'python3 -m opendbc.dbc.generator.generator', +) + # embed the bootstrap icons SVG into the binary def build_bootstrap_icons_src(target, source, env): data = open(str(source[0]), 'rb').read() @@ -52,6 +61,7 @@ if arch == "Darwin": else: ui_libs += ['GL', 'dl'] cabana_ui = ui_env.Program('cabana', ui_objs, LIBS=ui_libs) +ui_env.Requires(cabana_ui, generated_dbcs) if GetOption('extras'): cabana_core_test_env = env.Clone() @@ -67,7 +77,8 @@ if GetOption('extras'): cabana_core_test_env.Object('tests/cabana_core_routes', 'routes.cc'), cabana_core_test_env.Object('tests/cabana_core_qtstate', 'ui/qtstate.cc'), ] - cabana_core_test_env.Program('tests/test_cabana', cabana_core_test_objects, LIBS=[replay_lib, common]) + cabana_core_test = cabana_core_test_env.Program('tests/test_cabana', cabana_core_test_objects, LIBS=[replay_lib, common]) + cabana_core_test_env.Requires(cabana_core_test, generated_dbcs) output_json_file = 'openpilot/tools/cabana/dbc/car_fingerprint_to_dbc.json' generate_dbc = ui_env.Command('#' + output_json_file, From 3b2a75a4e08e13a35b6995d0295a96559beddb85 Mon Sep 17 00:00:00 2001 From: Trey Moen <50057480+greatgitsby@users.noreply.github.com> Date: Sat, 19 Sep 2026 18:41:58 -0700 Subject: [PATCH 35/79] cabana: resize heatmap and prevent signal clipping (#38975) * cabana: allow heatmap resizing without clipping * cabana: keep outer grid lines inside the heatmap * cabana: add a visible heatmap resize handle * cabana: use the standard pane divider for resizing --- .../tools/cabana/ui/widgets/binaryview.cc | 4 +-- .../tools/cabana/ui/widgets/detailwidget.cc | 26 ++++++++++++++----- .../tools/cabana/ui/widgets/detailwidget.h | 1 + 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/openpilot/tools/cabana/ui/widgets/binaryview.cc b/openpilot/tools/cabana/ui/widgets/binaryview.cc index 56f67075f4..818f8b42a8 100644 --- a/openpilot/tools/cabana/ui/widgets/binaryview.cc +++ b/openpilot/tools/cabana/ui/widgets/binaryview.cc @@ -348,8 +348,8 @@ void BinaryView::draw() { const float x = std::clamp(grid_pos_.x + IM_ROUND(column * column_width_), grid_pos_.x + 0.5f, right - 0.5f); painter->AddLine(ImVec2(x, grid_pos_.y), ImVec2(x, bottom), grid_color); } - for (int row = 0; row <= rows; ++row) { - const float y = grid_pos_.y + row * CELL_HEIGHT; + for (int row = 0; rows > 0 && row <= rows; ++row) { + const float y = std::clamp(grid_pos_.y + row * CELL_HEIGHT, grid_pos_.y + 0.5f, bottom - 0.5f); painter->AddLine(ImVec2(grid_pos_.x, y), ImVec2(right, y), grid_color); } diff --git a/openpilot/tools/cabana/ui/widgets/detailwidget.cc b/openpilot/tools/cabana/ui/widgets/detailwidget.cc index 67169e5561..6378d2f8ca 100644 --- a/openpilot/tools/cabana/ui/widgets/detailwidget.cc +++ b/openpilot/tools/cabana/ui/widgets/detailwidget.cc @@ -233,16 +233,29 @@ void DetailWidget::drawTabWidget() { page_tabs_.draw(); if (tab_widget_index_ == 0) { if (heatmap_visible_) { - // Keep most of a short window available for signal rows. + // Allow the heatmap to grow while reserving room for signal rows. + const float handle_height = style.DockingSeparatorSize; const float avail = ImGui::GetContentRegionAvail().y; - const float max_height = std::max(avail - style.ItemSpacing.y - SignalView::minimumHeight(), 1.0f); - const float height = std::min(binary_view_->minimumSizeHint().y, max_height); - ImGui::BeginChild("binary_view", ImVec2(0, std::max(height, 1.0f)), ImGuiChildFlags_None, ImGuiWindowFlags_HorizontalScrollbar); + const float max_height = std::max(avail - handle_height - style.ItemSpacing.y * 2 - SignalView::minimumHeight(), 1.0f); + const float min_height = std::min(ImGui::GetFrameHeight(), max_height); + float height = std::clamp(heatmap_height_ < 0 ? binary_view_->minimumSizeHint().y : heatmap_height_, min_height, max_height); + ImGui::BeginChild("binary_view", ImVec2(0, height), ImGuiChildFlags_None, ImGuiWindowFlags_HorizontalScrollbar); binary_view_rect_ = ImGui::GetCurrentWindow()->Rect(); binary_view_->draw(); ImGui::EndChild(); + + const ImVec2 pos = ImGui::GetCursorScreenPos(); + const ImVec2 size(std::max(ImGui::GetContentRegionAvail().x, 1.0f), handle_height); + const ImRect splitter(pos, ImVec2(pos.x + size.x, pos.y + size.y)); + float signal_height = std::max(avail - height - handle_height - style.ItemSpacing.y * 2, SignalView::minimumHeight()); + if (ImGui::SplitterBehavior(splitter, ImGui::GetID("##heatmap_resize"), ImGuiAxis_Y, + &height, &signal_height, min_height, SignalView::minimumHeight(), 4.0f)) { + heatmap_height_ = height; + } + ImGui::Dummy(size); } - ImGui::BeginChild("signal_view", ImVec2(0, 0), ImGuiChildFlags_None, + const float signal_height = std::max(ImGui::GetContentRegionAvail().y, SignalView::minimumHeight()); + ImGui::BeginChild("signal_view", ImVec2(0, signal_height), ImGuiChildFlags_None, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse); signal_view_rect_ = ImGui::GetCurrentWindow()->Rect(); signal_view_->draw(); @@ -254,8 +267,7 @@ void DetailWidget::drawTabWidget() { void DetailWidget::draw() { tabbar_.draw(); - ImGui::BeginChild("message_content", ImVec2(0, 0), ImGuiChildFlags_AlwaysUseWindowPadding, - ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse); + ImGui::BeginChild("message_content", ImVec2(0, 0), ImGuiChildFlags_AlwaysUseWindowPadding); drawToolBar(); if (warning_widget_visible_) { diff --git a/openpilot/tools/cabana/ui/widgets/detailwidget.h b/openpilot/tools/cabana/ui/widgets/detailwidget.h index 6c64a33bf8..f752488ee1 100644 --- a/openpilot/tools/cabana/ui/widgets/detailwidget.h +++ b/openpilot/tools/cabana/ui/widgets/detailwidget.h @@ -88,6 +88,7 @@ private: bool action_remove_msg_enabled_ = false; bool heatmap_live_ = true; bool heatmap_visible_ = true; + float heatmap_height_ = -1.0f; std::string heatmap_all_text_ = "All"; ImRect binary_view_rect_, signal_view_rect_; // child window rects of the last drawTabWidget std::unique_ptr history_log_; From 521db4c825d37eb5f29acf955daa88da003e4433 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Harald=20Sch=C3=A4fer?= Date: Sun, 20 Sep 2026 18:49:17 -0700 Subject: [PATCH 36/79] locationd: reduce initial gyro bias uncertainty (#38981) --- openpilot/selfdrive/locationd/models/pose_kf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpilot/selfdrive/locationd/models/pose_kf.py b/openpilot/selfdrive/locationd/models/pose_kf.py index a8ff80c713..f71503c201 100755 --- a/openpilot/selfdrive/locationd/models/pose_kf.py +++ b/openpilot/selfdrive/locationd/models/pose_kf.py @@ -40,7 +40,7 @@ class PoseKalman(KalmanFilter): initial_P = np.diag([0.01**2, 0.01**2, 0.01**2, 10**2, 10**2, 10**2, 1**2, 1**2, 1**2, - 1**2, 1**2, 1**2, + 0.1**2, 0.1**2, 0.1**2, 100**2, 100**2, 100**2, 0.01**2, 0.01**2, 0.01**2]) From 082c43da392e61a988f8ee6b5f070e140106f2df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Harald=20Sch=C3=A4fer?= Date: Tue, 22 Sep 2026 10:01:32 -0700 Subject: [PATCH 37/79] Chestnut: default GPU power limit to 100W (#38992) modeld: default Chestnut GPU power limit to 100W --- openpilot/selfdrive/modeld/modeld.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpilot/selfdrive/modeld/modeld.py b/openpilot/selfdrive/modeld/modeld.py index effb511e7d..a6d7b9a1a6 100755 --- a/openpilot/selfdrive/modeld/modeld.py +++ b/openpilot/selfdrive/modeld/modeld.py @@ -5,6 +5,7 @@ import ctypes from functools import cached_property import os os.environ['GMMU'] = '0' # for chestnut fast loading, noop for qcom +os.environ.setdefault('AM_POWER_LIMIT', '100') from tinygrad.device import Buffer, Device from tinygrad.dtype import DType, dtypes from tinygrad.engine.realize import lower_and_compile From 66fd3d1bd453312f9f11dc65da423de20f94d297 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Harald=20Sch=C3=A4fer?= Date: Tue, 22 Sep 2026 10:02:10 -0700 Subject: [PATCH 38/79] Ship precompiled Chestnut camera warps (#38991) --- .gitattributes | 2 +- .gitignore | 2 +- openpilot/selfdrive/modeld/SConscript | 43 ++++--------------- openpilot/selfdrive/modeld/modeld.py | 1 + .../big_driving_warp_1344x760_tinygrad.pkl | 3 ++ .../big_driving_warp_1928x1208_tinygrad.pkl | 3 ++ tools/release/build_release.sh | 2 + tools/release/release_files.py | 4 +- tools/release/setup_lfs.sh | 2 +- 9 files changed, 22 insertions(+), 40 deletions(-) create mode 100644 openpilot/selfdrive/modeld/models/big_driving_warp_1344x760_tinygrad.pkl create mode 100644 openpilot/selfdrive/modeld/models/big_driving_warp_1928x1208_tinygrad.pkl diff --git a/.gitattributes b/.gitattributes index ef658d3055..8a66e81e70 100644 --- a/.gitattributes +++ b/.gitattributes @@ -3,7 +3,7 @@ # to move existing files into LFS: # git add --renormalize . *.onnx filter=lfs diff=lfs merge=lfs -text -openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl filter=lfs diff=lfs merge=lfs -text +openpilot/selfdrive/modeld/models/big_*_tinygrad.pkl filter=lfs diff=lfs merge=lfs -text *.svg filter=lfs diff=lfs merge=lfs -text *.png filter=lfs diff=lfs merge=lfs -text *.gif filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore index ff67a1cab8..d267608124 100644 --- a/.gitignore +++ b/.gitignore @@ -50,7 +50,7 @@ st[0-9A-Za-z][0-9A-Za-z][0-9A-Za-z][0-9A-Za-z][0-9A-Za-z][0-9A-Za-z] *.stats *.pkl *.pkl* -!openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl +!openpilot/selfdrive/modeld/models/big_*_tinygrad.pkl config.json compile_commands.json compare_runtime*.html diff --git a/openpilot/selfdrive/modeld/SConscript b/openpilot/selfdrive/modeld/SConscript index 3d1bae9b44..1cf664661c 100644 --- a/openpilot/selfdrive/modeld/SConscript +++ b/openpilot/selfdrive/modeld/SConscript @@ -1,10 +1,8 @@ import glob import os -import time from SCons.Script import Action, Value from openpilot.common.transformations.camera import _ar_ox_fisheye, _os_fisheye from openpilot.common.transformations.model import MEDMODEL_INPUT_SIZE, DM_INPUT_SIZE -from openpilot.selfdrive.modeld.helpers import chestnut_present from openpilot.system.camerad.cameras.nv12_info import get_nv12_info @@ -24,12 +22,6 @@ else: # JIT=2 disables graph batching, which produces incorrect outputs after buffers change. tg_flags = 'DEV=METAL JIT=2' if arch == 'Darwin' else 'DEV=CPU:LLVM' -CHESTNUT = chestnut_present() -if CHESTNUT: - chestnut_tg_flags = 'DEBUG=1 DEV=USB+AMD:LLVM FRAME_DEV=CPU FLOAT16=1 JIT_BATCH_SIZE=0 GMMU=0 TC_OPT=2 TC_MIN_GLOBALS=32' - # the USB+AMD GPU takes an exclusive flock; serialize all targets that touch it - chestnut_lock = File("models/.chestnut.lock").abspath - # tinygrad calls brew which needs a $HOME in the env mac_brew_string = f'HOME={os.path.expanduser("~")}' if arch == 'Darwin' else '' @@ -38,20 +30,6 @@ compiler = Dir('#tinygrad_repo/examples/openpilot').abspath # build on all cores taskset = 'taskset -c 0-7 ' if arch == 'comma_arm64' else '' -def chestnut_action(command): - def do_compile(target, source, env): - from openpilot.system.hardware.chestnut.flash import link_up - # chestnut can enumerate before its PCIe link is up due to varying 12V power behavior across cars - for _ in range(10): - if link_up(): - break - time.sleep(1) - else: - print("Chestnut not ready, skipping warp build") - return - return env.Execute(command) - return Action(do_compile, " [CHESTNUT] $TARGET") - def compile_model(onnx_path, pkl_path): onnx_path, target_pkl_path = File(onnx_path).abspath, File(pkl_path).abspath cmd = (f'{tg_flags} {mac_brew_string} {taskset}python3 "{compiler}/compile_onnx.py" ' @@ -66,19 +44,14 @@ compile_model('models/dmonitoring_model.onnx', 'models/dmonitoring_model_tinygra compile_model('models/driving_supercombo.onnx', 'models/driving_tinygrad.pkl') model_w, model_h = MEDMODEL_INPUT_SIZE -for chestnut in [False, True] if CHESTNUT else [False]: - file_prefix, cmd_flags = ('big_', chestnut_tg_flags) if chestnut else ('', tg_flags) - for cam_w, cam_h in camera_configs: - warp_pkl_path = File(f"models/{file_prefix}driving_warp_{cam_w}x{cam_h}_tinygrad.pkl").abspath - stride, y_height, uv_height, _ = get_nv12_info(cam_w, cam_h) - cmd = (f'{cmd_flags} {mac_brew_string} {taskset}python3 "{compiler}/compile_warp.py" ' - f'--frame {cam_w},{cam_h},{stride},{y_height},{uv_height},{stride * (y_height + uv_height)} ' - f'--warp-to {model_w}x{model_h} --layout yuv420 --frames 2 ' - f'--output {warp_pkl_path}') - action = chestnut_action(cmd) if chestnut else cmd - node = lenv.Command(warp_pkl_path, tinygrad_files + warp_deps + [Value(cmd)], action) - if chestnut: - lenv.SideEffect(chestnut_lock, node) +for cam_w, cam_h in camera_configs: + warp_pkl_path = File(f"models/driving_warp_{cam_w}x{cam_h}_tinygrad.pkl").abspath + stride, y_height, uv_height, _ = get_nv12_info(cam_w, cam_h) + cmd = (f'{tg_flags} {mac_brew_string} {taskset}python3 "{compiler}/compile_warp.py" ' + f'--frame {cam_w},{cam_h},{stride},{y_height},{uv_height},{stride * (y_height + uv_height)} ' + f'--warp-to {model_w}x{model_h} --layout yuv420 --frames 2 ' + f'--output {warp_pkl_path}') + lenv.Command(warp_pkl_path, tinygrad_files + warp_deps + [Value(cmd)], cmd) dm_w, dm_h = DM_INPUT_SIZE for cam_w, cam_h in camera_configs: diff --git a/openpilot/selfdrive/modeld/modeld.py b/openpilot/selfdrive/modeld/modeld.py index a6d7b9a1a6..b79362b7a8 100755 --- a/openpilot/selfdrive/modeld/modeld.py +++ b/openpilot/selfdrive/modeld/modeld.py @@ -155,6 +155,7 @@ class ModelState: self.pack_inputs() with open(MODELS_DIR / f'{"big_" if chestnut else ""}driving_warp_{cam_w}x{cam_h}_tinygrad.pkl', 'rb') as f: self.run_warp = pickle.load(f)['run'] + self.run_warp.captured._linear = lower_and_compile(self.run_warp.captured._linear) self.run_model = jits['run'] self.run_model.captured._linear = lower_and_compile(self.run_model.captured._linear) self.outputs = {name: Tensor(np.zeros(shape, dtype=dtype), device=device).realize() for name, (shape, dtype, device) in jits['output_specs'].items()} diff --git a/openpilot/selfdrive/modeld/models/big_driving_warp_1344x760_tinygrad.pkl b/openpilot/selfdrive/modeld/models/big_driving_warp_1344x760_tinygrad.pkl new file mode 100644 index 0000000000..fdcaba2f00 --- /dev/null +++ b/openpilot/selfdrive/modeld/models/big_driving_warp_1344x760_tinygrad.pkl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6334d6b92a2cc095bfb1430a252aa2ede1902605d0f9ba89224aa5a87c7dc7c0 +size 863472 diff --git a/openpilot/selfdrive/modeld/models/big_driving_warp_1928x1208_tinygrad.pkl b/openpilot/selfdrive/modeld/models/big_driving_warp_1928x1208_tinygrad.pkl new file mode 100644 index 0000000000..91c4096993 --- /dev/null +++ b/openpilot/selfdrive/modeld/models/big_driving_warp_1928x1208_tinygrad.pkl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bda11a9c2e87636db364862ca68ebe4ff9d7baeeb39c0c7e2010b702ed543283 +size 863216 diff --git a/tools/release/build_release.sh b/tools/release/build_release.sh index 5b3b2a3a8a..686c3ab8d7 100755 --- a/tools/release/build_release.sh +++ b/tools/release/build_release.sh @@ -61,6 +61,8 @@ fi if [ -n "$INCLUDE_BIG_MODEL" ]; then test -f openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl + test -f openpilot/selfdrive/modeld/models/big_driving_warp_1344x760_tinygrad.pkl + test -f openpilot/selfdrive/modeld/models/big_driving_warp_1928x1208_tinygrad.pkl fi # Ensure no submodules in release diff --git a/tools/release/release_files.py b/tools/release/release_files.py index caa34d0f39..97145776b6 100755 --- a/tools/release/release_files.py +++ b/tools/release/release_files.py @@ -14,7 +14,7 @@ blacklist = [ "matlab.*.md", - # release scripts configure LFS for the big driving model + # release scripts configure LFS for the big driving model and warps ".lfsconfig", ".gitattributes", ".git$", @@ -31,7 +31,7 @@ if __name__ == "__main__": continue rf = os.fsdecode(tracked_file) - if not os.getenv("INCLUDE_BIG_MODEL") and rf == "openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl": + if not os.getenv("INCLUDE_BIG_MODEL") and rf.startswith("openpilot/selfdrive/modeld/models/big_"): continue blacklisted = any(re.search(p, rf) for p in blacklist) whitelisted = any(re.search(p, rf) for p in whitelist) diff --git a/tools/release/setup_lfs.sh b/tools/release/setup_lfs.sh index 68464af9d6..e2122a87cb 100644 --- a/tools/release/setup_lfs.sh +++ b/tools/release/setup_lfs.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash if [ -n "$INCLUDE_BIG_MODEL" ]; then - echo 'openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl filter=lfs diff=lfs merge=lfs -text' > .gitattributes + echo 'openpilot/selfdrive/modeld/models/big_*_tinygrad.pkl filter=lfs diff=lfs merge=lfs -text' > .gitattributes fi # Reset filters and hooks since releases exclude .venv. From ec9241f2967040ce0ed1f5e6137bca6ab2153ac7 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 22 Sep 2026 15:16:09 -0700 Subject: [PATCH 39/79] Bump submodules (#38996) * bump opendbc * bump panda --- opendbc_repo | 2 +- panda | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/opendbc_repo b/opendbc_repo index a3d3b7c6ca..f975309d21 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit a3d3b7c6ca76ed4c97f606e9b2a0e371e8fa52af +Subproject commit f975309d2144a6f1e3c8e4ad2490c799485e23dd diff --git a/panda b/panda index 5314c84d9e..92eb565169 160000 --- a/panda +++ b/panda @@ -1 +1 @@ -Subproject commit 5314c84d9e8d18aada38bd4fff86b2d42b6eb502 +Subproject commit 92eb565169fd553f4dfaf508c1a3f8dbc14fbfbf From e1862e2e170c0b6618d312aaa482819a1db251eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Harald=20Sch=C3=A4fer?= Date: Tue, 22 Sep 2026 15:21:09 -0700 Subject: [PATCH 40/79] Bump opendbc and panda for Rivian alpha longitudinal (#38995) * Bump opendbc and panda for Rivian alpha longitudinal * Bump opendbc to include Tesla reverts --------- Co-authored-by: Shane Smiskol --- opendbc_repo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opendbc_repo b/opendbc_repo index f975309d21..b6326fb2aa 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit f975309d2144a6f1e3c8e4ad2490c799485e23dd +Subproject commit b6326fb2aa8a8e88adeb2d3f0dec96b0960342e6 From 93eec9007732373cab263069edb49c0b647abb5f Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 22 Sep 2026 15:24:40 -0700 Subject: [PATCH 41/79] Tesla: auto-detect 3-bit steering control type (#38997) bump --- opendbc_repo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opendbc_repo b/opendbc_repo index b6326fb2aa..4134c0d1f5 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit b6326fb2aa8a8e88adeb2d3f0dec96b0960342e6 +Subproject commit 4134c0d1f5e8f695e35ea5fedbe88f6d0c3afb76 From 27fa4fca8e674b51ab3b350896deac0b55d4c41e Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 22 Sep 2026 16:16:21 -0700 Subject: [PATCH 42/79] process replay: new Y route (#38998) * new * PIPE?! * bump --- openpilot/selfdrive/test/process_replay/test_processes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpilot/selfdrive/test/process_replay/test_processes.py b/openpilot/selfdrive/test/process_replay/test_processes.py index 9447ecae19..49767c3090 100755 --- a/openpilot/selfdrive/test/process_replay/test_processes.py +++ b/openpilot/selfdrive/test/process_replay/test_processes.py @@ -36,7 +36,7 @@ source_segments = [ ("MAZDA", "bd6a637565e91581|2021-10-30--15-14-53--4"), # MAZDA.MAZDA_CX9_2021 ("FORD", "54827bf84c38b14f|2023-01-26--21-59-07--4"), # FORD.FORD_BRONCO_SPORT_MK1 ("RIVIAN", "bc095dc92e101734|000000db--ee9fe46e57--1"), # RIVIAN.RIVIAN_R1_GEN1 - ("TESLA", "2c912ca5de3b1ee9|0000025d--6eb6bcbca4--4"), # TESLA.TESLA_MODEL_Y + ("TESLA", "aebd8f1d4ea16066|000002d8--5850d3ea5c--2"), # TESLA.TESLA_MODEL_Y # Enable when port is tested and dashcamOnly is no longer set #("VOLKSWAGEN2", "3cfdec54aa035f3f|2022-07-19--23-45-10--2"), # VOLKSWAGEN.VOLKSWAGEN_PASSAT_NMS @@ -60,7 +60,7 @@ segments = [ ("MAZDA", "regenACF84CCF482|2024-08-30--03-21-55--0"), ("FORD", "regen755D8CB1E1F|2025-04-08--23-13-43--0"), ("RIVIAN", "regen5FCAC896BBE|2025-04-08--23-13-35--0"), - ("TESLA", "2c912ca5de3b1ee9|0000025d--6eb6bcbca4--4"), + ("TESLA", "aebd8f1d4ea16066|000002d8--5850d3ea5c--2"), ] # dashcamOnly makes don't need to be tested until a full port is done From 841076f6d18a6fb951783ed168bed1ddb0685011 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 22 Sep 2026 16:21:36 -0700 Subject: [PATCH 43/79] Revert "process replay: new Y route" (#38999) Revert "process replay: new Y route (#38998)" This reverts commit 27fa4fca8e674b51ab3b350896deac0b55d4c41e. --- openpilot/selfdrive/test/process_replay/test_processes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpilot/selfdrive/test/process_replay/test_processes.py b/openpilot/selfdrive/test/process_replay/test_processes.py index 49767c3090..9447ecae19 100755 --- a/openpilot/selfdrive/test/process_replay/test_processes.py +++ b/openpilot/selfdrive/test/process_replay/test_processes.py @@ -36,7 +36,7 @@ source_segments = [ ("MAZDA", "bd6a637565e91581|2021-10-30--15-14-53--4"), # MAZDA.MAZDA_CX9_2021 ("FORD", "54827bf84c38b14f|2023-01-26--21-59-07--4"), # FORD.FORD_BRONCO_SPORT_MK1 ("RIVIAN", "bc095dc92e101734|000000db--ee9fe46e57--1"), # RIVIAN.RIVIAN_R1_GEN1 - ("TESLA", "aebd8f1d4ea16066|000002d8--5850d3ea5c--2"), # TESLA.TESLA_MODEL_Y + ("TESLA", "2c912ca5de3b1ee9|0000025d--6eb6bcbca4--4"), # TESLA.TESLA_MODEL_Y # Enable when port is tested and dashcamOnly is no longer set #("VOLKSWAGEN2", "3cfdec54aa035f3f|2022-07-19--23-45-10--2"), # VOLKSWAGEN.VOLKSWAGEN_PASSAT_NMS @@ -60,7 +60,7 @@ segments = [ ("MAZDA", "regenACF84CCF482|2024-08-30--03-21-55--0"), ("FORD", "regen755D8CB1E1F|2025-04-08--23-13-43--0"), ("RIVIAN", "regen5FCAC896BBE|2025-04-08--23-13-35--0"), - ("TESLA", "aebd8f1d4ea16066|000002d8--5850d3ea5c--2"), + ("TESLA", "2c912ca5de3b1ee9|0000025d--6eb6bcbca4--4"), ] # dashcamOnly makes don't need to be tested until a full port is done From 8377c40da10606f8f2f59b163250a4cb69ea2354 Mon Sep 17 00:00:00 2001 From: Daniel Koepping Date: Tue, 22 Sep 2026 17:25:29 -0700 Subject: [PATCH 44/79] process replay: push new refs (#39004) --- .../selfdrive/test/process_replay/test_processes.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/openpilot/selfdrive/test/process_replay/test_processes.py b/openpilot/selfdrive/test/process_replay/test_processes.py index 9447ecae19..140a4ee9ca 100755 --- a/openpilot/selfdrive/test/process_replay/test_processes.py +++ b/openpilot/selfdrive/test/process_replay/test_processes.py @@ -74,11 +74,19 @@ EXCLUDED_PROCS = {"modeld", "dmonitoringmodeld"} def run_test_process(data): segment, cfg, args, cur_log_fn, ref_log_path, lr_dat = data - ref_log_msgs = list(LogReader(ref_log_path)) + ref_error = None + try: + ref_log_msgs = list(LogReader(ref_log_path)) + except Exception: + # New routes have no ref yet, still replay and publish new refs. + ref_log_msgs = [] + ref_error = f"Failed to load reference {ref_log_path}:\n{traceback.format_exc()}" lr = LogReader.from_bytes(lr_dat) res, log_msgs = test_process(cfg, lr, segment, ref_log_msgs, cur_log_fn, args.ignore_fields, args.ignore_msgs) # save logs so we can update refs save_log(cur_log_fn, log_msgs) + if ref_error is not None: + return (segment, cfg.proc_name, ref_error, ref_error) try: diff_data = diff_process(cfg, ref_log_msgs, log_msgs) except Exception: From a86351c3efcc72a407f8b16049861661a0904ccb Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 22 Sep 2026 17:27:51 -0700 Subject: [PATCH 45/79] process replay: new Y route (#39000) * new * PIPE?! * bump --------- Co-authored-by: Daniel Koepping --- openpilot/selfdrive/test/process_replay/test_processes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpilot/selfdrive/test/process_replay/test_processes.py b/openpilot/selfdrive/test/process_replay/test_processes.py index 140a4ee9ca..320b97296a 100755 --- a/openpilot/selfdrive/test/process_replay/test_processes.py +++ b/openpilot/selfdrive/test/process_replay/test_processes.py @@ -36,7 +36,7 @@ source_segments = [ ("MAZDA", "bd6a637565e91581|2021-10-30--15-14-53--4"), # MAZDA.MAZDA_CX9_2021 ("FORD", "54827bf84c38b14f|2023-01-26--21-59-07--4"), # FORD.FORD_BRONCO_SPORT_MK1 ("RIVIAN", "bc095dc92e101734|000000db--ee9fe46e57--1"), # RIVIAN.RIVIAN_R1_GEN1 - ("TESLA", "2c912ca5de3b1ee9|0000025d--6eb6bcbca4--4"), # TESLA.TESLA_MODEL_Y + ("TESLA", "aebd8f1d4ea16066|000002d8--5850d3ea5c--2"), # TESLA.TESLA_MODEL_Y # Enable when port is tested and dashcamOnly is no longer set #("VOLKSWAGEN2", "3cfdec54aa035f3f|2022-07-19--23-45-10--2"), # VOLKSWAGEN.VOLKSWAGEN_PASSAT_NMS @@ -60,7 +60,7 @@ segments = [ ("MAZDA", "regenACF84CCF482|2024-08-30--03-21-55--0"), ("FORD", "regen755D8CB1E1F|2025-04-08--23-13-43--0"), ("RIVIAN", "regen5FCAC896BBE|2025-04-08--23-13-35--0"), - ("TESLA", "2c912ca5de3b1ee9|0000025d--6eb6bcbca4--4"), + ("TESLA", "aebd8f1d4ea16066|000002d8--5850d3ea5c--2"), ] # dashcamOnly makes don't need to be tested until a full port is done From f00d226d39e3900b6628f158c2c93738d67c9b18 Mon Sep 17 00:00:00 2001 From: Daniel Koepping Date: Tue, 22 Sep 2026 19:24:51 -0700 Subject: [PATCH 46/79] add max critical alert sound (#39006) Add max critical alert sound after eight seconds --- .../assets/sounds/dm_critical_max.wav | 3 +++ openpilot/selfdrive/ui/soundd.py | 19 +++++++++++++------ 2 files changed, 16 insertions(+), 6 deletions(-) create mode 100644 openpilot/selfdrive/assets/sounds/dm_critical_max.wav diff --git a/openpilot/selfdrive/assets/sounds/dm_critical_max.wav b/openpilot/selfdrive/assets/sounds/dm_critical_max.wav new file mode 100644 index 0000000000..9526364f99 --- /dev/null +++ b/openpilot/selfdrive/assets/sounds/dm_critical_max.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6c4699e3a456a0df2bbfac7ab1eb8972fc8bd970296133fd322e214c61884407 +size 52374 diff --git a/openpilot/selfdrive/ui/soundd.py b/openpilot/selfdrive/ui/soundd.py index 55bf9c8466..aa0f54c5ad 100644 --- a/openpilot/selfdrive/ui/soundd.py +++ b/openpilot/selfdrive/ui/soundd.py @@ -18,7 +18,8 @@ SAMPLE_RATE = 48000 SAMPLE_BUFFER = 4096 # (approx 100ms) MAX_VOLUME = 1.0 MIN_VOLUME = 0.1 -ALERT_RAMP_TIME = 4 # seconds to ramp to max volume for warningImmediate +ALERT_RAMP_TIME = 4 # seconds to ramp critical alerts to max volume +ALERT_MAX_TIME = 8 # seconds before critical alerts switch to the max sound SELFDRIVE_STATE_TIMEOUT = 5 # 5 seconds FILTER_DT = 1. / (micd.SAMPLE_RATE / micd.FFT_SAMPLES) @@ -31,6 +32,7 @@ if HARDWARE.get_device_type() == "tizi": VOLUME_BASE = 10 AudibleAlert = log.SelfdriveState.AudibleAlert +CRITICAL_MAX = -1 # internal sound key, not an AudibleAlert sound_list: dict[int, tuple[str, int | None, float]] = { @@ -47,6 +49,7 @@ sound_list: dict[int, tuple[str, int | None, float]] = { AudibleAlert.warningSoft: ("critical.wav", None, MAX_VOLUME), AudibleAlert.warningImmediate: ("dm_critical.wav", None, MAX_VOLUME), + CRITICAL_MAX: ("dm_critical_max.wav", None, MAX_VOLUME), } def check_selfdrive_timeout_alert(sm): @@ -64,6 +67,7 @@ class Soundd: self.load_sounds() self.current_alert = AudibleAlert.none + self.current_sound = AudibleAlert.none self.current_volume = MIN_VOLUME self.current_sound_frame = 0 @@ -96,7 +100,7 @@ class Soundd: if self.current_alert != AudibleAlert.none: num_loops = sound_list[self.current_alert][1] - sound_data = self.loaded_sounds[self.current_alert] + sound_data = self.loaded_sounds[self.current_sound] written_frames = 0 current_sound_frame = self.current_sound_frame % len(sound_data) @@ -123,7 +127,7 @@ class Soundd: data_out[:frames, 0] = self.get_sound_data(frames) def update_alert(self, new_alert): - current_alert_played_once = self.current_alert == AudibleAlert.none or self.current_sound_frame >= len(self.loaded_sounds[self.current_alert]) + current_alert_played_once = self.current_alert == AudibleAlert.none or self.current_sound_frame >= len(self.loaded_sounds[self.current_sound]) # let looping sounds finish the current loop instead of cutting off mid tone if new_alert == AudibleAlert.none and self.current_alert != AudibleAlert.none and sound_list[self.current_alert][1] is None: if current_alert_played_once: @@ -134,10 +138,11 @@ class Soundd: return self.pending_stop = False if self.current_alert != new_alert and (new_alert != AudibleAlert.none or current_alert_played_once): - if new_alert == AudibleAlert.warningImmediate: + if new_alert in (AudibleAlert.warningSoft, AudibleAlert.warningImmediate): self.ramp_start_volume = self.current_volume self.ramp_start_time = time.monotonic() self.current_alert = new_alert + self.current_sound = new_alert self.current_sound_frame = 0 def get_audible_alert(self, sm): @@ -184,11 +189,13 @@ class Soundd: self.get_audible_alert(sm) - # Ramp up immediate warning sound over 4s - if self.current_alert == AudibleAlert.warningImmediate: + if self.current_alert in (AudibleAlert.warningSoft, AudibleAlert.warningImmediate): elapsed = time.monotonic() - self.ramp_start_time ramp_vol = float(np.interp(elapsed, [0, ALERT_RAMP_TIME], [self.ramp_start_volume, MAX_VOLUME])) self.current_volume = max(self.current_volume, ramp_vol) + if elapsed >= ALERT_MAX_TIME and self.current_sound != CRITICAL_MAX: + self.current_sound = CRITICAL_MAX + self.current_sound_frame = 0 rk.keep_time() From 4dbdd66e0c7120a66a258bf91d74350d3f64715d Mon Sep 17 00:00:00 2001 From: Daniel Koepping Date: Wed, 23 Sep 2026 10:43:03 -0700 Subject: [PATCH 47/79] fix AGNOS updater keyboard crash (#39005) * fix AGNOS updater keyboard across raylib versions * ui: handle raylib 5 keyboard argument errors --- openpilot/common/hardware/comma/updater | 4 ++-- openpilot/system/ui/widgets/mici_keyboard.py | 8 ++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/openpilot/common/hardware/comma/updater b/openpilot/common/hardware/comma/updater index 35227d2d79..241d0fc168 100755 --- a/openpilot/common/hardware/comma/updater +++ b/openpilot/common/hardware/comma/updater @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6a7adb302d378dda7b1788a841b89e4f905c872550a70a76650dcde977b4ece0 -size 24709209 +oid sha256:f92c0a9b41613e063b6a1cd086729f5555d9df6482249e171ed1e276272cad7d +size 24709520 diff --git a/openpilot/system/ui/widgets/mici_keyboard.py b/openpilot/system/ui/widgets/mici_keyboard.py index bc3041aa3c..97a33e95e7 100644 --- a/openpilot/system/ui/widgets/mici_keyboard.py +++ b/openpilot/system/ui/widgets/mici_keyboard.py @@ -353,8 +353,12 @@ class MiciKeyboard(Widget): # draw black circle behind selected key circle_alpha = int(self._selected_key_filter.x * 225) - rl.draw_circle_gradient(rl.Vector2(key_x + key.rect.width / 2, key_y + key.rect.height / 2), - SELECTED_CHAR_FONT_SIZE, rl.Color(0, 0, 0, circle_alpha), rl.BLANK) + try: + rl.draw_circle_gradient(rl.Vector2(key_x + key.rect.width / 2, key_y + key.rect.height / 2), + SELECTED_CHAR_FONT_SIZE, rl.Color(0, 0, 0, circle_alpha), rl.BLANK) + except (TypeError, RuntimeError): # AGNOS updater compatibility for raylib 5 + rl.draw_circle_gradient(int(key_x + key.rect.width / 2), int(key_y + key.rect.height / 2), + SELECTED_CHAR_FONT_SIZE, rl.Color(0, 0, 0, circle_alpha), rl.BLANK) else: # move other keys away from selected key a bit dx = key.original_position.x - self._closest_key[0].original_position.x From c5543b1a05104f50e2a4d51a44b084648aee94f2 Mon Sep 17 00:00:00 2001 From: Daniel Koepping Date: Wed, 23 Sep 2026 15:13:47 -0700 Subject: [PATCH 48/79] modeld: set realtime scheduling after model load (#39007) modeld: defer realtime scheduling until models are loaded --- openpilot/selfdrive/modeld/modeld.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpilot/selfdrive/modeld/modeld.py b/openpilot/selfdrive/modeld/modeld.py index b79362b7a8..4801cb549e 100755 --- a/openpilot/selfdrive/modeld/modeld.py +++ b/openpilot/selfdrive/modeld/modeld.py @@ -2,6 +2,7 @@ from collections.abc import Callable import base64 import ctypes +import gc from functools import cached_property import os os.environ['GMMU'] = '0' # for chestnut fast loading, noop for qcom @@ -234,7 +235,7 @@ def main(demo=False): params.put_bool("ChestnutLoading", CHESTNUT) params.remove("ChestnutActive") - config_realtime_process(7, 54) + gc.disable() # visionipc clients while True: @@ -284,6 +285,8 @@ def main(demo=False): params.put_bool("ChestnutLoading", False) cloudlog.warning(f"models loaded in {time.monotonic() - st:.1f}s, modeld starting") + config_realtime_process(7, 54) + # messaging pub_socks = ["modelV2", "drivingModelData", "cameraOdometry"] + (["chestnutGpuState"] if CHESTNUT else []) pm = PubMaster(pub_socks) From afa47035ba71a608b26282f4f571af921ab6ce08 Mon Sep 17 00:00:00 2001 From: Daniel Koepping Date: Wed, 23 Sep 2026 16:36:47 -0700 Subject: [PATCH 49/79] modeld: check model.chestnut in fallback (#39008) --- openpilot/selfdrive/modeld/modeld.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpilot/selfdrive/modeld/modeld.py b/openpilot/selfdrive/modeld/modeld.py index 4801cb549e..f33684dc47 100755 --- a/openpilot/selfdrive/modeld/modeld.py +++ b/openpilot/selfdrive/modeld/modeld.py @@ -406,7 +406,7 @@ def main(demo=False): run_count % round(ModelConstants.MODEL_RUN_FREQ / SERVICE_LIST['chestnutGpuState'].frequency) == 0) model_output = model.run(bufs, transforms, inputs, chestnut_state.send if send_chestnut else None) except Exception: - if not params.get_bool("ChestnutActive"): + if not model.chestnut: raise # fallback to small model cloudlog.exception("big model failed, fall back to small") From f2c772ff1182d07856e4e415dda7c8806de67954 Mon Sep 17 00:00:00 2001 From: Daniel Koepping Date: Thu, 24 Sep 2026 12:19:05 -0700 Subject: [PATCH 50/79] hardwared: log usb-c connected (#39010) hardwared: log usb cable connected --- openpilot/cereal/log.capnp | 1 + openpilot/common/hardware/usb.py | 5 +++++ openpilot/selfdrive/ui/ui_state.py | 4 ++-- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/openpilot/cereal/log.capnp b/openpilot/cereal/log.capnp index ab53884d53..60461bde61 100644 --- a/openpilot/cereal/log.capnp +++ b/openpilot/cereal/log.capnp @@ -695,6 +695,7 @@ struct PeripheralState { struct UsbState { devices @0 :List(Device); + connected @1 :Bool; struct Device { busnum @0 :UInt8; diff --git a/openpilot/common/hardware/usb.py b/openpilot/common/hardware/usb.py index c3c2050b6b..ba1d4f0cbc 100644 --- a/openpilot/common/hardware/usb.py +++ b/openpilot/common/hardware/usb.py @@ -36,6 +36,10 @@ def read_int(path: Path, base: int = 10) -> int: return 0 +def cable_connected() -> bool: + return read_int(TYPEC_CC_ORIENTATION_PATH) != 0 + + def usb_devices() -> list[Path]: try: devices = (d for d in USB_DEVICES_PATH.glob("*") if (d / "idVendor").exists()) @@ -91,3 +95,4 @@ def set_usb_state(device_state, devices: list[dict]) -> None: chestnut_present = True device_state.chestnutPresent = chestnut_present + device_state.usbState.connected = cable_connected() diff --git a/openpilot/selfdrive/ui/ui_state.py b/openpilot/selfdrive/ui/ui_state.py index ae1fad0b66..08f63228b7 100644 --- a/openpilot/selfdrive/ui/ui_state.py +++ b/openpilot/selfdrive/ui/ui_state.py @@ -12,7 +12,7 @@ from openpilot.common.swaglog import cloudlog from openpilot.selfdrive.ui.lib.prime_state import PrimeState from openpilot.system.ui.lib.application import gui_app from openpilot.common.hardware import HARDWARE, PC -from openpilot.common.hardware.usb import TYPEC_CC_ORIENTATION_PATH, get_usb_state, is_chestnut_usb_id, read_int +from openpilot.common.hardware.usb import cable_connected, get_usb_state, is_chestnut_usb_id from openpilot.selfdrive.modeld.helpers import chestnut_compiled BACKLIGHT_OFFROAD = 65 if HARDWARE.get_device_type() == "mici" else 50 @@ -252,7 +252,7 @@ class UIState: self.chestnut_active = self.params.get("ChestnutActive") self.chestnut_loading = self.params.get_bool("ChestnutLoading") now = time.monotonic() - if read_int(TYPEC_CC_ORIENTATION_PATH) != 0: + if cable_connected(): self.usb_disconnected_ts = None if not self.usb_connected: self.usb_connected = True From bbabd94c11cb160c96a81bfacbd4f4ec97ade9a9 Mon Sep 17 00:00:00 2001 From: Daniel Koepping Date: Thu, 24 Sep 2026 13:24:58 -0700 Subject: [PATCH 51/79] modeld: wait for chestnut to enumerate (#39011) * modeld: wait for chestnut to enumerate * trigger ci --- openpilot/selfdrive/modeld/helpers.py | 9 +++++++++ openpilot/selfdrive/modeld/modeld.py | 6 ++++-- openpilot/selfdrive/ui/ui_state.py | 6 +++--- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/openpilot/selfdrive/modeld/helpers.py b/openpilot/selfdrive/modeld/helpers.py index 0d88e12a6a..9ac772e697 100644 --- a/openpilot/selfdrive/modeld/helpers.py +++ b/openpilot/selfdrive/modeld/helpers.py @@ -1,4 +1,5 @@ import sys +import time from pathlib import Path from openpilot.common.hardware import AGNOS @@ -29,6 +30,14 @@ def chestnut_present() -> bool: pass return False +def wait_for_chestnut(timeout: float = 10.) -> None: + # the cable is detected before chestnut enumerates + st = time.monotonic() + while not chestnut_present(): + if time.monotonic() - st > timeout: + raise TimeoutError("chestnut did not enumerate") + time.sleep(0.1) + def chestnut_compiled() -> bool: path = modeld_pkl_path(chestnut=True) return path.is_file() and all( diff --git a/openpilot/selfdrive/modeld/modeld.py b/openpilot/selfdrive/modeld/modeld.py index f33684dc47..ee76d72f46 100755 --- a/openpilot/selfdrive/modeld/modeld.py +++ b/openpilot/selfdrive/modeld/modeld.py @@ -28,6 +28,7 @@ from msgq.visionipc import VisionIpcClient, VisionBuf from opendbc.car.car_helpers import get_demo_car_params from openpilot.common.swaglog import cloudlog from openpilot.common.params import Params +from openpilot.common.hardware.usb import cable_connected from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.realtime import config_realtime_process, DT_MDL from openpilot.common.transformations.camera import DEVICE_CAMERAS @@ -38,7 +39,7 @@ from openpilot.selfdrive.controls.lib.drive_helpers import get_accel_from_plan, from openpilot.selfdrive.modeld.parse_model_outputs import Parser from openpilot.selfdrive.modeld.fill_model_msg import fill_model_msg, fill_driving_model_data, fill_pose_msg, PublishState from openpilot.selfdrive.modeld.constants import ModelConstants, Plan -from openpilot.selfdrive.modeld.helpers import MODELS_DIR, chestnut_present, chestnut_compiled, modeld_pkl_path, load_oob +from openpilot.selfdrive.modeld.helpers import MODELS_DIR, chestnut_present, chestnut_compiled, modeld_pkl_path, load_oob, wait_for_chestnut SEND_RAW_PRED = os.getenv('SEND_RAW_PRED') @@ -228,7 +229,7 @@ class ModelState: def main(demo=False): cloudlog.warning("modeld init") - CHESTNUT = chestnut_present() and chestnut_compiled() + CHESTNUT = chestnut_compiled() and (chestnut_present() or cable_connected()) if CHESTNUT: os.environ['HCQDEV_WAIT_TIMEOUT_MS'] = '3000' params = Params() @@ -268,6 +269,7 @@ def main(demo=False): def load_big(): nonlocal big_model try: + wait_for_chestnut() m = ModelState(vipc_client_main.width, vipc_client_main.height, True) m.warmup() big_model = m diff --git a/openpilot/selfdrive/ui/ui_state.py b/openpilot/selfdrive/ui/ui_state.py index 08f63228b7..fecafc6b46 100644 --- a/openpilot/selfdrive/ui/ui_state.py +++ b/openpilot/selfdrive/ui/ui_state.py @@ -202,7 +202,7 @@ class UIState: self.status = UIStatus.DISENGAGED self.started_frame = self.sm.frame self.started_time = time.monotonic() - self.chestnut_present = self.sm["deviceState"].chestnutPresent + self.chestnut_present = self.sm["deviceState"].chestnutPresent or (self.chestnut_compiled and self.usb_connected) for callback in self._offroad_transition_callbacks: callback() @@ -222,10 +222,10 @@ class UIState: self.chestnut_state = ChestnutState.DISCONNECTED elif not self.chestnut_compiled: self.chestnut_state = ChestnutState.UNCOMPILED - elif self.chestnut_state == ChestnutState.FAILED or not detected or (model_seen and (not self.sm.alive["modelV2"] or not self.sm["modelV2"].big)): - self.chestnut_state = ChestnutState.FAILED elif self.chestnut_loading or not model_seen: self.chestnut_state = ChestnutState.LOADING + elif self.chestnut_state == ChestnutState.FAILED or not detected or (model_seen and (not self.sm.alive["modelV2"] or not self.sm["modelV2"].big)): + self.chestnut_state = ChestnutState.FAILED elif self.chestnut_active is False: self.chestnut_state = ChestnutState.FAILED else: From 027770d49cbbd136f2573ceaa29925875cc1c291 Mon Sep 17 00:00:00 2001 From: Daniel Koepping Date: Thu, 24 Sep 2026 14:59:23 -0700 Subject: [PATCH 52/79] modeld: fix 3s chestnut timeout (#39012) modeld: restore 3s chestnut timeout --- openpilot/selfdrive/modeld/modeld.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpilot/selfdrive/modeld/modeld.py b/openpilot/selfdrive/modeld/modeld.py index ee76d72f46..0d90f1225e 100755 --- a/openpilot/selfdrive/modeld/modeld.py +++ b/openpilot/selfdrive/modeld/modeld.py @@ -231,7 +231,8 @@ def main(demo=False): CHESTNUT = chestnut_compiled() and (chestnut_present() or cable_connected()) if CHESTNUT: - os.environ['HCQDEV_WAIT_TIMEOUT_MS'] = '3000' + from tinygrad.runtime.ops_amd import AMDDevice + AMDDevice.wait_timeout_ms = 3000 params = Params() params.put_bool("ChestnutLoading", CHESTNUT) params.remove("ChestnutActive") From 46b97e61958e56198b3ad31e66d808698075b1bc Mon Sep 17 00:00:00 2001 From: Amy Jeanes Date: Fri, 25 Sep 2026 19:22:22 +0100 Subject: [PATCH 53/79] ui: match tizi wide/narrow cam speed gates to mici (#39016) ui: match the comma 3X road camera speed gates to the comma 4 Experimental Mode switches the road view to the wide camera below 5 m/s (11 mph) and back to the narrow camera above 10 m/s (22 mph) on both devices. Also corrects the comma 4's mph comments (were 10 and 25 mph). --- openpilot/selfdrive/ui/mici/onroad/augmented_road_view.py | 4 ++-- openpilot/selfdrive/ui/onroad/augmented_road_view.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openpilot/selfdrive/ui/mici/onroad/augmented_road_view.py b/openpilot/selfdrive/ui/mici/onroad/augmented_road_view.py index ddc04d7560..3430d72f61 100644 --- a/openpilot/selfdrive/ui/mici/onroad/augmented_road_view.py +++ b/openpilot/selfdrive/ui/mici/onroad/augmented_road_view.py @@ -31,8 +31,8 @@ class BookmarkState(IntEnum): DRAGGING = 1 TRIGGERED = 2 -WIDE_CAM_MAX_SPEED = 5.0 # m/s (10 mph) -ROAD_CAM_MIN_SPEED = 10 # m/s (25 mph) +WIDE_CAM_MAX_SPEED = 5.0 # m/s (11 mph) +ROAD_CAM_MIN_SPEED = 10.0 # m/s (22 mph) CAM_Y_OFFSET = 20 diff --git a/openpilot/selfdrive/ui/onroad/augmented_road_view.py b/openpilot/selfdrive/ui/onroad/augmented_road_view.py index cae462e41d..a16fb3607b 100644 --- a/openpilot/selfdrive/ui/onroad/augmented_road_view.py +++ b/openpilot/selfdrive/ui/onroad/augmented_road_view.py @@ -25,8 +25,8 @@ BORDER_COLORS = { UIStatus.ENGAGED: rl.Color(0x16, 0x7F, 0x40, 0xFF), # Green for engaged state } -WIDE_CAM_MAX_SPEED = 10.0 # m/s (22 mph) -ROAD_CAM_MIN_SPEED = 15.0 # m/s (34 mph) +WIDE_CAM_MAX_SPEED = 5.0 # m/s (11 mph) +ROAD_CAM_MIN_SPEED = 10.0 # m/s (22 mph) INF_POINT = np.array([1000.0, 0.0, 0.0]) From b78331e042e9b627650c054c06cd054d57fc22da Mon Sep 17 00:00:00 2001 From: Daniel Koepping Date: Fri, 25 Sep 2026 15:29:22 -0700 Subject: [PATCH 54/79] chestnut: offroad alert for model error (#39018) chestnut: offroad alert for big model errors --- openpilot/common/params_keys.h | 1 + openpilot/selfdrive/selfdrived/alerts_offroad.json | 4 ++++ openpilot/system/hardware/chestnut/status.py | 4 ++++ 3 files changed, 9 insertions(+) diff --git a/openpilot/common/params_keys.h b/openpilot/common/params_keys.h index 96bef4daa7..f5e82b1d9d 100644 --- a/openpilot/common/params_keys.h +++ b/openpilot/common/params_keys.h @@ -93,6 +93,7 @@ inline static std::unordered_map keys = { {"ObdMultiplexingEnabled", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}}, {"Offroad_CarUnrecognized", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}}, {"Offroad_ChestnutBranch", {CLEAR_ON_MANAGER_START, JSON}}, + {"Offroad_ChestnutModelError", {CLEAR_ON_MANAGER_START, JSON}}, {"Offroad_ChestnutNotDetected", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}}, {"Offroad_ChestnutOverheated", {CLEAR_ON_MANAGER_START, JSON}}, {"Offroad_ChestnutPcieUnavailable", {CLEAR_ON_MANAGER_START, JSON}}, diff --git a/openpilot/selfdrive/selfdrived/alerts_offroad.json b/openpilot/selfdrive/selfdrived/alerts_offroad.json index c5ee4650a3..66eec0b8a8 100644 --- a/openpilot/selfdrive/selfdrived/alerts_offroad.json +++ b/openpilot/selfdrive/selfdrived/alerts_offroad.json @@ -21,6 +21,10 @@ "text": "Chestnut detected! Switch to the release-chestnut branch to use chestnut-class models.", "severity": -1 }, + "Offroad_ChestnutModelError": { + "text": "Chestnut model error. Restart the car to retry.", + "severity": 0 + }, "Offroad_ChestnutNotDetected": { "text": "Chestnut not detected. Check USB and 12V connections.", "severity": 0 diff --git a/openpilot/system/hardware/chestnut/status.py b/openpilot/system/hardware/chestnut/status.py index 8c38987dff..b088c56b22 100644 --- a/openpilot/system/hardware/chestnut/status.py +++ b/openpilot/system/hardware/chestnut/status.py @@ -23,6 +23,7 @@ class ChestnutStatus: self.link_failures = 0 self.model_loading_seen = False self.model_attempted = False + self.model_failed = False self.overheated = False self.usb_seen = False self.usb_failed = False @@ -42,11 +43,13 @@ class ChestnutStatus: self.link_failures = 0 self.model_loading_seen = False self.model_attempted = False + self.model_failed = False self.usb_seen = firmware_ok self.usb_failed = False self.model_loading_seen |= model_loading self.model_attempted |= self.model_loading_seen and not model_loading and model_active is not None + self.model_failed |= not offroad and self.model_attempted and model_active is False if not offroad and self.usb_seen and not firmware_ok: self.usb_failed = True @@ -91,6 +94,7 @@ class ChestnutStatus: else: pcie_alert = "Chestnut GPU unavailable. PCIe link is not up. Check the GPU is securely seated." set_alert("Offroad_ChestnutPcieUnavailable", self.pcie_failed, pcie_alert) + set_alert("Offroad_ChestnutModelError", self.model_failed and not (missing or self.pcie_failed)) set_alert("Offroad_ChestnutUncompiled", offroad and firmware_ok and not chestnut_compiled()) set_alert("Offroad_ChestnutUpdateFailed", offroad and firmware_failed) self.offroad = offroad From f474f059289295280c6f6c96fca3cbadd622f2a0 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Fri, 25 Sep 2026 16:12:28 -0700 Subject: [PATCH 55/79] modeld: fix failed build on PCs without LLVM (#39019) --- openpilot/selfdrive/modeld/SConscript | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/openpilot/selfdrive/modeld/SConscript b/openpilot/selfdrive/modeld/SConscript index 1cf664661c..2959d92e02 100644 --- a/openpilot/selfdrive/modeld/SConscript +++ b/openpilot/selfdrive/modeld/SConscript @@ -1,3 +1,4 @@ +import ctypes.util import glob import os from SCons.Script import Action, Value @@ -7,6 +8,13 @@ from openpilot.system.camerad.cameras.nv12_info import get_nv12_info Import('env', 'arch') + +# TODO: add GCC support to tinygrad? +# tinygrad's CPU:LLVM backend requires libLLVM, skip the build on PC if it's not available +if arch not in ('comma_arm64', 'Darwin') and ctypes.util.find_library('LLVM') is None: + print("modeld: LLVM not found, skipping tinygrad model compilation") + Return() + lenv = env.Clone() lenv.PrependENVPath('PYTHONPATH', Dir('#tinygrad_repo').abspath) From 49bbba371c29c5612e69b8ea9f906e151410e8bc Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Fri, 25 Sep 2026 16:15:25 -0700 Subject: [PATCH 56/79] add color to modeld warning print --- openpilot/selfdrive/modeld/SConscript | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpilot/selfdrive/modeld/SConscript b/openpilot/selfdrive/modeld/SConscript index 2959d92e02..c8892ef912 100644 --- a/openpilot/selfdrive/modeld/SConscript +++ b/openpilot/selfdrive/modeld/SConscript @@ -12,7 +12,7 @@ Import('env', 'arch') # TODO: add GCC support to tinygrad? # tinygrad's CPU:LLVM backend requires libLLVM, skip the build on PC if it's not available if arch not in ('comma_arm64', 'Darwin') and ctypes.util.find_library('LLVM') is None: - print("modeld: LLVM not found, skipping tinygrad model compilation") + print("\033[93mmodeld: LLVM not found, skipping tinygrad model compilation. Install LLVM to compile models.\033[0m") Return() lenv = env.Clone() From 134517ceb39fc98098823d1ba9130ee91573f03d Mon Sep 17 00:00:00 2001 From: Daniel Koepping Date: Mon, 28 Sep 2026 16:21:29 -0700 Subject: [PATCH 57/79] AGNOS 19.9 (#39026) --- launch_env.sh | 2 +- openpilot/common/hardware/comma/agnos.json | 24 +++++++++++----------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/launch_env.sh b/launch_env.sh index 0cb06e7e5b..0f913f287a 100755 --- a/launch_env.sh +++ b/launch_env.sh @@ -16,7 +16,7 @@ export VECLIB_MAXIMUM_THREADS=1 export QCOM_PRIORITY=12 if [ -z "$AGNOS_VERSION" ]; then - export AGNOS_VERSION="19.8" + export AGNOS_VERSION="19.9" fi export STAGING_ROOT="/data/safe_staging" diff --git a/openpilot/common/hardware/comma/agnos.json b/openpilot/common/hardware/comma/agnos.json index 92c6a4c7cb..96803bdb85 100644 --- a/openpilot/common/hardware/comma/agnos.json +++ b/openpilot/common/hardware/comma/agnos.json @@ -56,29 +56,29 @@ }, { "name": "boot", - "url": "https://commadist.azureedge.net/agnosupdate/boot-335c17579879614a3b6f9b58c867aa2e66609530258e12d9174d6007494fdb11.img.xz", - "hash": "335c17579879614a3b6f9b58c867aa2e66609530258e12d9174d6007494fdb11", - "hash_raw": "335c17579879614a3b6f9b58c867aa2e66609530258e12d9174d6007494fdb11", - "size": 46897152, + "url": "https://commadist.azureedge.net/agnosupdate/boot-b9c9b9261fca0cef9e6d9b198fa4b563767e6342afc7a61ab137b2b519260615.img.xz", + "hash": "b9c9b9261fca0cef9e6d9b198fa4b563767e6342afc7a61ab137b2b519260615", + "hash_raw": "b9c9b9261fca0cef9e6d9b198fa4b563767e6342afc7a61ab137b2b519260615", + "size": 46962688, "sparse": false, "full_check": true, "has_ab": true, - "ondevice_hash": "545d64ff38626d0aff49054cdb7abc735992178f4872fd26ac544d652af38544" + "ondevice_hash": "3ef4769bb1cc73bfc31f96f073b46af42dcc0bad4ab2dde32d11929745d0cd28" }, { "name": "system", - "url": "https://commadist.azureedge.net/agnosupdate/system-20d7a522190ba8b6351364beca9702f40303651577d7c3f6c2c1cb6b34e5db6b.img.xz", - "hash": "62c6ecab2772f48b90dd2e7f0c3bb413dd7577d1ae9aad971135ce0d9d141544", - "hash_raw": "20d7a522190ba8b6351364beca9702f40303651577d7c3f6c2c1cb6b34e5db6b", + "url": "https://commadist.azureedge.net/agnosupdate/system-80d9373348ddff2a5d1f534fab3420383939c96f3eba60baab6b15339fc602c7.img.xz", + "hash": "9aebca09b9d142718e71efbfe44b723010eec6e70067c043c63186067291739d", + "hash_raw": "80d9373348ddff2a5d1f534fab3420383939c96f3eba60baab6b15339fc602c7", "size": 4718592000, "sparse": true, "full_check": false, "has_ab": true, - "ondevice_hash": "14251b982859824ea12f5e5c63b336b6d6333779d42457aff7e43bbf9c9fd79b", + "ondevice_hash": "270b76c5c5568828e3883390b83ea445425a104dd42119d68f06093ce6b6fcef", "alt": { - "hash": "20d7a522190ba8b6351364beca9702f40303651577d7c3f6c2c1cb6b34e5db6b", - "url": "https://commadist.azureedge.net/agnosupdate/system-20d7a522190ba8b6351364beca9702f40303651577d7c3f6c2c1cb6b34e5db6b.img", + "hash": "80d9373348ddff2a5d1f534fab3420383939c96f3eba60baab6b15339fc602c7", + "url": "https://commadist.azureedge.net/agnosupdate/system-80d9373348ddff2a5d1f534fab3420383939c96f3eba60baab6b15339fc602c7.img", "size": 4718592000 } } -] +] \ No newline at end of file From 0c464e5c66418f11e9cdbcea824b10b97dad37dd Mon Sep 17 00:00:00 2001 From: Daniel Koepping Date: Mon, 28 Sep 2026 18:24:41 -0700 Subject: [PATCH 58/79] rm chestnut uncompiled alert (#39030) --- openpilot/common/params_keys.h | 1 - openpilot/selfdrive/selfdrived/alerts_offroad.json | 4 ---- openpilot/selfdrive/ui/ui_state.py | 2 -- openpilot/system/hardware/chestnut/status.py | 2 -- 4 files changed, 9 deletions(-) diff --git a/openpilot/common/params_keys.h b/openpilot/common/params_keys.h index f5e82b1d9d..6a334a7020 100644 --- a/openpilot/common/params_keys.h +++ b/openpilot/common/params_keys.h @@ -97,7 +97,6 @@ inline static std::unordered_map keys = { {"Offroad_ChestnutNotDetected", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}}, {"Offroad_ChestnutOverheated", {CLEAR_ON_MANAGER_START, JSON}}, {"Offroad_ChestnutPcieUnavailable", {CLEAR_ON_MANAGER_START, JSON}}, - {"Offroad_ChestnutUncompiled", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}}, {"Offroad_ChestnutUpdateFailed", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}}, {"Offroad_ChestnutUsbSlow", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}}, {"Offroad_ConnectivityNeeded", {CLEAR_ON_MANAGER_START, JSON}}, diff --git a/openpilot/selfdrive/selfdrived/alerts_offroad.json b/openpilot/selfdrive/selfdrived/alerts_offroad.json index 66eec0b8a8..0e99aa5418 100644 --- a/openpilot/selfdrive/selfdrived/alerts_offroad.json +++ b/openpilot/selfdrive/selfdrived/alerts_offroad.json @@ -37,10 +37,6 @@ "text": "%1", "severity": 0 }, - "Offroad_ChestnutUncompiled": { - "text": "Chestnut model not compiled. Keep ignition on and reboot the comma.", - "severity": 0 - }, "Offroad_ChestnutUpdateFailed": { "text": "Chestnut update failed. Check the USB cable.", "severity": 0 diff --git a/openpilot/selfdrive/ui/ui_state.py b/openpilot/selfdrive/ui/ui_state.py index fecafc6b46..9489673039 100644 --- a/openpilot/selfdrive/ui/ui_state.py +++ b/openpilot/selfdrive/ui/ui_state.py @@ -247,8 +247,6 @@ class UIState: self.always_on_dm = self.params.get_bool("AlwaysOnDM") self.experimental_mode = self.params.get_bool("ExperimentalMode") self.experimental_mode_confirmed = self.params.get_bool("ExperimentalModeConfirmed") - if not self.chestnut_compiled: - self.chestnut_compiled = chestnut_compiled() self.chestnut_active = self.params.get("ChestnutActive") self.chestnut_loading = self.params.get_bool("ChestnutLoading") now = time.monotonic() diff --git a/openpilot/system/hardware/chestnut/status.py b/openpilot/system/hardware/chestnut/status.py index b088c56b22..186e043262 100644 --- a/openpilot/system/hardware/chestnut/status.py +++ b/openpilot/system/hardware/chestnut/status.py @@ -1,7 +1,6 @@ import time from openpilot.common.hardware.usb import CHESTNUT_USB_PRODUCT, is_chestnut_usb_id -from openpilot.selfdrive.modeld.helpers import chestnut_compiled CHESTNUT_RELEASE_BRANCHES = ("release-chestnut", "release-chestnut-staging", "nightly-chestnut") @@ -95,6 +94,5 @@ class ChestnutStatus: pcie_alert = "Chestnut GPU unavailable. PCIe link is not up. Check the GPU is securely seated." set_alert("Offroad_ChestnutPcieUnavailable", self.pcie_failed, pcie_alert) set_alert("Offroad_ChestnutModelError", self.model_failed and not (missing or self.pcie_failed)) - set_alert("Offroad_ChestnutUncompiled", offroad and firmware_ok and not chestnut_compiled()) set_alert("Offroad_ChestnutUpdateFailed", offroad and firmware_failed) self.offroad = offroad From c8fb906815530460ed156f14e09e1f312bb0f851 Mon Sep 17 00:00:00 2001 From: Daniel Koepping Date: Mon, 28 Sep 2026 19:18:41 -0700 Subject: [PATCH 59/79] build nightly-chestnut-dev (#39031) --- Jenkinsfile | 15 ++++++++++++--- openpilot/common/version.py | 4 ++-- openpilot/system/hardware/chestnut/status.py | 2 +- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 576daba734..5facce903f 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -242,9 +242,18 @@ node { } if (env.BRANCH_NAME == '__nightly-chestnut') { - deviceStage("build nightly-chestnut", "mici-chestnut-ci", ["CHESTNUT=1"], [ - step("build nightly-chestnut", "SCONSFLAGS=-j4 INCLUDE_BIG_MODEL=1 PANDA_DEBUG_BUILD=1 RELEASE_BRANCH=nightly-chestnut $SOURCE_DIR/tools/release/build_release.sh TestChestnutOnroad"), - ]) + parallel ( + 'nightly-chestnut': { + deviceStage("build nightly-chestnut", "mici-chestnut-ci", ["CHESTNUT=1"], [ + step("build nightly-chestnut", "SCONSFLAGS=-j4 INCLUDE_BIG_MODEL=1 RELEASE_BRANCH=nightly-chestnut $SOURCE_DIR/tools/release/build_release.sh TestChestnutOnroad"), + ]) + }, + 'nightly-chestnut-dev': { + deviceStage("build nightly-chestnut-dev", "mici-chestnut-ci", ["CHESTNUT=1"], [ + step("build nightly-chestnut-dev", "SCONSFLAGS=-j4 INCLUDE_BIG_MODEL=1 PANDA_DEBUG_BUILD=1 RELEASE_BRANCH=nightly-chestnut-dev $SOURCE_DIR/tools/release/build_release.sh TestChestnutOnroad"), + ]) + }, + ) } if (!env.BRANCH_NAME.matches(excludeRegex)) { diff --git a/openpilot/common/version.py b/openpilot/common/version.py index 011161b99d..b6f0bc9d3f 100755 --- a/openpilot/common/version.py +++ b/openpilot/common/version.py @@ -10,8 +10,8 @@ from openpilot.common.basedir import BASEDIR from openpilot.common.swaglog import cloudlog from openpilot.common.git import get_commit, get_origin, get_branch, get_short_branch, get_commit_date -RELEASE_BRANCHES = ['release-tizi-staging', 'release-mici-staging', 'release-tizi', 'release-mici', 'nightly'] -TESTED_BRANCHES = RELEASE_BRANCHES + ['devel-staging', 'nightly-dev'] +RELEASE_BRANCHES = ['release-tizi-staging', 'release-mici-staging', 'release-tizi', 'release-mici', 'nightly', 'nightly-chestnut'] +TESTED_BRANCHES = RELEASE_BRANCHES + ['devel-staging', 'nightly-dev', 'nightly-chestnut-dev'] BUILD_METADATA_FILENAME = "build.json" diff --git a/openpilot/system/hardware/chestnut/status.py b/openpilot/system/hardware/chestnut/status.py index 186e043262..4e6c62fd08 100644 --- a/openpilot/system/hardware/chestnut/status.py +++ b/openpilot/system/hardware/chestnut/status.py @@ -3,7 +3,7 @@ import time from openpilot.common.hardware.usb import CHESTNUT_USB_PRODUCT, is_chestnut_usb_id -CHESTNUT_RELEASE_BRANCHES = ("release-chestnut", "release-chestnut-staging", "nightly-chestnut") +CHESTNUT_RELEASE_BRANCHES = ("release-chestnut", "release-chestnut-staging", "nightly-chestnut", "nightly-chestnut-dev") CHESTNUT_POWERED_VOLTAGE = 5000 GPU_TEMP_LIMIT = 100. MEMORY_TEMP_LIMIT = 95. From d2c0ce91ade7014162f0e99d881c9350193efa0a Mon Sep 17 00:00:00 2001 From: Robbe Derks Date: Thu, 1 Oct 2026 13:43:30 +0200 Subject: [PATCH 60/79] pandad: log NMI and hardfault resets (#38737) --- openpilot/cereal/log.capnp | 3 +++ openpilot/selfdrive/pandad/pandad.cc | 2 ++ 2 files changed, 5 insertions(+) diff --git a/openpilot/cereal/log.capnp b/openpilot/cereal/log.capnp index 60461bde61..5152d787ab 100644 --- a/openpilot/cereal/log.capnp +++ b/openpilot/cereal/log.capnp @@ -573,6 +573,9 @@ struct PandaState @0xa7649e2575e4591e { controlsAllowedRESERVED1 @38 :Bool; controlsAllowedRESERVED2 @39 :Bool; + nmiReset @40 :Bool; + hardfaultReset @41 :Bool; + enum FaultStatus { none @0; faultTemp @1; diff --git a/openpilot/selfdrive/pandad/pandad.cc b/openpilot/selfdrive/pandad/pandad.cc index 8b54ca6eaf..39e11165ba 100644 --- a/openpilot/selfdrive/pandad/pandad.cc +++ b/openpilot/selfdrive/pandad/pandad.cc @@ -127,6 +127,8 @@ void fill_panda_state(cereal::PandaState::Builder &ps, cereal::PandaState::Panda ps.setInterruptLoad(health.interrupt_load_pkt / 255.0f); ps.setFanPower(health.fan_power); ps.setSafetyRxChecksInvalid((health.flags_pkt & HEALTH_FLAG_SAFETY_RX_CHECKS_INVALID) != 0U); + ps.setNmiReset((health.flags_pkt & HEALTH_FLAG_NMI_RESET) != 0U); + ps.setHardfaultReset((health.flags_pkt & HEALTH_FLAG_HARDFAULT_RESET) != 0U); ps.setSpiErrorCount(health.spi_error_count_pkt); ps.setSbu1Voltage(health.sbu1_voltage_mV / 1000.0f); ps.setSbu2Voltage(health.sbu2_voltage_mV / 1000.0f); From 7ee974ab5bec3241d821e15fc5d2e980a80d4f9c Mon Sep 17 00:00:00 2001 From: Daniel Koepping Date: Thu, 1 Oct 2026 12:03:32 -0700 Subject: [PATCH 61/79] fix low speed curvature saturation alert (#39034) fix curvature saturation alert --- .../selfdrive/controls/lib/latcontrol_curvature.py | 11 +++++++++-- openpilot/selfdrive/controls/tests/test_latcontrol.py | 5 ++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/openpilot/selfdrive/controls/lib/latcontrol_curvature.py b/openpilot/selfdrive/controls/lib/latcontrol_curvature.py index ec3ad1bf16..398c32f3d5 100644 --- a/openpilot/selfdrive/controls/lib/latcontrol_curvature.py +++ b/openpilot/selfdrive/controls/lib/latcontrol_curvature.py @@ -1,11 +1,13 @@ import math +import numpy as np +from collections import deque from openpilot.cereal import log from openpilot.common.pid import PIDController from openpilot.selfdrive.controls.lib.latcontrol import LatControl from openpilot.selfdrive.controls.lib.drive_helpers import MAX_CURVATURE -CURVATURE_SATURATION_THRESHOLD = 1e-3 # 1/m +LAT_ACCEL_SATURATION_THRESHOLD = 0.5 # m/s^2 class LatControlCurvature(LatControl): @@ -20,6 +22,8 @@ class LatControlCurvature(LatControl): else: self.pid = None self.kf = 1. + self.desired_curvature_buffer_len = int(1. / self.dt) + self.desired_curvature_buffer = deque([0.] * self.desired_curvature_buffer_len, maxlen=self.desired_curvature_buffer_len) def reset(self): super().reset() @@ -30,6 +34,9 @@ class LatControlCurvature(LatControl): curvature_log = log.ControlsState.LateralCurvatureState.new_message() actual_curvature = -VM.calc_curvature(math.radians(CS.steeringAngleDeg - params.angleOffsetDeg), CS.vEgo, params.roll) error = desired_curvature - actual_curvature + self.desired_curvature_buffer.append(desired_curvature) + delay_frames = int(np.clip(lat_delay / self.dt + 1, 1, self.desired_curvature_buffer_len)) + expected_curvature = self.desired_curvature_buffer[-delay_frames] if not active: output_curvature = 0.0 @@ -53,6 +60,6 @@ class LatControlCurvature(LatControl): curvature_log.actualCurvature = float(actual_curvature) curvature_log.desiredCurvature = float(desired_curvature) curvature_log.output = float(output_curvature) - curvature_log.saturated = bool(self._check_saturation(abs(error) > CURVATURE_SATURATION_THRESHOLD, CS, + curvature_log.saturated = bool(self._check_saturation(abs(expected_curvature - actual_curvature) * CS.vEgo ** 2 > LAT_ACCEL_SATURATION_THRESHOLD, CS, False, curvature_limited)) return 0.0, float(output_curvature), curvature_log diff --git a/openpilot/selfdrive/controls/tests/test_latcontrol.py b/openpilot/selfdrive/controls/tests/test_latcontrol.py index 2b7a0cf695..47dd4f7f8e 100644 --- a/openpilot/selfdrive/controls/tests/test_latcontrol.py +++ b/openpilot/selfdrive/controls/tests/test_latcontrol.py @@ -8,17 +8,20 @@ from opendbc.car.honda.values import CAR as HONDA from opendbc.car.toyota.values import CAR as TOYOTA from opendbc.car.nissan.values import CAR as NISSAN from opendbc.car.gm.values import CAR as GM +from opendbc.car.volkswagen.values import CAR as VOLKSWAGEN from opendbc.car.vehicle_model import VehicleModel from openpilot.common.realtime import DT_CTRL from openpilot.selfdrive.controls.lib.latcontrol_pid import LatControlPID from openpilot.selfdrive.controls.lib.latcontrol_torque import LatControlTorque from openpilot.selfdrive.controls.lib.latcontrol_angle import LatControlAngle +from openpilot.selfdrive.controls.lib.latcontrol_curvature import LatControlCurvature class TestLatControl(OpenpilotTestCase): @parameterized.expand([(HONDA.HONDA_CIVIC, LatControlPID), (TOYOTA.TOYOTA_RAV4, LatControlTorque), - (NISSAN.NISSAN_LEAF, LatControlAngle), (GM.CHEVROLET_BOLT_EUV, LatControlTorque)]) + (NISSAN.NISSAN_LEAF, LatControlAngle), (GM.CHEVROLET_BOLT_EUV, LatControlTorque), + (VOLKSWAGEN.VOLKSWAGEN_ID4_MK2, LatControlCurvature)]) def test_saturation(self, car_name, controller): CarInterface = interfaces[car_name] CP = CarInterface.get_non_essential_params(car_name) From 78dccf0b482ea1298fd1f6fa59b0554187b02ce6 Mon Sep 17 00:00:00 2001 From: Daniel Koepping Date: Thu, 1 Oct 2026 16:19:08 -0700 Subject: [PATCH 62/79] ui: lead bar (#39033) * ui: lead bar * cleanup * add width * show on brake --- .../ui/mici/onroad/model_renderer.py | 121 ++++++++---------- 1 file changed, 53 insertions(+), 68 deletions(-) diff --git a/openpilot/selfdrive/ui/mici/onroad/model_renderer.py b/openpilot/selfdrive/ui/mici/onroad/model_renderer.py index dd0d6d9734..e927fa506e 100644 --- a/openpilot/selfdrive/ui/mici/onroad/model_renderer.py +++ b/openpilot/selfdrive/ui/mici/onroad/model_renderer.py @@ -1,11 +1,12 @@ import colorsys import numpy as np import pyray as rl -from openpilot.cereal import messaging +from openpilot.cereal import log, messaging from opendbc.car.structs import car from dataclasses import dataclass, field from openpilot.common.params import Params from openpilot.common.filter_simple import FirstOrderFilter +from openpilot.selfdrive.controls.radard import RADAR_TO_CAMERA from openpilot.selfdrive.locationd.calibrationd import HEIGHT_INIT from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus from openpilot.selfdrive.ui.mici.onroad import blend_colors @@ -16,6 +17,8 @@ from openpilot.system.ui.widgets import Widget CLIP_MARGIN = 500 MIN_DRAW_DISTANCE = 10.0 MAX_DRAW_DISTANCE = 100.0 +LEAD_BAR_LENGTH = 12.0 # px +LEAD_BAR_WIDTH = 1.8 # m THROTTLE_COLORS = [ rl.Color(13, 248, 122, 102), # HSLF(148/360, 0.94, 0.51, 0.4) @@ -42,11 +45,12 @@ class ModelPoints: projected_points: np.ndarray = field(default_factory=lambda: np.empty((0, 2), dtype=np.float32)) -@dataclass class LeadVehicle: - glow: list[tuple[float, float]] = field(default_factory=list) - chevron: list[tuple[float, float]] = field(default_factory=list) - fill_alpha: int = 0 + def __init__(self): + self.bar = np.empty((0, 2), dtype=np.float32) + self.d_filter = FirstOrderFilter(0.0, 0.2, 1 / gui_app.target_fps, initialized=False) + self.y_filter = FirstOrderFilter(0.0, 0.2, 1 / gui_app.target_fps, initialized=False) + self.fade_filter = FirstOrderFilter(0.0, 0.1, 1 / gui_app.target_fps) class ModelRenderer(Widget): @@ -134,8 +138,6 @@ class ModelRenderer(Widget): return self._update_model(lead_one, path_x_array) - if render_lead_indicator: - self._update_leads(radar_state, path_x_array) self._transform_dirty = False # Draw elements (hide when disengaged) @@ -143,8 +145,11 @@ class ModelRenderer(Widget): self._draw_lane_lines() self._draw_path(sm) - # if render_lead_indicator and radar_state: - # self._draw_lead_indicator() + if render_lead_indicator: + self._update_leads(sm) + self._draw_lead_indicator() + else: + self._lead_vehicles = [LeadVehicle(), LeadVehicle()] def _update_raw_points(self, model): """Update raw 3D points from model data""" @@ -160,21 +165,45 @@ class ModelRenderer(Widget): self._road_edge_stds = np.array(model.roadEdgeStds, dtype=np.float32) self._acceleration_x = np.array(model.acceleration.x, dtype=np.float32) - def _update_leads(self, radar_state, path_x_array): - """Update positions of lead vehicles""" - self._lead_vehicles = [LeadVehicle(), LeadVehicle()] - leads = [radar_state.leadOne, radar_state.leadTwo] + def _update_leads(self, sm): + plan = sm['longitudinalPlan'] + if plan.longitudinalPlanSource == log.LongitudinalPlan.LongitudinalPlanSource.e2e and len(sm['modelV2'].leadsV3) > 1: + leads = [(lead.prob > 0.5, lead.x[0], -lead.y[0]) for lead in list(sm['modelV2'].leadsV3)[:2]] + else: + radar = sm['radarState'] + leads = [(lead.present, lead.dRel + RADAR_TO_CAMERA, lead.yRel) for lead in (radar.leadOne, radar.leadTwo)] - for i, lead_data in enumerate(leads): - if lead_data and lead_data.present: - d_rel, y_rel, v_rel = lead_data.dRel, lead_data.yRel, lead_data.vRel - idx = self._get_path_length_idx(path_x_array, d_rel) + # both leads can be the same vehicle + if leads[0][0] and abs(leads[1][1] - leads[0][1]) < 3.0: + leads[1] = (False, 0.0, 0.0) - # Get z-coordinate from path at the lead vehicle position - z = self._path.raw_points[idx, 2] if idx < len(self._path.raw_points) else 0.0 - point = self._map_to_screen(d_rel, -y_rel, z + self._path_offset_z) - if point: - self._lead_vehicles[i] = self._update_lead_vehicle(d_rel, v_rel, point, self._rect) + ss, cs = sm['selfdriveState'], sm['carState'] + # braking disengages without making openpilot unavailable + available = ss.enabled or ss.engageable or cs.brakePressed + lane = (self._lane_lines[1].raw_points + self._lane_lines[2].raw_points) / 2 + opacity = 0.4 if ui_state.status == UIStatus.DISENGAGED else 0.8 + for lead, (present, d_rel, y_rel) in zip(self._lead_vehicles, leads, strict=True): + visible = available and present and d_rel < MAX_DRAW_DISTANCE and len(lane) > 0 + # snap to a new vehicle instead of sliding over + if not visible or abs(y_rel - lead.y_filter.x) > 3.0: + lead.d_filter.initialized = lead.y_filter.initialized = False + lead.fade_filter.update(opacity if visible else 0.0) + if visible: + lead.bar = self._get_lead_bar(lane, lead.d_filter.update(d_rel), lead.y_filter.update(y_rel)) + + def _get_lead_bar(self, lane, d_rel, y_rel): + # bar on the road behind the lead, following the lane + x = np.array([d_rel, d_rel - min(6.0, 0.25 * d_rel)]) + y = np.interp(x, lane[:, 0], lane[:, 1]) - np.interp(d_rel, lane[:, 0], lane[:, 1]) - y_rel + z = np.interp(x, self._path.raw_points[:, 0], self._path.raw_points[:, 2]) + self._path_offset_z + corners = np.vstack((np.column_stack((x, y + LEAD_BAR_WIDTH / 2, z)), np.column_stack((x, y - LEAD_BAR_WIDTH / 2, z))[::-1])) + pts = self._car_space_transform @ corners.T + bar = (pts[:2] / pts[2]).T + + far, near = bar[[0, 3]], bar[[1, 2]] + length = np.linalg.norm(near.mean(axis=0) - far.mean(axis=0)) + bar[[1, 2]] = far + (near - far) * np.clip(length, 3.0, LEAD_BAR_LENGTH) / length + return bar.astype(np.float32) def _update_model(self, lead, path_x_array): """Update model visualization data based on model message""" @@ -259,30 +288,6 @@ class ModelRenderer(Widget): self._exp_gradient.colors = segment_colors self._exp_gradient.stops = gradient_stops - def _update_lead_vehicle(self, d_rel, v_rel, point, rect): - speed_buff, lead_buff = 10.0, 40.0 - - # Calculate fill alpha - fill_alpha = 0 - if d_rel < lead_buff: - fill_alpha = 255 * (1.0 - (d_rel / lead_buff)) - if v_rel < 0: - fill_alpha += 255 * (-1 * (v_rel / speed_buff)) - fill_alpha = min(fill_alpha, 255) - - # Calculate size and position - sz = np.clip((25 * 30) / (d_rel / 3 + 30), 15.0, 30.0) * 1 - x = np.clip(point[0], 0.0, rect.width - sz / 2) - y = min(point[1], rect.height - sz * 0.6) - - g_xo = sz / 5 - g_yo = sz / 10 - - glow = [(x + (sz * 1.35) + g_xo, y + sz + g_yo), (x, y - g_yo), (x - (sz * 1.35) - g_xo, y + sz + g_yo)] - chevron = [(x + (sz * 1.25), y + sz), (x, y), (x - (sz * 1.25), y + sz)] - - return LeadVehicle(glow=glow, chevron=chevron, fill_alpha=int(fill_alpha)) - def _get_ll_color(self, prob: float, adjacent: bool, left: bool): alpha = np.clip(prob, 0.0, 0.7) if adjacent: @@ -360,13 +365,9 @@ class ModelRenderer(Widget): draw_polygon(self._rect, path_pts, gradient=gradient) def _draw_lead_indicator(self): - # Draw lead vehicles if available + offset = np.array([self._rect.x, self._rect.y], dtype=np.float32) for lead in self._lead_vehicles: - if not lead.glow or not lead.chevron: - continue - - rl.draw_triangle_fan(lead.glow, len(lead.glow), rl.Color(218, 202, 37, 255)) - rl.draw_triangle_fan(lead.chevron, len(lead.chevron), rl.Color(201, 34, 49, lead.fill_alpha)) + draw_polygon(self._rect, lead.bar + offset, rl.Color(255, 255, 255, int(255 * lead.fade_filter.x))) @staticmethod def _get_path_length_idx(pos_x_array: np.ndarray, path_height: float) -> int: @@ -376,22 +377,6 @@ class ModelRenderer(Widget): indices = np.where(pos_x_array <= path_height)[0] return indices[-1] if indices.size > 0 else 0 - def _map_to_screen(self, in_x, in_y, in_z): - """Project a point in car space to screen space""" - input_pt = np.array([in_x, in_y, in_z]) - pt = self._car_space_transform @ input_pt - - if abs(pt[2]) < 1e-6: - return None - - x, y = pt[0] / pt[2], pt[1] / pt[2] - - clip = self._clip_region - if not (clip.x <= x <= clip.x + clip.width and clip.y <= y <= clip.y + clip.height): - return None - - return (x, y) - def _map_line_to_polygon(self, line: np.ndarray, y_off: float, z_off: float, max_idx: int, allow_invert: bool = True) -> np.ndarray: """Convert 3D line to 2D polygon for rendering.""" if line.shape[0] == 0: From ec95db3f1fa19f76940497fedfdb62e09ea19912 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Harald=20Sch=C3=A4fer?= Date: Fri, 2 Oct 2026 18:02:42 -0700 Subject: [PATCH 63/79] Bump opendbc (#39035) --- opendbc_repo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opendbc_repo b/opendbc_repo index 4134c0d1f5..35f7e08134 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit 4134c0d1f5e8f695e35ea5fedbe88f6d0c3afb76 +Subproject commit 35f7e0813462607ef1d703e52313e7571e31405d From 01a670d6b2fc41c0fc79300b200906e4058f8d33 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Mon, 5 Oct 2026 12:03:45 -0700 Subject: [PATCH 64/79] ui: fix keyboard text overflow gradient --- openpilot/selfdrive/ui/mici/widgets/dialog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpilot/selfdrive/ui/mici/widgets/dialog.py b/openpilot/selfdrive/ui/mici/widgets/dialog.py index d5da6178f1..80625aa451 100644 --- a/openpilot/selfdrive/ui/mici/widgets/dialog.py +++ b/openpilot/selfdrive/ui/mici/widgets/dialog.py @@ -183,7 +183,7 @@ class BigInputDialog(BigDialogBase): # draw gradient on left side to indicate more text if text_size.x > text_field_rect.width: rl.draw_rectangle_gradient_ex(rl.Rectangle(text_field_rect.x, text_field_rect.y, 80, text_field_rect.height), - rl.BLACK, rl.BLANK, rl.BLANK, rl.BLACK) + rl.BLACK, rl.BLACK, rl.BLANK, rl.BLANK) # draw cursor blink_alpha = (math.sin(rl.get_time() * 6) + 1) / 2 From 910c08e0828c54ce7919ae14606166779a52bf94 Mon Sep 17 00:00:00 2001 From: Daniel Koepping Date: Mon, 5 Oct 2026 15:31:58 -0700 Subject: [PATCH 65/79] Revert #38464 (#39038) * Revert #38464 Reverts aa0ac919baa0df2fa792e281246d4a1a1e6a1652. --- openpilot/selfdrive/pandad/spi.cc | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/openpilot/selfdrive/pandad/spi.cc b/openpilot/selfdrive/pandad/spi.cc index 369032a533..f54c26e506 100644 --- a/openpilot/selfdrive/pandad/spi.cc +++ b/openpilot/selfdrive/pandad/spi.cc @@ -29,12 +29,6 @@ enum SpiError { const unsigned int SPI_ACK_TIMEOUT = 500; // milliseconds const std::string SPI_DEVICE = "/dev/spidev0.0"; -// TODO: fix SPI turnaround synchronization at the protocol level. -static uint64_t spi_last_bus_activity_ns = 0; // protected by hw_lock - -static void wait_for_spi_turnaround(uint64_t start_ns) { - while ((nanos_since_boot() - start_ns) < 400000) {} -} class LockEx { public: @@ -325,8 +319,6 @@ int PandaSpiHandle::spi_transfer(uint8_t endpoint, uint8_t *tx_data, uint16_t tx assert(tx_len < SPI_BUF_SIZE); assert(max_rx_len < SPI_BUF_SIZE); - wait_for_spi_turnaround(spi_last_bus_activity_ns); - xfer_count++; header = { .sync = SPI_SYNC, @@ -355,7 +347,6 @@ int PandaSpiHandle::spi_transfer(uint8_t endpoint, uint8_t *tx_data, uint16_t tx if (ret < 0) { goto fail; } - wait_for_spi_turnaround(nanos_since_boot()); // Send data if (tx_data != NULL) { @@ -398,7 +389,6 @@ int PandaSpiHandle::spi_transfer(uint8_t endpoint, uint8_t *tx_data, uint16_t tx memcpy(rx_data, rx_buf + 3, rx_data_len); } - spi_last_bus_activity_ns = nanos_since_boot(); return rx_data_len; fail: @@ -413,7 +403,6 @@ fail: } } - spi_last_bus_activity_ns = nanos_since_boot(); if (ret >= 0) ret = -1; return ret; } From 4bcf73282e35c49fa82e8f1ca9e10232a520e8f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Harald=20Sch=C3=A4fer?= Date: Mon, 5 Oct 2026 17:58:45 -0700 Subject: [PATCH 66/79] ResAction (#39037) * 7a933c1c-b3ba-4b31-be45-d2a03ca9fa9d/12864 * compiled --- openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl | 4 ++-- .../modeld/models/big_driving_warp_1344x760_tinygrad.pkl | 4 ++-- .../modeld/models/big_driving_warp_1928x1208_tinygrad.pkl | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl b/openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl index 91125f3f21..7925e6d828 100644 --- a/openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl +++ b/openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4937fc6f04367b28328fe3bc0d1463a245725d039ddaab29406cc565fd5c8ed4 -size 773082315 +oid sha256:cc75858d5e007b725b6264e194da4321d7874a212dc631b8f40a215dfd172cd2 +size 799992286 diff --git a/openpilot/selfdrive/modeld/models/big_driving_warp_1344x760_tinygrad.pkl b/openpilot/selfdrive/modeld/models/big_driving_warp_1344x760_tinygrad.pkl index fdcaba2f00..baa4f72f41 100644 --- a/openpilot/selfdrive/modeld/models/big_driving_warp_1344x760_tinygrad.pkl +++ b/openpilot/selfdrive/modeld/models/big_driving_warp_1344x760_tinygrad.pkl @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6334d6b92a2cc095bfb1430a252aa2ede1902605d0f9ba89224aa5a87c7dc7c0 -size 863472 +oid sha256:e80410f65706472e72150cfdcb0087595782e8aa366149286672511a6224ef54 +size 864120 diff --git a/openpilot/selfdrive/modeld/models/big_driving_warp_1928x1208_tinygrad.pkl b/openpilot/selfdrive/modeld/models/big_driving_warp_1928x1208_tinygrad.pkl index 91c4096993..242cc0fb9a 100644 --- a/openpilot/selfdrive/modeld/models/big_driving_warp_1928x1208_tinygrad.pkl +++ b/openpilot/selfdrive/modeld/models/big_driving_warp_1928x1208_tinygrad.pkl @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bda11a9c2e87636db364862ca68ebe4ff9d7baeeb39c0c7e2010b702ed543283 -size 863216 +oid sha256:54dff4bc4a64a8615823bb4906429a9db30f4b9fca9b8a56e9c84100227608bb +size 863864 From cce54f20d52c381499ae16119cb83c710c1f1083 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Mon, 5 Oct 2026 18:05:05 -0700 Subject: [PATCH 67/79] deprecate unused initData fields --- openpilot/cereal/log.capnp | 24 +++++++++++------------- openpilot/system/loggerd/logger.cc | 1 - 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/openpilot/cereal/log.capnp b/openpilot/cereal/log.capnp index 5152d787ab..48bbdb9797 100644 --- a/openpilot/cereal/log.capnp +++ b/openpilot/cereal/log.capnp @@ -167,12 +167,7 @@ struct InitData { gitSrcCommit @23 :Text; gitSrcCommitDate @24 :Text; - androidProperties @16 :Map(Text, Text); - - pandaInfo @8 :PandaInfo; - dirty @9 :Bool; - passive @12 :Bool; params @17 :Map(Text, Data); commands @19 :Map(Text, Data); @@ -190,6 +185,17 @@ struct InitData { mici @7; # comma four } + deprecated :group { + gctx @1 :Text; + androidBuildInfo @5 :Deprecated.AndroidBuildInfo; + androidSensors @6 :List(Deprecated.AndroidSensor); + chffrAndroidExtra @7 :ChffrAndroidExtra; + pandaInfo @8 :PandaInfo; + passive @12 :Bool; + iosBuildInfo @14 :Deprecated.IosBuildInfo; + androidProperties @16 :Map(Text, Text); + } + struct PandaInfo { hasPanda @0 :Bool; dongleId @1 :Text; @@ -200,14 +206,6 @@ struct InitData { struct ChffrAndroidExtra { allCameraCharacteristics @0 :Map(Text, Text); } - - deprecated :group { - gctx @1 :Text; - androidBuildInfo @5 :Deprecated.AndroidBuildInfo; - androidSensors @6 :List(Deprecated.AndroidSensor); - chffrAndroidExtra @7 :ChffrAndroidExtra; - iosBuildInfo @14 :Deprecated.IosBuildInfo; - } } struct FrameData { diff --git a/openpilot/system/loggerd/logger.cc b/openpilot/system/loggerd/logger.cc index f553760016..f9371314d3 100644 --- a/openpilot/system/loggerd/logger.cc +++ b/openpilot/system/loggerd/logger.cc @@ -47,7 +47,6 @@ kj::Array logger_build_init_data(bool route_log) { init.setGitCommitDate(params_map["GitCommitDate"]); init.setGitBranch(params_map["GitBranch"]); init.setGitRemote(params_map["GitRemote"]); - init.setPassive(false); init.setDongleId(params_map["DongleId"]); // for prebuilt branches From 84f9bdbc10df0184976c14457bddab008b141208 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Mon, 5 Oct 2026 18:19:04 -0700 Subject: [PATCH 68/79] jp: add initData tab (#39039) --- openpilot/tools/jotpluggler/app.cc | 6 + openpilot/tools/jotpluggler/app.h | 16 +++ openpilot/tools/jotpluggler/init_data.cc | 113 +++++++++++++++++++ openpilot/tools/jotpluggler/render.cc | 2 +- openpilot/tools/jotpluggler/runtime.cc | 4 +- openpilot/tools/jotpluggler/sketch_layout.cc | 22 ++++ openpilot/tools/jotpluggler/stream.cc | 1 + 7 files changed, 162 insertions(+), 2 deletions(-) create mode 100644 openpilot/tools/jotpluggler/init_data.cc diff --git a/openpilot/tools/jotpluggler/app.cc b/openpilot/tools/jotpluggler/app.cc index e6ba696bae..5cdcff5ed2 100644 --- a/openpilot/tools/jotpluggler/app.cc +++ b/openpilot/tools/jotpluggler/app.cc @@ -1614,6 +1614,7 @@ void draw_pane_windows(AppSession *session, UiState *state) { void draw_workspace(AppSession *session, const UiMetrics &ui, UiState *state) { state->custom_series.selected = false; state->logs.selected = false; + state->init_data_selected = false; ImGui::SetNextWindowPos(ImVec2(ui.content_x, ui.content_y)); ImGui::SetNextWindowSize(ImVec2(ui.content_w, ui.content_h)); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f)); @@ -1711,6 +1712,11 @@ void draw_workspace(AppSession *session, const UiMetrics &ui, UiState *state) { draw_logs_tab(session, state); ImGui::EndTabItem(); } + if (ImGui::BeginTabItem("initData##workspace_init_data")) { + state->init_data_selected = true; + draw_init_data_tab(session, state); + ImGui::EndTabItem(); + } if (custom_series_tab_open) { ImGuiTabItemFlags custom_flags = ImGuiTabItemFlags_None; if (state->custom_series.request_select) { diff --git a/openpilot/tools/jotpluggler/app.h b/openpilot/tools/jotpluggler/app.h index 9a6777ee90..964370023a 100644 --- a/openpilot/tools/jotpluggler/app.h +++ b/openpilot/tools/jotpluggler/app.h @@ -316,7 +316,19 @@ struct RouteIdentifier { } }; +struct InitDataSection { + std::string name; + std::vector> values; +}; + +struct InitDataSnapshot { + std::vector sections; +}; + +InitDataSnapshot extract_init_data(cereal::InitData::Reader reader); + struct RouteData { + std::optional init_data; std::vector series; std::vector paths; std::vector roots; @@ -340,6 +352,7 @@ struct RouteData { }; struct StreamExtractBatch { + std::optional init_data; std::vector series; std::vector can_messages; std::vector logs; @@ -706,6 +719,8 @@ struct UiState { DbcEditorState dbc_editor; CustomSeriesEditorState custom_series; LogsUiState logs; + bool init_data_selected = false; + std::string init_data_search; UndoStack undo; }; @@ -767,6 +782,7 @@ void draw_custom_series_editor(AppSession *session, UiState *state); // ***** void draw_logs_tab(AppSession *session, UiState *state); +void draw_init_data_tab(AppSession *session, UiState *state); // ***** // map diff --git a/openpilot/tools/jotpluggler/init_data.cc b/openpilot/tools/jotpluggler/init_data.cc new file mode 100644 index 0000000000..d4d2e8ed05 --- /dev/null +++ b/openpilot/tools/jotpluggler/init_data.cc @@ -0,0 +1,113 @@ +#include "tools/jotpluggler/app.h" + +#include + +namespace { + +std::string format_data(capnp::Data::Reader data) { + const bool text = std::all_of(data.begin(), data.end(), [](uint8_t c) { + return (c >= 32 && c < 127) || c == '\n' || c == '\r' || c == '\t'; + }); + if (data.size() == 0) return ""; + if (text) return std::string(reinterpret_cast(data.begin()), data.size()); + constexpr char hex[] = "0123456789abcdef"; + std::string value = "hex: "; + for (uint8_t c : data) { + value += hex[c >> 4]; + value += hex[c & 15]; + } + return value; +} + +std::string format_value(capnp::DynamicValue::Reader value) { + if (value.getType() == capnp::DynamicValue::DATA) return format_data(value.as()); + if (value.getType() == capnp::DynamicValue::TEXT) { + const auto text = value.as(); + return std::string(text.begin(), text.size()); + } + return kj::str(value).cStr(); +} + +} // namespace + +InitDataSnapshot extract_init_data(cereal::InitData::Reader reader) { + InitDataSnapshot snapshot; + InitDataSection metadata{"Device & software", {}}; + auto dynamic = capnp::toDynamic(reader); + for (auto field : dynamic.getSchema().getFields()) { + const std::string name = field.getProto().getName().cStr(); + if (name == "deprecated") continue; + if (name == "params" || name == "commands") { + InitDataSection section{name, {}}; + auto entries = dynamic.get(field).as().get("entries").as(); + for (auto entry : entries) { + auto item = entry.as(); + section.values.emplace_back(format_value(item.get("key")), format_value(item.get("value"))); + } + std::sort(section.values.begin(), section.values.end()); + snapshot.sections.push_back(std::move(section)); + } else { + metadata.values.emplace_back(name, format_value(dynamic.get(field))); + } + } + snapshot.sections.insert(snapshot.sections.begin(), std::move(metadata)); + return snapshot; +} + +void draw_init_data_tab(AppSession *session, UiState *state) { + ImGui::SetNextItemWidth(280.0f); + input_text_with_hint_string("##init_data_search", "Search fields and values...", &state->init_data_search); + ImGui::SameLine(); + ImGui::TextDisabled("Right-click a value to copy"); + ImGui::Separator(); + if (!session->route_data.init_data) { + const auto load = session->route_loader ? session->route_loader->snapshot() : RouteLoadSnapshot{}; + ImGui::TextWrapped("%s", load.active ? "Loading initData..." : "No initData available for this route or stream."); + return; + } + const std::string query = lowercase_copy(state->init_data_search); + const auto matches = [&](const auto &row) { + return query.empty() || lowercase_copy(row.first).find(query) != std::string::npos + || lowercase_copy(row.second).find(query) != std::string::npos; + }; + bool found = false; + if (ImGui::BeginChild("##init_data_content", ImVec2(0, 0), false)) { + for (const auto §ion : session->route_data.init_data->sections) { + const size_t count = std::count_if(section.values.begin(), section.values.end(), matches); + if (!query.empty() && count == 0) continue; + found = found || count > 0; + ImGui::PushID(section.name.c_str()); + const std::string label = section.name + " (" + std::to_string(count) + ")"; + if (ImGui::CollapsingHeader(label.c_str(), ImGuiTreeNodeFlags_DefaultOpen)) { + if (section.values.empty()) ImGui::TextDisabled("No entries"); + if (ImGui::BeginTable("##values", 2, ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_Resizable)) { + ImGui::TableSetupColumn("Field", ImGuiTableColumnFlags_WidthFixed, 230.0f); + ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableHeadersRow(); + for (size_t i = 0; i < section.values.size(); ++i) { + const auto &[key, value] = section.values[i]; + if (!matches(section.values[i])) continue; + ImGui::PushID(static_cast(i)); + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::TextUnformatted(key.c_str()); + ImGui::TableSetColumnIndex(1); + app_push_mono_font(); + ImGui::TextWrapped("%s", value.empty() ? "(empty)" : value.c_str()); + app_pop_mono_font(); + if (ImGui::BeginPopupContextItem("##copy")) { + if (ImGui::MenuItem("Copy value")) ImGui::SetClipboardText(value.c_str()); + if (ImGui::MenuItem("Copy field")) ImGui::SetClipboardText(key.c_str()); + ImGui::EndPopup(); + } + ImGui::PopID(); + } + ImGui::EndTable(); + } + } + ImGui::PopID(); + } + if (!query.empty() && !found) ImGui::TextDisabled("No matching fields or values."); + } + ImGui::EndChild(); +} diff --git a/openpilot/tools/jotpluggler/render.cc b/openpilot/tools/jotpluggler/render.cc index 54f0c16cc3..f7d991d2f9 100644 --- a/openpilot/tools/jotpluggler/render.cc +++ b/openpilot/tools/jotpluggler/render.cc @@ -83,7 +83,7 @@ void render_layout(AppSession *session, UiState *state, bool show_camera_feed) { draw_sidebar(session, ui, state, show_camera_feed); draw_workspace(session, ui, state); draw_sidebar_resizer(ui, state); - if (!state->custom_series.selected && !state->logs.selected) { + if (!state->custom_series.selected && !state->logs.selected && !state->init_data_selected) { draw_pane_windows(session, state); } draw_status_bar(*session, ui, state); diff --git a/openpilot/tools/jotpluggler/runtime.cc b/openpilot/tools/jotpluggler/runtime.cc index d7d0fc79c4..dfbb5474c9 100644 --- a/openpilot/tools/jotpluggler/runtime.cc +++ b/openpilot/tools/jotpluggler/runtime.cc @@ -48,7 +48,8 @@ CameraType decoder_camera_type(CameraViewKind view) { } bool stream_batch_has_data(const StreamExtractBatch &batch) { - return !batch.series.empty() + return batch.init_data.has_value() + || !batch.series.empty() || !batch.can_messages.empty() || !batch.logs.empty() || !batch.timeline.empty() @@ -526,6 +527,7 @@ struct StreamPoller::Impl { } merge_can_message_data(&dst->can_messages[it->second], &message); } + if (src->init_data) dst->init_data = std::move(src->init_data); if (!src->logs.empty()) { dst->logs.insert(dst->logs.end(), std::make_move_iterator(src->logs.begin()), diff --git a/openpilot/tools/jotpluggler/sketch_layout.cc b/openpilot/tools/jotpluggler/sketch_layout.cc index 5440d9dd8d..0c84514aa2 100644 --- a/openpilot/tools/jotpluggler/sketch_layout.cc +++ b/openpilot/tools/jotpluggler/sketch_layout.cc @@ -94,6 +94,7 @@ void decode_can_frame(const dbc::Database *can_dbc, #include "tools/jotpluggler/generated_event_extractors.h" struct LoadedRouteArtifacts { + std::optional init_data; std::vector series; std::vector can_messages; std::vector logs; @@ -1560,6 +1561,7 @@ LoadedRouteArtifacts load_route_series_parallel( LoadStats *stats) { struct SegmentResult { SeriesAccumulator series; + std::optional init_data; std::vector logs; std::vector timeline; std::vector thumbnails; @@ -1624,6 +1626,13 @@ LoadedRouteArtifacts load_route_series_parallel( const auto extract_start = LoadStats::Clock::now(); results[index].series = extract_segment_series(reader.events, schema, can_dbc, skip_raw_can, worker_budget, segment_workers); results[index].logs = extract_segment_logs(reader.events); + for (const Event &record : reader.events) { + if (record.which != cereal::Event::Which::INIT_DATA) continue; + with_parseable_event(record.data, [&](const cereal::Event::Reader &event) { + results[index].init_data = extract_init_data(event.getInitData()); + }); + if (results[index].init_data) break; + } results[index].timeline = extract_segment_timeline(reader.events); results[index].thumbnails = extract_segment_thumbnails(reader.events, segment_number); segment_stats.extract_seconds = std::chrono::duration(LoadStats::Clock::now() - extract_start).count(); @@ -1671,6 +1680,12 @@ LoadedRouteArtifacts load_route_series_parallel( } } LoadedRouteArtifacts artifacts; + for (SegmentResult &result : results) { + if (result.init_data) { + artifacts.init_data = std::move(result.init_data); + break; + } + } artifacts.series = collect_series(std::move(merged)); artifacts.can_messages = std::move(merged.can_messages); artifacts.logs = std::move(logs); @@ -1720,6 +1735,7 @@ std::vector collect_route_roots_for_paths(const std::vector init_data; const SchemaIndex &schema = SchemaIndex::instance(); SeriesAccumulator series = make_series_accumulator(schema); std::vector logs; @@ -1762,6 +1778,9 @@ void StreamAccumulator::appendEvent(kj::ArrayPtr data) { if (!impl_->time_offset.has_value()) { impl_->time_offset = boot_time; } + if (which == cereal::Event::Which::INIT_DATA) { + impl_->init_data = extract_init_data(event.getInitData()); + } if (which == cereal::Event::Which::CAR_PARAMS) { const std::string fingerprint = event.getCarParams().getCarFingerprint().cStr(); if (!fingerprint.empty() && fingerprint != impl_->car_fingerprint) { @@ -1803,6 +1822,8 @@ void StreamAccumulator::appendCanFrames(CanServiceKind service, const std::vecto StreamExtractBatch StreamAccumulator::takeBatch() { StreamExtractBatch batch; + batch.init_data = std::move(impl_->init_data); + impl_->init_data.reset(); batch.car_fingerprint = impl_->car_fingerprint; batch.dbc_name = impl_->detected_dbc_name; if (impl_->time_offset.has_value()) { @@ -1885,6 +1906,7 @@ RouteData load_route_data(const std::string &route_name, std::move(artifacts.enum_info), metadata.car_fingerprint, resolved_dbc); + route_data.init_data = std::move(artifacts.init_data); route_data.route_id = make_route_identifier(route, segments); build_camera_index(segments, route_data, &SegmentLogs::narrow_road, "narrowRoadEncodeIdx", &route_data.road_camera); build_camera_index(segments, route_data, &SegmentLogs::cabin, "cabinEncodeIdx", &route_data.cabin_camera); diff --git a/openpilot/tools/jotpluggler/stream.cc b/openpilot/tools/jotpluggler/stream.cc index fcfa6585bb..d5e8fa43b7 100644 --- a/openpilot/tools/jotpluggler/stream.cc +++ b/openpilot/tools/jotpluggler/stream.cc @@ -72,6 +72,7 @@ bool can_message_less(const CanMessageData &a, const CanMessageData &b) { } void apply_stream_batch(AppSession *session, UiState *state, StreamExtractBatch batch) { + if (batch.init_data) session->route_data.init_data = std::move(batch.init_data); if (batch.has_time_offset) { session->stream_time_offset = batch.time_offset; } From fc616407826c52abd1338a34e85ce9d4fcbd20e1 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Mon, 5 Oct 2026 18:31:10 -0700 Subject: [PATCH 69/79] jotpluggler: fix subset time bounds (#39041) --- openpilot/tools/jotpluggler/sketch_layout.cc | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/openpilot/tools/jotpluggler/sketch_layout.cc b/openpilot/tools/jotpluggler/sketch_layout.cc index 0c84514aa2..1971800c41 100644 --- a/openpilot/tools/jotpluggler/sketch_layout.cc +++ b/openpilot/tools/jotpluggler/sketch_layout.cc @@ -1202,9 +1202,12 @@ RouteData build_route_data(std::vector &&series_list, route_data.paths.reserve(series_list.size()); for (RouteSeries &series : series_list) { if (series.times.empty()) continue; - route_data.has_time_range = true; - route_data.x_min = route_data.series.empty() ? series.times.front() : std::min(route_data.x_min, series.times.front()); - route_data.x_max = route_data.series.empty() ? series.times.back() : std::max(route_data.x_max, series.times.back()); + // initData is copied into every segment with its original route timestamp. + if (series.path.rfind("/initData/", 0) != 0) { + route_data.x_min = !route_data.has_time_range ? series.times.front() : std::min(route_data.x_min, series.times.front()); + route_data.x_max = !route_data.has_time_range ? series.times.back() : std::max(route_data.x_max, series.times.back()); + route_data.has_time_range = true; + } route_data.paths.push_back(series.path); route_data.series.push_back(std::move(series)); } From 2b7c34a5c32f78f4892a07598cbe3827be880438 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Mon, 5 Oct 2026 18:42:04 -0700 Subject: [PATCH 70/79] jp: show text fields (#39042) --- openpilot/tools/jotpluggler/app.h | 11 +++++++++++ openpilot/tools/jotpluggler/browser.cc | 17 +++++++++++++++-- openpilot/tools/jotpluggler/common.cc | 11 +++++++++++ .../jotpluggler/generate_event_extractors.py | 18 +++++++++++++----- openpilot/tools/jotpluggler/plot.cc | 4 ++-- openpilot/tools/jotpluggler/sketch_layout.cc | 18 ++++++++++++++++++ openpilot/tools/jotpluggler/stream.cc | 6 +++++- 7 files changed, 75 insertions(+), 10 deletions(-) diff --git a/openpilot/tools/jotpluggler/app.h b/openpilot/tools/jotpluggler/app.h index 964370023a..e089609247 100644 --- a/openpilot/tools/jotpluggler/app.h +++ b/openpilot/tools/jotpluggler/app.h @@ -168,8 +168,19 @@ struct LogEntry { struct EnumInfo { std::vector names; + bool is_text = false; + std::unordered_map text_indices; + + double text_value(std::string text) { + is_text = true; + auto [it, inserted] = text_indices.try_emplace(text, names.size()); + if (inserted) names.push_back(std::move(text)); + return static_cast(it->second); + } }; +void merge_text_labels(RouteSeries *incoming, const EnumInfo &labels, EnumInfo *destination); + struct SeriesFormat { int decimals = 3; bool integer_like = false; diff --git a/openpilot/tools/jotpluggler/browser.cc b/openpilot/tools/jotpluggler/browser.cc index 91e4a707da..3342e82b37 100644 --- a/openpilot/tools/jotpluggler/browser.cc +++ b/openpilot/tools/jotpluggler/browser.cc @@ -310,8 +310,9 @@ std::string format_display_value(double display_value, const int idx = static_cast(std::llround(display_value)); if (idx >= 0 && std::abs(display_value - static_cast(idx)) < 0.01 && static_cast(idx) < enum_info->names.size() - && !enum_info->names[static_cast(idx)].empty()) { - return enum_info->names[static_cast(idx)]; + && (enum_info->is_text || !enum_info->names[static_cast(idx)].empty())) { + const std::string &name = enum_info->names[static_cast(idx)]; + return name.empty() ? "(empty)" : name; } } char buf[64] = {}; @@ -416,6 +417,18 @@ void draw_browser_node(AppSession *session, app_pop_mono_font(); } + if (hovered) { + auto it = session->route_data.enum_info.find(node.full_path); + if (it != session->route_data.enum_info.end() && it->second.is_text) { + ImGui::BeginTooltip(); + ImGui::TextUnformatted(node.full_path.c_str()); + ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f); + ImGui::TextWrapped("%s", value_text.c_str()); + ImGui::PopTextWrapPos(); + ImGui::EndTooltip(); + } + } + if (clicked) { const bool shift_down = ImGui::GetIO().KeyShift; const bool ctrl_down = ImGui::GetIO().KeyCtrl || ImGui::GetIO().KeySuper; diff --git a/openpilot/tools/jotpluggler/common.cc b/openpilot/tools/jotpluggler/common.cc index 50f5fc0b95..4136063607 100644 --- a/openpilot/tools/jotpluggler/common.cc +++ b/openpilot/tools/jotpluggler/common.cc @@ -185,3 +185,14 @@ std::string route_google_maps_url(const GpsTrace &trace) { if (url.size() <= 1900 || n == 0) return url; } } + +void merge_text_labels(RouteSeries *incoming, const EnumInfo &labels, EnumInfo *destination) { + std::vector remap; + remap.reserve(labels.names.size()); + for (const std::string &text : labels.names) { + remap.push_back(destination->text_value(text)); + } + for (double &value : incoming->values) { + value = remap.at(static_cast(value)); + } +} diff --git a/openpilot/tools/jotpluggler/generate_event_extractors.py b/openpilot/tools/jotpluggler/generate_event_extractors.py index a424ebc237..2dc506e4be 100644 --- a/openpilot/tools/jotpluggler/generate_event_extractors.py +++ b/openpilot/tools/jotpluggler/generate_event_extractors.py @@ -18,7 +18,7 @@ SCALAR_KINDS = { "enum": "Enum", } NESTED_TYPE_KINDS = {"struct", "list"} -IGNORED_TYPE_KINDS = {"void", "text", "data", "interface", "anyPointer"} +IGNORED_TYPE_KINDS = {"void", "data", "interface", "anyPointer"} def cxx_string(value): @@ -114,6 +114,11 @@ class Generator: self.emit(indent, f"append_fixed_scalar_point(&series->fixed_series[{slot}], tm, {double_expr});") return + if type_kind == "text": + target = f"ensure_dynamic_series({path_expr}, series)" if dynamic_path else f"&series->fixed_series[{self.add_fixed_path(path)}]" + self.emit(indent, f"append_text_point({target}, tm, {expr}, series);") + return + if type_kind == "struct": self.emit_struct(indent, schema, expr, path, path_expr, dynamic_path) return @@ -180,7 +185,7 @@ class Generator: self.emit(indent, f"const std::string {base_path_var} = {cxx_string(path)};") elem_scalar = scalar_kind(elem_type) - if elem_scalar is not None: + if elem_scalar is not None or elem_kind == "text": self.emit(indent, f"if ({list_expr}.size() <= 16) {{") index_var = self.tmp("i") self.emit(indent + 2, f"for (uint {index_var} = 0; {index_var} < {list_expr}.size(); ++{index_var}) {{") @@ -188,7 +193,10 @@ class Generator: self.emit(indent + 4, f"RouteSeries *{item_series} = ensure_list_scalar_series({base_path_var}, {index_var}, series);") if elem_scalar == "Enum": self.emit_enum_capture(indent + 4, f"{item_series}->path", enum_names(schema.elementType)) - self.emit(indent + 4, f"append_fixed_scalar_point({item_series}, tm, {self.scalar_double_expr(f'{list_expr}[{index_var}]', elem_scalar)});") + if elem_kind == "text": + self.emit(indent + 4, f"append_text_point({item_series}, tm, {list_expr}[{index_var}], series);") + else: + self.emit(indent + 4, f"append_fixed_scalar_point({item_series}, tm, {self.scalar_double_expr(f'{list_expr}[{index_var}]', elem_scalar)});") self.emit(indent + 2, "}") self.emit(indent, "}") return @@ -207,7 +215,7 @@ class Generator: self.emit(indent, "}") def node_emits(self, type_kind, type_proto, schema, seen=frozenset()): - if scalar_kind(type_proto) is not None: + if scalar_kind(type_proto) is not None or type_kind == "text": return True if type_kind == "struct": if schema is None: @@ -238,7 +246,7 @@ class Generator: elem_kind = elem_type.which() if elem_kind in IGNORED_TYPE_KINDS: return False - if scalar_kind(elem_type) is not None: + if scalar_kind(elem_type) is not None or elem_kind == "text": return True if elem_kind == "struct": return self.node_emits("struct", None, schema.elementType, seen) diff --git a/openpilot/tools/jotpluggler/plot.cc b/openpilot/tools/jotpluggler/plot.cc index 019eff7070..b8697c6872 100644 --- a/openpilot/tools/jotpluggler/plot.cc +++ b/openpilot/tools/jotpluggler/plot.cc @@ -117,8 +117,8 @@ ImU32 state_block_color(int value, float alpha = 1.0f) { std::string state_block_label(const PreparedCurve &curve, int value) { if (curve.enum_info != nullptr && value >= 0 && static_cast(value) < curve.enum_info->names.size()) { const std::string &name = curve.enum_info->names[static_cast(value)]; - if (!name.empty()) { - return name; + if (!name.empty() || curve.enum_info->is_text) { + return name.empty() ? "(empty)" : name; } } return std::to_string(value); diff --git a/openpilot/tools/jotpluggler/sketch_layout.cc b/openpilot/tools/jotpluggler/sketch_layout.cc index 1971800c41..8fb634f7fa 100644 --- a/openpilot/tools/jotpluggler/sketch_layout.cc +++ b/openpilot/tools/jotpluggler/sketch_layout.cc @@ -74,6 +74,8 @@ struct SeriesAccumulator { void append_fixed_scalar_point(RouteSeries *series, double tm, double value); void append_dynamic_scalar_point(const std::string &path, double tm, double value, SeriesAccumulator *series); +RouteSeries *ensure_dynamic_series(const std::string &path, SeriesAccumulator *series); +void append_text_point(RouteSeries *series, double tm, capnp::Text::Reader text, SeriesAccumulator *accumulator); RouteSeries *ensure_list_scalar_series(const std::string &base_path, size_t index, SeriesAccumulator *series); void append_can_frame(CanServiceKind service, uint8_t bus, @@ -959,7 +961,14 @@ void append_can_frame(CanServiceKind service, }); } +void append_text_point(RouteSeries *series, double tm, capnp::Text::Reader text, SeriesAccumulator *accumulator) { + const double value = accumulator->enum_info[series->path].text_value(std::string(text.begin(), text.size())); + append_fixed_scalar_point(series, tm, value); +} + void append_dynamic_scalar_point(const std::string &path, double tm, double value, SeriesAccumulator *series); +RouteSeries *ensure_dynamic_series(const std::string &path, SeriesAccumulator *series); +void append_text_point(RouteSeries *series, double tm, capnp::Text::Reader text, SeriesAccumulator *accumulator); void decode_can_frame(const dbc::Database *can_dbc, const std::string &service_name, @@ -1116,6 +1125,15 @@ void merge_series_accumulator(SeriesAccumulator *dst, SeriesAccumulator *src) { throw std::runtime_error("Fixed-series slot count mismatch during merge"); } + const auto remap_text = [&](RouteSeries &series) { + auto it = src->enum_info.find(series.path); + if (it != src->enum_info.end() && it->second.is_text) { + merge_text_labels(&series, it->second, &dst->enum_info[series.path]); + } + }; + for (RouteSeries &series : src->fixed_series) remap_text(series); + for (RouteSeries &series : src->dynamic_series) remap_text(series); + for (size_t i = 0; i < dst->fixed_series.size(); ++i) { merge_route_series(&dst->fixed_series[i], &src->fixed_series[i]); } diff --git a/openpilot/tools/jotpluggler/stream.cc b/openpilot/tools/jotpluggler/stream.cc index d5e8fa43b7..35bca3d3c5 100644 --- a/openpilot/tools/jotpluggler/stream.cc +++ b/openpilot/tools/jotpluggler/stream.cc @@ -84,7 +84,7 @@ void apply_stream_batch(AppSession *session, UiState *state, StreamExtractBatch } if (!batch.enum_info.empty()) { for (auto &[path, info] : batch.enum_info) { - session->route_data.enum_info[path] = std::move(info); + if (!info.is_text) session->route_data.enum_info[path] = std::move(info); } } @@ -93,6 +93,10 @@ void apply_stream_batch(AppSession *session, UiState *state, StreamExtractBatch std::vector touched_paths; touched_paths.reserve(batch.series.size()); for (RouteSeries &incoming : batch.series) { + auto labels = batch.enum_info.find(incoming.path); + if (labels != batch.enum_info.end() && labels->second.is_text) { + merge_text_labels(&incoming, labels->second, &session->route_data.enum_info[incoming.path]); + } touched_paths.push_back(incoming.path); auto existing_it = session->series_by_path.find(incoming.path); if (existing_it == session->series_by_path.end()) { From c933446486e6923c8468a979b6fab890b46f5cdb Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Mon, 5 Oct 2026 19:52:56 -0700 Subject: [PATCH 71/79] jp: fix file resolving --- openpilot/tools/jotpluggler/sketch_layout.cc | 16 ++- openpilot/tools/lib/file_downloader.py | 104 +++++++++++++++++++ openpilot/tools/replay/py_downloader.cc | 5 + openpilot/tools/replay/py_downloader.h | 3 + 4 files changed, 119 insertions(+), 9 deletions(-) diff --git a/openpilot/tools/jotpluggler/sketch_layout.cc b/openpilot/tools/jotpluggler/sketch_layout.cc index 8fb634f7fa..bca20ec20c 100644 --- a/openpilot/tools/jotpluggler/sketch_layout.cc +++ b/openpilot/tools/jotpluggler/sketch_layout.cc @@ -320,20 +320,18 @@ std::map trim_segments(std::map segments, co std::map load_segments_from_json(const json11::Json &json) { std::map segments; - static const std::regex rx(R"(\/(\d+)\/)"); - for (const auto &value : json.object_items()) { - for (const auto &url : value.second.array_items()) { - const std::string url_str = url.string_value(); - std::smatch match; - if (!std::regex_search(url_str, match, rx)) continue; - add_log_file_to_segments(&segments, std::stoi(match[1].str()), url_str); - } + for (const auto &[number, files] : json.object_items()) { + segments[std::stoi(number)] = { + files["rlog"].string_value(), files["qlog"].string_value(), files["narrow_road"].string_value(), + files["cabin"].string_value(), files["wide_road"].string_value(), files["qcamera"].string_value(), + }; } return segments; } std::map load_segments_from_server(const RouteSelection &route) { - const std::string result = PyDownloader::getRouteFiles(route.canonical_name); + const std::string selector = route.selector == LogSelector::RLog ? "r" : route.selector == LogSelector::QLog ? "q" : "a"; + const std::string result = PyDownloader::resolveRouteFiles(route.canonical_name, route.begin_segment, route.end_segment, selector); if (result.empty()) throw std::runtime_error("Failed to fetch route files for " + route.canonical_name); std::string parse_error; diff --git a/openpilot/tools/lib/file_downloader.py b/openpilot/tools/lib/file_downloader.py index fd517d7ada..b473740be9 100755 --- a/openpilot/tools/lib/file_downloader.py +++ b/openpilot/tools/lib/file_downloader.py @@ -5,6 +5,7 @@ Called by C++ replay/cabana via subprocess. Subcommands: route-files - Get route file URLs as JSON + resolve-route-files - Resolve logs and cameras from API and internal storage download - Download/decompress URL to local cache, print local path decompress - Decompress a local log file, print temporary path devices - List user's devices as JSON @@ -18,6 +19,8 @@ import os import shutil import sys import tempfile +import traceback +from urllib.parse import urlparse from openpilot.common.hardware.hw import Paths @@ -100,6 +103,100 @@ def cmd_route_files(args): api_call(lambda api: api.get(f"v1/route/{args.route}/files")) +def resolve_route_files(route_name: str, begin: int = 0, end: int = -1, selector: str = "a") -> dict[int, dict[str, str]]: + from openpilot.tools.lib.filereader import DATA_ENDPOINT + from openpilot.tools.lib.file_sources import InternalUnavailableException, internal_source + from openpilot.tools.lib.route import FileName, SegmentRange + from openpilot.tools.lib.api import CommaApi + from openpilot.tools.lib.auth_config import get_token + + sr = SegmentRange(route_name) + api_error = None + try: + route_files = CommaApi(get_token()).get(f"v1/route/{sr.route_name}/files") + except Exception as e: + api_error = e + route_files = {} + fields = {"rlog": FileName.RLOG, "qlog": FileName.QLOG, + "narrow_road": FileName.FCAMERA, "cabin": FileName.DCAMERA, "wide_road": FileName.ECAMERA, "qcamera": FileName.QCAMERA} + names = {name: key for key, fns in fields.items() for name in fns} + names.update({"rlog": "rlog", "qlog": "qlog"}) + api_files: dict[int, dict[str, str]] = {} + for url in (url for urls in route_files.values() for url in urls): + _, number, name = urlparse(url).path.rsplit("/", 2) + if name in names: + api_files.setdefault(int(number), {})[names[name]] = url + # Preserve replay's inclusive segment bounds. + open_ended = end < 0 + local_internal = os.path.isdir(DATA_ENDPOINT) + if open_ended: + end = max(begin, max(api_files, default=0)) + if local_internal: + internal_dir = os.path.join(DATA_ENDPOINT, sr.dongle_id, sr.log_id) + if os.path.isdir(internal_dir): + end = max(end, max((int(name) for name in os.listdir(internal_dir) if name.isdigit()), default=0)) + indices = list(range(max(0, begin), end + 1)) + segments: dict[int, dict[str, str]] = {seg: {} for seg in indices} + internal_errors: dict[str, Exception] = {} + + def resolve(key, requested): + files = {seg: api_files[seg][key] for seg in requested if key in api_files.get(seg, {})} + missing = [seg for seg in requested if seg not in files] + if missing: + try: + files.update(internal_source(sr, missing, fields[key])) + except Exception as e: + if e is not InternalUnavailableException: + internal_errors[key] = e + for seg, path in files.items(): + segments[seg][key] = path + + log_types = ["qlog"] if selector == "q" else ["rlog"] + if selector == "a": + log_types.append("qlog") + for key in log_types: + resolve(key, [seg for seg in indices if not segments[seg]]) + if open_ended and not local_internal: + # Remote internal storage exposes individual files, not a segment listing. + # Probe the contiguous tail beyond the API's last segment until no selected log exists. + number = end + 1 + while True: + internal_errors.clear() + segments[number] = {} + for key in log_types: + if not segments[number]: + resolve(key, [number]) + if not segments[number]: + if internal_errors: + print(f"Internal discovery stopped at segment {number}; keeping resolved logs", file=sys.stderr) + traceback.print_exception(next(iter(internal_errors.values()))) + del segments[number] + break + indices.append(number) + number += 1 + available = [seg for seg in indices if segments[seg]] + if not available: + if api_error is not None: + raise api_error + raise FileNotFoundError(f"No {selector} logs found for {sr.route_name} in segments {indices} through the API or internal storage") + for key in ("narrow_road", "cabin", "wide_road", "qcamera"): + resolve(key, available) + return {seg: segments[seg] for seg in available} + + +def cmd_resolve_route_files(args): + try: + result = resolve_route_files(args.route, args.begin, args.end, args.selector) + except Exception as e: + error = f"{type(e).__name__} resolving {args.route} (begin={args.begin}, end={args.end}, selector={args.selector}): {e}" + print(error, file=sys.stderr) + traceback.print_exc() + result = {"error": error} + json.dump(result, sys.stdout) + sys.stdout.write("\n") + sys.stdout.flush() + + def cmd_download(args): url = args.url use_cache = not args.no_cache @@ -232,6 +329,13 @@ def main(): p_rf.add_argument("route") p_rf.set_defaults(func=cmd_route_files) + p_resolve = subparsers.add_parser("resolve-route-files") + p_resolve.add_argument("route") + p_resolve.add_argument("--begin", type=int, default=0) + p_resolve.add_argument("--end", type=int, default=-1, help="Inclusive last segment; negative means open-ended") + p_resolve.add_argument("--selector", choices=("a", "r", "q"), default="a") + p_resolve.set_defaults(func=cmd_resolve_route_files) + p_dl = subparsers.add_parser("download") p_dl.add_argument("url") p_dl.add_argument("--no-cache", action="store_true") diff --git a/openpilot/tools/replay/py_downloader.cc b/openpilot/tools/replay/py_downloader.cc index 286d8a7319..9d4ed04122 100644 --- a/openpilot/tools/replay/py_downloader.cc +++ b/openpilot/tools/replay/py_downloader.cc @@ -229,6 +229,11 @@ std::string getRouteFiles(const std::string &route) { return runPython(DOWNLOADER_MODULE, {"route-files", route}); } +std::string resolveRouteFiles(const std::string &route, int begin, int end, const std::string &selector) { + return runPython(DOWNLOADER_MODULE, {"resolve-route-files", route, "--begin", std::to_string(begin), + "--end", std::to_string(end), "--selector", selector}); +} + std::string authenticate(const std::string &provider, std::atomic *abort) { return runPython(AUTH_MODULE, {provider, "--json"}, abort); } diff --git a/openpilot/tools/replay/py_downloader.h b/openpilot/tools/replay/py_downloader.h index fac66dabd5..92bc62140b 100644 --- a/openpilot/tools/replay/py_downloader.h +++ b/openpilot/tools/replay/py_downloader.h @@ -18,6 +18,9 @@ std::string decompress(const std::string &path, std::atomic *abort = nullp // Returns JSON string of route files (same format as /v1/route/.../files API) std::string getRouteFiles(const std::string &route); +// Resolves API/internal files into a JSON manifest keyed by segment number. End is inclusive. +std::string resolveRouteFiles(const std::string &route, int begin, int end, const std::string &selector); + // Browser sign-in; abort closes the local callback server. Returns a JSON status. std::string authenticate(const std::string &provider, std::atomic *abort); From ab8c84c3989bab34969d83e78c5fad3ee5b95112 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Harald=20Sch=C3=A4fer?= Date: Tue, 6 Oct 2026 12:01:13 -0700 Subject: [PATCH 72/79] Gas override boost (#39015) * Add gas override boost, mici boost bar, and Rivian maximum set speed * Refactor acceleration boost and gate accumulation on model limit * Fade acceleration boost with model acceleration * Limit boost buildup per gas override * Latch boost eligibility before gas override * Move model limiting check into AccelBoost * Use DT_MDL directly in AccelBoost * Clarify acceleration boost state names * Scale acceleration boost by speed instead of decaying it * Enable Experimental Mode by default * Scale boost buildup with the application speed ramp * Fix boost serialization and model-limit eligibility * Remove boost UI and Experimental Mode default changes * Combine boost update and application --- openpilot/cereal/log.capnp | 1 + .../selfdrive/controls/lib/accel_boost.py | 39 +++++++++++++++++++ .../controls/lib/longitudinal_planner.py | 5 +++ 3 files changed, 45 insertions(+) create mode 100644 openpilot/selfdrive/controls/lib/accel_boost.py diff --git a/openpilot/cereal/log.capnp b/openpilot/cereal/log.capnp index 48bbdb9797..82d4b1ba73 100644 --- a/openpilot/cereal/log.capnp +++ b/openpilot/cereal/log.capnp @@ -1240,6 +1240,7 @@ struct LongitudinalPlan @0xe00b5b3eba12876c { shouldStop @37: Bool; allowThrottle @38: Bool; allowBrake @39: Bool; + accelBoost @40 :Float32; solverExecutionTime @35 :Float32; diff --git a/openpilot/selfdrive/controls/lib/accel_boost.py b/openpilot/selfdrive/controls/lib/accel_boost.py new file mode 100644 index 0000000000..6059f8dabc --- /dev/null +++ b/openpilot/selfdrive/controls/lib/accel_boost.py @@ -0,0 +1,39 @@ +import numpy as np +from openpilot.common.constants import CV +from openpilot.common.realtime import DT_MDL + +ACCEL_BOOST_MAX = 0.2 +ACCEL_BOOST_RATE = 0.025 +ACCEL_BOOST_PER_OVERRIDE = 0.05 +ACCEL_BOOST_MIN_SPEED = 10 * CV.MPH_TO_MS + + +class AccelBoost: + def __init__(self): + self.total_boost = 0.0 + self.boost_this_override = 0.0 + self.boost_eligible = False + + def update(self, sm, output_a_target_e2e, output_a_target_mpc, a_cruise): + enabled = sm['selfdriveState'].enabled + gas_pressed = sm['carState'].gasPressed + v_ego = sm['carState'].vEgo + speed_scale = np.interp(v_ego, [ACCEL_BOOST_MIN_SPEED, 2 * ACCEL_BOOST_MIN_SPEED], [0.0, 1.0]) + boost_scale = speed_scale * np.interp(output_a_target_e2e, [-1.0, -0.5, 5.0], [0.0, 1.0, 1.0], right=0.0) + model_limited = (sm['selfdriveState'].experimentalMode and + output_a_target_e2e + self.total_boost * boost_scale < min(output_a_target_mpc, a_cruise) - 0.1) + + if not enabled or not gas_pressed: + self.boost_this_override = 0.0 + # Latch eligibility before the override changes the lead plan. + self.boost_eligible = enabled and model_limited + + if not enabled: + self.total_boost = 0.0 + elif gas_pressed and self.boost_eligible: + increase = min(ACCEL_BOOST_RATE * DT_MDL * speed_scale, + ACCEL_BOOST_PER_OVERRIDE - self.boost_this_override, ACCEL_BOOST_MAX - self.total_boost) + self.total_boost += increase + self.boost_this_override += increase + + return output_a_target_e2e + self.total_boost * boost_scale diff --git a/openpilot/selfdrive/controls/lib/longitudinal_planner.py b/openpilot/selfdrive/controls/lib/longitudinal_planner.py index cc1345a6ae..d453b677f8 100755 --- a/openpilot/selfdrive/controls/lib/longitudinal_planner.py +++ b/openpilot/selfdrive/controls/lib/longitudinal_planner.py @@ -8,6 +8,7 @@ from openpilot.common.constants import CV from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.realtime import DT_MDL from openpilot.selfdrive.modeld.constants import ModelConstants +from openpilot.selfdrive.controls.lib.accel_boost import AccelBoost from openpilot.selfdrive.controls.lib.longcontrol import LongCtrlState from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import LongitudinalMpc, LongitudinalPlanSource from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import T_IDXS as T_IDXS_MPC @@ -60,6 +61,7 @@ class LongitudinalPlanner: self.fcw = False self.dt = dt self.allow_throttle = True + self.accel_boost = AccelBoost() self.v_desired_filter = FirstOrderFilter(init_v, 2.0, self.dt) self.a_cruise = init_a @@ -135,6 +137,8 @@ class LongitudinalPlanner: accel_coast, self.allow_throttle) cruise_should_stop = should_stop(v_ego, self.a_cruise) + output_a_target_e2e = self.accel_boost.update(sm, output_a_target_e2e, output_a_target_mpc, self.a_cruise) + candidates = [(output_a_target_mpc, self.mpc.source, output_should_stop_mpc), (self.a_cruise, LongitudinalPlanSource.cruise, cruise_should_stop)] if sm['selfdriveState'].experimentalMode: @@ -165,6 +169,7 @@ class LongitudinalPlanner: longitudinalPlan.fcw = self.fcw longitudinalPlan.aTarget = float(self.output_a_target) + longitudinalPlan.accelBoost = float(self.accel_boost.total_boost) longitudinalPlan.shouldStop = bool(self.output_should_stop) longitudinalPlan.allowBrake = True longitudinalPlan.allowThrottle = bool(self.allow_throttle) From 5b2a59d288f9d1eb3123f7af81ffab4159600b1f Mon Sep 17 00:00:00 2001 From: stef <19478336+stefpi@users.noreply.github.com> Date: Tue, 6 Oct 2026 15:42:39 -0700 Subject: [PATCH 73/79] ui: bookmark fill (#38631) * bookmark fill * fade and sticky click * fade out animation * no funny animation * clean --------- Co-authored-by: Daniel Koepping --- .../assets/icons_mici/onroad/bookmark.png | 4 +- .../icons_mici/onroad/bookmark_fill.png | 3 ++ .../ui/mici/onroad/augmented_road_view.py | 51 +++++++++++++------ 3 files changed, 41 insertions(+), 17 deletions(-) create mode 100644 openpilot/selfdrive/assets/icons_mici/onroad/bookmark_fill.png diff --git a/openpilot/selfdrive/assets/icons_mici/onroad/bookmark.png b/openpilot/selfdrive/assets/icons_mici/onroad/bookmark.png index 305561f509..75a19e7983 100644 --- a/openpilot/selfdrive/assets/icons_mici/onroad/bookmark.png +++ b/openpilot/selfdrive/assets/icons_mici/onroad/bookmark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fd91685bf656e828648acf035a4737acb2c4709e8514cf0aa0a10fa470a9bb60 -size 11580 +oid sha256:92431d37a844206570d73d48f39dd8d934539a89668fe8d5f4cccae38f7b301b +size 11530 diff --git a/openpilot/selfdrive/assets/icons_mici/onroad/bookmark_fill.png b/openpilot/selfdrive/assets/icons_mici/onroad/bookmark_fill.png new file mode 100644 index 0000000000..94cc355358 --- /dev/null +++ b/openpilot/selfdrive/assets/icons_mici/onroad/bookmark_fill.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3efe7d12ae82e0f5a1bd773129cf1eafe3c8c36ff5cba43a6c95f295706062b5 +size 19724 diff --git a/openpilot/selfdrive/ui/mici/onroad/augmented_road_view.py b/openpilot/selfdrive/ui/mici/onroad/augmented_road_view.py index 3430d72f61..4b19f0db5f 100644 --- a/openpilot/selfdrive/ui/mici/onroad/augmented_road_view.py +++ b/openpilot/selfdrive/ui/mici/onroad/augmented_road_view.py @@ -14,7 +14,7 @@ from openpilot.selfdrive.ui.mici.onroad.cameraview import CameraView from openpilot.system.ui.lib.application import FontWeight, gui_app, MousePos, MouseEvent, TextAlignment, TextAlignmentVertical from openpilot.system.ui.widgets.label import UnifiedLabel from openpilot.system.ui.widgets import Widget -from openpilot.common.filter_simple import BounceFilter +from openpilot.common.filter_simple import BounceFilter, FirstOrderFilter from openpilot.common.transformations.camera import DEVICE_CAMERAS, DeviceCameraConfig, view_frame_from_device_frame from openpilot.common.transformations.orientation import rot_from_euler from enum import IntEnum @@ -38,7 +38,7 @@ CAM_Y_OFFSET = 20 class BookmarkIcon(Widget): - PEEK_THRESHOLD = 50 # If icon peeks out this much, snap it fully visible + PEEK_THRESHOLD = 150 # If icon peeks out this much, snap it fully visible FULL_VISIBLE_OFFSET = 200 # How far onscreen when fully visible HIDDEN_OFFSET = -50 # How far offscreen when hidden @@ -46,7 +46,9 @@ class BookmarkIcon(Widget): super().__init__() self._bookmark_callback = bookmark_callback self._icon = gui_app.texture("icons_mici/onroad/bookmark.png", 180, 180) + self._filled_icon = gui_app.texture("icons_mici/onroad/bookmark_fill.png", 180, 180) self._offset_filter = BounceFilter(0.0, 0.1, 1 / gui_app.target_fps) + self._active_alpha = FirstOrderFilter(0.0, 0.1, 1 / gui_app.target_fps) # State self._interacting = False @@ -56,6 +58,8 @@ class BookmarkIcon(Widget): self._is_swiping = False self._is_swiping_left: bool = False self._triggered_time: float = 0.0 + self._triggered_duration = 1.5 + self._swipe_expired = False def is_swiping_left(self) -> bool: """Check if currently swiping left (for scroller to disable).""" @@ -66,18 +70,25 @@ class BookmarkIcon(Widget): return interacting def _update_state(self): + if self._state == BookmarkState.TRIGGERED and rl.get_time() - self._triggered_time >= self._triggered_duration: + self._state = BookmarkState.HIDDEN + self._swipe_expired = self._is_swiping + + swipe_offset = self._swipe_start_x - self._swipe_current_x + armed = self._state == BookmarkState.DRAGGING and swipe_offset > self.PEEK_THRESHOLD if self._state == BookmarkState.DRAGGING: - # Allow pulling past activated position with rubber band effect - swipe_offset = self._swipe_start_x - self._swipe_current_x + # Snap to the released position when armed, while allowing further dragging or cancellation. + if armed: + swipe_offset += self.FULL_VISIBLE_OFFSET - self.PEEK_THRESHOLD swipe_offset = min(swipe_offset, self.FULL_VISIBLE_OFFSET + 50) self._offset_filter.update(swipe_offset) elif self._state == BookmarkState.TRIGGERED: - # Continue animating to fully visible - self._offset_filter.update(self.FULL_VISIBLE_OFFSET) - # Stay in TRIGGERED state for 1 second - if rl.get_time() - self._triggered_time >= 1.5: - self._state = BookmarkState.HIDDEN + # Let another left swipe move the same bookmark. + offset = self.FULL_VISIBLE_OFFSET + if self._is_swiping and self._is_swiping_left: + offset += min(swipe_offset, 50) + self._offset_filter.update(offset) elif self._state == BookmarkState.HIDDEN: self._offset_filter.update(self.HIDDEN_OFFSET) @@ -85,17 +96,23 @@ class BookmarkIcon(Widget): if self._offset_filter.x < 1e-3: self._interacting = False + self._active_alpha.update(float(armed or self._state == BookmarkState.TRIGGERED)) + def _handle_mouse_event(self, mouse_event: MouseEvent): if not ui_state.started: return if mouse_event.left_pressed: + if self._state == BookmarkState.TRIGGERED: + self._triggered_time = rl.get_time() + self._triggered_duration = 0.5 + # Store relative position within widget self._swipe_start_x = mouse_event.pos.x self._swipe_current_x = mouse_event.pos.x self._is_swiping = True self._is_swiping_left = False - self._state = BookmarkState.DRAGGING + self._swipe_expired = False elif mouse_event.left_down and self._is_swiping: self._swipe_current_x = mouse_event.pos.x @@ -103,30 +120,34 @@ class BookmarkIcon(Widget): self._is_swiping_left = swipe_offset > 0 if self._is_swiping_left: self._interacting = True + if self._state == BookmarkState.HIDDEN and not self._swipe_expired: + self._state = BookmarkState.DRAGGING elif mouse_event.left_released: - if self._is_swiping: + if self._is_swiping and self._state == BookmarkState.DRAGGING: swipe_distance = self._swipe_start_x - self._swipe_current_x # If peeking past threshold, transition to animating to fully visible and bookmark if swipe_distance > self.PEEK_THRESHOLD: self._state = BookmarkState.TRIGGERED self._triggered_time = rl.get_time() + self._triggered_duration = 1.5 self._bookmark_callback() else: # Otherwise, transition back to hidden self._state = BookmarkState.HIDDEN - # Reset swipe state - self._is_swiping = False - self._is_swiping_left = False + # Reset swipe state + self._is_swiping = False + self._is_swiping_left = False def _render(self, _): """Render the bookmark icon.""" if self._offset_filter.x > 0: icon_x = self.rect.x + self.rect.width - round(self._offset_filter.x) icon_y = self.rect.y + (self.rect.height - self._icon.height) / 2 # Vertically centered - rl.draw_texture_ex(self._icon, rl.Vector2(icon_x, icon_y), 0.0, 1.0, rl.WHITE) + for icon, alpha in ((self._icon, 1.0 - self._active_alpha.x), (self._filled_icon, self._active_alpha.x)): + rl.draw_texture_ex(icon, rl.Vector2(icon_x, icon_y), 0.0, 1.0, rl.Color(255, 255, 255, round(255 * alpha))) class AugmentedRoadView(CameraView): From 035a447d1f0f0ef5abf2ed9724e33d9265399019 Mon Sep 17 00:00:00 2001 From: Daniel Koepping Date: Tue, 6 Oct 2026 15:51:25 -0700 Subject: [PATCH 74/79] ui: longitudinal policy indicator (#39040) * ui: mici longitudinal icons * ui: rm orange state * ui: lead policy indicator * fix * show on op long * make own widget * sync long indicator with dmoji * fast fade in with dmoji --- .../assets/icons_mici/longitudinal/car.png | 3 + .../icons_mici/longitudinal/car_green.png | 3 + .../icons_mici/longitudinal/distance_1.png | 3 + .../longitudinal/distance_1_green.png | 3 + .../icons_mici/longitudinal/distance_2.png | 3 + .../longitudinal/distance_2_green.png | 3 + .../icons_mici/longitudinal/distance_3.png | 3 + .../longitudinal/distance_3_green.png | 3 + .../ui/mici/onroad/augmented_road_view.py | 4 + .../ui/mici/onroad/long_indicator.py | 89 +++++++++++++++++++ .../ui/mici/onroad/model_renderer.py | 2 +- 11 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 openpilot/selfdrive/assets/icons_mici/longitudinal/car.png create mode 100644 openpilot/selfdrive/assets/icons_mici/longitudinal/car_green.png create mode 100644 openpilot/selfdrive/assets/icons_mici/longitudinal/distance_1.png create mode 100644 openpilot/selfdrive/assets/icons_mici/longitudinal/distance_1_green.png create mode 100644 openpilot/selfdrive/assets/icons_mici/longitudinal/distance_2.png create mode 100644 openpilot/selfdrive/assets/icons_mici/longitudinal/distance_2_green.png create mode 100644 openpilot/selfdrive/assets/icons_mici/longitudinal/distance_3.png create mode 100644 openpilot/selfdrive/assets/icons_mici/longitudinal/distance_3_green.png create mode 100644 openpilot/selfdrive/ui/mici/onroad/long_indicator.py diff --git a/openpilot/selfdrive/assets/icons_mici/longitudinal/car.png b/openpilot/selfdrive/assets/icons_mici/longitudinal/car.png new file mode 100644 index 0000000000..221a8e1ceb --- /dev/null +++ b/openpilot/selfdrive/assets/icons_mici/longitudinal/car.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e0936a3d890a3ad68ffe021e4905b9b9e59ffc2ae19ee9c703daa3e7c92e7c78 +size 1793 diff --git a/openpilot/selfdrive/assets/icons_mici/longitudinal/car_green.png b/openpilot/selfdrive/assets/icons_mici/longitudinal/car_green.png new file mode 100644 index 0000000000..82d757faf6 --- /dev/null +++ b/openpilot/selfdrive/assets/icons_mici/longitudinal/car_green.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:57c7ba72642b0350ee764c924586fa66ff94915be4aba1813797e67eb4b74376 +size 33215 diff --git a/openpilot/selfdrive/assets/icons_mici/longitudinal/distance_1.png b/openpilot/selfdrive/assets/icons_mici/longitudinal/distance_1.png new file mode 100644 index 0000000000..3bccf1b0be --- /dev/null +++ b/openpilot/selfdrive/assets/icons_mici/longitudinal/distance_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:71dc069bd3dc5b11ba8e1b52eb3a1025ee0ad8c82834f94b4b2269ec0d401920 +size 237 diff --git a/openpilot/selfdrive/assets/icons_mici/longitudinal/distance_1_green.png b/openpilot/selfdrive/assets/icons_mici/longitudinal/distance_1_green.png new file mode 100644 index 0000000000..038d82958f --- /dev/null +++ b/openpilot/selfdrive/assets/icons_mici/longitudinal/distance_1_green.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:73a6fe02cf19105c0051a6664a424c1bf2ede364c6cf3791f235c86ffa203f39 +size 10348 diff --git a/openpilot/selfdrive/assets/icons_mici/longitudinal/distance_2.png b/openpilot/selfdrive/assets/icons_mici/longitudinal/distance_2.png new file mode 100644 index 0000000000..c164b7b9d4 --- /dev/null +++ b/openpilot/selfdrive/assets/icons_mici/longitudinal/distance_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4f500f118dca3eedbd86fc6cc943ad8be67fe5d4f7b474d5b452cd757ad805c3 +size 282 diff --git a/openpilot/selfdrive/assets/icons_mici/longitudinal/distance_2_green.png b/openpilot/selfdrive/assets/icons_mici/longitudinal/distance_2_green.png new file mode 100644 index 0000000000..b2b56b6c01 --- /dev/null +++ b/openpilot/selfdrive/assets/icons_mici/longitudinal/distance_2_green.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8a49996f0ce069a048abe94ba9424de655dcff53745676ab35e045204af88a3c +size 12103 diff --git a/openpilot/selfdrive/assets/icons_mici/longitudinal/distance_3.png b/openpilot/selfdrive/assets/icons_mici/longitudinal/distance_3.png new file mode 100644 index 0000000000..ef4b57207d --- /dev/null +++ b/openpilot/selfdrive/assets/icons_mici/longitudinal/distance_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8542374779f1908dc52c8a126bc041efea4c4d605afdd34e3bf8e520a7e78bf2 +size 320 diff --git a/openpilot/selfdrive/assets/icons_mici/longitudinal/distance_3_green.png b/openpilot/selfdrive/assets/icons_mici/longitudinal/distance_3_green.png new file mode 100644 index 0000000000..f4688fc2d3 --- /dev/null +++ b/openpilot/selfdrive/assets/icons_mici/longitudinal/distance_3_green.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:21e656b387c1a7e8de54d3e1b1c246f442cffe3c5e0924273d6d6d897c00b568 +size 13868 diff --git a/openpilot/selfdrive/ui/mici/onroad/augmented_road_view.py b/openpilot/selfdrive/ui/mici/onroad/augmented_road_view.py index 4b19f0db5f..eecc196946 100644 --- a/openpilot/selfdrive/ui/mici/onroad/augmented_road_view.py +++ b/openpilot/selfdrive/ui/mici/onroad/augmented_road_view.py @@ -8,6 +8,7 @@ from openpilot.selfdrive.ui.mici.onroad import SIDE_PANEL_WIDTH from openpilot.selfdrive.ui.mici.onroad.alert_renderer import AlertRenderer from openpilot.selfdrive.ui.mici.onroad.driver_state import DriverStateRenderer from openpilot.selfdrive.ui.mici.onroad.hud_renderer import HudRenderer +from openpilot.selfdrive.ui.mici.onroad.long_indicator import LongIndicator from openpilot.selfdrive.ui.mici.onroad.model_renderer import ModelRenderer from openpilot.selfdrive.ui.mici.onroad.confidence_ball import ConfidenceBall from openpilot.selfdrive.ui.mici.onroad.cameraview import CameraView @@ -170,6 +171,7 @@ class AugmentedRoadView(CameraView): self._model_renderer = ModelRenderer() self._hud_renderer = HudRenderer() + self._long_indicator = LongIndicator() self._alert_renderer = AlertRenderer() self._driver_state_renderer = DriverStateRenderer() self._confidence_ball = ConfidenceBall() @@ -246,6 +248,8 @@ class AugmentedRoadView(CameraView): self._driver_state_renderer.set_should_draw(should_draw_dmoji) self._driver_state_renderer.set_position(self._rect.x + 16, self._rect.y + 10) self._driver_state_renderer.render() + self._long_indicator.set_should_draw(not self._hud_renderer.drawing_top_icons()) + self._long_indicator.render(self._content_rect) self._hud_renderer.set_can_draw_top_icons(alert_to_render is None) self._hud_renderer.set_wheel_critical_icon(alert_to_render is not None and not not_animating_out and diff --git a/openpilot/selfdrive/ui/mici/onroad/long_indicator.py b/openpilot/selfdrive/ui/mici/onroad/long_indicator.py new file mode 100644 index 0000000000..1ff83082a2 --- /dev/null +++ b/openpilot/selfdrive/ui/mici/onroad/long_indicator.py @@ -0,0 +1,89 @@ +import pyray as rl +from openpilot.cereal import log +from openpilot.common.filter_simple import FirstOrderFilter +from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.system.ui.lib.application import gui_app +from openpilot.system.ui.widgets import Widget + +HIGHLIGHT_TIME = 2.5 # seconds + + +class LongIndicator(Widget): + def __init__(self): + super().__init__() + self._txt_lead_car = (self._texture('car', 35, 27), self._texture('car_green', 50, 42)) + self._txt_distance = [(self._texture('distance_1', 32, 7), self._texture('distance_1_green', 60, 35)), + (self._texture('distance_2', 40, 9), self._texture('distance_2_green', 68, 37)), + (self._texture('distance_3', 48, 11), self._texture('distance_3_green', 76, 39))] + self._alpha_filter = FirstOrderFilter(0.0, 0.05, 1 / gui_app.target_fps) + # crossfade between states, a white and a green filter per icon + self._lead_car_filters = (FirstOrderFilter(0.0, 0.1, 1 / gui_app.target_fps), FirstOrderFilter(0.0, 0.1, 1 / gui_app.target_fps)) + self._distance_filters = [(FirstOrderFilter(0.0, 0.1, 1 / gui_app.target_fps), FirstOrderFilter(0.0, 0.1, 1 / gui_app.target_fps)) + for _ in range(3)] + self._personality: int | None = None + self._personality_changed_time = -HIGHLIGHT_TIME + self._should_draw = False + + @staticmethod + def _texture(name: str, width: int, height: int) -> rl.Texture: + return gui_app.texture(f'icons_mici/longitudinal/{name}.png', width, height, keep_aspect_ratio=False) + + def set_should_draw(self, should_draw: bool): + self._should_draw = should_draw + + def _render(self, rect: rl.Rectangle) -> None: + sm = ui_state.sm + if sm.recv_frame['selfdriveState'] < ui_state.started_frame or not sm['selfdriveState'].enabled or not ui_state.has_longitudinal_control: + self._personality = None + self._personality_changed_time = -HIGHLIGHT_TIME + self._alpha_filter.x = 0.0 + return + + # hidden under alerts and set speed + visible = self._should_draw and sm['selfdriveState'].alertSize == log.SelfdriveState.AlertSize.none + alpha = self._alpha_filter.update(visible) + self._draw_lead_car(rect, alpha) + self._draw_distance_bars(rect, alpha, visible) + + def _draw_lead_car(self, rect: rl.Rectangle, alpha: float) -> None: + sm = ui_state.sm + plan = sm['longitudinalPlan'] + has_lead = sm.alive['longitudinalPlan'] and plan.hasLead + + e2e = has_lead and plan.longitudinalPlanSource == log.LongitudinalPlan.LongitudinalPlanSource.e2e + white_f, green_f = self._lead_car_filters + white_alpha = white_f.update(0.0 if e2e else 0.9 if has_lead else 0.35) + green_alpha = green_f.update(float(e2e)) + + white, green = self._txt_lead_car + self._draw_centered(white, rect, 100, white_alpha * alpha) + self._draw_centered(green, rect, 100, green_alpha * alpha) + + def _draw_distance_bars(self, rect: rl.Rectangle, alpha: float, visible: bool) -> None: + sm = ui_state.sm + now = rl.get_time() + personality = sm['selfdriveState'].personality.raw + if self._personality is not None and personality != self._personality: + self._personality_changed_time = now + self._personality = personality + # the personality alert covers the bars, hold the highlight until they show + if not visible and now - self._personality_changed_time < HIGHLIGHT_TIME: + self._personality_changed_time = now + highlight = now - self._personality_changed_time < HIGHLIGHT_TIME + + # blink at double the turn signal rate (2.67 Hz) while overriding the gas + overriding = any(e.name == log.OnroadEvent.EventName.gasPressedOverride for e in sm['onroadEvents']) + blink = 0.35 / 0.9 if overriding and now % 0.375 > 0.1875 else 1.0 + + count = personality + 1 + for i, ((white, green), y, (active_f, green_f)) in enumerate(zip(self._txt_distance, (122, 136, 152), self._distance_filters, strict=True)): + active = active_f.update(float(i < count)) + green_alpha = green_f.update(float(highlight and i == count - 1)) + # only lit bars blink on override + self._draw_centered(white, rect, y, (0.35 * (1 - active) + 0.9 * (active - green_alpha) * blink) * alpha) + self._draw_centered(green, rect, y, green_alpha * blink * alpha) + + @staticmethod + def _draw_centered(texture: rl.Texture, rect: rl.Rectangle, y: float, alpha: float) -> None: + pos = rl.Vector2(rect.x + 46 - texture.width / 2, rect.y + y - texture.height / 2) + rl.draw_texture_ex(texture, pos, 0.0, 1.0, rl.Color(255, 255, 255, round(255 * alpha))) diff --git a/openpilot/selfdrive/ui/mici/onroad/model_renderer.py b/openpilot/selfdrive/ui/mici/onroad/model_renderer.py index e927fa506e..e5dcfc44bf 100644 --- a/openpilot/selfdrive/ui/mici/onroad/model_renderer.py +++ b/openpilot/selfdrive/ui/mici/onroad/model_renderer.py @@ -180,7 +180,7 @@ class ModelRenderer(Widget): ss, cs = sm['selfdriveState'], sm['carState'] # braking disengages without making openpilot unavailable available = ss.enabled or ss.engageable or cs.brakePressed - lane = (self._lane_lines[1].raw_points + self._lane_lines[2].raw_points) / 2 + lane = self._path.raw_points opacity = 0.4 if ui_state.status == UIStatus.DISENGAGED else 0.8 for lead, (present, d_rel, y_rel) in zip(self._lead_vehicles, leads, strict=True): visible = available and present and d_rel < MAX_DRAW_DISTANCE and len(lane) > 0 From a0d47bcd08d9c359ccc7bd73145ed90ade2255ab Mon Sep 17 00:00:00 2001 From: Daniel Koepping Date: Tue, 6 Oct 2026 18:43:50 -0700 Subject: [PATCH 75/79] modeld: drop chestnut on fallback (#39044) --- openpilot/selfdrive/modeld/modeld.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/openpilot/selfdrive/modeld/modeld.py b/openpilot/selfdrive/modeld/modeld.py index 0d90f1225e..dbb64ec281 100755 --- a/openpilot/selfdrive/modeld/modeld.py +++ b/openpilot/selfdrive/modeld/modeld.py @@ -77,6 +77,12 @@ def get_action_from_model(model_output: dict[str, np.ndarray], prev_action: log. shouldStop=bool(stop)) +def drop_chestnut() -> None: + if "AMD" in Device._opened_devices: + for d in Device._opened_devices: + Device[d].pending.pop(Device["AMD"], None) + + class ChestnutGpuState: # GPU metrics require modeld's GPU context def __init__(self, pm: PubMaster, big: bool): @@ -281,6 +287,8 @@ def main(demo=False): loader.join(BIG_MODEL_TIMEOUT) model = big_model params.put_bool("ChestnutActive", model is not None) + if model is None: + drop_chestnut() small_model = ModelState(vipc_client_main.width, vipc_client_main.height, False) if model is None or CHESTNUT else None if model is None: @@ -413,6 +421,7 @@ def main(demo=False): raise # fallback to small model cloudlog.exception("big model failed, fall back to small") + drop_chestnut() params.put_bool("ChestnutActive", False) model = small_model if chestnut_state is not None: From 0e0c7c711d82992c98bcaea58e47bb50a255616a Mon Sep 17 00:00:00 2001 From: Warren Date: Tue, 6 Oct 2026 22:27:13 -0400 Subject: [PATCH 76/79] Mountain Dew Model (#39045) big model: desires-final (ea1600cb/12864) --- openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl b/openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl index 7925e6d828..002f78a658 100644 --- a/openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl +++ b/openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cc75858d5e007b725b6264e194da4321d7874a212dc631b8f40a215dfd172cd2 -size 799992286 +oid sha256:4fe66440ef38c94ddbe315e14174912975014a96384201a8a632aabbe2302d66 +size 773090994 From f59056e5b6f92e3136d59ae403097cf0653442ce Mon Sep 17 00:00:00 2001 From: Warren Date: Tue, 6 Oct 2026 22:34:21 -0400 Subject: [PATCH 77/79] Revert "Mountain Dew Model" (#39046) Revert "Mountain Dew Model (#39045)" This reverts commit 0e0c7c711d82992c98bcaea58e47bb50a255616a. --- openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl b/openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl index 002f78a658..7925e6d828 100644 --- a/openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl +++ b/openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4fe66440ef38c94ddbe315e14174912975014a96384201a8a632aabbe2302d66 -size 773090994 +oid sha256:cc75858d5e007b725b6264e194da4321d7874a212dc631b8f40a215dfd172cd2 +size 799992286 From 657d3a4f7c248fd3dd37d6520bf4ea91eeba57d7 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Tue, 6 Oct 2026 23:43:16 -0700 Subject: [PATCH 78/79] Fix Athena test port collision (#39048) --- openpilot/system/athena/tests/test_athenad.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openpilot/system/athena/tests/test_athenad.py b/openpilot/system/athena/tests/test_athenad.py index f9e4c0d334..a89c8800f9 100644 --- a/openpilot/system/athena/tests/test_athenad.py +++ b/openpilot/system/athena/tests/test_athenad.py @@ -58,9 +58,7 @@ def host(): class TestAthenadMethods(OpenpilotTestCase): @classmethod def setup_class(cls): - cls.SOCKET_PORT = 45454 athenad.Api = MockApi # ty: ignore[invalid-assignment] # test double - athenad.LOCAL_PORT_WHITELIST = {cls.SOCKET_PORT} def setup_method(self): self.default_params = { @@ -434,7 +432,7 @@ class TestAthenadMethods(OpenpilotTestCase): assert athenad.upload_queue.qsize() == 1 assert asdict(athenad.upload_queue.queue[-1]) == asdict(item1) - def test_start_local_proxy(self, mock_create_connection): + def test_start_local_proxy(self, mock_create_connection, mocker): end_event = threading.Event() ws_recv = queue.Queue() @@ -442,11 +440,13 @@ class TestAthenadMethods(OpenpilotTestCase): mock_ws = MockWebsocket(ws_recv, ws_send) mock_create_connection.return_value = mock_ws - echo_socket = EchoSocket(self.SOCKET_PORT) + echo_socket = EchoSocket(0) + port = echo_socket.socket.getsockname()[1] + mocker.patch('openpilot.system.athena.athenad.LOCAL_PORT_WHITELIST', {port}) socket_thread = threading.Thread(target=echo_socket.run) socket_thread.start() - athenad.startLocalProxy(end_event, 'ws://localhost:1234', self.SOCKET_PORT) + athenad.startLocalProxy(end_event, 'ws://localhost:1234', port) ws_recv.put_nowait(b'ping') try: From f21bfc36be5f423f31b2c2a948d1e2eff91b02e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Harald=20Sch=C3=A4fer?= Date: Wed, 7 Oct 2026 07:59:43 -0700 Subject: [PATCH 79/79] Enable Experimental Mode by default without confirmation (#39043) * Enable Experimental Mode by default without confirmation * Update toggles.py * Fix formatting in End-to-End Longitudinal Control description --- openpilot/common/params_keys.h | 3 +- .../selfdrive/ui/layouts/settings/toggles.py | 38 ++------------- openpilot/selfdrive/ui/mici/layouts/home.py | 2 +- .../ui/mici/layouts/settings/toggles.py | 48 ++----------------- openpilot/selfdrive/ui/onroad/exp_button.py | 9 +--- openpilot/selfdrive/ui/ui_state.py | 2 - 6 files changed, 10 insertions(+), 92 deletions(-) diff --git a/openpilot/common/params_keys.h b/openpilot/common/params_keys.h index 6a334a7020..93374fdc02 100644 --- a/openpilot/common/params_keys.h +++ b/openpilot/common/params_keys.h @@ -40,8 +40,7 @@ inline static std::unordered_map keys = { {"DriverTooDistracted", {CLEAR_ON_MANAGER_START | CLEAR_ON_IGNITION_ON, BOOL}}, {"DriverLockoutCount", {CLEAR_ON_MANAGER_START | CLEAR_ON_IGNITION_ON, INT, "0"}}, {"AlphaLongitudinalEnabled", {PERSISTENT | DEVELOPMENT_ONLY, BOOL}}, - {"ExperimentalMode", {PERSISTENT, BOOL}}, - {"ExperimentalModeConfirmed", {PERSISTENT, BOOL}}, + {"ExperimentalMode", {PERSISTENT, BOOL, "1"}}, {"FirmwareQueryDone", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}}, {"ForcePowerDown", {PERSISTENT, BOOL}}, {"GitBranch", {PERSISTENT, STRING}}, diff --git a/openpilot/selfdrive/ui/layouts/settings/toggles.py b/openpilot/selfdrive/ui/layouts/settings/toggles.py index 090c49f14e..06091782b1 100644 --- a/openpilot/selfdrive/ui/layouts/settings/toggles.py +++ b/openpilot/selfdrive/ui/layouts/settings/toggles.py @@ -3,10 +3,7 @@ from openpilot.common.params import Params, UnknownKeyName from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.list_view import multiple_button_item, toggle_item from openpilot.system.ui.widgets.scroller_tici import Scroller -from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog -from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.multilang import tr, tr_noop -from openpilot.system.ui.widgets import DialogResult from openpilot.selfdrive.ui.ui_state import ui_state PERSONALITY_TO_INT = log.LongitudinalPersonality.schema.enumerants @@ -156,15 +153,9 @@ class TogglesLayout(Widget): ui_state.update_params() e2e_description = tr( - "openpilot defaults to driving in chill mode. Experimental mode enables alpha-level features that aren't ready for chill mode. " + - "Experimental features are listed below:
" + "

End-to-End Longitudinal Control


" + "Let the driving model control the gas and brakes. openpilot will drive as it thinks a human would, including stopping for red lights and stop signs. " + - "Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; " + - "mistakes should be expected.
" + - "

New Driving Visualization


" + - "The driving visualization will transition to the road-facing wide-angle camera at low speeds to better show some turns. " + - "The Experimental mode logo will also be shown in the top right corner." + "Since the driving model decides the speed to drive, the set speed will only act as an upper bound. Mistakes should be expected.
" ) if ui_state.CP is not None: @@ -212,32 +203,11 @@ class TogglesLayout(Widget): icon = "experimental.png" if self._toggles["ExperimentalMode"].action_item.get_state() else "experimental_white.png" self._toggles["ExperimentalMode"].set_icon(icon) - def _handle_experimental_mode_toggle(self, state: bool): - confirmed = self._params.get_bool("ExperimentalModeConfirmed") - if state and not confirmed: - def confirm_callback(result: DialogResult): - if result == DialogResult.CONFIRM: - self._params.put_bool("ExperimentalMode", True, block=True) - self._params.put_bool("ExperimentalModeConfirmed", True, block=True) - else: - self._toggles["ExperimentalMode"].action_item.set_state(False) - self._update_experimental_mode_icon() - - # show confirmation dialog - content = (f"

{self._toggles['ExperimentalMode'].title}


" + - f"

{self._toggles['ExperimentalMode'].description}

") - dlg = ConfirmDialog(content, tr("Enable"), rich=True, callback=confirm_callback) - gui_app.push_widget(dlg) - else: - self._update_experimental_mode_icon() - self._params.put_bool("ExperimentalMode", state, block=True) - def _toggle_callback(self, state: bool, param: str): - if param == "ExperimentalMode": - self._handle_experimental_mode_toggle(state) - return - self._params.put_bool(param, state, block=True) + if param == "ExperimentalMode": + self._update_experimental_mode_icon() + if self._toggle_defs[param][3]: self._params.put_bool("OnroadCycleRequested", True, block=True) diff --git a/openpilot/selfdrive/ui/mici/layouts/home.py b/openpilot/selfdrive/ui/mici/layouts/home.py index b852bf9ea6..14370d67c4 100644 --- a/openpilot/selfdrive/ui/mici/layouts/home.py +++ b/openpilot/selfdrive/ui/mici/layouts/home.py @@ -173,7 +173,7 @@ class MiciHomeLayout(Widget): if self._mouse_down_t is not None: if time.monotonic() - self._mouse_down_t > 0.5: # long gating for experimental mode - only allow toggle if longitudinal control is available - if ui_state.has_longitudinal_control and ui_state.experimental_mode_confirmed: + if ui_state.has_longitudinal_control: ui_state.experimental_mode = not ui_state.experimental_mode ui_state.params.put("ExperimentalMode", ui_state.experimental_mode, block=True) self._mouse_down_t = None diff --git a/openpilot/selfdrive/ui/mici/layouts/settings/toggles.py b/openpilot/selfdrive/ui/mici/layouts/settings/toggles.py index 40a6188579..0cd552c6cd 100644 --- a/openpilot/selfdrive/ui/mici/layouts/settings/toggles.py +++ b/openpilot/selfdrive/ui/mici/layouts/settings/toggles.py @@ -1,10 +1,7 @@ -from collections.abc import Callable - from openpilot.cereal import log from openpilot.system.ui.widgets.scroller import NavScroller -from openpilot.selfdrive.ui.mici.widgets.button import BigParamControl, BigMultiParamToggle, BigToggle, GreyBigButton -from openpilot.selfdrive.ui.mici.widgets.dialog import BigConfirmationCircleButton +from openpilot.selfdrive.ui.mici.widgets.button import BigParamControl, BigMultiParamToggle from openpilot.system.ui.lib.application import gui_app from openpilot.selfdrive.ui.layouts.settings.common import restart_needed_callback from openpilot.selfdrive.ui.ui_state import ui_state @@ -12,31 +9,6 @@ from openpilot.selfdrive.ui.ui_state import ui_state PERSONALITY_TO_INT = log.LongitudinalPersonality.schema.enumerants -class ExperimentalModeConfirmPage(NavScroller): - def __init__(self, on_confirm: Callable[[], None]): - super().__init__() - - accept = BigConfirmationCircleButton("enable\nexperimental mode", - gui_app.texture("icons_mici/setup/driver_monitoring/dm_check.png", 64, 64), - lambda: self.dismiss(on_confirm)) - - self._scroller.add_widgets([ - GreyBigButton("enabling\nexperimental mode", "scroll to continue", - gui_app.texture("icons_mici/setup/warning.png", 64, 64)), - GreyBigButton("", "openpilot defaults to driving in chill mode."), - GreyBigButton("", "Experimental mode enables alpha-level features that aren't ready for chill mode."), - GreyBigButton("End-to-End Longitudinal Control"), - GreyBigButton("", "Let the driving model control the gas and brakes."), - GreyBigButton("", "openpilot will drive as it thinks a human would, including stopping for red lights and stop signs."), - GreyBigButton("", "The set speed will only act as an upper bound."), - GreyBigButton("", "This is an alpha quality feature; mistakes should be expected."), - GreyBigButton("New Driving Visualization"), - GreyBigButton("", "The path will change colors to communicate acceleration intent."), - GreyBigButton("", "Red for braking, green for acceleration, and gray for coasting."), - accept, - ]) - - class TogglesLayoutMici(NavScroller): def __init__(self): super().__init__() @@ -46,8 +18,8 @@ class TogglesLayoutMici(NavScroller): "Aggressive follows closer, with firmer gas and braking.\n" + "Relaxed leaves more space.\n" + "Use the steering wheel distance button on supported cars.") - self._experimental_btn = BigToggle("experimental mode", description_icon=gui_app.texture("icons_mici/experimental_mode.png", 64, 64), - initial_state=ui_state.params.get_bool("ExperimentalMode"), toggle_callback=self._on_experimental_mode, + self._experimental_btn = BigParamControl("experimental mode", "ExperimentalMode", + description_icon=gui_app.texture("icons_mici/experimental_mode.png", 64, 64), description="Let the driving model control gas and brakes.\n" + "Includes stopping for red lights and stop signs.\n" + "Set speed is a maximum, not a target.\n" + @@ -132,17 +104,3 @@ class TogglesLayoutMici(NavScroller): # Refresh toggles from params to mirror external changes for key, item in self._refresh_toggles: item.set_checked(ui_state.params.get_bool(key)) - - def _on_experimental_mode(self, state: bool): - if state and not ui_state.params.get_bool("ExperimentalModeConfirmed"): - # Don't show enabled state until confirm - self._experimental_btn.set_checked(False) - - def on_confirm(): - ui_state.params.put_bool("ExperimentalModeConfirmed", True) - ui_state.params.put_bool("ExperimentalMode", True) - self._experimental_btn.set_checked(True) - - gui_app.push_widget(ExperimentalModeConfirmPage(on_confirm)) - else: - ui_state.params.put_bool("ExperimentalMode", state) diff --git a/openpilot/selfdrive/ui/onroad/exp_button.py b/openpilot/selfdrive/ui/onroad/exp_button.py index 9a92ebc3c3..a122394c88 100644 --- a/openpilot/selfdrive/ui/onroad/exp_button.py +++ b/openpilot/selfdrive/ui/onroad/exp_button.py @@ -34,7 +34,7 @@ class ExpButton(Widget): def _handle_mouse_release(self, _): super()._handle_mouse_release(_) - if self._is_toggle_allowed(): + if ui_state.has_longitudinal_control: new_mode = not self._experimental_mode self._params.put_bool("ExperimentalMode", new_mode) @@ -61,10 +61,3 @@ class ExpButton(Widget): self._hold_end_time = self._held_mode = None return self._experimental_mode - - def _is_toggle_allowed(self): - if not self._params.get_bool("ExperimentalModeConfirmed"): - return False - - # Mirror exp mode toggle using persistent car params - return ui_state.has_longitudinal_control diff --git a/openpilot/selfdrive/ui/ui_state.py b/openpilot/selfdrive/ui/ui_state.py index 9489673039..647f977677 100644 --- a/openpilot/selfdrive/ui/ui_state.py +++ b/openpilot/selfdrive/ui/ui_state.py @@ -86,7 +86,6 @@ class UIState: self.is_release = self.params.get_bool("IsReleaseBranch") self.always_on_dm: bool = self.params.get_bool("AlwaysOnDM") self.experimental_mode: bool = self.params.get_bool("ExperimentalMode") - self.experimental_mode_confirmed: bool = self.params.get_bool("ExperimentalModeConfirmed") self.chestnut_present: bool = False self.chestnut_compiled: bool = chestnut_compiled() self.chestnut_active: bool | None = None @@ -246,7 +245,6 @@ class UIState: self.is_metric = self.params.get_bool("IsMetric") self.always_on_dm = self.params.get_bool("AlwaysOnDM") self.experimental_mode = self.params.get_bool("ExperimentalMode") - self.experimental_mode_confirmed = self.params.get_bool("ExperimentalModeConfirmed") self.chestnut_active = self.params.get("ChestnutActive") self.chestnut_loading = self.params.get_bool("ChestnutLoading") now = time.monotonic()