From 0dd59d04047b8bf52b9e07e25b93bedcee6b42e9 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Sat, 13 Dec 2025 01:32:06 -0800 Subject: [PATCH 01/39] comma four: fix missing WiFi show_event (#36858) * can't do this * can do this --- 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 d085fdf55f..1faf49311a 100644 --- a/selfdrive/ui/mici/layouts/settings/network/__init__.py +++ b/selfdrive/ui/mici/layouts/settings/network/__init__.py @@ -171,6 +171,8 @@ class NetworkLayoutMici(NavWidget): }.get(self._wifi_manager.current_network_metered, 'default')) def _switch_to_panel(self, panel_type: NetworkPanelType): + if panel_type == NetworkPanelType.WIFI: + self._wifi_ui.show_event() self._current_panel = panel_type def _render(self, rect: rl.Rectangle): From 3206784dd8467b1d384293710d26bb0629c3aefe Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Sat, 13 Dec 2025 01:35:10 -0800 Subject: [PATCH 02/39] comma four: rm duplicate wifi show_event --- selfdrive/ui/mici/layouts/settings/network/wifi_ui.py | 1 - 1 file changed, 1 deletion(-) diff --git a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py index 18c4dd5d6d..99e41431e1 100644 --- a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py +++ b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py @@ -350,7 +350,6 @@ class WifiUIMici(BigMultiOptionDialog): # Call super to prepare scroller; selection scroll is handled dynamically super().show_event() self._wifi_manager.set_active(True) - self._scroller.show_event() def hide_event(self): super().hide_event() From 350dc6a50fcc37b3967dbb736ee0417fd31b2946 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Sat, 13 Dec 2025 01:39:45 -0800 Subject: [PATCH 03/39] comma four: fix WiFi panel not starting at the top (#36859) * fix * fix --- system/ui/widgets/scroller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/ui/widgets/scroller.py b/system/ui/widgets/scroller.py index 72f76d90c4..4858569d2f 100644 --- a/system/ui/widgets/scroller.py +++ b/system/ui/widgets/scroller.py @@ -246,7 +246,7 @@ class Scroller(Widget): def show_event(self): super().show_event() if self._reset_scroll_at_show: - self.scroll_to(self.scroll_panel.get_offset()) + self.scroll_panel.set_offset(0.0) for item in self._items: item.show_event() From e9255d1e9c043dbc75169c5efc48bf9e1ac00a56 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Sat, 13 Dec 2025 01:41:01 -0800 Subject: [PATCH 04/39] NavWidget: disable nav bar for vertical scrollers (#36857) * disable nav bar vert scroller * cmt --- system/ui/widgets/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/system/ui/widgets/__init__.py b/system/ui/widgets/__init__.py index 546c682f33..097ac74c7e 100644 --- a/system/ui/widgets/__init__.py +++ b/system/ui/widgets/__init__.py @@ -270,13 +270,17 @@ class NavWidget(Widget, abc.ABC): in_dismiss_area = mouse_event.pos.y < self._rect.height * self.BACK_TOUCH_AREA_PERCENTAGE scroller_at_top = False + vertical_scroller = False # TODO: -20? snapping in WiFi dialog can make offset not be positive at the top if hasattr(self, '_scroller'): scroller_at_top = self._scroller.scroll_panel.get_offset() >= -20 and not self._scroller._horizontal + vertical_scroller = not self._scroller._horizontal elif hasattr(self, '_scroll_panel'): scroller_at_top = self._scroll_panel.get_offset() >= -20 and not self._scroll_panel._horizontal + vertical_scroller = not self._scroll_panel._horizontal - if in_dismiss_area or scroller_at_top: + # Vertical scrollers need to be at the top to swipe away to prevent erroneous swipes + if (not vertical_scroller and in_dismiss_area) or scroller_at_top: self._can_swipe_away = True self._back_button_start_pos = mouse_event.pos From f4dea7977b8a72d4eb0a4a7afc86e1b1f07d5529 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Sat, 13 Dec 2025 01:44:05 -0800 Subject: [PATCH 05/39] ui: improve network sort (#36855) * better sort * clean up --- system/ui/lib/wifi_manager.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index 28bd58f226..7e5f04ef6f 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -630,7 +630,8 @@ class WifiManager: known_connections = self._get_connections() networks = [Network.from_dbus(ssid, ap_list, ssid in known_connections) for ssid, ap_list in aps.items()] - networks.sort(key=lambda n: (-n.is_connected, n.ssid.lower())) + # sort with quantized strength to reduce jumping + networks.sort(key=lambda n: (-n.is_connected, -round(n.strength / 100 * 4), n.ssid.lower())) self._networks = networks self._update_ipv4_address() From 7a324fc377819472ce9df9074a5d79338dd93bca Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Sat, 13 Dec 2025 01:50:55 -0800 Subject: [PATCH 06/39] comma four: reset WiFi SSID scroll on show (#36861) reset scroll --- selfdrive/ui/mici/widgets/dialog.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/selfdrive/ui/mici/widgets/dialog.py b/selfdrive/ui/mici/widgets/dialog.py index b11056f993..e445c0c9c7 100644 --- a/selfdrive/ui/mici/widgets/dialog.py +++ b/selfdrive/ui/mici/widgets/dialog.py @@ -285,6 +285,10 @@ class BigDialogOptionButton(Widget): font_weight=FontWeight.DISPLAY_REGULAR, alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE, scroll=True) + def show_event(self): + super().show_event() + self._label.reset_scroll() + def set_selected(self, selected: bool): self._selected = selected From 6c5be6ddab1748bda122c0ac55c2b1ae5d0dcc47 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Sat, 13 Dec 2025 04:08:01 -0800 Subject: [PATCH 07/39] WifiUi: fix infinite wraps (#36863) * fix infinite wrap * fix selection * Revert "fix selection" This reverts commit 555c57922409312bf5d9efedf571994f157b9e44. * revert * revert * revert * revert * cleaner * cleaner * mypy!! --- .../ui/mici/layouts/settings/network/wifi_ui.py | 14 ++++++++------ selfdrive/ui/mici/widgets/dialog.py | 10 +++------- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py index 99e41431e1..5a79b8a639 100644 --- a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py +++ b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py @@ -389,12 +389,6 @@ class WifiUIMici(BigMultiOptionDialog): else: network_button = WifiItem(network) - def show_network_info_page(_network): - 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)) - self.add_button(network_button) # remove networks no longer present @@ -406,6 +400,14 @@ class WifiUIMici(BigMultiOptionDialog): self._wifi_manager.connect_to_network(ssid, password) self._update_buttons() + def _on_option_selected(self, option: str): + super()._on_option_selected(option) + + # only open if button is already selected + if option in self._networks and option == self._selected_option: + self._network_info_page.set_current_network(self._networks[option]) + self._should_open_network_info_page = True + def _connect_to_network(self, ssid: str): network = self._networks.get(ssid) if network is None: diff --git a/selfdrive/ui/mici/widgets/dialog.py b/selfdrive/ui/mici/widgets/dialog.py index e445c0c9c7..2118b62ed6 100644 --- a/selfdrive/ui/mici/widgets/dialog.py +++ b/selfdrive/ui/mici/widgets/dialog.py @@ -331,14 +331,10 @@ class BigMultiOptionDialog(BigDialogBase): self.add_button(BigDialogOptionButton(option)) def add_button(self, button: BigDialogOptionButton): - og_callback = button._click_callback + def click_callback(_btn=button): + self._on_option_selected(_btn.option) - def wrapped_callback(btn=button): - self._on_option_selected(btn.option) - if og_callback: - og_callback() - - button.set_click_callback(wrapped_callback) + button.set_click_callback(click_callback) self._scroller.add_widget(button) def show_event(self): From 2e8586fab5f328e34f902aa349f57699b3f4d8ed Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Sat, 13 Dec 2025 04:11:36 -0800 Subject: [PATCH 08/39] WifiUi: remove delayed network panel open (#36865) not used --- selfdrive/ui/mici/layouts/settings/network/wifi_ui.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py index 5a79b8a639..3129cd791b 100644 --- a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py +++ b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py @@ -330,7 +330,6 @@ class WifiUIMici(BigMultiOptionDialog): self._network_info_page = NetworkInfoPage(wifi_manager, self._connect_to_network, self._forget_network, self._open_network_manage_page) self._network_info_page.set_connecting(lambda: self._connecting) - self._should_open_network_info_page = False # wait for scroll_to animation self._loading_animation = LoadingAnimation() @@ -355,12 +354,6 @@ class WifiUIMici(BigMultiOptionDialog): super().hide_event() self._wifi_manager.set_active(False) - def _update_state(self): - super()._update_state() - if self._should_open_network_info_page: - self._should_open_network_info_page = False - self._open_network_manage_page() - def _open_network_manage_page(self, result=None): self._network_info_page.update_networks(self._networks) gui_app.set_modal_overlay(self._network_info_page) @@ -406,7 +399,7 @@ class WifiUIMici(BigMultiOptionDialog): # only open if button is already selected if option in self._networks and option == self._selected_option: self._network_info_page.set_current_network(self._networks[option]) - self._should_open_network_info_page = True + self._open_network_manage_page() def _connect_to_network(self, ssid: str): network = self._networks.get(ssid) From 65008d281fef2e279ab5491219df8bace87894ed Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Sat, 13 Dec 2025 04:33:27 -0800 Subject: [PATCH 09/39] comma four: fix WiFi scroll to (#36864) * fix selection * stash * Revert "stash" This reverts commit d04ed66b090641072c86b8ed7ed86dbdbf67fbd9. * clean up * clean up * move * fix --- .../mici/layouts/settings/network/wifi_ui.py | 6 ----- selfdrive/ui/mici/widgets/dialog.py | 26 ++++++++++++++----- system/ui/widgets/scroller.py | 2 +- 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py index 3129cd791b..ba9da7aaac 100644 --- a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py +++ b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py @@ -83,8 +83,6 @@ class WifiIcon(Widget): class WifiItem(BigDialogOptionButton): LEFT_MARGIN = 20 - HEIGHT = 54 - SELECTED_HEIGHT = 74 def __init__(self, network: Network): super().__init__(network.ssid) @@ -97,10 +95,6 @@ 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) diff --git a/selfdrive/ui/mici/widgets/dialog.py b/selfdrive/ui/mici/widgets/dialog.py index 2118b62ed6..b5374791e8 100644 --- a/selfdrive/ui/mici/widgets/dialog.py +++ b/selfdrive/ui/mici/widgets/dialog.py @@ -274,10 +274,13 @@ class BigInputDialog(BigDialogBase): class BigDialogOptionButton(Widget): + HEIGHT = 54 + SELECTED_HEIGHT = 74 + def __init__(self, option: str): super().__init__() self.option = option - self.set_rect(rl.Rectangle(0, 0, int(gui_app.width / 2 + 220), 64)) + self.set_rect(rl.Rectangle(0, 0, int(gui_app.width / 2 + 220), self.HEIGHT)) self._selected = False @@ -291,6 +294,7 @@ class BigDialogOptionButton(Widget): def set_selected(self, selected: bool): self._selected = selected + self._rect.height = self.SELECTED_HEIGHT if selected else self.HEIGHT def _render(self, _): if DEBUG: @@ -302,7 +306,7 @@ class BigDialogOptionButton(Widget): 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) @@ -323,7 +327,7 @@ class BigMultiOptionDialog(BigDialogBase): self._selected_option: str = self._default_option self._last_selected_option: str = self._selected_option - self._scroller = Scroller([], horizontal=False, pad_start=100, pad_end=100, spacing=0) + self._scroller = Scroller([], horizontal=False, pad_start=100, pad_end=100, spacing=0, snap_items=True) if self._right_btn is not None: self._scroller.set_enabled(lambda: not cast(Widget, self._right_btn).is_pressed) @@ -348,10 +352,20 @@ class BigMultiOptionDialog(BigDialogBase): def _on_option_selected(self, option: str): y_pos = 0.0 for btn in self._scroller._items: - if cast(BigDialogOptionButton, btn).option == option: - y_pos = btn.rect.y + btn = cast(BigDialogOptionButton, btn) + if btn.option == option: + rect_center_y = self._rect.y + self._rect.height / 2 + if btn._selected: + height = btn.rect.height + else: + # when selecting an option under current, account for changing heights + btn_center_y = btn.rect.y + btn.rect.height / 2 # not accurate, just to determine direction + height_offset = BigDialogOptionButton.SELECTED_HEIGHT - BigDialogOptionButton.HEIGHT + height = (BigDialogOptionButton.HEIGHT - height_offset) if rect_center_y < btn_center_y else BigDialogOptionButton.SELECTED_HEIGHT + y_pos = rect_center_y - (btn.rect.y + height / 2) + break - self._scroller.scroll_to(y_pos, smooth=True) + self._scroller.scroll_to(-y_pos, smooth=True) def _selected_option_changed(self): pass diff --git a/system/ui/widgets/scroller.py b/system/ui/widgets/scroller.py index 4858569d2f..2074de00b0 100644 --- a/system/ui/widgets/scroller.py +++ b/system/ui/widgets/scroller.py @@ -74,7 +74,7 @@ class Scroller(Widget): return # FIXME: the padding correction doesn't seem correct - scroll_offset = self.scroll_panel.get_offset() - pos + self._pad_end + scroll_offset = self.scroll_panel.get_offset() - pos if smooth: self._scrolling_to = scroll_offset else: From 1504e103808ae9a594b4f4bd1b9b3319fbf02838 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Sat, 13 Dec 2025 05:14:21 -0800 Subject: [PATCH 10/39] WifiUi: pause updates while user is scrolling (#36866) * pause updates while user is scrolling * clean up --- selfdrive/ui/mici/layouts/settings/network/wifi_ui.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py index ba9da7aaac..9f0c134406 100644 --- a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py +++ b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py @@ -9,6 +9,7 @@ from openpilot.selfdrive.ui.mici.widgets.dialog import BigMultiOptionDialog, Big 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 +from openpilot.system.ui.lib.scroll_panel2 import ScrollState def normalize_ssid(ssid: str) -> str: @@ -366,6 +367,11 @@ class WifiUIMici(BigMultiOptionDialog): self._network_info_page.update_networks(self._networks) def _update_buttons(self): + # Don't update buttons while user is actively scrolling + scroll_state = self._scroller.scroll_panel.state + if scroll_state != ScrollState.STEADY: + return + for network in self._networks.values(): # pop and re-insert to eliminate stuttering on update (prevents position lost for a frame) network_button_idx = next((i for i, btn in enumerate(self._scroller._items) if btn.option == network.ssid), None) From 1c135f7ff2c2691cd49749f7c7af82c2baa70423 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Sat, 13 Dec 2025 05:28:53 -0800 Subject: [PATCH 11/39] WifiUi: pause updates while user is interacting (#36868) int not scroll --- .../ui/mici/layouts/settings/network/wifi_ui.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py index 9f0c134406..7137169784 100644 --- a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py +++ b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py @@ -9,7 +9,6 @@ from openpilot.selfdrive.ui.mici.widgets.dialog import BigMultiOptionDialog, Big 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 -from openpilot.system.ui.lib.scroll_panel2 import ScrollState def normalize_ssid(ssid: str) -> str: @@ -317,6 +316,9 @@ class NetworkInfoPage(NavWidget): class WifiUIMici(BigMultiOptionDialog): + # Wait this long after user interacts with widget to update network list + INACTIVITY_TIMEOUT = 1 + def __init__(self, wifi_manager: WifiManager, back_callback: Callable): super().__init__([], None, None, right_btn_callback=None) @@ -332,6 +334,8 @@ class WifiUIMici(BigMultiOptionDialog): self._connecting: str | None = None self._networks: dict[str, Network] = {} + self._last_interaction_time = rl.get_time() + self._wifi_manager.add_callbacks( need_auth=self._on_need_auth, activated=self._on_activated, @@ -367,9 +371,8 @@ class WifiUIMici(BigMultiOptionDialog): self._network_info_page.update_networks(self._networks) def _update_buttons(self): - # Don't update buttons while user is actively scrolling - scroll_state = self._scroller.scroll_panel.state - if scroll_state != ScrollState.STEADY: + # Don't update buttons while user is actively interacting + if rl.get_time() - self._last_interaction_time < self.INACTIVITY_TIMEOUT: return for network in self._networks.values(): @@ -434,6 +437,11 @@ class WifiUIMici(BigMultiOptionDialog): def _on_disconnected(self): self._connecting = None + def _update_state(self): + super()._update_state() + if self.is_pressed: + self._last_interaction_time = rl.get_time() + def _render(self, _): super()._render(_) From 716ad288bb2d5e40d0fa9fd6ec8c81bf911ec433 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Sat, 13 Dec 2025 05:45:42 -0800 Subject: [PATCH 12/39] Widget: implement layout function (#36869) * we can implement layout to fix flashing * reorder * fix faster than normal snap and reduce duplicate calculations * yes --- system/ui/widgets/__init__.py | 10 +++++-- system/ui/widgets/scroller.py | 55 ++++++++++++++++++++--------------- 2 files changed, 38 insertions(+), 27 deletions(-) diff --git a/system/ui/widgets/__init__.py b/system/ui/widgets/__init__.py index 097ac74c7e..a3fed6d962 100644 --- a/system/ui/widgets/__init__.py +++ b/system/ui/widgets/__init__.py @@ -100,6 +100,7 @@ class Widget(abc.ABC): if not self.is_visible: return None + self._layout() ret = self._render(self._rect) # Keep track of whether mouse down started within the widget's rectangle @@ -151,13 +152,16 @@ class Widget(abc.ABC): 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.""" + def _layout(self) -> None: + """Optionally lay out child widgets separately. This is called before rendering.""" def _update_state(self): """Optionally update the widget's non-layout state. This is called before rendering.""" + @abc.abstractmethod + def _render(self, rect: rl.Rectangle) -> bool | int | None: + """Render the widget within the given rectangle.""" + def _update_layout_rects(self) -> None: """Optionally update any layout rects on Widget rect change.""" diff --git a/system/ui/widgets/scroller.py b/system/ui/widgets/scroller.py index 2074de00b0..f33ba941bf 100644 --- a/system/ui/widgets/scroller.py +++ b/system/ui/widgets/scroller.py @@ -52,6 +52,11 @@ class Scroller(Widget): self._zoom_filter = FirstOrderFilter(1.0, 0.2, 1 / gui_app.target_fps) self._zoom_out_t: float = 0.0 + # layout state + self._visible_items: list[Widget] = [] + self._content_size: float = 0.0 + self._scroll_offset: float = 0.0 + self._item_pos_filter = BounceFilter(0.0, 0.05, 1 / gui_app.target_fps) # when not pressed, snap to closest item to be center @@ -160,28 +165,28 @@ class Scroller(Widget): return self.scroll_panel.get_offset() - def _render(self, _): - visible_items = [item for item in self._items if item.is_visible] + def _layout(self): + self._visible_items = [item for item in self._items if item.is_visible] # Add line separator between items if self._line_separator is not None: - l = len(visible_items) - for i in range(1, len(visible_items)): - visible_items.insert(l - i, self._line_separator) + l = len(self._visible_items) + for i in range(1, len(self._visible_items)): + self._visible_items.insert(l - i, self._line_separator) - content_size = sum(item.rect.width if self._horizontal else item.rect.height for item in visible_items) - content_size += self._spacing * (len(visible_items) - 1) - content_size += self._pad_start + self._pad_end + self._content_size = sum(item.rect.width if self._horizontal else item.rect.height for item in self._visible_items) + self._content_size += self._spacing * (len(self._visible_items) - 1) + self._content_size += self._pad_start + self._pad_end - scroll_offset = self._get_scroll(visible_items, content_size) + self._scroll_offset = self._get_scroll(self._visible_items, self._content_size) rl.begin_scissor_mode(int(self._rect.x), int(self._rect.y), int(self._rect.width), int(self._rect.height)) - self._item_pos_filter.update(scroll_offset) + self._item_pos_filter.update(self._scroll_offset) cur_pos = 0 - for idx, item in enumerate(visible_items): + for idx, item in enumerate(self._visible_items): spacing = self._spacing if (idx > 0) else self._pad_start # Nicely lay out items horizontally/vertically if self._horizontal: @@ -195,29 +200,31 @@ class Scroller(Widget): # Consider scroll if self._horizontal: - x += scroll_offset + x += self._scroll_offset else: - y += scroll_offset + y += self._scroll_offset # Add some jello effect when scrolling if DO_JELLO: if self._horizontal: cx = self._rect.x + self._rect.width / 2 - jello_offset = scroll_offset - np.interp(x + item.rect.width / 2, - [self._rect.x, cx, self._rect.x + self._rect.width], - [self._item_pos_filter.x, scroll_offset, self._item_pos_filter.x]) + jello_offset = self._scroll_offset - np.interp(x + item.rect.width / 2, + [self._rect.x, cx, self._rect.x + self._rect.width], + [self._item_pos_filter.x, self._scroll_offset, self._item_pos_filter.x]) x -= np.clip(jello_offset, -20, 20) else: cy = self._rect.y + self._rect.height / 2 - jello_offset = scroll_offset - np.interp(y + item.rect.height / 2, - [self._rect.y, cy, self._rect.y + self._rect.height], - [self._item_pos_filter.x, scroll_offset, self._item_pos_filter.x]) + jello_offset = self._scroll_offset - np.interp(y + item.rect.height / 2, + [self._rect.y, cy, self._rect.y + self._rect.height], + [self._item_pos_filter.x, self._scroll_offset, self._item_pos_filter.x]) y -= np.clip(jello_offset, -20, 20) # Update item state item.set_position(round(x), round(y)) # round to prevent jumping when settling item.set_parent_rect(self._rect) + def _render(self, _): + for item in self._visible_items: # Skip rendering if not in viewport if not rl.check_collision_recs(item.rect, self._rect): continue @@ -227,17 +234,17 @@ class Scroller(Widget): 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) + rl.rl_translatef((1 - scale) * (item.rect.x + item.rect.width / 2) / scale, + (1 - scale) * (item.rect.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: - _real_content_size = content_size - self._rect.height + self._txt_scroll_indicator.height - scroll_bar_y = -scroll_offset / _real_content_size * self._rect.height + if SCROLL_BAR and not self._horizontal and len(self._visible_items) > 0: + _real_content_size = self._content_size - self._rect.height + self._txt_scroll_indicator.height + scroll_bar_y = -self._scroll_offset / _real_content_size * self._rect.height scroll_bar_y = min(max(scroll_bar_y, self._rect.y), self._rect.y + self._rect.height - self._txt_scroll_indicator.height) rl.draw_texture_ex(self._txt_scroll_indicator, rl.Vector2(self._rect.x, scroll_bar_y), 0, 1.0, rl.WHITE) From 7cabab69a176b01e0bdd8898d8ea94083b273c94 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Sat, 13 Dec 2025 06:15:31 -0800 Subject: [PATCH 13/39] comma four: follow current network (#36862) * stay * whoops * whoops * fix * fix div by z * we can implement layout to fix flashing * Revert "we can implement layout to fix flashing" This reverts commit 7278a1e2a6117aec775ef4fabee2fd68b3d064f3. * random * clean up * wtf * rev * smooth * we can implement layout to fix flashing * snap looks so much better * fix * rev * better name * cmt * less random * even less random * simpler * cmt * clean up * clean up * clean up --- .../ui/mici/layouts/settings/network/wifi_ui.py | 16 ++++++++++++++-- selfdrive/ui/mici/widgets/dialog.py | 4 ++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py index 7137169784..ed5454d8e0 100644 --- a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py +++ b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py @@ -334,7 +334,9 @@ class WifiUIMici(BigMultiOptionDialog): self._connecting: str | None = None self._networks: dict[str, Network] = {} + # widget state self._last_interaction_time = rl.get_time() + self._restore_selection = False self._wifi_manager.add_callbacks( need_auth=self._on_need_auth, @@ -390,14 +392,17 @@ class WifiUIMici(BigMultiOptionDialog): # remove networks no longer present self._scroller._items[:] = [btn for btn in self._scroller._items if btn.option in self._networks] + # try to restore previous selection to prevent jumping from adding/removing/reordering buttons + self._restore_selection = True + def _connect_with_password(self, ssid: str, password: str): if password: self._connecting = ssid self._wifi_manager.connect_to_network(ssid, password) self._update_buttons() - def _on_option_selected(self, option: str): - super()._on_option_selected(option) + def _on_option_selected(self, option: str, smooth_scroll: bool = True): + super()._on_option_selected(option, smooth_scroll) # only open if button is already selected if option in self._networks and option == self._selected_option: @@ -443,6 +448,13 @@ class WifiUIMici(BigMultiOptionDialog): self._last_interaction_time = rl.get_time() def _render(self, _): + # Update Scroller layout and restore current selection whenever buttons are updated, before first render + current_selection = self.get_selected_option() + if self._restore_selection and current_selection in self._networks: + self._scroller._layout() + BigMultiOptionDialog._on_option_selected(self, current_selection, smooth_scroll=False) + self._restore_selection = None + super()._render(_) if not self._networks: diff --git a/selfdrive/ui/mici/widgets/dialog.py b/selfdrive/ui/mici/widgets/dialog.py index b5374791e8..4021a11c23 100644 --- a/selfdrive/ui/mici/widgets/dialog.py +++ b/selfdrive/ui/mici/widgets/dialog.py @@ -349,7 +349,7 @@ class BigMultiOptionDialog(BigDialogBase): def get_selected_option(self) -> str: return self._selected_option - def _on_option_selected(self, option: str): + def _on_option_selected(self, option: str, smooth_scroll: bool = True): y_pos = 0.0 for btn in self._scroller._items: btn = cast(BigDialogOptionButton, btn) @@ -365,7 +365,7 @@ class BigMultiOptionDialog(BigDialogBase): y_pos = rect_center_y - (btn.rect.y + height / 2) break - self._scroller.scroll_to(-y_pos, smooth=True) + self._scroller.scroll_to(-y_pos, smooth=smooth_scroll) def _selected_option_changed(self): pass From f287d487e596d85deb1c528e4dc2d96e7e339525 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Sat, 13 Dec 2025 06:16:31 -0800 Subject: [PATCH 14/39] GuiScrollPanel2: fix possible crash (#36870) fix crash --- system/ui/lib/scroll_panel2.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/system/ui/lib/scroll_panel2.py b/system/ui/lib/scroll_panel2.py index 00ef95cc8b..0859071dac 100644 --- a/system/ui/lib/scroll_panel2.py +++ b/system/ui/lib/scroll_panel2.py @@ -175,7 +175,8 @@ class GuiScrollPanel2: # Do not update velocity on the same frame the mouse was released previous_mouse_pos = self._get_mouse_pos(cast(MouseEvent, self._previous_mouse_event)) delta_x = mouse_pos - previous_mouse_pos - self._velocity = delta_x / (mouse_event.t - cast(MouseEvent, self._previous_mouse_event).t) + delta_t = max((mouse_event.t - cast(MouseEvent, self._previous_mouse_event).t), 1e-6) + self._velocity = delta_x / delta_t self._velocity = max(-MAX_SPEED, min(MAX_SPEED, self._velocity)) self._velocity_buffer.append(self._velocity) From a3c638697fc6e7ec4cb9e2dc83af73d5c534b8ef Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Sat, 13 Dec 2025 06:26:27 -0800 Subject: [PATCH 15/39] WifiUi: tweak unselected button size (#36871) looks too spaces --- selfdrive/ui/mici/layouts/settings/network/wifi_ui.py | 4 ++-- selfdrive/ui/mici/widgets/dialog.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py index ed5454d8e0..793bdcf4a0 100644 --- a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py +++ b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py @@ -114,11 +114,11 @@ class WifiItem(BigDialogOptionButton): )) if self._selected: - self._label.set_font_size(74) + self._label.set_font_size(self.SELECTED_HEIGHT) 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(54) + self._label.set_font_size(self.HEIGHT) self._label.set_color(rl.Color(255, 255, 255, int(255 * 0.58))) self._label.set_font_weight(FontWeight.DISPLAY_REGULAR) diff --git a/selfdrive/ui/mici/widgets/dialog.py b/selfdrive/ui/mici/widgets/dialog.py index 4021a11c23..3d9aa3f9e2 100644 --- a/selfdrive/ui/mici/widgets/dialog.py +++ b/selfdrive/ui/mici/widgets/dialog.py @@ -274,7 +274,7 @@ class BigInputDialog(BigDialogBase): class BigDialogOptionButton(Widget): - HEIGHT = 54 + HEIGHT = 64 SELECTED_HEIGHT = 74 def __init__(self, option: str): @@ -302,11 +302,11 @@ class BigDialogOptionButton(Widget): # FIXME: offset x by -45 because scroller centers horizontally if self._selected: - self._label.set_font_size(74) + self._label.set_font_size(self.SELECTED_HEIGHT) 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(54) + self._label.set_font_size(self.HEIGHT) self._label.set_color(rl.Color(255, 255, 255, int(255 * 0.58))) self._label.set_font_weight(FontWeight.DISPLAY_REGULAR) From e2fd6f34e95c948049b623d27919dcf61f0096bd Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sat, 13 Dec 2025 12:56:32 -0800 Subject: [PATCH 16/39] rm dead unlog_ci_segment.py --- tools/replay/unlog_ci_segment.py | 108 ------------------------------- 1 file changed, 108 deletions(-) delete mode 100755 tools/replay/unlog_ci_segment.py diff --git a/tools/replay/unlog_ci_segment.py b/tools/replay/unlog_ci_segment.py deleted file mode 100755 index e5a7a3ffde..0000000000 --- a/tools/replay/unlog_ci_segment.py +++ /dev/null @@ -1,108 +0,0 @@ -#!/usr/bin/env python3 - -import argparse -import bisect -import select -import sys -import termios -import time -import tty -from collections import defaultdict - -import cereal.messaging as messaging -from openpilot.tools.lib.framereader import FrameReader -from openpilot.tools.lib.logreader import LogReader -from openpilot.tools.lib.openpilotci import get_url - -IGNORE = ['initData', 'sentinel'] - - -def input_ready(): - return select.select([sys.stdin], [], [], 0) == ([sys.stdin], [], []) - - -def replay(route, segment, loop): - route = route.replace('|', '/') - - lr = LogReader(get_url(route, segment, "rlog.bz2")) - fr = FrameReader(get_url(route, segment, "fcamera.hevc"), readahead=True) - - # Build mapping from frameId to segmentId from roadEncodeIdx, type == fullHEVC - msgs = [m for m in lr if m.which() not in IGNORE] - msgs = sorted(msgs, key=lambda m: m.logMonoTime) - times = [m.logMonoTime for m in msgs] - frame_idx = {m.roadEncodeIdx.frameId: m.roadEncodeIdx.segmentId for m in msgs if m.which() == 'roadEncodeIdx' and m.roadEncodeIdx.type == 'fullHEVC'} - - socks = {} - lag = 0.0 - i = 0 - max_i = len(msgs) - 2 - - while True: - msg = msgs[i].as_builder() - next_msg = msgs[i + 1] - - start_time = time.monotonic() - w = msg.which() - - if w == 'roadCameraState': - try: - img = fr.get(frame_idx[msg.roadCameraState.frameId]) - img = img[:, ::-1] # Convert RGB to BGR, which is what the camera outputs - msg.roadCameraState.image = img.flatten().tobytes() - except (KeyError, ValueError): - pass - - if w not in socks: - socks[w] = messaging.pub_sock(w) - - try: - if socks[w]: - socks[w].send(msg.to_bytes()) - except messaging.messaging_pyx.MultiplePublishersError: - socks[w] = None - - lag += (next_msg.logMonoTime - msg.logMonoTime) / 1e9 - lag -= time.monotonic() - start_time - - dt = max(lag, 0.0) - lag -= dt - time.sleep(dt) - - if lag < -1.0 and i % 1000 == 0: - print(f"{-lag:.2f} s behind") - - if input_ready(): - key = sys.stdin.read(1) - - # Handle pause - if key == " ": - while True: - if input_ready() and sys.stdin.read(1) == " ": - break - time.sleep(0.01) - - # Handle seek - dt = defaultdict(int, s=10, S=-10)[key] - new_time = msgs[i].logMonoTime + dt * 1e9 - i = bisect.bisect_left(times, new_time) - - i = (i + 1) % max_i if loop else min(i + 1, max_i) - - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument("--loop", action='store_true') - parser.add_argument("route") - parser.add_argument("segment") - args = parser.parse_args() - - orig_settings = termios.tcgetattr(sys.stdin) - tty.setcbreak(sys.stdin) - - try: - replay(args.route, args.segment, args.loop) - termios.tcsetattr(sys.stdin, termios.TCSADRAIN, orig_settings) - except Exception: - termios.tcsetattr(sys.stdin, termios.TCSADRAIN, orig_settings) - raise From 9d9e5aa02db229c56538301bcac69bc8fc988de8 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Mon, 15 Dec 2025 15:36:28 -0800 Subject: [PATCH 17/39] joystickd: add cruise control resume (#36876) * Add cruise control resume logic based on conditions * simple --- tools/joystick/joystickd.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/joystick/joystickd.py b/tools/joystick/joystickd.py index 673a5bc1d0..789dad5623 100755 --- a/tools/joystick/joystickd.py +++ b/tools/joystick/joystickd.py @@ -48,6 +48,7 @@ def joystickd_thread(): if CC.longActive: actuators.accel = 4.0 * float(np.clip(joystick_axes[0], -1, 1)) actuators.longControlState = LongCtrlState.pid if sm['carState'].vEgo > CP.vEgoStopping else LongCtrlState.stopping + CC.cruiseControl.resume = actuators.accel > 0.0 if CC.latActive: max_curvature = MAX_LAT_ACCEL / max(sm['carState'].vEgo ** 2, 5) From 9e4c2bcacf1a77200c349ddeaa116ece1941d2ac Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Mon, 15 Dec 2025 16:41:16 -0800 Subject: [PATCH 18/39] bump opendbc (#36878) * bump * update docs * bump * gotta go fast --- docs/CARS.md | 237 +++++++++--------- opendbc_repo | 2 +- .../test/process_replay/test_processes.py | 3 +- 3 files changed, 121 insertions(+), 121 deletions(-) diff --git a/docs/CARS.md b/docs/CARS.md index fcb44154bc..e0d61cd415 100644 --- a/docs/CARS.md +++ b/docs/CARS.md @@ -14,12 +14,12 @@ A supported vehicle is one that just works when you install a comma device. All |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
||| +|Audi|A3 2014-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#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,14](#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,14](#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,14](#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,14](#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,14](#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
||| @@ -31,7 +31,7 @@ A supported vehicle is one that just works when you install a comma device. All |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,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
||| +|CUPRA|Ateca 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#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
||| @@ -45,8 +45,8 @@ A supported vehicle is one that just works when you install a comma device. All |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|Focus 2018[2](#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[2](#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
||| @@ -79,7 +79,7 @@ A supported vehicle is one that just works when you install a comma device. All |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 2019-21|All|openpilot available[1](#footnotes)|0 mph|2 mph[4](#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
||| @@ -192,165 +192,164 @@ A supported vehicle is one that just works when you install a comma device. All |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|CT Hybrid 2017-18|Lexus Safety System+|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
||| +|Lexus|ES 2017-18|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|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 2017-18|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|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 2018-19|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
||| |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 2018-19|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
||| |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 2016|Lexus Safety System+|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
||| +|Lexus|RX 2017-19|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
||| |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 2016|Lexus Safety System+|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
||| +|Lexus|RX Hybrid 2017-19|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
||| |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
||| +|MAN|eTGE 2020-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#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,14](#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
||| +|Nissan[5](#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[5](#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[5](#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[5](#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
||| +|SEAT|Ateca 2016-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#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,14](#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[6](#footnotes)|openpilot available[1,7](#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[6](#footnotes)|openpilot available[1,7](#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[6](#footnotes)|openpilot available[1,7](#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[6](#footnotes)|openpilot available[1,7](#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[6](#footnotes)|openpilot available[1,7](#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[6](#footnotes)|openpilot available[1,7](#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[6](#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[6](#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[6](#footnotes)|openpilot available[1,7](#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[6](#footnotes)|openpilot available[1,7](#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[13](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#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
[15](#footnotes)||| +|Å koda|Kamiq 2021-23[11,13](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#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
[15](#footnotes)||| +|Å koda|Karoq 2019-23[13](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#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[13](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#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[13](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#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[13](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#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[13](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#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[13](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#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
[15](#footnotes)||| +|Å koda|Superb 2015-22[13](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#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[9](#footnotes)|Model 3 (with HW3) 2019-23[8](#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[9](#footnotes)|Model 3 (with HW4) 2024-25[8](#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[9](#footnotes)|Model Y (with HW3) 2020-23[8](#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[9](#footnotes)|Model Y (with HW4) 2024-25[8](#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 2016|Toyota Safety Sense P|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
||| +|Toyota|Avalon 2017-18|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
||| +|Toyota|Avalon 2019-21|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|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 2019-21|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|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 2018-20|All|Stock|0 mph[10](#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[10](#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 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
||| |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 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
||| |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 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
||| |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 2016|Toyota Safety Sense P|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|Prius 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|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 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|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|Prius v 2017|Toyota Safety Sense P|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
||| +|Toyota|RAV4 2016|Toyota Safety Sense P|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
||| +|Toyota|RAV4 2017-18|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
||| |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 2016|Toyota Safety Sense P|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
||| +|Toyota|RAV4 Hybrid 2017-18|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
||| |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
||| +|Toyota|Sienna 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
||| +|Volkswagen|Arteon 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#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,14](#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,14](#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,14](#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,14](#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,14](#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,14](#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,14](#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,14](#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,14](#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,14](#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,14](#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,14](#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,14](#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,14](#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,14](#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,14](#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,14](#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,14](#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,14](#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,14](#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,14](#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[12](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#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,14](#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,14](#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,14](#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
[15](#footnotes)||| +|Volkswagen|Polo GTI 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#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
[15](#footnotes)||| +|Volkswagen|T-Cross 2021|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#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
[15](#footnotes)||| +|Volkswagen|T-Roc 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#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,14](#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,14](#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,14](#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,14](#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,14](#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,14](#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,14](#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`.
-2By default, this car will use the stock Adaptive Cruise Control (ACC) for longitudinal control. If the Driver Support Unit (DSU) is disconnected, openpilot ACC will replace stock ACC. NOTE: disconnecting the DSU disables Automatic Emergency Braking (AEB).
-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.
-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.
+2Refers only to the Focus Mk4 (C519) available in Europe/China/Taiwan/Australasia, not the Focus Mk3 (C346) in North and South America/Southeast Asia.
+3See more setup details for GM.
+42019 Honda Civic 1.6L Diesel Sedan does not have ALC below 12mph.
+5See more setup details for Nissan.
+6In the non-US market, openpilot requires the car to come equipped with EyeSight with Lane Keep Assistance.
+7Enabling longitudinal control (alpha) will disable all EyeSight functionality, including AEB, LDW, and RAB.
+8Some 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.
+9See more setup details for Tesla.
+10openpilot operates above 28mph for Camry 4CYL L, 4CYL LE and 4CYL SE which don't have Full-Speed Range Dynamic Radar Cruise Control.
+11Not including the China market Kamiq, which is based on the (currently) unsupported PQ34 platform.
+12Refers only to the MQB-based European B8 Passat, not the NMS Passat in the USA/China/Mideast markets.
+13Some Å koda vehicles are equipped with heated windshields, which are known to block GPS signal needed for some comma four functionality.
+14Only available for vehicles using a gateway (J533) harness. At this time, vehicles using a camera harness are limited to using stock ACC.
+15Model-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/). diff --git a/opendbc_repo b/opendbc_repo index 6171d1a976..4bd6ffea21 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit 6171d1a976b632c4804e90e74a78370532a2f297 +Subproject commit 4bd6ffea2174f3c0fa01f728d612c0c9498b0b05 diff --git a/selfdrive/test/process_replay/test_processes.py b/selfdrive/test/process_replay/test_processes.py index 5868ca4c1c..59e1ae054e 100755 --- a/selfdrive/test/process_replay/test_processes.py +++ b/selfdrive/test/process_replay/test_processes.py @@ -44,7 +44,8 @@ segments = [ ("HYUNDAI", "regenAA0FC4ED71E|2025-04-08--22-57-50--0"), ("HYUNDAI2", "regenAFB9780D823|2025-04-08--23-00-34--0"), ("TOYOTA", "regen218A4DCFAA1|2025-04-08--22-57-51--0"), - ("TOYOTA2", "regen107352E20EB|2025-04-08--22-57-46--0"), + # TODO: get new RAV4 route without enableDsu + # ("TOYOTA2", "regen107352E20EB|2025-04-08--22-57-46--0"), ("TOYOTA3", "regen1455E3B4BDF|2025-04-09--03-26-06--0"), ("HONDA", "regenB328FF8BA0A|2025-04-08--22-57-45--0"), ("HONDA2", "regen6170C8C9A35|2025-04-08--22-57-46--0"), From 752ef8696af9687b7ea3d0309ab8f73990d6b61d Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Mon, 15 Dec 2025 19:04:11 -0800 Subject: [PATCH 19/39] sensord: remove last of dual IMU support (#36881) --- cereal/log.capnp | 6 +++--- cereal/services.py | 3 --- system/sensord/tests/test_sensord.py | 3 +-- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/cereal/log.capnp b/cereal/log.capnp index 686771e284..3a6432c84d 100644 --- a/cereal/log.capnp +++ b/cereal/log.capnp @@ -2524,13 +2524,10 @@ struct Event { controlsState @7 :ControlsState; selfdriveState @130 :SelfdriveState; gyroscope @99 :SensorEventData; - gyroscope2 @100 :SensorEventData; accelerometer @98 :SensorEventData; - accelerometer2 @101 :SensorEventData; magnetometer @95 :SensorEventData; lightSensor @96 :SensorEventData; temperatureSensor @97 :SensorEventData; - temperatureSensor2 @123 :SensorEventData; pandaStates @81 :List(PandaState); peripheralState @80 :PeripheralState; radarState @13 :RadarState; @@ -2693,5 +2690,8 @@ struct Event { liveLocationKalmanDEPRECATED @72 :LiveLocationKalman; liveTracksDEPRECATED @16 :List(LiveTracksDEPRECATED); onroadEventsDEPRECATED @68: List(Car.OnroadEventDEPRECATED); + gyroscope2DEPRECATED @100 :SensorEventData; + accelerometer2DEPRECATED @101 :SensorEventData; + temperatureSensor2DEPRECATED @123 :SensorEventData; } } diff --git a/cereal/services.py b/cereal/services.py index edeca412ce..f7269b79ce 100755 --- a/cereal/services.py +++ b/cereal/services.py @@ -13,13 +13,10 @@ _services: dict[str, tuple] = { # service: (should_log, frequency, qlog decimation (optional)) # note: the "EncodeIdx" packets will still be in the log "gyroscope": (True, 104., 104), - "gyroscope2": (True, 100., 100), "accelerometer": (True, 104., 104), - "accelerometer2": (True, 100., 100), "magnetometer": (True, 25.), "lightSensor": (True, 100., 100), "temperatureSensor": (True, 2., 200), - "temperatureSensor2": (True, 2., 200), "gpsNMEA": (True, 9.), "deviceState": (True, 2., 1), "touch": (True, 20., 1), diff --git a/system/sensord/tests/test_sensord.py b/system/sensord/tests/test_sensord.py index 1dab652386..5e98e12243 100644 --- a/system/sensord/tests/test_sensord.py +++ b/system/sensord/tests/test_sensord.py @@ -56,8 +56,7 @@ def get_irq_count(irq: int): return sum(per_cpu) def read_sensor_events(duration_sec): - sensor_types = ['accelerometer', 'gyroscope', 'magnetometer', 'accelerometer2', - 'gyroscope2', 'temperatureSensor', 'temperatureSensor2'] + sensor_types = ['accelerometer', 'gyroscope', 'magnetometer', 'temperatureSensor',] socks = {} poller = messaging.Poller() events = defaultdict(list) From 507f4209273246b3c7bef7538a4dbdb72958d634 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Mon, 15 Dec 2025 19:19:41 -0800 Subject: [PATCH 20/39] Toyota: prevent roll in ICE after pressing resume while wanting to stay stopped (#36877) * bump * only show alert when user can leave standstill * cmt * stash * bump * bump to master --- opendbc_repo | 2 +- selfdrive/car/car_specific.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/opendbc_repo b/opendbc_repo index 4bd6ffea21..39773a987e 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit 4bd6ffea2174f3c0fa01f728d612c0c9498b0b05 +Subproject commit 39773a987eaefd680c6befa390d7898945daf2e7 diff --git a/selfdrive/car/car_specific.py b/selfdrive/car/car_specific.py index 9e166a44d7..6210983d97 100644 --- a/selfdrive/car/car_specific.py +++ b/selfdrive/car/car_specific.py @@ -90,7 +90,8 @@ class CarSpecificEvents: events = self.create_common_events(CS, CS_prev, extra_gears=extra_gears) if self.CP.openpilotLongitudinalControl: - if CS.cruiseState.standstill and not CS.brakePressed: + # Only can leave standstill when planner wants to move + if CS.cruiseState.standstill and not CS.brakePressed and CC.cruiseControl.resume: events.add(EventName.resumeRequired) if CS.vEgo < self.CP.minEnableSpeed: events.add(EventName.belowEngageSpeed) From 545f7c6f2a37fce738a1081cde89434a067d89a8 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Mon, 15 Dec 2025 22:00:39 -0800 Subject: [PATCH 21/39] test_onroad: absolute memory usage test (#36885) * test_onroad: absolute memory usage test * show msgq size * reduce a little * bump msgq * Revert "bump msgq" This reverts commit 683d0ae9fc754f7b72e2bc4b256e9a3b0a60a127. --- msgq_repo | 2 +- selfdrive/test/test_onroad.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/msgq_repo b/msgq_repo index a16cf1f608..92999f6bc1 160000 --- a/msgq_repo +++ b/msgq_repo @@ -1 +1 @@ -Subproject commit a16cf1f608538d14f66bd6142230d8728f2d0abc +Subproject commit 92999f6bc19c16170ff984473b43c799162faca1 diff --git a/selfdrive/test/test_onroad.py b/selfdrive/test/test_onroad.py index b9506d0806..972f09be30 100644 --- a/selfdrive/test/test_onroad.py +++ b/selfdrive/test/test_onroad.py @@ -121,6 +121,7 @@ class TestOnroad: params.put_bool("RecordFront", True) set_params_enabled() os.environ['REPLAY'] = '1' + os.environ['MSGQ_PREALLOC'] = '1' os.environ['TESTING_CLOSET'] = '1' if os.path.exists(Paths.log_root()): shutil.rmtree(Paths.log_root()) @@ -283,11 +284,12 @@ class TestOnroad: print("------------------------------------------------") offset = int(SERVICE_LIST['deviceState'].frequency * LOG_OFFSET) mems = [m.deviceState.memoryUsagePercent for m in self.msgs['deviceState'][offset:]] - print("Memory usage: ", mems) + print("Overall memory usage: ", mems) + print("MSGQ (/dev/shm/) usage: ", subprocess.check_output(["du", "-hs", "/dev/shm"]).split()[0].decode()) # check for big leaks. note that memory usage is # expected to go up while the MSGQ buffers fill up - assert np.average(mems) <= 85, "Average memory usage above 85%" + assert np.average(mems) <= 82, "Average memory usage above 85%" assert np.max(np.diff(mems)) <= 4, "Max memory increase too high" assert np.average(np.diff(mems)) <= 1, "Average memory increase too high" From bcdeec3133643b4a6e2f16071cdcb130f2637c70 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Tue, 16 Dec 2025 13:27:14 -0800 Subject: [PATCH 22/39] Reduce pub-sub memory usage by 10x (#36884) less mem --- cereal/messaging/__init__.py | 16 +++++- cereal/messaging/socketmaster.cc | 5 +- cereal/services.py | 42 ++++++++------ msgq_repo | 2 +- selfdrive/debug/analyze-msg-size.py | 82 ++++++++++++++++++++++++++++ selfdrive/pandad/pandad.cc | 3 +- selfdrive/test/test_onroad.py | 2 +- system/loggerd/loggerd.cc | 2 +- system/loggerd/tests/test_loggerd.py | 15 +++-- tools/cabana/streams/devicestream.cc | 4 +- 10 files changed, 144 insertions(+), 29 deletions(-) create mode 100755 selfdrive/debug/analyze-msg-size.py diff --git a/cereal/messaging/__init__.py b/cereal/messaging/__init__.py index b03285f80a..0ad846f0f4 100644 --- a/cereal/messaging/__init__.py +++ b/cereal/messaging/__init__.py @@ -2,7 +2,7 @@ from msgq.ipc_pyx import Context, Poller, SubSocket, PubSocket, SocketEventHandle, toggle_fake_events, \ set_fake_prefix, get_fake_prefix, delete_fake_prefix, wait_for_one_event from msgq.ipc_pyx import MultiplePublishersError, IpcError -from msgq import fake_event_handle, pub_sock, sub_sock, drain_sock_raw +from msgq import fake_event_handle, drain_sock_raw import msgq import os @@ -18,6 +18,20 @@ from openpilot.common.util import MovingAverage NO_TRAVERSAL_LIMIT = 2**64-1 +def pub_sock(endpoint: str) -> PubSocket: + service = SERVICE_LIST.get(endpoint) + segment_size = service.queue_size if service else 0 + return msgq.pub_sock(endpoint, segment_size) + + +def sub_sock(endpoint: str, poller: Optional[Poller] = None, addr: str = "127.0.0.1", + conflate: bool = False, timeout: Optional[int] = None) -> SubSocket: + service = SERVICE_LIST.get(endpoint) + segment_size = service.queue_size if service else 0 + return msgq.sub_sock(endpoint, poller=poller, addr=addr, conflate=conflate, + timeout=timeout, segment_size=segment_size) + + def reset_context(): msgq.context = Context() diff --git a/cereal/messaging/socketmaster.cc b/cereal/messaging/socketmaster.cc index 7f7e2795c4..dfeeb807ee 100644 --- a/cereal/messaging/socketmaster.cc +++ b/cereal/messaging/socketmaster.cc @@ -50,7 +50,7 @@ SubMaster::SubMaster(const std::vector &service_list, const std::v assert(services.count(std::string(name)) > 0); service serv = services.at(std::string(name)); - SubSocket *socket = SubSocket::create(message_context.context(), name, address ? address : "127.0.0.1", true); + SubSocket *socket = SubSocket::create(message_context.context(), name, address ? address : "127.0.0.1", true, true, serv.queue_size); assert(socket != 0); bool is_polled = inList(poll, name) || poll.empty(); if (is_polled) poller_->registerSocket(socket); @@ -187,7 +187,8 @@ SubMaster::~SubMaster() { PubMaster::PubMaster(const std::vector &service_list) { for (auto name : service_list) { assert(services.count(name) > 0); - PubSocket *socket = PubSocket::create(message_context.context(), name); + service serv = services.at(std::string(name)); + PubSocket *socket = PubSocket::create(message_context.context(), name, true, serv.queue_size); assert(socket); sockets_[name] = socket; } diff --git a/cereal/services.py b/cereal/services.py index f7269b79ce..e7350aceac 100755 --- a/cereal/services.py +++ b/cereal/services.py @@ -1,12 +1,22 @@ #!/usr/bin/env python3 +from enum import IntEnum from typing import Optional +# TODO: this should be automatically determined using the capnp schema +class QueueSize(IntEnum): + BIG = 10 * 1024 * 1024 # 10MB - video frames, large AI outputs + MEDIUM = 2 * 1024 * 1024 # 2MB - high freq (CAN), livestream + SMALL = 250 * 1024 # 250KB - most services + + class Service: - def __init__(self, should_log: bool, frequency: float, decimation: Optional[int] = None): + def __init__(self, should_log: bool, frequency: float, decimation: Optional[int] = None, + queue_size: QueueSize = QueueSize.SMALL): self.should_log = should_log self.frequency = frequency self.decimation = decimation + self.queue_size = queue_size _services: dict[str, tuple] = { @@ -20,15 +30,15 @@ _services: dict[str, tuple] = { "gpsNMEA": (True, 9.), "deviceState": (True, 2., 1), "touch": (True, 20., 1), - "can": (True, 100., 2053), # decimation gives ~3 msgs in a full segment - "controlsState": (True, 100., 10), + "can": (True, 100., 2053, QueueSize.BIG), # decimation gives ~3 msgs in a full segment + "controlsState": (True, 100., 10, QueueSize.MEDIUM), "selfdriveState": (True, 100., 10), "pandaStates": (True, 10., 1), "peripheralState": (True, 2., 1), "radarState": (True, 20., 5), "roadEncodeIdx": (False, 20., 1), "liveTracks": (True, 20.), - "sendcan": (True, 100., 139), + "sendcan": (True, 100., 139, QueueSize.MEDIUM), "logMessage": (True, 0.), "errorLogMessage": (True, 0., 1), "liveCalibration": (True, 4., 4), @@ -40,7 +50,7 @@ _services: dict[str, tuple] = { "carOutput": (True, 100., 10), "longitudinalPlan": (True, 20., 10), "driverAssistance": (True, 20., 20), - "procLog": (True, 0.5, 15), + "procLog": (True, 0.5, 15, QueueSize.BIG), "gpsLocationExternal": (True, 10., 10), "gpsLocation": (True, 1., 1), "ubloxGnss": (True, 10.), @@ -62,7 +72,7 @@ _services: dict[str, tuple] = { "wideRoadEncodeIdx": (False, 20., 1), "wideRoadCameraState": (True, 20., 20), "drivingModelData": (True, 20., 10), - "modelV2": (True, 20.), + "modelV2": (True, 20., None, QueueSize.BIG), "managerState": (True, 2., 1), "uploaderState": (True, 0., 1), "navInstruction": (True, 1., 10), @@ -74,21 +84,21 @@ _services: dict[str, tuple] = { "rawAudioData": (False, 20.), "bookmarkButton": (True, 0., 1), "audioFeedback": (True, 0., 1), + "roadEncodeData": (False, 20., None, QueueSize.BIG), + "driverEncodeData": (False, 20., None, QueueSize.BIG), + "wideRoadEncodeData": (False, 20., None, QueueSize.BIG), + "qRoadEncodeData": (False, 20., None, QueueSize.BIG), # debug "uiDebug": (True, 0., 1), "testJoystick": (True, 0.), "alertDebug": (True, 20., 5), - "roadEncodeData": (False, 20.), - "driverEncodeData": (False, 20.), - "wideRoadEncodeData": (False, 20.), - "qRoadEncodeData": (False, 20.), "livestreamWideRoadEncodeIdx": (False, 20.), "livestreamRoadEncodeIdx": (False, 20.), "livestreamDriverEncodeIdx": (False, 20.), - "livestreamWideRoadEncodeData": (False, 20.), - "livestreamRoadEncodeData": (False, 20.), - "livestreamDriverEncodeData": (False, 20.), + "livestreamWideRoadEncodeData": (False, 20., None, QueueSize.MEDIUM), + "livestreamRoadEncodeData": (False, 20., None, QueueSize.MEDIUM), + "livestreamDriverEncodeData": (False, 20., None, QueueSize.MEDIUM), "customReservedRawData0": (True, 0.), "customReservedRawData1": (True, 0.), "customReservedRawData2": (True, 0.), @@ -106,13 +116,13 @@ def build_header(): h += "#include \n" h += "#include \n" - h += "struct service { std::string name; bool should_log; float frequency; int decimation; };\n" + h += "struct service { std::string name; bool should_log; float frequency; int decimation; size_t queue_size; };\n" h += "static std::map services = {\n" for k, v in SERVICE_LIST.items(): should_log = "true" if v.should_log else "false" decimation = -1 if v.decimation is None else v.decimation - h += ' { "%s", {"%s", %s, %f, %d}},\n' % \ - (k, k, should_log, v.frequency, decimation) + h += ' { "%s", {"%s", %s, %f, %d, %d}},\n' % \ + (k, k, should_log, v.frequency, decimation, v.queue_size) h += "};\n" h += "#endif\n" diff --git a/msgq_repo b/msgq_repo index 92999f6bc1..345878d914 160000 --- a/msgq_repo +++ b/msgq_repo @@ -1 +1 @@ -Subproject commit 92999f6bc19c16170ff984473b43c799162faca1 +Subproject commit 345878d9141d0470d1a96f8fb5e59efb61dd5cf9 diff --git a/selfdrive/debug/analyze-msg-size.py b/selfdrive/debug/analyze-msg-size.py new file mode 100755 index 0000000000..69015a6be2 --- /dev/null +++ b/selfdrive/debug/analyze-msg-size.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +import argparse +from tqdm import tqdm + +from cereal.services import SERVICE_LIST, QueueSize +from openpilot.tools.lib.logreader import LogReader + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Analyze message sizes from a log route") + parser.add_argument("route", nargs="?", default="98395b7c5b27882e/000000a8--f87e7cd255", + help="Log route to analyze (default: 98395b7c5b27882e/000000a8--f87e7cd255)") + args = parser.parse_args() + + lr = LogReader(args.route) + + szs = {} + for msg in tqdm(lr): + sz = len(msg.as_builder().to_bytes()) + msg_type = msg.which() + if msg_type not in szs: + szs[msg_type] = {'min': sz, 'max': sz, 'sum': sz, 'count': 1} + else: + szs[msg_type]['min'] = min(szs[msg_type]['min'], sz) + szs[msg_type]['max'] = max(szs[msg_type]['max'], sz) + szs[msg_type]['sum'] += sz + szs[msg_type]['count'] += 1 + + print() + print(f"{'Service':<36} {'Min (KB)':>12} {'Max (KB)':>12} {'Avg (KB)':>12} {'KB/min':>12} {'KB/sec':>12} {'Minutes in 10MB':>18} {'Seconds in Queue':>18}") + print("-" * 132) + def sort_key(x): + k, v = x + avg = v['sum'] / v['count'] + freq = SERVICE_LIST.get(k, None) + freq_val = freq.frequency if freq else 0.0 + kb_per_min = (avg * freq_val * 60) / 1024 if freq_val > 0 else 0.0 + return kb_per_min + total_kb_per_min = 0.0 + RINGBUFFER_SIZE_KB = 10 * 1024 # 10MB old default + for k, v in sorted(szs.items(), key=sort_key, reverse=True): + avg = v['sum'] / v['count'] + service = SERVICE_LIST.get(k, None) + freq_val = service.frequency if service else 0.0 + queue_size_kb = (service.queue_size / 1024) if service else 250 # default to SMALL + kb_per_min = (avg * freq_val * 60) / 1024 if freq_val > 0 else 0.0 + kb_per_sec = kb_per_min / 60 + minutes_in_buffer = RINGBUFFER_SIZE_KB / kb_per_min if kb_per_min > 0 else float('inf') + seconds_in_queue = (queue_size_kb / kb_per_sec) if kb_per_sec > 0 else float('inf') + total_kb_per_min += kb_per_min + min_str = f"{minutes_in_buffer:.2f}" if minutes_in_buffer != float('inf') else "inf" + sec_queue_str = f"{seconds_in_queue:.2f}" if seconds_in_queue != float('inf') else "inf" + print(f"{k:<36} {v['min']/1024:>12.2f} {v['max']/1024:>12.2f} {avg/1024:>12.2f} {kb_per_min:>12.2f} {kb_per_sec:>12.2f} {min_str:>18} {sec_queue_str:>18}") + + # Summary section + print() + print(f"Total usage: {total_kb_per_min / 1024:.2f} MB/min") + + # Calculate memory usage: old (10MB for all) vs new (from services.py) + OLD_SIZE = 10 * 1024 * 1024 # 10MB was the old default + old_total = len(SERVICE_LIST) * OLD_SIZE + + new_total = sum(s.queue_size for s in SERVICE_LIST.values()) + + # Count by queue size + size_counts = {QueueSize.BIG: 0, QueueSize.MEDIUM: 0, QueueSize.SMALL: 0} + for s in SERVICE_LIST.values(): + size_counts[s.queue_size] += 1 + + savings_pct = (1 - new_total / old_total) * 100 + + print() + print(f"{'Queue Size Comparison':<40}") + print("-" * 60) + print(f"{'Old (10MB default):':<30} {old_total / 1024 / 1024:>10.2f} MB") + print(f"{'New (from services.py):':<30} {new_total / 1024 / 1024:>10.2f} MB") + print(f"{'Savings:':<30} {savings_pct:>10.1f}%") + print() + print(f"{'Breakdown:':<30}") + print(f" BIG (10MB): {size_counts[QueueSize.BIG]:>3} services") + print(f" MEDIUM (2MB): {size_counts[QueueSize.MEDIUM]:>3} services") + print(f" SMALL (250KB): {size_counts[QueueSize.SMALL]:>3} services") diff --git a/selfdrive/pandad/pandad.cc b/selfdrive/pandad/pandad.cc index a76cbc46e3..2fd4a4def2 100644 --- a/selfdrive/pandad/pandad.cc +++ b/selfdrive/pandad/pandad.cc @@ -11,6 +11,7 @@ #include "cereal/gen/cpp/car.capnp.h" #include "cereal/messaging/messaging.h" +#include "cereal/services.h" #include "common/ratekeeper.h" #include "common/swaglog.h" #include "common/timing.h" @@ -82,7 +83,7 @@ void can_send_thread(std::vector pandas, bool fake_send) { AlignedBuffer aligned_buf; std::unique_ptr context(Context::create()); - std::unique_ptr subscriber(SubSocket::create(context.get(), "sendcan")); + std::unique_ptr subscriber(SubSocket::create(context.get(), "sendcan", "127.0.0.1", false, true, services.at("sendcan").queue_size)); assert(subscriber != NULL); subscriber->setTimeout(100); diff --git a/selfdrive/test/test_onroad.py b/selfdrive/test/test_onroad.py index 972f09be30..ed3fca5fb0 100644 --- a/selfdrive/test/test_onroad.py +++ b/selfdrive/test/test_onroad.py @@ -289,7 +289,7 @@ class TestOnroad: # check for big leaks. note that memory usage is # expected to go up while the MSGQ buffers fill up - assert np.average(mems) <= 82, "Average memory usage above 85%" + assert np.average(mems) <= 65, "Average memory usage too high" assert np.max(np.diff(mems)) <= 4, "Max memory increase too high" assert np.average(np.diff(mems)) <= 1, "Average memory increase too high" diff --git a/system/loggerd/loggerd.cc b/system/loggerd/loggerd.cc index 21de1ff33f..47da321024 100644 --- a/system/loggerd/loggerd.cc +++ b/system/loggerd/loggerd.cc @@ -238,7 +238,7 @@ void loggerd_thread() { if (it.should_log || (encoder && !livestream_encoder) || record_audio) { LOGD("logging %s", it.name.c_str()); - SubSocket * sock = SubSocket::create(ctx.get(), it.name); + SubSocket * sock = SubSocket::create(ctx.get(), it.name, "127.0.0.1", false, true, it.queue_size); assert(sock != NULL); poller->registerSocket(sock); service_state[sock] = { diff --git a/system/loggerd/tests/test_loggerd.py b/system/loggerd/tests/test_loggerd.py index 1cac16adcd..9703ac2f5f 100644 --- a/system/loggerd/tests/test_loggerd.py +++ b/system/loggerd/tests/test_loggerd.py @@ -24,7 +24,6 @@ from openpilot.system.version import get_version from openpilot.tools.lib.helpers import RE from openpilot.tools.lib.logreader import LogReader from msgq.visionipc import VisionIpcServer, VisionStreamType -from openpilot.common.transformations.camera import DEVICE_CAMERAS SentinelType = log.Sentinel.SentinelType @@ -99,13 +98,17 @@ class TestLoggerd: return sent_msgs def _publish_camera_and_audio_messages(self, num_segs=1, segment_length=5): - d = DEVICE_CAMERAS[("tici", "ar0231")] + # Use small frame sizes for testing (width, height, size, stride, uv_offset) + # NV12 format: size = stride * height * 1.5, uv_offset = stride * height + w, h = 320, 240 + frame_spec = (w, h, w * h * 3 // 2, w, w * h) streams = [ - (VisionStreamType.VISION_STREAM_ROAD, (d.fcam.width, d.fcam.height, 2048 * 2346, 2048, 2048 * 1216), "roadCameraState"), - (VisionStreamType.VISION_STREAM_DRIVER, (d.dcam.width, d.dcam.height, 2048 * 2346, 2048, 2048 * 1216), "driverCameraState"), - (VisionStreamType.VISION_STREAM_WIDE_ROAD, (d.ecam.width, d.ecam.height, 2048 * 2346, 2048, 2048 * 1216), "wideRoadCameraState"), + (VisionStreamType.VISION_STREAM_ROAD, frame_spec, "roadCameraState"), + (VisionStreamType.VISION_STREAM_DRIVER, frame_spec, "driverCameraState"), + (VisionStreamType.VISION_STREAM_WIDE_ROAD, frame_spec, "wideRoadCameraState"), ] + sm = messaging.SubMaster(["roadEncodeData"]) pm = messaging.PubMaster([s for _, _, s in streams] + ["rawAudioData"]) vipc_server = VisionIpcServer("camerad") for stream_type, frame_spec, _ in streams: @@ -139,6 +142,8 @@ class TestLoggerd: for _, _, state in streams: assert pm.wait_for_readers_to_update(state, timeout=5, dt=0.001) + sm.update(100) # wait for encode data publish + managed_processes["loggerd"].stop() managed_processes["encoderd"].stop() diff --git a/tools/cabana/streams/devicestream.cc b/tools/cabana/streams/devicestream.cc index 6de63dfbbc..462dd7a361 100644 --- a/tools/cabana/streams/devicestream.cc +++ b/tools/cabana/streams/devicestream.cc @@ -3,6 +3,8 @@ #include #include +#include "cereal/services.h" + #include #include #include @@ -20,7 +22,7 @@ void DeviceStream::streamThread() { std::unique_ptr context(Context::create()); std::string address = zmq_address.isEmpty() ? "127.0.0.1" : zmq_address.toStdString(); - std::unique_ptr sock(SubSocket::create(context.get(), "can", address)); + std::unique_ptr sock(SubSocket::create(context.get(), "can", address, false, true, services.at("can").queue_size)); assert(sock != NULL); // run as fast as messages come in while (!QThread::currentThread()->isInterruptionRequested()) { From 4624d8f9367cc50bd8c594aef0860f052e1839da Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 16 Dec 2025 15:42:59 -0800 Subject: [PATCH 23/39] four: hide untoggleable toggles (#36890) * hide toggles * enabled is redundant --- selfdrive/ui/mici/layouts/settings/toggles.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/selfdrive/ui/mici/layouts/settings/toggles.py b/selfdrive/ui/mici/layouts/settings/toggles.py index 8efb516a42..c16504fac8 100644 --- a/selfdrive/ui/mici/layouts/settings/toggles.py +++ b/selfdrive/ui/mici/layouts/settings/toggles.py @@ -78,13 +78,13 @@ class TogglesLayoutMici(NavWidget): # CP gating for experimental mode if ui_state.CP is not None: if ui_state.has_longitudinal_control: - self._experimental_btn.set_enabled(True) - self._personality_toggle.set_enabled(True) + self._experimental_btn.set_visible(True) + self._personality_toggle.set_visible(True) else: # no long for now - self._experimental_btn.set_enabled(False) + self._experimental_btn.set_visible(False) self._experimental_btn.set_checked(False) - self._personality_toggle.set_enabled(False) + self._personality_toggle.set_visible(False) ui_state.params.remove("ExperimentalMode") # Refresh toggles from params to mirror external changes From 4fa4237e3f352dda99b215242d2dacbd44172704 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Tue, 16 Dec 2025 15:51:35 -0800 Subject: [PATCH 24/39] bump msgq (#36891) * bump msgq * update prefix --- common/prefix.py | 2 +- msgq_repo | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/common/prefix.py b/common/prefix.py index 207f8477d7..b19ce1472b 100644 --- a/common/prefix.py +++ b/common/prefix.py @@ -11,7 +11,7 @@ from openpilot.system.hardware.hw import DEFAULT_DOWNLOAD_CACHE_ROOT class OpenpilotPrefix: def __init__(self, prefix: str = None, create_dirs_on_enter: bool = True, clean_dirs_on_exit: bool = True, shared_download_cache: bool = False): self.prefix = prefix if prefix else str(uuid.uuid4().hex[0:15]) - self.msgq_path = os.path.join(Paths.shm_path(), self.prefix) + self.msgq_path = os.path.join(Paths.shm_path(), "msgq_" + self.prefix) self.create_dirs_on_enter = create_dirs_on_enter self.clean_dirs_on_exit = clean_dirs_on_exit self.shared_download_cache = shared_download_cache diff --git a/msgq_repo b/msgq_repo index 345878d914..6abe47bc98 160000 --- a/msgq_repo +++ b/msgq_repo @@ -1 +1 @@ -Subproject commit 345878d9141d0470d1a96f8fb5e59efb61dd5cf9 +Subproject commit 6abe47bc98b83338b6ea04a87a6b2b5c65d09630 From 9768109ec11416648076163af230d82563666628 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 16 Dec 2025 16:19:53 -0800 Subject: [PATCH 25/39] ui: generic hold gesture (#36893) * generic * fix * use in home * clean up * rm * clean up --- selfdrive/ui/mici/layouts/home.py | 33 ++++++++----------------------- system/ui/widgets/__init__.py | 30 ++++++++++++++++++++++++++-- 2 files changed, 36 insertions(+), 27 deletions(-) diff --git a/selfdrive/ui/mici/layouts/home.py b/selfdrive/ui/mici/layouts/home.py index 9152bdc7fa..af97de92f4 100644 --- a/selfdrive/ui/mici/layouts/home.py +++ b/selfdrive/ui/mici/layouts/home.py @@ -1,5 +1,3 @@ -import time - from cereal import log import pyray as rl from collections.abc import Callable @@ -83,9 +81,6 @@ class MiciHomeLayout(Widget): self._on_settings_click: Callable | None = None self._last_refresh = 0 - self._mouse_down_t: None | float = None - self._did_long_press = False - self._is_pressed_prev = False self._version_text = None self._experimental_mode = False @@ -124,23 +119,13 @@ class MiciHomeLayout(Widget): def _update_params(self): self._experimental_mode = ui_state.params.get_bool("ExperimentalMode") + def _handle_long_press(self, _): + # long gating for experimental mode - only allow toggle if longitudinal control is available + if ui_state.has_longitudinal_control: + self._experimental_mode = not self._experimental_mode + ui_state.params.put("ExperimentalMode", self._experimental_mode) + def _update_state(self): - if self.is_pressed and not self._is_pressed_prev: - self._mouse_down_t = time.monotonic() - elif not self.is_pressed and self._is_pressed_prev: - self._mouse_down_t = None - self._did_long_press = False - self._is_pressed_prev = self.is_pressed - - if self._mouse_down_t is not None: - if time.monotonic() - self._mouse_down_t > 0.5: - # long gating for experimental mode - only allow toggle if longitudinal control is available - if ui_state.has_longitudinal_control: - self._experimental_mode = not self._experimental_mode - ui_state.params.put("ExperimentalMode", self._experimental_mode) - self._mouse_down_t = None - self._did_long_press = True - if rl.get_time() - self._last_refresh > 5.0: device_state = ui_state.sm['deviceState'] self._update_network_status(device_state) @@ -159,10 +144,8 @@ class MiciHomeLayout(Widget): self._on_settings_click = on_settings def _handle_mouse_release(self, mouse_pos: MousePos): - if not self._did_long_press: - if self._on_settings_click: - self._on_settings_click() - self._did_long_press = False + if self._on_settings_click: + self._on_settings_click() def _get_version_text(self) -> tuple[str, str, str, str] | None: description = ui_state.params.get("UpdaterCurrentDescription") diff --git a/system/ui/widgets/__init__.py b/system/ui/widgets/__init__.py index a3fed6d962..5b3b4a691c 100644 --- a/system/ui/widgets/__init__.py +++ b/system/ui/widgets/__init__.py @@ -20,6 +20,8 @@ class DialogResult(IntEnum): class Widget(abc.ABC): + LONG_PRESS_TIME = 0.5 + def __init__(self): self._rect: rl.Rectangle = rl.Rectangle(0, 0, 0, 0) self._parent_rect: rl.Rectangle | None = None @@ -33,6 +35,10 @@ class Widget(abc.ABC): self._multi_touch = False self.__was_awake = True + # Long press state (single touch only, slot 0) + self._long_press_start_t: float | None = None + self._long_press_fired: bool = False + @property def rect(self) -> rl.Rectangle: return self._rect @@ -127,19 +133,28 @@ class Widget(abc.ABC): self._handle_mouse_press(mouse_event.pos) self.__is_pressed[mouse_event.slot] = True self.__tracking_is_pressed[mouse_event.slot] = True + if mouse_event.slot == 0: + self._long_press_start_t = rl.get_time() + self._long_press_fired = False 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 + if mouse_event.slot == 0: + self._long_press_start_t = None + self._long_press_fired = False elif mouse_event.left_released: self._handle_mouse_event(mouse_event) - if self.__is_pressed[mouse_event.slot] and mouse_in_rect: + if self.__is_pressed[mouse_event.slot] and mouse_in_rect and not (mouse_event.slot == 0 and self._long_press_fired): self._handle_mouse_release(mouse_event.pos) self.__is_pressed[mouse_event.slot] = False self.__tracking_is_pressed[mouse_event.slot] = False + if mouse_event.slot == 0: + self._long_press_start_t = None + self._long_press_fired = False # Mouse/touch is still within our rect elif mouse_in_rect: @@ -150,8 +165,17 @@ class Widget(abc.ABC): # Mouse/touch left our rect but may come back into focus later elif not mouse_in_rect: self.__is_pressed[mouse_event.slot] = False + if mouse_event.slot == 0: + self._long_press_start_t = None + self._long_press_fired = False self._handle_mouse_event(mouse_event) + # Long press detection + if self._long_press_start_t is not None and not self._long_press_fired: + if (rl.get_time() - self._long_press_start_t) >= self.LONG_PRESS_TIME: + self._long_press_fired = True + self._handle_long_press(gui_app.last_mouse_event.pos) + def _layout(self) -> None: """Optionally lay out child widgets separately. This is called before rendering.""" @@ -175,9 +199,11 @@ class Widget(abc.ABC): self._click_callback() return False + def _handle_long_press(self, mouse_pos: MousePos) -> None: + """Optionally handle a long-press gesture.""" + def _handle_mouse_event(self, mouse_event: MouseEvent) -> None: """Optionally handle mouse events. This is called before rendering.""" - # Default implementation does nothing, can be overridden by subclasses def show_event(self): """Optionally handle show event. Parent must manually call this""" From 95350ad854de32e2753fdd1bd15314f84663561f Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 16 Dec 2025 16:56:19 -0800 Subject: [PATCH 26/39] four: simpler steer saturated alert (#36894) * looks good * fix * cleanup --- selfdrive/selfdrived/events.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/selfdrive/selfdrived/events.py b/selfdrive/selfdrived/events.py index a9b1683c51..c6dbcccbee 100755 --- a/selfdrive/selfdrived/events.py +++ b/selfdrive/selfdrived/events.py @@ -254,15 +254,6 @@ def below_steer_speed_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.S Priority.LOW, VisualAlert.none, AudibleAlert.prompt, 0.4) -def steer_saturated_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: - steer_text2 = "Steer Left" if sm['carControl'].actuators.torque > 0 else "Steer Right" - return Alert( - "Take Control", - steer_text2, - AlertStatus.userPrompt, AlertSize.mid, - Priority.LOW, VisualAlert.steerRequired, AudibleAlert.promptRepeat, 2.) - - def calibration_incomplete_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: first_word = 'Recalibrating' if sm['liveCalibration'].calStatus == log.LiveCalibrationData.Status.recalibrating else 'Calibrating' return Alert( @@ -1075,7 +1066,11 @@ if HARDWARE.get_device_type() == 'mici': Priority.LOW, VisualAlert.none, AudibleAlert.prompt, .1), }, EventName.steerSaturated: { - ET.WARNING: steer_saturated_alert, + ET.WARNING: Alert( + "take control", + "turn exceeds limit", + AlertStatus.userPrompt, AlertSize.mid, + Priority.LOW, VisualAlert.steerRequired, AudibleAlert.promptRepeat, 2.), }, EventName.calibrationIncomplete: { ET.PERMANENT: calibration_incomplete_alert, From 90ed6d739cf38c1ca6a363fa3a82d95c6f733f75 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Tue, 16 Dec 2025 18:14:47 -0800 Subject: [PATCH 27/39] test_onroad: relax memory threshold (#36895) --- selfdrive/test/test_onroad.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selfdrive/test/test_onroad.py b/selfdrive/test/test_onroad.py index ed3fca5fb0..f57751c067 100644 --- a/selfdrive/test/test_onroad.py +++ b/selfdrive/test/test_onroad.py @@ -289,7 +289,7 @@ class TestOnroad: # check for big leaks. note that memory usage is # expected to go up while the MSGQ buffers fill up - assert np.average(mems) <= 65, "Average memory usage too high" + assert np.average(mems) <= 80, "Average memory usage too high" assert np.max(np.diff(mems)) <= 4, "Max memory increase too high" assert np.average(np.diff(mems)) <= 1, "Average memory increase too high" From 6069c87b07c1a615a9688201939e419f40866f3d Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Tue, 16 Dec 2025 19:21:00 -0800 Subject: [PATCH 28/39] Update RELEASES.md for version 0.10.3 --- RELEASES.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/RELEASES.md b/RELEASES.md index fabe635c71..5f2a3459a0 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,5 +1,7 @@ -Version 0.10.3 (2025-12-10) +Version 0.10.3 (2025-12-17) ======================== +* New driving model +* New driver monitoring model Version 0.10.2 (2025-11-19) ======================== From c69c076acbc4136b56689024016542de8aec753e Mon Sep 17 00:00:00 2001 From: YassineYousfi Date: Tue, 16 Dec 2025 19:28:21 -0800 Subject: [PATCH 29/39] Update RELEASES.md --- RELEASES.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index 5f2a3459a0..618b28dc5a 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,7 +1,9 @@ Version 0.10.3 (2025-12-17) ======================== -* New driving model -* New driver monitoring model +* New driving model #36249 + * minor changes in the temporal policy architecture and on-policy training physics noise model +* New driver monitoring model #36409 + * trained with a new driver monitoring dataset including comma four data Version 0.10.2 (2025-11-19) ======================== From a112e6e882f37db113cdc7e05784cbf8a8bbb3c9 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 16 Dec 2025 19:31:49 -0800 Subject: [PATCH 30/39] ui: override default interactive timeout (#36898) * impl * fix one place * don't need in setup * fix onboarding * need here too --- selfdrive/ui/mici/layouts/onboarding.py | 18 +++++++++++-- .../ui/mici/onroad/driver_camera_dialog.py | 4 +-- selfdrive/ui/ui_state.py | 25 +++++++++++++------ system/ui/mici_setup.py | 5 ---- 4 files changed, 36 insertions(+), 16 deletions(-) diff --git a/selfdrive/ui/mici/layouts/onboarding.py b/selfdrive/ui/mici/layouts/onboarding.py index abf772ce58..29bc55c9c8 100644 --- a/selfdrive/ui/mici/layouts/onboarding.py +++ b/selfdrive/ui/mici/layouts/onboarding.py @@ -96,7 +96,6 @@ class TrainingGuideDMTutorial(Widget): # Wrap the continue callback to restore settings def wrapped_continue_callback(): device.set_offroad_brightness(None) - device.reset_interactive_timeout() continue_callback() self._dialog = DriverCameraSetupDialog(wrapped_continue_callback) @@ -112,7 +111,6 @@ class TrainingGuideDMTutorial(Widget): self._dialog.show_event() device.set_offroad_brightness(100) - device.reset_interactive_timeout(300) # 5 minutes def _update_state(self): super()._update_state() @@ -223,6 +221,14 @@ class TrainingGuide(Widget): TrainingGuideRecordFront(continue_callback=on_continue), ] + def show_event(self): + super().show_event() + device.set_override_interactive_timeout(300) + + def hide_event(self): + super().hide_event() + device.set_override_interactive_timeout(None) + def _advance_step(self): if self._step < len(self._steps) - 1: self._step += 1 @@ -319,6 +325,14 @@ class OnboardingWindow(Widget): self._training_guide = TrainingGuide(completed_callback=self._on_completed_training) self._decline_page = DeclinePage(back_callback=self._on_decline_back) + def show_event(self): + super().show_event() + device.set_override_interactive_timeout(300) + + def hide_event(self): + super().hide_event() + device.set_override_interactive_timeout(None) + @property def completed(self) -> bool: return self._accepted_terms and self._training_done diff --git a/selfdrive/ui/mici/onroad/driver_camera_dialog.py b/selfdrive/ui/mici/onroad/driver_camera_dialog.py index 9adb660d8b..af7fc33a47 100644 --- a/selfdrive/ui/mici/onroad/driver_camera_dialog.py +++ b/selfdrive/ui/mici/onroad/driver_camera_dialog.py @@ -51,14 +51,14 @@ class DriverCameraDialog(NavWidget): super().show_event() ui_state.params.put_bool("IsDriverViewEnabled", True) self._publish_alert_sound(None) - device.reset_interactive_timeout(300) + device.set_override_interactive_timeout(300) ui_state.params.remove("DriverTooDistracted") self._pm = messaging.PubMaster(['selfdriveState']) def hide_event(self): super().hide_event() ui_state.params.put_bool("IsDriverViewEnabled", False) - device.reset_interactive_timeout() + device.set_override_interactive_timeout(None) def _handle_mouse_release(self, _): ui_state.params.remove("DriverTooDistracted") diff --git a/selfdrive/ui/ui_state.py b/selfdrive/ui/ui_state.py index ef0696a22c..30a6565095 100644 --- a/selfdrive/ui/ui_state.py +++ b/selfdrive/ui/ui_state.py @@ -187,6 +187,7 @@ class Device: def __init__(self): self._ignition = False self._interaction_time: float = -1 + self._override_interactive_timeout: int | None = None self._interactive_timeout_callbacks: list[Callable] = [] self._prev_timed_out = False self._awake: bool = True @@ -200,11 +201,21 @@ class Device: def awake(self) -> bool: return self._awake - def reset_interactive_timeout(self, timeout: int = -1) -> None: - if timeout == -1: - ignition_timeout = 10 if gui_app.big_ui() else 5 - timeout = ignition_timeout if ui_state.ignition else 30 - self._interaction_time = time.monotonic() + timeout + def set_override_interactive_timeout(self, timeout: int | None) -> None: + # Override the interactive timeout duration temporarily + self._override_interactive_timeout = timeout + self._reset_interactive_timeout() + + @property + def interactive_timeout(self) -> int: + if self._override_interactive_timeout is not None: + return self._override_interactive_timeout + + ignition_timeout = 10 if gui_app.big_ui() else 5 + return ignition_timeout if ui_state.ignition else 30 + + def _reset_interactive_timeout(self) -> None: + self._interaction_time = time.monotonic() + self.interactive_timeout def add_interactive_timeout_callback(self, callback: Callable): self._interactive_timeout_callbacks.append(callback) @@ -212,7 +223,7 @@ class Device: def update(self): # do initial reset if self._interaction_time <= 0: - self.reset_interactive_timeout() + self._reset_interactive_timeout() self._update_brightness() self._update_wakefulness() @@ -252,7 +263,7 @@ class Device: self._ignition = ui_state.ignition if ignition_just_turned_off or any(ev.left_down for ev in gui_app.mouse_events): - self.reset_interactive_timeout() + self._reset_interactive_timeout() interaction_timeout = time.monotonic() > self._interaction_time if interaction_timeout and not self._prev_timed_out: diff --git a/system/ui/mici_setup.py b/system/ui/mici_setup.py index 9792c51e7c..84be5bc3da 100755 --- a/system/ui/mici_setup.py +++ b/system/ui/mici_setup.py @@ -18,7 +18,6 @@ from openpilot.common.utils import run_cmd from openpilot.system.hardware import HARDWARE from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.wifi_manager import WifiManager -from openpilot.selfdrive.ui.ui_state import device from openpilot.system.ui.lib.scroll_panel2 import GuiScrollPanel2 from openpilot.system.ui.widgets import Widget, DialogResult from openpilot.system.ui.widgets.button import (IconButton, SmallButton, WideRoundedButton, SmallerRoundedButton, @@ -226,10 +225,6 @@ class TermsPage(Widget): self._back_button.set_opacity(0.0) self._scroll_down_indicator.set_opacity(1.0) - def show_event(self): - super().show_event() - device.reset_interactive_timeout(300) - @property @abstractmethod def _content_height(self): From cecce82015b01e3e1dd22f2d605fda6eb82364bb Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 16 Dec 2025 20:34:51 -0800 Subject: [PATCH 31/39] ui: default text color 90% white (#36899) default 90% --- system/ui/lib/application.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/ui/lib/application.py b/system/ui/lib/application.py index 26e446612d..719d38284d 100644 --- a/system/ui/lib/application.py +++ b/system/ui/lib/application.py @@ -79,7 +79,7 @@ void main() { """ DEFAULT_TEXT_SIZE = 60 -DEFAULT_TEXT_COLOR = rl.WHITE +DEFAULT_TEXT_COLOR = rl.Color(255, 255, 255, int(255 * 0.9)) # Qt draws fonts accounting for ascent/descent differently, so compensate to match old styles # The real scales for the fonts below range from 1.212 to 1.266 From 31e46f929d2d4b4876027920d71759cb71b08ac8 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 16 Dec 2025 22:42:13 -0800 Subject: [PATCH 32/39] onboarding: fixup DM RHD detection (#36900) * helper * fix * use it * prop * bigger box * huh --- selfdrive/ui/mici/layouts/onboarding.py | 5 +++-- .../ui/mici/onroad/driver_camera_dialog.py | 18 ++++++++++-------- selfdrive/ui/mici/onroad/driver_state.py | 12 ++++++++++-- 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/selfdrive/ui/mici/layouts/onboarding.py b/selfdrive/ui/mici/layouts/onboarding.py index 29bc55c9c8..81109fb25b 100644 --- a/selfdrive/ui/mici/layouts/onboarding.py +++ b/selfdrive/ui/mici/layouts/onboarding.py @@ -41,8 +41,7 @@ class DriverCameraSetupDialog(DriverCameraDialog): return -1 # Position dmoji on opposite side from driver - # TODO: we don't have design for RHD yet - is_rhd = False + is_rhd = self.driver_state_renderer.is_rhd driver_state_rect = ( rect.x if is_rhd else rect.x + rect.width - self.driver_state_renderer.rect.width, rect.y + (rect.height - self.driver_state_renderer.rect.height) / 2, @@ -50,6 +49,8 @@ class DriverCameraSetupDialog(DriverCameraDialog): self.driver_state_renderer.set_position(*driver_state_rect) self.driver_state_renderer.render() + self._draw_face_detection(rect) + rl.end_scissor_mode() return -1 diff --git a/selfdrive/ui/mici/onroad/driver_camera_dialog.py b/selfdrive/ui/mici/onroad/driver_camera_dialog.py index af7fc33a47..150e436569 100644 --- a/selfdrive/ui/mici/onroad/driver_camera_dialog.py +++ b/selfdrive/ui/mici/onroad/driver_camera_dialog.py @@ -89,7 +89,9 @@ class DriverCameraDialog(NavWidget): self._publish_alert_sound(None) return -1 - self._draw_face_detection(rect) + driver_data = self._draw_face_detection(rect) + if driver_data is not None: + self._draw_eyes(rect, driver_data) # Position dmoji on opposite side from driver dm_state = ui_state.sm["driverMonitoringState"] @@ -159,12 +161,10 @@ class DriverCameraDialog(NavWidget): if self._glasses_texture is None: self._glasses_texture = gui_app.texture("icons_mici/onroad/glasses.png", self._glasses_size, self._glasses_size) - def _draw_face_detection(self, rect: rl.Rectangle) -> None: - driver_state = ui_state.sm["driverStateV2"] - is_rhd = driver_state.wheelOnRightProb > 0.5 - driver_data = driver_state.rightDriverData if is_rhd else driver_state.leftDriverData - face_detect = driver_data.faceProb > 0.7 - if not face_detect: + def _draw_face_detection(self, rect: rl.Rectangle): + dm_state = ui_state.sm["driverMonitoringState"] + driver_data = self.driver_state_renderer.get_driver_data() + if not dm_state.faceDetected: return # Get face position and orientation @@ -188,7 +188,7 @@ class DriverCameraDialog(NavWidget): scale_y = rect.height / 1080.0 fbox_x = rect.x + rect.width / 2 + offset_x * scale_x fbox_y = rect.y + rect.height / 2 + offset_y * scale_y - box_size = 50 + box_size = 75 line_thickness = 3 line_color = rl.Color(255, 255, 255, int(alpha * 255)) @@ -199,7 +199,9 @@ class DriverCameraDialog(NavWidget): line_thickness, line_color, ) + return driver_data + def _draw_eyes(self, rect: rl.Rectangle, driver_data): # Draw eye indicators based on eye probabilities eye_offset_x = 10 eye_offset_y = 10 diff --git a/selfdrive/ui/mici/onroad/driver_state.py b/selfdrive/ui/mici/onroad/driver_state.py index 080083c3e2..3862c299a9 100644 --- a/selfdrive/ui/mici/onroad/driver_state.py +++ b/selfdrive/ui/mici/onroad/driver_state.py @@ -78,6 +78,10 @@ class DriverStateRenderer(Widget): """Returns True if dmoji should appear active (either actually active or forced)""" return bool(self._force_active or self._is_active) + @property + def is_rhd(self) -> bool: + return self._is_rhd + def _render(self, _): if DEBUG: rl.draw_rectangle_lines_ex(self._rect, 1, rl.RED) @@ -171,10 +175,9 @@ class DriverStateRenderer(Widget): if f.x > 0.01: rl.draw_line_ex((start_x, start_y), (end_x, end_y), 12, color) - def _update_state(self): + def get_driver_data(self): sm = ui_state.sm - # Get monitoring state dm_state = sm["driverMonitoringState"] self._is_active = dm_state.isActiveMode self._is_rhd = dm_state.isRHD @@ -182,6 +185,11 @@ class DriverStateRenderer(Widget): driverstate = sm["driverStateV2"] driver_data = driverstate.rightDriverData if self._is_rhd else driverstate.leftDriverData + return driver_data + + def _update_state(self): + # Get monitoring state + driver_data = self.get_driver_data() driver_orient = driver_data.faceOrientation if len(driver_orient) != 3: From 99983d39c3a6f88ecdae5cbd9485a693ef77a6f6 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 16 Dec 2025 23:16:45 -0800 Subject: [PATCH 33/39] comma four: simpler DM onboarding (#36896) * rm confirm mode * kinda works * how * disabled * do this * do this * wait * here * something * fade in * 4s * clean up * copy * help * 30deg center * stuff * reset_interactive_timeout * rm * simple * simple * copy * 1.5x * smooth opacity * power off slider * fix * new icons and gradient and rounded * final check * fix * how the hell did this work * clean up * clean up * flip * cmt * uh yeah * remove this * revert this * lint * 45 * clean up * fix * no show time * question * rm * no use * () * lint * call --- .../driver_monitoring/dm_background.png | 4 +- .../onroad/driver_monitoring/dm_person.png | 4 +- .../setup/driver_monitoring/dm_check.png | 3 + .../setup/driver_monitoring/dm_question.png | 3 + .../assets/icons_mici/setup/orange_dm.png | 3 + .../setup/small_button_disabled.png | 3 + selfdrive/ui/mici/layouts/onboarding.py | 161 +++++++++++++++--- .../ui/mici/onroad/driver_camera_dialog.py | 5 +- selfdrive/ui/mici/onroad/driver_state.py | 63 +++---- system/ui/mici_setup.py | 24 ++- system/ui/widgets/button.py | 11 +- system/ui/widgets/slider.py | 15 +- 12 files changed, 220 insertions(+), 79 deletions(-) create mode 100644 selfdrive/assets/icons_mici/setup/driver_monitoring/dm_check.png create mode 100644 selfdrive/assets/icons_mici/setup/driver_monitoring/dm_question.png create mode 100644 selfdrive/assets/icons_mici/setup/orange_dm.png create mode 100644 selfdrive/assets/icons_mici/setup/small_button_disabled.png diff --git a/selfdrive/assets/icons_mici/onroad/driver_monitoring/dm_background.png b/selfdrive/assets/icons_mici/onroad/driver_monitoring/dm_background.png index 4d83ed5cd9..04ffc24356 100644 --- a/selfdrive/assets/icons_mici/onroad/driver_monitoring/dm_background.png +++ b/selfdrive/assets/icons_mici/onroad/driver_monitoring/dm_background.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f27352a18194a1c819e9eaea89cfc11d2964402df0a28efa3ba60ae2d972fe67 -size 13108 +oid sha256:b7eb870d01e5bf6c421e204026a4ea08e177731f2d6b5b17c4ad43c90c1c3e78 +size 23549 diff --git a/selfdrive/assets/icons_mici/onroad/driver_monitoring/dm_person.png b/selfdrive/assets/icons_mici/onroad/driver_monitoring/dm_person.png index 7aa7f0542a..540b2029a0 100644 --- a/selfdrive/assets/icons_mici/onroad/driver_monitoring/dm_person.png +++ b/selfdrive/assets/icons_mici/onroad/driver_monitoring/dm_person.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:25d66e42a28a3367eb40724d28652889089aa762438b475645269e0319c46009 -size 1431 +oid sha256:f7b3bb76ee2359076339285ea6bced5b680e5b919a1b7dee163f36cd819c9ea1 +size 1746 diff --git a/selfdrive/assets/icons_mici/setup/driver_monitoring/dm_check.png b/selfdrive/assets/icons_mici/setup/driver_monitoring/dm_check.png new file mode 100644 index 0000000000..92993e3e00 --- /dev/null +++ b/selfdrive/assets/icons_mici/setup/driver_monitoring/dm_check.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5b7dce550c008ff7a65ed19ccf308ecf92cd0118bb544978b7dd7393c5c27ae5 +size 809 diff --git a/selfdrive/assets/icons_mici/setup/driver_monitoring/dm_question.png b/selfdrive/assets/icons_mici/setup/driver_monitoring/dm_question.png new file mode 100644 index 0000000000..53a837afbe --- /dev/null +++ b/selfdrive/assets/icons_mici/setup/driver_monitoring/dm_question.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e102b8b2e71a25d9f818b37d6f75ed958430cb765a07ae50713995779fb6a886 +size 1388 diff --git a/selfdrive/assets/icons_mici/setup/orange_dm.png b/selfdrive/assets/icons_mici/setup/orange_dm.png new file mode 100644 index 0000000000..74cce9d975 --- /dev/null +++ b/selfdrive/assets/icons_mici/setup/orange_dm.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:38a108f96f85a154b698693b07f2e4214124b8f2545b7c4490cea0aa998d75fd +size 11855 diff --git a/selfdrive/assets/icons_mici/setup/small_button_disabled.png b/selfdrive/assets/icons_mici/setup/small_button_disabled.png new file mode 100644 index 0000000000..da8bb3eefd --- /dev/null +++ b/selfdrive/assets/icons_mici/setup/small_button_disabled.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6ed258d8e0531c19705953ded065c6d5e14929728a2909d8d4e335898fa5d080 +size 4056 diff --git a/selfdrive/ui/mici/layouts/onboarding.py b/selfdrive/ui/mici/layouts/onboarding.py index 81109fb25b..d358813411 100644 --- a/selfdrive/ui/mici/layouts/onboarding.py +++ b/selfdrive/ui/mici/layouts/onboarding.py @@ -1,11 +1,14 @@ from enum import IntEnum -from collections.abc import Callable import weakref +import math +import numpy as np import pyray as rl +from openpilot.common.filter_simple import FirstOrderFilter +from openpilot.system.hardware import HARDWARE from openpilot.system.ui.lib.application import FontWeight, gui_app from openpilot.system.ui.widgets import Widget -from openpilot.system.ui.widgets.button import SmallButton +from openpilot.system.ui.widgets.button import SmallButton, SmallCircleIconButton from openpilot.system.ui.widgets.label import UnifiedLabel from openpilot.system.ui.widgets.slider import SmallSlider from openpilot.system.ui.mici_setup import TermsHeader, TermsPage as SetupTermsPage @@ -24,11 +27,12 @@ class OnboardingState(IntEnum): class DriverCameraSetupDialog(DriverCameraDialog): - def __init__(self, confirm_callback: Callable): + def __init__(self): super().__init__(no_escape=True) - self.driver_state_renderer = DriverStateRenderer(confirm_mode=True, confirm_callback=confirm_callback) - self.driver_state_renderer.set_rect(rl.Rectangle(0, 0, 200, 200)) + self.driver_state_renderer = DriverStateRenderer(inset=True) + self.driver_state_renderer.set_rect(rl.Rectangle(0, 0, 120, 120)) self.driver_state_renderer.load_icons() + self.driver_state_renderer.set_force_active(True) def _render(self, rect): rl.begin_scissor_mode(int(rect.x), int(rect.y), int(rect.width), int(rect.height)) @@ -42,11 +46,10 @@ class DriverCameraSetupDialog(DriverCameraDialog): # Position dmoji on opposite side from driver is_rhd = self.driver_state_renderer.is_rhd - driver_state_rect = ( - rect.x if is_rhd else rect.x + rect.width - self.driver_state_renderer.rect.width, - rect.y + (rect.height - self.driver_state_renderer.rect.height) / 2, + self.driver_state_renderer.set_position( + rect.x + 8 if is_rhd else rect.x + rect.width - self.driver_state_renderer.rect.width - 8, + rect.y + 8, ) - self.driver_state_renderer.set_position(*driver_state_rect) self.driver_state_renderer.render() self._draw_face_detection(rect) @@ -89,17 +92,54 @@ class TrainingGuidePreDMTutorial(SetupTermsPage): )) +class DMBadFaceDetected(SetupTermsPage): + def __init__(self, continue_callback, back_callback): + super().__init__(continue_callback, back_callback, continue_text="power off") + self._title_header = TermsHeader("make sure comma four can see your face", gui_app.texture("icons_mici/setup/orange_dm.png", 60, 60)) + self._dm_label = UnifiedLabel("Re-mount if your face is occluded or driver monitoring has difficulty tracking your face.", 42, FontWeight.ROMAN) + + @property + def _content_height(self): + return self._dm_label.rect.y + self._dm_label.rect.height - self._scroll_panel.get_offset() + + def _render_content(self, scroll_offset): + self._title_header.render(rl.Rectangle( + self._rect.x + 16, + self._rect.y + 16 + scroll_offset, + self._title_header.rect.width, + self._title_header.rect.height, + )) + + self._dm_label.render(rl.Rectangle( + self._rect.x + 16, + self._title_header.rect.y + self._title_header.rect.height + 16, + self._rect.width - 32, + self._dm_label.get_content_height(int(self._rect.width - 32)), + )) + + class TrainingGuideDMTutorial(Widget): + PROGRESS_DURATION = 4 + LOOKING_THRESHOLD_DEG = 30.0 + def __init__(self, continue_callback): super().__init__() - self._title_header = TermsHeader("fill the circle to continue", gui_app.texture("icons_mici/setup/green_dm.png", 60, 60)) + self._back_button = SmallCircleIconButton(gui_app.texture("icons_mici/setup/driver_monitoring/dm_question.png", 48, 48)) + self._back_button.set_click_callback(self._show_bad_face_page) + self._good_button = SmallCircleIconButton(gui_app.texture("icons_mici/setup/driver_monitoring/dm_check.png", 48, 35)) # Wrap the continue callback to restore settings def wrapped_continue_callback(): device.set_offroad_brightness(None) continue_callback() - self._dialog = DriverCameraSetupDialog(wrapped_continue_callback) + self._good_button.set_click_callback(wrapped_continue_callback) + self._good_button.set_enabled(False) + + self._progress = FirstOrderFilter(0.0, 0.5, 1 / gui_app.target_fps) + self._dialog = DriverCameraSetupDialog() + self._bad_face_page = DMBadFaceDetected(HARDWARE.shutdown, self._hide_bad_face_page) + self._should_show_bad_face_page = False # Disable driver monitoring model when device times out for inactivity def inactivity_callback(): @@ -107,9 +147,20 @@ class TrainingGuideDMTutorial(Widget): device.add_interactive_timeout_callback(inactivity_callback) + def _show_bad_face_page(self): + self._bad_face_page.show_event() + self.hide_event() + self._should_show_bad_face_page = True + + def _hide_bad_face_page(self): + self._bad_face_page.hide_event() + self.show_event() + self._should_show_bad_face_page = False + def show_event(self): super().show_event() self._dialog.show_event() + self._progress.x = 0.0 device.set_offroad_brightness(100) @@ -118,19 +169,91 @@ class TrainingGuideDMTutorial(Widget): if device.awake: ui_state.params.put_bool("IsDriverViewEnabled", True) + sm = ui_state.sm + if sm.recv_frame.get("driverMonitoringState", 0) == 0: + return + + dm_state = sm["driverMonitoringState"] + driver_data = self._dialog.driver_state_renderer.get_driver_data() + + if len(driver_data.faceOrientation) == 3: + pitch, yaw, _ = driver_data.faceOrientation + looking_center = abs(math.degrees(pitch)) < self.LOOKING_THRESHOLD_DEG and abs(math.degrees(yaw)) < self.LOOKING_THRESHOLD_DEG + else: + looking_center = False + + # stay at 100% once reached + if (dm_state.faceDetected and looking_center) or self._progress.x > 0.99: + slow = self._progress.x < 0.25 + duration = self.PROGRESS_DURATION * 2 if slow else self.PROGRESS_DURATION + self._progress.x += 1.0 / (duration * gui_app.target_fps) + self._progress.x = min(1.0, self._progress.x) + else: + self._progress.update(0.0) + + self._good_button.set_enabled(self._progress.x >= 0.999) + def _render(self, _): + if self._should_show_bad_face_page: + return self._bad_face_page.render(self._rect) + self._dialog.render(self._rect) - rl.draw_rectangle_gradient_v(int(self._rect.x), int(self._rect.y + self._rect.height - self._title_header.rect.height * 1.5 - 32), - int(self._rect.width), int(self._title_header.rect.height * 1.5 + 32), - rl.BLANK, rl.Color(0, 0, 0, 150)) - self._title_header.render(rl.Rectangle( - self._rect.x + 16, - self._rect.y + self._rect.height - self._title_header.rect.height - 16, - self._title_header.rect.width, - self._title_header.rect.height, + rl.draw_rectangle_gradient_v(int(self._rect.x), int(self._rect.y + self._rect.height - 80), + int(self._rect.width), 80, rl.BLANK, rl.BLACK) + + # draw white ring around dm icon to indicate progress + ring_thickness = 8 + + # DM icon is 120x120, positioned on opposite side from driver + dm_size = 120 + is_rhd = self._dialog.driver_state_renderer._is_rhd + dm_center_x = (self._rect.x + dm_size / 2 + 8) if is_rhd else (self._rect.x + self._rect.width - dm_size / 2 - 8) + dm_center_y = self._rect.y + dm_size / 2 + 8 + icon_edge_radius = dm_size / 2 + outer_radius = icon_edge_radius + 1 # 2px outward from icon edge + inner_radius = outer_radius - ring_thickness # Inset by ring_thickness + start_angle = 90.0 # Start from bottom + end_angle = start_angle + self._progress.x * 360.0 # Clockwise + + # Fade in alpha + current_angle = end_angle - start_angle + alpha = int(np.interp(current_angle, [0.0, 45.0], [0, 255])) + + # White to green + color_t = np.clip(np.interp(current_angle, [45.0, 360.0], [0.0, 1.0]), 0.0, 1.0) + r = int(np.interp(color_t, [0.0, 1.0], [255, 0])) + g = int(np.interp(color_t, [0.0, 1.0], [255, 255])) + b = int(np.interp(color_t, [0.0, 1.0], [255, 64])) + ring_color = rl.Color(r, g, b, alpha) + + rl.draw_ring( + rl.Vector2(dm_center_x, dm_center_y), + inner_radius, + outer_radius, + start_angle, + end_angle, + 36, + ring_color, + ) + + self._back_button.render(rl.Rectangle( + self._rect.x + 8, + self._rect.y + self._rect.height - self._back_button.rect.height, + self._back_button.rect.width, + self._back_button.rect.height, )) + self._good_button.render(rl.Rectangle( + self._rect.x + self._rect.width - self._good_button.rect.width - 8, + self._rect.y + self._rect.height - self._good_button.rect.height, + self._good_button.rect.width, + self._good_button.rect.height, + )) + + # rounded border + rl.draw_rectangle_rounded_lines_ex(self._rect, 0.2 * 1.02, 10, 50, rl.BLACK) + class TrainingGuideRecordFront(SetupTermsPage): def __init__(self, continue_callback): diff --git a/selfdrive/ui/mici/onroad/driver_camera_dialog.py b/selfdrive/ui/mici/onroad/driver_camera_dialog.py index 150e436569..bab3d6e6f1 100644 --- a/selfdrive/ui/mici/onroad/driver_camera_dialog.py +++ b/selfdrive/ui/mici/onroad/driver_camera_dialog.py @@ -94,9 +94,8 @@ class DriverCameraDialog(NavWidget): self._draw_eyes(rect, driver_data) # Position dmoji on opposite side from driver - dm_state = ui_state.sm["driverMonitoringState"] driver_state_rect = ( - rect.x if dm_state.isRHD else rect.x + rect.width - self.driver_state_renderer.rect.width, + rect.x if self.driver_state_renderer.is_rhd else rect.x + rect.width - self.driver_state_renderer.rect.width, rect.y + (rect.height - self.driver_state_renderer.rect.height) / 2, ) self.driver_state_renderer.set_position(*driver_state_rect) @@ -140,7 +139,7 @@ class DriverCameraDialog(NavWidget): # Show first event (only one should be active at a time) event_name_str = str(dm_state.events[0].name).split('.')[-1] - alignment = rl.GuiTextAlignment.TEXT_ALIGN_RIGHT if dm_state.isRHD else rl.GuiTextAlignment.TEXT_ALIGN_LEFT + alignment = rl.GuiTextAlignment.TEXT_ALIGN_RIGHT if self.driver_state_renderer.is_rhd else rl.GuiTextAlignment.TEXT_ALIGN_LEFT shadow_rect = rl.Rectangle(rect.x + 2, rect.y + 2, rect.width, rect.height) gui_label(shadow_rect, event_name_str, font_size=40, font_weight=FontWeight.BOLD, diff --git a/selfdrive/ui/mici/onroad/driver_state.py b/selfdrive/ui/mici/onroad/driver_state.py index 3862c299a9..356d7ac832 100644 --- a/selfdrive/ui/mici/onroad/driver_state.py +++ b/selfdrive/ui/mici/onroad/driver_state.py @@ -1,9 +1,7 @@ import pyray as rl -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 @@ -22,15 +20,11 @@ class DriverStateRenderer(Widget): LINES_ANGLE_INCREMENT = 5 LINES_STALE_ANGLES = 3.0 # seconds - def __init__(self, lines: bool = False, confirm_mode: bool = False, confirm_callback: Callable | None = None): + def __init__(self, lines: bool = False, inset: bool = False): super().__init__() self.set_rect(rl.Rectangle(0, 0, self.BASE_SIZE, self.BASE_SIZE)) - self._lines = lines or confirm_mode - - # In confirm mode, user must fill out the circle to confirm some action in the UI - self._confirm_mode = confirm_mode - self._confirm_callback = confirm_callback - self._confirm_angles: dict[int, float] = {} # angle: timestamp + self._lines = lines + self._inset = inset # In line mode, track smoothed angles assert 360 % self.LINES_ANGLE_INCREMENT == 0 @@ -54,12 +48,20 @@ class DriverStateRenderer(Widget): def load_icons(self): """Load or reload the driver face icon texture""" - self._dm_person = gui_app.texture("icons_mici/onroad/driver_monitoring/dm_person.png", self._rect.width, self._rect.height) - self._dm_cone = gui_app.texture("icons_mici/onroad/driver_monitoring/dm_cone.png", self._rect.width, self._rect.height) + cone_and_person_size = round(52 / self.BASE_SIZE * self._rect.width) + + # If inset is enabled, push cone and person smaller by 2x the current inset space + if self._inset: + # Current inset space = (rect.width - cone_and_person_size) / 2 + current_inset = (self._rect.width - cone_and_person_size) / 2 + # Reduce size by 2x the current inset (1x on each side) + cone_and_person_size = round(cone_and_person_size - current_inset * 2) + + self._dm_person = gui_app.texture("icons_mici/onroad/driver_monitoring/dm_person.png", cone_and_person_size, cone_and_person_size) + self._dm_cone = gui_app.texture("icons_mici/onroad/driver_monitoring/dm_cone.png", cone_and_person_size, cone_and_person_size) center_size = round(36 / self.BASE_SIZE * self._rect.width) self._dm_center = gui_app.texture("icons_mici/onroad/driver_monitoring/dm_center.png", center_size, center_size) - background_size = round(52 / self.BASE_SIZE * self._rect.width) - self._dm_background = gui_app.texture("icons_mici/onroad/driver_monitoring/dm_background.png", background_size, background_size) + self._dm_background = gui_app.texture("icons_mici/onroad/driver_monitoring/dm_background.png", self._rect.width, self._rect.height) def set_should_draw(self, should_draw: bool): self._should_draw = should_draw @@ -87,11 +89,13 @@ class DriverStateRenderer(Widget): rl.draw_rectangle_lines_ex(self._rect, 1, rl.RED) rl.draw_texture(self._dm_background, - int(self._rect.x + (self._rect.width - self._dm_background.width) / 2), - int(self._rect.y + (self._rect.height - self._dm_background.height) / 2), + int(self._rect.x), + int(self._rect.y), rl.Color(255, 255, 255, int(255 * self._fade_filter.x))) - rl.draw_texture(self._dm_person, int(self._rect.x), int(self._rect.y), + rl.draw_texture(self._dm_person, + int(self._rect.x + (self._rect.width - self._dm_person.width) / 2), + int(self._rect.y + (self._rect.height - self._dm_person.height) / 2), rl.Color(255, 255, 255, int(255 * 0.9 * self._fade_filter.x))) if self.effective_active: @@ -124,38 +128,18 @@ class DriverStateRenderer(Widget): else: # remove old angles - now = rl.get_time() - self._confirm_angles = {angle: t for angle, t in self._confirm_angles.items() if now - t < self.LINES_STALE_ANGLES} - - looking_center = self._looking_center_filter.x > 0.2 for angle, f in self._head_angles.items(): dst_from_current = ((angle - self._rotation_filter.x) % 360) - 180 target = 1.0 if abs(dst_from_current) <= self.LINES_ANGLE_INCREMENT * 5 else 0.0 if not self._face_detected: target = 0.0 - if self._confirm_mode: - # Extra careful to not add angles when looking near center - if target > 0 and not looking_center: - self._confirm_angles[angle] = now - - # User is looking at area already confirmed, reduce target to indicate where they are - if angle in self._confirm_angles and target == 0: - target = 0.65 - # Reduce all line lengths when looking center if self._looking_center: target = np.interp(self._looking_center_filter.x, [0.0, 1.0], [target, 0.45]) f.update(target) - self._draw_line(angle, f, self._looking_center and angle not in self._confirm_angles) - - # if all lines placed, reset for next time and call callback - if self._confirm_mode: - if len(self._confirm_angles) >= 360 // self.LINES_ANGLE_INCREMENT: - self._confirm_angles = {} - if self._confirm_callback is not None: - self._confirm_callback() + self._draw_line(angle, f, self._looking_center) def _draw_line(self, angle: int, f: FirstOrderFilter, grey: bool): line_length = self._rect.width / 6 @@ -226,10 +210,7 @@ class DriverStateRenderer(Widget): rotation = math.degrees(math.atan2(pitch, yaw)) angle_diff = rotation - self._rotation_filter.x angle_diff = ((angle_diff + 180) % 360) - 180 - if PC and self._confirm_mode: - self._rotation_filter.x += 2 - else: - self._rotation_filter.update(self._rotation_filter.x + angle_diff) + self._rotation_filter.update(self._rotation_filter.x + angle_diff) if not self.should_draw: self._fade_filter.update(0.0) diff --git a/system/ui/mici_setup.py b/system/ui/mici_setup.py index 84be5bc3da..fda35be6eb 100755 --- a/system/ui/mici_setup.py +++ b/system/ui/mici_setup.py @@ -24,7 +24,7 @@ from openpilot.system.ui.widgets.button import (IconButton, SmallButton, WideRou SmallCircleIconButton, WidishRoundedButton, SmallRedPillButton, FullRoundedButton) from openpilot.system.ui.widgets.label import UnifiedLabel -from openpilot.system.ui.widgets.slider import LargerSlider +from openpilot.system.ui.widgets.slider import LargerSlider, SmallSlider from openpilot.selfdrive.ui.mici.layouts.settings.network import WifiUIMici from openpilot.selfdrive.ui.mici.widgets.dialog import BigInputDialog @@ -198,15 +198,20 @@ class TermsPage(Widget): self._scroll_panel = GuiScrollPanel2(horizontal=False) self._continue_text = continue_text - self._continue_button: WideRoundedButton | FullRoundedButton - if back_callback is not None: + self._continue_slider: bool = continue_text in ("reboot", "power off") + self._continue_button: WideRoundedButton | FullRoundedButton | SmallSlider + if self._continue_slider: + self._continue_button = SmallSlider(continue_text, confirm_callback=continue_callback) + self._scroll_panel.set_enabled(lambda: not self._continue_button.is_pressed) + elif back_callback is not None: self._continue_button = WideRoundedButton(continue_text) else: self._continue_button = FullRoundedButton(continue_text) self._continue_button.set_enabled(False) self._continue_button.set_opacity(0.0) self._continue_button.set_touch_valid_callback(self._scroll_panel.is_touch_valid) - self._continue_button.set_click_callback(continue_callback) + if not self._continue_slider: + self._continue_button.set_click_callback(continue_callback) self._enable_back = back_callback is not None self._back_button = SmallButton(back_text) @@ -225,6 +230,10 @@ class TermsPage(Widget): self._back_button.set_opacity(0.0) self._scroll_down_indicator.set_opacity(1.0) + def show_event(self): + super().show_event() + self.reset() + @property @abstractmethod def _content_height(self): @@ -265,6 +274,11 @@ class TermsPage(Widget): rl.draw_rectangle_gradient_v(int(self._rect.x), int(self._rect.y + self._rect.height - 20), int(self._rect.width), 20, rl.BLANK, rl.BLACK) + # fade out back button as slider is moved + if self._continue_slider and scroll_offset <= self._scrolled_down_offset: + self._back_button.set_opacity(1.0 - self._continue_button.slider_percentage) + self._back_button.set_visible(self._continue_button.slider_percentage < 0.99) + self._back_button.render(rl.Rectangle( self._rect.x + 8, self._rect.y + self._rect.height - self._back_button.rect.height, @@ -275,6 +289,8 @@ class TermsPage(Widget): continue_x = self._rect.x + 8 if self._enable_back: continue_x = self._rect.x + self._rect.width - self._continue_button.rect.width - 8 + if self._continue_slider: + continue_x += 8 self._continue_button.render(rl.Rectangle( continue_x, self._rect.y + self._rect.height - self._continue_button.rect.height, diff --git a/system/ui/widgets/button.py b/system/ui/widgets/button.py index 34b2a51a42..9c0ea75b42 100644 --- a/system/ui/widgets/button.py +++ b/system/ui/widgets/button.py @@ -201,6 +201,7 @@ class SmallCircleIconButton(Widget): self._opacity_filter = FirstOrderFilter(1.0, 0.1, 1 / gui_app.target_fps) self._icon_bg_txt = gui_app.texture("icons_mici/setup/small_button.png", 100, 100) self._icon_bg_pressed_txt = gui_app.texture("icons_mici/setup/small_button_pressed.png", 100, 100) + self._icon_bg_disabled_txt = gui_app.texture("icons_mici/setup/small_button_disabled.png", 100, 100) self._icon_txt = icon_txt def set_opacity(self, opacity: float, smooth: bool = False): @@ -210,12 +211,18 @@ class SmallCircleIconButton(Widget): self._opacity_filter.x = opacity def _render(self, _): - bg_txt = self._icon_bg_pressed_txt if self.is_pressed else self._icon_bg_txt white = rl.Color(255, 255, 255, int(255 * self._opacity_filter.x)) + if not self.enabled: + bg_txt = self._icon_bg_disabled_txt + icon_white = rl.Color(255, 255, 255, int(white.a * 0.35)) + else: + bg_txt = self._icon_bg_pressed_txt if self.is_pressed else self._icon_bg_txt + icon_white = white + rl.draw_texture(bg_txt, int(self.rect.x), int(self.rect.y), white) icon_x = self.rect.x + (self.rect.width - self._icon_txt.width) / 2 icon_y = self.rect.y + (self.rect.height - self._icon_txt.height) / 2 - rl.draw_texture(self._icon_txt, int(icon_x), int(icon_y), white) + rl.draw_texture(self._icon_txt, int(icon_x), int(icon_y), icon_white) class SmallButton(Widget): diff --git a/system/ui/widgets/slider.py b/system/ui/widgets/slider.py index b17d8f3b7c..455cdeef71 100644 --- a/system/ui/widgets/slider.py +++ b/system/ui/widgets/slider.py @@ -24,7 +24,7 @@ class SmallSlider(Widget): self._drag_threshold = -self._rect.width // 2 # State - self._opacity = 1.0 + self._opacity_filter = FirstOrderFilter(1.0, 0.1, 1 / gui_app.target_fps) self._confirmed_time = 0.0 self._confirm_callback_called = False # we keep dialog open by default, only call once self._start_x_circle = 0.0 @@ -54,8 +54,11 @@ class SmallSlider(Widget): self._confirmed_time = 0.0 self._confirm_callback_called = False - def set_opacity(self, opacity: float): - self._opacity = opacity + def set_opacity(self, opacity: float, smooth: bool = False): + if smooth: + self._opacity_filter.update(opacity) + else: + self._opacity_filter.x = opacity @property def slider_percentage(self): @@ -117,7 +120,7 @@ class SmallSlider(Widget): def _render(self, _): # TODO: iOS text shimmering animation - white = rl.Color(255, 255, 255, int(255 * self._opacity)) + white = rl.Color(255, 255, 255, int(255 * self._opacity_filter.x)) bg_txt_x = self._rect.x + (self._rect.width - self._bg_txt.width) / 2 bg_txt_y = self._rect.y + (self._rect.height - self._bg_txt.height) / 2 @@ -127,11 +130,11 @@ class SmallSlider(Widget): btn_y = self._rect.y + (self._rect.height - self._circle_bg_txt.height) / 2 if self._confirmed_time == 0.0 or self._scroll_x_circle > 0: - self._label.set_text_color(rl.Color(255, 255, 255, int(255 * 0.65 * (1.0 - self.slider_percentage) * self._opacity))) + self._label.set_text_color(rl.Color(255, 255, 255, int(255 * 0.65 * (1.0 - self.slider_percentage) * self._opacity_filter.x))) label_rect = rl.Rectangle( self._rect.x + 20, self._rect.y, - self._rect.width - self._circle_bg_txt.width - 20 * 3, + self._rect.width - self._circle_bg_txt.width - 20 * 2.5, self._rect.height, ) self._label.render(label_rect) From b9c3b1219a1687c9bf05b0bfc407ee227aace508 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 16 Dec 2025 23:45:34 -0800 Subject: [PATCH 34/39] ui: fix not showing networks if viewing right after startup --- selfdrive/ui/mici/layouts/settings/network/wifi_ui.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py index 793bdcf4a0..374539c4ce 100644 --- a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py +++ b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py @@ -335,7 +335,7 @@ class WifiUIMici(BigMultiOptionDialog): self._networks: dict[str, Network] = {} # widget state - self._last_interaction_time = rl.get_time() + self._last_interaction_time = -float('inf') self._restore_selection = False self._wifi_manager.add_callbacks( @@ -350,6 +350,7 @@ class WifiUIMici(BigMultiOptionDialog): # Call super to prepare scroller; selection scroll is handled dynamically super().show_event() self._wifi_manager.set_active(True) + self._last_interaction_time = -float('inf') def hide_event(self): super().hide_event() From d2125aafd4c931817ff9a83a8a6c1c8def105175 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Wed, 17 Dec 2025 01:23:49 -0800 Subject: [PATCH 35/39] Fix tici DM dialog memory leak (#36790) * not finished * no * debug * clean up * clean up --- selfdrive/ui/onroad/driver_camera_dialog.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/selfdrive/ui/onroad/driver_camera_dialog.py b/selfdrive/ui/onroad/driver_camera_dialog.py index 543ea35e81..f69ad8c49c 100644 --- a/selfdrive/ui/onroad/driver_camera_dialog.py +++ b/selfdrive/ui/onroad/driver_camera_dialog.py @@ -14,16 +14,20 @@ class DriverCameraDialog(CameraView): super().__init__("camerad", VisionStreamType.VISION_STREAM_DRIVER) self.driver_state_renderer = DriverStateRenderer() # TODO: this can grow unbounded, should be given some thought - device.add_interactive_timeout_callback(self.stop_dmonitoringmodeld) + device.add_interactive_timeout_callback(lambda: gui_app.set_modal_overlay(None)) ui_state.params.put_bool("IsDriverViewEnabled", True) - def stop_dmonitoringmodeld(self): + def hide_event(self): + super().hide_event() ui_state.params.put_bool("IsDriverViewEnabled", False) - gui_app.set_modal_overlay(None) + self.close() def _handle_mouse_release(self, _): super()._handle_mouse_release(_) - self.stop_dmonitoringmodeld() + gui_app.set_modal_overlay(None) + + def __del__(self): + self.close() def _render(self, rect): super()._render(rect) From 3fbd928b98ea8e63556d59cd3b14074f7d146afc Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Wed, 17 Dec 2025 01:53:17 -0800 Subject: [PATCH 36/39] Revert "ui: generic hold gesture (#36893)" This reverts commit 9768109ec11416648076163af230d82563666628. --- selfdrive/ui/mici/layouts/home.py | 33 +++++++++++++++++++++++-------- system/ui/widgets/__init__.py | 30 ++-------------------------- 2 files changed, 27 insertions(+), 36 deletions(-) diff --git a/selfdrive/ui/mici/layouts/home.py b/selfdrive/ui/mici/layouts/home.py index af97de92f4..9152bdc7fa 100644 --- a/selfdrive/ui/mici/layouts/home.py +++ b/selfdrive/ui/mici/layouts/home.py @@ -1,3 +1,5 @@ +import time + from cereal import log import pyray as rl from collections.abc import Callable @@ -81,6 +83,9 @@ class MiciHomeLayout(Widget): self._on_settings_click: Callable | None = None self._last_refresh = 0 + self._mouse_down_t: None | float = None + self._did_long_press = False + self._is_pressed_prev = False self._version_text = None self._experimental_mode = False @@ -119,13 +124,23 @@ class MiciHomeLayout(Widget): def _update_params(self): self._experimental_mode = ui_state.params.get_bool("ExperimentalMode") - def _handle_long_press(self, _): - # long gating for experimental mode - only allow toggle if longitudinal control is available - if ui_state.has_longitudinal_control: - self._experimental_mode = not self._experimental_mode - ui_state.params.put("ExperimentalMode", self._experimental_mode) - def _update_state(self): + if self.is_pressed and not self._is_pressed_prev: + self._mouse_down_t = time.monotonic() + elif not self.is_pressed and self._is_pressed_prev: + self._mouse_down_t = None + self._did_long_press = False + self._is_pressed_prev = self.is_pressed + + if self._mouse_down_t is not None: + if time.monotonic() - self._mouse_down_t > 0.5: + # long gating for experimental mode - only allow toggle if longitudinal control is available + if ui_state.has_longitudinal_control: + self._experimental_mode = not self._experimental_mode + ui_state.params.put("ExperimentalMode", self._experimental_mode) + self._mouse_down_t = None + self._did_long_press = True + if rl.get_time() - self._last_refresh > 5.0: device_state = ui_state.sm['deviceState'] self._update_network_status(device_state) @@ -144,8 +159,10 @@ class MiciHomeLayout(Widget): self._on_settings_click = on_settings def _handle_mouse_release(self, mouse_pos: MousePos): - if self._on_settings_click: - self._on_settings_click() + if not self._did_long_press: + if self._on_settings_click: + self._on_settings_click() + self._did_long_press = False def _get_version_text(self) -> tuple[str, str, str, str] | None: description = ui_state.params.get("UpdaterCurrentDescription") diff --git a/system/ui/widgets/__init__.py b/system/ui/widgets/__init__.py index 5b3b4a691c..a3fed6d962 100644 --- a/system/ui/widgets/__init__.py +++ b/system/ui/widgets/__init__.py @@ -20,8 +20,6 @@ class DialogResult(IntEnum): class Widget(abc.ABC): - LONG_PRESS_TIME = 0.5 - def __init__(self): self._rect: rl.Rectangle = rl.Rectangle(0, 0, 0, 0) self._parent_rect: rl.Rectangle | None = None @@ -35,10 +33,6 @@ class Widget(abc.ABC): self._multi_touch = False self.__was_awake = True - # Long press state (single touch only, slot 0) - self._long_press_start_t: float | None = None - self._long_press_fired: bool = False - @property def rect(self) -> rl.Rectangle: return self._rect @@ -133,28 +127,19 @@ class Widget(abc.ABC): self._handle_mouse_press(mouse_event.pos) self.__is_pressed[mouse_event.slot] = True self.__tracking_is_pressed[mouse_event.slot] = True - if mouse_event.slot == 0: - self._long_press_start_t = rl.get_time() - self._long_press_fired = False 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 - if mouse_event.slot == 0: - self._long_press_start_t = None - self._long_press_fired = False elif mouse_event.left_released: self._handle_mouse_event(mouse_event) - if self.__is_pressed[mouse_event.slot] and mouse_in_rect and not (mouse_event.slot == 0 and self._long_press_fired): + 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 - if mouse_event.slot == 0: - self._long_press_start_t = None - self._long_press_fired = False # Mouse/touch is still within our rect elif mouse_in_rect: @@ -165,17 +150,8 @@ class Widget(abc.ABC): # Mouse/touch left our rect but may come back into focus later elif not mouse_in_rect: self.__is_pressed[mouse_event.slot] = False - if mouse_event.slot == 0: - self._long_press_start_t = None - self._long_press_fired = False self._handle_mouse_event(mouse_event) - # Long press detection - if self._long_press_start_t is not None and not self._long_press_fired: - if (rl.get_time() - self._long_press_start_t) >= self.LONG_PRESS_TIME: - self._long_press_fired = True - self._handle_long_press(gui_app.last_mouse_event.pos) - def _layout(self) -> None: """Optionally lay out child widgets separately. This is called before rendering.""" @@ -199,11 +175,9 @@ class Widget(abc.ABC): self._click_callback() return False - def _handle_long_press(self, mouse_pos: MousePos) -> None: - """Optionally handle a long-press gesture.""" - def _handle_mouse_event(self, mouse_event: MouseEvent) -> None: """Optionally handle mouse events. This is called before rendering.""" + # Default implementation does nothing, can be overridden by subclasses def show_event(self): """Optionally handle show event. Parent must manually call this""" From 1646fd94b83ea98dc1120a53c4e88a37454e0c4c Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Wed, 17 Dec 2025 01:55:17 -0800 Subject: [PATCH 37/39] setup: go back to main page once connected (#36902) * call * break * print * fix * rm * debug * fix * yeah ideally wifiui has no clue about this * clean up * clean up * clean up * only need this * cu * rm * fix --- system/ui/lib/application.py | 7 +++++++ system/ui/mici_setup.py | 19 ++++++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/system/ui/lib/application.py b/system/ui/lib/application.py index 719d38284d..501a7ff371 100644 --- a/system/ui/lib/application.py +++ b/system/ui/lib/application.py @@ -218,6 +218,7 @@ class GuiApplication: self._trace_log_callback = None self._modal_overlay = ModalOverlay() self._modal_overlay_shown = False + self._modal_overlay_tick: Callable[[], None] | None = None self._mouse = MouseState(self._scale) self._mouse_events: list[MouseEvent] = [] @@ -347,6 +348,9 @@ class GuiApplication: self._modal_overlay = ModalOverlay(overlay=overlay, callback=callback) + def set_modal_overlay_tick(self, tick_function: Callable | None): + self._modal_overlay_tick = tick_function + def set_should_render(self, should_render: bool): self._should_render = should_render @@ -485,6 +489,9 @@ class GuiApplication: # Handle modal overlay rendering and input processing if self._handle_modal_overlay(): + # Allow a Widget to still run a function while overlay is shown + if self._modal_overlay_tick is not None: + self._modal_overlay_tick() yield False else: yield True diff --git a/system/ui/mici_setup.py b/system/ui/mici_setup.py index fda35be6eb..2c6090b4ac 100755 --- a/system/ui/mici_setup.py +++ b/system/ui/mici_setup.py @@ -454,9 +454,12 @@ class NetworkSetupPage(Widget): self._continue_button.set_click_callback(continue_callback) self._state = NetworkSetupState.MAIN + self._prev_has_internet = False def set_state(self, state: NetworkSetupState): self._state = state + if state == NetworkSetupState.WIFI_PANEL: + self._wifi_ui.show_event() def set_has_internet(self, has_internet: bool): if has_internet: @@ -468,6 +471,10 @@ class NetworkSetupPage(Widget): self._network_header.set_icon(self._no_wifi_txt) self._continue_button.set_enabled(False) + if has_internet and not self._prev_has_internet: + self.set_state(NetworkSetupState.MAIN) + self._prev_has_internet = has_internet + def show_event(self): super().show_event() self._state = NetworkSetupState.MAIN @@ -524,6 +531,8 @@ class Setup(Widget): self._network_monitor = NetworkConnectivityMonitor( lambda: self.state in (SetupState.NETWORK_SETUP, SetupState.NETWORK_SETUP_CUSTOM_SOFTWARE) ) + self._prev_has_internet = False + gui_app.set_modal_overlay_tick(self._modal_overlay_tick) self._start_page = StartPage() self._start_page.set_click_callback(self._getting_started_button_callback) @@ -541,6 +550,12 @@ class Setup(Widget): self._downloading_page = DownloadingPage() + def _modal_overlay_tick(self): + has_internet = self._network_monitor.network_connected.is_set() + if has_internet and not self._prev_has_internet: + gui_app.set_modal_overlay(None) + self._prev_has_internet = has_internet + def _update_state(self): self._wifi_manager.process_callbacks() @@ -614,7 +629,9 @@ class Setup(Widget): def render_network_setup(self, rect: rl.Rectangle): self._network_setup_page.render(rect) - self._network_setup_page.set_has_internet(self._network_monitor.network_connected.is_set()) + has_internet = self._network_monitor.network_connected.is_set() + self._prev_has_internet = has_internet + self._network_setup_page.set_has_internet(has_internet) def render_downloading(self, rect: rl.Rectangle): self._downloading_page.set_progress(self.download_progress) From be2818a131997214b8a284722e8060988422e100 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Wed, 17 Dec 2025 09:37:30 -0800 Subject: [PATCH 38/39] CI: tmp disable macOS due to brew bug (#36906) * need update? * try this * x * just disable it --- .github/workflows/tests.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index c5802b5cb2..f0028841a0 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -91,6 +91,7 @@ jobs: build_mac: name: build macOS + if: false # tmp disable due to brew install not working runs-on: ${{ ((github.repository == 'commaai/openpilot') && ((github.event_name != 'pull_request') || (github.event.pull_request.head.repo.full_name == 'commaai/openpilot'))) && 'namespace-profile-macos-8x14' || 'macos-latest' }} steps: - uses: actions/checkout@v4 From 4bfc28dec067e43074faed9cb6f9e59191519980 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Wed, 17 Dec 2025 10:19:05 -0800 Subject: [PATCH 39/39] lil more release notes --- RELEASES.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index 618b28dc5a..3028351400 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,9 +1,11 @@ Version 0.10.3 (2025-12-17) ======================== * New driving model #36249 - * minor changes in the temporal policy architecture and on-policy training physics noise model + * New temporal policy architecture + * New on-policy training physics noise model * New driver monitoring model #36409 - * trained with a new driver monitoring dataset including comma four data + * Trained on a new dataset, including comma four data +* Improved inter-process communication memory efficiency Version 0.10.2 (2025-11-19) ========================