From a981f78e2f0143890595c0425aa4f0727780408a Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Fri, 21 Nov 2025 11:23:54 -0800 Subject: [PATCH 001/110] use release branch from system.version --- selfdrive/ui/mici/layouts/home.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/selfdrive/ui/mici/layouts/home.py b/selfdrive/ui/mici/layouts/home.py index 014fc0c45f..75fdfe3e99 100644 --- a/selfdrive/ui/mici/layouts/home.py +++ b/selfdrive/ui/mici/layouts/home.py @@ -8,13 +8,11 @@ from openpilot.system.ui.widgets import Widget from openpilot.system.ui.lib.application import gui_app, FontWeight, DEFAULT_TEXT_COLOR, MousePos from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.system.ui.text import wrap_text -from openpilot.system.version import training_version +from openpilot.system.version import training_version, RELEASE_BRANCHES HEAD_BUTTON_FONT_SIZE = 40 HOME_PADDING = 8 -RELEASE_BRANCH = "release3" - NetworkType = log.DeviceState.NetworkType NETWORK_TYPES = { @@ -187,9 +185,9 @@ class MiciHomeLayout(Widget): if self._version_text is not None: # release branch - if self._version_text[0] == RELEASE_BRANCH: + if self._version_text[1] in RELEASE_BRANCHES: version_pos = rl.Vector2(text_pos.x, text_pos.y + self._openpilot_label.font_size + 16) - self._large_version_label.set_text(self._version_text[0]) + self._large_version_label.set_text("release") self._large_version_label.set_position(version_pos.x, version_pos.y) self._large_version_label.render() From ebc11fdbc83b1400e4829444869b5e6a89f8477c Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Fri, 21 Nov 2025 13:44:22 -0800 Subject: [PATCH 002/110] make "update available" alert clickable (#36670) * click to update * that's it * lil more --- selfdrive/ui/mici/layouts/home.py | 31 +++++++++------------ selfdrive/ui/mici/layouts/offroad_alerts.py | 10 ++++--- system/version.py | 2 +- 3 files changed, 20 insertions(+), 23 deletions(-) diff --git a/selfdrive/ui/mici/layouts/home.py b/selfdrive/ui/mici/layouts/home.py index 75fdfe3e99..6102265a87 100644 --- a/selfdrive/ui/mici/layouts/home.py +++ b/selfdrive/ui/mici/layouts/home.py @@ -185,27 +185,22 @@ class MiciHomeLayout(Widget): if self._version_text is not None: # release branch - if self._version_text[1] in RELEASE_BRANCHES: - version_pos = rl.Vector2(text_pos.x, text_pos.y + self._openpilot_label.font_size + 16) - self._large_version_label.set_text("release") - self._large_version_label.set_position(version_pos.x, version_pos.y) - self._large_version_label.render() + release_branch = self._version_text[1] in RELEASE_BRANCHES + version_pos = rl.Rectangle(text_pos.x, text_pos.y + self._openpilot_label.font_size + 16, 100, 44) + self._version_label.set_text(self._version_text[0]) + self._version_label.set_position(version_pos.x, version_pos.y) + self._version_label.render() - else: - version_pos = rl.Rectangle(text_pos.x, text_pos.y + self._openpilot_label.font_size + 16, 100, 44) - self._version_label.set_text(self._version_text[0]) - self._version_label.set_position(version_pos.x, version_pos.y) - self._version_label.render() + self._date_label.set_text(" " + self._version_text[3]) + self._date_label.set_position(version_pos.x + self._version_label.rect.width + 10, version_pos.y) + self._date_label.render() - self._date_label.set_text(" " + self._version_text[3]) - self._date_label.set_position(version_pos.x + self._version_label.rect.width + 10, version_pos.y) - self._date_label.render() - - self._branch_label.set_width(gui_app.width - self._version_label.rect.width - self._date_label.rect.width - 32) - self._branch_label.set_text(" " + self._version_text[1]) - self._branch_label.set_position(version_pos.x + self._version_label.rect.width + self._date_label.rect.width + 20, version_pos.y) - self._branch_label.render() + self._branch_label.set_width(gui_app.width - self._version_label.rect.width - self._date_label.rect.width - 32) + self._branch_label.set_text(" " + ("release" if release_branch else self._version_text[1])) + self._branch_label.set_position(version_pos.x + self._version_label.rect.width + self._date_label.rect.width + 20, version_pos.y) + self._branch_label.render() + if not release_branch: # 2nd line self._version_commit_label.set_text(self._version_text[2]) self._version_commit_label.set_position(version_pos.x, version_pos.y + self._date_label.font_size + 7) diff --git a/selfdrive/ui/mici/layouts/offroad_alerts.py b/selfdrive/ui/mici/layouts/offroad_alerts.py index 2e9a8bee3c..60f64b31b0 100644 --- a/selfdrive/ui/mici/layouts/offroad_alerts.py +++ b/selfdrive/ui/mici/layouts/offroad_alerts.py @@ -5,6 +5,7 @@ from dataclasses import dataclass from enum import IntEnum from openpilot.common.params import Params from openpilot.selfdrive.selfdrived.alertmanager import OFFROAD_ALERTS +from openpilot.system.hardware import HARDWARE from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.label import UnifiedLabel from openpilot.system.ui.widgets.scroller import Scroller @@ -220,6 +221,7 @@ class MiciOffroadAlerts(Widget): update_alert_data = AlertData(key="UpdateAvailable", text="", severity=-1) self.sorted_alerts.append(update_alert_data) update_alert_item = AlertItem(update_alert_data) + update_alert_item.set_click_callback(lambda: HARDWARE.reboot()) self.alert_items.append(update_alert_item) self._scroller.add_widget(update_alert_item) @@ -244,18 +246,18 @@ class MiciOffroadAlerts(Widget): if update_alert_data: if update_available: - # Default text - update_alert_data.text = "update available. go to comma.ai/blog to read the release notes." + version_string = "" # Get new version description and parse version and date new_desc = self.params.get("UpdaterNewDescription") or "" if new_desc: - # Parse description (format: "version / branch / commit / date") + # format: "version / branch / commit / date" parts = new_desc.split(" / ") if len(parts) > 3: version, date = parts[0], parts[3] - update_alert_data.text = f"update available\n openpilot {version}, {date}. go to comma.ai/blog to read the release notes." + version_string = f"\nopenpilot {version}, {date}\n" + update_alert_data.text = f"Update available {version_string}. Click to update. Read the release notes at blog.comma.ai." update_alert_data.visible = True active_count += 1 else: diff --git a/system/version.py b/system/version.py index 1f01b181cd..0cea616d23 100755 --- a/system/version.py +++ b/system/version.py @@ -10,7 +10,7 @@ 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-tici', 'release-tizi', 'nightly'] +RELEASE_BRANCHES = ['release-tizi-staging', 'release-mici-staging', 'release-tizi', 'release-mici', 'nightly'] TESTED_BRANCHES = RELEASE_BRANCHES + ['devel-staging', 'nightly-dev'] BUILD_METADATA_FILENAME = "build.json" From be8c5491b14b86dcf2c5401defba4dabe60c1a18 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Fri, 21 Nov 2025 14:25:21 -0800 Subject: [PATCH 003/110] even shorter --- selfdrive/ui/mici/layouts/onboarding.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selfdrive/ui/mici/layouts/onboarding.py b/selfdrive/ui/mici/layouts/onboarding.py index afc7bfce17..52dbb785d6 100644 --- a/selfdrive/ui/mici/layouts/onboarding.py +++ b/selfdrive/ui/mici/layouts/onboarding.py @@ -150,7 +150,7 @@ class TrainingGuideRecordFront(SetupTermsPage): super().__init__(on_continue, back_callback=on_back, back_text="no", continue_text="yes") self._title_header = TermsHeader("improve driver monitoring", gui_app.texture("icons_mici/setup/green_dm.png", 60, 60)) - self._dm_label = UnifiedLabel("Do you want to upload driver camera data to improve driver monitoring?", 42, + self._dm_label = UnifiedLabel("Do you want to upload driver camera data?", 42, FontWeight.ROMAN) def show_event(self): From d0c3972cc714664bb5869dccef5feda2b3022ae6 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Fri, 21 Nov 2025 18:55:01 -0800 Subject: [PATCH 004/110] update release branches (#36671) * update release branches * Update README.md --- README.md | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 19a30599bd..a77a80935d 100644 --- a/README.md +++ b/README.md @@ -49,13 +49,17 @@ To use openpilot in a car, you need four things: We have detailed instructions for [how to install the harness and device in a car](https://comma.ai/setup). Note that it's possible to run openpilot on [other hardware](https://blog.comma.ai/self-driving-car-for-free/), although it's not plug-and-play. + ### Branches -| branch | URL | description | -|------------------|----------------------------------------|-------------------------------------------------------------------------------------| -| `release3` | openpilot.comma.ai | This is openpilot's release branch. | -| `release3-staging` | openpilot-test.comma.ai | This is the staging branch for releases. Use it to get new releases slightly early. | -| `nightly` | openpilot-nightly.comma.ai | This is the bleeding edge development branch. Do not expect this to be stable. | -| `nightly-dev` | installer.comma.ai/commaai/nightly-dev | Same as nightly, but includes experimental development features for some cars. | + +Running `master` and other branches directly is supported, but it's recommended to run one of the following prebuilt branches: + +| comma four branch | comma 3X branch | URL | description | +|------------------------|------------------------|----------------------------------------|-------------------------------------------------------------------------------------| +| `release-mici` | `release-tizi` | openpilot.comma.ai | This is openpilot's release branch. | +| `release-mici-staging` | `release-tizi-staging` | openpilot-test.comma.ai | This is the staging branch for releases. Use it to get new releases slightly early. | +| `nightly` | `nightly` | openpilot-nightly.comma.ai | This is the bleeding edge development branch. Do not expect this to be stable. | +| `nightly-dev` | `nightly-dev` | installer.comma.ai/commaai/nightly-dev | Same as nightly, but includes experimental development features for some cars. | To start developing openpilot ------ From c67afb45aeb7b0e5e37f48adb8f0c1da1edeaf2f Mon Sep 17 00:00:00 2001 From: Bruce Wayne Date: Mon, 24 Nov 2025 14:20:20 -0800 Subject: [PATCH 005/110] dead test --- selfdrive/modeld/tests/__init__.py | 0 selfdrive/modeld/tests/test_modeld.py | 102 -------------------------- 2 files changed, 102 deletions(-) delete mode 100644 selfdrive/modeld/tests/__init__.py delete mode 100644 selfdrive/modeld/tests/test_modeld.py diff --git a/selfdrive/modeld/tests/__init__.py b/selfdrive/modeld/tests/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/selfdrive/modeld/tests/test_modeld.py b/selfdrive/modeld/tests/test_modeld.py deleted file mode 100644 index 6927c9e473..0000000000 --- a/selfdrive/modeld/tests/test_modeld.py +++ /dev/null @@ -1,102 +0,0 @@ -import numpy as np -import random - -import cereal.messaging as messaging -from msgq.visionipc import VisionIpcServer, VisionStreamType -from opendbc.car.car_helpers import get_demo_car_params -from openpilot.common.params import Params -from openpilot.common.transformations.camera import DEVICE_CAMERAS -from openpilot.common.realtime import DT_MDL -from openpilot.system.manager.process_config import managed_processes -from openpilot.selfdrive.test.process_replay.vision_meta import meta_from_camera_state - -CAM = DEVICE_CAMERAS[("tici", "ar0231")].fcam -IMG = np.zeros(int(CAM.width*CAM.height*(3/2)), dtype=np.uint8) -IMG_BYTES = IMG.flatten().tobytes() - - -class TestModeld: - - def setup_method(self): - self.vipc_server = VisionIpcServer("camerad") - self.vipc_server.create_buffers(VisionStreamType.VISION_STREAM_ROAD, 40, CAM.width, CAM.height) - self.vipc_server.create_buffers(VisionStreamType.VISION_STREAM_DRIVER, 40, CAM.width, CAM.height) - self.vipc_server.create_buffers(VisionStreamType.VISION_STREAM_WIDE_ROAD, 40, CAM.width, CAM.height) - self.vipc_server.start_listener() - Params().put("CarParams", get_demo_car_params().to_bytes()) - - self.sm = messaging.SubMaster(['modelV2', 'cameraOdometry']) - self.pm = messaging.PubMaster(['roadCameraState', 'wideRoadCameraState', 'liveCalibration']) - - managed_processes['modeld'].start() - self.pm.wait_for_readers_to_update("roadCameraState", 10) - - def teardown_method(self): - managed_processes['modeld'].stop() - del self.vipc_server - - def _send_frames(self, frame_id, cams=None): - if cams is None: - cams = ('roadCameraState', 'wideRoadCameraState') - - cs = None - for cam in cams: - msg = messaging.new_message(cam) - cs = getattr(msg, cam) - cs.frameId = frame_id - cs.timestampSof = int((frame_id * DT_MDL) * 1e9) - cs.timestampEof = int(cs.timestampSof + (DT_MDL * 1e9)) - cam_meta = meta_from_camera_state(cam) - - self.pm.send(msg.which(), msg) - self.vipc_server.send(cam_meta.stream, IMG_BYTES, cs.frameId, - cs.timestampSof, cs.timestampEof) - return cs - - def _wait(self): - self.sm.update(5000) - if self.sm['modelV2'].frameId != self.sm['cameraOdometry'].frameId: - self.sm.update(1000) - - def test_modeld(self): - for n in range(1, 500): - cs = self._send_frames(n) - self._wait() - - mdl = self.sm['modelV2'] - assert mdl.frameId == n - assert mdl.frameIdExtra == n - assert mdl.timestampEof == cs.timestampEof - assert mdl.frameAge == 0 - assert mdl.frameDropPerc == 0 - - odo = self.sm['cameraOdometry'] - assert odo.frameId == n - assert odo.timestampEof == cs.timestampEof - - def test_dropped_frames(self): - """ - modeld should only run on consecutive road frames - """ - frame_id = -1 - road_frames = list() - for n in range(1, 50): - if (random.random() < 0.1) and n > 3: - cams = random.choice([(), ('wideRoadCameraState', )]) - self._send_frames(n, cams) - else: - self._send_frames(n) - road_frames.append(n) - self._wait() - - if len(road_frames) < 3 or road_frames[-1] - road_frames[-2] == 1: - frame_id = road_frames[-1] - - mdl = self.sm['modelV2'] - odo = self.sm['cameraOdometry'] - assert mdl.frameId == frame_id - assert mdl.frameIdExtra == frame_id - assert odo.frameId == frame_id - if n != frame_id: - assert not self.sm.updated['modelV2'] - assert not self.sm.updated['cameraOdometry'] From f01391a7d9b5d0b7a56a346d85c7de42302656b7 Mon Sep 17 00:00:00 2001 From: felsager <76905857+felsager@users.noreply.github.com> Date: Tue, 25 Nov 2025 10:23:02 -0800 Subject: [PATCH 006/110] latcontrol_torque: delay independent jerk and lower kp and lower friction threshold (#36619) --- opendbc_repo | 2 +- selfdrive/controls/lib/latcontrol_torque.py | 35 +++++++++------------ selfdrive/test/process_replay/ref_commit | 2 +- 3 files changed, 17 insertions(+), 22 deletions(-) diff --git a/opendbc_repo b/opendbc_repo index b59f8bdcca..6171d1a976 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit b59f8bdcca8d375b4a5a652d2f2d2ec9cd3503d3 +Subproject commit 6171d1a976b632c4804e90e74a78370532a2f297 diff --git a/selfdrive/controls/lib/latcontrol_torque.py b/selfdrive/controls/lib/latcontrol_torque.py index 443fd1851c..0ba38736db 100644 --- a/selfdrive/controls/lib/latcontrol_torque.py +++ b/selfdrive/controls/lib/latcontrol_torque.py @@ -20,15 +20,17 @@ from openpilot.common.pid import PIDController # Additionally, there is friction in the steering wheel that needs # to be overcome to move it at all, this is compensated for too. -KP = 1.0 -KI = 0.3 -KD = 0.0 +KP = 0.8 +KI = 0.15 + INTERP_SPEEDS = [1, 1.5, 2.0, 3.0, 5, 7.5, 10, 15, 30] KP_INTERP = [250, 120, 65, 30, 11.5, 5.5, 3.5, 2.0, KP] LP_FILTER_CUTOFF_HZ = 1.2 +JERK_LOOKAHEAD_SECONDS = 0.19 +JERK_GAIN = 0.3 LAT_ACCEL_REQUEST_BUFFER_SECONDS = 1.0 -VERSION = 0 +VERSION = 1 class LatControlTorque(LatControl): def __init__(self, CP, CI, dt): @@ -36,13 +38,13 @@ class LatControlTorque(LatControl): self.torque_params = CP.lateralTuning.torque.as_builder() self.torque_from_lateral_accel = CI.torque_from_lateral_accel() self.lateral_accel_from_torque = CI.lateral_accel_from_torque() - self.pid = PIDController([INTERP_SPEEDS, KP_INTERP], KI, KD, rate=1/self.dt) + self.pid = PIDController([INTERP_SPEEDS, KP_INTERP], KI, rate=1/self.dt) self.update_limits() self.steering_angle_deadzone_deg = self.torque_params.steeringAngleDeadzoneDeg self.lat_accel_request_buffer_len = int(LAT_ACCEL_REQUEST_BUFFER_SECONDS / self.dt) self.lat_accel_request_buffer = deque([0.] * self.lat_accel_request_buffer_len , maxlen=self.lat_accel_request_buffer_len) - self.previous_measurement = 0.0 - self.measurement_rate_filter = FirstOrderFilter(0.0, 1 / (2 * np.pi * LP_FILTER_CUTOFF_HZ), self.dt) + self.lookahead_frames = int(JERK_LOOKAHEAD_SECONDS / self.dt) + self.jerk_filter = FirstOrderFilter(0.0, 1 / (2 * np.pi * LP_FILTER_CUTOFF_HZ), self.dt) def update_live_torque_params(self, latAccelFactor, latAccelOffset, friction): self.torque_params.latAccelFactor = latAccelFactor @@ -68,17 +70,15 @@ class LatControlTorque(LatControl): delay_frames = int(np.clip(lat_delay / self.dt, 1, self.lat_accel_request_buffer_len)) expected_lateral_accel = self.lat_accel_request_buffer[-delay_frames] - # TODO factor out lateral jerk from error to later replace it with delay independent alternative + lookahead_idx = int(np.clip(-delay_frames + self.lookahead_frames, -self.lat_accel_request_buffer_len+1, -2)) + raw_lateral_jerk = (self.lat_accel_request_buffer[lookahead_idx+1] - self.lat_accel_request_buffer[lookahead_idx-1]) / (2 * self.dt) + desired_lateral_jerk = self.jerk_filter.update(raw_lateral_jerk) future_desired_lateral_accel = desired_curvature * CS.vEgo ** 2 self.lat_accel_request_buffer.append(future_desired_lateral_accel) gravity_adjusted_future_lateral_accel = future_desired_lateral_accel - roll_compensation - desired_lateral_jerk = (future_desired_lateral_accel - expected_lateral_accel) / lat_delay + setpoint = expected_lateral_accel measurement = measured_curvature * CS.vEgo ** 2 - measurement_rate = self.measurement_rate_filter.update((measurement - self.previous_measurement) / self.dt) - self.previous_measurement = measurement - - setpoint = lat_delay * desired_lateral_jerk + expected_lateral_accel error = setpoint - measurement # do error correction in lateral acceleration space, convert at end to handle non-linear torque responses correctly @@ -86,15 +86,10 @@ class LatControlTorque(LatControl): ff = gravity_adjusted_future_lateral_accel # latAccelOffset corrects roll compensation bias from device roll misalignment relative to car roll ff -= self.torque_params.latAccelOffset - # TODO jerk is weighted by lat_delay for legacy reasons, but should be made independent of it - ff += get_friction(error, lateral_accel_deadzone, FRICTION_THRESHOLD, self.torque_params) + ff += get_friction(error + JERK_GAIN * desired_lateral_jerk, lateral_accel_deadzone, FRICTION_THRESHOLD, self.torque_params) freeze_integrator = steer_limited_by_safety or CS.steeringPressed or CS.vEgo < 5 - output_lataccel = self.pid.update(pid_log.error, - -measurement_rate, - feedforward=ff, - speed=CS.vEgo, - freeze_integrator=freeze_integrator) + output_lataccel = self.pid.update(pid_log.error, speed=CS.vEgo, feedforward=ff, freeze_integrator=freeze_integrator) output_torque = self.torque_from_lateral_accel(output_lataccel, self.torque_params) pid_log.active = True diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit index cdd4301fcf..4a58e321fc 100644 --- a/selfdrive/test/process_replay/ref_commit +++ b/selfdrive/test/process_replay/ref_commit @@ -1 +1 @@ -b508f43fb0481bce0859c9b6ab4f45ee690b8dab \ No newline at end of file +e0ad86508edb61b3eaa1b84662c515d2c3368295 \ No newline at end of file From 49178539f31f57c87986ed37af96017f6805be86 Mon Sep 17 00:00:00 2001 From: YassineYousfi Date: Tue, 25 Nov 2025 18:52:35 -0800 Subject: [PATCH 007/110] dm: DriverProb (#36687) * wip * ci * fix --- selfdrive/monitoring/dmonitoringd.py | 4 ++-- selfdrive/monitoring/helpers.py | 15 +++++++++------ 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/selfdrive/monitoring/dmonitoringd.py b/selfdrive/monitoring/dmonitoringd.py index 1dc256d467..1ac2c2dcba 100755 --- a/selfdrive/monitoring/dmonitoringd.py +++ b/selfdrive/monitoring/dmonitoringd.py @@ -39,8 +39,8 @@ def dmonitoringd_thread(): # save rhd virtual toggle every 5 mins if (sm['driverStateV2'].frameId % 6000 == 0 and not demo_mode and - DM.wheelpos_learner.filtered_stat.n > DM.settings._WHEELPOS_FILTER_MIN_COUNT and - DM.wheel_on_right == (DM.wheelpos_learner.filtered_stat.M > DM.settings._WHEELPOS_THRESHOLD)): + DM.wheelpos.prob_offseter.filtered_stat.n > DM.settings._WHEELPOS_FILTER_MIN_COUNT and + DM.wheel_on_right == (DM.wheelpos.prob_offseter.filtered_stat.M > DM.settings._WHEELPOS_THRESHOLD)): params.put_bool_nonblocking("IsRhdDetected", DM.wheel_on_right) def main(): diff --git a/selfdrive/monitoring/helpers.py b/selfdrive/monitoring/helpers.py index 7697e68b98..5b5e16dde3 100644 --- a/selfdrive/monitoring/helpers.py +++ b/selfdrive/monitoring/helpers.py @@ -98,7 +98,7 @@ class DriverPose: self.cfactor_pitch = 1. self.cfactor_yaw = 1. -class DriverPhone: +class DriverProb: def __init__(self, max_trackable): self.prob = 0. self.prob_offseter = RunningStatFilter(max_trackable=max_trackable) @@ -140,9 +140,9 @@ class DriverMonitoring: self.settings = settings if settings is not None else DRIVER_MONITOR_SETTINGS(device_type=HARDWARE.get_device_type()) # init driver status - self.wheelpos_learner = RunningStatFilter() + self.wheelpos = DriverProb(-1) self.pose = DriverPose(self.settings._POSE_OFFSET_MAX_COUNT) - self.phone = DriverPhone(self.settings._POSE_OFFSET_MAX_COUNT) + self.phone = DriverProb(self.settings._POSE_OFFSET_MAX_COUNT) self.blink = DriverBlink() self.always_on = always_on @@ -256,9 +256,12 @@ class DriverMonitoring: # calibrates only when there's movement and either face detected if car_speed > self.settings._WHEELPOS_CALIB_MIN_SPEED and (driver_state.leftDriverData.faceProb > self.settings._FACE_THRESHOLD or driver_state.rightDriverData.faceProb > self.settings._FACE_THRESHOLD): - self.wheelpos_learner.push_and_update(rhd_pred) - if self.wheelpos_learner.filtered_stat.n > self.settings._WHEELPOS_FILTER_MIN_COUNT or demo_mode: - self.wheel_on_right = self.wheelpos_learner.filtered_stat.M > self.settings._WHEELPOS_THRESHOLD + self.wheelpos.prob_offseter.push_and_update(rhd_pred) + + self.wheelpos.prob_calibrated = self.wheelpos.prob_offseter.filtered_stat.n > self.settings._WHEELPOS_FILTER_MIN_COUNT + + if self.wheelpos.prob_calibrated or demo_mode: + self.wheel_on_right = self.wheelpos.prob_offseter.filtered_stat.M > self.settings._WHEELPOS_THRESHOLD else: self.wheel_on_right = self.wheel_on_right_default # use default/saved if calibration is unfinished # make sure no switching when engaged From dd51bf2021859c478aeb9f4bd167f8c2a47858fa Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 27 Nov 2025 00:46:33 -0800 Subject: [PATCH 008/110] De-duplicate firehose layout (#36701) * consistent name * dedup * FMT * not sure why two --- selfdrive/ui/layouts/settings/firehose.py | 87 ++----------------- .../ui/mici/layouts/settings/firehose.py | 39 +++++---- .../ui/mici/layouts/settings/settings.py | 4 +- 3 files changed, 31 insertions(+), 99 deletions(-) diff --git a/selfdrive/ui/layouts/settings/firehose.py b/selfdrive/ui/layouts/settings/firehose.py index bbd4aef532..9b9b51b18c 100644 --- a/selfdrive/ui/layouts/settings/firehose.py +++ b/selfdrive/ui/layouts/settings/firehose.py @@ -1,19 +1,10 @@ import pyray as rl -import time -import threading -from openpilot.common.api import api_get -from openpilot.common.params import Params -from openpilot.common.swaglog import cloudlog -from openpilot.selfdrive.ui.ui_state import ui_state -from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID -from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE +from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.multilang import tr, trn, tr_noop from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel -from openpilot.system.ui.lib.wrap_text import wrap_text -from openpilot.system.ui.widgets import Widget -from openpilot.selfdrive.ui.lib.api_helpers import get_token +from openpilot.selfdrive.ui.mici.layouts.settings.firehose import FirehoseLayoutBase TITLE = tr_noop("Firehose Mode") DESCRIPTION = tr_noop( @@ -32,50 +23,17 @@ INSTRUCTIONS = tr_noop( ) -class FirehoseLayout(Widget): - PARAM_KEY = "ApiCache_FirehoseStats" - GREEN = rl.Color(46, 204, 113, 255) - RED = rl.Color(231, 76, 60, 255) - GRAY = rl.Color(68, 68, 68, 255) - LIGHT_GRAY = rl.Color(228, 228, 228, 255) - UPDATE_INTERVAL = 30 # seconds - +class FirehoseLayout(FirehoseLayoutBase): def __init__(self): super().__init__() - self.params = Params() - self.segment_count = self._get_segment_count() - self.scroll_panel = GuiScrollPanel() - self._content_height = 0 - - self.running = True - self.update_thread = threading.Thread(target=self._update_loop, daemon=True) - self.update_thread.start() - self.last_update_time = 0 - - def show_event(self): - self.scroll_panel.set_offset(0) - - def _get_segment_count(self) -> int: - stats = self.params.get(self.PARAM_KEY) - if not stats: - return 0 - try: - return int(stats.get("firehose", 0)) - except Exception: - cloudlog.exception(f"Failed to decode firehose stats: {stats}") - return 0 - - def __del__(self): - self.running = False - if self.update_thread and self.update_thread.is_alive(): - self.update_thread.join(timeout=1.0) + self._scroll_panel = GuiScrollPanel() def _render(self, rect: rl.Rectangle): # Calculate content dimensions content_rect = rl.Rectangle(rect.x, rect.y, rect.width, self._content_height) # Handle scrolling and render with clipping - scroll_offset = self.scroll_panel.update(rect, content_rect) + scroll_offset = self._scroll_panel.update(rect, content_rect) rl.begin_scissor_mode(int(rect.x), int(rect.y), int(rect.width), int(rect.height)) self._content_height = self._render_content(rect, scroll_offset) rl.end_scissor_mode() @@ -107,9 +65,9 @@ class FirehoseLayout(Widget): y += 20 + 20 # Contribution count (if available) - if self.segment_count > 0: + if self._segment_count > 0: contrib_text = trn("{} segment of your driving is in the training dataset so far.", - "{} segments of your driving is in the training dataset so far.", self.segment_count).format(self.segment_count) + "{} segments of your driving is in the training dataset so far.", self._segment_count).format(self._segment_count) y = self._draw_wrapped_text(x, y, w, contrib_text, gui_app.font(FontWeight.BOLD), 52, rl.WHITE) y += 20 + 20 @@ -121,7 +79,7 @@ class FirehoseLayout(Widget): y = self._draw_wrapped_text(x, y, w, tr(INSTRUCTIONS), gui_app.font(FontWeight.NORMAL), 40, self.LIGHT_GRAY) # bottom margin + remove effect of scroll offset - return int(round(y - self.scroll_panel.offset + 40)) + return int(round(y - self._scroll_panel.offset + 40)) def _draw_wrapped_text(self, x, y, width, text, font, font_size, color): wrapped = wrap_text(font, text, font_size, width) @@ -129,32 +87,3 @@ class FirehoseLayout(Widget): rl.draw_text_ex(font, line, rl.Vector2(x, y), font_size, 0, color) y += font_size * FONT_SCALE return round(y) - - def _get_status(self) -> tuple[str, rl.Color]: - network_type = ui_state.sm["deviceState"].networkType - network_metered = ui_state.sm["deviceState"].networkMetered - - if not network_metered and network_type != 0: # Not metered and connected - return tr("ACTIVE"), self.GREEN - else: - return tr("INACTIVE: connect to an unmetered network"), self.RED - - def _fetch_firehose_stats(self): - try: - dongle_id = self.params.get("DongleId") - if not dongle_id or dongle_id == UNREGISTERED_DONGLE_ID: - return - identity_token = get_token(dongle_id) - response = api_get(f"v1/devices/{dongle_id}/firehose_stats", access_token=identity_token) - if response.status_code == 200: - data = response.json() - self.segment_count = data.get("firehose", 0) - self.params.put(self.PARAM_KEY, data) - except Exception as e: - cloudlog.error(f"Failed to fetch firehose stats: {e}") - - def _update_loop(self): - while self.running: - if not ui_state.started: - self._fetch_firehose_stats() - time.sleep(self.UPDATE_INTERVAL) diff --git a/selfdrive/ui/mici/layouts/settings/firehose.py b/selfdrive/ui/mici/layouts/settings/firehose.py index 303d976b07..f2f8bcb1cb 100644 --- a/selfdrive/ui/mici/layouts/settings/firehose.py +++ b/selfdrive/ui/mici/layouts/settings/firehose.py @@ -12,8 +12,7 @@ from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE from openpilot.system.ui.lib.wrap_text import wrap_text from openpilot.system.ui.lib.scroll_panel2 import GuiScrollPanel2 from openpilot.system.ui.lib.multilang import tr, trn, tr_noop -from openpilot.system.ui.widgets import NavWidget - +from openpilot.system.ui.widgets import Widget, NavWidget TITLE = tr_noop("Firehose Mode") DESCRIPTION = tr_noop( @@ -34,9 +33,7 @@ FAQ_ITEMS = [ ] -class FirehoseLayoutMici(NavWidget): - BACK_TOUCH_AREA_PERCENTAGE = 0.1 - +class FirehoseLayoutBase(Widget): PARAM_KEY = "ApiCache_FirehoseStats" GREEN = rl.Color(46, 204, 113, 255) RED = rl.Color(231, 76, 60, 255) @@ -44,12 +41,10 @@ class FirehoseLayoutMici(NavWidget): LIGHT_GRAY = rl.Color(228, 228, 228, 255) UPDATE_INTERVAL = 30 # seconds - def __init__(self, back_callback): + def __init__(self): super().__init__() - self.set_back_callback(back_callback) - - self.params = Params() - self.segment_count = self._get_segment_count() + self._params = Params() + self._segment_count = self._get_segment_count() self._scroll_panel = GuiScrollPanel2(horizontal=False) self._content_height = 0 @@ -71,7 +66,7 @@ class FirehoseLayoutMici(NavWidget): self._scroll_panel.set_offset(0) def _get_segment_count(self) -> int: - stats = self.params.get(self.PARAM_KEY) + stats = self._params.get(self.PARAM_KEY) if not stats: return 0 try: @@ -111,9 +106,9 @@ class FirehoseLayoutMici(NavWidget): y += 20 # Contribution count (if available) - if self.segment_count > 0: + if self._segment_count > 0: contrib_text = trn("{} segment of your driving is in the training dataset so far.", - "{} segments of your driving is in the training dataset so far.", self.segment_count).format(self.segment_count) + "{} segments of your driving is in the training dataset so far.", self._segment_count).format(self._segment_count) y = self._draw_wrapped_text(x, y, w, contrib_text, gui_app.font(FontWeight.BOLD), 42, rl.WHITE) y += 20 @@ -165,9 +160,9 @@ class FirehoseLayoutMici(NavWidget): y += int(len(status_lines) * 48 * FONT_SCALE) + 20 # Contribution count - if self.segment_count > 0: + if self._segment_count > 0: contrib_text = trn("{} segment of your driving is in the training dataset so far.", - "{} segments of your driving is in the training dataset so far.", self.segment_count).format(self.segment_count) + "{} segments of your driving is in the training dataset so far.", self._segment_count).format(self._segment_count) contrib_lines = wrap_text(gui_app.font(FontWeight.BOLD), contrib_text, 42, w) y += int(len(contrib_lines) * 42 * FONT_SCALE) + 20 @@ -204,15 +199,15 @@ class FirehoseLayoutMici(NavWidget): def _fetch_firehose_stats(self): try: - dongle_id = self.params.get("DongleId") + dongle_id = self._params.get("DongleId") if not dongle_id or dongle_id == UNREGISTERED_DONGLE_ID: return identity_token = get_token(dongle_id) response = api_get(f"v1/devices/{dongle_id}/firehose_stats", access_token=identity_token) if response.status_code == 200: data = response.json() - self.segment_count = data.get("firehose", 0) - self.params.put(self.PARAM_KEY, data) + self._segment_count = data.get("firehose", 0) + self._params.put(self.PARAM_KEY, data) except Exception as e: cloudlog.error(f"Failed to fetch firehose stats: {e}") @@ -221,3 +216,11 @@ class FirehoseLayoutMici(NavWidget): if not ui_state.started: self._fetch_firehose_stats() time.sleep(self.UPDATE_INTERVAL) + + +class FirehoseLayout(FirehoseLayoutBase, NavWidget): + BACK_TOUCH_AREA_PERCENTAGE = 0.1 + + def __init__(self, back_callback): + super().__init__() + self.set_back_callback(back_callback) diff --git a/selfdrive/ui/mici/layouts/settings/settings.py b/selfdrive/ui/mici/layouts/settings/settings.py index 75238d581a..a452777748 100644 --- a/selfdrive/ui/mici/layouts/settings/settings.py +++ b/selfdrive/ui/mici/layouts/settings/settings.py @@ -10,7 +10,7 @@ from openpilot.selfdrive.ui.mici.layouts.settings.toggles import TogglesLayoutMi from openpilot.selfdrive.ui.mici.layouts.settings.network import NetworkLayoutMici from openpilot.selfdrive.ui.mici.layouts.settings.device import DeviceLayoutMici, PairBigButton from openpilot.selfdrive.ui.mici.layouts.settings.developer import DeveloperLayoutMici -from openpilot.selfdrive.ui.mici.layouts.settings.firehose import FirehoseLayoutMici +from openpilot.selfdrive.ui.mici.layouts.settings.firehose import FirehoseLayout from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.widgets import Widget, NavWidget @@ -67,7 +67,7 @@ class SettingsLayout(NavWidget): PanelType.NETWORK: PanelInfo("Network", NetworkLayoutMici(back_callback=lambda: self._set_current_panel(None))), PanelType.DEVICE: PanelInfo("Device", DeviceLayoutMici(back_callback=lambda: self._set_current_panel(None))), PanelType.DEVELOPER: PanelInfo("Developer", DeveloperLayoutMici(back_callback=lambda: self._set_current_panel(None))), - PanelType.FIREHOSE: PanelInfo("Firehose", FirehoseLayoutMici(back_callback=lambda: self._set_current_panel(None))), + PanelType.FIREHOSE: PanelInfo("Firehose", FirehoseLayout(back_callback=lambda: self._set_current_panel(None))), } self._font_medium = gui_app.font(FontWeight.MEDIUM) From 302e448b9378b980e64b49a8c16a89dd9d5fc843 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 27 Nov 2025 00:52:50 -0800 Subject: [PATCH 009/110] Revert "De-duplicate firehose layout (#36701)" This reverts commit dd51bf2021859c478aeb9f4bd167f8c2a47858fa. --- selfdrive/ui/layouts/settings/firehose.py | 87 +++++++++++++++++-- .../ui/mici/layouts/settings/firehose.py | 39 ++++----- .../ui/mici/layouts/settings/settings.py | 4 +- 3 files changed, 99 insertions(+), 31 deletions(-) diff --git a/selfdrive/ui/layouts/settings/firehose.py b/selfdrive/ui/layouts/settings/firehose.py index 9b9b51b18c..bbd4aef532 100644 --- a/selfdrive/ui/layouts/settings/firehose.py +++ b/selfdrive/ui/layouts/settings/firehose.py @@ -1,10 +1,19 @@ import pyray as rl +import time +import threading -from openpilot.system.ui.lib.application import gui_app, FontWeight +from openpilot.common.api import api_get +from openpilot.common.params import Params +from openpilot.common.swaglog import cloudlog +from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID +from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE from openpilot.system.ui.lib.multilang import tr, trn, tr_noop from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel -from openpilot.selfdrive.ui.mici.layouts.settings.firehose import FirehoseLayoutBase +from openpilot.system.ui.lib.wrap_text import wrap_text +from openpilot.system.ui.widgets import Widget +from openpilot.selfdrive.ui.lib.api_helpers import get_token TITLE = tr_noop("Firehose Mode") DESCRIPTION = tr_noop( @@ -23,17 +32,50 @@ INSTRUCTIONS = tr_noop( ) -class FirehoseLayout(FirehoseLayoutBase): +class FirehoseLayout(Widget): + PARAM_KEY = "ApiCache_FirehoseStats" + GREEN = rl.Color(46, 204, 113, 255) + RED = rl.Color(231, 76, 60, 255) + GRAY = rl.Color(68, 68, 68, 255) + LIGHT_GRAY = rl.Color(228, 228, 228, 255) + UPDATE_INTERVAL = 30 # seconds + def __init__(self): super().__init__() - self._scroll_panel = GuiScrollPanel() + self.params = Params() + self.segment_count = self._get_segment_count() + self.scroll_panel = GuiScrollPanel() + self._content_height = 0 + + self.running = True + self.update_thread = threading.Thread(target=self._update_loop, daemon=True) + self.update_thread.start() + self.last_update_time = 0 + + def show_event(self): + self.scroll_panel.set_offset(0) + + def _get_segment_count(self) -> int: + stats = self.params.get(self.PARAM_KEY) + if not stats: + return 0 + try: + return int(stats.get("firehose", 0)) + except Exception: + cloudlog.exception(f"Failed to decode firehose stats: {stats}") + return 0 + + def __del__(self): + self.running = False + if self.update_thread and self.update_thread.is_alive(): + self.update_thread.join(timeout=1.0) def _render(self, rect: rl.Rectangle): # Calculate content dimensions content_rect = rl.Rectangle(rect.x, rect.y, rect.width, self._content_height) # Handle scrolling and render with clipping - scroll_offset = self._scroll_panel.update(rect, content_rect) + scroll_offset = self.scroll_panel.update(rect, content_rect) rl.begin_scissor_mode(int(rect.x), int(rect.y), int(rect.width), int(rect.height)) self._content_height = self._render_content(rect, scroll_offset) rl.end_scissor_mode() @@ -65,9 +107,9 @@ class FirehoseLayout(FirehoseLayoutBase): y += 20 + 20 # Contribution count (if available) - if self._segment_count > 0: + if self.segment_count > 0: contrib_text = trn("{} segment of your driving is in the training dataset so far.", - "{} segments of your driving is in the training dataset so far.", self._segment_count).format(self._segment_count) + "{} segments of your driving is in the training dataset so far.", self.segment_count).format(self.segment_count) y = self._draw_wrapped_text(x, y, w, contrib_text, gui_app.font(FontWeight.BOLD), 52, rl.WHITE) y += 20 + 20 @@ -79,7 +121,7 @@ class FirehoseLayout(FirehoseLayoutBase): y = self._draw_wrapped_text(x, y, w, tr(INSTRUCTIONS), gui_app.font(FontWeight.NORMAL), 40, self.LIGHT_GRAY) # bottom margin + remove effect of scroll offset - return int(round(y - self._scroll_panel.offset + 40)) + return int(round(y - self.scroll_panel.offset + 40)) def _draw_wrapped_text(self, x, y, width, text, font, font_size, color): wrapped = wrap_text(font, text, font_size, width) @@ -87,3 +129,32 @@ class FirehoseLayout(FirehoseLayoutBase): rl.draw_text_ex(font, line, rl.Vector2(x, y), font_size, 0, color) y += font_size * FONT_SCALE return round(y) + + def _get_status(self) -> tuple[str, rl.Color]: + network_type = ui_state.sm["deviceState"].networkType + network_metered = ui_state.sm["deviceState"].networkMetered + + if not network_metered and network_type != 0: # Not metered and connected + return tr("ACTIVE"), self.GREEN + else: + return tr("INACTIVE: connect to an unmetered network"), self.RED + + def _fetch_firehose_stats(self): + try: + dongle_id = self.params.get("DongleId") + if not dongle_id or dongle_id == UNREGISTERED_DONGLE_ID: + return + identity_token = get_token(dongle_id) + response = api_get(f"v1/devices/{dongle_id}/firehose_stats", access_token=identity_token) + if response.status_code == 200: + data = response.json() + self.segment_count = data.get("firehose", 0) + self.params.put(self.PARAM_KEY, data) + except Exception as e: + cloudlog.error(f"Failed to fetch firehose stats: {e}") + + def _update_loop(self): + while self.running: + if not ui_state.started: + self._fetch_firehose_stats() + time.sleep(self.UPDATE_INTERVAL) diff --git a/selfdrive/ui/mici/layouts/settings/firehose.py b/selfdrive/ui/mici/layouts/settings/firehose.py index f2f8bcb1cb..303d976b07 100644 --- a/selfdrive/ui/mici/layouts/settings/firehose.py +++ b/selfdrive/ui/mici/layouts/settings/firehose.py @@ -12,7 +12,8 @@ from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE from openpilot.system.ui.lib.wrap_text import wrap_text from openpilot.system.ui.lib.scroll_panel2 import GuiScrollPanel2 from openpilot.system.ui.lib.multilang import tr, trn, tr_noop -from openpilot.system.ui.widgets import Widget, NavWidget +from openpilot.system.ui.widgets import NavWidget + TITLE = tr_noop("Firehose Mode") DESCRIPTION = tr_noop( @@ -33,7 +34,9 @@ FAQ_ITEMS = [ ] -class FirehoseLayoutBase(Widget): +class FirehoseLayoutMici(NavWidget): + BACK_TOUCH_AREA_PERCENTAGE = 0.1 + PARAM_KEY = "ApiCache_FirehoseStats" GREEN = rl.Color(46, 204, 113, 255) RED = rl.Color(231, 76, 60, 255) @@ -41,10 +44,12 @@ class FirehoseLayoutBase(Widget): LIGHT_GRAY = rl.Color(228, 228, 228, 255) UPDATE_INTERVAL = 30 # seconds - def __init__(self): + def __init__(self, back_callback): super().__init__() - self._params = Params() - self._segment_count = self._get_segment_count() + self.set_back_callback(back_callback) + + self.params = Params() + self.segment_count = self._get_segment_count() self._scroll_panel = GuiScrollPanel2(horizontal=False) self._content_height = 0 @@ -66,7 +71,7 @@ class FirehoseLayoutBase(Widget): self._scroll_panel.set_offset(0) def _get_segment_count(self) -> int: - stats = self._params.get(self.PARAM_KEY) + stats = self.params.get(self.PARAM_KEY) if not stats: return 0 try: @@ -106,9 +111,9 @@ class FirehoseLayoutBase(Widget): y += 20 # Contribution count (if available) - if self._segment_count > 0: + if self.segment_count > 0: contrib_text = trn("{} segment of your driving is in the training dataset so far.", - "{} segments of your driving is in the training dataset so far.", self._segment_count).format(self._segment_count) + "{} segments of your driving is in the training dataset so far.", self.segment_count).format(self.segment_count) y = self._draw_wrapped_text(x, y, w, contrib_text, gui_app.font(FontWeight.BOLD), 42, rl.WHITE) y += 20 @@ -160,9 +165,9 @@ class FirehoseLayoutBase(Widget): y += int(len(status_lines) * 48 * FONT_SCALE) + 20 # Contribution count - if self._segment_count > 0: + if self.segment_count > 0: contrib_text = trn("{} segment of your driving is in the training dataset so far.", - "{} segments of your driving is in the training dataset so far.", self._segment_count).format(self._segment_count) + "{} segments of your driving is in the training dataset so far.", self.segment_count).format(self.segment_count) contrib_lines = wrap_text(gui_app.font(FontWeight.BOLD), contrib_text, 42, w) y += int(len(contrib_lines) * 42 * FONT_SCALE) + 20 @@ -199,15 +204,15 @@ class FirehoseLayoutBase(Widget): def _fetch_firehose_stats(self): try: - dongle_id = self._params.get("DongleId") + dongle_id = self.params.get("DongleId") if not dongle_id or dongle_id == UNREGISTERED_DONGLE_ID: return identity_token = get_token(dongle_id) response = api_get(f"v1/devices/{dongle_id}/firehose_stats", access_token=identity_token) if response.status_code == 200: data = response.json() - self._segment_count = data.get("firehose", 0) - self._params.put(self.PARAM_KEY, data) + self.segment_count = data.get("firehose", 0) + self.params.put(self.PARAM_KEY, data) except Exception as e: cloudlog.error(f"Failed to fetch firehose stats: {e}") @@ -216,11 +221,3 @@ class FirehoseLayoutBase(Widget): if not ui_state.started: self._fetch_firehose_stats() time.sleep(self.UPDATE_INTERVAL) - - -class FirehoseLayout(FirehoseLayoutBase, NavWidget): - BACK_TOUCH_AREA_PERCENTAGE = 0.1 - - def __init__(self, back_callback): - super().__init__() - self.set_back_callback(back_callback) diff --git a/selfdrive/ui/mici/layouts/settings/settings.py b/selfdrive/ui/mici/layouts/settings/settings.py index a452777748..75238d581a 100644 --- a/selfdrive/ui/mici/layouts/settings/settings.py +++ b/selfdrive/ui/mici/layouts/settings/settings.py @@ -10,7 +10,7 @@ from openpilot.selfdrive.ui.mici.layouts.settings.toggles import TogglesLayoutMi from openpilot.selfdrive.ui.mici.layouts.settings.network import NetworkLayoutMici from openpilot.selfdrive.ui.mici.layouts.settings.device import DeviceLayoutMici, PairBigButton from openpilot.selfdrive.ui.mici.layouts.settings.developer import DeveloperLayoutMici -from openpilot.selfdrive.ui.mici.layouts.settings.firehose import FirehoseLayout +from openpilot.selfdrive.ui.mici.layouts.settings.firehose import FirehoseLayoutMici from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.widgets import Widget, NavWidget @@ -67,7 +67,7 @@ class SettingsLayout(NavWidget): PanelType.NETWORK: PanelInfo("Network", NetworkLayoutMici(back_callback=lambda: self._set_current_panel(None))), PanelType.DEVICE: PanelInfo("Device", DeviceLayoutMici(back_callback=lambda: self._set_current_panel(None))), PanelType.DEVELOPER: PanelInfo("Developer", DeveloperLayoutMici(back_callback=lambda: self._set_current_panel(None))), - PanelType.FIREHOSE: PanelInfo("Firehose", FirehoseLayout(back_callback=lambda: self._set_current_panel(None))), + PanelType.FIREHOSE: PanelInfo("Firehose", FirehoseLayoutMici(back_callback=lambda: self._set_current_panel(None))), } self._font_medium = gui_app.font(FontWeight.MEDIUM) From 50a797b0be47b5bb26540c4efb2957a72a45e58e Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 27 Nov 2025 01:00:03 -0800 Subject: [PATCH 010/110] De-duplicate firehose layout (#36703) * Reapply "De-duplicate firehose layout (#36701)" This reverts commit 302e448b9378b980e64b49a8c16a89dd9d5fc843. * fix * was here --- selfdrive/ui/layouts/settings/firehose.py | 84 ++----------------- .../ui/mici/layouts/settings/firehose.py | 39 +++++---- .../ui/mici/layouts/settings/settings.py | 4 +- 3 files changed, 30 insertions(+), 97 deletions(-) diff --git a/selfdrive/ui/layouts/settings/firehose.py b/selfdrive/ui/layouts/settings/firehose.py index bbd4aef532..ea83e962e6 100644 --- a/selfdrive/ui/layouts/settings/firehose.py +++ b/selfdrive/ui/layouts/settings/firehose.py @@ -1,19 +1,11 @@ import pyray as rl -import time -import threading -from openpilot.common.api import api_get -from openpilot.common.params import Params -from openpilot.common.swaglog import cloudlog -from openpilot.selfdrive.ui.ui_state import ui_state -from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE from openpilot.system.ui.lib.multilang import tr, trn, tr_noop from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel from openpilot.system.ui.lib.wrap_text import wrap_text -from openpilot.system.ui.widgets import Widget -from openpilot.selfdrive.ui.lib.api_helpers import get_token +from openpilot.selfdrive.ui.mici.layouts.settings.firehose import FirehoseLayoutBase TITLE = tr_noop("Firehose Mode") DESCRIPTION = tr_noop( @@ -32,50 +24,17 @@ INSTRUCTIONS = tr_noop( ) -class FirehoseLayout(Widget): - PARAM_KEY = "ApiCache_FirehoseStats" - GREEN = rl.Color(46, 204, 113, 255) - RED = rl.Color(231, 76, 60, 255) - GRAY = rl.Color(68, 68, 68, 255) - LIGHT_GRAY = rl.Color(228, 228, 228, 255) - UPDATE_INTERVAL = 30 # seconds - +class FirehoseLayout(FirehoseLayoutBase): def __init__(self): super().__init__() - self.params = Params() - self.segment_count = self._get_segment_count() - self.scroll_panel = GuiScrollPanel() - self._content_height = 0 - - self.running = True - self.update_thread = threading.Thread(target=self._update_loop, daemon=True) - self.update_thread.start() - self.last_update_time = 0 - - def show_event(self): - self.scroll_panel.set_offset(0) - - def _get_segment_count(self) -> int: - stats = self.params.get(self.PARAM_KEY) - if not stats: - return 0 - try: - return int(stats.get("firehose", 0)) - except Exception: - cloudlog.exception(f"Failed to decode firehose stats: {stats}") - return 0 - - def __del__(self): - self.running = False - if self.update_thread and self.update_thread.is_alive(): - self.update_thread.join(timeout=1.0) + self._scroll_panel = GuiScrollPanel() def _render(self, rect: rl.Rectangle): # Calculate content dimensions content_rect = rl.Rectangle(rect.x, rect.y, rect.width, self._content_height) # Handle scrolling and render with clipping - scroll_offset = self.scroll_panel.update(rect, content_rect) + scroll_offset = self._scroll_panel.update(rect, content_rect) rl.begin_scissor_mode(int(rect.x), int(rect.y), int(rect.width), int(rect.height)) self._content_height = self._render_content(rect, scroll_offset) rl.end_scissor_mode() @@ -107,9 +66,9 @@ class FirehoseLayout(Widget): y += 20 + 20 # Contribution count (if available) - if self.segment_count > 0: + if self._segment_count > 0: contrib_text = trn("{} segment of your driving is in the training dataset so far.", - "{} segments of your driving is in the training dataset so far.", self.segment_count).format(self.segment_count) + "{} segments of your driving is in the training dataset so far.", self._segment_count).format(self._segment_count) y = self._draw_wrapped_text(x, y, w, contrib_text, gui_app.font(FontWeight.BOLD), 52, rl.WHITE) y += 20 + 20 @@ -121,7 +80,7 @@ class FirehoseLayout(Widget): y = self._draw_wrapped_text(x, y, w, tr(INSTRUCTIONS), gui_app.font(FontWeight.NORMAL), 40, self.LIGHT_GRAY) # bottom margin + remove effect of scroll offset - return int(round(y - self.scroll_panel.offset + 40)) + return int(round(y - self._scroll_panel.offset + 40)) def _draw_wrapped_text(self, x, y, width, text, font, font_size, color): wrapped = wrap_text(font, text, font_size, width) @@ -129,32 +88,3 @@ class FirehoseLayout(Widget): rl.draw_text_ex(font, line, rl.Vector2(x, y), font_size, 0, color) y += font_size * FONT_SCALE return round(y) - - def _get_status(self) -> tuple[str, rl.Color]: - network_type = ui_state.sm["deviceState"].networkType - network_metered = ui_state.sm["deviceState"].networkMetered - - if not network_metered and network_type != 0: # Not metered and connected - return tr("ACTIVE"), self.GREEN - else: - return tr("INACTIVE: connect to an unmetered network"), self.RED - - def _fetch_firehose_stats(self): - try: - dongle_id = self.params.get("DongleId") - if not dongle_id or dongle_id == UNREGISTERED_DONGLE_ID: - return - identity_token = get_token(dongle_id) - response = api_get(f"v1/devices/{dongle_id}/firehose_stats", access_token=identity_token) - if response.status_code == 200: - data = response.json() - self.segment_count = data.get("firehose", 0) - self.params.put(self.PARAM_KEY, data) - except Exception as e: - cloudlog.error(f"Failed to fetch firehose stats: {e}") - - def _update_loop(self): - while self.running: - if not ui_state.started: - self._fetch_firehose_stats() - time.sleep(self.UPDATE_INTERVAL) diff --git a/selfdrive/ui/mici/layouts/settings/firehose.py b/selfdrive/ui/mici/layouts/settings/firehose.py index 303d976b07..f2f8bcb1cb 100644 --- a/selfdrive/ui/mici/layouts/settings/firehose.py +++ b/selfdrive/ui/mici/layouts/settings/firehose.py @@ -12,8 +12,7 @@ from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE from openpilot.system.ui.lib.wrap_text import wrap_text from openpilot.system.ui.lib.scroll_panel2 import GuiScrollPanel2 from openpilot.system.ui.lib.multilang import tr, trn, tr_noop -from openpilot.system.ui.widgets import NavWidget - +from openpilot.system.ui.widgets import Widget, NavWidget TITLE = tr_noop("Firehose Mode") DESCRIPTION = tr_noop( @@ -34,9 +33,7 @@ FAQ_ITEMS = [ ] -class FirehoseLayoutMici(NavWidget): - BACK_TOUCH_AREA_PERCENTAGE = 0.1 - +class FirehoseLayoutBase(Widget): PARAM_KEY = "ApiCache_FirehoseStats" GREEN = rl.Color(46, 204, 113, 255) RED = rl.Color(231, 76, 60, 255) @@ -44,12 +41,10 @@ class FirehoseLayoutMici(NavWidget): LIGHT_GRAY = rl.Color(228, 228, 228, 255) UPDATE_INTERVAL = 30 # seconds - def __init__(self, back_callback): + def __init__(self): super().__init__() - self.set_back_callback(back_callback) - - self.params = Params() - self.segment_count = self._get_segment_count() + self._params = Params() + self._segment_count = self._get_segment_count() self._scroll_panel = GuiScrollPanel2(horizontal=False) self._content_height = 0 @@ -71,7 +66,7 @@ class FirehoseLayoutMici(NavWidget): self._scroll_panel.set_offset(0) def _get_segment_count(self) -> int: - stats = self.params.get(self.PARAM_KEY) + stats = self._params.get(self.PARAM_KEY) if not stats: return 0 try: @@ -111,9 +106,9 @@ class FirehoseLayoutMici(NavWidget): y += 20 # Contribution count (if available) - if self.segment_count > 0: + if self._segment_count > 0: contrib_text = trn("{} segment of your driving is in the training dataset so far.", - "{} segments of your driving is in the training dataset so far.", self.segment_count).format(self.segment_count) + "{} segments of your driving is in the training dataset so far.", self._segment_count).format(self._segment_count) y = self._draw_wrapped_text(x, y, w, contrib_text, gui_app.font(FontWeight.BOLD), 42, rl.WHITE) y += 20 @@ -165,9 +160,9 @@ class FirehoseLayoutMici(NavWidget): y += int(len(status_lines) * 48 * FONT_SCALE) + 20 # Contribution count - if self.segment_count > 0: + if self._segment_count > 0: contrib_text = trn("{} segment of your driving is in the training dataset so far.", - "{} segments of your driving is in the training dataset so far.", self.segment_count).format(self.segment_count) + "{} segments of your driving is in the training dataset so far.", self._segment_count).format(self._segment_count) contrib_lines = wrap_text(gui_app.font(FontWeight.BOLD), contrib_text, 42, w) y += int(len(contrib_lines) * 42 * FONT_SCALE) + 20 @@ -204,15 +199,15 @@ class FirehoseLayoutMici(NavWidget): def _fetch_firehose_stats(self): try: - dongle_id = self.params.get("DongleId") + dongle_id = self._params.get("DongleId") if not dongle_id or dongle_id == UNREGISTERED_DONGLE_ID: return identity_token = get_token(dongle_id) response = api_get(f"v1/devices/{dongle_id}/firehose_stats", access_token=identity_token) if response.status_code == 200: data = response.json() - self.segment_count = data.get("firehose", 0) - self.params.put(self.PARAM_KEY, data) + self._segment_count = data.get("firehose", 0) + self._params.put(self.PARAM_KEY, data) except Exception as e: cloudlog.error(f"Failed to fetch firehose stats: {e}") @@ -221,3 +216,11 @@ class FirehoseLayoutMici(NavWidget): if not ui_state.started: self._fetch_firehose_stats() time.sleep(self.UPDATE_INTERVAL) + + +class FirehoseLayout(FirehoseLayoutBase, NavWidget): + BACK_TOUCH_AREA_PERCENTAGE = 0.1 + + def __init__(self, back_callback): + super().__init__() + self.set_back_callback(back_callback) diff --git a/selfdrive/ui/mici/layouts/settings/settings.py b/selfdrive/ui/mici/layouts/settings/settings.py index 75238d581a..a452777748 100644 --- a/selfdrive/ui/mici/layouts/settings/settings.py +++ b/selfdrive/ui/mici/layouts/settings/settings.py @@ -10,7 +10,7 @@ from openpilot.selfdrive.ui.mici.layouts.settings.toggles import TogglesLayoutMi from openpilot.selfdrive.ui.mici.layouts.settings.network import NetworkLayoutMici from openpilot.selfdrive.ui.mici.layouts.settings.device import DeviceLayoutMici, PairBigButton from openpilot.selfdrive.ui.mici.layouts.settings.developer import DeveloperLayoutMici -from openpilot.selfdrive.ui.mici.layouts.settings.firehose import FirehoseLayoutMici +from openpilot.selfdrive.ui.mici.layouts.settings.firehose import FirehoseLayout from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.widgets import Widget, NavWidget @@ -67,7 +67,7 @@ class SettingsLayout(NavWidget): PanelType.NETWORK: PanelInfo("Network", NetworkLayoutMici(back_callback=lambda: self._set_current_panel(None))), PanelType.DEVICE: PanelInfo("Device", DeviceLayoutMici(back_callback=lambda: self._set_current_panel(None))), PanelType.DEVELOPER: PanelInfo("Developer", DeveloperLayoutMici(back_callback=lambda: self._set_current_panel(None))), - PanelType.FIREHOSE: PanelInfo("Firehose", FirehoseLayoutMici(back_callback=lambda: self._set_current_panel(None))), + PanelType.FIREHOSE: PanelInfo("Firehose", FirehoseLayout(back_callback=lambda: self._set_current_panel(None))), } self._font_medium = gui_app.font(FontWeight.MEDIUM) From 946fd3f3879fd7b15a6a296d9303971bff8a222b Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 27 Nov 2025 01:00:33 -0800 Subject: [PATCH 011/110] NavWidget: draw black above top of widget when dismissing (#36702) draw rec --- system/ui/widgets/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/system/ui/widgets/__init__.py b/system/ui/widgets/__init__.py index 95858ec1b3..18a63a736d 100644 --- a/system/ui/widgets/__init__.py +++ b/system/ui/widgets/__init__.py @@ -359,6 +359,10 @@ class NavWidget(Widget, abc.ABC): self._nav_bar.set_position(bar_x, round(self._nav_bar_y_filter.x)) self._nav_bar.render() + # draw black above widget when dismissing + if self._rect.y > 0: + rl.draw_rectangle(int(self._rect.x), 0, int(self._rect.width), int(self._rect.y), rl.BLACK) + return ret def show_event(self): From 26261387f8b4188951d107567a067d22cf87e5e3 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 27 Nov 2025 01:37:16 -0800 Subject: [PATCH 012/110] Fix raylib ui spamming API calls (#36700) * intern * start * move * common caching * use constant for slep * works * add gating back * clean up * more * match cache logic * hate this circular * not needed since sync * no need for lock? * even qt had something like _load_initial_state for tests, keep * clean up * clean up * clean up * loading json as string works, else it will fail to parse json, catch that and log, and next api call will overwrite * move over firehose * clean up * fix test * no * flip * more * match qt * consistent * clean up * cmt * fix test! --- common/params_keys.h | 2 +- common/tests/test_params.py | 4 +- selfdrive/ui/lib/api_helpers.py | 93 ++++++++++++++++++- selfdrive/ui/lib/prime_state.py | 75 ++++----------- .../ui/mici/layouts/settings/firehose.py | 65 ++++--------- 5 files changed, 131 insertions(+), 108 deletions(-) diff --git a/common/params_keys.h b/common/params_keys.h index bf410825f1..fc7f410e40 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -10,7 +10,7 @@ inline static std::unordered_map keys = { {"AdbEnabled", {PERSISTENT, BOOL}}, {"AlwaysOnDM", {PERSISTENT, BOOL}}, {"ApiCache_Device", {PERSISTENT, STRING}}, - {"ApiCache_FirehoseStats", {PERSISTENT, JSON}}, + {"ApiCache_FirehoseStats", {PERSISTENT, STRING}}, {"AssistNowToken", {PERSISTENT, STRING}}, {"AthenadPid", {PERSISTENT, INT}}, {"AthenadUploadQueue", {PERSISTENT, JSON}}, diff --git a/common/tests/test_params.py b/common/tests/test_params.py index 592bf2c4b2..e0f213e02c 100644 --- a/common/tests/test_params.py +++ b/common/tests/test_params.py @@ -123,8 +123,8 @@ class TestParams: def test_params_get_type(self): # json - self.params.put("ApiCache_FirehoseStats", {"a": 0}) - assert self.params.get("ApiCache_FirehoseStats") == {"a": 0} + self.params.put("LiveParameters", {"a": 0}) + assert self.params.get("LiveParameters") == {"a": 0} # int self.params.put("BootCount", 1441) diff --git a/selfdrive/ui/lib/api_helpers.py b/selfdrive/ui/lib/api_helpers.py index 8ed1c22a63..31e844dd0a 100644 --- a/selfdrive/ui/lib/api_helpers.py +++ b/selfdrive/ui/lib/api_helpers.py @@ -1,7 +1,13 @@ import time +import threading +from collections.abc import Callable from functools import lru_cache -from openpilot.common.api import Api + +from openpilot.common.api import Api, api_get +from openpilot.common.params import Params +from openpilot.common.swaglog import cloudlog from openpilot.common.time_helpers import system_time_valid +from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID TOKEN_EXPIRY_HOURS = 2 @@ -16,3 +22,88 @@ def _get_token(dongle_id: str, t: int): def get_token(dongle_id: str): return _get_token(dongle_id, int(time.monotonic() / (TOKEN_EXPIRY_HOURS / 2 * 60 * 60))) + + +class RequestRepeater: + API_TIMEOUT = 10.0 # seconds for API requests + SLEEP_INTERVAL = 0.5 # seconds to sleep between checks in the worker thread + + def __init__(self, dongle_id: str, request_route: str, period: int, cache_key: str | None = None): + self._dongle_id = dongle_id + self._request_route = request_route + self._period = period # seconds + self._cache_key = cache_key + + self._request_done_callbacks: list[Callable[[str, bool], None]] = [] + self._prev_response_text = None + self._running = False + self._thread = None + self._params = Params() + + if self._cache_key is not None: + # Cache successful responses to params + def cache_response(response: str, success: bool): + if success and response != self._prev_response_text: + self._params.put(self._cache_key, response) + self._prev_response_text = response + + self.add_request_done_callback(cache_response) + + def add_request_done_callback(self, callback: Callable[[str, bool], None]): + self._request_done_callbacks.append(callback) + + def _do_callbacks(self, response_text: str, success: bool): + for callback in self._request_done_callbacks: + try: + callback(response_text, success) + except Exception as e: + cloudlog.error(f"RequestRepeater callback error: {e}") + + def load_cache(self): + # call callbacks with cached response + if self._cache_key is not None: + self._prev_response_text = self._params.get(self._cache_key) + if self._prev_response_text: + self._do_callbacks(self._prev_response_text, True) + + def start(self): + if self._thread and self._thread.is_alive(): + return + self._running = True + self._thread = threading.Thread(target=self._worker_thread, daemon=True) + self._thread.start() + + def stop(self): + self._running = False + if self._thread and self._thread.is_alive(): + self._thread.join(timeout=1.0) + + def _worker_thread(self): + # Avoid circular imports + from openpilot.selfdrive.ui.ui_state import ui_state, device + + while self._running: + # Don't run when device is asleep or onroad + if not ui_state.started and device.awake: + self._send_request() + + for _ in range(int(self._period / self.SLEEP_INTERVAL)): + if not self._running: + break + time.sleep(self.SLEEP_INTERVAL) + + def _send_request(self): + if not self._dongle_id or self._dongle_id == UNREGISTERED_DONGLE_ID: + return + + try: + identity_token = get_token(self._dongle_id) + response = api_get(self._request_route, timeout=self.API_TIMEOUT, access_token=identity_token) + self._do_callbacks(response.text, 200 <= response.status_code < 300) + + except Exception as e: + cloudlog.error(f"Failed to send request to {self._request_route}: {e}") + self._do_callbacks("", False) + + def __del__(self): + self.stop() diff --git a/selfdrive/ui/lib/prime_state.py b/selfdrive/ui/lib/prime_state.py index fc72b4f9c6..f3adebf4b6 100644 --- a/selfdrive/ui/lib/prime_state.py +++ b/selfdrive/ui/lib/prime_state.py @@ -1,13 +1,10 @@ from enum import IntEnum import os -import threading -import time +import json -from openpilot.common.api import api_get from openpilot.common.params import Params 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.ui.lib.api_helpers import RequestRepeater class PrimeType(IntEnum): @@ -22,17 +19,14 @@ class PrimeType(IntEnum): class PrimeState: - FETCH_INTERVAL = 5.0 # seconds between API calls - API_TIMEOUT = 10.0 # seconds for API requests - SLEEP_INTERVAL = 0.5 # seconds to sleep between checks in the worker thread - def __init__(self): self._params = Params() - self._lock = threading.Lock() self.prime_type: PrimeType = self._load_initial_state() - self._running = False - self._thread = None + dongle_id = self._params.get("DongleId") + self._request_repeater = RequestRepeater(dongle_id, f"v1.1/devices/{dongle_id}", 5, "ApiCache_Device") + self._request_repeater.add_request_done_callback(self._handle_reply) + self._request_repeater.load_cache() # sets prime_type from API response cache def _load_initial_state(self) -> PrimeType: prime_type_str = os.getenv("PRIME_TYPE") or self._params.get("PrimeType") @@ -43,61 +37,32 @@ class PrimeState: pass return PrimeType.UNKNOWN - def _fetch_prime_status(self) -> None: - dongle_id = self._params.get("DongleId") - if not dongle_id or dongle_id == UNREGISTERED_DONGLE_ID: + def _handle_reply(self, response: str, success: bool): + if not success: return try: - identity_token = get_token(dongle_id) - response = api_get(f"v1.1/devices/{dongle_id}", timeout=self.API_TIMEOUT, access_token=identity_token) - 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) + data = json.loads(response) + 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) except Exception as e: cloudlog.error(f"Failed to fetch prime status: {e}") def set_type(self, prime_type: PrimeType) -> None: - with self._lock: - if prime_type != self.prime_type: - self.prime_type = prime_type - self._params.put("PrimeType", int(prime_type)) - cloudlog.info(f"Prime type updated to {prime_type}") - - def _worker_thread(self) -> None: - while self._running: - self._fetch_prime_status() - - for _ in range(int(self.FETCH_INTERVAL / self.SLEEP_INTERVAL)): - if not self._running: - break - time.sleep(self.SLEEP_INTERVAL) + if prime_type != self.prime_type: + self.prime_type = prime_type + self._params.put("PrimeType", int(prime_type)) + cloudlog.info(f"Prime type updated to {prime_type}") def start(self) -> None: - if self._thread and self._thread.is_alive(): - return - self._running = True - self._thread = threading.Thread(target=self._worker_thread, daemon=True) - self._thread.start() - - def stop(self) -> None: - self._running = False - if self._thread and self._thread.is_alive(): - self._thread.join(timeout=1.0) + self._request_repeater.start() def get_type(self) -> PrimeType: - with self._lock: - return self.prime_type + return self.prime_type def is_prime(self) -> bool: - with self._lock: - return bool(self.prime_type > PrimeType.NONE) + return bool(self.prime_type > PrimeType.NONE) def is_paired(self) -> bool: - with self._lock: - return self.prime_type > PrimeType.UNPAIRED - - def __del__(self): - self.stop() + return self.prime_type > PrimeType.UNPAIRED diff --git a/selfdrive/ui/mici/layouts/settings/firehose.py b/selfdrive/ui/mici/layouts/settings/firehose.py index f2f8bcb1cb..5cc43322d0 100644 --- a/selfdrive/ui/mici/layouts/settings/firehose.py +++ b/selfdrive/ui/mici/layouts/settings/firehose.py @@ -1,18 +1,15 @@ -import threading -import time import pyray as rl +import json -from openpilot.common.api import api_get from openpilot.common.params import Params from openpilot.common.swaglog import cloudlog -from openpilot.selfdrive.ui.lib.api_helpers import get_token from openpilot.selfdrive.ui.ui_state import ui_state -from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE from openpilot.system.ui.lib.wrap_text import wrap_text from openpilot.system.ui.lib.scroll_panel2 import GuiScrollPanel2 from openpilot.system.ui.lib.multilang import tr, trn, tr_noop from openpilot.system.ui.widgets import Widget, NavWidget +from openpilot.selfdrive.ui.lib.api_helpers import RequestRepeater TITLE = tr_noop("Firehose Mode") DESCRIPTION = tr_noop( @@ -34,47 +31,37 @@ FAQ_ITEMS = [ class FirehoseLayoutBase(Widget): - PARAM_KEY = "ApiCache_FirehoseStats" GREEN = rl.Color(46, 204, 113, 255) RED = rl.Color(231, 76, 60, 255) GRAY = rl.Color(68, 68, 68, 255) LIGHT_GRAY = rl.Color(228, 228, 228, 255) - UPDATE_INTERVAL = 30 # seconds def __init__(self): super().__init__() - self._params = Params() - self._segment_count = self._get_segment_count() - + self._segment_count = 0 self._scroll_panel = GuiScrollPanel2(horizontal=False) self._content_height = 0 - self._running = True - self._update_thread = threading.Thread(target=self._update_loop, daemon=True) - self._update_thread.start() + dongle_id = Params().get("DongleId") + self._request_repeater = RequestRepeater(dongle_id, f"v1/devices/{dongle_id}/firehose_stats", 30, "ApiCache_FirehoseStats") + self._request_repeater.add_request_done_callback(self._handle_reply) + self._request_repeater.load_cache() + self._request_repeater.start() + + def _handle_reply(self, response: str, success: bool): + if not success: + return - def __del__(self): - self._running = False try: - if self._update_thread and self._update_thread.is_alive(): - self._update_thread.join(timeout=1.0) - except Exception: - pass + data = json.loads(response) + self._segment_count = data.get("firehose", 0) + except Exception as e: + cloudlog.error(f"Failed to fetch firehose stats: {e}") def show_event(self): super().show_event() self._scroll_panel.set_offset(0) - def _get_segment_count(self) -> int: - stats = self._params.get(self.PARAM_KEY) - if not stats: - return 0 - try: - return int(stats.get("firehose", 0)) - except Exception: - cloudlog.exception(f"Failed to decode firehose stats: {stats}") - return 0 - def _render(self, rect: rl.Rectangle): # compute total content height for scrolling content_height = self._measure_content_height(rect) @@ -197,26 +184,6 @@ class FirehoseLayoutBase(Widget): else: return tr("INACTIVE: connect to an unmetered network"), self.RED - def _fetch_firehose_stats(self): - try: - dongle_id = self._params.get("DongleId") - if not dongle_id or dongle_id == UNREGISTERED_DONGLE_ID: - return - identity_token = get_token(dongle_id) - response = api_get(f"v1/devices/{dongle_id}/firehose_stats", access_token=identity_token) - if response.status_code == 200: - data = response.json() - self._segment_count = data.get("firehose", 0) - self._params.put(self.PARAM_KEY, data) - except Exception as e: - cloudlog.error(f"Failed to fetch firehose stats: {e}") - - def _update_loop(self): - while self._running: - if not ui_state.started: - self._fetch_firehose_stats() - time.sleep(self.UPDATE_INTERVAL) - class FirehoseLayout(FirehoseLayoutBase, NavWidget): BACK_TOUCH_AREA_PERCENTAGE = 0.1 From b8d55987c25598e871e771c4cf100032e55b5145 Mon Sep 17 00:00:00 2001 From: Dean Lee Date: Thu, 27 Nov 2025 17:50:36 +0800 Subject: [PATCH 013/110] ui: extract and optimize mouse event processing (#36564) * extract and optimize mouse event processing * rm slot * merge mici mastere * add mouse --------- Co-authored-by: Shane Smiskol --- system/ui/widgets/__init__.py | 77 +++++++++++++++++++---------------- 1 file changed, 42 insertions(+), 35 deletions(-) diff --git a/system/ui/widgets/__init__.py b/system/ui/widgets/__init__.py index 18a63a736d..546c682f33 100644 --- a/system/ui/widgets/__init__.py +++ b/system/ui/widgets/__init__.py @@ -104,46 +104,53 @@ class Widget(abc.ABC): # Keep track of whether mouse down started within the widget's rectangle if self.enabled and self.__was_awake: - for mouse_event in gui_app.mouse_events: - if not self._multi_touch and mouse_event.slot != 0: - continue - - # Ignores touches/presses that start outside our rect - # Allows touch to leave the rect and come back in focus if mouse did not release - if mouse_event.left_pressed and self._touch_valid(): - if rl.check_collision_point_rec(mouse_event.pos, self._hit_rect): - self._handle_mouse_press(mouse_event.pos) - self.__is_pressed[mouse_event.slot] = True - self.__tracking_is_pressed[mouse_event.slot] = True - self._handle_mouse_event(mouse_event) - - # Callback such as scroll panel signifies user is scrolling - elif not self._touch_valid(): - self.__is_pressed[mouse_event.slot] = False - self.__tracking_is_pressed[mouse_event.slot] = False - - elif mouse_event.left_released: - self._handle_mouse_event(mouse_event) - if self.__is_pressed[mouse_event.slot] and rl.check_collision_point_rec(mouse_event.pos, self._hit_rect): - self._handle_mouse_release(mouse_event.pos) - self.__is_pressed[mouse_event.slot] = False - self.__tracking_is_pressed[mouse_event.slot] = False - - # Mouse/touch is still within our rect - elif rl.check_collision_point_rec(mouse_event.pos, self._hit_rect): - if self.__tracking_is_pressed[mouse_event.slot]: - self.__is_pressed[mouse_event.slot] = True - self._handle_mouse_event(mouse_event) - - # Mouse/touch left our rect but may come back into focus later - elif not rl.check_collision_point_rec(mouse_event.pos, self._hit_rect): - self.__is_pressed[mouse_event.slot] = False - self._handle_mouse_event(mouse_event) + self._process_mouse_events() self.__was_awake = device.awake return ret + def _process_mouse_events(self) -> None: + hit_rect = self._hit_rect + touch_valid = self._touch_valid() + + for mouse_event in gui_app.mouse_events: + if not self._multi_touch and mouse_event.slot != 0: + continue + + mouse_in_rect = rl.check_collision_point_rec(mouse_event.pos, hit_rect) + # Ignores touches/presses that start outside our rect + # Allows touch to leave the rect and come back in focus if mouse did not release + if mouse_event.left_pressed and touch_valid: + if mouse_in_rect: + self._handle_mouse_press(mouse_event.pos) + self.__is_pressed[mouse_event.slot] = True + self.__tracking_is_pressed[mouse_event.slot] = True + self._handle_mouse_event(mouse_event) + + # Callback such as scroll panel signifies user is scrolling + elif not touch_valid: + self.__is_pressed[mouse_event.slot] = False + self.__tracking_is_pressed[mouse_event.slot] = False + + elif mouse_event.left_released: + self._handle_mouse_event(mouse_event) + if self.__is_pressed[mouse_event.slot] and mouse_in_rect: + self._handle_mouse_release(mouse_event.pos) + self.__is_pressed[mouse_event.slot] = False + self.__tracking_is_pressed[mouse_event.slot] = False + + # Mouse/touch is still within our rect + elif mouse_in_rect: + if self.__tracking_is_pressed[mouse_event.slot]: + self.__is_pressed[mouse_event.slot] = True + self._handle_mouse_event(mouse_event) + + # Mouse/touch left our rect but may come back into focus later + elif not mouse_in_rect: + self.__is_pressed[mouse_event.slot] = False + self._handle_mouse_event(mouse_event) + @abc.abstractmethod def _render(self, rect: rl.Rectangle) -> bool | int | None: """Render the widget within the given rectangle.""" From ae6ada41621566424305bb4e71537eb1d8496e8b Mon Sep 17 00:00:00 2001 From: David <49467229+TheSecurityDev@users.noreply.github.com> Date: Thu, 27 Nov 2025 03:52:25 -0600 Subject: [PATCH 014/110] lint: Add PLE rule to ruff (#36595) * update linting rules to include new PLE (pylint error) rule * fix lint error --- pyproject.toml | 2 +- system/updated/casync/casync.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3f3804ba6a..3f3c8a72bb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -225,7 +225,7 @@ lint.select = [ "TRY203", "TRY400", "TRY401", # try/excepts "RUF008", "RUF100", "TID251", - "PLR1704", + "PLE", "PLR1704", ] lint.ignore = [ "E741", diff --git a/system/updated/casync/casync.py b/system/updated/casync/casync.py index 7a3303a9e9..79ac26f1c6 100755 --- a/system/updated/casync/casync.py +++ b/system/updated/casync/casync.py @@ -99,7 +99,7 @@ class DirectoryTarChunkReader(BinaryChunkReader): create_casync_tar_package(pathlib.Path(path), pathlib.Path(cache_file)) self.f = open(cache_file, "rb") - return super().__init__(self.f) + super().__init__(self.f) def __del__(self): self.f.close() From ae534ddeab171f89a4a18c6998bb5b7666b495e1 Mon Sep 17 00:00:00 2001 From: Logesh R Date: Thu, 27 Nov 2025 02:07:56 -0800 Subject: [PATCH 015/110] docs: Fix "Turn the speed blue" tutorial for Raylib UI (#36591) * docs: Fix "Turn the speed blue" tutorial for Raylib UI * just * obv * not replay --------- Co-authored-by: Shane Smiskol --- docs/how-to/turn-the-speed-blue.md | 37 ++++++++++++++---------------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/docs/how-to/turn-the-speed-blue.md b/docs/how-to/turn-the-speed-blue.md index eb6e75afa2..644c35e0ab 100644 --- a/docs/how-to/turn-the-speed-blue.md +++ b/docs/how-to/turn-the-speed-blue.md @@ -31,7 +31,7 @@ We'll run the `replay` tool with the demo route to get data streaming for testin tools/replay/replay --demo # in terminal 2 -selfdrive/ui/ui +./selfdrive/ui/ui.py ``` The openpilot UI should launch and show a replay of the demo route. @@ -43,39 +43,36 @@ If you have your own comma device, you can replace `--demo` with one of your own Now let’s update the speed display color in the UI. -Search for the function responsible for rendering UI text: +Search for the function responsible for rendering the current speed: ```bash -git grep "drawText" selfdrive/ui/qt/onroad/hud.cc +git grep "_draw_current_speed" selfdrive/ui/onroad/hud_renderer.py ``` -You’ll find the relevant code inside `selfdrive/ui/qt/onroad/hud.cc`, in this function: +You'll find the relevant code inside `selfdrive/ui/onroad/hud_renderer.py`, in this function: -```cpp -void HudRenderer::drawText(QPainter &p, int x, int y, const QString &text, int alpha) { - QRect real_rect = p.fontMetrics().boundingRect(text); - real_rect.moveCenter({x, y - real_rect.height() / 2}); - - p.setPen(QColor(0xff, 0xff, 0xff, alpha)); // <- this sets the speed text color - p.drawText(real_rect.x(), real_rect.bottom(), text); -} +```python +def _draw_current_speed(self, rect: rl.Rectangle) -> None: + """Draw the current vehicle speed and unit.""" + speed_text = str(round(self.speed)) + speed_text_size = measure_text_cached(self._font_bold, speed_text, FONT_SIZES.current_speed) + speed_pos = rl.Vector2(rect.x + rect.width / 2 - speed_text_size.x / 2, 180 - speed_text_size.y / 2) + rl.draw_text_ex(self._font_bold, speed_text, speed_pos, FONT_SIZES.current_speed, 0, COLORS.white) # <- this sets the speed text color ``` -Change the `QColor(...)` line to make it **blue** instead of white. A nice soft blue is `#8080FF`, which translates to: +Change `COLORS.white` to make it **blue** instead of white. A nice soft blue is `#8080FF`, which you can change inline: ```diff -- p.setPen(QColor(0xff, 0xff, 0xff, alpha)); -+ p.setPen(QColor(0x80, 0x80, 0xFF, alpha)); +- rl.draw_text_ex(self._font_bold, speed_text, speed_pos, FONT_SIZES.current_speed, 0, COLORS.white) ++ rl.draw_text_ex(self._font_bold, speed_text, speed_pos, FONT_SIZES.current_speed, 0, rl.Color(0x80, 0x80, 0xFF, 255)) ``` -This change will tint all speed-related UI text to blue with the same transparency (`alpha`). - --- -## 4. Rebuild the UI +## 4. Re-run the UI -After making changes, rebuild Openpilot so your new UI is compiled: +After making changes, re-run the UI to see your new UI: ```bash -scons -j$(nproc) && selfdrive/ui/ui +./selfdrive/ui/ui.py ``` ![](https://blog.comma.ai/img/blue_speed_ui.png) From 4bd6fb09954e4df5472131f19f695bb32415728a Mon Sep 17 00:00:00 2001 From: Dean Lee Date: Thu, 27 Nov 2025 18:17:16 +0800 Subject: [PATCH 016/110] ui: fix unconditional rl.end_scissor_mode() call in MiciLabel (#36660) fix incorrect end_scissor_mode usage --- system/ui/widgets/label.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/ui/widgets/label.py b/system/ui/widgets/label.py index 35e2708e62..b6e67d03a0 100644 --- a/system/ui/widgets/label.py +++ b/system/ui/widgets/label.py @@ -183,7 +183,7 @@ class MiciLabel(Widget): if self._scroll_state != ScrollState.STARTING: rl.draw_rectangle_gradient_h(int(rect.x), int(rect.y), fade_width, int(rect.height), rl.BLACK, rl.Color(0, 0, 0, 0)) - rl.end_scissor_mode() + rl.end_scissor_mode() # TODO: This should be a Widget class From 4ef82c4119ce8e8b7f360d2e21434ac441b25009 Mon Sep 17 00:00:00 2001 From: Dean Lee Date: Thu, 27 Nov 2025 18:20:49 +0800 Subject: [PATCH 017/110] ui: optimize matrix operations in scroller rendering (#36668) optimize matrix operations --- system/ui/widgets/scroller.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/system/ui/widgets/scroller.py b/system/ui/widgets/scroller.py index 9a04e84257..3cb6a1e188 100644 --- a/system/ui/widgets/scroller.py +++ b/system/ui/widgets/scroller.py @@ -224,12 +224,15 @@ class Scroller(Widget): # Scale each element around its own origin when scrolling scale = self._zoom_filter.x - rl.rl_push_matrix() - rl.rl_scalef(scale, scale, 1.0) - rl.rl_translatef((1 - scale) * (x + item.rect.width / 2) / scale, - (1 - scale) * (y + item.rect.height / 2) / scale, 0) - item.render() - rl.rl_pop_matrix() + if scale != 1.0: + rl.rl_push_matrix() + rl.rl_scalef(scale, scale, 1.0) + rl.rl_translatef((1 - scale) * (x + item.rect.width / 2) / scale, + (1 - scale) * (y + item.rect.height / 2) / scale, 0) + item.render() + rl.rl_pop_matrix() + else: + item.render() # Draw scroll indicator if SCROLL_BAR and not self._horizontal and len(visible_items) > 0: From 394f580f161195f135c2b5c3723db9c7bab06147 Mon Sep 17 00:00:00 2001 From: Najib Muhammad Date: Thu, 27 Nov 2025 11:26:29 +0100 Subject: [PATCH 018/110] fix the CI Weekly Report workflow so it does not fail on forks (#36664) --- .github/workflows/ci_weekly_report.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci_weekly_report.yaml b/.github/workflows/ci_weekly_report.yaml index 9821283cb5..37a46b2096 100644 --- a/.github/workflows/ci_weekly_report.yaml +++ b/.github/workflows/ci_weekly_report.yaml @@ -38,7 +38,7 @@ jobs: report: needs: [ci_matrix_run] runs-on: ubuntu-latest - if: always() + if: always() && github.repository == 'commaai/openpilot' steps: - name: Get job results uses: actions/github-script@v7 From 630e14fd7f61a1f2d8a5acad2d98b7dc304cddbd Mon Sep 17 00:00:00 2001 From: Dean Lee Date: Thu, 27 Nov 2025 18:28:13 +0800 Subject: [PATCH 019/110] ui: avoid unnecessary text cache invalidation in UnifiedLabel (#36676) avoid unnecessary text cache invalidation in UnifiedLabel --- system/ui/widgets/label.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/system/ui/widgets/label.py b/system/ui/widgets/label.py index b6e67d03a0..c90b111de3 100644 --- a/system/ui/widgets/label.py +++ b/system/ui/widgets/label.py @@ -446,7 +446,7 @@ class UnifiedLabel(Widget): def set_text(self, text: str | Callable[[], str]): """Update the text content.""" self._text = text - self._cached_text = None # Invalidate cache + # No need to update cache here, will be done on next render if needed @property def text(self) -> str: @@ -463,15 +463,17 @@ class UnifiedLabel(Widget): def set_font_size(self, size: int): """Update the font size.""" - self._font_size = size - self._spacing_pixels = size * self._letter_spacing # Recalculate spacing - self._cached_text = None # Invalidate cache + if self._font_size != size: + self._font_size = size + self._spacing_pixels = size * self._letter_spacing # Recalculate spacing + self._cached_text = None # Invalidate cache def set_letter_spacing(self, letter_spacing: float): """Update letter spacing (as percentage, e.g., 0.1 = 10%).""" - self._letter_spacing = letter_spacing - self._spacing_pixels = self._font_size * letter_spacing - self._cached_text = None # Invalidate cache + if self._letter_spacing != letter_spacing: + self._letter_spacing = letter_spacing + self._spacing_pixels = self._font_size * letter_spacing + self._cached_text = None # Invalidate cache def set_font_weight(self, font_weight: FontWeight): """Update the font weight.""" From d0489062b5e2e41cc2f9bf5c4400530abcd92489 Mon Sep 17 00:00:00 2001 From: Dean Lee Date: Thu, 27 Nov 2025 18:28:51 +0800 Subject: [PATCH 020/110] ui: remove unused members and variables (#36677) remove unused members and variables --- selfdrive/ui/mici/onroad/alert_renderer.py | 4 ---- selfdrive/ui/mici/onroad/hud_renderer.py | 12 ------------ 2 files changed, 16 deletions(-) diff --git a/selfdrive/ui/mici/onroad/alert_renderer.py b/selfdrive/ui/mici/onroad/alert_renderer.py index eb5555660a..bdf85acc38 100644 --- a/selfdrive/ui/mici/onroad/alert_renderer.py +++ b/selfdrive/ui/mici/onroad/alert_renderer.py @@ -89,10 +89,6 @@ ALERT_CRITICAL_REBOOT = Alert( class AlertRenderer(Widget): def __init__(self): super().__init__() - self.font_regular: rl.Font = gui_app.font(FontWeight.MEDIUM) - self.font_roman: rl.Font = gui_app.font(FontWeight.ROMAN) - self.font_bold: rl.Font = gui_app.font(FontWeight.BOLD) - self.font_display: rl.Font = gui_app.font(FontWeight.DISPLAY) self._alert_text1_label = UnifiedLabel(text="", font_size=ALERT_FONT_BIG, font_weight=FontWeight.DISPLAY, line_height=0.86, letter_spacing=-0.02) diff --git a/selfdrive/ui/mici/onroad/hud_renderer.py b/selfdrive/ui/mici/onroad/hud_renderer.py index bb5171d6e3..524eb11637 100644 --- a/selfdrive/ui/mici/onroad/hud_renderer.py +++ b/selfdrive/ui/mici/onroad/hud_renderer.py @@ -31,19 +31,7 @@ class FontSizes: @dataclass(frozen=True) class Colors: white: rl.Color = rl.WHITE - disengaged: rl.Color = rl.Color(145, 155, 149, 255) - override: rl.Color = rl.Color(145, 155, 149, 255) # Added - engaged: rl.Color = rl.Color(128, 216, 166, 255) - disengaged_bg: rl.Color = rl.Color(0, 0, 0, 153) - override_bg: rl.Color = rl.Color(145, 155, 149, 204) - engaged_bg: rl.Color = rl.Color(128, 216, 166, 204) - grey: rl.Color = rl.Color(166, 166, 166, 255) - dark_grey: rl.Color = rl.Color(114, 114, 114, 255) - black_translucent: rl.Color = rl.Color(0, 0, 0, 166) white_translucent: rl.Color = rl.Color(255, 255, 255, 200) - border_translucent: rl.Color = rl.Color(255, 255, 255, 75) - header_gradient_start: rl.Color = rl.Color(0, 0, 0, 114) - header_gradient_end: rl.Color = rl.BLANK FONT_SIZES = FontSizes() From 0a0fadb16a29cfd0565e50928ee2119ac884e479 Mon Sep 17 00:00:00 2001 From: Calvin Park Date: Thu, 27 Nov 2025 05:44:13 -0500 Subject: [PATCH 021/110] Skip onboarding on PC (#36688) * Skip onboarding on PC * do this instead --------- Co-authored-by: Shane Smiskol --- selfdrive/ui/mici/onroad/driver_state.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/selfdrive/ui/mici/onroad/driver_state.py b/selfdrive/ui/mici/onroad/driver_state.py index 369055846e..080083c3e2 100644 --- a/selfdrive/ui/mici/onroad/driver_state.py +++ b/selfdrive/ui/mici/onroad/driver_state.py @@ -3,6 +3,7 @@ from collections.abc import Callable import numpy as np import math from cereal import log +from openpilot.system.hardware import PC from openpilot.common.filter_simple import FirstOrderFilter from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.widgets import Widget @@ -217,7 +218,10 @@ class DriverStateRenderer(Widget): rotation = math.degrees(math.atan2(pitch, yaw)) angle_diff = rotation - self._rotation_filter.x angle_diff = ((angle_diff + 180) % 360) - 180 - self._rotation_filter.update(self._rotation_filter.x + angle_diff) + if PC and self._confirm_mode: + self._rotation_filter.x += 2 + else: + self._rotation_filter.update(self._rotation_filter.x + angle_diff) if not self.should_draw: self._fade_filter.update(0.0) From f8d0f22344de88cf33c74981ec2164d141345f36 Mon Sep 17 00:00:00 2001 From: Dean Lee Date: Thu, 27 Nov 2025 19:22:15 +0800 Subject: [PATCH 022/110] ui: ensure auto-scroll stops correctly near target (#36686) ensure auto-scroll stops correctly near target --- system/ui/lib/scroll_panel2.py | 1 + 1 file changed, 1 insertion(+) diff --git a/system/ui/lib/scroll_panel2.py b/system/ui/lib/scroll_panel2.py index 8d9caadfdd..8677c32f81 100644 --- a/system/ui/lib/scroll_panel2.py +++ b/system/ui/lib/scroll_panel2.py @@ -88,6 +88,7 @@ class GuiScrollPanel2: # Steady once we are close enough to the target if abs(dist) < 1 and abs(self._velocity) < MIN_VELOCITY: self.set_offset(target) + self._velocity = 0.0 self._state = ScrollState.STEADY elif abs(self._velocity) < MIN_VELOCITY: From 3959200a5b6ea17845b9c6e8f516f532c2123efd Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 27 Nov 2025 03:25:30 -0800 Subject: [PATCH 023/110] Scroll panel 2: use float for offset (#36705) * use float internally * use it everywhere -- this fixes all the problemS?! * round it everywhere * this looks so much better than before * rm --- selfdrive/ui/mici/layouts/settings/device.py | 2 +- selfdrive/ui/mici/layouts/settings/firehose.py | 2 +- system/ui/lib/scroll_panel2.py | 6 +++--- system/ui/mici_setup.py | 2 +- system/ui/widgets/scroller.py | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/selfdrive/ui/mici/layouts/settings/device.py b/selfdrive/ui/mici/layouts/settings/device.py index b5e0ea838d..988c823a99 100644 --- a/selfdrive/ui/mici/layouts/settings/device.py +++ b/selfdrive/ui/mici/layouts/settings/device.py @@ -39,7 +39,7 @@ class MiciFccModal(NavWidget): content_height += self._fcc_logo.height + 20 scroll_content_rect = rl.Rectangle(rect.x, rect.y, rect.width, content_height) - scroll_offset = self._scroll_panel.update(rect, scroll_content_rect.height) + scroll_offset = round(self._scroll_panel.update(rect, scroll_content_rect.height)) fcc_pos = rl.Vector2(rect.x + 20, rect.y + 20 + scroll_offset) diff --git a/selfdrive/ui/mici/layouts/settings/firehose.py b/selfdrive/ui/mici/layouts/settings/firehose.py index 5cc43322d0..ff75e6f8ed 100644 --- a/selfdrive/ui/mici/layouts/settings/firehose.py +++ b/selfdrive/ui/mici/layouts/settings/firehose.py @@ -65,7 +65,7 @@ class FirehoseLayoutBase(Widget): def _render(self, rect: rl.Rectangle): # compute total content height for scrolling content_height = self._measure_content_height(rect) - scroll_offset = self._scroll_panel.update(rect, content_height) + scroll_offset = round(self._scroll_panel.update(rect, content_height)) # start drawing with offset x = int(rect.x + 40) diff --git a/system/ui/lib/scroll_panel2.py b/system/ui/lib/scroll_panel2.py index 8677c32f81..ab93be9453 100644 --- a/system/ui/lib/scroll_panel2.py +++ b/system/ui/lib/scroll_panel2.py @@ -8,7 +8,7 @@ from openpilot.system.ui.lib.application import gui_app, MouseEvent from openpilot.system.hardware import TICI from collections import deque -MIN_VELOCITY = 2 # px/s, changes from auto scroll to steady state +MIN_VELOCITY = 10 # px/s, changes from auto scroll to steady state MIN_VELOCITY_FOR_CLICKING = 2 * 60 # px/s, accepts clicks while auto scrolling below this velocity MIN_DRAG_PIXELS = 12 AUTO_SCROLL_TC_SNAP = 0.025 @@ -202,8 +202,8 @@ class GuiScrollPanel2: def _get_mouse_pos(self, mouse_event: MouseEvent) -> float: return mouse_event.pos.x if self._horizontal else mouse_event.pos.y - def get_offset(self) -> int: - return round(self._offset.x if self._horizontal else self._offset.y) + def get_offset(self) -> float: + return self._offset.x if self._horizontal else self._offset.y def set_offset(self, value: float) -> None: if self._horizontal: diff --git a/system/ui/mici_setup.py b/system/ui/mici_setup.py index d7395f9b7a..9792c51e7c 100755 --- a/system/ui/mici_setup.py +++ b/system/ui/mici_setup.py @@ -244,7 +244,7 @@ class TermsPage(Widget): pass def _render(self, _): - scroll_offset = self._scroll_panel.update(self._rect, self._content_height + self._continue_button.rect.height + 16) + scroll_offset = round(self._scroll_panel.update(self._rect, self._content_height + self._continue_button.rect.height + 16)) if scroll_offset <= self._scrolled_down_offset: # don't show back if not enabled diff --git a/system/ui/widgets/scroller.py b/system/ui/widgets/scroller.py index 3cb6a1e188..72f76d90c4 100644 --- a/system/ui/widgets/scroller.py +++ b/system/ui/widgets/scroller.py @@ -124,7 +124,7 @@ class Scroller(Widget): self.scroll_panel.set_enabled(scroll_enabled and self.enabled) self.scroll_panel.update(self._rect, content_size) if not self._snap_items: - return self.scroll_panel.get_offset() + return round(self.scroll_panel.get_offset()) # Snap closest item to center center_pos = self._rect.x + self._rect.width / 2 if self._horizontal else self._rect.y + self._rect.height / 2 From ce596424cfb9b3b680e2255e701f0f32e2a65edd Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 27 Nov 2025 04:16:38 -0800 Subject: [PATCH 024/110] Fix steering arc artifacts (#36707) * fix arc artifacts * works but how * also this * Revert "also this" This reverts commit e8d5ed9af15568dcb178dd6da7a14d2c6191010e. * clean up * nl * clean up * more * print * print --- selfdrive/ui/mici/onroad/torque_bar.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/selfdrive/ui/mici/onroad/torque_bar.py b/selfdrive/ui/mici/onroad/torque_bar.py index 1f6dffe879..d7c9f27a92 100644 --- a/selfdrive/ui/mici/onroad/torque_bar.py +++ b/selfdrive/ui/mici/onroad/torque_bar.py @@ -130,6 +130,9 @@ def arc_bar_pts(cx: float, cy: float, pts = np.vstack((outer, cap_end, inner, cap_start, outer[:1])).astype(np.float32) + # Rotate to start from middle of cap for proper triangulation + pts = np.roll(pts, cap_segs, axis=0) + if DEBUG: n = len(pts) idx = int(time.monotonic() * 12) % max(1, n) # speed: 12 pts/sec From 10524353916f42908557a1711efd2a5b66169c7b Mon Sep 17 00:00:00 2001 From: Dean Lee Date: Thu, 27 Nov 2025 21:58:53 +0800 Subject: [PATCH 025/110] ui: speed up `mici/AugmentedRoadView` by optimizing _calc_frame_matrix caching (#36669) speed up AugmentedRoadView by optimizing _calc_frame_matrix caching --- .../ui/mici/onroad/augmented_road_view.py | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/selfdrive/ui/mici/onroad/augmented_road_view.py b/selfdrive/ui/mici/onroad/augmented_road_view.py index ab55f392f7..f1f4e66f5a 100644 --- a/selfdrive/ui/mici/onroad/augmented_road_view.py +++ b/selfdrive/ui/mici/onroad/augmented_road_view.py @@ -138,9 +138,7 @@ class AugmentedRoadView(CameraView): self.view_from_calib = view_frame_from_device_frame.copy() self.view_from_wide_calib = view_frame_from_device_frame.copy() - self._last_calib_time: float = 0 - self._last_rect_dims = (0.0, 0.0) - self._last_stream_type = stream_type + self._matrix_cache_key = (0, 0, 0, 0, stream_type) self._cached_matrix: np.ndarray | None = None self._content_rect = rl.Rectangle() self._last_click_time = 0.0 @@ -284,10 +282,19 @@ class AugmentedRoadView(CameraView): self.view_from_wide_calib = view_frame_from_device_frame @ wide_from_device @ device_from_calib def _calc_frame_matrix(self, rect: rl.Rectangle) -> np.ndarray: + v_ego_quantized = round(ui_state.sm['carState'].vEgo, 1) + cache_key = ( + ui_state.sm.recv_frame['liveCalibration'], + int(self._content_rect.width), + int(self._content_rect.height), + self.stream_type, + v_ego_quantized + ) + + if cache_key == self._matrix_cache_key and self._cached_matrix is not None: + return self._cached_matrix + # Get camera configuration - # TODO: cache with vEgo? - calib_time = ui_state.sm.recv_frame['liveCalibration'] - current_dims = (self._content_rect.width, self._content_rect.height) device_camera = self.device_camera or DEFAULT_DEVICE_CAMERA is_wide_camera = self.stream_type == WIDE_CAM intrinsic = device_camera.ecam.intrinsics if is_wide_camera else device_camera.fcam.intrinsics @@ -323,9 +330,7 @@ class AugmentedRoadView(CameraView): x_offset, y_offset = 0, 0 # Cache the computed transformation matrix to avoid recalculations - self._last_calib_time = calib_time - self._last_rect_dims = current_dims - self._last_stream_type = self.stream_type + self._matrix_cache_key = cache_key self._cached_matrix = np.array([ [zoom * 2 * cx / w, 0, -x_offset / w * 2], [0, zoom * 2 * cy / h, -y_offset / h * 2], From f07a40deb4f66c9bbd8299a7d7edefd1c4a2831d Mon Sep 17 00:00:00 2001 From: Jason Young <46612682+jyoung8607@users.noreply.github.com> Date: Fri, 28 Nov 2025 13:08:07 -0500 Subject: [PATCH 026/110] regen CARS.md (#36711) --- docs/CARS.md | 671 +++++++++++++++++++++++++-------------------------- 1 file changed, 335 insertions(+), 336 deletions(-) diff --git a/docs/CARS.md b/docs/CARS.md index 3ea12f651e..223f452e16 100644 --- a/docs/CARS.md +++ b/docs/CARS.md @@ -8,331 +8,331 @@ A supported vehicle is one that just works when you install a comma device. All |Make|Model|Supported Package|ACC|No ACC accel below|No ALC below|Steering Torque|Resume from stop|Hardware Needed
 |Video|Setup Video| |---|---|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:| -|Acura|ILX 2016-18|Technology Plus Package or AcuraWatch Plus|openpilot|26 mph|25 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Acura|ILX 2019|All|openpilot|26 mph|25 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Acura|MDX 2025|All except Type S|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch C connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Acura|RDX 2016-18|AcuraWatch Plus or Advance Package|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Acura|RDX 2019-21|All|openpilot available[1](#footnotes)|0 mph|3 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Acura|TLX 2021|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Audi|A3 2014-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Audi|A3 Sportback e-tron 2017-18|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Audi|Q2 2018|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Audi|Q3 2019-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Audi|RS3 2018|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Audi|S3 2015-17|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Chevrolet|Bolt EUV 2022-23|Premier or Premier Redline Trim without Super Cruise Package|openpilot available[1](#footnotes)|3 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 GM connector
- 1 comma 3X
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Chevrolet|Bolt EV 2022-23|2LT Trim with Adaptive Cruise Control Package|openpilot available[1](#footnotes)|3 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 GM connector
- 1 comma 3X
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Chevrolet|Equinox 2019-22|Adaptive Cruise Control (ACC)|openpilot available[1](#footnotes)|3 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 GM connector
- 1 comma 3X
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Chevrolet|Silverado 1500 2020-21|Safety Package II|openpilot available[1](#footnotes)|0 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 GM connector
- 1 comma 3X
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Chevrolet|Trailblazer 2021-22|Adaptive Cruise Control (ACC)|openpilot available[1](#footnotes)|3 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 GM connector
- 1 comma 3X
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Chrysler|Pacifica 2017-18|Adaptive Cruise Control (ACC)|Stock|0 mph|9 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 FCA connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Chrysler|Pacifica 2019-20|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 FCA connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Chrysler|Pacifica 2021-23|All|Stock|0 mph|39 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 FCA connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Chrysler|Pacifica Hybrid 2017-18|Adaptive Cruise Control (ACC)|Stock|0 mph|9 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 FCA connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Chrysler|Pacifica Hybrid 2019-25|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 FCA connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Acura|ILX 2016-18|Technology Plus Package or AcuraWatch Plus|openpilot|26 mph|25 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Acura|ILX 2019|All|openpilot|26 mph|25 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Acura|MDX 2025|All except Type S|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch C connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Acura|RDX 2016-18|AcuraWatch Plus or Advance Package|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Acura|RDX 2019-21|All|openpilot available[1](#footnotes)|0 mph|3 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Acura|TLX 2021|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Audi|A3 2014-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Audi|A3 Sportback e-tron 2017-18|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Audi|Q2 2018|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Audi|Q3 2019-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Audi|RS3 2018|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Audi|S3 2015-17|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Chevrolet|Bolt EUV 2022-23|Premier or Premier Redline Trim, without Super Cruise Package|openpilot available[1](#footnotes)|3 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 GM connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 harness box
- 1 mount
Buy Here
||| +|Chevrolet|Bolt EV 2022-23|2LT Trim with Adaptive Cruise Control Package|openpilot available[1](#footnotes)|3 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 GM connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 harness box
- 1 mount
Buy Here
||| +|Chevrolet|Equinox 2019-22|Adaptive Cruise Control (ACC)|openpilot available[1](#footnotes)|3 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 GM connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 harness box
- 1 mount
Buy Here
||| +|Chevrolet|Silverado 1500 2020-21|Safety Package II|openpilot available[1](#footnotes)|0 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 GM connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 harness box
- 1 mount
Buy Here
||| +|Chevrolet|Trailblazer 2021-22|Adaptive Cruise Control (ACC)|openpilot available[1](#footnotes)|3 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 GM connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 harness box
- 1 mount
Buy Here
||| +|Chrysler|Pacifica 2017-18|Adaptive Cruise Control (ACC)|Stock|0 mph|9 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 FCA connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Chrysler|Pacifica 2019-20|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 FCA connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Chrysler|Pacifica 2021-23|All|Stock|0 mph|39 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 FCA connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Chrysler|Pacifica Hybrid 2017-18|Adaptive Cruise Control (ACC)|Stock|0 mph|9 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 FCA connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Chrysler|Pacifica Hybrid 2019-25|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 FCA connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |comma|body|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|None||| -|CUPRA|Ateca 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Dodge|Durango 2020-21|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 FCA connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Ford|Bronco Sport 2021-24|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Ford|Escape 2020-22|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Ford|Escape 2023-24|Co-Pilot360 Assist+|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 USB-C coupler
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Ford|Escape Hybrid 2020-22|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Ford|Escape Hybrid 2023-24|Co-Pilot360 Assist+|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 USB-C coupler
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Ford|Escape Plug-in Hybrid 2020-22|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Ford|Escape Plug-in Hybrid 2023-24|Co-Pilot360 Assist+|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 USB-C coupler
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Ford|Expedition 2022-24|Co-Pilot360 Assist 2.0|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 USB-C coupler
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Ford|Explorer 2020-24|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Ford|Explorer Hybrid 2020-24|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Ford|F-150 2021-23|Co-Pilot360 Assist 2.0|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 USB-C coupler
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Ford|F-150 Hybrid 2021-23|Co-Pilot360 Assist 2.0|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 USB-C coupler
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Ford|Focus 2018[3](#footnotes)|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Ford|Focus Hybrid 2018[3](#footnotes)|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Ford|Kuga 2020-23|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Ford|Kuga Hybrid 2020-23|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Ford|Kuga Hybrid 2024|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 USB-C coupler
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Ford|Kuga Plug-in Hybrid 2020-23|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Ford|Kuga Plug-in Hybrid 2024|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 USB-C coupler
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Ford|Maverick 2022|LARIAT Luxury|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Ford|Maverick 2023-24|Co-Pilot360 Assist|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Ford|Maverick Hybrid 2022|LARIAT Luxury|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Ford|Maverick Hybrid 2023-24|Co-Pilot360 Assist|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Ford|Mustang Mach-E 2021-24|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 USB-C coupler
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Ford|Ranger 2024|Adaptive Cruise Control with Lane Centering|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 USB-C coupler
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Genesis|G70 2018|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai F connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Genesis|G70 2019-21|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai F connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Genesis|G70 2022-23|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai L connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Genesis|G80 2017|All|Stock|19 mph|37 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Hyundai J connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Genesis|G80 2018-19|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai H connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Genesis|G80 (2.5T Advanced Trim, with HDA II) 2024[6](#footnotes)|Highway Driving Assist II|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai P connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Genesis|G90 2017-20|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai C connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Genesis|GV60 (Advanced Trim) 2023[6](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Genesis|GV60 (Performance Trim) 2022-23[6](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai K connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Genesis|GV70 (2.5T Trim, without HDA II) 2022-24[6](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai L connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Genesis|GV70 (3.5T Trim, without HDA II) 2022-23[6](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai M connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Genesis|GV70 Electrified (Australia Only) 2022[6](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai Q connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Genesis|GV70 Electrified (with HDA II) 2023-24[6](#footnotes)|Highway Driving Assist II|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai Q connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Genesis|GV80 2023[6](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai M connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|GMC|Sierra 1500 2020-21|Driver Alert Package II|openpilot available[1](#footnotes)|0 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 GM connector
- 1 comma 3X
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Honda|Accord 2018-22|All|openpilot available[1](#footnotes)|0 mph|3 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Honda|Accord 2023-25|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch C connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Honda|Accord Hybrid 2018-22|All|openpilot available[1](#footnotes)|0 mph|3 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Honda|Accord Hybrid 2023-25|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch C connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Honda|City (Brazil only) 2023|All|openpilot available[1](#footnotes)|0 mph|14 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch B connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Honda|Civic 2016-18|Honda Sensing|openpilot|0 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Honda|Civic 2019-21|All|openpilot available[1](#footnotes)|0 mph|2 mph[5](#footnotes)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Honda|Civic 2022-24|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch B connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Honda|Civic Hatchback 2017-18|Honda Sensing|openpilot available[1](#footnotes)|0 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Honda|Civic Hatchback 2019-21|All|openpilot available[1](#footnotes)|0 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Honda|Civic Hatchback 2022-24|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch B connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Honda|Civic Hatchback Hybrid 2025-26|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch B connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Honda|Civic Hatchback Hybrid (Europe only) 2023|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch B connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Honda|Civic Hybrid 2025|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch B connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Honda|CR-V 2015-16|Touring Trim|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Honda|CR-V 2017-22|Honda Sensing|openpilot available[1](#footnotes)|0 mph|15 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Honda|CR-V 2023-25|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch C connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Honda|CR-V Hybrid 2017-22|Honda Sensing|openpilot available[1](#footnotes)|0 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Honda|CR-V Hybrid 2023-25|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch C connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Honda|e 2020|All|openpilot available[1](#footnotes)|0 mph|3 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Honda|Fit 2018-20|Honda Sensing|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Honda|Freed 2020|Honda Sensing|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Honda|HR-V 2019-22|Honda Sensing|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Honda|HR-V 2023-25|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch B connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Honda|Insight 2019-22|All|openpilot available[1](#footnotes)|0 mph|3 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Honda|Inspire 2018|All|openpilot available[1](#footnotes)|0 mph|3 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Honda|N-Box 2018|All|openpilot available[1](#footnotes)|0 mph|11 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Honda|Odyssey 2018-20|Honda Sensing|openpilot|26 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Honda|Odyssey 2021-25|All|openpilot available[1](#footnotes)|0 mph|43 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Honda|Passport 2019-25|All|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Honda|Passport 2026|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch C connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Honda|Pilot 2016-22|Honda Sensing|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Honda|Pilot 2023-25|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch C connector
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Honda|Ridgeline 2017-25|Honda Sensing|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Azera 2022|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai K connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Azera Hybrid 2019|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai C connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Azera Hybrid 2020|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai K connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Custin 2023|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai K connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Elantra 2017-18|Smart Cruise Control (SCC)|Stock|19 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Hyundai B connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Elantra 2019|Smart Cruise Control (SCC)|Stock|19 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Hyundai G connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Elantra 2021-23|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai K connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Elantra GT 2017-20|Smart Cruise Control (SCC)|Stock|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai E connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Elantra Hybrid 2021-23|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai K connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Genesis 2015-16|Smart Cruise Control (SCC)|Stock|19 mph|37 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Hyundai J connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|i30 2017-19|Smart Cruise Control (SCC)|Stock|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai E connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Ioniq 5 (Southeast Asia and Europe only) 2022-24[6](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai Q connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Ioniq 5 (with HDA II) 2022-24[6](#footnotes)|Highway Driving Assist II|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai Q connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Ioniq 5 (without HDA II) 2022-24[6](#footnotes)|Highway Driving Assist|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai K connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Ioniq 6 (with HDA II) 2023-24[6](#footnotes)|Highway Driving Assist II|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai P connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Ioniq Electric 2019|Smart Cruise Control (SCC)|Stock|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai C connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Ioniq Electric 2020|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai H connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Ioniq Hybrid 2017-19|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai C connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Ioniq Hybrid 2020-22|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai H connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Ioniq Plug-in Hybrid 2019|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai C connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Ioniq Plug-in Hybrid 2020-22|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai H connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Kona 2020|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|6 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Hyundai B connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Kona Electric 2018-21|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai G connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Kona Electric 2022-23|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai O connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Kona Electric (with HDA II, Korea only) 2023[6](#footnotes)|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai R connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Kona Hybrid 2020|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai I connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Nexo 2021|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai H connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Palisade 2020-22|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai H connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Santa Cruz 2022-24[6](#footnotes)|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai N connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Santa Fe 2019-20|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai D connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Santa Fe 2021-23|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai L connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Santa Fe Hybrid 2022-23|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai L connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Santa Fe Plug-in Hybrid 2022-23|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai L connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Sonata 2018-19|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai E connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Sonata 2020-23|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Sonata Hybrid 2020-23|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Staria 2023[6](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai K connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Tucson 2021|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Hyundai L connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Tucson 2022[6](#footnotes)|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai N connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Tucson 2023-24[6](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai N connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Tucson Diesel 2019|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai L connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Tucson Hybrid 2022-24[6](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai N connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Tucson Plug-in Hybrid 2024[6](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai N connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Veloster 2019-20|Smart Cruise Control (SCC)|Stock|5 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Hyundai E connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Jeep|Grand Cherokee 2016-18|Adaptive Cruise Control (ACC)|Stock|0 mph|9 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 FCA connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Jeep|Grand Cherokee 2019-21|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 FCA connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Carnival 2022-24[6](#footnotes)|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Carnival (China only) 2023[6](#footnotes)|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai K connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Ceed 2019-21|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai E connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|EV6 (Southeast Asia only) 2022-24[6](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai P connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|EV6 (with HDA II) 2022-24[6](#footnotes)|Highway Driving Assist II|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai P connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|EV6 (without HDA II) 2022-24[6](#footnotes)|Highway Driving Assist|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai L connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Forte 2019-21|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|6 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Hyundai G connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Forte 2022-23|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai E connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|K5 2021-24|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|K5 Hybrid 2020-22|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|K8 Hybrid (with HDA II) 2023[6](#footnotes)|Highway Driving Assist II|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai Q connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Niro EV 2019|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai H connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Niro EV 2020|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai F connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Niro EV 2021|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai C connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Niro EV 2022|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai H connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Niro EV (with HDA II) 2025[6](#footnotes)|Highway Driving Assist II|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai R connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Niro EV (without HDA II) 2023-25[6](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Niro Hybrid 2018|Smart Cruise Control (SCC)|Stock|10 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Hyundai C connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Niro Hybrid 2021|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai D connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Niro Hybrid 2022|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai F connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Niro Hybrid 2023[6](#footnotes)|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Niro Plug-in Hybrid 2018-19|All|Stock|10 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Hyundai C connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Niro Plug-in Hybrid 2020|Smart Cruise Control (SCC)|Stock|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai D connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Niro Plug-in Hybrid 2021|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai D connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Niro Plug-in Hybrid 2022|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai F connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Optima 2017|Advanced Smart Cruise Control|Stock|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai B connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Optima 2019-20|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai G connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Optima Hybrid 2019|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai H connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Seltos 2021|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Sorento 2018|Advanced Smart Cruise Control & LKAS|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai E connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Sorento 2019|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai E connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Sorento 2021-23[6](#footnotes)|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai K connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Sorento Hybrid 2021-23[6](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Sorento Plug-in Hybrid 2022-23[6](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Sportage 2023-24[6](#footnotes)|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai N connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Sportage Hybrid 2023[6](#footnotes)|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai N connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Stinger 2018-20|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai C connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Stinger 2022-23|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai K connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Telluride 2020-22|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai H connector
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Lexus|CT Hybrid 2017-18|Lexus Safety System+|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Lexus|ES 2017-18|All|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Lexus|ES 2019-25|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Lexus|ES Hybrid 2017-18|All|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Lexus|ES Hybrid 2019-25|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Lexus|GS F 2016|All|Stock|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Lexus|IS 2017-19|All|Stock|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Lexus|IS 2022-24|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Lexus|LC 2024-25|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Lexus|NX 2018-19|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Lexus|NX 2020-21|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Lexus|NX Hybrid 2018-19|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Lexus|NX Hybrid 2020-21|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Lexus|RC 2018-20|All|Stock|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Lexus|RC 2023|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Lexus|RX 2016|Lexus Safety System+|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Lexus|RX 2017-19|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Lexus|RX 2020-22|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Lexus|RX Hybrid 2016|Lexus Safety System+|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Lexus|RX Hybrid 2017-19|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Lexus|RX Hybrid 2020-22|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Lexus|UX Hybrid 2019-24|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Lincoln|Aviator 2020-24|Co-Pilot360 Plus|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Lincoln|Aviator Plug-in Hybrid 2020-24|Co-Pilot360 Plus|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|MAN|eTGE 2020-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|MAN|TGE 2017-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Mazda|CX-5 2022-25|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Mazda connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Mazda|CX-9 2021-23|All|Stock|0 mph|28 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Mazda connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Nissan[7](#footnotes)|Altima 2019-20|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Nissan B connector
- 1 USB-C coupler
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Nissan[7](#footnotes)|Leaf 2018-23|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Nissan A connector
- 1 USB-C coupler
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Nissan[7](#footnotes)|Rogue 2018-20|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Nissan A connector
- 1 USB-C coupler
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Nissan[7](#footnotes)|X-Trail 2017|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Nissan A connector
- 1 USB-C coupler
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Ram|1500 2019-24|Adaptive Cruise Control (ACC)|Stock|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ram connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Rivian|R1S 2022-24|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Rivian A connector
- 1 USB-C coupler
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Rivian|R1T 2022-24|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Rivian A connector
- 1 USB-C coupler
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|SEAT|Ateca 2016-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|SEAT|Leon 2014-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Subaru|Ascent 2019-21|All[8](#footnotes)|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Subaru A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| -|Subaru|Crosstrek 2018-19|EyeSight Driver Assistance[8](#footnotes)|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Subaru A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| -|Subaru|Crosstrek 2020-23|EyeSight Driver Assistance[8](#footnotes)|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Subaru A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| -|Subaru|Forester 2019-21|All[8](#footnotes)|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Subaru A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| -|Subaru|Impreza 2017-19|EyeSight Driver Assistance[8](#footnotes)|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Subaru A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| -|Subaru|Impreza 2020-22|EyeSight Driver Assistance[8](#footnotes)|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Subaru A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| -|Subaru|Legacy 2020-22|All[8](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Subaru B connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| -|Subaru|Outback 2020-22|All[8](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Subaru B connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| -|Subaru|XV 2018-19|EyeSight Driver Assistance[8](#footnotes)|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Subaru A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| -|Subaru|XV 2020-21|EyeSight Driver Assistance[8](#footnotes)|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Subaru A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| -|Škoda|Fabia 2022-23[15](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
[17](#footnotes)||| -|Škoda|Kamiq 2021-23[13,15](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
[17](#footnotes)||| -|Škoda|Karoq 2019-23[15](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Škoda|Kodiaq 2017-23[15](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Škoda|Octavia 2015-19[15](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Škoda|Octavia RS 2016[15](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Škoda|Octavia Scout 2017-19[15](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Škoda|Scala 2020-23[15](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
[17](#footnotes)||| -|Škoda|Superb 2015-22[15](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Tesla[11](#footnotes)|Model 3 (with HW3) 2019-23[10](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Tesla A connector
- 1 USB-C coupler
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Tesla[11](#footnotes)|Model 3 (with HW4) 2024-25[10](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Tesla B connector
- 1 USB-C coupler
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Tesla[11](#footnotes)|Model Y (with HW3) 2020-23[10](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Tesla A connector
- 1 USB-C coupler
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Tesla[11](#footnotes)|Model Y (with HW4) 2024-25[10](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Tesla B connector
- 1 USB-C coupler
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|Alphard 2019-20|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|Alphard Hybrid 2021|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|Avalon 2016|Toyota Safety Sense P|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|Avalon 2017-18|All|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|Avalon 2019-21|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|Avalon 2022|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|Avalon Hybrid 2019-21|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|Avalon Hybrid 2022|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|C-HR 2017-20|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|C-HR 2021|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|C-HR Hybrid 2017-20|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|C-HR Hybrid 2021-22|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|Camry 2018-20|All|Stock|0 mph[12](#footnotes)|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|Camry 2021-24|All|openpilot|0 mph[12](#footnotes)|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|Camry Hybrid 2018-20|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|Camry Hybrid 2021-24|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|Corolla 2017-19|All|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|Corolla 2020-22|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|Corolla Cross (Non-US only) 2020-23|All|openpilot|17 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|Corolla Cross Hybrid (Non-US only) 2020-22|All|openpilot|17 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|Corolla Hatchback 2019-22|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|Corolla Hybrid 2020-22|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|Corolla Hybrid (South America only) 2020-23|All|openpilot|17 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|Highlander 2017-19|All|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|Highlander 2020-23|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|Highlander Hybrid 2017-19|All|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|Highlander Hybrid 2020-23|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|Mirai 2021|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|Prius 2016|Toyota Safety Sense P|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|Prius 2017-20|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|Prius 2021-22|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|Prius Prime 2017-20|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|Prius Prime 2021-22|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|Prius v 2017|Toyota Safety Sense P|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|RAV4 2016|Toyota Safety Sense P|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|RAV4 2017-18|All|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|RAV4 2019-21|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|RAV4 2022|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|RAV4 2023-25|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|RAV4 Hybrid 2016|Toyota Safety Sense P|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|RAV4 Hybrid 2017-18|All|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|RAV4 Hybrid 2019-21|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|RAV4 Hybrid 2022|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|RAV4 Hybrid 2023-25|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|Sienna 2018-20|All|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Volkswagen|Arteon 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Volkswagen|Arteon eHybrid 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Volkswagen|Arteon R 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Volkswagen|Arteon Shooting Brake 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Volkswagen|Atlas 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Volkswagen|Atlas Cross Sport 2020-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Volkswagen|California 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Volkswagen|Caravelle 2020|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Volkswagen|CC 2018-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Volkswagen|Crafter 2017-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Volkswagen|e-Crafter 2018-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Volkswagen|e-Golf 2014-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Volkswagen|Golf 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Volkswagen|Golf Alltrack 2015-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Volkswagen|Golf GTD 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Volkswagen|Golf GTE 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Volkswagen|Golf GTI 2015-21|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Volkswagen|Golf R 2015-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Volkswagen|Golf SportsVan 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Volkswagen|Grand California 2019-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Volkswagen|Jetta 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Volkswagen|Jetta GLI 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Volkswagen|Passat 2015-22[14](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Volkswagen|Passat Alltrack 2015-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Volkswagen|Passat GTE 2015-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Volkswagen|Polo 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
[17](#footnotes)||| -|Volkswagen|Polo GTI 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
[17](#footnotes)||| -|Volkswagen|T-Cross 2021|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
[17](#footnotes)||| -|Volkswagen|T-Roc 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Volkswagen|Taos 2022-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Volkswagen|Teramont 2018-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Volkswagen|Teramont Cross Sport 2021-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Volkswagen|Teramont X 2021-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Volkswagen|Tiguan 2018-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Volkswagen|Tiguan eHybrid 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Volkswagen|Touran 2016-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|CUPRA|Ateca 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Dodge|Durango 2020-21|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 FCA connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Ford|Bronco Sport 2021-24|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Ford|Escape 2020-22|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Ford|Escape 2023-24|Co-Pilot360 Assist+|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Ford|Escape Hybrid 2020-22|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Ford|Escape Hybrid 2023-24|Co-Pilot360 Assist+|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Ford|Escape Plug-in Hybrid 2020-22|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Ford|Escape Plug-in Hybrid 2023-24|Co-Pilot360 Assist+|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Ford|Expedition 2022-24|Co-Pilot360 Assist 2.0|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Ford|Explorer 2020-24|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Ford|Explorer Hybrid 2020-24|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Ford|F-150 2021-23|Co-Pilot360 Assist 2.0|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Ford|F-150 Hybrid 2021-23|Co-Pilot360 Assist 2.0|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Ford|Focus 2018[3](#footnotes)|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Ford|Focus Hybrid 2018[3](#footnotes)|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Ford|Kuga 2020-23|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Ford|Kuga Hybrid 2020-23|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Ford|Kuga Hybrid 2024|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Ford|Kuga Plug-in Hybrid 2020-23|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Ford|Kuga Plug-in Hybrid 2024|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Ford|Maverick 2022|LARIAT Luxury|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Ford|Maverick 2023-24|Co-Pilot360 Assist|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Ford|Maverick Hybrid 2022|LARIAT Luxury|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Ford|Maverick Hybrid 2023-24|Co-Pilot360 Assist|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Ford|Mustang Mach-E 2021-24|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Ford|Ranger 2024|Adaptive Cruise Control with Lane Centering|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Genesis|G70 2018|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai F connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Genesis|G70 2019-21|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai F connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Genesis|G70 2022-23|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai L connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Genesis|G80 2017|All|Stock|19 mph|37 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Hyundai J connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Genesis|G80 2018-19|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai H connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Genesis|G80 (2.5T Advanced Trim, with HDA II) 2024|Highway Driving Assist II|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai P connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Genesis|G90 2017-20|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai C connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Genesis|GV60 (Advanced Trim) 2023|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Genesis|GV60 (Performance Trim) 2022-23|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai K connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Genesis|GV70 (2.5T Trim, without HDA II) 2022-24|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai L connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Genesis|GV70 (3.5T Trim, without HDA II) 2022-23|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai M connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Genesis|GV70 Electrified (Australia Only) 2022|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai Q connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Genesis|GV70 Electrified (with HDA II) 2023-24|Highway Driving Assist II|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai Q connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Genesis|GV80 2023|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai M connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|GMC|Sierra 1500 2020-21|Driver Alert Package II|openpilot available[1](#footnotes)|0 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 GM connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 harness box
- 1 mount
Buy Here
||| +|Honda|Accord 2018-22|All|openpilot available[1](#footnotes)|0 mph|3 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Honda|Accord 2023-25|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch C connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Honda|Accord Hybrid 2018-22|All|openpilot available[1](#footnotes)|0 mph|3 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Honda|Accord Hybrid 2023-25|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch C connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Honda|City (Brazil only) 2023|All|openpilot available[1](#footnotes)|0 mph|14 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch B connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Honda|Civic 2016-18|Honda Sensing|openpilot|0 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Honda|Civic 2019-21|All|openpilot available[1](#footnotes)|0 mph|2 mph[5](#footnotes)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Honda|Civic 2022-24|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch B connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Honda|Civic Hatchback 2017-18|Honda Sensing|openpilot available[1](#footnotes)|0 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Honda|Civic Hatchback 2019-21|All|openpilot available[1](#footnotes)|0 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Honda|Civic Hatchback 2022-24|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch B connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Honda|Civic Hatchback Hybrid 2025-26|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch B connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Honda|Civic Hatchback Hybrid (Europe only) 2023|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch B connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Honda|Civic Hybrid 2025-26|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch B connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Honda|CR-V 2015-16|Touring Trim|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Honda|CR-V 2017-22|Honda Sensing|openpilot available[1](#footnotes)|0 mph|15 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Honda|CR-V 2023-26|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch C connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Honda|CR-V Hybrid 2017-22|Honda Sensing|openpilot available[1](#footnotes)|0 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Honda|CR-V Hybrid 2023-25|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch C connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Honda|e 2020|All|openpilot available[1](#footnotes)|0 mph|3 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Honda|Fit 2018-20|Honda Sensing|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Honda|Freed 2020|Honda Sensing|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Honda|HR-V 2019-22|Honda Sensing|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Honda|HR-V 2023-25|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch B connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Honda|Insight 2019-22|All|openpilot available[1](#footnotes)|0 mph|3 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Honda|Inspire 2018|All|openpilot available[1](#footnotes)|0 mph|3 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Honda|N-Box 2018|All|openpilot available[1](#footnotes)|0 mph|11 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Honda|Odyssey 2018-20|Honda Sensing|openpilot|26 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Honda|Odyssey 2021-26|All|openpilot available[1](#footnotes)|0 mph|43 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Honda|Passport 2019-25|All|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Honda|Passport 2026|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch C connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Honda|Pilot 2016-22|Honda Sensing|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Honda|Pilot 2023-25|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch C connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Honda|Ridgeline 2017-25|Honda Sensing|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Hyundai|Azera 2022|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai K connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Hyundai|Azera Hybrid 2019|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai C connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Hyundai|Azera Hybrid 2020|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai K connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Hyundai|Custin 2023|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai K connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Hyundai|Elantra 2017-18|Smart Cruise Control (SCC)|Stock|19 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Hyundai B connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Hyundai|Elantra 2019|Smart Cruise Control (SCC)|Stock|19 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Hyundai G connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Hyundai|Elantra 2021-23|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai K connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Hyundai|Elantra GT 2017-20|Smart Cruise Control (SCC)|Stock|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai E connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Hyundai|Elantra Hybrid 2021-23|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai K connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Hyundai|Genesis 2015-16|Smart Cruise Control (SCC)|Stock|19 mph|37 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Hyundai J connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Hyundai|i30 2017-19|Smart Cruise Control (SCC)|Stock|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai E connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Hyundai|Ioniq 5 (Southeast Asia and Europe only) 2022-24|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai Q connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Hyundai|Ioniq 5 (with HDA II) 2022-24|Highway Driving Assist II|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai Q connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Hyundai|Ioniq 5 (without HDA II) 2022-24|Highway Driving Assist|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai K connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Hyundai|Ioniq 6 (with HDA II) 2023-24|Highway Driving Assist II|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai P connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Hyundai|Ioniq Electric 2019|Smart Cruise Control (SCC)|Stock|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai C connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Hyundai|Ioniq Electric 2020|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai H connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Hyundai|Ioniq Hybrid 2017-19|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai C connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Hyundai|Ioniq Hybrid 2020-22|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai H connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Hyundai|Ioniq Plug-in Hybrid 2019|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai C connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Hyundai|Ioniq Plug-in Hybrid 2020-22|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai H connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Hyundai|Kona 2020|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|6 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Hyundai B connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Hyundai|Kona Electric 2018-21|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai G connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Hyundai|Kona Electric 2022-23|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai O connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Hyundai|Kona Electric (with HDA II, Korea only) 2023|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai R connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Hyundai|Kona Hybrid 2020|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai I connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Hyundai|Nexo 2021|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai H connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Hyundai|Palisade 2020-22|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai H connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Hyundai|Santa Cruz 2022-24|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai N connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Hyundai|Santa Fe 2019-20|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai D connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Hyundai|Santa Fe 2021-23|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai L connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Hyundai|Santa Fe Hybrid 2022-23|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai L connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Hyundai|Santa Fe Plug-in Hybrid 2022-23|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai L connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Hyundai|Sonata 2018-19|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai E connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Hyundai|Sonata 2020-23|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Hyundai|Sonata Hybrid 2020-23|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Hyundai|Staria 2023|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai K connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Hyundai|Tucson 2021|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Hyundai L connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Hyundai|Tucson 2022|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai N connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Hyundai|Tucson 2023-24|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai N connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Hyundai|Tucson Diesel 2019|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai L connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Hyundai|Tucson Hybrid 2022-24|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai N connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Hyundai|Tucson Plug-in Hybrid 2024|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai N connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Hyundai|Veloster 2019-20|Smart Cruise Control (SCC)|Stock|5 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Hyundai E connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Jeep|Grand Cherokee 2016-18|Adaptive Cruise Control (ACC)|Stock|0 mph|9 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 FCA connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Jeep|Grand Cherokee 2019-21|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 FCA connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Kia|Carnival 2022-24|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Kia|Carnival (China only) 2023|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai K connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Kia|Ceed 2019-21|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai E connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Kia|EV6 (Southeast Asia only) 2022-24|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai P connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Kia|EV6 (with HDA II) 2022-24|Highway Driving Assist II|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai P connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Kia|EV6 (without HDA II) 2022-24|Highway Driving Assist|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai L connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Kia|Forte 2019-21|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|6 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Hyundai G connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Kia|Forte 2022-23|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai E connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Kia|K5 2021-24|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Kia|K5 Hybrid 2020-22|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Kia|K8 Hybrid (with HDA II) 2023|Highway Driving Assist II|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai Q connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Kia|Niro EV 2019|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai H connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Kia|Niro EV 2020|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai F connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Kia|Niro EV 2021|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai C connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Kia|Niro EV 2022|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai H connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Kia|Niro EV (with HDA II) 2025|Highway Driving Assist II|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai R connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Kia|Niro EV (without HDA II) 2023-25|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Kia|Niro Hybrid 2018|Smart Cruise Control (SCC)|Stock|10 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Hyundai C connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Kia|Niro Hybrid 2021|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai D connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Kia|Niro Hybrid 2022|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai F connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Kia|Niro Hybrid 2023|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Kia|Niro Plug-in Hybrid 2018-19|All|Stock|10 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Hyundai C connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Kia|Niro Plug-in Hybrid 2020|Smart Cruise Control (SCC)|Stock|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai D connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Kia|Niro Plug-in Hybrid 2021|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai D connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Kia|Niro Plug-in Hybrid 2022|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai F connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Kia|Optima 2017|Advanced Smart Cruise Control|Stock|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai B connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Kia|Optima 2019-20|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai G connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Kia|Optima Hybrid 2019|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai H connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Kia|Seltos 2021|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Kia|Sorento 2018|Advanced Smart Cruise Control & LKAS|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai E connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Kia|Sorento 2019|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai E connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Kia|Sorento 2021-23|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai K connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Kia|Sorento Hybrid 2021-23|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Kia|Sorento Plug-in Hybrid 2022-23|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Kia|Sportage 2023-24|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai N connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Kia|Sportage Hybrid 2023|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai N connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Kia|Stinger 2018-20|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai C connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Kia|Stinger 2022-23|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai K connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Kia|Telluride 2020-22|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai H connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Lexus|CT Hybrid 2017-18|Lexus Safety System+|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Lexus|ES 2017-18|All|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Lexus|ES 2019-25|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Lexus|ES Hybrid 2017-18|All|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Lexus|ES Hybrid 2019-25|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Lexus|GS F 2016|All|Stock|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Lexus|IS 2017-19|All|Stock|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Lexus|IS 2022-24|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Lexus|LC 2024-25|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Lexus|NX 2018-19|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Lexus|NX 2020-21|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Lexus|NX Hybrid 2018-19|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Lexus|NX Hybrid 2020-21|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Lexus|RC 2018-20|All|Stock|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Lexus|RC 2023|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Lexus|RX 2016|Lexus Safety System+|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Lexus|RX 2017-19|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Lexus|RX 2020-22|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Lexus|RX Hybrid 2016|Lexus Safety System+|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Lexus|RX Hybrid 2017-19|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Lexus|RX Hybrid 2020-22|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Lexus|UX Hybrid 2019-24|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Lincoln|Aviator 2020-24|Co-Pilot360 Plus|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Lincoln|Aviator Plug-in Hybrid 2020-24|Co-Pilot360 Plus|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|MAN|eTGE 2020-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|MAN|TGE 2017-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Mazda|CX-5 2022-25|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Mazda connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Mazda|CX-9 2021-23|All|Stock|0 mph|28 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Mazda connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Nissan[6](#footnotes)|Altima 2019-20, 2024|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Nissan B connector
- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Nissan[6](#footnotes)|Leaf 2018-23|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Nissan A connector
- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Nissan[6](#footnotes)|Rogue 2018-20|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Nissan A connector
- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Nissan[6](#footnotes)|X-Trail 2017|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Nissan A connector
- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Ram|1500 2019-24|Adaptive Cruise Control (ACC)|Stock|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Ram connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Rivian|R1S 2022-24|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Rivian A connector
- 1 USB-C coupler
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Rivian|R1T 2022-24|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Rivian A connector
- 1 USB-C coupler
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|SEAT|Ateca 2016-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|SEAT|Leon 2014-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Subaru|Ascent 2019-21|All[7](#footnotes)|openpilot available[1,8](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Subaru A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| +|Subaru|Crosstrek 2018-19|EyeSight Driver Assistance[7](#footnotes)|openpilot available[1,8](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Subaru A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| +|Subaru|Crosstrek 2020-23|EyeSight Driver Assistance[7](#footnotes)|openpilot available[1,8](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Subaru A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| +|Subaru|Forester 2019-21|All[7](#footnotes)|openpilot available[1,8](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Subaru A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| +|Subaru|Impreza 2017-19|EyeSight Driver Assistance[7](#footnotes)|openpilot available[1,8](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Subaru A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| +|Subaru|Impreza 2020-22|EyeSight Driver Assistance[7](#footnotes)|openpilot available[1,8](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Subaru A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| +|Subaru|Legacy 2020-22|All[7](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Subaru B connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| +|Subaru|Outback 2020-22|All[7](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Subaru B connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| +|Subaru|XV 2018-19|EyeSight Driver Assistance[7](#footnotes)|openpilot available[1,8](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Subaru A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| +|Subaru|XV 2020-21|EyeSight Driver Assistance[7](#footnotes)|openpilot available[1,8](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Subaru A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| +|Škoda|Fabia 2022-23[14](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
[16](#footnotes)||| +|Škoda|Kamiq 2021-23[12,14](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
[16](#footnotes)||| +|Škoda|Karoq 2019-23[14](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Škoda|Kodiaq 2017-23[14](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Škoda|Octavia 2015-19[14](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Škoda|Octavia RS 2016[14](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Škoda|Octavia Scout 2017-19[14](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Škoda|Scala 2020-23[14](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
[16](#footnotes)||| +|Škoda|Superb 2015-22[14](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Tesla[10](#footnotes)|Model 3 (with HW3) 2019-23[9](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Tesla A connector
- 1 USB-C coupler
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Tesla[10](#footnotes)|Model 3 (with HW4) 2024-25[9](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Tesla B connector
- 1 USB-C coupler
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Tesla[10](#footnotes)|Model Y (with HW3) 2020-23[9](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Tesla A connector
- 1 USB-C coupler
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Tesla[10](#footnotes)|Model Y (with HW4) 2024-25[9](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Tesla B connector
- 1 USB-C coupler
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Toyota|Alphard 2019-20|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Toyota|Alphard Hybrid 2021|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Toyota|Avalon 2016|Toyota Safety Sense P|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Toyota|Avalon 2017-18|All|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Toyota|Avalon 2019-21|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Toyota|Avalon 2022|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Toyota|Avalon Hybrid 2019-21|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Toyota|Avalon Hybrid 2022|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Toyota|C-HR 2017-20|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Toyota|C-HR 2021|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Toyota|C-HR Hybrid 2017-20|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Toyota|C-HR Hybrid 2021-22|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Toyota|Camry 2018-20|All|Stock|0 mph[11](#footnotes)|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Toyota|Camry 2021-24|All|openpilot|0 mph[11](#footnotes)|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Toyota|Camry Hybrid 2018-20|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Toyota|Camry Hybrid 2021-24|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Toyota|Corolla 2017-19|All|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Toyota|Corolla 2020-22|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Toyota|Corolla Cross (Non-US only) 2020-23|All|openpilot|17 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Toyota|Corolla Cross Hybrid (Non-US only) 2020-22|All|openpilot|17 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Toyota|Corolla Hatchback 2019-22|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Toyota|Corolla Hybrid 2020-22|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Toyota|Corolla Hybrid (South America only) 2020-23|All|openpilot|17 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Toyota|Highlander 2017-19|All|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Toyota|Highlander 2020-23|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Toyota|Highlander Hybrid 2017-19|All|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Toyota|Highlander Hybrid 2020-23|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Toyota|Mirai 2021|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Toyota|Prius 2016|Toyota Safety Sense P|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Toyota|Prius 2017-20|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Toyota|Prius 2021-22|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Toyota|Prius Prime 2017-20|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Toyota|Prius Prime 2021-22|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Toyota|Prius v 2017|Toyota Safety Sense P|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Toyota|RAV4 2016|Toyota Safety Sense P|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Toyota|RAV4 2017-18|All|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Toyota|RAV4 2019-21|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Toyota|RAV4 2022|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Toyota|RAV4 2023-25|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Toyota|RAV4 Hybrid 2016|Toyota Safety Sense P|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Toyota|RAV4 Hybrid 2017-18|All|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Toyota|RAV4 Hybrid 2019-21|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Toyota|RAV4 Hybrid 2022|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Toyota|RAV4 Hybrid 2023-25|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Toyota|Sienna 2018-20|All|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Volkswagen|Arteon 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Arteon eHybrid 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Arteon R 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Arteon Shooting Brake 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Atlas 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Atlas Cross Sport 2020-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|California 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Caravelle 2020|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|CC 2018-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Crafter 2017-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|e-Crafter 2018-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|e-Golf 2014-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Golf 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Golf Alltrack 2015-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Golf GTD 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Golf GTE 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Golf GTI 2015-21|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Golf R 2015-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Golf SportsVan 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Grand California 2019-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Jetta 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Jetta GLI 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Passat 2015-22[13](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Passat Alltrack 2015-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Passat GTE 2015-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Polo 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
[16](#footnotes)||| +|Volkswagen|Polo GTI 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
[16](#footnotes)||| +|Volkswagen|T-Cross 2021|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
[16](#footnotes)||| +|Volkswagen|T-Roc 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Taos 2022-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Teramont 2018-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Teramont Cross Sport 2021-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Teramont X 2021-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Tiguan 2018-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Tiguan eHybrid 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Touran 2016-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| ### Footnotes 1openpilot Longitudinal Control (Alpha) is available behind a toggle; the toggle is only available in non-release branches such as `devel` or `nightly-dev`.
@@ -340,18 +340,17 @@ A supported vehicle is one that just works when you install a comma device. All 3Refers only to the Focus Mk4 (C519) available in Europe/China/Taiwan/Australasia, not the Focus Mk3 (C346) in North and South America/Southeast Asia.
4See more setup details for GM.
52019 Honda Civic 1.6L Diesel Sedan does not have ALC below 12mph.
-6Requires a CAN FD panda kit if not using comma 3X for this CAN FD car.
-7See more setup details for Nissan.
-8In the non-US market, openpilot requires the car to come equipped with EyeSight with Lane Keep Assistance.
-9Enabling longitudinal control (alpha) will disable all EyeSight functionality, including AEB, LDW, and RAB.
-10Some 2023 model years have HW4. To check which hardware type your vehicle has, look for Autopilot computer under Software -> Additional Vehicle Information on your vehicle's touchscreen. See this page for more information.
-11See more setup details for Tesla.
-12openpilot operates above 28mph for Camry 4CYL L, 4CYL LE and 4CYL SE which don't have Full-Speed Range Dynamic Radar Cruise Control.
-13Not including the China market Kamiq, which is based on the (currently) unsupported PQ34 platform.
-14Refers only to the MQB-based European B8 Passat, not the NMS Passat in the USA/China/Mideast markets.
-15Some Škoda vehicles are equipped with heated windshields, which are known to block GPS signal needed for some comma 3X functionality.
-16Only available for vehicles using a gateway (J533) harness. At this time, vehicles using a camera harness are limited to using stock ACC.
-17Model-years 2022 and beyond may have a combined CAN gateway and BCM, which is supported by openpilot in software, but doesn't yet have a harness available from the comma store.
+6See more setup details for Nissan.
+7In the non-US market, openpilot requires the car to come equipped with EyeSight with Lane Keep Assistance.
+8Enabling longitudinal control (alpha) will disable all EyeSight functionality, including AEB, LDW, and RAB.
+9Some 2023 model years have HW4. To check which hardware type your vehicle has, look for Autopilot computer under Software -> Additional Vehicle Information on your vehicle's touchscreen. See this page for more information.
+10See more setup details for Tesla.
+11openpilot operates above 28mph for Camry 4CYL L, 4CYL LE and 4CYL SE which don't have Full-Speed Range Dynamic Radar Cruise Control.
+12Not including the China market Kamiq, which is based on the (currently) unsupported PQ34 platform.
+13Refers only to the MQB-based European B8 Passat, not the NMS Passat in the USA/China/Mideast markets.
+14Some Škoda vehicles are equipped with heated windshields, which are known to block GPS signal needed for some comma four functionality.
+15Only available for vehicles using a gateway (J533) harness. At this time, vehicles using a camera harness are limited to using stock ACC.
+16Model-years 2022 and beyond may have a combined CAN gateway and BCM, which is supported by openpilot in software, but doesn't yet have a harness available from the comma store.
## Community Maintained Cars Although they're not upstream, the community has openpilot running on other makes and models. See the 'Community Supported Models' section of each make [on our wiki](https://wiki.comma.ai/). From d3532d7d6f84d410e4be0efb9cb61a4b60f08b96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Harald=20Sch=C3=A4fer?= Date: Fri, 28 Nov 2025 17:25:54 -0800 Subject: [PATCH 027/110] URLFile: catch more (#36712) * catch * linter has a point --- tools/lib/url_file.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tools/lib/url_file.py b/tools/lib/url_file.py index 31c1e0ff11..f988fa9db1 100644 --- a/tools/lib/url_file.py +++ b/tools/lib/url_file.py @@ -6,6 +6,8 @@ from hashlib import sha256 from urllib3 import PoolManager, Retry from urllib3.response import BaseHTTPResponse from urllib3.util import Timeout +from urllib3.exceptions import MaxRetryError + from openpilot.common.utils import atomic_write_in_dir from openpilot.system.hardware.hw import Paths @@ -61,7 +63,10 @@ class URLFile: pass def _request(self, method: str, url: str, headers: dict[str, str] | None = None) -> BaseHTTPResponse: - return URLFile.pool_manager().request(method, url, timeout=self._timeout, headers=headers) + try: + return URLFile.pool_manager().request(method, url, timeout=self._timeout, headers=headers) + except MaxRetryError as e: + raise URLFileException(f"Failed to {method} {url}: {e}") from e def get_length_online(self) -> int: response = self._request('HEAD', self._url) From c32e2898acfcee4e8f0b57d2bbe07e4f61d1d2ed Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 28 Nov 2025 23:29:13 -0800 Subject: [PATCH 028/110] mici: split wifi and network settings (#36715) * split * clean up * better --- .../mici/layouts/settings/network/__init__.py | 129 ++++++++++++++++++ .../{network.py => network/wifi_ui.py} | 125 +---------------- 2 files changed, 131 insertions(+), 123 deletions(-) create mode 100644 selfdrive/ui/mici/layouts/settings/network/__init__.py rename selfdrive/ui/mici/layouts/settings/{network.py => network/wifi_ui.py} (78%) diff --git a/selfdrive/ui/mici/layouts/settings/network/__init__.py b/selfdrive/ui/mici/layouts/settings/network/__init__.py new file mode 100644 index 0000000000..017dead1bb --- /dev/null +++ b/selfdrive/ui/mici/layouts/settings/network/__init__.py @@ -0,0 +1,129 @@ +import pyray as rl +from enum import IntEnum +from collections.abc import Callable + +from openpilot.system.ui.widgets.scroller import Scroller +from openpilot.selfdrive.ui.mici.layouts.settings.network.wifi_ui import WifiUIMici +from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigMultiToggle, BigToggle +from openpilot.selfdrive.ui.mici.widgets.dialog import BigInputDialog +from openpilot.system.ui.lib.application import gui_app +from openpilot.system.ui.widgets import NavWidget +from openpilot.system.ui.lib.wifi_manager import WifiManager, Network, MeteredType + + +class NetworkPanelType(IntEnum): + NONE = 0 + WIFI = 1 + + +class NetworkLayoutMici(NavWidget): + def __init__(self, back_callback: Callable): + super().__init__() + + self._current_panel = NetworkPanelType.WIFI + self.set_back_enabled(lambda: self._current_panel == NetworkPanelType.NONE) + + self._wifi_manager = WifiManager() + self._wifi_manager.set_active(False) + self._wifi_ui = WifiUIMici(self._wifi_manager, back_callback=lambda: self._switch_to_panel(NetworkPanelType.NONE)) + + self._wifi_manager.add_callbacks( + networks_updated=self._on_network_updated, + ) + + _tethering_icon = "icons_mici/settings/network/tethering.png" + + # ******** Tethering ******** + def tethering_toggle_callback(checked: bool): + self._tethering_toggle_btn.set_enabled(False) + self._network_metered_btn.set_enabled(False) + self._wifi_manager.set_tethering_active(checked) + + self._tethering_toggle_btn = BigToggle("enable tethering", "", toggle_callback=tethering_toggle_callback) + + def tethering_password_callback(password: str): + if password: + self._wifi_manager.set_tethering_password(password) + + def tethering_password_clicked(): + tethering_password = self._wifi_manager.tethering_password + dlg = BigInputDialog("enter password...", tethering_password, minimum_length=8, + confirm_callback=tethering_password_callback) + gui_app.set_modal_overlay(dlg) + + txt_tethering = gui_app.texture(_tethering_icon, 64, 53) + self._tethering_password_btn = BigButton("tethering password", "", txt_tethering) + self._tethering_password_btn.set_click_callback(tethering_password_clicked) + + # ******** IP Address ******** + self._ip_address_btn = BigButton("IP Address", "Not connected") + + # ******** Network Metered ******** + def network_metered_callback(value: str): + self._network_metered_btn.set_enabled(False) + metered = { + 'default': MeteredType.UNKNOWN, + 'metered': MeteredType.YES, + 'unmetered': MeteredType.NO + }.get(value, MeteredType.UNKNOWN) + self._wifi_manager.set_current_network_metered(metered) + + # 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) + self._network_metered_btn.set_enabled(False) + + wifi_button = BigButton("wi-fi") + wifi_button.set_click_callback(lambda: self._switch_to_panel(NetworkPanelType.WIFI)) + + # Main scroller ---------------------------------- + self._scroller = Scroller([ + wifi_button, + self._network_metered_btn, + self._tethering_toggle_btn, + self._tethering_password_btn, + self._ip_address_btn, + ], snap_items=False) + + # Set up back navigation + self.set_back_callback(back_callback) + + def show_event(self): + super().show_event() + self._current_panel = NetworkPanelType.NONE + self._wifi_ui.show_event() + self._scroller.show_event() + + def hide_event(self): + super().hide_event() + self._wifi_ui.hide_event() + + def _on_network_updated(self, networks: list[Network]): + # Update tethering state + tethering_active = self._wifi_manager.is_tethering_active() + # TODO: use real signals (like activated/settings changed, etc.) to speed up re-enabling buttons + self._tethering_toggle_btn.set_enabled(True) + self._network_metered_btn.set_enabled(lambda: not tethering_active and bool(self._wifi_manager.ipv4_address)) + self._tethering_toggle_btn.set_checked(tethering_active) + + # Update IP address + self._ip_address_btn.set_value(self._wifi_manager.ipv4_address or "Not connected") + + # Update network metered + self._network_metered_btn.set_value( + { + MeteredType.UNKNOWN: 'default', + MeteredType.YES: 'metered', + MeteredType.NO: 'unmetered' + }.get(self._wifi_manager.current_network_metered, 'default')) + + def _switch_to_panel(self, panel_type: NetworkPanelType): + self._current_panel = panel_type + + def _render(self, rect: rl.Rectangle): + self._wifi_manager.process_callbacks() + + if self._current_panel == NetworkPanelType.WIFI: + self._wifi_ui.render(rect) + else: + self._scroller.render(rect) diff --git a/selfdrive/ui/mici/layouts/settings/network.py b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py similarity index 78% rename from selfdrive/ui/mici/layouts/settings/network.py rename to selfdrive/ui/mici/layouts/settings/network/wifi_ui.py index a62c1d153a..2ab46d1695 100644 --- a/selfdrive/ui/mici/layouts/settings/network.py +++ b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py @@ -1,28 +1,20 @@ import math import numpy as np import pyray as rl -from enum import IntEnum from collections.abc import Callable from openpilot.common.swaglog import cloudlog -from openpilot.system.ui.widgets.scroller import Scroller from openpilot.system.ui.widgets.label import UnifiedLabel -from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigMultiToggle, BigToggle from openpilot.selfdrive.ui.mici.widgets.dialog import BigMultiOptionDialog, BigInputDialog, BigDialogOptionButton, BigConfirmationDialogV2 from openpilot.system.ui.lib.application import gui_app, MousePos, FontWeight from openpilot.system.ui.widgets import Widget, NavWidget -from openpilot.system.ui.lib.wifi_manager import WifiManager, Network, SecurityType, MeteredType +from openpilot.system.ui.lib.wifi_manager import WifiManager, Network, SecurityType def normalize_ssid(ssid: str) -> str: return ssid.replace("’", "'") # for iPhone hotspots -class NetworkPanelType(IntEnum): - NONE = 0 - WIFI = 1 - - class LoadingAnimation(Widget): def _render(self, _): cx = int(self._rect.x + 70) @@ -392,7 +384,7 @@ class WifiUIMici(BigMultiOptionDialog): self._network_info_page.set_current_network(_network) self._should_open_network_info_page = True - network_button.set_click_callback(lambda _net=network,_button=network_button: _button._selected and show_network_info_page(_net)) + network_button.set_click_callback(lambda _net=network, _button=network_button: _button._selected and show_network_info_page(_net)) self.add_button(network_button) @@ -443,116 +435,3 @@ class WifiUIMici(BigMultiOptionDialog): if not self._networks: self._loading_animation.render(self._rect) - - -class NetworkLayoutMici(NavWidget): - def __init__(self, back_callback: Callable): - super().__init__() - - self._current_panel = NetworkPanelType.WIFI - self.set_back_enabled(lambda: self._current_panel == NetworkPanelType.NONE) - - self._wifi_manager = WifiManager() - self._wifi_manager.set_active(False) - self._wifi_ui = WifiUIMici(self._wifi_manager, back_callback=lambda: self._switch_to_panel(NetworkPanelType.NONE)) - - self._wifi_manager.add_callbacks( - networks_updated=self._on_network_updated, - ) - - _tethering_icon = "icons_mici/settings/network/tethering.png" - - # ******** Tethering ******** - def tethering_toggle_callback(checked: bool): - self._tethering_toggle_btn.set_enabled(False) - self._network_metered_btn.set_enabled(False) - self._wifi_manager.set_tethering_active(checked) - - self._tethering_toggle_btn = BigToggle("enable tethering", "", toggle_callback=tethering_toggle_callback) - - def tethering_password_callback(password: str): - if password: - self._wifi_manager.set_tethering_password(password) - - def tethering_password_clicked(): - tethering_password = self._wifi_manager.tethering_password - dlg = BigInputDialog("enter password...", tethering_password, minimum_length=8, - confirm_callback=tethering_password_callback) - gui_app.set_modal_overlay(dlg) - - txt_tethering = gui_app.texture(_tethering_icon, 64, 53) - self._tethering_password_btn = BigButton("tethering password", "", txt_tethering) - self._tethering_password_btn.set_click_callback(tethering_password_clicked) - - # ******** IP Address ******** - self._ip_address_btn = BigButton("IP Address", "Not connected") - - # ******** Network Metered ******** - def network_metered_callback(value: str): - self._network_metered_btn.set_enabled(False) - metered = { - 'default': MeteredType.UNKNOWN, - 'metered': MeteredType.YES, - 'unmetered': MeteredType.NO - }.get(value, MeteredType.UNKNOWN) - self._wifi_manager.set_current_network_metered(metered) - - # 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) - self._network_metered_btn.set_enabled(False) - - wifi_button = BigButton("wi-fi") - wifi_button.set_click_callback(lambda: self._switch_to_panel(NetworkPanelType.WIFI)) - - # Main scroller ---------------------------------- - self._scroller = Scroller([ - wifi_button, - self._network_metered_btn, - self._tethering_toggle_btn, - self._tethering_password_btn, - self._ip_address_btn, - ], snap_items=False) - - # Set up back navigation - self.set_back_callback(back_callback) - - def show_event(self): - super().show_event() - self._current_panel = NetworkPanelType.NONE - self._wifi_ui.show_event() - self._scroller.show_event() - - def hide_event(self): - super().hide_event() - self._wifi_ui.hide_event() - - def _on_network_updated(self, networks: list[Network]): - # Update tethering state - tethering_active = self._wifi_manager.is_tethering_active() - # TODO: use real signals (like activated/settings changed, etc.) to speed up re-enabling buttons - self._tethering_toggle_btn.set_enabled(True) - self._network_metered_btn.set_enabled(lambda: not tethering_active and bool(self._wifi_manager.ipv4_address)) - self._tethering_toggle_btn.set_checked(tethering_active) - - # Update IP address - self._ip_address_btn.set_value(self._wifi_manager.ipv4_address or "Not connected") - - # Update network metered - self._network_metered_btn.set_value( - { - MeteredType.UNKNOWN: 'default', - MeteredType.YES: 'metered', - MeteredType.NO: 'unmetered' - }.get(self._wifi_manager.current_network_metered, 'default')) - - def _switch_to_panel(self, panel_type: NetworkPanelType): - self._current_panel = panel_type - - def _render(self, rect: rl.Rectangle): - self._wifi_manager.process_callbacks() - - if self._current_panel == NetworkPanelType.WIFI: - self._wifi_ui.render(rect) - else: - self._scroller.render(rect) From 65f18c363be1bae0f9835d38a01eecab64c7e6ef Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 28 Nov 2025 23:41:55 -0800 Subject: [PATCH 029/110] Mici advanced network settings (#36716) * add back * forgot * clean up --- .../mici/layouts/settings/network/__init__.py | 53 ++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/selfdrive/ui/mici/layouts/settings/network/__init__.py b/selfdrive/ui/mici/layouts/settings/network/__init__.py index 017dead1bb..8c27d87d4e 100644 --- a/selfdrive/ui/mici/layouts/settings/network/__init__.py +++ b/selfdrive/ui/mici/layouts/settings/network/__init__.py @@ -4,8 +4,10 @@ from collections.abc import Callable from openpilot.system.ui.widgets.scroller import Scroller from openpilot.selfdrive.ui.mici.layouts.settings.network.wifi_ui import WifiUIMici -from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigMultiToggle, BigToggle +from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigMultiToggle, BigToggle, BigParamControl from openpilot.selfdrive.ui.mici.widgets.dialog import BigInputDialog +from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.selfdrive.ui.lib.prime_state import PrimeType from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.widgets import NavWidget from openpilot.system.ui.lib.wifi_manager import WifiManager, Network, MeteredType @@ -76,18 +78,47 @@ class NetworkLayoutMici(NavWidget): wifi_button = BigButton("wi-fi") wifi_button.set_click_callback(lambda: self._switch_to_panel(NetworkPanelType.WIFI)) + # ******** Advanced settings ******** + # ******** Roaming toggle ******** + self._roaming_btn = BigParamControl("enable roaming", "GsmRoaming", toggle_callback=self._toggle_roaming) + + # ******** APN settings ******** + self._apn_btn = BigButton("apn settings", "edit") + self._apn_btn.set_click_callback(self._edit_apn) + + # ******** Cellular metered toggle ******** + self._cellular_metered_btn = BigParamControl("cellular metered", "GsmMetered", toggle_callback=self._toggle_cellular_metered) + # Main scroller ---------------------------------- self._scroller = Scroller([ wifi_button, self._network_metered_btn, self._tethering_toggle_btn, self._tethering_password_btn, + # /* Advanced settings + self._roaming_btn, + self._apn_btn, + self._cellular_metered_btn, + # */ self._ip_address_btn, ], snap_items=False) + # Set initial config + roaming_enabled = ui_state.params.get_bool("GsmRoaming") + metered = ui_state.params.get_bool("GsmMetered") + self._wifi_manager.update_gsm_settings(roaming_enabled, ui_state.params.get("GsmApn") or "", metered) + # Set up back navigation self.set_back_callback(back_callback) + def _update_state(self): + # If not using prime SIM, show GSM settings and enable IPv4 forwarding + show_cell_settings = ui_state.prime_state.get_type() in (PrimeType.NONE, PrimeType.LITE) + self._wifi_manager.set_ipv4_forward(show_cell_settings) + self._roaming_btn.set_visible(show_cell_settings) + self._apn_btn.set_visible(show_cell_settings) + self._cellular_metered_btn.set_visible(show_cell_settings) + def show_event(self): super().show_event() self._current_panel = NetworkPanelType.NONE @@ -98,6 +129,26 @@ class NetworkLayoutMici(NavWidget): super().hide_event() self._wifi_ui.hide_event() + def _toggle_roaming(self, checked: bool): + self._wifi_manager.update_gsm_settings(checked, ui_state.params.get("GsmApn") or "", ui_state.params.get_bool("GsmMetered")) + + def _edit_apn(self): + def update_apn(apn: str): + apn = apn.strip() + if apn == "": + ui_state.params.remove("GsmApn") + else: + ui_state.params.put("GsmApn", apn) + + self._wifi_manager.update_gsm_settings(ui_state.params.get_bool("GsmRoaming"), apn, ui_state.params.get_bool("GsmMetered")) + + current_apn = ui_state.params.get("GsmApn") or "" + dlg = BigInputDialog("enter APN", current_apn, minimum_length=0, confirm_callback=update_apn) + gui_app.set_modal_overlay(dlg) + + def _toggle_cellular_metered(self, checked: bool): + self._wifi_manager.update_gsm_settings(ui_state.params.get_bool("GsmRoaming"), ui_state.params.get("GsmApn") or "", checked) + def _on_network_updated(self, networks: list[Network]): # Update tethering state tethering_active = self._wifi_manager.is_tethering_active() From d8c316faef4bf26f5e433ff480cad8cce7f7fbc2 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Sat, 29 Nov 2025 00:56:43 -0800 Subject: [PATCH 030/110] Fix wifi settings NavWidget --- selfdrive/ui/mici/layouts/settings/network/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/selfdrive/ui/mici/layouts/settings/network/__init__.py b/selfdrive/ui/mici/layouts/settings/network/__init__.py index 8c27d87d4e..d085fdf55f 100644 --- a/selfdrive/ui/mici/layouts/settings/network/__init__.py +++ b/selfdrive/ui/mici/layouts/settings/network/__init__.py @@ -112,6 +112,8 @@ class NetworkLayoutMici(NavWidget): self.set_back_callback(back_callback) def _update_state(self): + super()._update_state() + # If not using prime SIM, show GSM settings and enable IPv4 forwarding show_cell_settings = ui_state.prime_state.get_type() in (PrimeType.NONE, PrimeType.LITE) self._wifi_manager.set_ipv4_forward(show_cell_settings) From d6de3572cad975cb24bf64c8978f3521e88d36cb Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Sat, 29 Nov 2025 01:26:17 -0800 Subject: [PATCH 031/110] UnifiedLabel: split render (#36719) * split * rect --- system/ui/widgets/label.py | 85 ++++++++++++++++++++------------------ 1 file changed, 44 insertions(+), 41 deletions(-) diff --git a/system/ui/widgets/label.py b/system/ui/widgets/label.py index c90b111de3..432f21e598 100644 --- a/system/ui/widgets/label.py +++ b/system/ui/widgets/label.py @@ -599,13 +599,13 @@ class UnifiedLabel(Widget): return self._cached_total_height return 0.0 - def _render(self, rect: rl.Rectangle): + def _render(self, _): """Render the label.""" - if rect.width <= 0 or rect.height <= 0: + if self._rect.width <= 0 or self._rect.height <= 0: return # Determine available width - available_width = rect.width + available_width = self._rect.width if self._max_width is not None: available_width = min(available_width, self._max_width) @@ -633,7 +633,7 @@ class UnifiedLabel(Widget): line_height_needed = size.y * self._line_height # Check if this line fits - if current_height + line_height_needed > rect.height: + if current_height + line_height_needed > self._rect.height: # This line doesn't fit if len(visible_lines) == 0: # First line doesn't fit by height - still show it (will be clipped by scissor if needed) @@ -677,51 +677,54 @@ class UnifiedLabel(Widget): # Calculate vertical alignment offset if self._alignment_vertical == rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP: - start_y = rect.y + start_y = self._rect.y elif self._alignment_vertical == rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM: - start_y = rect.y + rect.height - total_visible_height + start_y = self._rect.y + self._rect.height - total_visible_height else: # TEXT_ALIGN_MIDDLE - start_y = rect.y + (rect.height - total_visible_height) / 2 + start_y = self._rect.y + (self._rect.height - total_visible_height) / 2 # Render each line current_y = start_y for idx, (line, size, emojis) in enumerate(zip(visible_lines, visible_sizes, visible_emojis, strict=True)): - # Calculate horizontal position - if self._alignment == rl.GuiTextAlignment.TEXT_ALIGN_LEFT: - line_x = rect.x + self._text_padding - elif self._alignment == rl.GuiTextAlignment.TEXT_ALIGN_CENTER: - line_x = rect.x + (rect.width - size.x) / 2 - elif self._alignment == rl.GuiTextAlignment.TEXT_ALIGN_RIGHT: - line_x = rect.x + rect.width - size.x - self._text_padding - else: - line_x = rect.x + self._text_padding - - # Render line with emojis - line_pos = rl.Vector2(line_x, current_y) - prev_index = 0 - - for start, end, emoji in emojis: - # Draw text before emoji - text_before = line[prev_index:start] - if text_before: - rl.draw_text_ex(self._font, text_before, line_pos, self._font_size, self._spacing_pixels, self._text_color) - width_before = measure_text_cached(self._font, text_before, self._font_size, self._spacing_pixels) - line_pos.x += width_before.x - - # Draw emoji - tex = emoji_tex(emoji) - emoji_scale = self._font_size / tex.height * FONT_SCALE - rl.draw_texture_ex(tex, line_pos, 0.0, emoji_scale, self._text_color) - # Emoji width is font_size * FONT_SCALE (as per measure_text_cached) - line_pos.x += self._font_size * FONT_SCALE - prev_index = end - - # Draw remaining text after last emoji - text_after = line[prev_index:] - if text_after: - rl.draw_text_ex(self._font, text_after, line_pos, self._font_size, self._spacing_pixels, self._text_color) + self._render_line(line, size, emojis, current_y) # Move to next line (if not last line) if idx < len(visible_lines) - 1: # Use current line's height * line_height for spacing to next line current_y += size.y * self._line_height + + def _render_line(self, line, size, emojis, current_y): + # Calculate horizontal position + if self._alignment == rl.GuiTextAlignment.TEXT_ALIGN_LEFT: + line_x = self._rect.x + self._text_padding + elif self._alignment == rl.GuiTextAlignment.TEXT_ALIGN_CENTER: + line_x = self._rect.x + (self._rect.width - size.x) / 2 + elif self._alignment == rl.GuiTextAlignment.TEXT_ALIGN_RIGHT: + line_x = self._rect.x + self._rect.width - size.x - self._text_padding + else: + line_x = self._rect.x + self._text_padding + + # Render line with emojis + line_pos = rl.Vector2(line_x, current_y) + prev_index = 0 + + for start, end, emoji in emojis: + # Draw text before emoji + text_before = line[prev_index:start] + if text_before: + rl.draw_text_ex(self._font, text_before, line_pos, self._font_size, self._spacing_pixels, self._text_color) + width_before = measure_text_cached(self._font, text_before, self._font_size, self._spacing_pixels) + line_pos.x += width_before.x + + # Draw emoji + tex = emoji_tex(emoji) + emoji_scale = self._font_size / tex.height * FONT_SCALE + rl.draw_texture_ex(tex, line_pos, 0.0, emoji_scale, self._text_color) + # Emoji width is font_size * FONT_SCALE (as per measure_text_cached) + line_pos.x += self._font_size * FONT_SCALE + prev_index = end + + # Draw remaining text after last emoji + text_after = line[prev_index:] + if text_after: + rl.draw_text_ex(self._font, text_after, line_pos, self._font_size, self._spacing_pixels, self._text_color) From cb718618d14b5b46aa2698d17194099ecd417de6 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Sat, 29 Nov 2025 02:12:09 -0800 Subject: [PATCH 032/110] fix multi option dialog text centering --- selfdrive/ui/mici/widgets/dialog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selfdrive/ui/mici/widgets/dialog.py b/selfdrive/ui/mici/widgets/dialog.py index d64ab65ef2..950d71319f 100644 --- a/selfdrive/ui/mici/widgets/dialog.py +++ b/selfdrive/ui/mici/widgets/dialog.py @@ -282,7 +282,7 @@ class BigDialogOptionButton(Widget): self._selected = False self._label = UnifiedLabel(option, font_size=70, text_color=rl.Color(255, 255, 255, int(255 * 0.58)), - font_weight=FontWeight.DISPLAY_REGULAR, alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP) + font_weight=FontWeight.DISPLAY_REGULAR, alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE) def set_selected(self, selected: bool): self._selected = selected From 088fc1cab1f4ccb6e2ba229e8cb3ef5a17fe0be8 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Sat, 29 Nov 2025 02:15:10 -0800 Subject: [PATCH 033/110] Unified label: add scrolling (#36717) * almost * works! * clean up * fix * trash * Revert "trash" This reverts commit 951d63382810d444fe08103f406a8c490cfcbe25. * fix some bugs and use * clean up * clean up * fix clipping * clean up * fix --- selfdrive/ui/mici/layouts/home.py | 6 +- .../mici/layouts/settings/network/wifi_ui.py | 6 +- selfdrive/ui/mici/widgets/dialog.py | 3 +- system/ui/widgets/label.py | 67 ++++++++++++++++++- 4 files changed, 76 insertions(+), 6 deletions(-) diff --git a/selfdrive/ui/mici/layouts/home.py b/selfdrive/ui/mici/layouts/home.py index 6102265a87..9152bdc7fa 100644 --- a/selfdrive/ui/mici/layouts/home.py +++ b/selfdrive/ui/mici/layouts/home.py @@ -3,7 +3,7 @@ import time from cereal import log import pyray as rl from collections.abc import Callable -from openpilot.system.ui.widgets.label import gui_label, MiciLabel +from openpilot.system.ui.widgets.label import gui_label, MiciLabel, UnifiedLabel from openpilot.system.ui.widgets import Widget from openpilot.system.ui.lib.application import gui_app, FontWeight, DEFAULT_TEXT_COLOR, MousePos from openpilot.selfdrive.ui.ui_state import ui_state @@ -113,7 +113,7 @@ class MiciHomeLayout(Widget): self._version_label = MiciLabel("", font_size=36, font_weight=FontWeight.ROMAN) self._large_version_label = MiciLabel("", font_size=64, color=rl.GRAY, font_weight=FontWeight.ROMAN) self._date_label = MiciLabel("", font_size=36, color=rl.GRAY, font_weight=FontWeight.ROMAN) - self._branch_label = MiciLabel("", font_size=36, color=rl.GRAY, font_weight=FontWeight.ROMAN, elide_right=False, scroll=True) + self._branch_label = UnifiedLabel("", font_size=36, text_color=rl.GRAY, font_weight=FontWeight.ROMAN, scroll=True) self._version_commit_label = MiciLabel("", font_size=36, color=rl.GRAY, font_weight=FontWeight.ROMAN) def show_event(self): @@ -195,7 +195,7 @@ class MiciHomeLayout(Widget): self._date_label.set_position(version_pos.x + self._version_label.rect.width + 10, version_pos.y) self._date_label.render() - self._branch_label.set_width(gui_app.width - self._version_label.rect.width - self._date_label.rect.width - 32) + self._branch_label.set_max_width(gui_app.width - self._version_label.rect.width - self._date_label.rect.width - 32) self._branch_label.set_text(" " + ("release" if release_branch else self._version_text[1])) self._branch_label.set_position(version_pos.x + self._version_label.rect.width + self._date_label.rect.width + 20, version_pos.y) self._branch_label.render() diff --git a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py index 2ab46d1695..eec16d884f 100644 --- a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py +++ b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py @@ -207,7 +207,7 @@ class NetworkInfoPage(NavWidget): self._connect_btn.set_click_callback(lambda: connect_callback(self._network.ssid) if self._network is not None else None) self._title = UnifiedLabel("", 64, FontWeight.DISPLAY, rl.Color(255, 255, 255, int(255 * 0.9)), - alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE) + alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE, scroll=True) self._subtitle = UnifiedLabel("", 36, FontWeight.ROMAN, rl.Color(255, 255, 255, int(255 * 0.9 * 0.65)), alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE) @@ -217,6 +217,10 @@ class NetworkInfoPage(NavWidget): self._network: Network | None = None self._connecting: Callable[[], str | None] | None = None + def show_event(self): + super().show_event() + self._title.reset_scroll() + def update_networks(self, networks: dict[str, Network]): # update current network from latest scan results for ssid, network in networks.items(): diff --git a/selfdrive/ui/mici/widgets/dialog.py b/selfdrive/ui/mici/widgets/dialog.py index 950d71319f..b11056f993 100644 --- a/selfdrive/ui/mici/widgets/dialog.py +++ b/selfdrive/ui/mici/widgets/dialog.py @@ -282,7 +282,8 @@ class BigDialogOptionButton(Widget): self._selected = False self._label = UnifiedLabel(option, font_size=70, text_color=rl.Color(255, 255, 255, int(255 * 0.58)), - font_weight=FontWeight.DISPLAY_REGULAR, alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE) + font_weight=FontWeight.DISPLAY_REGULAR, alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE, + scroll=True) def set_selected(self, selected: bool): self._selected = selected diff --git a/system/ui/widgets/label.py b/system/ui/widgets/label.py index 432f21e598..fd0516a986 100644 --- a/system/ui/widgets/label.py +++ b/system/ui/widgets/label.py @@ -412,6 +412,7 @@ class UnifiedLabel(Widget): max_width: int | None = None, elide: bool = True, wrap_text: bool = True, + scroll: bool = False, line_height: float = 1.0, letter_spacing: float = 0.0): super().__init__() @@ -426,10 +427,23 @@ class UnifiedLabel(Widget): self._max_width = max_width self._elide = elide self._wrap_text = wrap_text + self._scroll = scroll self._line_height = line_height * 0.9 self._letter_spacing = letter_spacing # 0.1 = 10% self._spacing_pixels = font_size * letter_spacing + # Scroll state + self._scroll = scroll + self._needs_scroll = False + self._scroll_offset = 0 + self._scroll_pause_t: float | None = None + self._scroll_state: ScrollState = ScrollState.STARTING + + # Scroll mode does not support eliding or multiline wrapping + if self._scroll: + self._elide = False + self._wrap_text = False + # Cached data self._cached_text: str | None = None self._cached_wrapped_lines: list[str] = [] @@ -490,6 +504,12 @@ class UnifiedLabel(Widget): """Update the vertical text alignment.""" self._alignment_vertical = alignment_vertical + def reset_scroll(self): + """Reset scroll state to initial position.""" + self._scroll_offset = 0 + self._scroll_pause_t = None + self._scroll_state = ScrollState.STARTING + def set_max_width(self, max_width: int | None): """Set the maximum width constraint for wrapping/eliding.""" if self._max_width != max_width: @@ -528,6 +548,9 @@ class UnifiedLabel(Widget): # Elide lines if needed (for width constraint) self._cached_wrapped_lines = [self._elide_line(line, content_width) for line in self._cached_wrapped_lines] + if self._scroll: + self._cached_wrapped_lines = self._cached_wrapped_lines[:1] # Only first line for scrolling + # Process each line: measure and find emojis self._cached_line_sizes = [] self._cached_line_emojis = [] @@ -540,6 +563,11 @@ class UnifiedLabel(Widget): size = rl.Vector2(0, self._font_size * FONT_SCALE) else: size = measure_text_cached(self._font, line, self._font_size, self._spacing_pixels) + + # This is the only line + if self._scroll: + self._needs_scroll = size.x > content_width + self._cached_line_sizes.append(size) # Calculate total height @@ -683,17 +711,53 @@ class UnifiedLabel(Widget): else: # TEXT_ALIGN_MIDDLE start_y = self._rect.y + (self._rect.height - total_visible_height) / 2 + # Only scissor when we know there is a single scrolling line + if self._needs_scroll: + rl.begin_scissor_mode(int(self._rect.x), int(self._rect.y), int(self._rect.width), int(self._rect.height)) + # Render each line current_y = start_y for idx, (line, size, emojis) in enumerate(zip(visible_lines, visible_sizes, visible_emojis, strict=True)): + if self._needs_scroll: + if self._scroll_state == ScrollState.STARTING: + if self._scroll_pause_t is None: + self._scroll_pause_t = rl.get_time() + 2.0 + if rl.get_time() >= self._scroll_pause_t: + self._scroll_state = ScrollState.SCROLLING + self._scroll_pause_t = None + + elif self._scroll_state == ScrollState.SCROLLING: + self._scroll_offset -= 0.8 / 60. * gui_app.target_fps + # don't fully hide + if self._scroll_offset <= -size.x - self._rect.width / 3: + self._scroll_offset = 0 + self._scroll_state = ScrollState.STARTING + self._scroll_pause_t = None + else: + self.reset_scroll() + self._render_line(line, size, emojis, current_y) + # Draw 2nd instance for scrolling + if self._needs_scroll and self._scroll_state != ScrollState.STARTING: + text2_scroll_offset = size.x + self._rect.width / 3 + self._render_line(line, size, emojis, current_y, text2_scroll_offset) + # Move to next line (if not last line) if idx < len(visible_lines) - 1: # Use current line's height * line_height for spacing to next line current_y += size.y * self._line_height - def _render_line(self, line, size, emojis, current_y): + if self._needs_scroll: + # draw black fade on left and right + fade_width = 20 + rl.draw_rectangle_gradient_h(int(self._rect.x + self._rect.width - fade_width), int(self._rect.y), fade_width, int(self._rect.height), rl.BLANK, rl.BLACK) + if self._scroll_state != ScrollState.STARTING: + rl.draw_rectangle_gradient_h(int(self._rect.x), int(self._rect.y), fade_width, int(self._rect.height), rl.BLACK, rl.BLANK) + + rl.end_scissor_mode() + + def _render_line(self, line, size, emojis, current_y, x_offset=0.0): # Calculate horizontal position if self._alignment == rl.GuiTextAlignment.TEXT_ALIGN_LEFT: line_x = self._rect.x + self._text_padding @@ -703,6 +767,7 @@ class UnifiedLabel(Widget): line_x = self._rect.x + self._rect.width - size.x - self._text_padding else: line_x = self._rect.x + self._text_padding + line_x += self._scroll_offset + x_offset # Render line with emojis line_pos = rl.Vector2(line_x, current_y) From 22003fd10a48bc0f35c65f40fdec5977f2a362c2 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Sat, 29 Nov 2025 02:15:38 -0800 Subject: [PATCH 034/110] rl.BLANK --- selfdrive/ui/mici/onroad/hud_renderer.py | 2 +- system/ui/widgets/label.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/selfdrive/ui/mici/onroad/hud_renderer.py b/selfdrive/ui/mici/onroad/hud_renderer.py index 524eb11637..cb9f7c6fcf 100644 --- a/selfdrive/ui/mici/onroad/hud_renderer.py +++ b/selfdrive/ui/mici/onroad/hud_renderer.py @@ -233,7 +233,7 @@ class HudRenderer(Widget): # draw drop shadow circle_radius = 162 // 2 rl.draw_circle_gradient(int(x + circle_radius), int(y + circle_radius), circle_radius, - rl.Color(0, 0, 0, int(255 / 2 * alpha)), rl.Color(0, 0, 0, 0)) + rl.Color(0, 0, 0, int(255 / 2 * alpha)), rl.BLANK) set_speed_color = rl.Color(255, 255, 255, int(255 * 0.9 * alpha)) max_color = rl.Color(255, 255, 255, int(255 * 0.9 * alpha)) diff --git a/system/ui/widgets/label.py b/system/ui/widgets/label.py index fd0516a986..91f05c3551 100644 --- a/system/ui/widgets/label.py +++ b/system/ui/widgets/label.py @@ -179,9 +179,9 @@ class MiciLabel(Widget): if self._needs_scroll: # draw black fade on left and right fade_width = 20 - rl.draw_rectangle_gradient_h(int(rect.x + rect.width - fade_width), int(rect.y), fade_width, int(rect.height), rl.Color(0, 0, 0, 0), rl.BLACK) + rl.draw_rectangle_gradient_h(int(rect.x + rect.width - fade_width), int(rect.y), fade_width, int(rect.height), rl.BLANK, rl.BLACK) if self._scroll_state != ScrollState.STARTING: - rl.draw_rectangle_gradient_h(int(rect.x), int(rect.y), fade_width, int(rect.height), rl.BLACK, rl.Color(0, 0, 0, 0)) + rl.draw_rectangle_gradient_h(int(rect.x), int(rect.y), fade_width, int(rect.height), rl.BLACK, rl.BLANK) rl.end_scissor_mode() From 6c39f6bb53466f2e284dfff5d9e44d634855ac52 Mon Sep 17 00:00:00 2001 From: Dean Lee Date: Sat, 29 Nov 2025 18:21:15 +0800 Subject: [PATCH 035/110] ui: Fix scroll logic for non-scrollable content (bounds_size > content_size) to prevent jitter (#36693) * Fix scroll logic for non-scrollable content to prevent jitter * one thing --------- Co-authored-by: Shane Smiskol --- system/ui/lib/scroll_panel2.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/system/ui/lib/scroll_panel2.py b/system/ui/lib/scroll_panel2.py index ab93be9453..00ef95cc8b 100644 --- a/system/ui/lib/scroll_panel2.py +++ b/system/ui/lib/scroll_panel2.py @@ -67,16 +67,18 @@ class GuiScrollPanel2: print() return self.get_offset() + def _get_offset_bounds(self, bounds_size: float, content_size: float) -> tuple[float, float]: + """Returns (max_offset, min_offset) for the given bounds and content size.""" + return 0.0, min(0.0, bounds_size - content_size) + def _update_state(self, bounds_size: float, content_size: float) -> None: """Runs per render frame, independent of mouse events. Updates auto-scrolling state and velocity.""" if self._state == ScrollState.AUTO_SCROLL: + max_offset, min_offset = self._get_offset_bounds(bounds_size, content_size) # simple exponential return if out of bounds - out_of_bounds = self.get_offset() > 0 or self.get_offset() < (bounds_size - content_size) + out_of_bounds = self.get_offset() > max_offset or self.get_offset() < min_offset if out_of_bounds and self._handle_out_of_bounds: - if self.get_offset() < (bounds_size - content_size): # too far right - target = bounds_size - content_size - else: # too far left - target = 0.0 + target = max_offset if self.get_offset() > max_offset else min_offset dt = rl.get_frame_time() or 1e-6 factor = 1.0 - math.exp(-BOUNCE_RETURN_RATE * dt) @@ -103,7 +105,9 @@ class GuiScrollPanel2: def _handle_mouse_event(self, mouse_event: MouseEvent, bounds: rl.Rectangle, bounds_size: float, content_size: float) -> None: - out_of_bounds = self.get_offset() > 0 or self.get_offset() < (bounds_size - content_size) + max_offset, min_offset = self._get_offset_bounds(bounds_size, content_size) + # simple exponential return if out of bounds + out_of_bounds = self.get_offset() > max_offset or self.get_offset() < min_offset if DEBUG: print('Mouse event:', mouse_event) From 1b20567c986b4c02bc89872dec7a5e0fa6436006 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Sat, 29 Nov 2025 02:30:32 -0800 Subject: [PATCH 036/110] Mici keyboard: alpha filter for key bg (#36720) * filter * tune * fix --- system/ui/widgets/mici_keyboard.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/system/ui/widgets/mici_keyboard.py b/system/ui/widgets/mici_keyboard.py index a4f4c7d09b..7459dc5731 100644 --- a/system/ui/widgets/mici_keyboard.py +++ b/system/ui/widgets/mici_keyboard.py @@ -4,7 +4,7 @@ import numpy as np from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos, MouseEvent from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.widgets import Widget -from openpilot.common.filter_simple import BounceFilter +from openpilot.common.filter_simple import BounceFilter, FirstOrderFilter CHAR_FONT_SIZE = 42 CHAR_NEAR_FONT_SIZE = CHAR_FONT_SIZE * 2 @@ -204,6 +204,7 @@ class MiciKeyboard(Widget): self._text: str = "" self._bg_scale_filter = BounceFilter(1.0, 0.1 * ANIMATION_SCALE, 1 / gui_app.target_fps) + self._selected_key_filter = FirstOrderFilter(0.0, 0.075 * ANIMATION_SCALE, 1 / gui_app.target_fps) def get_candidate_character(self) -> str: # return str of character about to be added to text @@ -309,6 +310,9 @@ class MiciKeyboard(Widget): self._text += ' ' def _update_state(self): + # update selected key filter + self._selected_key_filter.update(self._closest_key[0] is not None) + # unselect key after animation plays if self._unselect_key_t is not None and rl.get_time() > self._unselect_key_t: self._closest_key = (None, float('inf')) @@ -335,8 +339,9 @@ class MiciKeyboard(Widget): key.set_font_size(SELECTED_CHAR_FONT_SIZE) # draw black circle behind selected key + circle_alpha = int(self._selected_key_filter.x * 225) 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, 225), rl.BLANK) + 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 f1c2b1df7f8f9854d4602ced70847b49af8bad5e Mon Sep 17 00:00:00 2001 From: Dean Lee Date: Sat, 29 Nov 2025 19:14:23 +0800 Subject: [PATCH 037/110] ui: fix CameraView crash in mici due to stale frame (#36710) fix CameraView crash caused by stale frame --- selfdrive/ui/mici/onroad/cameraview.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/selfdrive/ui/mici/onroad/cameraview.py b/selfdrive/ui/mici/onroad/cameraview.py index 0f425b10da..995c4618f7 100644 --- a/selfdrive/ui/mici/onroad/cameraview.py +++ b/selfdrive/ui/mici/onroad/cameraview.py @@ -198,6 +198,8 @@ class CameraView(Widget): if self.shader and self.shader.id: rl.unload_shader(self.shader) + self.frame = None + self.available_streams.clear() self.client = None def __del__(self): @@ -234,6 +236,9 @@ class CameraView(Widget): if buffer: self._texture_needs_update = True self.frame = buffer + elif not self.client.is_connected(): + # ensure we clear the displayed frame when the connection is lost + self.frame = None if not self.frame: self._draw_placeholder(rect) From 8de89463741b648f7cd2a625b0e688005168388e Mon Sep 17 00:00:00 2001 From: Dean Lee Date: Sat, 29 Nov 2025 19:15:41 +0800 Subject: [PATCH 038/110] ui: skip _draw_set_speed when alpha is 0 (#36709) * Skip _draw_set_speed when alpha is 0 to reduce unnecessary draw calls * Update selfdrive/ui/mici/onroad/hud_renderer.py --------- Co-authored-by: Shane Smiskol --- selfdrive/ui/mici/onroad/hud_renderer.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/selfdrive/ui/mici/onroad/hud_renderer.py b/selfdrive/ui/mici/onroad/hud_renderer.py index cb9f7c6fcf..76b443842e 100644 --- a/selfdrive/ui/mici/onroad/hud_renderer.py +++ b/selfdrive/ui/mici/onroad/hud_renderer.py @@ -224,11 +224,13 @@ class HudRenderer(Widget): def _draw_set_speed(self, rect: rl.Rectangle) -> None: """Draw the MAX speed indicator box.""" - x = rect.x - y = rect.y - alpha = self._set_speed_alpha_filter.update(0 < rl.get_time() - self._set_speed_changed_time < SET_SPEED_PERSISTENCE and self._can_draw_top_icons and self._engaged) + if alpha < 1e-2: + return + + x = rect.x + y = rect.y # draw drop shadow circle_radius = 162 // 2 From 85a162dd4320f14cb8aba1bcd7ff427d9b753ce5 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Sun, 30 Nov 2025 02:48:05 -0800 Subject: [PATCH 039/110] more scons nodes --- system/manager/build.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/manager/build.py b/system/manager/build.py index c88befd454..d79e7fd2ad 100755 --- a/system/manager/build.py +++ b/system/manager/build.py @@ -14,7 +14,7 @@ from openpilot.system.version import get_build_metadata MAX_CACHE_SIZE = 4e9 if "CI" in os.environ else 2e9 CACHE_DIR = Path("/data/scons_cache" if AGNOS else "/tmp/scons_cache") -TOTAL_SCONS_NODES = 2280 +TOTAL_SCONS_NODES = 2705 MAX_BUILD_PROGRESS = 100 def build(spinner: Spinner, dirty: bool = False, minimal: bool = False) -> None: From cd7e3623334d6314848c4e484a53d456410cf410 Mon Sep 17 00:00:00 2001 From: David <49467229+TheSecurityDev@users.noreply.github.com> Date: Sun, 30 Nov 2025 15:34:37 -0600 Subject: [PATCH 040/110] ui: Add RECORD=1 for direct frame recording (#36729) * ui: add real-time video recording functionality with ffmpeg support * fix: record at consistent frame rate * add spaces * fix type * refactor: RECORD_FRAMES variable and related logic * fix: remove unnecessary texture check * support missing output extension * add wait for close with timeout * fix: ensure RECORD_OUTPUT has the correct file extension * flush on close and terminate if times out closing * ffmpeg hide banner * reduce ffmpeg spam * refactor: streamline ffmpeg arguments for video encoding * refactor: move size arg to variable and add yub420p conversion for native support * use render_width and render_height for size * fix: ensure even dimensions for video encoding when recording * rm itertools * simple * cleanup * docs --------- Co-authored-by: Adeeb Shihadeh --- system/ui/README.md | 1 + system/ui/lib/application.py | 54 +++++++++++++++++++++++++++++++++++- 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/system/ui/README.md b/system/ui/README.md index f81cb5573a..79a4dd32ea 100644 --- a/system/ui/README.md +++ b/system/ui/README.md @@ -10,6 +10,7 @@ Quick start: * set `BURN_IN=1` to get a burn-in heatmap version of the UI * 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` * https://www.raylib.com/cheatsheet/cheatsheet.html * https://electronstudio.github.io/raylib-python-cffi/README.html#quickstart diff --git a/system/ui/lib/application.py b/system/ui/lib/application.py index e3370a5f74..79e68aa67f 100644 --- a/system/ui/lib/application.py +++ b/system/ui/lib/application.py @@ -7,11 +7,13 @@ import sys import pyray as rl import threading import platform +import subprocess from contextlib import contextmanager from collections.abc import Callable from collections import deque from dataclasses import dataclass from enum import StrEnum +from pathlib import Path from typing import NamedTuple from importlib.resources import as_file, files from openpilot.common.swaglog import cloudlog @@ -36,6 +38,8 @@ SCALE = float(os.getenv("SCALE", "1.0")) GRID_SIZE = int(os.getenv("GRID", "0")) PROFILE_RENDER = int(os.getenv("PROFILE_RENDER", "0")) PROFILE_STATS = int(os.getenv("PROFILE_STATS", "100")) # Number of functions to show in profile output +RECORD = os.getenv("RECORD") == "1" +RECORD_OUTPUT = str(Path(os.getenv("RECORD_OUTPUT", "output")).with_suffix(".mp4")) GL_VERSION = """ #version 300 es @@ -197,10 +201,15 @@ class GuiApplication: else: self._scale = SCALE + # Scale, then ensure dimensions are even self._scaled_width = int(self._width * self._scale) self._scaled_height = int(self._height * self._scale) + self._scaled_width += self._scaled_width % 2 + self._scaled_height += self._scaled_height % 2 + self._render_texture: rl.RenderTexture | None = None self._burn_in_shader: rl.Shader | None = None + self._ffmpeg_proc: subprocess.Popen | None = None self._textures: dict[str, rl.Texture] = {} self._target_fps: int = _DEFAULT_FPS self._last_fps_log_time: float = time.monotonic() @@ -259,12 +268,33 @@ class GuiApplication: rl.set_config_flags(flags) rl.init_window(self._scaled_width, self._scaled_height, title) - needs_render_texture = self._scale != 1.0 or BURN_IN_MODE + + needs_render_texture = self._scale != 1.0 or BURN_IN_MODE or RECORD if self._scale != 1.0: rl.set_mouse_scale(1 / self._scale, 1 / self._scale) if needs_render_texture: self._render_texture = rl.load_render_texture(self._width, self._height) rl.set_texture_filter(self._render_texture.texture, rl.TextureFilter.TEXTURE_FILTER_BILINEAR) + + if RECORD: + ffmpeg_args = [ + 'ffmpeg', + '-v', 'warning', # Reduce ffmpeg log spam + '-stats', # Show encoding progress + '-f', 'rawvideo', # Input format + '-pix_fmt', 'rgba', # Input pixel format + '-s', f'{self._width}x{self._height}', # Input resolution + '-r', str(fps), # Input frame rate + '-i', 'pipe:0', # Input from stdin + '-vf', 'vflip,format=yuv420p', # Flip vertically and convert rgba to yuv420p + '-c:v', 'libx264', # Video codec + '-preset', 'ultrafast', # Encoding speed + '-y', # Overwrite existing file + '-f', 'mp4', # Output format + RECORD_OUTPUT, # Output file path + ] + self._ffmpeg_proc = subprocess.Popen(ffmpeg_args, stdin=subprocess.PIPE) + rl.set_target_fps(fps) self._target_fps = fps @@ -372,6 +402,16 @@ class GuiApplication: rl.unload_image(image) return texture + def close_ffmpeg(self): + if self._ffmpeg_proc is not None: + self._ffmpeg_proc.stdin.flush() + self._ffmpeg_proc.stdin.close() + try: + self._ffmpeg_proc.wait(timeout=5) + except subprocess.TimeoutExpired: + self._ffmpeg_proc.terminate() + self._ffmpeg_proc.wait() + def close(self): if not rl.is_window_ready(): return @@ -395,6 +435,8 @@ class GuiApplication: if not PC: self._mouse.stop() + self.close_ffmpeg() + rl.close_window() @property @@ -469,6 +511,15 @@ class GuiApplication: self._draw_grid() rl.end_drawing() + + if RECORD: + image = rl.load_image_from_texture(self._render_texture.texture) + data_size = image.width * image.height * 4 + data = bytes(rl.ffi.buffer(image.data, data_size)) + self._ffmpeg_proc.stdin.write(data) + self._ffmpeg_proc.stdin.flush() + rl.unload_image(image) + self._monitor_fps() self._frame += 1 @@ -594,6 +645,7 @@ class GuiApplication: # Strict mode: terminate UI if FPS drops too much if STRICT_MODE and fps < self._target_fps * FPS_CRITICAL_THRESHOLD: cloudlog.error(f"FPS dropped critically below {fps}. Shutting down UI.") + self.close_ffmpeg() os._exit(1) def _draw_touch_points(self): From 970afa96835de4b3e0c085599dbd82d2dd6d9c60 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sun, 30 Nov 2025 14:19:37 -0800 Subject: [PATCH 041/110] bump to 0.10.3 --- RELEASES.md | 3 +++ common/version.h | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/RELEASES.md b/RELEASES.md index 58044dc694..fabe635c71 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,3 +1,6 @@ +Version 0.10.3 (2025-12-10) +======================== + Version 0.10.2 (2025-11-19) ======================== * comma four support diff --git a/common/version.h b/common/version.h index ef20670781..c489ecc578 100644 --- a/common/version.h +++ b/common/version.h @@ -1 +1 @@ -#define COMMA_VERSION "0.10.2" +#define COMMA_VERSION "0.10.3" From 6d04251517cadb8ad5001bc1e223f93133ce041f Mon Sep 17 00:00:00 2001 From: Trey Moen <50057480+greatgitsby@users.noreply.github.com> Date: Sun, 30 Nov 2025 14:43:29 -0800 Subject: [PATCH 042/110] esim: remove bootstrap and delete (#36732) init --- system/hardware/base.py | 11 ----------- system/hardware/esim.py | 36 +----------------------------------- system/hardware/tici/esim.py | 33 --------------------------------- 3 files changed, 1 insertion(+), 79 deletions(-) diff --git a/system/hardware/base.py b/system/hardware/base.py index 17d0ec1614..4163b33791 100644 --- a/system/hardware/base.py +++ b/system/hardware/base.py @@ -65,10 +65,6 @@ class ThermalConfig: return ret class LPABase(ABC): - @abstractmethod - def bootstrap(self) -> None: - pass - @abstractmethod def list_profiles(self) -> list[Profile]: pass @@ -77,10 +73,6 @@ class LPABase(ABC): def get_active_profile(self) -> Profile | None: pass - @abstractmethod - def delete_profile(self, iccid: str) -> None: - pass - @abstractmethod def download_profile(self, qr: str, nickname: str | None = None) -> None: pass @@ -93,9 +85,6 @@ class LPABase(ABC): def switch_profile(self, iccid: str) -> None: pass - def is_comma_profile(self, iccid: str) -> bool: - return any(iccid.startswith(prefix) for prefix in ('8985235',)) - class HardwareBase(ABC): @staticmethod def get_cmdline() -> dict[str, str]: diff --git a/system/hardware/esim.py b/system/hardware/esim.py index 909ad41e03..1c98bb1e4e 100755 --- a/system/hardware/esim.py +++ b/system/hardware/esim.py @@ -3,55 +3,21 @@ import argparse import time from openpilot.system.hardware import HARDWARE -from openpilot.system.hardware.base import LPABase - - -def bootstrap(lpa: LPABase) -> None: - print('┌──────────────────────────────────────────────────────────────────────────────┐') - print('│ WARNING, PLEASE READ BEFORE PROCEEDING │') - print('│ │') - print('│ this is an irreversible operation that will remove the comma-provisioned │') - print('│ profile. │') - print('│ │') - print('│ after this operation, you must purchase a new eSIM from comma in order to │') - print('│ use the comma prime subscription again. │') - print('└──────────────────────────────────────────────────────────────────────────────┘') - print() - for severity in ('sure', '100% sure'): - print(f'are you {severity} you want to proceed? (y/N) ', end='') - confirm = input() - if confirm != 'y': - print('aborting') - exit(0) - lpa.bootstrap() if __name__ == '__main__': parser = argparse.ArgumentParser(prog='esim.py', description='manage eSIM profiles on your comma device', epilog='comma.ai') - parser.add_argument('--bootstrap', action='store_true', help='bootstrap the eUICC (required before downloading profiles)') parser.add_argument('--backend', choices=['qmi', 'at'], default='qmi', help='use the specified backend, defaults to qmi') parser.add_argument('--switch', metavar='iccid', help='switch to profile') - parser.add_argument('--delete', metavar='iccid', help='delete profile (warning: this cannot be undone)') parser.add_argument('--download', nargs=2, metavar=('qr', 'name'), help='download a profile using QR code (format: LPA:1$rsp.truphone.com$QRF-SPEEDTEST)') parser.add_argument('--nickname', nargs=2, metavar=('iccid', 'name'), help='update the nickname for a profile') args = parser.parse_args() mutated = False lpa = HARDWARE.get_sim_lpa() - if args.bootstrap: - bootstrap(lpa) - mutated = True - elif args.switch: + if args.switch: lpa.switch_profile(args.switch) mutated = True - elif args.delete: - confirm = input('are you sure you want to delete this profile? (y/N) ') - if confirm == 'y': - lpa.delete_profile(args.delete) - mutated = True - else: - print('cancelled') - exit(0) elif args.download: lpa.download_profile(args.download[0], args.download[1]) elif args.nickname: diff --git a/system/hardware/tici/esim.py b/system/hardware/tici/esim.py index 391ba45531..8896117694 100644 --- a/system/hardware/tici/esim.py +++ b/system/hardware/tici/esim.py @@ -31,16 +31,7 @@ class TiciLPA(LPABase): def get_active_profile(self) -> Profile | None: return next((p for p in self.list_profiles() if p.enabled), None) - def delete_profile(self, iccid: str) -> None: - self._validate_profile_exists(iccid) - latest = self.get_active_profile() - if latest is not None and latest.iccid == iccid: - raise LPAError('cannot delete active profile, switch to another profile first') - self._validate_successful(self._invoke('profile', 'delete', iccid)) - self._process_notifications() - def download_profile(self, qr: str, nickname: str | None = None) -> None: - self._check_bootstrapped() msgs = self._invoke('profile', 'download', '-a', qr) self._validate_successful(msgs) new_profile = next((m for m in msgs if m['payload']['message'] == 'es8p_meatadata_parse'), None) @@ -55,7 +46,6 @@ class TiciLPA(LPABase): self._validate_successful(self._invoke('profile', 'nickname', iccid, nickname)) def switch_profile(self, iccid: str) -> None: - self._check_bootstrapped() self._validate_profile_exists(iccid) latest = self.get_active_profile() if latest and latest.iccid == iccid: @@ -63,33 +53,10 @@ class TiciLPA(LPABase): self._validate_successful(self._invoke('profile', 'enable', iccid)) self._process_notifications() - def bootstrap(self) -> None: - """ - find all comma-provisioned profiles and delete them. they conflict with user-provisioned profiles - and must be deleted. - - **note**: this is a **very** destructive operation. you **must** purchase a new comma SIM in order - to use comma prime again. - """ - if self._is_bootstrapped(): - return - - for p in self.list_profiles(): - if self.is_comma_profile(p.iccid): - self._disable_profile(p.iccid) - self.delete_profile(p.iccid) - def _disable_profile(self, iccid: str) -> None: self._validate_successful(self._invoke('profile', 'disable', iccid)) self._process_notifications() - def _check_bootstrapped(self) -> None: - assert self._is_bootstrapped(), 'eUICC is not bootstrapped, please bootstrap before performing this operation' - - def _is_bootstrapped(self) -> bool: - """ check if any comma provisioned profiles are on the eUICC """ - return not any(self.is_comma_profile(iccid) for iccid in (p.iccid for p in self.list_profiles())) - def _invoke(self, *cmd: str): proc = subprocess.Popen(['sudo', '-E', 'lpac'] + list(cmd), stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=self.env) try: From ff755ed4bfec129d183b9d1fb573704ac041cdaa Mon Sep 17 00:00:00 2001 From: MVL Date: Sun, 30 Nov 2025 18:04:17 -0500 Subject: [PATCH 043/110] Honda - Rename AcuraWatch Plus to AcuraWatch (#36726) * Rename AcuraWatch Plus to AcuraWatch * Rename AcuraWatch Plus to AcuraWatch --- docs/CARS.md | 2 +- selfdrive/car/CARS_template.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/CARS.md b/docs/CARS.md index 223f452e16..fcb44154bc 100644 --- a/docs/CARS.md +++ b/docs/CARS.md @@ -368,7 +368,7 @@ If your car has the following packages or features, then it's a good candidate f | Make | Required Package/Features | | ---- | ------------------------- | -| Acura | Any car with AcuraWatch Plus will work. AcuraWatch Plus comes standard on many newer models. | +| Acura | Any car with AcuraWatch will work. AcuraWatch comes standard on many newer models. | | Ford | Any car with Lane Centering will likely work. | | Honda | Any car with Honda Sensing will work. Honda Sensing comes standard on many newer models. | | Subaru | Any car with EyeSight will work. EyeSight comes standard on many newer models. | diff --git a/selfdrive/car/CARS_template.md b/selfdrive/car/CARS_template.md index 463683fd3c..cd352b2ede 100644 --- a/selfdrive/car/CARS_template.md +++ b/selfdrive/car/CARS_template.md @@ -42,7 +42,7 @@ If your car has the following packages or features, then it's a good candidate f | Make | Required Package/Features | | ---- | ------------------------- | -| Acura | Any car with AcuraWatch Plus will work. AcuraWatch Plus comes standard on many newer models. | +| Acura | Any car with AcuraWatch will work. AcuraWatch comes standard on many newer models. | | Ford | Any car with Lane Centering will likely work. | | Honda | Any car with Honda Sensing will work. Honda Sensing comes standard on many newer models. | | Subaru | Any car with EyeSight will work. EyeSight comes standard on many newer models. | From 7521fd11e24e93148577608b8b8ec2c13208a011 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sun, 30 Nov 2025 15:08:32 -0800 Subject: [PATCH 044/110] common: rename atomic_write_in_dir -> atomic_write (#36733) rename --- common/tests/test_file_helpers.py | 6 +++--- common/utils.py | 2 +- system/statsd.py | 4 ++-- tools/lib/url_file.py | 6 +++--- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/common/tests/test_file_helpers.py b/common/tests/test_file_helpers.py index c7fe1984c5..c2b880f873 100644 --- a/common/tests/test_file_helpers.py +++ b/common/tests/test_file_helpers.py @@ -1,7 +1,7 @@ import os from uuid import uuid4 -from openpilot.common.utils import atomic_write_in_dir +from openpilot.common.utils import atomic_write class TestFileHelpers: @@ -15,5 +15,5 @@ class TestFileHelpers: assert f.read() == "test" os.remove(path) - def test_atomic_write_in_dir(self): - self.run_atomic_write_func(atomic_write_in_dir) + def test_atomic_write(self): + self.run_atomic_write_func(atomic_write) diff --git a/common/utils.py b/common/utils.py index 89c0601f06..684c7aeb75 100644 --- a/common/utils.py +++ b/common/utils.py @@ -32,7 +32,7 @@ class CallbackReader: @contextlib.contextmanager -def atomic_write_in_dir(path: str, mode: str = 'w', buffering: int = -1, encoding: str | None = None, newline: str | None = None, +def atomic_write(path: str, mode: str = 'w', buffering: int = -1, encoding: str | None = None, newline: str | None = None, overwrite: bool = False): """Write to a file atomically using a temporary file in the same directory as the destination file.""" dir_name = os.path.dirname(path) diff --git a/system/statsd.py b/system/statsd.py index c4216f5e76..33e9e9912d 100755 --- a/system/statsd.py +++ b/system/statsd.py @@ -13,7 +13,7 @@ from cereal.messaging import SubMaster from openpilot.system.hardware.hw import Paths from openpilot.common.swaglog import cloudlog from openpilot.system.hardware import HARDWARE -from openpilot.common.utils import atomic_write_in_dir +from openpilot.common.utils import atomic_write from openpilot.system.version import get_build_metadata from openpilot.system.loggerd.config import STATS_DIR_FILE_LIMIT, STATS_SOCKET, STATS_FLUSH_TIME_S @@ -167,7 +167,7 @@ def main() -> NoReturn: if len(os.listdir(STATS_DIR)) < STATS_DIR_FILE_LIMIT: if len(result) > 0: stats_path = os.path.join(STATS_DIR, f"{boot_uid}_{idx}") - with atomic_write_in_dir(stats_path) as f: + with atomic_write(stats_path) as f: f.write(result) idx += 1 else: diff --git a/tools/lib/url_file.py b/tools/lib/url_file.py index f988fa9db1..2bf3ba8209 100644 --- a/tools/lib/url_file.py +++ b/tools/lib/url_file.py @@ -9,7 +9,7 @@ from urllib3.util import Timeout from urllib3.exceptions import MaxRetryError -from openpilot.common.utils import atomic_write_in_dir +from openpilot.common.utils import atomic_write from openpilot.system.hardware.hw import Paths # Cache chunk size @@ -88,7 +88,7 @@ class URLFile: self._length = self.get_length_online() if not self._force_download and self._length != -1: - with atomic_write_in_dir(file_length_path, mode="w", overwrite=True) as file_length: + with atomic_write(file_length_path, mode="w", overwrite=True) as file_length: file_length.write(str(self._length)) return self._length @@ -111,7 +111,7 @@ class URLFile: # If we don't have a file, download it if not os.path.exists(full_path): data = self.read_aux(ll=CHUNK_SIZE) - with atomic_write_in_dir(full_path, mode="wb", overwrite=True) as new_cached_file: + with atomic_write(full_path, mode="wb", overwrite=True) as new_cached_file: new_cached_file.write(data) else: with open(full_path, "rb") as cached_file: From 436e3dec3edbb29e15ca45a08e227881a91b9947 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sun, 30 Nov 2025 15:14:31 -0800 Subject: [PATCH 045/110] manager: write power monitor flag atomically (#36734) --- common/utils.py | 2 +- system/manager/manager.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/common/utils.py b/common/utils.py index 684c7aeb75..71b29a0c4e 100644 --- a/common/utils.py +++ b/common/utils.py @@ -33,7 +33,7 @@ class CallbackReader: @contextlib.contextmanager def atomic_write(path: str, mode: str = 'w', buffering: int = -1, encoding: str | None = None, newline: str | None = None, - overwrite: bool = False): + overwrite: bool = False): """Write to a file atomically using a temporary file in the same directory as the destination file.""" dir_name = os.path.dirname(path) diff --git a/system/manager/manager.py b/system/manager/manager.py index 8db13346e3..2d80c78ff5 100755 --- a/system/manager/manager.py +++ b/system/manager/manager.py @@ -9,6 +9,7 @@ import traceback from cereal import log import cereal.messaging as messaging import openpilot.system.sentry as sentry +from openpilot.common.utils import atomic_write from openpilot.common.params import Params, ParamKeyFlag from openpilot.common.text_window import TextWindow from openpilot.system.hardware import HARDWARE @@ -162,7 +163,7 @@ def manager_thread() -> None: # kick AGNOS power monitoring watchdog try: if sm.all_checks(['deviceState']): - with open("/var/tmp/power_watchdog", "w") as f: + with atomic_write("/var/tmp/power_watchdog", "w", overwrite=True) as f: f.write(str(time.monotonic())) except Exception: pass From 151d256dd619bece66895c7ef7c98e3bb6bdf4db Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sun, 30 Nov 2025 15:29:40 -0800 Subject: [PATCH 046/110] add param for agnos power monitor --- common/params_keys.h | 1 + 1 file changed, 1 insertion(+) diff --git a/common/params_keys.h b/common/params_keys.h index fc7f410e40..c496ad2854 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -71,6 +71,7 @@ inline static std::unordered_map keys = { {"LastGPSPosition", {PERSISTENT, STRING}}, {"LastManagerExitReason", {CLEAR_ON_MANAGER_START, STRING}}, {"LastOffroadStatusPacket", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, JSON}}, + {"LastAgnosPowerMonitorShutdown", {CLEAR_ON_MANAGER_START, STRING}}, {"LastPowerDropDetected", {CLEAR_ON_MANAGER_START, STRING}}, {"LastUpdateException", {CLEAR_ON_MANAGER_START, STRING}}, {"LastUpdateRouteCount", {PERSISTENT, INT, "0"}}, From 749e236bc0bfba1de800bc39acb1ef2356bb8756 Mon Sep 17 00:00:00 2001 From: Dean Lee Date: Mon, 1 Dec 2025 08:06:33 +0800 Subject: [PATCH 047/110] ui: fix EGL_BAD_MATCH error when running profile_onroad.py on device (#36608) fix failed to create EGL image:12297 error on device --- selfdrive/ui/tests/profile_onroad.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/selfdrive/ui/tests/profile_onroad.py b/selfdrive/ui/tests/profile_onroad.py index b1fa4acc48..fde4f25ffe 100755 --- a/selfdrive/ui/tests/profile_onroad.py +++ b/selfdrive/ui/tests/profile_onroad.py @@ -88,9 +88,9 @@ if __name__ == "__main__": print("Running...") patch_submaster(message_chunks) - W, H = 1928, 1208 + W, H = 2048, 1216 vipc = VisionIpcServer("camerad") - vipc.create_buffers(VisionStreamType.VISION_STREAM_ROAD, 5, 1928, 1208) + vipc.create_buffers(VisionStreamType.VISION_STREAM_ROAD, 5, W, H) vipc.start_listener() yuv_buffer_size = W * H + (W // 2) * (H // 2) * 2 yuv_data = np.random.randint(0, 256, yuv_buffer_size, dtype=np.uint8).tobytes() From 8ffe3f287e735864368f4096f5563d52ee3f08d1 Mon Sep 17 00:00:00 2001 From: Trey Moen <50057480+greatgitsby@users.noreply.github.com> Date: Sun, 30 Nov 2025 16:09:50 -0800 Subject: [PATCH 048/110] fix: openpilot build on ubuntu aarch64 (#36675) breaks on linux --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3f3c8a72bb..b2acf1c09b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -125,7 +125,7 @@ dev = [ tools = [ "metadrive-simulator @ https://github.com/commaai/metadrive/releases/download/MetaDrive-minimal-0.4.2.4/metadrive_simulator-0.4.2.4-py3-none-any.whl ; (platform_machine != 'aarch64')", - "dearpygui>=2.1.0", + "dearpygui>=2.1.0; (sys_platform != 'linux' or platform_machine != 'aarch64')", # not vended for linux aarch64 ] [project.urls] From 693c83f74c57c91fffd51ef1edf8cef74870391d Mon Sep 17 00:00:00 2001 From: Dean Lee Date: Tue, 2 Dec 2025 05:32:21 +0800 Subject: [PATCH 049/110] replay: fix dangling pointers in logging calls (#36738) fix dangling pointers in logging calls --- tools/replay/replay.cc | 6 +++++- tools/replay/seg_mgr.cc | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/tools/replay/replay.cc b/tools/replay/replay.cc index c9ab7e7e2b..fb13ead034 100644 --- a/tools/replay/replay.cc +++ b/tools/replay/replay.cc @@ -31,6 +31,8 @@ void Replay::setupServices(const std::vector &allow, const std::vec sockets_.resize(event_schema.getUnionFields().size(), nullptr); std::vector active_services; + active_services.reserve(services.size()); + for (const auto &[name, _] : services) { bool is_blocked = std::find(block.begin(), block.end(), name) != block.end(); bool is_allowed = allow.empty() || std::find(allow.begin(), allow.end(), name) != allow.end(); @@ -40,7 +42,9 @@ void Replay::setupServices(const std::vector &allow, const std::vec active_services.push_back(name.c_str()); } } - rInfo("active services: %s", join(active_services, ", ").c_str()); + + std::string services_str = join(active_services, ", "); + rInfo("active services: %s", services_str.c_str()); if (!sm_) { pm_ = std::make_unique(active_services); } diff --git a/tools/replay/seg_mgr.cc b/tools/replay/seg_mgr.cc index ee034fb083..f4e865d476 100644 --- a/tools/replay/seg_mgr.cc +++ b/tools/replay/seg_mgr.cc @@ -91,7 +91,8 @@ bool SegmentManager::mergeSegments(const SegmentMap::iterator &begin, const Segm auto &merged_events = merged_event_data->events; merged_events.reserve(total_event_count); - rDebug("merging segments: %s", join(segments_to_merge, ", ").c_str()); + std::string segments_str = join(segments_to_merge, ", "); + rDebug("merging segments: %s", segments_str.c_str()); for (int n : segments_to_merge) { const auto &events = segments_.at(n)->log->events; if (events.empty()) continue; From dc654b439a946972b2f476465407fb2e743f576a Mon Sep 17 00:00:00 2001 From: Maxime Desroches Date: Mon, 1 Dec 2025 20:48:04 -0800 Subject: [PATCH 050/110] Revert "Fix raylib ui spamming API calls (#36700)" (#36744) This reverts commit 26261387f8b4188951d107567a067d22cf87e5e3. --- common/params_keys.h | 2 +- common/tests/test_params.py | 4 +- selfdrive/ui/lib/api_helpers.py | 93 +------------------ selfdrive/ui/lib/prime_state.py | 75 +++++++++++---- .../ui/mici/layouts/settings/firehose.py | 65 +++++++++---- 5 files changed, 108 insertions(+), 131 deletions(-) diff --git a/common/params_keys.h b/common/params_keys.h index c496ad2854..d6104e7497 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -10,7 +10,7 @@ inline static std::unordered_map keys = { {"AdbEnabled", {PERSISTENT, BOOL}}, {"AlwaysOnDM", {PERSISTENT, BOOL}}, {"ApiCache_Device", {PERSISTENT, STRING}}, - {"ApiCache_FirehoseStats", {PERSISTENT, STRING}}, + {"ApiCache_FirehoseStats", {PERSISTENT, JSON}}, {"AssistNowToken", {PERSISTENT, STRING}}, {"AthenadPid", {PERSISTENT, INT}}, {"AthenadUploadQueue", {PERSISTENT, JSON}}, diff --git a/common/tests/test_params.py b/common/tests/test_params.py index e0f213e02c..592bf2c4b2 100644 --- a/common/tests/test_params.py +++ b/common/tests/test_params.py @@ -123,8 +123,8 @@ class TestParams: def test_params_get_type(self): # json - self.params.put("LiveParameters", {"a": 0}) - assert self.params.get("LiveParameters") == {"a": 0} + self.params.put("ApiCache_FirehoseStats", {"a": 0}) + assert self.params.get("ApiCache_FirehoseStats") == {"a": 0} # int self.params.put("BootCount", 1441) diff --git a/selfdrive/ui/lib/api_helpers.py b/selfdrive/ui/lib/api_helpers.py index 31e844dd0a..8ed1c22a63 100644 --- a/selfdrive/ui/lib/api_helpers.py +++ b/selfdrive/ui/lib/api_helpers.py @@ -1,13 +1,7 @@ import time -import threading -from collections.abc import Callable from functools import lru_cache - -from openpilot.common.api import Api, api_get -from openpilot.common.params import Params -from openpilot.common.swaglog import cloudlog +from openpilot.common.api import Api from openpilot.common.time_helpers import system_time_valid -from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID TOKEN_EXPIRY_HOURS = 2 @@ -22,88 +16,3 @@ def _get_token(dongle_id: str, t: int): def get_token(dongle_id: str): return _get_token(dongle_id, int(time.monotonic() / (TOKEN_EXPIRY_HOURS / 2 * 60 * 60))) - - -class RequestRepeater: - API_TIMEOUT = 10.0 # seconds for API requests - SLEEP_INTERVAL = 0.5 # seconds to sleep between checks in the worker thread - - def __init__(self, dongle_id: str, request_route: str, period: int, cache_key: str | None = None): - self._dongle_id = dongle_id - self._request_route = request_route - self._period = period # seconds - self._cache_key = cache_key - - self._request_done_callbacks: list[Callable[[str, bool], None]] = [] - self._prev_response_text = None - self._running = False - self._thread = None - self._params = Params() - - if self._cache_key is not None: - # Cache successful responses to params - def cache_response(response: str, success: bool): - if success and response != self._prev_response_text: - self._params.put(self._cache_key, response) - self._prev_response_text = response - - self.add_request_done_callback(cache_response) - - def add_request_done_callback(self, callback: Callable[[str, bool], None]): - self._request_done_callbacks.append(callback) - - def _do_callbacks(self, response_text: str, success: bool): - for callback in self._request_done_callbacks: - try: - callback(response_text, success) - except Exception as e: - cloudlog.error(f"RequestRepeater callback error: {e}") - - def load_cache(self): - # call callbacks with cached response - if self._cache_key is not None: - self._prev_response_text = self._params.get(self._cache_key) - if self._prev_response_text: - self._do_callbacks(self._prev_response_text, True) - - def start(self): - if self._thread and self._thread.is_alive(): - return - self._running = True - self._thread = threading.Thread(target=self._worker_thread, daemon=True) - self._thread.start() - - def stop(self): - self._running = False - if self._thread and self._thread.is_alive(): - self._thread.join(timeout=1.0) - - def _worker_thread(self): - # Avoid circular imports - from openpilot.selfdrive.ui.ui_state import ui_state, device - - while self._running: - # Don't run when device is asleep or onroad - if not ui_state.started and device.awake: - self._send_request() - - for _ in range(int(self._period / self.SLEEP_INTERVAL)): - if not self._running: - break - time.sleep(self.SLEEP_INTERVAL) - - def _send_request(self): - if not self._dongle_id or self._dongle_id == UNREGISTERED_DONGLE_ID: - return - - try: - identity_token = get_token(self._dongle_id) - response = api_get(self._request_route, timeout=self.API_TIMEOUT, access_token=identity_token) - self._do_callbacks(response.text, 200 <= response.status_code < 300) - - except Exception as e: - cloudlog.error(f"Failed to send request to {self._request_route}: {e}") - self._do_callbacks("", False) - - def __del__(self): - self.stop() diff --git a/selfdrive/ui/lib/prime_state.py b/selfdrive/ui/lib/prime_state.py index f3adebf4b6..fc72b4f9c6 100644 --- a/selfdrive/ui/lib/prime_state.py +++ b/selfdrive/ui/lib/prime_state.py @@ -1,10 +1,13 @@ from enum import IntEnum import os -import json +import threading +import time +from openpilot.common.api import api_get from openpilot.common.params import Params from openpilot.common.swaglog import cloudlog -from openpilot.selfdrive.ui.lib.api_helpers import RequestRepeater +from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID +from openpilot.selfdrive.ui.lib.api_helpers import get_token class PrimeType(IntEnum): @@ -19,14 +22,17 @@ class PrimeType(IntEnum): class PrimeState: + FETCH_INTERVAL = 5.0 # seconds between API calls + API_TIMEOUT = 10.0 # seconds for API requests + SLEEP_INTERVAL = 0.5 # seconds to sleep between checks in the worker thread + def __init__(self): self._params = Params() + self._lock = threading.Lock() self.prime_type: PrimeType = self._load_initial_state() - dongle_id = self._params.get("DongleId") - self._request_repeater = RequestRepeater(dongle_id, f"v1.1/devices/{dongle_id}", 5, "ApiCache_Device") - self._request_repeater.add_request_done_callback(self._handle_reply) - self._request_repeater.load_cache() # sets prime_type from API response cache + self._running = False + self._thread = None def _load_initial_state(self) -> PrimeType: prime_type_str = os.getenv("PRIME_TYPE") or self._params.get("PrimeType") @@ -37,32 +43,61 @@ class PrimeState: pass return PrimeType.UNKNOWN - def _handle_reply(self, response: str, success: bool): - if not success: + def _fetch_prime_status(self) -> None: + dongle_id = self._params.get("DongleId") + if not dongle_id or dongle_id == UNREGISTERED_DONGLE_ID: return try: - data = json.loads(response) - 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) + identity_token = get_token(dongle_id) + response = api_get(f"v1.1/devices/{dongle_id}", timeout=self.API_TIMEOUT, access_token=identity_token) + 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) except Exception as e: cloudlog.error(f"Failed to fetch prime status: {e}") def set_type(self, prime_type: PrimeType) -> None: - if prime_type != self.prime_type: - self.prime_type = prime_type - self._params.put("PrimeType", int(prime_type)) - cloudlog.info(f"Prime type updated to {prime_type}") + with self._lock: + if prime_type != self.prime_type: + self.prime_type = prime_type + self._params.put("PrimeType", int(prime_type)) + cloudlog.info(f"Prime type updated to {prime_type}") + + def _worker_thread(self) -> None: + while self._running: + self._fetch_prime_status() + + for _ in range(int(self.FETCH_INTERVAL / self.SLEEP_INTERVAL)): + if not self._running: + break + time.sleep(self.SLEEP_INTERVAL) def start(self) -> None: - self._request_repeater.start() + if self._thread and self._thread.is_alive(): + return + self._running = True + self._thread = threading.Thread(target=self._worker_thread, daemon=True) + self._thread.start() + + def stop(self) -> None: + self._running = False + if self._thread and self._thread.is_alive(): + self._thread.join(timeout=1.0) def get_type(self) -> PrimeType: - return self.prime_type + with self._lock: + return self.prime_type def is_prime(self) -> bool: - return bool(self.prime_type > PrimeType.NONE) + with self._lock: + return bool(self.prime_type > PrimeType.NONE) def is_paired(self) -> bool: - return self.prime_type > PrimeType.UNPAIRED + with self._lock: + return self.prime_type > PrimeType.UNPAIRED + + def __del__(self): + self.stop() diff --git a/selfdrive/ui/mici/layouts/settings/firehose.py b/selfdrive/ui/mici/layouts/settings/firehose.py index ff75e6f8ed..dcb4b6c84d 100644 --- a/selfdrive/ui/mici/layouts/settings/firehose.py +++ b/selfdrive/ui/mici/layouts/settings/firehose.py @@ -1,15 +1,18 @@ +import threading +import time import pyray as rl -import json +from openpilot.common.api import api_get from openpilot.common.params import Params from openpilot.common.swaglog import cloudlog +from openpilot.selfdrive.ui.lib.api_helpers import get_token from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE from openpilot.system.ui.lib.wrap_text import wrap_text from openpilot.system.ui.lib.scroll_panel2 import GuiScrollPanel2 from openpilot.system.ui.lib.multilang import tr, trn, tr_noop from openpilot.system.ui.widgets import Widget, NavWidget -from openpilot.selfdrive.ui.lib.api_helpers import RequestRepeater TITLE = tr_noop("Firehose Mode") DESCRIPTION = tr_noop( @@ -31,37 +34,47 @@ FAQ_ITEMS = [ class FirehoseLayoutBase(Widget): + PARAM_KEY = "ApiCache_FirehoseStats" GREEN = rl.Color(46, 204, 113, 255) RED = rl.Color(231, 76, 60, 255) GRAY = rl.Color(68, 68, 68, 255) LIGHT_GRAY = rl.Color(228, 228, 228, 255) + UPDATE_INTERVAL = 30 # seconds def __init__(self): super().__init__() - self._segment_count = 0 + self._params = Params() + self._segment_count = self._get_segment_count() + self._scroll_panel = GuiScrollPanel2(horizontal=False) self._content_height = 0 - dongle_id = Params().get("DongleId") - self._request_repeater = RequestRepeater(dongle_id, f"v1/devices/{dongle_id}/firehose_stats", 30, "ApiCache_FirehoseStats") - self._request_repeater.add_request_done_callback(self._handle_reply) - self._request_repeater.load_cache() - self._request_repeater.start() - - def _handle_reply(self, response: str, success: bool): - if not success: - return + self._running = True + self._update_thread = threading.Thread(target=self._update_loop, daemon=True) + self._update_thread.start() + def __del__(self): + self._running = False try: - data = json.loads(response) - self._segment_count = data.get("firehose", 0) - except Exception as e: - cloudlog.error(f"Failed to fetch firehose stats: {e}") + if self._update_thread and self._update_thread.is_alive(): + self._update_thread.join(timeout=1.0) + except Exception: + pass def show_event(self): super().show_event() self._scroll_panel.set_offset(0) + def _get_segment_count(self) -> int: + stats = self._params.get(self.PARAM_KEY) + if not stats: + return 0 + try: + return int(stats.get("firehose", 0)) + except Exception: + cloudlog.exception(f"Failed to decode firehose stats: {stats}") + return 0 + def _render(self, rect: rl.Rectangle): # compute total content height for scrolling content_height = self._measure_content_height(rect) @@ -184,6 +197,26 @@ class FirehoseLayoutBase(Widget): else: return tr("INACTIVE: connect to an unmetered network"), self.RED + def _fetch_firehose_stats(self): + try: + dongle_id = self._params.get("DongleId") + if not dongle_id or dongle_id == UNREGISTERED_DONGLE_ID: + return + identity_token = get_token(dongle_id) + response = api_get(f"v1/devices/{dongle_id}/firehose_stats", access_token=identity_token) + if response.status_code == 200: + data = response.json() + self._segment_count = data.get("firehose", 0) + self._params.put(self.PARAM_KEY, data) + except Exception as e: + cloudlog.error(f"Failed to fetch firehose stats: {e}") + + def _update_loop(self): + while self._running: + if not ui_state.started: + self._fetch_firehose_stats() + time.sleep(self.UPDATE_INTERVAL) + class FirehoseLayout(FirehoseLayoutBase, NavWidget): BACK_TOUCH_AREA_PERCENTAGE = 0.1 From 62b7abcd917677cf2a8c923b3ba5c96874c5d6fb Mon Sep 17 00:00:00 2001 From: Maxime Desroches Date: Mon, 1 Dec 2025 21:13:43 -0800 Subject: [PATCH 051/110] Fix raylib ui spamming API calls (#36745) fix --- selfdrive/ui/lib/prime_state.py | 4 +++- selfdrive/ui/mici/layouts/settings/firehose.py | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/selfdrive/ui/lib/prime_state.py b/selfdrive/ui/lib/prime_state.py index fc72b4f9c6..e1ef387bf7 100644 --- a/selfdrive/ui/lib/prime_state.py +++ b/selfdrive/ui/lib/prime_state.py @@ -67,8 +67,10 @@ class PrimeState: cloudlog.info(f"Prime type updated to {prime_type}") def _worker_thread(self) -> None: + from openpilot.selfdrive.ui.ui_state import ui_state, device while self._running: - self._fetch_prime_status() + if not ui_state.started and device._awake: + self._fetch_prime_status() for _ in range(int(self.FETCH_INTERVAL / self.SLEEP_INTERVAL)): if not self._running: diff --git a/selfdrive/ui/mici/layouts/settings/firehose.py b/selfdrive/ui/mici/layouts/settings/firehose.py index dcb4b6c84d..10e52bb3b4 100644 --- a/selfdrive/ui/mici/layouts/settings/firehose.py +++ b/selfdrive/ui/mici/layouts/settings/firehose.py @@ -6,7 +6,7 @@ from openpilot.common.api import api_get from openpilot.common.params import Params from openpilot.common.swaglog import cloudlog from openpilot.selfdrive.ui.lib.api_helpers import get_token -from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.selfdrive.ui.ui_state import ui_state, device from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE from openpilot.system.ui.lib.wrap_text import wrap_text @@ -213,7 +213,7 @@ class FirehoseLayoutBase(Widget): def _update_loop(self): while self._running: - if not ui_state.started: + if not ui_state.started and device._awake: self._fetch_firehose_stats() time.sleep(self.UPDATE_INTERVAL) From 65e551c671e403e8830ffc33fec5c27e19d3a1e7 Mon Sep 17 00:00:00 2001 From: Maxime Desroches Date: Mon, 1 Dec 2025 21:32:07 -0800 Subject: [PATCH 052/110] Handle invalid frame fd when creating EGL image (#36743) catch --- system/ui/lib/egl.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/system/ui/lib/egl.py b/system/ui/lib/egl.py index d119a8a832..69236482b0 100644 --- a/system/ui/lib/egl.py +++ b/system/ui/lib/egl.py @@ -128,8 +128,12 @@ def init_egl() -> bool: def create_egl_image(width: int, height: int, stride: int, fd: int, uv_offset: int) -> EGLImage | None: assert _egl.initialized, "EGL not initialized" - # Duplicate fd since EGL needs it - dup_fd = os.dup(fd) + try: + # Duplicate fd since EGL needs it + dup_fd = os.dup(fd) + except OSError as e: + cloudlog.exception(f"Failed to duplicate frame fd when creating EGL image: {e}") + return None # Create image attributes for EGL img_attrs = [ From cabfa7b735fd89e11beb142a663d15c0d67f9859 Mon Sep 17 00:00:00 2001 From: Trey Moen <50057480+greatgitsby@users.noreply.github.com> Date: Mon, 1 Dec 2025 22:45:11 -0800 Subject: [PATCH 053/110] Revert "esim: remove bootstrap and delete (#36732)" (#36747) This reverts commit 6d04251517cadb8ad5001bc1e223f93133ce041f. --- system/hardware/base.py | 11 +++++++++++ system/hardware/esim.py | 36 +++++++++++++++++++++++++++++++++++- system/hardware/tici/esim.py | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 1 deletion(-) diff --git a/system/hardware/base.py b/system/hardware/base.py index 4163b33791..17d0ec1614 100644 --- a/system/hardware/base.py +++ b/system/hardware/base.py @@ -65,6 +65,10 @@ class ThermalConfig: return ret class LPABase(ABC): + @abstractmethod + def bootstrap(self) -> None: + pass + @abstractmethod def list_profiles(self) -> list[Profile]: pass @@ -73,6 +77,10 @@ class LPABase(ABC): def get_active_profile(self) -> Profile | None: pass + @abstractmethod + def delete_profile(self, iccid: str) -> None: + pass + @abstractmethod def download_profile(self, qr: str, nickname: str | None = None) -> None: pass @@ -85,6 +93,9 @@ class LPABase(ABC): def switch_profile(self, iccid: str) -> None: pass + def is_comma_profile(self, iccid: str) -> bool: + return any(iccid.startswith(prefix) for prefix in ('8985235',)) + class HardwareBase(ABC): @staticmethod def get_cmdline() -> dict[str, str]: diff --git a/system/hardware/esim.py b/system/hardware/esim.py index 1c98bb1e4e..909ad41e03 100755 --- a/system/hardware/esim.py +++ b/system/hardware/esim.py @@ -3,21 +3,55 @@ import argparse import time from openpilot.system.hardware import HARDWARE +from openpilot.system.hardware.base import LPABase + + +def bootstrap(lpa: LPABase) -> None: + print('┌──────────────────────────────────────────────────────────────────────────────┐') + print('│ WARNING, PLEASE READ BEFORE PROCEEDING │') + print('│ │') + print('│ this is an irreversible operation that will remove the comma-provisioned │') + print('│ profile. │') + print('│ │') + print('│ after this operation, you must purchase a new eSIM from comma in order to │') + print('│ use the comma prime subscription again. │') + print('└──────────────────────────────────────────────────────────────────────────────┘') + print() + for severity in ('sure', '100% sure'): + print(f'are you {severity} you want to proceed? (y/N) ', end='') + confirm = input() + if confirm != 'y': + print('aborting') + exit(0) + lpa.bootstrap() if __name__ == '__main__': parser = argparse.ArgumentParser(prog='esim.py', description='manage eSIM profiles on your comma device', epilog='comma.ai') + parser.add_argument('--bootstrap', action='store_true', help='bootstrap the eUICC (required before downloading profiles)') parser.add_argument('--backend', choices=['qmi', 'at'], default='qmi', help='use the specified backend, defaults to qmi') parser.add_argument('--switch', metavar='iccid', help='switch to profile') + parser.add_argument('--delete', metavar='iccid', help='delete profile (warning: this cannot be undone)') parser.add_argument('--download', nargs=2, metavar=('qr', 'name'), help='download a profile using QR code (format: LPA:1$rsp.truphone.com$QRF-SPEEDTEST)') parser.add_argument('--nickname', nargs=2, metavar=('iccid', 'name'), help='update the nickname for a profile') args = parser.parse_args() mutated = False lpa = HARDWARE.get_sim_lpa() - if args.switch: + if args.bootstrap: + bootstrap(lpa) + mutated = True + elif args.switch: lpa.switch_profile(args.switch) mutated = True + elif args.delete: + confirm = input('are you sure you want to delete this profile? (y/N) ') + if confirm == 'y': + lpa.delete_profile(args.delete) + mutated = True + else: + print('cancelled') + exit(0) elif args.download: lpa.download_profile(args.download[0], args.download[1]) elif args.nickname: diff --git a/system/hardware/tici/esim.py b/system/hardware/tici/esim.py index 8896117694..391ba45531 100644 --- a/system/hardware/tici/esim.py +++ b/system/hardware/tici/esim.py @@ -31,7 +31,16 @@ class TiciLPA(LPABase): def get_active_profile(self) -> Profile | None: return next((p for p in self.list_profiles() if p.enabled), None) + def delete_profile(self, iccid: str) -> None: + self._validate_profile_exists(iccid) + latest = self.get_active_profile() + if latest is not None and latest.iccid == iccid: + raise LPAError('cannot delete active profile, switch to another profile first') + self._validate_successful(self._invoke('profile', 'delete', iccid)) + self._process_notifications() + def download_profile(self, qr: str, nickname: str | None = None) -> None: + self._check_bootstrapped() msgs = self._invoke('profile', 'download', '-a', qr) self._validate_successful(msgs) new_profile = next((m for m in msgs if m['payload']['message'] == 'es8p_meatadata_parse'), None) @@ -46,6 +55,7 @@ class TiciLPA(LPABase): self._validate_successful(self._invoke('profile', 'nickname', iccid, nickname)) def switch_profile(self, iccid: str) -> None: + self._check_bootstrapped() self._validate_profile_exists(iccid) latest = self.get_active_profile() if latest and latest.iccid == iccid: @@ -53,10 +63,33 @@ class TiciLPA(LPABase): self._validate_successful(self._invoke('profile', 'enable', iccid)) self._process_notifications() + def bootstrap(self) -> None: + """ + find all comma-provisioned profiles and delete them. they conflict with user-provisioned profiles + and must be deleted. + + **note**: this is a **very** destructive operation. you **must** purchase a new comma SIM in order + to use comma prime again. + """ + if self._is_bootstrapped(): + return + + for p in self.list_profiles(): + if self.is_comma_profile(p.iccid): + self._disable_profile(p.iccid) + self.delete_profile(p.iccid) + def _disable_profile(self, iccid: str) -> None: self._validate_successful(self._invoke('profile', 'disable', iccid)) self._process_notifications() + def _check_bootstrapped(self) -> None: + assert self._is_bootstrapped(), 'eUICC is not bootstrapped, please bootstrap before performing this operation' + + def _is_bootstrapped(self) -> bool: + """ check if any comma provisioned profiles are on the eUICC """ + return not any(self.is_comma_profile(iccid) for iccid in (p.iccid for p in self.list_profiles())) + def _invoke(self, *cmd: str): proc = subprocess.Popen(['sudo', '-E', 'lpac'] + list(cmd), stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=self.env) try: From fa18bb9261dff30f051d1e989dd253e90c78c322 Mon Sep 17 00:00:00 2001 From: Maxime Desroches Date: Mon, 1 Dec 2025 22:55:14 -0800 Subject: [PATCH 054/110] ui: restart if crash (#36746) * simpler * mypy your are going to be replaced very soon --- system/manager/manager.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/system/manager/manager.py b/system/manager/manager.py index 2d80c78ff5..15f8a2b793 100755 --- a/system/manager/manager.py +++ b/system/manager/manager.py @@ -155,6 +155,10 @@ def manager_thread() -> None: print(running) cloudlog.debug(running) + if 'ui' in managed_processes and managed_processes['ui'].proc is not None and not managed_processes['ui'].proc.is_alive(): + cloudlog.error(f'Restarting UI (exitcode {managed_processes["ui"].proc.exitcode})') + managed_processes['ui'].restart() + # send managerState msg = messaging.new_message('managerState', valid=True) msg.managerState.processes = [p.get_process_state_msg() for p in managed_processes.values()] From cfb0a1c18ce2c03f71085af251a600959aa08fae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Harald=20Sch=C3=A4fer?= Date: Mon, 1 Dec 2025 23:11:03 -0800 Subject: [PATCH 055/110] URLFile multirange (#36740) * url file multirange * cleanup urlfile * time * fixup * raise * Diskfile --- tools/lib/filereader.py | 15 +++++++-- tools/lib/url_file.py | 74 ++++++++++++++++++++--------------------- 2 files changed, 49 insertions(+), 40 deletions(-) diff --git a/tools/lib/filereader.py b/tools/lib/filereader.py index ee9ee294bb..f5418be81a 100644 --- a/tools/lib/filereader.py +++ b/tools/lib/filereader.py @@ -1,4 +1,5 @@ import os +import io import posixpath import socket from functools import cache @@ -41,9 +42,17 @@ def file_exists(fn): return URLFile(fn).get_length_online() != -1 return os.path.exists(fn) +class DiskFile(io.BufferedReader): + def get_multi_range(self, ranges: list[tuple[int, int]]) -> list[bytes]: + parts = [] + for r in ranges: + self.seek(r[0]) + parts.append(self.read(r[1] - r[0])) + return parts -def FileReader(fn, debug=False): +def FileReader(fn): fn = resolve_name(fn) if fn.startswith(("http://", "https://")): - return URLFile(fn, debug=debug) - return open(fn, "rb") + return URLFile(fn) + else: + return DiskFile(open(fn, "rb")) diff --git a/tools/lib/url_file.py b/tools/lib/url_file.py index 2bf3ba8209..01c6c5dc47 100644 --- a/tools/lib/url_file.py +++ b/tools/lib/url_file.py @@ -1,13 +1,11 @@ +import re import logging import os import socket -import time from hashlib import sha256 from urllib3 import PoolManager, Retry from urllib3.response import BaseHTTPResponse from urllib3.util import Timeout -from urllib3.exceptions import MaxRetryError - from openpilot.common.utils import atomic_write from openpilot.system.hardware.hw import Paths @@ -42,12 +40,11 @@ class URLFile: URLFile._pool_manager = PoolManager(num_pools=10, maxsize=100, socket_options=socket_options, retries=retries) return URLFile._pool_manager - def __init__(self, url: str, timeout: int = 10, debug: bool = False, cache: bool | None = None): + def __init__(self, url: str, timeout: int = 10, cache: bool | None = None): self._url = url self._timeout = Timeout(connect=timeout, read=timeout) self._pos = 0 self._length: int | None = None - self._debug = debug # True by default, false if FILEREADER_CACHE is defined, but can be overwritten by the cache input self._force_download = not int(os.environ.get("FILEREADER_CACHE", "0")) if cache is not None: @@ -63,10 +60,7 @@ class URLFile: pass def _request(self, method: str, url: str, headers: dict[str, str] | None = None) -> BaseHTTPResponse: - try: - return URLFile.pool_manager().request(method, url, timeout=self._timeout, headers=headers) - except MaxRetryError as e: - raise URLFileException(f"Failed to {method} {url}: {e}") from e + return URLFile.pool_manager().request(method, url, timeout=self._timeout, headers=headers) def get_length_online(self) -> int: response = self._request('HEAD', self._url) @@ -125,39 +119,45 @@ class URLFile: return response def read_aux(self, ll: int | None = None) -> bytes: - download_range = False - headers = {} - if self._pos != 0 or ll is not None: - if ll is None: - end = self.get_length() - 1 - else: - end = min(self._pos + ll, self.get_length()) - 1 - if self._pos >= end: - return b"" - headers['Range'] = f"bytes={self._pos}-{end}" - download_range = True + if ll is None: + length = self.get_length() + if length == -1: + raise URLFileException(f"Remote file is empty or doesn't exist: {self._url}") + end = length + else: + end = self._pos + ll + data = self.get_multi_range([(self._pos, end)]) + self._pos += len(data[0]) + return data[0] - if self._debug: - t1 = time.monotonic() + def get_multi_range(self, ranges: list[tuple[int, int]]) -> list[bytes]: + # HTTP range requests are inclusive + assert all(e > s for s, e in ranges), "Range end must be greater than start" + rs = [f"{s}-{e-1}" for s, e in ranges if e > s] - response = self._request('GET', self._url, headers=headers) - ret = response.data + r = self._request("GET", self._url, headers={"Range": "bytes=" + ",".join(rs)}) + if r.status not in [200, 206]: + raise URLFileException(f"Expected 206 or 200 response {r.status} ({self._url})") - if self._debug: - t2 = time.monotonic() - if t2 - t1 > 0.1: - print(f"get {self._url} {headers!r} {t2 - t1:.3f} slow") + ctype = (r.headers.get("content-type") or "").lower() + if "multipart/byteranges" not in ctype: + return [r.data,] - response_code = response.status - if response_code == 416: # Requested Range Not Satisfiable - raise URLFileException(f"Error, range out of bounds {response_code} {headers} ({self._url}): {repr(ret)[:500]}") - if download_range and response_code != 206: # Partial Content - raise URLFileException(f"Error, requested range but got unexpected response {response_code} {headers} ({self._url}): {repr(ret)[:500]}") - if (not download_range) and response_code != 200: # OK - raise URLFileException(f"Error {response_code} {headers} ({self._url}): {repr(ret)[:500]}") + m = re.search(r'boundary="?([^";]+)"?', ctype) + if not m: + raise URLFileException(f"Missing multipart boundary ({self._url})") + boundary = m.group(1).encode() - self._pos += len(ret) - return ret + parts = [] + for chunk in r.data.split(b"--" + boundary): + if b"\r\n\r\n" not in chunk: + continue + payload = chunk.split(b"\r\n\r\n", 1)[1].rstrip(b"\r\n") + if payload and payload != b"--": + parts.append(payload) + if len(parts) != len(ranges): + raise URLFileException(f"Expected {len(ranges)} parts, got {len(parts)} ({self._url})") + return parts def seek(self, pos: int) -> None: self._pos = pos From ae402d3ac774e6ae06b58a6e920542b0daf4013f Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 2 Dec 2025 13:02:01 -0800 Subject: [PATCH 056/110] Revert "ui: speed up `mici/AugmentedRoadView` by optimizing _calc_frame_matrix caching" (#36749) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Revert "ui: speed up `mici/AugmentedRoadView` by optimizing _calc_frame_matri…" This reverts commit 10524353916f42908557a1711efd2a5b66169c7b. --- .../ui/mici/onroad/augmented_road_view.py | 23 ++++++++----------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/selfdrive/ui/mici/onroad/augmented_road_view.py b/selfdrive/ui/mici/onroad/augmented_road_view.py index f1f4e66f5a..ab55f392f7 100644 --- a/selfdrive/ui/mici/onroad/augmented_road_view.py +++ b/selfdrive/ui/mici/onroad/augmented_road_view.py @@ -138,7 +138,9 @@ class AugmentedRoadView(CameraView): self.view_from_calib = view_frame_from_device_frame.copy() self.view_from_wide_calib = view_frame_from_device_frame.copy() - self._matrix_cache_key = (0, 0, 0, 0, stream_type) + self._last_calib_time: float = 0 + self._last_rect_dims = (0.0, 0.0) + self._last_stream_type = stream_type self._cached_matrix: np.ndarray | None = None self._content_rect = rl.Rectangle() self._last_click_time = 0.0 @@ -282,19 +284,10 @@ class AugmentedRoadView(CameraView): self.view_from_wide_calib = view_frame_from_device_frame @ wide_from_device @ device_from_calib def _calc_frame_matrix(self, rect: rl.Rectangle) -> np.ndarray: - v_ego_quantized = round(ui_state.sm['carState'].vEgo, 1) - cache_key = ( - ui_state.sm.recv_frame['liveCalibration'], - int(self._content_rect.width), - int(self._content_rect.height), - self.stream_type, - v_ego_quantized - ) - - if cache_key == self._matrix_cache_key and self._cached_matrix is not None: - return self._cached_matrix - # Get camera configuration + # TODO: cache with vEgo? + calib_time = ui_state.sm.recv_frame['liveCalibration'] + current_dims = (self._content_rect.width, self._content_rect.height) device_camera = self.device_camera or DEFAULT_DEVICE_CAMERA is_wide_camera = self.stream_type == WIDE_CAM intrinsic = device_camera.ecam.intrinsics if is_wide_camera else device_camera.fcam.intrinsics @@ -330,7 +323,9 @@ class AugmentedRoadView(CameraView): x_offset, y_offset = 0, 0 # Cache the computed transformation matrix to avoid recalculations - self._matrix_cache_key = cache_key + self._last_calib_time = calib_time + self._last_rect_dims = current_dims + self._last_stream_type = self.stream_type self._cached_matrix = np.array([ [zoom * 2 * cx / w, 0, -x_offset / w * 2], [0, zoom * 2 * cy / h, -y_offset / h * 2], From ae6250e685d2e4fd659bac864572b77ab991de82 Mon Sep 17 00:00:00 2001 From: Dean Lee Date: Wed, 3 Dec 2025 05:09:24 +0800 Subject: [PATCH 057/110] ui/CameraView: use consistent 2-space indentation (#36748) use consistent 2-space indentation --- selfdrive/ui/onroad/cameraview.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/selfdrive/ui/onroad/cameraview.py b/selfdrive/ui/onroad/cameraview.py index 87db7cc636..881a916df7 100644 --- a/selfdrive/ui/onroad/cameraview.py +++ b/selfdrive/ui/onroad/cameraview.py @@ -337,12 +337,12 @@ class CameraView(Widget): self._initialize_textures() def _initialize_textures(self): - self._clear_textures() - if not TICI: - self.texture_y = rl.load_texture_from_image(rl.Image(None, int(self.client.stride), - int(self.client.height), 1, rl.PixelFormat.PIXELFORMAT_UNCOMPRESSED_GRAYSCALE)) - self.texture_uv = rl.load_texture_from_image(rl.Image(None, int(self.client.stride // 2), - int(self.client.height // 2), 1, rl.PixelFormat.PIXELFORMAT_UNCOMPRESSED_GRAY_ALPHA)) + self._clear_textures() + if not TICI: + self.texture_y = rl.load_texture_from_image(rl.Image(None, int(self.client.stride), + int(self.client.height), 1, rl.PixelFormat.PIXELFORMAT_UNCOMPRESSED_GRAYSCALE)) + self.texture_uv = rl.load_texture_from_image(rl.Image(None, int(self.client.stride // 2), + int(self.client.height // 2), 1, rl.PixelFormat.PIXELFORMAT_UNCOMPRESSED_GRAY_ALPHA)) def _clear_textures(self): if self.texture_y and self.texture_y.id: From 63563c3561a88b7d08c679387aa5ae7609ebaf67 Mon Sep 17 00:00:00 2001 From: Chechulin Serhii <78239416+keefeere@users.noreply.github.com> Date: Tue, 2 Dec 2025 23:13:13 +0200 Subject: [PATCH 058/110] ui: fix - translate display text of updater_state (#36649) * Add updater_state translation * Move STATE_TO_DISPLAY_TEXT on top --- selfdrive/ui/layouts/settings/software.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/selfdrive/ui/layouts/settings/software.py b/selfdrive/ui/layouts/settings/software.py index 4b8b7015f8..e0df8f2705 100644 --- a/selfdrive/ui/layouts/settings/software.py +++ b/selfdrive/ui/layouts/settings/software.py @@ -14,6 +14,13 @@ from openpilot.system.ui.widgets.scroller_tici import Scroller # TODO: remove this. updater fails to respond on startup if time is not correct UPDATED_TIMEOUT = 10 # seconds to wait for updated to respond +# Mapping updater internal states to translated display strings +STATE_TO_DISPLAY_TEXT = { + "checking...": tr("checking..."), + "downloading...": tr("downloading..."), + "finalizing update...": tr("finalizing update..."), +} + def time_ago(date: datetime.datetime | None) -> str: if not date: @@ -100,7 +107,9 @@ class SoftwareLayout(Widget): # Updater responded self._waiting_for_updater = False self._download_btn.action_item.set_enabled(False) - self._download_btn.action_item.set_value(updater_state) + # Use the mapping, with a fallback to the original state string + display_text = STATE_TO_DISPLAY_TEXT.get(updater_state, updater_state) + self._download_btn.action_item.set_value(display_text) else: if failed_count > 0: self._download_btn.action_item.set_value(tr("failed to check for update")) From dc02a2d3859310e7861c1a22e63d52a4ccda9d2f Mon Sep 17 00:00:00 2001 From: YassineYousfi Date: Tue, 2 Dec 2025 15:17:59 -0800 Subject: [PATCH 059/110] dm: adjust cold start pose offsets (#36739) * dm: adjust cold start offsets and thresholds * change just offsets for now --- selfdrive/monitoring/helpers.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/selfdrive/monitoring/helpers.py b/selfdrive/monitoring/helpers.py index 5b5e16dde3..1ed04e705a 100644 --- a/selfdrive/monitoring/helpers.py +++ b/selfdrive/monitoring/helpers.py @@ -47,9 +47,9 @@ class DRIVER_MONITOR_SETTINGS: self._POSE_YAW_THRESHOLD = 0.4020 self._POSE_YAW_THRESHOLD_SLACK = 0.5042 self._POSE_YAW_THRESHOLD_STRICT = self._POSE_YAW_THRESHOLD - self._PITCH_NATURAL_OFFSET = 0.029 # initial value before offset is learned + self._PITCH_NATURAL_OFFSET = 0.011 # initial value before offset is learned self._PITCH_NATURAL_THRESHOLD = 0.449 - self._YAW_NATURAL_OFFSET = 0.097 # initial value before offset is learned + self._YAW_NATURAL_OFFSET = 0.075 # initial value before offset is learned self._PITCH_MAX_OFFSET = 0.124 self._PITCH_MIN_OFFSET = -0.0881 self._YAW_MAX_OFFSET = 0.289 @@ -234,8 +234,11 @@ class DriverMonitoring: self.settings._YAW_MIN_OFFSET), self.settings._YAW_MAX_OFFSET) pitch_error = 0 if pitch_error > 0 else abs(pitch_error) # no positive pitch limit yaw_error = abs(yaw_error) - if pitch_error > (self.settings._POSE_PITCH_THRESHOLD*self.pose.cfactor_pitch if self.pose.calibrated else self.settings._PITCH_NATURAL_THRESHOLD) or \ - yaw_error > self.settings._POSE_YAW_THRESHOLD*self.pose.cfactor_yaw: + + pitch_threshold = self.settings._POSE_PITCH_THRESHOLD * self.pose.cfactor_pitch if self.pose.calibrated else self.settings._PITCH_NATURAL_THRESHOLD + yaw_threshold = self.settings._POSE_YAW_THRESHOLD * self.pose.cfactor_yaw + + if pitch_error > pitch_threshold or yaw_error > yaw_threshold: distracted_types.append(DistractedType.DISTRACTED_POSE) if (self.blink.left + self.blink.right)*0.5 > self.settings._BLINK_THRESHOLD: From 5393308d035c123cdb5a0aabb93e20af664ffd9f Mon Sep 17 00:00:00 2001 From: Bruce Wayne Date: Tue, 2 Dec 2025 15:54:21 -0800 Subject: [PATCH 060/110] Logreader: print errors --- tools/lib/logreader.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tools/lib/logreader.py b/tools/lib/logreader.py index 8d84cdbd5d..f9a90490b9 100755 --- a/tools/lib/logreader.py +++ b/tools/lib/logreader.py @@ -181,6 +181,8 @@ def auto_source(identifier: str, sources: list[Source], default_mode: ReadMode) # We've found all files, return them if len(needed_seg_idxs) == 0: return cast(list[str], list(valid_files.values())) + else: + raise FileNotFoundError(f"Did not find {fn} for seg idxs {needed_seg_idxs} of {sr.route_name}") except Exception as e: exceptions[source.__name__] = e From e7d349bf36044cf4868fffd69f71a9193a08638d Mon Sep 17 00:00:00 2001 From: Maxime Desroches Date: Tue, 2 Dec 2025 16:45:09 -0800 Subject: [PATCH 061/110] Revert "ui: restart if crash (#36746)" (#36754) This reverts commit fa18bb9261dff30f051d1e989dd253e90c78c322. --- system/manager/manager.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/system/manager/manager.py b/system/manager/manager.py index 15f8a2b793..2d80c78ff5 100755 --- a/system/manager/manager.py +++ b/system/manager/manager.py @@ -155,10 +155,6 @@ def manager_thread() -> None: print(running) cloudlog.debug(running) - if 'ui' in managed_processes and managed_processes['ui'].proc is not None and not managed_processes['ui'].proc.is_alive(): - cloudlog.error(f'Restarting UI (exitcode {managed_processes["ui"].proc.exitcode})') - managed_processes['ui'].restart() - # send managerState msg = messaging.new_message('managerState', valid=True) msg.managerState.processes = [p.get_process_state_msg() for p in managed_processes.values()] From 5fd090616419035e4f18afd0b740a504afd1d726 Mon Sep 17 00:00:00 2001 From: Maxime Desroches Date: Tue, 2 Dec 2025 17:10:24 -0800 Subject: [PATCH 062/110] allow restarting processes after crash (#36755) more --- system/manager/process.py | 7 ++++++- system/manager/process_config.py | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/system/manager/process.py b/system/manager/process.py index e6b6a44c40..1e24198267 100644 --- a/system/manager/process.py +++ b/system/manager/process.py @@ -67,6 +67,7 @@ class ManagerProcess(ABC): enabled = True name = "" shutting_down = False + restart_if_crash = False @abstractmethod def prepare(self) -> None: @@ -167,13 +168,14 @@ class NativeProcess(ManagerProcess): class PythonProcess(ManagerProcess): - def __init__(self, name, module, should_run, enabled=True, sigkill=False): + def __init__(self, name, module, should_run, enabled=True, sigkill=False, restart_if_crash=False): self.name = name self.module = module self.should_run = should_run self.enabled = enabled self.sigkill = sigkill self.launcher = launcher + self.restart_if_crash = restart_if_crash def prepare(self) -> None: if self.enabled: @@ -252,6 +254,9 @@ def ensure_running(procs: ValuesView[ManagerProcess], started: bool, params=None running = [] for p in procs: if p.enabled and p.name not in not_run and p.should_run(started, params, CP): + if p.restart_if_crash and p.proc is not None and not p.proc.is_alive(): + cloudlog.error(f'Restarting {p.name} (exitcode {p.proc.exitcode})') + p.restart() running.append(p) else: p.stop(block=False) diff --git a/system/manager/process_config.py b/system/manager/process_config.py index 8cf3e8c14c..0b99183193 100644 --- a/system/manager/process_config.py +++ b/system/manager/process_config.py @@ -80,7 +80,7 @@ procs = [ PythonProcess("dmonitoringmodeld", "selfdrive.modeld.dmonitoringmodeld", driverview, enabled=(WEBCAM or not PC)), PythonProcess("sensord", "system.sensord.sensord", only_onroad, enabled=not PC), - PythonProcess("ui", "selfdrive.ui.ui", always_run), + PythonProcess("ui", "selfdrive.ui.ui", always_run, restart_if_crash=True), PythonProcess("soundd", "selfdrive.ui.soundd", driverview), PythonProcess("locationd", "selfdrive.locationd.locationd", only_onroad), NativeProcess("_pandad", "selfdrive/pandad", ["./pandad"], always_run, enabled=False), From 83dad85cdd800d79d77ff964da72103218c12ef9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Harald=20Sch=C3=A4fer?= Date: Wed, 3 Dec 2025 12:55:33 -0800 Subject: [PATCH 063/110] Dark Souls Model (#36764) a4cf2707-3d69-49ea-af8b-f91cd3285249/400 --- selfdrive/modeld/models/driving_policy.onnx | 2 +- selfdrive/modeld/models/driving_vision.onnx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/selfdrive/modeld/models/driving_policy.onnx b/selfdrive/modeld/models/driving_policy.onnx index 1e764af9ba..ec451ba736 100644 --- a/selfdrive/modeld/models/driving_policy.onnx +++ b/selfdrive/modeld/models/driving_policy.onnx @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c5a1f0655ddf266ed42ad1980389d96f47cc5e756da1fa3ca1477a920bb9b157 +oid sha256:e2929b07deb9fb1e492c7fa2832c51ac9e472bfe0b80730fdbbe263735866580 size 13926324 diff --git a/selfdrive/modeld/models/driving_vision.onnx b/selfdrive/modeld/models/driving_vision.onnx index 441c4a16af..e5ef27810a 100644 --- a/selfdrive/modeld/models/driving_vision.onnx +++ b/selfdrive/modeld/models/driving_vision.onnx @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8f16d548ea4eb5d01518a9e90d4527cd97c31a84bcaf6f695dead8f0015fecc4 +oid sha256:2194eaee8a8c40f79a6f783d198991b1bf70a54b5885053e63789eab040a5228 size 46271942 From 7ea6cfcbdfbfe6c593c6479555fbb6d3f087b598 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Wed, 3 Dec 2025 20:00:19 -0800 Subject: [PATCH 064/110] remove unecessary function --- selfdrive/ui/mici/onroad/driver_camera_dialog.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/selfdrive/ui/mici/onroad/driver_camera_dialog.py b/selfdrive/ui/mici/onroad/driver_camera_dialog.py index f2fa5e8fe8..9179e9e463 100644 --- a/selfdrive/ui/mici/onroad/driver_camera_dialog.py +++ b/selfdrive/ui/mici/onroad/driver_camera_dialog.py @@ -28,7 +28,7 @@ class DriverCameraDialog(NavWidget): if not no_escape: # TODO: this can grow unbounded, should be given some thought device.add_interactive_timeout_callback(self.stop_dmonitoringmodeld) - self.set_back_callback(self._dismiss) + self.set_back_callback(self.stop_dmonitoringmodeld) self.set_back_enabled(not no_escape) # Load eye icons @@ -58,9 +58,6 @@ class DriverCameraDialog(NavWidget): def _handle_mouse_release(self, _): ui_state.params.remove("DriverTooDistracted") - def _dismiss(self): - self.stop_dmonitoringmodeld() - def close(self): if self._camera_view: self._camera_view.close() From 9e55577cc775be2feef780e4a573b2227ddef339 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Wed, 3 Dec 2025 20:41:58 -0800 Subject: [PATCH 065/110] Clean up DM dialog CameraView bound method (#36770) * clean up * why not? * clean up --- .../ui/mici/onroad/driver_camera_dialog.py | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/selfdrive/ui/mici/onroad/driver_camera_dialog.py b/selfdrive/ui/mici/onroad/driver_camera_dialog.py index 9179e9e463..624a659ebc 100644 --- a/selfdrive/ui/mici/onroad/driver_camera_dialog.py +++ b/selfdrive/ui/mici/onroad/driver_camera_dialog.py @@ -15,12 +15,19 @@ EventName = log.OnroadEvent.EventName EVENT_TO_INT = EventName.schema.enumerants +class DriverCameraView(CameraView): + def _calc_frame_matrix(self, rect: rl.Rectangle): + base = super()._calc_frame_matrix(rect) + driver_view_ratio = 1.5 + base[0, 0] *= driver_view_ratio + base[1, 1] *= driver_view_ratio + return base + + class DriverCameraDialog(NavWidget): def __init__(self, no_escape=False): super().__init__() - self._camera_view = CameraView("camerad", VisionStreamType.VISION_STREAM_DRIVER) - self._original_calc_frame_matrix = self._camera_view._calc_frame_matrix - self._camera_view._calc_frame_matrix = self._calc_driver_frame_matrix + self._camera_view = DriverCameraView("camerad", VisionStreamType.VISION_STREAM_DRIVER) self.driver_state_renderer = DriverStateRenderer(lines=True) self.driver_state_renderer.set_rect(rl.Rectangle(0, 0, 200, 200)) self.driver_state_renderer.load_icons() @@ -218,13 +225,6 @@ class DriverCameraDialog(NavWidget): glasses_prob = driver_data.sunglassesProb rl.draw_texture_v(self._glasses_texture, glasses_pos, rl.Color(70, 80, 161, int(255 * glasses_prob))) - def _calc_driver_frame_matrix(self, rect: rl.Rectangle): - base = self._original_calc_frame_matrix(rect) - driver_view_ratio = 1.5 - base[0, 0] *= driver_view_ratio - base[1, 1] *= driver_view_ratio - return base - if __name__ == "__main__": gui_app.init_window("Driver Camera View (mici)") From cc7dd066d211053bd24ab930955203a97ccb97fa Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Wed, 3 Dec 2025 21:55:05 -0800 Subject: [PATCH 066/110] ui: call modal hide_event (#36772) * start, not fully working since hide is called before last render * clean up --- system/ui/lib/application.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/system/ui/lib/application.py b/system/ui/lib/application.py index 79e68aa67f..26e446612d 100644 --- a/system/ui/lib/application.py +++ b/system/ui/lib/application.py @@ -339,6 +339,9 @@ class GuiApplication: def set_modal_overlay(self, overlay, callback: Callable | None = None): if self._modal_overlay.overlay is not None: + if hasattr(self._modal_overlay.overlay, 'hide_event'): + self._modal_overlay.overlay.hide_event() + if self._modal_overlay.callback is not None: self._modal_overlay.callback(-1) @@ -557,6 +560,8 @@ class GuiApplication: # Clear the overlay and execute the callback original_modal = self._modal_overlay self._modal_overlay = ModalOverlay() + if hasattr(original_modal.overlay, 'hide_event'): + original_modal.overlay.hide_event() if original_modal.callback is not None: original_modal.callback(result) return True From 4edbc7d0cf4dec949faf75cc5365d83ff5f6c388 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Wed, 3 Dec 2025 22:19:30 -0800 Subject: [PATCH 067/110] DriverCameraDialog: proper clean up (#36775) * fixes leak * wait can't do this, we need close after all * wait can't do this, we need close after all * clean up memory --- selfdrive/ui/mici/onroad/driver_camera_dialog.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/selfdrive/ui/mici/onroad/driver_camera_dialog.py b/selfdrive/ui/mici/onroad/driver_camera_dialog.py index 624a659ebc..e5399b85d3 100644 --- a/selfdrive/ui/mici/onroad/driver_camera_dialog.py +++ b/selfdrive/ui/mici/onroad/driver_camera_dialog.py @@ -34,8 +34,8 @@ class DriverCameraDialog(NavWidget): self._pm = messaging.PubMaster(['selfdriveState']) if not no_escape: # TODO: this can grow unbounded, should be given some thought - device.add_interactive_timeout_callback(self.stop_dmonitoringmodeld) - self.set_back_callback(self.stop_dmonitoringmodeld) + device.add_interactive_timeout_callback(lambda: gui_app.set_modal_overlay(None)) + self.set_back_callback(lambda: gui_app.set_modal_overlay(None)) self.set_back_enabled(not no_escape) # Load eye icons @@ -47,10 +47,6 @@ class DriverCameraDialog(NavWidget): self._load_eye_textures() - def stop_dmonitoringmodeld(self): - ui_state.params.put_bool("IsDriverViewEnabled", False) - gui_app.set_modal_overlay(None) - def show_event(self): super().show_event() ui_state.params.put_bool("IsDriverViewEnabled", True) @@ -60,11 +56,15 @@ class DriverCameraDialog(NavWidget): def hide_event(self): super().hide_event() + ui_state.params.put_bool("IsDriverViewEnabled", False) device.reset_interactive_timeout() def _handle_mouse_release(self, _): ui_state.params.remove("DriverTooDistracted") + def __del__(self): + self.close() + def close(self): if self._camera_view: self._camera_view.close() From 93f2076c7e7ac1274cfdc7b39567fd1319395e6b Mon Sep 17 00:00:00 2001 From: Dean Lee Date: Thu, 4 Dec 2025 17:54:11 +0800 Subject: [PATCH 068/110] ui: fix crash caused by double shader unload in CameraView (#36778) fix double free isuue --- selfdrive/ui/mici/onroad/cameraview.py | 1 + 1 file changed, 1 insertion(+) diff --git a/selfdrive/ui/mici/onroad/cameraview.py b/selfdrive/ui/mici/onroad/cameraview.py index 995c4618f7..f3e0ef409e 100644 --- a/selfdrive/ui/mici/onroad/cameraview.py +++ b/selfdrive/ui/mici/onroad/cameraview.py @@ -197,6 +197,7 @@ class CameraView(Widget): # Clean up shader if self.shader and self.shader.id: rl.unload_shader(self.shader) + self.shader.id = 0 self.frame = None self.available_streams.clear() From 45b7d60263da75363a3a3fa8caff1532da1e384c Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 4 Dec 2025 01:56:38 -0800 Subject: [PATCH 069/110] ui: fix dialog memory leak (#36767) * weakref alternative * and here * clean up * fix * rm --- selfdrive/ui/mici/widgets/dialog.py | 10 ++++++++++ system/ui/widgets/__init__.py | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/selfdrive/ui/mici/widgets/dialog.py b/selfdrive/ui/mici/widgets/dialog.py index b11056f993..26845765c3 100644 --- a/selfdrive/ui/mici/widgets/dialog.py +++ b/selfdrive/ui/mici/widgets/dialog.py @@ -40,6 +40,11 @@ class BigDialogBase(NavWidget, abc.ABC): # move to right side self._right_btn._rect.x = self._rect.x + self._rect.width - self._right_btn._rect.width + def hide_event(self): + # Free reference to self to allow refcount to go to zero + super().hide_event() + self.set_back_callback(None) + def _render(self, _) -> DialogResult: """ Allows `gui_app.set_modal_overlay(BigDialog(...))`. @@ -163,6 +168,11 @@ class BigInputDialog(BigDialogBase): confirm_callback(self._keyboard.text()) self._confirm_callback = confirm_callback_wrapper + def hide_event(self): + # Free reference to self to allow refcount to go to zero + super().hide_event() + self._confirm_callback = None + def _update_state(self): super()._update_state() diff --git a/system/ui/widgets/__init__.py b/system/ui/widgets/__init__.py index 546c682f33..efa35bc016 100644 --- a/system/ui/widgets/__init__.py +++ b/system/ui/widgets/__init__.py @@ -252,7 +252,7 @@ class NavWidget(Widget, abc.ABC): def set_back_enabled(self, enabled: bool | Callable[[], bool]) -> None: self._back_enabled = enabled - def set_back_callback(self, callback: Callable[[], None]) -> None: + def set_back_callback(self, callback: Callable[[], None] | None) -> None: self._back_callback = callback def _handle_mouse_event(self, mouse_event: MouseEvent) -> None: From cd9b08492ec3c8b840edc53656ef70dc3760ae90 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 4 Dec 2025 02:00:45 -0800 Subject: [PATCH 070/110] ui: small TrainingGuide clean up --- selfdrive/ui/mici/layouts/onboarding.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/selfdrive/ui/mici/layouts/onboarding.py b/selfdrive/ui/mici/layouts/onboarding.py index 52dbb785d6..a196d20c4a 100644 --- a/selfdrive/ui/mici/layouts/onboarding.py +++ b/selfdrive/ui/mici/layouts/onboarding.py @@ -92,11 +92,10 @@ class TrainingGuideDMTutorial(Widget): super().__init__() self._title_header = TermsHeader("fill the circle to continue", gui_app.texture("icons_mici/setup/green_dm.png", 60, 60)) - self._original_continue_callback = continue_callback - # Wrap the continue callback to restore settings def wrapped_continue_callback(): - self._restore_settings() + device.set_offroad_brightness(None) + device.reset_interactive_timeout() continue_callback() self._dialog = DriverCameraSetupDialog(wrapped_continue_callback) @@ -114,10 +113,6 @@ class TrainingGuideDMTutorial(Widget): device.set_offroad_brightness(100) device.reset_interactive_timeout(300) # 5 minutes - def _restore_settings(self): - device.set_offroad_brightness(None) - device.reset_interactive_timeout() - def _update_state(self): super()._update_state() if device.awake: From 2947af42fc8f44b5bd0cdf290e01840c89a031b1 Mon Sep 17 00:00:00 2001 From: Dean Lee Date: Thu, 4 Dec 2025 18:23:37 +0800 Subject: [PATCH 071/110] ui: fix TraningGuide leak (#36763) * fix TraningGuide leak * other thing * this is truly the simplest way --------- Co-authored-by: Shane Smiskol --- selfdrive/ui/mici/layouts/onboarding.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/selfdrive/ui/mici/layouts/onboarding.py b/selfdrive/ui/mici/layouts/onboarding.py index a196d20c4a..abf772ce58 100644 --- a/selfdrive/ui/mici/layouts/onboarding.py +++ b/selfdrive/ui/mici/layouts/onboarding.py @@ -1,6 +1,7 @@ from enum import IntEnum from collections.abc import Callable +import weakref import pyray as rl from openpilot.system.ui.lib.application import FontWeight, gui_app from openpilot.system.ui.widgets import Widget @@ -209,11 +210,17 @@ class TrainingGuide(Widget): self._completed_callback = completed_callback self._step = 0 + self_ref = weakref.ref(self) + + def on_continue(): + if obj := self_ref(): + obj._advance_step() + self._steps = [ - TrainingGuideAttentionNotice(continue_callback=self._advance_step), - TrainingGuidePreDMTutorial(continue_callback=self._advance_step), - TrainingGuideDMTutorial(continue_callback=self._advance_step), - TrainingGuideRecordFront(continue_callback=self._advance_step), + TrainingGuideAttentionNotice(continue_callback=on_continue), + TrainingGuidePreDMTutorial(continue_callback=on_continue), + TrainingGuideDMTutorial(continue_callback=on_continue), + TrainingGuideRecordFront(continue_callback=on_continue), ] def _advance_step(self): From f962a36fd8e0a186f0fe15232744e916857cc794 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 4 Dec 2025 02:39:41 -0800 Subject: [PATCH 072/110] Fix ui crashing replay/selfdrived (#36760) * fix * clean up * type hint --- selfdrive/ui/mici/onroad/driver_camera_dialog.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/selfdrive/ui/mici/onroad/driver_camera_dialog.py b/selfdrive/ui/mici/onroad/driver_camera_dialog.py index e5399b85d3..9adb660d8b 100644 --- a/selfdrive/ui/mici/onroad/driver_camera_dialog.py +++ b/selfdrive/ui/mici/onroad/driver_camera_dialog.py @@ -31,7 +31,7 @@ class DriverCameraDialog(NavWidget): self.driver_state_renderer = DriverStateRenderer(lines=True) self.driver_state_renderer.set_rect(rl.Rectangle(0, 0, 200, 200)) self.driver_state_renderer.load_icons() - self._pm = messaging.PubMaster(['selfdriveState']) + self._pm: messaging.PubMaster | None = None if not no_escape: # TODO: this can grow unbounded, should be given some thought device.add_interactive_timeout_callback(lambda: gui_app.set_modal_overlay(None)) @@ -53,6 +53,7 @@ class DriverCameraDialog(NavWidget): self._publish_alert_sound(None) device.reset_interactive_timeout(300) ui_state.params.remove("DriverTooDistracted") + self._pm = messaging.PubMaster(['selfdriveState']) def hide_event(self): super().hide_event() @@ -107,6 +108,9 @@ class DriverCameraDialog(NavWidget): def _publish_alert_sound(self, dm_state): """Publish selfdriveState with only alertSound field set""" + if self._pm is None: + return + msg = messaging.new_message('selfdriveState') if dm_state is not None and len(dm_state.events): event_name = EVENT_TO_INT[dm_state.events[0].name] From e72e5d4ebeb926a3b62ba3cd8c9cb0d1b3332524 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Harald=20Sch=C3=A4fer?= Date: Thu, 4 Dec 2025 13:11:27 -0800 Subject: [PATCH 073/110] beeps in key (#36765) beeps in keyt --- selfdrive/assets/sounds/disengage.wav | 4 ++-- selfdrive/assets/sounds/engage.wav | 4 ++-- selfdrive/assets/sounds/make_beeps.py | 19 +++++++++++++++++++ 3 files changed, 23 insertions(+), 4 deletions(-) create mode 100644 selfdrive/assets/sounds/make_beeps.py diff --git a/selfdrive/assets/sounds/disengage.wav b/selfdrive/assets/sounds/disengage.wav index 8983884b25..7bfd97ad71 100644 --- a/selfdrive/assets/sounds/disengage.wav +++ b/selfdrive/assets/sounds/disengage.wav @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c94582be9d921146b3c356e08a7352700c309cb407877c1180542811b2d637fa -size 48078 +oid sha256:42bd04a57b527c787a0555503e02a203f7d672c12d448769a3f41f17befbf013 +size 48044 diff --git a/selfdrive/assets/sounds/engage.wav b/selfdrive/assets/sounds/engage.wav index 39d4c749c8..8633b5ac2d 100644 --- a/selfdrive/assets/sounds/engage.wav +++ b/selfdrive/assets/sounds/engage.wav @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bc2b12bfe816a79307660b6b3d2de87a7643c6ccbfc9d1b33804645ad717682a -size 48078 +oid sha256:b1e177499d9439367179cc57a6301b6162393972e3a136cc35c5fdac026bf10a +size 48044 diff --git a/selfdrive/assets/sounds/make_beeps.py b/selfdrive/assets/sounds/make_beeps.py new file mode 100644 index 0000000000..6161e80e74 --- /dev/null +++ b/selfdrive/assets/sounds/make_beeps.py @@ -0,0 +1,19 @@ +import numpy as np +from scipy.io import wavfile + + +sr = 48000 +max_int16 = 2**15 - 1 + +def harmonic_beep(freq, duration_seconds): + n_total = int(sr * duration_seconds) + + signal = np.sin(2 * np.pi * freq * np.arange(n_total) / sr) + x = np.arange(n_total) + exp_scale = np.exp(-x/5.5e3) + return max_int16 * signal * exp_scale + +engage_beep = harmonic_beep(1661.219, 0.5) +wavfile.write("engage.wav", sr, engage_beep.astype(np.int16)) +disengage_beep = harmonic_beep(1318.51, 0.5) +wavfile.write("disengage.wav", sr, disengage_beep.astype(np.int16)) From 224e2c271be888e80c1df9728cd448ca1e0a8702 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 4 Dec 2025 16:51:18 -0800 Subject: [PATCH 074/110] Revert "ui: fix dialog memory leak" (#36787) Revert "ui: fix dialog memory leak (#36767)" This reverts commit 45b7d60263da75363a3a3fa8caff1532da1e384c. --- selfdrive/ui/mici/widgets/dialog.py | 10 ---------- system/ui/widgets/__init__.py | 2 +- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/selfdrive/ui/mici/widgets/dialog.py b/selfdrive/ui/mici/widgets/dialog.py index 26845765c3..b11056f993 100644 --- a/selfdrive/ui/mici/widgets/dialog.py +++ b/selfdrive/ui/mici/widgets/dialog.py @@ -40,11 +40,6 @@ class BigDialogBase(NavWidget, abc.ABC): # move to right side self._right_btn._rect.x = self._rect.x + self._rect.width - self._right_btn._rect.width - def hide_event(self): - # Free reference to self to allow refcount to go to zero - super().hide_event() - self.set_back_callback(None) - def _render(self, _) -> DialogResult: """ Allows `gui_app.set_modal_overlay(BigDialog(...))`. @@ -168,11 +163,6 @@ class BigInputDialog(BigDialogBase): confirm_callback(self._keyboard.text()) self._confirm_callback = confirm_callback_wrapper - def hide_event(self): - # Free reference to self to allow refcount to go to zero - super().hide_event() - self._confirm_callback = None - def _update_state(self): super()._update_state() diff --git a/system/ui/widgets/__init__.py b/system/ui/widgets/__init__.py index efa35bc016..546c682f33 100644 --- a/system/ui/widgets/__init__.py +++ b/system/ui/widgets/__init__.py @@ -252,7 +252,7 @@ class NavWidget(Widget, abc.ABC): def set_back_enabled(self, enabled: bool | Callable[[], bool]) -> None: self._back_enabled = enabled - def set_back_callback(self, callback: Callable[[], None] | None) -> None: + def set_back_callback(self, callback: Callable[[], None]) -> None: self._back_callback = callback def _handle_mouse_event(self, mouse_event: MouseEvent) -> None: From 0965650f6109dc54f4a81c882b9c6511f3cc433a Mon Sep 17 00:00:00 2001 From: Robbe Derks Date: Fri, 5 Dec 2025 23:12:39 +0100 Subject: [PATCH 075/110] Bump panda (#36783) * panda bump * try this one * this breaks it? * still broken, right? * fixed? * second try --- panda | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/panda b/panda index 615009cf0f..1ffad74f88 160000 --- a/panda +++ b/panda @@ -1 +1 @@ -Subproject commit 615009cf0f8fb8f3feadac160fbb0a07e4de171b +Subproject commit 1ffad74f88e5683d9cd7c472e823928e28037e9e From d4d6134d3b825c21013f48e1c0af3867eae9652c Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 5 Dec 2025 18:18:58 -0800 Subject: [PATCH 076/110] UnifiedLabel: fix clipping descenders (#36793) * fix * can also do this * but then y is off. this is from font_scale I think * fix * cmt --- system/ui/widgets/label.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/system/ui/widgets/label.py b/system/ui/widgets/label.py index 91f05c3551..97b293083d 100644 --- a/system/ui/widgets/label.py +++ b/system/ui/widgets/label.py @@ -712,8 +712,9 @@ class UnifiedLabel(Widget): start_y = self._rect.y + (self._rect.height - total_visible_height) / 2 # Only scissor when we know there is a single scrolling line + # Pad a little since descenders like g or j may overflow below rect from font_scale if self._needs_scroll: - rl.begin_scissor_mode(int(self._rect.x), int(self._rect.y), int(self._rect.width), int(self._rect.height)) + rl.begin_scissor_mode(int(self._rect.x), int(self._rect.y - self._font_size / 2), int(self._rect.width), int(self._rect.height + self._font_size)) # Render each line current_y = start_y From 239d690a4336c1dd6f8e9f202bc035ecbf236f1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=80=8C=20crwusiz=20=E3=80=8D?= <43285072+crwusiz@users.noreply.github.com> Date: Tue, 9 Dec 2025 09:32:56 +0900 Subject: [PATCH 077/110] Multilang: update kor translation (#36795) --- selfdrive/ui/translations/app_ko.po | 56 ++++++++++++++--------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/selfdrive/ui/translations/app_ko.po b/selfdrive/ui/translations/app_ko.po index 5a3e891b87..f12aebaeb3 100644 --- a/selfdrive/ui/translations/app_ko.po +++ b/selfdrive/ui/translations/app_ko.po @@ -68,10 +68,10 @@ msgid "" "control alpha. Changing this setting will restart openpilot if the car is " "powered on." msgstr "" -"경고: 이 차량에서 openpilot의 종방향 제어는 알파 버전이며 자동 긴급 제동" -"(AEB)을 비활성화합니다.

이 차량에서는 openpilot 종방향 제어 대신 " -"차량 내장 ACC가 기본으로 사용됩니다. openpilot 종방향 제어로 전환하려면 이 설" -"정을 켜세요. 종방향 제어 알파를 켤 때는 실험 모드 사용을 권장합니다. 차량 전" +"경고: 이 차량에서 openpilot의 롱컨 제어는 알파 버전이며 자동 긴급 제동" +"(AEB)을 비활성화합니다.

이 차량에서는 openpilot 롱컨 제어 대신 " +"차량 내장 ACC가 기본으로 사용됩니다. openpilot 롱컨 제어로 전환하려면 이 설" +"정을 켜세요. 롱컨 제어 알파를 켤 때는 실험 모드 사용을 권장합니다. 차량 전" "원이 켜져 있는 경우 이 설정을 변경하면 openpilot이 재시작됩니다." #: selfdrive/ui/layouts/settings/device.py:148 @@ -130,7 +130,7 @@ msgstr "동의" #: selfdrive/ui/layouts/settings/toggles.py:70 #, python-format msgid "Always-On Driver Monitoring" -msgstr "항상 켜짐 운전자 모니터링" +msgstr "운전자 모니터링 항상 켜짐" #: selfdrive/ui/layouts/settings/toggles.py:186 #, python-format @@ -138,7 +138,7 @@ msgid "" "An alpha version of openpilot longitudinal control can be tested, along with " "Experimental mode, on non-release branches." msgstr "" -"openpilot 종방향 제어 알파 버전은 실험 모드와 함께 비릴리스 브랜치에서 테스트" +"openpilot 롱컨 제어 알파 버전은 실험 모드와 함께 비릴리스 브랜치에서 테스트" "할 수 있습니다." #: selfdrive/ui/layouts/settings/device.py:187 @@ -192,7 +192,7 @@ msgstr "확인" #: selfdrive/ui/widgets/exp_mode_button.py:50 #, python-format msgid "CHILL MODE ON" -msgstr "칠 모드 켜짐" +msgstr "안정적 모드 켜짐" #: system/ui/widgets/network.py:155 selfdrive/ui/layouts/sidebar.py:73 #: selfdrive/ui/layouts/sidebar.py:134 selfdrive/ui/layouts/sidebar.py:136 @@ -283,7 +283,7 @@ msgstr "해제 후 재시작" #: selfdrive/ui/layouts/settings/device.py:103 #, python-format msgid "Disengage to Reset Calibration" -msgstr "해제 후 보정 재설정" +msgstr "해제 후 캘리브레이션 재설정" #: selfdrive/ui/layouts/settings/toggles.py:32 msgid "Display speed in km/h instead of mph." @@ -372,7 +372,7 @@ msgstr "openpilot 사용" msgid "" "Enable the openpilot longitudinal control (alpha) toggle to allow " "Experimental mode." -msgstr "실험 모드를 사용하려면 openpilot 종방향 제어(알파) 토글을 켜세요." +msgstr "실험 모드를 사용하려면 openpilot 롱컨 제어(알파) 토글을 켜세요." #: system/ui/widgets/network.py:204 #, python-format @@ -415,7 +415,7 @@ msgid "" "Experimental mode is currently unavailable on this car since the car's stock " "ACC is used for longitudinal control." msgstr "" -"이 차량은 종방향 제어에 순정 ACC를 사용하므로 현재 실험 모드를 사용할 수 없습" +"이 차량은 롱컨 제어에 순정 ACC를 사용하므로 현재 실험 모드를 사용할 수 없습" "니다." #: system/ui/widgets/network.py:373 @@ -430,11 +430,11 @@ msgstr "설정 완료" #: selfdrive/ui/layouts/settings/settings.py:66 msgid "Firehose" -msgstr "Firehose" +msgstr "파이어호스" #: selfdrive/ui/layouts/settings/firehose.py:18 msgid "Firehose Mode" -msgstr "Firehose 모드" +msgstr "파이어호스 모드" #: selfdrive/ui/layouts/settings/firehose.py:25 msgid "" @@ -462,7 +462,7 @@ msgstr "" "최대의 효과를 위해 주 1회는 장치를 실내로 가져와 품질 좋은 USB‑C 어댑터와 " "Wi‑Fi에 연결하세요.\n" "\n" -"핫스팟이나 무제한 SIM에 연결되어 있다면 주행 중에도 Firehose 모드가 동작합니" +"핫스팟이나 무제한 SIM에 연결되어 있다면 주행 중에도 파이어호스 모드가 동작합니" "다.\n" "\n" "\n" @@ -470,7 +470,7 @@ msgstr "" "\n" "어떻게, 어디서 운전하는지가 중요한가요? 아니요. 평소처럼 운전하세요.\n" "\n" -"Firehose 모드에서 모든 세그먼트가 가져가지나요? 아니요. 일부 세그먼트만 선택" +"파이어호스 모드에서 모든 구간을 가져가지나요? 아니요. 일부 구간만 선택" "적으로 가져갑니다.\n" "\n" "좋은 USB‑C 어댑터는 무엇인가요? 빠른 휴대폰 또는 노트북 충전기면 충분합니" @@ -544,7 +544,7 @@ msgstr "LTE" #: selfdrive/ui/layouts/settings/developer.py:64 #, python-format msgid "Longitudinal Maneuver Mode" -msgstr "종방향 매뉴버 모드" +msgstr "롱컨 기동 모드" #: selfdrive/ui/onroad/hud_renderer.py:148 #, python-format @@ -623,7 +623,7 @@ msgstr "미리보기" #: selfdrive/ui/widgets/prime.py:44 #, python-format msgid "PRIME FEATURES:" -msgstr "prime 기능:" +msgstr "프라임 기능:" #: selfdrive/ui/layouts/settings/device.py:48 #, python-format @@ -646,7 +646,7 @@ msgid "" "Pair your device with comma connect (connect.comma.ai) and claim your comma " "prime offer." msgstr "" -"장치를 comma connect(connect.comma.ai)와 페어링하고 comma prime 혜택을 받으세" +"장치를 comma connect(connect.comma.ai)와 페어링하고 comma 프라임 혜택을 받으세" "요." #: selfdrive/ui/widgets/setup.py:91 @@ -748,7 +748,7 @@ msgstr "규제 정보" #: selfdrive/ui/layouts/settings/toggles.py:98 #, python-format msgid "Relaxed" -msgstr "편안함" +msgstr "편안한" #: selfdrive/ui/widgets/prime.py:47 #, python-format @@ -773,7 +773,7 @@ msgstr "재설정" #: selfdrive/ui/layouts/settings/device.py:51 #, python-format msgid "Reset Calibration" -msgstr "보정 재설정" +msgstr "캘리브레이션 재설정" #: selfdrive/ui/layouts/settings/device.py:65 #, python-format @@ -841,7 +841,7 @@ msgid "" "cycle through these personalities with your steering wheel distance button." msgstr "" "표준을 권장합니다. 공격적 모드에서는 앞차를 더 가깝게 따라가고 가감속이 더 적" -"극적입니다. 편안함 모드에서는 앞차와 거리를 더 둡니다. 지원 차량에서는 스티어" +"극적입니다. 편안한 모드에서는 앞차와 거리를 더 둡니다. 지원 차량에서는 스티어" "링의 차간 버튼으로 이 성향들을 전환할 수 있습니다." #: selfdrive/ui/onroad/alert_renderer.py:59 @@ -892,7 +892,7 @@ msgstr "제거" #: selfdrive/ui/layouts/sidebar.py:117 msgid "Unknown" -msgstr "알 수 없음" +msgstr "알수없음" #: selfdrive/ui/layouts/settings/software.py:48 #, python-format @@ -994,7 +994,7 @@ msgstr "카메라 시작 중" #: selfdrive/ui/widgets/prime.py:63 #, python-format msgid "comma prime" -msgstr "comma prime" +msgstr "comma 프라임" #: system/ui/widgets/network.py:142 #, python-format @@ -1054,7 +1054,7 @@ msgstr "지금" #: selfdrive/ui/layouts/settings/developer.py:71 #, python-format msgid "openpilot Longitudinal Control (Alpha)" -msgstr "openpilot 종방향 제어(알파)" +msgstr "openpilot 롱컨 제어(알파)" #: selfdrive/ui/onroad/alert_renderer.py:51 #, python-format @@ -1076,9 +1076,9 @@ msgid "" "some turns. The Experimental mode logo will also be shown in the top right " "corner." msgstr "" -"openpilot은 기본적으로 칠 모드로 주행합니다. 실험 모드를 사용하면 칠 모드에 " +"openpilot은 기본적으로 안정적 모드로 주행합니다. 실험 모드를 사용하면 안정적 모드에 " "아직 준비되지 않은 알파 수준의 기능이 활성화됩니다. 실험 기능은 아래와 같습니" -"다:

엔드투엔드 종방향 제어


주행 모델이 가속과 제동을 제어합니" +"다:

엔드투엔드 롱컨 제어


주행 모델이 가속과 제동을 제어합니" "다. openpilot은 빨간 신호 및 정지 표지에서의 정지를 포함해 사람이 운전한다고 " "판단하는 방식으로 주행합니다. 주행 속도는 모델이 결정하므로 설정 속도는 상한" "으로만 동작합니다. 알파 품질 기능이므로 오작동이 발생할 수 있습니다.

" @@ -1111,7 +1111,7 @@ msgstr "" #: selfdrive/ui/layouts/settings/toggles.py:183 #, python-format msgid "openpilot longitudinal control may come in a future update." -msgstr "openpilot 종방향 제어는 향후 업데이트에서 제공될 수 있습니다." +msgstr "openpilot 롱컨 제어는 향후 업데이트에서 제공될 수 있습니다." #: selfdrive/ui/layouts/settings/device.py:26 msgid "" @@ -1177,7 +1177,7 @@ msgstr[0] "{}분 전" #, python-format msgid "{} segment of your driving is in the training dataset so far." msgid_plural "{} segments of your driving is in the training dataset so far." -msgstr[0] "현재까지 귀하의 주행 {}세그먼트가 학습 데이터셋에 포함되었습니다." +msgstr[0] "현재까지 귀하의 주행 {}구간이 학습 데이터셋에 포함되었습니다." #: selfdrive/ui/widgets/prime.py:62 #, python-format @@ -1187,4 +1187,4 @@ msgstr "✓ 구독됨" #: selfdrive/ui/widgets/setup.py:22 #, python-format msgid "🔥 Firehose Mode 🔥" -msgstr "🔥 Firehose 모드 🔥" +msgstr "🔥 파이어호스 모드 🔥" From a6645a1be189ee189517982939b1f964b8d4aa38 Mon Sep 17 00:00:00 2001 From: Dean Lee Date: Tue, 9 Dec 2025 08:36:35 +0800 Subject: [PATCH 078/110] cabana: add automatic session save/restore (#36736) adds auto session save/store --- tools/cabana/binaryview.cc | 5 +--- tools/cabana/chart/chartswidget.cc | 26 +++++++++++++++++++ tools/cabana/chart/chartswidget.h | 2 ++ tools/cabana/dbc/dbc.h | 6 +++++ tools/cabana/detailwidget.cc | 40 +++++++++++++++++++++++++----- tools/cabana/detailwidget.h | 7 +++++- tools/cabana/mainwin.cc | 36 +++++++++++++++++++++++++++ tools/cabana/mainwin.h | 2 ++ tools/cabana/settings.cc | 4 +++ tools/cabana/settings.h | 6 +++++ 10 files changed, 123 insertions(+), 11 deletions(-) diff --git a/tools/cabana/binaryview.cc b/tools/cabana/binaryview.cc index eb0af5b64a..b5a68c6b26 100644 --- a/tools/cabana/binaryview.cc +++ b/tools/cabana/binaryview.cc @@ -275,16 +275,13 @@ void BinaryViewModel::refresh() { row_count = can->lastMessage(msg_id).dat.size(); items.resize(row_count * column_count); } - int valid_rows = std::min(can->lastMessage(msg_id).dat.size(), row_count); - for (int i = 0; i < valid_rows * column_count; ++i) { - items[i].valid = true; - } endResetModel(); updateState(); } void BinaryViewModel::updateItem(int row, int col, uint8_t val, const QColor &color) { auto &item = items[row * column_count + col]; + item.valid = true; if (item.val != val || item.bg_color != color) { item.val = val; item.bg_color = color; diff --git a/tools/cabana/chart/chartswidget.cc b/tools/cabana/chart/chartswidget.cc index 3e9e452b90..aba25dcf83 100644 --- a/tools/cabana/chart/chartswidget.cc +++ b/tools/cabana/chart/chartswidget.cc @@ -322,6 +322,32 @@ void ChartsWidget::splitChart(ChartView *src_chart) { } } +QStringList ChartsWidget::serializeChartIds() const { + QStringList chart_ids; + for (auto c : charts) { + QStringList ids; + for (const auto& s : c->sigs) + ids += QString("%1|%2").arg(s.msg_id.toString(), s.sig->name); + chart_ids += ids.join(','); + } + std::reverse(chart_ids.begin(), chart_ids.end()); + return chart_ids; +} + +void ChartsWidget::restoreChartsFromIds(const QStringList& chart_ids) { + for (const auto& chart_id : chart_ids) { + int index = 0; + for (const auto& part : chart_id.split(',')) { + const auto sig_parts = part.split('|'); + if (sig_parts.size() != 2) continue; + MessageId msg_id = MessageId::fromString(sig_parts[0]); + if (auto* msg = dbc()->msg(msg_id)) + if (auto* sig = msg->sig(sig_parts[1])) + showChart(msg_id, sig, true, index++ > 0); + } + } +} + void ChartsWidget::setColumnCount(int n) { n = std::clamp(n, 1, MAX_COLUMN_COUNT); if (column_count != n) { diff --git a/tools/cabana/chart/chartswidget.h b/tools/cabana/chart/chartswidget.h index 46e7f546b0..f87b1276c5 100644 --- a/tools/cabana/chart/chartswidget.h +++ b/tools/cabana/chart/chartswidget.h @@ -43,6 +43,8 @@ public: ChartsWidget(QWidget *parent = nullptr); void showChart(const MessageId &id, const cabana::Signal *sig, bool show, bool merge); inline bool hasSignal(const MessageId &id, const cabana::Signal *sig) { return findChart(id, sig) != nullptr; } + QStringList serializeChartIds() const; + void restoreChartsFromIds(const QStringList &chart_ids); public slots: void setColumnCount(int n); diff --git a/tools/cabana/dbc/dbc.h b/tools/cabana/dbc/dbc.h index d2b25bc5f2..134d88a919 100644 --- a/tools/cabana/dbc/dbc.h +++ b/tools/cabana/dbc/dbc.h @@ -20,6 +20,12 @@ struct MessageId { return QString("%1:%2").arg(source).arg(QString::number(address, 16).toUpper()); } + inline static MessageId fromString(const QString &str) { + auto parts = str.split(':'); + if (parts.size() != 2) return {}; + return MessageId{.source = uint8_t(parts[0].toUInt()), .address = parts[1].toUInt(nullptr, 16)}; + } + bool operator==(const MessageId &other) const { return source == other.source && address == other.address; } diff --git a/tools/cabana/detailwidget.cc b/tools/cabana/detailwidget.cc index 4eda46f37b..35492c8efa 100644 --- a/tools/cabana/detailwidget.cc +++ b/tools/cabana/detailwidget.cc @@ -118,10 +118,7 @@ void DetailWidget::showTabBarContextMenu(const QPoint &pt) { } } -void DetailWidget::setMessage(const MessageId &message_id) { - if (std::exchange(msg_id, message_id) == message_id) return; - - tabbar->blockSignals(true); +int DetailWidget::findOrAddTab(const MessageId& message_id) { int index = tabbar->count() - 1; for (/**/; index >= 0; --index) { if (tabbar->tabData(index).value() == message_id) break; @@ -131,6 +128,14 @@ void DetailWidget::setMessage(const MessageId &message_id) { tabbar->setTabData(index, QVariant::fromValue(message_id)); tabbar->setTabToolTip(index, msgName(message_id)); } + return index; +} + +void DetailWidget::setMessage(const MessageId &message_id) { + if (std::exchange(msg_id, message_id) == message_id) return; + + tabbar->blockSignals(true); + int index = findOrAddTab(message_id); tabbar->setCurrentIndex(index); tabbar->blockSignals(false); @@ -142,6 +147,29 @@ void DetailWidget::setMessage(const MessageId &message_id) { setUpdatesEnabled(true); } +std::pair DetailWidget::serializeMessageIds() const { + QStringList msgs; + for (int i = 0; i < tabbar->count(); ++i) { + MessageId id = tabbar->tabData(i).value(); + msgs.append(id.toString()); + } + return std::make_pair(msg_id.toString(), msgs); +} + +void DetailWidget::restoreTabs(const QString active_msg_id, const QStringList& msg_ids) { + tabbar->blockSignals(true); + for (const auto& str_id : msg_ids) { + MessageId id = MessageId::fromString(str_id); + if (dbc()->msg(id) != nullptr) + findOrAddTab(id); + } + tabbar->blockSignals(false); + + auto active_id = MessageId::fromString(active_msg_id); + if (dbc()->msg(active_id) != nullptr) + setMessage(active_id); +} + void DetailWidget::refresh() { QStringList warnings; auto msg = dbc()->msg(msg_id); @@ -244,13 +272,13 @@ CenterWidget::CenterWidget(QWidget *parent) : QWidget(parent) { main_layout->addWidget(welcome_widget = createWelcomeWidget()); } -void CenterWidget::setMessage(const MessageId &msg_id) { +DetailWidget* CenterWidget::ensureDetailWidget() { if (!detail_widget) { delete welcome_widget; welcome_widget = nullptr; layout()->addWidget(detail_widget = new DetailWidget(((MainWindow*)parentWidget())->charts_widget, this)); } - detail_widget->setMessage(msg_id); + return detail_widget; } void CenterWidget::clear() { diff --git a/tools/cabana/detailwidget.h b/tools/cabana/detailwidget.h index 6df164b442..0fe1535c7a 100644 --- a/tools/cabana/detailwidget.h +++ b/tools/cabana/detailwidget.h @@ -34,9 +34,12 @@ public: DetailWidget(ChartsWidget *charts, QWidget *parent); void setMessage(const MessageId &message_id); void refresh(); + std::pair serializeMessageIds() const; + void restoreTabs(const QString active_msg_id, const QStringList &msg_ids); private: void createToolBar(); + int findOrAddTab(const MessageId& message_id); void showTabBarContextMenu(const QPoint &pt); void editMsg(); void removeMsg(); @@ -60,7 +63,9 @@ class CenterWidget : public QWidget { Q_OBJECT public: CenterWidget(QWidget *parent); - void setMessage(const MessageId &msg_id); + void setMessage(const MessageId &message_id) { ensureDetailWidget()->setMessage(message_id); } + DetailWidget* getDetailWidget() { return detail_widget; } + DetailWidget* ensureDetailWidget(); void clear(); private: diff --git a/tools/cabana/mainwin.cc b/tools/cabana/mainwin.cc index d65fc5b760..2d070acff6 100644 --- a/tools/cabana/mainwin.cc +++ b/tools/cabana/mainwin.cc @@ -235,6 +235,8 @@ void MainWindow::DBCFileChanged() { title.push_back(tr("(%1) %2").arg(toString(dbc()->sources(f)), f->name())); } setWindowFilePath(title.join(" | ")); + + QTimer::singleShot(0, this, &::MainWindow::restoreSessionState); } void MainWindow::selectAndOpenStream() { @@ -563,6 +565,7 @@ void MainWindow::closeEvent(QCloseEvent *event) { settings.message_header_state = messages_widget->saveHeaderState(); } + saveSessionState(); QWidget::closeEvent(event); } @@ -607,6 +610,39 @@ void MainWindow::toggleFullScreen() { } } +void MainWindow::saveSessionState() { + settings.recent_dbc_file = ""; + settings.active_msg_id = ""; + settings.selected_msg_ids.clear(); + settings.active_charts.clear(); + + for (auto &f : dbc()->allDBCFiles()) + if (!f->isEmpty()) { settings.recent_dbc_file = f->filename; break; } + + if (auto *detail = center_widget->getDetailWidget()) { + auto [active_id, ids] = detail->serializeMessageIds(); + settings.active_msg_id = active_id; + settings.selected_msg_ids = ids; + } + if (charts_widget) + settings.active_charts = charts_widget->serializeChartIds(); +} + +void MainWindow::restoreSessionState() { + if (settings.recent_dbc_file.isEmpty() || dbc()->nonEmptyDBCCount() == 0) return; + + QString dbc_file; + for (auto& f : dbc()->allDBCFiles()) + if (!f->isEmpty()) { dbc_file = f->filename; break; } + if (dbc_file != settings.recent_dbc_file) return; + + if (!settings.selected_msg_ids.isEmpty()) + center_widget->ensureDetailWidget()->restoreTabs(settings.active_msg_id, settings.selected_msg_ids); + + if (charts_widget != nullptr && !settings.active_charts.empty()) + charts_widget->restoreChartsFromIds(settings.active_charts); +} + // HelpOverlay HelpOverlay::HelpOverlay(MainWindow *parent) : QWidget(parent) { setAttribute(Qt::WA_NoSystemBackground, true); diff --git a/tools/cabana/mainwin.h b/tools/cabana/mainwin.h index 9bc94c090f..1da59f93e3 100644 --- a/tools/cabana/mainwin.h +++ b/tools/cabana/mainwin.h @@ -72,6 +72,8 @@ protected: void updateLoadSaveMenus(); void createDockWidgets(); void eventsMerged(); + void saveSessionState(); + void restoreSessionState(); VideoWidget *video_widget = nullptr; QDockWidget *video_dock; diff --git a/tools/cabana/settings.cc b/tools/cabana/settings.cc index cccc9b6d9a..e7b1129a30 100644 --- a/tools/cabana/settings.cc +++ b/tools/cabana/settings.cc @@ -41,6 +41,10 @@ void settings_op(SettingOperation op) { op(s, "log_path", settings.log_path); op(s, "drag_direction", (int &)settings.drag_direction); op(s, "suppress_defined_signals", settings.suppress_defined_signals); + op(s, "recent_dbc_file", settings.recent_dbc_file); + op(s, "active_msg_id", settings.active_msg_id); + op(s, "selected_msg_ids", settings.selected_msg_ids); + op(s, "active_charts", settings.active_charts); } Settings::Settings() { diff --git a/tools/cabana/settings.h b/tools/cabana/settings.h index e75c519ac7..7ab50d1494 100644 --- a/tools/cabana/settings.h +++ b/tools/cabana/settings.h @@ -46,6 +46,12 @@ public: QByteArray message_header_state; DragDirection drag_direction = MsbFirst; + // session data + QString recent_dbc_file; + QString active_msg_id; + QStringList selected_msg_ids; + QStringList active_charts; + signals: void changed(); }; From 4e74e0f755bf5e31791e699ebf89adfaa911a6ca Mon Sep 17 00:00:00 2001 From: Dean Lee Date: Tue, 9 Dec 2025 08:36:55 +0800 Subject: [PATCH 079/110] cabana: fix UI hang when switching streams (#36735) fix UI hang when switching streams --- tools/cabana/mainwin.cc | 10 +++++++++- tools/cabana/mainwin.h | 1 + tools/replay/replay.cc | 2 +- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/tools/cabana/mainwin.cc b/tools/cabana/mainwin.cc index 2d070acff6..1ea3733ed0 100644 --- a/tools/cabana/mainwin.cc +++ b/tools/cabana/mainwin.cc @@ -313,11 +313,19 @@ void MainWindow::loadFromClipboard(SourceSet s, bool close_all) { } void MainWindow::openStream(AbstractStream *stream, const QString &dbc_file) { + if (can) { + QObject::connect(can, &QObject::destroyed, this, [=]() { startStream(stream, dbc_file); }); + can->deleteLater(); + } else { + startStream(stream, dbc_file); + } +} + +void MainWindow::startStream(AbstractStream *stream, QString dbc_file) { center_widget->clear(); delete messages_widget; delete video_splitter; - delete can; can = stream; can->setParent(this); // take ownership can->start(); diff --git a/tools/cabana/mainwin.h b/tools/cabana/mainwin.h index 1da59f93e3..92c2714ae7 100644 --- a/tools/cabana/mainwin.h +++ b/tools/cabana/mainwin.h @@ -44,6 +44,7 @@ signals: void updateProgressBar(uint64_t cur, uint64_t total, bool success); protected: + void startStream(AbstractStream *stream, QString dbc_file); bool eventFilter(QObject *obj, QEvent *event) override; void remindSaveChanges(); void closeFile(SourceSet s = SOURCE_ALL); diff --git a/tools/replay/replay.cc b/tools/replay/replay.cc index fb13ead034..cc105dd10e 100644 --- a/tools/replay/replay.cc +++ b/tools/replay/replay.cc @@ -63,7 +63,6 @@ void Replay::setupSegmentManager(bool has_filters) { } Replay::~Replay() { - seg_mgr_.reset(); if (stream_thread_.joinable()) { rInfo("shutdown: in progress..."); interruptStream([this]() { @@ -74,6 +73,7 @@ Replay::~Replay() { rInfo("shutdown: done"); } camera_server_.reset(); + seg_mgr_.reset(); } bool Replay::load() { From cce2e4d357e16ec401b39fe778e563d0394d052a Mon Sep 17 00:00:00 2001 From: Matt Purnell <65473602+mpurnell1@users.noreply.github.com> Date: Mon, 8 Dec 2025 18:38:35 -0600 Subject: [PATCH 080/110] tools: Handle smaller terminal sizes in replay (#36766) * Only show help if there's room for it * show less * wording --- tools/replay/consoleui.cc | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tools/replay/consoleui.cc b/tools/replay/consoleui.cc index a4f3677ff3..4d43df2da8 100644 --- a/tools/replay/consoleui.cc +++ b/tools/replay/consoleui.cc @@ -115,7 +115,12 @@ void ConsoleUI::initWindows() { w[Win::Log] = newwin(log_height - 2, max_width - 2 * BORDER_SIZE, 18, BORDER_SIZE); scrollok(w[Win::Log], true); } - w[Win::Help] = newwin(5, max_width - (2 * BORDER_SIZE), max_height - 6, BORDER_SIZE); + if (max_height >= 23) { + w[Win::Help] = newwin(5, max_width - (2 * BORDER_SIZE), max_height - 6, BORDER_SIZE); + } else if (max_height >= 17) { + w[Win::Help] = newwin(1, max_width - (2 * BORDER_SIZE), max_height - 1, BORDER_SIZE); + mvwprintw(w[Win::Help], 0, 0, "Expand screen vertically to list available commands"); + } // set the title bar wbkgd(w[Win::Title], A_REVERSE); @@ -124,7 +129,7 @@ void ConsoleUI::initWindows() { // show windows on the real screen refresh(); displayTimelineDesc(); - displayHelp(); + if (max_height >= 23) displayHelp(); updateSummary(); updateTimeline(); for (auto win : w) { From fadf7ff1e5f7187be532748b86a128709b3eab93 Mon Sep 17 00:00:00 2001 From: Chechulin Serhii <78239416+keefeere@users.noreply.github.com> Date: Tue, 9 Dec 2025 02:40:29 +0200 Subject: [PATCH 081/110] ui: feature Ukrainian translation (#36646) * Add Ukrainian lang * update_translations.py * Add Ukrainian strings * Small patch to display translated update states * Revert "Small patch to display translated update states" This reverts commit b0545f4e109f451a21e4e5884259dbb881d7a58e. * Revert "update_translations.py" This reverts commit 79eea20c33f1b1d542b62a782ab1b67bc9277026. * fix so these meaningless edits --- selfdrive/ui/translations/app_uk.po | 1258 ++++++++++++++++++++++ selfdrive/ui/translations/languages.json | 1 + 2 files changed, 1259 insertions(+) create mode 100644 selfdrive/ui/translations/app_uk.po diff --git a/selfdrive/ui/translations/app_uk.po b/selfdrive/ui/translations/app_uk.po new file mode 100644 index 0000000000..cf78fb5a33 --- /dev/null +++ b/selfdrive/ui/translations/app_uk.po @@ -0,0 +1,1258 @@ +# Ukrainian translations for PACKAGE package. +# Copyright (C) 2025 THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# Automatically generated, 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-11-19 12:21+0200\n" +"PO-Revision-Date: 2025-11-19 13:27+0200\n" +"Last-Translator: KeeFeeRe \n" +"Language-Team: none\n" +"Language: uk\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " +"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" +"X-Generator: Poedit 3.8\n" + +#: selfdrive/ui/layouts/settings/device.py:160 +#, python-format +msgid " Steering torque response calibration is complete." +msgstr " Калібрування реакції крутного моменту керма завершено." + +#: selfdrive/ui/layouts/settings/device.py:158 +#, python-format +msgid " Steering torque response calibration is {}% complete." +msgstr "Калібрування реакції крутного моменту керма завершено на {}%." + +#: selfdrive/ui/layouts/settings/device.py:133 +#, python-format +msgid " Your device is pointed {:.1f}° {} and {:.1f}° {}." +msgstr " Ваш пристрій нахилено на {:.1f}° {} та {:.1f}° {}." + +#: selfdrive/ui/layouts/sidebar.py:43 +msgid "--" +msgstr "--" + +#: selfdrive/ui/widgets/prime.py:47 +#, python-format +msgid "1 year of drive storage" +msgstr "1 рік зберігання поїздок" + +#: selfdrive/ui/widgets/prime.py:47 +#, python-format +msgid "24/7 LTE connectivity" +msgstr "Підключення LTE 24/7" + +#: selfdrive/ui/layouts/sidebar.py:46 +msgid "2G" +msgstr "2G" + +#: selfdrive/ui/layouts/sidebar.py:47 +msgid "3G" +msgstr "3G" + +#: selfdrive/ui/layouts/sidebar.py:49 +msgid "5G" +msgstr "5G" + +#: selfdrive/ui/layouts/settings/developer.py:23 +msgid "" +"WARNING: openpilot longitudinal control is in alpha for this car and will " +"disable Automatic Emergency Braking (AEB).

On this car, openpilot " +"defaults to the car's built-in ACC instead of openpilot's longitudinal " +"control. Enable this to switch to openpilot longitudinal control. Enabling " +"Experimental mode is recommended when enabling openpilot longitudinal " +"control alpha. Changing this setting will restart openpilot if the car is " +"powered on." +msgstr "" +"ПОПЕРЕДЖЕННЯ: поздовжнє керування openpilot для цього автомобіля знаходиться " +"в стадії альфа-тестування і вимкне автоматичне екстрене гальмування (AEB)." + +#: selfdrive/ui/layouts/settings/device.py:148 +#, python-format +msgid "

Steering lag calibration is complete." +msgstr "

Калібрування затримки кермування завершено." + +#: selfdrive/ui/layouts/settings/device.py:146 +#, python-format +msgid "

Steering lag calibration is {}% complete." +msgstr "

Калібрування затримки кермування завершено на {}%." + +#: selfdrive/ui/layouts/settings/firehose.py:138 +#, python-format +msgid "ACTIVE" +msgstr "АКТИВНИЙ" + +#: selfdrive/ui/layouts/settings/developer.py:15 +msgid "" +"ADB (Android Debug Bridge) allows connecting to your device over USB or over " +"the network. See https://docs.comma.ai/how-to/connect-to-comma for more info." +msgstr "" +"ADB (Android Debug Bridge) дозволяє підключатися до вашого пристрою через " +"USB або мережу. Дивіться https://docs.comma.ai/how-to/connect-to-comma для " +"отримання додаткової інформації." + +#: selfdrive/ui/widgets/ssh_key.py:30 +msgid "ADD" +msgstr "ДОДАТИ" + +#: system/ui/widgets/network.py:139 +#, python-format +msgid "APN Setting" +msgstr "Налаштування APN" + +#: selfdrive/ui/widgets/offroad_alerts.py:109 +#, python-format +msgid "Acknowledge Excessive Actuation" +msgstr "Визнайте надмірне спрацьовування" + +#: system/ui/widgets/network.py:74 system/ui/widgets/network.py:95 +#, python-format +msgid "Advanced" +msgstr "Розширені" + +#: selfdrive/ui/layouts/settings/toggles.py:98 +#, python-format +msgid "Aggressive" +msgstr "Агресивн." + +#: selfdrive/ui/layouts/onboarding.py:116 +#, python-format +msgid "Agree" +msgstr "Погодитися" + +#: selfdrive/ui/layouts/settings/toggles.py:70 +#, python-format +msgid "Always-On Driver Monitoring" +msgstr "Постійний моніторинг водія" + +#: selfdrive/ui/layouts/settings/toggles.py:186 +#, python-format +msgid "" +"An alpha version of openpilot longitudinal control can be tested, along with " +"Experimental mode, on non-release branches." +msgstr "" +"Альфа-версію поздовжнього керування openpilot можна протестувати разом з " +"експериментальним режимом на нерелізних гілках." + +#: selfdrive/ui/layouts/settings/device.py:187 +#, python-format +msgid "Are you sure you want to power off?" +msgstr "Ви впевнені, що хочете вимкнути?" + +#: selfdrive/ui/layouts/settings/device.py:175 +#, python-format +msgid "Are you sure you want to reboot?" +msgstr "Ви впевнені, що хочете перезавантажити?" + +#: selfdrive/ui/layouts/settings/device.py:119 +#, python-format +msgid "Are you sure you want to reset calibration?" +msgstr "Ви впевнені, що хочете скинути калібрування?" + +#: selfdrive/ui/layouts/settings/software.py:171 +#, python-format +msgid "Are you sure you want to uninstall?" +msgstr "Ви впевнені, що хочете видалити?" + +#: system/ui/widgets/network.py:99 +#: selfdrive/ui/layouts/onboarding.py:147 +#, python-format +msgid "Back" +msgstr "Назад" + +#: selfdrive/ui/widgets/prime.py:38 +#, python-format +msgid "Become a comma prime member at connect.comma.ai" +msgstr "Станьте членом comma prime на connect.comma.ai" + +#: selfdrive/ui/widgets/pairing_dialog.py:119 +#, python-format +msgid "Bookmark connect.comma.ai to your home screen to use it like an app" +msgstr "" +"Додайте connect.comma.ai до головного екрану, щоб використовувати його як " +"додаток." + +#: selfdrive/ui/layouts/settings/device.py:68 +#, python-format +msgid "CHANGE" +msgstr "ЗМІНИТИ" + +#: selfdrive/ui/layouts/settings/software.py:50 +#: selfdrive/ui/layouts/settings/software.py:115 +#: selfdrive/ui/layouts/settings/software.py:126 +#: selfdrive/ui/layouts/settings/software.py:155 +#, python-format +msgid "CHECK" +msgstr "ПЕРЕВІРИТИ" + +#: selfdrive/ui/widgets/exp_mode_button.py:50 +#, python-format +msgid "CHILL MODE ON" +msgstr "СПОКІЙНИЙ РЕЖИМ" + +#: system/ui/widgets/network.py:155 +#: selfdrive/ui/layouts/sidebar.py:73 +#: selfdrive/ui/layouts/sidebar.py:134 +#: selfdrive/ui/layouts/sidebar.py:136 +#: selfdrive/ui/layouts/sidebar.py:138 +#, python-format +msgid "CONNECT" +msgstr "CONNECT" + +#: system/ui/widgets/network.py:369 +#, python-format +msgid "CONNECTING..." +msgstr "ПІДКЛЮЧА..." + +#: system/ui/widgets/confirm_dialog.py:23 system/ui/widgets/option_dialog.py:35 +#: system/ui/widgets/network.py:318 system/ui/widgets/keyboard.py:81 +#, python-format +msgid "Cancel" +msgstr "Скасувати" + +#: system/ui/widgets/network.py:134 +#, python-format +msgid "Cellular Metered" +msgstr "Лімітне стільникове з'єднання" + +#: selfdrive/ui/layouts/settings/device.py:68 +#, python-format +msgid "Change Language" +msgstr "Змінити мову" + +#: selfdrive/ui/layouts/settings/toggles.py:125 +#, python-format +msgid "Changing this setting will restart openpilot if the car is powered on." +msgstr "" +"Зміна цього параметра призведе до перезапуску openpilot, якщо автомобіль " +"увімкнено." + +#: selfdrive/ui/widgets/pairing_dialog.py:118 +#, python-format +msgid "Click \"add new device\" and scan the QR code on the right" +msgstr "Натисніть «додати новий пристрій» і відскануйте QR-код праворуч." + +#: selfdrive/ui/widgets/offroad_alerts.py:104 +#, python-format +msgid "Close" +msgstr "Закрити" + +#: selfdrive/ui/layouts/settings/software.py:49 +#, python-format +msgid "Current Version" +msgstr "Поточна версія" + +#: selfdrive/ui/layouts/settings/software.py:118 +#, python-format +msgid "DOWNLOAD" +msgstr "ВАНТАЖ" + +#: selfdrive/ui/layouts/onboarding.py:115 +#, python-format +msgid "Decline" +msgstr "Відхилити" + +#: selfdrive/ui/layouts/onboarding.py:148 +#, python-format +msgid "Decline, uninstall openpilot" +msgstr "Відхилити, видалити openpilot" + +#: selfdrive/ui/layouts/settings/settings.py:64 +msgid "Developer" +msgstr "Розробник" + +#: selfdrive/ui/layouts/settings/settings.py:59 +msgid "Device" +msgstr "Пристрій" + +#: selfdrive/ui/layouts/settings/toggles.py:58 +#, python-format +msgid "Disengage on Accelerator Pedal" +msgstr "Вимкнення при натисканні на педаль газу" + +#: selfdrive/ui/layouts/settings/device.py:184 +#, python-format +msgid "Disengage to Power Off" +msgstr "Вимкніть openpilot, щоб вимкнути пристрій" + +#: selfdrive/ui/layouts/settings/device.py:172 +#, python-format +msgid "Disengage to Reboot" +msgstr "Вимкніть openpilot, щоб перезавантажити" + +#: selfdrive/ui/layouts/settings/device.py:103 +#, python-format +msgid "Disengage to Reset Calibration" +msgstr "Деактивуйте для скидання калібрування" + +#: selfdrive/ui/layouts/settings/toggles.py:32 +msgid "Display speed in km/h instead of mph." +msgstr "Відображати швидкість у км/год замість миль/год." + +#: selfdrive/ui/layouts/settings/device.py:59 +#, python-format +msgid "Dongle ID" +msgstr "ID ключа" + +#: selfdrive/ui/layouts/settings/software.py:50 +#, python-format +msgid "Download" +msgstr "Завантажити" + +#: selfdrive/ui/layouts/settings/device.py:62 +#, python-format +msgid "Driver Camera" +msgstr "Камера водія" + +#: selfdrive/ui/layouts/settings/toggles.py:96 +#, python-format +msgid "Driving Personality" +msgstr "Стиль водіння" + +#: system/ui/widgets/network.py:123 system/ui/widgets/network.py:139 +#, python-format +msgid "EDIT" +msgstr "РЕДАГ." + +#: selfdrive/ui/layouts/sidebar.py:138 +msgid "ERROR" +msgstr "ПОМИЛКА" + +#: selfdrive/ui/layouts/sidebar.py:45 +msgid "ETH" +msgstr "ETH" + +#: selfdrive/ui/widgets/exp_mode_button.py:50 +#, python-format +msgid "EXPERIMENTAL MODE ON" +msgstr "ЕКСПЕРИМЕНТ. РЕЖИМ" + +#: selfdrive/ui/layouts/settings/toggles.py:228 +#: selfdrive/ui/layouts/settings/developer.py:166 +#, python-format +msgid "Enable" +msgstr "Увімкнути" + +#: selfdrive/ui/layouts/settings/developer.py:39 +#, python-format +msgid "Enable ADB" +msgstr "Увімкнути ADB" + +#: selfdrive/ui/layouts/settings/toggles.py:64 +#, python-format +msgid "Enable Lane Departure Warnings" +msgstr "Увімкнути попередження про виїзд зі смуги" + +#: system/ui/widgets/network.py:129 +#, python-format +msgid "Enable Roaming" +msgstr "Увімкнути роумінг" + +#: selfdrive/ui/layouts/settings/developer.py:48 +#, python-format +msgid "Enable SSH" +msgstr "Увімкнути SSH" + +#: system/ui/widgets/network.py:120 +#, python-format +msgid "Enable Tethering" +msgstr "Увімкнути точку доступу" + +#: selfdrive/ui/layouts/settings/toggles.py:30 +msgid "Enable driver monitoring even when openpilot is not engaged." +msgstr "Увімкнути моніторинг водія, навіть коли openpilot не ввімкнено." + +#: selfdrive/ui/layouts/settings/toggles.py:46 +#, python-format +msgid "Enable openpilot" +msgstr "Увімкнути openpilot" + +#: selfdrive/ui/layouts/settings/toggles.py:189 +#, python-format +msgid "" +"Enable the openpilot longitudinal control (alpha) toggle to allow " +"Experimental mode." +msgstr "" +"Увімкніть перемикач поздовжнього керування openpilot (альфа), щоб увімкнути " +"експериментальний режим." + +#: system/ui/widgets/network.py:204 +#, python-format +msgid "Enter APN" +msgstr "Введіть APN" + +#: system/ui/widgets/network.py:241 +#, python-format +msgid "Enter SSID" +msgstr "Введіть SSID" + +#: system/ui/widgets/network.py:254 +#, python-format +msgid "Enter new tethering password" +msgstr "Введіть новий пароль для модему" + +#: system/ui/widgets/network.py:237 system/ui/widgets/network.py:314 +#, python-format +msgid "Enter password" +msgstr "Введіть пароль" + +#: selfdrive/ui/widgets/ssh_key.py:89 +#, python-format +msgid "Enter your GitHub username" +msgstr "Введіть ваш логін GitHub" + +#: system/ui/widgets/list_view.py:123 system/ui/widgets/list_view.py:160 +#, python-format +msgid "Error" +msgstr "Помилка" + +#: selfdrive/ui/layouts/settings/toggles.py:52 +#, python-format +msgid "Experimental Mode" +msgstr "Експериментальний режим" + +#: selfdrive/ui/layouts/settings/toggles.py:181 +#, python-format +msgid "" +"Experimental mode is currently unavailable on this car since the car's stock " +"ACC is used for longitudinal control." +msgstr "" +"Експериментальний режим наразі недоступний для цього автомобіля, оскільки " +"для поздовжнього керування використовується штатний адаптивний круїз-" +"контроль (ACC)." + +#: system/ui/widgets/network.py:373 +#, python-format +msgid "FORGETTING..." +msgstr "ЗАБУВАЮ..." + +#: selfdrive/ui/widgets/setup.py:44 +#, python-format +msgid "Finish Setup" +msgstr "Завершити налаштування" + +#: selfdrive/ui/layouts/settings/settings.py:63 +msgid "Firehose" +msgstr "Злива" + +#: selfdrive/ui/layouts/settings/firehose.py:18 +msgid "Firehose Mode" +msgstr "Режим зливи" + +#: selfdrive/ui/layouts/settings/firehose.py:25 +msgid "" +"For maximum effectiveness, bring your device inside and connect to a good " +"USB-C adapter and Wi-Fi weekly.\n" +"\n" +"Firehose Mode can also work while you're driving if connected to a hotspot " +"or unlimited SIM card.\n" +"\n" +"\n" +"Frequently Asked Questions\n" +"\n" +"Does it matter how or where I drive? Nope, just drive as you normally " +"would.\n" +"\n" +"Do all of my segments get pulled in Firehose Mode? No, we selectively pull a " +"subset of your segments.\n" +"\n" +"What's a good USB-C adapter? Any fast phone or laptop charger should be " +"fine.\n" +"\n" +"Does it matter which software I run? Yes, only upstream openpilot (and " +"particular forks) are able to be used for training." +msgstr "" +"Для максимальної ефективності щотижня заносьте пристрій у приміщення та " +"підключайте його до якісного адаптера USB-C і Wi-Fi.\n" +"\n" +"Режим Зливи також може працювати під час руху, якщо пристрій підключено до " +"точки доступу або SIM-картки з необмеженим трафіком.\n" +"\n" +"\n" +"Поширені запитання\n" +"\n" +"Чи має значення, як і де я їду? Ні, просто їдьте, як зазвичай.\n" +"\n" +"Чи всі мої сегменти потрапляють у режим Зливи? Ні, ми вибірково вибираємо " +"підмножину ваших сегментів.\n" +"\n" +"Що таке хороший адаптер USB-C? Будь-який швидкий зарядний пристрій для " +"телефону або ноутбука підійде.\n" +"\n" +"Чи має значення, яке програмне забезпечення я використовую? Так, для " +"навчання можна використовувати тільки upstream openpilot (і певні його " +"форки)." + +#: system/ui/widgets/network.py:318 system/ui/widgets/network.py:451 +#, python-format +msgid "Forget" +msgstr "Заб-и" + +#: system/ui/widgets/network.py:319 +#, python-format +msgid "Forget Wi-Fi Network \"{}\"?" +msgstr "Забути мережу Wi-Fi \"{}\"?" + +#: selfdrive/ui/layouts/sidebar.py:71 +#: selfdrive/ui/layouts/sidebar.py:125 +msgid "GOOD" +msgstr "ДОБРА" + +#: selfdrive/ui/widgets/pairing_dialog.py:117 +#, python-format +msgid "Go to https://connect.comma.ai on your phone" +msgstr "Перейдіть на сайт https://connect.comma.ai на своєму телефоні." + +#: selfdrive/ui/layouts/sidebar.py:129 +msgid "HIGH" +msgstr "ВИСОКА" + +#: system/ui/widgets/network.py:155 +#, python-format +msgid "Hidden Network" +msgstr "Прихована мережа" + +#: selfdrive/ui/layouts/settings/firehose.py:140 +#, python-format +msgid "INACTIVE: connect to an unmetered network" +msgstr "НЕАКТИВНО: підключення до мережі без ліміту трафіку" + +#: selfdrive/ui/layouts/settings/software.py:53 +#: selfdrive/ui/layouts/settings/software.py:144 +#, python-format +msgid "INSTALL" +msgstr "ВСТАНОВ." + +#: system/ui/widgets/network.py:150 +#, python-format +msgid "IP Address" +msgstr "IP-адреса" + +#: selfdrive/ui/layouts/settings/software.py:53 +#, python-format +msgid "Install Update" +msgstr "Встановити оновлення" + +#: selfdrive/ui/layouts/settings/developer.py:56 +#, python-format +msgid "Joystick Debug Mode" +msgstr "Режим зневадження джойстика" + +#: selfdrive/ui/widgets/ssh_key.py:29 +msgid "LOADING" +msgstr "ЗАВАНТАЖЕННЯ" + +#: selfdrive/ui/layouts/sidebar.py:48 +msgid "LTE" +msgstr "LTE" + +#: selfdrive/ui/layouts/settings/developer.py:64 +#, python-format +msgid "Longitudinal Maneuver Mode" +msgstr "Режим поздовжнього маневрування" + +#: selfdrive/ui/onroad/hud_renderer.py:148 +#, python-format +msgid "MAX" +msgstr "МАКС" + +#: selfdrive/ui/widgets/setup.py:75 +#, python-format +msgid "" +"Maximize your training data uploads to improve openpilot's driving models." +msgstr "" +"Максимізуйте завантаження навчальних даних, щоб поліпшити моделі openpilot." + +#: selfdrive/ui/layouts/settings/device.py:59 +#: selfdrive/ui/layouts/settings/device.py:60 +#, python-format +msgid "N/A" +msgstr "Н/Д" + +#: selfdrive/ui/layouts/sidebar.py:142 +msgid "NO" +msgstr "НЕМАЄ" + +#: selfdrive/ui/layouts/settings/settings.py:60 +msgid "Network" +msgstr "Мережа" + +#: selfdrive/ui/widgets/ssh_key.py:114 +#, python-format +msgid "No SSH keys found" +msgstr "Не знайдено ключів SSH" + +#: selfdrive/ui/widgets/ssh_key.py:126 +#, python-format +msgid "No SSH keys found for user '{}'" +msgstr "Користувач '{}' не має ключів на GitHub" + +#: selfdrive/ui/widgets/offroad_alerts.py:320 +#, python-format +msgid "No release notes available." +msgstr "Інформація про випуск відсутня." + +#: selfdrive/ui/layouts/sidebar.py:73 +#: selfdrive/ui/layouts/sidebar.py:134 +msgid "OFFLINE" +msgstr "ОФЛАЙН" + +#: system/ui/widgets/confirm_dialog.py:93 system/ui/widgets/html_render.py:263 +#: selfdrive/ui/layouts/sidebar.py:127 +#, python-format +msgid "OK" +msgstr "OK" + +#: selfdrive/ui/layouts/sidebar.py:72 +#: selfdrive/ui/layouts/sidebar.py:136 +#: selfdrive/ui/layouts/sidebar.py:144 +msgid "ONLINE" +msgstr "ОНЛАЙН" + +#: selfdrive/ui/widgets/setup.py:20 +#, python-format +msgid "Open" +msgstr "ВІДКРИТИ" + +#: selfdrive/ui/layouts/settings/device.py:48 +#, python-format +msgid "PAIR" +msgstr "ПІДКЛЮЧИТИ" + +#: selfdrive/ui/layouts/sidebar.py:142 +msgid "PANDA" +msgstr "PANDA" + +#: selfdrive/ui/layouts/settings/device.py:62 +#, python-format +msgid "PREVIEW" +msgstr "ПОКАЖИ" + +#: selfdrive/ui/widgets/prime.py:44 +#, python-format +msgid "PRIME FEATURES:" +msgstr "XАРАКТЕРИСТИКИ PRIME:" + +#: selfdrive/ui/layouts/settings/device.py:48 +#, python-format +msgid "Pair Device" +msgstr "Підключити пристрій" + +#: selfdrive/ui/widgets/setup.py:19 +#, python-format +msgid "Pair device" +msgstr "Підключити пристрій" + +#: selfdrive/ui/widgets/pairing_dialog.py:92 +#, python-format +msgid "Pair your device to your comma account" +msgstr "Підключіть свій пристрій до обліковки comma connect" + +#: selfdrive/ui/widgets/setup.py:48 +#: selfdrive/ui/layouts/settings/device.py:24 +#, python-format +msgid "" +"Pair your device with comma connect (connect.comma.ai) and claim your comma " +"prime offer." +msgstr "" +"Підключіть свій пристрій до comma connect (connect.comma.ai) і отримайте " +"свою пропозицію comma prime." + +#: selfdrive/ui/widgets/setup.py:91 +#, python-format +msgid "Please connect to Wi-Fi to complete initial pairing" +msgstr "Будь ласка, підключіться до Wi-Fi, щоб завершити початкове сполучення." + +#: selfdrive/ui/layouts/settings/device.py:55 +#: selfdrive/ui/layouts/settings/device.py:187 +#, python-format +msgid "Power Off" +msgstr "Вимкнути" + +#: system/ui/widgets/network.py:144 +#, python-format +msgid "Prevent large data uploads when on a metered Wi-Fi connection" +msgstr "" +"Запобігайте завантаженню великих обсягів даних під час використання Wi-Fi-" +"з'єднання з обмеженим трафіком" + +#: system/ui/widgets/network.py:135 +#, python-format +msgid "Prevent large data uploads when on a metered cellular connection" +msgstr "" +"Запобігати великим завантаженням даних під час лімітного стільникового " +"з'єднання" + +#: selfdrive/ui/layouts/settings/device.py:25 +msgid "" +"Preview the driver facing camera to ensure that driver monitoring has good " +"visibility. (vehicle must be off)" +msgstr "" +"Попередньо перегляньте камеру, спрямовану на водія, щоб переконатися, що " +"система моніторингу водія має добру видимість. (автомобіль повинен бути " +"вимкнений)" + +#: selfdrive/ui/widgets/pairing_dialog.py:150 +#, python-format +msgid "QR Code Error" +msgstr "Помилка QR-коду" + +#: selfdrive/ui/widgets/ssh_key.py:31 +msgid "REMOVE" +msgstr "ВИДАЛИТИ" + +#: selfdrive/ui/layouts/settings/device.py:51 +#, python-format +msgid "RESET" +msgstr "Скинути" + +#: selfdrive/ui/layouts/settings/device.py:65 +#, python-format +msgid "REVIEW" +msgstr "ДИВИТИСЬ" + +#: selfdrive/ui/layouts/settings/device.py:55 +#: selfdrive/ui/layouts/settings/device.py:175 +#, python-format +msgid "Reboot" +msgstr "Перезавантажити" + +#: selfdrive/ui/onroad/alert_renderer.py:66 +#, python-format +msgid "Reboot Device" +msgstr "Перезавантажте пристрій" + +#: selfdrive/ui/widgets/offroad_alerts.py:112 +#, python-format +msgid "Reboot and Update" +msgstr "Перезавантажити та оновити" + +#: selfdrive/ui/layouts/settings/toggles.py:27 +msgid "" +"Receive alerts to steer back into the lane when your vehicle drifts over a " +"detected lane line without a turn signal activated while driving over 31 mph " +"(50 km/h)." +msgstr "" +"Отримувати попередження про необхідність повернутися в смугу, коли ваш " +"автомобіль перетинає виявлену лінію розмітки без увімкненого сигналу " +"повороту під час руху зі швидкістю понад 31 миль/год (50 км/год)." + +#: selfdrive/ui/layouts/settings/toggles.py:76 +#, python-format +msgid "Record and Upload Driver Camera" +msgstr "Писати та вантажити відео з камери водія" + +#: selfdrive/ui/layouts/settings/toggles.py:82 +#, python-format +msgid "Record and Upload Microphone Audio" +msgstr "Запис та завантаження аудіо з мікрофона" + +#: selfdrive/ui/layouts/settings/toggles.py:33 +msgid "" +"Record and store microphone audio while driving. The audio will be included " +"in the dashcam video in comma connect." +msgstr "" +"Записуйте та зберігайте аудіо з мікрофона під час руху. Аудіо буде включено " +"до відео з відеореєстратора в comma connect." + +#: selfdrive/ui/layouts/settings/device.py:67 +#, python-format +msgid "Regulatory" +msgstr "Нормативні документи" + +#: selfdrive/ui/layouts/settings/toggles.py:98 +#, python-format +msgid "Relaxed" +msgstr "Спокійний" + +#: selfdrive/ui/widgets/prime.py:47 +#, python-format +msgid "Remote access" +msgstr "Віддалений доступ" + +#: selfdrive/ui/widgets/prime.py:47 +#, python-format +msgid "Remote snapshots" +msgstr "Віддалені знімки" + +#: selfdrive/ui/widgets/ssh_key.py:123 +#, python-format +msgid "Request timed out" +msgstr "Час запиту вичерпано" + +#: selfdrive/ui/layouts/settings/device.py:119 +#, python-format +msgid "Reset" +msgstr "Скинути" + +#: selfdrive/ui/layouts/settings/device.py:51 +#, python-format +msgid "Reset Calibration" +msgstr "Скинути калібрування" + +#: selfdrive/ui/layouts/settings/device.py:65 +#, python-format +msgid "Review Training Guide" +msgstr "Переглянути посібник з навчання" + +#: selfdrive/ui/layouts/settings/device.py:27 +msgid "Review the rules, features, and limitations of openpilot" +msgstr "Перегляньте правила, функції та обмеження openpilot" + +#: selfdrive/ui/layouts/settings/software.py:61 +#, python-format +msgid "SELECT" +msgstr "ВИБРАТИ" + +#: selfdrive/ui/layouts/settings/developer.py:53 +#, python-format +msgid "SSH Keys" +msgstr "SSH ключі" + +#: system/ui/widgets/network.py:310 +#, python-format +msgid "Scanning Wi-Fi networks..." +msgstr "Пошук мереж..." + +#: system/ui/widgets/option_dialog.py:36 +#, python-format +msgid "Select" +msgstr "Вибрати" + +#: selfdrive/ui/layouts/settings/software.py:191 +#, python-format +msgid "Select a branch" +msgstr "Виберіть гілку" + +#: selfdrive/ui/layouts/settings/device.py:91 +#, python-format +msgid "Select a language" +msgstr "Виберіть мову" + +#: selfdrive/ui/layouts/settings/device.py:60 +#, python-format +msgid "Serial" +msgstr "Серійний номер" + +#: selfdrive/ui/widgets/offroad_alerts.py:106 +#, python-format +msgid "Snooze Update" +msgstr "Відкласти оновлення" + +#: selfdrive/ui/layouts/settings/settings.py:62 +msgid "Software" +msgstr "Програма" + +#: selfdrive/ui/layouts/settings/toggles.py:98 +#, python-format +msgid "Standard" +msgstr "Стандарт" + +#: selfdrive/ui/layouts/settings/toggles.py:22 +msgid "" +"Standard is recommended. In aggressive mode, openpilot will follow lead cars " +"closer and be more aggressive with the gas and brake. In relaxed mode " +"openpilot will stay further away from lead cars. On supported cars, you can " +"cycle through these personalities with your steering wheel distance button." +msgstr "" +"Рекомендується стандартний режим. В агресивному режимі openpilot буде " +"триматися ближче до автомобілів попереду і більш агресивно використовувати " +"газ і гальма. У спокійному режимі openpilot буде триматися на більшій " +"відстані від автомобілів попереду. На підтримуваних автомобілях ви можете " +"перемикатися між цими режимами за допомогою кнопки дистанції на кермі." + +#: selfdrive/ui/onroad/alert_renderer.py:59 +#: selfdrive/ui/onroad/alert_renderer.py:65 +#, python-format +msgid "System Unresponsive" +msgstr "Система не реагує" + +#: selfdrive/ui/onroad/alert_renderer.py:58 +#, python-format +msgid "TAKE CONTROL IMMEDIATELY" +msgstr "КЕРМУЙТЕ НЕГАЙНО" + +#: selfdrive/ui/layouts/sidebar.py:71 +#: selfdrive/ui/layouts/sidebar.py:125 +#: selfdrive/ui/layouts/sidebar.py:127 +#: selfdrive/ui/layouts/sidebar.py:129 +msgid "TEMP" +msgstr "ТЕМП" + +#: selfdrive/ui/layouts/settings/software.py:61 +#, python-format +msgid "Target Branch" +msgstr "Цільова гілка" + +#: system/ui/widgets/network.py:124 +#, python-format +msgid "Tethering Password" +msgstr "Пароль для точки доступу" + +#: selfdrive/ui/layouts/settings/settings.py:61 +msgid "Toggles" +msgstr "Перемикачі" + +#: selfdrive/ui/layouts/settings/software.py:72 +#, python-format +msgid "UNINSTALL" +msgstr "ВИДАЛИТИ" + +#: selfdrive/ui/layouts/home.py:155 +#, python-format +msgid "UPDATE" +msgstr "ОНОВИТИ" + +#: selfdrive/ui/layouts/settings/software.py:72 +#: selfdrive/ui/layouts/settings/software.py:171 +#, python-format +msgid "Uninstall" +msgstr "Видалити" + +#: selfdrive/ui/layouts/sidebar.py:117 +msgid "Unknown" +msgstr "Невідомо" + +#: selfdrive/ui/layouts/settings/software.py:48 +#, python-format +msgid "Updates are only downloaded while the car is off." +msgstr "Оновлення завантажуються лише тоді, коли автомобіль вимкнено." + +#: selfdrive/ui/widgets/prime.py:33 +#, python-format +msgid "Upgrade Now" +msgstr "Оновити зараз" + +#: selfdrive/ui/layouts/settings/toggles.py:31 +msgid "" +"Upload data from the driver facing camera and help improve the driver " +"monitoring algorithm." +msgstr "" +"Завантажуйте дані з камери, спрямованої на водія, та допоможіть покращити " +"алгоритм моніторингу водія." + +#: selfdrive/ui/layouts/settings/toggles.py:88 +#, python-format +msgid "Use Metric System" +msgstr "Використовувати метричну систему" + +#: selfdrive/ui/layouts/settings/toggles.py:17 +msgid "" +"Use the openpilot system for adaptive cruise control and lane keep driver " +"assistance. Your attention is required at all times to use this feature." +msgstr "" +"Використовуйте систему openpilot для адаптивного круїз-контролю та допомоги " +"в утриманні смуги руху. Ваша увага потрібна постійно при використанні цієї " +"функції. Зміна цього налаштування набуває чинності після вимкнення живлення " +"автомобіля." + +#: selfdrive/ui/layouts/sidebar.py:72 +#: selfdrive/ui/layouts/sidebar.py:144 +msgid "VEHICLE" +msgstr "АВТО" + +#: selfdrive/ui/layouts/settings/device.py:67 +#, python-format +msgid "VIEW" +msgstr "ДИВИСЬ" + +#: selfdrive/ui/onroad/alert_renderer.py:52 +#, python-format +msgid "Waiting to start" +msgstr "Очікування початку" + +#: selfdrive/ui/layouts/settings/developer.py:19 +msgid "" +"Warning: This grants SSH access to all public keys in your GitHub settings. " +"Never enter a GitHub username other than your own. A comma employee will " +"NEVER ask you to add their GitHub username." +msgstr "" +"Попередження: це надає доступ по SSH до всіх публічних ключів у ваших " +"налаштуваннях GitHub. Ніколи не вводьте ім'я користувача GitHub, окрім " +"вашого власного. Співробітник comma НІКОЛИ не попросить вас додати його ім'я " +"користувача GitHub." + +#: selfdrive/ui/layouts/onboarding.py:111 +#, python-format +msgid "Welcome to openpilot" +msgstr "Ласкаво просимо до openpilot" + +#: selfdrive/ui/layouts/settings/toggles.py:20 +msgid "When enabled, pressing the accelerator pedal will disengage openpilot." +msgstr "Якщо увімкнено, натискання на педаль акселератора вимкне openpilot." + +#: selfdrive/ui/layouts/sidebar.py:44 +msgid "Wi-Fi" +msgstr "Wi-Fi" + +#: system/ui/widgets/network.py:144 +#, python-format +msgid "Wi-Fi Network Metered" +msgstr "Трафік Wi-Fi" + +#: system/ui/widgets/network.py:314 +#, python-format +msgid "Wrong password" +msgstr "Невірний пароль" + +#: selfdrive/ui/layouts/onboarding.py:145 +#, python-format +msgid "You must accept the Terms and Conditions in order to use openpilot." +msgstr "Ви повинні прийняти Умови та положення, щоб користуватися openpilot." + +#: selfdrive/ui/layouts/onboarding.py:112 +#, python-format +msgid "" +"You must accept the Terms and Conditions to use openpilot. Read the latest " +"terms at https://comma.ai/terms before continuing." +msgstr "" +"Ви повинні прийняти Умови використання, щоб користуватися openpilot. Перед " +"тим, як продовжити, ознайомтеся з останніми умовами на сайті https://" +"comma.ai/terms." + +#: selfdrive/ui/onroad/driver_camera_dialog.py:34 +#, python-format +msgid "camera starting" +msgstr "запуск камери" + +#: selfdrive/ui/layouts/settings/software.py:105 +#, python-format +msgid "checking..." +msgstr "перевіряю..." + +#: selfdrive/ui/widgets/prime.py:63 +#, python-format +msgid "comma prime" +msgstr "comma prime" + +#: system/ui/widgets/network.py:142 +#, python-format +msgid "default" +msgstr "замовч." + +#: selfdrive/ui/layouts/settings/device.py:133 +#, python-format +msgid "down" +msgstr "вниз" + +#: selfdrive/ui/layouts/settings/software.py:106 +#, python-format +msgid "downloading..." +msgstr "завантажую..." + +#: selfdrive/ui/layouts/settings/software.py:114 +#, python-format +msgid "failed to check for update" +msgstr "не вдалося перевірити оновлення" + +#: selfdrive/ui/layouts/settings/software.py:107 +#, python-format +msgid "finalizing update..." +msgstr "завершую..." + +#: system/ui/widgets/network.py:237 system/ui/widgets/network.py:314 +#, python-format +msgid "for \"{}\"" +msgstr "для \"{}\"" + +#: selfdrive/ui/onroad/hud_renderer.py:177 +#, python-format +msgid "km/h" +msgstr "км/год" + +#: system/ui/widgets/network.py:204 +#, python-format +msgid "leave blank for automatic configuration" +msgstr "залиште порожнім для автоматичного налаштування" + +#: selfdrive/ui/layouts/settings/device.py:134 +#, python-format +msgid "left" +msgstr "вліво" + +#: system/ui/widgets/network.py:142 +#, python-format +msgid "metered" +msgstr "обмеж." + +#: selfdrive/ui/onroad/hud_renderer.py:177 +#, python-format +msgid "mph" +msgstr "миль/год" + +#: selfdrive/ui/layouts/settings/software.py:20 +#, python-format +msgid "never" +msgstr "ніколи" + +#: selfdrive/ui/layouts/settings/software.py:31 +#, python-format +msgid "now" +msgstr "зараз" + +#: selfdrive/ui/layouts/settings/developer.py:71 +#, python-format +msgid "openpilot Longitudinal Control (Alpha)" +msgstr "Поздовжнє керування openpilot (Альфа)" + +#: selfdrive/ui/onroad/alert_renderer.py:51 +#, python-format +msgid "openpilot Unavailable" +msgstr "openpilot Недоступний" + +#: selfdrive/ui/layouts/settings/toggles.py:158 +#, python-format +msgid "" +"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." +msgstr "" +"openpilot за замовчуванням працює в режимі спокій. Експериментальний режим " +"увімкне функції альфа-рівня, які ще не готові для режиму спокій. " +"Експериментальні функції перелічені нижче:

Кінцевий поздовжній " +"контроль


Дозвольте моделі водіння контролювати газ і гальма. " +"openpilot буде керувати автомобілем так, як це робив би людина, включаючи " +"зупинку на червоне світло і знаки зупинки. Оскільки модель водіння визначає " +"швидкість руху, задана швидкість буде діяти лише як верхня межа. Це функція " +"альфа-рівня; слід очікувати помилок.

Нова візуалізація водіння
Візуалізація водіння перейде на ширококутну камеру, спрямовану на " +"дорогу, при низьких швидкостях, щоб краще показувати деякі повороти. Логотип " +"експериментального режиму також буде показаний у верхньому правому куті." + +#: selfdrive/ui/layouts/settings/device.py:165 +#, python-format +msgid "" +"openpilot is continuously calibrating, resetting is rarely required. " +"Resetting calibration will restart openpilot if the car is powered on." +msgstr "" +"openpilot постійно калібрується, скидання рідко потрібне. Скидання " +"калібрування призведе до перезапуску openpilot, якщо автомобіль увімкнено." + +#: selfdrive/ui/layouts/settings/firehose.py:20 +msgid "" +"openpilot learns to drive by watching humans, like you, drive.\n" +"\n" +"Firehose Mode allows you to maximize your training data uploads to improve " +"openpilot's driving models. More data means bigger models, which means " +"better Experimental Mode." +msgstr "" +"openpilot вчиться керувати автомобілем, спостерігаючи за тим, як це роблять " +"люди, такі як ви.\n" +"\n" +"Режим зливи дозволяє максимально збільшити обсяг завантажуваних навчальних " +"даних, щоб поліпшити моделі керування автомобілем openpilot. Більше даних " +"означає більші моделі, а це означає кращий експериментальний режим." + +#: selfdrive/ui/layouts/settings/toggles.py:183 +#, python-format +msgid "openpilot longitudinal control may come in a future update." +msgstr "Поздовжнє керування openpilot може з'явитися в майбутньому оновленні." + +#: selfdrive/ui/layouts/settings/device.py:26 +msgid "" +"openpilot requires the device to be mounted within 4° left or right and " +"within 5° up or 9° down." +msgstr "" +"Для роботи openpilot потрібно, щоб пристрій був встановлений з нахилом не " +"більше 4° вліво або вправо та не більше 5° вгору або 9° вниз. openpilot " +"постійно калібрується, тому скидання калібрування потрібне рідко." + +#: selfdrive/ui/layouts/settings/device.py:134 +#, python-format +msgid "right" +msgstr "вправо" + +#: system/ui/widgets/network.py:142 +#, python-format +msgid "unmetered" +msgstr "необмеж." + +#: selfdrive/ui/layouts/settings/device.py:133 +#, python-format +msgid "up" +msgstr "вгору" + +#: selfdrive/ui/layouts/settings/software.py:125 +#, python-format +msgid "up to date, last checked never" +msgstr "оновлено, ніколи не перевірялось" + +#: selfdrive/ui/layouts/settings/software.py:123 +#, python-format +msgid "up to date, last checked {}" +msgstr "оновлено, перевірив {}" + +#: selfdrive/ui/layouts/settings/software.py:117 +#, python-format +msgid "update available" +msgstr "доступне оновлення" + +#: selfdrive/ui/layouts/home.py:169 +#, python-format +msgid "{} ALERT" +msgid_plural "{} ALERTS" +msgstr[0] "{} СПОВІЩЕННЯ" +msgstr[1] "{} СПОВІЩЕННЯ" +msgstr[2] "{} СПОВІЩЕНЬ" + +#: selfdrive/ui/layouts/settings/software.py:40 +#, python-format +msgid "{} day ago" +msgid_plural "{} days ago" +msgstr[0] "{} день тому" +msgstr[1] "{} дні тому" +msgstr[2] "{} днів тому" + +#: selfdrive/ui/layouts/settings/software.py:37 +#, python-format +msgid "{} hour ago" +msgid_plural "{} hours ago" +msgstr[0] "{} година тому" +msgstr[1] "{} години тому" +msgstr[2] "{} годин тому" + +#: selfdrive/ui/layouts/settings/software.py:34 +#, python-format +msgid "{} minute ago" +msgid_plural "{} minutes ago" +msgstr[0] "{} хвилина тому" +msgstr[1] "{} хвилини тому" +msgstr[2] "{} хвилин тому" + +#: selfdrive/ui/layouts/settings/firehose.py:111 +#, python-format +msgid "{} segment of your driving is in the training dataset so far." +msgid_plural "{} segments of your driving is in the training dataset so far." +msgstr[0] "" +"{} сегмент вашого водіння на даний момент містяться в тренувальному наборі " +"даних." +msgstr[1] "" +"{} сегменти вашого водіння на даний момент містяться в тренувальному наборі " +"даних." +msgstr[2] "" +"{} сегментів вашого водіння на даний момент містяться в тренувальному наборі " +"даних." + +#: selfdrive/ui/widgets/prime.py:62 +#, python-format +msgid "✓ SUBSCRIBED" +msgstr "✓ ПІДПИСАНО" + +#: selfdrive/ui/widgets/setup.py:22 +#, python-format +msgid "🔥 Firehose Mode 🔥" +msgstr "🌧️ Режим зливи 🌧️" diff --git a/selfdrive/ui/translations/languages.json b/selfdrive/ui/translations/languages.json index b0674dee82..47e673ce89 100644 --- a/selfdrive/ui/translations/languages.json +++ b/selfdrive/ui/translations/languages.json @@ -5,6 +5,7 @@ "Português": "pt-BR", "Español": "es", "Türkçe": "tr", + "Українська": "uk", "العربية": "ar", "ไทย": "th", "中文(繁體)": "zh-CHT", From 7119412d35e9b1a425db7eeb6cadc26d2caa9eb7 Mon Sep 17 00:00:00 2001 From: Matt Purnell <65473602+mpurnell1@users.noreply.github.com> Date: Mon, 8 Dec 2025 18:41:45 -0600 Subject: [PATCH 082/110] updated: fix skipped test case (#36786) Fix three failing tests --- system/updated/tests/test_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/updated/tests/test_base.py b/system/updated/tests/test_base.py index 699a0f0bd3..c4894f2711 100644 --- a/system/updated/tests/test_base.py +++ b/system/updated/tests/test_base.py @@ -133,7 +133,7 @@ class TestBaseUpdate: class ParamsBaseUpdateTest(TestBaseUpdate): def _test_finalized_update(self, branch, version, agnos_version, release_notes): assert self.params.get("UpdaterNewDescription").startswith(f"{version} / {branch}") - assert self.params.get("UpdaterNewReleaseNotes") == f"{release_notes}\n" + assert self.params.get("UpdaterNewReleaseNotes") == f"{release_notes}\n".encode() super()._test_finalized_update(branch, version, agnos_version, release_notes) def send_check_for_updates_signal(self, updated: ManagerProcess): From fb807cc007b32787b726d505109ba7b47eb1b45e Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Mon, 8 Dec 2025 18:39:47 -0800 Subject: [PATCH 083/110] ui: video diff tool (#36737) * video diff * format * duplicate * try * WINDOWED * ? * correct res * Revert "correct res" This reverts commit f90991192fce93a31d1b581a4f0ff93a7a972337. * save to report/ * add duplicate * work? * fix * more * more * and this * ffmpeg * branch * uncmt * test preview * Revert "uncmt" This reverts commit b02404dbbe515fd861717f831c7bb0243442ddbc. * create openpilot_master_ui_mici_raylib * ahh * push to master * copy and always run * test * does cmt break it? * who did this * fix? * fix that * hmm * hmm * ah this was moving it, and then the job below didn't run on master * google ai overview lied to me * use markdown to start * need to add to one branch * ???? * oof * no * this work? * test * try this * clean up master branch name * more cleanup more cleanup * don't fail for no diff! don't fail for no diff! * back * add to cmt * test it * should work * fix that * back * clean up * clean up * save to report * pull_request_target * sort --------- Co-authored-by: Shane Smiskol --- .github/workflows/mici_raylib_ui_preview.yaml | 151 +++++++++++++ .github/workflows/tests.yaml | 26 +++ pyproject.toml | 1 + selfdrive/ui/tests/.gitignore | 5 + selfdrive/ui/tests/diff/diff.py | 201 ++++++++++++++++++ selfdrive/ui/tests/diff/replay.py | 97 +++++++++ uv.lock | 41 +++- 7 files changed, 520 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/mici_raylib_ui_preview.yaml create mode 100755 selfdrive/ui/tests/diff/diff.py create mode 100755 selfdrive/ui/tests/diff/replay.py diff --git a/.github/workflows/mici_raylib_ui_preview.yaml b/.github/workflows/mici_raylib_ui_preview.yaml new file mode 100644 index 0000000000..707825b1ac --- /dev/null +++ b/.github/workflows/mici_raylib_ui_preview.yaml @@ -0,0 +1,151 @@ +name: "mici raylib ui preview" +on: + push: + branches: + - master + pull_request_target: + types: [assigned, opened, synchronize, reopened, edited] + branches: + - 'master' + paths: + - 'selfdrive/assets/**' + - 'selfdrive/ui/**' + - 'system/ui/**' + workflow_dispatch: + +env: + UI_JOB_NAME: "Create mici raylib UI Report" + REPORT_NAME: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && 'master' || github.event.number }} + SHA: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && github.sha || github.event.pull_request.head.sha }} + BRANCH_NAME: "openpilot/pr-${{ github.event.number }}-mici-raylib-ui" + MASTER_BRANCH_NAME: "openpilot_master_ui_mici_raylib" + # All report files are pushed here + REPORT_FILES_BRANCH_NAME: "mici-raylib-ui-reports" + +jobs: + preview: + if: github.repository == 'commaai/openpilot' + name: preview + runs-on: ubuntu-latest + timeout-minutes: 20 + permissions: + contents: read + pull-requests: write + actions: read + steps: + - uses: actions/checkout@v4 + with: + submodules: true + + - name: Waiting for ui generation to end + uses: lewagon/wait-on-check-action@v1.3.4 + with: + ref: ${{ env.SHA }} + check-name: ${{ env.UI_JOB_NAME }} + repo-token: ${{ secrets.GITHUB_TOKEN }} + allowed-conclusions: success + wait-interval: 20 + + - name: Getting workflow run ID + id: get_run_id + run: | + echo "run_id=$(curl https://api.github.com/repos/${{ github.repository }}/commits/${{ env.SHA }}/check-runs | jq -r '.check_runs[] | select(.name == "${{ env.UI_JOB_NAME }}") | .html_url | capture("(?[0-9]+)") | .number')" >> $GITHUB_OUTPUT + + - name: Getting proposed ui # filename: pr_ui/mici_ui_replay.mp4 + id: download-artifact + uses: dawidd6/action-download-artifact@v6 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + run_id: ${{ steps.get_run_id.outputs.run_id }} + search_artifacts: true + name: mici-raylib-report-1-${{ env.REPORT_NAME }} + path: ${{ github.workspace }}/pr_ui + + - name: Getting master ui # filename: master_ui_raylib/mici_ui_replay.mp4 + uses: actions/checkout@v4 + with: + repository: commaai/ci-artifacts + ssh-key: ${{ secrets.CI_ARTIFACTS_DEPLOY_KEY }} + path: ${{ github.workspace }}/master_ui_raylib + ref: ${{ env.MASTER_BRANCH_NAME }} + + - name: Saving new master ui + if: github.ref == 'refs/heads/master' && github.event_name == 'push' + working-directory: ${{ github.workspace }}/master_ui_raylib + run: | + git checkout --orphan=new_master_ui_mici_raylib + git rm -rf * + git branch -D ${{ env.MASTER_BRANCH_NAME }} + git branch -m ${{ env.MASTER_BRANCH_NAME }} + git config user.name "GitHub Actions Bot" + git config user.email "<>" + mv ${{ github.workspace }}/pr_ui/* . + git add . + git commit -m "mici raylib video for commit ${{ env.SHA }}" + git push origin ${{ env.MASTER_BRANCH_NAME }} --force + + - name: Setup FFmpeg + uses: AnimMouse/setup-ffmpeg@ae28d57dabbb148eff63170b6bf7f2b60062cbae + + - name: Finding diff + if: github.event_name == 'pull_request_target' + id: find_diff + run: | + # Find the video file from PR + pr_video="${{ github.workspace }}/pr_ui/mici_ui_replay_proposed.mp4" + mv "${{ github.workspace }}/pr_ui/mici_ui_replay.mp4" "$pr_video" + + master_video="${{ github.workspace }}/pr_ui/mici_ui_replay_master.mp4" + mv "${{ github.workspace }}/master_ui_raylib/mici_ui_replay.mp4" "$master_video" + + # Run report + export PYTHONPATH=${{ github.workspace }} + baseurl="https://github.com/commaai/ci-artifacts/raw/refs/heads/${{ env.BRANCH_NAME }}" + diff_exit_code=0 + python3 ${{ github.workspace }}/selfdrive/ui/tests/diff/diff.py "${{ github.workspace }}/pr_ui/mici_ui_replay_master.mp4" "${{ github.workspace }}/pr_ui/mici_ui_replay_proposed.mp4" "diff.html" --basedir "$baseurl" --no-open || diff_exit_code=$? + + # Copy diff report files + cp ${{ github.workspace }}/selfdrive/ui/tests/diff/report/diff.html ${{ github.workspace }}/pr_ui/ + cp ${{ github.workspace }}/selfdrive/ui/tests/diff/report/diff.mp4 ${{ github.workspace }}/pr_ui/ + + REPORT_URL="https://commaai.github.io/ci-artifacts/diff_pr_${{ github.event.number }}.html" + if [ $diff_exit_code -eq 0 ]; then + DIFF="✅ Videos are identical! [View Diff Report]($REPORT_URL)" + else + DIFF="❌ Videos differ! [View Diff Report]($REPORT_URL)" + fi + echo "DIFF=$DIFF" >> "$GITHUB_OUTPUT" + + - name: Saving proposed ui + if: github.event_name == 'pull_request_target' + working-directory: ${{ github.workspace }}/master_ui_raylib + run: | + # Overwrite PR branch w/ proposed ui, and master ui at this point in time for future reference + git config user.name "GitHub Actions Bot" + git config user.email "<>" + git checkout --orphan=${{ env.BRANCH_NAME }} + git rm -rf * + mv ${{ github.workspace }}/pr_ui/* . + git add . + git commit -m "mici raylib video for PR #${{ github.event.number }}" + git push origin ${{ env.BRANCH_NAME }} --force + + # Append diff report to report files branch + git fetch origin ${{ env.REPORT_FILES_BRANCH_NAME }} + git checkout ${{ env.REPORT_FILES_BRANCH_NAME }} + cp ${{ github.workspace }}/selfdrive/ui/tests/diff/report/diff.html diff_pr_${{ github.event.number }}.html + git add diff_pr_${{ github.event.number }}.html + git commit -m "mici raylib ui diff report for PR #${{ github.event.number }}" || echo "No changes to commit" + git push origin ${{ env.REPORT_FILES_BRANCH_NAME }} + + - name: Comment Video on PR + if: github.event_name == 'pull_request_target' + uses: thollander/actions-comment-pull-request@v2 + with: + message: | + + ## mici raylib UI Preview + ${{ steps.find_diff.outputs.DIFF }} + comment_tag: run_id_video_mici_raylib + pr_number: ${{ github.event.number }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 5ca020424c..c5802b5cb2 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -265,3 +265,29 @@ jobs: with: name: raylib-report-${{ inputs.run_number || '1' }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && 'master' || github.event.number }} path: selfdrive/ui/tests/test_ui/raylib_report/screenshots + + create_mici_raylib_ui_report: + name: Create mici raylib UI Report + runs-on: ${{ + (github.repository == 'commaai/openpilot') && + ((github.event_name != 'pull_request') || + (github.event.pull_request.head.repo.full_name == 'commaai/openpilot')) + && fromJSON('["namespace-profile-amd64-8x16", "namespace-experiments:docker.builds.local-cache=separate"]') + || fromJSON('["ubuntu-24.04"]') }} + steps: + - uses: actions/checkout@v4 + with: + submodules: true + - uses: ./.github/workflows/setup-with-retry + - name: Build openpilot + run: ${{ env.RUN }} "scons -j$(nproc)" + - name: Create mici raylib UI Report + run: > + ${{ env.RUN }} "PYTHONWARNINGS=ignore && + source selfdrive/test/setup_xvfb.sh && + WINDOWED=1 python3 selfdrive/ui/tests/diff/replay.py" + - name: Upload Raylib UI Report + uses: actions/upload-artifact@v4 + with: + name: mici-raylib-report-${{ inputs.run_number || '1' }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && 'master' || github.event.number }} + path: selfdrive/ui/tests/diff/report diff --git a/pyproject.toml b/pyproject.toml index b2acf1c09b..b45a808f1d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -85,6 +85,7 @@ docs = [ ] testing = [ + "coverage", "hypothesis ==6.47.*", "mypy", "pytest", diff --git a/selfdrive/ui/tests/.gitignore b/selfdrive/ui/tests/.gitignore index d926a7ae86..98f2a5e8ce 100644 --- a/selfdrive/ui/tests/.gitignore +++ b/selfdrive/ui/tests/.gitignore @@ -2,3 +2,8 @@ test test_translations test_ui/report_1 test_ui/raylib_report + +diff/*.mp4 +diff/*.html +diff/.coverage +diff/htmlcov/ diff --git a/selfdrive/ui/tests/diff/diff.py b/selfdrive/ui/tests/diff/diff.py new file mode 100755 index 0000000000..be7af5438a --- /dev/null +++ b/selfdrive/ui/tests/diff/diff.py @@ -0,0 +1,201 @@ +#!/usr/bin/env python3 +import os +import sys +import subprocess +import tempfile +import base64 +import webbrowser +import argparse +from pathlib import Path +from openpilot.common.basedir import BASEDIR + +DIFF_OUT_DIR = Path(BASEDIR) / "selfdrive" / "ui" / "tests" / "diff" / "report" + + +def extract_frames(video_path, output_dir): + output_pattern = str(output_dir / "frame_%04d.png") + cmd = ['ffmpeg', '-i', video_path, '-vsync', '0', output_pattern, '-y'] + subprocess.run(cmd, capture_output=True, check=True) + frames = sorted(output_dir.glob("frame_*.png")) + return frames + + +def compare_frames(frame1_path, frame2_path): + result = subprocess.run(['cmp', '-s', frame1_path, frame2_path]) + return result.returncode == 0 + + +def frame_to_data_url(frame_path): + with open(frame_path, 'rb') as f: + data = f.read() + return f"data:image/png;base64,{base64.b64encode(data).decode()}" + + +def create_diff_video(video1, video2, output_path): + """Create a diff video using ffmpeg blend filter with difference mode.""" + print("Creating diff video...") + cmd = ['ffmpeg', '-i', video1, '-i', video2, '-filter_complex', '[0:v]blend=all_mode=difference', '-vsync', '0', '-y', output_path] + subprocess.run(cmd, capture_output=True, check=True) + + +def find_differences(video1, video2): + with tempfile.TemporaryDirectory() as tmpdir: + tmpdir = Path(tmpdir) + + print(f"Extracting frames from {video1}...") + frames1_dir = tmpdir / "frames1" + frames1_dir.mkdir() + frames1 = extract_frames(video1, frames1_dir) + + print(f"Extracting frames from {video2}...") + frames2_dir = tmpdir / "frames2" + frames2_dir.mkdir() + frames2 = extract_frames(video2, frames2_dir) + + if len(frames1) != len(frames2): + print(f"WARNING: Frame count mismatch: {len(frames1)} vs {len(frames2)}") + min_frames = min(len(frames1), len(frames2)) + frames1 = frames1[:min_frames] + frames2 = frames2[:min_frames] + + print(f"Comparing {len(frames1)} frames...") + different_frames = [] + frame_data = [] + + for i, (f1, f2) in enumerate(zip(frames1, frames2, strict=False)): + is_different = not compare_frames(f1, f2) + if is_different: + different_frames.append(i) + + if i < 10 or i >= len(frames1) - 10 or is_different: + frame_data.append({'index': i, 'different': is_different, 'frame1_url': frame_to_data_url(f1), 'frame2_url': frame_to_data_url(f2)}) + + return different_frames, frame_data, len(frames1) + + +def generate_html_report(video1, video2, basedir, different_frames, frame_data, total_frames): + chunks = [] + if different_frames: + current_chunk = [different_frames[0]] + for i in range(1, len(different_frames)): + if different_frames[i] == different_frames[i - 1] + 1: + current_chunk.append(different_frames[i]) + else: + chunks.append(current_chunk) + current_chunk = [different_frames[i]] + chunks.append(current_chunk) + + result_text = ( + f"✅ Videos are identical! ({total_frames} frames)" + if len(different_frames) == 0 + else f"❌ Found {len(different_frames)} different frames out of {total_frames} total ({(len(different_frames) / total_frames * 100):.1f}%)" + ) + + html = f"""

UI Diff

+ + + + + + +
+

Video 1

+ +
+

Video 2

+ +
+

Pixel Diff

+ +
+ +
+

Results: {result_text}

+""" + return html + + +def main(): + parser = argparse.ArgumentParser(description='Compare two videos and generate HTML diff report') + parser.add_argument('video1', help='First video file') + parser.add_argument('video2', help='Second video file') + parser.add_argument('output', nargs='?', default='diff.html', help='Output HTML file (default: diff.html)') + parser.add_argument("--basedir", type=str, help="Base directory for output", default="") + parser.add_argument('--no-open', action='store_true', help='Do not open HTML report in browser') + + args = parser.parse_args() + + os.makedirs(DIFF_OUT_DIR, exist_ok=True) + + print("=" * 60) + print("VIDEO DIFF - HTML REPORT") + print("=" * 60) + print(f"Video 1: {args.video1}") + print(f"Video 2: {args.video2}") + print(f"Output: {args.output}") + print() + + # Create diff video + diff_video_path = os.path.join(os.path.dirname(args.output), DIFF_OUT_DIR / "diff.mp4") + create_diff_video(args.video1, args.video2, diff_video_path) + + different_frames, frame_data, total_frames = find_differences(args.video1, args.video2) + + if different_frames is None: + sys.exit(1) + + print() + print("Generating HTML report...") + html = generate_html_report(args.video1, args.video2, args.basedir, different_frames, frame_data, total_frames) + + with open(DIFF_OUT_DIR / args.output, 'w') as f: + f.write(html) + + # Open in browser by default + if not args.no_open: + print(f"Opening {args.output} in browser...") + webbrowser.open(f'file://{os.path.abspath(DIFF_OUT_DIR / args.output)}') + + return 0 if len(different_frames) == 0 else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/selfdrive/ui/tests/diff/replay.py b/selfdrive/ui/tests/diff/replay.py new file mode 100755 index 0000000000..44df75fa5d --- /dev/null +++ b/selfdrive/ui/tests/diff/replay.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 +import os +import time +import coverage +import pyray as rl +from openpilot.selfdrive.ui.tests.diff.diff import DIFF_OUT_DIR + +os.environ["RECORD"] = "1" +if "RECORD_OUTPUT" not in os.environ: + os.environ["RECORD_OUTPUT"] = "mici_ui_replay.mp4" + +os.environ["RECORD_OUTPUT"] = os.path.join(DIFF_OUT_DIR, os.environ["RECORD_OUTPUT"]) + +from openpilot.common.params import Params +from openpilot.system.version import terms_version, training_version +from openpilot.system.ui.lib.application import gui_app, MousePos, MouseEvent +from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.selfdrive.ui.mici.layouts.main import MiciMainLayout + +FPS = 60 +HEADLESS = os.getenv("WINDOWED", "0") == "1" + +SCRIPT = [ + (0, None), + (FPS * 1, (100, 100)), + (FPS * 2, None), +] + + +def setup_state(): + params = Params() + params.put("HasAcceptedTerms", terms_version) + params.put("CompletedTrainingVersion", training_version) + params.put("DongleId", "test123456789") + params.put("UpdaterCurrentDescription", "0.10.1 / test-branch / abc1234 / Nov 30") + return None + + +def inject_click(x, y): + press_event = MouseEvent(pos=MousePos(x, y), slot=0, left_pressed=True, left_released=False, left_down=True, t=time.monotonic()) + + release_event = MouseEvent(pos=MousePos(x, y), slot=0, left_pressed=False, left_released=True, left_down=False, t=time.monotonic()) + + with gui_app._mouse._lock: + gui_app._mouse._events.append(press_event) + gui_app._mouse._events.append(release_event) + + +def run_replay(): + setup_state() + os.makedirs(DIFF_OUT_DIR, exist_ok=True) + + if not HEADLESS: + rl.set_config_flags(rl.FLAG_WINDOW_HIDDEN) + gui_app.init_window("ui diff test", fps=FPS) + main_layout = MiciMainLayout() + main_layout.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) + + frame = 0 + script_index = 0 + + for should_render in gui_app.render(): + while script_index < len(SCRIPT) and SCRIPT[script_index][0] == frame: + _, coords = SCRIPT[script_index] + if coords is not None: + inject_click(*coords) + script_index += 1 + + ui_state.update() + + if should_render: + main_layout.render() + + frame += 1 + + if script_index >= len(SCRIPT): + break + + gui_app.close() + + print(f"Total frames: {frame}") + print(f"Video saved to: {os.environ['RECORD_OUTPUT']}") + + +def main(): + cov = coverage.coverage(source=['openpilot.selfdrive.ui.mici']) + with cov.collect(): + run_replay() + cov.stop() + cov.save() + cov.report() + cov.html_report(directory=os.path.join(DIFF_OUT_DIR, 'htmlcov')) + print("HTML report: htmlcov/index.html") + + +if __name__ == "__main__": + main() diff --git a/uv.lock b/uv.lock index c34a6d9b71..650220c875 100644 --- a/uv.lock +++ b/uv.lock @@ -371,6 +371,41 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" }, ] +[[package]] +name = "coverage" +version = "7.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/89/26/4a96807b193b011588099c3b5c89fbb05294e5b90e71018e065465f34eb6/coverage-7.12.0.tar.gz", hash = "sha256:fc11e0a4e372cb5f282f16ef90d4a585034050ccda536451901abfb19a57f40c", size = 819341, upload-time = "2025-11-18T13:34:20.766Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/0c/0dfe7f0487477d96432e4815537263363fb6dd7289743a796e8e51eabdf2/coverage-7.12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aa124a3683d2af98bd9d9c2bfa7a5076ca7e5ab09fdb96b81fa7d89376ae928f", size = 217535, upload-time = "2025-11-18T13:32:08.812Z" }, + { url = "https://files.pythonhosted.org/packages/9b/f5/f9a4a053a5bbff023d3bec259faac8f11a1e5a6479c2ccf586f910d8dac7/coverage-7.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d93fbf446c31c0140208dcd07c5d882029832e8ed7891a39d6d44bd65f2316c3", size = 218044, upload-time = "2025-11-18T13:32:10.329Z" }, + { url = "https://files.pythonhosted.org/packages/95/c5/84fc3697c1fa10cd8571919bf9693f693b7373278daaf3b73e328d502bc8/coverage-7.12.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:52ca620260bd8cd6027317bdd8b8ba929be1d741764ee765b42c4d79a408601e", size = 248440, upload-time = "2025-11-18T13:32:12.536Z" }, + { url = "https://files.pythonhosted.org/packages/f4/36/2d93fbf6a04670f3874aed397d5a5371948a076e3249244a9e84fb0e02d6/coverage-7.12.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f3433ffd541380f3a0e423cff0f4926d55b0cc8c1d160fdc3be24a4c03aa65f7", size = 250361, upload-time = "2025-11-18T13:32:13.852Z" }, + { url = "https://files.pythonhosted.org/packages/5d/49/66dc65cc456a6bfc41ea3d0758c4afeaa4068a2b2931bf83be6894cf1058/coverage-7.12.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7bbb321d4adc9f65e402c677cd1c8e4c2d0105d3ce285b51b4d87f1d5db5245", size = 252472, upload-time = "2025-11-18T13:32:15.068Z" }, + { url = "https://files.pythonhosted.org/packages/35/1f/ebb8a18dffd406db9fcd4b3ae42254aedcaf612470e8712f12041325930f/coverage-7.12.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22a7aade354a72dff3b59c577bfd18d6945c61f97393bc5fb7bd293a4237024b", size = 248592, upload-time = "2025-11-18T13:32:16.328Z" }, + { url = "https://files.pythonhosted.org/packages/da/a8/67f213c06e5ea3b3d4980df7dc344d7fea88240b5fe878a5dcbdfe0e2315/coverage-7.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3ff651dcd36d2fea66877cd4a82de478004c59b849945446acb5baf9379a1b64", size = 250167, upload-time = "2025-11-18T13:32:17.687Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/e52aef68154164ea40cc8389c120c314c747fe63a04b013a5782e989b77f/coverage-7.12.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:31b8b2e38391a56e3cea39d22a23faaa7c3fc911751756ef6d2621d2a9daf742", size = 248238, upload-time = "2025-11-18T13:32:19.2Z" }, + { url = "https://files.pythonhosted.org/packages/1f/a4/4d88750bcf9d6d66f77865e5a05a20e14db44074c25fd22519777cb69025/coverage-7.12.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:297bc2da28440f5ae51c845a47c8175a4db0553a53827886e4fb25c66633000c", size = 247964, upload-time = "2025-11-18T13:32:21.027Z" }, + { url = "https://files.pythonhosted.org/packages/a7/6b/b74693158899d5b47b0bf6238d2c6722e20ba749f86b74454fac0696bb00/coverage-7.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6ff7651cc01a246908eac162a6a86fc0dbab6de1ad165dfb9a1e2ec660b44984", size = 248862, upload-time = "2025-11-18T13:32:22.304Z" }, + { url = "https://files.pythonhosted.org/packages/18/de/6af6730227ce0e8ade307b1cc4a08e7f51b419a78d02083a86c04ccceb29/coverage-7.12.0-cp311-cp311-win32.whl", hash = "sha256:313672140638b6ddb2c6455ddeda41c6a0b208298034544cfca138978c6baed6", size = 220033, upload-time = "2025-11-18T13:32:23.714Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a1/e7f63021a7c4fe20994359fcdeae43cbef4a4d0ca36a5a1639feeea5d9e1/coverage-7.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:a1783ed5bd0d5938d4435014626568dc7f93e3cb99bc59188cc18857c47aa3c4", size = 220966, upload-time = "2025-11-18T13:32:25.599Z" }, + { url = "https://files.pythonhosted.org/packages/77/e8/deae26453f37c20c3aa0c4433a1e32cdc169bf415cce223a693117aa3ddd/coverage-7.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:4648158fd8dd9381b5847622df1c90ff314efbfc1df4550092ab6013c238a5fc", size = 219637, upload-time = "2025-11-18T13:32:27.265Z" }, + { url = "https://files.pythonhosted.org/packages/02/bf/638c0427c0f0d47638242e2438127f3c8ee3cfc06c7fdeb16778ed47f836/coverage-7.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:29644c928772c78512b48e14156b81255000dcfd4817574ff69def189bcb3647", size = 217704, upload-time = "2025-11-18T13:32:28.906Z" }, + { url = "https://files.pythonhosted.org/packages/08/e1/706fae6692a66c2d6b871a608bbde0da6281903fa0e9f53a39ed441da36a/coverage-7.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8638cbb002eaa5d7c8d04da667813ce1067080b9a91099801a0053086e52b736", size = 218064, upload-time = "2025-11-18T13:32:30.161Z" }, + { url = "https://files.pythonhosted.org/packages/a9/8b/eb0231d0540f8af3ffda39720ff43cb91926489d01524e68f60e961366e4/coverage-7.12.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:083631eeff5eb9992c923e14b810a179798bb598e6a0dd60586819fc23be6e60", size = 249560, upload-time = "2025-11-18T13:32:31.835Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a1/67fb52af642e974d159b5b379e4d4c59d0ebe1288677fbd04bbffe665a82/coverage-7.12.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:99d5415c73ca12d558e07776bd957c4222c687b9f1d26fa0e1b57e3598bdcde8", size = 252318, upload-time = "2025-11-18T13:32:33.178Z" }, + { url = "https://files.pythonhosted.org/packages/41/e5/38228f31b2c7665ebf9bdfdddd7a184d56450755c7e43ac721c11a4b8dab/coverage-7.12.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e949ebf60c717c3df63adb4a1a366c096c8d7fd8472608cd09359e1bd48ef59f", size = 253403, upload-time = "2025-11-18T13:32:34.45Z" }, + { url = "https://files.pythonhosted.org/packages/ec/4b/df78e4c8188f9960684267c5a4897836f3f0f20a20c51606ee778a1d9749/coverage-7.12.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d907ddccbca819afa2cd014bc69983b146cca2735a0b1e6259b2a6c10be1e70", size = 249984, upload-time = "2025-11-18T13:32:35.747Z" }, + { url = "https://files.pythonhosted.org/packages/ba/51/bb163933d195a345c6f63eab9e55743413d064c291b6220df754075c2769/coverage-7.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b1518ecbad4e6173f4c6e6c4a46e49555ea5679bf3feda5edb1b935c7c44e8a0", size = 251339, upload-time = "2025-11-18T13:32:37.352Z" }, + { url = "https://files.pythonhosted.org/packages/15/40/c9b29cdb8412c837cdcbc2cfa054547dd83affe6cbbd4ce4fdb92b6ba7d1/coverage-7.12.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:51777647a749abdf6f6fd8c7cffab12de68ab93aab15efc72fbbb83036c2a068", size = 249489, upload-time = "2025-11-18T13:32:39.212Z" }, + { url = "https://files.pythonhosted.org/packages/c8/da/b3131e20ba07a0de4437a50ef3b47840dfabf9293675b0cd5c2c7f66dd61/coverage-7.12.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:42435d46d6461a3b305cdfcad7cdd3248787771f53fe18305548cba474e6523b", size = 249070, upload-time = "2025-11-18T13:32:40.598Z" }, + { url = "https://files.pythonhosted.org/packages/70/81/b653329b5f6302c08d683ceff6785bc60a34be9ae92a5c7b63ee7ee7acec/coverage-7.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5bcead88c8423e1855e64b8057d0544e33e4080b95b240c2a355334bb7ced937", size = 250929, upload-time = "2025-11-18T13:32:42.915Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/250ac3bca9f252a5fb1338b5ad01331ebb7b40223f72bef5b1b2cb03aa64/coverage-7.12.0-cp312-cp312-win32.whl", hash = "sha256:dcbb630ab034e86d2a0f79aefd2be07e583202f41e037602d438c80044957baa", size = 220241, upload-time = "2025-11-18T13:32:44.665Z" }, + { url = "https://files.pythonhosted.org/packages/64/1c/77e79e76d37ce83302f6c21980b45e09f8aa4551965213a10e62d71ce0ab/coverage-7.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:2fd8354ed5d69775ac42986a691fbf68b4084278710cee9d7c3eaa0c28fa982a", size = 221051, upload-time = "2025-11-18T13:32:46.008Z" }, + { url = "https://files.pythonhosted.org/packages/31/f5/641b8a25baae564f9e52cac0e2667b123de961985709a004e287ee7663cc/coverage-7.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:737c3814903be30695b2de20d22bcc5428fdae305c61ba44cdc8b3252984c49c", size = 219692, upload-time = "2025-11-18T13:32:47.372Z" }, + { url = "https://files.pythonhosted.org/packages/ce/a3/43b749004e3c09452e39bb56347a008f0a0668aad37324a99b5c8ca91d9e/coverage-7.12.0-py3-none-any.whl", hash = "sha256:159d50c0b12e060b15ed3d39f87ed43d4f7f7ad40b8a534f4dd331adbb51104a", size = 209503, upload-time = "2025-11-18T13:34:18.892Z" }, +] + [[package]] name = "crcmod" version = "1.7" @@ -1349,6 +1384,7 @@ docs = [ ] testing = [ { name = "codespell" }, + { name = "coverage" }, { name = "hypothesis" }, { name = "mypy" }, { name = "pre-commit-hooks" }, @@ -1364,7 +1400,7 @@ testing = [ { name = "ruff" }, ] tools = [ - { name = "dearpygui" }, + { name = "dearpygui", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, { name = "metadrive-simulator", marker = "platform_machine != 'aarch64'" }, ] @@ -1378,10 +1414,11 @@ requires-dist = [ { name = "casadi", specifier = ">=3.6.6" }, { name = "cffi" }, { name = "codespell", marker = "extra == 'testing'" }, + { name = "coverage", marker = "extra == 'testing'" }, { name = "crcmod" }, { name = "cython" }, { name = "dbus-next", marker = "extra == 'dev'" }, - { name = "dearpygui", marker = "extra == 'tools'", specifier = ">=2.1.0" }, + { name = "dearpygui", marker = "(platform_machine != 'aarch64' and extra == 'tools') or (sys_platform != 'linux' and extra == 'tools')", specifier = ">=2.1.0" }, { name = "dictdiffer", marker = "extra == 'dev'" }, { name = "future-fstrings" }, { name = "hypothesis", marker = "extra == 'testing'", specifier = "==6.47.*" }, From 48a42a9c53b894da327f832c527be4f4c80a5702 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=80=8C=20crwusiz=20=E3=80=8D?= <43285072+crwusiz@users.noreply.github.com> Date: Tue, 9 Dec 2025 11:43:31 +0900 Subject: [PATCH 084/110] UI: Color Constants Uppercase (#36796) --- selfdrive/ui/mici/onroad/hud_renderer.py | 8 ++-- selfdrive/ui/onroad/hud_renderer.py | 52 ++++++++++++------------ 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/selfdrive/ui/mici/onroad/hud_renderer.py b/selfdrive/ui/mici/onroad/hud_renderer.py index 76b443842e..7f489ccf98 100644 --- a/selfdrive/ui/mici/onroad/hud_renderer.py +++ b/selfdrive/ui/mici/onroad/hud_renderer.py @@ -30,8 +30,8 @@ class FontSizes: @dataclass(frozen=True) class Colors: - white: rl.Color = rl.WHITE - white_translucent: rl.Color = rl.Color(255, 255, 255, 200) + WHITE = rl.WHITE + WHITE_TRANSLUCENT = rl.Color(255, 255, 255, 200) FONT_SIZES = FontSizes() @@ -269,9 +269,9 @@ class HudRenderer(Widget): speed_text = str(round(self.speed)) speed_text_size = measure_text_cached(self._font_bold, speed_text, FONT_SIZES.current_speed) speed_pos = rl.Vector2(rect.x + rect.width / 2 - speed_text_size.x / 2, 180 - speed_text_size.y / 2) - rl.draw_text_ex(self._font_bold, speed_text, speed_pos, FONT_SIZES.current_speed, 0, COLORS.white) + rl.draw_text_ex(self._font_bold, speed_text, speed_pos, FONT_SIZES.current_speed, 0, COLORS.WHITE) unit_text = tr("km/h") if ui_state.is_metric else tr("mph") unit_text_size = measure_text_cached(self._font_medium, unit_text, FONT_SIZES.speed_unit) unit_pos = rl.Vector2(rect.x + rect.width / 2 - unit_text_size.x / 2, 290 - unit_text_size.y / 2) - rl.draw_text_ex(self._font_medium, unit_text, unit_pos, FONT_SIZES.speed_unit, 0, COLORS.white_translucent) + rl.draw_text_ex(self._font_medium, unit_text, unit_pos, FONT_SIZES.speed_unit, 0, COLORS.WHITE_TRANSLUCENT) diff --git a/selfdrive/ui/onroad/hud_renderer.py b/selfdrive/ui/onroad/hud_renderer.py index a2459c27e2..79f150deea 100644 --- a/selfdrive/ui/onroad/hud_renderer.py +++ b/selfdrive/ui/onroad/hud_renderer.py @@ -35,20 +35,20 @@ class FontSizes: @dataclass(frozen=True) class Colors: - white: rl.Color = rl.WHITE - disengaged: rl.Color = rl.Color(145, 155, 149, 255) - override: rl.Color = rl.Color(145, 155, 149, 255) # Added - engaged: rl.Color = rl.Color(128, 216, 166, 255) - disengaged_bg: rl.Color = rl.Color(0, 0, 0, 153) - override_bg: rl.Color = rl.Color(145, 155, 149, 204) - engaged_bg: rl.Color = rl.Color(128, 216, 166, 204) - grey: rl.Color = rl.Color(166, 166, 166, 255) - dark_grey: rl.Color = rl.Color(114, 114, 114, 255) - black_translucent: rl.Color = rl.Color(0, 0, 0, 166) - white_translucent: rl.Color = rl.Color(255, 255, 255, 200) - border_translucent: rl.Color = rl.Color(255, 255, 255, 75) - header_gradient_start: rl.Color = rl.Color(0, 0, 0, 114) - header_gradient_end: rl.Color = rl.BLANK + WHITE = rl.WHITE + DISENGAGED = rl.Color(145, 155, 149, 255) + OVERRIDE = rl.Color(145, 155, 149, 255) # Added + ENGAGED = rl.Color(128, 216, 166, 255) + DISENGAGED_BG = rl.Color(0, 0, 0, 153) + OVERRIDE_BG = rl.Color(145, 155, 149, 204) + ENGAGED_BG = rl.Color(128, 216, 166, 204) + GREY = rl.Color(166, 166, 166, 255) + DARK_GREY = rl.Color(114, 114, 114, 255) + BLACK_TRANSLUCENT = rl.Color(0, 0, 0, 166) + WHITE_TRANSLUCENT = rl.Color(255, 255, 255, 200) + BORDER_TRANSLUCENT = rl.Color(255, 255, 255, 75) + HEADER_GRADIENT_START = rl.Color(0, 0, 0, 114) + HEADER_GRADIENT_END = rl.BLANK UI_CONFIG = UIConfig() @@ -108,8 +108,8 @@ class HudRenderer(Widget): int(rect.y), int(rect.width), UI_CONFIG.header_height, - COLORS.header_gradient_start, - COLORS.header_gradient_end, + COLORS.HEADER_GRADIENT_START, + COLORS.HEADER_GRADIENT_END, ) if self.is_cruise_available: @@ -131,19 +131,19 @@ class HudRenderer(Widget): y = rect.y + 45 set_speed_rect = rl.Rectangle(x, y, set_speed_width, UI_CONFIG.set_speed_height) - rl.draw_rectangle_rounded(set_speed_rect, 0.35, 10, COLORS.black_translucent) - rl.draw_rectangle_rounded_lines_ex(set_speed_rect, 0.35, 10, 6, COLORS.border_translucent) + rl.draw_rectangle_rounded(set_speed_rect, 0.35, 10, COLORS.BLACK_TRANSLUCENT) + rl.draw_rectangle_rounded_lines_ex(set_speed_rect, 0.35, 10, 6, COLORS.BORDER_TRANSLUCENT) - max_color = COLORS.grey - set_speed_color = COLORS.dark_grey + max_color = COLORS.GREY + set_speed_color = COLORS.DARK_GREY if self.is_cruise_set: - set_speed_color = COLORS.white + set_speed_color = COLORS.WHITE if ui_state.status == UIStatus.ENGAGED: - max_color = COLORS.engaged + max_color = COLORS.ENGAGED elif ui_state.status == UIStatus.DISENGAGED: - max_color = COLORS.disengaged + max_color = COLORS.DISENGAGED elif ui_state.status == UIStatus.OVERRIDE: - max_color = COLORS.override + max_color = COLORS.OVERRIDE max_text = tr("MAX") max_text_width = measure_text_cached(self._font_semi_bold, max_text, FONT_SIZES.max_speed).x @@ -172,9 +172,9 @@ class HudRenderer(Widget): speed_text = str(round(self.speed)) speed_text_size = measure_text_cached(self._font_bold, speed_text, FONT_SIZES.current_speed) speed_pos = rl.Vector2(rect.x + rect.width / 2 - speed_text_size.x / 2, 180 - speed_text_size.y / 2) - rl.draw_text_ex(self._font_bold, speed_text, speed_pos, FONT_SIZES.current_speed, 0, COLORS.white) + rl.draw_text_ex(self._font_bold, speed_text, speed_pos, FONT_SIZES.current_speed, 0, COLORS.WHITE) unit_text = tr("km/h") if ui_state.is_metric else tr("mph") unit_text_size = measure_text_cached(self._font_medium, unit_text, FONT_SIZES.speed_unit) unit_pos = rl.Vector2(rect.x + rect.width / 2 - unit_text_size.x / 2, 290 - unit_text_size.y / 2) - rl.draw_text_ex(self._font_medium, unit_text, unit_pos, FONT_SIZES.speed_unit, 0, COLORS.white_translucent) + rl.draw_text_ex(self._font_medium, unit_text, unit_pos, FONT_SIZES.speed_unit, 0, COLORS.WHITE_TRANSLUCENT) From d5f694650295e6a02efdb86e80194c2c6e751341 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Mon, 8 Dec 2025 18:48:58 -0800 Subject: [PATCH 085/110] camerad: probe os first --- system/camerad/cameras/spectra.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/system/camerad/cameras/spectra.cc b/system/camerad/cameras/spectra.cc index caf7871573..0d93b70465 100644 --- a/system/camerad/cameras/spectra.cc +++ b/system/camerad/cameras/spectra.cc @@ -1004,8 +1004,8 @@ bool SpectraCamera::openSensor() { }; // Figure out which sensor we have - if (!init_sensor_lambda(new OX03C10) && - !init_sensor_lambda(new OS04C10)) { + if (!init_sensor_lambda(new OS04C10) && + !init_sensor_lambda(new OX03C10)) { LOGE("** sensor %d FAILED bringup, disabling", cc.camera_num); enabled = false; return false; From 8d9e203130fa2c3c75af24955a628c278a694b83 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Mon, 8 Dec 2025 19:25:09 -0800 Subject: [PATCH 086/110] raylib ui diff: swipe around (#36807) * swipe support * swipe around * same --- selfdrive/ui/tests/diff/replay.py | 62 +++++++++++++++++++++++++------ 1 file changed, 50 insertions(+), 12 deletions(-) diff --git a/selfdrive/ui/tests/diff/replay.py b/selfdrive/ui/tests/diff/replay.py index 44df75fa5d..cbb2d606e0 100755 --- a/selfdrive/ui/tests/diff/replay.py +++ b/selfdrive/ui/tests/diff/replay.py @@ -3,6 +3,7 @@ import os import time import coverage import pyray as rl +from dataclasses import dataclass from openpilot.selfdrive.ui.tests.diff.diff import DIFF_OUT_DIR os.environ["RECORD"] = "1" @@ -20,10 +21,28 @@ from openpilot.selfdrive.ui.mici.layouts.main import MiciMainLayout FPS = 60 HEADLESS = os.getenv("WINDOWED", "0") == "1" + +@dataclass +class DummyEvent: + click: bool = False + # TODO: add some kind of intensity + swipe_left: bool = False + swipe_right: bool = False + swipe_down: bool = False + + SCRIPT = [ - (0, None), - (FPS * 1, (100, 100)), - (FPS * 2, None), + (0, DummyEvent()), + (FPS * 1, DummyEvent(swipe_right=True)), + (FPS * 2, DummyEvent(swipe_left=True)), + (FPS * 3, DummyEvent(swipe_left=True)), + (FPS * 4, DummyEvent(click=True)), + (FPS * 5, DummyEvent(click=True)), + (FPS * 6, DummyEvent(swipe_left=True)), + (FPS * 7, DummyEvent(swipe_left=True)), + (FPS * 8, DummyEvent(swipe_right=True)), + (FPS * 9, DummyEvent(swipe_down=True)), + (FPS * 10, DummyEvent()), ] @@ -36,14 +55,34 @@ def setup_state(): return None -def inject_click(x, y): - press_event = MouseEvent(pos=MousePos(x, y), slot=0, left_pressed=True, left_released=False, left_down=True, t=time.monotonic()) - - release_event = MouseEvent(pos=MousePos(x, y), slot=0, left_pressed=False, left_released=True, left_down=False, t=time.monotonic()) +def inject_click(coords): + events = [] + x, y = coords[0] + events.append(MouseEvent(pos=MousePos(x, y), slot=0, left_pressed=True, left_released=False, left_down=False, t=time.monotonic())) + for x, y in coords[1:]: + events.append(MouseEvent(pos=MousePos(x, y), slot=0, left_pressed=False, left_released=False, left_down=True, t=time.monotonic())) + x, y = coords[-1] + events.append(MouseEvent(pos=MousePos(x, y), slot=0, left_pressed=False, left_released=True, left_down=False, t=time.monotonic())) with gui_app._mouse._lock: - gui_app._mouse._events.append(press_event) - gui_app._mouse._events.append(release_event) + gui_app._mouse._events.extend(events) + + +def handle_event(event: DummyEvent): + if event.click: + inject_click([(gui_app.width // 2, gui_app.height // 2)]) + if event.swipe_left: + inject_click([(gui_app.width * 3 // 4, gui_app.height // 2), + (gui_app.width // 4, gui_app.height // 2), + (0, gui_app.height // 2)]) + if event.swipe_right: + inject_click([(gui_app.width // 4, gui_app.height // 2), + (gui_app.width * 3 // 4, gui_app.height // 2), + (gui_app.width, gui_app.height // 2)]) + if event.swipe_down: + inject_click([(gui_app.width // 2, gui_app.height // 4), + (gui_app.width // 2, gui_app.height * 3 // 4), + (gui_app.width // 2, gui_app.height)]) def run_replay(): @@ -61,9 +100,8 @@ def run_replay(): for should_render in gui_app.render(): while script_index < len(SCRIPT) and SCRIPT[script_index][0] == frame: - _, coords = SCRIPT[script_index] - if coords is not None: - inject_click(*coords) + _, event = SCRIPT[script_index] + handle_event(event) script_index += 1 ui_state.update() From c85db43705d829672abf8b60683eeeccafafe841 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Mon, 8 Dec 2025 20:14:19 -0800 Subject: [PATCH 087/110] camerad: misc labeling/cleanup (#36809) * what's 2c * include * no idea what this means * register comments --- system/camerad/sensors/os04c10.cc | 3 ++- system/camerad/sensors/os04c10_registers.h | 26 +++++++++++----------- system/camerad/sensors/ox03c10.cc | 5 +++-- 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/system/camerad/sensors/os04c10.cc b/system/camerad/sensors/os04c10.cc index 38be4ecca4..62c26ca809 100644 --- a/system/camerad/sensors/os04c10.cc +++ b/system/camerad/sensors/os04c10.cc @@ -1,6 +1,7 @@ #include #include "system/camerad/sensors/sensor.h" +#include "third_party/linux/include/msm_camsensor_sdk.h" namespace { @@ -51,7 +52,7 @@ OS04C10::OS04C10() { probe_expected_data = 0x5304; bits_per_pixel = 12; mipi_format = CAM_FORMAT_MIPI_RAW_12; - frame_data_type = 0x2c; + frame_data_type = CSI_RAW12; mclk_frequency = 24000000; // Hz // TODO: this was set from logs. actually calculate it out diff --git a/system/camerad/sensors/os04c10_registers.h b/system/camerad/sensors/os04c10_registers.h index 7cd9e97be5..28d6b3310c 100644 --- a/system/camerad/sensors/os04c10_registers.h +++ b/system/camerad/sensors/os04c10_registers.h @@ -4,10 +4,10 @@ const struct i2c_random_wr_payload start_reg_array_os04c10[] = {{0x100, 1}}; const struct i2c_random_wr_payload stop_reg_array_os04c10[] = {{0x100, 0}}; const struct i2c_random_wr_payload init_array_os04c10[] = { - // DP_2688X1520_NEWSTG_MIPI0776Mbps_30FPS_10BIT_FOURLANE - {0x0103, 0x01}, + // baseed on DP_2688X1520_NEWSTG_MIPI0776Mbps_30FPS_10BIT_FOURLANE + {0x0103, 0x01}, // software reset - // PLL + // PLL + clocks {0x0301, 0xe4}, {0x0303, 0x01}, {0x0305, 0xb6}, @@ -24,7 +24,7 @@ const struct i2c_random_wr_payload init_array_os04c10[] = { {0x3106, 0x21}, {0x3107, 0xa1}, - // ? + // Analog/timing fine-tuning block {0x3624, 0x00}, {0x3625, 0x4c}, {0x3660, 0x04}, @@ -101,7 +101,7 @@ const struct i2c_random_wr_payload init_array_os04c10[] = { {0x3f00, 0x0b}, {0x3f06, 0x04}, - // BLC + // BLC - black level correction {0x400a, 0x01}, {0x400b, 0x50}, {0x400e, 0x08}, @@ -157,7 +157,7 @@ const struct i2c_random_wr_payload init_array_os04c10[] = { {0x5180, 0x70}, {0x5181, 0x10}, - // DPC + // DPC - defective pixel correction {0x520a, 0x03}, {0x520b, 0x06}, {0x520c, 0x0c}, @@ -248,7 +248,7 @@ const struct i2c_random_wr_payload init_array_os04c10[] = { {0x4008, 0x01}, {0x4009, 0x06}, - // FSIN + // FSIN - frame sync {0x3002, 0x22}, {0x3663, 0x22}, {0x368a, 0x04}, @@ -276,8 +276,8 @@ const struct i2c_random_wr_payload init_array_os04c10[] = { {0x3816, 0x03}, {0x3817, 0x01}, - {0x380c, 0x0b}, {0x380d, 0xac}, // HTS - {0x380e, 0x06}, {0x380f, 0x9c}, // VTS + {0x380c, 0x0b}, {0x380d, 0xac}, // HTS (line length) + {0x380e, 0x06}, {0x380f, 0x9c}, // VTS (frame length) {0x3820, 0xb3}, {0x3821, 0x01}, @@ -309,17 +309,17 @@ const struct i2c_random_wr_payload init_array_os04c10[] = { // initialize exposure {0x3503, 0x88}, - // long + // long exposure {0x3500, 0x00}, {0x3501, 0x00}, {0x3502, 0x10}, {0x3508, 0x00}, {0x3509, 0x80}, {0x350a, 0x04}, {0x350b, 0x00}, - // short + // short exposure {0x3510, 0x00}, {0x3511, 0x00}, {0x3512, 0x40}, {0x350c, 0x00}, {0x350d, 0x80}, {0x350e, 0x04}, {0x350f, 0x00}, - // wb + // white balance // b {0x5100, 0x06}, {0x5101, 0x7e}, {0x5140, 0x06}, {0x5141, 0x7e}, @@ -332,7 +332,7 @@ const struct i2c_random_wr_payload init_array_os04c10[] = { }; const struct i2c_random_wr_payload ife_downscale_override_array_os04c10[] = { - // OS04C10_AA_00_02_17_wAO_2688x1524_MIPI728Mbps_Linear12bit_20FPS_4Lane_MCLK24MHz + // based on OS04C10_AA_00_02_17_wAO_2688x1524_MIPI728Mbps_Linear12bit_20FPS_4Lane_MCLK24MHz {0x3c8c, 0x40}, {0x3714, 0x24}, {0x37c2, 0x04}, diff --git a/system/camerad/sensors/ox03c10.cc b/system/camerad/sensors/ox03c10.cc index 6f7e658f48..05d58f03c6 100644 --- a/system/camerad/sensors/ox03c10.cc +++ b/system/camerad/sensors/ox03c10.cc @@ -1,6 +1,7 @@ #include #include "system/camerad/sensors/sensor.h" +#include "third_party/linux/include/msm_camsensor_sdk.h" namespace { @@ -40,8 +41,8 @@ OX03C10::OX03C10() { probe_expected_data = 0x5803; bits_per_pixel = 12; mipi_format = CAM_FORMAT_MIPI_RAW_12; - frame_data_type = 0x2c; // one is 0x2a, two are 0x2b - mclk_frequency = 24000000; //Hz + frame_data_type = CSI_RAW12; + mclk_frequency = 24000000; // Hz readout_time_ns = 14697000; From 34fed9f9082db34b95922204975f189cf9d09cfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Harald=20Sch=C3=A4fer?= Date: Tue, 9 Dec 2025 10:19:10 -0800 Subject: [PATCH 088/110] URLFILE: Need to catch max retry (#36815) Need to catch max retry --- tools/lib/url_file.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tools/lib/url_file.py b/tools/lib/url_file.py index 01c6c5dc47..c791444f74 100644 --- a/tools/lib/url_file.py +++ b/tools/lib/url_file.py @@ -9,6 +9,7 @@ from urllib3.util import Timeout from openpilot.common.utils import atomic_write from openpilot.system.hardware.hw import Paths +from urllib3.exceptions import MaxRetryError # Cache chunk size K = 1000 @@ -60,7 +61,10 @@ class URLFile: pass def _request(self, method: str, url: str, headers: dict[str, str] | None = None) -> BaseHTTPResponse: - return URLFile.pool_manager().request(method, url, timeout=self._timeout, headers=headers) + try: + return URLFile.pool_manager().request(method, url, timeout=self._timeout, headers=headers) + except MaxRetryError as e: + raise URLFileException(f"Failed to {method} {url}: {e}") from e def get_length_online(self) -> int: response = self._request('HEAD', self._url) From 6bbc3f4d1c0ec47b8da736ed1102e663c43273c2 Mon Sep 17 00:00:00 2001 From: clintonsteiner <47841949+clintonsteiner@users.noreply.github.com> Date: Tue, 9 Dec 2025 13:04:03 -0600 Subject: [PATCH 089/110] pyproject: remove pytools pinning (#36812) * pyproject: remove pytools pinning * issue requiring pin is fixed * https://github.com/inducer/pyopencl/issues/827 * uv lock --------- Co-authored-by: Adeeb Shihadeh --- pyproject.toml | 2 +- uv.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b45a808f1d..d5dc95e1b1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -116,7 +116,7 @@ dev = [ "pyautogui", "pygame", "pyopencl; platform_machine != 'aarch64'", # broken on arm64 - "pytools < 2024.1.11; platform_machine != 'aarch64'", # pyopencl use a broken version + "pytools>=2025.1.6; platform_machine != 'aarch64'", "pywinctl", "pyprof2calltree", "tabulate", diff --git a/uv.lock b/uv.lock index 650220c875..b179517e0b 100644 --- a/uv.lock +++ b/uv.lock @@ -1459,7 +1459,7 @@ requires-dist = [ { name = "pytest-subtests", marker = "extra == 'testing'" }, { name = "pytest-timeout", marker = "extra == 'testing'" }, { name = "pytest-xdist", marker = "extra == 'testing'", git = "https://github.com/sshane/pytest-xdist?rev=2b4372bd62699fb412c4fe2f95bf9f01bd2018da" }, - { name = "pytools", marker = "platform_machine != 'aarch64' and extra == 'dev'", specifier = "<2024.1.11" }, + { name = "pytools", marker = "platform_machine != 'aarch64' and extra == 'dev'", specifier = ">=2025.1.6" }, { name = "pywinctl", marker = "extra == 'dev'" }, { name = "pyzmq" }, { name = "qrcode" }, @@ -4488,16 +4488,16 @@ sdist = { url = "https://files.pythonhosted.org/packages/ef/c6/2c5999de3bb153352 [[package]] name = "pytools" -version = "2024.1.10" +version = "2025.2.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "platformdirs", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, { name = "siphash24", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, { name = "typing-extensions", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ee/0f/56e109c0307f831b5d598ad73976aaaa84b4d0e98da29a642e797eaa940c/pytools-2024.1.10.tar.gz", hash = "sha256:9af6f4b045212c49be32bb31fe19606c478ee4b09631886d05a32459f4ce0a12", size = 81741, upload-time = "2024-07-17T18:47:38.287Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/7b/f885a57e61ded45b5b10ca60f0b7575c9fb9a282e7513d0e23a33ee647e1/pytools-2025.2.5.tar.gz", hash = "sha256:a7f5350644d46d98ee9c7e67b4b41693308aa0f5e9b188d8f0694b27dc94e3a2", size = 85594, upload-time = "2025-10-07T15:53:30.49Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/66/cf/0a6aaa44b1f9e02b8c0648b5665a82246a93bcc75224c167b4fafa25c093/pytools-2024.1.10-py3-none-any.whl", hash = "sha256:9cabb71038048291400e244e2da441a051d86053339bc484e64e58d8ea263f44", size = 88108, upload-time = "2024-07-17T18:47:36.173Z" }, + { url = "https://files.pythonhosted.org/packages/f6/84/c42c29ca4bff35baa286df70b0097e0b1c88fd57e8e6bdb09cb161a6f3c1/pytools-2025.2.5-py3-none-any.whl", hash = "sha256:42e93751ec425781e103bbcd769ba35ecbacd43339c2905401608f2fdc30cf19", size = 98811, upload-time = "2025-10-07T15:53:29.089Z" }, ] [[package]] From dfd56a46d2cc263ed6054229f202357a111fed9e Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 9 Dec 2025 15:37:13 -0800 Subject: [PATCH 090/110] mici cameraview: log timings (#36816) missing from mici --- selfdrive/ui/mici/onroad/augmented_road_view.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/selfdrive/ui/mici/onroad/augmented_road_view.py b/selfdrive/ui/mici/onroad/augmented_road_view.py index ab55f392f7..71ca03cccf 100644 --- a/selfdrive/ui/mici/onroad/augmented_road_view.py +++ b/selfdrive/ui/mici/onroad/augmented_road_view.py @@ -1,6 +1,7 @@ +import time import numpy as np import pyray as rl -from cereal import car, log +from cereal import messaging, car, log from msgq.visionipc import VisionStreamType from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus from openpilot.selfdrive.ui.mici.onroad import SIDE_PANEL_WIDTH @@ -160,6 +161,9 @@ class AugmentedRoadView(CameraView): self._fade_texture = gui_app.texture("icons_mici/onroad/onroad_fade.png") + # debug + self._pm = messaging.PubMaster(['uiDebug']) + def is_swiping_left(self) -> bool: """Check if currently swiping left (for scroller to disable).""" return self._bookmark_icon.is_swiping_left() @@ -179,6 +183,7 @@ class AugmentedRoadView(CameraView): super()._handle_mouse_release(mouse_pos) def _render(self, _): + start_draw = time.monotonic() self._switch_stream_if_needed(ui_state.sm) # Update calibration before rendering @@ -244,6 +249,11 @@ class AugmentedRoadView(CameraView): rl.draw_rectangle(int(self.rect.x), int(self.rect.y), int(self.rect.width), int(self.rect.height), rl.Color(0, 0, 0, 175)) self._offroad_label.render(self._content_rect) + # publish uiDebug + msg = messaging.new_message('uiDebug') + msg.uiDebug.drawTimeMillis = (time.monotonic() - start_draw) * 1000 + self._pm.send('uiDebug', msg) + def _switch_stream_if_needed(self, sm): if sm['selfdriveState'].experimentalMode and WIDE_CAM in self.available_streams: v_ego = sm['carState'].vEgo From f78bacf96bf0da41828463141994bd38661cd6fa Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 9 Dec 2025 15:38:43 -0800 Subject: [PATCH 091/110] mici ui replay: temp remove swipes (#36818) hmm it IS nondeterm --- selfdrive/ui/tests/diff/replay.py | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/selfdrive/ui/tests/diff/replay.py b/selfdrive/ui/tests/diff/replay.py index cbb2d606e0..9da157660e 100755 --- a/selfdrive/ui/tests/diff/replay.py +++ b/selfdrive/ui/tests/diff/replay.py @@ -33,16 +33,9 @@ class DummyEvent: SCRIPT = [ (0, DummyEvent()), - (FPS * 1, DummyEvent(swipe_right=True)), - (FPS * 2, DummyEvent(swipe_left=True)), - (FPS * 3, DummyEvent(swipe_left=True)), - (FPS * 4, DummyEvent(click=True)), - (FPS * 5, DummyEvent(click=True)), - (FPS * 6, DummyEvent(swipe_left=True)), - (FPS * 7, DummyEvent(swipe_left=True)), - (FPS * 8, DummyEvent(swipe_right=True)), - (FPS * 9, DummyEvent(swipe_down=True)), - (FPS * 10, DummyEvent()), + (FPS * 1, DummyEvent(click=True)), + (FPS * 2, DummyEvent(click=True)), + (FPS * 3, DummyEvent()), ] From 53b7adedc26d1040aa4063648961038fb25c160a Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Wed, 10 Dec 2025 00:29:03 -0800 Subject: [PATCH 092/110] Fix UI timing test (#36823) * why did no one tell me about this?! * not necessary --- selfdrive/test/test_onroad.py | 5 +++-- selfdrive/ui/mici/onroad/cameraview.py | 1 - selfdrive/ui/onroad/cameraview.py | 1 - 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/selfdrive/test/test_onroad.py b/selfdrive/test/test_onroad.py index 27cc17624e..b9506d0806 100644 --- a/selfdrive/test/test_onroad.py +++ b/selfdrive/test/test_onroad.py @@ -206,8 +206,9 @@ class TestOnroad: result += "-------------- UI Draw Timing ------------------\n" result += "------------------------------------------------\n" - # skip first few frames -- connecting to vipc - ts = self.ts['uiDebug']['drawTimeMillis'][15:] + # other processes preempt ui while starting up + offset = int(20 * LOG_OFFSET) + ts = self.ts['uiDebug']['drawTimeMillis'][offset:] result += f"min {min(ts):.2f}ms\n" result += f"max {max(ts):.2f}ms\n" result += f"std {np.std(ts):.2f}ms\n" diff --git a/selfdrive/ui/mici/onroad/cameraview.py b/selfdrive/ui/mici/onroad/cameraview.py index f3e0ef409e..89a4926ce9 100644 --- a/selfdrive/ui/mici/onroad/cameraview.py +++ b/selfdrive/ui/mici/onroad/cameraview.py @@ -107,7 +107,6 @@ else: class CameraView(Widget): def __init__(self, name: str, stream_type: VisionStreamType): super().__init__() - # TODO: implement a receiver and connect thread self._name = name # Primary stream self.client = VisionIpcClient(name, stream_type, conflate=True) diff --git a/selfdrive/ui/onroad/cameraview.py b/selfdrive/ui/onroad/cameraview.py index 881a916df7..5443948465 100644 --- a/selfdrive/ui/onroad/cameraview.py +++ b/selfdrive/ui/onroad/cameraview.py @@ -68,7 +68,6 @@ else: class CameraView(Widget): def __init__(self, name: str, stream_type: VisionStreamType): super().__init__() - # TODO: implement a receiver and connect thread self._name = name # Primary stream self.client = VisionIpcClient(name, stream_type, conflate=True) From ff5b75d16479291a9060405e471102408358e232 Mon Sep 17 00:00:00 2001 From: rj-lynch Date: Thu, 11 Dec 2025 00:35:35 +0000 Subject: [PATCH 093/110] Refactor CarSpecificEvents Class extracting BRAND_EXTRA_GEARS (#36805) * Brand Extra Gears Dict added. Gear data removed from CarSpecificEvents Update method, data now held in global variable. * Added elif for Ford and Nissan events creation. BRAND_EXTRA_GEARS now extracted from CarSpecificEvents * Amended Chrysler and Toyota create_common_events calls. * format * can do this! * consis * whoops * type --------- Co-authored-by: RJ Co-authored-by: Shane Smiskol --- selfdrive/car/car_specific.py | 40 +++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/selfdrive/car/car_specific.py b/selfdrive/car/car_specific.py index 5319d286b0..9e166a44d7 100644 --- a/selfdrive/car/car_specific.py +++ b/selfdrive/car/car_specific.py @@ -26,6 +26,18 @@ class MockCarState: return CS +BRAND_EXTRA_GEARS = { + 'ford': [GearShifter.low, GearShifter.manumatic], + 'nissan': [GearShifter.brake], + 'chrysler': [GearShifter.low], + 'honda': [GearShifter.sport], + 'toyota': [GearShifter.sport], + 'gm': [GearShifter.sport, GearShifter.low, GearShifter.eco, GearShifter.manumatic], + 'volkswagen': [GearShifter.eco, GearShifter.sport, GearShifter.manumatic], + 'hyundai': [GearShifter.sport, GearShifter.manumatic] +} + + class CarSpecificEvents: def __init__(self, CP: structs.CarParams): self.CP = CP @@ -36,17 +48,13 @@ class CarSpecificEvents: self.silent_steer_warning = True def update(self, CS: car.CarState, CS_prev: car.CarState, CC: car.CarControl): + extra_gears = BRAND_EXTRA_GEARS.get(self.CP.brand, None) + if self.CP.brand in ('body', 'mock'): events = Events() - elif self.CP.brand == 'ford': - events = self.create_common_events(CS, CS_prev, extra_gears=[GearShifter.low, GearShifter.manumatic]) - - elif self.CP.brand == 'nissan': - events = self.create_common_events(CS, CS_prev, extra_gears=[GearShifter.brake]) - elif self.CP.brand == 'chrysler': - events = self.create_common_events(CS, CS_prev, extra_gears=[GearShifter.low]) + events = self.create_common_events(CS, CS_prev, extra_gears=extra_gears) # Low speed steer alert hysteresis logic if self.CP.minSteerSpeed > 0. and CS.vEgo < (self.CP.minSteerSpeed + 0.5): @@ -57,7 +65,7 @@ class CarSpecificEvents: events.add(EventName.belowSteerSpeed) elif self.CP.brand == 'honda': - events = self.create_common_events(CS, CS_prev, extra_gears=[GearShifter.sport], pcm_enable=False) + events = self.create_common_events(CS, CS_prev, extra_gears=extra_gears, pcm_enable=False) if self.CP.pcmCruise and CS.vEgo < self.CP.minEnableSpeed: events.add(EventName.belowEngageSpeed) @@ -79,7 +87,7 @@ class CarSpecificEvents: elif self.CP.brand == 'toyota': # TODO: when we check for unexpected disengagement, check gear not S1, S2, S3 - events = self.create_common_events(CS, CS_prev, extra_gears=[GearShifter.sport]) + events = self.create_common_events(CS, CS_prev, extra_gears=extra_gears) if self.CP.openpilotLongitudinalControl: if CS.cruiseState.standstill and not CS.brakePressed: @@ -94,9 +102,7 @@ class CarSpecificEvents: events.add(EventName.manualRestart) elif self.CP.brand == 'gm': - events = self.create_common_events(CS, CS_prev, extra_gears=[GearShifter.sport, GearShifter.low, - GearShifter.eco, GearShifter.manumatic], - pcm_enable=self.CP.pcmCruise) + events = self.create_common_events(CS, CS_prev, extra_gears=extra_gears, pcm_enable=self.CP.pcmCruise) # Enabling at a standstill with brake is allowed # TODO: verify 17 Volt can enable for the first time at a stop and allow for all GMs @@ -107,8 +113,7 @@ class CarSpecificEvents: events.add(EventName.resumeRequired) elif self.CP.brand == 'volkswagen': - events = self.create_common_events(CS, CS_prev, extra_gears=[GearShifter.eco, GearShifter.sport, GearShifter.manumatic], - pcm_enable=self.CP.pcmCruise) + events = self.create_common_events(CS, CS_prev, extra_gears=extra_gears, pcm_enable=self.CP.pcmCruise) if self.CP.openpilotLongitudinalControl: if CS.vEgo < self.CP.minEnableSpeed + 0.5: @@ -121,15 +126,14 @@ class CarSpecificEvents: # events.add(EventName.steerTimeLimit) elif self.CP.brand == 'hyundai': - events = self.create_common_events(CS, CS_prev, extra_gears=(GearShifter.sport, GearShifter.manumatic), - pcm_enable=self.CP.pcmCruise, allow_button_cancel=False) + events = self.create_common_events(CS, CS_prev, extra_gears=extra_gears, pcm_enable=self.CP.pcmCruise, allow_button_cancel=False) else: - events = self.create_common_events(CS, CS_prev) + events = self.create_common_events(CS, CS_prev, extra_gears=extra_gears) return events - def create_common_events(self, CS: structs.CarState, CS_prev: car.CarState, extra_gears=None, pcm_enable=True, + def create_common_events(self, CS: structs.CarState, CS_prev: car.CarState, extra_gears: list | None = None, pcm_enable=True, allow_button_cancel=True): events = Events() From a49273d9d41c948bf7c6e9946b21a62a870cc307 Mon Sep 17 00:00:00 2001 From: Dean Lee Date: Fri, 12 Dec 2025 01:22:30 +0800 Subject: [PATCH 094/110] remove unused #include "common/params.h" from hardware.h (#36827) remove include --- system/hardware/tici/hardware.h | 1 - 1 file changed, 1 deletion(-) diff --git a/system/hardware/tici/hardware.h b/system/hardware/tici/hardware.h index 8a0c066942..d59b45efcb 100644 --- a/system/hardware/tici/hardware.h +++ b/system/hardware/tici/hardware.h @@ -7,7 +7,6 @@ #include #include // for std::clamp -#include "common/params.h" #include "common/util.h" #include "system/hardware/base.h" From d8125f50d27c8877f74887f4456b1a81bef42cb9 Mon Sep 17 00:00:00 2001 From: YassineYousfi Date: Thu, 11 Dec 2025 09:38:53 -0800 Subject: [PATCH 095/110] dm: speedup stat filters convergence (#36756) * dm: speedup stat filters convergence * lint --- selfdrive/monitoring/helpers.py | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/selfdrive/monitoring/helpers.py b/selfdrive/monitoring/helpers.py index 1ed04e705a..3377ce6c68 100644 --- a/selfdrive/monitoring/helpers.py +++ b/selfdrive/monitoring/helpers.py @@ -40,6 +40,9 @@ class DRIVER_MONITOR_SETTINGS: self._PHONE_THRESH2 = 15.0 self._PHONE_MAX_OFFSET = 0.06 self._PHONE_MIN_OFFSET = 0.025 + self._PHONE_DATA_AVG = 0.05 + self._PHONE_DATA_VAR = 3*0.005 + self._PHONE_MAX_COUNT = int(360 / self._DT_DMON) self._POSE_PITCH_THRESHOLD = 0.3133 self._POSE_PITCH_THRESHOLD_SLACK = 0.3237 @@ -50,6 +53,8 @@ class DRIVER_MONITOR_SETTINGS: self._PITCH_NATURAL_OFFSET = 0.011 # initial value before offset is learned self._PITCH_NATURAL_THRESHOLD = 0.449 self._YAW_NATURAL_OFFSET = 0.075 # initial value before offset is learned + self._PITCH_NATURAL_VAR = 3*0.01 + self._YAW_NATURAL_VAR = 3*0.05 self._PITCH_MAX_OFFSET = 0.124 self._PITCH_MIN_OFFSET = -0.0881 self._YAW_MAX_OFFSET = 0.289 @@ -70,6 +75,9 @@ class DRIVER_MONITOR_SETTINGS: self._WHEELPOS_CALIB_MIN_SPEED = 11 self._WHEELPOS_THRESHOLD = 0.5 self._WHEELPOS_FILTER_MIN_COUNT = int(15 / self._DT_DMON) # allow 15 seconds to converge wheel side + self._WHEELPOS_DATA_AVG = 0.03 + self._WHEELPOS_DATA_VAR = 3*5.5e-5 + self._WHEELPOS_MAX_COUNT = -1 self._RECOVERY_FACTOR_MAX = 5. # relative to minus step change self._RECOVERY_FACTOR_MIN = 1.25 # relative to minus step change @@ -78,30 +86,33 @@ class DRIVER_MONITOR_SETTINGS: self._MAX_TERMINAL_DURATION = int(30 / self._DT_DMON) # not allowed to engage after 30s of terminal alerts class DistractedType: + NOT_DISTRACTED = 0 DISTRACTED_POSE = 1 << 0 DISTRACTED_BLINK = 1 << 1 DISTRACTED_PHONE = 1 << 2 class DriverPose: - def __init__(self, max_trackable): + def __init__(self, settings): + pitch_filter_raw_priors = (settings._PITCH_NATURAL_OFFSET, settings._PITCH_NATURAL_VAR, 2) + yaw_filter_raw_priors = (settings._YAW_NATURAL_OFFSET, settings._YAW_NATURAL_VAR, 2) self.yaw = 0. self.pitch = 0. self.roll = 0. self.yaw_std = 0. self.pitch_std = 0. self.roll_std = 0. - self.pitch_offseter = RunningStatFilter(max_trackable=max_trackable) - self.yaw_offseter = RunningStatFilter(max_trackable=max_trackable) + self.pitch_offseter = RunningStatFilter(raw_priors=pitch_filter_raw_priors, max_trackable=settings._POSE_OFFSET_MAX_COUNT) + self.yaw_offseter = RunningStatFilter(raw_priors=yaw_filter_raw_priors, max_trackable=settings._POSE_OFFSET_MAX_COUNT) self.calibrated = False self.low_std = True self.cfactor_pitch = 1. self.cfactor_yaw = 1. class DriverProb: - def __init__(self, max_trackable): + def __init__(self, raw_priors, max_trackable): self.prob = 0. - self.prob_offseter = RunningStatFilter(max_trackable=max_trackable) + self.prob_offseter = RunningStatFilter(raw_priors=raw_priors, max_trackable=max_trackable) self.prob_calibrated = False class DriverBlink: @@ -140,9 +151,11 @@ class DriverMonitoring: self.settings = settings if settings is not None else DRIVER_MONITOR_SETTINGS(device_type=HARDWARE.get_device_type()) # init driver status - self.wheelpos = DriverProb(-1) - self.pose = DriverPose(self.settings._POSE_OFFSET_MAX_COUNT) - self.phone = DriverProb(self.settings._POSE_OFFSET_MAX_COUNT) + wheelpos_filter_raw_priors = (self.settings._WHEELPOS_DATA_AVG, self.settings._WHEELPOS_DATA_VAR, 2) + phone_filter_raw_priors = (self.settings._PHONE_DATA_AVG, self.settings._PHONE_DATA_VAR, 2) + self.wheelpos = DriverProb(raw_priors=wheelpos_filter_raw_priors, max_trackable=self.settings._WHEELPOS_MAX_COUNT) + self.phone = DriverProb(raw_priors=phone_filter_raw_priors, max_trackable=self.settings._PHONE_MAX_COUNT) + self.pose = DriverPose(settings=self.settings) self.blink = DriverBlink() self.always_on = always_on From 1391434f54d446857f009d51f1919c2e7b58b652 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Thu, 11 Dec 2025 11:22:08 -0800 Subject: [PATCH 096/110] setup: fix uv install fail (#36839) * pipefail * curl retry --- tools/install_python_dependencies.sh | 4 ++-- tools/setup.sh | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/tools/install_python_dependencies.sh b/tools/install_python_dependencies.sh index cdbaca32cf..c2db249cf2 100755 --- a/tools/install_python_dependencies.sh +++ b/tools/install_python_dependencies.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -set -e +set -euo pipefail # Increase the pip timeout to handle TimeoutError export PIP_DEFAULT_TIMEOUT=200 @@ -10,7 +10,7 @@ cd "$ROOT" if ! command -v "uv" > /dev/null 2>&1; then echo "installing uv..." - curl -LsSf https://astral.sh/uv/install.sh | sh + curl -LsSf --retry 5 --retry-delay 5 --retry-all-errors https://astral.sh/uv/install.sh | sh UV_BIN="$HOME/.local/bin" PATH="$UV_BIN:$PATH" fi diff --git a/tools/setup.sh b/tools/setup.sh index e0a9a4f6a6..fd7efcee90 100755 --- a/tools/setup.sh +++ b/tools/setup.sh @@ -1,6 +1,5 @@ #!/usr/bin/env bash - -set -e +set -euo pipefail RED='\033[0;31m' GREEN='\033[0;32m' From c61ed10015d10241f33df1e0ecb0ff1e8ac661de Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Thu, 11 Dec 2025 13:04:59 -0800 Subject: [PATCH 097/110] USB GPU benchmarking (#36840) * test boot time * lil nicer * cleanup * revert that --------- Co-authored-by: Comma Device --- scripts/usbgpu/benchmark.sh | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100755 scripts/usbgpu/benchmark.sh diff --git a/scripts/usbgpu/benchmark.sh b/scripts/usbgpu/benchmark.sh new file mode 100755 index 0000000000..04a76d054e --- /dev/null +++ b/scripts/usbgpu/benchmark.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +set -e + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" +cd $DIR/../../tinygrad_repo + +GREEN='\033[0;32m' +NC='\033[0m' + + +#export DEBUG=2 +export PYTHONPATH=. +export AM_RESET=1 +export AMD=1 +export AMD_IFACE=USB +export AMD_LLVM=1 + +python3 -m unittest -q --buffer test.test_tiny.TestTiny.test_plus \ + > /tmp/test_tiny.log 2>&1 || (cat /tmp/test_tiny.log; exit 1) +printf "${GREEN}Booted in ${SECONDS}s${NC}\n" +printf "${GREEN}=============${NC}\n" + +printf "\n\n" +printf "${GREEN}Transfer speeds:${NC}\n" +printf "${GREEN}================${NC}\n" +python3 test/external/external_test_usb_asm24.py TestDevCopySpeeds From edede31c320163cdf771352918e66f85b4b663eb Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 11 Dec 2025 18:00:43 -0800 Subject: [PATCH 098/110] athenad: get ES256 key (#36845) * fix * why not format * fix typing * cast --- common/api.py | 10 ++++++---- system/athena/athenad.py | 9 +++------ system/athena/registration.py | 4 +++- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/common/api.py b/common/api.py index 656c05ed59..7ea278038d 100644 --- a/common/api.py +++ b/common/api.py @@ -7,9 +7,10 @@ from openpilot.system.version import get_version API_HOST = os.getenv('API_HOST', 'https://api.commadotai.com') - # name : jwt signature algorithm -KEYS = {"id_rsa" : "RS256", - "id_ecdsa" : "ES256"} +# name: jwt signature algorithm +KEYS = {"id_rsa": "RS256", + "id_ecdsa": "ES256"} + class Api: def __init__(self, dongle_id): @@ -50,7 +51,8 @@ def api_get(endpoint, method='GET', timeout=None, access_token=None, **params): return requests.request(method, API_HOST + "/" + endpoint, timeout=timeout, headers=headers, params=params) -def get_key_pair(): + +def get_key_pair() -> tuple[str, str, str] | tuple[None, None, None]: for key in KEYS: if os.path.isfile(Paths.persist_root() + f'/comma/{key}') and os.path.isfile(Paths.persist_root() + f'/comma/{key}.pub'): with open(Paths.persist_root() + f'/comma/{key}') as private, open(Paths.persist_root() + f'/comma/{key}.pub') as public: diff --git a/system/athena/athenad.py b/system/athena/athenad.py index 50c4f5408f..3b71a9c31f 100755 --- a/system/athena/athenad.py +++ b/system/athena/athenad.py @@ -30,7 +30,7 @@ from websocket import (ABNF, WebSocket, WebSocketException, WebSocketTimeoutExce import cereal.messaging as messaging from cereal import log from cereal.services import SERVICE_LIST -from openpilot.common.api import Api +from openpilot.common.api import Api, get_key_pair from openpilot.common.utils import CallbackReader, get_upload_stream from openpilot.common.params import Params from openpilot.common.realtime import set_core_affinity @@ -523,11 +523,8 @@ def startLocalProxy(global_end_event: threading.Event, remote_ws_uri: str, local @dispatcher.add_method def getPublicKey() -> str | None: - if not os.path.isfile(Paths.persist_root() + '/comma/id_rsa.pub'): - return None - - with open(Paths.persist_root() + '/comma/id_rsa.pub') as f: - return f.read() + _, _, public_key = get_key_pair() + return public_key @dispatcher.add_method diff --git a/system/athena/registration.py b/system/athena/registration.py index 81aee1ebf9..405b2423f2 100755 --- a/system/athena/registration.py +++ b/system/athena/registration.py @@ -2,6 +2,7 @@ import time import json import jwt +from typing import cast from pathlib import Path from datetime import datetime, timedelta, UTC @@ -69,7 +70,8 @@ def register(show_spinner=False) -> str | None: start_time = time.monotonic() while True: try: - register_token = jwt.encode({'register': True, 'exp': datetime.now(UTC).replace(tzinfo=None) + timedelta(hours=1)}, private_key, algorithm=jwt_algo) + register_token = jwt.encode({'register': True, 'exp': datetime.now(UTC).replace(tzinfo=None) + timedelta(hours=1)}, + cast(str, private_key), algorithm=jwt_algo) cloudlog.info("getting pilotauth") resp = api_get("v2/pilotauth/", method='POST', timeout=15, imei=imei1, imei2=imei2, serial=serial, public_key=public_key, register_token=register_token) From 13693e3a0ae1e25b1d90c959bdb138848ac9f700 Mon Sep 17 00:00:00 2001 From: Matt Purnell <65473602+mpurnell1@users.noreply.github.com> Date: Thu, 11 Dec 2025 20:19:59 -0600 Subject: [PATCH 099/110] loggerd: Fix test that fails on non-TICI devices (#36846) Only check for TICI files on TICI --- system/loggerd/tests/test_loggerd.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/system/loggerd/tests/test_loggerd.py b/system/loggerd/tests/test_loggerd.py index c6a4b12e63..1cac16adcd 100644 --- a/system/loggerd/tests/test_loggerd.py +++ b/system/loggerd/tests/test_loggerd.py @@ -16,6 +16,7 @@ from openpilot.common.basedir import BASEDIR from openpilot.common.params import Params from openpilot.common.timeout import Timeout from openpilot.system.hardware.hw import Paths +from openpilot.system.hardware import TICI from openpilot.system.loggerd.xattr_cache import getxattr from openpilot.system.loggerd.deleter import PRESERVE_ATTR_NAME, PRESERVE_ATTR_VALUE from openpilot.system.manager.process_config import managed_processes @@ -221,13 +222,16 @@ class TestLoggerd: assert abs(boot.wallTimeNanos - time.time_ns()) < 5*1e9 # within 5s assert boot.launchLog == launch_log - for fn in ["console-ramoops", "pmsg-ramoops-0"]: - path = Path(os.path.join("/sys/fs/pstore/", fn)) - if path.is_file(): - with open(path, "rb") as f: - expected_val = f.read() - bootlog_val = [e.value for e in boot.pstore.entries if e.key == fn][0] - assert expected_val == bootlog_val + if TICI: + for fn in ["console-ramoops", "pmsg-ramoops-0"]: + path = Path(os.path.join("/sys/fs/pstore/", fn)) + if path.is_file(): + with open(path, "rb") as f: + expected_val = f.read() + bootlog_val = [e.value for e in boot.pstore.entries if e.key == fn][0] + assert expected_val == bootlog_val + else: + assert len(boot.pstore.entries) == 0 # next one should increment by one bl1 = re.match(RE.LOG_ID_V2, bootlog_path.name) From 2d91aa5abc44db65202f752e93d134683346241d Mon Sep 17 00:00:00 2001 From: Suyog Shinde <64534620+SuyogShinde942@users.noreply.github.com> Date: Thu, 11 Dec 2025 21:24:32 -0600 Subject: [PATCH 100/110] locationd: fix velocity calibration using wrong pose field (#36844) --- selfdrive/locationd/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selfdrive/locationd/helpers.py b/selfdrive/locationd/helpers.py index bf4588a40c..2a3ac8b861 100644 --- a/selfdrive/locationd/helpers.py +++ b/selfdrive/locationd/helpers.py @@ -172,7 +172,7 @@ class PoseCalibrator: ned_from_calib_euler = self._ned_from_calib(pose.orientation) angular_velocity_calib = self._transform_calib_from_device(pose.angular_velocity) acceleration_calib = self._transform_calib_from_device(pose.acceleration) - velocity_calib = self._transform_calib_from_device(pose.angular_velocity) + velocity_calib = self._transform_calib_from_device(pose.velocity) return Pose(ned_from_calib_euler, velocity_calib, acceleration_calib, angular_velocity_calib) From 0871a35c10d6fdb465b1ebba205628c2cb2a0ed7 Mon Sep 17 00:00:00 2001 From: Bruce Wayne Date: Thu, 11 Dec 2025 19:43:53 -0800 Subject: [PATCH 101/110] Revert "Dark Souls Model (#36764)" This reverts commit 83dad85cdd800d79d77ff964da72103218c12ef9. --- selfdrive/modeld/models/driving_policy.onnx | 2 +- selfdrive/modeld/models/driving_vision.onnx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/selfdrive/modeld/models/driving_policy.onnx b/selfdrive/modeld/models/driving_policy.onnx index ec451ba736..1e764af9ba 100644 --- a/selfdrive/modeld/models/driving_policy.onnx +++ b/selfdrive/modeld/models/driving_policy.onnx @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e2929b07deb9fb1e492c7fa2832c51ac9e472bfe0b80730fdbbe263735866580 +oid sha256:c5a1f0655ddf266ed42ad1980389d96f47cc5e756da1fa3ca1477a920bb9b157 size 13926324 diff --git a/selfdrive/modeld/models/driving_vision.onnx b/selfdrive/modeld/models/driving_vision.onnx index e5ef27810a..441c4a16af 100644 --- a/selfdrive/modeld/models/driving_vision.onnx +++ b/selfdrive/modeld/models/driving_vision.onnx @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2194eaee8a8c40f79a6f783d198991b1bf70a54b5885053e63789eab040a5228 +oid sha256:8f16d548ea4eb5d01518a9e90d4527cd97c31a84bcaf6f695dead8f0015fecc4 size 46271942 From b2e7dffa590f10901a98152ba5789a412bdb88a3 Mon Sep 17 00:00:00 2001 From: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com> Date: Thu, 11 Dec 2025 20:24:45 -0800 Subject: [PATCH 102/110] modeld_v2: support planplus outputs (#1532) * conditional concatenation * v13 * modifucatuins --- sunnypilot/modeld_v2/fill_model_msg.py | 5 ++--- sunnypilot/modeld_v2/modeld.py | 5 ++++- sunnypilot/modeld_v2/parse_model_outputs_split.py | 2 ++ sunnypilot/models/helpers.py | 2 +- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/sunnypilot/modeld_v2/fill_model_msg.py b/sunnypilot/modeld_v2/fill_model_msg.py index ee0eb48684..57e968d02f 100644 --- a/sunnypilot/modeld_v2/fill_model_msg.py +++ b/sunnypilot/modeld_v2/fill_model_msg.py @@ -11,13 +11,12 @@ SEND_RAW_PRED = os.getenv('SEND_RAW_PRED') ConfidenceClass = log.ModelDataV2.ConfidenceClass -def get_curvature_from_output(output, vego, lat_action_t, mlsim): +def get_curvature_from_output(output, plan, vego, lat_action_t, mlsim): if not mlsim: if desired_curv := output.get('desired_curvature'): # If the model outputs the desired curvature, use that directly return float(desired_curv[0, 0]) - plan_output = output['plan'][0] - return float(get_curvature_from_plan(plan_output[:, Plan.T_FROM_CURRENT_EULER][:, 2], plan_output[:, Plan.ORIENTATION_RATE][:, 2], + return float(get_curvature_from_plan(plan[:, Plan.T_FROM_CURRENT_EULER][:, 2], plan[:, Plan.ORIENTATION_RATE][:, 2], ModelConstants.T_IDXS, vego, lat_action_t)) diff --git a/sunnypilot/modeld_v2/modeld.py b/sunnypilot/modeld_v2/modeld.py index 0fd45940d8..82eb099e7e 100755 --- a/sunnypilot/modeld_v2/modeld.py +++ b/sunnypilot/modeld_v2/modeld.py @@ -28,6 +28,7 @@ from openpilot.sunnypilot.models.helpers import get_active_bundle from openpilot.sunnypilot.models.runners.helpers import get_model_runner PROCESS_NAME = "selfdrive.modeld.modeld_tinygrad" +RECOVERY_POWER = 1.0 # The higher this number the more aggressively the model will recover to lanecenter, too high and it will ping-pong class FrameMeta: @@ -156,11 +157,13 @@ class ModelState(ModelStateBase): def get_action_from_model(self, model_output: dict[str, np.ndarray], prev_action: log.ModelDataV2.Action, lat_action_t: float, long_action_t: float, v_ego: float) -> log.ModelDataV2.Action: plan = model_output['plan'][0] + if 'planplus' in model_output: + plan = plan + RECOVERY_POWER*model_output['planplus'][0] desired_accel, should_stop = get_accel_from_plan(plan[:, Plan.VELOCITY][:, 0], plan[:, Plan.ACCELERATION][:, 0], self.constants.T_IDXS, action_t=long_action_t) desired_accel = smooth_value(desired_accel, prev_action.desiredAcceleration, self.LONG_SMOOTH_SECONDS) - desired_curvature = get_curvature_from_output(model_output, v_ego, lat_action_t, self.mlsim) + desired_curvature = get_curvature_from_output(model_output, plan, v_ego, lat_action_t, self.mlsim) if self.generation is not None and self.generation >= 10: # smooth curvature for post FOF models if v_ego > self.MIN_LAT_CONTROL_SPEED: desired_curvature = smooth_value(desired_curvature, prev_action.desiredCurvature, self.LAT_SMOOTH_SECONDS) diff --git a/sunnypilot/modeld_v2/parse_model_outputs_split.py b/sunnypilot/modeld_v2/parse_model_outputs_split.py index 9cf321a1b6..a099facd15 100644 --- a/sunnypilot/modeld_v2/parse_model_outputs_split.py +++ b/sunnypilot/modeld_v2/parse_model_outputs_split.py @@ -108,6 +108,8 @@ class Parser: plan_in_N, plan_out_N = (SplitModelConstants.PLAN_MHP_N, SplitModelConstants.PLAN_MHP_SELECTION) if plan_mhp else (0, 0) self.parse_mdn('plan', outs, in_N=plan_in_N, out_N=plan_out_N, out_shape=(SplitModelConstants.IDX_N, SplitModelConstants.PLAN_WIDTH)) + if 'planplus' in outs: + self.parse_mdn('planplus', outs, in_N=plan_in_N, out_N=plan_out_N, out_shape=(SplitModelConstants.IDX_N, SplitModelConstants.PLAN_WIDTH)) def split_outputs(self, outs: dict[str, np.ndarray]) -> None: if 'desired_curvature' in outs: diff --git a/sunnypilot/models/helpers.py b/sunnypilot/models/helpers.py index a581185371..ce6625a1f0 100644 --- a/sunnypilot/models/helpers.py +++ b/sunnypilot/models/helpers.py @@ -19,7 +19,7 @@ from openpilot.system.hardware.hw import Paths from pathlib import Path # see the README.md for more details on the model selector versioning -CURRENT_SELECTOR_VERSION = 12 +CURRENT_SELECTOR_VERSION = 13 REQUIRED_MIN_SELECTOR_VERSION = 12 USE_ONNX = os.getenv('USE_ONNX', PC) From 9947206ccdb71f698119f6782300af377f4e735e Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 11 Dec 2025 21:52:24 -0800 Subject: [PATCH 103/110] comma four: fix wrapping steer right (#36848) rm extra space --- selfdrive/ui/mici/onroad/alert_renderer.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/selfdrive/ui/mici/onroad/alert_renderer.py b/selfdrive/ui/mici/onroad/alert_renderer.py index bdf85acc38..7ee83ff880 100644 --- a/selfdrive/ui/mici/onroad/alert_renderer.py +++ b/selfdrive/ui/mici/onroad/alert_renderer.py @@ -200,11 +200,11 @@ class AlertRenderer(Widget): text_x = self._rect.x + ALERT_MARGIN text_width = self._rect.width - ALERT_MARGIN if icon_side == 'left': - text_x = self._rect.x + self._txt_turn_signal_right.width + 20 * 2 - text_width = self._rect.width - ALERT_MARGIN - self._txt_turn_signal_right.width - 20 * 2 + text_x = self._rect.x + self._txt_turn_signal_right.width + text_width = self._rect.width - ALERT_MARGIN - self._txt_turn_signal_right.width elif icon_side == 'right': text_x = self._rect.x + ALERT_MARGIN - text_width = self._rect.width - ALERT_MARGIN - self._txt_turn_signal_right.width - 20 * 2 + text_width = self._rect.width - ALERT_MARGIN - self._txt_turn_signal_right.width text_rect = rl.Rectangle( text_x, From 928672999bd1a1f5d28a0110b363ac6a5f9c86b5 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Fri, 12 Dec 2025 02:31:06 -0500 Subject: [PATCH 104/110] ui: consolidate `NoElideButtonAction` (#1569) --- selfdrive/ui/sunnypilot/layouts/settings/models.py | 12 ++++-------- system/ui/sunnypilot/lib/utils.py | 12 ++++++++++++ 2 files changed, 16 insertions(+), 8 deletions(-) create mode 100644 system/ui/sunnypilot/lib/utils.py diff --git a/selfdrive/ui/sunnypilot/layouts/settings/models.py b/selfdrive/ui/sunnypilot/layouts/settings/models.py index 43f0d25b47..16d406ea98 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/models.py +++ b/selfdrive/ui/sunnypilot/layouts/settings/models.py @@ -21,7 +21,8 @@ from openpilot.system.ui.widgets.toggle import ON_COLOR from openpilot.sunnypilot.models.runners.constants import CUSTOM_MODEL_PATH from openpilot.system.ui.sunnypilot.lib.styles import style -from openpilot.system.ui.sunnypilot.widgets.list_view import ButtonActionSP, ListItemSP, toggle_item_sp, option_item_sp +from openpilot.system.ui.sunnypilot.lib.utils import NoElideButtonAction +from openpilot.system.ui.sunnypilot.widgets.list_view import ListItemSP, toggle_item_sp, option_item_sp from openpilot.system.ui.sunnypilot.widgets.progress_bar import progress_item from openpilot.system.ui.sunnypilot.widgets.tree_dialog import TreeOptionDialog, TreeNode, TreeFolder @@ -29,11 +30,6 @@ if gui_app.sunnypilot_ui(): from openpilot.system.ui.sunnypilot.widgets.list_view import button_item_sp as button_item -class ModelAction(ButtonActionSP): - def get_width_hint(self): - return super().get_width_hint() + 1 - - class ModelsLayout(Widget): def __init__(self): super().__init__() @@ -55,7 +51,7 @@ class ModelsLayout(Widget): self.current_model_item = ListItemSP( title=tr("Current Model"), description="", - action_item=ModelAction(tr("SELECT")), + action_item=NoElideButtonAction(tr("SELECT")), callback=self._handle_current_model_clicked ) @@ -70,7 +66,7 @@ class ModelsLayout(Widget): self.clear_cache_item = ListItemSP( title=tr("Clear Model Cache"), description="", - action_item=ModelAction(tr("CLEAR")), + action_item=NoElideButtonAction(tr("CLEAR")), callback=self._clear_cache ) diff --git a/system/ui/sunnypilot/lib/utils.py b/system/ui/sunnypilot/lib/utils.py new file mode 100644 index 0000000000..09230da14b --- /dev/null +++ b/system/ui/sunnypilot/lib/utils.py @@ -0,0 +1,12 @@ +""" +Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + +This file is part of sunnypilot and is licensed under the MIT License. +See the LICENSE.md file in the root directory for more details. +""" +from openpilot.system.ui.sunnypilot.widgets.list_view import ButtonActionSP + + +class NoElideButtonAction(ButtonActionSP): + def get_width_hint(self): + return super().get_width_hint() + 1 From 436ff5aa427f3d1e289188f1cf00271a44d2329c Mon Sep 17 00:00:00 2001 From: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com> Date: Thu, 11 Dec 2025 23:40:33 -0800 Subject: [PATCH 105/110] ui: OSM panel (#1515) * param to control stock vs sp ui * init styles * SP Toggles * Lint * optimizations * sp raylib preview * fix callback * fix ui preview * dialog txt * compare vs what used to be done before InputDialog * merge origin raylib toggles * tree dialog * less trees for the planet * the heck * save the trees we got icons * Update process.py * Remove 'sunnypilot_ui' Removed 'sunnypilot_ui' parameter from params_keys.h * Update raylib_screenshots.py Removed the parameter setting for 'sunnypilot_ui' in the test. * ui: fuzzy search helper * better tree. fully dynamic and stuff * rm * more indent * Squashed commit of the following: commit 6b5b686fa5f1b6c8ba05cb98606bcb5e22fdcfd9 Author: discountchubbs Date: Mon Nov 24 17:16:17 2025 -0800 more indent commit 76bc538ac74898e026ee67502ddcb2a2a78eb429 Merge: 53eb821dc4 c53e2134e2 Author: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com> Date: Mon Nov 24 17:15:48 2025 -0800 Merge branch 'master' into rl-tree-dialog commit 53eb821dc41a09f0bbe5ddbc6957ddfb6023c979 Merge: 82e1ebe97e 844f4cbc74 Author: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com> Date: Mon Nov 24 11:54:55 2025 -0800 Merge branch 'master' into rl-tree-dialog commit 82e1ebe97ecae94f14adadcf77ac7a68d55cc0fe Author: discountchubbs Date: Mon Nov 24 10:23:35 2025 -0800 rm commit da3ff45bb6efe5f0bbd06eb6e4f61a7a3dd844b0 Merge: 41da513fca a829a1b972 Author: discountchubbs Date: Mon Nov 24 10:19:08 2025 -0800 Merge remote-tracking branch 'origin/rl-tree-dialog' into rl-tree-dialog commit 41da513fcaf5a5ff302427734813b005b6385f0b Author: discountchubbs Date: Mon Nov 24 10:18:43 2025 -0800 better tree. fully dynamic and stuff commit b2950149fbafda7d58bb5ed2b316f59ccc8a47eb Merge: 4fb8e4beed 924e5a3211 Author: discountchubbs Date: Mon Nov 24 10:17:51 2025 -0800 Merge remote-tracking branch 'origin/input-dialog' into rl-tree-dialog commit a829a1b97269de24dc707188808ad5b623968dfa Merge: 848290d07e 9edc36ca66 Author: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com> Date: Mon Nov 24 10:16:28 2025 -0800 Merge branch 'master' into rl-tree-dialog commit 4fb8e4beede0e6771c25aaac7c8b8f90b2f21c03 Merge: 848290d07e af4f0f8372 Author: discountchubbs Date: Mon Nov 24 10:16:20 2025 -0800 Merge remote-tracking branch 'origin/fuzzy-dialog' into rl-tree-dialog commit af4f0f8372a63fdaeb94fe3b64703fbcca057b9c Merge: 1d5f0ab282 3cd55260d9 Author: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com> Date: Mon Nov 24 06:39:29 2025 -0800 Merge branch 'master' into fuzzy-dialog commit 1d5f0ab282a01419d03e856b4a5a205f3bfb02aa Author: discountchubbs Date: Sun Nov 23 11:28:59 2025 -0800 ui: fuzzy search helper commit 848290d07eaab2ed5ba948bc8dd63338dc070ac1 Author: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com> Date: Fri Nov 21 21:08:07 2025 -0800 Update raylib_screenshots.py Removed the parameter setting for 'sunnypilot_ui' in the test. commit 6694928a467641d500bf7b81ad82308ff1652eb7 Author: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com> Date: Fri Nov 21 21:06:57 2025 -0800 Remove 'sunnypilot_ui' Removed 'sunnypilot_ui' parameter from params_keys.h commit b3c90ef7b2bdd0328442cdbeff7b13bcb260744e Merge: 0d3bc959c8 457b6634fd Author: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com> Date: Fri Nov 21 21:06:04 2025 -0800 Merge branch 'master' into rl-tree-dialog commit 924e5a3211b3db71d785cd578ee2bdc31de5f495 Merge: a4ee4ba76d d92d2cb683 Author: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com> Date: Fri Nov 21 19:33:18 2025 -0800 Merge branch 'master' into input-dialog commit a4ee4ba76d238fbb8da5261056da649e1dfbaab9 Merge: e911de5968 4f13a0f775 Author: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com> Date: Fri Nov 21 16:24:31 2025 -0800 Merge branch 'master' into input-dialog commit e911de59684c44e02784ca98937cbdeb76401e05 Merge: cea6e00819 0ba5cbea91 Author: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com> Date: Fri Nov 21 13:50:33 2025 -0800 Merge branch 'master' into input-dialog commit cea6e00819af66f3b46b9d72282a4e550c325798 Merge: d7b8ce86ed 8184cd8a6a Author: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com> Date: Fri Nov 21 12:01:45 2025 -0800 Merge branch 'master' into input-dialog commit 0d3bc959c8547d97b14503cec73a67f5760ffb99 Author: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com> Date: Wed Nov 19 20:29:30 2025 -0800 Update process.py commit 4f3c19ffb54db603528280e9aa941f719c764185 Author: James Vecellio Date: Wed Nov 19 20:28:59 2025 -0800 save the trees we got icons commit ae5c44355db6a625f9e257db2f62b2a5691a827f Author: discountchubbs Date: Wed Nov 19 13:38:04 2025 -0800 the heck commit 066438ad107ce9a3e9b4b2a1c0de062c523bc747 Merge: 9532675814 e74460f3a8 Author: discountchubbs Date: Wed Nov 19 12:18:17 2025 -0800 Merge remote-tracking branch 'origin/rl-tree-dialog' into rl-tree-dialog commit 95326758146bfe690b826d319c40f3bc7140392c Author: discountchubbs Date: Wed Nov 19 12:17:52 2025 -0800 less trees for the planet commit e74460f3a82d1338edb667bdf3423560270cbeab Merge: c347db376a 423a7d2ed0 Author: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com> Date: Wed Nov 19 09:37:44 2025 -0800 Merge branch 'rl-sp-toggles' into rl-tree-dialog commit c347db376a08d3f3ee6d5cd66176bc12ec842582 Author: discountchubbs Date: Wed Nov 19 09:36:33 2025 -0800 tree dialog commit c9bd67b26121fce0637aeec924b777ef73a66575 Author: discountchubbs Date: Wed Nov 19 09:34:08 2025 -0800 merge origin raylib toggles commit d7b8ce86edaccb89452db51f4f69cf67643a0824 Author: discountchubbs Date: Mon Nov 17 20:21:33 2025 -0800 compare vs what used to be done before InputDialog commit 2d3d104658d660e8f9f7c12e312544c419a5d406 Merge: ded02895f4 f1025f6ee9 Author: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com> Date: Mon Nov 17 19:24:20 2025 -0800 Merge branch 'master' into input-dialog commit ded02895f44a241afd3c9857760a18ec5e1406c4 Author: discountchubbs Date: Mon Nov 17 19:22:01 2025 -0800 dialog txt commit 9778a925b09438c7eb9618dfb0fc38eb13d53a39 Merge: cb03d08397 08e85808c5 Author: Jason Wen Date: Sun Nov 16 03:16:58 2025 -0500 input dialog commit 423a7d2ed0249e12743c2b36e1053d5a446f8d33 Author: nayan Date: Sun Nov 16 11:15:28 2025 -0500 fix ui preview commit e4e10d4b87d0bdf4fe2e766122935e5ee3706a5b Author: nayan Date: Sun Nov 16 11:15:22 2025 -0500 fix callback commit 362e9ce04becc1ef1d16d2f23daa01cb2d5303c4 Author: nayan Date: Sun Nov 16 09:53:28 2025 -0500 sp raylib preview commit 3946e643f68fdb5784a2b42839162903a5f4a592 Author: nayan Date: Sat Nov 15 20:24:20 2025 -0500 optimizations commit 0c37a385967c201d7db6e23ca3506bc21a22d698 Author: nayan Date: Sat Nov 15 09:42:12 2025 -0500 Lint commit 9c5acf61c047c81fd17bd02e9354225aabf5f7e8 Author: nayan Date: Sat Nov 15 09:29:07 2025 -0500 SP Toggles commit 121b304fe0dcd8bfad02a05aaba6d3e0c9d3029e Author: nayan Date: Sat Nov 15 09:28:58 2025 -0500 init styles commit 47d848293b54a6adf83c964120ed74dfbdf58359 Author: nayan Date: Sat Nov 15 09:28:43 2025 -0500 param to control stock vs sp ui * Squashed commit of the following: commit 70ad001add04810c6d30fd27e44299245b2b5c86 Merge: 142663c490 844f4cbc74 Author: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com> Date: Mon Nov 24 11:54:58 2025 -0800 Merge branch 'master' into rl-progress-bar commit 142663c490b8ec88770fc0d8bc16cfcfff3f7a62 Author: discountchubbs Date: Sat Nov 22 20:12:44 2025 -0800 smoother updating commit 4476e418dd5cbd84e7ef1c65bc85c21a7806c6c7 Author: discountchubbs Date: Sat Nov 22 09:55:17 2025 -0800 easier to see commit ad66c22e88569c86f64095fab56a3c79310b63a3 Merge: 0c46ef5948 457b6634fd Author: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com> Date: Sat Nov 22 07:45:05 2025 -0800 Merge branch 'master' into rl-progress-bar commit 0c46ef59482f678ec944ce772ac4202d257d29fd Author: discountchubbs Date: Sat Nov 22 07:42:59 2025 -0800 freaking test dir commit 11c19aad246b1e0bba02f49347517cf9af41a9d8 Merge: 7785238d54 8184cd8a6a Author: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com> Date: Fri Nov 21 12:01:36 2025 -0800 Merge branch 'master' into rl-progress-bar commit 7785238d54e60f537418aea319e449ebccccbeac Author: discountchubbs Date: Wed Nov 19 07:47:10 2025 -0800 raylib: progress bar * OSM panel * fetching * only show if fav_param is used in the call * flattened and custom search query * conditional for mypy * sunny's new x,y makes this even easier! * download all * add back the rough estimate * not sure i like the 'Download' * simplify the path * actual size as of today * format * more simple * only show on download or delete * loathing loathing, unadulterated loathing, i loathe it all * loathing loathing, unadulterated loathing, i loathe it all * # Conflicts: # system/ui/sunnypilot/lib/styles.py # system/ui/sunnypilot/widgets/tree_dialog.py * search * st * Update osm.py * one second updates: its heavy process, which isnt really noticeable during downloads ayways. the once a second ensures responsiveness on the ui, while also maintaining 20fps on device for country/state downloads. * efficient? i hope * big boi texts * big boi texts * use our own classes * need to clear all params when delete all * more * collateral lol * do not behave as selected if canceled during US->States dialog * more * instead of timestamp, let's just show formatted time * disable button when downloading dbs * should be the buttons being disabled * well gotta re-enable them too * empty country * might be bigger now * fixes for mapd manager * should stay as a json * sanitize it a bit * revert * only nuke if the cancel button is called * always try to update the labels * Revert "always try to update the labels" This reverts commit ba0988fc06734234598f8235fdfe10270e1a3025. * re-enable button after download is complete * disable all while downloading (till we could cancel and re-download) * fix progress bar not filling up as intended for smaller total counts * revert * use new --------- Co-authored-by: nayan Co-authored-by: Jason Wen --- .../ui/sunnypilot/layouts/settings/osm.py | 222 +++++++++++++++++- sunnypilot/mapd/mapd_manager.py | 9 +- 2 files changed, 213 insertions(+), 18 deletions(-) diff --git a/selfdrive/ui/sunnypilot/layouts/settings/osm.py b/selfdrive/ui/sunnypilot/layouts/settings/osm.py index d57a0de9d8..3726a820f5 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/osm.py +++ b/selfdrive/ui/sunnypilot/layouts/settings/osm.py @@ -4,27 +4,229 @@ Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. This file is part of sunnypilot and is licensed under the MIT License. See the LICENSE.md file in the root directory for more details. """ +import datetime +import os +import platform +import requests +import shutil +import threading +from pathlib import Path +from time import monotonic + from openpilot.common.params import Params +from openpilot.selfdrive.ui.ui_state import device, ui_state +from openpilot.selfdrive.ui.layouts.settings.software import time_ago +from openpilot.system.hardware.hw import Paths +from openpilot.system.ui.lib.application import gui_app +from openpilot.system.ui.lib.multilang import tr +from openpilot.system.ui.widgets import DialogResult, Widget +from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog +from openpilot.system.ui.widgets.list_view import text_item from openpilot.system.ui.widgets.scroller_tici import Scroller -from openpilot.system.ui.widgets import Widget + +from openpilot.system.ui.sunnypilot.lib.utils import NoElideButtonAction +from openpilot.system.ui.sunnypilot.widgets.list_view import ListItemSP +from openpilot.system.ui.sunnypilot.widgets.tree_dialog import TreeFolder, TreeNode, TreeOptionDialog +from openpilot.system.ui.sunnypilot.widgets.progress_bar import progress_item + +MAP_PATH = Path(Paths.mapd_root()) / "offline" class OSMLayout(Widget): def __init__(self): super().__init__() - - self._params = Params() - items = self._initialize_items() - self._scroller = Scroller(items, line_separator=True, spacing=0) + self._current_percent = 0 + self._last_map_size_update = 0 + self._mem_params = Params("/dev/shm/params") if platform.system() != "Darwin" else ui_state.params + self._initialize_items() + self._update_map_size() + self._progress.set_visible(False) + self._state_btn.set_visible(False) + self._mapd_version.action_item.set_text(ui_state.params.get("MapdVersion") or "Loading...") + self._scroller = Scroller(self.items, line_separator=True, spacing=0) def _initialize_items(self): - items = [ + self._mapd_version = text_item(tr("Mapd Version"), lambda: ui_state.params.get("MapdVersion") or "Loading...") + self._delete_maps_btn = ListItemSP(tr("Downloaded Maps"), action_item=NoElideButtonAction(tr("DELETE"), enabled=True), callback=self._delete_maps) + self._progress = progress_item(tr("Downloading Map")) + self._update_btn = ListItemSP(tr("Database Update"), action_item=NoElideButtonAction(tr("CHECK"), enabled=True), callback=self._update_db) + self._country_btn = ListItemSP(tr("Country"), action_item=NoElideButtonAction(tr("SELECT"), enabled=True), callback=lambda: self._select_region("Country")) + self._state_btn = ListItemSP(tr("State"), action_item=NoElideButtonAction(tr("SELECT"), enabled=True), callback=lambda: self._select_region("State")) - ] - return items + self.items = [self._mapd_version, self._delete_maps_btn, self._progress, self._update_btn, self._country_btn, self._state_btn] - def _render(self, rect): - self._scroller.render(rect) + def _show_confirm(self, msg, confirm_text, func): + gui_app.set_modal_overlay(ConfirmDialog(msg, confirm_text), lambda res: func() if res == DialogResult.CONFIRM else None) + + def calculate_size(self): + total_size = 0 + directories_to_scan = [MAP_PATH] if MAP_PATH.exists() else [] + while directories_to_scan: + try: + for entry in os.scandir(directories_to_scan.pop()): + if entry.is_file(): + total_size += entry.stat().st_size + elif entry.is_dir(): + directories_to_scan.append(entry.path) + except OSError: + pass + self._delete_maps_btn.action_item.set_value(f"{total_size / 1024 ** 2:.2f} MB" if total_size < 1024 ** 3 else f"{total_size / 1024 ** 3:.2f} GB") + + def _update_map_size(self): + threading.Thread(target=self.calculate_size, daemon=True).start() + + def _do_delete_maps(self): + if MAP_PATH.exists(): + shutil.rmtree(MAP_PATH) + + for param in ("OsmDownloadedDate", "OsmLocal", "OsmLocationName", "OsmLocationTitle", "OsmStateName", "OsmStateTitle"): + ui_state.params.remove(param) + + self._delete_maps_btn.action_item.set_enabled(True) + self._delete_maps_btn.action_item.set_text(tr("DELETE")) + self._update_map_size() + + def _on_confirm_delete_maps(self): + self._delete_maps_btn.action_item.set_enabled(False) + self._delete_maps_btn.action_item.set_text("DELETING...") + threading.Thread(target=self._do_delete_maps).start() + + def _delete_maps(self): + self._show_confirm(tr("This will delete ALL downloaded maps\n\nAre you sure you want to delete all maps?"), + tr("Yes, delete all maps"), self._on_confirm_delete_maps) + + def _update_db(self): + self._show_confirm(tr("This will start the download process and it might take a while to complete."), tr("Start Download"), + lambda: ui_state.params.put_bool("OsmDbUpdatesCheck", True)) + + def _select_region(self, region_type): + is_country = region_type == "Country" + btn = self._country_btn if is_country else self._state_btn + btn.action_item.set_enabled(False) + btn.action_item.set_text(tr("FETCHING...")) + threading.Thread(target=self._do_select_region, args=(region_type, btn)).start() + + def _handle_region_selection(self, region_type, locations, key, res, ref): + if res != DialogResult.CONFIRM or not ref: + if region_type == "State" and res == DialogResult.CANCEL: + if ui_state.params.get("OsmLocationName") == "US" and not ui_state.params.get("OsmStateName"): + ui_state.params.remove("OsmLocationName") + ui_state.params.remove("OsmLocationTitle") + ui_state.params.remove("OsmLocal") + self._update_labels() + return + + if region_type == "Country": + ui_state.params.put_bool("OsmLocal", True) + ui_state.params.remove("OsmStateName") + ui_state.params.remove("OsmStateTitle") + + ui_state.params.put(f"{key}Name", ref) + name = next((n.data['display_name'] for n in locations if n.ref == ref), ref) + ui_state.params.put(f"{key}Title", name) + + if ref == "US" and region_type == "Country": + self._select_region("State") + else: + self._update_db() + + def _do_select_region(self, region_type, btn): + base_url = "https://raw.githubusercontent.com/pfeiferj/openpilot-mapd/main/" + url = base_url + ("nation_bounding_boxes.json" if region_type == "Country" else "us_states_bounding_boxes.json") + try: + data = requests.get(url, timeout=10).json() + locations = sorted([TreeNode(ref=k, data={'display_name': v['full_name']}) for k, v in data.items()], key=lambda n: n.data['display_name']) + except Exception: + locations = [] + + if region_type == "State": + locations.insert(0, TreeNode(ref="All", data={'display_name': tr("All states (~6.0 GB)")})) + + btn.action_item.set_enabled(True) + btn.action_item.set_text(tr("SELECT")) + + key = "OsmLocation" if region_type == "Country" else "OsmState" + current = ui_state.params.get(f"{key}Name") or "" + + dialog = TreeOptionDialog(tr(f"Select {region_type}"), [TreeFolder(folder="", nodes=locations)], current_ref=current, search_prompt="Perform a search") + dialog.on_exit = lambda res: self._handle_region_selection(region_type, locations, key, res, dialog.selection_ref) + gui_app.set_modal_overlay(dialog, callback=lambda res: self._handle_region_selection(region_type, locations, key, res, dialog.selection_ref)) + + def _update_labels(self): + downloading = bool(self._mem_params.get("OSMDownloadLocations")) + self._country_btn.set_enabled(not downloading) + self._state_btn.set_enabled(not downloading) + self._state_btn.set_visible(ui_state.params.get("OsmLocationName") == "US") + self._update_btn.set_visible(bool(ui_state.params.get("OsmLocationName"))) + + self._country_btn.action_item.set_value(ui_state.params.get("OsmLocationTitle") or "") + self._state_btn.action_item.set_value(ui_state.params.get("OsmStateTitle") or "") + + pending = ui_state.params.get_bool("OsmDbUpdatesCheck") + if downloading or pending: + if downloading: + device.reset_interactive_timeout() + self._update_map_size() + self._progress.set_visible(True) + progress = ui_state.params.get("OSMDownloadProgress") + total = progress.get('total_files', 0) if progress else 0 + done = progress.get('downloaded_files', 0) if progress else 0 + failed = total > 0 and not downloading and done < total + + if total > 0: + progress_perc = max(0.0, min(100.0, (done / total) * 100.0)) + else: + progress_perc = 0.0 + + if failed: + text = "0% - Downloading Maps" + btn_text = tr("Error: Invalid download. Retry.") + self._current_percent = 0.0 + elif total > 0 and downloading: + self._current_percent = progress_perc + perc_int = int(progress_perc) + text = f"{perc_int}% - Downloading Maps" + btn_text = f"{done}/{total} ({perc_int}%)" + else: + self._current_percent = 0.0 + text = "0% - Downloading Maps" + btn_text = tr("Downloading Maps...") + + self._progress.action_item.update(self._current_percent, text, show_progress=total > 0 and downloading and not failed) + self._update_btn.action_item.set_enabled(not downloading) # TODO-SP: introduce CANCEL database download with mapd + self._update_btn.action_item.set_value(btn_text) + self._country_btn.action_item.set_enabled(not downloading) + self._state_btn.action_item.set_enabled(not downloading) + self._delete_maps_btn.action_item.set_enabled(not downloading) + else: + self._progress.set_visible(False) + self._update_btn.action_item.set_enabled(True) + self._country_btn.action_item.set_enabled(True) + self._state_btn.action_item.set_enabled(True) + self._delete_maps_btn.action_item.set_enabled(True) + + ts = ui_state.params.get("OsmDownloadedDate") + dt: datetime.datetime | None = None + + if ts: + try: + ts_f = float(ts) + if ts_f > 0: + dt = datetime.datetime.fromtimestamp(ts_f, tz=datetime.UTC) + except (ValueError, TypeError): + dt = None + + formatted = time_ago(dt) + self._update_btn.action_item.set_value(tr("Last checked {}").format(formatted)) def show_event(self): self._scroller.show_event() + + def _update_state(self): + now = monotonic() + if now - self._last_map_size_update >= 1.0: + self._last_map_size_update = now + self._update_labels() + + def _render(self, rect): + self._scroller.render(rect) diff --git a/sunnypilot/mapd/mapd_manager.py b/sunnypilot/mapd/mapd_manager.py index 1211c1ecc6..9304f8f0b7 100755 --- a/sunnypilot/mapd/mapd_manager.py +++ b/sunnypilot/mapd/mapd_manager.py @@ -64,12 +64,7 @@ def request_refresh_osm_location_data(nations: list[str], states: list[str] = No "states": states or [] } - osm_download_locations_dump = json.dumps({ - "nations": nations, - "states": states or [] - }) - - print(f"Downloading maps for {osm_download_locations_dump}") + print(f"Downloading maps for {json.dumps(osm_download_locations)}") mem_params.put("OSMDownloadLocations", osm_download_locations) @@ -103,8 +98,6 @@ def filter_nations_and_states(nations: list[str], states: list[str] = None) -> t def update_osm_db() -> None: - # last_downloaded_date = params.get("OsmDownloadedDate", return_default=True) - # if params.get_bool("OsmDbUpdatesCheck") or time.monotonic() - last_downloaded_date >= 604800: # 7 days * 24 hours/day * 60 if params.get_bool("OsmDbUpdatesCheck"): cleanup_old_osm_data(get_files_for_cleanup()) country = params.get("OsmLocationName", return_default=True) From 46257aca02777737b04e2ce371e3a6e11c4f6a02 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Fri, 12 Dec 2025 03:16:57 -0500 Subject: [PATCH 106/110] ui: `HtmlModalSP` (#1570) * ui: `HtmlModalSP` * less --- system/ui/sunnypilot/widgets/html_render.py | 27 +++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 system/ui/sunnypilot/widgets/html_render.py diff --git a/system/ui/sunnypilot/widgets/html_render.py b/system/ui/sunnypilot/widgets/html_render.py new file mode 100644 index 0000000000..259067723e --- /dev/null +++ b/system/ui/sunnypilot/widgets/html_render.py @@ -0,0 +1,27 @@ +""" +Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + +This file is part of sunnypilot and is licensed under the MIT License. +See the LICENSE.md file in the root directory for more details. +""" +from openpilot.system.ui.lib.application import gui_app +from openpilot.system.ui.widgets import DialogResult +from openpilot.system.ui.widgets.html_render import HtmlModal + + +class HtmlModalSP(HtmlModal): + def __init__(self, file_path=None, text=None, callback=None): + super().__init__(file_path=file_path, text=text) + self._callback = callback + self._dialog_result = DialogResult.NO_ACTION + self._ok_button._click_callback = self._on_ok_clicked + + def _on_ok_clicked(self): + self._dialog_result = DialogResult.CONFIRM + gui_app.set_modal_overlay(None) + + if self._callback: + self._callback(self._dialog_result) + + def reset(self): + self._dialog_result = DialogResult.NO_ACTION From 9421e1cbfebdb2970afd777cf4642bc5af5795cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Harald=20Sch=C3=A4fer?= Date: Fri, 12 Dec 2025 18:04:16 -0800 Subject: [PATCH 107/110] Dark Souls 2 (#36849) 4b78e2e6-660f-4155-9105-81d4d8c658cd/400 --- selfdrive/modeld/models/driving_policy.onnx | 2 +- selfdrive/modeld/models/driving_vision.onnx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/selfdrive/modeld/models/driving_policy.onnx b/selfdrive/modeld/models/driving_policy.onnx index 1e764af9ba..e0eb918125 100644 --- a/selfdrive/modeld/models/driving_policy.onnx +++ b/selfdrive/modeld/models/driving_policy.onnx @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c5a1f0655ddf266ed42ad1980389d96f47cc5e756da1fa3ca1477a920bb9b157 +oid sha256:f8fe9a71b0fd428a045a82ed50790179f77aa664391198f078e11e7b2cb2c2d7 size 13926324 diff --git a/selfdrive/modeld/models/driving_vision.onnx b/selfdrive/modeld/models/driving_vision.onnx index 441c4a16af..76c96670a9 100644 --- a/selfdrive/modeld/models/driving_vision.onnx +++ b/selfdrive/modeld/models/driving_vision.onnx @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8f16d548ea4eb5d01518a9e90d4527cd97c31a84bcaf6f695dead8f0015fecc4 +oid sha256:1dc66bc06f250b577653ccbeaa2c6521b3d46749f601d0a1a366419e929ca438 size 46271942 From cc119b2a37c2503f0e1c54c3f7508113cb29685d Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 12 Dec 2025 21:37:26 -0800 Subject: [PATCH 108/110] comma four: adjust Wifi scroller sizes (#36854) * adjust sizes * back --- .../ui/mici/layouts/settings/network/wifi_ui.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py index eec16d884f..18c4dd5d6d 100644 --- a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py +++ b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py @@ -83,11 +83,13 @@ class WifiIcon(Widget): class WifiItem(BigDialogOptionButton): LEFT_MARGIN = 20 + HEIGHT = 54 + SELECTED_HEIGHT = 74 def __init__(self, network: Network): super().__init__(network.ssid) - self.set_rect(rl.Rectangle(0, 0, gui_app.width, 64)) + self.set_rect(rl.Rectangle(0, 0, gui_app.width, self.HEIGHT)) self._selected_txt = gui_app.texture("icons_mici/settings/network/new/wifi_selected.png", 48, 96) @@ -95,6 +97,10 @@ class WifiItem(BigDialogOptionButton): self._wifi_icon = WifiIcon() self._wifi_icon.set_current_network(network) + def set_selected(self, selected: bool): + super().set_selected(selected) + self._rect.height = self.SELECTED_HEIGHT if selected else self.HEIGHT + def set_current_network(self, network: Network): self._network = network self._wifi_icon.set_current_network(network) @@ -109,7 +115,7 @@ class WifiItem(BigDialogOptionButton): self._wifi_icon.render(rl.Rectangle( self._rect.x + self.LEFT_MARGIN, self._rect.y, - self._rect.height, + self.SELECTED_HEIGHT, self._rect.height )) @@ -118,7 +124,7 @@ class WifiItem(BigDialogOptionButton): self._label.set_color(rl.Color(255, 255, 255, int(255 * 0.9))) self._label.set_font_weight(FontWeight.DISPLAY) else: - self._label.set_font_size(70) + self._label.set_font_size(54) self._label.set_color(rl.Color(255, 255, 255, int(255 * 0.58))) self._label.set_font_weight(FontWeight.DISPLAY_REGULAR) From cab2a28e1092a01cfe69f30ec95b59ebd6f3d4c3 Mon Sep 17 00:00:00 2001 From: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com> Date: Fri, 12 Dec 2025 21:50:57 -0800 Subject: [PATCH 109/110] ui: Developer panel extension (#1521) * ui: developer panel * comment out * double translate * quickboot and more efficient file checking * use HtmlModalSP! * ui: `HtmlModalSP` * less * lint * less * just use existing dir on PC * grammar * match official * rename * biiig --------- Co-authored-by: Jason Wen --- .../sunnypilot/layouts/settings/developer.py | 106 ++++++++++++++++++ .../sunnypilot/layouts/settings/settings.py | 4 +- sunnypilot/sunnylink/params_metadata.json | 4 +- 3 files changed, 110 insertions(+), 4 deletions(-) create mode 100644 selfdrive/ui/sunnypilot/layouts/settings/developer.py diff --git a/selfdrive/ui/sunnypilot/layouts/settings/developer.py b/selfdrive/ui/sunnypilot/layouts/settings/developer.py new file mode 100644 index 0000000000..f3a2433373 --- /dev/null +++ b/selfdrive/ui/sunnypilot/layouts/settings/developer.py @@ -0,0 +1,106 @@ +""" +Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + +This file is part of sunnypilot and is licensed under the MIT License. +See the LICENSE.md file in the root directory for more details. +""" +import datetime +import os +from pathlib import Path + +from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.selfdrive.ui.layouts.settings.developer import DeveloperLayout +from openpilot.system.hardware import PC +from openpilot.system.hardware.hw import Paths +from openpilot.system.ui.lib.application import gui_app +from openpilot.system.ui.lib.multilang import tr +from openpilot.system.ui.widgets import DialogResult +from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog +from openpilot.system.ui.widgets.list_view import button_item + +from openpilot.system.ui.sunnypilot.widgets.html_render import HtmlModalSP +from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp + +PREBUILT_PATH = os.path.join(Paths.comma_home(), "prebuilt") if PC else "/data/openpilot/prebuilt" + + +class DeveloperLayoutSP(DeveloperLayout): + def __init__(self): + super().__init__() + self.error_log_path = os.path.join(Paths.crash_log_root(), "error.log") + self._is_release_branch: bool = self._is_release or ui_state.params.get_bool("IsReleaseSpBranch") + self._is_development_branch: bool = ui_state.params.get_bool("IsTestedBranch") or ui_state.params.get_bool("IsDevelopmentBranch") + self._initialize_items() + + for item in self.items: + self._scroller.add_widget(item) + + def _initialize_items(self): + self.show_advanced_controls = toggle_item_sp(tr("Show Advanced Controls"), + tr("Toggle visibility of advanced sunnypilot controls.
This only changes the visibility of the toggles; " + + "it does not change the actual enabled/disabled state."), param="ShowAdvancedControls") + + self.enable_github_runner_toggle = toggle_item_sp(tr("GitHub Runner Service"), tr("Enables or disables the GitHub runner service."), + param="EnableGithubRunner") + + self.enable_copyparty_toggle = toggle_item_sp(tr("copyparty Service"), + tr("copyparty is a very capable file server, you can use it to download your routes, view your logs " + + "and even make some edits on some files from your browser. " + + "Requires you to connect to your comma locally via its IP address."), param="EnableCopyparty") + + self.prebuilt_toggle = toggle_item_sp(tr("Quickboot Mode"), "", param="QuickBootToggle", callback=self._on_prebuilt_toggled) + + self.error_log_btn = button_item(tr("Error Log"), tr("VIEW"), tr("View the error log for sunnypilot crashes."), callback=self._on_error_log_clicked) + + self.items: list = [self.show_advanced_controls, self.enable_github_runner_toggle, self.enable_copyparty_toggle, self.prebuilt_toggle, self.error_log_btn,] + + @staticmethod + def _on_prebuilt_toggled(state): + if state: + Path(PREBUILT_PATH).touch(exist_ok=True) + else: + os.remove(PREBUILT_PATH) + ui_state.params.put_bool("QuickBootToggle", state) + + def _on_delete_confirm(self, result): + if result == DialogResult.CONFIRM: + if os.path.exists(self.error_log_path): + os.remove(self.error_log_path) + + def _on_error_log_closed(self, result, log_exists): + if result == DialogResult.CONFIRM and log_exists: + dialog2 = ConfirmDialog(tr("Would you like to delete this log?"), tr("Yes"), tr("No"), rich=False) + gui_app.set_modal_overlay(dialog2, callback=self._on_delete_confirm) + + def _on_error_log_clicked(self): + text = "" + if os.path.exists(self.error_log_path): + text = f"{datetime.datetime.fromtimestamp(os.path.getmtime(self.error_log_path)).strftime('%d-%b-%Y %H:%M:%S').upper()}

" + try: + with open(self.error_log_path) as file: + text += file.read() + except Exception: + pass + dialog = HtmlModalSP(text=text, callback=lambda result: self._on_error_log_closed(result, os.path.exists(self.error_log_path))) + gui_app.set_modal_overlay(dialog) + + def _update_state(self): + disable_updates = ui_state.params.get_bool("DisableUpdates") + show_advanced = ui_state.params.get_bool("ShowAdvancedControls") + + if (prebuilt_file := os.path.exists(PREBUILT_PATH)) != ui_state.params.get_bool("QuickBootToggle"): + ui_state.params.put_bool("QuickBootToggle", prebuilt_file) + self.prebuilt_toggle.action_item.set_state(prebuilt_file) + + self.prebuilt_toggle.set_visible(show_advanced and not (self._is_release_branch or self._is_development_branch)) + self.prebuilt_toggle.action_item.set_enabled(disable_updates) + + if disable_updates: + self.prebuilt_toggle.set_description(tr("When toggled on, this creates a prebuilt file to allow accelerated boot times. When toggled off, it " + + "removes the prebuilt file so compilation of locally edited cpp files can be made.")) + else: + self.prebuilt_toggle.set_description(tr("Quickboot mode requires updates to be disabled.
Enable 'Disable Updates' in the Software panel first.")) + + self.enable_copyparty_toggle.set_visible(show_advanced) + self.enable_github_runner_toggle.set_visible(show_advanced and not self._is_release_branch) + self.error_log_btn.set_visible(not self._is_release_branch) diff --git a/selfdrive/ui/sunnypilot/layouts/settings/settings.py b/selfdrive/ui/sunnypilot/layouts/settings/settings.py index 4de2cacafd..103dd99efd 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/settings.py +++ b/selfdrive/ui/sunnypilot/layouts/settings/settings.py @@ -9,7 +9,6 @@ from enum import IntEnum import pyray as rl from openpilot.selfdrive.ui.layouts.settings import settings as OP -from openpilot.selfdrive.ui.layouts.settings.developer import DeveloperLayout from openpilot.selfdrive.ui.sunnypilot.layouts.settings.device import DeviceLayoutSP from openpilot.selfdrive.ui.layouts.settings.firehose import FirehoseLayout from openpilot.selfdrive.ui.sunnypilot.layouts.settings.software import SoftwareLayoutSP @@ -31,6 +30,7 @@ from openpilot.selfdrive.ui.sunnypilot.layouts.settings.steering import Steering from openpilot.selfdrive.ui.sunnypilot.layouts.settings.cruise import CruiseLayout from openpilot.selfdrive.ui.sunnypilot.layouts.settings.visuals import VisualsLayout from openpilot.selfdrive.ui.sunnypilot.layouts.settings.display import DisplayLayout +from openpilot.selfdrive.ui.sunnypilot.layouts.settings.developer import DeveloperLayoutSP # from openpilot.selfdrive.ui.sunnypilot.layouts.settings.navigation import NavigationLayout @@ -125,7 +125,7 @@ class SettingsLayoutSP(OP.SettingsLayout): OP.PanelType.TRIPS: PanelInfo(tr_noop("Trips"), TripsLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_trips.png"), OP.PanelType.VEHICLE: PanelInfo(tr_noop("Vehicle"), VehicleLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_vehicle.png"), OP.PanelType.FIREHOSE: PanelInfo(tr_noop("Firehose"), FirehoseLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_firehose.png"), - OP.PanelType.DEVELOPER: PanelInfo(tr_noop("Developer"), DeveloperLayout(), icon="icons/shell.png"), + OP.PanelType.DEVELOPER: PanelInfo(tr_noop("Developer"), DeveloperLayoutSP(), icon="icons/shell.png"), } def _draw_sidebar(self, rect: rl.Rectangle): diff --git a/sunnypilot/sunnylink/params_metadata.json b/sunnypilot/sunnylink/params_metadata.json index 83ec3c7af8..4cc6b18089 100644 --- a/sunnypilot/sunnylink/params_metadata.json +++ b/sunnypilot/sunnylink/params_metadata.json @@ -258,11 +258,11 @@ "description": "" }, "EnableCopyparty": { - "title": "Enable Copyparty", + "title": "copyparty Service", "description": "" }, "EnableGithubRunner": { - "title": "Enable GitHub Runner", + "title": "GitHub Runner Service", "description": "" }, "EnableSunnylinkUploader": { From e0a27514e220a1547ce0f4912137ed8598a59129 Mon Sep 17 00:00:00 2001 From: discountchubbs Date: Sat, 13 Dec 2025 07:01:57 -0800 Subject: [PATCH 110/110] Research report --- sunnypilot/common/README.md | 189 ++++++++++-------- .../common/assets/io_usage_param_watcher.png | 3 + .../assets/io_usage_pre_paramwatcher.png | 3 + .../memory_usage_00000025--d50a4a3471.png | 3 + .../assets/memory_usage_pre_paramwatcher.png | 3 + 5 files changed, 113 insertions(+), 88 deletions(-) create mode 100644 sunnypilot/common/assets/io_usage_param_watcher.png create mode 100644 sunnypilot/common/assets/io_usage_pre_paramwatcher.png create mode 100644 sunnypilot/common/assets/memory_usage_00000025--d50a4a3471.png create mode 100644 sunnypilot/common/assets/memory_usage_pre_paramwatcher.png diff --git a/sunnypilot/common/README.md b/sunnypilot/common/README.md index fe42a58790..5c17e2702c 100644 --- a/sunnypilot/common/README.md +++ b/sunnypilot/common/README.md @@ -1,109 +1,122 @@ -# Comparative Analysis of Parameter Access Methods: `Params::get` vs. `ParamWatcher` +
-## Inefficiencies in Standard Parameter Access -The standard `Params::get()` method executes a full file I/O lifecycle—opening, allocating, reading, and closing—for every function call. This approach results in significant CPU overhead and memory churn due to the frequency of these operations in the user interface loop. +# Comparative Analysis of Parameter Getter Methods: Params vs. ParamWatcher -### System Overhead Analysis -* **System Call Overhead**: Every read operation requires context switches into kernel mode. The `Params::get` function calls `util::read_file` (sunnypilot, 2025), which subsequently invokes `std::ifstream` (sunnypilot, 2025). -* **Impact**: Frequent context switching degrades performance (Linux man-pages, 2025 -a; Linux man-pages, 2025 -b). -* **C++ Stream Overhead**: The use of `std::ifstream` introduces additional overhead for maintaining stream state and buffering compared to raw file descriptors (cppreference.com, n.d.-a; Codezup, 2025). -* **Memory Churn**: The instantiation of `std::string result(size, '\0');` forces heap allocation and deallocation during every call (sunnypilot, 2025). This stresses the memory allocator and can lead to fragmentation (cppreference.com, n.d.). +James (sunnypilot Developer)
+December 13, 2025 -## The ParamWatcher Optimization -The `ParamWatcher` implementation utilizes OS-level file system events, such as `inotify` on Linux or `FSEvents` on macOS, to maintain a Random Access Memory (RAM) cache. This architecture eliminates the need for continuous polling. +
-### Performance Comparison -| Feature | Standard `Params::get` | Optimized `ParamWatcher` | -| :--- | :--- | :--- | -| **Workflow** | `open` → `malloc` → `read` → `close` | `dict.get()` (RAM lookup) | -| **Complexity** | **O(N * F)** (Linear to toggles & FPS) | **O(1)** (Constant time) | -| **Disk I/O** | ~1,000 reads/sec (50 toggles @ 20FPS) | **0 reads/sec** (Steady state) | -| **Memory** | New string object per call (High GC pressure) | Returns reference (Zero GC pressure) | -## Architectural Mismatch of Standard Modules -Standard C++ modules like `std::ifstream` are optimized for **throughput**—reading large files sequentially—rather than **latency** required for polling small files frequently. +##

Abstract -* **The I/O Trap**: Even when a file resides in the OS page cache (RAM), invoking `open()` and `read()` forces a CPU mode switch (User → Kernel → User). Executing this sequence 1,000 times per second consumes CPU cycles merely to verify state constancy. -* **The Memory Trap**: The `std::string` class allocates memory on the heap. Repeated allocation creates short-lived objects, which in C++ fragments memory. In Python (which wraps this), it triggers the Garbage Collector, pausing the UI. -* **The Query Mismatch**: `Params::get` queries the current value every frame, whereas `ParamWatcher` waits for a notification of change, serving cached values in the interim. +This research report examines the inefficiencies in standard parameter access methods within sunnypilot and proposes an optimized alternative, ParamWatcher. The standard `Params::get()` method incurs significant CPU and memory overhead due to repeated file I/O operations (sunnypilot, 2025). ParamWatcher utilizes OS-level file system events (inotify on Linux, FSEvents on macOS) to maintain an in-memory cache, reducing I/O to near zero (Linux man-pages, 2025-c; Apple Inc., n.d.-a). Empirical benchmarks with ~10 million parameter accesses demonstrate a 14.5x CPU speedup and flat memory usage (514.7 KB vs. 497.7 KB for base Params, with only 17 KB overhead). The implementation employs a process-local singleton pattern for efficiency in multi-process architectures (Gamma et al., 1994). Results indicate ParamWatcher eliminates UI stutters and GC pauses, enhancing system responsiveness without compromising data freshness. -## Implementation Analysis -The `ParamWatcher` class provides a cross-platform solution for monitoring file system changes, specifically targeting the parameter files used in Openpilot. The implementation leverages the `ctypes` library to interface directly with operating system kernels, bypassing higher-level abstractions for maximum performance. +**Keywords:** parameter access, file I/O optimization, event-driven caching, autonomous driving systems, performance benchmarking -### Linux Implementation (`_run_linux`) -The Linux implementation interacts directly with the kernel's `inotify` subsystem (Linux man-pages, 2025 -c). +## Introduction -* **Library Loading**: `libc = ctypes.CDLL('libc.so.6')` loads the standard C library to access system calls. -* **Initialization**: `inotify_init()` is called to create a new inotify instance, returning a file descriptor. -* **Watch Setup**: `inotify_add_watch(fd, path, mask)` registers the parameters directory. The mask includes `IN_MODIFY | IN_CREATE | IN_DELETE | IN_MOVED_TO | IN_CLOSE_WRITE` (Linux Kernel Organization, 2005) to capture all relevant file changes. -* **Event Loop**: - * **Polling**: `select.epoll()` is used to efficiently wait for activity on the file descriptor without busy-waiting. - * **Reading**: When events occur, `os.read(fd, 1024)` retrieves the raw binary event data. - * **Parsing**: The code uses Python's `struct` module (`struct.unpack_from("iIII", ...)`) to parse the C-style `inotify_event` structures directly from the buffer, avoiding the overhead of defining `ctypes` structures. - * **Handling**: Extracted filenames are passed to `_trigger_callbacks`, which invalidates the specific cache entry (`self._cache.pop(path, None)`), forcing a fresh read on the next access. +In sunnypilot, efficient parameter management is important for real-time system access. The standard `Params::get()` method, implemented in C++ and wrapped in Python, performs full file I/O cycles for each access, leading to high CPU overhead and memory churn (sunnypilot, 2025). This is particularly problematic in UI loops where parameters are queried frequently (e.g., 50 toggles at 20 FPS equates to ~1,000 reads/second). -### macOS Implementation (`_run_darwin`) -The macOS implementation uses the `FSEvents` API from the `CoreServices` framework (Apple Inc., n.d.-a), which is more efficient than `kqueue` for directory monitoring. +This inefficiency stems from architectural mismatches: C++ streams are designed for throughput, not latency (cppreference.com, n.d.-a). Each call triggers kernel mode switches, heap allocations, and garbage collection in Python, causing UI stutters (Linux man-pages, 2025-a; Linux man-pages, 2025-b). -* **Framework Loading**: `ctypes.cdll.LoadLibrary` loads `CoreServices` and `CoreFoundation`. -* **Callback Definition**: `CFUNCTYPE` is used to define a C-compatible callback function. This function is invoked by the OS whenever a change occurs in the watched directory. -* **Stream Creation**: `FSEventStreamCreate` creates a stream for the target directory. The `kFSEventStreamCreateFlagFileEvents` flag is used to request file-level granularity where available. -* **Event Filtering**: The callback filters events using flags such as `kFSEventStreamEventFlagItemCreated` and `kFSEventStreamEventFlagItemModified` to ensure only relevant file changes trigger updates (Apple Inc., n.d.-c). -* **Scheduling**: `FSEventStreamScheduleWithRunLoop` attaches the stream to the current thread's run loop (Apple Inc., n.d.-b). -* **Execution**: `CFRunLoopRun()` starts the event loop. This passes control to the OS, which wakes the thread only when necessary. -* **Handling**: Inside the callback, the code iterates through the changed paths provided by the OS. It extracts the filename and calls `_trigger_callbacks` to invalidate the cache for that specific parameter. +### Inefficiencies in Standard Parameter Access +The standard `Params::get()` method executes a full file I/O lifecycle—opening, allocating, reading, and closing—for every function call. This results in significant CPU overhead and memory churn due to the frequency of these operations in the user interface loop. -### Python ctypes Integration -The use of `ctypes` (Python Software Foundation, 2025) is a strategic choice. It allows the Python interpreter to load shared libraries (`libc.so.6` on Linux, `CoreServices` on macOS) and call C functions directly. This approach avoids the overhead of spawning subprocesses or compiling external C extensions, keeping the codebase pure Python while achieving C-level system integration. +#### System Overhead Analysis +- **System Call Overhead**: Every read operation requires context switches into kernel mode. The `Params::get` function calls `util::read_file` (sunnypilot, 2025), which subsequently invokes `std::ifstream` (sunnypilot, 2025). +- **Impact**: Frequent context switching degrades performance (Linux man-pages, 2025-a; Linux man-pages, 2025-b). +- **C++ Stream Overhead**: The use of `std::ifstream` introduces additional overhead for maintaining stream state and buffering compared to raw file descriptors (cppreference.com, n.d.-a; Codezup, 2025). +- **Memory Churn**: The instantiation of `std::string result(size, '\0');` forces heap allocation and deallocation during every call (sunnypilot, 2025). This stresses the memory allocator and can lead to fragmentation (cppreference.com, n.d.-b). -### Memory Impact Analysis - With 232 defined parameters in `param_keys.h`, the maximum static RAM footprint of `ParamWatcher` is estimated to be **less than 250 KB**. Even if every single parameter were cached simultaneously, this static usage is negligible. Importantly, this stable footprint is likely more probable to maintain no trend of memory increase, whenc compared to the standard `Params::get` approach, which generates **megabytes** of short-lived "garbage" allocations per second, forcing the Python Garbage Collector to pause execution repeatedly. +This report introduces ParamWatcher, an event-driven caching solution using OS file system events. It shifts from polling to notifications, caching converted values in static RAM. I propose that ParamWatcher achieves minimum 10x+ CPU gains with bounded non increasing memory, improving sunnypilot's performance both on latency and responsiveness. -## Architectural Integration: The Process-Local Singleton Pattern -To ensure resource efficiency within openpilot's multi-process architecture (e.g., `ui`, `controlsd`, `modeld`), `ParamWatcher` implements the Singleton design pattern (Gamma et al., 1994) using the Python `__new__` allocator. +## Method -### Process Isolation and Concurrency -In the context of Python's memory model, a Singleton ensures a single instance exists *per process*. This behavior aligns with openpilot's multiprocess design: +### Materials +- **System:** sunnypilot, running on macOS, Ubuntu/Linux, comma 3x, and comma four. +- **Parameters:** 231 defined keys in `param_keys.h`. +- **Tools:** Python 3, tracemalloc for memory profiling, time.perf_counter for CPU timing, ctypes for OS integration (Python Software Foundation, 2025). -* **Intra-Process Efficiency**: Within a single heavy process like `ui`, multiple sub-components (e.g., `UIState`, `SunnylinkState`) import and use `Params`. The Singleton pattern ensures they share a single `inotify` thread and a unified RAM cache. This prevents the proliferation of redundant watcher threads, which would otherwise compete for the Global Interpreter Lock (GIL). -* **Inter-Process Safety**: Distinct processes (e.g., `modeld` vs. `ui`) maintain completely isolated `ParamWatcher` instances. This isolation eliminates the need for complex Inter-Process Communication (IPC) locking mechanisms for the cache, as each process synchronizes its independent state via the OS file system events. +### Implementation Details +ParamWatcher provides cross-platform file system monitoring using ctypes for direct OS integration (Python Software Foundation, 2025). -### Empirical Verification -Runtime analysis demonstrates that multiple instantiation attempts result in a shared object reference, minimizing memory footprint. +#### Linux Implementation +On Linux, ParamWatcher uses the inotify subsystem for efficient file change detection (Linux man-pages, 2025-c). It loads `libc.so.6` to access system calls, initializes an inotify instance, and watches the parameters directory for events like `IN_MODIFY` and `IN_CLOSE_WRITE` (Linux Kernel Organization, 2005). Events are polled with `select.epoll()` and parsed using `struct.unpack_from()` to avoid ctypes overhead. Filenames are extracted and passed to cache invalidation, ensuring real-time updates without polling (Codezup, 2025). -* **Test Case**: Instantiating `ParamWatcher` in `UIStateSP` and subsequently in a standalone script within the same process. -* **Result**: Both instances report the exact same memory address (`4814358960`) and share the same background thread ID (`6114635776`). -* **Impact**: The system incurs the overhead of the watcher thread (measured at < 0.1% CPU idle usage) only once per active process, regardless of import frequency. The average CPU usage across one minute was 0.002%. +- **Library Loading**: `libc = ctypes.CDLL('libc.so.6')` loads the standard C library to access system calls. +- **Initialization**: `inotify_init()` is called to create a new inotify instance, returning a file descriptor. +- **Watch Setup**: `inotify_add_watch(fd, path, mask)` registers the parameters directory. The mask includes `IN_MODIFY | IN_CREATE | IN_DELETE | IN_MOVED_TO | IN_CLOSE_WRITE` (Linux Kernel Organization, 2005) to capture all relevant file changes. +- **Event Loop**: + - **Polling**: `select.epoll()` is used to efficiently wait for activity on the file descriptor without busy-waiting. + - **Reading**: When events occur, `os.read(fd, 1024)` retrieves the raw binary event data. + - **Parsing**: The code uses Python's `struct` module (`struct.unpack_from("iIII", ...)`) to parse the C-style `inotify_event` structures directly from the buffer, avoiding the overhead of defining `ctypes` structures. + - **Handling**: Extracted filenames are passed to `_trigger_callbacks`, which invalidates the specific cache entry (`self._cache.pop(path, None)`), forcing a fresh read on the next access. -## Limitations and Trade-offs -While `ParamWatcher` offers superior performance for UI rendering, it presents specific trade-offs: +#### macOS Implementation +On macOS, ParamWatcher leverages FSEvents from CoreServices for directory monitoring (Apple Inc., n.d.-a). It defines a C-compatible callback using `CFUNCTYPE`, creates an `FSEventStream` with `kFSEventStreamCreateFlagFileEvents`, and schedules it on the run loop (Apple Inc., n.d.-b). Events are filtered for modifications, creations, and renames (Apple Inc., n.d.-c), triggering cache invalidation for affected parameters. -* **Static RAM Usage**: `ParamWatcher` maintains a persistent dictionary cache of all accessed parameters (~50KB), whereas `Params::get` uses zero static RAM but incurs high dynamic memory access. -* **Event Latency**: In high-load scenarios, `inotify` events may experience slight delays or coalescing compared to direct reads. However, for user interface applications, this latency (<10ms) is imperceptible. -* **Complexity**: The solution (the process singleton approach) requires managing a background thread and OS-specific event loops, increasing code complexity compared to the synchronous `Params::get` function. +- **Framework Loading**: `ctypes.cdll.LoadLibrary` loads `CoreServices` and `CoreFoundation`. +- **Callback Definition**: `CFUNCTYPE` is used to define a C-compatible callback function. This function is invoked by the OS whenever a change occurs in the watched directory. +- **Stream Creation**: `FSEventStreamCreate` creates a stream for the target directory. The `kFSEventStreamCreateFlagFileEvents` flag is used to request file-level granularity where available. +- **Event Filtering**: The callback filters events using flags such as `kFSEventStreamEventFlagItemCreated` and `kFSEventStreamEventFlagItemModified` to ensure only relevant file changes trigger updates (Apple Inc., n.d.-c). +- **Scheduling**: `FSEventStreamScheduleWithRunLoop` attaches the stream to the current thread's run loop (Apple Inc., n.d.-b). +- **Execution**: `CFRunLoopRun()` starts the event loop. This passes control to the OS, which wakes the thread only when necessary. +- **Handling**: Inside the callback, the code iterates through the changed paths provided by the OS. It extracts the filename and calls `_trigger_callbacks` to invalidate the cache for that specific parameter. -## Alternative Architecture Considered: ZeroMQ Service (ZMQ) -During the development of `ParamWatcher`, a Client-Server architecture using ZMQ was evaluated. In this architecture, a single background service process would monitor file system events and publish changes over a ZMQ PUB socket to multiple client processes (SUB). +### Procedure +Benchmarks simulated heavy load: +- **Memory Test:** ~10 million gets (43,290 loops over 231 keys), measured with tracemalloc. +- **CPU Test:** Same load, timed with perf_counter. +- Comparisons: Base Params vs. ParamWatcher. -### Trade-off Analysis -| Metric | In-Process (Current) | ZMQ Service (Rejected) | -| :--- | :--- |:------------------------------------------------------| -| **Memory Usage** | Low (1 thread/process) | High (1 full Python process + ZMQ buffers per client) | -| **CPU Usage** | Low (Direct callback) | High (Serialization + TCP Stack + Deserialization) | -| **Latency** | Instant (<0.1ms) | Variable (TCP Loopback overhead) | -| **Scalability** | Limited by OS file handles | Limited by TCP ports/buffers | -| **Robustness** | Process-isolated failure | Single point of failure (Service crash affects all) | +## Results -### Decision Rationale -While the ZMQ approach offers better isolation and reduces the total number of OS file watchers (1 vs N), the overhead of inter-process communication (IPC) proved excessive for this use case. -* **Efficiency**: Even with 50+ processes, the memory footprint of 50 simple threads is significantly lower than the overhead of a dedicated Python service process plus the ZMQ context in every client. -* **Complexity**: The ZMQ architecture introduced synchronization challenges (e.g., service startup race conditions, "Address already in use" errors) that outweighed its benefits. -* **Performance**: The latency of serializing messages and passing them through the TCP stack is orders of magnitude higher than a direct function call within the same process memory space. +### Memory Usage +Using tracemalloc for peak memory measurement during ~10 million parameter accesses (43,290 loops over 231 keys), base Params peaked at 497.7 KB, while ParamWatcher peaked at 514.7 KB (17 KB overhead). ParamWatcher's memory remained flat post-initialization, preventing churn. -## Conclusion -Replacing polling mechanisms with event-driven caching shifts the computational load from kernel space (syscalls) to user space (RAM). This transition eliminates I/O overhead and UI stutters caused by garbage collection, resulting in a more responsive user experience. The In-Process Singleton approach was selected as the optimal balance between performance, complexity, and resource efficiency. +| Condition | Memory (KB) | Overhead | +|-----------|-------------|----------| +| Base Params | 497.7 | - | +| ParamWatcher | 514.7 | 17 KB | +### CPU Performance +ParamWatcher was 14.5x faster: 4.52s vs. 65.43s to complete ~10 million param gets. + +| Condition | Time (s) | Speedup | +|-----------|----------|---------| +| Base Params | 65.43 | 1x | +| ParamWatcher | 4.52 | 14.5x | + +### Scalability +No degradation at scale; cache invalidation maintained freshness. + +See Appendix A for visual graphs of memory usage over a 30-minute time span, captured on comma four. These two routes are of equal conditions: each route started completely unplugged to two minutes offroad, followed by onroad state, with ambient temperature at 75 degrees fahrenheit. These routes are direct comparisons of pre-ParamWatcher and ParamWatcher implementations. Appendix A also includes I/O capture graphs for those 30-minute routes, demonstrating reductions in file system activity post-ParamWatcher. + +## Discussion + +ParamWatcher successfully optimizes parameter access, delivering substantial CPU gains with minimal memory overhead. The event-driven approach eliminates I/O bottlenecks, reducing GC pressure and UI stutters (cppreference.com, n.d.-b). The 17 KB memory overhead is negligible compared to the megabytes of churn from base Params, ensuring bounded usage in multi-process environments via the singleton pattern (Gamma et al., 1994). + +Results demonstrate scalability without degradation, with cache invalidation maintaining data freshness. This optimization enhances system responsiveness. +Limitations include potential event latency in high-load scenarios (<10 ms, imperceptible for UI) and increased complexity from background threads. +Trade-offs: Static RAM (~17 KB) vs. dynamic churn; benefits outweigh costs for param-heavy workloads. + +##
Appendix A: Memory Usage Graphs + +### Base Params Memory Usage +![Base Params Memory Usage](assets/memory_usage_pre_paramwatcher.png) + +### ParamWatcher Memory Usage +![ParamWatcher Memory Usage](assets/memory_usage_00000025--d50a4a3471.png) + +### Base Params IO Usage +![Base Params IO Usage](assets/io_usage_pre_paramwatcher.png) + +### ParamWatcher IO Usage +![Base Params IO Usage](assets/io_usage_param_watcher.png) + + +##
References -## References Apple Inc. (n.d.-a). *File System Events*. Retrieved from https://developer.apple.com/documentation/coreservices/file_system_events Apple Inc. (n.d.-b). *CFRunLoop*. Retrieved from https://developer.apple.com/documentation/corefoundation/cfrunloop @@ -116,17 +129,17 @@ cppreference.com. (n.d.-a). *std::basic_ifstream*. Retrieved from https://en.cpp cppreference.com. (n.d.-b). *std::basic_string*. Retrieved from https://en.cppreference.com/w/cpp/string/basic_string/basic_string -Linux man-pages. (2025 -a). *open(2)*. Retrieved from https://man7.org/linux/man-pages/man2/open.2.html - -Linux man-pages. (2025-b). *read(2)*. Retrieved from https://man7.org/linux/man-pages/man2/read.2.html - -Linux man-pages. (2025 -c). *inotify(7)*. Retrieved from https://man7.org/linux/man-pages/man7/inotify.7.html +Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). *Design Patterns: Elements of Reusable Object-Oriented Software*. Addison-Wesley. Linux Kernel Organization. (2005). *include/uapi/linux/inotify.h*. Retrieved from https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/uapi/linux/inotify.h -Python Software Foundation. (2025). *ctypes — A foreign function library for Python*. Retrieved from https://docs.python.org/3/library/ctypes.html +Linux man-pages. (2025-a). *open(2)*. Retrieved from https://man7.org/linux/man-pages/man2/open.2.html -Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). *Design Patterns: Elements of Reusable Object-Oriented Software*. Addison-Wesley. +Linux man-pages. (2025-b). *read(2)*. Retrieved from https://man7.org/linux/man-pages/man2/read.2.html + +Linux man-pages. (2025-c). *inotify(7)*. Retrieved from https://man7.org/linux/man-pages/man7/inotify.7.html + +Python Software Foundation. (2025). *ctypes — A foreign function library for Python*. Retrieved from https://docs.python.org/3/library/ctypes.html sunnypilot. (2025). *common/params.cc* [Source code]. GitHub. https://github.com/sunnypilot/sunnypilot/blob/master/common/params.cc#L180C1-L206C2 diff --git a/sunnypilot/common/assets/io_usage_param_watcher.png b/sunnypilot/common/assets/io_usage_param_watcher.png new file mode 100644 index 0000000000..1fa5a0960c --- /dev/null +++ b/sunnypilot/common/assets/io_usage_param_watcher.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b94f93515223b41fba5250ffb7c795ebb1640c524de5296393b02bd36a202a18 +size 566837 diff --git a/sunnypilot/common/assets/io_usage_pre_paramwatcher.png b/sunnypilot/common/assets/io_usage_pre_paramwatcher.png new file mode 100644 index 0000000000..ede6256cb2 --- /dev/null +++ b/sunnypilot/common/assets/io_usage_pre_paramwatcher.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:696dc900066c78de38e64f0636716a588f68f2fa4a3d4fbfdb5ca3f8ab49e922 +size 291424 diff --git a/sunnypilot/common/assets/memory_usage_00000025--d50a4a3471.png b/sunnypilot/common/assets/memory_usage_00000025--d50a4a3471.png new file mode 100644 index 0000000000..070e65b7fd --- /dev/null +++ b/sunnypilot/common/assets/memory_usage_00000025--d50a4a3471.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:abb4a5ec3108d337cb4aab0d009c5dd7c03f601f725188fcb933f7dc4cd1b1fa +size 248658 diff --git a/sunnypilot/common/assets/memory_usage_pre_paramwatcher.png b/sunnypilot/common/assets/memory_usage_pre_paramwatcher.png new file mode 100644 index 0000000000..d169afa212 --- /dev/null +++ b/sunnypilot/common/assets/memory_usage_pre_paramwatcher.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a27f8ba0633e806083ce41bb51fbabcd027f8ad713534dc04298cc2f350b3389 +size 311370