From 99c2fcc797c406774690e55e4e9e21973072a5fa Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Wed, 11 Feb 2026 15:05:10 -0800 Subject: [PATCH 001/311] ui: reduce wifi dbus calls (#37170) * 2 -> 1 * cmt --- system/ui/lib/wifi_manager.py | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index e0a9e7ca7f..1d1dfc1145 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -527,13 +527,9 @@ class WifiManager: threading.Thread(target=worker, daemon=True).start() - def _update_current_network_metered(self) -> None: - if self._wifi_device is None: - cloudlog.warning("No WiFi device found") - return - + def _update_current_network_metered(self, active_conns) -> None: self._current_network_metered = MeteredType.UNKNOWN - for active_conn in self._get_active_connections(): + for active_conn in active_conns: conn_addr = DBusAddress(active_conn, bus_name=NM, interface=NM_ACTIVE_CONNECTION_IFACE) conn_type = self._router_main.send_and_get_reply(Properties(conn_addr).get('Type')).body[0][1] @@ -634,19 +630,17 @@ class WifiManager: networks.sort(key=lambda n: (-n.is_connected, -n.is_saved, -round(n.strength / 100 * 2), n.ssid.lower())) self._networks = networks - self._update_ipv4_address() - self._update_current_network_metered() + # Get active connections once + active_conns = self._get_active_connections() + self._update_ipv4_address(active_conns) + self._update_current_network_metered(active_conns) self._enqueue_callbacks(self._networks_updated, self._networks) - def _update_ipv4_address(self): - if self._wifi_device is None: - cloudlog.warning("No WiFi device found") - return - + def _update_ipv4_address(self, active_conns): self._ipv4_address = "" - for conn_path in self._get_active_connections(): + for conn_path in active_conns: conn_addr = DBusAddress(conn_path, bus_name=NM, interface=NM_ACTIVE_CONNECTION_IFACE) conn_type = self._router_main.send_and_get_reply(Properties(conn_addr).get('Type')).body[0][1] if conn_type == '802-11-wireless': From d977a5bd627292c4329e1fd07ac5483aa3750ab2 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Wed, 11 Feb 2026 15:13:30 -0800 Subject: [PATCH 002/311] ui: reduce wifi dbus calls during scanning pt. 2 (#37171) one GetAll instead of 2 calls per wifi activeconnection can't trust anyone these days --- system/ui/lib/wifi_manager.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index 1d1dfc1145..9d06580334 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -531,10 +531,10 @@ class WifiManager: self._current_network_metered = MeteredType.UNKNOWN for active_conn in active_conns: conn_addr = DBusAddress(active_conn, bus_name=NM, interface=NM_ACTIVE_CONNECTION_IFACE) - conn_type = self._router_main.send_and_get_reply(Properties(conn_addr).get('Type')).body[0][1] + props = self._router_main.send_and_get_reply(Properties(conn_addr).get_all()).body[0] - if conn_type == '802-11-wireless': - conn_path = self._router_main.send_and_get_reply(Properties(conn_addr).get('Connection')).body[0][1] + if props.get('Type', ('s', ''))[1] == '802-11-wireless': + conn_path = props.get('Connection', ('o', '/'))[1] if conn_path == "/": continue @@ -642,9 +642,9 @@ class WifiManager: for conn_path in active_conns: conn_addr = DBusAddress(conn_path, bus_name=NM, interface=NM_ACTIVE_CONNECTION_IFACE) - conn_type = self._router_main.send_and_get_reply(Properties(conn_addr).get('Type')).body[0][1] - if conn_type == '802-11-wireless': - ip4config_path = self._router_main.send_and_get_reply(Properties(conn_addr).get('Ip4Config')).body[0][1] + props = self._router_main.send_and_get_reply(Properties(conn_addr).get_all()).body[0] + if props.get('Type', ('s', ''))[1] == '802-11-wireless': + ip4config_path = props.get('Ip4Config', ('o', '/'))[1] if ip4config_path != "/": ip4config_addr = DBusAddress(ip4config_path, bus_name=NM, interface=NM_IP4_CONFIG_IFACE) From cddc3b9e8f69f28a23e165c0c138c8880691ac9b Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Wed, 11 Feb 2026 15:25:53 -0800 Subject: [PATCH 003/311] Reduce wifi dbus calls pt. 3 (#37172) * fewer calls to set metered * print * hell yeah * Revert "hell yeah" This reverts commit 0e0786bc204821329febc62a1b8dfd870e9aeb6e. * Revert "print" This reverts commit e9c7550496e9835888cb71c7dd622cbfb4624fbf. --- system/ui/lib/wifi_manager.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index 9d06580334..8cc99c620d 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -555,10 +555,10 @@ class WifiManager: def worker(): for active_conn in self._get_active_connections(): conn_addr = DBusAddress(active_conn, bus_name=NM, interface=NM_ACTIVE_CONNECTION_IFACE) - conn_type = self._router_main.send_and_get_reply(Properties(conn_addr).get('Type')).body[0][1] + props = self._router_main.send_and_get_reply(Properties(conn_addr).get_all()).body[0] - if conn_type == '802-11-wireless' and not self.is_tethering_active(): - conn_path = self._router_main.send_and_get_reply(Properties(conn_addr).get('Connection')).body[0][1] + if props.get('Type', ('s', ''))[1] == '802-11-wireless' and not self.is_tethering_active(): + conn_path = props.get('Connection', ('o', '/'))[1] if conn_path == "/": continue From f03efab907f4328c05fd05451ac4c067bbd3e8e5 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Wed, 11 Feb 2026 16:30:40 -0800 Subject: [PATCH 004/311] Reduce wifi dbus calls pt. 4 (#37174) * combine active AP and all APs into getall * combine these two functions reducing more calls * little clean up * down here --- system/ui/lib/wifi_manager.py | 61 +++++++++++++++-------------------- 1 file changed, 26 insertions(+), 35 deletions(-) diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index 8cc99c620d..8912d2fd9a 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -527,30 +527,6 @@ class WifiManager: threading.Thread(target=worker, daemon=True).start() - def _update_current_network_metered(self, active_conns) -> None: - self._current_network_metered = MeteredType.UNKNOWN - for active_conn in active_conns: - conn_addr = DBusAddress(active_conn, bus_name=NM, interface=NM_ACTIVE_CONNECTION_IFACE) - props = self._router_main.send_and_get_reply(Properties(conn_addr).get_all()).body[0] - - if props.get('Type', ('s', ''))[1] == '802-11-wireless': - conn_path = props.get('Connection', ('o', '/'))[1] - if conn_path == "/": - continue - - settings = self._get_connection_settings(conn_path) - - if len(settings) == 0: - cloudlog.warning(f'Failed to get connection settings for {conn_path}') - continue - - metered_prop = settings['connection'].get('metered', ('i', 0))[1] - if metered_prop == MeteredType.YES: - self._current_network_metered = MeteredType.YES - elif metered_prop == MeteredType.NO: - self._current_network_metered = MeteredType.NO - return - def set_current_network_metered(self, metered: MeteredType): def worker(): for active_conn in self._get_active_connections(): @@ -595,10 +571,11 @@ class WifiManager: cloudlog.warning("No WiFi device found") return - # returns '/' if no active AP + # NOTE: AccessPoints property may exclude hidden APs (use GetAllAccessPoints method if needed) wifi_addr = DBusAddress(self._wifi_device, NM, interface=NM_WIRELESS_IFACE) - active_ap_path = self._router_main.send_and_get_reply(Properties(wifi_addr).get('ActiveAccessPoint')).body[0][1] - ap_paths = self._router_main.send_and_get_reply(new_method_call(wifi_addr, 'GetAllAccessPoints')).body[0] + wifi_props = self._router_main.send_and_get_reply(Properties(wifi_addr).get_all()).body[0] + active_ap_path = wifi_props.get('ActiveAccessPoint', ('o', '/'))[1] + ap_paths = wifi_props.get('AccessPoints', ('ao', []))[1] aps: dict[str, list[AccessPoint]] = {} @@ -630,20 +607,20 @@ class WifiManager: networks.sort(key=lambda n: (-n.is_connected, -n.is_saved, -round(n.strength / 100 * 2), n.ssid.lower())) self._networks = networks - # Get active connections once - active_conns = self._get_active_connections() - self._update_ipv4_address(active_conns) - self._update_current_network_metered(active_conns) + self._update_active_connection_info() self._enqueue_callbacks(self._networks_updated, self._networks) - def _update_ipv4_address(self, active_conns): + def _update_active_connection_info(self): self._ipv4_address = "" + self._current_network_metered = MeteredType.UNKNOWN - for conn_path in active_conns: - conn_addr = DBusAddress(conn_path, bus_name=NM, interface=NM_ACTIVE_CONNECTION_IFACE) + for active_conn in self._get_active_connections(): + conn_addr = DBusAddress(active_conn, bus_name=NM, interface=NM_ACTIVE_CONNECTION_IFACE) props = self._router_main.send_and_get_reply(Properties(conn_addr).get_all()).body[0] + if props.get('Type', ('s', ''))[1] == '802-11-wireless': + # IPv4 address ip4config_path = props.get('Ip4Config', ('o', '/'))[1] if ip4config_path != "/": @@ -653,7 +630,21 @@ class WifiManager: for entry in address_data: if 'address' in entry: self._ipv4_address = entry['address'][1] - return + break + + # Metered status + conn_path = props.get('Connection', ('o', '/'))[1] + if conn_path != "/": + settings = self._get_connection_settings(conn_path) + + if len(settings) > 0: + metered_prop = settings['connection'].get('metered', ('i', 0))[1] + + if metered_prop == MeteredType.YES: + self._current_network_metered = MeteredType.YES + elif metered_prop == MeteredType.NO: + self._current_network_metered = MeteredType.NO + return def __del__(self): self.stop() From 0072449b015b394040b0e345634086331fc7198c Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Wed, 11 Feb 2026 17:07:41 -0800 Subject: [PATCH 005/311] WifiManager: cache connections until new/removed connection (#37175) * new/removed conns signal * clean up * only get connections when adding/removing not every refresh * add debug * block * Revert "block" This reverts commit 30bbffca8d2db21c53d7a3601ae46bf05e2a7cd5. * rm debug * block on any new message, faster conn rem/add reaction * better names --- system/ui/lib/wifi_manager.py | 91 ++++++++++++++++++++++------------- 1 file changed, 58 insertions(+), 33 deletions(-) diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index 8912d2fd9a..aa486a8c03 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -146,6 +146,7 @@ class WifiManager: self._wifi_device: str | None = None # State + self._connections: dict[str, str] = {} # ssid -> connection path, updated via NM signals self._connecting_to_ssid: str = "" self._ipv4_address: str = "" self._current_network_metered: MeteredType = MeteredType.UNKNOWN @@ -181,7 +182,8 @@ class WifiManager: self._scan_thread.start() self._state_thread.start() - if Params is not None and self._tethering_ssid not in self._get_connections(): + self._update_connections() + if Params is not None and self._tethering_ssid not in self._connections: self._add_tethering_connection() self._tethering_password = self._get_tethering_password() @@ -235,45 +237,69 @@ class WifiManager: self._last_network_update = 0.0 def _monitor_state(self): - rule = MatchRule( - type="signal", - interface=NM_DEVICE_IFACE, - member="StateChanged", - path=self._wifi_device, + # Filter for signals + rules = ( + MatchRule( + type="signal", + interface=NM_DEVICE_IFACE, + member="StateChanged", + path=self._wifi_device, + ), + MatchRule( + type="signal", + interface=NM_SETTINGS_IFACE, + member="NewConnection", + path=NM_SETTINGS_PATH, + ), + MatchRule( + type="signal", + interface=NM_SETTINGS_IFACE, + member="ConnectionRemoved", + path=NM_SETTINGS_PATH, + ) ) - # Filter for StateChanged signal - self._conn_monitor.send_and_get_reply(message_bus.AddMatch(rule)) + for rule in rules: + self._conn_monitor.send_and_get_reply(message_bus.AddMatch(rule)) - with self._conn_monitor.filter(rule, bufsize=SIGNAL_QUEUE_SIZE) as q: + with (self._conn_monitor.filter(rules[0], bufsize=SIGNAL_QUEUE_SIZE) as state_q, + self._conn_monitor.filter(rules[1], bufsize=SIGNAL_QUEUE_SIZE) as new_conn_q, + self._conn_monitor.filter(rules[2], bufsize=SIGNAL_QUEUE_SIZE) as removed_conn_q): while not self._exit: if not self._active: time.sleep(1) continue - # Block until a matching signal arrives try: - msg = self._conn_monitor.recv_until_filtered(q, timeout=1) + self._conn_monitor.recv_messages(timeout=1) except TimeoutError: continue - new_state, previous_state, change_reason = msg.body + # Connection added/removed + if len(new_conn_q) or len(removed_conn_q): + new_conn_q.clear() + removed_conn_q.clear() + self._update_connections() - # BAD PASSWORD - if new_state == NMDeviceState.NEED_AUTH and change_reason == NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT and len(self._connecting_to_ssid): - self.forget_connection(self._connecting_to_ssid, block=True) - self._enqueue_callbacks(self._need_auth, self._connecting_to_ssid) - self._connecting_to_ssid = "" + # Device state changes + while len(state_q): + new_state, previous_state, change_reason = state_q.popleft().body - elif new_state == NMDeviceState.ACTIVATED: - if len(self._activated): - self._update_networks() - self._enqueue_callbacks(self._activated) - self._connecting_to_ssid = "" + # BAD PASSWORD + if new_state == NMDeviceState.NEED_AUTH and change_reason == NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT and len(self._connecting_to_ssid): + self.forget_connection(self._connecting_to_ssid, block=True) + self._enqueue_callbacks(self._need_auth, self._connecting_to_ssid) + self._connecting_to_ssid = "" - elif new_state == NMDeviceState.DISCONNECTED and change_reason != NM_DEVICE_STATE_REASON_NEW_ACTIVATION: - self._connecting_to_ssid = "" - self._enqueue_callbacks(self._forgotten) + elif new_state == NMDeviceState.ACTIVATED: + if len(self._activated): + self._update_networks() + self._enqueue_callbacks(self._activated) + self._connecting_to_ssid = "" + + elif new_state == NMDeviceState.DISCONNECTED and change_reason != NM_DEVICE_STATE_REASON_NEW_ACTIVATION: + self._connecting_to_ssid = "" + self._enqueue_callbacks(self._forgotten) def _network_scanner(self): while not self._exit: @@ -307,7 +333,7 @@ class WifiManager: cloudlog.exception(f"Error getting adapter type {adapter_type}: {e}") return None - def _get_connections(self) -> dict[str, str]: + def _update_connections(self) -> None: settings_addr = DBusAddress(NM_SETTINGS_PATH, bus_name=NM, interface=NM_SETTINGS_IFACE) known_connections = self._router_main.send_and_get_reply(new_method_call(settings_addr, 'ListConnections')).body[0] @@ -323,7 +349,7 @@ class WifiManager: ssid = settings['802-11-wireless']['ssid'][1].decode("utf-8", "replace") if ssid != "": conns[ssid] = conn_path - return conns + self._connections = conns def _get_active_connections(self): return self._router_main.send_and_get_reply(Properties(self._nm).get('ActiveConnections')).body[0][1] @@ -413,7 +439,7 @@ class WifiManager: def forget_connection(self, ssid: str, block: bool = False): def worker(): - conn_path = self._get_connections().get(ssid, None) + conn_path = self._connections.get(ssid, None) if conn_path is not None: conn_addr = DBusAddress(conn_path, bus_name=NM, interface=NM_CONNECTION_IFACE) self._router_main.send_and_get_reply(new_method_call(conn_addr, 'Delete')) @@ -429,7 +455,7 @@ class WifiManager: def activate_connection(self, ssid: str, block: bool = False): def worker(): - conn_path = self._get_connections().get(ssid, None) + conn_path = self._connections.get(ssid, None) if conn_path is not None: if self._wifi_device is None: cloudlog.warning("No WiFi device found") @@ -465,7 +491,7 @@ class WifiManager: def set_tethering_password(self, password: str): def worker(): - conn_path = self._get_connections().get(self._tethering_ssid, None) + conn_path = self._connections.get(self._tethering_ssid, None) if conn_path is None: cloudlog.warning('No tethering connection found') return @@ -490,7 +516,7 @@ class WifiManager: threading.Thread(target=worker, daemon=True).start() def _get_tethering_password(self) -> str: - conn_path = self._get_connections().get(self._tethering_ssid, None) + conn_path = self._connections.get(self._tethering_ssid, None) if conn_path is None: cloudlog.warning('No tethering connection found') return '' @@ -601,8 +627,7 @@ class WifiManager: # catch all for parsing errors cloudlog.exception(f"Failed to parse AP properties for {ap_path}") - known_connections = self._get_connections() - networks = [Network.from_dbus(ssid, ap_list, ssid in known_connections) for ssid, ap_list in aps.items()] + networks = [Network.from_dbus(ssid, ap_list, ssid in self._connections) for ssid, ap_list in aps.items()] # sort with quantized strength to reduce jumping networks.sort(key=lambda n: (-n.is_connected, -n.is_saved, -round(n.strength / 100 * 2), n.ssid.lower())) self._networks = networks From 6cf95918c57671cb2266b72c890217c5170e3840 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Wed, 11 Feb 2026 23:02:07 -0800 Subject: [PATCH 006/311] WifiManager: clean up connections (#37179) * fix recent connect regression from connection not being known yet * always update connections in background, keep track via signals only. no getallconnections each time one is added/deleted. matches c++ * works * clean up * clean up * clean up --- system/ui/lib/wifi_manager.py | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index aa486a8c03..1c80fa28d6 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -182,7 +182,7 @@ class WifiManager: self._scan_thread.start() self._state_thread.start() - self._update_connections() + self._init_connections() if Params is not None and self._tethering_ssid not in self._connections: self._add_tethering_connection() @@ -266,20 +266,18 @@ class WifiManager: self._conn_monitor.filter(rules[1], bufsize=SIGNAL_QUEUE_SIZE) as new_conn_q, self._conn_monitor.filter(rules[2], bufsize=SIGNAL_QUEUE_SIZE) as removed_conn_q): while not self._exit: - if not self._active: - time.sleep(1) - continue - try: self._conn_monitor.recv_messages(timeout=1) except TimeoutError: continue # Connection added/removed - if len(new_conn_q) or len(removed_conn_q): - new_conn_q.clear() - removed_conn_q.clear() - self._update_connections() + while len(removed_conn_q): + conn_path = removed_conn_q.popleft().body[0] + self._connection_removed(conn_path) + while len(new_conn_q): + conn_path = new_conn_q.popleft().body[0] + self._new_connection(conn_path) # Device state changes while len(state_q): @@ -333,7 +331,7 @@ class WifiManager: cloudlog.exception(f"Error getting adapter type {adapter_type}: {e}") return None - def _update_connections(self) -> None: + def _init_connections(self) -> None: settings_addr = DBusAddress(NM_SETTINGS_PATH, bus_name=NM, interface=NM_SETTINGS_IFACE) known_connections = self._router_main.send_and_get_reply(new_method_call(settings_addr, 'ListConnections')).body[0] @@ -351,6 +349,19 @@ class WifiManager: conns[ssid] = conn_path self._connections = conns + def _new_connection(self, conn_path: str): + settings = self._get_connection_settings(conn_path) + + if "802-11-wireless" in settings: + ssid = settings['802-11-wireless']['ssid'][1].decode("utf-8", "replace") + if ssid != "": + self._connections[ssid] = conn_path + if ssid != self._tethering_ssid: + self.activate_connection(ssid, block=True) + + def _connection_removed(self, conn_path: str): + self._connections = {ssid: path for ssid, path in self._connections.items() if path != conn_path} + def _get_active_connections(self): return self._router_main.send_and_get_reply(Properties(self._nm).get('ActiveConnections')).body[0][1] @@ -433,7 +444,6 @@ class WifiManager: settings_addr = DBusAddress(NM_SETTINGS_PATH, bus_name=NM, interface=NM_SETTINGS_IFACE) self._router_main.send_and_get_reply(new_method_call(settings_addr, 'AddConnection', 'a{sa{sv}}', (connection,))) - self.activate_connection(ssid, block=True) threading.Thread(target=worker, daemon=True).start() @@ -592,6 +602,9 @@ class WifiManager: cloudlog.warning(f"Failed to request scan: {reply}") def _update_networks(self): + if not self._active: + return + with self._lock: if self._wifi_device is None: cloudlog.warning("No WiFi device found") From b084294dc0384e3cc86832e5591dc3340ed017c6 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Wed, 11 Feb 2026 23:05:04 -0800 Subject: [PATCH 007/311] incorrect -> wrong --- selfdrive/ui/mici/layouts/settings/network/wifi_ui.py | 2 +- 1 file changed, 1 insertion(+), 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 db68632c0e..955dc4bfaf 100644 --- a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py +++ b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py @@ -425,7 +425,7 @@ class WifiUIMici(BigMultiOptionDialog): self._on_need_auth(network.ssid, False) def _on_need_auth(self, ssid, incorrect_password=True): - hint = "incorrect password..." if incorrect_password else "enter password..." + hint = "wrong password..." if incorrect_password else "enter password..." dlg = BigInputDialog(hint, "", minimum_length=8, confirm_callback=lambda _password: self._connect_with_password(ssid, _password)) # go back to the manage network page From 13b71b4e81825370828af81b02cda28033fe247b Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Wed, 11 Feb 2026 23:14:13 -0800 Subject: [PATCH 008/311] WifiManager: update networks on scan (#37177) * like c++ wifiman * rename to scan * can do this can do this * Revert "can do this" This reverts commit 295f7f49d448c6aacdde2ef810904df86357840b. * kinda useless now * clean up --- system/ui/lib/wifi_manager.py | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index 1c80fa28d6..833a204cbb 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -25,7 +25,7 @@ from openpilot.system.ui.lib.networkmanager import (NM, NM_WIRELESS_IFACE, NM_80 NM_SETTINGS_IFACE, NM_CONNECTION_IFACE, NM_DEVICE_IFACE, NM_DEVICE_TYPE_WIFI, NM_DEVICE_TYPE_MODEM, NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT, NM_DEVICE_STATE_REASON_NEW_ACTIVATION, NM_ACTIVE_CONNECTION_IFACE, - NM_IP4_CONFIG_IFACE, NMDeviceState) + NM_IP4_CONFIG_IFACE, NM_PROPERTIES_IFACE, NMDeviceState) try: from openpilot.common.params import Params @@ -153,7 +153,7 @@ class WifiManager: self._tethering_password: str = "" self._ipv4_forward = False - self._last_network_update: float = 0.0 + self._last_network_scan: float = 0.0 self._callback_queue: list[Callable] = [] self._tethering_ssid = "weedle" @@ -232,10 +232,6 @@ class WifiManager: def set_active(self, active: bool): self._active = active - # Scan immediately if we haven't scanned in a while - if active and time.monotonic() - self._last_network_update > SCAN_PERIOD_SECONDS / 2: - self._last_network_update = 0.0 - def _monitor_state(self): # Filter for signals rules = ( @@ -256,7 +252,13 @@ class WifiManager: interface=NM_SETTINGS_IFACE, member="ConnectionRemoved", path=NM_SETTINGS_PATH, - ) + ), + MatchRule( + type="signal", + interface=NM_PROPERTIES_IFACE, + member="PropertiesChanged", + path=self._wifi_device, + ), ) for rule in rules: @@ -264,7 +266,8 @@ class WifiManager: with (self._conn_monitor.filter(rules[0], bufsize=SIGNAL_QUEUE_SIZE) as state_q, self._conn_monitor.filter(rules[1], bufsize=SIGNAL_QUEUE_SIZE) as new_conn_q, - self._conn_monitor.filter(rules[2], bufsize=SIGNAL_QUEUE_SIZE) as removed_conn_q): + self._conn_monitor.filter(rules[2], bufsize=SIGNAL_QUEUE_SIZE) as removed_conn_q, + self._conn_monitor.filter(rules[3], bufsize=SIGNAL_QUEUE_SIZE) as props_q): while not self._exit: try: self._conn_monitor.recv_messages(timeout=1) @@ -279,6 +282,12 @@ class WifiManager: conn_path = new_conn_q.popleft().body[0] self._new_connection(conn_path) + # PropertiesChanged on wifi device (LastScan = scan complete) + while len(props_q): + iface, changed, _ = props_q.popleft().body + if iface == NM_WIRELESS_IFACE and 'LastScan' in changed: + self._update_networks() + # Device state changes while len(state_q): new_state, previous_state, change_reason = state_q.popleft().body @@ -302,12 +311,9 @@ class WifiManager: def _network_scanner(self): while not self._exit: if self._active: - if time.monotonic() - self._last_network_update > SCAN_PERIOD_SECONDS: - # Scan for networks every 10 seconds - # TODO: should update when scan is complete (PropertiesChanged), but this is more than good enough for now - self._update_networks() + if time.monotonic() - self._last_network_scan > SCAN_PERIOD_SECONDS: self._request_scan() - self._last_network_update = time.monotonic() + self._last_network_scan = time.monotonic() time.sleep(1 / 2.) def _wait_for_wifi_device(self): From a46007d84d024bc699e3db8abd91c7ce0aff10b2 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Wed, 11 Feb 2026 23:14:38 -0800 Subject: [PATCH 009/311] WifiManager: safeguard an error response (#37182) safeguard --- system/ui/lib/wifi_manager.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index 833a204cbb..3d4da444be 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -661,7 +661,13 @@ class WifiManager: for active_conn in self._get_active_connections(): conn_addr = DBusAddress(active_conn, bus_name=NM, interface=NM_ACTIVE_CONNECTION_IFACE) - props = self._router_main.send_and_get_reply(Properties(conn_addr).get_all()).body[0] + reply = self._router_main.send_and_get_reply(Properties(conn_addr).get_all()) + + if reply.header.message_type == MessageType.error: + cloudlog.warning(f"Failed to get active connection properties for {active_conn}: {reply}") + continue + + props = reply.body[0] if props.get('Type', ('s', ''))[1] == '802-11-wireless': # IPv4 address From af1583cdfc3f99fffefb1cc15320959d213239a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Harald=20Sch=C3=A4fer?= Date: Thu, 12 Feb 2026 08:59:19 -0800 Subject: [PATCH 010/311] Reapply tgwarp w NV12 fix (#37168) * Revert "Revert tgwarp again (#37161)" This reverts commit 45099e7fcd384bb2dac36b55a6c10ba9de58a1e5. * Weird uv sizes * Fix interleaving * Fix on CPU * make CPU safe * Prevent corruption without clone * Claude knows speeed * fix interleaving * less kernels * blob caching * This is still slightly faster * Comment for blob cache --- Dockerfile.openpilot_base | 37 - SConstruct | 1 - common/SConscript | 1 - common/clutil.cc | 98 -- common/clutil.h | 28 - common/mat.h | 85 - msgq_repo | 2 +- selfdrive/modeld/SConscript | 57 +- selfdrive/modeld/compile_warp.py | 209 +++ selfdrive/modeld/dmonitoringmodeld.py | 39 +- selfdrive/modeld/modeld.py | 68 +- selfdrive/modeld/models/commonmodel.cc | 64 - selfdrive/modeld/models/commonmodel.h | 97 -- selfdrive/modeld/models/commonmodel.pxd | 27 - selfdrive/modeld/models/commonmodel_pyx.pxd | 13 - selfdrive/modeld/models/commonmodel_pyx.pyx | 74 - selfdrive/modeld/runners/tinygrad_helpers.py | 8 - selfdrive/modeld/transforms/loadyuv.cc | 76 - selfdrive/modeld/transforms/loadyuv.cl | 47 - selfdrive/modeld/transforms/loadyuv.h | 20 - selfdrive/modeld/transforms/transform.cc | 97 -- selfdrive/modeld/transforms/transform.cl | 54 - selfdrive/modeld/transforms/transform.h | 25 - selfdrive/test/process_replay/model_replay.py | 4 +- .../test/process_replay/process_replay.py | 17 +- system/camerad/SConscript | 2 +- system/camerad/cameras/camera_common.cc | 5 +- system/camerad/cameras/camera_common.h | 2 +- system/camerad/cameras/camera_qcom2.cc | 22 +- system/camerad/cameras/spectra.cc | 4 +- system/camerad/cameras/spectra.h | 2 +- system/hardware/tici/tests/test_power_draw.py | 2 +- system/loggerd/SConscript | 5 +- third_party/opencl/include/CL/cl.h | 1452 ----------------- third_party/opencl/include/CL/cl_d3d10.h | 131 -- third_party/opencl/include/CL/cl_d3d11.h | 131 -- .../opencl/include/CL/cl_dx9_media_sharing.h | 132 -- third_party/opencl/include/CL/cl_egl.h | 136 -- third_party/opencl/include/CL/cl_ext.h | 391 ----- third_party/opencl/include/CL/cl_ext_qcom.h | 255 --- third_party/opencl/include/CL/cl_gl.h | 167 -- third_party/opencl/include/CL/cl_gl_ext.h | 74 - third_party/opencl/include/CL/cl_platform.h | 1333 --------------- third_party/opencl/include/CL/opencl.h | 59 - tools/cabana/SConscript | 2 - tools/install_ubuntu_dependencies.sh | 3 - tools/replay/SConscript | 5 - tools/webcam/README.md | 4 - 48 files changed, 327 insertions(+), 5240 deletions(-) delete mode 100644 common/clutil.cc delete mode 100644 common/clutil.h delete mode 100644 common/mat.h create mode 100755 selfdrive/modeld/compile_warp.py delete mode 100644 selfdrive/modeld/models/commonmodel.cc delete mode 100644 selfdrive/modeld/models/commonmodel.h delete mode 100644 selfdrive/modeld/models/commonmodel.pxd delete mode 100644 selfdrive/modeld/models/commonmodel_pyx.pxd delete mode 100644 selfdrive/modeld/models/commonmodel_pyx.pyx delete mode 100644 selfdrive/modeld/runners/tinygrad_helpers.py delete mode 100644 selfdrive/modeld/transforms/loadyuv.cc delete mode 100644 selfdrive/modeld/transforms/loadyuv.cl delete mode 100644 selfdrive/modeld/transforms/loadyuv.h delete mode 100644 selfdrive/modeld/transforms/transform.cc delete mode 100644 selfdrive/modeld/transforms/transform.cl delete mode 100644 selfdrive/modeld/transforms/transform.h delete mode 100644 third_party/opencl/include/CL/cl.h delete mode 100644 third_party/opencl/include/CL/cl_d3d10.h delete mode 100644 third_party/opencl/include/CL/cl_d3d11.h delete mode 100644 third_party/opencl/include/CL/cl_dx9_media_sharing.h delete mode 100644 third_party/opencl/include/CL/cl_egl.h delete mode 100644 third_party/opencl/include/CL/cl_ext.h delete mode 100644 third_party/opencl/include/CL/cl_ext_qcom.h delete mode 100644 third_party/opencl/include/CL/cl_gl.h delete mode 100644 third_party/opencl/include/CL/cl_gl_ext.h delete mode 100644 third_party/opencl/include/CL/cl_platform.h delete mode 100644 third_party/opencl/include/CL/opencl.h diff --git a/Dockerfile.openpilot_base b/Dockerfile.openpilot_base index 44d8d95e95..8a60412993 100644 --- a/Dockerfile.openpilot_base +++ b/Dockerfile.openpilot_base @@ -18,43 +18,6 @@ RUN /tmp/tools/install_ubuntu_dependencies.sh && \ cd /usr/lib/gcc/arm-none-eabi/* && \ rm -rf arm/ thumb/nofp thumb/v6* thumb/v8* thumb/v7+fp thumb/v7-r+fp.sp -# Add OpenCL -RUN apt-get update && apt-get install -y --no-install-recommends \ - apt-utils \ - alien \ - unzip \ - tar \ - curl \ - xz-utils \ - dbus \ - gcc-arm-none-eabi \ - tmux \ - vim \ - libx11-6 \ - wget \ - && rm -rf /var/lib/apt/lists/* - -RUN mkdir -p /tmp/opencl-driver-intel && \ - cd /tmp/opencl-driver-intel && \ - wget https://github.com/intel/llvm/releases/download/2024-WW14/oclcpuexp-2024.17.3.0.09_rel.tar.gz && \ - wget https://github.com/oneapi-src/oneTBB/releases/download/v2021.12.0/oneapi-tbb-2021.12.0-lin.tgz && \ - mkdir -p /opt/intel/oclcpuexp_2024.17.3.0.09_rel && \ - cd /opt/intel/oclcpuexp_2024.17.3.0.09_rel && \ - tar -zxvf /tmp/opencl-driver-intel/oclcpuexp-2024.17.3.0.09_rel.tar.gz && \ - mkdir -p /etc/OpenCL/vendors && \ - echo /opt/intel/oclcpuexp_2024.17.3.0.09_rel/x64/libintelocl.so > /etc/OpenCL/vendors/intel_expcpu.icd && \ - cd /opt/intel && \ - tar -zxvf /tmp/opencl-driver-intel/oneapi-tbb-2021.12.0-lin.tgz && \ - ln -s /opt/intel/oneapi-tbb-2021.12.0/lib/intel64/gcc4.8/libtbb.so /opt/intel/oclcpuexp_2024.17.3.0.09_rel/x64 && \ - ln -s /opt/intel/oneapi-tbb-2021.12.0/lib/intel64/gcc4.8/libtbbmalloc.so /opt/intel/oclcpuexp_2024.17.3.0.09_rel/x64 && \ - ln -s /opt/intel/oneapi-tbb-2021.12.0/lib/intel64/gcc4.8/libtbb.so.12 /opt/intel/oclcpuexp_2024.17.3.0.09_rel/x64 && \ - ln -s /opt/intel/oneapi-tbb-2021.12.0/lib/intel64/gcc4.8/libtbbmalloc.so.2 /opt/intel/oclcpuexp_2024.17.3.0.09_rel/x64 && \ - mkdir -p /etc/ld.so.conf.d && \ - echo /opt/intel/oclcpuexp_2024.17.3.0.09_rel/x64 > /etc/ld.so.conf.d/libintelopenclexp.conf && \ - ldconfig -f /etc/ld.so.conf.d/libintelopenclexp.conf && \ - cd / && \ - rm -rf /tmp/opencl-driver-intel - ENV NVIDIA_VISIBLE_DEVICES=all ENV NVIDIA_DRIVER_CAPABILITIES=graphics,utility,compute ENV QTWEBENGINE_DISABLE_SANDBOX=1 diff --git a/SConstruct b/SConstruct index ca5b7b6cb7..4f04be624c 100644 --- a/SConstruct +++ b/SConstruct @@ -94,7 +94,6 @@ env = Environment( # Arch-specific flags and paths if arch == "larch64": - env.Append(CPPPATH=["#third_party/opencl/include"]) env.Append(LIBPATH=[ "/usr/local/lib", "/system/vendor/lib64", diff --git a/common/SConscript b/common/SConscript index 1c68cf05c7..15a0e5eff1 100644 --- a/common/SConscript +++ b/common/SConscript @@ -5,7 +5,6 @@ common_libs = [ 'swaglog.cc', 'util.cc', 'ratekeeper.cc', - 'clutil.cc', ] _common = env.Library('common', common_libs, LIBS="json11") diff --git a/common/clutil.cc b/common/clutil.cc deleted file mode 100644 index f8381a7e09..0000000000 --- a/common/clutil.cc +++ /dev/null @@ -1,98 +0,0 @@ -#include "common/clutil.h" - -#include -#include -#include - -#include "common/util.h" -#include "common/swaglog.h" - -namespace { // helper functions - -template -std::string get_info(Func get_info_func, Id id, Name param_name) { - size_t size = 0; - CL_CHECK(get_info_func(id, param_name, 0, NULL, &size)); - std::string info(size, '\0'); - CL_CHECK(get_info_func(id, param_name, size, info.data(), NULL)); - return info; -} -inline std::string get_platform_info(cl_platform_id id, cl_platform_info name) { return get_info(&clGetPlatformInfo, id, name); } -inline std::string get_device_info(cl_device_id id, cl_device_info name) { return get_info(&clGetDeviceInfo, id, name); } - -void cl_print_info(cl_platform_id platform, cl_device_id device) { - size_t work_group_size = 0; - cl_device_type device_type = 0; - clGetDeviceInfo(device, CL_DEVICE_MAX_WORK_GROUP_SIZE, sizeof(work_group_size), &work_group_size, NULL); - clGetDeviceInfo(device, CL_DEVICE_TYPE, sizeof(device_type), &device_type, NULL); - const char *type_str = "Other..."; - switch (device_type) { - case CL_DEVICE_TYPE_CPU: type_str ="CL_DEVICE_TYPE_CPU"; break; - case CL_DEVICE_TYPE_GPU: type_str = "CL_DEVICE_TYPE_GPU"; break; - case CL_DEVICE_TYPE_ACCELERATOR: type_str = "CL_DEVICE_TYPE_ACCELERATOR"; break; - } - - LOGD("vendor: %s", get_platform_info(platform, CL_PLATFORM_VENDOR).c_str()); - LOGD("platform version: %s", get_platform_info(platform, CL_PLATFORM_VERSION).c_str()); - LOGD("profile: %s", get_platform_info(platform, CL_PLATFORM_PROFILE).c_str()); - LOGD("extensions: %s", get_platform_info(platform, CL_PLATFORM_EXTENSIONS).c_str()); - LOGD("name: %s", get_device_info(device, CL_DEVICE_NAME).c_str()); - LOGD("device version: %s", get_device_info(device, CL_DEVICE_VERSION).c_str()); - LOGD("max work group size: %zu", work_group_size); - LOGD("type = %d, %s", (int)device_type, type_str); -} - -void cl_print_build_errors(cl_program program, cl_device_id device) { - cl_build_status status; - clGetProgramBuildInfo(program, device, CL_PROGRAM_BUILD_STATUS, sizeof(status), &status, NULL); - size_t log_size; - clGetProgramBuildInfo(program, device, CL_PROGRAM_BUILD_LOG, 0, NULL, &log_size); - std::string log(log_size, '\0'); - clGetProgramBuildInfo(program, device, CL_PROGRAM_BUILD_LOG, log_size, &log[0], NULL); - - LOGE("build failed; status=%d, log: %s", status, log.c_str()); -} - -} // namespace - -cl_device_id cl_get_device_id(cl_device_type device_type) { - cl_uint num_platforms = 0; - CL_CHECK(clGetPlatformIDs(0, NULL, &num_platforms)); - std::unique_ptr platform_ids = std::make_unique(num_platforms); - CL_CHECK(clGetPlatformIDs(num_platforms, &platform_ids[0], NULL)); - - for (size_t i = 0; i < num_platforms; ++i) { - LOGD("platform[%zu] CL_PLATFORM_NAME: %s", i, get_platform_info(platform_ids[i], CL_PLATFORM_NAME).c_str()); - - // Get first device - if (cl_device_id device_id = NULL; clGetDeviceIDs(platform_ids[i], device_type, 1, &device_id, NULL) == 0 && device_id) { - cl_print_info(platform_ids[i], device_id); - return device_id; - } - } - LOGE("No valid openCL platform found"); - assert(0); - return nullptr; -} - -cl_context cl_create_context(cl_device_id device_id) { - return CL_CHECK_ERR(clCreateContext(NULL, 1, &device_id, NULL, NULL, &err)); -} - -void cl_release_context(cl_context context) { - clReleaseContext(context); -} - -cl_program cl_program_from_file(cl_context ctx, cl_device_id device_id, const char* path, const char* args) { - return cl_program_from_source(ctx, device_id, util::read_file(path), args); -} - -cl_program cl_program_from_source(cl_context ctx, cl_device_id device_id, const std::string& src, const char* args) { - const char *csrc = src.c_str(); - cl_program prg = CL_CHECK_ERR(clCreateProgramWithSource(ctx, 1, &csrc, NULL, &err)); - if (int err = clBuildProgram(prg, 1, &device_id, args, NULL, NULL); err != 0) { - cl_print_build_errors(prg, device_id); - assert(0); - } - return prg; -} diff --git a/common/clutil.h b/common/clutil.h deleted file mode 100644 index b364e79d45..0000000000 --- a/common/clutil.h +++ /dev/null @@ -1,28 +0,0 @@ -#pragma once - -#ifdef __APPLE__ -#include -#else -#include -#endif - -#include - -#define CL_CHECK(_expr) \ - do { \ - assert(CL_SUCCESS == (_expr)); \ - } while (0) - -#define CL_CHECK_ERR(_expr) \ - ({ \ - cl_int err = CL_INVALID_VALUE; \ - __typeof__(_expr) _ret = _expr; \ - assert(_ret&& err == CL_SUCCESS); \ - _ret; \ - }) - -cl_device_id cl_get_device_id(cl_device_type device_type); -cl_context cl_create_context(cl_device_id device_id); -void cl_release_context(cl_context context); -cl_program cl_program_from_source(cl_context ctx, cl_device_id device_id, const std::string& src, const char* args = nullptr); -cl_program cl_program_from_file(cl_context ctx, cl_device_id device_id, const char* path, const char* args); diff --git a/common/mat.h b/common/mat.h deleted file mode 100644 index 8e10d61971..0000000000 --- a/common/mat.h +++ /dev/null @@ -1,85 +0,0 @@ -#pragma once - -typedef struct vec3 { - float v[3]; -} vec3; - -typedef struct vec4 { - float v[4]; -} vec4; - -typedef struct mat3 { - float v[3*3]; -} mat3; - -typedef struct mat4 { - float v[4*4]; -} mat4; - -static inline mat3 matmul3(const mat3 &a, const mat3 &b) { - mat3 ret = {{0.0}}; - for (int r=0; r<3; r++) { - for (int c=0; c<3; c++) { - float v = 0.0; - for (int k=0; k<3; k++) { - v += a.v[r*3+k] * b.v[k*3+c]; - } - ret.v[r*3+c] = v; - } - } - return ret; -} - -static inline vec3 matvecmul3(const mat3 &a, const vec3 &b) { - vec3 ret = {{0.0}}; - for (int r=0; r<3; r++) { - for (int c=0; c<3; c++) { - ret.v[r] += a.v[r*3+c] * b.v[c]; - } - } - return ret; -} - -static inline mat4 matmul(const mat4 &a, const mat4 &b) { - mat4 ret = {{0.0}}; - for (int r=0; r<4; r++) { - for (int c=0; c<4; c++) { - float v = 0.0; - for (int k=0; k<4; k++) { - v += a.v[r*4+k] * b.v[k*4+c]; - } - ret.v[r*4+c] = v; - } - } - return ret; -} - -static inline vec4 matvecmul(const mat4 &a, const vec4 &b) { - vec4 ret = {{0.0}}; - for (int r=0; r<4; r++) { - for (int c=0; c<4; c++) { - ret.v[r] += a.v[r*4+c] * b.v[c]; - } - } - return ret; -} - -// scales the input and output space of a transformation matrix -// that assumes pixel-center origin. -static inline mat3 transform_scale_buffer(const mat3 &in, float s) { - // in_pt = ( transform(out_pt/s + 0.5) - 0.5) * s - - mat3 transform_out = {{ - 1.0f/s, 0.0f, 0.5f, - 0.0f, 1.0f/s, 0.5f, - 0.0f, 0.0f, 1.0f, - }}; - - mat3 transform_in = {{ - s, 0.0f, -0.5f*s, - 0.0f, s, -0.5f*s, - 0.0f, 0.0f, 1.0f, - }}; - - return matmul3(transform_in, matmul3(in, transform_out)); -} diff --git a/msgq_repo b/msgq_repo index 4c4e814ed5..2c191c1a72 160000 --- a/msgq_repo +++ b/msgq_repo @@ -1 +1 @@ -Subproject commit 4c4e814ed592db52431bb53d37f0bb93299e960c +Subproject commit 2c191c1a72ae8119b93b49e1bc12d4a99b751855 diff --git a/selfdrive/modeld/SConscript b/selfdrive/modeld/SConscript index 91f3597447..b13a84f70a 100644 --- a/selfdrive/modeld/SConscript +++ b/selfdrive/modeld/SConscript @@ -1,35 +1,12 @@ import os import glob -Import('env', 'envCython', 'arch', 'cereal', 'messaging', 'common', 'visionipc') +Import('env', 'arch') lenv = env.Clone() -lenvCython = envCython.Clone() -libs = [cereal, messaging, visionipc, common, 'capnp', 'kj', 'pthread'] -frameworks = [] - -common_src = [ - "models/commonmodel.cc", - "transforms/loadyuv.cc", - "transforms/transform.cc", -] - -# OpenCL is a framework on Mac -if arch == "Darwin": - frameworks += ['OpenCL'] -else: - libs += ['OpenCL'] - -# Set path definitions -for pathdef, fn in {'TRANSFORM': 'transforms/transform.cl', 'LOADYUV': 'transforms/loadyuv.cl'}.items(): - for xenv in (lenv, lenvCython): - xenv['CXXFLAGS'].append(f'-D{pathdef}_PATH=\\"{File(fn).abspath}\\"') - -# Compile cython -cython_libs = envCython["LIBS"] + libs -commonmodel_lib = lenv.Library('commonmodel', common_src) -lenvCython.Program('models/commonmodel_pyx.so', 'models/commonmodel_pyx.pyx', LIBS=[commonmodel_lib, *cython_libs], FRAMEWORKS=frameworks) -tinygrad_files = sorted(["#"+x for x in glob.glob(env.Dir("#tinygrad_repo").relpath + "/**", recursive=True, root_dir=env.Dir("#").abspath) if 'pycache' not in x]) +tinygrad_root = env.Dir("#").abspath +tinygrad_files = ["#"+x for x in glob.glob(env.Dir("#tinygrad_repo").relpath + "/**", recursive=True, root_dir=tinygrad_root) + if 'pycache' not in x and os.path.isfile(os.path.join(tinygrad_root, x))] # Get model metadata for model_name in ['driving_vision', 'driving_policy', 'dmonitoring_model']: @@ -38,22 +15,36 @@ for model_name in ['driving_vision', 'driving_policy', 'dmonitoring_model']: cmd = f'python3 {Dir("#selfdrive/modeld").abspath}/get_model_metadata.py {fn}.onnx' lenv.Command(fn + "_metadata.pkl", [fn + ".onnx"] + tinygrad_files + script_files, cmd) +# compile warp +# THREADS=0 is need to prevent bug: https://github.com/tinygrad/tinygrad/issues/14689 +tg_flags = { + 'larch64': 'DEV=QCOM FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0', + 'Darwin': f'DEV=CPU THREADS=0 HOME={os.path.expanduser("~")}', # tinygrad calls brew which needs a $HOME in the env +}.get(arch, 'DEV=CPU CPU_LLVM=1 THREADS=0') +image_flag = { + 'larch64': 'IMAGE=2', +}.get(arch, 'IMAGE=0') +script_files = [File(Dir("#selfdrive/modeld").File("compile_warp.py").abspath)] +cmd = f'{tg_flags} python3 {Dir("#selfdrive/modeld").abspath}/compile_warp.py ' +from openpilot.common.transformations.camera import _ar_ox_fisheye, _os_fisheye +warp_targets = [] +for cam in [_ar_ox_fisheye, _os_fisheye]: + w, h = cam.width, cam.height + warp_targets += [File(f"models/warp_{w}x{h}_tinygrad.pkl").abspath, File(f"models/dm_warp_{w}x{h}_tinygrad.pkl").abspath] +lenv.Command(warp_targets, tinygrad_files + script_files, cmd) + def tg_compile(flags, model_name): pythonpath_string = 'PYTHONPATH="${PYTHONPATH}:' + env.Dir("#tinygrad_repo").abspath + '"' fn = File(f"models/{model_name}").abspath return lenv.Command( fn + "_tinygrad.pkl", [fn + ".onnx"] + tinygrad_files, - f'{pythonpath_string} {flags} python3 {Dir("#tinygrad_repo").abspath}/examples/openpilot/compile3.py {fn}.onnx {fn}_tinygrad.pkl' + f'{pythonpath_string} {flags} {image_flag} python3 {Dir("#tinygrad_repo").abspath}/examples/openpilot/compile3.py {fn}.onnx {fn}_tinygrad.pkl' ) # Compile small models for model_name in ['driving_vision', 'driving_policy', 'dmonitoring_model']: - flags = { - 'larch64': 'DEV=QCOM FLOAT16=1 NOLOCALS=1 IMAGE=2 JIT_BATCH_SIZE=0', - 'Darwin': f'DEV=CPU HOME={os.path.expanduser("~")}', # tinygrad calls brew which needs a $HOME in the env - }.get(arch, 'DEV=CPU CPU_LLVM=1') - tg_compile(flags, model_name) + tg_compile(tg_flags, model_name) # Compile BIG model if USB GPU is available if "USBGPU" in os.environ: diff --git a/selfdrive/modeld/compile_warp.py b/selfdrive/modeld/compile_warp.py new file mode 100755 index 0000000000..75cc65f84c --- /dev/null +++ b/selfdrive/modeld/compile_warp.py @@ -0,0 +1,209 @@ +#!/usr/bin/env python3 +import time +import pickle +import numpy as np +from pathlib import Path +from tinygrad.tensor import Tensor +from tinygrad.helpers import Context +from tinygrad.device import Device +from tinygrad.engine.jit import TinyJit + +from openpilot.system.camerad.cameras.nv12_info import get_nv12_info +from openpilot.common.transformations.model import MEDMODEL_INPUT_SIZE, DM_INPUT_SIZE +from openpilot.common.transformations.camera import _ar_ox_fisheye, _os_fisheye + +MODELS_DIR = Path(__file__).parent / 'models' + +CAMERA_CONFIGS = [ + (_ar_ox_fisheye.width, _ar_ox_fisheye.height), # tici: 1928x1208 + (_os_fisheye.width, _os_fisheye.height), # mici: 1344x760 +] + +UV_SCALE_MATRIX = np.array([[0.5, 0, 0], [0, 0.5, 0], [0, 0, 1]], dtype=np.float32) +UV_SCALE_MATRIX_INV = np.linalg.inv(UV_SCALE_MATRIX) + +IMG_BUFFER_SHAPE = (30, MEDMODEL_INPUT_SIZE[1] // 2, MEDMODEL_INPUT_SIZE[0] // 2) + + +def warp_pkl_path(w, h): + return MODELS_DIR / f'warp_{w}x{h}_tinygrad.pkl' + + +def dm_warp_pkl_path(w, h): + return MODELS_DIR / f'dm_warp_{w}x{h}_tinygrad.pkl' + + +def warp_perspective_tinygrad(src_flat, M_inv, dst_shape, src_shape, stride_pad): + w_dst, h_dst = dst_shape + h_src, w_src = src_shape + + x = Tensor.arange(w_dst).reshape(1, w_dst).expand(h_dst, w_dst).reshape(-1) + y = Tensor.arange(h_dst).reshape(h_dst, 1).expand(h_dst, w_dst).reshape(-1) + + # inline 3x3 matmul as elementwise to avoid reduce op (enables fusion with gather) + src_x = M_inv[0, 0] * x + M_inv[0, 1] * y + M_inv[0, 2] + src_y = M_inv[1, 0] * x + M_inv[1, 1] * y + M_inv[1, 2] + src_w = M_inv[2, 0] * x + M_inv[2, 1] * y + M_inv[2, 2] + + src_x = src_x / src_w + src_y = src_y / src_w + + x_nn_clipped = Tensor.round(src_x).clip(0, w_src - 1).cast('int') + y_nn_clipped = Tensor.round(src_y).clip(0, h_src - 1).cast('int') + idx = y_nn_clipped * (w_src + stride_pad) + x_nn_clipped + + return src_flat[idx] + + +def frames_to_tensor(frames, model_w, model_h): + H = (frames.shape[0] * 2) // 3 + W = frames.shape[1] + in_img1 = Tensor.cat(frames[0:H:2, 0::2], + frames[1:H:2, 0::2], + frames[0:H:2, 1::2], + frames[1:H:2, 1::2], + frames[H:H+H//4].reshape((H//2, W//2)), + frames[H+H//4:H+H//2].reshape((H//2, W//2)), dim=0).reshape((6, H//2, W//2)) + return in_img1 + + +def make_frame_prepare(cam_w, cam_h, model_w, model_h): + stride, y_height, uv_height, _ = get_nv12_info(cam_w, cam_h) + uv_offset = stride * y_height + stride_pad = stride - cam_w + + def frame_prepare_tinygrad(input_frame, M_inv): + # UV_SCALE @ M_inv @ UV_SCALE_INV simplifies to elementwise scaling + M_inv_uv = M_inv * Tensor([[1.0, 1.0, 0.5], [1.0, 1.0, 0.5], [2.0, 2.0, 1.0]]) + # deinterleave NV12 UV plane (UVUV... -> separate U, V) + uv = input_frame[uv_offset:uv_offset + uv_height * stride].reshape(uv_height, stride) + with Context(SPLIT_REDUCEOP=0): + y = warp_perspective_tinygrad(input_frame[:cam_h*stride], + M_inv, (model_w, model_h), + (cam_h, cam_w), stride_pad).realize() + u = warp_perspective_tinygrad(uv[:cam_h//2, :cam_w:2].flatten(), + M_inv_uv, (model_w//2, model_h//2), + (cam_h//2, cam_w//2), 0).realize() + v = warp_perspective_tinygrad(uv[:cam_h//2, 1:cam_w:2].flatten(), + M_inv_uv, (model_w//2, model_h//2), + (cam_h//2, cam_w//2), 0).realize() + yuv = y.cat(u).cat(v).reshape((model_h * 3 // 2, model_w)) + tensor = frames_to_tensor(yuv, model_w, model_h) + return tensor + return frame_prepare_tinygrad + + +def make_update_img_input(frame_prepare, model_w, model_h): + def update_img_input_tinygrad(tensor, frame, M_inv): + M_inv = M_inv.to(Device.DEFAULT) + new_img = frame_prepare(frame, M_inv) + full_buffer = tensor[6:].cat(new_img, dim=0).contiguous() + return full_buffer, Tensor.cat(full_buffer[:6], full_buffer[-6:], dim=0).contiguous().reshape(1, 12, model_h//2, model_w//2) + return update_img_input_tinygrad + + +def make_update_both_imgs(frame_prepare, model_w, model_h): + update_img = make_update_img_input(frame_prepare, model_w, model_h) + + def update_both_imgs_tinygrad(calib_img_buffer, new_img, M_inv, + calib_big_img_buffer, new_big_img, M_inv_big): + calib_img_buffer, calib_img_pair = update_img(calib_img_buffer, new_img, M_inv) + calib_big_img_buffer, calib_big_img_pair = update_img(calib_big_img_buffer, new_big_img, M_inv_big) + return calib_img_buffer, calib_img_pair, calib_big_img_buffer, calib_big_img_pair + return update_both_imgs_tinygrad + + +def make_warp_dm(cam_w, cam_h, dm_w, dm_h): + stride, y_height, _, _ = get_nv12_info(cam_w, cam_h) + stride_pad = stride - cam_w + + def warp_dm(input_frame, M_inv): + M_inv = M_inv.to(Device.DEFAULT) + result = warp_perspective_tinygrad(input_frame[:cam_h*stride], M_inv, (dm_w, dm_h), (cam_h, cam_w), stride_pad).reshape(-1, dm_h * dm_w) + return result + return warp_dm + + +def compile_modeld_warp(cam_w, cam_h): + model_w, model_h = MEDMODEL_INPUT_SIZE + _, _, _, yuv_size = get_nv12_info(cam_w, cam_h) + + print(f"Compiling modeld warp for {cam_w}x{cam_h}...") + + frame_prepare = make_frame_prepare(cam_w, cam_h, model_w, model_h) + update_both_imgs = make_update_both_imgs(frame_prepare, model_w, model_h) + update_img_jit = TinyJit(update_both_imgs, prune=True) + + full_buffer = Tensor.zeros(IMG_BUFFER_SHAPE, dtype='uint8').contiguous().realize() + big_full_buffer = Tensor.zeros(IMG_BUFFER_SHAPE, dtype='uint8').contiguous().realize() + full_buffer_np = np.zeros(IMG_BUFFER_SHAPE, dtype=np.uint8) + big_full_buffer_np = np.zeros(IMG_BUFFER_SHAPE, dtype=np.uint8) + + for i in range(10): + new_frame_np = (32 * np.random.randn(yuv_size).astype(np.float32) + 128).clip(0, 255).astype(np.uint8) + img_inputs = [full_buffer, + Tensor.from_blob(new_frame_np.ctypes.data, (yuv_size,), dtype='uint8').realize(), + Tensor(Tensor.randn(3, 3).mul(8).realize().numpy(), device='NPY')] + new_big_frame_np = (32 * np.random.randn(yuv_size).astype(np.float32) + 128).clip(0, 255).astype(np.uint8) + big_img_inputs = [big_full_buffer, + Tensor.from_blob(new_big_frame_np.ctypes.data, (yuv_size,), dtype='uint8').realize(), + Tensor(Tensor.randn(3, 3).mul(8).realize().numpy(), device='NPY')] + inputs = img_inputs + big_img_inputs + Device.default.synchronize() + + inputs_np = [x.numpy() for x in inputs] + inputs_np[0] = full_buffer_np + inputs_np[3] = big_full_buffer_np + + st = time.perf_counter() + out = update_img_jit(*inputs) + full_buffer = out[0].contiguous().realize().clone() + big_full_buffer = out[2].contiguous().realize().clone() + mt = time.perf_counter() + Device.default.synchronize() + et = time.perf_counter() + print(f" [{i+1}/10] enqueue {(mt-st)*1e3:6.2f} ms -- total {(et-st)*1e3:6.2f} ms") + + pkl_path = warp_pkl_path(cam_w, cam_h) + with open(pkl_path, "wb") as f: + pickle.dump(update_img_jit, f) + print(f" Saved to {pkl_path}") + + jit = pickle.load(open(pkl_path, "rb")) + jit(*inputs) + + +def compile_dm_warp(cam_w, cam_h): + dm_w, dm_h = DM_INPUT_SIZE + _, _, _, yuv_size = get_nv12_info(cam_w, cam_h) + + print(f"Compiling DM warp for {cam_w}x{cam_h}...") + + warp_dm = make_warp_dm(cam_w, cam_h, dm_w, dm_h) + warp_dm_jit = TinyJit(warp_dm, prune=True) + + for i in range(10): + inputs = [Tensor.from_blob((32 * Tensor.randn(yuv_size,) + 128).cast(dtype='uint8').realize().numpy().ctypes.data, (yuv_size,), dtype='uint8'), + Tensor(Tensor.randn(3, 3).mul(8).realize().numpy(), device='NPY')] + Device.default.synchronize() + st = time.perf_counter() + warp_dm_jit(*inputs) + mt = time.perf_counter() + Device.default.synchronize() + et = time.perf_counter() + print(f" [{i+1}/10] enqueue {(mt-st)*1e3:6.2f} ms -- total {(et-st)*1e3:6.2f} ms") + + pkl_path = dm_warp_pkl_path(cam_w, cam_h) + with open(pkl_path, "wb") as f: + pickle.dump(warp_dm_jit, f) + print(f" Saved to {pkl_path}") + + +def run_and_save_pickle(): + for cam_w, cam_h in CAMERA_CONFIGS: + compile_modeld_warp(cam_w, cam_h) + compile_dm_warp(cam_w, cam_h) + + +if __name__ == "__main__": + run_and_save_pickle() diff --git a/selfdrive/modeld/dmonitoringmodeld.py b/selfdrive/modeld/dmonitoringmodeld.py index 7177998571..94871c1990 100755 --- a/selfdrive/modeld/dmonitoringmodeld.py +++ b/selfdrive/modeld/dmonitoringmodeld.py @@ -3,7 +3,6 @@ import os from openpilot.system.hardware import TICI os.environ['DEV'] = 'QCOM' if TICI else 'CPU' from tinygrad.tensor import Tensor -from tinygrad.dtype import dtypes import time import pickle import numpy as np @@ -16,32 +15,35 @@ from openpilot.common.swaglog import cloudlog from openpilot.common.realtime import config_realtime_process from openpilot.common.transformations.model import dmonitoringmodel_intrinsics from openpilot.common.transformations.camera import _ar_ox_fisheye, _os_fisheye -from openpilot.selfdrive.modeld.models.commonmodel_pyx import CLContext, MonitoringModelFrame +from openpilot.system.camerad.cameras.nv12_info import get_nv12_info from openpilot.selfdrive.modeld.parse_model_outputs import sigmoid, safe_exp -from openpilot.selfdrive.modeld.runners.tinygrad_helpers import qcom_tensor_from_opencl_address PROCESS_NAME = "selfdrive.modeld.dmonitoringmodeld" SEND_RAW_PRED = os.getenv('SEND_RAW_PRED') MODEL_PKL_PATH = Path(__file__).parent / 'models/dmonitoring_model_tinygrad.pkl' METADATA_PATH = Path(__file__).parent / 'models/dmonitoring_model_metadata.pkl' - +MODELS_DIR = Path(__file__).parent / 'models' class ModelState: inputs: dict[str, np.ndarray] output: np.ndarray - def __init__(self, cl_ctx): + def __init__(self): with open(METADATA_PATH, 'rb') as f: model_metadata = pickle.load(f) self.input_shapes = model_metadata['input_shapes'] self.output_slices = model_metadata['output_slices'] - self.frame = MonitoringModelFrame(cl_ctx) self.numpy_inputs = { 'calib': np.zeros(self.input_shapes['calib'], dtype=np.float32), } + self.warp_inputs_np = {'transform': np.zeros((3,3), dtype=np.float32)} + self.warp_inputs = {k: Tensor(v, device='NPY') for k,v in self.warp_inputs_np.items()} + self.frame_buf_params = None self.tensor_inputs = {k: Tensor(v, device='NPY').realize() for k,v in self.numpy_inputs.items()} + self._blob_cache : dict[int, Tensor] = {} + self.image_warp = None with open(MODEL_PKL_PATH, "rb") as f: self.model_run = pickle.load(f) @@ -50,16 +52,20 @@ class ModelState: t1 = time.perf_counter() - input_img_cl = self.frame.prepare(buf, transform.flatten()) - if TICI: - # The imgs tensors are backed by opencl memory, only need init once - if 'input_img' not in self.tensor_inputs: - self.tensor_inputs['input_img'] = qcom_tensor_from_opencl_address(input_img_cl.mem_address, self.input_shapes['input_img'], dtype=dtypes.uint8) - else: - self.tensor_inputs['input_img'] = Tensor(self.frame.buffer_from_cl(input_img_cl).reshape(self.input_shapes['input_img']), dtype=dtypes.uint8).realize() + if self.image_warp is None: + self.frame_buf_params = get_nv12_info(buf.width, buf.height) + warp_path = MODELS_DIR / f'dm_warp_{buf.width}x{buf.height}_tinygrad.pkl' + with open(warp_path, "rb") as f: + self.image_warp = pickle.load(f) + ptr = buf.data.ctypes.data + # There is a ringbuffer of imgs, just cache tensors pointing to all of them + if ptr not in self._blob_cache: + self._blob_cache[ptr] = Tensor.from_blob(ptr, (self.frame_buf_params[3],), dtype='uint8') + self.warp_inputs_np['transform'][:] = transform[:] + self.tensor_inputs['input_img'] = self.image_warp(self._blob_cache[ptr], self.warp_inputs['transform']).realize() - output = self.model_run(**self.tensor_inputs).numpy().flatten() + output = self.model_run(**self.tensor_inputs).contiguous().realize().uop.base.buffer.numpy().flatten() t2 = time.perf_counter() return output, t2 - t1 @@ -107,12 +113,11 @@ def get_driverstate_packet(model_output, frame_id: int, location_ts: int, exec_t def main(): config_realtime_process(7, 5) - cl_context = CLContext() - model = ModelState(cl_context) + model = ModelState() cloudlog.warning("models loaded, dmonitoringmodeld starting") cloudlog.warning("connecting to driver stream") - vipc_client = VisionIpcClient("camerad", VisionStreamType.VISION_STREAM_DRIVER, True, cl_context) + vipc_client = VisionIpcClient("camerad", VisionStreamType.VISION_STREAM_DRIVER, True) while not vipc_client.connect(False): time.sleep(0.1) assert vipc_client.is_connected() diff --git a/selfdrive/modeld/modeld.py b/selfdrive/modeld/modeld.py index 846bb8d2c3..df000979e8 100755 --- a/selfdrive/modeld/modeld.py +++ b/selfdrive/modeld/modeld.py @@ -7,7 +7,6 @@ if USBGPU: os.environ['DEV'] = 'AMD' os.environ['AMD_IFACE'] = 'USB' from tinygrad.tensor import Tensor -from tinygrad.dtype import dtypes import time import pickle import numpy as np @@ -22,14 +21,13 @@ from openpilot.common.params import Params from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.realtime import config_realtime_process, DT_MDL from openpilot.common.transformations.camera import DEVICE_CAMERAS +from openpilot.system.camerad.cameras.nv12_info import get_nv12_info from openpilot.common.transformations.model import get_warp_matrix from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper from openpilot.selfdrive.controls.lib.drive_helpers import get_accel_from_plan, smooth_value, get_curvature_from_plan from openpilot.selfdrive.modeld.parse_model_outputs import Parser from openpilot.selfdrive.modeld.fill_model_msg import fill_model_msg, fill_pose_msg, PublishState from openpilot.selfdrive.modeld.constants import ModelConstants, Plan -from openpilot.selfdrive.modeld.models.commonmodel_pyx import DrivingModelFrame, CLContext -from openpilot.selfdrive.modeld.runners.tinygrad_helpers import qcom_tensor_from_opencl_address PROCESS_NAME = "selfdrive.modeld.modeld" @@ -39,11 +37,15 @@ VISION_PKL_PATH = Path(__file__).parent / 'models/driving_vision_tinygrad.pkl' POLICY_PKL_PATH = Path(__file__).parent / 'models/driving_policy_tinygrad.pkl' VISION_METADATA_PATH = Path(__file__).parent / 'models/driving_vision_metadata.pkl' POLICY_METADATA_PATH = Path(__file__).parent / 'models/driving_policy_metadata.pkl' +MODELS_DIR = Path(__file__).parent / 'models' LAT_SMOOTH_SECONDS = 0.0 LONG_SMOOTH_SECONDS = 0.3 MIN_LAT_CONTROL_SPEED = 0.3 +IMG_QUEUE_SHAPE = (6*(ModelConstants.MODEL_RUN_FREQ//ModelConstants.MODEL_CONTEXT_FREQ + 1), 128, 256) +assert IMG_QUEUE_SHAPE[0] == 30 + def get_action_from_model(model_output: dict[str, np.ndarray], prev_action: log.ModelDataV2.Action, lat_action_t: float, long_action_t: float, v_ego: float) -> log.ModelDataV2.Action: @@ -136,12 +138,11 @@ class InputQueues: return out class ModelState: - frames: dict[str, DrivingModelFrame] inputs: dict[str, np.ndarray] output: np.ndarray prev_desire: np.ndarray # for tracking the rising edge of the pulse - def __init__(self, context: CLContext): + def __init__(self): with open(VISION_METADATA_PATH, 'rb') as f: vision_metadata = pickle.load(f) self.vision_input_shapes = vision_metadata['input_shapes'] @@ -155,7 +156,6 @@ class ModelState: self.policy_output_slices = policy_metadata['output_slices'] policy_output_size = policy_metadata['output_shapes']['outputs'][1] - self.frames = {name: DrivingModelFrame(context, ModelConstants.MODEL_RUN_FREQ//ModelConstants.MODEL_CONTEXT_FREQ) for name in self.vision_input_names} self.prev_desire = np.zeros(ModelConstants.DESIRE_LEN, dtype=np.float32) # policy inputs @@ -165,13 +165,18 @@ class ModelState: self.full_input_queues.update_dtypes_and_shapes({k: self.numpy_inputs[k].dtype}, {k: self.numpy_inputs[k].shape}) self.full_input_queues.reset() - # img buffers are managed in openCL transform code - self.vision_inputs: dict[str, Tensor] = {} + self.img_queues = {'img': Tensor.zeros(IMG_QUEUE_SHAPE, dtype='uint8').contiguous().realize(), + 'big_img': Tensor.zeros(IMG_QUEUE_SHAPE, dtype='uint8').contiguous().realize()} + self.full_frames : dict[str, Tensor] = {} + self._blob_cache : dict[int, Tensor] = {} + self.transforms_np = {k: np.zeros((3,3), dtype=np.float32) for k in self.img_queues} + self.transforms = {k: Tensor(v, device='NPY').realize() for k, v in self.transforms_np.items()} self.vision_output = np.zeros(vision_output_size, dtype=np.float32) self.policy_inputs = {k: Tensor(v, device='NPY').realize() for k,v in self.numpy_inputs.items()} self.policy_output = np.zeros(policy_output_size, dtype=np.float32) self.parser = Parser() - + self.frame_buf_params : dict[str, tuple[int, int, int, int]] = {} + self.update_imgs = None with open(VISION_PKL_PATH, "rb") as f: self.vision_run = pickle.load(f) @@ -188,23 +193,33 @@ class ModelState: inputs['desire_pulse'][0] = 0 new_desire = np.where(inputs['desire_pulse'] - self.prev_desire > .99, inputs['desire_pulse'], 0) self.prev_desire[:] = inputs['desire_pulse'] + if self.update_imgs is None: + for key in bufs.keys(): + w, h = bufs[key].width, bufs[key].height + self.frame_buf_params[key] = get_nv12_info(w, h) + warp_path = MODELS_DIR / f'warp_{w}x{h}_tinygrad.pkl' + with open(warp_path, "rb") as f: + self.update_imgs = pickle.load(f) - imgs_cl = {name: self.frames[name].prepare(bufs[name], transforms[name].flatten()) for name in self.vision_input_names} + for key in bufs.keys(): + ptr = bufs[key].data.ctypes.data + yuv_size = self.frame_buf_params[key][3] + # There is a ringbuffer of imgs, just cache tensors pointing to all of them + if ptr not in self._blob_cache: + self._blob_cache[ptr] = Tensor.from_blob(ptr, (yuv_size,), dtype='uint8') + self.full_frames[key] = self._blob_cache[ptr] + for key in bufs.keys(): + self.transforms_np[key][:,:] = transforms[key][:,:] - if TICI and not USBGPU: - # The imgs tensors are backed by opencl memory, only need init once - for key in imgs_cl: - if key not in self.vision_inputs: - self.vision_inputs[key] = qcom_tensor_from_opencl_address(imgs_cl[key].mem_address, self.vision_input_shapes[key], dtype=dtypes.uint8) - else: - for key in imgs_cl: - frame_input = self.frames[key].buffer_from_cl(imgs_cl[key]).reshape(self.vision_input_shapes[key]) - self.vision_inputs[key] = Tensor(frame_input, dtype=dtypes.uint8).realize() + out = self.update_imgs(self.img_queues['img'], self.full_frames['img'], self.transforms['img'], + self.img_queues['big_img'], self.full_frames['big_img'], self.transforms['big_img']) + self.img_queues['img'], self.img_queues['big_img'] = out[0].realize(), out[2].realize() + vision_inputs = {'img': out[1], 'big_img': out[3]} if prepare_only: return None - self.vision_output = self.vision_run(**self.vision_inputs).contiguous().realize().uop.base.buffer.numpy() + self.vision_output = self.vision_run(**vision_inputs).contiguous().realize().uop.base.buffer.numpy().flatten() vision_outputs_dict = self.parser.parse_vision_outputs(self.slice_outputs(self.vision_output, self.vision_output_slices)) self.full_input_queues.enqueue({'features_buffer': vision_outputs_dict['hidden_state'], 'desire_pulse': new_desire}) @@ -212,9 +227,8 @@ class ModelState: self.numpy_inputs[k][:] = self.full_input_queues.get(k)[k] self.numpy_inputs['traffic_convention'][:] = inputs['traffic_convention'] - self.policy_output = self.policy_run(**self.policy_inputs).numpy().flatten() + self.policy_output = self.policy_run(**self.policy_inputs).contiguous().realize().uop.base.buffer.numpy().flatten() policy_outputs_dict = self.parser.parse_policy_outputs(self.slice_outputs(self.policy_output, self.policy_output_slices)) - combined_outputs_dict = {**vision_outputs_dict, **policy_outputs_dict} if SEND_RAW_PRED: combined_outputs_dict['raw_pred'] = np.concatenate([self.vision_output.copy(), self.policy_output.copy()]) @@ -231,10 +245,8 @@ def main(demo=False): config_realtime_process(7, 54) st = time.monotonic() - cloudlog.warning("setting up CL context") - cl_context = CLContext() - cloudlog.warning("CL context ready; loading model") - model = ModelState(cl_context) + cloudlog.warning("loading model") + model = ModelState() cloudlog.warning(f"models loaded in {time.monotonic() - st:.1f}s, modeld starting") # visionipc clients @@ -247,8 +259,8 @@ def main(demo=False): time.sleep(.1) vipc_client_main_stream = VisionStreamType.VISION_STREAM_WIDE_ROAD if main_wide_camera else VisionStreamType.VISION_STREAM_ROAD - vipc_client_main = VisionIpcClient("camerad", vipc_client_main_stream, True, cl_context) - vipc_client_extra = VisionIpcClient("camerad", VisionStreamType.VISION_STREAM_WIDE_ROAD, False, cl_context) + vipc_client_main = VisionIpcClient("camerad", vipc_client_main_stream, True) + vipc_client_extra = VisionIpcClient("camerad", VisionStreamType.VISION_STREAM_WIDE_ROAD, False) cloudlog.warning(f"vision stream set up, main_wide_camera: {main_wide_camera}, use_extra_client: {use_extra_client}") while not vipc_client_main.connect(False): diff --git a/selfdrive/modeld/models/commonmodel.cc b/selfdrive/modeld/models/commonmodel.cc deleted file mode 100644 index d3341e76ec..0000000000 --- a/selfdrive/modeld/models/commonmodel.cc +++ /dev/null @@ -1,64 +0,0 @@ -#include "selfdrive/modeld/models/commonmodel.h" - -#include -#include - -#include "common/clutil.h" - -DrivingModelFrame::DrivingModelFrame(cl_device_id device_id, cl_context context, int _temporal_skip) : ModelFrame(device_id, context) { - input_frames = std::make_unique(buf_size); - temporal_skip = _temporal_skip; - input_frames_cl = CL_CHECK_ERR(clCreateBuffer(context, CL_MEM_READ_WRITE, buf_size, NULL, &err)); - img_buffer_20hz_cl = CL_CHECK_ERR(clCreateBuffer(context, CL_MEM_READ_WRITE, (temporal_skip+1)*frame_size_bytes, NULL, &err)); - region.origin = temporal_skip * frame_size_bytes; - region.size = frame_size_bytes; - last_img_cl = CL_CHECK_ERR(clCreateSubBuffer(img_buffer_20hz_cl, CL_MEM_READ_WRITE, CL_BUFFER_CREATE_TYPE_REGION, ®ion, &err)); - - loadyuv_init(&loadyuv, context, device_id, MODEL_WIDTH, MODEL_HEIGHT); - init_transform(device_id, context, MODEL_WIDTH, MODEL_HEIGHT); -} - -cl_mem* DrivingModelFrame::prepare(cl_mem yuv_cl, int frame_width, int frame_height, int frame_stride, int frame_uv_offset, const mat3& projection) { - run_transform(yuv_cl, MODEL_WIDTH, MODEL_HEIGHT, frame_width, frame_height, frame_stride, frame_uv_offset, projection); - - for (int i = 0; i < temporal_skip; i++) { - CL_CHECK(clEnqueueCopyBuffer(q, img_buffer_20hz_cl, img_buffer_20hz_cl, (i+1)*frame_size_bytes, i*frame_size_bytes, frame_size_bytes, 0, nullptr, nullptr)); - } - loadyuv_queue(&loadyuv, q, y_cl, u_cl, v_cl, last_img_cl); - - copy_queue(&loadyuv, q, img_buffer_20hz_cl, input_frames_cl, 0, 0, frame_size_bytes); - copy_queue(&loadyuv, q, last_img_cl, input_frames_cl, 0, frame_size_bytes, frame_size_bytes); - - // NOTE: Since thneed is using a different command queue, this clFinish is needed to ensure the image is ready. - clFinish(q); - return &input_frames_cl; -} - -DrivingModelFrame::~DrivingModelFrame() { - deinit_transform(); - loadyuv_destroy(&loadyuv); - CL_CHECK(clReleaseMemObject(input_frames_cl)); - CL_CHECK(clReleaseMemObject(img_buffer_20hz_cl)); - CL_CHECK(clReleaseMemObject(last_img_cl)); - CL_CHECK(clReleaseCommandQueue(q)); -} - - -MonitoringModelFrame::MonitoringModelFrame(cl_device_id device_id, cl_context context) : ModelFrame(device_id, context) { - input_frames = std::make_unique(buf_size); - input_frame_cl = CL_CHECK_ERR(clCreateBuffer(context, CL_MEM_READ_WRITE, buf_size, NULL, &err)); - - init_transform(device_id, context, MODEL_WIDTH, MODEL_HEIGHT); -} - -cl_mem* MonitoringModelFrame::prepare(cl_mem yuv_cl, int frame_width, int frame_height, int frame_stride, int frame_uv_offset, const mat3& projection) { - run_transform(yuv_cl, MODEL_WIDTH, MODEL_HEIGHT, frame_width, frame_height, frame_stride, frame_uv_offset, projection); - clFinish(q); - return &y_cl; -} - -MonitoringModelFrame::~MonitoringModelFrame() { - deinit_transform(); - CL_CHECK(clReleaseMemObject(input_frame_cl)); - CL_CHECK(clReleaseCommandQueue(q)); -} diff --git a/selfdrive/modeld/models/commonmodel.h b/selfdrive/modeld/models/commonmodel.h deleted file mode 100644 index 176d7eb6dc..0000000000 --- a/selfdrive/modeld/models/commonmodel.h +++ /dev/null @@ -1,97 +0,0 @@ -#pragma once - -#include -#include -#include - -#include - -#define CL_USE_DEPRECATED_OPENCL_1_2_APIS -#ifdef __APPLE__ -#include -#else -#include -#endif - -#include "common/mat.h" -#include "selfdrive/modeld/transforms/loadyuv.h" -#include "selfdrive/modeld/transforms/transform.h" - -class ModelFrame { -public: - ModelFrame(cl_device_id device_id, cl_context context) { - q = CL_CHECK_ERR(clCreateCommandQueue(context, device_id, 0, &err)); - } - virtual ~ModelFrame() {} - virtual cl_mem* prepare(cl_mem yuv_cl, int frame_width, int frame_height, int frame_stride, int frame_uv_offset, const mat3& projection) { return NULL; } - uint8_t* buffer_from_cl(cl_mem *in_frames, int buffer_size) { - CL_CHECK(clEnqueueReadBuffer(q, *in_frames, CL_TRUE, 0, buffer_size, input_frames.get(), 0, nullptr, nullptr)); - clFinish(q); - return &input_frames[0]; - } - - int MODEL_WIDTH; - int MODEL_HEIGHT; - int MODEL_FRAME_SIZE; - int buf_size; - -protected: - cl_mem y_cl, u_cl, v_cl; - Transform transform; - cl_command_queue q; - std::unique_ptr input_frames; - - void init_transform(cl_device_id device_id, cl_context context, int model_width, int model_height) { - y_cl = CL_CHECK_ERR(clCreateBuffer(context, CL_MEM_READ_WRITE, model_width * model_height, NULL, &err)); - u_cl = CL_CHECK_ERR(clCreateBuffer(context, CL_MEM_READ_WRITE, (model_width / 2) * (model_height / 2), NULL, &err)); - v_cl = CL_CHECK_ERR(clCreateBuffer(context, CL_MEM_READ_WRITE, (model_width / 2) * (model_height / 2), NULL, &err)); - transform_init(&transform, context, device_id); - } - - void deinit_transform() { - transform_destroy(&transform); - CL_CHECK(clReleaseMemObject(v_cl)); - CL_CHECK(clReleaseMemObject(u_cl)); - CL_CHECK(clReleaseMemObject(y_cl)); - } - - void run_transform(cl_mem yuv_cl, int model_width, int model_height, int frame_width, int frame_height, int frame_stride, int frame_uv_offset, const mat3& projection) { - transform_queue(&transform, q, - yuv_cl, frame_width, frame_height, frame_stride, frame_uv_offset, - y_cl, u_cl, v_cl, model_width, model_height, projection); - } -}; - -class DrivingModelFrame : public ModelFrame { -public: - DrivingModelFrame(cl_device_id device_id, cl_context context, int _temporal_skip); - ~DrivingModelFrame(); - cl_mem* prepare(cl_mem yuv_cl, int frame_width, int frame_height, int frame_stride, int frame_uv_offset, const mat3& projection); - - const int MODEL_WIDTH = 512; - const int MODEL_HEIGHT = 256; - const int MODEL_FRAME_SIZE = MODEL_WIDTH * MODEL_HEIGHT * 3 / 2; - const int buf_size = MODEL_FRAME_SIZE * 2; // 2 frames are temporal_skip frames apart - const size_t frame_size_bytes = MODEL_FRAME_SIZE * sizeof(uint8_t); - -private: - LoadYUVState loadyuv; - cl_mem img_buffer_20hz_cl, last_img_cl, input_frames_cl; - cl_buffer_region region; - int temporal_skip; -}; - -class MonitoringModelFrame : public ModelFrame { -public: - MonitoringModelFrame(cl_device_id device_id, cl_context context); - ~MonitoringModelFrame(); - cl_mem* prepare(cl_mem yuv_cl, int frame_width, int frame_height, int frame_stride, int frame_uv_offset, const mat3& projection); - - const int MODEL_WIDTH = 1440; - const int MODEL_HEIGHT = 960; - const int MODEL_FRAME_SIZE = MODEL_WIDTH * MODEL_HEIGHT; - const int buf_size = MODEL_FRAME_SIZE; - -private: - cl_mem input_frame_cl; -}; diff --git a/selfdrive/modeld/models/commonmodel.pxd b/selfdrive/modeld/models/commonmodel.pxd deleted file mode 100644 index 4ac64d9172..0000000000 --- a/selfdrive/modeld/models/commonmodel.pxd +++ /dev/null @@ -1,27 +0,0 @@ -# distutils: language = c++ - -from msgq.visionipc.visionipc cimport cl_device_id, cl_context, cl_mem - -cdef extern from "common/mat.h": - cdef struct mat3: - float v[9] - -cdef extern from "common/clutil.h": - cdef unsigned long CL_DEVICE_TYPE_DEFAULT - cl_device_id cl_get_device_id(unsigned long) - cl_context cl_create_context(cl_device_id) - void cl_release_context(cl_context) - -cdef extern from "selfdrive/modeld/models/commonmodel.h": - cppclass ModelFrame: - int buf_size - unsigned char * buffer_from_cl(cl_mem*, int); - cl_mem * prepare(cl_mem, int, int, int, int, mat3) - - cppclass DrivingModelFrame: - int buf_size - DrivingModelFrame(cl_device_id, cl_context, int) - - cppclass MonitoringModelFrame: - int buf_size - MonitoringModelFrame(cl_device_id, cl_context) diff --git a/selfdrive/modeld/models/commonmodel_pyx.pxd b/selfdrive/modeld/models/commonmodel_pyx.pxd deleted file mode 100644 index 0bb798625b..0000000000 --- a/selfdrive/modeld/models/commonmodel_pyx.pxd +++ /dev/null @@ -1,13 +0,0 @@ -# distutils: language = c++ - -from msgq.visionipc.visionipc cimport cl_mem -from msgq.visionipc.visionipc_pyx cimport CLContext as BaseCLContext - -cdef class CLContext(BaseCLContext): - pass - -cdef class CLMem: - cdef cl_mem * mem - - @staticmethod - cdef create(void*) diff --git a/selfdrive/modeld/models/commonmodel_pyx.pyx b/selfdrive/modeld/models/commonmodel_pyx.pyx deleted file mode 100644 index 5b7d11bc71..0000000000 --- a/selfdrive/modeld/models/commonmodel_pyx.pyx +++ /dev/null @@ -1,74 +0,0 @@ -# distutils: language = c++ -# cython: c_string_encoding=ascii, language_level=3 - -import numpy as np -cimport numpy as cnp -from libc.string cimport memcpy -from libc.stdint cimport uintptr_t - -from msgq.visionipc.visionipc cimport cl_mem -from msgq.visionipc.visionipc_pyx cimport VisionBuf, CLContext as BaseCLContext -from .commonmodel cimport CL_DEVICE_TYPE_DEFAULT, cl_get_device_id, cl_create_context, cl_release_context -from .commonmodel cimport mat3, ModelFrame as cppModelFrame, DrivingModelFrame as cppDrivingModelFrame, MonitoringModelFrame as cppMonitoringModelFrame - - -cdef class CLContext(BaseCLContext): - def __cinit__(self): - self.device_id = cl_get_device_id(CL_DEVICE_TYPE_DEFAULT) - self.context = cl_create_context(self.device_id) - - def __dealloc__(self): - if self.context: - cl_release_context(self.context) - -cdef class CLMem: - @staticmethod - cdef create(void * cmem): - mem = CLMem() - mem.mem = cmem - return mem - - @property - def mem_address(self): - return (self.mem) - -def cl_from_visionbuf(VisionBuf buf): - return CLMem.create(&buf.buf.buf_cl) - - -cdef class ModelFrame: - cdef cppModelFrame * frame - cdef int buf_size - - def __dealloc__(self): - del self.frame - - def prepare(self, VisionBuf buf, float[:] projection): - cdef mat3 cprojection - memcpy(cprojection.v, &projection[0], 9*sizeof(float)) - cdef cl_mem * data - data = self.frame.prepare(buf.buf.buf_cl, buf.width, buf.height, buf.stride, buf.uv_offset, cprojection) - return CLMem.create(data) - - def buffer_from_cl(self, CLMem in_frames): - cdef unsigned char * data2 - data2 = self.frame.buffer_from_cl(in_frames.mem, self.buf_size) - return np.asarray( data2) - - -cdef class DrivingModelFrame(ModelFrame): - cdef cppDrivingModelFrame * _frame - - def __cinit__(self, CLContext context, int temporal_skip): - self._frame = new cppDrivingModelFrame(context.device_id, context.context, temporal_skip) - self.frame = (self._frame) - self.buf_size = self._frame.buf_size - -cdef class MonitoringModelFrame(ModelFrame): - cdef cppMonitoringModelFrame * _frame - - def __cinit__(self, CLContext context): - self._frame = new cppMonitoringModelFrame(context.device_id, context.context) - self.frame = (self._frame) - self.buf_size = self._frame.buf_size - diff --git a/selfdrive/modeld/runners/tinygrad_helpers.py b/selfdrive/modeld/runners/tinygrad_helpers.py deleted file mode 100644 index 776381341c..0000000000 --- a/selfdrive/modeld/runners/tinygrad_helpers.py +++ /dev/null @@ -1,8 +0,0 @@ - -from tinygrad.tensor import Tensor -from tinygrad.helpers import to_mv - -def qcom_tensor_from_opencl_address(opencl_address, shape, dtype): - cl_buf_desc_ptr = to_mv(opencl_address, 8).cast('Q')[0] - rawbuf_ptr = to_mv(cl_buf_desc_ptr, 0x100).cast('Q')[20] # offset 0xA0 is a raw gpu pointer. - return Tensor.from_blob(rawbuf_ptr, shape, dtype=dtype, device='QCOM') diff --git a/selfdrive/modeld/transforms/loadyuv.cc b/selfdrive/modeld/transforms/loadyuv.cc deleted file mode 100644 index c93f5cd038..0000000000 --- a/selfdrive/modeld/transforms/loadyuv.cc +++ /dev/null @@ -1,76 +0,0 @@ -#include "selfdrive/modeld/transforms/loadyuv.h" - -#include -#include -#include - -void loadyuv_init(LoadYUVState* s, cl_context ctx, cl_device_id device_id, int width, int height) { - memset(s, 0, sizeof(*s)); - - s->width = width; - s->height = height; - - char args[1024]; - snprintf(args, sizeof(args), - "-cl-fast-relaxed-math -cl-denorms-are-zero " - "-DTRANSFORMED_WIDTH=%d -DTRANSFORMED_HEIGHT=%d", - width, height); - cl_program prg = cl_program_from_file(ctx, device_id, LOADYUV_PATH, args); - - s->loadys_krnl = CL_CHECK_ERR(clCreateKernel(prg, "loadys", &err)); - s->loaduv_krnl = CL_CHECK_ERR(clCreateKernel(prg, "loaduv", &err)); - s->copy_krnl = CL_CHECK_ERR(clCreateKernel(prg, "copy", &err)); - - // done with this - CL_CHECK(clReleaseProgram(prg)); -} - -void loadyuv_destroy(LoadYUVState* s) { - CL_CHECK(clReleaseKernel(s->loadys_krnl)); - CL_CHECK(clReleaseKernel(s->loaduv_krnl)); - CL_CHECK(clReleaseKernel(s->copy_krnl)); -} - -void loadyuv_queue(LoadYUVState* s, cl_command_queue q, - cl_mem y_cl, cl_mem u_cl, cl_mem v_cl, - cl_mem out_cl) { - cl_int global_out_off = 0; - - CL_CHECK(clSetKernelArg(s->loadys_krnl, 0, sizeof(cl_mem), &y_cl)); - CL_CHECK(clSetKernelArg(s->loadys_krnl, 1, sizeof(cl_mem), &out_cl)); - CL_CHECK(clSetKernelArg(s->loadys_krnl, 2, sizeof(cl_int), &global_out_off)); - - const size_t loadys_work_size = (s->width*s->height)/8; - CL_CHECK(clEnqueueNDRangeKernel(q, s->loadys_krnl, 1, NULL, - &loadys_work_size, NULL, 0, 0, NULL)); - - const size_t loaduv_work_size = ((s->width/2)*(s->height/2))/8; - global_out_off += (s->width*s->height); - - CL_CHECK(clSetKernelArg(s->loaduv_krnl, 0, sizeof(cl_mem), &u_cl)); - CL_CHECK(clSetKernelArg(s->loaduv_krnl, 1, sizeof(cl_mem), &out_cl)); - CL_CHECK(clSetKernelArg(s->loaduv_krnl, 2, sizeof(cl_int), &global_out_off)); - - CL_CHECK(clEnqueueNDRangeKernel(q, s->loaduv_krnl, 1, NULL, - &loaduv_work_size, NULL, 0, 0, NULL)); - - global_out_off += (s->width/2)*(s->height/2); - - CL_CHECK(clSetKernelArg(s->loaduv_krnl, 0, sizeof(cl_mem), &v_cl)); - CL_CHECK(clSetKernelArg(s->loaduv_krnl, 1, sizeof(cl_mem), &out_cl)); - CL_CHECK(clSetKernelArg(s->loaduv_krnl, 2, sizeof(cl_int), &global_out_off)); - - CL_CHECK(clEnqueueNDRangeKernel(q, s->loaduv_krnl, 1, NULL, - &loaduv_work_size, NULL, 0, 0, NULL)); -} - -void copy_queue(LoadYUVState* s, cl_command_queue q, cl_mem src, cl_mem dst, - size_t src_offset, size_t dst_offset, size_t size) { - CL_CHECK(clSetKernelArg(s->copy_krnl, 0, sizeof(cl_mem), &src)); - CL_CHECK(clSetKernelArg(s->copy_krnl, 1, sizeof(cl_mem), &dst)); - CL_CHECK(clSetKernelArg(s->copy_krnl, 2, sizeof(cl_int), &src_offset)); - CL_CHECK(clSetKernelArg(s->copy_krnl, 3, sizeof(cl_int), &dst_offset)); - const size_t copy_work_size = size/8; - CL_CHECK(clEnqueueNDRangeKernel(q, s->copy_krnl, 1, NULL, - ©_work_size, NULL, 0, 0, NULL)); -} \ No newline at end of file diff --git a/selfdrive/modeld/transforms/loadyuv.cl b/selfdrive/modeld/transforms/loadyuv.cl deleted file mode 100644 index 970187a6d7..0000000000 --- a/selfdrive/modeld/transforms/loadyuv.cl +++ /dev/null @@ -1,47 +0,0 @@ -#define UV_SIZE ((TRANSFORMED_WIDTH/2)*(TRANSFORMED_HEIGHT/2)) - -__kernel void loadys(__global uchar8 const * const Y, - __global uchar * out, - int out_offset) -{ - const int gid = get_global_id(0); - const int ois = gid * 8; - const int oy = ois / TRANSFORMED_WIDTH; - const int ox = ois % TRANSFORMED_WIDTH; - - const uchar8 ys = Y[gid]; - - // 02 - // 13 - - __global uchar* outy0; - __global uchar* outy1; - if ((oy & 1) == 0) { - outy0 = out + out_offset; //y0 - outy1 = out + out_offset + UV_SIZE*2; //y2 - } else { - outy0 = out + out_offset + UV_SIZE; //y1 - outy1 = out + out_offset + UV_SIZE*3; //y3 - } - - vstore4(ys.s0246, 0, outy0 + (oy/2) * (TRANSFORMED_WIDTH/2) + ox/2); - vstore4(ys.s1357, 0, outy1 + (oy/2) * (TRANSFORMED_WIDTH/2) + ox/2); -} - -__kernel void loaduv(__global uchar8 const * const in, - __global uchar8 * out, - int out_offset) -{ - const int gid = get_global_id(0); - const uchar8 inv = in[gid]; - out[gid + out_offset / 8] = inv; -} - -__kernel void copy(__global uchar8 * in, - __global uchar8 * out, - int in_offset, - int out_offset) -{ - const int gid = get_global_id(0); - out[gid + out_offset / 8] = in[gid + in_offset / 8]; -} diff --git a/selfdrive/modeld/transforms/loadyuv.h b/selfdrive/modeld/transforms/loadyuv.h deleted file mode 100644 index 659059cd25..0000000000 --- a/selfdrive/modeld/transforms/loadyuv.h +++ /dev/null @@ -1,20 +0,0 @@ -#pragma once - -#include "common/clutil.h" - -typedef struct { - int width, height; - cl_kernel loadys_krnl, loaduv_krnl, copy_krnl; -} LoadYUVState; - -void loadyuv_init(LoadYUVState* s, cl_context ctx, cl_device_id device_id, int width, int height); - -void loadyuv_destroy(LoadYUVState* s); - -void loadyuv_queue(LoadYUVState* s, cl_command_queue q, - cl_mem y_cl, cl_mem u_cl, cl_mem v_cl, - cl_mem out_cl); - - -void copy_queue(LoadYUVState* s, cl_command_queue q, cl_mem src, cl_mem dst, - size_t src_offset, size_t dst_offset, size_t size); \ No newline at end of file diff --git a/selfdrive/modeld/transforms/transform.cc b/selfdrive/modeld/transforms/transform.cc deleted file mode 100644 index 305643cf42..0000000000 --- a/selfdrive/modeld/transforms/transform.cc +++ /dev/null @@ -1,97 +0,0 @@ -#include "selfdrive/modeld/transforms/transform.h" - -#include -#include - -#include "common/clutil.h" - -void transform_init(Transform* s, cl_context ctx, cl_device_id device_id) { - memset(s, 0, sizeof(*s)); - - cl_program prg = cl_program_from_file(ctx, device_id, TRANSFORM_PATH, ""); - s->krnl = CL_CHECK_ERR(clCreateKernel(prg, "warpPerspective", &err)); - // done with this - CL_CHECK(clReleaseProgram(prg)); - - s->m_y_cl = CL_CHECK_ERR(clCreateBuffer(ctx, CL_MEM_READ_WRITE, 3*3*sizeof(float), NULL, &err)); - s->m_uv_cl = CL_CHECK_ERR(clCreateBuffer(ctx, CL_MEM_READ_WRITE, 3*3*sizeof(float), NULL, &err)); -} - -void transform_destroy(Transform* s) { - CL_CHECK(clReleaseMemObject(s->m_y_cl)); - CL_CHECK(clReleaseMemObject(s->m_uv_cl)); - CL_CHECK(clReleaseKernel(s->krnl)); -} - -void transform_queue(Transform* s, - cl_command_queue q, - cl_mem in_yuv, int in_width, int in_height, int in_stride, int in_uv_offset, - cl_mem out_y, cl_mem out_u, cl_mem out_v, - int out_width, int out_height, - const mat3& projection) { - const int zero = 0; - - // sampled using pixel center origin - // (because that's how fastcv and opencv does it) - - mat3 projection_y = projection; - - // in and out uv is half the size of y. - mat3 projection_uv = transform_scale_buffer(projection, 0.5); - - CL_CHECK(clEnqueueWriteBuffer(q, s->m_y_cl, CL_TRUE, 0, 3*3*sizeof(float), (void*)projection_y.v, 0, NULL, NULL)); - CL_CHECK(clEnqueueWriteBuffer(q, s->m_uv_cl, CL_TRUE, 0, 3*3*sizeof(float), (void*)projection_uv.v, 0, NULL, NULL)); - - const int in_y_width = in_width; - const int in_y_height = in_height; - const int in_y_px_stride = 1; - const int in_uv_width = in_width/2; - const int in_uv_height = in_height/2; - const int in_uv_px_stride = 2; - const int in_u_offset = in_uv_offset; - const int in_v_offset = in_uv_offset + 1; - - const int out_y_width = out_width; - const int out_y_height = out_height; - const int out_uv_width = out_width/2; - const int out_uv_height = out_height/2; - - CL_CHECK(clSetKernelArg(s->krnl, 0, sizeof(cl_mem), &in_yuv)); // src - CL_CHECK(clSetKernelArg(s->krnl, 1, sizeof(cl_int), &in_stride)); // src_row_stride - CL_CHECK(clSetKernelArg(s->krnl, 2, sizeof(cl_int), &in_y_px_stride)); // src_px_stride - CL_CHECK(clSetKernelArg(s->krnl, 3, sizeof(cl_int), &zero)); // src_offset - CL_CHECK(clSetKernelArg(s->krnl, 4, sizeof(cl_int), &in_y_height)); // src_rows - CL_CHECK(clSetKernelArg(s->krnl, 5, sizeof(cl_int), &in_y_width)); // src_cols - CL_CHECK(clSetKernelArg(s->krnl, 6, sizeof(cl_mem), &out_y)); // dst - CL_CHECK(clSetKernelArg(s->krnl, 7, sizeof(cl_int), &out_y_width)); // dst_row_stride - CL_CHECK(clSetKernelArg(s->krnl, 8, sizeof(cl_int), &zero)); // dst_offset - CL_CHECK(clSetKernelArg(s->krnl, 9, sizeof(cl_int), &out_y_height)); // dst_rows - CL_CHECK(clSetKernelArg(s->krnl, 10, sizeof(cl_int), &out_y_width)); // dst_cols - CL_CHECK(clSetKernelArg(s->krnl, 11, sizeof(cl_mem), &s->m_y_cl)); // M - - const size_t work_size_y[2] = {(size_t)out_y_width, (size_t)out_y_height}; - - CL_CHECK(clEnqueueNDRangeKernel(q, s->krnl, 2, NULL, - (const size_t*)&work_size_y, NULL, 0, 0, NULL)); - - const size_t work_size_uv[2] = {(size_t)out_uv_width, (size_t)out_uv_height}; - - CL_CHECK(clSetKernelArg(s->krnl, 2, sizeof(cl_int), &in_uv_px_stride)); // src_px_stride - CL_CHECK(clSetKernelArg(s->krnl, 3, sizeof(cl_int), &in_u_offset)); // src_offset - CL_CHECK(clSetKernelArg(s->krnl, 4, sizeof(cl_int), &in_uv_height)); // src_rows - CL_CHECK(clSetKernelArg(s->krnl, 5, sizeof(cl_int), &in_uv_width)); // src_cols - CL_CHECK(clSetKernelArg(s->krnl, 6, sizeof(cl_mem), &out_u)); // dst - CL_CHECK(clSetKernelArg(s->krnl, 7, sizeof(cl_int), &out_uv_width)); // dst_row_stride - CL_CHECK(clSetKernelArg(s->krnl, 8, sizeof(cl_int), &zero)); // dst_offset - CL_CHECK(clSetKernelArg(s->krnl, 9, sizeof(cl_int), &out_uv_height)); // dst_rows - CL_CHECK(clSetKernelArg(s->krnl, 10, sizeof(cl_int), &out_uv_width)); // dst_cols - CL_CHECK(clSetKernelArg(s->krnl, 11, sizeof(cl_mem), &s->m_uv_cl)); // M - - CL_CHECK(clEnqueueNDRangeKernel(q, s->krnl, 2, NULL, - (const size_t*)&work_size_uv, NULL, 0, 0, NULL)); - CL_CHECK(clSetKernelArg(s->krnl, 3, sizeof(cl_int), &in_v_offset)); // src_ofset - CL_CHECK(clSetKernelArg(s->krnl, 6, sizeof(cl_mem), &out_v)); // dst - - CL_CHECK(clEnqueueNDRangeKernel(q, s->krnl, 2, NULL, - (const size_t*)&work_size_uv, NULL, 0, 0, NULL)); -} diff --git a/selfdrive/modeld/transforms/transform.cl b/selfdrive/modeld/transforms/transform.cl deleted file mode 100644 index 2ca25920cd..0000000000 --- a/selfdrive/modeld/transforms/transform.cl +++ /dev/null @@ -1,54 +0,0 @@ -#define INTER_BITS 5 -#define INTER_TAB_SIZE (1 << INTER_BITS) -#define INTER_SCALE 1.f / INTER_TAB_SIZE - -#define INTER_REMAP_COEF_BITS 15 -#define INTER_REMAP_COEF_SCALE (1 << INTER_REMAP_COEF_BITS) - -__kernel void warpPerspective(__global const uchar * src, - int src_row_stride, int src_px_stride, int src_offset, int src_rows, int src_cols, - __global uchar * dst, - int dst_row_stride, int dst_offset, int dst_rows, int dst_cols, - __constant float * M) -{ - int dx = get_global_id(0); - int dy = get_global_id(1); - - if (dx < dst_cols && dy < dst_rows) - { - float X0 = M[0] * dx + M[1] * dy + M[2]; - float Y0 = M[3] * dx + M[4] * dy + M[5]; - float W = M[6] * dx + M[7] * dy + M[8]; - W = W != 0.0f ? INTER_TAB_SIZE / W : 0.0f; - int X = rint(X0 * W), Y = rint(Y0 * W); - - int sx = convert_short_sat(X >> INTER_BITS); - int sy = convert_short_sat(Y >> INTER_BITS); - - short sx_clamp = clamp(sx, 0, src_cols - 1); - short sx_p1_clamp = clamp(sx + 1, 0, src_cols - 1); - short sy_clamp = clamp(sy, 0, src_rows - 1); - short sy_p1_clamp = clamp(sy + 1, 0, src_rows - 1); - int v0 = convert_int(src[mad24(sy_clamp, src_row_stride, src_offset + sx_clamp*src_px_stride)]); - int v1 = convert_int(src[mad24(sy_clamp, src_row_stride, src_offset + sx_p1_clamp*src_px_stride)]); - int v2 = convert_int(src[mad24(sy_p1_clamp, src_row_stride, src_offset + sx_clamp*src_px_stride)]); - int v3 = convert_int(src[mad24(sy_p1_clamp, src_row_stride, src_offset + sx_p1_clamp*src_px_stride)]); - - short ay = (short)(Y & (INTER_TAB_SIZE - 1)); - short ax = (short)(X & (INTER_TAB_SIZE - 1)); - float taby = 1.f/INTER_TAB_SIZE*ay; - float tabx = 1.f/INTER_TAB_SIZE*ax; - - int dst_index = mad24(dy, dst_row_stride, dst_offset + dx); - - int itab0 = convert_short_sat_rte( (1.0f-taby)*(1.0f-tabx) * INTER_REMAP_COEF_SCALE ); - int itab1 = convert_short_sat_rte( (1.0f-taby)*tabx * INTER_REMAP_COEF_SCALE ); - int itab2 = convert_short_sat_rte( taby*(1.0f-tabx) * INTER_REMAP_COEF_SCALE ); - int itab3 = convert_short_sat_rte( taby*tabx * INTER_REMAP_COEF_SCALE ); - - int val = v0 * itab0 + v1 * itab1 + v2 * itab2 + v3 * itab3; - - uchar pix = convert_uchar_sat((val + (1 << (INTER_REMAP_COEF_BITS-1))) >> INTER_REMAP_COEF_BITS); - dst[dst_index] = pix; - } -} diff --git a/selfdrive/modeld/transforms/transform.h b/selfdrive/modeld/transforms/transform.h deleted file mode 100644 index 771a7054b3..0000000000 --- a/selfdrive/modeld/transforms/transform.h +++ /dev/null @@ -1,25 +0,0 @@ -#pragma once - -#define CL_USE_DEPRECATED_OPENCL_1_2_APIS -#ifdef __APPLE__ -#include -#else -#include -#endif - -#include "common/mat.h" - -typedef struct { - cl_kernel krnl; - cl_mem m_y_cl, m_uv_cl; -} Transform; - -void transform_init(Transform* s, cl_context ctx, cl_device_id device_id); - -void transform_destroy(Transform* transform); - -void transform_queue(Transform* s, cl_command_queue q, - cl_mem yuv, int in_width, int in_height, int in_stride, int in_uv_offset, - cl_mem out_y, cl_mem out_u, cl_mem out_v, - int out_width, int out_height, - const mat3& projection); diff --git a/selfdrive/test/process_replay/model_replay.py b/selfdrive/test/process_replay/model_replay.py index d35474f373..3f671f610d 100755 --- a/selfdrive/test/process_replay/model_replay.py +++ b/selfdrive/test/process_replay/model_replay.py @@ -34,8 +34,8 @@ GITHUB = GithubUtils(API_TOKEN, DATA_TOKEN) EXEC_TIMINGS = [ # model, instant max, average max - ("modelV2", 0.035, 0.025), - ("driverStateV2", 0.02, 0.015), + ("modelV2", 0.05, 0.028), + ("driverStateV2", 0.05, 0.015), ] def get_log_fn(test_route, ref="master"): diff --git a/selfdrive/test/process_replay/process_replay.py b/selfdrive/test/process_replay/process_replay.py index 8af72e5f4e..d168a7e800 100755 --- a/selfdrive/test/process_replay/process_replay.py +++ b/selfdrive/test/process_replay/process_replay.py @@ -4,6 +4,7 @@ import time import copy import heapq import signal +import numpy as np from collections import Counter from dataclasses import dataclass, field from itertools import islice @@ -23,6 +24,7 @@ from openpilot.common.params import Params from openpilot.common.prefix import OpenpilotPrefix from openpilot.common.timeout import Timeout from openpilot.common.realtime import DT_CTRL +from openpilot.system.camerad.cameras.nv12_info import get_nv12_info from openpilot.system.manager.process_config import managed_processes from openpilot.selfdrive.test.process_replay.vision_meta import meta_from_camera_state, available_streams from openpilot.selfdrive.test.process_replay.migration import migrate_all @@ -203,7 +205,8 @@ class ProcessContainer: if meta.camera_state in self.cfg.vision_pubs: assert frs[meta.camera_state].pix_fmt == 'nv12' frame_size = (frs[meta.camera_state].w, frs[meta.camera_state].h) - vipc_server.create_buffers(meta.stream, 2, *frame_size) + stride, y_height, _, yuv_size = get_nv12_info(frame_size[0], frame_size[1]) + vipc_server.create_buffers_with_sizes(meta.stream, 2, frame_size[0], frame_size[1], yuv_size, stride, stride * y_height) vipc_server.start_listener() self.vipc_server = vipc_server @@ -300,7 +303,17 @@ class ProcessContainer: camera_meta = meta_from_camera_state(m.which()) assert frs is not None img = frs[m.which()].get(camera_state.frameId) - self.vipc_server.send(camera_meta.stream, img.flatten().tobytes(), + + h, w = frs[m.which()].h, frs[m.which()].w + stride, y_height, _, yuv_size = get_nv12_info(w, h) + uv_offset = stride * y_height + padded_img = np.zeros(((uv_offset //stride) + (h // 2), stride)) + padded_img[:h, :w] = img[:h * w].reshape((-1, w)) + padded_img[uv_offset // stride:uv_offset // stride + h // 2, :w] = img[h * w:].reshape((-1, w)) + img_bytes = np.zeros((yuv_size,), dtype=np.uint8) + img_bytes[:padded_img.size] = padded_img.flatten() + + self.vipc_server.send(camera_meta.stream, img_bytes.tobytes(), camera_state.frameId, camera_state.timestampSof, camera_state.timestampEof) self.msg_queue = [] diff --git a/system/camerad/SConscript b/system/camerad/SConscript index e288c6d8b0..c28330b32c 100644 --- a/system/camerad/SConscript +++ b/system/camerad/SConscript @@ -1,6 +1,6 @@ Import('env', 'arch', 'messaging', 'common', 'visionipc') -libs = [common, 'OpenCL', messaging, visionipc] +libs = [common, messaging, visionipc] if arch != "Darwin": camera_obj = env.Object(['cameras/camera_qcom2.cc', 'cameras/camera_common.cc', 'cameras/spectra.cc', diff --git a/system/camerad/cameras/camera_common.cc b/system/camerad/cameras/camera_common.cc index 88bca7f775..329192b63a 100644 --- a/system/camerad/cameras/camera_common.cc +++ b/system/camerad/cameras/camera_common.cc @@ -7,7 +7,7 @@ #include "system/camerad/cameras/spectra.h" -void CameraBuf::init(cl_device_id device_id, cl_context context, SpectraCamera *cam, VisionIpcServer * v, int frame_cnt, VisionStreamType type) { +void CameraBuf::init(SpectraCamera *cam, VisionIpcServer * v, int frame_cnt, VisionStreamType type) { vipc_server = v; stream_type = type; frame_buf_count = frame_cnt; @@ -21,9 +21,8 @@ void CameraBuf::init(cl_device_id device_id, cl_context context, SpectraCamera * const int raw_frame_size = (sensor->frame_height + sensor->extra_height) * sensor->frame_stride; for (int i = 0; i < frame_buf_count; i++) { camera_bufs_raw[i].allocate(raw_frame_size); - camera_bufs_raw[i].init_cl(device_id, context); } - LOGD("allocated %d CL buffers", frame_buf_count); + LOGD("allocated %d buffers", frame_buf_count); } vipc_server->create_buffers_with_sizes(stream_type, VIPC_BUFFER_COUNT, out_img_width, out_img_height, cam->yuv_size, cam->stride, cam->uv_offset); diff --git a/system/camerad/cameras/camera_common.h b/system/camerad/cameras/camera_common.h index c26859cbc4..7f35e06a83 100644 --- a/system/camerad/cameras/camera_common.h +++ b/system/camerad/cameras/camera_common.h @@ -36,7 +36,7 @@ public: CameraBuf() = default; ~CameraBuf(); - void init(cl_device_id device_id, cl_context context, SpectraCamera *cam, VisionIpcServer * v, int frame_cnt, VisionStreamType type); + void init(SpectraCamera *cam, VisionIpcServer * v, int frame_cnt, VisionStreamType type); void sendFrameToVipc(); }; diff --git a/system/camerad/cameras/camera_qcom2.cc b/system/camerad/cameras/camera_qcom2.cc index d741e13cf3..6a7f599ab6 100644 --- a/system/camerad/cameras/camera_qcom2.cc +++ b/system/camerad/cameras/camera_qcom2.cc @@ -12,16 +12,8 @@ #include #include -#ifdef __TICI__ -#include "CL/cl_ext_qcom.h" -#else -#define CL_PRIORITY_HINT_HIGH_QCOM NULL -#define CL_CONTEXT_PRIORITY_HINT_QCOM NULL -#endif - #include "media/cam_sensor_cmn_header.h" -#include "common/clutil.h" #include "common/params.h" #include "common/swaglog.h" @@ -57,7 +49,7 @@ public: CameraState(SpectraMaster *master, const CameraConfig &config) : camera(master, config) {}; ~CameraState(); - void init(VisionIpcServer *v, cl_device_id device_id, cl_context ctx); + void init(VisionIpcServer *v); void update_exposure_score(float desired_ev, int exp_t, int exp_g_idx, float exp_gain); void set_camera_exposure(float grey_frac); void set_exposure_rect(); @@ -68,8 +60,8 @@ public: } }; -void CameraState::init(VisionIpcServer *v, cl_device_id device_id, cl_context ctx) { - camera.camera_open(v, device_id, ctx); +void CameraState::init(VisionIpcServer *v) { + camera.camera_open(v); if (!camera.enabled) return; @@ -257,11 +249,7 @@ void CameraState::sendState() { void camerad_thread() { // TODO: centralize enabled handling - cl_device_id device_id = cl_get_device_id(CL_DEVICE_TYPE_DEFAULT); - const cl_context_properties props[] = {CL_CONTEXT_PRIORITY_HINT_QCOM, CL_PRIORITY_HINT_HIGH_QCOM, 0}; - cl_context ctx = CL_CHECK_ERR(clCreateContext(props, 1, &device_id, NULL, NULL, &err)); - - VisionIpcServer v("camerad", device_id, ctx); + VisionIpcServer v("camerad"); // *** initial ISP init *** SpectraMaster m; @@ -271,7 +259,7 @@ void camerad_thread() { std::vector> cams; for (const auto &config : ALL_CAMERA_CONFIGS) { auto cam = std::make_unique(&m, config); - cam->init(&v, device_id, ctx); + cam->init(&v); cams.emplace_back(std::move(cam)); } diff --git a/system/camerad/cameras/spectra.cc b/system/camerad/cameras/spectra.cc index 5c3e7a9d23..73e0a78da3 100644 --- a/system/camerad/cameras/spectra.cc +++ b/system/camerad/cameras/spectra.cc @@ -274,7 +274,7 @@ int SpectraCamera::clear_req_queue() { return ret; } -void SpectraCamera::camera_open(VisionIpcServer *v, cl_device_id device_id, cl_context ctx) { +void SpectraCamera::camera_open(VisionIpcServer *v) { if (!openSensor()) { return; } @@ -296,7 +296,7 @@ void SpectraCamera::camera_open(VisionIpcServer *v, cl_device_id device_id, cl_c linkDevices(); LOGD("camera init %d", cc.camera_num); - buf.init(device_id, ctx, this, v, ife_buf_depth, cc.stream_type); + buf.init(this, v, ife_buf_depth, cc.stream_type); camera_map_bufs(); clearAndRequeue(1); } diff --git a/system/camerad/cameras/spectra.h b/system/camerad/cameras/spectra.h index 13cb13f98f..a02b8a6cac 100644 --- a/system/camerad/cameras/spectra.h +++ b/system/camerad/cameras/spectra.h @@ -113,7 +113,7 @@ public: SpectraCamera(SpectraMaster *master, const CameraConfig &config); ~SpectraCamera(); - void camera_open(VisionIpcServer *v, cl_device_id device_id, cl_context ctx); + void camera_open(VisionIpcServer *v); bool handle_camera_event(const cam_req_mgr_message *event_data); void camera_close(); void camera_map_bufs(); diff --git a/system/hardware/tici/tests/test_power_draw.py b/system/hardware/tici/tests/test_power_draw.py index a92e4c8de8..c4401c9583 100644 --- a/system/hardware/tici/tests/test_power_draw.py +++ b/system/hardware/tici/tests/test_power_draw.py @@ -32,7 +32,7 @@ class Proc: PROCS = [ Proc(['camerad'], 1.65, atol=0.4, msgs=['roadCameraState', 'wideRoadCameraState', 'driverCameraState']), - Proc(['modeld'], 1.24, atol=0.2, msgs=['modelV2']), + Proc(['modeld'], 1.5, atol=0.2, msgs=['modelV2']), Proc(['dmonitoringmodeld'], 0.65, atol=0.35, msgs=['driverStateV2']), Proc(['encoderd'], 0.23, msgs=[]), ] diff --git a/system/loggerd/SConscript b/system/loggerd/SConscript index cf169f4dc6..cc8ef7c88f 100644 --- a/system/loggerd/SConscript +++ b/system/loggerd/SConscript @@ -2,16 +2,13 @@ Import('env', 'arch', 'messaging', 'common', 'visionipc') libs = [common, messaging, visionipc, 'avformat', 'avcodec', 'avutil', - 'yuv', 'OpenCL', 'pthread', 'zstd'] + 'yuv', 'pthread', 'zstd'] src = ['logger.cc', 'zstd_writer.cc', 'video_writer.cc', 'encoder/encoder.cc', 'encoder/v4l_encoder.cc', 'encoder/jpeg_encoder.cc'] if arch != "larch64": src += ['encoder/ffmpeg_encoder.cc'] if arch == "Darwin": - # fix OpenCL - del libs[libs.index('OpenCL')] - env['FRAMEWORKS'] = ['OpenCL'] # exclude v4l del src[src.index('encoder/v4l_encoder.cc')] diff --git a/third_party/opencl/include/CL/cl.h b/third_party/opencl/include/CL/cl.h deleted file mode 100644 index 0086319f5f..0000000000 --- a/third_party/opencl/include/CL/cl.h +++ /dev/null @@ -1,1452 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2008-2015 The Khronos Group Inc. - * - * Permission is hereby granted, free of charge, to any person obtaining a - * copy of this software and/or associated documentation files (the - * "Materials"), to deal in the Materials without restriction, including - * without limitation the rights to use, copy, modify, merge, publish, - * distribute, sublicense, and/or sell copies of the Materials, and to - * permit persons to whom the Materials are furnished to do so, subject to - * the following conditions: - * - * The above copyright notice and this permission notice shall be included - * in all copies or substantial portions of the Materials. - * - * MODIFICATIONS TO THIS FILE MAY MEAN IT NO LONGER ACCURATELY REFLECTS - * KHRONOS STANDARDS. THE UNMODIFIED, NORMATIVE VERSIONS OF KHRONOS - * SPECIFICATIONS AND HEADER INFORMATION ARE LOCATED AT - * https://www.khronos.org/registry/ - * - * THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. - * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY - * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, - * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE - * MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS. - ******************************************************************************/ - -#ifndef __OPENCL_CL_H -#define __OPENCL_CL_H - -#ifdef __APPLE__ -#include -#else -#include -#endif - -#ifdef __cplusplus -extern "C" { -#endif - -/******************************************************************************/ - -typedef struct _cl_platform_id * cl_platform_id; -typedef struct _cl_device_id * cl_device_id; -typedef struct _cl_context * cl_context; -typedef struct _cl_command_queue * cl_command_queue; -typedef struct _cl_mem * cl_mem; -typedef struct _cl_program * cl_program; -typedef struct _cl_kernel * cl_kernel; -typedef struct _cl_event * cl_event; -typedef struct _cl_sampler * cl_sampler; - -typedef cl_uint cl_bool; /* WARNING! Unlike cl_ types in cl_platform.h, cl_bool is not guaranteed to be the same size as the bool in kernels. */ -typedef cl_ulong cl_bitfield; -typedef cl_bitfield cl_device_type; -typedef cl_uint cl_platform_info; -typedef cl_uint cl_device_info; -typedef cl_bitfield cl_device_fp_config; -typedef cl_uint cl_device_mem_cache_type; -typedef cl_uint cl_device_local_mem_type; -typedef cl_bitfield cl_device_exec_capabilities; -typedef cl_bitfield cl_device_svm_capabilities; -typedef cl_bitfield cl_command_queue_properties; -typedef intptr_t cl_device_partition_property; -typedef cl_bitfield cl_device_affinity_domain; - -typedef intptr_t cl_context_properties; -typedef cl_uint cl_context_info; -typedef cl_bitfield cl_queue_properties; -typedef cl_uint cl_command_queue_info; -typedef cl_uint cl_channel_order; -typedef cl_uint cl_channel_type; -typedef cl_bitfield cl_mem_flags; -typedef cl_bitfield cl_svm_mem_flags; -typedef cl_uint cl_mem_object_type; -typedef cl_uint cl_mem_info; -typedef cl_bitfield cl_mem_migration_flags; -typedef cl_uint cl_image_info; -typedef cl_uint cl_buffer_create_type; -typedef cl_uint cl_addressing_mode; -typedef cl_uint cl_filter_mode; -typedef cl_uint cl_sampler_info; -typedef cl_bitfield cl_map_flags; -typedef intptr_t cl_pipe_properties; -typedef cl_uint cl_pipe_info; -typedef cl_uint cl_program_info; -typedef cl_uint cl_program_build_info; -typedef cl_uint cl_program_binary_type; -typedef cl_int cl_build_status; -typedef cl_uint cl_kernel_info; -typedef cl_uint cl_kernel_arg_info; -typedef cl_uint cl_kernel_arg_address_qualifier; -typedef cl_uint cl_kernel_arg_access_qualifier; -typedef cl_bitfield cl_kernel_arg_type_qualifier; -typedef cl_uint cl_kernel_work_group_info; -typedef cl_uint cl_kernel_sub_group_info; -typedef cl_uint cl_event_info; -typedef cl_uint cl_command_type; -typedef cl_uint cl_profiling_info; -typedef cl_bitfield cl_sampler_properties; -typedef cl_uint cl_kernel_exec_info; - -typedef struct _cl_image_format { - cl_channel_order image_channel_order; - cl_channel_type image_channel_data_type; -} cl_image_format; - -typedef struct _cl_image_desc { - cl_mem_object_type image_type; - size_t image_width; - size_t image_height; - size_t image_depth; - size_t image_array_size; - size_t image_row_pitch; - size_t image_slice_pitch; - cl_uint num_mip_levels; - cl_uint num_samples; -#ifdef __GNUC__ - __extension__ /* Prevents warnings about anonymous union in -pedantic builds */ -#endif - union { - cl_mem buffer; - cl_mem mem_object; - }; -} cl_image_desc; - -typedef struct _cl_buffer_region { - size_t origin; - size_t size; -} cl_buffer_region; - - -/******************************************************************************/ - -/* Error Codes */ -#define CL_SUCCESS 0 -#define CL_DEVICE_NOT_FOUND -1 -#define CL_DEVICE_NOT_AVAILABLE -2 -#define CL_COMPILER_NOT_AVAILABLE -3 -#define CL_MEM_OBJECT_ALLOCATION_FAILURE -4 -#define CL_OUT_OF_RESOURCES -5 -#define CL_OUT_OF_HOST_MEMORY -6 -#define CL_PROFILING_INFO_NOT_AVAILABLE -7 -#define CL_MEM_COPY_OVERLAP -8 -#define CL_IMAGE_FORMAT_MISMATCH -9 -#define CL_IMAGE_FORMAT_NOT_SUPPORTED -10 -#define CL_BUILD_PROGRAM_FAILURE -11 -#define CL_MAP_FAILURE -12 -#define CL_MISALIGNED_SUB_BUFFER_OFFSET -13 -#define CL_EXEC_STATUS_ERROR_FOR_EVENTS_IN_WAIT_LIST -14 -#define CL_COMPILE_PROGRAM_FAILURE -15 -#define CL_LINKER_NOT_AVAILABLE -16 -#define CL_LINK_PROGRAM_FAILURE -17 -#define CL_DEVICE_PARTITION_FAILED -18 -#define CL_KERNEL_ARG_INFO_NOT_AVAILABLE -19 - -#define CL_INVALID_VALUE -30 -#define CL_INVALID_DEVICE_TYPE -31 -#define CL_INVALID_PLATFORM -32 -#define CL_INVALID_DEVICE -33 -#define CL_INVALID_CONTEXT -34 -#define CL_INVALID_QUEUE_PROPERTIES -35 -#define CL_INVALID_COMMAND_QUEUE -36 -#define CL_INVALID_HOST_PTR -37 -#define CL_INVALID_MEM_OBJECT -38 -#define CL_INVALID_IMAGE_FORMAT_DESCRIPTOR -39 -#define CL_INVALID_IMAGE_SIZE -40 -#define CL_INVALID_SAMPLER -41 -#define CL_INVALID_BINARY -42 -#define CL_INVALID_BUILD_OPTIONS -43 -#define CL_INVALID_PROGRAM -44 -#define CL_INVALID_PROGRAM_EXECUTABLE -45 -#define CL_INVALID_KERNEL_NAME -46 -#define CL_INVALID_KERNEL_DEFINITION -47 -#define CL_INVALID_KERNEL -48 -#define CL_INVALID_ARG_INDEX -49 -#define CL_INVALID_ARG_VALUE -50 -#define CL_INVALID_ARG_SIZE -51 -#define CL_INVALID_KERNEL_ARGS -52 -#define CL_INVALID_WORK_DIMENSION -53 -#define CL_INVALID_WORK_GROUP_SIZE -54 -#define CL_INVALID_WORK_ITEM_SIZE -55 -#define CL_INVALID_GLOBAL_OFFSET -56 -#define CL_INVALID_EVENT_WAIT_LIST -57 -#define CL_INVALID_EVENT -58 -#define CL_INVALID_OPERATION -59 -#define CL_INVALID_GL_OBJECT -60 -#define CL_INVALID_BUFFER_SIZE -61 -#define CL_INVALID_MIP_LEVEL -62 -#define CL_INVALID_GLOBAL_WORK_SIZE -63 -#define CL_INVALID_PROPERTY -64 -#define CL_INVALID_IMAGE_DESCRIPTOR -65 -#define CL_INVALID_COMPILER_OPTIONS -66 -#define CL_INVALID_LINKER_OPTIONS -67 -#define CL_INVALID_DEVICE_PARTITION_COUNT -68 -#define CL_INVALID_PIPE_SIZE -69 -#define CL_INVALID_DEVICE_QUEUE -70 - -/* OpenCL Version */ -#define CL_VERSION_1_0 1 -#define CL_VERSION_1_1 1 -#define CL_VERSION_1_2 1 -#define CL_VERSION_2_0 1 -#define CL_VERSION_2_1 1 - -/* cl_bool */ -#define CL_FALSE 0 -#define CL_TRUE 1 -#define CL_BLOCKING CL_TRUE -#define CL_NON_BLOCKING CL_FALSE - -/* cl_platform_info */ -#define CL_PLATFORM_PROFILE 0x0900 -#define CL_PLATFORM_VERSION 0x0901 -#define CL_PLATFORM_NAME 0x0902 -#define CL_PLATFORM_VENDOR 0x0903 -#define CL_PLATFORM_EXTENSIONS 0x0904 -#define CL_PLATFORM_HOST_TIMER_RESOLUTION 0x0905 - -/* cl_device_type - bitfield */ -#define CL_DEVICE_TYPE_DEFAULT (1 << 0) -#define CL_DEVICE_TYPE_CPU (1 << 1) -#define CL_DEVICE_TYPE_GPU (1 << 2) -#define CL_DEVICE_TYPE_ACCELERATOR (1 << 3) -#define CL_DEVICE_TYPE_CUSTOM (1 << 4) -#define CL_DEVICE_TYPE_ALL 0xFFFFFFFF - -/* cl_device_info */ -#define CL_DEVICE_TYPE 0x1000 -#define CL_DEVICE_VENDOR_ID 0x1001 -#define CL_DEVICE_MAX_COMPUTE_UNITS 0x1002 -#define CL_DEVICE_MAX_WORK_ITEM_DIMENSIONS 0x1003 -#define CL_DEVICE_MAX_WORK_GROUP_SIZE 0x1004 -#define CL_DEVICE_MAX_WORK_ITEM_SIZES 0x1005 -#define CL_DEVICE_PREFERRED_VECTOR_WIDTH_CHAR 0x1006 -#define CL_DEVICE_PREFERRED_VECTOR_WIDTH_SHORT 0x1007 -#define CL_DEVICE_PREFERRED_VECTOR_WIDTH_INT 0x1008 -#define CL_DEVICE_PREFERRED_VECTOR_WIDTH_LONG 0x1009 -#define CL_DEVICE_PREFERRED_VECTOR_WIDTH_FLOAT 0x100A -#define CL_DEVICE_PREFERRED_VECTOR_WIDTH_DOUBLE 0x100B -#define CL_DEVICE_MAX_CLOCK_FREQUENCY 0x100C -#define CL_DEVICE_ADDRESS_BITS 0x100D -#define CL_DEVICE_MAX_READ_IMAGE_ARGS 0x100E -#define CL_DEVICE_MAX_WRITE_IMAGE_ARGS 0x100F -#define CL_DEVICE_MAX_MEM_ALLOC_SIZE 0x1010 -#define CL_DEVICE_IMAGE2D_MAX_WIDTH 0x1011 -#define CL_DEVICE_IMAGE2D_MAX_HEIGHT 0x1012 -#define CL_DEVICE_IMAGE3D_MAX_WIDTH 0x1013 -#define CL_DEVICE_IMAGE3D_MAX_HEIGHT 0x1014 -#define CL_DEVICE_IMAGE3D_MAX_DEPTH 0x1015 -#define CL_DEVICE_IMAGE_SUPPORT 0x1016 -#define CL_DEVICE_MAX_PARAMETER_SIZE 0x1017 -#define CL_DEVICE_MAX_SAMPLERS 0x1018 -#define CL_DEVICE_MEM_BASE_ADDR_ALIGN 0x1019 -#define CL_DEVICE_MIN_DATA_TYPE_ALIGN_SIZE 0x101A -#define CL_DEVICE_SINGLE_FP_CONFIG 0x101B -#define CL_DEVICE_GLOBAL_MEM_CACHE_TYPE 0x101C -#define CL_DEVICE_GLOBAL_MEM_CACHELINE_SIZE 0x101D -#define CL_DEVICE_GLOBAL_MEM_CACHE_SIZE 0x101E -#define CL_DEVICE_GLOBAL_MEM_SIZE 0x101F -#define CL_DEVICE_MAX_CONSTANT_BUFFER_SIZE 0x1020 -#define CL_DEVICE_MAX_CONSTANT_ARGS 0x1021 -#define CL_DEVICE_LOCAL_MEM_TYPE 0x1022 -#define CL_DEVICE_LOCAL_MEM_SIZE 0x1023 -#define CL_DEVICE_ERROR_CORRECTION_SUPPORT 0x1024 -#define CL_DEVICE_PROFILING_TIMER_RESOLUTION 0x1025 -#define CL_DEVICE_ENDIAN_LITTLE 0x1026 -#define CL_DEVICE_AVAILABLE 0x1027 -#define CL_DEVICE_COMPILER_AVAILABLE 0x1028 -#define CL_DEVICE_EXECUTION_CAPABILITIES 0x1029 -#define CL_DEVICE_QUEUE_PROPERTIES 0x102A /* deprecated */ -#define CL_DEVICE_QUEUE_ON_HOST_PROPERTIES 0x102A -#define CL_DEVICE_NAME 0x102B -#define CL_DEVICE_VENDOR 0x102C -#define CL_DRIVER_VERSION 0x102D -#define CL_DEVICE_PROFILE 0x102E -#define CL_DEVICE_VERSION 0x102F -#define CL_DEVICE_EXTENSIONS 0x1030 -#define CL_DEVICE_PLATFORM 0x1031 -#define CL_DEVICE_DOUBLE_FP_CONFIG 0x1032 -/* 0x1033 reserved for CL_DEVICE_HALF_FP_CONFIG */ -#define CL_DEVICE_PREFERRED_VECTOR_WIDTH_HALF 0x1034 -#define CL_DEVICE_HOST_UNIFIED_MEMORY 0x1035 /* deprecated */ -#define CL_DEVICE_NATIVE_VECTOR_WIDTH_CHAR 0x1036 -#define CL_DEVICE_NATIVE_VECTOR_WIDTH_SHORT 0x1037 -#define CL_DEVICE_NATIVE_VECTOR_WIDTH_INT 0x1038 -#define CL_DEVICE_NATIVE_VECTOR_WIDTH_LONG 0x1039 -#define CL_DEVICE_NATIVE_VECTOR_WIDTH_FLOAT 0x103A -#define CL_DEVICE_NATIVE_VECTOR_WIDTH_DOUBLE 0x103B -#define CL_DEVICE_NATIVE_VECTOR_WIDTH_HALF 0x103C -#define CL_DEVICE_OPENCL_C_VERSION 0x103D -#define CL_DEVICE_LINKER_AVAILABLE 0x103E -#define CL_DEVICE_BUILT_IN_KERNELS 0x103F -#define CL_DEVICE_IMAGE_MAX_BUFFER_SIZE 0x1040 -#define CL_DEVICE_IMAGE_MAX_ARRAY_SIZE 0x1041 -#define CL_DEVICE_PARENT_DEVICE 0x1042 -#define CL_DEVICE_PARTITION_MAX_SUB_DEVICES 0x1043 -#define CL_DEVICE_PARTITION_PROPERTIES 0x1044 -#define CL_DEVICE_PARTITION_AFFINITY_DOMAIN 0x1045 -#define CL_DEVICE_PARTITION_TYPE 0x1046 -#define CL_DEVICE_REFERENCE_COUNT 0x1047 -#define CL_DEVICE_PREFERRED_INTEROP_USER_SYNC 0x1048 -#define CL_DEVICE_PRINTF_BUFFER_SIZE 0x1049 -#define CL_DEVICE_IMAGE_PITCH_ALIGNMENT 0x104A -#define CL_DEVICE_IMAGE_BASE_ADDRESS_ALIGNMENT 0x104B -#define CL_DEVICE_MAX_READ_WRITE_IMAGE_ARGS 0x104C -#define CL_DEVICE_MAX_GLOBAL_VARIABLE_SIZE 0x104D -#define CL_DEVICE_QUEUE_ON_DEVICE_PROPERTIES 0x104E -#define CL_DEVICE_QUEUE_ON_DEVICE_PREFERRED_SIZE 0x104F -#define CL_DEVICE_QUEUE_ON_DEVICE_MAX_SIZE 0x1050 -#define CL_DEVICE_MAX_ON_DEVICE_QUEUES 0x1051 -#define CL_DEVICE_MAX_ON_DEVICE_EVENTS 0x1052 -#define CL_DEVICE_SVM_CAPABILITIES 0x1053 -#define CL_DEVICE_GLOBAL_VARIABLE_PREFERRED_TOTAL_SIZE 0x1054 -#define CL_DEVICE_MAX_PIPE_ARGS 0x1055 -#define CL_DEVICE_PIPE_MAX_ACTIVE_RESERVATIONS 0x1056 -#define CL_DEVICE_PIPE_MAX_PACKET_SIZE 0x1057 -#define CL_DEVICE_PREFERRED_PLATFORM_ATOMIC_ALIGNMENT 0x1058 -#define CL_DEVICE_PREFERRED_GLOBAL_ATOMIC_ALIGNMENT 0x1059 -#define CL_DEVICE_PREFERRED_LOCAL_ATOMIC_ALIGNMENT 0x105A -#define CL_DEVICE_IL_VERSION 0x105B -#define CL_DEVICE_MAX_NUM_SUB_GROUPS 0x105C -#define CL_DEVICE_SUB_GROUP_INDEPENDENT_FORWARD_PROGRESS 0x105D - -/* cl_device_fp_config - bitfield */ -#define CL_FP_DENORM (1 << 0) -#define CL_FP_INF_NAN (1 << 1) -#define CL_FP_ROUND_TO_NEAREST (1 << 2) -#define CL_FP_ROUND_TO_ZERO (1 << 3) -#define CL_FP_ROUND_TO_INF (1 << 4) -#define CL_FP_FMA (1 << 5) -#define CL_FP_SOFT_FLOAT (1 << 6) -#define CL_FP_CORRECTLY_ROUNDED_DIVIDE_SQRT (1 << 7) - -/* cl_device_mem_cache_type */ -#define CL_NONE 0x0 -#define CL_READ_ONLY_CACHE 0x1 -#define CL_READ_WRITE_CACHE 0x2 - -/* cl_device_local_mem_type */ -#define CL_LOCAL 0x1 -#define CL_GLOBAL 0x2 - -/* cl_device_exec_capabilities - bitfield */ -#define CL_EXEC_KERNEL (1 << 0) -#define CL_EXEC_NATIVE_KERNEL (1 << 1) - -/* cl_command_queue_properties - bitfield */ -#define CL_QUEUE_OUT_OF_ORDER_EXEC_MODE_ENABLE (1 << 0) -#define CL_QUEUE_PROFILING_ENABLE (1 << 1) -#define CL_QUEUE_ON_DEVICE (1 << 2) -#define CL_QUEUE_ON_DEVICE_DEFAULT (1 << 3) - -/* cl_context_info */ -#define CL_CONTEXT_REFERENCE_COUNT 0x1080 -#define CL_CONTEXT_DEVICES 0x1081 -#define CL_CONTEXT_PROPERTIES 0x1082 -#define CL_CONTEXT_NUM_DEVICES 0x1083 - -/* cl_context_properties */ -#define CL_CONTEXT_PLATFORM 0x1084 -#define CL_CONTEXT_INTEROP_USER_SYNC 0x1085 - -/* cl_device_partition_property */ -#define CL_DEVICE_PARTITION_EQUALLY 0x1086 -#define CL_DEVICE_PARTITION_BY_COUNTS 0x1087 -#define CL_DEVICE_PARTITION_BY_COUNTS_LIST_END 0x0 -#define CL_DEVICE_PARTITION_BY_AFFINITY_DOMAIN 0x1088 - -/* cl_device_affinity_domain */ -#define CL_DEVICE_AFFINITY_DOMAIN_NUMA (1 << 0) -#define CL_DEVICE_AFFINITY_DOMAIN_L4_CACHE (1 << 1) -#define CL_DEVICE_AFFINITY_DOMAIN_L3_CACHE (1 << 2) -#define CL_DEVICE_AFFINITY_DOMAIN_L2_CACHE (1 << 3) -#define CL_DEVICE_AFFINITY_DOMAIN_L1_CACHE (1 << 4) -#define CL_DEVICE_AFFINITY_DOMAIN_NEXT_PARTITIONABLE (1 << 5) - -/* cl_device_svm_capabilities */ -#define CL_DEVICE_SVM_COARSE_GRAIN_BUFFER (1 << 0) -#define CL_DEVICE_SVM_FINE_GRAIN_BUFFER (1 << 1) -#define CL_DEVICE_SVM_FINE_GRAIN_SYSTEM (1 << 2) -#define CL_DEVICE_SVM_ATOMICS (1 << 3) - -/* cl_command_queue_info */ -#define CL_QUEUE_CONTEXT 0x1090 -#define CL_QUEUE_DEVICE 0x1091 -#define CL_QUEUE_REFERENCE_COUNT 0x1092 -#define CL_QUEUE_PROPERTIES 0x1093 -#define CL_QUEUE_SIZE 0x1094 -#define CL_QUEUE_DEVICE_DEFAULT 0x1095 - -/* cl_mem_flags and cl_svm_mem_flags - bitfield */ -#define CL_MEM_READ_WRITE (1 << 0) -#define CL_MEM_WRITE_ONLY (1 << 1) -#define CL_MEM_READ_ONLY (1 << 2) -#define CL_MEM_USE_HOST_PTR (1 << 3) -#define CL_MEM_ALLOC_HOST_PTR (1 << 4) -#define CL_MEM_COPY_HOST_PTR (1 << 5) -/* reserved (1 << 6) */ -#define CL_MEM_HOST_WRITE_ONLY (1 << 7) -#define CL_MEM_HOST_READ_ONLY (1 << 8) -#define CL_MEM_HOST_NO_ACCESS (1 << 9) -#define CL_MEM_SVM_FINE_GRAIN_BUFFER (1 << 10) /* used by cl_svm_mem_flags only */ -#define CL_MEM_SVM_ATOMICS (1 << 11) /* used by cl_svm_mem_flags only */ -#define CL_MEM_KERNEL_READ_AND_WRITE (1 << 12) - -/* cl_mem_migration_flags - bitfield */ -#define CL_MIGRATE_MEM_OBJECT_HOST (1 << 0) -#define CL_MIGRATE_MEM_OBJECT_CONTENT_UNDEFINED (1 << 1) - -/* cl_channel_order */ -#define CL_R 0x10B0 -#define CL_A 0x10B1 -#define CL_RG 0x10B2 -#define CL_RA 0x10B3 -#define CL_RGB 0x10B4 -#define CL_RGBA 0x10B5 -#define CL_BGRA 0x10B6 -#define CL_ARGB 0x10B7 -#define CL_INTENSITY 0x10B8 -#define CL_LUMINANCE 0x10B9 -#define CL_Rx 0x10BA -#define CL_RGx 0x10BB -#define CL_RGBx 0x10BC -#define CL_DEPTH 0x10BD -#define CL_DEPTH_STENCIL 0x10BE -#define CL_sRGB 0x10BF -#define CL_sRGBx 0x10C0 -#define CL_sRGBA 0x10C1 -#define CL_sBGRA 0x10C2 -#define CL_ABGR 0x10C3 - -/* cl_channel_type */ -#define CL_SNORM_INT8 0x10D0 -#define CL_SNORM_INT16 0x10D1 -#define CL_UNORM_INT8 0x10D2 -#define CL_UNORM_INT16 0x10D3 -#define CL_UNORM_SHORT_565 0x10D4 -#define CL_UNORM_SHORT_555 0x10D5 -#define CL_UNORM_INT_101010 0x10D6 -#define CL_SIGNED_INT8 0x10D7 -#define CL_SIGNED_INT16 0x10D8 -#define CL_SIGNED_INT32 0x10D9 -#define CL_UNSIGNED_INT8 0x10DA -#define CL_UNSIGNED_INT16 0x10DB -#define CL_UNSIGNED_INT32 0x10DC -#define CL_HALF_FLOAT 0x10DD -#define CL_FLOAT 0x10DE -#define CL_UNORM_INT24 0x10DF -#define CL_UNORM_INT_101010_2 0x10E0 - -/* cl_mem_object_type */ -#define CL_MEM_OBJECT_BUFFER 0x10F0 -#define CL_MEM_OBJECT_IMAGE2D 0x10F1 -#define CL_MEM_OBJECT_IMAGE3D 0x10F2 -#define CL_MEM_OBJECT_IMAGE2D_ARRAY 0x10F3 -#define CL_MEM_OBJECT_IMAGE1D 0x10F4 -#define CL_MEM_OBJECT_IMAGE1D_ARRAY 0x10F5 -#define CL_MEM_OBJECT_IMAGE1D_BUFFER 0x10F6 -#define CL_MEM_OBJECT_PIPE 0x10F7 - -/* cl_mem_info */ -#define CL_MEM_TYPE 0x1100 -#define CL_MEM_FLAGS 0x1101 -#define CL_MEM_SIZE 0x1102 -#define CL_MEM_HOST_PTR 0x1103 -#define CL_MEM_MAP_COUNT 0x1104 -#define CL_MEM_REFERENCE_COUNT 0x1105 -#define CL_MEM_CONTEXT 0x1106 -#define CL_MEM_ASSOCIATED_MEMOBJECT 0x1107 -#define CL_MEM_OFFSET 0x1108 -#define CL_MEM_USES_SVM_POINTER 0x1109 - -/* cl_image_info */ -#define CL_IMAGE_FORMAT 0x1110 -#define CL_IMAGE_ELEMENT_SIZE 0x1111 -#define CL_IMAGE_ROW_PITCH 0x1112 -#define CL_IMAGE_SLICE_PITCH 0x1113 -#define CL_IMAGE_WIDTH 0x1114 -#define CL_IMAGE_HEIGHT 0x1115 -#define CL_IMAGE_DEPTH 0x1116 -#define CL_IMAGE_ARRAY_SIZE 0x1117 -#define CL_IMAGE_BUFFER 0x1118 -#define CL_IMAGE_NUM_MIP_LEVELS 0x1119 -#define CL_IMAGE_NUM_SAMPLES 0x111A - -/* cl_pipe_info */ -#define CL_PIPE_PACKET_SIZE 0x1120 -#define CL_PIPE_MAX_PACKETS 0x1121 - -/* cl_addressing_mode */ -#define CL_ADDRESS_NONE 0x1130 -#define CL_ADDRESS_CLAMP_TO_EDGE 0x1131 -#define CL_ADDRESS_CLAMP 0x1132 -#define CL_ADDRESS_REPEAT 0x1133 -#define CL_ADDRESS_MIRRORED_REPEAT 0x1134 - -/* cl_filter_mode */ -#define CL_FILTER_NEAREST 0x1140 -#define CL_FILTER_LINEAR 0x1141 - -/* cl_sampler_info */ -#define CL_SAMPLER_REFERENCE_COUNT 0x1150 -#define CL_SAMPLER_CONTEXT 0x1151 -#define CL_SAMPLER_NORMALIZED_COORDS 0x1152 -#define CL_SAMPLER_ADDRESSING_MODE 0x1153 -#define CL_SAMPLER_FILTER_MODE 0x1154 -#define CL_SAMPLER_MIP_FILTER_MODE 0x1155 -#define CL_SAMPLER_LOD_MIN 0x1156 -#define CL_SAMPLER_LOD_MAX 0x1157 - -/* cl_map_flags - bitfield */ -#define CL_MAP_READ (1 << 0) -#define CL_MAP_WRITE (1 << 1) -#define CL_MAP_WRITE_INVALIDATE_REGION (1 << 2) - -/* cl_program_info */ -#define CL_PROGRAM_REFERENCE_COUNT 0x1160 -#define CL_PROGRAM_CONTEXT 0x1161 -#define CL_PROGRAM_NUM_DEVICES 0x1162 -#define CL_PROGRAM_DEVICES 0x1163 -#define CL_PROGRAM_SOURCE 0x1164 -#define CL_PROGRAM_BINARY_SIZES 0x1165 -#define CL_PROGRAM_BINARIES 0x1166 -#define CL_PROGRAM_NUM_KERNELS 0x1167 -#define CL_PROGRAM_KERNEL_NAMES 0x1168 -#define CL_PROGRAM_IL 0x1169 - -/* cl_program_build_info */ -#define CL_PROGRAM_BUILD_STATUS 0x1181 -#define CL_PROGRAM_BUILD_OPTIONS 0x1182 -#define CL_PROGRAM_BUILD_LOG 0x1183 -#define CL_PROGRAM_BINARY_TYPE 0x1184 -#define CL_PROGRAM_BUILD_GLOBAL_VARIABLE_TOTAL_SIZE 0x1185 - -/* cl_program_binary_type */ -#define CL_PROGRAM_BINARY_TYPE_NONE 0x0 -#define CL_PROGRAM_BINARY_TYPE_COMPILED_OBJECT 0x1 -#define CL_PROGRAM_BINARY_TYPE_LIBRARY 0x2 -#define CL_PROGRAM_BINARY_TYPE_EXECUTABLE 0x4 - -/* cl_build_status */ -#define CL_BUILD_SUCCESS 0 -#define CL_BUILD_NONE -1 -#define CL_BUILD_ERROR -2 -#define CL_BUILD_IN_PROGRESS -3 - -/* cl_kernel_info */ -#define CL_KERNEL_FUNCTION_NAME 0x1190 -#define CL_KERNEL_NUM_ARGS 0x1191 -#define CL_KERNEL_REFERENCE_COUNT 0x1192 -#define CL_KERNEL_CONTEXT 0x1193 -#define CL_KERNEL_PROGRAM 0x1194 -#define CL_KERNEL_ATTRIBUTES 0x1195 -#define CL_KERNEL_MAX_NUM_SUB_GROUPS 0x11B9 -#define CL_KERNEL_COMPILE_NUM_SUB_GROUPS 0x11BA - -/* cl_kernel_arg_info */ -#define CL_KERNEL_ARG_ADDRESS_QUALIFIER 0x1196 -#define CL_KERNEL_ARG_ACCESS_QUALIFIER 0x1197 -#define CL_KERNEL_ARG_TYPE_NAME 0x1198 -#define CL_KERNEL_ARG_TYPE_QUALIFIER 0x1199 -#define CL_KERNEL_ARG_NAME 0x119A - -/* cl_kernel_arg_address_qualifier */ -#define CL_KERNEL_ARG_ADDRESS_GLOBAL 0x119B -#define CL_KERNEL_ARG_ADDRESS_LOCAL 0x119C -#define CL_KERNEL_ARG_ADDRESS_CONSTANT 0x119D -#define CL_KERNEL_ARG_ADDRESS_PRIVATE 0x119E - -/* cl_kernel_arg_access_qualifier */ -#define CL_KERNEL_ARG_ACCESS_READ_ONLY 0x11A0 -#define CL_KERNEL_ARG_ACCESS_WRITE_ONLY 0x11A1 -#define CL_KERNEL_ARG_ACCESS_READ_WRITE 0x11A2 -#define CL_KERNEL_ARG_ACCESS_NONE 0x11A3 - -/* cl_kernel_arg_type_qualifer */ -#define CL_KERNEL_ARG_TYPE_NONE 0 -#define CL_KERNEL_ARG_TYPE_CONST (1 << 0) -#define CL_KERNEL_ARG_TYPE_RESTRICT (1 << 1) -#define CL_KERNEL_ARG_TYPE_VOLATILE (1 << 2) -#define CL_KERNEL_ARG_TYPE_PIPE (1 << 3) - -/* cl_kernel_work_group_info */ -#define CL_KERNEL_WORK_GROUP_SIZE 0x11B0 -#define CL_KERNEL_COMPILE_WORK_GROUP_SIZE 0x11B1 -#define CL_KERNEL_LOCAL_MEM_SIZE 0x11B2 -#define CL_KERNEL_PREFERRED_WORK_GROUP_SIZE_MULTIPLE 0x11B3 -#define CL_KERNEL_PRIVATE_MEM_SIZE 0x11B4 -#define CL_KERNEL_GLOBAL_WORK_SIZE 0x11B5 - -/* cl_kernel_sub_group_info */ -#define CL_KERNEL_MAX_SUB_GROUP_SIZE_FOR_NDRANGE 0x2033 -#define CL_KERNEL_SUB_GROUP_COUNT_FOR_NDRANGE 0x2034 -#define CL_KERNEL_LOCAL_SIZE_FOR_SUB_GROUP_COUNT 0x11B8 - -/* cl_kernel_exec_info */ -#define CL_KERNEL_EXEC_INFO_SVM_PTRS 0x11B6 -#define CL_KERNEL_EXEC_INFO_SVM_FINE_GRAIN_SYSTEM 0x11B7 - -/* cl_event_info */ -#define CL_EVENT_COMMAND_QUEUE 0x11D0 -#define CL_EVENT_COMMAND_TYPE 0x11D1 -#define CL_EVENT_REFERENCE_COUNT 0x11D2 -#define CL_EVENT_COMMAND_EXECUTION_STATUS 0x11D3 -#define CL_EVENT_CONTEXT 0x11D4 - -/* cl_command_type */ -#define CL_COMMAND_NDRANGE_KERNEL 0x11F0 -#define CL_COMMAND_TASK 0x11F1 -#define CL_COMMAND_NATIVE_KERNEL 0x11F2 -#define CL_COMMAND_READ_BUFFER 0x11F3 -#define CL_COMMAND_WRITE_BUFFER 0x11F4 -#define CL_COMMAND_COPY_BUFFER 0x11F5 -#define CL_COMMAND_READ_IMAGE 0x11F6 -#define CL_COMMAND_WRITE_IMAGE 0x11F7 -#define CL_COMMAND_COPY_IMAGE 0x11F8 -#define CL_COMMAND_COPY_IMAGE_TO_BUFFER 0x11F9 -#define CL_COMMAND_COPY_BUFFER_TO_IMAGE 0x11FA -#define CL_COMMAND_MAP_BUFFER 0x11FB -#define CL_COMMAND_MAP_IMAGE 0x11FC -#define CL_COMMAND_UNMAP_MEM_OBJECT 0x11FD -#define CL_COMMAND_MARKER 0x11FE -#define CL_COMMAND_ACQUIRE_GL_OBJECTS 0x11FF -#define CL_COMMAND_RELEASE_GL_OBJECTS 0x1200 -#define CL_COMMAND_READ_BUFFER_RECT 0x1201 -#define CL_COMMAND_WRITE_BUFFER_RECT 0x1202 -#define CL_COMMAND_COPY_BUFFER_RECT 0x1203 -#define CL_COMMAND_USER 0x1204 -#define CL_COMMAND_BARRIER 0x1205 -#define CL_COMMAND_MIGRATE_MEM_OBJECTS 0x1206 -#define CL_COMMAND_FILL_BUFFER 0x1207 -#define CL_COMMAND_FILL_IMAGE 0x1208 -#define CL_COMMAND_SVM_FREE 0x1209 -#define CL_COMMAND_SVM_MEMCPY 0x120A -#define CL_COMMAND_SVM_MEMFILL 0x120B -#define CL_COMMAND_SVM_MAP 0x120C -#define CL_COMMAND_SVM_UNMAP 0x120D - -/* command execution status */ -#define CL_COMPLETE 0x0 -#define CL_RUNNING 0x1 -#define CL_SUBMITTED 0x2 -#define CL_QUEUED 0x3 - -/* cl_buffer_create_type */ -#define CL_BUFFER_CREATE_TYPE_REGION 0x1220 - -/* cl_profiling_info */ -#define CL_PROFILING_COMMAND_QUEUED 0x1280 -#define CL_PROFILING_COMMAND_SUBMIT 0x1281 -#define CL_PROFILING_COMMAND_START 0x1282 -#define CL_PROFILING_COMMAND_END 0x1283 -#define CL_PROFILING_COMMAND_COMPLETE 0x1284 - -/********************************************************************************************************/ - -/* Platform API */ -extern CL_API_ENTRY cl_int CL_API_CALL -clGetPlatformIDs(cl_uint /* num_entries */, - cl_platform_id * /* platforms */, - cl_uint * /* num_platforms */) CL_API_SUFFIX__VERSION_1_0; - -extern CL_API_ENTRY cl_int CL_API_CALL -clGetPlatformInfo(cl_platform_id /* platform */, - cl_platform_info /* param_name */, - size_t /* param_value_size */, - void * /* param_value */, - size_t * /* param_value_size_ret */) CL_API_SUFFIX__VERSION_1_0; - -/* Device APIs */ -extern CL_API_ENTRY cl_int CL_API_CALL -clGetDeviceIDs(cl_platform_id /* platform */, - cl_device_type /* device_type */, - cl_uint /* num_entries */, - cl_device_id * /* devices */, - cl_uint * /* num_devices */) CL_API_SUFFIX__VERSION_1_0; - -extern CL_API_ENTRY cl_int CL_API_CALL -clGetDeviceInfo(cl_device_id /* device */, - cl_device_info /* param_name */, - size_t /* param_value_size */, - void * /* param_value */, - size_t * /* param_value_size_ret */) CL_API_SUFFIX__VERSION_1_0; - -extern CL_API_ENTRY cl_int CL_API_CALL -clCreateSubDevices(cl_device_id /* in_device */, - const cl_device_partition_property * /* properties */, - cl_uint /* num_devices */, - cl_device_id * /* out_devices */, - cl_uint * /* num_devices_ret */) CL_API_SUFFIX__VERSION_1_2; - -extern CL_API_ENTRY cl_int CL_API_CALL -clRetainDevice(cl_device_id /* device */) CL_API_SUFFIX__VERSION_1_2; - -extern CL_API_ENTRY cl_int CL_API_CALL -clReleaseDevice(cl_device_id /* device */) CL_API_SUFFIX__VERSION_1_2; - -extern CL_API_ENTRY cl_int CL_API_CALL -clSetDefaultDeviceCommandQueue(cl_context /* context */, - cl_device_id /* device */, - cl_command_queue /* command_queue */) CL_API_SUFFIX__VERSION_2_1; - -extern CL_API_ENTRY cl_int CL_API_CALL -clGetDeviceAndHostTimer(cl_device_id /* device */, - cl_ulong* /* device_timestamp */, - cl_ulong* /* host_timestamp */) CL_API_SUFFIX__VERSION_2_1; - -extern CL_API_ENTRY cl_int CL_API_CALL -clGetHostTimer(cl_device_id /* device */, - cl_ulong * /* host_timestamp */) CL_API_SUFFIX__VERSION_2_1; - - -/* Context APIs */ -extern CL_API_ENTRY cl_context CL_API_CALL -clCreateContext(const cl_context_properties * /* properties */, - cl_uint /* num_devices */, - const cl_device_id * /* devices */, - void (CL_CALLBACK * /* pfn_notify */)(const char *, const void *, size_t, void *), - void * /* user_data */, - cl_int * /* errcode_ret */) CL_API_SUFFIX__VERSION_1_0; - -extern CL_API_ENTRY cl_context CL_API_CALL -clCreateContextFromType(const cl_context_properties * /* properties */, - cl_device_type /* device_type */, - void (CL_CALLBACK * /* pfn_notify*/ )(const char *, const void *, size_t, void *), - void * /* user_data */, - cl_int * /* errcode_ret */) CL_API_SUFFIX__VERSION_1_0; - -extern CL_API_ENTRY cl_int CL_API_CALL -clRetainContext(cl_context /* context */) CL_API_SUFFIX__VERSION_1_0; - -extern CL_API_ENTRY cl_int CL_API_CALL -clReleaseContext(cl_context /* context */) CL_API_SUFFIX__VERSION_1_0; - -extern CL_API_ENTRY cl_int CL_API_CALL -clGetContextInfo(cl_context /* context */, - cl_context_info /* param_name */, - size_t /* param_value_size */, - void * /* param_value */, - size_t * /* param_value_size_ret */) CL_API_SUFFIX__VERSION_1_0; - -/* Command Queue APIs */ -extern CL_API_ENTRY cl_command_queue CL_API_CALL -clCreateCommandQueueWithProperties(cl_context /* context */, - cl_device_id /* device */, - const cl_queue_properties * /* properties */, - cl_int * /* errcode_ret */) CL_API_SUFFIX__VERSION_2_0; - -extern CL_API_ENTRY cl_int CL_API_CALL -clRetainCommandQueue(cl_command_queue /* command_queue */) CL_API_SUFFIX__VERSION_1_0; - -extern CL_API_ENTRY cl_int CL_API_CALL -clReleaseCommandQueue(cl_command_queue /* command_queue */) CL_API_SUFFIX__VERSION_1_0; - -extern CL_API_ENTRY cl_int CL_API_CALL -clGetCommandQueueInfo(cl_command_queue /* command_queue */, - cl_command_queue_info /* param_name */, - size_t /* param_value_size */, - void * /* param_value */, - size_t * /* param_value_size_ret */) CL_API_SUFFIX__VERSION_1_0; - -/* Memory Object APIs */ -extern CL_API_ENTRY cl_mem CL_API_CALL -clCreateBuffer(cl_context /* context */, - cl_mem_flags /* flags */, - size_t /* size */, - void * /* host_ptr */, - cl_int * /* errcode_ret */) CL_API_SUFFIX__VERSION_1_0; - -extern CL_API_ENTRY cl_mem CL_API_CALL -clCreateSubBuffer(cl_mem /* buffer */, - cl_mem_flags /* flags */, - cl_buffer_create_type /* buffer_create_type */, - const void * /* buffer_create_info */, - cl_int * /* errcode_ret */) CL_API_SUFFIX__VERSION_1_1; - -extern CL_API_ENTRY cl_mem CL_API_CALL -clCreateImage(cl_context /* context */, - cl_mem_flags /* flags */, - const cl_image_format * /* image_format */, - const cl_image_desc * /* image_desc */, - void * /* host_ptr */, - cl_int * /* errcode_ret */) CL_API_SUFFIX__VERSION_1_2; - -extern CL_API_ENTRY cl_mem CL_API_CALL -clCreatePipe(cl_context /* context */, - cl_mem_flags /* flags */, - cl_uint /* pipe_packet_size */, - cl_uint /* pipe_max_packets */, - const cl_pipe_properties * /* properties */, - cl_int * /* errcode_ret */) CL_API_SUFFIX__VERSION_2_0; - -extern CL_API_ENTRY cl_int CL_API_CALL -clRetainMemObject(cl_mem /* memobj */) CL_API_SUFFIX__VERSION_1_0; - -extern CL_API_ENTRY cl_int CL_API_CALL -clReleaseMemObject(cl_mem /* memobj */) CL_API_SUFFIX__VERSION_1_0; - -extern CL_API_ENTRY cl_int CL_API_CALL -clGetSupportedImageFormats(cl_context /* context */, - cl_mem_flags /* flags */, - cl_mem_object_type /* image_type */, - cl_uint /* num_entries */, - cl_image_format * /* image_formats */, - cl_uint * /* num_image_formats */) CL_API_SUFFIX__VERSION_1_0; - -extern CL_API_ENTRY cl_int CL_API_CALL -clGetMemObjectInfo(cl_mem /* memobj */, - cl_mem_info /* param_name */, - size_t /* param_value_size */, - void * /* param_value */, - size_t * /* param_value_size_ret */) CL_API_SUFFIX__VERSION_1_0; - -extern CL_API_ENTRY cl_int CL_API_CALL -clGetImageInfo(cl_mem /* image */, - cl_image_info /* param_name */, - size_t /* param_value_size */, - void * /* param_value */, - size_t * /* param_value_size_ret */) CL_API_SUFFIX__VERSION_1_0; - -extern CL_API_ENTRY cl_int CL_API_CALL -clGetPipeInfo(cl_mem /* pipe */, - cl_pipe_info /* param_name */, - size_t /* param_value_size */, - void * /* param_value */, - size_t * /* param_value_size_ret */) CL_API_SUFFIX__VERSION_2_0; - - -extern CL_API_ENTRY cl_int CL_API_CALL -clSetMemObjectDestructorCallback(cl_mem /* memobj */, - void (CL_CALLBACK * /*pfn_notify*/)( cl_mem /* memobj */, void* /*user_data*/), - void * /*user_data */ ) CL_API_SUFFIX__VERSION_1_1; - -/* SVM Allocation APIs */ -extern CL_API_ENTRY void * CL_API_CALL -clSVMAlloc(cl_context /* context */, - cl_svm_mem_flags /* flags */, - size_t /* size */, - cl_uint /* alignment */) CL_API_SUFFIX__VERSION_2_0; - -extern CL_API_ENTRY void CL_API_CALL -clSVMFree(cl_context /* context */, - void * /* svm_pointer */) CL_API_SUFFIX__VERSION_2_0; - -/* Sampler APIs */ -extern CL_API_ENTRY cl_sampler CL_API_CALL -clCreateSamplerWithProperties(cl_context /* context */, - const cl_sampler_properties * /* normalized_coords */, - cl_int * /* errcode_ret */) CL_API_SUFFIX__VERSION_2_0; - -extern CL_API_ENTRY cl_int CL_API_CALL -clRetainSampler(cl_sampler /* sampler */) CL_API_SUFFIX__VERSION_1_0; - -extern CL_API_ENTRY cl_int CL_API_CALL -clReleaseSampler(cl_sampler /* sampler */) CL_API_SUFFIX__VERSION_1_0; - -extern CL_API_ENTRY cl_int CL_API_CALL -clGetSamplerInfo(cl_sampler /* sampler */, - cl_sampler_info /* param_name */, - size_t /* param_value_size */, - void * /* param_value */, - size_t * /* param_value_size_ret */) CL_API_SUFFIX__VERSION_1_0; - -/* Program Object APIs */ -extern CL_API_ENTRY cl_program CL_API_CALL -clCreateProgramWithSource(cl_context /* context */, - cl_uint /* count */, - const char ** /* strings */, - const size_t * /* lengths */, - cl_int * /* errcode_ret */) CL_API_SUFFIX__VERSION_1_0; - -extern CL_API_ENTRY cl_program CL_API_CALL -clCreateProgramWithBinary(cl_context /* context */, - cl_uint /* num_devices */, - const cl_device_id * /* device_list */, - const size_t * /* lengths */, - const unsigned char ** /* binaries */, - cl_int * /* binary_status */, - cl_int * /* errcode_ret */) CL_API_SUFFIX__VERSION_1_0; - -extern CL_API_ENTRY cl_program CL_API_CALL -clCreateProgramWithBuiltInKernels(cl_context /* context */, - cl_uint /* num_devices */, - const cl_device_id * /* device_list */, - const char * /* kernel_names */, - cl_int * /* errcode_ret */) CL_API_SUFFIX__VERSION_1_2; - -extern CL_API_ENTRY cl_program CL_API_CALL -clCreateProgramWithIL(cl_context /* context */, - const void* /* il */, - size_t /* length */, - cl_int* /* errcode_ret */) CL_API_SUFFIX__VERSION_2_1; - - -extern CL_API_ENTRY cl_int CL_API_CALL -clRetainProgram(cl_program /* program */) CL_API_SUFFIX__VERSION_1_0; - -extern CL_API_ENTRY cl_int CL_API_CALL -clReleaseProgram(cl_program /* program */) CL_API_SUFFIX__VERSION_1_0; - -extern CL_API_ENTRY cl_int CL_API_CALL -clBuildProgram(cl_program /* program */, - cl_uint /* num_devices */, - const cl_device_id * /* device_list */, - const char * /* options */, - void (CL_CALLBACK * /* pfn_notify */)(cl_program /* program */, void * /* user_data */), - void * /* user_data */) CL_API_SUFFIX__VERSION_1_0; - -extern CL_API_ENTRY cl_int CL_API_CALL -clCompileProgram(cl_program /* program */, - cl_uint /* num_devices */, - const cl_device_id * /* device_list */, - const char * /* options */, - cl_uint /* num_input_headers */, - const cl_program * /* input_headers */, - const char ** /* header_include_names */, - void (CL_CALLBACK * /* pfn_notify */)(cl_program /* program */, void * /* user_data */), - void * /* user_data */) CL_API_SUFFIX__VERSION_1_2; - -extern CL_API_ENTRY cl_program CL_API_CALL -clLinkProgram(cl_context /* context */, - cl_uint /* num_devices */, - const cl_device_id * /* device_list */, - const char * /* options */, - cl_uint /* num_input_programs */, - const cl_program * /* input_programs */, - void (CL_CALLBACK * /* pfn_notify */)(cl_program /* program */, void * /* user_data */), - void * /* user_data */, - cl_int * /* errcode_ret */ ) CL_API_SUFFIX__VERSION_1_2; - - -extern CL_API_ENTRY cl_int CL_API_CALL -clUnloadPlatformCompiler(cl_platform_id /* platform */) CL_API_SUFFIX__VERSION_1_2; - -extern CL_API_ENTRY cl_int CL_API_CALL -clGetProgramInfo(cl_program /* program */, - cl_program_info /* param_name */, - size_t /* param_value_size */, - void * /* param_value */, - size_t * /* param_value_size_ret */) CL_API_SUFFIX__VERSION_1_0; - -extern CL_API_ENTRY cl_int CL_API_CALL -clGetProgramBuildInfo(cl_program /* program */, - cl_device_id /* device */, - cl_program_build_info /* param_name */, - size_t /* param_value_size */, - void * /* param_value */, - size_t * /* param_value_size_ret */) CL_API_SUFFIX__VERSION_1_0; - -/* Kernel Object APIs */ -extern CL_API_ENTRY cl_kernel CL_API_CALL -clCreateKernel(cl_program /* program */, - const char * /* kernel_name */, - cl_int * /* errcode_ret */) CL_API_SUFFIX__VERSION_1_0; - -extern CL_API_ENTRY cl_int CL_API_CALL -clCreateKernelsInProgram(cl_program /* program */, - cl_uint /* num_kernels */, - cl_kernel * /* kernels */, - cl_uint * /* num_kernels_ret */) CL_API_SUFFIX__VERSION_1_0; - -extern CL_API_ENTRY cl_kernel CL_API_CALL -clCloneKernel(cl_kernel /* source_kernel */, - cl_int* /* errcode_ret */) CL_API_SUFFIX__VERSION_2_1; - -extern CL_API_ENTRY cl_int CL_API_CALL -clRetainKernel(cl_kernel /* kernel */) CL_API_SUFFIX__VERSION_1_0; - -extern CL_API_ENTRY cl_int CL_API_CALL -clReleaseKernel(cl_kernel /* kernel */) CL_API_SUFFIX__VERSION_1_0; - -extern CL_API_ENTRY cl_int CL_API_CALL -clSetKernelArg(cl_kernel /* kernel */, - cl_uint /* arg_index */, - size_t /* arg_size */, - const void * /* arg_value */) CL_API_SUFFIX__VERSION_1_0; - -extern CL_API_ENTRY cl_int CL_API_CALL -clSetKernelArgSVMPointer(cl_kernel /* kernel */, - cl_uint /* arg_index */, - const void * /* arg_value */) CL_API_SUFFIX__VERSION_2_0; - -extern CL_API_ENTRY cl_int CL_API_CALL -clSetKernelExecInfo(cl_kernel /* kernel */, - cl_kernel_exec_info /* param_name */, - size_t /* param_value_size */, - const void * /* param_value */) CL_API_SUFFIX__VERSION_2_0; - -extern CL_API_ENTRY cl_int CL_API_CALL -clGetKernelInfo(cl_kernel /* kernel */, - cl_kernel_info /* param_name */, - size_t /* param_value_size */, - void * /* param_value */, - size_t * /* param_value_size_ret */) CL_API_SUFFIX__VERSION_1_0; - -extern CL_API_ENTRY cl_int CL_API_CALL -clGetKernelArgInfo(cl_kernel /* kernel */, - cl_uint /* arg_indx */, - cl_kernel_arg_info /* param_name */, - size_t /* param_value_size */, - void * /* param_value */, - size_t * /* param_value_size_ret */) CL_API_SUFFIX__VERSION_1_2; - -extern CL_API_ENTRY cl_int CL_API_CALL -clGetKernelWorkGroupInfo(cl_kernel /* kernel */, - cl_device_id /* device */, - cl_kernel_work_group_info /* param_name */, - size_t /* param_value_size */, - void * /* param_value */, - size_t * /* param_value_size_ret */) CL_API_SUFFIX__VERSION_1_0; - -extern CL_API_ENTRY cl_int CL_API_CALL -clGetKernelSubGroupInfo(cl_kernel /* kernel */, - cl_device_id /* device */, - cl_kernel_sub_group_info /* param_name */, - size_t /* input_value_size */, - const void* /*input_value */, - size_t /* param_value_size */, - void* /* param_value */, - size_t* /* param_value_size_ret */ ) CL_API_SUFFIX__VERSION_2_1; - - -/* Event Object APIs */ -extern CL_API_ENTRY cl_int CL_API_CALL -clWaitForEvents(cl_uint /* num_events */, - const cl_event * /* event_list */) CL_API_SUFFIX__VERSION_1_0; - -extern CL_API_ENTRY cl_int CL_API_CALL -clGetEventInfo(cl_event /* event */, - cl_event_info /* param_name */, - size_t /* param_value_size */, - void * /* param_value */, - size_t * /* param_value_size_ret */) CL_API_SUFFIX__VERSION_1_0; - -extern CL_API_ENTRY cl_event CL_API_CALL -clCreateUserEvent(cl_context /* context */, - cl_int * /* errcode_ret */) CL_API_SUFFIX__VERSION_1_1; - -extern CL_API_ENTRY cl_int CL_API_CALL -clRetainEvent(cl_event /* event */) CL_API_SUFFIX__VERSION_1_0; - -extern CL_API_ENTRY cl_int CL_API_CALL -clReleaseEvent(cl_event /* event */) CL_API_SUFFIX__VERSION_1_0; - -extern CL_API_ENTRY cl_int CL_API_CALL -clSetUserEventStatus(cl_event /* event */, - cl_int /* execution_status */) CL_API_SUFFIX__VERSION_1_1; - -extern CL_API_ENTRY cl_int CL_API_CALL -clSetEventCallback( cl_event /* event */, - cl_int /* command_exec_callback_type */, - void (CL_CALLBACK * /* pfn_notify */)(cl_event, cl_int, void *), - void * /* user_data */) CL_API_SUFFIX__VERSION_1_1; - -/* Profiling APIs */ -extern CL_API_ENTRY cl_int CL_API_CALL -clGetEventProfilingInfo(cl_event /* event */, - cl_profiling_info /* param_name */, - size_t /* param_value_size */, - void * /* param_value */, - size_t * /* param_value_size_ret */) CL_API_SUFFIX__VERSION_1_0; - -/* Flush and Finish APIs */ -extern CL_API_ENTRY cl_int CL_API_CALL -clFlush(cl_command_queue /* command_queue */) CL_API_SUFFIX__VERSION_1_0; - -extern CL_API_ENTRY cl_int CL_API_CALL -clFinish(cl_command_queue /* command_queue */) CL_API_SUFFIX__VERSION_1_0; - -/* Enqueued Commands APIs */ -extern CL_API_ENTRY cl_int CL_API_CALL -clEnqueueReadBuffer(cl_command_queue /* command_queue */, - cl_mem /* buffer */, - cl_bool /* blocking_read */, - size_t /* offset */, - size_t /* size */, - void * /* ptr */, - cl_uint /* num_events_in_wait_list */, - const cl_event * /* event_wait_list */, - cl_event * /* event */) CL_API_SUFFIX__VERSION_1_0; - -extern CL_API_ENTRY cl_int CL_API_CALL -clEnqueueReadBufferRect(cl_command_queue /* command_queue */, - cl_mem /* buffer */, - cl_bool /* blocking_read */, - const size_t * /* buffer_offset */, - const size_t * /* host_offset */, - const size_t * /* region */, - size_t /* buffer_row_pitch */, - size_t /* buffer_slice_pitch */, - size_t /* host_row_pitch */, - size_t /* host_slice_pitch */, - void * /* ptr */, - cl_uint /* num_events_in_wait_list */, - const cl_event * /* event_wait_list */, - cl_event * /* event */) CL_API_SUFFIX__VERSION_1_1; - -extern CL_API_ENTRY cl_int CL_API_CALL -clEnqueueWriteBuffer(cl_command_queue /* command_queue */, - cl_mem /* buffer */, - cl_bool /* blocking_write */, - size_t /* offset */, - size_t /* size */, - const void * /* ptr */, - cl_uint /* num_events_in_wait_list */, - const cl_event * /* event_wait_list */, - cl_event * /* event */) CL_API_SUFFIX__VERSION_1_0; - -extern CL_API_ENTRY cl_int CL_API_CALL -clEnqueueWriteBufferRect(cl_command_queue /* command_queue */, - cl_mem /* buffer */, - cl_bool /* blocking_write */, - const size_t * /* buffer_offset */, - const size_t * /* host_offset */, - const size_t * /* region */, - size_t /* buffer_row_pitch */, - size_t /* buffer_slice_pitch */, - size_t /* host_row_pitch */, - size_t /* host_slice_pitch */, - const void * /* ptr */, - cl_uint /* num_events_in_wait_list */, - const cl_event * /* event_wait_list */, - cl_event * /* event */) CL_API_SUFFIX__VERSION_1_1; - -extern CL_API_ENTRY cl_int CL_API_CALL -clEnqueueFillBuffer(cl_command_queue /* command_queue */, - cl_mem /* buffer */, - const void * /* pattern */, - size_t /* pattern_size */, - size_t /* offset */, - size_t /* size */, - cl_uint /* num_events_in_wait_list */, - const cl_event * /* event_wait_list */, - cl_event * /* event */) CL_API_SUFFIX__VERSION_1_2; - -extern CL_API_ENTRY cl_int CL_API_CALL -clEnqueueCopyBuffer(cl_command_queue /* command_queue */, - cl_mem /* src_buffer */, - cl_mem /* dst_buffer */, - size_t /* src_offset */, - size_t /* dst_offset */, - size_t /* size */, - cl_uint /* num_events_in_wait_list */, - const cl_event * /* event_wait_list */, - cl_event * /* event */) CL_API_SUFFIX__VERSION_1_0; - -extern CL_API_ENTRY cl_int CL_API_CALL -clEnqueueCopyBufferRect(cl_command_queue /* command_queue */, - cl_mem /* src_buffer */, - cl_mem /* dst_buffer */, - const size_t * /* src_origin */, - const size_t * /* dst_origin */, - const size_t * /* region */, - size_t /* src_row_pitch */, - size_t /* src_slice_pitch */, - size_t /* dst_row_pitch */, - size_t /* dst_slice_pitch */, - cl_uint /* num_events_in_wait_list */, - const cl_event * /* event_wait_list */, - cl_event * /* event */) CL_API_SUFFIX__VERSION_1_1; - -extern CL_API_ENTRY cl_int CL_API_CALL -clEnqueueReadImage(cl_command_queue /* command_queue */, - cl_mem /* image */, - cl_bool /* blocking_read */, - const size_t * /* origin[3] */, - const size_t * /* region[3] */, - size_t /* row_pitch */, - size_t /* slice_pitch */, - void * /* ptr */, - cl_uint /* num_events_in_wait_list */, - const cl_event * /* event_wait_list */, - cl_event * /* event */) CL_API_SUFFIX__VERSION_1_0; - -extern CL_API_ENTRY cl_int CL_API_CALL -clEnqueueWriteImage(cl_command_queue /* command_queue */, - cl_mem /* image */, - cl_bool /* blocking_write */, - const size_t * /* origin[3] */, - const size_t * /* region[3] */, - size_t /* input_row_pitch */, - size_t /* input_slice_pitch */, - const void * /* ptr */, - cl_uint /* num_events_in_wait_list */, - const cl_event * /* event_wait_list */, - cl_event * /* event */) CL_API_SUFFIX__VERSION_1_0; - -extern CL_API_ENTRY cl_int CL_API_CALL -clEnqueueFillImage(cl_command_queue /* command_queue */, - cl_mem /* image */, - const void * /* fill_color */, - const size_t * /* origin[3] */, - const size_t * /* region[3] */, - cl_uint /* num_events_in_wait_list */, - const cl_event * /* event_wait_list */, - cl_event * /* event */) CL_API_SUFFIX__VERSION_1_2; - -extern CL_API_ENTRY cl_int CL_API_CALL -clEnqueueCopyImage(cl_command_queue /* command_queue */, - cl_mem /* src_image */, - cl_mem /* dst_image */, - const size_t * /* src_origin[3] */, - const size_t * /* dst_origin[3] */, - const size_t * /* region[3] */, - cl_uint /* num_events_in_wait_list */, - const cl_event * /* event_wait_list */, - cl_event * /* event */) CL_API_SUFFIX__VERSION_1_0; - -extern CL_API_ENTRY cl_int CL_API_CALL -clEnqueueCopyImageToBuffer(cl_command_queue /* command_queue */, - cl_mem /* src_image */, - cl_mem /* dst_buffer */, - const size_t * /* src_origin[3] */, - const size_t * /* region[3] */, - size_t /* dst_offset */, - cl_uint /* num_events_in_wait_list */, - const cl_event * /* event_wait_list */, - cl_event * /* event */) CL_API_SUFFIX__VERSION_1_0; - -extern CL_API_ENTRY cl_int CL_API_CALL -clEnqueueCopyBufferToImage(cl_command_queue /* command_queue */, - cl_mem /* src_buffer */, - cl_mem /* dst_image */, - size_t /* src_offset */, - const size_t * /* dst_origin[3] */, - const size_t * /* region[3] */, - cl_uint /* num_events_in_wait_list */, - const cl_event * /* event_wait_list */, - cl_event * /* event */) CL_API_SUFFIX__VERSION_1_0; - -extern CL_API_ENTRY void * CL_API_CALL -clEnqueueMapBuffer(cl_command_queue /* command_queue */, - cl_mem /* buffer */, - cl_bool /* blocking_map */, - cl_map_flags /* map_flags */, - size_t /* offset */, - size_t /* size */, - cl_uint /* num_events_in_wait_list */, - const cl_event * /* event_wait_list */, - cl_event * /* event */, - cl_int * /* errcode_ret */) CL_API_SUFFIX__VERSION_1_0; - -extern CL_API_ENTRY void * CL_API_CALL -clEnqueueMapImage(cl_command_queue /* command_queue */, - cl_mem /* image */, - cl_bool /* blocking_map */, - cl_map_flags /* map_flags */, - const size_t * /* origin[3] */, - const size_t * /* region[3] */, - size_t * /* image_row_pitch */, - size_t * /* image_slice_pitch */, - cl_uint /* num_events_in_wait_list */, - const cl_event * /* event_wait_list */, - cl_event * /* event */, - cl_int * /* errcode_ret */) CL_API_SUFFIX__VERSION_1_0; - -extern CL_API_ENTRY cl_int CL_API_CALL -clEnqueueUnmapMemObject(cl_command_queue /* command_queue */, - cl_mem /* memobj */, - void * /* mapped_ptr */, - cl_uint /* num_events_in_wait_list */, - const cl_event * /* event_wait_list */, - cl_event * /* event */) CL_API_SUFFIX__VERSION_1_0; - -extern CL_API_ENTRY cl_int CL_API_CALL -clEnqueueMigrateMemObjects(cl_command_queue /* command_queue */, - cl_uint /* num_mem_objects */, - const cl_mem * /* mem_objects */, - cl_mem_migration_flags /* flags */, - cl_uint /* num_events_in_wait_list */, - const cl_event * /* event_wait_list */, - cl_event * /* event */) CL_API_SUFFIX__VERSION_1_2; - -extern CL_API_ENTRY cl_int CL_API_CALL -clEnqueueNDRangeKernel(cl_command_queue /* command_queue */, - cl_kernel /* kernel */, - cl_uint /* work_dim */, - const size_t * /* global_work_offset */, - const size_t * /* global_work_size */, - const size_t * /* local_work_size */, - cl_uint /* num_events_in_wait_list */, - const cl_event * /* event_wait_list */, - cl_event * /* event */) CL_API_SUFFIX__VERSION_1_0; - -extern CL_API_ENTRY cl_int CL_API_CALL -clEnqueueNativeKernel(cl_command_queue /* command_queue */, - void (CL_CALLBACK * /*user_func*/)(void *), - void * /* args */, - size_t /* cb_args */, - cl_uint /* num_mem_objects */, - const cl_mem * /* mem_list */, - const void ** /* args_mem_loc */, - cl_uint /* num_events_in_wait_list */, - const cl_event * /* event_wait_list */, - cl_event * /* event */) CL_API_SUFFIX__VERSION_1_0; - -extern CL_API_ENTRY cl_int CL_API_CALL -clEnqueueMarkerWithWaitList(cl_command_queue /* command_queue */, - cl_uint /* num_events_in_wait_list */, - const cl_event * /* event_wait_list */, - cl_event * /* event */) CL_API_SUFFIX__VERSION_1_2; - -extern CL_API_ENTRY cl_int CL_API_CALL -clEnqueueBarrierWithWaitList(cl_command_queue /* command_queue */, - cl_uint /* num_events_in_wait_list */, - const cl_event * /* event_wait_list */, - cl_event * /* event */) CL_API_SUFFIX__VERSION_1_2; - -extern CL_API_ENTRY cl_int CL_API_CALL -clEnqueueSVMFree(cl_command_queue /* command_queue */, - cl_uint /* num_svm_pointers */, - void *[] /* svm_pointers[] */, - void (CL_CALLBACK * /*pfn_free_func*/)(cl_command_queue /* queue */, - cl_uint /* num_svm_pointers */, - void *[] /* svm_pointers[] */, - void * /* user_data */), - void * /* user_data */, - cl_uint /* num_events_in_wait_list */, - const cl_event * /* event_wait_list */, - cl_event * /* event */) CL_API_SUFFIX__VERSION_2_0; - -extern CL_API_ENTRY cl_int CL_API_CALL -clEnqueueSVMMemcpy(cl_command_queue /* command_queue */, - cl_bool /* blocking_copy */, - void * /* dst_ptr */, - const void * /* src_ptr */, - size_t /* size */, - cl_uint /* num_events_in_wait_list */, - const cl_event * /* event_wait_list */, - cl_event * /* event */) CL_API_SUFFIX__VERSION_2_0; - -extern CL_API_ENTRY cl_int CL_API_CALL -clEnqueueSVMMemFill(cl_command_queue /* command_queue */, - void * /* svm_ptr */, - const void * /* pattern */, - size_t /* pattern_size */, - size_t /* size */, - cl_uint /* num_events_in_wait_list */, - const cl_event * /* event_wait_list */, - cl_event * /* event */) CL_API_SUFFIX__VERSION_2_0; - -extern CL_API_ENTRY cl_int CL_API_CALL -clEnqueueSVMMap(cl_command_queue /* command_queue */, - cl_bool /* blocking_map */, - cl_map_flags /* flags */, - void * /* svm_ptr */, - size_t /* size */, - cl_uint /* num_events_in_wait_list */, - const cl_event * /* event_wait_list */, - cl_event * /* event */) CL_API_SUFFIX__VERSION_2_0; - -extern CL_API_ENTRY cl_int CL_API_CALL -clEnqueueSVMUnmap(cl_command_queue /* command_queue */, - void * /* svm_ptr */, - cl_uint /* num_events_in_wait_list */, - const cl_event * /* event_wait_list */, - cl_event * /* event */) CL_API_SUFFIX__VERSION_2_0; - -extern CL_API_ENTRY cl_int CL_API_CALL -clEnqueueSVMMigrateMem(cl_command_queue /* command_queue */, - cl_uint /* num_svm_pointers */, - const void ** /* svm_pointers */, - const size_t * /* sizes */, - cl_mem_migration_flags /* flags */, - cl_uint /* num_events_in_wait_list */, - const cl_event * /* event_wait_list */, - cl_event * /* event */) CL_API_SUFFIX__VERSION_2_1; - - -/* Extension function access - * - * Returns the extension function address for the given function name, - * or NULL if a valid function can not be found. The client must - * check to make sure the address is not NULL, before using or - * calling the returned function address. - */ -extern CL_API_ENTRY void * CL_API_CALL -clGetExtensionFunctionAddressForPlatform(cl_platform_id /* platform */, - const char * /* func_name */) CL_API_SUFFIX__VERSION_1_2; - - -/* Deprecated OpenCL 1.1 APIs */ -extern CL_API_ENTRY CL_EXT_PREFIX__VERSION_1_1_DEPRECATED cl_mem CL_API_CALL -clCreateImage2D(cl_context /* context */, - cl_mem_flags /* flags */, - const cl_image_format * /* image_format */, - size_t /* image_width */, - size_t /* image_height */, - size_t /* image_row_pitch */, - void * /* host_ptr */, - cl_int * /* errcode_ret */) CL_EXT_SUFFIX__VERSION_1_1_DEPRECATED; - -extern CL_API_ENTRY CL_EXT_PREFIX__VERSION_1_1_DEPRECATED cl_mem CL_API_CALL -clCreateImage3D(cl_context /* context */, - cl_mem_flags /* flags */, - const cl_image_format * /* image_format */, - size_t /* image_width */, - size_t /* image_height */, - size_t /* image_depth */, - size_t /* image_row_pitch */, - size_t /* image_slice_pitch */, - void * /* host_ptr */, - cl_int * /* errcode_ret */) CL_EXT_SUFFIX__VERSION_1_1_DEPRECATED; - -extern CL_API_ENTRY CL_EXT_PREFIX__VERSION_1_1_DEPRECATED cl_int CL_API_CALL -clEnqueueMarker(cl_command_queue /* command_queue */, - cl_event * /* event */) CL_EXT_SUFFIX__VERSION_1_1_DEPRECATED; - -extern CL_API_ENTRY CL_EXT_PREFIX__VERSION_1_1_DEPRECATED cl_int CL_API_CALL -clEnqueueWaitForEvents(cl_command_queue /* command_queue */, - cl_uint /* num_events */, - const cl_event * /* event_list */) CL_EXT_SUFFIX__VERSION_1_1_DEPRECATED; - -extern CL_API_ENTRY CL_EXT_PREFIX__VERSION_1_1_DEPRECATED cl_int CL_API_CALL -clEnqueueBarrier(cl_command_queue /* command_queue */) CL_EXT_SUFFIX__VERSION_1_1_DEPRECATED; - -extern CL_API_ENTRY CL_EXT_PREFIX__VERSION_1_1_DEPRECATED cl_int CL_API_CALL -clUnloadCompiler(void) CL_EXT_SUFFIX__VERSION_1_1_DEPRECATED; - -extern CL_API_ENTRY CL_EXT_PREFIX__VERSION_1_1_DEPRECATED void * CL_API_CALL -clGetExtensionFunctionAddress(const char * /* func_name */) CL_EXT_SUFFIX__VERSION_1_1_DEPRECATED; - -/* Deprecated OpenCL 2.0 APIs */ -extern CL_API_ENTRY CL_EXT_PREFIX__VERSION_1_2_DEPRECATED cl_command_queue CL_API_CALL -clCreateCommandQueue(cl_context /* context */, - cl_device_id /* device */, - cl_command_queue_properties /* properties */, - cl_int * /* errcode_ret */) CL_EXT_SUFFIX__VERSION_1_2_DEPRECATED; - - -extern CL_API_ENTRY CL_EXT_PREFIX__VERSION_1_2_DEPRECATED cl_sampler CL_API_CALL -clCreateSampler(cl_context /* context */, - cl_bool /* normalized_coords */, - cl_addressing_mode /* addressing_mode */, - cl_filter_mode /* filter_mode */, - cl_int * /* errcode_ret */) CL_EXT_SUFFIX__VERSION_1_2_DEPRECATED; - -extern CL_API_ENTRY CL_EXT_PREFIX__VERSION_1_2_DEPRECATED cl_int CL_API_CALL -clEnqueueTask(cl_command_queue /* command_queue */, - cl_kernel /* kernel */, - cl_uint /* num_events_in_wait_list */, - const cl_event * /* event_wait_list */, - cl_event * /* event */) CL_EXT_SUFFIX__VERSION_1_2_DEPRECATED; - -#ifdef __cplusplus -} -#endif - -#endif /* __OPENCL_CL_H */ - diff --git a/third_party/opencl/include/CL/cl_d3d10.h b/third_party/opencl/include/CL/cl_d3d10.h deleted file mode 100644 index d5960a43f7..0000000000 --- a/third_party/opencl/include/CL/cl_d3d10.h +++ /dev/null @@ -1,131 +0,0 @@ -/********************************************************************************** - * Copyright (c) 2008-2015 The Khronos Group Inc. - * - * Permission is hereby granted, free of charge, to any person obtaining a - * copy of this software and/or associated documentation files (the - * "Materials"), to deal in the Materials without restriction, including - * without limitation the rights to use, copy, modify, merge, publish, - * distribute, sublicense, and/or sell copies of the Materials, and to - * permit persons to whom the Materials are furnished to do so, subject to - * the following conditions: - * - * The above copyright notice and this permission notice shall be included - * in all copies or substantial portions of the Materials. - * - * MODIFICATIONS TO THIS FILE MAY MEAN IT NO LONGER ACCURATELY REFLECTS - * KHRONOS STANDARDS. THE UNMODIFIED, NORMATIVE VERSIONS OF KHRONOS - * SPECIFICATIONS AND HEADER INFORMATION ARE LOCATED AT - * https://www.khronos.org/registry/ - * - * THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. - * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY - * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, - * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE - * MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS. - **********************************************************************************/ - -/* $Revision: 11708 $ on $Date: 2010-06-13 23:36:24 -0700 (Sun, 13 Jun 2010) $ */ - -#ifndef __OPENCL_CL_D3D10_H -#define __OPENCL_CL_D3D10_H - -#include -#include -#include - -#ifdef __cplusplus -extern "C" { -#endif - -/****************************************************************************** - * cl_khr_d3d10_sharing */ -#define cl_khr_d3d10_sharing 1 - -typedef cl_uint cl_d3d10_device_source_khr; -typedef cl_uint cl_d3d10_device_set_khr; - -/******************************************************************************/ - -/* Error Codes */ -#define CL_INVALID_D3D10_DEVICE_KHR -1002 -#define CL_INVALID_D3D10_RESOURCE_KHR -1003 -#define CL_D3D10_RESOURCE_ALREADY_ACQUIRED_KHR -1004 -#define CL_D3D10_RESOURCE_NOT_ACQUIRED_KHR -1005 - -/* cl_d3d10_device_source_nv */ -#define CL_D3D10_DEVICE_KHR 0x4010 -#define CL_D3D10_DXGI_ADAPTER_KHR 0x4011 - -/* cl_d3d10_device_set_nv */ -#define CL_PREFERRED_DEVICES_FOR_D3D10_KHR 0x4012 -#define CL_ALL_DEVICES_FOR_D3D10_KHR 0x4013 - -/* cl_context_info */ -#define CL_CONTEXT_D3D10_DEVICE_KHR 0x4014 -#define CL_CONTEXT_D3D10_PREFER_SHARED_RESOURCES_KHR 0x402C - -/* cl_mem_info */ -#define CL_MEM_D3D10_RESOURCE_KHR 0x4015 - -/* cl_image_info */ -#define CL_IMAGE_D3D10_SUBRESOURCE_KHR 0x4016 - -/* cl_command_type */ -#define CL_COMMAND_ACQUIRE_D3D10_OBJECTS_KHR 0x4017 -#define CL_COMMAND_RELEASE_D3D10_OBJECTS_KHR 0x4018 - -/******************************************************************************/ - -typedef CL_API_ENTRY cl_int (CL_API_CALL *clGetDeviceIDsFromD3D10KHR_fn)( - cl_platform_id platform, - cl_d3d10_device_source_khr d3d_device_source, - void * d3d_object, - cl_d3d10_device_set_khr d3d_device_set, - cl_uint num_entries, - cl_device_id * devices, - cl_uint * num_devices) CL_API_SUFFIX__VERSION_1_0; - -typedef CL_API_ENTRY cl_mem (CL_API_CALL *clCreateFromD3D10BufferKHR_fn)( - cl_context context, - cl_mem_flags flags, - ID3D10Buffer * resource, - cl_int * errcode_ret) CL_API_SUFFIX__VERSION_1_0; - -typedef CL_API_ENTRY cl_mem (CL_API_CALL *clCreateFromD3D10Texture2DKHR_fn)( - cl_context context, - cl_mem_flags flags, - ID3D10Texture2D * resource, - UINT subresource, - cl_int * errcode_ret) CL_API_SUFFIX__VERSION_1_0; - -typedef CL_API_ENTRY cl_mem (CL_API_CALL *clCreateFromD3D10Texture3DKHR_fn)( - cl_context context, - cl_mem_flags flags, - ID3D10Texture3D * resource, - UINT subresource, - cl_int * errcode_ret) CL_API_SUFFIX__VERSION_1_0; - -typedef CL_API_ENTRY cl_int (CL_API_CALL *clEnqueueAcquireD3D10ObjectsKHR_fn)( - cl_command_queue command_queue, - cl_uint num_objects, - const cl_mem * mem_objects, - cl_uint num_events_in_wait_list, - const cl_event * event_wait_list, - cl_event * event) CL_API_SUFFIX__VERSION_1_0; - -typedef CL_API_ENTRY cl_int (CL_API_CALL *clEnqueueReleaseD3D10ObjectsKHR_fn)( - cl_command_queue command_queue, - cl_uint num_objects, - const cl_mem * mem_objects, - cl_uint num_events_in_wait_list, - const cl_event * event_wait_list, - cl_event * event) CL_API_SUFFIX__VERSION_1_0; - -#ifdef __cplusplus -} -#endif - -#endif /* __OPENCL_CL_D3D10_H */ - diff --git a/third_party/opencl/include/CL/cl_d3d11.h b/third_party/opencl/include/CL/cl_d3d11.h deleted file mode 100644 index 39f9072398..0000000000 --- a/third_party/opencl/include/CL/cl_d3d11.h +++ /dev/null @@ -1,131 +0,0 @@ -/********************************************************************************** - * Copyright (c) 2008-2015 The Khronos Group Inc. - * - * Permission is hereby granted, free of charge, to any person obtaining a - * copy of this software and/or associated documentation files (the - * "Materials"), to deal in the Materials without restriction, including - * without limitation the rights to use, copy, modify, merge, publish, - * distribute, sublicense, and/or sell copies of the Materials, and to - * permit persons to whom the Materials are furnished to do so, subject to - * the following conditions: - * - * The above copyright notice and this permission notice shall be included - * in all copies or substantial portions of the Materials. - * - * MODIFICATIONS TO THIS FILE MAY MEAN IT NO LONGER ACCURATELY REFLECTS - * KHRONOS STANDARDS. THE UNMODIFIED, NORMATIVE VERSIONS OF KHRONOS - * SPECIFICATIONS AND HEADER INFORMATION ARE LOCATED AT - * https://www.khronos.org/registry/ - * - * THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. - * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY - * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, - * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE - * MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS. - **********************************************************************************/ - -/* $Revision: 11708 $ on $Date: 2010-06-13 23:36:24 -0700 (Sun, 13 Jun 2010) $ */ - -#ifndef __OPENCL_CL_D3D11_H -#define __OPENCL_CL_D3D11_H - -#include -#include -#include - -#ifdef __cplusplus -extern "C" { -#endif - -/****************************************************************************** - * cl_khr_d3d11_sharing */ -#define cl_khr_d3d11_sharing 1 - -typedef cl_uint cl_d3d11_device_source_khr; -typedef cl_uint cl_d3d11_device_set_khr; - -/******************************************************************************/ - -/* Error Codes */ -#define CL_INVALID_D3D11_DEVICE_KHR -1006 -#define CL_INVALID_D3D11_RESOURCE_KHR -1007 -#define CL_D3D11_RESOURCE_ALREADY_ACQUIRED_KHR -1008 -#define CL_D3D11_RESOURCE_NOT_ACQUIRED_KHR -1009 - -/* cl_d3d11_device_source */ -#define CL_D3D11_DEVICE_KHR 0x4019 -#define CL_D3D11_DXGI_ADAPTER_KHR 0x401A - -/* cl_d3d11_device_set */ -#define CL_PREFERRED_DEVICES_FOR_D3D11_KHR 0x401B -#define CL_ALL_DEVICES_FOR_D3D11_KHR 0x401C - -/* cl_context_info */ -#define CL_CONTEXT_D3D11_DEVICE_KHR 0x401D -#define CL_CONTEXT_D3D11_PREFER_SHARED_RESOURCES_KHR 0x402D - -/* cl_mem_info */ -#define CL_MEM_D3D11_RESOURCE_KHR 0x401E - -/* cl_image_info */ -#define CL_IMAGE_D3D11_SUBRESOURCE_KHR 0x401F - -/* cl_command_type */ -#define CL_COMMAND_ACQUIRE_D3D11_OBJECTS_KHR 0x4020 -#define CL_COMMAND_RELEASE_D3D11_OBJECTS_KHR 0x4021 - -/******************************************************************************/ - -typedef CL_API_ENTRY cl_int (CL_API_CALL *clGetDeviceIDsFromD3D11KHR_fn)( - cl_platform_id platform, - cl_d3d11_device_source_khr d3d_device_source, - void * d3d_object, - cl_d3d11_device_set_khr d3d_device_set, - cl_uint num_entries, - cl_device_id * devices, - cl_uint * num_devices) CL_API_SUFFIX__VERSION_1_2; - -typedef CL_API_ENTRY cl_mem (CL_API_CALL *clCreateFromD3D11BufferKHR_fn)( - cl_context context, - cl_mem_flags flags, - ID3D11Buffer * resource, - cl_int * errcode_ret) CL_API_SUFFIX__VERSION_1_2; - -typedef CL_API_ENTRY cl_mem (CL_API_CALL *clCreateFromD3D11Texture2DKHR_fn)( - cl_context context, - cl_mem_flags flags, - ID3D11Texture2D * resource, - UINT subresource, - cl_int * errcode_ret) CL_API_SUFFIX__VERSION_1_2; - -typedef CL_API_ENTRY cl_mem (CL_API_CALL *clCreateFromD3D11Texture3DKHR_fn)( - cl_context context, - cl_mem_flags flags, - ID3D11Texture3D * resource, - UINT subresource, - cl_int * errcode_ret) CL_API_SUFFIX__VERSION_1_2; - -typedef CL_API_ENTRY cl_int (CL_API_CALL *clEnqueueAcquireD3D11ObjectsKHR_fn)( - cl_command_queue command_queue, - cl_uint num_objects, - const cl_mem * mem_objects, - cl_uint num_events_in_wait_list, - const cl_event * event_wait_list, - cl_event * event) CL_API_SUFFIX__VERSION_1_2; - -typedef CL_API_ENTRY cl_int (CL_API_CALL *clEnqueueReleaseD3D11ObjectsKHR_fn)( - cl_command_queue command_queue, - cl_uint num_objects, - const cl_mem * mem_objects, - cl_uint num_events_in_wait_list, - const cl_event * event_wait_list, - cl_event * event) CL_API_SUFFIX__VERSION_1_2; - -#ifdef __cplusplus -} -#endif - -#endif /* __OPENCL_CL_D3D11_H */ - diff --git a/third_party/opencl/include/CL/cl_dx9_media_sharing.h b/third_party/opencl/include/CL/cl_dx9_media_sharing.h deleted file mode 100644 index 2729e8b9e8..0000000000 --- a/third_party/opencl/include/CL/cl_dx9_media_sharing.h +++ /dev/null @@ -1,132 +0,0 @@ -/********************************************************************************** - * Copyright (c) 2008-2015 The Khronos Group Inc. - * - * Permission is hereby granted, free of charge, to any person obtaining a - * copy of this software and/or associated documentation files (the - * "Materials"), to deal in the Materials without restriction, including - * without limitation the rights to use, copy, modify, merge, publish, - * distribute, sublicense, and/or sell copies of the Materials, and to - * permit persons to whom the Materials are furnished to do so, subject to - * the following conditions: - * - * The above copyright notice and this permission notice shall be included - * in all copies or substantial portions of the Materials. - * - * MODIFICATIONS TO THIS FILE MAY MEAN IT NO LONGER ACCURATELY REFLECTS - * KHRONOS STANDARDS. THE UNMODIFIED, NORMATIVE VERSIONS OF KHRONOS - * SPECIFICATIONS AND HEADER INFORMATION ARE LOCATED AT - * https://www.khronos.org/registry/ - * - * THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. - * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY - * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, - * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE - * MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS. - **********************************************************************************/ - -/* $Revision: 11708 $ on $Date: 2010-06-13 23:36:24 -0700 (Sun, 13 Jun 2010) $ */ - -#ifndef __OPENCL_CL_DX9_MEDIA_SHARING_H -#define __OPENCL_CL_DX9_MEDIA_SHARING_H - -#include -#include - -#ifdef __cplusplus -extern "C" { -#endif - -/******************************************************************************/ -/* cl_khr_dx9_media_sharing */ -#define cl_khr_dx9_media_sharing 1 - -typedef cl_uint cl_dx9_media_adapter_type_khr; -typedef cl_uint cl_dx9_media_adapter_set_khr; - -#if defined(_WIN32) -#include -typedef struct _cl_dx9_surface_info_khr -{ - IDirect3DSurface9 *resource; - HANDLE shared_handle; -} cl_dx9_surface_info_khr; -#endif - - -/******************************************************************************/ - -/* Error Codes */ -#define CL_INVALID_DX9_MEDIA_ADAPTER_KHR -1010 -#define CL_INVALID_DX9_MEDIA_SURFACE_KHR -1011 -#define CL_DX9_MEDIA_SURFACE_ALREADY_ACQUIRED_KHR -1012 -#define CL_DX9_MEDIA_SURFACE_NOT_ACQUIRED_KHR -1013 - -/* cl_media_adapter_type_khr */ -#define CL_ADAPTER_D3D9_KHR 0x2020 -#define CL_ADAPTER_D3D9EX_KHR 0x2021 -#define CL_ADAPTER_DXVA_KHR 0x2022 - -/* cl_media_adapter_set_khr */ -#define CL_PREFERRED_DEVICES_FOR_DX9_MEDIA_ADAPTER_KHR 0x2023 -#define CL_ALL_DEVICES_FOR_DX9_MEDIA_ADAPTER_KHR 0x2024 - -/* cl_context_info */ -#define CL_CONTEXT_ADAPTER_D3D9_KHR 0x2025 -#define CL_CONTEXT_ADAPTER_D3D9EX_KHR 0x2026 -#define CL_CONTEXT_ADAPTER_DXVA_KHR 0x2027 - -/* cl_mem_info */ -#define CL_MEM_DX9_MEDIA_ADAPTER_TYPE_KHR 0x2028 -#define CL_MEM_DX9_MEDIA_SURFACE_INFO_KHR 0x2029 - -/* cl_image_info */ -#define CL_IMAGE_DX9_MEDIA_PLANE_KHR 0x202A - -/* cl_command_type */ -#define CL_COMMAND_ACQUIRE_DX9_MEDIA_SURFACES_KHR 0x202B -#define CL_COMMAND_RELEASE_DX9_MEDIA_SURFACES_KHR 0x202C - -/******************************************************************************/ - -typedef CL_API_ENTRY cl_int (CL_API_CALL *clGetDeviceIDsFromDX9MediaAdapterKHR_fn)( - cl_platform_id platform, - cl_uint num_media_adapters, - cl_dx9_media_adapter_type_khr * media_adapter_type, - void * media_adapters, - cl_dx9_media_adapter_set_khr media_adapter_set, - cl_uint num_entries, - cl_device_id * devices, - cl_uint * num_devices) CL_API_SUFFIX__VERSION_1_2; - -typedef CL_API_ENTRY cl_mem (CL_API_CALL *clCreateFromDX9MediaSurfaceKHR_fn)( - cl_context context, - cl_mem_flags flags, - cl_dx9_media_adapter_type_khr adapter_type, - void * surface_info, - cl_uint plane, - cl_int * errcode_ret) CL_API_SUFFIX__VERSION_1_2; - -typedef CL_API_ENTRY cl_int (CL_API_CALL *clEnqueueAcquireDX9MediaSurfacesKHR_fn)( - cl_command_queue command_queue, - cl_uint num_objects, - const cl_mem * mem_objects, - cl_uint num_events_in_wait_list, - const cl_event * event_wait_list, - cl_event * event) CL_API_SUFFIX__VERSION_1_2; - -typedef CL_API_ENTRY cl_int (CL_API_CALL *clEnqueueReleaseDX9MediaSurfacesKHR_fn)( - cl_command_queue command_queue, - cl_uint num_objects, - const cl_mem * mem_objects, - cl_uint num_events_in_wait_list, - const cl_event * event_wait_list, - cl_event * event) CL_API_SUFFIX__VERSION_1_2; - -#ifdef __cplusplus -} -#endif - -#endif /* __OPENCL_CL_DX9_MEDIA_SHARING_H */ - diff --git a/third_party/opencl/include/CL/cl_egl.h b/third_party/opencl/include/CL/cl_egl.h deleted file mode 100644 index a765bd5266..0000000000 --- a/third_party/opencl/include/CL/cl_egl.h +++ /dev/null @@ -1,136 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2008-2015 The Khronos Group Inc. - * - * Permission is hereby granted, free of charge, to any person obtaining a - * copy of this software and/or associated documentation files (the - * "Materials"), to deal in the Materials without restriction, including - * without limitation the rights to use, copy, modify, merge, publish, - * distribute, sublicense, and/or sell copies of the Materials, and to - * permit persons to whom the Materials are furnished to do so, subject to - * the following conditions: - * - * The above copyright notice and this permission notice shall be included - * in all copies or substantial portions of the Materials. - * - * MODIFICATIONS TO THIS FILE MAY MEAN IT NO LONGER ACCURATELY REFLECTS - * KHRONOS STANDARDS. THE UNMODIFIED, NORMATIVE VERSIONS OF KHRONOS - * SPECIFICATIONS AND HEADER INFORMATION ARE LOCATED AT - * https://www.khronos.org/registry/ - * - * THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. - * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY - * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, - * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE - * MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS. - ******************************************************************************/ - -#ifndef __OPENCL_CL_EGL_H -#define __OPENCL_CL_EGL_H - -#ifdef __APPLE__ - -#else -#include -#endif - -#ifdef __cplusplus -extern "C" { -#endif - - -/* Command type for events created with clEnqueueAcquireEGLObjectsKHR */ -#define CL_COMMAND_EGL_FENCE_SYNC_OBJECT_KHR 0x202F -#define CL_COMMAND_ACQUIRE_EGL_OBJECTS_KHR 0x202D -#define CL_COMMAND_RELEASE_EGL_OBJECTS_KHR 0x202E - -/* Error type for clCreateFromEGLImageKHR */ -#define CL_INVALID_EGL_OBJECT_KHR -1093 -#define CL_EGL_RESOURCE_NOT_ACQUIRED_KHR -1092 - -/* CLeglImageKHR is an opaque handle to an EGLImage */ -typedef void* CLeglImageKHR; - -/* CLeglDisplayKHR is an opaque handle to an EGLDisplay */ -typedef void* CLeglDisplayKHR; - -/* CLeglSyncKHR is an opaque handle to an EGLSync object */ -typedef void* CLeglSyncKHR; - -/* properties passed to clCreateFromEGLImageKHR */ -typedef intptr_t cl_egl_image_properties_khr; - - -#define cl_khr_egl_image 1 - -extern CL_API_ENTRY cl_mem CL_API_CALL -clCreateFromEGLImageKHR(cl_context /* context */, - CLeglDisplayKHR /* egldisplay */, - CLeglImageKHR /* eglimage */, - cl_mem_flags /* flags */, - const cl_egl_image_properties_khr * /* properties */, - cl_int * /* errcode_ret */) CL_API_SUFFIX__VERSION_1_0; - -typedef CL_API_ENTRY cl_mem (CL_API_CALL *clCreateFromEGLImageKHR_fn)( - cl_context context, - CLeglDisplayKHR egldisplay, - CLeglImageKHR eglimage, - cl_mem_flags flags, - const cl_egl_image_properties_khr * properties, - cl_int * errcode_ret); - - -extern CL_API_ENTRY cl_int CL_API_CALL -clEnqueueAcquireEGLObjectsKHR(cl_command_queue /* command_queue */, - cl_uint /* num_objects */, - const cl_mem * /* mem_objects */, - cl_uint /* num_events_in_wait_list */, - const cl_event * /* event_wait_list */, - cl_event * /* event */) CL_API_SUFFIX__VERSION_1_0; - -typedef CL_API_ENTRY cl_int (CL_API_CALL *clEnqueueAcquireEGLObjectsKHR_fn)( - cl_command_queue command_queue, - cl_uint num_objects, - const cl_mem * mem_objects, - cl_uint num_events_in_wait_list, - const cl_event * event_wait_list, - cl_event * event); - - -extern CL_API_ENTRY cl_int CL_API_CALL -clEnqueueReleaseEGLObjectsKHR(cl_command_queue /* command_queue */, - cl_uint /* num_objects */, - const cl_mem * /* mem_objects */, - cl_uint /* num_events_in_wait_list */, - const cl_event * /* event_wait_list */, - cl_event * /* event */) CL_API_SUFFIX__VERSION_1_0; - -typedef CL_API_ENTRY cl_int (CL_API_CALL *clEnqueueReleaseEGLObjectsKHR_fn)( - cl_command_queue command_queue, - cl_uint num_objects, - const cl_mem * mem_objects, - cl_uint num_events_in_wait_list, - const cl_event * event_wait_list, - cl_event * event); - - -#define cl_khr_egl_event 1 - -extern CL_API_ENTRY cl_event CL_API_CALL -clCreateEventFromEGLSyncKHR(cl_context /* context */, - CLeglSyncKHR /* sync */, - CLeglDisplayKHR /* display */, - cl_int * /* errcode_ret */) CL_API_SUFFIX__VERSION_1_0; - -typedef CL_API_ENTRY cl_event (CL_API_CALL *clCreateEventFromEGLSyncKHR_fn)( - cl_context context, - CLeglSyncKHR sync, - CLeglDisplayKHR display, - cl_int * errcode_ret); - -#ifdef __cplusplus -} -#endif - -#endif /* __OPENCL_CL_EGL_H */ diff --git a/third_party/opencl/include/CL/cl_ext.h b/third_party/opencl/include/CL/cl_ext.h deleted file mode 100644 index 7941583895..0000000000 --- a/third_party/opencl/include/CL/cl_ext.h +++ /dev/null @@ -1,391 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2008-2015 The Khronos Group Inc. - * - * Permission is hereby granted, free of charge, to any person obtaining a - * copy of this software and/or associated documentation files (the - * "Materials"), to deal in the Materials without restriction, including - * without limitation the rights to use, copy, modify, merge, publish, - * distribute, sublicense, and/or sell copies of the Materials, and to - * permit persons to whom the Materials are furnished to do so, subject to - * the following conditions: - * - * The above copyright notice and this permission notice shall be included - * in all copies or substantial portions of the Materials. - * - * MODIFICATIONS TO THIS FILE MAY MEAN IT NO LONGER ACCURATELY REFLECTS - * KHRONOS STANDARDS. THE UNMODIFIED, NORMATIVE VERSIONS OF KHRONOS - * SPECIFICATIONS AND HEADER INFORMATION ARE LOCATED AT - * https://www.khronos.org/registry/ - * - * THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. - * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY - * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, - * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE - * MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS. - ******************************************************************************/ - -/* $Revision: 11928 $ on $Date: 2010-07-13 09:04:56 -0700 (Tue, 13 Jul 2010) $ */ - -/* cl_ext.h contains OpenCL extensions which don't have external */ -/* (OpenGL, D3D) dependencies. */ - -#ifndef __CL_EXT_H -#define __CL_EXT_H - -#ifdef __cplusplus -extern "C" { -#endif - -#ifdef __APPLE__ - #include - #include -#else - #include -#endif - -/* cl_khr_fp16 extension - no extension #define since it has no functions */ -#define CL_DEVICE_HALF_FP_CONFIG 0x1033 - -/* Memory object destruction - * - * Apple extension for use to manage externally allocated buffers used with cl_mem objects with CL_MEM_USE_HOST_PTR - * - * Registers a user callback function that will be called when the memory object is deleted and its resources - * freed. Each call to clSetMemObjectCallbackFn registers the specified user callback function on a callback - * stack associated with memobj. The registered user callback functions are called in the reverse order in - * which they were registered. The user callback functions are called and then the memory object is deleted - * and its resources freed. This provides a mechanism for the application (and libraries) using memobj to be - * notified when the memory referenced by host_ptr, specified when the memory object is created and used as - * the storage bits for the memory object, can be reused or freed. - * - * The application may not call CL api's with the cl_mem object passed to the pfn_notify. - * - * Please check for the "cl_APPLE_SetMemObjectDestructor" extension using clGetDeviceInfo(CL_DEVICE_EXTENSIONS) - * before using. - */ -#define cl_APPLE_SetMemObjectDestructor 1 -cl_int CL_API_ENTRY clSetMemObjectDestructorAPPLE( cl_mem /* memobj */, - void (* /*pfn_notify*/)( cl_mem /* memobj */, void* /*user_data*/), - void * /*user_data */ ) CL_EXT_SUFFIX__VERSION_1_0; - - -/* Context Logging Functions - * - * The next three convenience functions are intended to be used as the pfn_notify parameter to clCreateContext(). - * Please check for the "cl_APPLE_ContextLoggingFunctions" extension using clGetDeviceInfo(CL_DEVICE_EXTENSIONS) - * before using. - * - * clLogMessagesToSystemLog fowards on all log messages to the Apple System Logger - */ -#define cl_APPLE_ContextLoggingFunctions 1 -extern void CL_API_ENTRY clLogMessagesToSystemLogAPPLE( const char * /* errstr */, - const void * /* private_info */, - size_t /* cb */, - void * /* user_data */ ) CL_EXT_SUFFIX__VERSION_1_0; - -/* clLogMessagesToStdout sends all log messages to the file descriptor stdout */ -extern void CL_API_ENTRY clLogMessagesToStdoutAPPLE( const char * /* errstr */, - const void * /* private_info */, - size_t /* cb */, - void * /* user_data */ ) CL_EXT_SUFFIX__VERSION_1_0; - -/* clLogMessagesToStderr sends all log messages to the file descriptor stderr */ -extern void CL_API_ENTRY clLogMessagesToStderrAPPLE( const char * /* errstr */, - const void * /* private_info */, - size_t /* cb */, - void * /* user_data */ ) CL_EXT_SUFFIX__VERSION_1_0; - - -/************************ -* cl_khr_icd extension * -************************/ -#define cl_khr_icd 1 - -/* cl_platform_info */ -#define CL_PLATFORM_ICD_SUFFIX_KHR 0x0920 - -/* Additional Error Codes */ -#define CL_PLATFORM_NOT_FOUND_KHR -1001 - -extern CL_API_ENTRY cl_int CL_API_CALL -clIcdGetPlatformIDsKHR(cl_uint /* num_entries */, - cl_platform_id * /* platforms */, - cl_uint * /* num_platforms */); - -typedef CL_API_ENTRY cl_int (CL_API_CALL *clIcdGetPlatformIDsKHR_fn)( - cl_uint /* num_entries */, - cl_platform_id * /* platforms */, - cl_uint * /* num_platforms */); - - -/* Extension: cl_khr_image2D_buffer - * - * This extension allows a 2D image to be created from a cl_mem buffer without a copy. - * The type associated with a 2D image created from a buffer in an OpenCL program is image2d_t. - * Both the sampler and sampler-less read_image built-in functions are supported for 2D images - * and 2D images created from a buffer. Similarly, the write_image built-ins are also supported - * for 2D images created from a buffer. - * - * When the 2D image from buffer is created, the client must specify the width, - * height, image format (i.e. channel order and channel data type) and optionally the row pitch - * - * The pitch specified must be a multiple of CL_DEVICE_IMAGE_PITCH_ALIGNMENT pixels. - * The base address of the buffer must be aligned to CL_DEVICE_IMAGE_BASE_ADDRESS_ALIGNMENT pixels. - */ - -/************************************* - * cl_khr_initalize_memory extension * - *************************************/ - -#define CL_CONTEXT_MEMORY_INITIALIZE_KHR 0x2030 - - -/************************************** - * cl_khr_terminate_context extension * - **************************************/ - -#define CL_DEVICE_TERMINATE_CAPABILITY_KHR 0x2031 -#define CL_CONTEXT_TERMINATE_KHR 0x2032 - -#define cl_khr_terminate_context 1 -extern CL_API_ENTRY cl_int CL_API_CALL clTerminateContextKHR(cl_context /* context */) CL_EXT_SUFFIX__VERSION_1_2; - -typedef CL_API_ENTRY cl_int (CL_API_CALL *clTerminateContextKHR_fn)(cl_context /* context */) CL_EXT_SUFFIX__VERSION_1_2; - - -/* - * Extension: cl_khr_spir - * - * This extension adds support to create an OpenCL program object from a - * Standard Portable Intermediate Representation (SPIR) instance - */ - -#define CL_DEVICE_SPIR_VERSIONS 0x40E0 -#define CL_PROGRAM_BINARY_TYPE_INTERMEDIATE 0x40E1 - - -/****************************************** -* cl_nv_device_attribute_query extension * -******************************************/ -/* cl_nv_device_attribute_query extension - no extension #define since it has no functions */ -#define CL_DEVICE_COMPUTE_CAPABILITY_MAJOR_NV 0x4000 -#define CL_DEVICE_COMPUTE_CAPABILITY_MINOR_NV 0x4001 -#define CL_DEVICE_REGISTERS_PER_BLOCK_NV 0x4002 -#define CL_DEVICE_WARP_SIZE_NV 0x4003 -#define CL_DEVICE_GPU_OVERLAP_NV 0x4004 -#define CL_DEVICE_KERNEL_EXEC_TIMEOUT_NV 0x4005 -#define CL_DEVICE_INTEGRATED_MEMORY_NV 0x4006 - -/********************************* -* cl_amd_device_attribute_query * -*********************************/ -#define CL_DEVICE_PROFILING_TIMER_OFFSET_AMD 0x4036 - -/********************************* -* cl_arm_printf extension -*********************************/ -#define CL_PRINTF_CALLBACK_ARM 0x40B0 -#define CL_PRINTF_BUFFERSIZE_ARM 0x40B1 - -#ifdef CL_VERSION_1_1 - /*********************************** - * cl_ext_device_fission extension * - ***********************************/ - #define cl_ext_device_fission 1 - - extern CL_API_ENTRY cl_int CL_API_CALL - clReleaseDeviceEXT( cl_device_id /*device*/ ) CL_EXT_SUFFIX__VERSION_1_1; - - typedef CL_API_ENTRY cl_int - (CL_API_CALL *clReleaseDeviceEXT_fn)( cl_device_id /*device*/ ) CL_EXT_SUFFIX__VERSION_1_1; - - extern CL_API_ENTRY cl_int CL_API_CALL - clRetainDeviceEXT( cl_device_id /*device*/ ) CL_EXT_SUFFIX__VERSION_1_1; - - typedef CL_API_ENTRY cl_int - (CL_API_CALL *clRetainDeviceEXT_fn)( cl_device_id /*device*/ ) CL_EXT_SUFFIX__VERSION_1_1; - - typedef cl_ulong cl_device_partition_property_ext; - extern CL_API_ENTRY cl_int CL_API_CALL - clCreateSubDevicesEXT( cl_device_id /*in_device*/, - const cl_device_partition_property_ext * /* properties */, - cl_uint /*num_entries*/, - cl_device_id * /*out_devices*/, - cl_uint * /*num_devices*/ ) CL_EXT_SUFFIX__VERSION_1_1; - - typedef CL_API_ENTRY cl_int - ( CL_API_CALL * clCreateSubDevicesEXT_fn)( cl_device_id /*in_device*/, - const cl_device_partition_property_ext * /* properties */, - cl_uint /*num_entries*/, - cl_device_id * /*out_devices*/, - cl_uint * /*num_devices*/ ) CL_EXT_SUFFIX__VERSION_1_1; - - /* cl_device_partition_property_ext */ - #define CL_DEVICE_PARTITION_EQUALLY_EXT 0x4050 - #define CL_DEVICE_PARTITION_BY_COUNTS_EXT 0x4051 - #define CL_DEVICE_PARTITION_BY_NAMES_EXT 0x4052 - #define CL_DEVICE_PARTITION_BY_AFFINITY_DOMAIN_EXT 0x4053 - - /* clDeviceGetInfo selectors */ - #define CL_DEVICE_PARENT_DEVICE_EXT 0x4054 - #define CL_DEVICE_PARTITION_TYPES_EXT 0x4055 - #define CL_DEVICE_AFFINITY_DOMAINS_EXT 0x4056 - #define CL_DEVICE_REFERENCE_COUNT_EXT 0x4057 - #define CL_DEVICE_PARTITION_STYLE_EXT 0x4058 - - /* error codes */ - #define CL_DEVICE_PARTITION_FAILED_EXT -1057 - #define CL_INVALID_PARTITION_COUNT_EXT -1058 - #define CL_INVALID_PARTITION_NAME_EXT -1059 - - /* CL_AFFINITY_DOMAINs */ - #define CL_AFFINITY_DOMAIN_L1_CACHE_EXT 0x1 - #define CL_AFFINITY_DOMAIN_L2_CACHE_EXT 0x2 - #define CL_AFFINITY_DOMAIN_L3_CACHE_EXT 0x3 - #define CL_AFFINITY_DOMAIN_L4_CACHE_EXT 0x4 - #define CL_AFFINITY_DOMAIN_NUMA_EXT 0x10 - #define CL_AFFINITY_DOMAIN_NEXT_FISSIONABLE_EXT 0x100 - - /* cl_device_partition_property_ext list terminators */ - #define CL_PROPERTIES_LIST_END_EXT ((cl_device_partition_property_ext) 0) - #define CL_PARTITION_BY_COUNTS_LIST_END_EXT ((cl_device_partition_property_ext) 0) - #define CL_PARTITION_BY_NAMES_LIST_END_EXT ((cl_device_partition_property_ext) 0 - 1) - -/********************************* -* cl_qcom_ext_host_ptr extension -*********************************/ - -#define CL_MEM_EXT_HOST_PTR_QCOM (1 << 29) - -#define CL_DEVICE_EXT_MEM_PADDING_IN_BYTES_QCOM 0x40A0 -#define CL_DEVICE_PAGE_SIZE_QCOM 0x40A1 -#define CL_IMAGE_ROW_ALIGNMENT_QCOM 0x40A2 -#define CL_IMAGE_SLICE_ALIGNMENT_QCOM 0x40A3 -#define CL_MEM_HOST_UNCACHED_QCOM 0x40A4 -#define CL_MEM_HOST_WRITEBACK_QCOM 0x40A5 -#define CL_MEM_HOST_WRITETHROUGH_QCOM 0x40A6 -#define CL_MEM_HOST_WRITE_COMBINING_QCOM 0x40A7 - -typedef cl_uint cl_image_pitch_info_qcom; - -extern CL_API_ENTRY cl_int CL_API_CALL -clGetDeviceImageInfoQCOM(cl_device_id device, - size_t image_width, - size_t image_height, - const cl_image_format *image_format, - cl_image_pitch_info_qcom param_name, - size_t param_value_size, - void *param_value, - size_t *param_value_size_ret); - -typedef struct _cl_mem_ext_host_ptr -{ - /* Type of external memory allocation. */ - /* Legal values will be defined in layered extensions. */ - cl_uint allocation_type; - - /* Host cache policy for this external memory allocation. */ - cl_uint host_cache_policy; - -} cl_mem_ext_host_ptr; - -/********************************* -* cl_qcom_ion_host_ptr extension -*********************************/ - -#define CL_MEM_ION_HOST_PTR_QCOM 0x40A8 - -typedef struct _cl_mem_ion_host_ptr -{ - /* Type of external memory allocation. */ - /* Must be CL_MEM_ION_HOST_PTR_QCOM for ION allocations. */ - cl_mem_ext_host_ptr ext_host_ptr; - - /* ION file descriptor */ - int ion_filedesc; - - /* Host pointer to the ION allocated memory */ - void* ion_hostptr; - -} cl_mem_ion_host_ptr; - -#endif /* CL_VERSION_1_1 */ - - -#ifdef CL_VERSION_2_0 -/********************************* -* cl_khr_sub_groups extension -*********************************/ -#define cl_khr_sub_groups 1 - -typedef cl_uint cl_kernel_sub_group_info_khr; - -/* cl_khr_sub_group_info */ -#define CL_KERNEL_MAX_SUB_GROUP_SIZE_FOR_NDRANGE_KHR 0x2033 -#define CL_KERNEL_SUB_GROUP_COUNT_FOR_NDRANGE_KHR 0x2034 - -extern CL_API_ENTRY cl_int CL_API_CALL -clGetKernelSubGroupInfoKHR(cl_kernel /* in_kernel */, - cl_device_id /*in_device*/, - cl_kernel_sub_group_info_khr /* param_name */, - size_t /*input_value_size*/, - const void * /*input_value*/, - size_t /*param_value_size*/, - void* /*param_value*/, - size_t* /*param_value_size_ret*/ ) CL_EXT_SUFFIX__VERSION_2_0_DEPRECATED; - -typedef CL_API_ENTRY cl_int - ( CL_API_CALL * clGetKernelSubGroupInfoKHR_fn)(cl_kernel /* in_kernel */, - cl_device_id /*in_device*/, - cl_kernel_sub_group_info_khr /* param_name */, - size_t /*input_value_size*/, - const void * /*input_value*/, - size_t /*param_value_size*/, - void* /*param_value*/, - size_t* /*param_value_size_ret*/ ) CL_EXT_SUFFIX__VERSION_2_0_DEPRECATED; -#endif /* CL_VERSION_2_0 */ - -#ifdef CL_VERSION_2_1 -/********************************* -* cl_khr_priority_hints extension -*********************************/ -#define cl_khr_priority_hints 1 - -typedef cl_uint cl_queue_priority_khr; - -/* cl_command_queue_properties */ -#define CL_QUEUE_PRIORITY_KHR 0x1096 - -/* cl_queue_priority_khr */ -#define CL_QUEUE_PRIORITY_HIGH_KHR (1<<0) -#define CL_QUEUE_PRIORITY_MED_KHR (1<<1) -#define CL_QUEUE_PRIORITY_LOW_KHR (1<<2) - -#endif /* CL_VERSION_2_1 */ - -#ifdef CL_VERSION_2_1 -/********************************* -* cl_khr_throttle_hints extension -*********************************/ -#define cl_khr_throttle_hints 1 - -typedef cl_uint cl_queue_throttle_khr; - -/* cl_command_queue_properties */ -#define CL_QUEUE_THROTTLE_KHR 0x1097 - -/* cl_queue_throttle_khr */ -#define CL_QUEUE_THROTTLE_HIGH_KHR (1<<0) -#define CL_QUEUE_THROTTLE_MED_KHR (1<<1) -#define CL_QUEUE_THROTTLE_LOW_KHR (1<<2) - -#endif /* CL_VERSION_2_1 */ - -#ifdef __cplusplus -} -#endif - - -#endif /* __CL_EXT_H */ diff --git a/third_party/opencl/include/CL/cl_ext_qcom.h b/third_party/opencl/include/CL/cl_ext_qcom.h deleted file mode 100644 index 6328a1cd93..0000000000 --- a/third_party/opencl/include/CL/cl_ext_qcom.h +++ /dev/null @@ -1,255 +0,0 @@ -/* Copyright (c) 2009-2017 Qualcomm Technologies, Inc. All Rights Reserved. - * Qualcomm Technologies Proprietary and Confidential. - */ - -#ifndef __OPENCL_CL_EXT_QCOM_H -#define __OPENCL_CL_EXT_QCOM_H - -// Needed by cl_khr_egl_event extension -#include -#include -#include - -#ifdef __cplusplus -extern "C" { -#endif - - -/************************************ - * cl_qcom_create_buffer_from_image * - ************************************/ - -#define CL_BUFFER_FROM_IMAGE_ROW_PITCH_QCOM 0x40C0 -#define CL_BUFFER_FROM_IMAGE_SLICE_PITCH_QCOM 0x40C1 - -extern CL_API_ENTRY cl_mem CL_API_CALL -clCreateBufferFromImageQCOM(cl_mem image, - cl_mem_flags flags, - cl_int *errcode_ret); - - -/************************************ - * cl_qcom_limited_printf extension * - ************************************/ - -/* Builtin printf function buffer size in bytes. */ -#define CL_DEVICE_PRINTF_BUFFER_SIZE_QCOM 0x1049 - - -/************************************* - * cl_qcom_extended_images extension * - *************************************/ - -#define CL_CONTEXT_ENABLE_EXTENDED_IMAGES_QCOM 0x40AA -#define CL_DEVICE_EXTENDED_IMAGE2D_MAX_WIDTH_QCOM 0x40AB -#define CL_DEVICE_EXTENDED_IMAGE2D_MAX_HEIGHT_QCOM 0x40AC -#define CL_DEVICE_EXTENDED_IMAGE3D_MAX_WIDTH_QCOM 0x40AD -#define CL_DEVICE_EXTENDED_IMAGE3D_MAX_HEIGHT_QCOM 0x40AE -#define CL_DEVICE_EXTENDED_IMAGE3D_MAX_DEPTH_QCOM 0x40AF - -/************************************* - * cl_qcom_perf_hint extension * - *************************************/ - -typedef cl_uint cl_perf_hint; - -#define CL_CONTEXT_PERF_HINT_QCOM 0x40C2 - -/*cl_perf_hint*/ -#define CL_PERF_HINT_HIGH_QCOM 0x40C3 -#define CL_PERF_HINT_NORMAL_QCOM 0x40C4 -#define CL_PERF_HINT_LOW_QCOM 0x40C5 - -extern CL_API_ENTRY cl_int CL_API_CALL -clSetPerfHintQCOM(cl_context context, - cl_perf_hint perf_hint); - -// This extension is published at Khronos, so its definitions are made in cl_ext.h. -// This duplication is for backward compatibility. - -#ifndef CL_MEM_ANDROID_NATIVE_BUFFER_HOST_PTR_QCOM - -/********************************* -* cl_qcom_android_native_buffer_host_ptr extension -*********************************/ - -#define CL_MEM_ANDROID_NATIVE_BUFFER_HOST_PTR_QCOM 0x40C6 - - -typedef struct _cl_mem_android_native_buffer_host_ptr -{ - // Type of external memory allocation. - // Must be CL_MEM_ANDROID_NATIVE_BUFFER_HOST_PTR_QCOM for Android native buffers. - cl_mem_ext_host_ptr ext_host_ptr; - - // Virtual pointer to the android native buffer - void* anb_ptr; - -} cl_mem_android_native_buffer_host_ptr; - -#endif //#ifndef CL_MEM_ANDROID_NATIVE_BUFFER_HOST_PTR_QCOM - -/*********************************** -* cl_img_egl_image extension * -************************************/ -typedef void* CLeglImageIMG; -typedef void* CLeglDisplayIMG; - -extern CL_API_ENTRY cl_mem CL_API_CALL -clCreateFromEGLImageIMG(cl_context context, - cl_mem_flags flags, - CLeglImageIMG image, - CLeglDisplayIMG display, - cl_int *errcode_ret); - - -/********************************* -* cl_qcom_other_image extension -*********************************/ - -// Extended flag for creating/querying QCOM non-standard images -#define CL_MEM_OTHER_IMAGE_QCOM (1<<25) - -// cl_channel_type -#define CL_QCOM_UNORM_MIPI10 0x4159 -#define CL_QCOM_UNORM_MIPI12 0x415A -#define CL_QCOM_UNSIGNED_MIPI10 0x415B -#define CL_QCOM_UNSIGNED_MIPI12 0x415C -#define CL_QCOM_UNORM_INT10 0x415D -#define CL_QCOM_UNORM_INT12 0x415E -#define CL_QCOM_UNSIGNED_INT16 0x415F - -// cl_channel_order -// Dedicate 0x4130-0x415F range for QCOM extended image formats -// 0x4130 - 0x4132 range is assigned to pixel-oriented compressed format -#define CL_QCOM_BAYER 0x414E - -#define CL_QCOM_NV12 0x4133 -#define CL_QCOM_NV12_Y 0x4134 -#define CL_QCOM_NV12_UV 0x4135 - -#define CL_QCOM_TILED_NV12 0x4136 -#define CL_QCOM_TILED_NV12_Y 0x4137 -#define CL_QCOM_TILED_NV12_UV 0x4138 - -#define CL_QCOM_P010 0x413C -#define CL_QCOM_P010_Y 0x413D -#define CL_QCOM_P010_UV 0x413E - -#define CL_QCOM_TILED_P010 0x413F -#define CL_QCOM_TILED_P010_Y 0x4140 -#define CL_QCOM_TILED_P010_UV 0x4141 - - -#define CL_QCOM_TP10 0x4145 -#define CL_QCOM_TP10_Y 0x4146 -#define CL_QCOM_TP10_UV 0x4147 - -#define CL_QCOM_TILED_TP10 0x4148 -#define CL_QCOM_TILED_TP10_Y 0x4149 -#define CL_QCOM_TILED_TP10_UV 0x414A - -/********************************* -* cl_qcom_compressed_image extension -*********************************/ - -// Extended flag for creating/querying QCOM non-planar compressed images -#define CL_MEM_COMPRESSED_IMAGE_QCOM (1<<27) - -// Extended image format -// cl_channel_order -#define CL_QCOM_COMPRESSED_RGBA 0x4130 -#define CL_QCOM_COMPRESSED_RGBx 0x4131 - -#define CL_QCOM_COMPRESSED_NV12_Y 0x413A -#define CL_QCOM_COMPRESSED_NV12_UV 0x413B - -#define CL_QCOM_COMPRESSED_P010 0x4142 -#define CL_QCOM_COMPRESSED_P010_Y 0x4143 -#define CL_QCOM_COMPRESSED_P010_UV 0x4144 - -#define CL_QCOM_COMPRESSED_TP10 0x414B -#define CL_QCOM_COMPRESSED_TP10_Y 0x414C -#define CL_QCOM_COMPRESSED_TP10_UV 0x414D - -#define CL_QCOM_COMPRESSED_NV12_4R 0x414F -#define CL_QCOM_COMPRESSED_NV12_4R_Y 0x4150 -#define CL_QCOM_COMPRESSED_NV12_4R_UV 0x4151 -/********************************* -* cl_qcom_compressed_yuv_image_read extension -*********************************/ - -// Extended flag for creating/querying QCOM compressed images -#define CL_MEM_COMPRESSED_YUV_IMAGE_QCOM (1<<28) - -// Extended image format -#define CL_QCOM_COMPRESSED_NV12 0x10C4 - -// Extended flag for setting ION buffer allocation type -#define CL_MEM_ION_HOST_PTR_COMPRESSED_YUV_QCOM 0x40CD -#define CL_MEM_ION_HOST_PTR_PROTECTED_COMPRESSED_YUV_QCOM 0x40CE - -/********************************* -* cl_qcom_accelerated_image_ops -*********************************/ -#define CL_MEM_OBJECT_WEIGHT_IMAGE_QCOM 0x4110 -#define CL_DEVICE_HOF_MAX_NUM_PHASES_QCOM 0x4111 -#define CL_DEVICE_HOF_MAX_FILTER_SIZE_X_QCOM 0x4112 -#define CL_DEVICE_HOF_MAX_FILTER_SIZE_Y_QCOM 0x4113 -#define CL_DEVICE_BLOCK_MATCHING_MAX_REGION_SIZE_X_QCOM 0x4114 -#define CL_DEVICE_BLOCK_MATCHING_MAX_REGION_SIZE_Y_QCOM 0x4115 - -//Extended flag for specifying weight image type -#define CL_WEIGHT_IMAGE_SEPARABLE_QCOM (1<<0) - -// Box Filter -typedef struct _cl_box_filter_size_qcom -{ - // Width of box filter on X direction. - float box_filter_width; - - // Height of box filter on Y direction. - float box_filter_height; -} cl_box_filter_size_qcom; - -// HOF Weight Image Desc -typedef struct _cl_weight_desc_qcom -{ - /** Coordinate of the "center" point of the weight image, - based on the weight image's top-left corner as the origin. */ - size_t center_coord_x; - size_t center_coord_y; - cl_bitfield flags; -} cl_weight_desc_qcom; - -typedef struct _cl_weight_image_desc_qcom -{ - cl_image_desc image_desc; - cl_weight_desc_qcom weight_desc; -} cl_weight_image_desc_qcom; - -/************************************* - * cl_qcom_protected_context extension * - *************************************/ - -#define CL_CONTEXT_PROTECTED_QCOM 0x40C7 -#define CL_MEM_ION_HOST_PTR_PROTECTED_QCOM 0x40C8 - -/************************************* - * cl_qcom_priority_hint extension * - *************************************/ -#define CL_PRIORITY_HINT_NONE_QCOM 0 -typedef cl_uint cl_priority_hint; - -#define CL_CONTEXT_PRIORITY_HINT_QCOM 0x40C9 - -/*cl_priority_hint*/ -#define CL_PRIORITY_HINT_HIGH_QCOM 0x40CA -#define CL_PRIORITY_HINT_NORMAL_QCOM 0x40CB -#define CL_PRIORITY_HINT_LOW_QCOM 0x40CC - -#ifdef __cplusplus -} -#endif - -#endif /* __OPENCL_CL_EXT_QCOM_H */ diff --git a/third_party/opencl/include/CL/cl_gl.h b/third_party/opencl/include/CL/cl_gl.h deleted file mode 100644 index 945daa83d7..0000000000 --- a/third_party/opencl/include/CL/cl_gl.h +++ /dev/null @@ -1,167 +0,0 @@ -/********************************************************************************** - * Copyright (c) 2008-2015 The Khronos Group Inc. - * - * Permission is hereby granted, free of charge, to any person obtaining a - * copy of this software and/or associated documentation files (the - * "Materials"), to deal in the Materials without restriction, including - * without limitation the rights to use, copy, modify, merge, publish, - * distribute, sublicense, and/or sell copies of the Materials, and to - * permit persons to whom the Materials are furnished to do so, subject to - * the following conditions: - * - * The above copyright notice and this permission notice shall be included - * in all copies or substantial portions of the Materials. - * - * MODIFICATIONS TO THIS FILE MAY MEAN IT NO LONGER ACCURATELY REFLECTS - * KHRONOS STANDARDS. THE UNMODIFIED, NORMATIVE VERSIONS OF KHRONOS - * SPECIFICATIONS AND HEADER INFORMATION ARE LOCATED AT - * https://www.khronos.org/registry/ - * - * THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. - * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY - * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, - * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE - * MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS. - **********************************************************************************/ - -#ifndef __OPENCL_CL_GL_H -#define __OPENCL_CL_GL_H - -#ifdef __APPLE__ -#include -#else -#include -#endif - -#ifdef __cplusplus -extern "C" { -#endif - -typedef cl_uint cl_gl_object_type; -typedef cl_uint cl_gl_texture_info; -typedef cl_uint cl_gl_platform_info; -typedef struct __GLsync *cl_GLsync; - -/* cl_gl_object_type = 0x2000 - 0x200F enum values are currently taken */ -#define CL_GL_OBJECT_BUFFER 0x2000 -#define CL_GL_OBJECT_TEXTURE2D 0x2001 -#define CL_GL_OBJECT_TEXTURE3D 0x2002 -#define CL_GL_OBJECT_RENDERBUFFER 0x2003 -#define CL_GL_OBJECT_TEXTURE2D_ARRAY 0x200E -#define CL_GL_OBJECT_TEXTURE1D 0x200F -#define CL_GL_OBJECT_TEXTURE1D_ARRAY 0x2010 -#define CL_GL_OBJECT_TEXTURE_BUFFER 0x2011 - -/* cl_gl_texture_info */ -#define CL_GL_TEXTURE_TARGET 0x2004 -#define CL_GL_MIPMAP_LEVEL 0x2005 -#define CL_GL_NUM_SAMPLES 0x2012 - - -extern CL_API_ENTRY cl_mem CL_API_CALL -clCreateFromGLBuffer(cl_context /* context */, - cl_mem_flags /* flags */, - cl_GLuint /* bufobj */, - int * /* errcode_ret */) CL_API_SUFFIX__VERSION_1_0; - -extern CL_API_ENTRY cl_mem CL_API_CALL -clCreateFromGLTexture(cl_context /* context */, - cl_mem_flags /* flags */, - cl_GLenum /* target */, - cl_GLint /* miplevel */, - cl_GLuint /* texture */, - cl_int * /* errcode_ret */) CL_API_SUFFIX__VERSION_1_2; - -extern CL_API_ENTRY cl_mem CL_API_CALL -clCreateFromGLRenderbuffer(cl_context /* context */, - cl_mem_flags /* flags */, - cl_GLuint /* renderbuffer */, - cl_int * /* errcode_ret */) CL_API_SUFFIX__VERSION_1_0; - -extern CL_API_ENTRY cl_int CL_API_CALL -clGetGLObjectInfo(cl_mem /* memobj */, - cl_gl_object_type * /* gl_object_type */, - cl_GLuint * /* gl_object_name */) CL_API_SUFFIX__VERSION_1_0; - -extern CL_API_ENTRY cl_int CL_API_CALL -clGetGLTextureInfo(cl_mem /* memobj */, - cl_gl_texture_info /* param_name */, - size_t /* param_value_size */, - void * /* param_value */, - size_t * /* param_value_size_ret */) CL_API_SUFFIX__VERSION_1_0; - -extern CL_API_ENTRY cl_int CL_API_CALL -clEnqueueAcquireGLObjects(cl_command_queue /* command_queue */, - cl_uint /* num_objects */, - const cl_mem * /* mem_objects */, - cl_uint /* num_events_in_wait_list */, - const cl_event * /* event_wait_list */, - cl_event * /* event */) CL_API_SUFFIX__VERSION_1_0; - -extern CL_API_ENTRY cl_int CL_API_CALL -clEnqueueReleaseGLObjects(cl_command_queue /* command_queue */, - cl_uint /* num_objects */, - const cl_mem * /* mem_objects */, - cl_uint /* num_events_in_wait_list */, - const cl_event * /* event_wait_list */, - cl_event * /* event */) CL_API_SUFFIX__VERSION_1_0; - - -/* Deprecated OpenCL 1.1 APIs */ -extern CL_API_ENTRY CL_EXT_PREFIX__VERSION_1_1_DEPRECATED cl_mem CL_API_CALL -clCreateFromGLTexture2D(cl_context /* context */, - cl_mem_flags /* flags */, - cl_GLenum /* target */, - cl_GLint /* miplevel */, - cl_GLuint /* texture */, - cl_int * /* errcode_ret */) CL_EXT_SUFFIX__VERSION_1_1_DEPRECATED; - -extern CL_API_ENTRY CL_EXT_PREFIX__VERSION_1_1_DEPRECATED cl_mem CL_API_CALL -clCreateFromGLTexture3D(cl_context /* context */, - cl_mem_flags /* flags */, - cl_GLenum /* target */, - cl_GLint /* miplevel */, - cl_GLuint /* texture */, - cl_int * /* errcode_ret */) CL_EXT_SUFFIX__VERSION_1_1_DEPRECATED; - -/* cl_khr_gl_sharing extension */ - -#define cl_khr_gl_sharing 1 - -typedef cl_uint cl_gl_context_info; - -/* Additional Error Codes */ -#define CL_INVALID_GL_SHAREGROUP_REFERENCE_KHR -1000 - -/* cl_gl_context_info */ -#define CL_CURRENT_DEVICE_FOR_GL_CONTEXT_KHR 0x2006 -#define CL_DEVICES_FOR_GL_CONTEXT_KHR 0x2007 - -/* Additional cl_context_properties */ -#define CL_GL_CONTEXT_KHR 0x2008 -#define CL_EGL_DISPLAY_KHR 0x2009 -#define CL_GLX_DISPLAY_KHR 0x200A -#define CL_WGL_HDC_KHR 0x200B -#define CL_CGL_SHAREGROUP_KHR 0x200C - -extern CL_API_ENTRY cl_int CL_API_CALL -clGetGLContextInfoKHR(const cl_context_properties * /* properties */, - cl_gl_context_info /* param_name */, - size_t /* param_value_size */, - void * /* param_value */, - size_t * /* param_value_size_ret */) CL_API_SUFFIX__VERSION_1_0; - -typedef CL_API_ENTRY cl_int (CL_API_CALL *clGetGLContextInfoKHR_fn)( - const cl_context_properties * properties, - cl_gl_context_info param_name, - size_t param_value_size, - void * param_value, - size_t * param_value_size_ret); - -#ifdef __cplusplus -} -#endif - -#endif /* __OPENCL_CL_GL_H */ diff --git a/third_party/opencl/include/CL/cl_gl_ext.h b/third_party/opencl/include/CL/cl_gl_ext.h deleted file mode 100644 index e3c14c6408..0000000000 --- a/third_party/opencl/include/CL/cl_gl_ext.h +++ /dev/null @@ -1,74 +0,0 @@ -/********************************************************************************** - * Copyright (c) 2008-2015 The Khronos Group Inc. - * - * Permission is hereby granted, free of charge, to any person obtaining a - * copy of this software and/or associated documentation files (the - * "Materials"), to deal in the Materials without restriction, including - * without limitation the rights to use, copy, modify, merge, publish, - * distribute, sublicense, and/or sell copies of the Materials, and to - * permit persons to whom the Materials are furnished to do so, subject to - * the following conditions: - * - * The above copyright notice and this permission notice shall be included - * in all copies or substantial portions of the Materials. - * - * MODIFICATIONS TO THIS FILE MAY MEAN IT NO LONGER ACCURATELY REFLECTS - * KHRONOS STANDARDS. THE UNMODIFIED, NORMATIVE VERSIONS OF KHRONOS - * SPECIFICATIONS AND HEADER INFORMATION ARE LOCATED AT - * https://www.khronos.org/registry/ - * - * THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. - * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY - * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, - * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE - * MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS. - **********************************************************************************/ - -/* $Revision: 11708 $ on $Date: 2010-06-13 23:36:24 -0700 (Sun, 13 Jun 2010) $ */ - -/* cl_gl_ext.h contains vendor (non-KHR) OpenCL extensions which have */ -/* OpenGL dependencies. */ - -#ifndef __OPENCL_CL_GL_EXT_H -#define __OPENCL_CL_GL_EXT_H - -#ifdef __cplusplus -extern "C" { -#endif - -#ifdef __APPLE__ - #include -#else - #include -#endif - -/* - * For each extension, follow this template - * cl_VEN_extname extension */ -/* #define cl_VEN_extname 1 - * ... define new types, if any - * ... define new tokens, if any - * ... define new APIs, if any - * - * If you need GLtypes here, mirror them with a cl_GLtype, rather than including a GL header - * This allows us to avoid having to decide whether to include GL headers or GLES here. - */ - -/* - * cl_khr_gl_event extension - * See section 9.9 in the OpenCL 1.1 spec for more information - */ -#define CL_COMMAND_GL_FENCE_SYNC_OBJECT_KHR 0x200D - -extern CL_API_ENTRY cl_event CL_API_CALL -clCreateEventFromGLsyncKHR(cl_context /* context */, - cl_GLsync /* cl_GLsync */, - cl_int * /* errcode_ret */) CL_EXT_SUFFIX__VERSION_1_1; - -#ifdef __cplusplus -} -#endif - -#endif /* __OPENCL_CL_GL_EXT_H */ diff --git a/third_party/opencl/include/CL/cl_platform.h b/third_party/opencl/include/CL/cl_platform.h deleted file mode 100644 index 4e334a2918..0000000000 --- a/third_party/opencl/include/CL/cl_platform.h +++ /dev/null @@ -1,1333 +0,0 @@ -/********************************************************************************** - * Copyright (c) 2008-2015 The Khronos Group Inc. - * - * Permission is hereby granted, free of charge, to any person obtaining a - * copy of this software and/or associated documentation files (the - * "Materials"), to deal in the Materials without restriction, including - * without limitation the rights to use, copy, modify, merge, publish, - * distribute, sublicense, and/or sell copies of the Materials, and to - * permit persons to whom the Materials are furnished to do so, subject to - * the following conditions: - * - * The above copyright notice and this permission notice shall be included - * in all copies or substantial portions of the Materials. - * - * MODIFICATIONS TO THIS FILE MAY MEAN IT NO LONGER ACCURATELY REFLECTS - * KHRONOS STANDARDS. THE UNMODIFIED, NORMATIVE VERSIONS OF KHRONOS - * SPECIFICATIONS AND HEADER INFORMATION ARE LOCATED AT - * https://www.khronos.org/registry/ - * - * THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. - * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY - * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, - * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE - * MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS. - **********************************************************************************/ - -/* $Revision: 11803 $ on $Date: 2010-06-25 10:02:12 -0700 (Fri, 25 Jun 2010) $ */ - -#ifndef __CL_PLATFORM_H -#define __CL_PLATFORM_H - -#ifdef __APPLE__ - /* Contains #defines for AVAILABLE_MAC_OS_X_VERSION_10_6_AND_LATER below */ - #include -#endif - -#ifdef __cplusplus -extern "C" { -#endif - -#if defined(_WIN32) - #define CL_API_ENTRY - #define CL_API_CALL __stdcall - #define CL_CALLBACK __stdcall -#else - #define CL_API_ENTRY - #define CL_API_CALL - #define CL_CALLBACK -#endif - -/* - * Deprecation flags refer to the last version of the header in which the - * feature was not deprecated. - * - * E.g. VERSION_1_1_DEPRECATED means the feature is present in 1.1 without - * deprecation but is deprecated in versions later than 1.1. - */ - -#ifdef __APPLE__ - #define CL_EXTENSION_WEAK_LINK __attribute__((weak_import)) - #define CL_API_SUFFIX__VERSION_1_0 AVAILABLE_MAC_OS_X_VERSION_10_6_AND_LATER - #define CL_EXT_SUFFIX__VERSION_1_0 CL_EXTENSION_WEAK_LINK AVAILABLE_MAC_OS_X_VERSION_10_6_AND_LATER - #define CL_API_SUFFIX__VERSION_1_1 AVAILABLE_MAC_OS_X_VERSION_10_7_AND_LATER - #define GCL_API_SUFFIX__VERSION_1_1 AVAILABLE_MAC_OS_X_VERSION_10_7_AND_LATER - #define CL_EXT_SUFFIX__VERSION_1_1 CL_EXTENSION_WEAK_LINK AVAILABLE_MAC_OS_X_VERSION_10_7_AND_LATER - #define CL_EXT_SUFFIX__VERSION_1_0_DEPRECATED CL_EXTENSION_WEAK_LINK AVAILABLE_MAC_OS_X_VERSION_10_6_AND_LATER_BUT_DEPRECATED_IN_MAC_OS_X_VERSION_10_7 - - #ifdef AVAILABLE_MAC_OS_X_VERSION_10_8_AND_LATER - #define CL_API_SUFFIX__VERSION_1_2 AVAILABLE_MAC_OS_X_VERSION_10_8_AND_LATER - #define GCL_API_SUFFIX__VERSION_1_2 AVAILABLE_MAC_OS_X_VERSION_10_8_AND_LATER - #define CL_EXT_SUFFIX__VERSION_1_2 CL_EXTENSION_WEAK_LINK AVAILABLE_MAC_OS_X_VERSION_10_8_AND_LATER - #define CL_EXT_PREFIX__VERSION_1_1_DEPRECATED - #define CL_EXT_SUFFIX__VERSION_1_1_DEPRECATED CL_EXTENSION_WEAK_LINK AVAILABLE_MAC_OS_X_VERSION_10_7_AND_LATER_BUT_DEPRECATED_IN_MAC_OS_X_VERSION_10_8 - #else - #warning This path should never happen outside of internal operating system development. AvailabilityMacros do not function correctly here! - #define CL_API_SUFFIX__VERSION_1_2 AVAILABLE_MAC_OS_X_VERSION_10_7_AND_LATER - #define GCL_API_SUFFIX__VERSION_1_2 AVAILABLE_MAC_OS_X_VERSION_10_7_AND_LATER - #define CL_EXT_SUFFIX__VERSION_1_2 CL_EXTENSION_WEAK_LINK AVAILABLE_MAC_OS_X_VERSION_10_7_AND_LATER - #define CL_EXT_SUFFIX__VERSION_1_1_DEPRECATED CL_EXTENSION_WEAK_LINK AVAILABLE_MAC_OS_X_VERSION_10_7_AND_LATER - #endif -#else - #define CL_EXTENSION_WEAK_LINK - #define CL_API_SUFFIX__VERSION_1_0 - #define CL_EXT_SUFFIX__VERSION_1_0 - #define CL_API_SUFFIX__VERSION_1_1 - #define CL_EXT_SUFFIX__VERSION_1_1 - #define CL_API_SUFFIX__VERSION_1_2 - #define CL_EXT_SUFFIX__VERSION_1_2 - #define CL_API_SUFFIX__VERSION_2_0 - #define CL_EXT_SUFFIX__VERSION_2_0 - #define CL_API_SUFFIX__VERSION_2_1 - #define CL_EXT_SUFFIX__VERSION_2_1 - - #ifdef __GNUC__ - #ifdef CL_USE_DEPRECATED_OPENCL_1_0_APIS - #define CL_EXT_SUFFIX__VERSION_1_0_DEPRECATED - #define CL_EXT_PREFIX__VERSION_1_0_DEPRECATED - #else - #define CL_EXT_SUFFIX__VERSION_1_0_DEPRECATED __attribute__((deprecated)) - #define CL_EXT_PREFIX__VERSION_1_0_DEPRECATED - #endif - - #ifdef CL_USE_DEPRECATED_OPENCL_1_1_APIS - #define CL_EXT_SUFFIX__VERSION_1_1_DEPRECATED - #define CL_EXT_PREFIX__VERSION_1_1_DEPRECATED - #else - #define CL_EXT_SUFFIX__VERSION_1_1_DEPRECATED __attribute__((deprecated)) - #define CL_EXT_PREFIX__VERSION_1_1_DEPRECATED - #endif - - #ifdef CL_USE_DEPRECATED_OPENCL_1_2_APIS - #define CL_EXT_SUFFIX__VERSION_1_2_DEPRECATED - #define CL_EXT_PREFIX__VERSION_1_2_DEPRECATED - #else - #define CL_EXT_SUFFIX__VERSION_1_2_DEPRECATED __attribute__((deprecated)) - #define CL_EXT_PREFIX__VERSION_1_2_DEPRECATED - #endif - - #ifdef CL_USE_DEPRECATED_OPENCL_2_0_APIS - #define CL_EXT_SUFFIX__VERSION_2_0_DEPRECATED - #define CL_EXT_PREFIX__VERSION_2_0_DEPRECATED - #else - #define CL_EXT_SUFFIX__VERSION_2_0_DEPRECATED __attribute__((deprecated)) - #define CL_EXT_PREFIX__VERSION_2_0_DEPRECATED - #endif - #elif _WIN32 - #ifdef CL_USE_DEPRECATED_OPENCL_1_0_APIS - #define CL_EXT_SUFFIX__VERSION_1_0_DEPRECATED - #define CL_EXT_PREFIX__VERSION_1_0_DEPRECATED - #else - #define CL_EXT_SUFFIX__VERSION_1_0_DEPRECATED - #define CL_EXT_PREFIX__VERSION_1_0_DEPRECATED __declspec(deprecated) - #endif - - #ifdef CL_USE_DEPRECATED_OPENCL_1_1_APIS - #define CL_EXT_SUFFIX__VERSION_1_1_DEPRECATED - #define CL_EXT_PREFIX__VERSION_1_1_DEPRECATED - #else - #define CL_EXT_SUFFIX__VERSION_1_1_DEPRECATED - #define CL_EXT_PREFIX__VERSION_1_1_DEPRECATED __declspec(deprecated) - #endif - - #ifdef CL_USE_DEPRECATED_OPENCL_1_2_APIS - #define CL_EXT_SUFFIX__VERSION_1_2_DEPRECATED - #define CL_EXT_PREFIX__VERSION_1_2_DEPRECATED - #else - #define CL_EXT_SUFFIX__VERSION_1_2_DEPRECATED - #define CL_EXT_PREFIX__VERSION_1_2_DEPRECATED __declspec(deprecated) - #endif - - #ifdef CL_USE_DEPRECATED_OPENCL_2_0_APIS - #define CL_EXT_SUFFIX__VERSION_2_0_DEPRECATED - #define CL_EXT_PREFIX__VERSION_2_0_DEPRECATED - #else - #define CL_EXT_SUFFIX__VERSION_2_0_DEPRECATED - #define CL_EXT_PREFIX__VERSION_2_0_DEPRECATED __declspec(deprecated) - #endif - #else - #define CL_EXT_SUFFIX__VERSION_1_0_DEPRECATED - #define CL_EXT_PREFIX__VERSION_1_0_DEPRECATED - - #define CL_EXT_SUFFIX__VERSION_1_1_DEPRECATED - #define CL_EXT_PREFIX__VERSION_1_1_DEPRECATED - - #define CL_EXT_SUFFIX__VERSION_1_2_DEPRECATED - #define CL_EXT_PREFIX__VERSION_1_2_DEPRECATED - - #define CL_EXT_SUFFIX__VERSION_2_0_DEPRECATED - #define CL_EXT_PREFIX__VERSION_2_0_DEPRECATED - #endif -#endif - -#if (defined (_WIN32) && defined(_MSC_VER)) - -/* scalar types */ -typedef signed __int8 cl_char; -typedef unsigned __int8 cl_uchar; -typedef signed __int16 cl_short; -typedef unsigned __int16 cl_ushort; -typedef signed __int32 cl_int; -typedef unsigned __int32 cl_uint; -typedef signed __int64 cl_long; -typedef unsigned __int64 cl_ulong; - -typedef unsigned __int16 cl_half; -typedef float cl_float; -typedef double cl_double; - -/* Macro names and corresponding values defined by OpenCL */ -#define CL_CHAR_BIT 8 -#define CL_SCHAR_MAX 127 -#define CL_SCHAR_MIN (-127-1) -#define CL_CHAR_MAX CL_SCHAR_MAX -#define CL_CHAR_MIN CL_SCHAR_MIN -#define CL_UCHAR_MAX 255 -#define CL_SHRT_MAX 32767 -#define CL_SHRT_MIN (-32767-1) -#define CL_USHRT_MAX 65535 -#define CL_INT_MAX 2147483647 -#define CL_INT_MIN (-2147483647-1) -#define CL_UINT_MAX 0xffffffffU -#define CL_LONG_MAX ((cl_long) 0x7FFFFFFFFFFFFFFFLL) -#define CL_LONG_MIN ((cl_long) -0x7FFFFFFFFFFFFFFFLL - 1LL) -#define CL_ULONG_MAX ((cl_ulong) 0xFFFFFFFFFFFFFFFFULL) - -#define CL_FLT_DIG 6 -#define CL_FLT_MANT_DIG 24 -#define CL_FLT_MAX_10_EXP +38 -#define CL_FLT_MAX_EXP +128 -#define CL_FLT_MIN_10_EXP -37 -#define CL_FLT_MIN_EXP -125 -#define CL_FLT_RADIX 2 -#define CL_FLT_MAX 340282346638528859811704183484516925440.0f -#define CL_FLT_MIN 1.175494350822287507969e-38f -#define CL_FLT_EPSILON 0x1.0p-23f - -#define CL_DBL_DIG 15 -#define CL_DBL_MANT_DIG 53 -#define CL_DBL_MAX_10_EXP +308 -#define CL_DBL_MAX_EXP +1024 -#define CL_DBL_MIN_10_EXP -307 -#define CL_DBL_MIN_EXP -1021 -#define CL_DBL_RADIX 2 -#define CL_DBL_MAX 179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881250404026184124858368.0 -#define CL_DBL_MIN 2.225073858507201383090e-308 -#define CL_DBL_EPSILON 2.220446049250313080847e-16 - -#define CL_M_E 2.718281828459045090796 -#define CL_M_LOG2E 1.442695040888963387005 -#define CL_M_LOG10E 0.434294481903251816668 -#define CL_M_LN2 0.693147180559945286227 -#define CL_M_LN10 2.302585092994045901094 -#define CL_M_PI 3.141592653589793115998 -#define CL_M_PI_2 1.570796326794896557999 -#define CL_M_PI_4 0.785398163397448278999 -#define CL_M_1_PI 0.318309886183790691216 -#define CL_M_2_PI 0.636619772367581382433 -#define CL_M_2_SQRTPI 1.128379167095512558561 -#define CL_M_SQRT2 1.414213562373095145475 -#define CL_M_SQRT1_2 0.707106781186547572737 - -#define CL_M_E_F 2.71828174591064f -#define CL_M_LOG2E_F 1.44269502162933f -#define CL_M_LOG10E_F 0.43429449200630f -#define CL_M_LN2_F 0.69314718246460f -#define CL_M_LN10_F 2.30258512496948f -#define CL_M_PI_F 3.14159274101257f -#define CL_M_PI_2_F 1.57079637050629f -#define CL_M_PI_4_F 0.78539818525314f -#define CL_M_1_PI_F 0.31830987334251f -#define CL_M_2_PI_F 0.63661974668503f -#define CL_M_2_SQRTPI_F 1.12837922573090f -#define CL_M_SQRT2_F 1.41421353816986f -#define CL_M_SQRT1_2_F 0.70710676908493f - -#define CL_NAN (CL_INFINITY - CL_INFINITY) -#define CL_HUGE_VALF ((cl_float) 1e50) -#define CL_HUGE_VAL ((cl_double) 1e500) -#define CL_MAXFLOAT CL_FLT_MAX -#define CL_INFINITY CL_HUGE_VALF - -#else - -#include - -/* scalar types */ -typedef int8_t cl_char; -typedef uint8_t cl_uchar; -typedef int16_t cl_short __attribute__((aligned(2))); -typedef uint16_t cl_ushort __attribute__((aligned(2))); -typedef int32_t cl_int __attribute__((aligned(4))); -typedef uint32_t cl_uint __attribute__((aligned(4))); -typedef int64_t cl_long __attribute__((aligned(8))); -typedef uint64_t cl_ulong __attribute__((aligned(8))); - -typedef uint16_t cl_half __attribute__((aligned(2))); -typedef float cl_float __attribute__((aligned(4))); -typedef double cl_double __attribute__((aligned(8))); - -/* Macro names and corresponding values defined by OpenCL */ -#define CL_CHAR_BIT 8 -#define CL_SCHAR_MAX 127 -#define CL_SCHAR_MIN (-127-1) -#define CL_CHAR_MAX CL_SCHAR_MAX -#define CL_CHAR_MIN CL_SCHAR_MIN -#define CL_UCHAR_MAX 255 -#define CL_SHRT_MAX 32767 -#define CL_SHRT_MIN (-32767-1) -#define CL_USHRT_MAX 65535 -#define CL_INT_MAX 2147483647 -#define CL_INT_MIN (-2147483647-1) -#define CL_UINT_MAX 0xffffffffU -#define CL_LONG_MAX ((cl_long) 0x7FFFFFFFFFFFFFFFLL) -#define CL_LONG_MIN ((cl_long) -0x7FFFFFFFFFFFFFFFLL - 1LL) -#define CL_ULONG_MAX ((cl_ulong) 0xFFFFFFFFFFFFFFFFULL) - -#define CL_FLT_DIG 6 -#define CL_FLT_MANT_DIG 24 -#define CL_FLT_MAX_10_EXP +38 -#define CL_FLT_MAX_EXP +128 -#define CL_FLT_MIN_10_EXP -37 -#define CL_FLT_MIN_EXP -125 -#define CL_FLT_RADIX 2 -#define CL_FLT_MAX 0x1.fffffep127f -#define CL_FLT_MIN 0x1.0p-126f -#define CL_FLT_EPSILON 0x1.0p-23f - -#define CL_DBL_DIG 15 -#define CL_DBL_MANT_DIG 53 -#define CL_DBL_MAX_10_EXP +308 -#define CL_DBL_MAX_EXP +1024 -#define CL_DBL_MIN_10_EXP -307 -#define CL_DBL_MIN_EXP -1021 -#define CL_DBL_RADIX 2 -#define CL_DBL_MAX 0x1.fffffffffffffp1023 -#define CL_DBL_MIN 0x1.0p-1022 -#define CL_DBL_EPSILON 0x1.0p-52 - -#define CL_M_E 2.718281828459045090796 -#define CL_M_LOG2E 1.442695040888963387005 -#define CL_M_LOG10E 0.434294481903251816668 -#define CL_M_LN2 0.693147180559945286227 -#define CL_M_LN10 2.302585092994045901094 -#define CL_M_PI 3.141592653589793115998 -#define CL_M_PI_2 1.570796326794896557999 -#define CL_M_PI_4 0.785398163397448278999 -#define CL_M_1_PI 0.318309886183790691216 -#define CL_M_2_PI 0.636619772367581382433 -#define CL_M_2_SQRTPI 1.128379167095512558561 -#define CL_M_SQRT2 1.414213562373095145475 -#define CL_M_SQRT1_2 0.707106781186547572737 - -#define CL_M_E_F 2.71828174591064f -#define CL_M_LOG2E_F 1.44269502162933f -#define CL_M_LOG10E_F 0.43429449200630f -#define CL_M_LN2_F 0.69314718246460f -#define CL_M_LN10_F 2.30258512496948f -#define CL_M_PI_F 3.14159274101257f -#define CL_M_PI_2_F 1.57079637050629f -#define CL_M_PI_4_F 0.78539818525314f -#define CL_M_1_PI_F 0.31830987334251f -#define CL_M_2_PI_F 0.63661974668503f -#define CL_M_2_SQRTPI_F 1.12837922573090f -#define CL_M_SQRT2_F 1.41421353816986f -#define CL_M_SQRT1_2_F 0.70710676908493f - -#if defined( __GNUC__ ) - #define CL_HUGE_VALF __builtin_huge_valf() - #define CL_HUGE_VAL __builtin_huge_val() - #define CL_NAN __builtin_nanf( "" ) -#else - #define CL_HUGE_VALF ((cl_float) 1e50) - #define CL_HUGE_VAL ((cl_double) 1e500) - float nanf( const char * ); - #define CL_NAN nanf( "" ) -#endif -#define CL_MAXFLOAT CL_FLT_MAX -#define CL_INFINITY CL_HUGE_VALF - -#endif - -#include - -/* Mirror types to GL types. Mirror types allow us to avoid deciding which 87s to load based on whether we are using GL or GLES here. */ -typedef unsigned int cl_GLuint; -typedef int cl_GLint; -typedef unsigned int cl_GLenum; - -/* - * Vector types - * - * Note: OpenCL requires that all types be naturally aligned. - * This means that vector types must be naturally aligned. - * For example, a vector of four floats must be aligned to - * a 16 byte boundary (calculated as 4 * the natural 4-byte - * alignment of the float). The alignment qualifiers here - * will only function properly if your compiler supports them - * and if you don't actively work to defeat them. For example, - * in order for a cl_float4 to be 16 byte aligned in a struct, - * the start of the struct must itself be 16-byte aligned. - * - * Maintaining proper alignment is the user's responsibility. - */ - -/* Define basic vector types */ -#if defined( __VEC__ ) - #include /* may be omitted depending on compiler. AltiVec spec provides no way to detect whether the header is required. */ - typedef vector unsigned char __cl_uchar16; - typedef vector signed char __cl_char16; - typedef vector unsigned short __cl_ushort8; - typedef vector signed short __cl_short8; - typedef vector unsigned int __cl_uint4; - typedef vector signed int __cl_int4; - typedef vector float __cl_float4; - #define __CL_UCHAR16__ 1 - #define __CL_CHAR16__ 1 - #define __CL_USHORT8__ 1 - #define __CL_SHORT8__ 1 - #define __CL_UINT4__ 1 - #define __CL_INT4__ 1 - #define __CL_FLOAT4__ 1 -#endif - -#if defined( __SSE__ ) - #if defined( __MINGW64__ ) - #include - #else - #include - #endif - #if defined( __GNUC__ ) - typedef float __cl_float4 __attribute__((vector_size(16))); - #else - typedef __m128 __cl_float4; - #endif - #define __CL_FLOAT4__ 1 -#endif - -#if defined( __SSE2__ ) - #if defined( __MINGW64__ ) - #include - #else - #include - #endif - #if defined( __GNUC__ ) - typedef cl_uchar __cl_uchar16 __attribute__((vector_size(16))); - typedef cl_char __cl_char16 __attribute__((vector_size(16))); - typedef cl_ushort __cl_ushort8 __attribute__((vector_size(16))); - typedef cl_short __cl_short8 __attribute__((vector_size(16))); - typedef cl_uint __cl_uint4 __attribute__((vector_size(16))); - typedef cl_int __cl_int4 __attribute__((vector_size(16))); - typedef cl_ulong __cl_ulong2 __attribute__((vector_size(16))); - typedef cl_long __cl_long2 __attribute__((vector_size(16))); - typedef cl_double __cl_double2 __attribute__((vector_size(16))); - #else - typedef __m128i __cl_uchar16; - typedef __m128i __cl_char16; - typedef __m128i __cl_ushort8; - typedef __m128i __cl_short8; - typedef __m128i __cl_uint4; - typedef __m128i __cl_int4; - typedef __m128i __cl_ulong2; - typedef __m128i __cl_long2; - typedef __m128d __cl_double2; - #endif - #define __CL_UCHAR16__ 1 - #define __CL_CHAR16__ 1 - #define __CL_USHORT8__ 1 - #define __CL_SHORT8__ 1 - #define __CL_INT4__ 1 - #define __CL_UINT4__ 1 - #define __CL_ULONG2__ 1 - #define __CL_LONG2__ 1 - #define __CL_DOUBLE2__ 1 -#endif - -#if defined( __MMX__ ) - #include - #if defined( __GNUC__ ) - typedef cl_uchar __cl_uchar8 __attribute__((vector_size(8))); - typedef cl_char __cl_char8 __attribute__((vector_size(8))); - typedef cl_ushort __cl_ushort4 __attribute__((vector_size(8))); - typedef cl_short __cl_short4 __attribute__((vector_size(8))); - typedef cl_uint __cl_uint2 __attribute__((vector_size(8))); - typedef cl_int __cl_int2 __attribute__((vector_size(8))); - typedef cl_ulong __cl_ulong1 __attribute__((vector_size(8))); - typedef cl_long __cl_long1 __attribute__((vector_size(8))); - typedef cl_float __cl_float2 __attribute__((vector_size(8))); - #else - typedef __m64 __cl_uchar8; - typedef __m64 __cl_char8; - typedef __m64 __cl_ushort4; - typedef __m64 __cl_short4; - typedef __m64 __cl_uint2; - typedef __m64 __cl_int2; - typedef __m64 __cl_ulong1; - typedef __m64 __cl_long1; - typedef __m64 __cl_float2; - #endif - #define __CL_UCHAR8__ 1 - #define __CL_CHAR8__ 1 - #define __CL_USHORT4__ 1 - #define __CL_SHORT4__ 1 - #define __CL_INT2__ 1 - #define __CL_UINT2__ 1 - #define __CL_ULONG1__ 1 - #define __CL_LONG1__ 1 - #define __CL_FLOAT2__ 1 -#endif - -#if defined( __AVX__ ) - #if defined( __MINGW64__ ) - #include - #else - #include - #endif - #if defined( __GNUC__ ) - typedef cl_float __cl_float8 __attribute__((vector_size(32))); - typedef cl_double __cl_double4 __attribute__((vector_size(32))); - #else - typedef __m256 __cl_float8; - typedef __m256d __cl_double4; - #endif - #define __CL_FLOAT8__ 1 - #define __CL_DOUBLE4__ 1 -#endif - -/* Define capabilities for anonymous struct members. */ -#if defined( __GNUC__) && ! defined( __STRICT_ANSI__ ) -#define __CL_HAS_ANON_STRUCT__ 1 -#define __CL_ANON_STRUCT__ __extension__ -#elif defined( _WIN32) && (_MSC_VER >= 1500) - /* Microsoft Developer Studio 2008 supports anonymous structs, but - * complains by default. */ -#define __CL_HAS_ANON_STRUCT__ 1 -#define __CL_ANON_STRUCT__ - /* Disable warning C4201: nonstandard extension used : nameless - * struct/union */ -#pragma warning( push ) -#pragma warning( disable : 4201 ) -#else -#define __CL_HAS_ANON_STRUCT__ 0 -#define __CL_ANON_STRUCT__ -#endif - -/* Define alignment keys */ -#if defined( __GNUC__ ) - #define CL_ALIGNED(_x) __attribute__ ((aligned(_x))) -#elif defined( _WIN32) && (_MSC_VER) - /* Alignment keys neutered on windows because MSVC can't swallow function arguments with alignment requirements */ - /* http://msdn.microsoft.com/en-us/library/373ak2y1%28VS.71%29.aspx */ - /* #include */ - /* #define CL_ALIGNED(_x) _CRT_ALIGN(_x) */ - #define CL_ALIGNED(_x) -#else - #warning Need to implement some method to align data here - #define CL_ALIGNED(_x) -#endif - -/* Indicate whether .xyzw, .s0123 and .hi.lo are supported */ -#if __CL_HAS_ANON_STRUCT__ - /* .xyzw and .s0123...{f|F} are supported */ - #define CL_HAS_NAMED_VECTOR_FIELDS 1 - /* .hi and .lo are supported */ - #define CL_HAS_HI_LO_VECTOR_FIELDS 1 -#endif - -/* Define cl_vector types */ - -/* ---- cl_charn ---- */ -typedef union -{ - cl_char CL_ALIGNED(2) s[2]; -#if __CL_HAS_ANON_STRUCT__ - __CL_ANON_STRUCT__ struct{ cl_char x, y; }; - __CL_ANON_STRUCT__ struct{ cl_char s0, s1; }; - __CL_ANON_STRUCT__ struct{ cl_char lo, hi; }; -#endif -#if defined( __CL_CHAR2__) - __cl_char2 v2; -#endif -}cl_char2; - -typedef union -{ - cl_char CL_ALIGNED(4) s[4]; -#if __CL_HAS_ANON_STRUCT__ - __CL_ANON_STRUCT__ struct{ cl_char x, y, z, w; }; - __CL_ANON_STRUCT__ struct{ cl_char s0, s1, s2, s3; }; - __CL_ANON_STRUCT__ struct{ cl_char2 lo, hi; }; -#endif -#if defined( __CL_CHAR2__) - __cl_char2 v2[2]; -#endif -#if defined( __CL_CHAR4__) - __cl_char4 v4; -#endif -}cl_char4; - -/* cl_char3 is identical in size, alignment and behavior to cl_char4. See section 6.1.5. */ -typedef cl_char4 cl_char3; - -typedef union -{ - cl_char CL_ALIGNED(8) s[8]; -#if __CL_HAS_ANON_STRUCT__ - __CL_ANON_STRUCT__ struct{ cl_char x, y, z, w; }; - __CL_ANON_STRUCT__ struct{ cl_char s0, s1, s2, s3, s4, s5, s6, s7; }; - __CL_ANON_STRUCT__ struct{ cl_char4 lo, hi; }; -#endif -#if defined( __CL_CHAR2__) - __cl_char2 v2[4]; -#endif -#if defined( __CL_CHAR4__) - __cl_char4 v4[2]; -#endif -#if defined( __CL_CHAR8__ ) - __cl_char8 v8; -#endif -}cl_char8; - -typedef union -{ - cl_char CL_ALIGNED(16) s[16]; -#if __CL_HAS_ANON_STRUCT__ - __CL_ANON_STRUCT__ struct{ cl_char x, y, z, w, __spacer4, __spacer5, __spacer6, __spacer7, __spacer8, __spacer9, sa, sb, sc, sd, se, sf; }; - __CL_ANON_STRUCT__ struct{ cl_char s0, s1, s2, s3, s4, s5, s6, s7, s8, s9, sA, sB, sC, sD, sE, sF; }; - __CL_ANON_STRUCT__ struct{ cl_char8 lo, hi; }; -#endif -#if defined( __CL_CHAR2__) - __cl_char2 v2[8]; -#endif -#if defined( __CL_CHAR4__) - __cl_char4 v4[4]; -#endif -#if defined( __CL_CHAR8__ ) - __cl_char8 v8[2]; -#endif -#if defined( __CL_CHAR16__ ) - __cl_char16 v16; -#endif -}cl_char16; - - -/* ---- cl_ucharn ---- */ -typedef union -{ - cl_uchar CL_ALIGNED(2) s[2]; -#if __CL_HAS_ANON_STRUCT__ - __CL_ANON_STRUCT__ struct{ cl_uchar x, y; }; - __CL_ANON_STRUCT__ struct{ cl_uchar s0, s1; }; - __CL_ANON_STRUCT__ struct{ cl_uchar lo, hi; }; -#endif -#if defined( __cl_uchar2__) - __cl_uchar2 v2; -#endif -}cl_uchar2; - -typedef union -{ - cl_uchar CL_ALIGNED(4) s[4]; -#if __CL_HAS_ANON_STRUCT__ - __CL_ANON_STRUCT__ struct{ cl_uchar x, y, z, w; }; - __CL_ANON_STRUCT__ struct{ cl_uchar s0, s1, s2, s3; }; - __CL_ANON_STRUCT__ struct{ cl_uchar2 lo, hi; }; -#endif -#if defined( __CL_UCHAR2__) - __cl_uchar2 v2[2]; -#endif -#if defined( __CL_UCHAR4__) - __cl_uchar4 v4; -#endif -}cl_uchar4; - -/* cl_uchar3 is identical in size, alignment and behavior to cl_uchar4. See section 6.1.5. */ -typedef cl_uchar4 cl_uchar3; - -typedef union -{ - cl_uchar CL_ALIGNED(8) s[8]; -#if __CL_HAS_ANON_STRUCT__ - __CL_ANON_STRUCT__ struct{ cl_uchar x, y, z, w; }; - __CL_ANON_STRUCT__ struct{ cl_uchar s0, s1, s2, s3, s4, s5, s6, s7; }; - __CL_ANON_STRUCT__ struct{ cl_uchar4 lo, hi; }; -#endif -#if defined( __CL_UCHAR2__) - __cl_uchar2 v2[4]; -#endif -#if defined( __CL_UCHAR4__) - __cl_uchar4 v4[2]; -#endif -#if defined( __CL_UCHAR8__ ) - __cl_uchar8 v8; -#endif -}cl_uchar8; - -typedef union -{ - cl_uchar CL_ALIGNED(16) s[16]; -#if __CL_HAS_ANON_STRUCT__ - __CL_ANON_STRUCT__ struct{ cl_uchar x, y, z, w, __spacer4, __spacer5, __spacer6, __spacer7, __spacer8, __spacer9, sa, sb, sc, sd, se, sf; }; - __CL_ANON_STRUCT__ struct{ cl_uchar s0, s1, s2, s3, s4, s5, s6, s7, s8, s9, sA, sB, sC, sD, sE, sF; }; - __CL_ANON_STRUCT__ struct{ cl_uchar8 lo, hi; }; -#endif -#if defined( __CL_UCHAR2__) - __cl_uchar2 v2[8]; -#endif -#if defined( __CL_UCHAR4__) - __cl_uchar4 v4[4]; -#endif -#if defined( __CL_UCHAR8__ ) - __cl_uchar8 v8[2]; -#endif -#if defined( __CL_UCHAR16__ ) - __cl_uchar16 v16; -#endif -}cl_uchar16; - - -/* ---- cl_shortn ---- */ -typedef union -{ - cl_short CL_ALIGNED(4) s[2]; -#if __CL_HAS_ANON_STRUCT__ - __CL_ANON_STRUCT__ struct{ cl_short x, y; }; - __CL_ANON_STRUCT__ struct{ cl_short s0, s1; }; - __CL_ANON_STRUCT__ struct{ cl_short lo, hi; }; -#endif -#if defined( __CL_SHORT2__) - __cl_short2 v2; -#endif -}cl_short2; - -typedef union -{ - cl_short CL_ALIGNED(8) s[4]; -#if __CL_HAS_ANON_STRUCT__ - __CL_ANON_STRUCT__ struct{ cl_short x, y, z, w; }; - __CL_ANON_STRUCT__ struct{ cl_short s0, s1, s2, s3; }; - __CL_ANON_STRUCT__ struct{ cl_short2 lo, hi; }; -#endif -#if defined( __CL_SHORT2__) - __cl_short2 v2[2]; -#endif -#if defined( __CL_SHORT4__) - __cl_short4 v4; -#endif -}cl_short4; - -/* cl_short3 is identical in size, alignment and behavior to cl_short4. See section 6.1.5. */ -typedef cl_short4 cl_short3; - -typedef union -{ - cl_short CL_ALIGNED(16) s[8]; -#if __CL_HAS_ANON_STRUCT__ - __CL_ANON_STRUCT__ struct{ cl_short x, y, z, w; }; - __CL_ANON_STRUCT__ struct{ cl_short s0, s1, s2, s3, s4, s5, s6, s7; }; - __CL_ANON_STRUCT__ struct{ cl_short4 lo, hi; }; -#endif -#if defined( __CL_SHORT2__) - __cl_short2 v2[4]; -#endif -#if defined( __CL_SHORT4__) - __cl_short4 v4[2]; -#endif -#if defined( __CL_SHORT8__ ) - __cl_short8 v8; -#endif -}cl_short8; - -typedef union -{ - cl_short CL_ALIGNED(32) s[16]; -#if __CL_HAS_ANON_STRUCT__ - __CL_ANON_STRUCT__ struct{ cl_short x, y, z, w, __spacer4, __spacer5, __spacer6, __spacer7, __spacer8, __spacer9, sa, sb, sc, sd, se, sf; }; - __CL_ANON_STRUCT__ struct{ cl_short s0, s1, s2, s3, s4, s5, s6, s7, s8, s9, sA, sB, sC, sD, sE, sF; }; - __CL_ANON_STRUCT__ struct{ cl_short8 lo, hi; }; -#endif -#if defined( __CL_SHORT2__) - __cl_short2 v2[8]; -#endif -#if defined( __CL_SHORT4__) - __cl_short4 v4[4]; -#endif -#if defined( __CL_SHORT8__ ) - __cl_short8 v8[2]; -#endif -#if defined( __CL_SHORT16__ ) - __cl_short16 v16; -#endif -}cl_short16; - - -/* ---- cl_ushortn ---- */ -typedef union -{ - cl_ushort CL_ALIGNED(4) s[2]; -#if __CL_HAS_ANON_STRUCT__ - __CL_ANON_STRUCT__ struct{ cl_ushort x, y; }; - __CL_ANON_STRUCT__ struct{ cl_ushort s0, s1; }; - __CL_ANON_STRUCT__ struct{ cl_ushort lo, hi; }; -#endif -#if defined( __CL_USHORT2__) - __cl_ushort2 v2; -#endif -}cl_ushort2; - -typedef union -{ - cl_ushort CL_ALIGNED(8) s[4]; -#if __CL_HAS_ANON_STRUCT__ - __CL_ANON_STRUCT__ struct{ cl_ushort x, y, z, w; }; - __CL_ANON_STRUCT__ struct{ cl_ushort s0, s1, s2, s3; }; - __CL_ANON_STRUCT__ struct{ cl_ushort2 lo, hi; }; -#endif -#if defined( __CL_USHORT2__) - __cl_ushort2 v2[2]; -#endif -#if defined( __CL_USHORT4__) - __cl_ushort4 v4; -#endif -}cl_ushort4; - -/* cl_ushort3 is identical in size, alignment and behavior to cl_ushort4. See section 6.1.5. */ -typedef cl_ushort4 cl_ushort3; - -typedef union -{ - cl_ushort CL_ALIGNED(16) s[8]; -#if __CL_HAS_ANON_STRUCT__ - __CL_ANON_STRUCT__ struct{ cl_ushort x, y, z, w; }; - __CL_ANON_STRUCT__ struct{ cl_ushort s0, s1, s2, s3, s4, s5, s6, s7; }; - __CL_ANON_STRUCT__ struct{ cl_ushort4 lo, hi; }; -#endif -#if defined( __CL_USHORT2__) - __cl_ushort2 v2[4]; -#endif -#if defined( __CL_USHORT4__) - __cl_ushort4 v4[2]; -#endif -#if defined( __CL_USHORT8__ ) - __cl_ushort8 v8; -#endif -}cl_ushort8; - -typedef union -{ - cl_ushort CL_ALIGNED(32) s[16]; -#if __CL_HAS_ANON_STRUCT__ - __CL_ANON_STRUCT__ struct{ cl_ushort x, y, z, w, __spacer4, __spacer5, __spacer6, __spacer7, __spacer8, __spacer9, sa, sb, sc, sd, se, sf; }; - __CL_ANON_STRUCT__ struct{ cl_ushort s0, s1, s2, s3, s4, s5, s6, s7, s8, s9, sA, sB, sC, sD, sE, sF; }; - __CL_ANON_STRUCT__ struct{ cl_ushort8 lo, hi; }; -#endif -#if defined( __CL_USHORT2__) - __cl_ushort2 v2[8]; -#endif -#if defined( __CL_USHORT4__) - __cl_ushort4 v4[4]; -#endif -#if defined( __CL_USHORT8__ ) - __cl_ushort8 v8[2]; -#endif -#if defined( __CL_USHORT16__ ) - __cl_ushort16 v16; -#endif -}cl_ushort16; - -/* ---- cl_intn ---- */ -typedef union -{ - cl_int CL_ALIGNED(8) s[2]; -#if __CL_HAS_ANON_STRUCT__ - __CL_ANON_STRUCT__ struct{ cl_int x, y; }; - __CL_ANON_STRUCT__ struct{ cl_int s0, s1; }; - __CL_ANON_STRUCT__ struct{ cl_int lo, hi; }; -#endif -#if defined( __CL_INT2__) - __cl_int2 v2; -#endif -}cl_int2; - -typedef union -{ - cl_int CL_ALIGNED(16) s[4]; -#if __CL_HAS_ANON_STRUCT__ - __CL_ANON_STRUCT__ struct{ cl_int x, y, z, w; }; - __CL_ANON_STRUCT__ struct{ cl_int s0, s1, s2, s3; }; - __CL_ANON_STRUCT__ struct{ cl_int2 lo, hi; }; -#endif -#if defined( __CL_INT2__) - __cl_int2 v2[2]; -#endif -#if defined( __CL_INT4__) - __cl_int4 v4; -#endif -}cl_int4; - -/* cl_int3 is identical in size, alignment and behavior to cl_int4. See section 6.1.5. */ -typedef cl_int4 cl_int3; - -typedef union -{ - cl_int CL_ALIGNED(32) s[8]; -#if __CL_HAS_ANON_STRUCT__ - __CL_ANON_STRUCT__ struct{ cl_int x, y, z, w; }; - __CL_ANON_STRUCT__ struct{ cl_int s0, s1, s2, s3, s4, s5, s6, s7; }; - __CL_ANON_STRUCT__ struct{ cl_int4 lo, hi; }; -#endif -#if defined( __CL_INT2__) - __cl_int2 v2[4]; -#endif -#if defined( __CL_INT4__) - __cl_int4 v4[2]; -#endif -#if defined( __CL_INT8__ ) - __cl_int8 v8; -#endif -}cl_int8; - -typedef union -{ - cl_int CL_ALIGNED(64) s[16]; -#if __CL_HAS_ANON_STRUCT__ - __CL_ANON_STRUCT__ struct{ cl_int x, y, z, w, __spacer4, __spacer5, __spacer6, __spacer7, __spacer8, __spacer9, sa, sb, sc, sd, se, sf; }; - __CL_ANON_STRUCT__ struct{ cl_int s0, s1, s2, s3, s4, s5, s6, s7, s8, s9, sA, sB, sC, sD, sE, sF; }; - __CL_ANON_STRUCT__ struct{ cl_int8 lo, hi; }; -#endif -#if defined( __CL_INT2__) - __cl_int2 v2[8]; -#endif -#if defined( __CL_INT4__) - __cl_int4 v4[4]; -#endif -#if defined( __CL_INT8__ ) - __cl_int8 v8[2]; -#endif -#if defined( __CL_INT16__ ) - __cl_int16 v16; -#endif -}cl_int16; - - -/* ---- cl_uintn ---- */ -typedef union -{ - cl_uint CL_ALIGNED(8) s[2]; -#if __CL_HAS_ANON_STRUCT__ - __CL_ANON_STRUCT__ struct{ cl_uint x, y; }; - __CL_ANON_STRUCT__ struct{ cl_uint s0, s1; }; - __CL_ANON_STRUCT__ struct{ cl_uint lo, hi; }; -#endif -#if defined( __CL_UINT2__) - __cl_uint2 v2; -#endif -}cl_uint2; - -typedef union -{ - cl_uint CL_ALIGNED(16) s[4]; -#if __CL_HAS_ANON_STRUCT__ - __CL_ANON_STRUCT__ struct{ cl_uint x, y, z, w; }; - __CL_ANON_STRUCT__ struct{ cl_uint s0, s1, s2, s3; }; - __CL_ANON_STRUCT__ struct{ cl_uint2 lo, hi; }; -#endif -#if defined( __CL_UINT2__) - __cl_uint2 v2[2]; -#endif -#if defined( __CL_UINT4__) - __cl_uint4 v4; -#endif -}cl_uint4; - -/* cl_uint3 is identical in size, alignment and behavior to cl_uint4. See section 6.1.5. */ -typedef cl_uint4 cl_uint3; - -typedef union -{ - cl_uint CL_ALIGNED(32) s[8]; -#if __CL_HAS_ANON_STRUCT__ - __CL_ANON_STRUCT__ struct{ cl_uint x, y, z, w; }; - __CL_ANON_STRUCT__ struct{ cl_uint s0, s1, s2, s3, s4, s5, s6, s7; }; - __CL_ANON_STRUCT__ struct{ cl_uint4 lo, hi; }; -#endif -#if defined( __CL_UINT2__) - __cl_uint2 v2[4]; -#endif -#if defined( __CL_UINT4__) - __cl_uint4 v4[2]; -#endif -#if defined( __CL_UINT8__ ) - __cl_uint8 v8; -#endif -}cl_uint8; - -typedef union -{ - cl_uint CL_ALIGNED(64) s[16]; -#if __CL_HAS_ANON_STRUCT__ - __CL_ANON_STRUCT__ struct{ cl_uint x, y, z, w, __spacer4, __spacer5, __spacer6, __spacer7, __spacer8, __spacer9, sa, sb, sc, sd, se, sf; }; - __CL_ANON_STRUCT__ struct{ cl_uint s0, s1, s2, s3, s4, s5, s6, s7, s8, s9, sA, sB, sC, sD, sE, sF; }; - __CL_ANON_STRUCT__ struct{ cl_uint8 lo, hi; }; -#endif -#if defined( __CL_UINT2__) - __cl_uint2 v2[8]; -#endif -#if defined( __CL_UINT4__) - __cl_uint4 v4[4]; -#endif -#if defined( __CL_UINT8__ ) - __cl_uint8 v8[2]; -#endif -#if defined( __CL_UINT16__ ) - __cl_uint16 v16; -#endif -}cl_uint16; - -/* ---- cl_longn ---- */ -typedef union -{ - cl_long CL_ALIGNED(16) s[2]; -#if __CL_HAS_ANON_STRUCT__ - __CL_ANON_STRUCT__ struct{ cl_long x, y; }; - __CL_ANON_STRUCT__ struct{ cl_long s0, s1; }; - __CL_ANON_STRUCT__ struct{ cl_long lo, hi; }; -#endif -#if defined( __CL_LONG2__) - __cl_long2 v2; -#endif -}cl_long2; - -typedef union -{ - cl_long CL_ALIGNED(32) s[4]; -#if __CL_HAS_ANON_STRUCT__ - __CL_ANON_STRUCT__ struct{ cl_long x, y, z, w; }; - __CL_ANON_STRUCT__ struct{ cl_long s0, s1, s2, s3; }; - __CL_ANON_STRUCT__ struct{ cl_long2 lo, hi; }; -#endif -#if defined( __CL_LONG2__) - __cl_long2 v2[2]; -#endif -#if defined( __CL_LONG4__) - __cl_long4 v4; -#endif -}cl_long4; - -/* cl_long3 is identical in size, alignment and behavior to cl_long4. See section 6.1.5. */ -typedef cl_long4 cl_long3; - -typedef union -{ - cl_long CL_ALIGNED(64) s[8]; -#if __CL_HAS_ANON_STRUCT__ - __CL_ANON_STRUCT__ struct{ cl_long x, y, z, w; }; - __CL_ANON_STRUCT__ struct{ cl_long s0, s1, s2, s3, s4, s5, s6, s7; }; - __CL_ANON_STRUCT__ struct{ cl_long4 lo, hi; }; -#endif -#if defined( __CL_LONG2__) - __cl_long2 v2[4]; -#endif -#if defined( __CL_LONG4__) - __cl_long4 v4[2]; -#endif -#if defined( __CL_LONG8__ ) - __cl_long8 v8; -#endif -}cl_long8; - -typedef union -{ - cl_long CL_ALIGNED(128) s[16]; -#if __CL_HAS_ANON_STRUCT__ - __CL_ANON_STRUCT__ struct{ cl_long x, y, z, w, __spacer4, __spacer5, __spacer6, __spacer7, __spacer8, __spacer9, sa, sb, sc, sd, se, sf; }; - __CL_ANON_STRUCT__ struct{ cl_long s0, s1, s2, s3, s4, s5, s6, s7, s8, s9, sA, sB, sC, sD, sE, sF; }; - __CL_ANON_STRUCT__ struct{ cl_long8 lo, hi; }; -#endif -#if defined( __CL_LONG2__) - __cl_long2 v2[8]; -#endif -#if defined( __CL_LONG4__) - __cl_long4 v4[4]; -#endif -#if defined( __CL_LONG8__ ) - __cl_long8 v8[2]; -#endif -#if defined( __CL_LONG16__ ) - __cl_long16 v16; -#endif -}cl_long16; - - -/* ---- cl_ulongn ---- */ -typedef union -{ - cl_ulong CL_ALIGNED(16) s[2]; -#if __CL_HAS_ANON_STRUCT__ - __CL_ANON_STRUCT__ struct{ cl_ulong x, y; }; - __CL_ANON_STRUCT__ struct{ cl_ulong s0, s1; }; - __CL_ANON_STRUCT__ struct{ cl_ulong lo, hi; }; -#endif -#if defined( __CL_ULONG2__) - __cl_ulong2 v2; -#endif -}cl_ulong2; - -typedef union -{ - cl_ulong CL_ALIGNED(32) s[4]; -#if __CL_HAS_ANON_STRUCT__ - __CL_ANON_STRUCT__ struct{ cl_ulong x, y, z, w; }; - __CL_ANON_STRUCT__ struct{ cl_ulong s0, s1, s2, s3; }; - __CL_ANON_STRUCT__ struct{ cl_ulong2 lo, hi; }; -#endif -#if defined( __CL_ULONG2__) - __cl_ulong2 v2[2]; -#endif -#if defined( __CL_ULONG4__) - __cl_ulong4 v4; -#endif -}cl_ulong4; - -/* cl_ulong3 is identical in size, alignment and behavior to cl_ulong4. See section 6.1.5. */ -typedef cl_ulong4 cl_ulong3; - -typedef union -{ - cl_ulong CL_ALIGNED(64) s[8]; -#if __CL_HAS_ANON_STRUCT__ - __CL_ANON_STRUCT__ struct{ cl_ulong x, y, z, w; }; - __CL_ANON_STRUCT__ struct{ cl_ulong s0, s1, s2, s3, s4, s5, s6, s7; }; - __CL_ANON_STRUCT__ struct{ cl_ulong4 lo, hi; }; -#endif -#if defined( __CL_ULONG2__) - __cl_ulong2 v2[4]; -#endif -#if defined( __CL_ULONG4__) - __cl_ulong4 v4[2]; -#endif -#if defined( __CL_ULONG8__ ) - __cl_ulong8 v8; -#endif -}cl_ulong8; - -typedef union -{ - cl_ulong CL_ALIGNED(128) s[16]; -#if __CL_HAS_ANON_STRUCT__ - __CL_ANON_STRUCT__ struct{ cl_ulong x, y, z, w, __spacer4, __spacer5, __spacer6, __spacer7, __spacer8, __spacer9, sa, sb, sc, sd, se, sf; }; - __CL_ANON_STRUCT__ struct{ cl_ulong s0, s1, s2, s3, s4, s5, s6, s7, s8, s9, sA, sB, sC, sD, sE, sF; }; - __CL_ANON_STRUCT__ struct{ cl_ulong8 lo, hi; }; -#endif -#if defined( __CL_ULONG2__) - __cl_ulong2 v2[8]; -#endif -#if defined( __CL_ULONG4__) - __cl_ulong4 v4[4]; -#endif -#if defined( __CL_ULONG8__ ) - __cl_ulong8 v8[2]; -#endif -#if defined( __CL_ULONG16__ ) - __cl_ulong16 v16; -#endif -}cl_ulong16; - - -/* --- cl_floatn ---- */ - -typedef union -{ - cl_float CL_ALIGNED(8) s[2]; -#if __CL_HAS_ANON_STRUCT__ - __CL_ANON_STRUCT__ struct{ cl_float x, y; }; - __CL_ANON_STRUCT__ struct{ cl_float s0, s1; }; - __CL_ANON_STRUCT__ struct{ cl_float lo, hi; }; -#endif -#if defined( __CL_FLOAT2__) - __cl_float2 v2; -#endif -}cl_float2; - -typedef union -{ - cl_float CL_ALIGNED(16) s[4]; -#if __CL_HAS_ANON_STRUCT__ - __CL_ANON_STRUCT__ struct{ cl_float x, y, z, w; }; - __CL_ANON_STRUCT__ struct{ cl_float s0, s1, s2, s3; }; - __CL_ANON_STRUCT__ struct{ cl_float2 lo, hi; }; -#endif -#if defined( __CL_FLOAT2__) - __cl_float2 v2[2]; -#endif -#if defined( __CL_FLOAT4__) - __cl_float4 v4; -#endif -}cl_float4; - -/* cl_float3 is identical in size, alignment and behavior to cl_float4. See section 6.1.5. */ -typedef cl_float4 cl_float3; - -typedef union -{ - cl_float CL_ALIGNED(32) s[8]; -#if __CL_HAS_ANON_STRUCT__ - __CL_ANON_STRUCT__ struct{ cl_float x, y, z, w; }; - __CL_ANON_STRUCT__ struct{ cl_float s0, s1, s2, s3, s4, s5, s6, s7; }; - __CL_ANON_STRUCT__ struct{ cl_float4 lo, hi; }; -#endif -#if defined( __CL_FLOAT2__) - __cl_float2 v2[4]; -#endif -#if defined( __CL_FLOAT4__) - __cl_float4 v4[2]; -#endif -#if defined( __CL_FLOAT8__ ) - __cl_float8 v8; -#endif -}cl_float8; - -typedef union -{ - cl_float CL_ALIGNED(64) s[16]; -#if __CL_HAS_ANON_STRUCT__ - __CL_ANON_STRUCT__ struct{ cl_float x, y, z, w, __spacer4, __spacer5, __spacer6, __spacer7, __spacer8, __spacer9, sa, sb, sc, sd, se, sf; }; - __CL_ANON_STRUCT__ struct{ cl_float s0, s1, s2, s3, s4, s5, s6, s7, s8, s9, sA, sB, sC, sD, sE, sF; }; - __CL_ANON_STRUCT__ struct{ cl_float8 lo, hi; }; -#endif -#if defined( __CL_FLOAT2__) - __cl_float2 v2[8]; -#endif -#if defined( __CL_FLOAT4__) - __cl_float4 v4[4]; -#endif -#if defined( __CL_FLOAT8__ ) - __cl_float8 v8[2]; -#endif -#if defined( __CL_FLOAT16__ ) - __cl_float16 v16; -#endif -}cl_float16; - -/* --- cl_doublen ---- */ - -typedef union -{ - cl_double CL_ALIGNED(16) s[2]; -#if __CL_HAS_ANON_STRUCT__ - __CL_ANON_STRUCT__ struct{ cl_double x, y; }; - __CL_ANON_STRUCT__ struct{ cl_double s0, s1; }; - __CL_ANON_STRUCT__ struct{ cl_double lo, hi; }; -#endif -#if defined( __CL_DOUBLE2__) - __cl_double2 v2; -#endif -}cl_double2; - -typedef union -{ - cl_double CL_ALIGNED(32) s[4]; -#if __CL_HAS_ANON_STRUCT__ - __CL_ANON_STRUCT__ struct{ cl_double x, y, z, w; }; - __CL_ANON_STRUCT__ struct{ cl_double s0, s1, s2, s3; }; - __CL_ANON_STRUCT__ struct{ cl_double2 lo, hi; }; -#endif -#if defined( __CL_DOUBLE2__) - __cl_double2 v2[2]; -#endif -#if defined( __CL_DOUBLE4__) - __cl_double4 v4; -#endif -}cl_double4; - -/* cl_double3 is identical in size, alignment and behavior to cl_double4. See section 6.1.5. */ -typedef cl_double4 cl_double3; - -typedef union -{ - cl_double CL_ALIGNED(64) s[8]; -#if __CL_HAS_ANON_STRUCT__ - __CL_ANON_STRUCT__ struct{ cl_double x, y, z, w; }; - __CL_ANON_STRUCT__ struct{ cl_double s0, s1, s2, s3, s4, s5, s6, s7; }; - __CL_ANON_STRUCT__ struct{ cl_double4 lo, hi; }; -#endif -#if defined( __CL_DOUBLE2__) - __cl_double2 v2[4]; -#endif -#if defined( __CL_DOUBLE4__) - __cl_double4 v4[2]; -#endif -#if defined( __CL_DOUBLE8__ ) - __cl_double8 v8; -#endif -}cl_double8; - -typedef union -{ - cl_double CL_ALIGNED(128) s[16]; -#if __CL_HAS_ANON_STRUCT__ - __CL_ANON_STRUCT__ struct{ cl_double x, y, z, w, __spacer4, __spacer5, __spacer6, __spacer7, __spacer8, __spacer9, sa, sb, sc, sd, se, sf; }; - __CL_ANON_STRUCT__ struct{ cl_double s0, s1, s2, s3, s4, s5, s6, s7, s8, s9, sA, sB, sC, sD, sE, sF; }; - __CL_ANON_STRUCT__ struct{ cl_double8 lo, hi; }; -#endif -#if defined( __CL_DOUBLE2__) - __cl_double2 v2[8]; -#endif -#if defined( __CL_DOUBLE4__) - __cl_double4 v4[4]; -#endif -#if defined( __CL_DOUBLE8__ ) - __cl_double8 v8[2]; -#endif -#if defined( __CL_DOUBLE16__ ) - __cl_double16 v16; -#endif -}cl_double16; - -/* Macro to facilitate debugging - * Usage: - * Place CL_PROGRAM_STRING_DEBUG_INFO on the line before the first line of your source. - * The first line ends with: CL_PROGRAM_STRING_DEBUG_INFO \" - * Each line thereafter of OpenCL C source must end with: \n\ - * The last line ends in "; - * - * Example: - * - * const char *my_program = CL_PROGRAM_STRING_DEBUG_INFO "\ - * kernel void foo( int a, float * b ) \n\ - * { \n\ - * // my comment \n\ - * *b[ get_global_id(0)] = a; \n\ - * } \n\ - * "; - * - * This should correctly set up the line, (column) and file information for your source - * string so you can do source level debugging. - */ -#define __CL_STRINGIFY( _x ) # _x -#define _CL_STRINGIFY( _x ) __CL_STRINGIFY( _x ) -#define CL_PROGRAM_STRING_DEBUG_INFO "#line " _CL_STRINGIFY(__LINE__) " \"" __FILE__ "\" \n\n" - -#ifdef __cplusplus -} -#endif - -#undef __CL_HAS_ANON_STRUCT__ -#undef __CL_ANON_STRUCT__ -#if defined( _WIN32) && (_MSC_VER >= 1500) -#pragma warning( pop ) -#endif - -#endif /* __CL_PLATFORM_H */ diff --git a/third_party/opencl/include/CL/opencl.h b/third_party/opencl/include/CL/opencl.h deleted file mode 100644 index 9855cd75e7..0000000000 --- a/third_party/opencl/include/CL/opencl.h +++ /dev/null @@ -1,59 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2008-2015 The Khronos Group Inc. - * - * Permission is hereby granted, free of charge, to any person obtaining a - * copy of this software and/or associated documentation files (the - * "Materials"), to deal in the Materials without restriction, including - * without limitation the rights to use, copy, modify, merge, publish, - * distribute, sublicense, and/or sell copies of the Materials, and to - * permit persons to whom the Materials are furnished to do so, subject to - * the following conditions: - * - * The above copyright notice and this permission notice shall be included - * in all copies or substantial portions of the Materials. - * - * MODIFICATIONS TO THIS FILE MAY MEAN IT NO LONGER ACCURATELY REFLECTS - * KHRONOS STANDARDS. THE UNMODIFIED, NORMATIVE VERSIONS OF KHRONOS - * SPECIFICATIONS AND HEADER INFORMATION ARE LOCATED AT - * https://www.khronos.org/registry/ - * - * THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. - * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY - * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, - * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE - * MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS. - ******************************************************************************/ - -/* $Revision: 11708 $ on $Date: 2010-06-13 23:36:24 -0700 (Sun, 13 Jun 2010) $ */ - -#ifndef __OPENCL_H -#define __OPENCL_H - -#ifdef __cplusplus -extern "C" { -#endif - -#ifdef __APPLE__ - -#include -#include -#include -#include - -#else - -#include -#include -#include -#include - -#endif - -#ifdef __cplusplus -} -#endif - -#endif /* __OPENCL_H */ - diff --git a/tools/cabana/SConscript b/tools/cabana/SConscript index d4cfc67280..b79d046fca 100644 --- a/tools/cabana/SConscript +++ b/tools/cabana/SConscript @@ -57,11 +57,9 @@ base_frameworks = qt_env['FRAMEWORKS'] base_libs = [common, messaging, cereal, visionipc, 'm', 'ssl', 'crypto', 'pthread'] + qt_env["LIBS"] if arch == "Darwin": - base_frameworks.append('OpenCL') base_frameworks.append('QtCharts') base_frameworks.append('QtSerialBus') else: - base_libs.append('OpenCL') base_libs.append('Qt5Charts') base_libs.append('Qt5SerialBus') diff --git a/tools/install_ubuntu_dependencies.sh b/tools/install_ubuntu_dependencies.sh index 5c2131d4bf..f428e9972a 100755 --- a/tools/install_ubuntu_dependencies.sh +++ b/tools/install_ubuntu_dependencies.sh @@ -58,9 +58,6 @@ function install_ubuntu_common_requirements() { libzmq3-dev \ libzstd-dev \ libsqlite3-dev \ - opencl-headers \ - ocl-icd-libopencl1 \ - ocl-icd-opencl-dev \ portaudio19-dev \ qttools5-dev-tools \ libqt5svg5-dev \ diff --git a/tools/replay/SConscript b/tools/replay/SConscript index 136c4119f6..698ab9885d 100644 --- a/tools/replay/SConscript +++ b/tools/replay/SConscript @@ -6,11 +6,6 @@ replay_env['CCFLAGS'] += ['-Wno-deprecated-declarations'] base_frameworks = [] base_libs = [common, messaging, cereal, visionipc, 'm', 'ssl', 'crypto', 'pthread'] -if arch == "Darwin": - base_frameworks.append('OpenCL') -else: - base_libs.append('OpenCL') - replay_lib_src = ["replay.cc", "consoleui.cc", "camera.cc", "filereader.cc", "logreader.cc", "framereader.cc", "route.cc", "util.cc", "seg_mgr.cc", "timeline.cc", "api.cc"] if arch != "Darwin": diff --git a/tools/webcam/README.md b/tools/webcam/README.md index 67ad2cc8cb..6abbc47935 100644 --- a/tools/webcam/README.md +++ b/tools/webcam/README.md @@ -10,10 +10,6 @@ What's needed: ## Setup openpilot - Follow [this readme](../README.md) to install and build the requirements -- Install OpenCL Driver (Ubuntu) -``` -sudo apt install pocl-opencl-icd -``` ## Connect the hardware - Connect the camera first From 7665045fc6737ed37e7a18528fedf8fda7eb0d03 Mon Sep 17 00:00:00 2001 From: David <49467229+TheSecurityDev@users.noreply.github.com> Date: Thu, 12 Feb 2026 12:46:34 -0600 Subject: [PATCH 011/311] ui replay: fix coverage reporting to include imports (#37180) Fix coverage reporting by adjusting MiciMainLayout import --- selfdrive/ui/tests/diff/replay.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/selfdrive/ui/tests/diff/replay.py b/selfdrive/ui/tests/diff/replay.py index 9da157660e..fdba5c3502 100755 --- a/selfdrive/ui/tests/diff/replay.py +++ b/selfdrive/ui/tests/diff/replay.py @@ -16,7 +16,6 @@ from openpilot.common.params import Params from openpilot.system.version import terms_version, training_version from openpilot.system.ui.lib.application import gui_app, MousePos, MouseEvent from openpilot.selfdrive.ui.ui_state import ui_state -from openpilot.selfdrive.ui.mici.layouts.main import MiciMainLayout FPS = 60 HEADLESS = os.getenv("WINDOWED", "0") == "1" @@ -85,6 +84,9 @@ def run_replay(): if not HEADLESS: rl.set_config_flags(rl.FLAG_WINDOW_HIDDEN) gui_app.init_window("ui diff test", fps=FPS) + + from openpilot.selfdrive.ui.mici.layouts.main import MiciMainLayout # import here for coverage + main_layout = MiciMainLayout() main_layout.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) @@ -117,7 +119,6 @@ def main(): cov = coverage.coverage(source=['openpilot.selfdrive.ui.mici']) with cov.collect(): run_replay() - cov.stop() cov.save() cov.report() cov.html_report(directory=os.path.join(DIFF_OUT_DIR, 'htmlcov')) From 2b7f91d151ce1cd86b3c46e44adde022d43affa0 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 12 Feb 2026 13:40:00 -0800 Subject: [PATCH 012/311] WifiManager: update networks on active (#37186) * immediately * only if active --- system/ui/lib/wifi_manager.py | 78 ++++++++++++++++++++--------------- 1 file changed, 44 insertions(+), 34 deletions(-) diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index 3d4da444be..86fcdb79fb 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -232,6 +232,10 @@ class WifiManager: def set_active(self, active: bool): self._active = active + # Update networks immediately when activating for UI + if active: + self._update_networks(block=False) + def _monitor_state(self): # Filter for signals rules = ( @@ -607,53 +611,59 @@ class WifiManager: if reply.header.message_type == MessageType.error: cloudlog.warning(f"Failed to request scan: {reply}") - def _update_networks(self): + def _update_networks(self, block: bool = True): if not self._active: return - with self._lock: - if self._wifi_device is None: - cloudlog.warning("No WiFi device found") - return + def worker(): + with self._lock: + if self._wifi_device is None: + cloudlog.warning("No WiFi device found") + return - # NOTE: AccessPoints property may exclude hidden APs (use GetAllAccessPoints method if needed) - wifi_addr = DBusAddress(self._wifi_device, NM, interface=NM_WIRELESS_IFACE) - wifi_props = self._router_main.send_and_get_reply(Properties(wifi_addr).get_all()).body[0] - active_ap_path = wifi_props.get('ActiveAccessPoint', ('o', '/'))[1] - ap_paths = wifi_props.get('AccessPoints', ('ao', []))[1] + # NOTE: AccessPoints property may exclude hidden APs (use GetAllAccessPoints method if needed) + wifi_addr = DBusAddress(self._wifi_device, NM, interface=NM_WIRELESS_IFACE) + wifi_props = self._router_main.send_and_get_reply(Properties(wifi_addr).get_all()).body[0] + active_ap_path = wifi_props.get('ActiveAccessPoint', ('o', '/'))[1] + ap_paths = wifi_props.get('AccessPoints', ('ao', []))[1] - aps: dict[str, list[AccessPoint]] = {} + aps: dict[str, list[AccessPoint]] = {} - for ap_path in ap_paths: - ap_addr = DBusAddress(ap_path, NM, interface=NM_ACCESS_POINT_IFACE) - ap_props = self._router_main.send_and_get_reply(Properties(ap_addr).get_all()) + for ap_path in ap_paths: + ap_addr = DBusAddress(ap_path, NM, interface=NM_ACCESS_POINT_IFACE) + ap_props = self._router_main.send_and_get_reply(Properties(ap_addr).get_all()) - # some APs have been seen dropping off during iteration - if ap_props.header.message_type == MessageType.error: - cloudlog.warning(f"Failed to get AP properties for {ap_path}") - continue - - try: - ap = AccessPoint.from_dbus(ap_props.body[0], ap_path, active_ap_path) - if ap.ssid == "": + # some APs have been seen dropping off during iteration + if ap_props.header.message_type == MessageType.error: + cloudlog.warning(f"Failed to get AP properties for {ap_path}") continue - if ap.ssid not in aps: - aps[ap.ssid] = [] + try: + ap = AccessPoint.from_dbus(ap_props.body[0], ap_path, active_ap_path) + if ap.ssid == "": + continue - aps[ap.ssid].append(ap) - except Exception: - # catch all for parsing errors - cloudlog.exception(f"Failed to parse AP properties for {ap_path}") + if ap.ssid not in aps: + aps[ap.ssid] = [] - networks = [Network.from_dbus(ssid, ap_list, ssid in self._connections) for ssid, ap_list in aps.items()] - # sort with quantized strength to reduce jumping - networks.sort(key=lambda n: (-n.is_connected, -n.is_saved, -round(n.strength / 100 * 2), n.ssid.lower())) - self._networks = networks + aps[ap.ssid].append(ap) + except Exception: + # catch all for parsing errors + cloudlog.exception(f"Failed to parse AP properties for {ap_path}") - self._update_active_connection_info() + networks = [Network.from_dbus(ssid, ap_list, ssid in self._connections) for ssid, ap_list in aps.items()] + # sort with quantized strength to reduce jumping + networks.sort(key=lambda n: (-n.is_connected, -n.is_saved, -round(n.strength / 100 * 2), n.ssid.lower())) + self._networks = networks - self._enqueue_callbacks(self._networks_updated, self._networks) + self._update_active_connection_info() + + self._enqueue_callbacks(self._networks_updated, self._networks) + + if block: + worker() + else: + threading.Thread(target=worker, daemon=True).start() def _update_active_connection_info(self): self._ipv4_address = "" From 2e9b980fc216d434790004a9c832794d928eef59 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 12 Feb 2026 13:48:55 -0800 Subject: [PATCH 013/311] remove lang_button --- selfdrive/ui/mici/layouts/settings/device.py | 1 - 1 file changed, 1 deletion(-) diff --git a/selfdrive/ui/mici/layouts/settings/device.py b/selfdrive/ui/mici/layouts/settings/device.py index a47231340a..1346267465 100644 --- a/selfdrive/ui/mici/layouts/settings/device.py +++ b/selfdrive/ui/mici/layouts/settings/device.py @@ -322,7 +322,6 @@ class DeviceLayoutMici(NavWidget): PairBigButton(), review_training_guide_btn, driver_cam_btn, - # lang_button, reset_calibration_btn, uninstall_openpilot_btn, regulatory_btn, From 1257d31a5602a6edd6c6cd82e05ceee705272720 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 12 Feb 2026 15:00:50 -0800 Subject: [PATCH 014/311] WifiManager: dbus debug flag (#37188) * add dbus debug wrapper * no --- system/ui/lib/wifi_manager.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index 86fcdb79fb..b2f0774228 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -37,6 +37,23 @@ DEFAULT_TETHERING_PASSWORD = "swagswagcomma" SIGNAL_QUEUE_SIZE = 10 SCAN_PERIOD_SECONDS = 5 +DEBUG = False +_dbus_call_idx = 0 + + +def _wrap_router(router): + def _wrap(orig): + def wrapper(msg, **kw): + global _dbus_call_idx + _dbus_call_idx += 1 + if DEBUG: + h = msg.header.fields + print(f"[DBUS #{_dbus_call_idx}] {h.get(6, '?')} {h.get(3, '?')} {msg.body}") + return orig(msg, **kw) + return wrapper + router.send_and_get_reply = _wrap(router.send_and_get_reply) + router.send = _wrap(router.send) + class SecurityType(IntEnum): OPEN = 0 @@ -134,6 +151,7 @@ class WifiManager: # DBus connections try: self._router_main = DBusRouter(open_dbus_connection_threading(bus="SYSTEM")) # used by scanner / general method calls + _wrap_router(self._router_main) self._conn_monitor = open_dbus_connection_blocking(bus="SYSTEM") # used by state monitor thread self._nm = DBusAddress(NM_PATH, bus_name=NM, interface=NM_IFACE) except FileNotFoundError: From eb5cd542d9dbe2031e70142b94025a4bcfb00a91 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 12 Feb 2026 16:23:58 -0800 Subject: [PATCH 015/311] WifiUi: add new networks to end, delete buttons on exit (#37189) * add networks to end, remove bad scroller restore logic that sometimes starts in the middle * works * almost * wifi slash * clean up * clean up * opactiy * more clean up * more clean up * set enabled and network missing on regain network * cmt --- .../mici/layouts/settings/network/wifi_ui.py | 79 ++++++++++--------- 1 file changed, 42 insertions(+), 37 deletions(-) diff --git a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py index 955dc4bfaf..2b5c6be6e1 100644 --- a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py +++ b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py @@ -36,20 +36,29 @@ class WifiIcon(Widget): super().__init__() self.set_rect(rl.Rectangle(0, 0, 86, 64)) + self._wifi_slash_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_slash.png", 86, 64) self._wifi_low_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_low.png", 86, 64) self._wifi_medium_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_medium.png", 86, 64) self._wifi_full_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_full.png", 86, 64) self._lock_txt = gui_app.texture("icons_mici/settings/network/new/lock.png", 22, 32) self._network: Network | None = None + self._network_missing = False # if network disappeared from scan results self._scale = 1.0 + self._opacity = 1.0 def set_current_network(self, network: Network): self._network = network + def set_network_missing(self, missing: bool): + self._network_missing = missing + def set_scale(self, scale: float): self._scale = scale + def set_opacity(self, opacity: float): + self._opacity = opacity + @staticmethod def get_strength_icon_idx(strength: int) -> int: return round(strength / 100 * 2) @@ -60,23 +69,26 @@ class WifiIcon(Widget): # Determine which wifi strength icon to use strength = self.get_strength_icon_idx(self._network.strength) - if strength == 2: + if self._network_missing: + strength_icon = self._wifi_slash_txt + elif strength == 2: strength_icon = self._wifi_full_txt elif strength == 1: strength_icon = self._wifi_medium_txt else: strength_icon = self._wifi_low_txt + tint = rl.Color(255, 255, 255, int(255 * self._opacity)) icon_x = int(self._rect.x + (self._rect.width - strength_icon.width * self._scale) // 2) icon_y = int(self._rect.y + (self._rect.height - strength_icon.height * self._scale) // 2) - rl.draw_texture_ex(strength_icon, (icon_x, icon_y), 0.0, self._scale, rl.WHITE) + rl.draw_texture_ex(strength_icon, (icon_x, icon_y), 0.0, self._scale, tint) # Render lock icon at lower right of wifi icon if secured if self._network.security_type not in (SecurityType.OPEN, SecurityType.UNSUPPORTED): lock_scale = self._scale * 1.1 lock_x = int(icon_x + 1 + strength_icon.width * self._scale - self._lock_txt.width * lock_scale / 2) lock_y = int(icon_y + 1 + strength_icon.height * self._scale - self._lock_txt.height * lock_scale / 2) - rl.draw_texture_ex(self._lock_txt, (lock_x, lock_y), 0.0, lock_scale, rl.WHITE) + rl.draw_texture_ex(self._lock_txt, (lock_x, lock_y), 0.0, lock_scale, tint) class WifiItem(BigDialogOptionButton): @@ -93,16 +105,26 @@ class WifiItem(BigDialogOptionButton): self._wifi_icon = WifiIcon() self._wifi_icon.set_current_network(network) + def set_network_missing(self, missing: bool): + self._wifi_icon.set_network_missing(missing) + def set_current_network(self, network: Network): self._network = network self._wifi_icon.set_current_network(network) + # reset if we see the network again + self.set_enabled(True) + self.set_network_missing(False) + def _render(self, _): + disabled_alpha = 0.35 if not self.enabled else 1.0 + if self._network.is_connected: selected_x = int(self._rect.x - self._selected_txt.width / 2) selected_y = int(self._rect.y + (self._rect.height - self._selected_txt.height) / 2) rl.draw_texture(self._selected_txt, selected_x, selected_y, rl.WHITE) + self._wifi_icon.set_opacity(disabled_alpha) self._wifi_icon.set_scale((1.0 if self._selected else 0.65) * 0.7) self._wifi_icon.render(rl.Rectangle( self._rect.x + self.LEFT_MARGIN, @@ -113,11 +135,11 @@ class WifiItem(BigDialogOptionButton): if self._selected: self._label.set_font_size(self.SELECTED_HEIGHT) - self._label.set_color(rl.Color(255, 255, 255, int(255 * 0.9))) + self._label.set_color(rl.Color(255, 255, 255, int(255 * 0.9 * disabled_alpha))) self._label.set_font_weight(FontWeight.DISPLAY) else: self._label.set_font_size(self.HEIGHT) - self._label.set_color(rl.Color(255, 255, 255, int(255 * 0.58))) + self._label.set_color(rl.Color(255, 255, 255, int(255 * 0.58 * disabled_alpha))) self._label.set_font_weight(FontWeight.DISPLAY_REGULAR) label_offset = self.LEFT_MARGIN + self._wifi_icon.rect.width + 20 @@ -314,9 +336,6 @@ 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) @@ -332,10 +351,6 @@ class WifiUIMici(BigMultiOptionDialog): self._connecting: str | None = None self._networks: dict[str, Network] = {} - # widget state - self._last_interaction_time = -float('inf') - self._restore_selection = False - self._wifi_manager.add_callbacks( need_auth=self._on_need_auth, activated=self._on_activated, @@ -348,11 +363,12 @@ 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() self._wifi_manager.set_active(False) + # clear scroller items to remove old networks on next show + self._scroller._items.clear() def _open_network_manage_page(self, result=None): self._network_info_page.update_networks(self._networks) @@ -372,27 +388,28 @@ class WifiUIMici(BigMultiOptionDialog): self._network_info_page.update_networks(self._networks) def _update_buttons(self): - # Don't update buttons while user is actively interacting - if rl.get_time() - self._last_interaction_time < self.INACTIVITY_TIMEOUT: - return + # Only add new buttons to the end. Update existing buttons without re-sorting so user can freely scroll around 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) if network_button_idx is not None: - network_button = self._scroller._items.pop(network_button_idx) # Update network on existing button - network_button.set_current_network(network) + self._scroller._items[network_button_idx].set_current_network(network) else: network_button = WifiItem(network) + self._scroller.add_widget(network_button) - self._scroller.add_widget(network_button) + # Move connected network to the start + connected_btn_idx = next((i for i, btn in enumerate(self._scroller._items) if btn._network.is_connected), None) + if connected_btn_idx is not None and connected_btn_idx > 0: + self._scroller._items.insert(0, self._scroller._items.pop(connected_btn_idx)) + self._scroller._layout() # fixes selected style single frame stutter - # 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 + # Disable networks no longer present + for btn in self._scroller._items: + if btn.option not in self._networks: + btn.set_enabled(False) + btn.set_network_missing(True) def _connect_with_password(self, ssid: str, password: str): if password: @@ -440,19 +457,7 @@ 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, _): - # 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) - self._restore_selection = None - super()._render(_) if not self._networks: From f142f1cd706c454770cab7db73686a8072cc179c Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 12 Feb 2026 16:24:39 -0800 Subject: [PATCH 016/311] scroller: move scissor to render --- system/ui/widgets/scroller.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/system/ui/widgets/scroller.py b/system/ui/widgets/scroller.py index 03c49fca65..43539d128b 100644 --- a/system/ui/widgets/scroller.py +++ b/system/ui/widgets/scroller.py @@ -223,9 +223,6 @@ class Scroller(Widget): 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(self._scroll_offset) cur_pos = 0 @@ -267,6 +264,9 @@ class Scroller(Widget): item.set_parent_rect(self._rect) def _render(self, _): + rl.begin_scissor_mode(int(self._rect.x), int(self._rect.y), + int(self._rect.width), int(self._rect.height)) + for item in self._visible_items: # Skip rendering if not in viewport if not rl.check_collision_recs(item.rect, self._rect): From 0fa8e01d1ffbe31a6a90426b3cb2dc3c27e2390b Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 12 Feb 2026 18:39:08 -0800 Subject: [PATCH 017/311] Wifi ui: render scroller gradient under (#37193) * gradient under * blend mode works * Revert "blend mode works" This reverts commit 092924fbd6dc40cbb937cac8578257ba5a28a7ef. * everywehre * everywehre --- selfdrive/assets/icons_mici/buttons/button_rectangle.png | 4 ++-- .../assets/icons_mici/buttons/button_rectangle_disabled.png | 4 ++-- .../assets/icons_mici/buttons/button_rectangle_hover.png | 3 --- .../assets/icons_mici/buttons/button_rectangle_pressed.png | 4 ++-- selfdrive/ui/mici/widgets/button.py | 5 ++--- 5 files changed, 8 insertions(+), 12 deletions(-) delete mode 100644 selfdrive/assets/icons_mici/buttons/button_rectangle_hover.png diff --git a/selfdrive/assets/icons_mici/buttons/button_rectangle.png b/selfdrive/assets/icons_mici/buttons/button_rectangle.png index 230c537d6d..4ccf6995b1 100644 --- a/selfdrive/assets/icons_mici/buttons/button_rectangle.png +++ b/selfdrive/assets/icons_mici/buttons/button_rectangle.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ffb293236f5f8f7da44b5a3c4c0b72e86c4e1fdb04f89c94507af008ff7de139 -size 8210 +oid sha256:5dedb4139a7ddeafcdaf050144769e490643820db726201a15250e1042eb6d15 +size 7982 diff --git a/selfdrive/assets/icons_mici/buttons/button_rectangle_disabled.png b/selfdrive/assets/icons_mici/buttons/button_rectangle_disabled.png index 76e75d5421..5e891588f5 100644 --- a/selfdrive/assets/icons_mici/buttons/button_rectangle_disabled.png +++ b/selfdrive/assets/icons_mici/buttons/button_rectangle_disabled.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bda53863c9a46c50a1e2920a76c2d2f1fe4df8a94b8d2e26f5d83eef3a9c3bd3 -size 3627 +oid sha256:d527dcff61fa66902681706b4916586244b8cf0520086ac980ff782ab2d99ce7 +size 4778 diff --git a/selfdrive/assets/icons_mici/buttons/button_rectangle_hover.png b/selfdrive/assets/icons_mici/buttons/button_rectangle_hover.png deleted file mode 100644 index a9fd28cc35..0000000000 --- a/selfdrive/assets/icons_mici/buttons/button_rectangle_hover.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6b55e43c50e805ac5e8357e5943374ed02d756cefa3aaffb58c568a0b125c30b -size 7750 diff --git a/selfdrive/assets/icons_mici/buttons/button_rectangle_pressed.png b/selfdrive/assets/icons_mici/buttons/button_rectangle_pressed.png index 779c219fcb..2cbf1cedb8 100644 --- a/selfdrive/assets/icons_mici/buttons/button_rectangle_pressed.png +++ b/selfdrive/assets/icons_mici/buttons/button_rectangle_pressed.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5528e9c041b824f005bf1ef6e49b2dbbc4ba10f994b0726d2a17a4fbf8c80f55 -size 21379 +oid sha256:c6b1b0f1270a596b5ac150dee8ade54794de55b2033a529d4a17176f688aa6f0 +size 56738 diff --git a/selfdrive/ui/mici/widgets/button.py b/selfdrive/ui/mici/widgets/button.py index 29844ccda1..36b1922385 100644 --- a/selfdrive/ui/mici/widgets/button.py +++ b/selfdrive/ui/mici/widgets/button.py @@ -143,7 +143,6 @@ class BigButton(Widget): self._txt_default_bg = gui_app.texture("icons_mici/buttons/button_rectangle.png", 402, 180) self._txt_pressed_bg = gui_app.texture("icons_mici/buttons/button_rectangle_pressed.png", 402, 180) self._txt_disabled_bg = gui_app.texture("icons_mici/buttons/button_rectangle_disabled.png", 402, 180) - self._txt_hover_bg = gui_app.texture("icons_mici/buttons/button_rectangle_hover.png", 402, 180) def _width_hint(self) -> int: # Single line if scrolling, so hide behind icon if exists @@ -215,14 +214,14 @@ class BigButton(Widget): if not self.enabled: txt_bg = self._txt_disabled_bg elif self.is_pressed: - txt_bg = self._txt_hover_bg + txt_bg = self._txt_pressed_bg scale = self._scale_filter.update(PRESSED_SCALE if self.is_pressed else 1.0) btn_x = self._rect.x + (self._rect.width * (1 - scale)) / 2 btn_y = self._rect.y + (self._rect.height * (1 - scale)) / 2 - rl.draw_texture_ex(txt_bg, (btn_x, btn_y), 0, scale, rl.WHITE) self._draw_content(btn_y) + rl.draw_texture_ex(txt_bg, (btn_x, btn_y), 0, scale, rl.WHITE) class BigToggle(BigButton): From 98bc70344f0b2a286f001dd1a0771620af6577e4 Mon Sep 17 00:00:00 2001 From: David <49467229+TheSecurityDev@users.noreply.github.com> Date: Thu, 12 Feb 2026 21:19:25 -0600 Subject: [PATCH 018/311] fix: use correct display ID for WSL2 when setting up Xvfb (#36697) use correct display ID for wsl --- selfdrive/test/setup_xvfb.sh | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/selfdrive/test/setup_xvfb.sh b/selfdrive/test/setup_xvfb.sh index 692b84d65f..b185e2b431 100755 --- a/selfdrive/test/setup_xvfb.sh +++ b/selfdrive/test/setup_xvfb.sh @@ -2,7 +2,11 @@ # Sets up a virtual display for running map renderer and simulator without an X11 display -DISP_ID=99 +if uname -r | grep -q "WSL2"; then + DISP_ID=0 # WSLg uses display :0 +else + DISP_ID=99 # Standard Xvfb display +fi export DISPLAY=:$DISP_ID sudo Xvfb $DISPLAY -screen 0 2160x1080x24 2>/dev/null & From 132f10365a2a15e08fce7badb010af5cd67fdcce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Harald=20Sch=C3=A4fer?= Date: Thu, 12 Feb 2026 19:52:22 -0800 Subject: [PATCH 019/311] relax dm timing tgwarp (#37191) --- selfdrive/test/process_replay/model_replay.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selfdrive/test/process_replay/model_replay.py b/selfdrive/test/process_replay/model_replay.py index 3f671f610d..a6ccaa1047 100755 --- a/selfdrive/test/process_replay/model_replay.py +++ b/selfdrive/test/process_replay/model_replay.py @@ -35,7 +35,7 @@ GITHUB = GithubUtils(API_TOKEN, DATA_TOKEN) EXEC_TIMINGS = [ # model, instant max, average max ("modelV2", 0.05, 0.028), - ("driverStateV2", 0.05, 0.015), + ("driverStateV2", 0.05, 0.016), ] def get_log_fn(test_route, ref="master"): From 2e21deeae83384e35cbacbf55e3624425dc5ddd6 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 12 Feb 2026 20:48:34 -0800 Subject: [PATCH 020/311] WifiUi: fix up wrong password dialog (#37195) * debug why so slow * forget after * i'm not sure why this is a thing * better forget connecting reset * ???? * has lag * fix * clean up * should be fine --- .../ui/mici/layouts/settings/network/wifi_ui.py | 15 +++++++++++---- system/ui/lib/wifi_manager.py | 10 +++++----- system/ui/widgets/network.py | 2 +- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py index 2b5c6be6e1..5c9616524c 100644 --- a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py +++ b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py @@ -445,14 +445,21 @@ class WifiUIMici(BigMultiOptionDialog): hint = "wrong password..." if incorrect_password else "enter password..." dlg = BigInputDialog(hint, "", minimum_length=8, confirm_callback=lambda _password: self._connect_with_password(ssid, _password)) - # go back to the manage network page - gui_app.set_modal_overlay(dlg, self._open_network_manage_page) + + def on_close(result=None): + gui_app.set_modal_overlay_tick(None) + self._open_network_manage_page(result) + + # Process wifi callbacks while the keyboard is shown so forgotten clears connecting state + gui_app.set_modal_overlay_tick(self._wifi_manager.process_callbacks) + gui_app.set_modal_overlay(dlg, on_close) def _on_activated(self): self._connecting = None - def _on_forgotten(self): - self._connecting = None + def _on_forgotten(self, ssid): + if self._connecting == ssid: + self._connecting = None def _on_disconnected(self): self._connecting = None diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index b2f0774228..72373ecbea 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -183,7 +183,7 @@ class WifiManager: # Callbacks self._need_auth: list[Callable[[str], None]] = [] self._activated: list[Callable[[], None]] = [] - self._forgotten: list[Callable[[], None]] = [] + self._forgotten: list[Callable[[str], None]] = [] self._networks_updated: list[Callable[[list[Network]], None]] = [] self._disconnected: list[Callable[[], None]] = [] @@ -211,7 +211,7 @@ class WifiManager: def add_callbacks(self, need_auth: Callable[[str], None] | None = None, activated: Callable[[], None] | None = None, - forgotten: Callable[[], None] | None = None, + forgotten: Callable[[str], None] | None = None, networks_updated: Callable[[list[Network]], None] | None = None, disconnected: Callable[[], None] | None = None): if need_auth is not None: @@ -316,8 +316,8 @@ class WifiManager: # BAD PASSWORD if new_state == NMDeviceState.NEED_AUTH and change_reason == NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT and len(self._connecting_to_ssid): - self.forget_connection(self._connecting_to_ssid, block=True) self._enqueue_callbacks(self._need_auth, self._connecting_to_ssid) + self.forget_connection(self._connecting_to_ssid, block=True) self._connecting_to_ssid = "" elif new_state == NMDeviceState.ACTIVATED: @@ -327,8 +327,8 @@ class WifiManager: self._connecting_to_ssid = "" elif new_state == NMDeviceState.DISCONNECTED and change_reason != NM_DEVICE_STATE_REASON_NEW_ACTIVATION: + self._enqueue_callbacks(self._forgotten, self._connecting_to_ssid) self._connecting_to_ssid = "" - self._enqueue_callbacks(self._forgotten) def _network_scanner(self): while not self._exit: @@ -484,7 +484,7 @@ class WifiManager: if len(self._forgotten): self._update_networks() - self._enqueue_callbacks(self._forgotten) + self._enqueue_callbacks(self._forgotten, ssid) if block: worker() diff --git a/system/ui/widgets/network.py b/system/ui/widgets/network.py index 8f5168958f..9de44c585c 100644 --- a/system/ui/widgets/network.py +++ b/system/ui/widgets/network.py @@ -463,7 +463,7 @@ class WifiManagerUI(Widget): if self.state == UIState.CONNECTING: self.state = UIState.IDLE - def _on_forgotten(self): + def _on_forgotten(self, _): if self.state == UIState.FORGETTING: self.state = UIState.IDLE From a61badb564bba4f7524e6004e344cb33ba80877c Mon Sep 17 00:00:00 2001 From: felsager <76905857+felsager@users.noreply.github.com> Date: Thu, 12 Feb 2026 20:59:14 -0800 Subject: [PATCH 021/311] test_following_distance: bump error margin when initial speed is 0 (#37196) --- selfdrive/controls/tests/test_following_distance.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/selfdrive/controls/tests/test_following_distance.py b/selfdrive/controls/tests/test_following_distance.py index 8f66d89bf8..ad1ff1a189 100644 --- a/selfdrive/controls/tests/test_following_distance.py +++ b/selfdrive/controls/tests/test_following_distance.py @@ -42,4 +42,5 @@ class TestFollowingDistance: simulation_steady_state = run_following_distance_simulation(v_lead, e2e=self.e2e, personality=self.personality) correct_steady_state = desired_follow_distance(v_lead, v_lead, get_T_FOLLOW(self.personality)) err_ratio = 0.2 if self.e2e else 0.1 - assert simulation_steady_state == pytest.approx(correct_steady_state, abs=err_ratio * correct_steady_state + .5) + abs_err_margin = 0.5 if v_lead > 0.0 else 1.15 + assert simulation_steady_state == pytest.approx(correct_steady_state, abs=err_ratio * correct_steady_state + abs_err_margin) From 9b7bf4a101560b57da23c4000b9e7e89e0f4e222 Mon Sep 17 00:00:00 2001 From: David <49467229+TheSecurityDev@users.noreply.github.com> Date: Fri, 13 Feb 2026 11:26:14 -0600 Subject: [PATCH 022/311] mici ui replay: fix indeterminism with swiping and animations (#37110) * fix: get_time and get_frame_time determinism * remove some hackiness * don't need that --- selfdrive/ui/tests/diff/replay.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/selfdrive/ui/tests/diff/replay.py b/selfdrive/ui/tests/diff/replay.py index fdba5c3502..1efc6dde92 100755 --- a/selfdrive/ui/tests/diff/replay.py +++ b/selfdrive/ui/tests/diff/replay.py @@ -90,8 +90,11 @@ def run_replay(): main_layout = MiciMainLayout() main_layout.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) - frame = 0 script_index = 0 + frame = 0 + # Override raylib timing functions to return deterministic values based on frame count instead of real time + rl.get_frame_time = lambda: 1.0 / FPS + rl.get_time = lambda: frame / FPS for should_render in gui_app.render(): while script_index < len(SCRIPT) and SCRIPT[script_index][0] == frame: From 2ba6df2506a3b3e664131f67513b5bcd0c08075d Mon Sep 17 00:00:00 2001 From: YassineYousfi Date: Fri, 13 Feb 2026 10:14:24 -0800 Subject: [PATCH 023/311] chunk tinygrad pkl below GitHub max size - NoCache and AlwaysBuild (#37194) * nocache * + * fixes * lint * not split * use pathlib * cleanup * better * even better --- .gitignore | 1 + release/build_release.sh | 3 +-- selfdrive/modeld/SConscript | 23 +++++++++++++--- selfdrive/modeld/dmonitoringmodeld.py | 4 +-- selfdrive/modeld/external_pickle.py | 38 +++++++++++++++++++++++++++ selfdrive/modeld/modeld.py | 8 +++--- 6 files changed, 65 insertions(+), 12 deletions(-) create mode 100755 selfdrive/modeld/external_pickle.py diff --git a/.gitignore b/.gitignore index 1fef0a1255..af18f06628 100644 --- a/.gitignore +++ b/.gitignore @@ -65,6 +65,7 @@ cppcheck_report.txt comma*.sh selfdrive/modeld/models/*.pkl +selfdrive/modeld/models/*.pkl.* # openpilot log files *.bz2 diff --git a/release/build_release.sh b/release/build_release.sh index 220da05c17..7bc6732c68 100755 --- a/release/build_release.sh +++ b/release/build_release.sh @@ -72,8 +72,7 @@ find . -name '*.pyc' -delete find . -name 'moc_*' -delete find . -name '__pycache__' -delete rm -rf .sconsign.dblite Jenkinsfile release/ -rm selfdrive/modeld/models/driving_vision.onnx -rm selfdrive/modeld/models/driving_policy.onnx +rm -f selfdrive/modeld/models/*.onnx find third_party/ -name '*x86*' -exec rm -r {} + find third_party/ -name '*Darwin*' -exec rm -r {} + diff --git a/selfdrive/modeld/SConscript b/selfdrive/modeld/SConscript index b13a84f70a..1808cfec2f 100644 --- a/selfdrive/modeld/SConscript +++ b/selfdrive/modeld/SConscript @@ -3,6 +3,7 @@ import glob Import('env', 'arch') lenv = env.Clone() +CHUNK_BYTES = int(os.environ.get("TG_CHUNK_BYTES", str(45 * 1024 * 1024))) tinygrad_root = env.Dir("#").abspath tinygrad_files = ["#"+x for x in glob.glob(env.Dir("#tinygrad_repo").relpath + "/**", recursive=True, root_dir=tinygrad_root) @@ -36,12 +37,28 @@ lenv.Command(warp_targets, tinygrad_files + script_files, cmd) def tg_compile(flags, model_name): pythonpath_string = 'PYTHONPATH="${PYTHONPATH}:' + env.Dir("#tinygrad_repo").abspath + '"' fn = File(f"models/{model_name}").abspath - return lenv.Command( - fn + "_tinygrad.pkl", + + out = fn + "_tinygrad.pkl" + full = out + ".full" + parts = out + ".parts" + + full_node = lenv.Command( + full, [fn + ".onnx"] + tinygrad_files, - f'{pythonpath_string} {flags} {image_flag} python3 {Dir("#tinygrad_repo").abspath}/examples/openpilot/compile3.py {fn}.onnx {fn}_tinygrad.pkl' + f'{pythonpath_string} {flags} {image_flag} python3 {Dir("#tinygrad_repo").abspath}/examples/openpilot/compile3.py {fn}.onnx {full}' ) + split_script = File(Dir("#selfdrive/modeld").File("external_pickle.py").abspath) + parts_node = lenv.Command( + parts, + [full_node, split_script, Value(str(CHUNK_BYTES))], + [f'python3 {split_script.abspath} {full} {out} {CHUNK_BYTES}', Delete(full)], + ) + + lenv.NoCache(parts_node) + lenv.AlwaysBuild(parts_node) + return parts_node + # Compile small models for model_name in ['driving_vision', 'driving_policy', 'dmonitoring_model']: tg_compile(tg_flags, model_name) diff --git a/selfdrive/modeld/dmonitoringmodeld.py b/selfdrive/modeld/dmonitoringmodeld.py index 94871c1990..956ea8a6a2 100755 --- a/selfdrive/modeld/dmonitoringmodeld.py +++ b/selfdrive/modeld/dmonitoringmodeld.py @@ -17,6 +17,7 @@ from openpilot.common.transformations.model import dmonitoringmodel_intrinsics from openpilot.common.transformations.camera import _ar_ox_fisheye, _os_fisheye from openpilot.system.camerad.cameras.nv12_info import get_nv12_info from openpilot.selfdrive.modeld.parse_model_outputs import sigmoid, safe_exp +from openpilot.selfdrive.modeld.external_pickle import load_external_pickle PROCESS_NAME = "selfdrive.modeld.dmonitoringmodeld" SEND_RAW_PRED = os.getenv('SEND_RAW_PRED') @@ -44,8 +45,7 @@ class ModelState: self.tensor_inputs = {k: Tensor(v, device='NPY').realize() for k,v in self.numpy_inputs.items()} self._blob_cache : dict[int, Tensor] = {} self.image_warp = None - with open(MODEL_PKL_PATH, "rb") as f: - self.model_run = pickle.load(f) + self.model_run = load_external_pickle(MODEL_PKL_PATH) def run(self, buf: VisionBuf, calib: np.ndarray, transform: np.ndarray) -> tuple[np.ndarray, float]: self.numpy_inputs['calib'][0,:] = calib diff --git a/selfdrive/modeld/external_pickle.py b/selfdrive/modeld/external_pickle.py new file mode 100755 index 0000000000..d60a9632a6 --- /dev/null +++ b/selfdrive/modeld/external_pickle.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +import hashlib +import pickle +import sys +from pathlib import Path + +def split_pickle(full_path: Path, out_prefix: Path, chunk_bytes: int) -> None: + data = full_path.read_bytes() + out_dir = out_prefix.parent + + for p in out_dir.glob(f"{out_prefix.name}.data-*"): + p.unlink() + + total = (len(data) + chunk_bytes - 1) // chunk_bytes + names = [] + for i in range(0, len(data), chunk_bytes): + name = f"{out_prefix.name}.data-{(i // chunk_bytes) + 1:04d}-of-{total:04d}" + (out_dir / name).write_bytes(data[i:i + chunk_bytes]) + names.append(name) + + manifest = hashlib.sha256(data).hexdigest() + "\n" + "\n".join(names) + "\n" + (out_dir / (out_prefix.name + ".parts")).write_text(manifest) + +def load_external_pickle(prefix: Path): + parts = prefix.parent / (prefix.name + ".parts") + lines = parts.read_text().splitlines() + expected_hash, chunk_names = lines[0], lines[1:] + + data = bytearray() + for name in chunk_names: + data += (prefix.parent / name).read_bytes() + + if hashlib.sha256(data).hexdigest() != expected_hash: + raise RuntimeError(f"hash mismatch loading {prefix}") + return pickle.loads(data) + +if __name__ == "__main__": + split_pickle(Path(sys.argv[1]), Path(sys.argv[2]), int(sys.argv[3])) diff --git a/selfdrive/modeld/modeld.py b/selfdrive/modeld/modeld.py index df000979e8..a8b633bf02 100755 --- a/selfdrive/modeld/modeld.py +++ b/selfdrive/modeld/modeld.py @@ -28,6 +28,7 @@ from openpilot.selfdrive.controls.lib.drive_helpers import get_accel_from_plan, from openpilot.selfdrive.modeld.parse_model_outputs import Parser from openpilot.selfdrive.modeld.fill_model_msg import fill_model_msg, fill_pose_msg, PublishState from openpilot.selfdrive.modeld.constants import ModelConstants, Plan +from openpilot.selfdrive.modeld.external_pickle import load_external_pickle PROCESS_NAME = "selfdrive.modeld.modeld" @@ -177,11 +178,8 @@ class ModelState: self.parser = Parser() self.frame_buf_params : dict[str, tuple[int, int, int, int]] = {} self.update_imgs = None - with open(VISION_PKL_PATH, "rb") as f: - self.vision_run = pickle.load(f) - - with open(POLICY_PKL_PATH, "rb") as f: - self.policy_run = pickle.load(f) + self.vision_run = load_external_pickle(VISION_PKL_PATH) + self.policy_run = load_external_pickle(POLICY_PKL_PATH) def slice_outputs(self, model_outputs: np.ndarray, output_slices: dict[str, slice]) -> dict[str, np.ndarray]: parsed_model_outputs = {k: model_outputs[np.newaxis, v] for k,v in output_slices.items()} From 49a611df59958e1bedb5a16146fd10a28f983a64 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Fri, 13 Feb 2026 14:08:26 -0800 Subject: [PATCH 024/311] CI: don't block on badges job for release builds --- .github/workflows/prebuilt.yaml | 2 +- .github/workflows/release.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/prebuilt.yaml b/.github/workflows/prebuilt.yaml index 921c27465b..c0a2baa8e3 100644 --- a/.github/workflows/prebuilt.yaml +++ b/.github/workflows/prebuilt.yaml @@ -28,7 +28,7 @@ jobs: wait-interval: 30 running-workflow-name: 'build prebuilt' repo-token: ${{ secrets.GITHUB_TOKEN }} - check-regexp: ^((?!.*(build master-ci).*).)*$ + check-regexp: ^((?!.*(build master-ci|create badges).*).)*$ - uses: actions/checkout@v6 with: submodules: true diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 0f34dbe435..159902d519 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -29,7 +29,7 @@ jobs: wait-interval: 30 running-workflow-name: 'build master-ci' repo-token: ${{ secrets.GITHUB_TOKEN }} - check-regexp: ^((?!.*(build prebuilt).*).)*$ + check-regexp: ^((?!.*(build prebuilt|create badges).*).)*$ - uses: actions/checkout@v6 with: submodules: true From c91225b52e21a8f75a9b617feb9116bec22ac6ca Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 13 Feb 2026 15:37:07 -0800 Subject: [PATCH 025/311] WifiUi: reset networks on panel hide (#37199) * stash * fix setup * clean up * clean up * clean up * set active as safeguard --- selfdrive/ui/mici/layouts/settings/network/__init__.py | 6 ++++-- selfdrive/ui/mici/layouts/settings/network/wifi_ui.py | 8 ++------ system/ui/mici_setup.py | 6 ++++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/selfdrive/ui/mici/layouts/settings/network/__init__.py b/selfdrive/ui/mici/layouts/settings/network/__init__.py index fb1d56a1f6..7de9b52278 100644 --- a/selfdrive/ui/mici/layouts/settings/network/__init__.py +++ b/selfdrive/ui/mici/layouts/settings/network/__init__.py @@ -123,12 +123,12 @@ class NetworkLayoutMici(NavWidget): def show_event(self): super().show_event() self._current_panel = NetworkPanelType.NONE - self._wifi_ui.show_event() + self._wifi_manager.set_active(True) self._scroller.show_event() def hide_event(self): super().hide_event() - self._wifi_ui.hide_event() + self._wifi_manager.set_active(False) def _toggle_roaming(self, checked: bool): self._wifi_manager.update_gsm_settings(checked, ui_state.params.get("GsmApn") or "", ui_state.params.get_bool("GsmMetered")) @@ -186,6 +186,8 @@ class NetworkLayoutMici(NavWidget): def _switch_to_panel(self, panel_type: NetworkPanelType): if panel_type == NetworkPanelType.WIFI: self._wifi_ui.show_event() + elif self._current_panel == NetworkPanelType.WIFI: + self._wifi_ui.hide_event() self._current_panel = panel_type def _render(self, rect: rl.Rectangle): diff --git a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py index 5c9616524c..d25b28d63b 100644 --- a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py +++ b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py @@ -360,15 +360,11 @@ class WifiUIMici(BigMultiOptionDialog): ) def show_event(self): - # Call super to prepare scroller; selection scroll is handled dynamically + # Clear scroller items and update from latest scan results super().show_event() self._wifi_manager.set_active(True) - - def hide_event(self): - super().hide_event() - self._wifi_manager.set_active(False) - # clear scroller items to remove old networks on next show self._scroller._items.clear() + self._update_buttons() def _open_network_manage_page(self, result=None): self._network_info_page.update_networks(self._networks) diff --git a/system/ui/mici_setup.py b/system/ui/mici_setup.py index 78b6b5cecb..1322e95ef0 100755 --- a/system/ui/mici_setup.py +++ b/system/ui/mici_setup.py @@ -457,6 +457,8 @@ class NetworkSetupPage(Widget): self._prev_has_internet = False def set_state(self, state: NetworkSetupState): + if self._state == NetworkSetupState.WIFI_PANEL and state != NetworkSetupState.WIFI_PANEL: + self._wifi_ui.hide_event() self._state = state if state == NetworkSetupState.WIFI_PANEL: self._wifi_ui.show_event() @@ -478,11 +480,11 @@ class NetworkSetupPage(Widget): def show_event(self): super().show_event() self._state = NetworkSetupState.MAIN - self._wifi_ui.show_event() def hide_event(self): super().hide_event() - self._wifi_ui.hide_event() + if self._state == NetworkSetupState.WIFI_PANEL: + self._wifi_ui.hide_event() def _render(self, _): if self._state == NetworkSetupState.MAIN: From 1b426a31608a1d7aeec35a4ac76055a4b952f470 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 13 Feb 2026 16:15:59 -0800 Subject: [PATCH 026/311] wifi button shows connecting (#37202) * connecting wifi button * use real wifi strength * simplify * meh cursor brought up edge case --- .../mici/layouts/settings/network/__init__.py | 28 +++++++++++-------- system/ui/lib/wifi_manager.py | 4 +++ 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/selfdrive/ui/mici/layouts/settings/network/__init__.py b/selfdrive/ui/mici/layouts/settings/network/__init__.py index 7de9b52278..3a0da36bb1 100644 --- a/selfdrive/ui/mici/layouts/settings/network/__init__.py +++ b/selfdrive/ui/mici/layouts/settings/network/__init__.py @@ -160,18 +160,24 @@ class NetworkLayoutMici(NavWidget): # Update wi-fi button with ssid and ip address # TODO: make sure we handle hidden ssids + connecting_ssid = self._wifi_manager.connecting_to_ssid connected_network = next((network for network in networks if network.is_connected), None) - self._wifi_button.set_text(normalize_ssid(connected_network.ssid) if connected_network is not None else "wi-fi") - self._wifi_button.set_value(self._wifi_manager.ipv4_address or "not connected") - if connected_network is not None: - strength = WifiIcon.get_strength_icon_idx(connected_network.strength) - if strength == 2: - strength_icon = self._wifi_full_txt - elif strength == 1: - strength_icon = self._wifi_medium_txt - else: - strength_icon = self._wifi_low_txt - self._wifi_button.set_icon(strength_icon) + if connecting_ssid: + display_network = next((n for n in networks if n.ssid == connecting_ssid), None) + self._wifi_button.set_text(normalize_ssid(connecting_ssid)) + self._wifi_button.set_value("connecting...") + elif connected_network is not None: + display_network = connected_network + self._wifi_button.set_text(normalize_ssid(connected_network.ssid)) + self._wifi_button.set_value(self._wifi_manager.ipv4_address or "not connected") + else: + display_network = None + self._wifi_button.set_text("wi-fi") + self._wifi_button.set_value("not connected") + + if display_network is not None: + strength = WifiIcon.get_strength_icon_idx(display_network.strength) + self._wifi_button.set_icon(self._wifi_full_txt if strength == 2 else self._wifi_medium_txt if strength == 1 else self._wifi_low_txt) else: self._wifi_button.set_icon(self._wifi_slash_txt) diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index 72373ecbea..2dfbe63cc3 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -233,6 +233,10 @@ class WifiManager: def current_network_metered(self) -> MeteredType: return self._current_network_metered + @property + def connecting_to_ssid(self) -> str: + return self._connecting_to_ssid + @property def tethering_password(self) -> str: return self._tethering_password From 10065c8c288598ca4cce1c4e01d2dad68f4790bb Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 13 Feb 2026 17:02:42 -0800 Subject: [PATCH 027/311] WifiManager: handle failed state change (#37205) * handle connecting to network that drops out w/ wrong password (no longer says connected and now deletes connection) * clean up * combine --- selfdrive/ui/mici/layouts/settings/network/wifi_ui.py | 5 +++-- system/ui/lib/networkmanager.py | 2 ++ system/ui/lib/wifi_manager.py | 8 ++++++-- 3 files changed, 11 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 d25b28d63b..1c11c09731 100644 --- a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py +++ b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py @@ -367,8 +367,9 @@ class WifiUIMici(BigMultiOptionDialog): self._update_buttons() 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) + if self._network_info_page._network is not None and self._network_info_page._network.ssid in self._networks: + self._network_info_page.update_networks(self._networks) + gui_app.set_modal_overlay(self._network_info_page) def _forget_network(self, ssid: str): network = self._networks.get(ssid) diff --git a/system/ui/lib/networkmanager.py b/system/ui/lib/networkmanager.py index ffa2ff4db9..19d54e6516 100644 --- a/system/ui/lib/networkmanager.py +++ b/system/ui/lib/networkmanager.py @@ -11,6 +11,7 @@ class NMDeviceState(IntEnum): IP_CONFIG = 70 ACTIVATED = 100 DEACTIVATING = 110 + FAILED = 120 # NetworkManager constants @@ -29,6 +30,7 @@ NM_IP4_CONFIG_IFACE = 'org.freedesktop.NetworkManager.IP4Config' NM_DEVICE_TYPE_WIFI = 2 NM_DEVICE_TYPE_MODEM = 8 +NM_DEVICE_STATE_REASON_NO_SECRETS = 7 NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT = 8 NM_DEVICE_STATE_REASON_NEW_ACTIVATION = 60 diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index 2dfbe63cc3..187199b5c9 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -23,7 +23,8 @@ from openpilot.system.ui.lib.networkmanager import (NM, NM_WIRELESS_IFACE, NM_80 NM_802_11_AP_FLAGS_PRIVACY, NM_802_11_AP_FLAGS_WPS, NM_PATH, NM_IFACE, NM_ACCESS_POINT_IFACE, NM_SETTINGS_PATH, NM_SETTINGS_IFACE, NM_CONNECTION_IFACE, NM_DEVICE_IFACE, - NM_DEVICE_TYPE_WIFI, NM_DEVICE_TYPE_MODEM, NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT, + NM_DEVICE_TYPE_WIFI, NM_DEVICE_TYPE_MODEM, NM_DEVICE_STATE_REASON_NO_SECRETS, + NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT, NM_DEVICE_STATE_REASON_NEW_ACTIVATION, NM_ACTIVE_CONNECTION_IFACE, NM_IP4_CONFIG_IFACE, NM_PROPERTIES_IFACE, NMDeviceState) @@ -319,7 +320,10 @@ class WifiManager: new_state, previous_state, change_reason = state_q.popleft().body # BAD PASSWORD - if new_state == NMDeviceState.NEED_AUTH and change_reason == NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT and len(self._connecting_to_ssid): + # - strong network rejects with NEED_AUTH+SUPPLICANT_DISCONNECT + # - weak/gone network fails with FAILED+NO_SECRETS + if len(self._connecting_to_ssid) and ((new_state == NMDeviceState.NEED_AUTH and change_reason == NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT) or + (new_state == NMDeviceState.FAILED and change_reason == NM_DEVICE_STATE_REASON_NO_SECRETS)): self._enqueue_callbacks(self._need_auth, self._connecting_to_ssid) self.forget_connection(self._connecting_to_ssid, block=True) self._connecting_to_ssid = "" From cd03aa19a190224ba7d098741729b2c0d4e010db Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 13 Feb 2026 17:14:51 -0800 Subject: [PATCH 028/311] WifiManager: fix forgetting wrong network (#37187) * not sure how works but does? * clean up * clean up --- system/ui/lib/wifi_manager.py | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index 187199b5c9..e199c74f2d 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -167,6 +167,7 @@ class WifiManager: # State self._connections: dict[str, str] = {} # ssid -> connection path, updated via NM signals self._connecting_to_ssid: str = "" + self._prev_connecting_to_ssid: str = "" self._ipv4_address: str = "" self._current_network_metered: MeteredType = MeteredType.UNKNOWN self._tethering_password: str = "" @@ -242,6 +243,10 @@ class WifiManager: def tethering_password(self) -> str: return self._tethering_password + def _set_connecting(self, ssid: str): + self._prev_connecting_to_ssid = self._connecting_to_ssid + self._connecting_to_ssid = ssid + def _enqueue_callbacks(self, cbs: list[Callable], *args): for cb in cbs: self._callback_queue.append(lambda _cb=cb: _cb(*args)) @@ -319,23 +324,29 @@ class WifiManager: while len(state_q): new_state, previous_state, change_reason = state_q.popleft().body - # BAD PASSWORD + # BAD PASSWORD - use prev if current has already moved on to a new connection # - strong network rejects with NEED_AUTH+SUPPLICANT_DISCONNECT # - weak/gone network fails with FAILED+NO_SECRETS - if len(self._connecting_to_ssid) and ((new_state == NMDeviceState.NEED_AUTH and change_reason == NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT) or - (new_state == NMDeviceState.FAILED and change_reason == NM_DEVICE_STATE_REASON_NO_SECRETS)): - self._enqueue_callbacks(self._need_auth, self._connecting_to_ssid) - self.forget_connection(self._connecting_to_ssid, block=True) - self._connecting_to_ssid = "" + if ((new_state == NMDeviceState.NEED_AUTH and change_reason == NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT) or + (new_state == NMDeviceState.FAILED and change_reason == NM_DEVICE_STATE_REASON_NO_SECRETS)): + failed_ssid = self._prev_connecting_to_ssid or self._connecting_to_ssid + if failed_ssid: + self._enqueue_callbacks(self._need_auth, failed_ssid) + self.forget_connection(failed_ssid, block=True) + self._prev_connecting_to_ssid = "" + if self._connecting_to_ssid == failed_ssid: + self._connecting_to_ssid = "" elif new_state == NMDeviceState.ACTIVATED: if len(self._activated): self._update_networks() self._enqueue_callbacks(self._activated) + self._prev_connecting_to_ssid = "" self._connecting_to_ssid = "" elif new_state == NMDeviceState.DISCONNECTED and change_reason != NM_DEVICE_STATE_REASON_NEW_ACTIVATION: self._enqueue_callbacks(self._forgotten, self._connecting_to_ssid) + self._prev_connecting_to_ssid = "" self._connecting_to_ssid = "" def _network_scanner(self): @@ -447,9 +458,10 @@ class WifiManager: self._router_main.send_and_get_reply(new_method_call(settings_addr, 'AddConnection', 'a{sa{sv}}', (connection,))) def connect_to_network(self, ssid: str, password: str, hidden: bool = False): + self._set_connecting(ssid) + def worker(): # Clear all connections that may already exist to the network we are connecting to - self._connecting_to_ssid = ssid self.forget_connection(ssid, block=True) connection = { @@ -500,6 +512,8 @@ class WifiManager: threading.Thread(target=worker, daemon=True).start() def activate_connection(self, ssid: str, block: bool = False): + self._set_connecting(ssid) + def worker(): conn_path = self._connections.get(ssid, None) if conn_path is not None: @@ -507,7 +521,6 @@ class WifiManager: cloudlog.warning("No WiFi device found") return - self._connecting_to_ssid = ssid self._router_main.send(new_method_call(self._nm, 'ActivateConnection', 'ooo', (conn_path, self._wifi_device, "/"))) From 9bb6e997aad6c073c7991afcef41dc27a9c06f1b Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 13 Feb 2026 17:20:54 -0800 Subject: [PATCH 029/311] Make more icons 90% white (#37206) * 90% icons * fix! --- selfdrive/ui/mici/layouts/home.py | 2 +- selfdrive/ui/mici/widgets/button.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/selfdrive/ui/mici/layouts/home.py b/selfdrive/ui/mici/layouts/home.py index f5dab7249a..d4bbb74914 100644 --- a/selfdrive/ui/mici/layouts/home.py +++ b/selfdrive/ui/mici/layouts/home.py @@ -249,7 +249,7 @@ class MiciHomeLayout(Widget): # Offset by difference in height between slashless and slash icons to make center align match rl.draw_texture(self._wifi_slash_txt, int(last_x), int(self._rect.y + self.rect.height - self._wifi_slash_txt.height / 2 - (self._wifi_slash_txt.height - self._wifi_none_txt.height) / 2 - Y_CENTER), - rl.Color(255, 255, 255, 255)) + rl.Color(255, 255, 255, int(255 * 0.9))) last_x += self._wifi_slash_txt.width + ITEM_SPACING # draw experimental icon diff --git a/selfdrive/ui/mici/widgets/button.py b/selfdrive/ui/mici/widgets/button.py index 36b1922385..101f3bb56e 100644 --- a/selfdrive/ui/mici/widgets/button.py +++ b/selfdrive/ui/mici/widgets/button.py @@ -54,7 +54,7 @@ class BigCircleButton(Widget): def _draw_content(self, btn_y: float): # draw icon - icon_color = rl.WHITE if self.enabled else rl.Color(255, 255, 255, int(255 * 0.35)) + icon_color = rl.Color(255, 255, 255, int(255 * 0.9)) if self.enabled else rl.Color(255, 255, 255, int(255 * 0.35)) rl.draw_texture_ex(self._txt_icon, (self._rect.x + (self._rect.width - self._txt_icon.width) / 2 + self._icon_offset[0], btn_y + (self._rect.height - self._txt_icon.height) / 2 + self._icon_offset[1]), 0, 1.0, icon_color) @@ -206,7 +206,7 @@ class BigButton(Widget): source_rec = rl.Rectangle(0, 0, self._txt_icon.width, self._txt_icon.height) dest_rec = rl.Rectangle(x, y, self._txt_icon.width, self._txt_icon.height) origin = rl.Vector2(self._txt_icon.width / 2, self._txt_icon.height / 2) - rl.draw_texture_pro(self._txt_icon, source_rec, dest_rec, origin, rotation, rl.WHITE) + rl.draw_texture_pro(self._txt_icon, source_rec, dest_rec, origin, rotation, rl.Color(255, 255, 255, int(255 * 0.9))) def _render(self, _): # draw _txt_default_bg From 5a9fdde156c467953eab79985097149acb057647 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 13 Feb 2026 17:30:59 -0800 Subject: [PATCH 030/311] WifiUi: use WifiManager forget (#37208) * start * clean up forget --- .../ui/mici/layouts/settings/network/wifi_ui.py | 10 +--------- system/ui/lib/wifi_manager.py | 15 +++++++++------ system/ui/widgets/scroller.py | 2 +- 3 files changed, 11 insertions(+), 16 deletions(-) diff --git a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py index 1c11c09731..b27e03ed14 100644 --- a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py +++ b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py @@ -342,7 +342,7 @@ class WifiUIMici(BigMultiOptionDialog): # Set up back navigation self.set_back_callback(back_callback) - self._network_info_page = NetworkInfoPage(wifi_manager, self._connect_to_network, self._forget_network, self._open_network_manage_page) + self._network_info_page = NetworkInfoPage(wifi_manager, self._connect_to_network, wifi_manager.forget_connection, self._open_network_manage_page) self._network_info_page.set_connecting(lambda: self._connecting) self._loading_animation = LoadingAnimation() @@ -371,14 +371,6 @@ class WifiUIMici(BigMultiOptionDialog): self._network_info_page.update_networks(self._networks) gui_app.set_modal_overlay(self._network_info_page) - def _forget_network(self, ssid: str): - network = self._networks.get(ssid) - if network is None: - cloudlog.warning(f"Trying to forget unknown network: {ssid}") - return - - self._wifi_manager.forget_connection(network.ssid) - def _on_network_updated(self, networks: list[Network]): self._networks = {network.ssid: network for network in networks} self._update_buttons() diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index e199c74f2d..2a4a9ab711 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -498,13 +498,16 @@ class WifiManager: def forget_connection(self, ssid: str, block: bool = False): def worker(): conn_path = self._connections.get(ssid, None) - if conn_path is not None: - conn_addr = DBusAddress(conn_path, bus_name=NM, interface=NM_CONNECTION_IFACE) - self._router_main.send_and_get_reply(new_method_call(conn_addr, 'Delete')) + if conn_path is None: + cloudlog.warning(f"Trying to forget unknown connection: {ssid}") + return - if len(self._forgotten): - self._update_networks() - self._enqueue_callbacks(self._forgotten, ssid) + conn_addr = DBusAddress(conn_path, bus_name=NM, interface=NM_CONNECTION_IFACE) + self._router_main.send_and_get_reply(new_method_call(conn_addr, 'Delete')) + + if len(self._forgotten): + self._update_networks() + self._enqueue_callbacks(self._forgotten, ssid) if block: worker() diff --git a/system/ui/widgets/scroller.py b/system/ui/widgets/scroller.py index 43539d128b..fcba1952ce 100644 --- a/system/ui/widgets/scroller.py +++ b/system/ui/widgets/scroller.py @@ -267,7 +267,7 @@ class Scroller(Widget): rl.begin_scissor_mode(int(self._rect.x), int(self._rect.y), int(self._rect.width), int(self._rect.height)) - for item in self._visible_items: + for item in reversed(self._visible_items): # Skip rendering if not in viewport if not rl.check_collision_recs(item.rect, self._rect): continue From 2dac616bef98ac92fd4d4490545076c69880c1f8 Mon Sep 17 00:00:00 2001 From: Nick Date: Fri, 13 Feb 2026 17:43:53 -0800 Subject: [PATCH 031/311] keyboard: fix hint text truncation and add trailing ellipsis (#37207) Widen the hint label rect so it doesn't reserve right-side space for the hidden backspace button, preventing unnecessary text eliding. Also show the blinking cursor over the hint and add trailing ellipsis to hint strings for consistency. Co-authored-by: Cursor --- selfdrive/ui/mici/layouts/settings/developer.py | 2 +- .../ui/mici/layouts/settings/network/__init__.py | 2 +- selfdrive/ui/mici/widgets/dialog.py | 13 +++++++++---- system/ui/mici_setup.py | 2 +- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/selfdrive/ui/mici/layouts/settings/developer.py b/selfdrive/ui/mici/layouts/settings/developer.py index b6145e042e..ad68d6ee94 100644 --- a/selfdrive/ui/mici/layouts/settings/developer.py +++ b/selfdrive/ui/mici/layouts/settings/developer.py @@ -29,7 +29,7 @@ class DeveloperLayoutMici(NavWidget): def ssh_keys_callback(): github_username = ui_state.params.get("GithubUsername") or "" - dlg = BigInputDialog("enter GitHub username", github_username, confirm_callback=github_username_callback) + dlg = BigInputDialog("enter GitHub username...", github_username, confirm_callback=github_username_callback) if not system_time_valid(): dlg = BigDialog("Please connect to Wi-Fi to fetch your key", "") gui_app.set_modal_overlay(dlg) diff --git a/selfdrive/ui/mici/layouts/settings/network/__init__.py b/selfdrive/ui/mici/layouts/settings/network/__init__.py index 3a0da36bb1..bda619feef 100644 --- a/selfdrive/ui/mici/layouts/settings/network/__init__.py +++ b/selfdrive/ui/mici/layouts/settings/network/__init__.py @@ -144,7 +144,7 @@ class NetworkLayoutMici(NavWidget): self._wifi_manager.update_gsm_settings(ui_state.params.get_bool("GsmRoaming"), apn, ui_state.params.get_bool("GsmMetered")) current_apn = ui_state.params.get("GsmApn") or "" - dlg = BigInputDialog("enter APN", current_apn, minimum_length=0, confirm_callback=update_apn) + dlg = BigInputDialog("enter APN...", current_apn, minimum_length=0, confirm_callback=update_apn) gui_app.set_modal_overlay(dlg) def _toggle_cellular_metered(self, checked: bool): diff --git a/selfdrive/ui/mici/widgets/dialog.py b/selfdrive/ui/mici/widgets/dialog.py index b88ac20494..6b4cb92c16 100644 --- a/selfdrive/ui/mici/widgets/dialog.py +++ b/selfdrive/ui/mici/widgets/dialog.py @@ -195,11 +195,13 @@ class BigInputDialog(BigDialogBase): rl.BLACK, rl.BLANK) # draw cursor + blink_alpha = (math.sin(rl.get_time() * 6) + 1) / 2 if text: - blink_alpha = (math.sin(rl.get_time() * 6) + 1) / 2 cursor_x = min(text_x + text_size.x + 3, text_field_rect.x + text_field_rect.width) - rl.draw_rectangle_rounded(rl.Rectangle(int(cursor_x), int(text_field_rect.y), 4, int(text_size.y)), - 1, 4, rl.Color(255, 255, 255, int(255 * blink_alpha))) + else: + cursor_x = text_field_rect.x - 6 + rl.draw_rectangle_rounded(rl.Rectangle(int(cursor_x), int(text_field_rect.y), 4, int(text_size.y)), + 1, 4, rl.Color(255, 255, 255, int(255 * blink_alpha))) # draw backspace icon with nice fade self._backspace_img_alpha.update(255 * bool(text)) @@ -209,7 +211,10 @@ class BigInputDialog(BigDialogBase): if not text and self._hint_label.text and not candidate_char: # draw description if no text entered yet and not drawing candidate char - self._hint_label.render(text_field_rect) + hint_rect = rl.Rectangle(text_field_rect.x, text_field_rect.y, + self._rect.width - text_field_rect.x - PADDING, + text_field_rect.height) + self._hint_label.render(hint_rect) # TODO: move to update state # make rect take up entire area so it's easier to click diff --git a/system/ui/mici_setup.py b/system/ui/mici_setup.py index 1322e95ef0..b5c0d05281 100755 --- a/system/ui/mici_setup.py +++ b/system/ui/mici_setup.py @@ -639,7 +639,7 @@ class Setup(Widget): if result == DialogResult.CANCEL: self._set_state(SetupState.SOFTWARE_SELECTION) - keyboard = BigInputDialog("custom software URL", confirm_callback=handle_keyboard_result) + keyboard = BigInputDialog("custom software URL...", confirm_callback=handle_keyboard_result) gui_app.set_modal_overlay(keyboard, callback=handle_keyboard_exit) def use_openpilot(self): From 065096455915cb46b0f67e9bf1cc4f87f19e07f2 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Fri, 13 Feb 2026 23:39:55 -0500 Subject: [PATCH 032/311] [TIZI/TICI] ui: only fetch roles and users when the sunnylink panel is opened (#1697) * sunnylink: only roles and users when the sunnylink panel is opened * shadow * thread-safe --- .../sunnypilot/layouts/settings/sunnylink.py | 5 +++++ sunnypilot/sunnylink/sunnylink_state.py | 18 ++++++++++++++---- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/selfdrive/ui/sunnypilot/layouts/settings/sunnylink.py b/selfdrive/ui/sunnypilot/layouts/settings/sunnylink.py index 00baf0cccf..7955f13202 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/sunnylink.py +++ b/selfdrive/ui/sunnypilot/layouts/settings/sunnylink.py @@ -355,5 +355,10 @@ class SunnylinkLayout(Widget): def show_event(self): super().show_event() + ui_state.sunnylink_state.set_settings_open(True) self._scroller.show_event() self._sunnylink_description.set_visible(False) + + def hide_event(self): + super().hide_event() + ui_state.sunnylink_state.set_settings_open(False) diff --git a/sunnypilot/sunnylink/sunnylink_state.py b/sunnypilot/sunnylink/sunnylink_state.py index acd20d23ae..927d041991 100644 --- a/sunnypilot/sunnylink/sunnylink_state.py +++ b/sunnypilot/sunnylink/sunnylink_state.py @@ -109,6 +109,8 @@ class SunnylinkState: self.sunnylink_dongle_id = self._params.get("SunnylinkDongleId") self._api = SunnylinkApi(self.sunnylink_dongle_id) + self._panel_open = False + self._load_initial_state() def _load_initial_state(self) -> None: @@ -166,10 +168,14 @@ class SunnylinkState: def _worker_thread(self) -> None: while self._running: - self._sm.update() - if self.is_connected(): - self._fetch_roles() - self._fetch_users() + with self._lock: + panel_open = self._panel_open + + if panel_open: + self._sm.update() + if self.is_connected(): + self._fetch_roles() + self._fetch_users() for _ in range(int(self.FETCH_INTERVAL / self.SLEEP_INTERVAL)): if not self._running: @@ -221,5 +227,9 @@ class SunnylinkState: else: return style.ITEM_TEXT_VALUE_COLOR + def set_settings_open(self, _open: bool) -> None: + with self._lock: + self._panel_open = _open + def __del__(self): self.stop() From 4af41ffce6e92d3d75d498ad53de56c9969cdbb8 Mon Sep 17 00:00:00 2001 From: David <49467229+TheSecurityDev@users.noreply.github.com> Date: Sat, 14 Feb 2026 15:20:44 -0600 Subject: [PATCH 033/311] ui diff: ensure video name matches output (#37211) * auto name diff.mp4 * ensure output file has .html extension --- selfdrive/ui/tests/diff/diff.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/selfdrive/ui/tests/diff/diff.py b/selfdrive/ui/tests/diff/diff.py index a581f68747..bde6d44238 100755 --- a/selfdrive/ui/tests/diff/diff.py +++ b/selfdrive/ui/tests/diff/diff.py @@ -62,7 +62,7 @@ def find_differences(video1, video2): return different_frames, len(frames1) -def generate_html_report(video1, video2, basedir, different_frames, total_frames): +def generate_html_report(video1, video2, basedir, different_frames, total_frames, diff_video_name): chunks = [] if different_frames: current_chunk = [different_frames[0]] @@ -100,7 +100,7 @@ def generate_html_report(video1, video2, basedir, different_frames, total_frames

Pixel Diff

@@ -152,6 +152,9 @@ def main(): args = parser.parse_args() + if not args.output.lower().endswith('.html'): + args.output += '.html' + os.makedirs(DIFF_OUT_DIR, exist_ok=True) print("=" * 60) @@ -162,8 +165,9 @@ def main(): print(f"Output: {args.output}") print() - # Create diff video - diff_video_path = os.path.join(os.path.dirname(args.output), DIFF_OUT_DIR / "diff.mp4") + # Create diff video with name derived from output HTML + diff_video_name = Path(args.output).stem + '.mp4' + diff_video_path = str(DIFF_OUT_DIR / diff_video_name) create_diff_video(args.video1, args.video2, diff_video_path) different_frames, total_frames = find_differences(args.video1, args.video2) @@ -173,7 +177,7 @@ def main(): print() print("Generating HTML report...") - html = generate_html_report(args.video1, args.video2, args.basedir, different_frames, total_frames) + html = generate_html_report(args.video1, args.video2, args.basedir, different_frames, total_frames, diff_video_name) with open(DIFF_OUT_DIR / args.output, 'w') as f: f.write(html) From ecde60419871820269657a8b88c839a824ecfd73 Mon Sep 17 00:00:00 2001 From: David <49467229+TheSecurityDev@users.noreply.github.com> Date: Sat, 14 Feb 2026 15:21:09 -0600 Subject: [PATCH 034/311] ui replay: use openpilot prefix (#37185) * fix: use openpilot prefix * fix ui_state import * comment --- selfdrive/ui/tests/diff/replay.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/selfdrive/ui/tests/diff/replay.py b/selfdrive/ui/tests/diff/replay.py index 1efc6dde92..a668cc776b 100755 --- a/selfdrive/ui/tests/diff/replay.py +++ b/selfdrive/ui/tests/diff/replay.py @@ -13,9 +13,9 @@ if "RECORD_OUTPUT" not in os.environ: os.environ["RECORD_OUTPUT"] = os.path.join(DIFF_OUT_DIR, os.environ["RECORD_OUTPUT"]) from openpilot.common.params import Params +from openpilot.common.prefix import OpenpilotPrefix from openpilot.system.version import terms_version, training_version from openpilot.system.ui.lib.application import gui_app, MousePos, MouseEvent -from openpilot.selfdrive.ui.ui_state import ui_state FPS = 60 HEADLESS = os.getenv("WINDOWED", "0") == "1" @@ -78,6 +78,9 @@ def handle_event(event: DummyEvent): def run_replay(): + from openpilot.selfdrive.ui.ui_state import ui_state # import here for correct param setup (e.g. training guide) + from openpilot.selfdrive.ui.mici.layouts.main import MiciMainLayout # import here for coverage + setup_state() os.makedirs(DIFF_OUT_DIR, exist_ok=True) @@ -85,7 +88,6 @@ def run_replay(): rl.set_config_flags(rl.FLAG_WINDOW_HIDDEN) gui_app.init_window("ui diff test", fps=FPS) - from openpilot.selfdrive.ui.mici.layouts.main import MiciMainLayout # import here for coverage main_layout = MiciMainLayout() main_layout.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) @@ -119,13 +121,14 @@ def run_replay(): def main(): - cov = coverage.coverage(source=['openpilot.selfdrive.ui.mici']) - with cov.collect(): - run_replay() - cov.save() - cov.report() - cov.html_report(directory=os.path.join(DIFF_OUT_DIR, 'htmlcov')) - print("HTML report: htmlcov/index.html") + with OpenpilotPrefix(): + cov = coverage.coverage(source=['openpilot.selfdrive.ui.mici']) + with cov.collect(): + run_replay() + cov.save() + cov.report() + cov.html_report(directory=os.path.join(DIFF_OUT_DIR, 'htmlcov')) + print("HTML report: htmlcov/index.html") if __name__ == "__main__": From ae6aa0f0088bbe97c959b17d6d0358b92e7b1587 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sat, 14 Feb 2026 13:39:14 -0800 Subject: [PATCH 035/311] Remove gcc@13 installation from mac_setup.sh (#37213) * Remove gcc@13 installation from mac_setup.sh Removed installation of gcc@13 from mac_setup.sh. * no cache * Revert "no cache" This reverts commit fc27f7dc9e6dab4b61703433130531f12dbe334b. --- tools/mac_setup.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/tools/mac_setup.sh b/tools/mac_setup.sh index 0ae0b35359..fba7699beb 100755 --- a/tools/mac_setup.sh +++ b/tools/mac_setup.sh @@ -47,7 +47,6 @@ brew "qt@5" brew "zeromq" cask "gcc-arm-embedded" brew "portaudio" -brew "gcc@13" EOS echo "[ ] finished brew install t=$SECONDS" From 56d3014298cf0f33803ed424a365e4dec85c686d Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sat, 14 Feb 2026 13:42:21 -0800 Subject: [PATCH 036/311] Remove pycurl handling from mac_setup.sh (#37214) --- tools/mac_setup.sh | 7 ------- 1 file changed, 7 deletions(-) diff --git a/tools/mac_setup.sh b/tools/mac_setup.sh index fba7699beb..26ef4dd9a5 100755 --- a/tools/mac_setup.sh +++ b/tools/mac_setup.sh @@ -42,7 +42,6 @@ brew "glfw" brew "libusb" brew "libtool" brew "llvm" -brew "openssl@3.0" brew "qt@5" brew "zeromq" cask "gcc-arm-embedded" @@ -59,12 +58,6 @@ export LDFLAGS="$LDFLAGS -L${BREW_PREFIX}/opt/bzip2/lib" export CPPFLAGS="$CPPFLAGS -I${BREW_PREFIX}/opt/zlib/include" export CPPFLAGS="$CPPFLAGS -I${BREW_PREFIX}/opt/bzip2/include" -# pycurl curl/openssl backend dependencies -export LDFLAGS="$LDFLAGS -L${BREW_PREFIX}/opt/openssl@3/lib" -export CPPFLAGS="$CPPFLAGS -I${BREW_PREFIX}/opt/openssl@3/include" -export PYCURL_CURL_CONFIG=/usr/bin/curl-config -export PYCURL_SSL_LIBRARY=openssl - # install python dependencies $DIR/install_python_dependencies.sh echo "[ ] installed python dependencies t=$SECONDS" From 96d1b876bbd7441cac08bab842aded6729ae1c6d Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sat, 14 Feb 2026 20:54:09 -0800 Subject: [PATCH 037/311] pandad: remove multi-panda + USB support (#37217) * pandad: remove multi-panda support * lil more * mac * skip mac --- Jenkinsfile | 10 +- selfdrive/pandad/.gitignore | 2 +- selfdrive/pandad/SConscript | 13 +- selfdrive/pandad/main.cc | 4 +- selfdrive/pandad/panda.cc | 38 +-- selfdrive/pandad/panda.h | 9 +- selfdrive/pandad/panda_comms.cc | 227 ------------- selfdrive/pandad/panda_comms.h | 61 +--- selfdrive/pandad/panda_safety.cc | 27 +- selfdrive/pandad/pandad.cc | 314 ++++++------------ selfdrive/pandad/pandad.h | 7 +- selfdrive/pandad/pandad.py | 51 ++- selfdrive/pandad/spi.cc | 4 +- ...protocol.cc => test_pandad_canprotocol.cc} | 14 +- .../pandad/tests/test_pandad_loopback.py | 18 +- selfdrive/pandad/tests/test_pandad_spi.py | 2 +- 16 files changed, 187 insertions(+), 614 deletions(-) delete mode 100644 selfdrive/pandad/panda_comms.cc rename selfdrive/pandad/tests/{test_pandad_usbprotocol.cc => test_pandad_canprotocol.cc} (88%) diff --git a/Jenkinsfile b/Jenkinsfile index c095eda8a9..6f81755d4e 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -216,12 +216,6 @@ node { step("test manager", "pytest system/manager/test/test_manager.py"), ]) }, - 'loopback': { - deviceStage("loopback", "tizi-loopback", ["UNSAFE=1"], [ - step("build openpilot", "cd system/manager && ./build.py"), - step("test pandad loopback", "pytest selfdrive/pandad/tests/test_pandad_loopback.py"), - ]) - }, 'camerad OX03C10': { deviceStage("OX03C10", "tizi-ox03c10", ["UNSAFE=1"], [ step("build", "cd system/manager && ./build.py"), @@ -251,11 +245,9 @@ node { 'tizi': { deviceStage("tizi", "tizi", ["UNSAFE=1"], [ step("build openpilot", "cd system/manager && ./build.py"), - step("test pandad loopback", "SINGLE_PANDA=1 pytest selfdrive/pandad/tests/test_pandad_loopback.py"), + step("test pandad loopback", "pytest selfdrive/pandad/tests/test_pandad_loopback.py"), step("test pandad spi", "pytest selfdrive/pandad/tests/test_pandad_spi.py"), step("test amp", "pytest system/hardware/tici/tests/test_amplifier.py"), - // TODO: enable once new AGNOS is available - // step("test esim", "pytest system/hardware/tici/tests/test_esim.py"), step("test qcomgpsd", "pytest system/qcomgpsd/tests/test_qcomgpsd.py", [diffPaths: ["system/qcomgpsd/"]]), ]) }, diff --git a/selfdrive/pandad/.gitignore b/selfdrive/pandad/.gitignore index f7226cdb87..cb292405c9 100644 --- a/selfdrive/pandad/.gitignore +++ b/selfdrive/pandad/.gitignore @@ -1,3 +1,3 @@ pandad pandad_api_impl.cpp -tests/test_pandad_usbprotocol +tests/test_pandad_canprotocol diff --git a/selfdrive/pandad/SConscript b/selfdrive/pandad/SConscript index 5e0b782c1e..fd59db9853 100644 --- a/selfdrive/pandad/SConscript +++ b/selfdrive/pandad/SConscript @@ -1,9 +1,10 @@ -Import('env', 'common', 'messaging') +Import('env', 'arch', 'common', 'messaging') -libs = ['usb-1.0', common, messaging, 'pthread'] -panda = env.Library('panda', ['panda.cc', 'panda_comms.cc', 'spi.cc']) +if arch != "Darwin": + libs = [common, messaging, 'pthread'] + panda = env.Library('panda', ['panda.cc', 'spi.cc']) -env.Program('pandad', ['main.cc', 'pandad.cc', 'panda_safety.cc'], LIBS=[panda] + libs) + env.Program('pandad', ['main.cc', 'pandad.cc', 'panda_safety.cc'], LIBS=[panda] + libs) -if GetOption('extras'): - env.Program('tests/test_pandad_usbprotocol', ['tests/test_pandad_usbprotocol.cc'], LIBS=[panda] + libs) + if GetOption('extras'): + env.Program('tests/test_pandad_canprotocol', ['tests/test_pandad_canprotocol.cc'], LIBS=[panda] + libs) diff --git a/selfdrive/pandad/main.cc b/selfdrive/pandad/main.cc index b63d884a45..ef30d6037c 100644 --- a/selfdrive/pandad/main.cc +++ b/selfdrive/pandad/main.cc @@ -16,7 +16,7 @@ int main(int argc, char *argv[]) { assert(err == 0); } - std::vector serials(argv + 1, argv + argc); - pandad_main_thread(serials); + std::string serial = (argc > 1) ? argv[1] : ""; + pandad_main_thread(serial); return 0; } diff --git a/selfdrive/pandad/panda.cc b/selfdrive/pandad/panda.cc index 93e139f0ec..edc2228c0c 100644 --- a/selfdrive/pandad/panda.cc +++ b/selfdrive/pandad/panda.cc @@ -12,19 +12,9 @@ const bool PANDAD_MAXOUT = getenv("PANDAD_MAXOUT") != nullptr; -Panda::Panda(std::string serial, uint32_t bus_offset) : bus_offset(bus_offset) { - // try USB first, then SPI - try { - handle = std::make_unique(serial); - LOGW("connected to %s over USB", serial.c_str()); - } catch (std::exception &e) { -#ifndef __APPLE__ - handle = std::make_unique(serial); - LOGW("connected to %s over SPI", serial.c_str()); -#else - throw e; -#endif - } +Panda::Panda(std::string serial) { + handle = std::make_unique(serial); + LOGW("connected to %s over SPI", serial.c_str()); hw_type = get_hw_type(); can_reset_communications(); @@ -42,20 +32,8 @@ std::string Panda::hw_serial() { return handle->hw_serial; } -std::vector Panda::list(bool usb_only) { - std::vector serials = PandaUsbHandle::list(); - -#ifndef __APPLE__ - if (!usb_only) { - for (const auto &s : PandaSpiHandle::list()) { - if (std::find(serials.begin(), serials.end(), s) == serials.end()) { - serials.push_back(s); - } - } - } -#endif - - return serials; +std::vector Panda::list() { + return PandaSpiHandle::list(); } void Panda::set_safety_model(cereal::CarParams::SafetyModel safety_model, uint16_t safety_param) { @@ -195,7 +173,7 @@ void Panda::pack_can_buffer(const capnp::List::Reader &can_data for (const auto &cmsg : can_data_list) { // check if the message is intended for this panda uint8_t bus = cmsg.getSrc(); - if (bus < bus_offset || bus >= (bus_offset + PANDA_BUS_OFFSET)) { + if (bus >= PANDA_BUS_OFFSET) { continue; } auto can_data = cmsg.getDat(); @@ -207,7 +185,7 @@ void Panda::pack_can_buffer(const capnp::List::Reader &can_data header.addr = cmsg.getAddress(); header.extended = (cmsg.getAddress() >= 0x800) ? 1 : 0; header.data_len_code = data_len_code; - header.bus = bus - bus_offset; + header.bus = bus; header.checksum = 0; memcpy(&send_buf[pos], (uint8_t *)&header, sizeof(can_header)); @@ -283,7 +261,7 @@ bool Panda::unpack_can_buffer(uint8_t *data, uint32_t &size, std::vector handle; + std::unique_ptr handle; public: - Panda(std::string serial="", uint32_t bus_offset=0); + Panda(std::string serial); cereal::PandaState::PandaType hw_type = cereal::PandaState::PandaType::UNKNOWN; - const uint32_t bus_offset; bool connected(); bool comms_healthy(); std::string hw_serial(); // Static functions - static std::vector list(bool usb_only=false); + static std::vector list(); // Panda functionality cereal::PandaState::PandaType get_hw_type(); @@ -91,7 +90,7 @@ protected: uint8_t receive_buffer[RECV_SIZE + sizeof(can_header) + 64]; uint32_t receive_buffer_size = 0; - Panda(uint32_t bus_offset) : bus_offset(bus_offset) {} + Panda() {} void pack_can_buffer(const capnp::List::Reader &can_data_list, std::function write_func); bool unpack_can_buffer(uint8_t *data, uint32_t &size, std::vector &out_vec); diff --git a/selfdrive/pandad/panda_comms.cc b/selfdrive/pandad/panda_comms.cc deleted file mode 100644 index 8a20f397d3..0000000000 --- a/selfdrive/pandad/panda_comms.cc +++ /dev/null @@ -1,227 +0,0 @@ -#include "selfdrive/pandad/panda.h" - -#include -#include -#include - -#include "common/swaglog.h" - -static libusb_context *init_usb_ctx() { - libusb_context *context = nullptr; - int err = libusb_init(&context); - if (err != 0) { - LOGE("libusb initialization error"); - return nullptr; - } - -#if LIBUSB_API_VERSION >= 0x01000106 - libusb_set_option(context, LIBUSB_OPTION_LOG_LEVEL, LIBUSB_LOG_LEVEL_INFO); -#else - libusb_set_debug(context, 3); -#endif - return context; -} - -PandaUsbHandle::PandaUsbHandle(std::string serial) : PandaCommsHandle(serial) { - // init libusb - ssize_t num_devices; - libusb_device **dev_list = NULL; - int err = 0; - ctx = init_usb_ctx(); - if (!ctx) { goto fail; } - - // connect by serial - num_devices = libusb_get_device_list(ctx, &dev_list); - if (num_devices < 0) { goto fail; } - for (size_t i = 0; i < num_devices; ++i) { - libusb_device_descriptor desc; - libusb_get_device_descriptor(dev_list[i], &desc); - if (desc.idVendor == 0x3801 && desc.idProduct == 0xddcc) { - int ret = libusb_open(dev_list[i], &dev_handle); - if (dev_handle == NULL || ret < 0) { goto fail; } - - unsigned char desc_serial[26] = { 0 }; - ret = libusb_get_string_descriptor_ascii(dev_handle, desc.iSerialNumber, desc_serial, std::size(desc_serial)); - if (ret < 0) { goto fail; } - - hw_serial = std::string((char *)desc_serial, ret); - if (serial.empty() || serial == hw_serial) { - break; - } - libusb_close(dev_handle); - dev_handle = NULL; - } - } - if (dev_handle == NULL) goto fail; - libusb_free_device_list(dev_list, 1); - dev_list = nullptr; - - if (libusb_kernel_driver_active(dev_handle, 0) == 1) { - libusb_detach_kernel_driver(dev_handle, 0); - } - - err = libusb_set_configuration(dev_handle, 1); - if (err != 0) { goto fail; } - - err = libusb_claim_interface(dev_handle, 0); - if (err != 0) { goto fail; } - - return; - -fail: - if (dev_list != NULL) { - libusb_free_device_list(dev_list, 1); - } - cleanup(); - throw std::runtime_error("Error connecting to panda"); -} - -PandaUsbHandle::~PandaUsbHandle() { - std::lock_guard lk(hw_lock); - cleanup(); - connected = false; -} - -void PandaUsbHandle::cleanup() { - if (dev_handle) { - libusb_release_interface(dev_handle, 0); - libusb_close(dev_handle); - } - - if (ctx) { - libusb_exit(ctx); - } -} - -std::vector PandaUsbHandle::list() { - static std::unique_ptr context(init_usb_ctx(), libusb_exit); - // init libusb - ssize_t num_devices; - libusb_device **dev_list = NULL; - std::vector serials; - if (!context) { return serials; } - - num_devices = libusb_get_device_list(context.get(), &dev_list); - if (num_devices < 0) { - LOGE("libusb can't get device list"); - goto finish; - } - for (size_t i = 0; i < num_devices; ++i) { - libusb_device *device = dev_list[i]; - libusb_device_descriptor desc; - libusb_get_device_descriptor(device, &desc); - if (desc.idVendor == 0x3801 && desc.idProduct == 0xddcc) { - libusb_device_handle *handle = NULL; - int ret = libusb_open(device, &handle); - if (ret < 0) { goto finish; } - - unsigned char desc_serial[26] = { 0 }; - ret = libusb_get_string_descriptor_ascii(handle, desc.iSerialNumber, desc_serial, std::size(desc_serial)); - libusb_close(handle); - if (ret < 0) { goto finish; } - - serials.push_back(std::string((char *)desc_serial, ret)); - } - } - -finish: - if (dev_list != NULL) { - libusb_free_device_list(dev_list, 1); - } - return serials; -} - -void PandaUsbHandle::handle_usb_issue(int err, const char func[]) { - LOGE_100("usb error %d \"%s\" in %s", err, libusb_strerror((enum libusb_error)err), func); - if (err == LIBUSB_ERROR_NO_DEVICE) { - LOGE("lost connection"); - connected = false; - } - // TODO: check other errors, is simply retrying okay? -} - -int PandaUsbHandle::control_write(uint8_t bRequest, uint16_t wValue, uint16_t wIndex, unsigned int timeout) { - int err; - const uint8_t bmRequestType = LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR | LIBUSB_RECIPIENT_DEVICE; - - if (!connected) { - return LIBUSB_ERROR_NO_DEVICE; - } - - std::lock_guard lk(hw_lock); - do { - err = libusb_control_transfer(dev_handle, bmRequestType, bRequest, wValue, wIndex, NULL, 0, timeout); - if (err < 0) handle_usb_issue(err, __func__); - } while (err < 0 && connected); - - return err; -} - -int PandaUsbHandle::control_read(uint8_t bRequest, uint16_t wValue, uint16_t wIndex, unsigned char *data, uint16_t wLength, unsigned int timeout) { - int err; - const uint8_t bmRequestType = LIBUSB_ENDPOINT_IN | LIBUSB_REQUEST_TYPE_VENDOR | LIBUSB_RECIPIENT_DEVICE; - - if (!connected) { - return LIBUSB_ERROR_NO_DEVICE; - } - - std::lock_guard lk(hw_lock); - do { - err = libusb_control_transfer(dev_handle, bmRequestType, bRequest, wValue, wIndex, data, wLength, timeout); - if (err < 0) handle_usb_issue(err, __func__); - } while (err < 0 && connected); - - return err; -} - -int PandaUsbHandle::bulk_write(unsigned char endpoint, unsigned char* data, int length, unsigned int timeout) { - int err; - int transferred = 0; - - if (!connected) { - return 0; - } - - std::lock_guard lk(hw_lock); - do { - // Try sending can messages. If the receive buffer on the panda is full it will NAK - // and libusb will try again. After 5ms, it will time out. We will drop the messages. - err = libusb_bulk_transfer(dev_handle, endpoint, data, length, &transferred, timeout); - - if (err == LIBUSB_ERROR_TIMEOUT) { - LOGW("Transmit buffer full"); - break; - } else if (err != 0 || length != transferred) { - handle_usb_issue(err, __func__); - } - } while (err != 0 && connected); - - return transferred; -} - -int PandaUsbHandle::bulk_read(unsigned char endpoint, unsigned char* data, int length, unsigned int timeout) { - int err; - int transferred = 0; - - if (!connected) { - return 0; - } - - std::lock_guard lk(hw_lock); - - do { - err = libusb_bulk_transfer(dev_handle, endpoint, data, length, &transferred, timeout); - - if (err == LIBUSB_ERROR_TIMEOUT) { - break; // timeout is okay to exit, recv still happened - } else if (err == LIBUSB_ERROR_OVERFLOW) { - comms_healthy = false; - LOGE_100("overflow got 0x%x", transferred); - } else if (err != 0) { - handle_usb_issue(err, __func__); - } - - } while (err != 0 && connected); - - return transferred; -} diff --git a/selfdrive/pandad/panda_comms.h b/selfdrive/pandad/panda_comms.h index 9c452faf6d..cdfb5019b6 100644 --- a/selfdrive/pandad/panda_comms.h +++ b/selfdrive/pandad/panda_comms.h @@ -6,67 +6,20 @@ #include #include -#ifndef __APPLE__ -#include -#endif - -#include - #define TIMEOUT 0 #define SPI_BUF_SIZE 2048 -// comms base class -class PandaCommsHandle { +class PandaSpiHandle { public: - PandaCommsHandle(std::string serial) {} - virtual ~PandaCommsHandle() {} - virtual void cleanup() = 0; - std::string hw_serial; std::atomic connected = true; std::atomic comms_healthy = true; - static std::vector list(); - // HW communication - virtual int control_write(uint8_t request, uint16_t param1, uint16_t param2, unsigned int timeout=TIMEOUT) = 0; - virtual int control_read(uint8_t request, uint16_t param1, uint16_t param2, unsigned char *data, uint16_t length, unsigned int timeout=TIMEOUT) = 0; - virtual int bulk_write(unsigned char endpoint, unsigned char* data, int length, unsigned int timeout=TIMEOUT) = 0; - virtual int bulk_read(unsigned char endpoint, unsigned char* data, int length, unsigned int timeout=TIMEOUT) = 0; -}; - -class PandaUsbHandle : public PandaCommsHandle { -public: - PandaUsbHandle(std::string serial); - ~PandaUsbHandle(); - int control_write(uint8_t request, uint16_t param1, uint16_t param2, unsigned int timeout=TIMEOUT); - int control_read(uint8_t request, uint16_t param1, uint16_t param2, unsigned char *data, uint16_t length, unsigned int timeout=TIMEOUT); - int bulk_write(unsigned char endpoint, unsigned char* data, int length, unsigned int timeout=TIMEOUT); - int bulk_read(unsigned char endpoint, unsigned char* data, int length, unsigned int timeout=TIMEOUT); - void cleanup(); - - static std::vector list(); - -private: - libusb_context *ctx = NULL; - libusb_device_handle *dev_handle = NULL; - std::recursive_mutex hw_lock; - void handle_usb_issue(int err, const char func[]); -}; - -#ifndef __APPLE__ -struct __attribute__((packed)) spi_header { - uint8_t sync; - uint8_t endpoint; - uint16_t tx_len; - uint16_t max_rx_len; -}; - -class PandaSpiHandle : public PandaCommsHandle { -public: PandaSpiHandle(std::string serial); ~PandaSpiHandle(); + int control_write(uint8_t request, uint16_t param1, uint16_t param2, unsigned int timeout=TIMEOUT); int control_read(uint8_t request, uint16_t param1, uint16_t param2, unsigned char *data, uint16_t length, unsigned int timeout=TIMEOUT); int bulk_write(unsigned char endpoint, unsigned char* data, int length, unsigned int timeout=TIMEOUT); @@ -81,13 +34,19 @@ private: uint8_t rx_buf[SPI_BUF_SIZE]; inline static std::recursive_mutex hw_lock; + struct __attribute__((packed)) spi_header { + uint8_t sync; + uint8_t endpoint; + uint16_t tx_len; + uint16_t max_rx_len; + }; + int wait_for_ack(uint8_t ack, uint8_t tx, unsigned int timeout, unsigned int length); int bulk_transfer(uint8_t endpoint, uint8_t *tx_data, uint16_t tx_len, uint8_t *rx_data, uint16_t rx_len, unsigned int timeout); int spi_transfer(uint8_t endpoint, uint8_t *tx_data, uint16_t tx_len, uint8_t *rx_data, uint16_t max_rx_len, unsigned int timeout); int spi_transfer_retry(uint8_t endpoint, uint8_t *tx_data, uint16_t tx_len, uint8_t *rx_data, uint16_t max_rx_len, unsigned int timeout); - int lltransfer(spi_ioc_transfer &t); + int lltransfer(struct spi_ioc_transfer &t); spi_header header; uint32_t xfer_count = 0; }; -#endif diff --git a/selfdrive/pandad/panda_safety.cc b/selfdrive/pandad/panda_safety.cc index b089503417..32d129bc2e 100644 --- a/selfdrive/pandad/panda_safety.cc +++ b/selfdrive/pandad/panda_safety.cc @@ -23,19 +23,15 @@ void PandaSafety::updateMultiplexingMode() { // Initialize to ELM327 without OBD multiplexing for initial fingerprinting if (!initialized_) { prev_obd_multiplexing_ = false; - for (int i = 0; i < pandas_.size(); ++i) { - pandas_[i]->set_safety_model(cereal::CarParams::SafetyModel::ELM327, 1U); - } + panda_->set_safety_model(cereal::CarParams::SafetyModel::ELM327, 1U); initialized_ = true; } // Switch between multiplexing modes based on the OBD multiplexing request bool obd_multiplexing_requested = params_.getBool("ObdMultiplexingEnabled"); if (obd_multiplexing_requested != prev_obd_multiplexing_) { - for (int i = 0; i < pandas_.size(); ++i) { - const uint16_t safety_param = (i > 0 || !obd_multiplexing_requested) ? 1U : 0U; - pandas_[i]->set_safety_model(cereal::CarParams::SafetyModel::ELM327, safety_param); - } + const uint16_t safety_param = obd_multiplexing_requested ? 0U : 1U; + panda_->set_safety_model(cereal::CarParams::SafetyModel::ELM327, safety_param); prev_obd_multiplexing_ = obd_multiplexing_requested; params_.putBool("ObdMultiplexingChanged", true); } @@ -65,17 +61,10 @@ void PandaSafety::setSafetyMode(const std::string ¶ms_string) { auto safety_configs = car_params.getSafetyConfigs(); uint16_t alternative_experience = car_params.getAlternativeExperience(); - for (int i = 0; i < pandas_.size(); ++i) { - // Default to SILENT safety model if not specified - cereal::CarParams::SafetyModel safety_model = cereal::CarParams::SafetyModel::SILENT; - uint16_t safety_param = 0U; - if (i < safety_configs.size()) { - safety_model = safety_configs[i].getSafetyModel(); - safety_param = safety_configs[i].getSafetyParam(); - } + cereal::CarParams::SafetyModel safety_model = safety_configs[0].getSafetyModel(); + uint16_t safety_param = safety_configs[0].getSafetyParam(); - LOGW("Panda %d: setting safety model: %d, param: %d, alternative experience: %d", i, (int)safety_model, safety_param, alternative_experience); - pandas_[i]->set_alternative_experience(alternative_experience); - pandas_[i]->set_safety_model(safety_model, safety_param); - } + LOGW("setting safety model: %d, param: %d, alternative experience: %d", (int)safety_model, safety_param, alternative_experience); + panda_->set_alternative_experience(alternative_experience); + panda_->set_safety_model(safety_model, safety_param); } diff --git a/selfdrive/pandad/pandad.cc b/selfdrive/pandad/pandad.cc index 2fd4a4def2..d048dbd0c3 100644 --- a/selfdrive/pandad/pandad.cc +++ b/selfdrive/pandad/pandad.cc @@ -1,6 +1,5 @@ #include "selfdrive/pandad/pandad.h" -#include #include #include #include @@ -18,45 +17,24 @@ #include "common/util.h" #include "system/hardware/hw.h" -// -- Multi-panda conventions -- -// Ordering: -// - The internal panda will always be the first panda -// - Consecutive pandas will be sorted based on panda type, and then serial number -// Connecting: -// - If a panda connection is dropped, pandad will reconnect to all pandas -// - If a panda is added, we will only reconnect when we are offroad -// CAN buses: -// - Each panda will have its block of 4 buses. E.g.: the second panda will use -// bus numbers 4, 5, 6 and 7 -// - The internal panda will always be used for accessing the OBD2 port, -// and thus firmware queries -// Safety: -// - SafetyConfig is a list, which is mapped to the connected pandas -// - If there are more pandas connected than there are SafetyConfigs, -// the excess pandas will remain in "silent" or "noOutput" mode -// Ignition: -// - If any of the ignition sources in any panda is high, ignition is high - #define MAX_IR_PANDA_VAL 50 #define CUTOFF_IL 400 #define SATURATE_IL 1000 ExitHandler do_exit; -bool check_all_connected(const std::vector &pandas) { - for (const auto& panda : pandas) { - if (!panda->connected()) { - do_exit = true; - return false; - } +bool check_connected(Panda *panda) { + if (!panda->connected()) { + do_exit = true; + return false; } return true; } -Panda *connect(std::string serial="", uint32_t index=0) { +Panda *connect(std::string serial) { std::unique_ptr panda; try { - panda = std::make_unique(serial, (index * PANDA_BUS_OFFSET)); + panda = std::make_unique(serial); } catch (std::exception &e) { return nullptr; } @@ -78,7 +56,7 @@ Panda *connect(std::string serial="", uint32_t index=0) { return panda.release(); } -void can_send_thread(std::vector pandas, bool fake_send) { +void can_send_thread(Panda *panda, bool fake_send) { util::set_thread_name("pandad_can_send"); AlignedBuffer aligned_buf; @@ -88,7 +66,7 @@ void can_send_thread(std::vector pandas, bool fake_send) { subscriber->setTimeout(100); // run as fast as messages come in - while (!do_exit && check_all_connected(pandas)) { + while (!do_exit && check_connected(panda)) { std::unique_ptr msg(subscriber->receive()); if (!msg) { continue; @@ -99,25 +77,20 @@ void can_send_thread(std::vector pandas, bool fake_send) { // Don't send if older than 1 second if ((nanos_since_boot() - event.getLogMonoTime() < 1e9) && !fake_send) { - for (const auto& panda : pandas) { - LOGT("sending sendcan to panda: %s", (panda->hw_serial()).c_str()); - panda->can_send(event.getSendcan()); - LOGT("sendcan sent to panda: %s", (panda->hw_serial()).c_str()); - } + LOGT("sending sendcan to panda: %s", (panda->hw_serial()).c_str()); + panda->can_send(event.getSendcan()); + LOGT("sendcan sent to panda: %s", (panda->hw_serial()).c_str()); } else { LOGE("sendcan too old to send: %" PRIu64 ", %" PRIu64, nanos_since_boot(), event.getLogMonoTime()); } } } -void can_recv(std::vector &pandas, PubMaster *pm) { +void can_recv(Panda *panda, PubMaster *pm) { static std::vector raw_can_data; { - bool comms_healthy = true; raw_can_data.clear(); - for (const auto& panda : pandas) { - comms_healthy &= panda->can_receive(raw_can_data); - } + bool comms_healthy = panda->can_receive(raw_can_data); MessageBuilder msg; auto evt = msg.initEvent(); @@ -187,102 +160,72 @@ void fill_panda_can_state(cereal::PandaState::PandaCanState::Builder &cs, const cs.setCanCoreResetCnt(can_health.can_core_reset_cnt); } -std::optional send_panda_states(PubMaster *pm, const std::vector &pandas, bool is_onroad, bool spoofing_started) { - bool ignition_local = false; - const uint32_t pandas_cnt = pandas.size(); - +std::optional send_panda_states(PubMaster *pm, Panda *panda, bool is_onroad, bool spoofing_started) { // build msg MessageBuilder msg; auto evt = msg.initEvent(); - auto pss = evt.initPandaStates(pandas_cnt); + auto pss = evt.initPandaStates(1); - std::vector pandaStates; - pandaStates.reserve(pandas_cnt); - - std::vector> pandaCanStates; - pandaCanStates.reserve(pandas_cnt); - - const bool red_panda_comma_three = (pandas.size() == 2) && - (pandas[0]->hw_type == cereal::PandaState::PandaType::DOS) && - (pandas[1]->hw_type == cereal::PandaState::PandaType::RED_PANDA); - - for (const auto& panda : pandas){ - auto health_opt = panda->get_state(); - if (!health_opt) { - return std::nullopt; - } - - health_t health = *health_opt; - - std::array can_health{}; - for (uint32_t i = 0; i < PANDA_CAN_CNT; i++) { - auto can_health_opt = panda->get_can_state(i); - if (!can_health_opt) { - return std::nullopt; - } - can_health[i] = *can_health_opt; - } - pandaCanStates.push_back(can_health); - - if (spoofing_started) { - health.ignition_line_pkt = 1; - } - - // on comma three setups with a red panda, the dos can - // get false positive ignitions due to the harness box - // without a harness connector, so ignore it - if (red_panda_comma_three && (panda->hw_type == cereal::PandaState::PandaType::DOS)) { - health.ignition_line_pkt = 0; - } - - ignition_local |= ((health.ignition_line_pkt != 0) || (health.ignition_can_pkt != 0)); - - pandaStates.push_back(health); + auto health_opt = panda->get_state(); + if (!health_opt) { + return std::nullopt; } - for (uint32_t i = 0; i < pandas_cnt; i++) { - auto panda = pandas[i]; - const auto &health = pandaStates[i]; + health_t health = *health_opt; - // Make sure CAN buses are live: safety_setter_thread does not work if Panda CAN are silent and there is only one other CAN node - if (health.safety_mode_pkt == (uint8_t)(cereal::CarParams::SafetyModel::SILENT)) { - panda->set_safety_model(cereal::CarParams::SafetyModel::NO_OUTPUT); + std::array can_health{}; + for (uint32_t i = 0; i < PANDA_CAN_CNT; i++) { + auto can_health_opt = panda->get_can_state(i); + if (!can_health_opt) { + return std::nullopt; } + can_health[i] = *can_health_opt; + } - bool power_save_desired = !ignition_local; - if (health.power_save_enabled_pkt != power_save_desired) { - panda->set_power_saving(power_save_desired); - } + if (spoofing_started) { + health.ignition_line_pkt = 1; + } - // set safety mode to NO_OUTPUT when car is off or we're not onroad. ELM327 is an alternative if we want to leverage athenad/connect - bool should_close_relay = !ignition_local || !is_onroad; - if (should_close_relay && (health.safety_mode_pkt != (uint8_t)(cereal::CarParams::SafetyModel::NO_OUTPUT))) { - panda->set_safety_model(cereal::CarParams::SafetyModel::NO_OUTPUT); - } + bool ignition_local = ((health.ignition_line_pkt != 0) || (health.ignition_can_pkt != 0)); - if (!panda->comms_healthy()) { - evt.setValid(false); - } + // Make sure CAN buses are live: safety_setter_thread does not work if Panda CAN are silent and there is only one other CAN node + if (health.safety_mode_pkt == (uint8_t)(cereal::CarParams::SafetyModel::SILENT)) { + panda->set_safety_model(cereal::CarParams::SafetyModel::NO_OUTPUT); + } - auto ps = pss[i]; - fill_panda_state(ps, panda->hw_type, health); + bool power_save_desired = !ignition_local; + if (health.power_save_enabled_pkt != power_save_desired) { + panda->set_power_saving(power_save_desired); + } - auto cs = std::array{ps.initCanState0(), ps.initCanState1(), ps.initCanState2()}; - for (uint32_t j = 0; j < PANDA_CAN_CNT; j++) { - fill_panda_can_state(cs[j], pandaCanStates[i][j]); - } + // set safety mode to NO_OUTPUT when car is off or we're not onroad. ELM327 is an alternative if we want to leverage athenad/connect + bool should_close_relay = !ignition_local || !is_onroad; + if (should_close_relay && (health.safety_mode_pkt != (uint8_t)(cereal::CarParams::SafetyModel::NO_OUTPUT))) { + panda->set_safety_model(cereal::CarParams::SafetyModel::NO_OUTPUT); + } - // Convert faults bitset to capnp list - std::bitset fault_bits(health.faults_pkt); - auto faults = ps.initFaults(fault_bits.count()); + if (!panda->comms_healthy()) { + evt.setValid(false); + } - size_t j = 0; - for (size_t f = size_t(cereal::PandaState::FaultType::RELAY_MALFUNCTION); - f <= size_t(cereal::PandaState::FaultType::HEARTBEAT_LOOP_WATCHDOG); f++) { - if (fault_bits.test(f)) { - faults.set(j, cereal::PandaState::FaultType(f)); - j++; - } + auto ps = pss[0]; + fill_panda_state(ps, panda->hw_type, health); + + auto cs = std::array{ps.initCanState0(), ps.initCanState1(), ps.initCanState2()}; + for (uint32_t j = 0; j < PANDA_CAN_CNT; j++) { + fill_panda_can_state(cs[j], can_health[j]); + } + + // Convert faults bitset to capnp list + std::bitset fault_bits(health.faults_pkt); + auto faults = ps.initFaults(fault_bits.count()); + + size_t j = 0; + for (size_t f = size_t(cereal::PandaState::FaultType::RELAY_MALFUNCTION); + f <= size_t(cereal::PandaState::FaultType::HEARTBEAT_LOOP_WATCHDOG); f++) { + if (fault_bits.test(f)) { + faults.set(j, cereal::PandaState::FaultType(f)); + j++; } } @@ -323,46 +266,22 @@ void send_peripheral_state(Panda *panda, PubMaster *pm) { pm->send("peripheralState", msg); } -void process_panda_state(std::vector &pandas, PubMaster *pm, bool engaged, bool is_onroad, bool spoofing_started) { - std::vector connected_serials; - for (Panda *p : pandas) { - connected_serials.push_back(p->hw_serial()); +void process_panda_state(Panda *panda, PubMaster *pm, bool engaged, bool is_onroad, bool spoofing_started) { + auto ignition_opt = send_panda_states(pm, panda, is_onroad, spoofing_started); + if (!ignition_opt) { + LOGE("Failed to get ignition_opt"); + return; } - { - auto ignition_opt = send_panda_states(pm, pandas, is_onroad, spoofing_started); - if (!ignition_opt) { - LOGE("Failed to get ignition_opt"); - return; - } - - // check if we should have pandad reconnect - if (!ignition_opt.value()) { - bool comms_healthy = true; - for (const auto &panda : pandas) { - comms_healthy &= panda->comms_healthy(); - } - - if (!comms_healthy) { - LOGE("Reconnecting, communication to pandas not healthy"); - do_exit = true; - - } else { - // check for new pandas - for (std::string &s : Panda::list(true)) { - if (!std::count(connected_serials.begin(), connected_serials.end(), s)) { - LOGW("Reconnecting to new panda: %s", s.c_str()); - do_exit = true; - break; - } - } - } - } - - for (const auto &panda : pandas) { - panda->send_heartbeat(engaged); + // check if we should have pandad reconnect + if (!ignition_opt.value()) { + if (!panda->comms_healthy()) { + LOGE("Reconnecting, communication to panda not healthy"); + do_exit = true; } } + + panda->send_heartbeat(engaged); } void process_peripheral_state(Panda *panda, PubMaster *pm, bool no_fan_control) { @@ -429,30 +348,29 @@ void process_peripheral_state(Panda *panda, PubMaster *pm, bool no_fan_control) } } -void pandad_run(std::vector &pandas) { +void pandad_run(Panda *panda) { const bool no_fan_control = getenv("NO_FAN_CONTROL") != nullptr; const bool spoofing_started = getenv("STARTED") != nullptr; const bool fake_send = getenv("FAKESEND") != nullptr; // Start the CAN send thread - std::thread send_thread(can_send_thread, pandas, fake_send); + std::thread send_thread(can_send_thread, panda, fake_send); Params params; RateKeeper rk("pandad", 100); SubMaster sm({"selfdriveState"}); PubMaster pm({"can", "pandaStates", "peripheralState"}); - PandaSafety panda_safety(pandas); - Panda *peripheral_panda = pandas[0]; + PandaSafety panda_safety(panda); bool engaged = false; bool is_onroad = false; // Main loop: receive CAN data and process states - while (!do_exit && check_all_connected(pandas)) { - can_recv(pandas, &pm); + while (!do_exit && check_connected(panda)) { + can_recv(panda, &pm); // Process peripheral state at 20 Hz if (rk.frame() % 5 == 0) { - process_peripheral_state(peripheral_panda, &pm, no_fan_control); + process_peripheral_state(panda, &pm, no_fan_control); } // Process panda state at 10 Hz @@ -460,25 +378,23 @@ void pandad_run(std::vector &pandas) { sm.update(0); engaged = sm.allAliveAndValid({"selfdriveState"}) && sm["selfdriveState"].getSelfdriveState().getEnabled(); is_onroad = params.getBool("IsOnroad"); - process_panda_state(pandas, &pm, engaged, is_onroad, spoofing_started); + process_panda_state(panda, &pm, engaged, is_onroad, spoofing_started); panda_safety.configureSafetyMode(is_onroad); } // Send out peripheralState at 2Hz if (rk.frame() % 50 == 0) { - send_peripheral_state(peripheral_panda, &pm); + send_peripheral_state(panda, &pm); } - // Forward logs from pandas to cloudlog if available - for (auto *panda : pandas) { - std::string log = panda->serial_read(); - if (!log.empty()) { - if (log.find("Register 0x") != std::string::npos) { - // Log register divergent faults as errors - LOGE("%s", log.c_str()); - } else { - LOGD("%s", log.c_str()); - } + // Forward logs from panda to cloudlog if available + std::string log = panda->serial_read(); + if (!log.empty()) { + if (log.find("Register 0x") != std::string::npos) { + // Log register divergent faults as errors + LOGE("%s", log.c_str()); + } else { + LOGD("%s", log.c_str()); } } @@ -487,52 +403,38 @@ void pandad_run(std::vector &pandas) { // Close relay on exit to prevent a fault if (is_onroad && !engaged) { - for (auto &p : pandas) { - if (p->connected()) { - p->set_safety_model(cereal::CarParams::SafetyModel::NO_OUTPUT); - } + if (panda->connected()) { + panda->set_safety_model(cereal::CarParams::SafetyModel::NO_OUTPUT); } } send_thread.join(); } -void pandad_main_thread(std::vector serials) { - if (serials.size() == 0) { - serials = Panda::list(); +void pandad_main_thread(std::string serial) { + if (serial.empty()) { + auto serials = Panda::list(); - if (serials.size() == 0) { + if (serials.empty()) { LOGW("no pandas found, exiting"); return; } + serial = serials[0]; } - std::string serials_str; - for (int i = 0; i < serials.size(); i++) { - serials_str += serials[i]; - if (i < serials.size() - 1) serials_str += ", "; - } - LOGW("connecting to pandas: %s", serials_str.c_str()); + LOGW("connecting to panda: %s", serial.c_str()); - // connect to all provided serials - std::vector pandas; - for (int i = 0; i < serials.size() && !do_exit; /**/) { - Panda *p = connect(serials[i], i); - if (!p) { - util::sleep_for(100); - continue; - } - - pandas.push_back(p); - ++i; + Panda *panda = nullptr; + while (!do_exit) { + panda = connect(serial); + if (panda) break; + util::sleep_for(100); } if (!do_exit) { - LOGW("connected to all pandas"); - pandad_run(pandas); + LOGW("connected to panda"); + pandad_run(panda); } - for (Panda *panda : pandas) { - delete panda; - } + delete panda; } diff --git a/selfdrive/pandad/pandad.h b/selfdrive/pandad/pandad.h index 637807e074..aa10d1ae4b 100644 --- a/selfdrive/pandad/pandad.h +++ b/selfdrive/pandad/pandad.h @@ -1,16 +1,15 @@ #pragma once #include -#include #include "common/params.h" #include "selfdrive/pandad/panda.h" -void pandad_main_thread(std::vector serials); +void pandad_main_thread(std::string serial); class PandaSafety { public: - PandaSafety(const std::vector &pandas) : pandas_(pandas) {} + PandaSafety(Panda *panda) : panda_(panda) {} void configureSafetyMode(bool is_onroad); private: @@ -22,6 +21,6 @@ private: bool log_once_ = false; bool safety_configured_ = false; bool prev_obd_multiplexing_ = false; - std::vector pandas_; + Panda *panda_; Params params_; }; diff --git a/selfdrive/pandad/pandad.py b/selfdrive/pandad/pandad.py index d5c58ddd6d..df2b4f7ee8 100755 --- a/selfdrive/pandad/pandad.py +++ b/selfdrive/pandad/pandad.py @@ -110,46 +110,35 @@ def main() -> None: cloudlog.info(f"{len(panda_serials)} panda(s) found, connecting - {panda_serials}") - # Flash pandas - pandas: list[Panda] = [] - for serial in panda_serials: - pandas.append(flash_panda(serial)) + # Flash the first panda + panda_serial = panda_serials[0] + panda = flash_panda(panda_serial) # Ensure internal panda is present if expected - internal_pandas = [panda for panda in pandas if panda.is_internal()] - if HARDWARE.has_internal_panda() and len(internal_pandas) == 0: + if HARDWARE.has_internal_panda() and not panda.is_internal(): cloudlog.error("Internal panda is missing, trying again") no_internal_panda_count += 1 continue no_internal_panda_count = 0 - # sort pandas to have deterministic order - # * the internal one is always first - # * then sort by hardware type - # * as a last resort, sort by serial number - pandas.sort(key=lambda x: (not x.is_internal(), x.get_type(), x.get_usb_serial())) - panda_serials = [p.get_usb_serial() for p in pandas] + # log panda fw version + params.put("PandaSignatures", panda.get_signature()) - # log panda fw versions - params.put("PandaSignatures", b','.join(p.get_signature() for p in pandas)) + # check health for lost heartbeat + health = panda.health() + if health["heartbeat_lost"]: + params.put_bool("PandaHeartbeatLost", True) + cloudlog.event("heartbeat lost", deviceState=health, serial=panda.get_usb_serial()) + if health["som_reset_triggered"]: + params.put_bool("PandaSomResetTriggered", True) + cloudlog.event("panda.som_reset_triggered", health=health, serial=panda.get_usb_serial()) - for panda in pandas: - # check health for lost heartbeat - health = panda.health() - if health["heartbeat_lost"]: - params.put_bool("PandaHeartbeatLost", True) - cloudlog.event("heartbeat lost", deviceState=health, serial=panda.get_usb_serial()) - if health["som_reset_triggered"]: - params.put_bool("PandaSomResetTriggered", True) - cloudlog.event("panda.som_reset_triggered", health=health, serial=panda.get_usb_serial()) + if first_run: + # reset panda to ensure we're in a good state + cloudlog.info(f"Resetting panda {panda.get_usb_serial()}") + panda.reset(reconnect=True) - if first_run: - # reset panda to ensure we're in a good state - cloudlog.info(f"Resetting panda {panda.get_usb_serial()}") - panda.reset(reconnect=True) - - for p in pandas: - p.close() + panda.close() # TODO: wrap all panda exceptions in a base panda exception except (usb1.USBErrorNoDevice, usb1.USBErrorPipe): # a panda was disconnected while setting everything up. let's try again @@ -166,7 +155,7 @@ def main() -> None: # run pandad with all connected serials as arguments os.environ['MANAGER_DAEMON'] = 'pandad' - process = subprocess.Popen(["./pandad", *panda_serials], cwd=os.path.join(BASEDIR, "selfdrive/pandad")) + process = subprocess.Popen(["./pandad", panda_serial], cwd=os.path.join(BASEDIR, "selfdrive/pandad")) process.wait() diff --git a/selfdrive/pandad/spi.cc b/selfdrive/pandad/spi.cc index b6ee57801a..25682e6b17 100644 --- a/selfdrive/pandad/spi.cc +++ b/selfdrive/pandad/spi.cc @@ -1,4 +1,3 @@ -#ifndef __APPLE__ #include #include #include @@ -55,7 +54,7 @@ private: util::hexdump(tx_buf, std::min((int)header.tx_len, 8)).c_str()); \ } while (0) -PandaSpiHandle::PandaSpiHandle(std::string serial) : PandaCommsHandle(serial) { +PandaSpiHandle::PandaSpiHandle(std::string serial) { int ret; const int uid_len = 12; uint8_t uid[uid_len] = {0}; @@ -407,4 +406,3 @@ fail: if (ret >= 0) ret = -1; return ret; } -#endif diff --git a/selfdrive/pandad/tests/test_pandad_usbprotocol.cc b/selfdrive/pandad/tests/test_pandad_canprotocol.cc similarity index 88% rename from selfdrive/pandad/tests/test_pandad_usbprotocol.cc rename to selfdrive/pandad/tests/test_pandad_canprotocol.cc index 11f7184efd..9b53392f6b 100644 --- a/selfdrive/pandad/tests/test_pandad_usbprotocol.cc +++ b/selfdrive/pandad/tests/test_pandad_canprotocol.cc @@ -1,13 +1,15 @@ #define CATCH_CONFIG_MAIN #define CATCH_CONFIG_ENABLE_BENCHMARKING +#include + #include "catch2/catch.hpp" #include "cereal/messaging/messaging.h" #include "common/util.h" #include "selfdrive/pandad/panda.h" struct PandaTest : public Panda { - PandaTest(uint32_t bus_offset, int can_list_size, cereal::PandaState::PandaType hw_type); + PandaTest(int can_list_size, cereal::PandaState::PandaType hw_type); void test_can_send(); void test_can_recv(uint32_t chunk_size = 0); void test_chunked_can_recv(); @@ -19,7 +21,7 @@ struct PandaTest : public Panda { capnp::List::Reader can_data_list; }; -PandaTest::PandaTest(uint32_t bus_offset_, int can_list_size, cereal::PandaState::PandaType hw_type) : can_list_size(can_list_size), Panda(bus_offset_) { +PandaTest::PandaTest(int can_list_size, cereal::PandaState::PandaType hw_type) : can_list_size(can_list_size), Panda() { this->hw_type = hw_type; int data_limit = ((hw_type == cereal::PandaState::PandaType::RED_PANDA) ? std::size(dlc_to_len) : 8); // prepare test data @@ -40,7 +42,7 @@ PandaTest::PandaTest(uint32_t bus_offset_, int can_list_size, cereal::PandaState uint32_t id = util::random_int(0, std::size(dlc_to_len) - 1); const std::string &dat = test_data[dlc_to_len[id]]; can.setAddress(i); - can.setSrc(util::random_int(0, 2) + bus_offset); + can.setSrc(util::random_int(0, 2)); can.setDat(kj::ArrayPtr((uint8_t *)dat.data(), dat.size())); total_pakets_size += sizeof(can_header) + dat.size(); } @@ -103,9 +105,8 @@ void PandaTest::test_can_recv(uint32_t rx_chunk_size) { } TEST_CASE("send/recv CAN 2.0 packets") { - auto bus_offset = GENERATE(0, 4); auto can_list_size = GENERATE(1, 3, 5, 10, 30, 60, 100, 200); - PandaTest test(bus_offset, can_list_size, cereal::PandaState::PandaType::DOS); + PandaTest test(can_list_size, cereal::PandaState::PandaType::DOS); SECTION("can_send") { test.test_can_send(); @@ -119,9 +120,8 @@ TEST_CASE("send/recv CAN 2.0 packets") { } TEST_CASE("send/recv CAN FD packets") { - auto bus_offset = GENERATE(0, 4); auto can_list_size = GENERATE(1, 3, 5, 10, 30, 60, 100, 200); - PandaTest test(bus_offset, can_list_size, cereal::PandaState::PandaType::RED_PANDA); + PandaTest test(can_list_size, cereal::PandaState::PandaType::RED_PANDA); SECTION("can_send") { test.test_can_send(); diff --git a/selfdrive/pandad/tests/test_pandad_loopback.py b/selfdrive/pandad/tests/test_pandad_loopback.py index eff70d2544..fd4a99be62 100644 --- a/selfdrive/pandad/tests/test_pandad_loopback.py +++ b/selfdrive/pandad/tests/test_pandad_loopback.py @@ -13,12 +13,11 @@ from openpilot.common.utils import retry from openpilot.common.params import Params from openpilot.common.timeout import Timeout from openpilot.selfdrive.pandad import can_list_to_can_capnp -from openpilot.system.hardware import TICI from openpilot.selfdrive.test.helpers import with_processes @retry(attempts=3) -def setup_pandad(num_pandas): +def setup_pandad(): params = Params() params.clear_all() params.put_bool("IsOnroad", False) @@ -29,16 +28,12 @@ def setup_pandad(num_pandas): any(ps.pandaType == log.PandaState.PandaType.unknown for ps in sm['pandaStates']): sm.update(1000) - found_pandas = len(sm['pandaStates']) - assert num_pandas == found_pandas, "connected pandas ({found_pandas}) doesn't match expected panda count ({num_pandas}). \ - connect another panda for multipanda tests." - # pandad safety setting relies on these params cp = car.CarParams.new_message() safety_config = car.CarParams.SafetyConfig.new_message() safety_config.safetyModel = car.CarParams.SafetyModel.allOutput - cp.safetyConfigs = [safety_config]*num_pandas + cp.safetyConfigs = [safety_config] params.put_bool("IsOnroad", True) params.put_bool("FirmwareQueryDone", True) @@ -49,12 +44,12 @@ def setup_pandad(num_pandas): while any(ps.safetyModel != car.CarParams.SafetyModel.allOutput for ps in sm['pandaStates']): sm.update(1000) -def send_random_can_messages(sendcan, count, num_pandas=1): +def send_random_can_messages(sendcan, count): sent_msgs = defaultdict(set) for _ in range(count): to_send = [] for __ in range(random.randrange(20)): - bus = random.choice([b for b in range(3*num_pandas) if b % 4 != 3]) + bus = random.choice(range(3)) addr = random.randrange(1, 1<<29) dat = bytes(random.getrandbits(8) for _ in range(random.randrange(1, 9))) if (addr, dat) in sent_msgs[bus]: @@ -74,8 +69,7 @@ class TestBoarddLoopback: @with_processes(['pandad']) def test_loopback(self): - num_pandas = 2 if TICI and "SINGLE_PANDA" not in os.environ else 1 - setup_pandad(num_pandas) + setup_pandad() sendcan = messaging.pub_sock('sendcan') can = messaging.sub_sock('can', conflate=False, timeout=100) @@ -86,7 +80,7 @@ class TestBoarddLoopback: for i in range(n): print(f"pandad loopback {i}/{n}") - sent_msgs = send_random_can_messages(sendcan, random.randrange(20, 100), num_pandas) + sent_msgs = send_random_can_messages(sendcan, random.randrange(20, 100)) sent_loopback = copy.deepcopy(sent_msgs) sent_loopback.update({k+128: copy.deepcopy(v) for k, v in sent_msgs.items()}) diff --git a/selfdrive/pandad/tests/test_pandad_spi.py b/selfdrive/pandad/tests/test_pandad_spi.py index da4b181993..69dfb67e93 100644 --- a/selfdrive/pandad/tests/test_pandad_spi.py +++ b/selfdrive/pandad/tests/test_pandad_spi.py @@ -22,7 +22,7 @@ class TestBoarddSpi: @with_processes(['pandad']) def test_spi_corruption(self, subtests): - setup_pandad(1) + setup_pandad() sendcan = messaging.pub_sock('sendcan') socks = {s: messaging.sub_sock(s, conflate=False, timeout=100) for s in ('can', 'pandaStates', 'peripheralState')} From eea07462fad217a2e98decf7392818dbb0f22478 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sat, 14 Feb 2026 21:00:29 -0800 Subject: [PATCH 038/311] Drop support for Intel macOS (#37215) * Drop support for Intel macOS * arch.sh * scons * platform.sh * lil more * mv tici --- SConstruct | 17 ++-------- scripts/platform.sh | 51 ++++++++++++++++++++++++++++ third_party/acados/build.sh | 11 +++--- third_party/libyuv/build.sh | 15 ++------ third_party/raylib/build.sh | 8 ++--- tools/install_python_dependencies.sh | 2 +- tools/mac_setup.sh | 11 ++---- tools/op.sh | 28 +-------------- tools/plotjuggler/juggle.py | 2 +- 9 files changed, 69 insertions(+), 76 deletions(-) create mode 100755 scripts/platform.sh diff --git a/SConstruct b/SConstruct index 4f04be624c..05885c0b5d 100644 --- a/SConstruct +++ b/SConstruct @@ -2,7 +2,6 @@ import os import subprocess import sys import sysconfig -import platform import shlex import numpy as np @@ -24,19 +23,8 @@ AddOption('--minimal', default=os.path.exists(File('#.gitattributes').abspath), # minimal by default on release branch (where there's no LFS) help='the minimum build to run openpilot. no tests, tools, etc.') -# Detect platform -arch = subprocess.check_output(["uname", "-m"], encoding='utf8').rstrip() -if platform.system() == "Darwin": - arch = "Darwin" - brew_prefix = subprocess.check_output(['brew', '--prefix'], encoding='utf8').strip() -elif arch == "aarch64" and os.path.isfile('/TICI'): - arch = "larch64" -assert arch in [ - "larch64", # linux tici arm64 - "aarch64", # linux pc arm64 - "x86_64", # linux pc x64 - "Darwin", # macOS arm64 (x86 not supported) -] +# Detect platform (see scripts/platform.sh) +arch = subprocess.check_output(["bash", "-c", "source scripts/platform.sh >&2 && echo $OPENPILOT_ARCH"], encoding='utf8', stderr=subprocess.PIPE).rstrip() env = Environment( ENV={ @@ -103,6 +91,7 @@ if arch == "larch64": env.Append(CCFLAGS=arch_flags) env.Append(CXXFLAGS=arch_flags) elif arch == "Darwin": + brew_prefix = subprocess.check_output(['brew', '--prefix'], encoding='utf8').strip() env.Append(LIBPATH=[ f"{brew_prefix}/lib", f"{brew_prefix}/opt/openssl@3.0/lib", diff --git a/scripts/platform.sh b/scripts/platform.sh new file mode 100755 index 0000000000..1cbd8aa607 --- /dev/null +++ b/scripts/platform.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +# +# Centralized platform and architecture detection for openpilot. +# Source this script to get OPENPILOT_ARCH set to one of: +# larch64 - linux tici arm64 +# aarch64 - linux pc arm64 +# x86_64 - linux pc x64 +# Darwin - macOS arm64 +# + +RED='\033[0;31m' +GREEN='\033[0;32m' +NC='\033[0m' + +OPENPILOT_ARCH=$(uname -m) + +# ── check OS and normalize arch ────────────────────────────── +if [ -f /TICI ]; then + # TICI runs AGNOS — no OS validation needed + OPENPILOT_ARCH="larch64" + +elif [[ "$OSTYPE" == "darwin"* ]]; then + if [[ "$OPENPILOT_ARCH" == "x86_64" ]]; then + echo -e " ↳ [${RED}✗${NC}] Intel-based Macs are not supported!" + echo " openpilot requires an Apple Silicon Mac (M1 or newer)." + exit 1 + fi + echo -e " ↳ [${GREEN}✔${NC}] macOS detected." + OPENPILOT_ARCH="Darwin" + +elif [[ "$OSTYPE" == "linux-gnu"* ]]; then + if [ -f "/etc/os-release" ]; then + source /etc/os-release + case "$VERSION_CODENAME" in + "jammy" | "kinetic" | "noble" | "focal") + echo -e " ↳ [${GREEN}✔${NC}] Ubuntu $VERSION_CODENAME detected." + ;; + *) + echo -e " ↳ [${RED}✗${NC}] Incompatible Ubuntu version $VERSION_CODENAME detected!" + exit 1 + ;; + esac + else + echo -e " ↳ [${RED}✗${NC}] No /etc/os-release on your system. Make sure you're running on Ubuntu, or similar!" + exit 1 + fi + +else + echo -e " ↳ [${RED}✗${NC}] OS type $OSTYPE not supported!" + exit 1 +fi diff --git a/third_party/acados/build.sh b/third_party/acados/build.sh index 2c4d839ae7..cca1743506 100755 --- a/third_party/acados/build.sh +++ b/third_party/acados/build.sh @@ -6,18 +6,18 @@ export ZERO_AR_DATE=1 DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" -ARCHNAME="x86_64" +source "$DIR/../../scripts/platform.sh" +ARCHNAME="$OPENPILOT_ARCH" + BLAS_TARGET="X64_AUTOMATIC" if [ -f /TICI ]; then - ARCHNAME="larch64" BLAS_TARGET="ARMV8A_ARM_CORTEX_A57" fi ACADOS_FLAGS="-DACADOS_WITH_QPOASES=ON -UBLASFEO_TARGET -DBLASFEO_TARGET=$BLAS_TARGET" if [[ "$OSTYPE" == "darwin"* ]]; then - ACADOS_FLAGS="$ACADOS_FLAGS -DCMAKE_OSX_ARCHITECTURES=arm64;x86_64 -DCMAKE_MACOSX_RPATH=1" - ARCHNAME="Darwin" + ACADOS_FLAGS="$ACADOS_FLAGS -DCMAKE_OSX_ARCHITECTURES=arm64 -DCMAKE_MACOSX_RPATH=1" fi if [ ! -d acados_repo/ ]; then @@ -57,8 +57,7 @@ fi cd $DIR/acados_repo/interfaces/acados_template/tera_renderer/ if [[ "$OSTYPE" == "darwin"* ]]; then cargo build --verbose --release --target aarch64-apple-darwin - cargo build --verbose --release --target x86_64-apple-darwin - lipo -create -output target/release/t_renderer target/x86_64-apple-darwin/release/t_renderer target/aarch64-apple-darwin/release/t_renderer + cp target/aarch64-apple-darwin/release/t_renderer target/release/t_renderer else cargo build --verbose --release fi diff --git a/third_party/libyuv/build.sh b/third_party/libyuv/build.sh index 35a7d947a2..0e4e10133b 100755 --- a/third_party/libyuv/build.sh +++ b/third_party/libyuv/build.sh @@ -6,14 +6,8 @@ export ZERO_AR_DATE=1 DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" -ARCHNAME=$(uname -m) -if [ -f /TICI ]; then - ARCHNAME="larch64" -fi - -if [[ "$OSTYPE" == "darwin"* ]]; then - ARCHNAME="Darwin" -fi +source "$DIR/../../scripts/platform.sh" +ARCHNAME="$OPENPILOT_ARCH" cd $DIR if [ ! -d libyuv ]; then @@ -35,8 +29,3 @@ rm -rf $DIR/include mkdir -p $INSTALL_DIR/lib cp $DIR/libyuv/libyuv.a $INSTALL_DIR/lib cp -r $DIR/libyuv/include $DIR - -## To create universal binary on Darwin: -## ``` -## lipo -create -output Darwin/libyuv.a path-to-x64/libyuv.a path-to-arm64/libyuv.a -## ``` diff --git a/third_party/raylib/build.sh b/third_party/raylib/build.sh index d20f9d33af..b8408cd88c 100755 --- a/third_party/raylib/build.sh +++ b/third_party/raylib/build.sh @@ -20,9 +20,9 @@ cd $DIR RAYLIB_PLATFORM="PLATFORM_DESKTOP" -ARCHNAME=$(uname -m) +source "$DIR/../../scripts/platform.sh" +ARCHNAME="$OPENPILOT_ARCH" if [ -f /TICI ]; then - ARCHNAME="larch64" RAYLIB_PLATFORM="PLATFORM_COMMA" elif [[ "$OSTYPE" == "linux"* ]]; then # required dependencies on Linux PC @@ -33,10 +33,6 @@ elif [[ "$OSTYPE" == "linux"* ]]; then libxrandr-dev fi -if [[ "$OSTYPE" == "darwin"* ]]; then - ARCHNAME="Darwin" -fi - INSTALL_DIR="$DIR/$ARCHNAME" rm -rf $INSTALL_DIR mkdir -p $INSTALL_DIR diff --git a/tools/install_python_dependencies.sh b/tools/install_python_dependencies.sh index 4454845fcd..bf7accbbd5 100755 --- a/tools/install_python_dependencies.sh +++ b/tools/install_python_dependencies.sh @@ -23,7 +23,7 @@ echo "installing python packages..." uv sync --frozen --all-extras source .venv/bin/activate -if [[ "$(uname)" == 'Darwin' ]]; then +if [[ "$OSTYPE" == "darwin"* ]]; then touch "$ROOT"/.env echo "export OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES" >> "$ROOT"/.env fi diff --git a/tools/mac_setup.sh b/tools/mac_setup.sh index 26ef4dd9a5..7e126d4f57 100755 --- a/tools/mac_setup.sh +++ b/tools/mac_setup.sh @@ -3,7 +3,7 @@ set -e DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" ROOT="$(cd $DIR/../ && pwd)" -ARCH=$(uname -m) +source $ROOT/scripts/platform.sh # homebrew update is slow export HOMEBREW_NO_AUTO_UPDATE=1 @@ -21,13 +21,8 @@ if [[ $(command -v brew) == "" ]]; then echo "[ ] installed brew t=$SECONDS" # make brew available now - if [[ $ARCH == "x86_64" ]]; then - echo 'eval "$(/usr/local/bin/brew shellenv)"' >> $RC_FILE - eval "$(/usr/local/bin/brew shellenv)" - else - echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> $RC_FILE - eval "$(/opt/homebrew/bin/brew shellenv)" - fi + echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> $RC_FILE + eval "$(/opt/homebrew/bin/brew shellenv)" else brew up fi diff --git a/tools/op.sh b/tools/op.sh index 8c41926e0c..afee4cfb7e 100755 --- a/tools/op.sh +++ b/tools/op.sh @@ -122,33 +122,7 @@ function op_check_git() { function op_check_os() { echo "Checking for compatible os version..." - if [[ "$OSTYPE" == "linux-gnu"* ]]; then - - if [ -f "/etc/os-release" ]; then - source /etc/os-release - case "$VERSION_CODENAME" in - "jammy" | "kinetic" | "noble" | "focal") - echo -e " ↳ [${GREEN}✔${NC}] Ubuntu $VERSION_CODENAME detected." - ;; - * ) - echo -e " ↳ [${RED}✗${NC}] Incompatible Ubuntu version $VERSION_CODENAME detected!" - loge "ERROR_INCOMPATIBLE_UBUNTU" "$VERSION_CODENAME" - return 1 - ;; - esac - else - echo -e " ↳ [${RED}✗${NC}] No /etc/os-release on your system. Make sure you're running on Ubuntu, or similar!" - loge "ERROR_UNKNOWN_UBUNTU" - return 1 - fi - - elif [[ "$OSTYPE" == "darwin"* ]]; then - echo -e " ↳ [${GREEN}✔${NC}] macOS detected." - else - echo -e " ↳ [${RED}✗${NC}] OS type $OSTYPE not supported!" - loge "ERROR_UNKNOWN_OS" "$OSTYPE" - return 1 - fi + source "$OPENPILOT_ROOT/scripts/platform.sh" } function op_check_python() { diff --git a/tools/plotjuggler/juggle.py b/tools/plotjuggler/juggle.py index 142e640504..c04efd50b4 100755 --- a/tools/plotjuggler/juggle.py +++ b/tools/plotjuggler/juggle.py @@ -31,7 +31,7 @@ MAX_STREAMING_BUFFER_SIZE = 1000 def install(): m = f"{platform.system()}-{platform.machine()}" - supported = ("Linux-x86_64", "Linux-aarch64", "Darwin-arm64", "Darwin-x86_64") + supported = ("Linux-x86_64", "Linux-aarch64", "Darwin-arm64") if m not in supported: raise Exception(f"Unsupported platform: '{m}'. Supported platforms: {supported}") From f67f84109e633908e922a59b1c9a05e130dd61f4 Mon Sep 17 00:00:00 2001 From: commaci-public <60409688+commaci-public@users.noreply.github.com> Date: Sat, 14 Feb 2026 21:02:11 -0800 Subject: [PATCH 039/311] [bot] Update Python packages (#37166) * Update Python packages * clear cache * try this * Revert "try this" This reverts commit 79f21ea0956509c58e5e5ba65a08ae0b2cbd204b. * Revert "clear cache" This reverts commit aa49ac5bd3b6cecb25cf9cbfe1e07ec4ad608d63. * revert for now --------- Co-authored-by: Vehicle Researcher Co-authored-by: Adeeb Shihadeh --- msgq_repo | 2 +- opendbc_repo | 2 +- panda | 2 +- uv.lock | 118 +++++++++++++++++++++++++-------------------------- 4 files changed, 62 insertions(+), 62 deletions(-) diff --git a/msgq_repo b/msgq_repo index 2c191c1a72..fa514e7dd7 160000 --- a/msgq_repo +++ b/msgq_repo @@ -1 +1 @@ -Subproject commit 2c191c1a72ae8119b93b49e1bc12d4a99b751855 +Subproject commit fa514e7dd77025952d94e893c622a5006e260321 diff --git a/opendbc_repo b/opendbc_repo index 143e87f3e4..245cb1f205 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit 143e87f3e4565abd1d2d70c32adcb3167d9c64ca +Subproject commit 245cb1f2056071a7625e2ad7e4515f57784515bd diff --git a/panda b/panda index b99d796924..24fe11466d 160000 --- a/panda +++ b/panda @@ -1 +1 @@ -Subproject commit b99d79692409514d296d85f367897fa2f86daaa5 +Subproject commit 24fe11466de37a1a761c16e35353ab39602e03e0 diff --git a/uv.lock b/uv.lock index 8909934e9a..2f095265b4 100644 --- a/uv.lock +++ b/uv.lock @@ -415,11 +415,11 @@ wheels = [ [[package]] name = "filelock" -version = "3.20.3" +version = "3.24.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1d/65/ce7f1b70157833bf3cb851b556a37d4547ceafc158aa9b34b36782f23696/filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1", size = 19485, upload-time = "2026-01-09T17:55:05.421Z" } +sdist = { url = "https://files.pythonhosted.org/packages/00/cd/fa3ab025a8f9772e8a9146d8fd8eef6d62649274d231ca84249f54a0de4a/filelock-3.24.0.tar.gz", hash = "sha256:aeeab479339ddf463a1cdd1f15a6e6894db976071e5883efc94d22ed5139044b", size = 37166, upload-time = "2026-02-14T16:05:28.723Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701, upload-time = "2026-01-09T17:55:04.334Z" }, + { url = "https://files.pythonhosted.org/packages/d9/dd/d7e7f4f49180e8591c9e1281d15ecf8e7f25eb2c829771d9682f1f9fe0c8/filelock-3.24.0-py3-none-any.whl", hash = "sha256:eebebb403d78363ef7be8e236b63cc6760b0004c7464dceaba3fd0afbd637ced", size = 23977, upload-time = "2026-02-14T16:05:27.578Z" }, ] [[package]] @@ -1144,30 +1144,30 @@ wheels = [ [[package]] name = "pillow" -version = "12.1.0" +version = "12.1.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d0/02/d52c733a2452ef1ffcc123b68e6606d07276b0e358db70eabad7e40042b7/pillow-12.1.0.tar.gz", hash = "sha256:5c5ae0a06e9ea030ab786b0251b32c7e4ce10e58d983c0d5c56029455180b5b9", size = 46977283, upload-time = "2026-01-02T09:13:29.892Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/42/5c74462b4fd957fcd7b13b04fb3205ff8349236ea74c7c375766d6c82288/pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4", size = 46980264, upload-time = "2026-02-11T04:23:07.146Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/31/dc53fe21a2f2996e1b7d92bf671cdb157079385183ef7c1ae08b485db510/pillow-12.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a332ac4ccb84b6dde65dbace8431f3af08874bf9770719d32a635c4ef411b18b", size = 5262642, upload-time = "2026-01-02T09:11:10.138Z" }, - { url = "https://files.pythonhosted.org/packages/ab/c1/10e45ac9cc79419cedf5121b42dcca5a50ad2b601fa080f58c22fb27626e/pillow-12.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:907bfa8a9cb790748a9aa4513e37c88c59660da3bcfffbd24a7d9e6abf224551", size = 4657464, upload-time = "2026-01-02T09:11:12.319Z" }, - { url = "https://files.pythonhosted.org/packages/ad/26/7b82c0ab7ef40ebede7a97c72d473bda5950f609f8e0c77b04af574a0ddb/pillow-12.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:efdc140e7b63b8f739d09a99033aa430accce485ff78e6d311973a67b6bf3208", size = 6234878, upload-time = "2026-01-02T09:11:14.096Z" }, - { url = "https://files.pythonhosted.org/packages/76/25/27abc9792615b5e886ca9411ba6637b675f1b77af3104710ac7353fe5605/pillow-12.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bef9768cab184e7ae6e559c032e95ba8d07b3023c289f79a2bd36e8bf85605a5", size = 8044868, upload-time = "2026-01-02T09:11:15.903Z" }, - { url = "https://files.pythonhosted.org/packages/0a/ea/f200a4c36d836100e7bc738fc48cd963d3ba6372ebc8298a889e0cfc3359/pillow-12.1.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:742aea052cf5ab5034a53c3846165bc3ce88d7c38e954120db0ab867ca242661", size = 6349468, upload-time = "2026-01-02T09:11:17.631Z" }, - { url = "https://files.pythonhosted.org/packages/11/8f/48d0b77ab2200374c66d344459b8958c86693be99526450e7aee714e03e4/pillow-12.1.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a6dfc2af5b082b635af6e08e0d1f9f1c4e04d17d4e2ca0ef96131e85eda6eb17", size = 7041518, upload-time = "2026-01-02T09:11:19.389Z" }, - { url = "https://files.pythonhosted.org/packages/1d/23/c281182eb986b5d31f0a76d2a2c8cd41722d6fb8ed07521e802f9bba52de/pillow-12.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:609e89d9f90b581c8d16358c9087df76024cf058fa693dd3e1e1620823f39670", size = 6462829, upload-time = "2026-01-02T09:11:21.28Z" }, - { url = "https://files.pythonhosted.org/packages/25/ef/7018273e0faac099d7b00982abdcc39142ae6f3bd9ceb06de09779c4a9d6/pillow-12.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:43b4899cfd091a9693a1278c4982f3e50f7fb7cff5153b05174b4afc9593b616", size = 7166756, upload-time = "2026-01-02T09:11:23.559Z" }, - { url = "https://files.pythonhosted.org/packages/8f/c8/993d4b7ab2e341fe02ceef9576afcf5830cdec640be2ac5bee1820d693d4/pillow-12.1.0-cp312-cp312-win32.whl", hash = "sha256:aa0c9cc0b82b14766a99fbe6084409972266e82f459821cd26997a488a7261a7", size = 6328770, upload-time = "2026-01-02T09:11:25.661Z" }, - { url = "https://files.pythonhosted.org/packages/a7/87/90b358775a3f02765d87655237229ba64a997b87efa8ccaca7dd3e36e7a7/pillow-12.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:d70534cea9e7966169ad29a903b99fc507e932069a881d0965a1a84bb57f6c6d", size = 7033406, upload-time = "2026-01-02T09:11:27.474Z" }, - { url = "https://files.pythonhosted.org/packages/5d/cf/881b457eccacac9e5b2ddd97d5071fb6d668307c57cbf4e3b5278e06e536/pillow-12.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:65b80c1ee7e14a87d6a068dd3b0aea268ffcabfe0498d38661b00c5b4b22e74c", size = 2452612, upload-time = "2026-01-02T09:11:29.309Z" }, + { url = "https://files.pythonhosted.org/packages/07/d3/8df65da0d4df36b094351dce696f2989bec731d4f10e743b1c5f4da4d3bf/pillow-12.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab323b787d6e18b3d91a72fc99b1a2c28651e4358749842b8f8dfacd28ef2052", size = 5262803, upload-time = "2026-02-11T04:20:47.653Z" }, + { url = "https://files.pythonhosted.org/packages/d6/71/5026395b290ff404b836e636f51d7297e6c83beceaa87c592718747e670f/pillow-12.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:adebb5bee0f0af4909c30db0d890c773d1a92ffe83da908e2e9e720f8edf3984", size = 4657601, upload-time = "2026-02-11T04:20:49.328Z" }, + { url = "https://files.pythonhosted.org/packages/b1/2e/1001613d941c67442f745aff0f7cc66dd8df9a9c084eb497e6a543ee6f7e/pillow-12.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb66b7cc26f50977108790e2456b7921e773f23db5630261102233eb355a3b79", size = 6234995, upload-time = "2026-02-11T04:20:51.032Z" }, + { url = "https://files.pythonhosted.org/packages/07/26/246ab11455b2549b9233dbd44d358d033a2f780fa9007b61a913c5b2d24e/pillow-12.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee2810642b2898bb187ced9b349e95d2a7272930796e022efaf12e99dccd293", size = 8045012, upload-time = "2026-02-11T04:20:52.882Z" }, + { url = "https://files.pythonhosted.org/packages/b2/8b/07587069c27be7535ac1fe33874e32de118fbd34e2a73b7f83436a88368c/pillow-12.1.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0b1cd6232e2b618adcc54d9882e4e662a089d5768cd188f7c245b4c8c44a397", size = 6349638, upload-time = "2026-02-11T04:20:54.444Z" }, + { url = "https://files.pythonhosted.org/packages/ff/79/6df7b2ee763d619cda2fb4fea498e5f79d984dae304d45a8999b80d6cf5c/pillow-12.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7aac39bcf8d4770d089588a2e1dd111cbaa42df5a94be3114222057d68336bd0", size = 7041540, upload-time = "2026-02-11T04:20:55.97Z" }, + { url = "https://files.pythonhosted.org/packages/2c/5e/2ba19e7e7236d7529f4d873bdaf317a318896bac289abebd4bb00ef247f0/pillow-12.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ab174cd7d29a62dd139c44bf74b698039328f45cb03b4596c43473a46656b2f3", size = 6462613, upload-time = "2026-02-11T04:20:57.542Z" }, + { url = "https://files.pythonhosted.org/packages/03/03/31216ec124bb5c3dacd74ce8efff4cc7f52643653bad4825f8f08c697743/pillow-12.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:339ffdcb7cbeaa08221cd401d517d4b1fe7a9ed5d400e4a8039719238620ca35", size = 7166745, upload-time = "2026-02-11T04:20:59.196Z" }, + { url = "https://files.pythonhosted.org/packages/1f/e7/7c4552d80052337eb28653b617eafdef39adfb137c49dd7e831b8dc13bc5/pillow-12.1.1-cp312-cp312-win32.whl", hash = "sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a", size = 6328823, upload-time = "2026-02-11T04:21:01.385Z" }, + { url = "https://files.pythonhosted.org/packages/3d/17/688626d192d7261bbbf98846fc98995726bddc2c945344b65bec3a29d731/pillow-12.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6", size = 7033367, upload-time = "2026-02-11T04:21:03.536Z" }, + { url = "https://files.pythonhosted.org/packages/ed/fe/a0ef1f73f939b0eca03ee2c108d0043a87468664770612602c63266a43c4/pillow-12.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523", size = 2453811, upload-time = "2026-02-11T04:21:05.116Z" }, ] [[package]] name = "platformdirs" -version = "4.5.1" +version = "4.9.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/d5/763666321efaded11112de8b7a7f2273dd8d1e205168e73c334e54b0ab9a/platformdirs-4.9.1.tar.gz", hash = "sha256:f310f16e89c4e29117805d8328f7c10876eeff36c94eac879532812110f7d39f", size = 28392, upload-time = "2026-02-14T21:02:44.973Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, + { url = "https://files.pythonhosted.org/packages/70/77/e8c95e95f1d4cdd88c90a96e31980df7e709e51059fac150046ad67fac63/platformdirs-4.9.1-py3-none-any.whl", hash = "sha256:61d8b967d34791c162d30d60737369cbbd77debad5b981c4bfda1842e71e0d66", size = 21307, upload-time = "2026-02-14T21:02:43.492Z" }, ] [[package]] @@ -1328,14 +1328,14 @@ wheels = [ [[package]] name = "pyee" -version = "13.0.0" +version = "13.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/95/03/1fd98d5841cd7964a27d729ccf2199602fe05eb7a405c1462eb7277945ed/pyee-13.0.0.tar.gz", hash = "sha256:b391e3c5a434d1f5118a25615001dbc8f669cf410ab67d04c4d4e07c55481c37", size = 31250, upload-time = "2025-03-17T18:53:15.955Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8b/04/e7c1fe4dc78a6fdbfd6c337b1c3732ff543b8a397683ab38378447baa331/pyee-13.0.1.tar.gz", hash = "sha256:0b931f7c14535667ed4c7e0d531716368715e860b988770fc7eb8578d1f67fc8", size = 31655, upload-time = "2026-02-14T21:12:28.044Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/4d/b9add7c84060d4c1906abe9a7e5359f2a60f7a9a4f67268b2766673427d8/pyee-13.0.0-py3-none-any.whl", hash = "sha256:48195a3cddb3b1515ce0695ed76036b5ccc2ef3a9f963ff9f77aec0139845498", size = 15730, upload-time = "2025-03-17T18:53:14.532Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c4/b4d4827c93ef43c01f599ef31453ccc1c132b353284fc6c87d535c233129/pyee-13.0.1-py3-none-any.whl", hash = "sha256:af2f8fede4171ef667dfded53f96e2ed0d6e6bd7ee3bb46437f77e3b57689228", size = 15659, upload-time = "2026-02-14T21:12:26.263Z" }, ] [[package]] @@ -4055,27 +4055,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.15.0" +version = "0.15.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c8/39/5cee96809fbca590abea6b46c6d1c586b49663d1d2830a751cc8fc42c666/ruff-0.15.0.tar.gz", hash = "sha256:6bdea47cdbea30d40f8f8d7d69c0854ba7c15420ec75a26f463290949d7f7e9a", size = 4524893, upload-time = "2026-02-03T17:53:35.357Z" } +sdist = { url = "https://files.pythonhosted.org/packages/04/dc/4e6ac71b511b141cf626357a3946679abeba4cf67bc7cc5a17920f31e10d/ruff-0.15.1.tar.gz", hash = "sha256:c590fe13fb57c97141ae975c03a1aedb3d3156030cabd740d6ff0b0d601e203f", size = 4540855, upload-time = "2026-02-12T23:09:09.998Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/88/3fd1b0aa4b6330d6aaa63a285bc96c9f71970351579152d231ed90914586/ruff-0.15.0-py3-none-linux_armv6l.whl", hash = "sha256:aac4ebaa612a82b23d45964586f24ae9bc23ca101919f5590bdb368d74ad5455", size = 10354332, upload-time = "2026-02-03T17:52:54.892Z" }, - { url = "https://files.pythonhosted.org/packages/72/f6/62e173fbb7eb75cc29fe2576a1e20f0a46f671a2587b5f604bfb0eaf5f6f/ruff-0.15.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:dcd4be7cc75cfbbca24a98d04d0b9b36a270d0833241f776b788d59f4142b14d", size = 10767189, upload-time = "2026-02-03T17:53:19.778Z" }, - { url = "https://files.pythonhosted.org/packages/99/e4/968ae17b676d1d2ff101d56dc69cf333e3a4c985e1ec23803df84fc7bf9e/ruff-0.15.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d747e3319b2bce179c7c1eaad3d884dc0a199b5f4d5187620530adf9105268ce", size = 10075384, upload-time = "2026-02-03T17:53:29.241Z" }, - { url = "https://files.pythonhosted.org/packages/a2/bf/9843c6044ab9e20af879c751487e61333ca79a2c8c3058b15722386b8cae/ruff-0.15.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:650bd9c56ae03102c51a5e4b554d74d825ff3abe4db22b90fd32d816c2e90621", size = 10481363, upload-time = "2026-02-03T17:52:43.332Z" }, - { url = "https://files.pythonhosted.org/packages/55/d9/4ada5ccf4cd1f532db1c8d44b6f664f2208d3d93acbeec18f82315e15193/ruff-0.15.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6664b7eac559e3048223a2da77769c2f92b43a6dfd4720cef42654299a599c9", size = 10187736, upload-time = "2026-02-03T17:53:00.522Z" }, - { url = "https://files.pythonhosted.org/packages/86/e2/f25eaecd446af7bb132af0a1d5b135a62971a41f5366ff41d06d25e77a91/ruff-0.15.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f811f97b0f092b35320d1556f3353bf238763420ade5d9e62ebd2b73f2ff179", size = 10968415, upload-time = "2026-02-03T17:53:15.705Z" }, - { url = "https://files.pythonhosted.org/packages/e7/dc/f06a8558d06333bf79b497d29a50c3a673d9251214e0d7ec78f90b30aa79/ruff-0.15.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:761ec0a66680fab6454236635a39abaf14198818c8cdf691e036f4bc0f406b2d", size = 11809643, upload-time = "2026-02-03T17:53:23.031Z" }, - { url = "https://files.pythonhosted.org/packages/dd/45/0ece8db2c474ad7df13af3a6d50f76e22a09d078af63078f005057ca59eb/ruff-0.15.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:940f11c2604d317e797b289f4f9f3fa5555ffe4fb574b55ed006c3d9b6f0eb78", size = 11234787, upload-time = "2026-02-03T17:52:46.432Z" }, - { url = "https://files.pythonhosted.org/packages/8a/d9/0e3a81467a120fd265658d127db648e4d3acfe3e4f6f5d4ea79fac47e587/ruff-0.15.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcbca3d40558789126da91d7ef9a7c87772ee107033db7191edefa34e2c7f1b4", size = 11112797, upload-time = "2026-02-03T17:52:49.274Z" }, - { url = "https://files.pythonhosted.org/packages/b2/cb/8c0b3b0c692683f8ff31351dfb6241047fa873a4481a76df4335a8bff716/ruff-0.15.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9a121a96db1d75fa3eb39c4539e607f628920dd72ff1f7c5ee4f1b768ac62d6e", size = 11033133, upload-time = "2026-02-03T17:53:33.105Z" }, - { url = "https://files.pythonhosted.org/packages/f8/5e/23b87370cf0f9081a8c89a753e69a4e8778805b8802ccfe175cc410e50b9/ruff-0.15.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5298d518e493061f2eabd4abd067c7e4fb89e2f63291c94332e35631c07c3662", size = 10442646, upload-time = "2026-02-03T17:53:06.278Z" }, - { url = "https://files.pythonhosted.org/packages/e1/9a/3c94de5ce642830167e6d00b5c75aacd73e6347b4c7fc6828699b150a5ee/ruff-0.15.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:afb6e603d6375ff0d6b0cee563fa21ab570fd15e65c852cb24922cef25050cf1", size = 10195750, upload-time = "2026-02-03T17:53:26.084Z" }, - { url = "https://files.pythonhosted.org/packages/30/15/e396325080d600b436acc970848d69df9c13977942fb62bb8722d729bee8/ruff-0.15.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:77e515f6b15f828b94dc17d2b4ace334c9ddb7d9468c54b2f9ed2b9c1593ef16", size = 10676120, upload-time = "2026-02-03T17:53:09.363Z" }, - { url = "https://files.pythonhosted.org/packages/8d/c9/229a23d52a2983de1ad0fb0ee37d36e0257e6f28bfd6b498ee2c76361874/ruff-0.15.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6f6e80850a01eb13b3e42ee0ebdf6e4497151b48c35051aab51c101266d187a3", size = 11201636, upload-time = "2026-02-03T17:52:57.281Z" }, - { url = "https://files.pythonhosted.org/packages/6f/b0/69adf22f4e24f3677208adb715c578266842e6e6a3cc77483f48dd999ede/ruff-0.15.0-py3-none-win32.whl", hash = "sha256:238a717ef803e501b6d51e0bdd0d2c6e8513fe9eec14002445134d3907cd46c3", size = 10465945, upload-time = "2026-02-03T17:53:12.591Z" }, - { url = "https://files.pythonhosted.org/packages/51/ad/f813b6e2c97e9b4598be25e94a9147b9af7e60523b0cb5d94d307c15229d/ruff-0.15.0-py3-none-win_amd64.whl", hash = "sha256:dd5e4d3301dc01de614da3cdffc33d4b1b96fb89e45721f1598e5532ccf78b18", size = 11564657, upload-time = "2026-02-03T17:52:51.893Z" }, - { url = "https://files.pythonhosted.org/packages/f6/b0/2d823f6e77ebe560f4e397d078487e8d52c1516b331e3521bc75db4272ca/ruff-0.15.0-py3-none-win_arm64.whl", hash = "sha256:c480d632cc0ca3f0727acac8b7d053542d9e114a462a145d0b00e7cd658c515a", size = 10865753, upload-time = "2026-02-03T17:53:03.014Z" }, + { url = "https://files.pythonhosted.org/packages/23/bf/e6e4324238c17f9d9120a9d60aa99a7daaa21204c07fcd84e2ef03bb5fd1/ruff-0.15.1-py3-none-linux_armv6l.whl", hash = "sha256:b101ed7cf4615bda6ffe65bdb59f964e9f4a0d3f85cbf0e54f0ab76d7b90228a", size = 10367819, upload-time = "2026-02-12T23:09:03.598Z" }, + { url = "https://files.pythonhosted.org/packages/b3/ea/c8f89d32e7912269d38c58f3649e453ac32c528f93bb7f4219258be2e7ed/ruff-0.15.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:939c995e9277e63ea632cc8d3fae17aa758526f49a9a850d2e7e758bfef46602", size = 10798618, upload-time = "2026-02-12T23:09:22.928Z" }, + { url = "https://files.pythonhosted.org/packages/5e/0f/1d0d88bc862624247d82c20c10d4c0f6bb2f346559d8af281674cf327f15/ruff-0.15.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1d83466455fdefe60b8d9c8df81d3c1bbb2115cede53549d3b522ce2bc703899", size = 10148518, upload-time = "2026-02-12T23:08:58.339Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c8/291c49cefaa4a9248e986256df2ade7add79388fe179e0691be06fae6f37/ruff-0.15.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9457e3c3291024866222b96108ab2d8265b477e5b1534c7ddb1810904858d16", size = 10518811, upload-time = "2026-02-12T23:09:31.865Z" }, + { url = "https://files.pythonhosted.org/packages/c3/1a/f5707440e5ae43ffa5365cac8bbb91e9665f4a883f560893829cf16a606b/ruff-0.15.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:92c92b003e9d4f7fbd33b1867bb15a1b785b1735069108dfc23821ba045b29bc", size = 10196169, upload-time = "2026-02-12T23:09:17.306Z" }, + { url = "https://files.pythonhosted.org/packages/2a/ff/26ddc8c4da04c8fd3ee65a89c9fb99eaa5c30394269d424461467be2271f/ruff-0.15.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fe5c41ab43e3a06778844c586251eb5a510f67125427625f9eb2b9526535779", size = 10990491, upload-time = "2026-02-12T23:09:25.503Z" }, + { url = "https://files.pythonhosted.org/packages/fc/00/50920cb385b89413f7cdb4bb9bc8fc59c1b0f30028d8bccc294189a54955/ruff-0.15.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66a6dd6df4d80dc382c6484f8ce1bcceb55c32e9f27a8b94c32f6c7331bf14fb", size = 11843280, upload-time = "2026-02-12T23:09:19.88Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6d/2f5cad8380caf5632a15460c323ae326f1e1a2b5b90a6ee7519017a017ca/ruff-0.15.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a4a42cbb8af0bda9bcd7606b064d7c0bc311a88d141d02f78920be6acb5aa83", size = 11274336, upload-time = "2026-02-12T23:09:14.907Z" }, + { url = "https://files.pythonhosted.org/packages/a3/1d/5f56cae1d6c40b8a318513599b35ea4b075d7dc1cd1d04449578c29d1d75/ruff-0.15.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ab064052c31dddada35079901592dfba2e05f5b1e43af3954aafcbc1096a5b2", size = 11137288, upload-time = "2026-02-12T23:09:07.475Z" }, + { url = "https://files.pythonhosted.org/packages/cd/20/6f8d7d8f768c93b0382b33b9306b3b999918816da46537d5a61635514635/ruff-0.15.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:5631c940fe9fe91f817a4c2ea4e81f47bee3ca4aa646134a24374f3c19ad9454", size = 11070681, upload-time = "2026-02-12T23:08:55.43Z" }, + { url = "https://files.pythonhosted.org/packages/9a/67/d640ac76069f64cdea59dba02af2e00b1fa30e2103c7f8d049c0cff4cafd/ruff-0.15.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:68138a4ba184b4691ccdc39f7795c66b3c68160c586519e7e8444cf5a53e1b4c", size = 10486401, upload-time = "2026-02-12T23:09:27.927Z" }, + { url = "https://files.pythonhosted.org/packages/65/3d/e1429f64a3ff89297497916b88c32a5cc88eeca7e9c787072d0e7f1d3e1e/ruff-0.15.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:518f9af03bfc33c03bdb4cb63fabc935341bb7f54af500f92ac309ecfbba6330", size = 10197452, upload-time = "2026-02-12T23:09:12.147Z" }, + { url = "https://files.pythonhosted.org/packages/78/83/e2c3bade17dad63bf1e1c2ffaf11490603b760be149e1419b07049b36ef2/ruff-0.15.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:da79f4d6a826caaea95de0237a67e33b81e6ec2e25fc7e1993a4015dffca7c61", size = 10693900, upload-time = "2026-02-12T23:09:34.418Z" }, + { url = "https://files.pythonhosted.org/packages/a1/27/fdc0e11a813e6338e0706e8b39bb7a1d61ea5b36873b351acee7e524a72a/ruff-0.15.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3dd86dccb83cd7d4dcfac303ffc277e6048600dfc22e38158afa208e8bf94a1f", size = 11227302, upload-time = "2026-02-12T23:09:36.536Z" }, + { url = "https://files.pythonhosted.org/packages/f6/58/ac864a75067dcbd3b95be5ab4eb2b601d7fbc3d3d736a27e391a4f92a5c1/ruff-0.15.1-py3-none-win32.whl", hash = "sha256:660975d9cb49b5d5278b12b03bb9951d554543a90b74ed5d366b20e2c57c2098", size = 10462555, upload-time = "2026-02-12T23:09:29.899Z" }, + { url = "https://files.pythonhosted.org/packages/e0/5e/d4ccc8a27ecdb78116feac4935dfc39d1304536f4296168f91ed3ec00cd2/ruff-0.15.1-py3-none-win_amd64.whl", hash = "sha256:c820fef9dd5d4172a6570e5721704a96c6679b80cf7be41659ed439653f62336", size = 11599956, upload-time = "2026-02-12T23:09:01.157Z" }, + { url = "https://files.pythonhosted.org/packages/2a/07/5bda6a85b220c64c65686bc85bd0bbb23b29c62b3a9f9433fa55f17cda93/ruff-0.15.1-py3-none-win_arm64.whl", hash = "sha256:5ff7d5f0f88567850f45081fac8f4ec212be8d0b963e385c3f7d0d2eb4899416", size = 10874604, upload-time = "2026-02-12T23:09:05.515Z" }, ] [[package]] @@ -4212,26 +4212,26 @@ wheels = [ [[package]] name = "ty" -version = "0.0.15" +version = "0.0.17" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4e/25/257602d316b9333089b688a7a11b33ebc660b74e8dacf400dc3dfdea1594/ty-0.0.15.tar.gz", hash = "sha256:4f9a5b8df208c62dba56e91b93bed8b5bb714839691b8cff16d12c983bfa1174", size = 5101936, upload-time = "2026-02-05T01:06:34.922Z" } +sdist = { url = "https://files.pythonhosted.org/packages/66/c3/41ae6346443eedb65b96761abfab890a48ce2aa5a8a27af69c5c5d99064d/ty-0.0.17.tar.gz", hash = "sha256:847ed6c120913e280bf9b54d8eaa7a1049708acb8824ad234e71498e8ad09f97", size = 5167209, upload-time = "2026-02-13T13:26:36.835Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/c5/35626e732b79bf0e6213de9f79aff59b5f247c0a1e3ce0d93e675ab9b728/ty-0.0.15-py3-none-linux_armv6l.whl", hash = "sha256:68e092458516c61512dac541cde0a5e4e5842df00b4e81881ead8f745ddec794", size = 10138374, upload-time = "2026-02-05T01:07:03.804Z" }, - { url = "https://files.pythonhosted.org/packages/d5/8a/48fd81664604848f79d03879b3ca3633762d457a069b07e09fb1b87edd6e/ty-0.0.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:79f2e75289eae3cece94c51118b730211af4ba5762906f52a878041b67e54959", size = 9947858, upload-time = "2026-02-05T01:06:47.453Z" }, - { url = "https://files.pythonhosted.org/packages/b6/85/c1ac8e97bcd930946f4c94db85b675561d590b4e72703bf3733419fc3973/ty-0.0.15-py3-none-macosx_11_0_arm64.whl", hash = "sha256:112a7b26e63e48cc72c8c5b03227d1db280cfa57a45f2df0e264c3a016aa8c3c", size = 9443220, upload-time = "2026-02-05T01:06:44.98Z" }, - { url = "https://files.pythonhosted.org/packages/3c/d9/244bc02599d950f7a4298fbc0c1b25cc808646b9577bdf7a83470b2d1cec/ty-0.0.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71f62a2644972975a657d9dc867bf901235cde51e8d24c20311067e7afd44a56", size = 9949976, upload-time = "2026-02-05T01:07:01.515Z" }, - { url = "https://files.pythonhosted.org/packages/7e/ab/3a0daad66798c91a33867a3ececf17d314ac65d4ae2bbbd28cbfde94da63/ty-0.0.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9e48b42be2d257317c85b78559233273b655dd636fc61e7e1d69abd90fd3cba4", size = 9965918, upload-time = "2026-02-05T01:06:54.283Z" }, - { url = "https://files.pythonhosted.org/packages/39/4e/e62b01338f653059a7c0cd09d1a326e9a9eedc351a0f0de9db0601658c3d/ty-0.0.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27dd5b52a421e6871c5bfe9841160331b60866ed2040250cb161886478ab3e4f", size = 10424943, upload-time = "2026-02-05T01:07:08.777Z" }, - { url = "https://files.pythonhosted.org/packages/65/b5/7aa06655ce69c0d4f3e845d2d85e79c12994b6d84c71699cfb437e0bc8cf/ty-0.0.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76b85c9ec2219e11c358a7db8e21b7e5c6674a1fb9b6f633836949de98d12286", size = 10964692, upload-time = "2026-02-05T01:06:37.103Z" }, - { url = "https://files.pythonhosted.org/packages/13/04/36fdfe1f3c908b471e246e37ce3d011175584c26d3853e6c5d9a0364564c/ty-0.0.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9e8204c61d8ede4f21f2975dce74efdb80fafb2fae1915c666cceb33ea3c90b", size = 10692225, upload-time = "2026-02-05T01:06:49.714Z" }, - { url = "https://files.pythonhosted.org/packages/13/41/5bf882649bd8b64ded5fbce7fb8d77fb3b868de1a3b1a6c4796402b47308/ty-0.0.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af87c3be7c944bb4d6609d6c63e4594944b0028c7bd490a525a82b88fe010d6d", size = 10516776, upload-time = "2026-02-05T01:06:52.047Z" }, - { url = "https://files.pythonhosted.org/packages/56/75/66852d7e004f859839c17ffe1d16513c1e7cc04bcc810edb80ca022a9124/ty-0.0.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:50dccf7398505e5966847d366c9e4c650b8c225411c2a68c32040a63b9521eea", size = 9928828, upload-time = "2026-02-05T01:06:56.647Z" }, - { url = "https://files.pythonhosted.org/packages/65/72/96bc16c7b337a3ef358fd227b3c8ef0c77405f3bfbbfb59ee5915f0d9d71/ty-0.0.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:bd797b8f231a4f4715110259ad1ad5340a87b802307f3e06d92bfb37b858a8f3", size = 9978960, upload-time = "2026-02-05T01:06:29.567Z" }, - { url = "https://files.pythonhosted.org/packages/a0/18/d2e316a35b626de2227f832cd36d21205e4f5d96fd036a8af84c72ecec1b/ty-0.0.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9deb7f20e18b25440a9aa4884f934ba5628ef456dbde91819d5af1a73da48af3", size = 10135903, upload-time = "2026-02-05T01:06:59.256Z" }, - { url = "https://files.pythonhosted.org/packages/02/d3/b617a79c9dad10c888d7c15cd78859e0160b8772273637b9c4241a049491/ty-0.0.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7b31b3de031255b90a5f4d9cb3d050feae246067c87130e5a6861a8061c71754", size = 10615879, upload-time = "2026-02-05T01:07:06.661Z" }, - { url = "https://files.pythonhosted.org/packages/fb/b0/2652a73c71c77296a6343217063f05745da60c67b7e8a8e25f2064167fce/ty-0.0.15-py3-none-win32.whl", hash = "sha256:9362c528ceb62c89d65c216336d28d500bc9f4c10418413f63ebc16886e16cc1", size = 9578058, upload-time = "2026-02-05T01:06:42.928Z" }, - { url = "https://files.pythonhosted.org/packages/84/6e/08a4aedebd2a6ce2784b5bc3760e43d1861f1a184734a78215c2d397c1df/ty-0.0.15-py3-none-win_amd64.whl", hash = "sha256:4db040695ae67c5524f59cb8179a8fa277112e69042d7dfdac862caa7e3b0d9c", size = 10457112, upload-time = "2026-02-05T01:06:39.885Z" }, - { url = "https://files.pythonhosted.org/packages/b3/be/1991f2bc12847ae2d4f1e3ac5dcff8bb7bc1261390645c0755bb55616355/ty-0.0.15-py3-none-win_arm64.whl", hash = "sha256:e5a98d4119e77d6136461e16ae505f8f8069002874ab073de03fbcb1a5e8bf25", size = 9937490, upload-time = "2026-02-05T01:06:32.388Z" }, + { url = "https://files.pythonhosted.org/packages/c0/01/0ef15c22a1c54b0f728ceff3f62d478dbf8b0dcf8ff7b80b954f79584f3e/ty-0.0.17-py3-none-linux_armv6l.whl", hash = "sha256:64a9a16555cc8867d35c2647c2f1afbd3cae55f68fd95283a574d1bb04fe93e0", size = 10192793, upload-time = "2026-02-13T13:27:13.943Z" }, + { url = "https://files.pythonhosted.org/packages/0f/2c/f4c322d9cded56edc016b1092c14b95cf58c8a33b4787316ea752bb9418e/ty-0.0.17-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:eb2dbd8acd5c5a55f4af0d479523e7c7265a88542efe73ed3d696eb1ba7b6454", size = 10051977, upload-time = "2026-02-13T13:26:57.741Z" }, + { url = "https://files.pythonhosted.org/packages/4c/a5/43746c1ff81e784f5fc303afc61fe5bcd85d0fcf3ef65cb2cef78c7486c7/ty-0.0.17-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f18f5fd927bc628deb9ea2df40f06b5f79c5ccf355db732025a3e8e7152801f6", size = 9564639, upload-time = "2026-02-13T13:26:42.781Z" }, + { url = "https://files.pythonhosted.org/packages/d6/b8/280b04e14a9c0474af574f929fba2398b5e1c123c1e7735893b4cd73d13c/ty-0.0.17-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5383814d1d7a5cc53b3b07661856bab04bb2aac7a677c8d33c55169acdaa83df", size = 10061204, upload-time = "2026-02-13T13:27:00.152Z" }, + { url = "https://files.pythonhosted.org/packages/2a/d7/493e1607d8dfe48288d8a768a2adc38ee27ef50e57f0af41ff273987cda0/ty-0.0.17-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9c20423b8744b484f93e7bf2ef8a9724bca2657873593f9f41d08bd9f83444c9", size = 10013116, upload-time = "2026-02-13T13:26:34.543Z" }, + { url = "https://files.pythonhosted.org/packages/80/ef/22f3ed401520afac90dbdf1f9b8b7755d85b0d5c35c1cb35cf5bd11b59c2/ty-0.0.17-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e6f5b1aba97db9af86517b911674b02f5bc310750485dc47603a105bd0e83ddd", size = 10533623, upload-time = "2026-02-13T13:26:31.449Z" }, + { url = "https://files.pythonhosted.org/packages/75/ce/744b15279a11ac7138832e3a55595706b4a8a209c9f878e3ab8e571d9032/ty-0.0.17-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:488bce1a9bea80b851a97cd34c4d2ffcd69593d6c3f54a72ae02e5c6e47f3d0c", size = 11069750, upload-time = "2026-02-13T13:26:48.638Z" }, + { url = "https://files.pythonhosted.org/packages/f2/be/1133c91f15a0e00d466c24f80df486d630d95d1b2af63296941f7473812f/ty-0.0.17-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8df66b91ec84239420985ec215e7f7549bfda2ac036a3b3c065f119d1c06825a", size = 10870862, upload-time = "2026-02-13T13:26:54.715Z" }, + { url = "https://files.pythonhosted.org/packages/3e/4a/a2ed209ef215b62b2d3246e07e833081e07d913adf7e0448fc204be443d6/ty-0.0.17-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:002139e807c53002790dfefe6e2f45ab0e04012e76db3d7c8286f96ec121af8f", size = 10628118, upload-time = "2026-02-13T13:26:45.439Z" }, + { url = "https://files.pythonhosted.org/packages/b3/0c/87476004cb5228e9719b98afffad82c3ef1f84334bde8527bcacba7b18cb/ty-0.0.17-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6c4e01f05ce82e5d489ab3900ca0899a56c4ccb52659453780c83e5b19e2b64c", size = 10038185, upload-time = "2026-02-13T13:27:02.693Z" }, + { url = "https://files.pythonhosted.org/packages/46/4b/98f0b3ba9aef53c1f0305519536967a4aa793a69ed72677b0a625c5313ac/ty-0.0.17-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2b226dd1e99c0d2152d218c7e440150d1a47ce3c431871f0efa073bbf899e881", size = 10047644, upload-time = "2026-02-13T13:27:05.474Z" }, + { url = "https://files.pythonhosted.org/packages/93/e0/06737bb80aa1a9103b8651d2eb691a7e53f1ed54111152be25f4a02745db/ty-0.0.17-py3-none-musllinux_1_2_i686.whl", hash = "sha256:8b11f1da7859e0ad69e84b3c5ef9a7b055ceed376a432fad44231bdfc48061c2", size = 10231140, upload-time = "2026-02-13T13:27:10.844Z" }, + { url = "https://files.pythonhosted.org/packages/7c/79/e2a606bd8852383ba9abfdd578f4a227bd18504145381a10a5f886b4e751/ty-0.0.17-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:c04e196809ff570559054d3e011425fd7c04161529eb551b3625654e5f2434cb", size = 10718344, upload-time = "2026-02-13T13:26:51.66Z" }, + { url = "https://files.pythonhosted.org/packages/c5/2d/2663984ac11de6d78f74432b8b14ba64d170b45194312852b7543cf7fd56/ty-0.0.17-py3-none-win32.whl", hash = "sha256:305b6ed150b2740d00a817b193373d21f0767e10f94ac47abfc3b2e5a5aec809", size = 9672932, upload-time = "2026-02-13T13:27:08.522Z" }, + { url = "https://files.pythonhosted.org/packages/de/b5/39be78f30b31ee9f5a585969930c7248354db90494ff5e3d0756560fb731/ty-0.0.17-py3-none-win_amd64.whl", hash = "sha256:531828267527aee7a63e972f54e5eee21d9281b72baf18e5c2850c6b862add83", size = 10542138, upload-time = "2026-02-13T13:27:17.084Z" }, + { url = "https://files.pythonhosted.org/packages/40/b7/f875c729c5d0079640c75bad2c7e5d43edc90f16ba242f28a11966df8f65/ty-0.0.17-py3-none-win_arm64.whl", hash = "sha256:de9810234c0c8d75073457e10a84825b9cd72e6629826b7f01c7a0b266ae25b1", size = 10023068, upload-time = "2026-02-13T13:26:39.637Z" }, ] [[package]] From ced5f417b88c62f26248ab6e604d1c9bd182d31a Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sat, 14 Feb 2026 21:16:26 -0800 Subject: [PATCH 040/311] MetaDrive: slim down & enable CI test (#37216) * MetaDrive slimming * enable * lock * modeld fix * minimal --- .github/workflows/tests.yaml | 1 - pyproject.toml | 2 +- selfdrive/modeld/modeld.py | 7 +- uv.lock | 155 +---------------------------------- 4 files changed, 8 insertions(+), 157 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index fe7c8ef336..aab0f58b0a 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -238,7 +238,6 @@ jobs: (github.event.pull_request.head.repo.full_name == 'commaai/openpilot')) && fromJSON('["namespace-profile-amd64-8x16", "namespace-experiments:docker.builds.local-cache=separate"]') || fromJSON('["ubuntu-24.04"]') }} - if: false # FIXME: Started to timeout recently steps: - uses: actions/checkout@v6 with: diff --git a/pyproject.toml b/pyproject.toml index f959c613cc..2a6d619f7f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -105,7 +105,7 @@ dev = [ ] tools = [ - "metadrive-simulator @ https://github.com/commaai/metadrive/releases/download/MetaDrive-minimal-0.4.2.4/metadrive_simulator-0.4.2.4-py3-none-any.whl ; (platform_machine != 'aarch64')", + "metadrive-simulator @ git+https://github.com/commaai/metadrive.git@minimal ; (platform_machine != 'aarch64')", "dearpygui>=2.1.0; (sys_platform != 'linux' or platform_machine != 'aarch64')", # not vended for linux aarch64 ] diff --git a/selfdrive/modeld/modeld.py b/selfdrive/modeld/modeld.py index a8b633bf02..3fe3e0e6d6 100755 --- a/selfdrive/modeld/modeld.py +++ b/selfdrive/modeld/modeld.py @@ -203,9 +203,10 @@ class ModelState: ptr = bufs[key].data.ctypes.data yuv_size = self.frame_buf_params[key][3] # There is a ringbuffer of imgs, just cache tensors pointing to all of them - if ptr not in self._blob_cache: - self._blob_cache[ptr] = Tensor.from_blob(ptr, (yuv_size,), dtype='uint8') - self.full_frames[key] = self._blob_cache[ptr] + cache_key = (key, ptr) + if cache_key not in self._blob_cache: + self._blob_cache[cache_key] = Tensor.from_blob(ptr, (yuv_size,), dtype='uint8') + self.full_frames[key] = self._blob_cache[cache_key] for key in bufs.keys(): self.transforms_np[key][:,:] = transforms[key][:,:] diff --git a/uv.lock b/uv.lock index 2f095265b4..cd077d3de7 100644 --- a/uv.lock +++ b/uv.lock @@ -205,15 +205,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, ] -[[package]] -name = "cloudpickle" -version = "3.1.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/27/fb/576f067976d320f5f0114a8d9fa1215425441bb35627b1993e5afd8111e5/cloudpickle-3.1.2.tar.gz", hash = "sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414", size = 22330, upload-time = "2025-11-03T09:25:26.604Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl", hash = "sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a", size = 22228, upload-time = "2025-11-03T09:25:25.534Z" }, -] - [[package]] name = "codespell" version = "2.4.1" @@ -404,24 +395,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, ] -[[package]] -name = "farama-notifications" -version = "0.0.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2e/2c/8384832b7a6b1fd6ba95bbdcae26e7137bb3eedc955c42fd5cdcc086cfbf/Farama-Notifications-0.0.4.tar.gz", hash = "sha256:13fceff2d14314cf80703c8266462ebf3733c7d165336eee998fc58e545efd18", size = 2131, upload-time = "2023-02-27T18:28:41.047Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/05/2c/ffc08c54c05cdce6fbed2aeebc46348dbe180c6d2c541c7af7ba0aa5f5f8/Farama_Notifications-0.0.4-py3-none-any.whl", hash = "sha256:14de931035a41961f7c056361dc7f980762a143d05791ef5794a751a2caf05ae", size = 2511, upload-time = "2023-02-27T18:28:39.447Z" }, -] - -[[package]] -name = "filelock" -version = "3.24.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/00/cd/fa3ab025a8f9772e8a9146d8fd8eef6d62649274d231ca84249f54a0de4a/filelock-3.24.0.tar.gz", hash = "sha256:aeeab479339ddf463a1cdd1f15a6e6894db976071e5883efc94d22ed5139044b", size = 37166, upload-time = "2026-02-14T16:05:28.723Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/dd/d7e7f4f49180e8591c9e1281d15ecf8e7f25eb2c829771d9682f1f9fe0c8/filelock-3.24.0-py3-none-any.whl", hash = "sha256:eebebb403d78363ef7be8e236b63cc6760b0004c7464dceaba3fd0afbd637ced", size = 23977, upload-time = "2026-02-14T16:05:27.578Z" }, -] - [[package]] name = "fonttools" version = "4.61.1" @@ -489,21 +462,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/df/c0/87c2073e0c72515bb8733d4eef7b21548e8d189f094b5dad20b0ecaf64f6/google_crc32c-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:3cc0c8912038065eafa603b238abf252e204accab2a704c63b9e14837a854962", size = 34437, upload-time = "2025-12-16T00:35:21.395Z" }, ] -[[package]] -name = "gymnasium" -version = "1.2.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cloudpickle" }, - { name = "farama-notifications" }, - { name = "numpy" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/76/59/653a9417d98ed3e29ef9734ba52c3495f6c6823b8d5c0c75369f25111708/gymnasium-1.2.3.tar.gz", hash = "sha256:2b2cb5b5fbbbdf3afb9f38ca952cc48aa6aa3e26561400d940747fda3ad42509", size = 829230, upload-time = "2025-12-18T16:51:10.234Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/56/d3/ea5f088e3638dbab12e5c20d6559d5b3bdaeaa1f2af74e526e6815836285/gymnasium-1.2.3-py3-none-any.whl", hash = "sha256:e6314bba8f549c7fdcc8677f7cd786b64908af6e79b57ddaa5ce1825bffb5373", size = 952113, upload-time = "2025-12-18T16:51:08.445Z" }, -] - [[package]] name = "hypothesis" version = "6.47.5" @@ -615,32 +573,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/94/6d/344a164d32d65d503ffe9201cd74cf13a020099dc446554d1e50b07f167b/libusb1-3.3.1-py3-none-win_amd64.whl", hash = "sha256:6e21b772d80d6487fbb55d3d2141218536db302da82f1983754e96c72781c102", size = 141080, upload-time = "2025-03-24T05:36:46.594Z" }, ] -[[package]] -name = "lxml" -version = "6.0.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/aa/88/262177de60548e5a2bfc46ad28232c9e9cbde697bd94132aeb80364675cb/lxml-6.0.2.tar.gz", hash = "sha256:cd79f3367bd74b317dda655dc8fcfa304d9eb6e4fb06b7168c5cf27f96e0cd62", size = 4073426, upload-time = "2025-09-22T04:04:59.287Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/c8/8ff2bc6b920c84355146cd1ab7d181bc543b89241cfb1ebee824a7c81457/lxml-6.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a59f5448ba2ceccd06995c95ea59a7674a10de0810f2ce90c9006f3cbc044456", size = 8661887, upload-time = "2025-09-22T04:01:17.265Z" }, - { url = "https://files.pythonhosted.org/packages/37/6f/9aae1008083bb501ef63284220ce81638332f9ccbfa53765b2b7502203cf/lxml-6.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e8113639f3296706fbac34a30813929e29247718e88173ad849f57ca59754924", size = 4667818, upload-time = "2025-09-22T04:01:19.688Z" }, - { url = "https://files.pythonhosted.org/packages/f1/ca/31fb37f99f37f1536c133476674c10b577e409c0a624384147653e38baf2/lxml-6.0.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a8bef9b9825fa8bc816a6e641bb67219489229ebc648be422af695f6e7a4fa7f", size = 4950807, upload-time = "2025-09-22T04:01:21.487Z" }, - { url = "https://files.pythonhosted.org/packages/da/87/f6cb9442e4bada8aab5ae7e1046264f62fdbeaa6e3f6211b93f4c0dd97f1/lxml-6.0.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:65ea18d710fd14e0186c2f973dc60bb52039a275f82d3c44a0e42b43440ea534", size = 5109179, upload-time = "2025-09-22T04:01:23.32Z" }, - { url = "https://files.pythonhosted.org/packages/c8/20/a7760713e65888db79bbae4f6146a6ae5c04e4a204a3c48896c408cd6ed2/lxml-6.0.2-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c371aa98126a0d4c739ca93ceffa0fd7a5d732e3ac66a46e74339acd4d334564", size = 5023044, upload-time = "2025-09-22T04:01:25.118Z" }, - { url = "https://files.pythonhosted.org/packages/a2/b0/7e64e0460fcb36471899f75831509098f3fd7cd02a3833ac517433cb4f8f/lxml-6.0.2-cp312-cp312-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:700efd30c0fa1a3581d80a748157397559396090a51d306ea59a70020223d16f", size = 5359685, upload-time = "2025-09-22T04:01:27.398Z" }, - { url = "https://files.pythonhosted.org/packages/b9/e1/e5df362e9ca4e2f48ed6411bd4b3a0ae737cc842e96877f5bf9428055ab4/lxml-6.0.2-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c33e66d44fe60e72397b487ee92e01da0d09ba2d66df8eae42d77b6d06e5eba0", size = 5654127, upload-time = "2025-09-22T04:01:29.629Z" }, - { url = "https://files.pythonhosted.org/packages/c6/d1/232b3309a02d60f11e71857778bfcd4acbdb86c07db8260caf7d008b08f8/lxml-6.0.2-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90a345bbeaf9d0587a3aaffb7006aa39ccb6ff0e96a57286c0cb2fd1520ea192", size = 5253958, upload-time = "2025-09-22T04:01:31.535Z" }, - { url = "https://files.pythonhosted.org/packages/35/35/d955a070994725c4f7d80583a96cab9c107c57a125b20bb5f708fe941011/lxml-6.0.2-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:064fdadaf7a21af3ed1dcaa106b854077fbeada827c18f72aec9346847cd65d0", size = 4711541, upload-time = "2025-09-22T04:01:33.801Z" }, - { url = "https://files.pythonhosted.org/packages/1e/be/667d17363b38a78c4bd63cfd4b4632029fd68d2c2dc81f25ce9eb5224dd5/lxml-6.0.2-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fbc74f42c3525ac4ffa4b89cbdd00057b6196bcefe8bce794abd42d33a018092", size = 5267426, upload-time = "2025-09-22T04:01:35.639Z" }, - { url = "https://files.pythonhosted.org/packages/ea/47/62c70aa4a1c26569bc958c9ca86af2bb4e1f614e8c04fb2989833874f7ae/lxml-6.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6ddff43f702905a4e32bc24f3f2e2edfe0f8fde3277d481bffb709a4cced7a1f", size = 5064917, upload-time = "2025-09-22T04:01:37.448Z" }, - { url = "https://files.pythonhosted.org/packages/bd/55/6ceddaca353ebd0f1908ef712c597f8570cc9c58130dbb89903198e441fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6da5185951d72e6f5352166e3da7b0dc27aa70bd1090b0eb3f7f7212b53f1bb8", size = 4788795, upload-time = "2025-09-22T04:01:39.165Z" }, - { url = "https://files.pythonhosted.org/packages/cf/e8/fd63e15da5e3fd4c2146f8bbb3c14e94ab850589beab88e547b2dbce22e1/lxml-6.0.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:57a86e1ebb4020a38d295c04fc79603c7899e0df71588043eb218722dabc087f", size = 5676759, upload-time = "2025-09-22T04:01:41.506Z" }, - { url = "https://files.pythonhosted.org/packages/76/47/b3ec58dc5c374697f5ba37412cd2728f427d056315d124dd4b61da381877/lxml-6.0.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2047d8234fe735ab77802ce5f2297e410ff40f5238aec569ad7c8e163d7b19a6", size = 5255666, upload-time = "2025-09-22T04:01:43.363Z" }, - { url = "https://files.pythonhosted.org/packages/19/93/03ba725df4c3d72afd9596eef4a37a837ce8e4806010569bedfcd2cb68fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f91fd2b2ea15a6800c8e24418c0775a1694eefc011392da73bc6cef2623b322", size = 5277989, upload-time = "2025-09-22T04:01:45.215Z" }, - { url = "https://files.pythonhosted.org/packages/c6/80/c06de80bfce881d0ad738576f243911fccf992687ae09fd80b734712b39c/lxml-6.0.2-cp312-cp312-win32.whl", hash = "sha256:3ae2ce7d6fedfb3414a2b6c5e20b249c4c607f72cb8d2bb7cc9c6ec7c6f4e849", size = 3611456, upload-time = "2025-09-22T04:01:48.243Z" }, - { url = "https://files.pythonhosted.org/packages/f7/d7/0cdfb6c3e30893463fb3d1e52bc5f5f99684a03c29a0b6b605cfae879cd5/lxml-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:72c87e5ee4e58a8354fb9c7c84cbf95a1c8236c127a5d1b7683f04bed8361e1f", size = 4011793, upload-time = "2025-09-22T04:01:50.042Z" }, - { url = "https://files.pythonhosted.org/packages/ea/7b/93c73c67db235931527301ed3785f849c78991e2e34f3fd9a6663ffda4c5/lxml-6.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:61cb10eeb95570153e0c0e554f58df92ecf5109f75eacad4a95baa709e26c3d6", size = 3672836, upload-time = "2025-09-22T04:01:52.145Z" }, -] - [[package]] name = "mapbox-earcut" version = "2.0.0" @@ -726,57 +658,13 @@ wheels = [ [[package]] name = "metadrive-simulator" -version = "0.4.2.4" -source = { url = "https://github.com/commaai/metadrive/releases/download/MetaDrive-minimal-0.4.2.4/metadrive_simulator-0.4.2.4-py3-none-any.whl" } +version = "0.4.2.3" +source = { git = "https://github.com/commaai/metadrive.git?rev=minimal#2716f55a9c7b928ce957a497a15c2c19840c08bc" } dependencies = [ - { name = "filelock" }, - { name = "gymnasium" }, - { name = "lxml" }, - { name = "matplotlib" }, { name = "numpy" }, - { name = "opencv-python-headless" }, { name = "panda3d" }, { name = "panda3d-gltf" }, - { name = "pillow" }, - { name = "progressbar" }, - { name = "psutil" }, - { name = "pygments" }, - { name = "requests" }, - { name = "shapely" }, - { name = "tqdm" }, - { name = "yapf" }, ] -wheels = [ - { url = "https://github.com/commaai/metadrive/releases/download/MetaDrive-minimal-0.4.2.4/metadrive_simulator-0.4.2.4-py3-none-any.whl", hash = "sha256:d0afaf3b005e35e14b929d5491d2d5b64562d0c1cd5093ba969fb63908670dd4" }, -] - -[package.metadata] -requires-dist = [ - { name = "cuda-python", marker = "extra == 'cuda'", specifier = "==12.0.0" }, - { name = "filelock" }, - { name = "glfw", marker = "extra == 'cuda'" }, - { name = "gym", marker = "extra == 'gym'", specifier = ">=0.19.0,<=0.26.0" }, - { name = "gymnasium", specifier = ">=0.28" }, - { name = "lxml" }, - { name = "matplotlib" }, - { name = "numpy", specifier = ">=1.21.6" }, - { name = "opencv-python-headless" }, - { name = "panda3d", specifier = "==1.10.14" }, - { name = "panda3d-gltf", specifier = "==0.13" }, - { name = "pillow" }, - { name = "progressbar" }, - { name = "psutil" }, - { name = "pygments" }, - { name = "pyopengl", marker = "extra == 'cuda'", specifier = "==3.1.6" }, - { name = "pyopengl-accelerate", marker = "extra == 'cuda'", specifier = "==3.1.6" }, - { name = "pyrr", marker = "extra == 'cuda'", specifier = "==0.10.3" }, - { name = "requests" }, - { name = "shapely" }, - { name = "tqdm" }, - { name = "yapf" }, - { name = "zmq", marker = "extra == 'ros'" }, -] -provides-extras = ["cuda", "gym", "ros"] [[package]] name = "mkdocs" @@ -1033,7 +921,7 @@ requires-dist = [ { name = "libusb1" }, { name = "mapbox-earcut" }, { name = "matplotlib", marker = "extra == 'dev'" }, - { name = "metadrive-simulator", marker = "platform_machine != 'aarch64' and extra == 'tools'", url = "https://github.com/commaai/metadrive/releases/download/MetaDrive-minimal-0.4.2.4/metadrive_simulator-0.4.2.4-py3-none-any.whl" }, + { name = "metadrive-simulator", marker = "platform_machine != 'aarch64' and extra == 'tools'", git = "https://github.com/commaai/metadrive.git?rev=minimal" }, { name = "mkdocs", marker = "extra == 'docs'" }, { name = "numpy", specifier = ">=2.0" }, { name = "onnx", specifier = ">=1.14.0" }, @@ -1191,12 +1079,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/12/46/eba9be9daa403fa94854ce16a458c29df9a01c6c047931c3d8be6016cd9a/pre_commit_hooks-6.0.0-py2.py3-none-any.whl", hash = "sha256:76161b76d321d2f8ee2a8e0b84c30ee8443e01376121fd1c90851e33e3bd7ee2", size = 41338, upload-time = "2025-08-09T19:25:03.513Z" }, ] -[[package]] -name = "progressbar" -version = "2.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a3/a6/b8e451f6cff1c99b4747a2f7235aa904d2d49e8e1464e0b798272aa84358/progressbar-2.5.tar.gz", hash = "sha256:5d81cb529da2e223b53962afd6c8ca0f05c6670e40309a7219eacc36af9b6c63", size = 10046, upload-time = "2018-06-29T02:32:00.222Z" } - [[package]] name = "propcache" version = "0.4.1" @@ -4127,25 +4009,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e1/c6/76dc613121b793286a3f91621d7b75a2b493e0390ddca50f11993eadf192/setuptools-82.0.0-py3-none-any.whl", hash = "sha256:70b18734b607bd1da571d097d236cfcfacaf01de45717d59e6e04b96877532e0", size = 1003468, upload-time = "2026-02-08T15:08:38.723Z" }, ] -[[package]] -name = "shapely" -version = "2.1.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/4d/bc/0989043118a27cccb4e906a46b7565ce36ca7b57f5a18b78f4f1b0f72d9d/shapely-2.1.2.tar.gz", hash = "sha256:2ed4ecb28320a433db18a5bf029986aa8afcfd740745e78847e330d5d94922a9", size = 315489, upload-time = "2025-09-24T13:51:41.432Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/24/c0/f3b6453cf2dfa99adc0ba6675f9aaff9e526d2224cbd7ff9c1a879238693/shapely-2.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fe2533caae6a91a543dec62e8360fe86ffcdc42a7c55f9dfd0128a977a896b94", size = 1833550, upload-time = "2025-09-24T13:50:30.019Z" }, - { url = "https://files.pythonhosted.org/packages/86/07/59dee0bc4b913b7ab59ab1086225baca5b8f19865e6101db9ebb7243e132/shapely-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ba4d1333cc0bc94381d6d4308d2e4e008e0bd128bdcff5573199742ee3634359", size = 1643556, upload-time = "2025-09-24T13:50:32.291Z" }, - { url = "https://files.pythonhosted.org/packages/26/29/a5397e75b435b9895cd53e165083faed5d12fd9626eadec15a83a2411f0f/shapely-2.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0bd308103340030feef6c111d3eb98d50dc13feea33affc8a6f9fa549e9458a3", size = 2988308, upload-time = "2025-09-24T13:50:33.862Z" }, - { url = "https://files.pythonhosted.org/packages/b9/37/e781683abac55dde9771e086b790e554811a71ed0b2b8a1e789b7430dd44/shapely-2.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1e7d4d7ad262a48bb44277ca12c7c78cb1b0f56b32c10734ec9a1d30c0b0c54b", size = 3099844, upload-time = "2025-09-24T13:50:35.459Z" }, - { url = "https://files.pythonhosted.org/packages/d8/f3/9876b64d4a5a321b9dc482c92bb6f061f2fa42131cba643c699f39317cb9/shapely-2.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e9eddfe513096a71896441a7c37db72da0687b34752c4e193577a145c71736fc", size = 3988842, upload-time = "2025-09-24T13:50:37.478Z" }, - { url = "https://files.pythonhosted.org/packages/d1/a0/704c7292f7014c7e74ec84eddb7b109e1fbae74a16deae9c1504b1d15565/shapely-2.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:980c777c612514c0cf99bc8a9de6d286f5e186dcaf9091252fcd444e5638193d", size = 4152714, upload-time = "2025-09-24T13:50:39.9Z" }, - { url = "https://files.pythonhosted.org/packages/53/46/319c9dc788884ad0785242543cdffac0e6530e4d0deb6c4862bc4143dcf3/shapely-2.1.2-cp312-cp312-win32.whl", hash = "sha256:9111274b88e4d7b54a95218e243282709b330ef52b7b86bc6aaf4f805306f454", size = 1542745, upload-time = "2025-09-24T13:50:41.414Z" }, - { url = "https://files.pythonhosted.org/packages/ec/bf/cb6c1c505cb31e818e900b9312d514f381fbfa5c4363edfce0fcc4f8c1a4/shapely-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:743044b4cfb34f9a67205cee9279feaf60ba7d02e69febc2afc609047cb49179", size = 1722861, upload-time = "2025-09-24T13:50:43.35Z" }, -] - [[package]] name = "six" version = "1.17.0" @@ -4300,18 +4163,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/38/63/188f7cb41ab35d795558325d5cc8ab552171d5498cfb178fd14409651e18/xattr-1.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2aaa5d66af6523332189108f34e966ca120ff816dfa077ca34b31e6263f8a236", size = 37754, upload-time = "2025-10-13T22:16:15.306Z" }, ] -[[package]] -name = "yapf" -version = "0.43.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "platformdirs" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/23/97/b6f296d1e9cc1ec25c7604178b48532fa5901f721bcf1b8d8148b13e5588/yapf-0.43.0.tar.gz", hash = "sha256:00d3aa24bfedff9420b2e0d5d9f5ab6d9d4268e72afbf59bb3fa542781d5218e", size = 254907, upload-time = "2024-11-14T00:11:41.584Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/37/81/6acd6601f61e31cfb8729d3da6d5df966f80f374b78eff83760714487338/yapf-0.43.0-py3-none-any.whl", hash = "sha256:224faffbc39c428cb095818cf6ef5511fdab6f7430a10783fdfb292ccf2852ca", size = 256158, upload-time = "2024-11-14T00:11:39.37Z" }, -] - [[package]] name = "yarl" version = "1.22.0" From 4166c9fccbf1fbfbaba2da1964f15585d24145a3 Mon Sep 17 00:00:00 2001 From: Andi Radulescu Date: Sun, 15 Feb 2026 19:44:06 +0200 Subject: [PATCH 041/311] ci: fix first-interaction action missing required input (#37221) actions/first-interaction@v3 requires both issue_message and pr_message inputs, but only pr_message was provided, causing the action to fail. --- .github/workflows/auto_pr_review.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/auto_pr_review.yaml b/.github/workflows/auto_pr_review.yaml index cf12360e6e..63cb062ebe 100644 --- a/.github/workflows/auto_pr_review.yaml +++ b/.github/workflows/auto_pr_review.yaml @@ -40,6 +40,7 @@ jobs: if: github.event.pull_request.head.repo.full_name != 'commaai/openpilot' with: repo_token: ${{ secrets.GITHUB_TOKEN }} + issue_message: "" pr_message: | Thanks for contributing to openpilot! In order for us to review your PR as quickly as possible, check the following: From 27f89e66346bb18a302673d30972d9fb27f7467a Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sun, 15 Feb 2026 16:39:38 -0800 Subject: [PATCH 042/311] jenkins: merge & speedup camera tests (#37223) --- Jenkinsfile | 6 +- selfdrive/test/helpers.py | 37 +++++++++ system/camerad/test/test_camerad.py | 107 ++++++++++++++++++++------- system/camerad/test/test_exposure.py | 51 ------------- 4 files changed, 120 insertions(+), 81 deletions(-) delete mode 100644 system/camerad/test/test_exposure.py diff --git a/Jenkinsfile b/Jenkinsfile index 6f81755d4e..e58ac817eb 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -219,15 +219,13 @@ node { 'camerad OX03C10': { deviceStage("OX03C10", "tizi-ox03c10", ["UNSAFE=1"], [ step("build", "cd system/manager && ./build.py"), - step("test camerad", "pytest system/camerad/test/test_camerad.py", [timeout: 60]), - step("test exposure", "pytest system/camerad/test/test_exposure.py"), + step("test camerad", "pytest system/camerad/test/test_camerad.py", [timeout: 90]), ]) }, 'camerad OS04C10': { deviceStage("OS04C10", "tici-os04c10", ["UNSAFE=1"], [ step("build", "cd system/manager && ./build.py"), - step("test camerad", "pytest system/camerad/test/test_camerad.py", [timeout: 60]), - step("test exposure", "pytest system/camerad/test/test_exposure.py"), + step("test camerad", "pytest system/camerad/test/test_camerad.py", [timeout: 90]), ]) }, 'sensord': { diff --git a/selfdrive/test/helpers.py b/selfdrive/test/helpers.py index 81635aa31f..5dfc1c3ec8 100644 --- a/selfdrive/test/helpers.py +++ b/selfdrive/test/helpers.py @@ -37,6 +37,43 @@ def release_only(f): return wrap +def collect_logs(services, duration): + socks = [messaging.sub_sock(s, conflate=False, timeout=100) for s in services] + logs = [] + start = time.monotonic() + while time.monotonic() - start < duration: + for s in socks: + logs.extend(messaging.drain_sock(s)) + return logs + + +@contextlib.contextmanager +def log_collector(services): + """Background thread that continuously drains messages from services. + Use when the main thread needs to do blocking work (e.g. capturing images).""" + socks = [messaging.sub_sock(s, conflate=False, timeout=100) for s in services] + raw_logs = [] + lock = threading.Lock() + stop_event = threading.Event() + + def _drain(): + while not stop_event.is_set(): + for s in socks: + msgs = messaging.drain_sock(s) + if msgs: + with lock: + raw_logs.extend(msgs) + time.sleep(0.01) + + thread = threading.Thread(target=_drain, daemon=True) + thread.start() + try: + yield raw_logs, lock + finally: + stop_event.set() + thread.join(timeout=2) + + @contextlib.contextmanager def processes_context(processes, init_time=0, ignore_stopped=None): ignore_stopped = [] if ignore_stopped is None else ignore_stopped diff --git a/system/camerad/test/test_camerad.py b/system/camerad/test/test_camerad.py index 1f3f97b082..5f8de86899 100644 --- a/system/camerad/test/test_camerad.py +++ b/system/camerad/test/test_camerad.py @@ -3,51 +3,103 @@ import time import pytest import numpy as np -import cereal.messaging as messaging from cereal.services import SERVICE_LIST -from openpilot.system.manager.process_config import managed_processes from openpilot.tools.lib.log_time_series import msgs_to_time_series +from openpilot.system.camerad.snapshot import get_snapshots +from openpilot.selfdrive.test.helpers import collect_logs, log_collector, processes_context TEST_TIMESPAN = 10 CAMERAS = ('roadCameraState', 'driverCameraState', 'wideRoadCameraState') +EXPOSURE_STABLE_COUNT = 3 +EXPOSURE_RANGE = (0.15, 0.35) +MAX_TEST_TIME = 25 + + +def _numpy_rgb2gray(im): + return np.clip(im[:,:,2] * 0.114 + im[:,:,1] * 0.587 + im[:,:,0] * 0.299, 0, 255).astype(np.uint8) + +def _exposure_stats(im): + h, w = im.shape[:2] + gray = _numpy_rgb2gray(im[h//10:9*h//10, w//10:9*w//10]) + return float(np.median(gray) / 255.), float(np.mean(gray) / 255.) + +def _in_range(median, mean): + lo, hi = EXPOSURE_RANGE + return lo < median < hi and lo < mean < hi + +def _exposure_stable(results): + return all( + len(v) >= EXPOSURE_STABLE_COUNT and all(_in_range(*s) for s in v[-EXPOSURE_STABLE_COUNT:]) + for v in results.values() + ) def run_and_log(procs, services, duration): - logs = [] - - try: - for p in procs: - managed_processes[p].start() - socks = [messaging.sub_sock(s, conflate=False, timeout=100) for s in services] - - start_time = time.monotonic() - while time.monotonic() - start_time < duration: - for s in socks: - logs.extend(messaging.drain_sock(s)) - for p in procs: - assert managed_processes[p].proc.is_alive() - finally: - for p in procs: - managed_processes[p].stop() - - return logs + with processes_context(procs): + return collect_logs(services, duration) @pytest.fixture(scope="module") -def logs(): - logs = run_and_log(["camerad", ], CAMERAS, TEST_TIMESPAN) - ts = msgs_to_time_series(logs) +def _camera_session(): + """Single camerad session that collects logs and exposure data. + Runs until exposure stabilizes (min TEST_TIMESPAN seconds for enough log data).""" + with processes_context(["camerad"]), log_collector(CAMERAS) as (raw_logs, lock): + exposure = {cam: [] for cam in CAMERAS} + start = time.monotonic() + while time.monotonic() - start < MAX_TEST_TIME: + rpic, dpic = get_snapshots(frame="roadCameraState", front_frame="driverCameraState") + wpic, _ = get_snapshots(frame="wideRoadCameraState") + for cam, img in zip(CAMERAS, [rpic, dpic, wpic], strict=True): + exposure[cam].append(_exposure_stats(img)) + + if time.monotonic() - start >= TEST_TIMESPAN and _exposure_stable(exposure): + break + + elapsed = time.monotonic() - start + + with lock: + ts = msgs_to_time_series(raw_logs) for cam in CAMERAS: - expected_frames = SERVICE_LIST[cam].frequency * TEST_TIMESPAN + expected_frames = SERVICE_LIST[cam].frequency * elapsed cnt = len(ts[cam]['t']) assert expected_frames*0.8 < cnt < expected_frames*1.2, f"unexpected frame count {cam}: {expected_frames=}, got {cnt}" dts = np.abs(np.diff([ts[cam]['timestampSof']/1e6]) - 1000/SERVICE_LIST[cam].frequency) assert (dts < 1.0).all(), f"{cam} dts(ms) out of spec: max diff {dts.max()}, 99 percentile {np.percentile(dts, 99)}" - return ts + + return ts, exposure + +@pytest.fixture(scope="module") +def logs(_camera_session): + return _camera_session[0] + +@pytest.fixture(scope="module") +def exposure_data(_camera_session): + return _camera_session[1] @pytest.mark.tici class TestCamerad: + @pytest.mark.parametrize("cam", CAMERAS) + def test_camera_exposure(self, exposure_data, cam): + lo, hi = EXPOSURE_RANGE + checks = exposure_data[cam] + assert len(checks) >= EXPOSURE_STABLE_COUNT, f"{cam}: only got {len(checks)} samples" + + # check that exposure converges into the valid range + passed = sum(_in_range(med, mean) for med, mean in checks) + assert passed >= EXPOSURE_STABLE_COUNT, \ + f"{cam}: only {passed}/{len(checks)} checks in range. " + \ + " | ".join(f"#{i+1}: med={m:.4f} mean={u:.4f}" for i, (m, u) in enumerate(checks)) + + # check that exposure is stable once converged (no regressions) + in_range = False + for i, (median, mean) in enumerate(checks): + ok = _in_range(median, mean) + if in_range and not ok: + pytest.fail(f"{cam}: exposure regressed on sample {i+1} " + + f"(median={median:.4f}, mean={mean:.4f}, expected: ({lo}, {hi}))") + in_range = ok + def test_frame_skips(self, logs): for c in CAMERAS: assert set(np.diff(logs[c]['frameId'])) == {1, }, f"{c} has frame skips" @@ -91,7 +143,10 @@ class TestCamerad: def test_stress_test(self): os.environ['SPECTRA_ERROR_PROB'] = '0.008' - logs = run_and_log(["camerad", ], CAMERAS, 10) + try: + logs = run_and_log(["camerad", ], CAMERAS, 10) + finally: + del os.environ['SPECTRA_ERROR_PROB'] ts = msgs_to_time_series(logs) # we should see some jumps from introduced errors diff --git a/system/camerad/test/test_exposure.py b/system/camerad/test/test_exposure.py deleted file mode 100644 index 6f89e04800..0000000000 --- a/system/camerad/test/test_exposure.py +++ /dev/null @@ -1,51 +0,0 @@ -import time -import numpy as np -import pytest - -from openpilot.selfdrive.test.helpers import with_processes -from openpilot.system.camerad.snapshot import get_snapshots - -TEST_TIME = 45 -REPEAT = 5 - -@pytest.mark.tici -class TestCamerad: - @classmethod - def setup_class(cls): - pass - - def _numpy_rgb2gray(self, im): - ret = np.clip(im[:,:,2] * 0.114 + im[:,:,1] * 0.587 + im[:,:,0] * 0.299, 0, 255).astype(np.uint8) - return ret - - def _is_exposure_okay(self, i, med_mean=None): - if med_mean is None: - med_mean = np.array([[0.18,0.3],[0.18,0.3]]) - h, w = i.shape[:2] - i = i[h//10:9*h//10,w//10:9*w//10] - med_ex, mean_ex = med_mean - i = self._numpy_rgb2gray(i) - i_median = np.median(i) / 255. - i_mean = np.mean(i) / 255. - print([i_median, i_mean]) - return med_ex[0] < i_median < med_ex[1] and mean_ex[0] < i_mean < mean_ex[1] - - @with_processes(['camerad']) - def test_camera_operation(self): - passed = 0 - start = time.monotonic() - while time.monotonic() - start < TEST_TIME and passed < REPEAT: - rpic, dpic = get_snapshots(frame="roadCameraState", front_frame="driverCameraState") - wpic, _ = get_snapshots(frame="wideRoadCameraState") - - res = self._is_exposure_okay(rpic) - res = res and self._is_exposure_okay(dpic) - res = res and self._is_exposure_okay(wpic) - - if passed > 0 and not res: - passed = -passed # fails test if any failure after first sus - break - - passed += int(res) - time.sleep(2) - assert passed >= REPEAT From c393973916de984ae2444383e1de5685505f67ce Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sun, 15 Feb 2026 17:45:03 -0800 Subject: [PATCH 043/311] disable sim test, still not ready for it --- .github/workflows/tests.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index aab0f58b0a..fe7c8ef336 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -238,6 +238,7 @@ jobs: (github.event.pull_request.head.repo.full_name == 'commaai/openpilot')) && fromJSON('["namespace-profile-amd64-8x16", "namespace-experiments:docker.builds.local-cache=separate"]') || fromJSON('["ubuntu-24.04"]') }} + if: false # FIXME: Started to timeout recently steps: - uses: actions/checkout@v6 with: From 03a4f7ef9ac2955fffb18c570efe4e9d89742edc Mon Sep 17 00:00:00 2001 From: David <49467229+TheSecurityDev@users.noreply.github.com> Date: Sun, 15 Feb 2026 22:03:30 -0600 Subject: [PATCH 044/311] ui: add big (tizi) replay (#37198) * init: tizi_replay.py from pr 37123 * separate coverage folder * ui replay: adjust HOLD constant, fix coverage, use separate folder for coverage * openpilot prefix * fix directory * fix ui_state * fix settings click pos * remove * attempt merge replay files * remove * todo * fix recording * spacing * simplify * comment * refactor hold * refactor: remove layout definitions from VARIANTS and import conditionally in run_replay * refactor: remove VARIANTS config * add argparser with --big flag and improve coverage sources * refactor * lowercase * refactor: combine scripts * add types * refactor: move imports for gui_app and ui_state to improve coverage and organization * update * update script * comment * fix headless * todo * fix: get_time and get_frame_time determinism * todo * remove file accidently commited * fix: improve inject_click and handle_event for deterministic event timestamps * comment * simplify add * refactor script building * fix mici clicks * pass in pm * fix wifi state * refactor clicks * more refactor * click cancel instead of remove overlay * setup_send_fn * add setup fn * dummy update * change * remove todo * rename fn to frame_fn * refactor * fix workflow * rename raylib ui preview to old * rename mici workflow * fix diff videos * ignore sub html and mp4 files * rename for diff * rename for diff again (mici) * use ScriptEvent instead of DummyEvent, and move mouse events directly to it; rename hold to wait * fix: only import MouseEvent for type hint to fix coverage * adjust settings button click * clarify * move ScriptEvent to replay_script * add handle_event function * remove passing in setup function, and refactor click events * clean * formatting * refactor * no import * comment * refactor * refactor setup functions to replay_setup * refactor * add ReplayContext * refactor * move more setup functions * refactor and simplify * refactor * refactor: add Script class * refactor: enhance Script event handling and add wait functionality * refactor * remove setup_and_click * use script.setup instead * comments * rename wait_frames to wait_after * add comments * revert workflows * revert rename * move arg parsing to main * remove quotes * add type * return types * type * VariantType * rename to LayoutVariant * clarify * switch * todo * Revert "fix diff videos" This reverts commit 7a6e45a409cb7e6d7a330317639fcee74ef8bd31. * add todos * add more coverage * wait 2 frames by default * add comment * comment * switch * fix space Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * remove extra Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Remove unnecessary blank line in ReplayContext class * simplify --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Adeeb Shihadeh --- selfdrive/ui/tests/.gitignore | 5 +- selfdrive/ui/tests/diff/replay.py | 140 ++++++------- selfdrive/ui/tests/diff/replay_script.py | 249 +++++++++++++++++++++++ 3 files changed, 318 insertions(+), 76 deletions(-) create mode 100644 selfdrive/ui/tests/diff/replay_script.py diff --git a/selfdrive/ui/tests/.gitignore b/selfdrive/ui/tests/.gitignore index 98f2a5e8ce..90a32ffca2 100644 --- a/selfdrive/ui/tests/.gitignore +++ b/selfdrive/ui/tests/.gitignore @@ -3,7 +3,6 @@ test_translations test_ui/report_1 test_ui/raylib_report -diff/*.mp4 -diff/*.html +diff/**/*.mp4 +diff/**/*.html diff/.coverage -diff/htmlcov/ diff --git a/selfdrive/ui/tests/diff/replay.py b/selfdrive/ui/tests/diff/replay.py index a668cc776b..1cb0b42ada 100755 --- a/selfdrive/ui/tests/diff/replay.py +++ b/selfdrive/ui/tests/diff/replay.py @@ -1,41 +1,21 @@ #!/usr/bin/env python3 import os -import time +import argparse import coverage import pyray as rl -from dataclasses import dataclass -from openpilot.selfdrive.ui.tests.diff.diff import DIFF_OUT_DIR - -os.environ["RECORD"] = "1" -if "RECORD_OUTPUT" not in os.environ: - os.environ["RECORD_OUTPUT"] = "mici_ui_replay.mp4" - -os.environ["RECORD_OUTPUT"] = os.path.join(DIFF_OUT_DIR, os.environ["RECORD_OUTPUT"]) +from typing import Literal +from collections.abc import Callable +from cereal.messaging import PubMaster from openpilot.common.params import Params from openpilot.common.prefix import OpenpilotPrefix +from openpilot.selfdrive.ui.tests.diff.diff import DIFF_OUT_DIR from openpilot.system.version import terms_version, training_version -from openpilot.system.ui.lib.application import gui_app, MousePos, MouseEvent + +LayoutVariant = Literal["mici", "tizi"] FPS = 60 -HEADLESS = os.getenv("WINDOWED", "0") == "1" - - -@dataclass -class DummyEvent: - click: bool = False - # TODO: add some kind of intensity - swipe_left: bool = False - swipe_right: bool = False - swipe_down: bool = False - - -SCRIPT = [ - (0, DummyEvent()), - (FPS * 1, DummyEvent(click=True)), - (FPS * 2, DummyEvent(click=True)), - (FPS * 3, DummyEvent()), -] +HEADLESS = os.getenv("WINDOWED", "0") != "1" def setup_state(): @@ -44,66 +24,60 @@ def setup_state(): params.put("CompletedTrainingVersion", training_version) params.put("DongleId", "test123456789") params.put("UpdaterCurrentDescription", "0.10.1 / test-branch / abc1234 / Nov 30") - return None -def inject_click(coords): - events = [] - x, y = coords[0] - events.append(MouseEvent(pos=MousePos(x, y), slot=0, left_pressed=True, left_released=False, left_down=False, t=time.monotonic())) - for x, y in coords[1:]: - events.append(MouseEvent(pos=MousePos(x, y), slot=0, left_pressed=False, left_released=False, left_down=True, t=time.monotonic())) - x, y = coords[-1] - events.append(MouseEvent(pos=MousePos(x, y), slot=0, left_pressed=False, left_released=True, left_down=False, t=time.monotonic())) - - with gui_app._mouse._lock: - gui_app._mouse._events.extend(events) - - -def handle_event(event: DummyEvent): - if event.click: - inject_click([(gui_app.width // 2, gui_app.height // 2)]) - if event.swipe_left: - inject_click([(gui_app.width * 3 // 4, gui_app.height // 2), - (gui_app.width // 4, gui_app.height // 2), - (0, gui_app.height // 2)]) - if event.swipe_right: - inject_click([(gui_app.width // 4, gui_app.height // 2), - (gui_app.width * 3 // 4, gui_app.height // 2), - (gui_app.width, gui_app.height // 2)]) - if event.swipe_down: - inject_click([(gui_app.width // 2, gui_app.height // 4), - (gui_app.width // 2, gui_app.height * 3 // 4), - (gui_app.width // 2, gui_app.height)]) - - -def run_replay(): - from openpilot.selfdrive.ui.ui_state import ui_state # import here for correct param setup (e.g. training guide) - from openpilot.selfdrive.ui.mici.layouts.main import MiciMainLayout # import here for coverage +def run_replay(variant: LayoutVariant) -> None: + from openpilot.selfdrive.ui.ui_state import ui_state # Import within OpenpilotPrefix context so param values are setup correctly + from openpilot.system.ui.lib.application import gui_app # Import here for accurate coverage + from openpilot.selfdrive.ui.tests.diff.replay_script import build_script setup_state() os.makedirs(DIFF_OUT_DIR, exist_ok=True) - if not HEADLESS: + if HEADLESS: rl.set_config_flags(rl.FLAG_WINDOW_HIDDEN) gui_app.init_window("ui diff test", fps=FPS) - - main_layout = MiciMainLayout() + # Dynamically import main layout based on variant + if variant == "mici": + from openpilot.selfdrive.ui.mici.layouts.main import MiciMainLayout as MainLayout + else: + from openpilot.selfdrive.ui.layouts.main import MainLayout + main_layout = MainLayout() main_layout.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) + pm = PubMaster(["deviceState", "pandaStates", "driverStateV2", "selfdriveState"]) + script = build_script(pm, main_layout, variant) script_index = 0 + + send_fn: Callable | None = None frame = 0 # Override raylib timing functions to return deterministic values based on frame count instead of real time rl.get_frame_time = lambda: 1.0 / FPS rl.get_time = lambda: frame / FPS + # Main loop to replay events and render frames for should_render in gui_app.render(): - while script_index < len(SCRIPT) and SCRIPT[script_index][0] == frame: - _, event = SCRIPT[script_index] - handle_event(event) + # Handle all events for the current frame + while script_index < len(script) and script[script_index][0] == frame: + _, event = script[script_index] + # Call setup function, if any + if event.setup: + event.setup() + # Send mouse events to the application + if event.mouse_events: + with gui_app._mouse._lock: + gui_app._mouse._events.extend(event.mouse_events) + # Update persistent send function + if event.send_fn is not None: + send_fn = event.send_fn + # Move to next script event script_index += 1 + # Keep sending cereal messages for persistent states (onroad, alerts) + if send_fn: + send_fn() + ui_state.update() if should_render: @@ -111,7 +85,7 @@ def run_replay(): frame += 1 - if script_index >= len(SCRIPT): + if script_index >= len(script): break gui_app.close() @@ -121,14 +95,34 @@ def run_replay(): def main(): + parser = argparse.ArgumentParser() + parser.add_argument('--big', action='store_true', help='Use big UI layout (tizi/tici) instead of mici layout') + args = parser.parse_args() + + variant: LayoutVariant = 'tizi' if args.big else 'mici' + + if args.big: + os.environ["BIG"] = "1" + os.environ["RECORD"] = "1" + os.environ["RECORD_OUTPUT"] = os.path.join(DIFF_OUT_DIR, os.environ.get("RECORD_OUTPUT", f"{variant}_ui_replay.mp4")) + + print(f"Running {variant} UI replay...") with OpenpilotPrefix(): - cov = coverage.coverage(source=['openpilot.selfdrive.ui.mici']) + sources = ["openpilot.system.ui"] + if variant == "mici": + sources.append("openpilot.selfdrive.ui.mici") + omit = ["**/*tizi*", "**/*tici*"] # exclude files containing "tizi" or "tici" + else: + sources.extend(["openpilot.selfdrive.ui.layouts", "openpilot.selfdrive.ui.onroad", "openpilot.selfdrive.ui.widgets"]) + omit = ["**/*mici*"] # exclude files containing "mici" + cov = coverage.Coverage(source=sources, omit=omit) with cov.collect(): - run_replay() + run_replay(variant) cov.save() cov.report() - cov.html_report(directory=os.path.join(DIFF_OUT_DIR, 'htmlcov')) - print("HTML report: htmlcov/index.html") + directory = os.path.join(DIFF_OUT_DIR, f"htmlcov-{variant}") + cov.html_report(directory=directory) + print(f"HTML report: {directory}/index.html") if __name__ == "__main__": diff --git a/selfdrive/ui/tests/diff/replay_script.py b/selfdrive/ui/tests/diff/replay_script.py new file mode 100644 index 0000000000..9f2104ec49 --- /dev/null +++ b/selfdrive/ui/tests/diff/replay_script.py @@ -0,0 +1,249 @@ +from __future__ import annotations +from typing import TYPE_CHECKING +from collections.abc import Callable +from dataclasses import dataclass + +from cereal import car, log, messaging +from cereal.messaging import PubMaster +from openpilot.common.basedir import BASEDIR +from openpilot.common.params import Params +from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert +from openpilot.selfdrive.ui.tests.diff.replay import FPS, LayoutVariant +from openpilot.system.updated.updated import parse_release_notes + +WAIT = int(FPS * 0.5) # Default frames to wait after events + +AlertSize = log.SelfdriveState.AlertSize +AlertStatus = log.SelfdriveState.AlertStatus + +BRANCH_NAME = "this-is-a-really-super-mega-ultra-max-extreme-ultimate-long-branch-name" + + +@dataclass +class ScriptEvent: + if TYPE_CHECKING: + # Only import for type checking to avoid excluding the application code from coverage + from openpilot.system.ui.lib.application import MouseEvent + + setup: Callable | None = None # Setup function to run prior to adding mouse events + mouse_events: list[MouseEvent] | None = None # Mouse events to send to the application on this event's frame + send_fn: Callable | None = None # When set, the main loop uses this as the new persistent sender + + +ScriptEntry = tuple[int, ScriptEvent] # (frame, event) + + +class Script: + def __init__(self, fps: int) -> None: + self.fps = fps + self.frame = 0 + self.entries: list[ScriptEntry] = [] + + def get_frame_time(self) -> float: + return self.frame / self.fps + + def add(self, event: ScriptEvent, before: int = 0, after: int = 0) -> None: + """Add event to the script, optionally with the given number of frames to wait before or after the event.""" + self.frame += before + self.entries.append((self.frame, event)) + self.frame += after + + def end(self) -> None: + """Add a final empty event to mark the end of the script.""" + self.add(ScriptEvent()) # Without this, it will just end on the last event without waiting for any specified delay after it + + def wait(self, frames: int) -> None: + """Add a delay for the given number of frames followed by an empty event.""" + self.add(ScriptEvent(), before=frames) + + def setup(self, fn: Callable, wait_after: int = WAIT) -> None: + """Add a setup function to be called immediately followed by a delay of the given number of frames.""" + self.add(ScriptEvent(setup=fn), after=wait_after) + + def set_send(self, fn: Callable, wait_after: int = WAIT) -> None: + """Set a new persistent send function to be called every frame.""" + self.add(ScriptEvent(send_fn=fn), after=wait_after) + + # TODO: Also add more complex gestures, like swipe or drag + def click(self, x: int, y: int, wait_after: int = WAIT, wait_between: int = 2) -> None: + """Add a click event to the script for the given position and specify frames to wait between mouse events or after the click.""" + # NOTE: By default we wait a couple frames between mouse events so pressed states will be rendered + from openpilot.system.ui.lib.application import MouseEvent, MousePos + + # TODO: Add support for long press (left_down=True) + mouse_down = MouseEvent(pos=MousePos(x, y), slot=0, left_pressed=True, left_released=False, left_down=False, t=self.get_frame_time()) + self.add(ScriptEvent(mouse_events=[mouse_down]), after=wait_between) + mouse_up = MouseEvent(pos=MousePos(x, y), slot=0, left_pressed=False, left_released=True, left_down=False, t=self.get_frame_time()) + self.add(ScriptEvent(mouse_events=[mouse_up]), after=wait_after) + + +# --- Setup functions --- + +def put_update_params(params: Params | None = None) -> None: + if params is None: + params = Params() + params.put("UpdaterCurrentReleaseNotes", parse_release_notes(BASEDIR)) + params.put("UpdaterNewReleaseNotes", parse_release_notes(BASEDIR)) + params.put("UpdaterTargetBranch", BRANCH_NAME) + + +def setup_offroad_alerts() -> None: + put_update_params(Params()) + set_offroad_alert("Offroad_TemperatureTooHigh", True, extra_text='99C') + set_offroad_alert("Offroad_ExcessiveActuation", True, extra_text='longitudinal') + set_offroad_alert("Offroad_IsTakingSnapshot", True) + + +def setup_update_available() -> None: + params = Params() + params.put_bool("UpdateAvailable", True) + params.put("UpdaterNewDescription", f"0.10.2 / {BRANCH_NAME} / 0a1b2c3 / Jan 01") + put_update_params(params) + + +def setup_developer_params() -> None: + CP = car.CarParams() + CP.alphaLongitudinalAvailable = True + Params().put("CarParamsPersistent", CP.to_bytes()) + + +# --- Send functions --- + +def send_onroad(pm: PubMaster) -> None: + ds = messaging.new_message('deviceState') + ds.deviceState.started = True + ds.deviceState.networkType = log.DeviceState.NetworkType.wifi + + ps = messaging.new_message('pandaStates', 1) + ps.pandaStates[0].pandaType = log.PandaState.PandaType.dos + ps.pandaStates[0].ignitionLine = True + + pm.send('deviceState', ds) + pm.send('pandaStates', ps) + + +def make_network_state_setup(pm: PubMaster, network_type) -> Callable: + def _send() -> None: + ds = messaging.new_message('deviceState') + ds.deviceState.networkType = network_type + pm.send('deviceState', ds) + return _send + + +def make_alert_setup(pm: PubMaster, size, text1, text2, status) -> Callable: + def _send() -> None: + send_onroad(pm) + alert = messaging.new_message('selfdriveState') + ss = alert.selfdriveState + ss.alertSize = size + ss.alertText1 = text1 + ss.alertText2 = text2 + ss.alertStatus = status + pm.send('selfdriveState', alert) + return _send + + +# --- Script builders --- + +def build_mici_script(pm: PubMaster, main_layout, script: Script) -> None: + """Build the replay script for the mici layout.""" + from openpilot.system.ui.lib.application import gui_app + + center = (gui_app.width // 2, gui_app.height // 2) + + # TODO: Explore more + script.wait(FPS) + script.click(*center, FPS) # Open settings + script.click(*center, FPS) # Open toggles + script.end() + + +def build_tizi_script(pm: PubMaster, main_layout, script: Script) -> None: + """Build the replay script for the tizi layout.""" + + def make_home_refresh_setup(fn: Callable) -> Callable: + """Return setup function that calls the given function to modify state and forces an immediate refresh on the home layout.""" + from openpilot.selfdrive.ui.layouts.main import MainState + + def setup(): + fn() + main_layout._layouts[MainState.HOME].last_refresh = 0 + + return setup + + # TODO: Better way of organizing the events + + # === Homescreen === + script.set_send(make_network_state_setup(pm, log.DeviceState.NetworkType.wifi)) + + # === Offroad Alerts (auto-transitions via HomeLayout refresh) === + script.setup(make_home_refresh_setup(setup_offroad_alerts)) + + # === Update Available (auto-transitions via HomeLayout refresh) === + script.setup(make_home_refresh_setup(setup_update_available)) + + # === Settings - Device (click sidebar settings button) === + script.click(150, 90) + script.click(1985, 790) # reset calibration confirmation + script.click(650, 750) # cancel + + # === Settings - Network === + script.click(278, 450) + script.click(1880, 100) # advanced network settings + script.click(630, 80) # back + + # === Settings - Toggles === + script.click(278, 600) + script.click(1200, 280) # experimental mode description + + # === Settings - Software === + script.setup(put_update_params, wait_after=0) + script.click(278, 720) + + # === Settings - Firehose === + script.click(278, 845) + + # === Settings - Developer (set CarParamsPersistent first) === + script.setup(setup_developer_params, wait_after=0) + script.click(278, 950) + script.click(2000, 960) # toggle alpha long + script.click(1500, 875) # confirm + + # === Keyboard modal (SSH keys button in developer panel) === + script.click(1930, 470) # click SSH keys + script.click(1930, 115) # click cancel on keyboard + + # === Close settings === + script.click(250, 160) + + # === Onroad === + script.set_send(lambda: send_onroad(pm)) + script.click(1000, 500) # click onroad to toggle sidebar + + # === Onroad alerts === + # Small alert (normal) + script.set_send(make_alert_setup(pm, AlertSize.small, "Small Alert", "This is a small alert", AlertStatus.normal)) + # Medium alert (userPrompt) + script.set_send(make_alert_setup(pm, AlertSize.mid, "Medium Alert", "This is a medium alert", AlertStatus.userPrompt)) + # Full alert (critical) + script.set_send(make_alert_setup(pm, AlertSize.full, "DISENGAGE IMMEDIATELY", "Driver Distracted", AlertStatus.critical)) + # Full alert multiline + script.set_send(make_alert_setup(pm, AlertSize.full, "Reverse\nGear", "", AlertStatus.normal)) + # Full alert long text + script.set_send(make_alert_setup(pm, AlertSize.full, "TAKE CONTROL IMMEDIATELY", "Calibration Invalid: Remount Device & Recalibrate", AlertStatus.userPrompt)) + + # End + script.end() + + +def build_script(pm: PubMaster, main_layout, variant: LayoutVariant) -> list[ScriptEntry]: + """Build the replay script for the appropriate layout variant and return list of script entries.""" + print(f"Building {variant} replay script...") + + script = Script(FPS) + builder = build_tizi_script if variant == 'tizi' else build_mici_script + builder(pm, main_layout, script) + + print(f"Built replay script with {len(script.entries)} events and {script.frame} frames ({script.get_frame_time():.2f} seconds)") + + return script.entries From 8831b11a2496d3002c2ab73c40b50aec5813f60d Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sun, 15 Feb 2026 20:11:17 -0800 Subject: [PATCH 045/311] remove old raylib screenshot tool (#37225) --- .github/workflows/raylib_ui_preview.yaml | 175 -- .github/workflows/tests.yaml | 26 - pyproject.toml | 2 - selfdrive/ui/tests/.gitignore | 1 - .../ui/tests/test_ui/print_mouse_coords.py | 36 - .../ui/tests/test_ui/raylib_screenshots.py | 317 --- selfdrive/ui/tests/test_ui/template.html | 34 - tools/jotpluggler/pluggle.py | 16 +- uv.lock | 2484 ----------------- 9 files changed, 7 insertions(+), 3084 deletions(-) delete mode 100644 .github/workflows/raylib_ui_preview.yaml delete mode 100755 selfdrive/ui/tests/test_ui/print_mouse_coords.py delete mode 100755 selfdrive/ui/tests/test_ui/raylib_screenshots.py delete mode 100644 selfdrive/ui/tests/test_ui/template.html diff --git a/.github/workflows/raylib_ui_preview.yaml b/.github/workflows/raylib_ui_preview.yaml deleted file mode 100644 index 9044a97f53..0000000000 --- a/.github/workflows/raylib_ui_preview.yaml +++ /dev/null @@ -1,175 +0,0 @@ -name: "raylib ui preview" -on: - push: - branches: - - master - pull_request_target: - types: [assigned, opened, synchronize, reopened, edited] - branches: - - 'master' - paths: - - 'selfdrive/assets/**' - - 'selfdrive/ui/**' - - 'system/ui/**' - workflow_dispatch: - -env: - UI_JOB_NAME: "Create raylib UI Report" - REPORT_NAME: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && 'master' || github.event.number }} - SHA: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && github.sha || github.event.pull_request.head.sha }} - BRANCH_NAME: "openpilot/pr-${{ github.event.number }}-raylib-ui" - -jobs: - preview: - if: github.repository == 'commaai/openpilot' - name: preview - runs-on: ubuntu-latest - timeout-minutes: 20 - permissions: - contents: read - pull-requests: write - actions: read - steps: - - name: Waiting for ui generation to start - run: sleep 30 - - - name: Waiting for ui generation to end - uses: lewagon/wait-on-check-action@v1.3.4 - with: - ref: ${{ env.SHA }} - check-name: ${{ env.UI_JOB_NAME }} - repo-token: ${{ secrets.GITHUB_TOKEN }} - allowed-conclusions: success - wait-interval: 20 - - - name: Getting workflow run ID - id: get_run_id - run: | - echo "run_id=$(curl https://api.github.com/repos/${{ github.repository }}/commits/${{ env.SHA }}/check-runs | jq -r '.check_runs[] | select(.name == "${{ env.UI_JOB_NAME }}") | .html_url | capture("(?[0-9]+)") | .number')" >> $GITHUB_OUTPUT - - - name: Getting proposed ui - id: download-artifact - uses: dawidd6/action-download-artifact@v6 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - run_id: ${{ steps.get_run_id.outputs.run_id }} - search_artifacts: true - name: raylib-report-1-${{ env.REPORT_NAME }} - path: ${{ github.workspace }}/pr_ui - - - name: Getting master ui - uses: actions/checkout@v6 - with: - repository: commaai/ci-artifacts - ssh-key: ${{ secrets.CI_ARTIFACTS_DEPLOY_KEY }} - path: ${{ github.workspace }}/master_ui_raylib - ref: openpilot_master_ui_raylib - - - name: Saving new master ui - if: github.ref == 'refs/heads/master' && github.event_name == 'push' - working-directory: ${{ github.workspace }}/master_ui_raylib - run: | - git checkout --orphan=new_master_ui_raylib - git rm -rf * - git branch -D openpilot_master_ui_raylib - git branch -m openpilot_master_ui_raylib - git config user.name "GitHub Actions Bot" - git config user.email "<>" - mv ${{ github.workspace }}/pr_ui/*.png . - git add . - git commit -m "raylib screenshots for commit ${{ env.SHA }}" - git push origin openpilot_master_ui_raylib --force - - - name: Finding diff - if: github.event_name == 'pull_request_target' - id: find_diff - run: >- - sudo apt-get update && sudo apt-get install -y imagemagick - - scenes=$(find ${{ github.workspace }}/pr_ui/*.png -type f -printf "%f\n" | cut -d '.' -f 1 | grep -v 'pair_device') - A=($scenes) - - DIFF="" - TABLE="
All Screenshots" - TABLE="${TABLE}" - - for ((i=0; i<${#A[*]}; i=i+1)); - do - # Check if the master file exists - if [ ! -f "${{ github.workspace }}/master_ui_raylib/${A[$i]}.png" ]; then - # This is a new file in PR UI that doesn't exist in master - DIFF="${DIFF}
" - DIFF="${DIFF}${A[$i]} : \$\${\\color{cyan}\\text{NEW}}\$\$" - DIFF="${DIFF}
" - - DIFF="${DIFF}" - DIFF="${DIFF} " - DIFF="${DIFF}" - - DIFF="${DIFF}
" - DIFF="${DIFF}
" - elif ! compare -fuzz 2% -highlight-color DeepSkyBlue1 -lowlight-color Black -compose Src ${{ github.workspace }}/master_ui_raylib/${A[$i]}.png ${{ github.workspace }}/pr_ui/${A[$i]}.png ${{ github.workspace }}/pr_ui/${A[$i]}_diff.png; then - convert ${{ github.workspace }}/pr_ui/${A[$i]}_diff.png -transparent black mask.png - composite mask.png ${{ github.workspace }}/master_ui_raylib/${A[$i]}.png composite_diff.png - convert -delay 100 ${{ github.workspace }}/master_ui_raylib/${A[$i]}.png composite_diff.png -loop 0 ${{ github.workspace }}/pr_ui/${A[$i]}_diff.gif - - mv ${{ github.workspace }}/master_ui_raylib/${A[$i]}.png ${{ github.workspace }}/pr_ui/${A[$i]}_master_ref.png - - DIFF="${DIFF}
" - DIFF="${DIFF}${A[$i]} : \$\${\\color{red}\\text{DIFFERENT}}\$\$" - DIFF="${DIFF}" - - DIFF="${DIFF}" - DIFF="${DIFF} " - DIFF="${DIFF} " - DIFF="${DIFF}" - - DIFF="${DIFF}" - DIFF="${DIFF} " - DIFF="${DIFF} " - DIFF="${DIFF}" - - DIFF="${DIFF}
master proposed
diff composite diff
" - DIFF="${DIFF}
" - else - rm -f ${{ github.workspace }}/pr_ui/${A[$i]}_diff.png - fi - - INDEX=$(($i % 2)) - if [[ $INDEX -eq 0 ]]; then - TABLE="${TABLE}" - fi - TABLE="${TABLE} " - if [[ $INDEX -eq 1 || $(($i + 1)) -eq ${#A[*]} ]]; then - TABLE="${TABLE}" - fi - done - - TABLE="${TABLE}" - - echo "DIFF=$DIFF$TABLE" >> "$GITHUB_OUTPUT" - - - name: Saving proposed ui - if: github.event_name == 'pull_request_target' - working-directory: ${{ github.workspace }}/master_ui_raylib - run: | - git config user.name "GitHub Actions Bot" - git config user.email "<>" - git checkout --orphan=${{ env.BRANCH_NAME }} - git rm -rf * - mv ${{ github.workspace }}/pr_ui/* . - git add . - git commit -m "raylib screenshots for PR #${{ github.event.number }}" - git push origin ${{ env.BRANCH_NAME }} --force - - - name: Comment Screenshots on PR - if: github.event_name == 'pull_request_target' - uses: thollander/actions-comment-pull-request@v2 - with: - message: | - - ## raylib UI Preview - ${{ steps.find_diff.outputs.DIFF }} - comment_tag: run_id_screenshots_raylib - pr_number: ${{ github.event.number }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index fe7c8ef336..2cca344423 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -255,32 +255,6 @@ jobs: source selfdrive/test/setup_vsound.sh && \ CI=1 pytest -s tools/sim/tests/test_metadrive_bridge.py" - create_raylib_ui_report: - name: Create raylib UI Report - runs-on: ${{ - (github.repository == 'commaai/openpilot') && - ((github.event_name != 'pull_request') || - (github.event.pull_request.head.repo.full_name == 'commaai/openpilot')) - && fromJSON('["namespace-profile-amd64-8x16", "namespace-experiments:docker.builds.local-cache=separate"]') - || fromJSON('["ubuntu-24.04"]') }} - steps: - - uses: actions/checkout@v6 - with: - submodules: true - - uses: ./.github/workflows/setup-with-retry - - name: Build openpilot - run: ${{ env.RUN }} "scons -j$(nproc)" - - name: Create raylib UI Report - run: > - ${{ env.RUN }} "PYTHONWARNINGS=ignore && - source selfdrive/test/setup_xvfb.sh && - python3 selfdrive/ui/tests/test_ui/raylib_screenshots.py" - - name: Upload Raylib UI Report - uses: actions/upload-artifact@v6 - with: - name: raylib-report-${{ inputs.run_number || '1' }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && 'master' || github.event.number }} - path: selfdrive/ui/tests/test_ui/raylib_report/screenshots - create_mici_raylib_ui_report: name: Create mici raylib UI Report runs-on: ${{ diff --git a/pyproject.toml b/pyproject.toml index 2a6d619f7f..8eb86101af 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -100,8 +100,6 @@ dev = [ "matplotlib", "opencv-python-headless", "parameterized >=0.8, <0.9", - "pyautogui", - "pywinctl", ] tools = [ diff --git a/selfdrive/ui/tests/.gitignore b/selfdrive/ui/tests/.gitignore index 90a32ffca2..39e2f8970c 100644 --- a/selfdrive/ui/tests/.gitignore +++ b/selfdrive/ui/tests/.gitignore @@ -1,7 +1,6 @@ test test_translations test_ui/report_1 -test_ui/raylib_report diff/**/*.mp4 diff/**/*.html diff --git a/selfdrive/ui/tests/test_ui/print_mouse_coords.py b/selfdrive/ui/tests/test_ui/print_mouse_coords.py deleted file mode 100755 index 1e88ce57d3..0000000000 --- a/selfdrive/ui/tests/test_ui/print_mouse_coords.py +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env python3 -""" -Simple script to print mouse coordinates on Ubuntu. -Run with: python print_mouse_coords.py -Press Ctrl+C to exit. -""" - -from pynput import mouse - -print("Mouse coordinate printer - Press Ctrl+C to exit") -print("Click to set the top left origin") - -origin: tuple[int, int] | None = None -clicks: list[tuple[int, int]] = [] - - -def on_click(x, y, button, pressed): - global origin, clicks - if pressed: # Only on mouse down, not up - if origin is None: - origin = (x, y) - print(f"Origin set to: {x},{y}") - else: - rel_x = x - origin[0] - rel_y = y - origin[1] - clicks.append((rel_x, rel_y)) - print(f"Clicks: {clicks}") - - -if __name__ == "__main__": - try: - # Start mouse listener - with mouse.Listener(on_click=on_click) as listener: - listener.join() - except KeyboardInterrupt: - print("\nExiting...") diff --git a/selfdrive/ui/tests/test_ui/raylib_screenshots.py b/selfdrive/ui/tests/test_ui/raylib_screenshots.py deleted file mode 100755 index 481ac111be..0000000000 --- a/selfdrive/ui/tests/test_ui/raylib_screenshots.py +++ /dev/null @@ -1,317 +0,0 @@ -#!/usr/bin/env python3 -import os -import sys -import shutil -import time -import pathlib -from collections import namedtuple - -import pyautogui -import pywinctl - -from cereal import car, log -from cereal import messaging -from cereal.messaging import PubMaster -from openpilot.common.basedir import BASEDIR -from openpilot.common.params import Params -from openpilot.common.prefix import OpenpilotPrefix -from openpilot.selfdrive.test.helpers import with_processes -from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert -from openpilot.system.updated.updated import parse_release_notes -from openpilot.system.version import terms_version, training_version - -AlertSize = log.SelfdriveState.AlertSize -AlertStatus = log.SelfdriveState.AlertStatus - -TEST_DIR = pathlib.Path(__file__).parent -TEST_OUTPUT_DIR = TEST_DIR / "raylib_report" -SCREENSHOTS_DIR = TEST_OUTPUT_DIR / "screenshots" -UI_DELAY = 0.5 - -BRANCH_NAME = "this-is-a-really-super-mega-ultra-max-extreme-ultimate-long-branch-name" -VERSION = f"0.10.1 / {BRANCH_NAME} / 7864838 / Oct 03" - -# Offroad alerts to test -OFFROAD_ALERTS = ['Offroad_IsTakingSnapshot'] - - -def put_update_params(params: Params): - params.put("UpdaterCurrentReleaseNotes", parse_release_notes(BASEDIR)) - params.put("UpdaterNewReleaseNotes", parse_release_notes(BASEDIR)) - params.put("UpdaterTargetBranch", BRANCH_NAME) - - -def setup_homescreen(click, pm: PubMaster): - pass - - -def setup_homescreen_update_available(click, pm: PubMaster): - params = Params() - params.put_bool("UpdateAvailable", True) - put_update_params(params) - setup_offroad_alert(click, pm) - - -def setup_settings(click, pm: PubMaster): - click(100, 100) - - -def close_settings(click, pm: PubMaster): - click(240, 216) - - -def setup_settings_network(click, pm: PubMaster): - setup_settings(click, pm) - click(278, 450) - - -def setup_settings_network_advanced(click, pm: PubMaster): - setup_settings_network(click, pm) - click(1880, 100) - - -def setup_settings_toggles(click, pm: PubMaster): - setup_settings(click, pm) - click(278, 600) - - -def setup_settings_software(click, pm: PubMaster): - put_update_params(Params()) - setup_settings(click, pm) - click(278, 720) - - -def setup_settings_software_download(click, pm: PubMaster): - params = Params() - # setup_settings_software but with "DOWNLOAD" button to test long text - params.put("UpdaterState", "idle") - params.put_bool("UpdaterFetchAvailable", True) - setup_settings_software(click, pm) - - -def setup_settings_software_release_notes(click, pm: PubMaster): - setup_settings_software(click, pm) - click(588, 110) # expand description for current version - - -def setup_settings_software_branch_switcher(click, pm: PubMaster): - setup_settings_software(click, pm) - params = Params() - params.put("UpdaterAvailableBranches", f"master,nightly,release,{BRANCH_NAME}") - params.put("GitBranch", BRANCH_NAME) # should be on top - params.put("UpdaterTargetBranch", "nightly") # should be selected - click(1984, 449) - - -def setup_settings_firehose(click, pm: PubMaster): - setup_settings(click, pm) - click(278, 845) - - -def setup_settings_developer(click, pm: PubMaster): - CP = car.CarParams() - CP.alphaLongitudinalAvailable = True # show alpha long control toggle - Params().put("CarParamsPersistent", CP.to_bytes()) - - setup_settings(click, pm) - click(278, 950) - - -def setup_keyboard(click, pm: PubMaster): - setup_settings_developer(click, pm) - click(1930, 470) - - -def setup_pair_device(click, pm: PubMaster): - click(1950, 800) - - -def setup_offroad_alert(click, pm: PubMaster): - put_update_params(Params()) - set_offroad_alert("Offroad_TemperatureTooHigh", True, extra_text='99C') - set_offroad_alert("Offroad_ExcessiveActuation", True, extra_text='longitudinal') - for alert in OFFROAD_ALERTS: - set_offroad_alert(alert, True) - - setup_settings(click, pm) - close_settings(click, pm) - - -def setup_confirmation_dialog(click, pm: PubMaster): - setup_settings(click, pm) - click(1985, 791) # reset calibration - - -def setup_experimental_mode_description(click, pm: PubMaster): - setup_settings_toggles(click, pm) - click(1200, 280) # expand description for experimental mode - - -def setup_openpilot_long_confirmation_dialog(click, pm: PubMaster): - setup_settings_developer(click, pm) - click(2000, 960) # toggle openpilot longitudinal control - - -def setup_onroad(click, pm: PubMaster): - ds = messaging.new_message('deviceState') - ds.deviceState.started = True - - ps = messaging.new_message('pandaStates', 1) - ps.pandaStates[0].pandaType = log.PandaState.PandaType.dos - ps.pandaStates[0].ignitionLine = True - - driverState = messaging.new_message('driverStateV2') - driverState.driverStateV2.leftDriverData.faceOrientation = [0, 0, 0] - - for _ in range(5): - pm.send('deviceState', ds) - pm.send('pandaStates', ps) - pm.send('driverStateV2', driverState) - ds.clear_write_flag() - ps.clear_write_flag() - driverState.clear_write_flag() - time.sleep(0.05) - - -def setup_onroad_sidebar(click, pm: PubMaster): - setup_onroad(click, pm) - click(100, 100) # open sidebar - - -def setup_onroad_alert(click, pm: PubMaster, size: log.SelfdriveState.AlertSize, text1: str, text2: str, status: log.SelfdriveState.AlertStatus): - setup_onroad(click, pm) - alert = messaging.new_message('selfdriveState') - ss = alert.selfdriveState - ss.alertSize = size - ss.alertText1 = text1 - ss.alertText2 = text2 - ss.alertStatus = status - for _ in range(5): - pm.send('selfdriveState', alert) - alert.clear_write_flag() - time.sleep(0.05) - - -def setup_onroad_small_alert(click, pm: PubMaster): - setup_onroad_alert(click, pm, AlertSize.small, "Small Alert", "This is a small alert", AlertStatus.normal) - - -def setup_onroad_medium_alert(click, pm: PubMaster): - setup_onroad_alert(click, pm, AlertSize.mid, "Medium Alert", "This is a medium alert", AlertStatus.userPrompt) - - -def setup_onroad_full_alert(click, pm: PubMaster): - setup_onroad_alert(click, pm, AlertSize.full, "DISENGAGE IMMEDIATELY", "Driver Distracted", AlertStatus.critical) - - -def setup_onroad_full_alert_multiline(click, pm: PubMaster): - setup_onroad_alert(click, pm, AlertSize.full, "Reverse\nGear", "", AlertStatus.normal) - - -def setup_onroad_full_alert_long_text(click, pm: PubMaster): - setup_onroad_alert(click, pm, AlertSize.full, "TAKE CONTROL IMMEDIATELY", "Calibration Invalid: Remount Device & Recalibrate", AlertStatus.userPrompt) - - -CASES = { - "homescreen": setup_homescreen, - "homescreen_paired": setup_homescreen, - "homescreen_prime": setup_homescreen, - "homescreen_update_available": setup_homescreen_update_available, - "homescreen_unifont": setup_homescreen, - "settings_device": setup_settings, - "settings_network": setup_settings_network, - "settings_network_advanced": setup_settings_network_advanced, - "settings_toggles": setup_settings_toggles, - "settings_software": setup_settings_software, - "settings_software_download": setup_settings_software_download, - "settings_software_release_notes": setup_settings_software_release_notes, - "settings_software_branch_switcher": setup_settings_software_branch_switcher, - "settings_firehose": setup_settings_firehose, - "settings_developer": setup_settings_developer, - "keyboard": setup_keyboard, - "pair_device": setup_pair_device, - "offroad_alert": setup_offroad_alert, - "confirmation_dialog": setup_confirmation_dialog, - "experimental_mode_description": setup_experimental_mode_description, - "openpilot_long_confirmation_dialog": setup_openpilot_long_confirmation_dialog, - "onroad": setup_onroad, - "onroad_sidebar": setup_onroad_sidebar, - "onroad_small_alert": setup_onroad_small_alert, - "onroad_medium_alert": setup_onroad_medium_alert, - "onroad_full_alert": setup_onroad_full_alert, - "onroad_full_alert_multiline": setup_onroad_full_alert_multiline, - "onroad_full_alert_long_text": setup_onroad_full_alert_long_text, -} - - -class TestUI: - def __init__(self): - os.environ["SCALE"] = os.getenv("SCALE", "1") - os.environ["BIG"] = "1" - sys.modules["mouseinfo"] = False - - def setup(self): - # Seed minimal offroad state - self.pm = PubMaster(["deviceState", "pandaStates", "driverStateV2", "selfdriveState"]) - ds = messaging.new_message('deviceState') - ds.deviceState.networkType = log.DeviceState.NetworkType.wifi - for _ in range(5): - self.pm.send('deviceState', ds) - ds.clear_write_flag() - time.sleep(0.05) - time.sleep(0.5) - try: - self.ui = pywinctl.getWindowsWithTitle("UI")[0] - except Exception as e: - print(f"failed to find ui window, assuming that it's in the top left (for Xvfb) {e}") - self.ui = namedtuple("bb", ["left", "top", "width", "height"])(0, 0, 2160, 1080) - - def screenshot(self, name: str): - full_screenshot = pyautogui.screenshot() - cropped = full_screenshot.crop((self.ui.left, self.ui.top, self.ui.left + self.ui.width, self.ui.top + self.ui.height)) - cropped.save(SCREENSHOTS_DIR / f"{name}.png") - - def click(self, x: int, y: int, *args, **kwargs): - pyautogui.mouseDown(self.ui.left + x, self.ui.top + y, *args, **kwargs) - time.sleep(0.01) - pyautogui.mouseUp(self.ui.left + x, self.ui.top + y, *args, **kwargs) - - @with_processes(["ui"]) - def test_ui(self, name, setup_case): - self.setup() - time.sleep(UI_DELAY) # wait for UI to start - setup_case(self.click, self.pm) - self.screenshot(name) - - -def create_screenshots(): - if TEST_OUTPUT_DIR.exists(): - shutil.rmtree(TEST_OUTPUT_DIR) - SCREENSHOTS_DIR.mkdir(parents=True) - - t = TestUI() - for name, setup in CASES.items(): - with OpenpilotPrefix(): - params = Params() - params.put("DongleId", "123456789012345") - - # Set branch name - params.put("UpdaterCurrentDescription", VERSION) - params.put("UpdaterNewDescription", VERSION) - - # Set terms and training version (to skip onboarding) - params.put("HasAcceptedTerms", terms_version) - params.put("CompletedTrainingVersion", training_version) - - if name == "homescreen_paired": - params.put("PrimeType", 0) # NONE - elif name == "homescreen_prime": - params.put("PrimeType", 2) # LITE - elif name == "homescreen_unifont": - params.put("LanguageSetting", "zh-CHT") # Traditional Chinese - - t.test_ui(name, setup) - - -if __name__ == "__main__": - create_screenshots() diff --git a/selfdrive/ui/tests/test_ui/template.html b/selfdrive/ui/tests/test_ui/template.html deleted file mode 100644 index 68df5879e6..0000000000 --- a/selfdrive/ui/tests/test_ui/template.html +++ /dev/null @@ -1,34 +0,0 @@ - - - - -{% for name, (image, ref_image) in cases.items() %} - -

{{name}}

-
-
- -
-
- -
- -{% endfor %} - \ No newline at end of file diff --git a/tools/jotpluggler/pluggle.py b/tools/jotpluggler/pluggle.py index 6fb8d5049b..92664ae5b3 100755 --- a/tools/jotpluggler/pluggle.py +++ b/tools/jotpluggler/pluggle.py @@ -1,8 +1,6 @@ #!/usr/bin/env python3 import argparse import os -import pyautogui -import subprocess import dearpygui.dearpygui as dpg import multiprocessing import uuid @@ -316,11 +314,12 @@ def main(route_to_load=None, layout_to_load=None): dpg.create_context() # TODO: find better way of calculating display scaling - try: - w, h = next(tuple(map(int, l.split()[0].split('x'))) for l in subprocess.check_output(['xrandr']).decode().split('\n') if '*' in l) # actual resolution - scale = pyautogui.size()[0] / w # scaled resolution - except Exception: - scale = 1 + #try: + # w, h = next(tuple(map(int, l.split()[0].split('x'))) for l in subprocess.check_output(['xrandr']).decode().split('\n') if '*' in l) # actual resolution + # scale = pyautogui.size()[0] / w # scaled resolution + #except Exception: + # scale = 1 + scale = 1 with dpg.font_registry(): default_font = dpg.add_font(os.path.join(BASEDIR, "selfdrive/assets/fonts/JetBrainsMono-Medium.ttf"), int(13 * scale * 2)) # 2x then scale for hidpi @@ -328,9 +327,8 @@ def main(route_to_load=None, layout_to_load=None): dpg.set_global_font_scale(0.5) viewport_width, viewport_height = int(1200 * scale), int(800 * scale) - mouse_x, mouse_y = pyautogui.position() # TODO: find better way of creating the window where the user is (default dpg behavior annoying on multiple displays) dpg.create_viewport( - title='JotPluggler', width=viewport_width, height=viewport_height, x_pos=mouse_x - viewport_width // 2, y_pos=mouse_y - viewport_height // 2 + title='JotPluggler', width=viewport_width, height=viewport_height, ) dpg.setup_dearpygui() diff --git a/uv.lock b/uv.lock index cd077d3de7..51e71ce645 100644 --- a/uv.lock +++ b/uv.lock @@ -374,18 +374,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, ] -[[package]] -name = "ewmhlib" -version = "0.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "python-xlib", marker = "sys_platform == 'linux'" }, - { name = "typing-extensions" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/2f/3a/46ca34abf0725a754bc44ef474ad34aedcc3ea23b052d97b18b76715a6a9/EWMHlib-0.2-py3-none-any.whl", hash = "sha256:f5b07d8cfd4c7734462ee744c32d490f2f3233fa7ab354240069344208d2f6f5", size = 46657, upload-time = "2024-04-17T08:15:56.338Z" }, -] - [[package]] name = "execnet" version = "2.1.2" @@ -720,17 +708,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/16/2e/9acc86985bfad8f2c2d30291b27cd2bb4c74cea08695bd540906ed744249/ml_dtypes-0.5.4-cp312-cp312-win_arm64.whl", hash = "sha256:9bad06436568442575beb2d03389aa7456c690a5b05892c471215bfd8cf39460", size = 160793, upload-time = "2025-11-17T22:31:55.358Z" }, ] -[[package]] -name = "mouseinfo" -version = "0.1.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyperclip" }, - { name = "python3-xlib", marker = "sys_platform == 'linux'" }, - { name = "rubicon-objc", marker = "sys_platform == 'darwin'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/28/fa/b2ba8229b9381e8f6381c1dcae6f4159a7f72349e414ed19cfbbd1817173/MouseInfo-0.1.3.tar.gz", hash = "sha256:2c62fb8885062b8e520a3cce0a297c657adcc08c60952eb05bc8256ef6f7f6e7", size = 10850, upload-time = "2020-03-27T21:20:10.136Z" } - [[package]] name = "mpmath" version = "1.3.0" @@ -873,8 +850,6 @@ dev = [ { name = "matplotlib" }, { name = "opencv-python-headless" }, { name = "parameterized" }, - { name = "pyautogui" }, - { name = "pywinctl" }, ] docs = [ { name = "jinja2" }, @@ -930,7 +905,6 @@ requires-dist = [ { name = "pre-commit-hooks", marker = "extra == 'testing'" }, { name = "psutil" }, { name = "pyaudio" }, - { name = "pyautogui", marker = "extra == 'dev'" }, { name = "pycapnp", specifier = "==2.1.0" }, { name = "pycryptodome" }, { name = "pyjwt" }, @@ -943,7 +917,6 @@ requires-dist = [ { name = "pytest-subtests", marker = "extra == 'testing'" }, { name = "pytest-timeout", marker = "extra == 'testing'" }, { name = "pytest-xdist", marker = "extra == 'testing'", git = "https://github.com/sshane/pytest-xdist?rev=2b4372bd62699fb412c4fe2f95bf9f01bd2018da" }, - { name = "pywinctl", marker = "extra == 'dev'" }, { name = "pyzmq" }, { name = "qrcode" }, { name = "raylib", specifier = ">5.5.0.3" }, @@ -1144,22 +1117,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b0/6a/d25812e5f79f06285767ec607b39149d02aa3b31d50c2269768f48768930/PyAudio-0.2.14-cp312-cp312-win_amd64.whl", hash = "sha256:12f2f1ba04e06ff95d80700a78967897a489c05e093e3bffa05a84ed9c0a7fa3", size = 164126, upload-time = "2023-11-07T07:11:41.539Z" }, ] -[[package]] -name = "pyautogui" -version = "0.9.54" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mouseinfo" }, - { name = "pygetwindow" }, - { name = "pymsgbox" }, - { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, - { name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin'" }, - { name = "pyscreeze" }, - { name = "python3-xlib", marker = "sys_platform == 'linux'" }, - { name = "pytweening" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/65/ff/cdae0a8c2118a0de74b6cf4cbcdcaf8fd25857e6c3f205ce4b1794b27814/PyAutoGUI-0.9.54.tar.gz", hash = "sha256:dd1d29e8fd118941cb193f74df57e5c6ff8e9253b99c7b04f39cfc69f3ae04b2", size = 61236, upload-time = "2023-05-24T20:11:32.972Z" } - [[package]] name = "pycapnp" version = "2.1.0" @@ -1220,15 +1177,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/c4/b4d4827c93ef43c01f599ef31453ccc1c132b353284fc6c87d535c233129/pyee-13.0.1-py3-none-any.whl", hash = "sha256:af2f8fede4171ef667dfded53f96e2ed0d6e6bd7ee3bb46437f77e3b57689228", size = 15659, upload-time = "2026-02-14T21:12:26.263Z" }, ] -[[package]] -name = "pygetwindow" -version = "0.0.9" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyrect" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e1/70/c7a4f46dbf06048c6d57d9489b8e0f9c4c3d36b7479f03c5ca97eaa2541d/PyGetWindow-0.0.9.tar.gz", hash = "sha256:17894355e7d2b305cd832d717708384017c1698a90ce24f6f7fbf0242dd0a688", size = 9699, upload-time = "2020-10-04T02:12:50.806Z" } - [[package]] name = "pygments" version = "2.19.2" @@ -1269,2342 +1217,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8e/ec/6e02b2561d056ea5b33046e3cad21238e6a9097b97d6ccc0fbe52b50c858/pylibsrtp-1.0.0-cp310-abi3-win_arm64.whl", hash = "sha256:2696bdb2180d53ac55d0eb7b58048a2aa30cd4836dd2ca683669889137a94d2a", size = 1159246, upload-time = "2025-10-13T16:12:30.285Z" }, ] -[[package]] -name = "pymonctl" -version = "0.92" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "ewmhlib", marker = "sys_platform == 'linux'" }, - { name = "pyobjc", marker = "sys_platform == 'darwin'" }, - { name = "python-xlib", marker = "sys_platform == 'linux'" }, - { name = "pywin32", marker = "sys_platform == 'win32'" }, - { name = "typing-extensions" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/2d/13/076a20da28b82be281f7e43e16d9da0f545090f5d14b2125699232b9feba/PyMonCtl-0.92-py3-none-any.whl", hash = "sha256:2495d8dab78f9a7dbce37e74543e60b8bd404a35c3108935697dda7768611b5a", size = 45945, upload-time = "2024-04-22T10:07:09.566Z" }, -] - -[[package]] -name = "pymsgbox" -version = "2.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ae/6a/e80da7594ee598a776972d09e2813df2b06b3bc29218f440631dfa7c78a8/pymsgbox-2.0.1.tar.gz", hash = "sha256:98d055c49a511dcc10fa08c3043e7102d468f5e4b3a83c6d3c61df722c7d798d", size = 20768, upload-time = "2025-09-09T00:38:56.863Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6f/3e/08c8cac81b2b2f7502746e6b9c8e5b0ec6432cd882c605560fc409aaf087/pymsgbox-2.0.1-py3-none-any.whl", hash = "sha256:5de8ec19bca2ca7e6c09d39c817c83f17c75cee80275235f43a9931db699f73b", size = 9994, upload-time = "2025-09-09T00:38:55.672Z" }, -] - -[[package]] -name = "pyobjc" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-accessibility", marker = "platform_release >= '20.0'" }, - { name = "pyobjc-framework-accounts", marker = "platform_release >= '12.0'" }, - { name = "pyobjc-framework-addressbook" }, - { name = "pyobjc-framework-adservices", marker = "platform_release >= '20.0'" }, - { name = "pyobjc-framework-adsupport", marker = "platform_release >= '18.0'" }, - { name = "pyobjc-framework-applescriptkit" }, - { name = "pyobjc-framework-applescriptobjc", marker = "platform_release >= '10.0'" }, - { name = "pyobjc-framework-applicationservices" }, - { name = "pyobjc-framework-apptrackingtransparency", marker = "platform_release >= '20.0'" }, - { name = "pyobjc-framework-arkit", marker = "platform_release >= '25.0'" }, - { name = "pyobjc-framework-audiovideobridging", marker = "platform_release >= '12.0'" }, - { name = "pyobjc-framework-authenticationservices", marker = "platform_release >= '19.0'" }, - { name = "pyobjc-framework-automaticassessmentconfiguration", marker = "platform_release >= '19.0'" }, - { name = "pyobjc-framework-automator" }, - { name = "pyobjc-framework-avfoundation", marker = "platform_release >= '11.0'" }, - { name = "pyobjc-framework-avkit", marker = "platform_release >= '13.0'" }, - { name = "pyobjc-framework-avrouting", marker = "platform_release >= '22.0'" }, - { name = "pyobjc-framework-backgroundassets", marker = "platform_release >= '22.0'" }, - { name = "pyobjc-framework-browserenginekit", marker = "platform_release >= '23.4'" }, - { name = "pyobjc-framework-businesschat", marker = "platform_release >= '18.0'" }, - { name = "pyobjc-framework-calendarstore", marker = "platform_release >= '9.0'" }, - { name = "pyobjc-framework-callkit", marker = "platform_release >= '20.0'" }, - { name = "pyobjc-framework-carbon" }, - { name = "pyobjc-framework-cfnetwork" }, - { name = "pyobjc-framework-cinematic", marker = "platform_release >= '23.0'" }, - { name = "pyobjc-framework-classkit", marker = "platform_release >= '20.0'" }, - { name = "pyobjc-framework-cloudkit", marker = "platform_release >= '14.0'" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-collaboration", marker = "platform_release >= '9.0'" }, - { name = "pyobjc-framework-colorsync", marker = "platform_release >= '17.0'" }, - { name = "pyobjc-framework-compositorservices", marker = "platform_release >= '25.0'" }, - { name = "pyobjc-framework-contacts", marker = "platform_release >= '15.0'" }, - { name = "pyobjc-framework-contactsui", marker = "platform_release >= '15.0'" }, - { name = "pyobjc-framework-coreaudio" }, - { name = "pyobjc-framework-coreaudiokit" }, - { name = "pyobjc-framework-corebluetooth", marker = "platform_release >= '14.0'" }, - { name = "pyobjc-framework-coredata" }, - { name = "pyobjc-framework-corehaptics", marker = "platform_release >= '19.0'" }, - { name = "pyobjc-framework-corelocation", marker = "platform_release >= '10.0'" }, - { name = "pyobjc-framework-coremedia", marker = "platform_release >= '11.0'" }, - { name = "pyobjc-framework-coremediaio", marker = "platform_release >= '11.0'" }, - { name = "pyobjc-framework-coremidi" }, - { name = "pyobjc-framework-coreml", marker = "platform_release >= '17.0'" }, - { name = "pyobjc-framework-coremotion", marker = "platform_release >= '19.0'" }, - { name = "pyobjc-framework-coreservices" }, - { name = "pyobjc-framework-corespotlight", marker = "platform_release >= '17.0'" }, - { name = "pyobjc-framework-coretext" }, - { name = "pyobjc-framework-corewlan", marker = "platform_release >= '10.0'" }, - { name = "pyobjc-framework-cryptotokenkit", marker = "platform_release >= '14.0'" }, - { name = "pyobjc-framework-datadetection", marker = "platform_release >= '21.0'" }, - { name = "pyobjc-framework-devicecheck", marker = "platform_release >= '19.0'" }, - { name = "pyobjc-framework-devicediscoveryextension", marker = "platform_release >= '24.0'" }, - { name = "pyobjc-framework-dictionaryservices", marker = "platform_release >= '9.0'" }, - { name = "pyobjc-framework-discrecording" }, - { name = "pyobjc-framework-discrecordingui" }, - { name = "pyobjc-framework-diskarbitration" }, - { name = "pyobjc-framework-dvdplayback" }, - { name = "pyobjc-framework-eventkit", marker = "platform_release >= '12.0'" }, - { name = "pyobjc-framework-exceptionhandling" }, - { name = "pyobjc-framework-executionpolicy", marker = "platform_release >= '19.0'" }, - { name = "pyobjc-framework-extensionkit", marker = "platform_release >= '22.0'" }, - { name = "pyobjc-framework-externalaccessory", marker = "platform_release >= '17.0'" }, - { name = "pyobjc-framework-fileprovider", marker = "platform_release >= '19.0'" }, - { name = "pyobjc-framework-fileproviderui", marker = "platform_release >= '19.0'" }, - { name = "pyobjc-framework-findersync", marker = "platform_release >= '14.0'" }, - { name = "pyobjc-framework-fsevents", marker = "platform_release >= '9.0'" }, - { name = "pyobjc-framework-fskit", marker = "platform_release >= '24.4'" }, - { name = "pyobjc-framework-gamecenter", marker = "platform_release >= '12.0'" }, - { name = "pyobjc-framework-gamecontroller", marker = "platform_release >= '13.0'" }, - { name = "pyobjc-framework-gamekit", marker = "platform_release >= '12.0'" }, - { name = "pyobjc-framework-gameplaykit", marker = "platform_release >= '15.0'" }, - { name = "pyobjc-framework-gamesave", marker = "platform_release >= '25.0'" }, - { name = "pyobjc-framework-healthkit", marker = "platform_release >= '22.0'" }, - { name = "pyobjc-framework-imagecapturecore", marker = "platform_release >= '10.0'" }, - { name = "pyobjc-framework-inputmethodkit", marker = "platform_release >= '9.0'" }, - { name = "pyobjc-framework-installerplugins" }, - { name = "pyobjc-framework-instantmessage", marker = "platform_release >= '9.0'" }, - { name = "pyobjc-framework-intents", marker = "platform_release >= '16.0'" }, - { name = "pyobjc-framework-intentsui", marker = "platform_release >= '21.0'" }, - { name = "pyobjc-framework-iobluetooth" }, - { name = "pyobjc-framework-iobluetoothui" }, - { name = "pyobjc-framework-iosurface", marker = "platform_release >= '10.0'" }, - { name = "pyobjc-framework-ituneslibrary", marker = "platform_release >= '10.0'" }, - { name = "pyobjc-framework-kernelmanagement", marker = "platform_release >= '20.0'" }, - { name = "pyobjc-framework-latentsemanticmapping" }, - { name = "pyobjc-framework-launchservices" }, - { name = "pyobjc-framework-libdispatch", marker = "platform_release >= '12.0'" }, - { name = "pyobjc-framework-libxpc", marker = "platform_release >= '12.0'" }, - { name = "pyobjc-framework-linkpresentation", marker = "platform_release >= '19.0'" }, - { name = "pyobjc-framework-localauthentication", marker = "platform_release >= '14.0'" }, - { name = "pyobjc-framework-localauthenticationembeddedui", marker = "platform_release >= '21.0'" }, - { name = "pyobjc-framework-mailkit", marker = "platform_release >= '21.0'" }, - { name = "pyobjc-framework-mapkit", marker = "platform_release >= '13.0'" }, - { name = "pyobjc-framework-mediaaccessibility", marker = "platform_release >= '13.0'" }, - { name = "pyobjc-framework-mediaextension", marker = "platform_release >= '24.0'" }, - { name = "pyobjc-framework-medialibrary", marker = "platform_release >= '13.0'" }, - { name = "pyobjc-framework-mediaplayer", marker = "platform_release >= '16.0'" }, - { name = "pyobjc-framework-mediatoolbox", marker = "platform_release >= '13.0'" }, - { name = "pyobjc-framework-metal", marker = "platform_release >= '15.0'" }, - { name = "pyobjc-framework-metalfx", marker = "platform_release >= '22.0'" }, - { name = "pyobjc-framework-metalkit", marker = "platform_release >= '15.0'" }, - { name = "pyobjc-framework-metalperformanceshaders", marker = "platform_release >= '17.0'" }, - { name = "pyobjc-framework-metalperformanceshadersgraph", marker = "platform_release >= '20.0'" }, - { name = "pyobjc-framework-metrickit", marker = "platform_release >= '21.0'" }, - { name = "pyobjc-framework-mlcompute", marker = "platform_release >= '20.0'" }, - { name = "pyobjc-framework-modelio", marker = "platform_release >= '15.0'" }, - { name = "pyobjc-framework-multipeerconnectivity", marker = "platform_release >= '14.0'" }, - { name = "pyobjc-framework-naturallanguage", marker = "platform_release >= '18.0'" }, - { name = "pyobjc-framework-netfs", marker = "platform_release >= '10.0'" }, - { name = "pyobjc-framework-network", marker = "platform_release >= '18.0'" }, - { name = "pyobjc-framework-networkextension", marker = "platform_release >= '15.0'" }, - { name = "pyobjc-framework-notificationcenter", marker = "platform_release >= '14.0'" }, - { name = "pyobjc-framework-opendirectory", marker = "platform_release >= '10.0'" }, - { name = "pyobjc-framework-osakit" }, - { name = "pyobjc-framework-oslog", marker = "platform_release >= '19.0'" }, - { name = "pyobjc-framework-passkit", marker = "platform_release >= '20.0'" }, - { name = "pyobjc-framework-pencilkit", marker = "platform_release >= '19.0'" }, - { name = "pyobjc-framework-phase", marker = "platform_release >= '21.0'" }, - { name = "pyobjc-framework-photos", marker = "platform_release >= '15.0'" }, - { name = "pyobjc-framework-photosui", marker = "platform_release >= '15.0'" }, - { name = "pyobjc-framework-preferencepanes" }, - { name = "pyobjc-framework-pushkit", marker = "platform_release >= '19.0'" }, - { name = "pyobjc-framework-quartz" }, - { name = "pyobjc-framework-quicklookthumbnailing", marker = "platform_release >= '19.0'" }, - { name = "pyobjc-framework-replaykit", marker = "platform_release >= '20.0'" }, - { name = "pyobjc-framework-safariservices", marker = "platform_release >= '16.0'" }, - { name = "pyobjc-framework-safetykit", marker = "platform_release >= '22.0'" }, - { name = "pyobjc-framework-scenekit", marker = "platform_release >= '11.0'" }, - { name = "pyobjc-framework-screencapturekit", marker = "platform_release >= '21.4'" }, - { name = "pyobjc-framework-screensaver" }, - { name = "pyobjc-framework-screentime", marker = "platform_release >= '20.0'" }, - { name = "pyobjc-framework-scriptingbridge", marker = "platform_release >= '9.0'" }, - { name = "pyobjc-framework-searchkit" }, - { name = "pyobjc-framework-security" }, - { name = "pyobjc-framework-securityfoundation" }, - { name = "pyobjc-framework-securityinterface" }, - { name = "pyobjc-framework-securityui", marker = "platform_release >= '24.4'" }, - { name = "pyobjc-framework-sensitivecontentanalysis", marker = "platform_release >= '23.0'" }, - { name = "pyobjc-framework-servicemanagement", marker = "platform_release >= '10.0'" }, - { name = "pyobjc-framework-sharedwithyou", marker = "platform_release >= '22.0'" }, - { name = "pyobjc-framework-sharedwithyoucore", marker = "platform_release >= '22.0'" }, - { name = "pyobjc-framework-shazamkit", marker = "platform_release >= '21.0'" }, - { name = "pyobjc-framework-social", marker = "platform_release >= '12.0'" }, - { name = "pyobjc-framework-soundanalysis", marker = "platform_release >= '19.0'" }, - { name = "pyobjc-framework-speech", marker = "platform_release >= '19.0'" }, - { name = "pyobjc-framework-spritekit", marker = "platform_release >= '13.0'" }, - { name = "pyobjc-framework-storekit", marker = "platform_release >= '11.0'" }, - { name = "pyobjc-framework-symbols", marker = "platform_release >= '23.0'" }, - { name = "pyobjc-framework-syncservices" }, - { name = "pyobjc-framework-systemconfiguration" }, - { name = "pyobjc-framework-systemextensions", marker = "platform_release >= '19.0'" }, - { name = "pyobjc-framework-threadnetwork", marker = "platform_release >= '22.0'" }, - { name = "pyobjc-framework-uniformtypeidentifiers", marker = "platform_release >= '20.0'" }, - { name = "pyobjc-framework-usernotifications", marker = "platform_release >= '18.0'" }, - { name = "pyobjc-framework-usernotificationsui", marker = "platform_release >= '20.0'" }, - { name = "pyobjc-framework-videosubscriberaccount", marker = "platform_release >= '18.0'" }, - { name = "pyobjc-framework-videotoolbox", marker = "platform_release >= '12.0'" }, - { name = "pyobjc-framework-virtualization", marker = "platform_release >= '20.0'" }, - { name = "pyobjc-framework-vision", marker = "platform_release >= '17.0'" }, - { name = "pyobjc-framework-webkit" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/17/06/d77639ba166cc09aed2d32ae204811b47bc5d40e035cdc9bff7fff72ec5f/pyobjc-12.1.tar.gz", hash = "sha256:686d6db3eb3182fac9846b8ce3eedf4c7d2680b21b8b8d6e6df054a17e92a12d", size = 11345, upload-time = "2025-11-14T10:07:28.155Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/00/1085de7b73abf37ec27ad59f7a1d7a406e6e6da45720bced2e198fdf1ddf/pyobjc-12.1-py3-none-any.whl", hash = "sha256:6f8c36cf87b1159d2ca1aa387ffc3efcd51cc3da13ef47c65f45e6d9fbccc729", size = 4226, upload-time = "2025-11-14T09:30:25.185Z" }, -] - -[[package]] -name = "pyobjc-core" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b8/b6/d5612eb40be4fd5ef88c259339e6313f46ba67577a95d86c3470b951fce0/pyobjc_core-12.1.tar.gz", hash = "sha256:2bb3903f5387f72422145e1466b3ac3f7f0ef2e9960afa9bcd8961c5cbf8bd21", size = 1000532, upload-time = "2025-11-14T10:08:28.292Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/64/5a/6b15e499de73050f4a2c88fff664ae154307d25dc04da8fb38998a428358/pyobjc_core-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:818bcc6723561f207e5b5453efe9703f34bc8781d11ce9b8be286bb415eb4962", size = 678335, upload-time = "2025-11-14T09:32:20.107Z" }, -] - -[[package]] -name = "pyobjc-framework-accessibility" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-quartz" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/2d/87/8ca40428d05a668fecc638f2f47dba86054dbdc35351d247f039749de955/pyobjc_framework_accessibility-12.1.tar.gz", hash = "sha256:5ff362c3425edc242d49deec11f5f3e26e565cefb6a2872eda59ab7362149772", size = 29800, upload-time = "2025-11-14T10:08:31.949Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/95/9ea0d1c16316b4b5babf4b0515e9a133ac64269d3ec031f15ee9c7c2a8c1/pyobjc_framework_accessibility-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:537691a0b28fedb8385cd093df069a6e5d7e027629671fc47b50210404eca20b", size = 11335, upload-time = "2025-11-14T09:35:30.81Z" }, -] - -[[package]] -name = "pyobjc-framework-accounts" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/65/10/f6fe336c7624d6753c1f6edac102310ce4434d49b548c479e8e6420d4024/pyobjc_framework_accounts-12.1.tar.gz", hash = "sha256:76d62c5e7b831eb8f4c9ca6abaf79d9ed961dfffe24d89a041fb1de97fe56a3e", size = 15202, upload-time = "2025-11-14T10:08:33.995Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ac/70/5f9214250f92fbe2e07f35778875d2771d612f313af2a0e4bacba80af28e/pyobjc_framework_accounts-12.1-py2.py3-none-any.whl", hash = "sha256:e1544ad11a2f889a7aaed649188d0e76d58595a27eec07ca663847a7adb21ae5", size = 5104, upload-time = "2025-11-14T09:35:40.246Z" }, -] - -[[package]] -name = "pyobjc-framework-addressbook" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/18/28/0404af2a1c6fa8fd266df26fb6196a8f3fb500d6fe3dab94701949247bea/pyobjc_framework_addressbook-12.1.tar.gz", hash = "sha256:c48b740cf981103cef1743d0804a226d86481fcb839bd84b80e9a586187e8000", size = 44359, upload-time = "2025-11-14T10:08:37.687Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b6/33/da709c69cbb60df9522cd614d5c23c15b649b72e5d62fed1048e75c70e7b/pyobjc_framework_addressbook-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7893dd784322f4674299fb3ca40cb03385e5eddb78defd38f08c0b730813b56c", size = 12894, upload-time = "2025-11-14T09:35:47.498Z" }, -] - -[[package]] -name = "pyobjc-framework-adservices" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/19/04/1c3d3e0a1ac981664f30b33407dcdf8956046ecde6abc88832cf2aa535f4/pyobjc_framework_adservices-12.1.tar.gz", hash = "sha256:7a31fc8d5c6fd58f012db87c89ba581361fc905114bfb912e0a3a87475c02183", size = 11793, upload-time = "2025-11-14T10:08:39.56Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ad/13/f7796469b25f50750299c4b0e95dc2f75c7c7fc4c93ef2c644f947f10529/pyobjc_framework_adservices-12.1-py2.py3-none-any.whl", hash = "sha256:9ca3c55e35b2abb3149a0bce5de9a1f7e8ee4f8642036910ca8586ab2e161538", size = 3492, upload-time = "2025-11-14T09:35:57.344Z" }, -] - -[[package]] -name = "pyobjc-framework-adsupport" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/43/77/f26a2e9994d4df32e9b3680c8014e350b0f1c78d7673b3eba9de2e04816f/pyobjc_framework_adsupport-12.1.tar.gz", hash = "sha256:9a68480e76de567c339dca29a8c739d6d7b5cad30e1cd585ff6e49ec2fc283dd", size = 11645, upload-time = "2025-11-14T10:08:41.439Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cd/1a/3e90d5a09953bde7b60946cd09cca1411aed05dea855cb88cb9e944c7006/pyobjc_framework_adsupport-12.1-py2.py3-none-any.whl", hash = "sha256:97dcd8799dd61f047bb2eb788bbde81f86e95241b5e5173a3a61cfc05b5598b1", size = 3401, upload-time = "2025-11-14T09:35:59.039Z" }, -] - -[[package]] -name = "pyobjc-framework-applescriptkit" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/cd/f1/e0c07b2a9eb98f1a2050f153d287a52a92f873eeddb41b74c52c144d8767/pyobjc_framework_applescriptkit-12.1.tar.gz", hash = "sha256:cb09f88cf0ad9753dedc02720065818f854b50e33eb4194f0ea34de6d7a3eb33", size = 11451, upload-time = "2025-11-14T10:08:43.328Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/70/6c399c6ebc37a4e48acf63967e0a916878aedfe420531f6d739215184c0c/pyobjc_framework_applescriptkit-12.1-py2.py3-none-any.whl", hash = "sha256:b955fc017b524027f635d92a8a45a5fd9fbae898f3e03de16ecd94aa4c4db987", size = 4352, upload-time = "2025-11-14T09:36:00.705Z" }, -] - -[[package]] -name = "pyobjc-framework-applescriptobjc" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c0/4b/e4d1592207cbe17355e01828bdd11dd58f31356108f6a49f5e0484a5df50/pyobjc_framework_applescriptobjc-12.1.tar.gz", hash = "sha256:dce080ed07409b0dda2fee75d559bd312ea1ef0243a4338606440f282a6a0f5f", size = 11588, upload-time = "2025-11-14T10:08:45.037Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3e/5f/9ce6706399706930eb29c5308037109c30cfb36f943a6df66fdf38cc842a/pyobjc_framework_applescriptobjc-12.1-py2.py3-none-any.whl", hash = "sha256:79068f982cc22471712ce808c0a8fd5deea11258fc8d8c61968a84b1962a3d10", size = 4454, upload-time = "2025-11-14T09:36:02.276Z" }, -] - -[[package]] -name = "pyobjc-framework-applicationservices" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-coretext" }, - { name = "pyobjc-framework-quartz" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/be/6a/d4e613c8e926a5744fc47a9e9fea08384a510dc4f27d844f7ad7a2d793bd/pyobjc_framework_applicationservices-12.1.tar.gz", hash = "sha256:c06abb74f119bc27aeb41bf1aef8102c0ae1288aec1ac8665ea186a067a8945b", size = 103247, upload-time = "2025-11-14T10:08:52.18Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/37/a7/55fa88def5c02732c4b747606ff1cbce6e1f890734bbd00f5596b21eaa02/pyobjc_framework_applicationservices-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c8f6e2fb3b3e9214ab4864ef04eee18f592b46a986c86ea0113448b310520532", size = 32835, upload-time = "2025-11-14T09:36:11.855Z" }, -] - -[[package]] -name = "pyobjc-framework-apptrackingtransparency" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0d/de/f24348982ecab0cb13067c348fc5fbc882c60d704ca290bada9a2b3e594b/pyobjc_framework_apptrackingtransparency-12.1.tar.gz", hash = "sha256:e25bf4e4dfa2d929993ee8e852b28fdf332fa6cde0a33328fdc3b2f502fa50ec", size = 12407, upload-time = "2025-11-14T10:08:54.118Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/19/b2/90120b93ecfb099b6af21696c26356ad0f2182bdef72b6cba28aa6472ca6/pyobjc_framework_apptrackingtransparency-12.1-py2.py3-none-any.whl", hash = "sha256:23a98ade55495f2f992ecf62c3cbd8f648cbd68ba5539c3f795bf66de82e37ca", size = 3879, upload-time = "2025-11-14T09:36:26.425Z" }, -] - -[[package]] -name = "pyobjc-framework-arkit" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c9/8b/843fe08e696bca8e7fc129344965ab6280f8336f64f01ba0a8862d219c3f/pyobjc_framework_arkit-12.1.tar.gz", hash = "sha256:0c5c6b702926179700b68ba29b8247464c3b609fd002a07a3308e72cfa953adf", size = 35814, upload-time = "2025-11-14T10:08:57.55Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/21/1e/64c55b409243b3eb9abc7a99e7b27ad4e16b9e74bc4b507fb7e7b81fd41a/pyobjc_framework_arkit-12.1-py2.py3-none-any.whl", hash = "sha256:f6d39e28d858ee03f052d6780a552247e682204382dbc090f1d3192fa1b21493", size = 8302, upload-time = "2025-11-14T09:36:28.127Z" }, -] - -[[package]] -name = "pyobjc-framework-audiovideobridging" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9f/51/f81581e7a3c5cb6c9254c6f1e1ee1d614930493761dec491b5b0d49544b9/pyobjc_framework_audiovideobridging-12.1.tar.gz", hash = "sha256:6230ace6bec1f38e8a727c35d054a7be54e039b3053f98e6dd8d08d6baee2625", size = 38457, upload-time = "2025-11-14T10:09:01.122Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/8e/a28badfcc6c731696e3d3a8a83927bd844d992f9152f903c2fee355702ca/pyobjc_framework_audiovideobridging-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:010021502649e2cca4e999a7c09358d48c6b0ed83530bbc0b85bba6834340e4b", size = 11052, upload-time = "2025-11-14T09:36:34.475Z" }, -] - -[[package]] -name = "pyobjc-framework-authenticationservices" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6c/18/86218de3bf67fc1d810065f353d9df70c740de567ebee8550d476cb23862/pyobjc_framework_authenticationservices-12.1.tar.gz", hash = "sha256:cef71faeae2559f5c0ff9a81c9ceea1c81108e2f4ec7de52a98c269feff7a4b6", size = 58683, upload-time = "2025-11-14T10:09:06.003Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f1/1d/e9f296fe1ee9a074ff6c45ce9eb109fc3b45696de000f373265c8e42fd47/pyobjc_framework_authenticationservices-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:6fd5ce10fe5359cbbfe03eb12cab3e01992b32ab65653c579b00ac93cf674985", size = 20738, upload-time = "2025-11-14T09:36:51.094Z" }, -] - -[[package]] -name = "pyobjc-framework-automaticassessmentconfiguration" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e4/24/080afe8189c47c4bb3daa191ccfd962400ca31a67c14b0f7c2d002c2e249/pyobjc_framework_automaticassessmentconfiguration-12.1.tar.gz", hash = "sha256:2b732c02d9097682ca16e48f5d3b10056b740bc091e217ee4d5715194c8970b1", size = 21895, upload-time = "2025-11-14T10:09:08.779Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f2/b2/fbec3d649bf275d7a9604e5f56015be02ef8dcf002f4ae4d760436b8e222/pyobjc_framework_automaticassessmentconfiguration-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c2e22ea67d7e6d6a84d968169f83d92b59857a49ab12132de07345adbfea8a62", size = 9332, upload-time = "2025-11-14T09:37:07.083Z" }, -] - -[[package]] -name = "pyobjc-framework-automator" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/7e/08/362bf6ac2bba393c46cf56078d4578b692b56857c385e47690637a72f0dd/pyobjc_framework_automator-12.1.tar.gz", hash = "sha256:7491a99347bb30da3a3f744052a03434ee29bee3e2ae520576f7e796740e4ba7", size = 186068, upload-time = "2025-11-14T10:09:20.82Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e3/36/2e8c36ddf20d501f9d344ed694e39021190faffc44b596f3a430bf437174/pyobjc_framework_automator-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4df9aec77f0fbca66cd3534d1b8398fe6f3e3c2748c0fc12fec2546c7f2e3ffd", size = 10034, upload-time = "2025-11-14T09:37:20.293Z" }, -] - -[[package]] -name = "pyobjc-framework-avfoundation" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-coreaudio" }, - { name = "pyobjc-framework-coremedia" }, - { name = "pyobjc-framework-quartz" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/cd/42/c026ab308edc2ed5582d8b4b93da6b15d1b6557c0086914a4aabedd1f032/pyobjc_framework_avfoundation-12.1.tar.gz", hash = "sha256:eda0bb60be380f9ba2344600c4231dd58a3efafa99fdc65d3673ecfbb83f6fcb", size = 310047, upload-time = "2025-11-14T10:09:40.069Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a6/00/ca471e5dd33f040f69320832e45415d00440260bf7f8221a9df4c4662659/pyobjc_framework_avfoundation-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bf634f89265b4d93126153200d885b6de4859ed6b3bc65e69ff75540bc398406", size = 83375, upload-time = "2025-11-14T09:37:47.262Z" }, -] - -[[package]] -name = "pyobjc-framework-avkit" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-quartz" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/34/a9/e44db1a1f26e2882c140f1d502d508b1f240af9048909dcf1e1a687375b4/pyobjc_framework_avkit-12.1.tar.gz", hash = "sha256:a5c0ddb0cb700f9b09c8afeca2c58952d554139e9bb078236d2355b1fddfb588", size = 28473, upload-time = "2025-11-14T10:09:43.105Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/75/34/e77b18f7ed0bd707afd388702e910bdf2d0acee39d1139e8619c916d3eb4/pyobjc_framework_avkit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eef2c0a51465de025a4509db05ef18ca2b678bb00ee0a8fbad7fd470edfd58f9", size = 11613, upload-time = "2025-11-14T09:38:19.78Z" }, -] - -[[package]] -name = "pyobjc-framework-avrouting" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6e/83/15bf6c28ec100dae7f92d37c9e117b3b4ee6b4873db062833e16f1cfd6c4/pyobjc_framework_avrouting-12.1.tar.gz", hash = "sha256:6a6c5e583d14f6501df530a9d0559a32269a821fc8140e3646015f097155cd1c", size = 20031, upload-time = "2025-11-14T10:09:45.701Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/68/54/fa24f666525c1332a11b2de959c9877b0fe08f00f29ecf96964b24246c13/pyobjc_framework_avrouting-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4c0fb0d3d260527320377a70c87688ca5e4a208b09fddcae2b4257d7fe9b1e18", size = 8450, upload-time = "2025-11-14T09:38:34.941Z" }, -] - -[[package]] -name = "pyobjc-framework-backgroundassets" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/34/d1/e917fba82790495152fd3508c5053827658881cf7e9887ba60def5e3f221/pyobjc_framework_backgroundassets-12.1.tar.gz", hash = "sha256:8da34df9ae4519c360c429415477fdaf3fbba5addbc647b3340b8783454eb419", size = 26210, upload-time = "2025-11-14T10:09:48.792Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/de/34/bbba61f0e8ecb0fe0da7aa2c9ea15f7cb0dca2fb2914fcdcd77b782b5c11/pyobjc_framework_backgroundassets-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2c11cb98650c1a4bc68eeb4b040541ba96613434c5957e98e9bb363413b23c91", size = 10786, upload-time = "2025-11-14T09:38:48.341Z" }, -] - -[[package]] -name = "pyobjc-framework-browserenginekit" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-coreaudio" }, - { name = "pyobjc-framework-coremedia" }, - { name = "pyobjc-framework-quartz" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5d/b9/39f9de1730e6f8e73be0e4f0c6087cd9439cbe11645b8052d22e1fb8e69b/pyobjc_framework_browserenginekit-12.1.tar.gz", hash = "sha256:6a1a34a155778ab55ab5f463e885f2a3b4680231264e1fe078e62ddeccce49ed", size = 29120, upload-time = "2025-11-14T10:09:51.582Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/46/e0/8d2cebbfcfd6aacb805ae0ae7ba931f6a39140540b2e1e96719e7be28359/pyobjc_framework_browserenginekit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d15766bb841b081447015c9626e2a766febfe651f487893d29c5d72bef976b94", size = 11545, upload-time = "2025-11-14T09:39:00.988Z" }, -] - -[[package]] -name = "pyobjc-framework-businesschat" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/4d/da/bc09b6ed19e9ea38ecca9387c291ca11fa680a8132d82b27030f82551c23/pyobjc_framework_businesschat-12.1.tar.gz", hash = "sha256:f6fa3a8369a1a51363e1757530128741d9d09ed90692a1d6777a4c0fbad25868", size = 12055, upload-time = "2025-11-14T10:09:53.436Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/53/88/4c727424b05efa33ed7f6c45e40333e5a8a8dc5bb238e34695addd68463b/pyobjc_framework_businesschat-12.1-py2.py3-none-any.whl", hash = "sha256:f66ce741507b324de3c301d72ba0cfa6aaf7093d7235972332807645c118cc29", size = 3474, upload-time = "2025-11-14T09:39:10.771Z" }, -] - -[[package]] -name = "pyobjc-framework-calendarstore" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/88/41/ae955d1c44dcc18b5b9df45c679e9a08311a0f853b9d981bca760cf1eef2/pyobjc_framework_calendarstore-12.1.tar.gz", hash = "sha256:f9a798d560a3c99ad4c0d2af68767bc5695d8b1aabef04d8377861cd1d6d1670", size = 52272, upload-time = "2025-11-14T10:09:58.48Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fa/70/f68aebdb7d3fa2dec2e9da9e9cdaa76d370de326a495917dbcde7bb7711e/pyobjc_framework_calendarstore-12.1-py2.py3-none-any.whl", hash = "sha256:18533e0fcbcdd29ee5884dfbd30606710f65df9b688bf47daee1438ee22e50cc", size = 5285, upload-time = "2025-11-14T09:39:12.473Z" }, -] - -[[package]] -name = "pyobjc-framework-callkit" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/1a/c0/1859d4532d39254df085309aff55b85323576f00a883626325af40da4653/pyobjc_framework_callkit-12.1.tar.gz", hash = "sha256:fd6dc9688b785aab360139d683be56f0844bf68bf5e45d0eb770cb68221083cc", size = 29171, upload-time = "2025-11-14T10:10:01.336Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/b7/b3a498b14751b4be6af5272c9be9ded718aa850ebf769b052c7d610a142a/pyobjc_framework_callkit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:12adc0ace464a057f8908187698e1d417c6c53619797a69d096f4329bffb1089", size = 11334, upload-time = "2025-11-14T09:39:18.622Z" }, -] - -[[package]] -name = "pyobjc-framework-carbon" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0c/0f/9ab8e518a4e5ac4a1e2fdde38a054c32aef82787ff7f30927345c18b7765/pyobjc_framework_carbon-12.1.tar.gz", hash = "sha256:57a72807db252d5746caccc46da4bd20ff8ea9e82109af9f72735579645ff4f0", size = 37293, upload-time = "2025-11-14T10:10:04.464Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/9e/91853c8f98b9d5bccf464113908620c94cc12c2a3e4625f3ce172e3ea4bc/pyobjc_framework_carbon-12.1-py2.py3-none-any.whl", hash = "sha256:f8b719b3c7c5cf1d61ac7c45a8a70b5e5e5a83fa02f5194c2a48a7e81a3d1b7f", size = 4625, upload-time = "2025-11-14T09:39:27.937Z" }, -] - -[[package]] -name = "pyobjc-framework-cfnetwork" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d2/6a/f5f0f191956e187db85312cbffcc41bf863670d121b9190b4a35f0d36403/pyobjc_framework_cfnetwork-12.1.tar.gz", hash = "sha256:2d16e820f2d43522c793f55833fda89888139d7a84ca5758548ba1f3a325a88d", size = 44383, upload-time = "2025-11-14T10:10:08.428Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/0b/28034e63f3a25b30ede814469c3f57d44268cbced19664c84a8664200f9d/pyobjc_framework_cfnetwork-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:92760da248c757085fc39bce4388a0f6f0b67540e51edf60a92ad60ca907d071", size = 19135, upload-time = "2025-11-14T09:39:36.382Z" }, -] - -[[package]] -name = "pyobjc-framework-cinematic" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-avfoundation" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-coremedia" }, - { name = "pyobjc-framework-metal" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/67/4e/f4cc7f9f7f66df0290c90fe445f1ff5aa514c6634f5203fe049161053716/pyobjc_framework_cinematic-12.1.tar.gz", hash = "sha256:795068c30447548c0e8614e9c432d4b288b13d5614622ef2f9e3246132329b06", size = 21215, upload-time = "2025-11-14T10:10:10.795Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c9/a0/cd85c827ce5535c08d936e5723c16ee49f7ff633f2e9881f4f58bf83e4ce/pyobjc_framework_cinematic-12.1-py2.py3-none-any.whl", hash = "sha256:c003543bb6908379680a93dfd77a44228686b86c118cf3bc930f60241d0cd141", size = 5031, upload-time = "2025-11-14T09:39:49.003Z" }, -] - -[[package]] -name = "pyobjc-framework-classkit" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ac/ef/67815278023b344a79c7e95f748f647245d6f5305136fc80615254ad447c/pyobjc_framework_classkit-12.1.tar.gz", hash = "sha256:8d1e9dd75c3d14938ff533d88b72bca2d34918e4461f418ea323bfb2498473b4", size = 26298, upload-time = "2025-11-14T10:10:13.406Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/87/5e/cf43c647af872499fc8e80cc6ac6e9ad77d9c77861dc2e62bdd9b01473ce/pyobjc_framework_classkit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c027a3cd9be5fee3f605589118b8b278297c384a271f224c1a98b224e0c087e6", size = 8877, upload-time = "2025-11-14T09:39:54.979Z" }, -] - -[[package]] -name = "pyobjc-framework-cloudkit" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-accounts" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-coredata" }, - { name = "pyobjc-framework-corelocation" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/2d/09/762ee4f3ae8568b8e0e5392c705bc4aa1929aa454646c124ca470f1bf9fc/pyobjc_framework_cloudkit-12.1.tar.gz", hash = "sha256:1dddd38e60863f88adb3d1d37d3b4ccb9cbff48c4ef02ab50e36fa40c2379d2f", size = 53730, upload-time = "2025-11-14T10:10:17.831Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/35/71/cbef7179bf1a594558ea27f1e5ad18f5c17ef71a8a24192aae16127bc849/pyobjc_framework_cloudkit-12.1-py2.py3-none-any.whl", hash = "sha256:875e37bf1a2ce3d05c2492692650104f2d908b56b71a0aedf6620bc517c6c9ca", size = 11090, upload-time = "2025-11-14T09:40:04.207Z" }, -] - -[[package]] -name = "pyobjc-framework-cocoa" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/02/a3/16ca9a15e77c061a9250afbae2eae26f2e1579eb8ca9462ae2d2c71e1169/pyobjc_framework_cocoa-12.1.tar.gz", hash = "sha256:5556c87db95711b985d5efdaaf01c917ddd41d148b1e52a0c66b1a2e2c5c1640", size = 2772191, upload-time = "2025-11-14T10:13:02.069Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/95/bf/ee4f27ec3920d5c6fc63c63e797c5b2cc4e20fe439217085d01ea5b63856/pyobjc_framework_cocoa-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:547c182837214b7ec4796dac5aee3aa25abc665757b75d7f44f83c994bcb0858", size = 384590, upload-time = "2025-11-14T09:41:17.336Z" }, -] - -[[package]] -name = "pyobjc-framework-collaboration" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/64/21/77fe64b39eae98412de1a0d33e9c735aa9949d53fff6b2d81403572b410b/pyobjc_framework_collaboration-12.1.tar.gz", hash = "sha256:2afa264d3233fc0a03a56789c6fefe655ffd81a2da4ba1dc79ea0c45931ad47b", size = 14299, upload-time = "2025-11-14T10:13:04.631Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/66/1507de01f1e2b309f8e11553a52769e4e2e9939ed770b5b560ef5bc27bc1/pyobjc_framework_collaboration-12.1-py2.py3-none-any.whl", hash = "sha256:182d6e6080833b97f9bef61738ae7bacb509714538f0d7281e5f0814c804b315", size = 4907, upload-time = "2025-11-14T09:42:55.781Z" }, -] - -[[package]] -name = "pyobjc-framework-colorsync" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c0/b4/706e4cc9db25b400201fc90f3edfaa1ab2d51b400b19437b043a68532078/pyobjc_framework_colorsync-12.1.tar.gz", hash = "sha256:d69dab7df01245a8c1bd536b9231c97993a5d1a2765d77692ce40ebbe6c1b8e9", size = 25269, upload-time = "2025-11-14T10:13:07.522Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/e1/82e45c712f43905ee1e6d585180764e8fa6b6f1377feb872f9f03c8c1fb8/pyobjc_framework_colorsync-12.1-py2.py3-none-any.whl", hash = "sha256:41e08d5b9a7af4b380c9adab24c7ff59dfd607b3073ae466693a3e791d8ffdc9", size = 6020, upload-time = "2025-11-14T09:42:57.504Z" }, -] - -[[package]] -name = "pyobjc-framework-compositorservices" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-metal" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/54/c5/0ba31d7af7e464b7f7ece8c2bd09112bdb0b7260848402e79ba6aacc622c/pyobjc_framework_compositorservices-12.1.tar.gz", hash = "sha256:028e357bbee7fbd3723339a321bbe14e6da5a772708a661a13eea5f17c89e4ab", size = 23292, upload-time = "2025-11-14T10:13:10.392Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/34/5a2de8d531dbb88023898e0b5d2ce8edee14751af6c70e6103f6aa31a669/pyobjc_framework_compositorservices-12.1-py2.py3-none-any.whl", hash = "sha256:9ef22d4eacd492e13099b9b8936db892cdbbef1e3d23c3484e0ed749f83c4984", size = 5910, upload-time = "2025-11-14T09:42:59.154Z" }, -] - -[[package]] -name = "pyobjc-framework-contacts" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/4b/a0/ce0542d211d4ea02f5cbcf72ee0a16b66b0d477a4ba5c32e00117703f2f0/pyobjc_framework_contacts-12.1.tar.gz", hash = "sha256:89bca3c5cf31404b714abaa1673577e1aaad6f2ef49d4141c6dbcc0643a789ad", size = 42378, upload-time = "2025-11-14T10:13:14.203Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/32/c8/2c4638c0d06447886a34070eebb9ba57407d4dd5f0fcb7ab642568272b88/pyobjc_framework_contacts-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2e5ce33b686eb9c0a39351938a756442ea8dea88f6ae2f16bff5494a8569c687", size = 12165, upload-time = "2025-11-14T09:43:05.119Z" }, -] - -[[package]] -name = "pyobjc-framework-contactsui" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-contacts" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/cd/0c/7bb7f898456a81d88d06a1084a42e374519d2e40a668a872b69b11f8c1f9/pyobjc_framework_contactsui-12.1.tar.gz", hash = "sha256:aaeca7c9e0c9c4e224d73636f9a558f9368c2c7422155a41fd4d7a13613a77c1", size = 18769, upload-time = "2025-11-14T10:13:16.301Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/ab/319aa52dfe6f836f4dc542282c2c13996222d4f5c9ea7ff8f391b12dac83/pyobjc_framework_contactsui-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:057f40d2f6eb1b169a300675ec75cc7a747cddcbcee8ece133e652a7086c5ab5", size = 7888, upload-time = "2025-11-14T09:43:18.502Z" }, -] - -[[package]] -name = "pyobjc-framework-coreaudio" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/84/d1/0b884c5564ab952ff5daa949128c64815300556019c1bba0cf2ca752a1a0/pyobjc_framework_coreaudio-12.1.tar.gz", hash = "sha256:a9e72925fcc1795430496ce0bffd4ddaa92c22460a10308a7283ade830089fe1", size = 75077, upload-time = "2025-11-14T10:13:22.345Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a9/48/05b5192122e23140cf583eac99ccc5bf615591d6ff76483ba986c38ee750/pyobjc_framework_coreaudio-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a5ad6309779663f846ab36fe6c49647e470b7e08473c3e48b4f004017bdb68a4", size = 36908, upload-time = "2025-11-14T09:43:36.108Z" }, -] - -[[package]] -name = "pyobjc-framework-coreaudiokit" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-coreaudio" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/41/1c/5c7e39b9361d4eec99b9115b593edd9825388acd594cb3b4519f8f1ac12c/pyobjc_framework_coreaudiokit-12.1.tar.gz", hash = "sha256:b83624f8de3068ab2ca279f786be0804da5cf904ff9979d96007b69ef4869e1e", size = 20137, upload-time = "2025-11-14T10:13:24.611Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/19/d7/f171c04c6496afeaad2ab658b0c810682c8407127edc94d4b3f3b90c2bb1/pyobjc_framework_coreaudiokit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:97d5dd857e73d5b597cfc980972b021314b760e2f5bdde7bbba0334fbf404722", size = 7273, upload-time = "2025-11-14T09:43:55.411Z" }, -] - -[[package]] -name = "pyobjc-framework-corebluetooth" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/4b/25/d21d6cb3fd249c2c2aa96ee54279f40876a0c93e7161b3304bf21cbd0bfe/pyobjc_framework_corebluetooth-12.1.tar.gz", hash = "sha256:8060c1466d90bbb9100741a1091bb79975d9ba43911c9841599879fc45c2bbe0", size = 33157, upload-time = "2025-11-14T10:13:28.064Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/56/01fef62a479cdd6ff9ee40b6e062a205408ff386ce5ba56d7e14a71fcf73/pyobjc_framework_corebluetooth-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe72c9732ee6c5c793b9543f08c1f5bdd98cd95dfc9d96efd5708ec9d6eeb213", size = 13209, upload-time = "2025-11-14T09:44:08.203Z" }, -] - -[[package]] -name = "pyobjc-framework-coredata" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3e/c5/8cd46cd4f1b7cf88bdeed3848f830ea9cdcc4e55cd0287a968a2838033fb/pyobjc_framework_coredata-12.1.tar.gz", hash = "sha256:1e47d3c5e51fdc87a90da62b97cae1bc49931a2bb064db1305827028e1fc0ffa", size = 124348, upload-time = "2025-11-14T10:13:36.435Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/29/fe24dc81e0f154805534923a56fe572c3b296092f086cf5a239fccc2d46a/pyobjc_framework_coredata-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a3ee3581ca23ead0b152257e98622fe0bf7e7948f30a62a25a17cafe28fe015e", size = 16409, upload-time = "2025-11-14T09:44:23.582Z" }, -] - -[[package]] -name = "pyobjc-framework-corehaptics" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3a/2f/74a3da79d9188b05dd4be4428a819ea6992d4dfaedf7d629027cf1f57bfc/pyobjc_framework_corehaptics-12.1.tar.gz", hash = "sha256:521dd2986c8a4266d583dd9ed9ae42053b11ae7d3aa89bf53fbee88307d8db10", size = 22164, upload-time = "2025-11-14T10:13:38.941Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/25/f4/f469d6a9cac7c195f3d08fa65f94c32dd1dcf97a54b481be648fb3a7a5f3/pyobjc_framework_corehaptics-12.1-py2.py3-none-any.whl", hash = "sha256:a3b07d36ddf5c86a9cdaa411ab53d09553d26ea04fc7d4f82d21a84f0fc05fc0", size = 5382, upload-time = "2025-11-14T09:44:34.725Z" }, -] - -[[package]] -name = "pyobjc-framework-corelocation" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/cc/79/b75885e0d75397dc2fe1ed9ca80be2b64c18b817f5fb924277cb1bf7b163/pyobjc_framework_corelocation-12.1.tar.gz", hash = "sha256:3674e9353f949d91dde6230ad68f6d5748a7f0424751e08a2c09d06050d66231", size = 53511, upload-time = "2025-11-14T10:13:43.384Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/71/57/1b670890fbf650f1a00afe5ee897ea3856a4a1417c2304c633ee2e978ed0/pyobjc_framework_corelocation-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8c35ad29a062fea7d417fd8997a9309660ba7963f2847c004e670efbe6bb5b00", size = 12721, upload-time = "2025-11-14T09:44:41.185Z" }, -] - -[[package]] -name = "pyobjc-framework-coremedia" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/da/7d/5ad600ff7aedfef8ba8f51b11d9aaacdf247b870bd14045d6e6f232e3df9/pyobjc_framework_coremedia-12.1.tar.gz", hash = "sha256:166c66a9c01e7a70103f3ca44c571431d124b9070612ef63a1511a4e6d9d84a7", size = 89566, upload-time = "2025-11-14T10:13:49.788Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/ae/f773cdc33c34a3f9ce6db829dbf72661b65c28ea9efaec8940364185b977/pyobjc_framework_coremedia-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:161a627f5c8cd30a5ebb935189f740e21e6cd94871a9afd463efdb5d51e255fa", size = 29396, upload-time = "2025-11-14T09:44:57.563Z" }, -] - -[[package]] -name = "pyobjc-framework-coremediaio" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/08/8e/23baee53ccd6c011c965cff62eb55638b4088c3df27d2bf05004105d6190/pyobjc_framework_coremediaio-12.1.tar.gz", hash = "sha256:880b313b28f00b27775d630174d09e0b53d1cdbadb74216618c9dd5b3eb6806a", size = 51100, upload-time = "2025-11-14T10:13:54.277Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/0c/9425c53c9a8c26e468e065ba12ef076bab20197ff7c82052a6dddd46d42b/pyobjc_framework_coremediaio-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1108f8a278928fbca465f95123ea4a56456bd6571c1dc8b91793e6c61d624517", size = 17277, upload-time = "2025-11-14T09:45:17.457Z" }, -] - -[[package]] -name = "pyobjc-framework-coremidi" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/75/96/2d583060a71a73c8a7e6d92f2a02675621b63c1f489f2639e020fae34792/pyobjc_framework_coremidi-12.1.tar.gz", hash = "sha256:3c6f1fd03997c3b0f20ab8545126b1ce5f0cddcc1587dffacad876c161da8c54", size = 55587, upload-time = "2025-11-14T10:13:58.903Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e3/2d/99520f6f1685e4cad816e55cbf6d85f8ce6ea908107950e2d37dc17219d8/pyobjc_framework_coremidi-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e84ffc1de59691c04201b0872e184fe55b5589f3a14876bd14460f3b5f3cd109", size = 24317, upload-time = "2025-11-14T09:45:34.92Z" }, -] - -[[package]] -name = "pyobjc-framework-coreml" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/30/2d/baa9ea02cbb1c200683cb7273b69b4bee5070e86f2060b77e6a27c2a9d7e/pyobjc_framework_coreml-12.1.tar.gz", hash = "sha256:0d1a4216891a18775c9e0170d908714c18e4f53f9dc79fb0f5263b2aa81609ba", size = 40465, upload-time = "2025-11-14T10:14:02.265Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bb/39/4defef0deb25c5d7e3b7826d301e71ac5b54ef901b7dac4db1adc00f172d/pyobjc_framework_coreml-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:10dc8e8db53d7631ebc712cad146e3a9a9a443f4e1a037e844149a24c3c42669", size = 11356, upload-time = "2025-11-14T09:45:52.271Z" }, -] - -[[package]] -name = "pyobjc-framework-coremotion" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/2c/eb/abef7d405670cf9c844befc2330a46ee59f6ff7bac6f199bf249561a2ca6/pyobjc_framework_coremotion-12.1.tar.gz", hash = "sha256:8e1b094d34084cc8cf07bedc0630b4ee7f32b0215011f79c9e3cd09d205a27c7", size = 33851, upload-time = "2025-11-14T10:14:05.619Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/75/89fa4aab818aeca21ac0a60b7ceb89a9e685df0ddd3828d36a6f84a0cff0/pyobjc_framework_coremotion-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a77908ab83c422030f913a2a761d196359ab47f6d1e7c76f21de2c6c05ea2f5f", size = 10406, upload-time = "2025-11-14T09:46:05.076Z" }, -] - -[[package]] -name = "pyobjc-framework-coreservices" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-fsevents" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/4c/b3/52338a3ff41713f7d7bccaf63bef4ba4a8f2ce0c7eaff39a3629d022a79a/pyobjc_framework_coreservices-12.1.tar.gz", hash = "sha256:fc6a9f18fc6da64c166fe95f2defeb7ac8a9836b3b03bb6a891d36035260dbaa", size = 366150, upload-time = "2025-11-14T10:14:28.133Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/61/6c/33984caaf497fc5a6f86350d7ca4fac8abeb2bc33203edc96955a21e8c05/pyobjc_framework_coreservices-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8751dc2edcb7cfa248bf8a274c4d6493e8d53ef28a843827a4fc9a0a8b04b8be", size = 30206, upload-time = "2025-11-14T09:46:22.732Z" }, -] - -[[package]] -name = "pyobjc-framework-corespotlight" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/99/d0/88ca73b0cf23847af463334989dd8f98e44f801b811e7e1d8a5627ec20b4/pyobjc_framework_corespotlight-12.1.tar.gz", hash = "sha256:57add47380cd0bbb9793f50a4a4b435a90d4ebd2a33698e058cb353ddfb0d068", size = 38002, upload-time = "2025-11-14T10:14:31.948Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f6/3b/d3031eddff8029859de6d92b1f741625b1c233748889141a6a5a89b96f0e/pyobjc_framework_corespotlight-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bfcea64ab3250e2886d202b8731be3817b5ac0c8c9f43e77d0d5a0b6602e71a7", size = 9996, upload-time = "2025-11-14T09:46:47.157Z" }, -] - -[[package]] -name = "pyobjc-framework-coretext" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-quartz" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/29/da/682c9c92a39f713bd3c56e7375fa8f1b10ad558ecb075258ab6f1cdd4a6d/pyobjc_framework_coretext-12.1.tar.gz", hash = "sha256:e0adb717738fae395dc645c9e8a10bb5f6a4277e73cba8fa2a57f3b518e71da5", size = 90124, upload-time = "2025-11-14T10:14:38.596Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cd/0f/ddf45bf0e3ba4fbdc7772de4728fd97ffc34a0b5a15e1ab1115b202fe4ae/pyobjc_framework_coretext-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d246fa654bdbf43bae3969887d58f0b336c29b795ad55a54eb76397d0e62b93c", size = 30108, upload-time = "2025-11-14T09:47:04.228Z" }, -] - -[[package]] -name = "pyobjc-framework-corewlan" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/88/71/739a5d023566b506b3fd3d2412983faa95a8c16226c0dcd0f67a9294a342/pyobjc_framework_corewlan-12.1.tar.gz", hash = "sha256:a9d82ec71ef61f37e1d611caf51a4203f3dbd8caf827e98128a1afaa0fd2feb5", size = 32417, upload-time = "2025-11-14T10:14:41.921Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4e/31/3e9cf2c0ac3c979062958eae7a275b602515c9c76fd30680e1ee0fea82ae/pyobjc_framework_corewlan-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:5cba04c0550fc777767cd3a5471e4ed837406ab182d7d5c273bc5ce6ea237bfe", size = 9958, upload-time = "2025-11-14T09:47:22.474Z" }, -] - -[[package]] -name = "pyobjc-framework-cryptotokenkit" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6b/7c/d03ff4f74054578577296f33bc669fce16c7827eb1a553bb372b5aab30ca/pyobjc_framework_cryptotokenkit-12.1.tar.gz", hash = "sha256:c95116b4b7a41bf5b54aff823a4ef6f4d9da4d0441996d6d2c115026a42d82f5", size = 32716, upload-time = "2025-11-14T10:14:45.024Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0f/c7/aecba253cf21303b2c9f3ce03fc0e987523609d7839ea8e0a688ae816c96/pyobjc_framework_cryptotokenkit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ef51a86c1d0125fabdfad0b3efa51098fb03660d8dad2787d82e8b71c9f189de", size = 12633, upload-time = "2025-11-14T09:47:35.707Z" }, -] - -[[package]] -name = "pyobjc-framework-datadetection" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/db/97/9b03832695ec4d3008e6150ddfdc581b0fda559d9709a98b62815581259a/pyobjc_framework_datadetection-12.1.tar.gz", hash = "sha256:95539e46d3bc970ce890aa4a97515db10b2690597c5dd362996794572e5d5de0", size = 12323, upload-time = "2025-11-14T10:14:46.769Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/70/1c/5d2f941501e84da8fef8ef3fd378b5c083f063f083f97dd3e8a07f0404b3/pyobjc_framework_datadetection-12.1-py2.py3-none-any.whl", hash = "sha256:4dc8e1d386d655b44b2681a4a2341fb2fc9addbf3dda14cb1553cd22be6a5387", size = 3497, upload-time = "2025-11-14T09:47:45.826Z" }, -] - -[[package]] -name = "pyobjc-framework-devicecheck" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/cd/af/c676107c40d51f55d0a42043865d7246db821d01241b518ea1d3b3ef1394/pyobjc_framework_devicecheck-12.1.tar.gz", hash = "sha256:567e85fc1f567b3fe64ac1cdc323d989509331f64ee54fbcbde2001aec5adbdb", size = 12885, upload-time = "2025-11-14T10:14:48.804Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c5/d8/1f1b13fa4775b6474c9ad0f4b823953eaeb6c11bd6f03fa8479429b36577/pyobjc_framework_devicecheck-12.1-py2.py3-none-any.whl", hash = "sha256:ffd58148bdef4a1ee8548b243861b7d97a686e73808ca0efac5bef3c430e4a15", size = 3684, upload-time = "2025-11-14T09:47:47.25Z" }, -] - -[[package]] -name = "pyobjc-framework-devicediscoveryextension" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/91/b0/e6e2ed6a7f4b689746818000a003ff7ab9c10945df66398ae8d323ae9579/pyobjc_framework_devicediscoveryextension-12.1.tar.gz", hash = "sha256:60e12445fad97ff1f83472255c943685a8f3a9d95b3126d887cfe769b7261044", size = 14718, upload-time = "2025-11-14T10:14:50.723Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/0c/005fe8db1e19135f493a3de8c8d38031e1ad2d626de4ef89f282acf4aff7/pyobjc_framework_devicediscoveryextension-12.1-py2.py3-none-any.whl", hash = "sha256:d6d6b606d27d4d88efc0bed4727c375e749149b360290c3ad2afc52337739a1b", size = 4321, upload-time = "2025-11-14T09:47:48.78Z" }, -] - -[[package]] -name = "pyobjc-framework-dictionaryservices" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-coreservices" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/7a/c0/daf03cdaf6d4e04e0cf164db358378c07facd21e4e3f8622505d72573e2c/pyobjc_framework_dictionaryservices-12.1.tar.gz", hash = "sha256:354158f3c55d66681fa903c7b3cb05a435b717fa78d0cef44d258d61156454a7", size = 10573, upload-time = "2025-11-14T10:14:53.961Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/13/ab308e934146cfd54691ddad87e572cd1edb6659d795903c4c75904e2d7d/pyobjc_framework_dictionaryservices-12.1-py2.py3-none-any.whl", hash = "sha256:578854eec17fa473ac17ab30050a7bbb2ab69f17c5c49b673695254c3e88ad4b", size = 3930, upload-time = "2025-11-14T09:47:50.782Z" }, -] - -[[package]] -name = "pyobjc-framework-discrecording" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c4/87/8bd4544793bfcdf507174abddd02b1f077b48fab0004b3db9a63142ce7e9/pyobjc_framework_discrecording-12.1.tar.gz", hash = "sha256:6defc8ea97fb33b4d43870c673710c04c3dc48be30cdf78ba28191a922094990", size = 55607, upload-time = "2025-11-14T10:14:58.276Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/70/14a5aa348a5eba16e8773bb56698575cf114aa55aa303037b7000fc53959/pyobjc_framework_discrecording-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:865f1551e58459da6073360afc8f2cc452472c676ba83dcaa9b0c44e7775e4b5", size = 14566, upload-time = "2025-11-14T09:47:57.503Z" }, -] - -[[package]] -name = "pyobjc-framework-discrecordingui" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-discrecording" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/30/63/8667f5bb1ecb556add04e86b278cb358dc1f2f03862705cae6f09097464c/pyobjc_framework_discrecordingui-12.1.tar.gz", hash = "sha256:6793d4a1a7f3219d063f39d87f1d4ebbbb3347e35d09194a193cfe16cba718a8", size = 16450, upload-time = "2025-11-14T10:15:00.254Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f2/4e/76016130c27b98943c5758a05beab3ba1bc9349ee881e1dfc509ea954233/pyobjc_framework_discrecordingui-12.1-py2.py3-none-any.whl", hash = "sha256:6544ef99cad3dee95716c83cb207088768b6ecd3de178f7e1b17df5997689dfd", size = 4702, upload-time = "2025-11-14T09:48:08.01Z" }, -] - -[[package]] -name = "pyobjc-framework-diskarbitration" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3a/42/f75fcabec1a0033e4c5235cc8225773f610321d565b63bf982c10c6bbee4/pyobjc_framework_diskarbitration-12.1.tar.gz", hash = "sha256:6703bc5a09b38a720c9ffca356b58f7e99fa76fc988c9ec4d87112344e63dfc2", size = 17121, upload-time = "2025-11-14T10:15:02.223Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/48/65/c1f54c47af17cb6b923eab85e95f22396c52f90ee8f5b387acffad9a99ea/pyobjc_framework_diskarbitration-12.1-py2.py3-none-any.whl", hash = "sha256:54caf3079fe4ae5ac14466a9b68923ee260a1a88a8290686b4a2015ba14c2db6", size = 4877, upload-time = "2025-11-14T09:48:09.945Z" }, -] - -[[package]] -name = "pyobjc-framework-dvdplayback" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/cf/dd/7859a58e8dd336c77f83feb76d502e9623c394ea09322e29a03f5bc04d32/pyobjc_framework_dvdplayback-12.1.tar.gz", hash = "sha256:279345d4b5fb2c47dd8e5c2fd289e644b6648b74f5c25079805eeb61bfc4a9cd", size = 32332, upload-time = "2025-11-14T10:15:05.257Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/29/7d/22c07c28fab1f15f0d364806e39a6ca63c737c645fe7e98e157878b5998c/pyobjc_framework_dvdplayback-12.1-py2.py3-none-any.whl", hash = "sha256:af911cc222272a55b46a1a02a46a355279aecfd8132231d8d1b279e252b8ad4c", size = 8243, upload-time = "2025-11-14T09:48:11.824Z" }, -] - -[[package]] -name = "pyobjc-framework-eventkit" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b6/42/4ec97e641fdcf30896fe76476181622954cb017117b1429f634d24816711/pyobjc_framework_eventkit-12.1.tar.gz", hash = "sha256:7c1882be2f444b1d0f71e9a0cd1e9c04ad98e0261292ab741fc9de0b8bbbbae9", size = 28538, upload-time = "2025-11-14T10:15:07.878Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/35/142f43227627d6324993869d354b9e57eb1e88c4e229e2271592254daf25/pyobjc_framework_eventkit-12.1-py2.py3-none-any.whl", hash = "sha256:3d2d36d5bd9e0a13887a6ac7cdd36675985ebe2a9cb3cdf8cec0725670c92c60", size = 6820, upload-time = "2025-11-14T09:48:14.035Z" }, -] - -[[package]] -name = "pyobjc-framework-exceptionhandling" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f1/17/5c9d4164f7ccf6b9100be0ad597a7857395dd58ea492cba4f0e9c0b77049/pyobjc_framework_exceptionhandling-12.1.tar.gz", hash = "sha256:7f0719eeea6695197fce0e7042342daa462683dc466eb6a442aad897032ab00d", size = 16694, upload-time = "2025-11-14T10:15:10.173Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/ad/8e05acf3635f20ea7d878be30d58a484c8b901a8552c501feb7893472f86/pyobjc_framework_exceptionhandling-12.1-py2.py3-none-any.whl", hash = "sha256:2f1eae14cf0162e53a0888d9ffe63f047501fe583a23cdc9c966e89f48cf4713", size = 7113, upload-time = "2025-11-14T09:48:15.685Z" }, -] - -[[package]] -name = "pyobjc-framework-executionpolicy" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/95/11/db765e76e7b00e1521d7bb3a61ae49b59e7573ac108da174720e5d96b61b/pyobjc_framework_executionpolicy-12.1.tar.gz", hash = "sha256:682866589365cd01d3a724d8a2781794b5cba1e152411a58825ea52d7b972941", size = 12594, upload-time = "2025-11-14T10:15:12.077Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/51/2c/f10352398f10f244401ab8f53cabd127dc3f5dbbfc8de83464661d716671/pyobjc_framework_executionpolicy-12.1-py2.py3-none-any.whl", hash = "sha256:c3a9eca3bd143cf202787dd5e3f40d954c198f18a5e0b8b3e2fcdd317bf33a52", size = 3739, upload-time = "2025-11-14T09:48:17.35Z" }, -] - -[[package]] -name = "pyobjc-framework-extensionkit" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9a/d4/e9b1f74d29ad9dea3d60468d59b80e14ed3a19f9f7a25afcbc10d29c8a1e/pyobjc_framework_extensionkit-12.1.tar.gz", hash = "sha256:773987353e8aba04223dbba3149253db944abfb090c35318b3a770195b75da6d", size = 18694, upload-time = "2025-11-14T10:15:14.104Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/d9/8064dad6114a489e5439cc20d9fb0dd64cfc406d875b4a3c87015b3f6266/pyobjc_framework_extensionkit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7e01d705c7ac6d080ae34a81db6d9b81875eabefa63fd6eafbfa30f676dd780b", size = 7932, upload-time = "2025-11-14T09:48:23.653Z" }, -] - -[[package]] -name = "pyobjc-framework-externalaccessory" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8e/35/86c097ae2fdf912c61c1276e80f3e090a3fc898c75effdf51d86afec456b/pyobjc_framework_externalaccessory-12.1.tar.gz", hash = "sha256:079f770a115d517a6ab87db1b8a62ca6cdf6c35ae65f45eecc21b491e78776c0", size = 20958, upload-time = "2025-11-14T10:15:16.419Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/52/984034396089766b6e5ff3be0f93470e721c420fa9d1076398557532234f/pyobjc_framework_externalaccessory-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:dedbf7a09375ac19668156c1417bd7829565b164a246b714e225b9cbb6a351ad", size = 8932, upload-time = "2025-11-14T09:48:37.393Z" }, -] - -[[package]] -name = "pyobjc-framework-fileprovider" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/cc/9a/724b1fae5709f8860f06a6a2a46de568f9bb8bdb2e2aae45b4e010368f51/pyobjc_framework_fileprovider-12.1.tar.gz", hash = "sha256:45034e0d00ae153c991aa01cb1fd41874650a30093e77ba73401dcce5534c8ad", size = 43071, upload-time = "2025-11-14T10:15:19.989Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/f5/56f0751a2988b2caca89d6800c8f29246828d1a7498bb676ef1ab28000b7/pyobjc_framework_fileprovider-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:89b140ea8369512ddf4164b007cbe35b4d97d1dcb8affa12a7264c0ab8d56e45", size = 21003, upload-time = "2025-11-14T09:48:53.128Z" }, -] - -[[package]] -name = "pyobjc-framework-fileproviderui" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-fileprovider" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/50/00/234f9b93f75255845df81d9d5ea20cb83ecb5c0a4e59147168b622dd0b9d/pyobjc_framework_fileproviderui-12.1.tar.gz", hash = "sha256:15296429d9db0955abc3242b2920b7a810509a85118dbc185f3ac8234e5a6165", size = 12437, upload-time = "2025-11-14T10:15:22.044Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/65/cc4397511bd0af91993d6302a2aed205296a9ad626146eefdfc8a9624219/pyobjc_framework_fileproviderui-12.1-py2.py3-none-any.whl", hash = "sha256:521a914055089e28631018bd78df4c4f7416e98b4150f861d4a5bc97d5b1ffe4", size = 3715, upload-time = "2025-11-14T09:49:04.213Z" }, -] - -[[package]] -name = "pyobjc-framework-findersync" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e4/63/c8da472e0910238a905bc48620e005a1b8ae7921701408ca13e5fb0bfb4b/pyobjc_framework_findersync-12.1.tar.gz", hash = "sha256:c513104cef0013c233bf8655b527df665ce6f840c8bc0b3781e996933d4dcfa6", size = 13507, upload-time = "2025-11-14T10:15:24.161Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/9f/ec7f393e3e2fd11cbdf930d884a0ba81078bdb61920b3cba4f264de8b446/pyobjc_framework_findersync-12.1-py2.py3-none-any.whl", hash = "sha256:e07abeca52c486cf14927f617afc27afa7a3828b99fab3ad02355105fb29203e", size = 4889, upload-time = "2025-11-14T09:49:05.763Z" }, -] - -[[package]] -name = "pyobjc-framework-fsevents" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/43/17/21f45d2bca2efc72b975f2dfeae7a163dbeabb1236c1f188578403fd4f09/pyobjc_framework_fsevents-12.1.tar.gz", hash = "sha256:a22350e2aa789dec59b62da869c1b494a429f8c618854b1383d6473f4c065a02", size = 26487, upload-time = "2025-11-14T10:15:26.796Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2d/e3/2c5eeea390c0b053e2d73b223af3ec87a3e99a8106e8d3ee79942edb0822/pyobjc_framework_fsevents-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a2949358513fd7bc622fb362b5c4af4fc24fc6307320070ca410885e5e13d975", size = 13141, upload-time = "2025-11-14T09:49:11.947Z" }, -] - -[[package]] -name = "pyobjc-framework-fskit" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a1/55/d00246d6e6d9756e129e1d94bc131c99eece2daa84b2696f6442b8a22177/pyobjc_framework_fskit-12.1.tar.gz", hash = "sha256:ec54e941cdb0b7d800616c06ca76a93685bd7119b8aa6eb4e7a3ee27658fc7ba", size = 42372, upload-time = "2025-11-14T10:15:30.411Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/a9/0c47469fe80fa14bc698bb0a5b772b44283cc3aca0f67e7f70ab45e09b24/pyobjc_framework_fskit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:50972897adea86508cfee33ec4c23aa91dede97e9da1640ea2fe74702b065be1", size = 20250, upload-time = "2025-11-14T09:49:28.065Z" }, -] - -[[package]] -name = "pyobjc-framework-gamecenter" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d2/f8/b5fd86f6b722d4259228922e125b50e0a6975120a1c4d957e990fb84e42c/pyobjc_framework_gamecenter-12.1.tar.gz", hash = "sha256:de4118f14c9cf93eb0316d49da410faded3609ce9cd63425e9ef878cebb7ea72", size = 31473, upload-time = "2025-11-14T10:15:33.38Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/16/ee/b496cc4248c5b901e159d6d9a437da9b86a3105fc3999a66744ba2b2c884/pyobjc_framework_gamecenter-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e8d6d10b868be7c00c2d5a0944cc79315945735dcf17eaa3fec1a7986d26be9b", size = 18868, upload-time = "2025-11-14T09:49:44.767Z" }, -] - -[[package]] -name = "pyobjc-framework-gamecontroller" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/21/14/353bb1fe448cd833839fd199ab26426c0248088753e63c22fe19dc07530f/pyobjc_framework_gamecontroller-12.1.tar.gz", hash = "sha256:64ed3cc4844b67f1faeb540c7cc8d512c84f70b3a4bafdb33d4663a2b2a2b1d8", size = 54554, upload-time = "2025-11-14T10:15:37.591Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/06/28/9f03d0ef7c78340441f78b19fb2d2c952af04a240da5ed30c7cf2d0d0f4e/pyobjc_framework_gamecontroller-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:878aa6590c1510e91bfc8710d6c880e7a8f3656a7b7b6f4f3af487a6f677ccd5", size = 20949, upload-time = "2025-11-14T09:50:01.608Z" }, -] - -[[package]] -name = "pyobjc-framework-gamekit" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-quartz" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/52/7b/d625c0937557f7e2e64200fdbeb867d2f6f86b2f148b8d6bfe085e32d872/pyobjc_framework_gamekit-12.1.tar.gz", hash = "sha256:014d032c3484093f1409f8f631ba8a0fd2ff7a3ae23fd9d14235340889854c16", size = 63833, upload-time = "2025-11-14T10:15:42.842Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c4/05/1c49e1030dc9f2812fa8049442158be76c32f271075f4571f94e4389ea86/pyobjc_framework_gamekit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2eee796d5781157f2c5684f7ef4c2a7ace9d9b408a26a9e7e92e8adf5a3f63d7", size = 22493, upload-time = "2025-11-14T09:50:19.129Z" }, -] - -[[package]] -name = "pyobjc-framework-gameplaykit" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-spritekit" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e2/11/c310bbc2526f95cce662cc1f1359bb11e2458eab0689737b4850d0f6acb7/pyobjc_framework_gameplaykit-12.1.tar.gz", hash = "sha256:935ebd806d802888969357946245d35a304c530c86f1ffe584e2cf21f0a608a8", size = 41511, upload-time = "2025-11-14T10:15:46.529Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/35/1f/e5fe404f92ec0f9c8c37b00d6cb3ba96ee396c7f91b0a41a39b64bfc2743/pyobjc_framework_gameplaykit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:309b0d7479f702830c9be92dbe5855ac2557a9d23f05f063caf9d9fdb85ff5f0", size = 13150, upload-time = "2025-11-14T09:50:36.884Z" }, -] - -[[package]] -name = "pyobjc-framework-gamesave" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/1b/1f/8d05585c844535e75dbc242dd6bdfecfc613d074dcb700362d1c908fb403/pyobjc_framework_gamesave-12.1.tar.gz", hash = "sha256:eb731c97aa644e78a87838ed56d0e5bdbaae125bdc8854a7772394877312cc2e", size = 12654, upload-time = "2025-11-14T10:15:48.344Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/59/ec/93d48cb048a1b35cea559cc9261b07f0d410078b3af029121302faa410d0/pyobjc_framework_gamesave-12.1-py2.py3-none-any.whl", hash = "sha256:432e69f8404be9290d42c89caba241a3156ed52013947978ac54f0f032a14ffd", size = 3689, upload-time = "2025-11-14T09:50:47.263Z" }, -] - -[[package]] -name = "pyobjc-framework-healthkit" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/af/67/436630d00ba1028ea33cc9df2fc28e081481433e5075600f2ea1ff00f45e/pyobjc_framework_healthkit-12.1.tar.gz", hash = "sha256:29c5e5de54b41080b7a4b0207698ac6f600dcb9149becc9c6b3a69957e200e5c", size = 91802, upload-time = "2025-11-14T10:15:54.661Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/65/87/bb1c438de51c4fa733a99ce4d3301e585f14d7efd94031a97707c0be2b46/pyobjc_framework_healthkit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:15b6fc958ff5de42888b18dffdec839cb36d2dd8b82076ed2f21a51db5271109", size = 20799, upload-time = "2025-11-14T09:50:54.531Z" }, -] - -[[package]] -name = "pyobjc-framework-imagecapturecore" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6d/a1/39347381fc7d3cd5ab942d86af347b25c73f0ddf6f5227d8b4d8f5328016/pyobjc_framework_imagecapturecore-12.1.tar.gz", hash = "sha256:c4776c59f4db57727389d17e1ffd9c567b854b8db52198b3ccc11281711074e5", size = 46397, upload-time = "2025-11-14T10:15:58.541Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/50/13/632957b284dec3743d73fb30dbdf03793b3cf1b4c62e61e6484d870f3879/pyobjc_framework_imagecapturecore-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a2777e17ff71fb5a327a897e48c5c7b5a561723a80f990d26e6ed5a1b8748816", size = 16012, upload-time = "2025-11-14T09:51:12.058Z" }, -] - -[[package]] -name = "pyobjc-framework-inputmethodkit" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5d/b8/d33dd8b7306029bbbd80525bf833fc547e6a223c494bf69a534487283a28/pyobjc_framework_inputmethodkit-12.1.tar.gz", hash = "sha256:f63b6fe2fa7f1412eae63baea1e120e7865e3b68ccfb7d8b0a4aadb309f2b278", size = 23054, upload-time = "2025-11-14T10:16:01.464Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/01/c2/59bea66405784b25f5d4e821467ba534a0b92dfc98e07257c971e2a8ed73/pyobjc_framework_inputmethodkit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0b7d813d46a060572fc0c14ef832e4fe538ebf64e5cab80ee955191792ce0110", size = 9506, upload-time = "2025-11-14T09:51:26.924Z" }, -] - -[[package]] -name = "pyobjc-framework-installerplugins" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e7/60/ca4ab04eafa388a97a521db7d60a812e2f81a3c21c2372587872e6b074f9/pyobjc_framework_installerplugins-12.1.tar.gz", hash = "sha256:1329a193bd2e92a2320a981a9a421a9b99749bade3e5914358923e94fe995795", size = 25277, upload-time = "2025-11-14T10:16:04.379Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/99/1f/31dca45db3342882a628aa1b27707a283d4dc7ef558fddd2533175a0661a/pyobjc_framework_installerplugins-12.1-py2.py3-none-any.whl", hash = "sha256:d2201c81b05bdbe0abf0af25db58dc230802573463bea322f8b2863e37b511d5", size = 4813, upload-time = "2025-11-14T09:51:37.836Z" }, -] - -[[package]] -name = "pyobjc-framework-instantmessage" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-quartz" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d4/67/66754e0d26320ba24a33608ca94d3f38e60ee6b2d2e094cb6269b346fdd4/pyobjc_framework_instantmessage-12.1.tar.gz", hash = "sha256:f453118d5693dc3c94554791bd2aaafe32a8b03b0e3d8ec3934b44b7fdd1f7e7", size = 31217, upload-time = "2025-11-14T10:16:07.693Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/38/6ae95b5c87d887c075bd5f4f7cca3d21dafd0a77cfdde870e87ca17579eb/pyobjc_framework_instantmessage-12.1-py2.py3-none-any.whl", hash = "sha256:cd91d38e8f356afd726b6ea8c235699316ea90edfd3472965c251efbf4150bc9", size = 5436, upload-time = "2025-11-14T09:51:39.557Z" }, -] - -[[package]] -name = "pyobjc-framework-intents" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f1/a1/3bab6139e94b97eca098e1562f5d6840e3ff10ea1f7fd704a17111a97d5b/pyobjc_framework_intents-12.1.tar.gz", hash = "sha256:bd688c3ab34a18412f56e459e9dae29e1f4152d3c2048fcacdef5fc49dfb9765", size = 132262, upload-time = "2025-11-14T10:16:16.428Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7a/90/e9489492ae90b4c1ffd02c1221c0432b8768d475787e7887f79032c2487a/pyobjc_framework_intents-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0ea9f3e79bf4baf6c7b0fd2d2797184ed51a372bf7f32974b4424f9bd067ef50", size = 32156, upload-time = "2025-11-14T09:51:49.438Z" }, -] - -[[package]] -name = "pyobjc-framework-intentsui" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-intents" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/19/cf/f0e385b9cfbf153d68efe8d19e5ae672b59acbbfc1f9b58faaefc5ec8c9e/pyobjc_framework_intentsui-12.1.tar.gz", hash = "sha256:16bdf4b7b91c0d1ec9d5513a1182861f1b5b7af95d4f4218ff7cf03032d57f99", size = 19784, upload-time = "2025-11-14T10:16:18.716Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f1/17/06812542a9028f5b2dcce56f52f25633c08b638faacd43bad862aad1b41d/pyobjc_framework_intentsui-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:cb894fcc4c9ea613a424dcf6fb48142d51174559b82cfdafac8cb47555c842cf", size = 8983, upload-time = "2025-11-14T09:52:07.667Z" }, -] - -[[package]] -name = "pyobjc-framework-iobluetooth" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e4/aa/ca3944bbdfead4201b4ae6b51510942c5a7d8e5e2dc3139a071c74061fdf/pyobjc_framework_iobluetooth-12.1.tar.gz", hash = "sha256:8a434118812f4c01dfc64339d41fe8229516864a59d2803e9094ee4cbe2b7edd", size = 155241, upload-time = "2025-11-14T10:16:28.896Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/b6/933b56afb5e84c3c35c074c9e30d7b701c6038989d4867867bdaa7ab618b/pyobjc_framework_iobluetooth-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:111a6e54be9e9dcf77fa2bf84fdac09fae339aa33087d8647ea7ffbd34765d4c", size = 40439, upload-time = "2025-11-14T09:52:26.071Z" }, -] - -[[package]] -name = "pyobjc-framework-iobluetoothui" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-iobluetooth" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8f/39/31d9a4e8565a4b1ec0a9ad81480dc0879f3df28799eae3bc22d1dd53705d/pyobjc_framework_iobluetoothui-12.1.tar.gz", hash = "sha256:81f8158bdfb2966a574b6988eb346114d6a4c277300c8c0a978c272018184e6f", size = 16495, upload-time = "2025-11-14T10:16:31.212Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e3/c9/69aeda0cdb5d25d30dc4596a1c5b464fc81b5c0c4e28efc54b7e11bde51c/pyobjc_framework_iobluetoothui-12.1-py2.py3-none-any.whl", hash = "sha256:a6d8ab98efa3029130577a57ee96b183c35c39b0f1c53a7534f8838260fab993", size = 4045, upload-time = "2025-11-14T09:52:42.201Z" }, -] - -[[package]] -name = "pyobjc-framework-iosurface" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/07/61/0f12ad67a72d434e1c84b229ec760b5be71f53671ee9018593961c8bfeb7/pyobjc_framework_iosurface-12.1.tar.gz", hash = "sha256:4b9d0c66431aa296f3ca7c4f84c00dc5fc961194830ad7682fdbbc358fa0db55", size = 17690, upload-time = "2025-11-14T10:16:33.282Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/88/ad/793d98a7ed9b775dc8cce54144cdab0df1808a1960ee017e46189291a8f3/pyobjc_framework_iosurface-12.1-py2.py3-none-any.whl", hash = "sha256:e784e248397cfebef4655d2c0025766d3eaa4a70474e363d084fc5ce2a4f2a3f", size = 4902, upload-time = "2025-11-14T09:52:43.899Z" }, -] - -[[package]] -name = "pyobjc-framework-ituneslibrary" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f5/46/d9bcec88675bf4ee887b9707bd245e2a793e7cb916cf310f286741d54b1f/pyobjc_framework_ituneslibrary-12.1.tar.gz", hash = "sha256:7f3aa76c4d05f6fa6015056b88986cacbda107c3f29520dd35ef0936c7367a6e", size = 23730, upload-time = "2025-11-14T10:16:36.127Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/de/92/b598694a1713ee46f45c4bfb1a0425082253cbd2b1caf9f8fd50f292b017/pyobjc_framework_ituneslibrary-12.1-py2.py3-none-any.whl", hash = "sha256:fb678d7c3ff14c81672e09c015e25880dac278aa819971f4d5f75d46465932ef", size = 5205, upload-time = "2025-11-14T09:52:45.733Z" }, -] - -[[package]] -name = "pyobjc-framework-kernelmanagement" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0a/7e/ecbac119866e8ac2cce700d7a48a4297946412ac7cbc243a7084a6582fb1/pyobjc_framework_kernelmanagement-12.1.tar.gz", hash = "sha256:488062893ac2074e0c8178667bf864a21f7909c11111de2f6a10d9bc579df59d", size = 11773, upload-time = "2025-11-14T10:16:38.216Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/94/32/04325a20f39d88d6d712437e536961a9e6a4ec19f204f241de6ed54d1d84/pyobjc_framework_kernelmanagement-12.1-py2.py3-none-any.whl", hash = "sha256:926381bfbfbc985c3e6dfcb7004af21bb16ff66ecbc08912b925989a705944ff", size = 3704, upload-time = "2025-11-14T09:52:47.268Z" }, -] - -[[package]] -name = "pyobjc-framework-latentsemanticmapping" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/88/3c/b621dac54ae8e77ac25ee75dd93e310e2d6e0faaf15b8da13513258d6657/pyobjc_framework_latentsemanticmapping-12.1.tar.gz", hash = "sha256:f0b1fa823313eefecbf1539b4ed4b32461534b7a35826c2cd9f6024411dc9284", size = 15526, upload-time = "2025-11-14T10:16:40.149Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/29/8e/74a7eb29b545f294485cd3cf70557b4a35616555fe63021edbb3e0ea4c20/pyobjc_framework_latentsemanticmapping-12.1-py2.py3-none-any.whl", hash = "sha256:7d760213b42bc8b1bc1472e1873c0f78ee80f987225978837b1fecdceddbdbf4", size = 5471, upload-time = "2025-11-14T09:52:48.939Z" }, -] - -[[package]] -name = "pyobjc-framework-launchservices" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-coreservices" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/37/d0/24673625922b0ad21546be5cf49e5ec1afaa4553ae92f222adacdc915907/pyobjc_framework_launchservices-12.1.tar.gz", hash = "sha256:4d2d34c9bd6fb7f77566155b539a2c70283d1f0326e1695da234a93ef48352dc", size = 20470, upload-time = "2025-11-14T10:16:42.499Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/08/af/9a0aebaab4c15632dc8fcb3669c68fa541a3278d99541d9c5f966fbc0909/pyobjc_framework_launchservices-12.1-py2.py3-none-any.whl", hash = "sha256:e63e78fceeed4d4dc807f9dabd5cf90407e4f552fab6a0d75a8d0af63094ad3c", size = 3905, upload-time = "2025-11-14T09:52:50.71Z" }, -] - -[[package]] -name = "pyobjc-framework-libdispatch" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/26/e8/75b6b9b3c88b37723c237e5a7600384ea2d84874548671139db02e76652b/pyobjc_framework_libdispatch-12.1.tar.gz", hash = "sha256:4035535b4fae1b5e976f3e0e38b6e3442ffea1b8aa178d0ca89faa9b8ecdea41", size = 38277, upload-time = "2025-11-14T10:16:46.235Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/83/6f/96e15c7b2f7b51fc53252216cd0bed0c3541bc0f0aeb32756fefd31bed7d/pyobjc_framework_libdispatch-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0e9570d7a9a3136f54b0b834683bf3f206acd5df0e421c30f8fd4f8b9b556789", size = 15650, upload-time = "2025-11-14T09:52:59.284Z" }, -] - -[[package]] -name = "pyobjc-framework-libxpc" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/16/e4/364db7dc26f235e3d7eaab2f92057f460b39800bffdec3128f113388ac9f/pyobjc_framework_libxpc-12.1.tar.gz", hash = "sha256:e46363a735f3ecc9a2f91637750623f90ee74f9938a4e7c833e01233174af44d", size = 35186, upload-time = "2025-11-14T10:16:49.503Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/82/7f/fdec72430f90921b154517a6f9bbeefa7bacfb16b91320742eb16a5955c5/pyobjc_framework_libxpc-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ba93e91e9ca79603dd265382e9f80e9bd32309cd09c8ac3e6489fc5b233676c8", size = 19730, upload-time = "2025-11-14T09:53:17.113Z" }, -] - -[[package]] -name = "pyobjc-framework-linkpresentation" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-quartz" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e3/58/c0c5919d883485ccdb6dccd8ecfe50271d2f6e6ab7c9b624789235ccec5a/pyobjc_framework_linkpresentation-12.1.tar.gz", hash = "sha256:84df6779591bb93217aa8bd82c10e16643441678547d2d73ba895475a02ade94", size = 13330, upload-time = "2025-11-14T10:16:52.169Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ad/51/226eb45f196f3bf93374713571aae6c8a4760389e1d9435c4a4cc3f38ea4/pyobjc_framework_linkpresentation-12.1-py2.py3-none-any.whl", hash = "sha256:853a84c7b525b77b114a7a8d798aef83f528ed3a6803bda12184fe5af4e79a47", size = 3865, upload-time = "2025-11-14T09:53:28.386Z" }, -] - -[[package]] -name = "pyobjc-framework-localauthentication" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-security" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8d/0e/7e5d9a58bb3d5b79a75d925557ef68084171526191b1c0929a887a553d4f/pyobjc_framework_localauthentication-12.1.tar.gz", hash = "sha256:2284f587d8e1206166e4495b33f420c1de486c36c28c4921d09eec858a699d05", size = 29947, upload-time = "2025-11-14T10:16:54.923Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/05/93/91761ad4e5fa1c3ec25819865d1ccfbee033987147087bff4fcce67a4dc4/pyobjc_framework_localauthentication-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3af1acd287d830cc7f912f46cde0dab054952bde0adaf66c8e8524311a68d279", size = 10773, upload-time = "2025-11-14T09:53:34.074Z" }, -] - -[[package]] -name = "pyobjc-framework-localauthenticationembeddedui" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-localauthentication" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/31/20/83ab4180e29b9a4a44d735c7f88909296c6adbe6250e8e00a156aff753e1/pyobjc_framework_localauthenticationembeddedui-12.1.tar.gz", hash = "sha256:a15ec44bf2769c872e86c6b550b6dd4f58d4eda40ad9ff00272a67d279d1d4e9", size = 13611, upload-time = "2025-11-14T10:16:57.145Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/30/7d/0d46639c7a26b6af928ab4c822cd28b733791e02ac28cc84c3014bcf7dc7/pyobjc_framework_localauthenticationembeddedui-12.1-py2.py3-none-any.whl", hash = "sha256:a7ce7b56346597b9f4768be61938cbc8fc5b1292137225b6c7f631b9cde97cd7", size = 3991, upload-time = "2025-11-14T09:53:42.958Z" }, -] - -[[package]] -name = "pyobjc-framework-mailkit" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/2a/98/3d9028620c1cd32ff4fb031155aba3b5511e980cdd114dd51383be9cb51b/pyobjc_framework_mailkit-12.1.tar.gz", hash = "sha256:d5574b7259baec17096410efcaacf5d45c7bb5f893d4c25cbb7072369799b652", size = 20996, upload-time = "2025-11-14T10:16:59.449Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/70/8d/3c968b736a3a8bd9d8e870b39b1c772a013eea1b81b89fc4efad9021a6cb/pyobjc_framework_mailkit-12.1-py2.py3-none-any.whl", hash = "sha256:536ac0c4ea3560364cd159a6512c3c18a744a12e4e0883c07df0f8a2ff21e3fe", size = 4871, upload-time = "2025-11-14T09:53:44.697Z" }, -] - -[[package]] -name = "pyobjc-framework-mapkit" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-corelocation" }, - { name = "pyobjc-framework-quartz" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/36/bb/2a668203c20e509a648c35e803d79d0c7f7816dacba74eb5ad8acb186790/pyobjc_framework_mapkit-12.1.tar.gz", hash = "sha256:dbc32dc48e821aaa9b4294402c240adbc1c6834e658a07677b7c19b7990533c5", size = 63520, upload-time = "2025-11-14T10:17:04.221Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/11/00/a3de41cdf3e6cd7a144e38999fe1ea9777ad19e19d863f2da862e7affe7b/pyobjc_framework_mapkit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:84ad7766271c114bdc423e4e2ff5433e5fc6771a3338b5f8e4b54d0340775800", size = 22518, upload-time = "2025-11-14T09:53:52.727Z" }, -] - -[[package]] -name = "pyobjc-framework-mediaaccessibility" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e0/10/dc1007e56944ed2e981e69e7b2fed2b2202c79b0d5b742b29b1081d1cbdd/pyobjc_framework_mediaaccessibility-12.1.tar.gz", hash = "sha256:cc4e3b1d45e84133d240318d53424eff55968f5c6873c2c53267598853445a3f", size = 16325, upload-time = "2025-11-14T10:17:07.454Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a2/0c/7fb5462561f59d739192c6d02ba0fd36ad7841efac5a8398a85a030ef7fc/pyobjc_framework_mediaaccessibility-12.1-py2.py3-none-any.whl", hash = "sha256:2ff8845c97dd52b0e5cf53990291e6d77c8a73a7aac0e9235d62d9a4256916d1", size = 4800, upload-time = "2025-11-14T09:54:05.04Z" }, -] - -[[package]] -name = "pyobjc-framework-mediaextension" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-avfoundation" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-coremedia" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d6/aa/1e8015711df1cdb5e4a0aa0ed4721409d39971ae6e1e71915e3ab72423a3/pyobjc_framework_mediaextension-12.1.tar.gz", hash = "sha256:44409d63cc7d74e5724a68e3f9252cb62fd0fd3ccf0ca94c6a33e5c990149953", size = 39425, upload-time = "2025-11-14T10:17:11.486Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2b/ed/99038bcf72ec68e452709af10a087c1377c2d595ba4e66d7a2b0775145d2/pyobjc_framework_mediaextension-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:442bc3a759efb5c154cb75d643a5e182297093533fcdd1c24be6f64f68b93371", size = 38973, upload-time = "2025-11-14T09:54:16.701Z" }, -] - -[[package]] -name = "pyobjc-framework-medialibrary" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-quartz" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/34/e9/848ebd02456f8fdb41b42298ec585bfed5899dbd30306ea5b0a7e4c4b341/pyobjc_framework_medialibrary-12.1.tar.gz", hash = "sha256:690dcca09b62511df18f58e8566cb33d9652aae09fe63a83f594bd018b5edfcd", size = 15995, upload-time = "2025-11-14T10:17:15.45Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/cd/eeaf8585a343fda5b8cf3b8f144c872d1057c845202098b9441a39b76cb0/pyobjc_framework_medialibrary-12.1-py2.py3-none-any.whl", hash = "sha256:1f03ad6802a5c6e19ee3208b065689d3ec79defe1052cb80e00f54e1eff5f2a0", size = 4361, upload-time = "2025-11-14T09:54:32.259Z" }, -] - -[[package]] -name = "pyobjc-framework-mediaplayer" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-avfoundation" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e9/f0/851f6f47e11acbd62d5f5dcb8274afc969135e30018591f75bf3cbf6417f/pyobjc_framework_mediaplayer-12.1.tar.gz", hash = "sha256:5ef3f669bdf837d87cdb5a486ec34831542360d14bcba099c7c2e0383380794c", size = 35402, upload-time = "2025-11-14T10:17:18.97Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/58/c0/038ee3efd286c0fbc89c1e0cb688f4670ed0e5803aa36e739e79ffc91331/pyobjc_framework_mediaplayer-12.1-py2.py3-none-any.whl", hash = "sha256:85d9baec131807bfdf0f4c24d4b943e83cce806ab31c95c7e19c78e3fb7eefc8", size = 7120, upload-time = "2025-11-14T09:54:33.901Z" }, -] - -[[package]] -name = "pyobjc-framework-mediatoolbox" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a3/71/be5879380a161f98212a336b432256f307d1dcbaaaeb8ec988aea2ada2cd/pyobjc_framework_mediatoolbox-12.1.tar.gz", hash = "sha256:385b48746a5f08756ee87afc14037e552954c427ed5745d7ece31a21a7bad5ab", size = 22305, upload-time = "2025-11-14T10:17:22.501Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/94/d5ee221f2afbc64b2a7074efe25387cd8700e8116518904b28091ea6ad74/pyobjc_framework_mediatoolbox-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d7bcfeeff3fbf7e9e556ecafd8eaed2411df15c52baf134efa7480494e6faf6d", size = 12818, upload-time = "2025-11-14T09:54:41.251Z" }, -] - -[[package]] -name = "pyobjc-framework-metal" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e7/06/a84f7eb8561d5631954b9458cfca04b690b80b5b85ce70642bc89335f52a/pyobjc_framework_metal-12.1.tar.gz", hash = "sha256:bb554877d5ee2bf3f340ad88e8fe1b85baab7b5ec4bd6ae0f4f7604147e3eae7", size = 181847, upload-time = "2025-11-14T10:17:34.157Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/48/9286d06e1b14c11b65d3fea1555edc0061d9ebe11898dff8a14089e3a4c9/pyobjc_framework_metal-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:38ab566b5a2979a43e13593d3eb12000a45e574576fe76996a5e1eb75ad7ac78", size = 75841, upload-time = "2025-11-14T09:55:06.801Z" }, -] - -[[package]] -name = "pyobjc-framework-metalfx" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-metal" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f1/09/ce5c74565677fde66de3b9d35389066b19e5d1bfef9d9a4ad80f0c858c0c/pyobjc_framework_metalfx-12.1.tar.gz", hash = "sha256:1551b686fb80083a97879ce0331bdb1d4c9b94557570b7ecc35ebf40ff65c90b", size = 29470, upload-time = "2025-11-14T10:17:37.16Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/0b/508e3af499694f4eec74cc3ab0530e38db76e43a27db9ecb98c50c68f5f9/pyobjc_framework_metalfx-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a4418ae5c2eb77ec00695fa720a547638dc252dfd77ecb6feb88f713f5a948fd", size = 15062, upload-time = "2025-11-14T09:55:37.352Z" }, -] - -[[package]] -name = "pyobjc-framework-metalkit" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-metal" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/14/15/5091147aae12d4011a788b93971c3376aaaf9bf32aa935a2c9a06a71e18b/pyobjc_framework_metalkit-12.1.tar.gz", hash = "sha256:14cc5c256f0e3471b412a5b3582cb2a0d36d3d57401a8aa09e433252d1c34824", size = 25473, upload-time = "2025-11-14T10:17:39.721Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/c0/c8b5b060895cd51493afe3f09909b7e34893b1161cf4d93bc8e3cd306129/pyobjc_framework_metalkit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1c4869076571d94788fe539fabfdd568a5c8e340936c7726d2551196640bd152", size = 8755, upload-time = "2025-11-14T09:55:51.683Z" }, -] - -[[package]] -name = "pyobjc-framework-metalperformanceshaders" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-metal" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c5/68/58da38e54aa0d8c19f0d3084d8c84e92d54cc8c9254041f07119d86aa073/pyobjc_framework_metalperformanceshaders-12.1.tar.gz", hash = "sha256:b198e755b95a1de1525e63c3b14327ae93ef1d88359e6be1ce554a3493755b50", size = 137301, upload-time = "2025-11-14T10:17:49.554Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/62/84/d505496fca9341e0cb11258ace7640cd986fe3e831f8b4749035e9f82109/pyobjc_framework_metalperformanceshaders-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c00e786c352b3ff5d86cf0cf3a830dc9f6fc32a03ae1a7539d20d11324adb2e8", size = 33242, upload-time = "2025-11-14T09:56:09.354Z" }, -] - -[[package]] -name = "pyobjc-framework-metalperformanceshadersgraph" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-metalperformanceshaders" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a7/56/7ad0cd085532f7bdea9a8d4e9a2dfde376d26dd21e5eabdf1a366040eff8/pyobjc_framework_metalperformanceshadersgraph-12.1.tar.gz", hash = "sha256:b8fd017b47698037d7b172d41bed7a4835f4c4f2a288235819d200005f89ee35", size = 42992, upload-time = "2025-11-14T10:17:53.502Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e2/c9/5e7fd0d4bc9bdf7b442f36e020677c721ba9b4c1dc1fa3180085f22a4ef9/pyobjc_framework_metalperformanceshadersgraph-12.1-py2.py3-none-any.whl", hash = "sha256:85a1c7a6114ada05c7924b3235a1a98c45359410d148097488f15aee5ebb6ab9", size = 6481, upload-time = "2025-11-14T09:56:23.66Z" }, -] - -[[package]] -name = "pyobjc-framework-metrickit" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ba/13/5576ddfbc0b174810a49171e2dbe610bdafd3b701011c6ecd9b3a461de8a/pyobjc_framework_metrickit-12.1.tar.gz", hash = "sha256:77841daf6b36ba0c19df88545fd910c0516acf279e6b7b4fa0a712a046eaa9f1", size = 27627, upload-time = "2025-11-14T10:17:56.353Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/04/8da5126e47306438c99750f1dfed430d7cc388f6b7f420ae748f3060ab96/pyobjc_framework_metrickit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3ec96e9ec7dc37fbce57dae277f0d89c66ffe1c3fa2feaca1b7125f8b2b29d87", size = 8120, upload-time = "2025-11-14T09:56:28.73Z" }, -] - -[[package]] -name = "pyobjc-framework-mlcompute" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/00/69/15f8ce96c14383aa783c8e4bc1e6d936a489343bb197b8e71abb3ddc1cb8/pyobjc_framework_mlcompute-12.1.tar.gz", hash = "sha256:3281db120273dcc56e97becffd5cedf9c62042788289f7be6ea067a863164f1e", size = 40698, upload-time = "2025-11-14T10:17:59.792Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ac/f7/4614b9ccd0151795e328b9ed881fbcbb13e577a8ec4ae3507edb1a462731/pyobjc_framework_mlcompute-12.1-py2.py3-none-any.whl", hash = "sha256:4f0fc19551d710a03dfc4c7129299897544ff8ea76db6c7539ecc2f9b2571bde", size = 6744, upload-time = "2025-11-14T09:56:36.973Z" }, -] - -[[package]] -name = "pyobjc-framework-modelio" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-quartz" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b4/11/32c358111b623b4a0af9e90470b198fffc068b45acac74e1ba711aee7199/pyobjc_framework_modelio-12.1.tar.gz", hash = "sha256:d041d7bca7c2a4526344d3e593347225b7a2e51a499b3aa548895ba516d1bdbb", size = 66482, upload-time = "2025-11-14T10:18:04.92Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f6/0e/b8331100f0d658ecb3e87e75c108e2ae8ac7c78b521fd5ad0205b60a2584/pyobjc_framework_modelio-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:68d971917c289fdddf69094c74915d2ccb746b42b150e0bdc16d8161e6164022", size = 20193, upload-time = "2025-11-14T09:56:44.296Z" }, -] - -[[package]] -name = "pyobjc-framework-multipeerconnectivity" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/87/35/0d0bb6881004cb238cfd7bf74f4b2e42601a1accdf27b2189ec61cf3a2dc/pyobjc_framework_multipeerconnectivity-12.1.tar.gz", hash = "sha256:7123f734b7174cacbe92a51a62b4645cc9033f6b462ff945b504b62e1b9e6c1c", size = 22816, upload-time = "2025-11-14T10:18:07.363Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/33/8d/0646ff7db36942829f0e84be18ba44bc5cd96d6a81651f8e7dc0974821c1/pyobjc_framework_multipeerconnectivity-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1c3bd254a16debed321debf4858f9c9b7d41572ddf1058a4bacf6a5bcfedeeff", size = 12001, upload-time = "2025-11-14T09:57:01.027Z" }, -] - -[[package]] -name = "pyobjc-framework-naturallanguage" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8a/d1/c81c0cdbb198d498edc9bc5fbb17e79b796450c17bb7541adbf502f9ad65/pyobjc_framework_naturallanguage-12.1.tar.gz", hash = "sha256:cb27a1e1e5b2913d308c49fcd2fd04ab5ea87cb60cac4a576a91ebf6a50e52f6", size = 23524, upload-time = "2025-11-14T10:18:09.883Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/d8/715a11111f76c80769cb267a19ecf2a4ac76152a6410debb5a4790422256/pyobjc_framework_naturallanguage-12.1-py2.py3-none-any.whl", hash = "sha256:a02ef383ec88948ca28f03ab8995523726b3bc75c49f593b5c89c218bcbce7ce", size = 5320, upload-time = "2025-11-14T09:57:10.294Z" }, -] - -[[package]] -name = "pyobjc-framework-netfs" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/46/68/4bf0e5b8cc0780cf7acf0aec54def58c8bcf8d733db0bd38f5a264d1af06/pyobjc_framework_netfs-12.1.tar.gz", hash = "sha256:e8d0c25f41d7d9ced1aa2483238d0a80536df21f4b588640a72e1bdb87e75c1e", size = 14799, upload-time = "2025-11-14T10:18:11.85Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/6b/8c2f223879edd3e3f030d0a9c9ba812775519c6d0c257e3e7255785ca6e7/pyobjc_framework_netfs-12.1-py2.py3-none-any.whl", hash = "sha256:0021f8b141e693d3821524c170e9c645090eb320e80c2935ddb978a6e8b8da81", size = 4163, upload-time = "2025-11-14T09:57:11.845Z" }, -] - -[[package]] -name = "pyobjc-framework-network" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/38/13/a71270a1b0a9ec979e68b8ec84b0f960e908b17b51cb3cac246a74d52b6b/pyobjc_framework_network-12.1.tar.gz", hash = "sha256:dbf736ff84d1caa41224e86ff84d34b4e9eb6918ae4e373a44d3cb597648a16a", size = 56990, upload-time = "2025-11-14T10:18:16.714Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9d/ef/a53f04f43e93932817f2ea71689dcc8afe3b908d631c21d11ec30c7b2e87/pyobjc_framework_network-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:5e53aad64eae2933fe12d49185d66aca62fb817abf8a46f86b01e436ce1b79e4", size = 19613, upload-time = "2025-11-14T09:57:19.571Z" }, -] - -[[package]] -name = "pyobjc-framework-networkextension" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/bf/3e/ac51dbb2efa16903e6af01f3c1f5a854c558661a7a5375c3e8767ac668e8/pyobjc_framework_networkextension-12.1.tar.gz", hash = "sha256:36abc339a7f214ab6a05cb2384a9df912f247163710741e118662bd049acfa2e", size = 62796, upload-time = "2025-11-14T10:18:21.769Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f6/14/4934b10ade5ad0518001bfc25260d926816b9c7d08d85ef45e8a61fdef1b/pyobjc_framework_networkextension-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:adc9baacfc532944d67018e381c7645f66a9fa0064939a5a841476d81422cdcc", size = 14376, upload-time = "2025-11-14T09:57:36.132Z" }, -] - -[[package]] -name = "pyobjc-framework-notificationcenter" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c6/12/ae0fe82fb1e02365c9fe9531c9de46322f7af09e3659882212c6bf24d75e/pyobjc_framework_notificationcenter-12.1.tar.gz", hash = "sha256:2d09f5ab9dc39770bae4fa0c7cfe961e6c440c8fc465191d403633dccc941094", size = 21282, upload-time = "2025-11-14T10:18:24.51Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d8/05/3168637dd425257df5693c2ceafecf92d2e6833c0aaa6594d894a528d797/pyobjc_framework_notificationcenter-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:82a735bd63f315f0a56abd206373917b7d09a0ae35fd99f1639a0fac4c525c0a", size = 9895, upload-time = "2025-11-14T09:57:51.151Z" }, -] - -[[package]] -name = "pyobjc-framework-opendirectory" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5b/11/bc2f71d3077b3bd078dccad5c0c5c57ec807fefe3d90c97b97dd0ed3d04b/pyobjc_framework_opendirectory-12.1.tar.gz", hash = "sha256:2c63ce5dd179828ef2d8f9e3961da3bfa971a57db07a6c34eedc296548a928bb", size = 61049, upload-time = "2025-11-14T10:18:29.336Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d6/e7/3c2dece9c5b28af28a44d72a27b35ea5ffac31fed7cbd8d696ea75dc4a81/pyobjc_framework_opendirectory-12.1-py2.py3-none-any.whl", hash = "sha256:b5b5a5cf3cc2fb25147b16b79f046b90e3982bf3ded1b210a993d8cfdba737c4", size = 11845, upload-time = "2025-11-14T09:58:00.175Z" }, -] - -[[package]] -name = "pyobjc-framework-osakit" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/cb/b9/bf52c555c75a83aa45782122432fa06066bb76469047f13d06fb31e585c4/pyobjc_framework_osakit-12.1.tar.gz", hash = "sha256:36ea6acf03483dc1e4344a0cce7250a9656f44277d12bc265fa86d4cbde01f23", size = 17102, upload-time = "2025-11-14T10:18:31.354Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/99/10/30a15d7b23e6fcfa63d41ca4c7356c39ff81300249de89c3ff28216a9790/pyobjc_framework_osakit-12.1-py2.py3-none-any.whl", hash = "sha256:c49165336856fd75113d2e264a98c6deb235f1bd033eae48f661d4d832d85e6b", size = 4162, upload-time = "2025-11-14T09:58:01.953Z" }, -] - -[[package]] -name = "pyobjc-framework-oslog" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-coremedia" }, - { name = "pyobjc-framework-quartz" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/12/42/805c9b4ac6ad25deb4215989d8fc41533d01e07ffd23f31b65620bade546/pyobjc_framework_oslog-12.1.tar.gz", hash = "sha256:d0ec6f4e3d1689d5e4341bc1130c6f24cb4ad619939f6c14d11a7e80c0ac4553", size = 21193, upload-time = "2025-11-14T10:18:33.645Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ee/60/0b742347d484068e9d6867cd95dedd1810c790b6aca45f6ef1d0f089f1f5/pyobjc_framework_oslog-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:072a41d36fcf780a070f13ac2569f8bafbb5ae4792fab4136b1a4d602dd9f5b4", size = 7813, upload-time = "2025-11-14T09:58:07.768Z" }, -] - -[[package]] -name = "pyobjc-framework-passkit" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6c/d4/2afb59fb0f99eb2f03888850887e536f1ef64b303fd756283679471a5189/pyobjc_framework_passkit-12.1.tar.gz", hash = "sha256:d8c27c352e86a3549bf696504e6b25af5f2134b173d9dd60d66c6d3da53bb078", size = 53835, upload-time = "2025-11-14T10:18:37.906Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d8/dc/9cb27e8b7b00649af5e802815ffa8928bd8a619f2984a1bea7dabd28f741/pyobjc_framework_passkit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7e95a484ec529dbf1d44f5f7f1406502a77bda733511e117856e3dca9fa29c5c", size = 14102, upload-time = "2025-11-14T09:58:20.903Z" }, -] - -[[package]] -name = "pyobjc-framework-pencilkit" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/7e/43/859068016bcbe7d80597d5c579de0b84b0da62c5c55cdf9cc940e9f9c0f8/pyobjc_framework_pencilkit-12.1.tar.gz", hash = "sha256:d404982d1f7a474369f3e7fea3fbd6290326143fa4138d64b6753005a6263dc4", size = 17664, upload-time = "2025-11-14T10:18:40.045Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/26/daf47dcfced8f7326218dced5c68ed2f3b522ec113329218ce1305809535/pyobjc_framework_pencilkit-12.1-py2.py3-none-any.whl", hash = "sha256:33b88e5ed15724a12fd8bf27a68614b654ff739d227e81161298bc0d03acca4f", size = 4206, upload-time = "2025-11-14T09:58:30.814Z" }, -] - -[[package]] -name = "pyobjc-framework-phase" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-avfoundation" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/96/51/3b25eaf7ca85f38ceef892fdf066b7faa0fec716f35ea928c6ffec6ae311/pyobjc_framework_phase-12.1.tar.gz", hash = "sha256:3a69005c572f6fd777276a835115eb8359a33673d4a87e754209f99583534475", size = 32730, upload-time = "2025-11-14T10:18:43.102Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ae/9f/1ae45db731e8d6dd3e0b408c3accd0cf3236849e671f95c7c8cf95687240/pyobjc_framework_phase-12.1-py2.py3-none-any.whl", hash = "sha256:99a1c1efc6644f5312cce3693117d4e4482538f65ad08fe59e41e2579b67ab17", size = 6902, upload-time = "2025-11-14T09:58:32.436Z" }, -] - -[[package]] -name = "pyobjc-framework-photos" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b8/53/f8a3dc7f711034d2283e289cd966fb7486028ea132a24260290ff32d3525/pyobjc_framework_photos-12.1.tar.gz", hash = "sha256:adb68aaa29e186832d3c36a0b60b0592a834e24c5263e9d78c956b2b77dce563", size = 47034, upload-time = "2025-11-14T10:18:47.27Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/13/38/e6f25aec46a1a9d0a310795606cc43f9823d41c3e152114b814b597835a8/pyobjc_framework_photos-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eda8a584a851506a1ebbb2ee8de2cb1ed9e3431e6a642ef6a9543e32117d17b9", size = 12358, upload-time = "2025-11-14T09:58:38.131Z" }, -] - -[[package]] -name = "pyobjc-framework-photosui" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/40/a5/14c538828ed1a420e047388aedc4a2d7d9292030d81bf6b1ced2ec27b6e9/pyobjc_framework_photosui-12.1.tar.gz", hash = "sha256:9141234bb9d17687f1e8b66303158eccdd45132341fbe5e892174910035f029a", size = 29886, upload-time = "2025-11-14T10:18:50.238Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/16/a2/b5afca8039b1a659a2a979bb1bdbdddfdf9b1d2724a2cc4633dca2573d5f/pyobjc_framework_photosui-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:713e3bb25feb5ea891e67260c2c0769cab44a7f11b252023bfcf9f8c29dd1206", size = 11714, upload-time = "2025-11-14T09:58:53.674Z" }, -] - -[[package]] -name = "pyobjc-framework-preferencepanes" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/90/bc/e87df041d4f7f6b7721bf7996fa02aa0255939fb0fac0ecb294229765f92/pyobjc_framework_preferencepanes-12.1.tar.gz", hash = "sha256:b2a02f9049f136bdeca7642b3307637b190850e5853b74b5c372bc7d88ef9744", size = 24543, upload-time = "2025-11-14T10:18:53.259Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/36/7b/8ceec1ab0446224d685e243e2770c5a5c92285bcab0b9324dbe7a893ae5a/pyobjc_framework_preferencepanes-12.1-py2.py3-none-any.whl", hash = "sha256:1b3af9db9e0cfed8db28c260b2cf9a22c15fda5f0ff4c26157b17f99a0e29bbf", size = 4797, upload-time = "2025-11-14T09:59:03.998Z" }, -] - -[[package]] -name = "pyobjc-framework-pushkit" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a9/45/de756b62709add6d0615f86e48291ee2bee40223e7dde7bbe68a952593f0/pyobjc_framework_pushkit-12.1.tar.gz", hash = "sha256:829a2fc8f4780e75fc2a41217290ee0ff92d4ade43c42def4d7e5af436d8ae82", size = 19465, upload-time = "2025-11-14T10:18:57.727Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b9/01/74cf1dd0764c590de05dc1e87d168031e424f834721940b7bb02c67fe821/pyobjc_framework_pushkit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7bdf472a55ac65154e03f54ae0bcad64c4cf45e9b1acba62f15107f2bc994d69", size = 8177, upload-time = "2025-11-14T09:59:11.155Z" }, -] - -[[package]] -name = "pyobjc-framework-quartz" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/94/18/cc59f3d4355c9456fc945eae7fe8797003c4da99212dd531ad1b0de8a0c6/pyobjc_framework_quartz-12.1.tar.gz", hash = "sha256:27f782f3513ac88ec9b6c82d9767eef95a5cf4175ce88a1e5a65875fee799608", size = 3159099, upload-time = "2025-11-14T10:21:24.31Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/9b/780f057e5962f690f23fdff1083a4cfda5a96d5b4d3bb49505cac4f624f2/pyobjc_framework_quartz-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7730cdce46c7e985535b5a42c31381af4aa6556e5642dc55b5e6597595e57a16", size = 218798, upload-time = "2025-11-14T10:00:01.236Z" }, -] - -[[package]] -name = "pyobjc-framework-quicklookthumbnailing" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-quartz" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/97/1a/b90539500e9a27c2049c388d85a824fc0704009b11e33b05009f52a6dc67/pyobjc_framework_quicklookthumbnailing-12.1.tar.gz", hash = "sha256:4f7e09e873e9bda236dce6e2f238cab571baeb75eca2e0bc0961d5fcd85f3c8f", size = 14790, upload-time = "2025-11-14T10:21:26.442Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/22/7bd07b5b44bf8540514a9f24bc46da68812c1fd6c63bb2d3496e5ea44bf0/pyobjc_framework_quicklookthumbnailing-12.1-py2.py3-none-any.whl", hash = "sha256:5efe50b0318188b3a4147681788b47fce64709f6fe0e1b5d020e408ef40ab08e", size = 4234, upload-time = "2025-11-14T10:01:02.209Z" }, -] - -[[package]] -name = "pyobjc-framework-replaykit" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/35/f8/b92af879734d91c1726227e7a03b9e68ab8d9d2bb1716d1a5c29254087f2/pyobjc_framework_replaykit-12.1.tar.gz", hash = "sha256:95801fd35c329d7302b2541f2754e6574bf36547ab869fbbf41e408dfa07268a", size = 23312, upload-time = "2025-11-14T10:21:29.18Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/fc/c68d2111b2655148d88574959d3d8b21d3a003573013301d4d2a7254c1af/pyobjc_framework_replaykit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b0528c2a6188440fdc2017f0924c0a0f15d0a2f6aa295f1d1c2d6b3894c22f1d", size = 10120, upload-time = "2025-11-14T10:01:08.397Z" }, -] - -[[package]] -name = "pyobjc-framework-safariservices" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3e/4b/8f896bafbdbfa180a5ba1e21a6f5dc63150c09cba69d85f68708e02866ae/pyobjc_framework_safariservices-12.1.tar.gz", hash = "sha256:6a56f71c1e692bca1f48fe7c40e4c5a41e148b4e3c6cfb185fd80a4d4a951897", size = 25165, upload-time = "2025-11-14T10:21:32.041Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/67/3a/8c525562fd782c88bc44e8c07fc2c073919f98dead08fffd50f280ef1afa/pyobjc_framework_safariservices-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b475abc82504fc1c0801096a639562d6a6d37370193e8e4a406de9199a7cea13", size = 7281, upload-time = "2025-11-14T10:01:21.238Z" }, -] - -[[package]] -name = "pyobjc-framework-safetykit" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-quartz" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f4/bf/ad6bf60ceb61614c9c9f5758190971e9b90c45b1c7a244e45db64138b6c2/pyobjc_framework_safetykit-12.1.tar.gz", hash = "sha256:0cd4850659fb9b5632fd8ad21f2de6863e8303ff0d51c5cc9c0034aac5db08d8", size = 20086, upload-time = "2025-11-14T10:21:34.212Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/0c/08a20fb7516405186c0fe7299530edd4aa22c24f73290198312447f26c8c/pyobjc_framework_safetykit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e4977f7069a23252053d1a42b1a053aefc19b85c960a5214b05daf3c037a6f16", size = 8550, upload-time = "2025-11-14T10:01:32.885Z" }, -] - -[[package]] -name = "pyobjc-framework-scenekit" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-quartz" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/94/8c/1f4005cf0cb68f84dd98b93bbc0974ee7851bb33d976791c85e042dc2278/pyobjc_framework_scenekit-12.1.tar.gz", hash = "sha256:1bd5b866f31fd829f26feac52e807ed942254fd248115c7c742cfad41d949426", size = 101212, upload-time = "2025-11-14T10:21:41.265Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/f1/4986bd96e0ba0f60bff482a6b135b9d6db65d56578d535751f18f88190f0/pyobjc_framework_scenekit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:40aea10098893f0b06191f1e79d7b25e12e36a9265549d324238bdb25c7e6df0", size = 33597, upload-time = "2025-11-14T10:01:51.297Z" }, -] - -[[package]] -name = "pyobjc-framework-screencapturekit" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-coremedia" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/2d/7f/73458db1361d2cb408f43821a1e3819318a0f81885f833d78d93bdc698e0/pyobjc_framework_screencapturekit-12.1.tar.gz", hash = "sha256:50992c6128b35ab45d9e336f0993ddd112f58b8c8c8f0892a9cb42d61bd1f4c9", size = 32573, upload-time = "2025-11-14T10:21:44.497Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/05/a8/533acdbf26e0a908ff640d3a445481f3c948682ca887be6711b5fcf82682/pyobjc_framework_screencapturekit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:27df138ce2dfa9d4aae5106d4877e9ed694b5a174643c058f1c48678ffc7001a", size = 11504, upload-time = "2025-11-14T10:02:11.36Z" }, -] - -[[package]] -name = "pyobjc-framework-screensaver" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/7d/99/7cfbce880cea61253a44eed594dce66c2b2fbf29e37eaedcd40cffa949e9/pyobjc_framework_screensaver-12.1.tar.gz", hash = "sha256:c4ca111317c5a3883b7eace0a9e7dd72bc6ffaa2ca954bdec918c3ab7c65c96f", size = 22229, upload-time = "2025-11-14T10:21:47.299Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/a4/2481711f2e9557b90bac74fa8bf821162cf7b65835732ae560fd52e9037e/pyobjc_framework_screensaver-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a3c90c2299eac6d01add81427ae2f90d7724f15d676261e838d7a7750f812322", size = 8422, upload-time = "2025-11-14T10:02:24.49Z" }, -] - -[[package]] -name = "pyobjc-framework-screentime" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/10/11/ba18f905321895715dac3cae2071c2789745ae13605b283b8114b41e0459/pyobjc_framework_screentime-12.1.tar.gz", hash = "sha256:583de46b365543bbbcf27cd70eedd375d397441d64a2cf43c65286fd9c91af55", size = 13413, upload-time = "2025-11-14T10:21:49.17Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/27/06/904174de6170e11b53673cc5844e5f13394eeeed486e0bcdf5288c1b0853/pyobjc_framework_screentime-12.1-py2.py3-none-any.whl", hash = "sha256:d34a068ec8ba2704987fcd05c37c9a9392de61d92933e6e71c8e4eaa4dfce029", size = 3963, upload-time = "2025-11-14T10:02:32.577Z" }, -] - -[[package]] -name = "pyobjc-framework-scriptingbridge" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0c/cb/adc0a09e8c4755c2281bd12803a87f36e0832a8fc853a2d663433dbb72ce/pyobjc_framework_scriptingbridge-12.1.tar.gz", hash = "sha256:0e90f866a7e6a8aeaf723d04c826657dd528c8c1b91e7a605f8bb947c74ad082", size = 20339, upload-time = "2025-11-14T10:21:51.769Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/51/46/e0b07d2b3ff9effb8b1179a6cc681a953d3dfbf0eb8b1d6a0e54cef2e922/pyobjc_framework_scriptingbridge-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8083cd68c559c55a3787b2e74fc983c8665e5078571475aaeabf4f34add36b62", size = 8356, upload-time = "2025-11-14T10:02:38.559Z" }, -] - -[[package]] -name = "pyobjc-framework-searchkit" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-coreservices" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6e/60/a38523198430e14fdef21ebe62a93c43aedd08f1f3a07ea3d96d9997db5d/pyobjc_framework_searchkit-12.1.tar.gz", hash = "sha256:ddd94131dabbbc2d7c3f17db3da87c1a712c431310eef16f07187771e7e85226", size = 30942, upload-time = "2025-11-14T10:21:55.483Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/72/46/4f9cd3011f47b43b21b2924ab3770303c3f0a4d16f05550d38c5fcb42e78/pyobjc_framework_searchkit-12.1-py2.py3-none-any.whl", hash = "sha256:844ce62b7296b19da8db7dedd539d07f7b3fb3bb8b029c261f7bcf0e01a97758", size = 3733, upload-time = "2025-11-14T10:02:47.026Z" }, -] - -[[package]] -name = "pyobjc-framework-security" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/80/aa/796e09a3e3d5cee32ebeebb7dcf421b48ea86e28c387924608a05e3f668b/pyobjc_framework_security-12.1.tar.gz", hash = "sha256:7fecb982bd2f7c4354513faf90ba4c53c190b7e88167984c2d0da99741de6da9", size = 168044, upload-time = "2025-11-14T10:22:06.334Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/76/66/5160c0f938fc0515fe8d9af146aac1b093f7ef285ce797fedae161b6c0e8/pyobjc_framework_security-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab42e55f5b782332be5442750fcd9637ee33247d57c7b1d5801bc0e24ee13278", size = 41280, upload-time = "2025-11-14T10:02:58.097Z" }, -] - -[[package]] -name = "pyobjc-framework-securityfoundation" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-security" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/57/d5/c2b77e83c1585ba43e5f00c917273ba4bf7ed548c1b691f6766eb0418d52/pyobjc_framework_securityfoundation-12.1.tar.gz", hash = "sha256:1f39f4b3db6e3bd3a420aaf4923228b88e48c90692cf3612b0f6f1573302a75d", size = 12669, upload-time = "2025-11-14T10:22:09.256Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/93/1e/349fb71a413b37b1b41e712c7ca180df82144478f8a9a59497d66d0f2ea2/pyobjc_framework_securityfoundation-12.1-py2.py3-none-any.whl", hash = "sha256:579cf23e63434226f78ffe0afb8426e971009588e4ad812c478d47dfd558201c", size = 3792, upload-time = "2025-11-14T10:03:14.459Z" }, -] - -[[package]] -name = "pyobjc-framework-securityinterface" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-security" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f9/64/bf5b5d82655112a2314422ee649f1e1e73d4381afa87e1651ce7e8444694/pyobjc_framework_securityinterface-12.1.tar.gz", hash = "sha256:deef11ad03be8d9ff77db6e7ac40f6b641ee2d72eaafcf91040537942472e88b", size = 25552, upload-time = "2025-11-14T10:22:12.098Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/59/3e/17889a6de03dc813606bb97887dc2c4c2d4e7c8f266bc439548bae756e90/pyobjc_framework_securityinterface-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:5cb5e79a73ea17663ebd29e350401162d93e42343da7d96c77efb38ae64ff01f", size = 10783, upload-time = "2025-11-14T10:03:20.202Z" }, -] - -[[package]] -name = "pyobjc-framework-securityui" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-security" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/83/3f/d870305f5dec58cd02966ca06ac29b69fb045d8b46dfb64e2da31f295345/pyobjc_framework_securityui-12.1.tar.gz", hash = "sha256:f1435fed85edc57533c334a4efc8032170424b759da184cb7a7a950ceea0e0b6", size = 12184, upload-time = "2025-11-14T10:22:14.323Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/36/7f/eff9ffdd34511cc95a60e5bd62f1cfbcbcec1a5012ef1168161506628c87/pyobjc_framework_securityui-12.1-py2.py3-none-any.whl", hash = "sha256:3e988b83c9a2bb0393207eaa030fc023a8708a975ac5b8ea0508cdafc2b60705", size = 3594, upload-time = "2025-11-14T10:03:29.628Z" }, -] - -[[package]] -name = "pyobjc-framework-sensitivecontentanalysis" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-quartz" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e7/ce/17bf31753e14cb4d64fffaaba2377453c4977c2c5d3cf2ff0a3db30026c7/pyobjc_framework_sensitivecontentanalysis-12.1.tar.gz", hash = "sha256:2c615ac10e93eb547b32b214cd45092056bee0e79696426fd09978dc3e670f25", size = 13745, upload-time = "2025-11-14T10:22:16.447Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/95/23/c99568a0d4e38bd8337d52e4ae25a0b0bd540577f2e06f3430c951d73209/pyobjc_framework_sensitivecontentanalysis-12.1-py2.py3-none-any.whl", hash = "sha256:faf19d32d4599ac2b18fb1ccdc3e33b2b242bdf34c02e69978bd62d3643ad068", size = 4230, upload-time = "2025-11-14T10:03:31.26Z" }, -] - -[[package]] -name = "pyobjc-framework-servicemanagement" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/31/d0/b26c83ae96ab55013df5fedf89337d4d62311b56ce3f520fc7597d223d82/pyobjc_framework_servicemanagement-12.1.tar.gz", hash = "sha256:08120981749a698033a1d7a6ab99dbbe412c5c0d40f2b4154014b52113511c1d", size = 14585, upload-time = "2025-11-14T10:22:18.735Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ee/5d/1009c32189f9cb26da0124b4a60640ed26dd8ad453810594f0cbfab0ff70/pyobjc_framework_servicemanagement-12.1-py2.py3-none-any.whl", hash = "sha256:9a2941f16eeb71e55e1cd94f50197f91520778c7f48ad896761f5e78725cc08f", size = 5357, upload-time = "2025-11-14T10:03:32.928Z" }, -] - -[[package]] -name = "pyobjc-framework-sharedwithyou" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-sharedwithyoucore" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0f/8b/8ab209a143c11575a857e2111acc5427fb4986b84708b21324cbcbf5591b/pyobjc_framework_sharedwithyou-12.1.tar.gz", hash = "sha256:167d84794a48f408ee51f885210c616fda1ec4bff3dd8617a4b5547f61b05caf", size = 24791, upload-time = "2025-11-14T10:22:21.248Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/ee/e5113ce985a480d13a0fa3d41a242c8068dc09b3c13210557cf5cc6a544a/pyobjc_framework_sharedwithyou-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a99a6ebc6b6de7bc8663b1f07332fab9560b984a57ce344dc5703f25258f258d", size = 8763, upload-time = "2025-11-14T10:03:38.467Z" }, -] - -[[package]] -name = "pyobjc-framework-sharedwithyoucore" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/55/ef/84059c5774fd5435551ab7ab40b51271cfb9997b0d21f491c6b429fe57a8/pyobjc_framework_sharedwithyoucore-12.1.tar.gz", hash = "sha256:0813149eeb755d718b146ec9365eb4ca3262b6af9ff9ba7db2f7b6f4fd104518", size = 22350, upload-time = "2025-11-14T10:22:23.611Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/dd/0e/0c2b0591ebc72d437dccca7a1e7164c5f11dde2189d4f4c707a132bab740/pyobjc_framework_sharedwithyoucore-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ed928266ae9d577ff73de72a03bebc66a751918eb59ca660a9eca157392f17be", size = 8530, upload-time = "2025-11-14T10:03:50.839Z" }, -] - -[[package]] -name = "pyobjc-framework-shazamkit" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ed/2c/8d82c5066cc376de68ad8c1454b7c722c7a62215e5c2f9dac5b33a6c3d42/pyobjc_framework_shazamkit-12.1.tar.gz", hash = "sha256:71db2addd016874639a224ed32b2000b858802b0370c595a283cce27f76883fe", size = 22518, upload-time = "2025-11-14T10:22:25.996Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/5e/7d60d8e7b036b20d0e94cd7c4563e7414653344482e85fbc7facffabc95f/pyobjc_framework_shazamkit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e184dd0f61a604b1cfcf44418eb95b943e7b8f536058a29e4b81acadd27a9420", size = 8577, upload-time = "2025-11-14T10:04:04.182Z" }, -] - -[[package]] -name = "pyobjc-framework-social" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/31/21/afc6f37dfdd2cafcba0227e15240b5b0f1f4ad57621aeefda2985ac9560e/pyobjc_framework_social-12.1.tar.gz", hash = "sha256:1963db6939e92ae40dd9d68852e8f88111cbfd37a83a9fdbc9a0c08993ca7e60", size = 13184, upload-time = "2025-11-14T10:22:28.048Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f6/fb/090867e332d49a1e492e4b8972ac6034d1c7d17cf39f546077f35be58c46/pyobjc_framework_social-12.1-py2.py3-none-any.whl", hash = "sha256:2f3b36ba5769503b1bc945f85fd7b255d42d7f6e417d78567507816502ff2b44", size = 4462, upload-time = "2025-11-14T10:04:14.578Z" }, -] - -[[package]] -name = "pyobjc-framework-soundanalysis" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6b/d6/5039b61edc310083425f87ce2363304d3a87617e941c1d07968c63b5638d/pyobjc_framework_soundanalysis-12.1.tar.gz", hash = "sha256:e2deead8b9a1c4513dbdcf703b21650dcb234b60a32d08afcec4895582b040b1", size = 14804, upload-time = "2025-11-14T10:22:29.998Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/53/d3/8df5183d52d20d459225d3f5d24f55e01b8cd9fe587ed972e3f20dd18709/pyobjc_framework_soundanalysis-12.1-py2.py3-none-any.whl", hash = "sha256:8b2029ab48c1a9772f247f0aea995e8c3ff4706909002a9c1551722769343a52", size = 4188, upload-time = "2025-11-14T10:04:16.12Z" }, -] - -[[package]] -name = "pyobjc-framework-speech" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8d/3d/194cf19fe7a56c2be5dfc28f42b3b597a62ebb1e1f52a7dd9c55b917ac6c/pyobjc_framework_speech-12.1.tar.gz", hash = "sha256:2a2a546ba6c52d5dd35ddcfee3fd9226a428043d1719597e8701851a6566afdd", size = 25218, upload-time = "2025-11-14T10:22:32.505Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/1b/224cb98c9c32a6d5e68072f89d26444095be54c6f461efe4fefe9d1330a5/pyobjc_framework_speech-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:cae4b88ef9563157a6c9e66b37778fc4022ee44dd1a2a53081c2adbb69698945", size = 9254, upload-time = "2025-11-14T10:04:21.361Z" }, -] - -[[package]] -name = "pyobjc-framework-spritekit" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-quartz" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b6/78/d683ebe0afb49f46d2d21d38c870646e7cb3c2e83251f264e79d357b1b74/pyobjc_framework_spritekit-12.1.tar.gz", hash = "sha256:a851f4ef5aa65cc9e08008644a528e83cb31021a1c0f17ebfce4de343764d403", size = 64470, upload-time = "2025-11-14T10:22:37.569Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/38/97c3b6c3437e3e9267fb4e1cd86e0da4eff07e0abe7cd6923644d2dfc878/pyobjc_framework_spritekit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1649e57c25145795d04bb6a1ec44c20ef7cf0af7c60a9f6f5bc7998dd269db1e", size = 17802, upload-time = "2025-11-14T10:04:35.346Z" }, -] - -[[package]] -name = "pyobjc-framework-storekit" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/00/87/8a66a145feb026819775d44975c71c1c64df4e5e9ea20338f01456a61208/pyobjc_framework_storekit-12.1.tar.gz", hash = "sha256:818452e67e937a10b5c8451758274faa44ad5d4329df0fa85735115fb0608da9", size = 34574, upload-time = "2025-11-14T10:22:40.73Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/9f/938985e506de0cc3a543e44e1f9990e9e2fb8980b8f3bcfc8f7921d09061/pyobjc_framework_storekit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9fe2d65a2b644bb6b4fdd3002292cba153560917de3dd6cf969431fa32d21dd0", size = 12819, upload-time = "2025-11-14T10:04:50.945Z" }, -] - -[[package]] -name = "pyobjc-framework-symbols" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9a/ce/a48819eb8524fa2dc11fb3dd40bb9c4dcad0596fe538f5004923396c2c6c/pyobjc_framework_symbols-12.1.tar.gz", hash = "sha256:7d8e999b8a59c97d38d1d343b6253b1b7d04bf50b665700957d89c8ac43b9110", size = 12782, upload-time = "2025-11-14T10:22:42.609Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f0/ea/6e9af9c750d68109ac54fbffb5463e33a7b54ffe8b9901a5b6b603b7884b/pyobjc_framework_symbols-12.1-py2.py3-none-any.whl", hash = "sha256:c72eecbc25f6bfcd39c733067276270057c5aca684be20fdc56def645f2b6446", size = 3331, upload-time = "2025-11-14T10:05:01.333Z" }, -] - -[[package]] -name = "pyobjc-framework-syncservices" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-coredata" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/21/91/6d03a988831ddb0fb001b13573560e9a5bcccde575b99350f98fe56a2dd4/pyobjc_framework_syncservices-12.1.tar.gz", hash = "sha256:6a213e93d9ce15128810987e4c5de8c73cfab1564ac8d273e6b437a49965e976", size = 31032, upload-time = "2025-11-14T10:22:45.902Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/54/ac/a83cdd120e279ee905e9085afda90992159ed30c6a728b2c56fa2d36b6ea/pyobjc_framework_syncservices-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0cd629bea95692aad2d26196657cde2fbadedae252c7846964228661a600b900", size = 13411, upload-time = "2025-11-14T10:05:07.741Z" }, -] - -[[package]] -name = "pyobjc-framework-systemconfiguration" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/90/7d/50848df8e1c6b5e13967dee9fb91d3391fe1f2399d2d0797d2fc5edb32ba/pyobjc_framework_systemconfiguration-12.1.tar.gz", hash = "sha256:90fe04aa059876a21626931c71eaff742a27c79798a46347fd053d7008ec496e", size = 59158, upload-time = "2025-11-14T10:22:53.056Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d3/d3/bb935c3d4bae9e6ce4a52638e30eea7039c480dd96bc4f0777c9fabda21b/pyobjc_framework_systemconfiguration-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0e5bb9103d39483964431db7125195c59001b7bff2961869cfe157b4c861e52d", size = 21578, upload-time = "2025-11-14T10:05:25.572Z" }, -] - -[[package]] -name = "pyobjc-framework-systemextensions" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/12/01/8a706cd3f7dfcb9a5017831f2e6f9e5538298e90052db3bb8163230cbc4f/pyobjc_framework_systemextensions-12.1.tar.gz", hash = "sha256:243e043e2daee4b5c46cd90af5fff46b34596aac25011bab8ba8a37099685eeb", size = 20701, upload-time = "2025-11-14T10:22:58.257Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0a/cc/a42883d6ad0ae257a7fa62660b4dd13be15f8fa657922f9a5b6697f26e28/pyobjc_framework_systemextensions-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:01fac4f8d88c0956d9fc714d24811cd070e67200ba811904317d91e849e38233", size = 9166, upload-time = "2025-11-14T10:05:41.479Z" }, -] - -[[package]] -name = "pyobjc-framework-threadnetwork" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/62/7e/f1816c3461e4121186f2f7750c58af083d1826bbd73f72728da3edcf4915/pyobjc_framework_threadnetwork-12.1.tar.gz", hash = "sha256:e071eedb41bfc1b205111deb54783ec5a035ccd6929e6e0076336107fdd046ee", size = 12788, upload-time = "2025-11-14T10:23:00.329Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4f/b8/94b37dd353302c051a76f1a698cf55b5ad50ca061db7f0f332aa9e195766/pyobjc_framework_threadnetwork-12.1-py2.py3-none-any.whl", hash = "sha256:07d937748fc54199f5ec04d5a408e8691a870481c11b641785c2adc279dd8e4b", size = 3771, upload-time = "2025-11-14T10:05:49.899Z" }, -] - -[[package]] -name = "pyobjc-framework-uniformtypeidentifiers" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/65/b8/dd9d2a94509a6c16d965a7b0155e78edf520056313a80f0cd352413f0d0b/pyobjc_framework_uniformtypeidentifiers-12.1.tar.gz", hash = "sha256:64510a6df78336579e9c39b873cfcd03371c4b4be2cec8af75a8a3d07dff607d", size = 17030, upload-time = "2025-11-14T10:23:02.222Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4e/5f/1f10f5275b06d213c9897850f1fca9c881c741c1f9190cea6db982b71824/pyobjc_framework_uniformtypeidentifiers-12.1-py2.py3-none-any.whl", hash = "sha256:ec5411e39152304d2a7e0e426c3058fa37a00860af64e164794e0bcffee813f2", size = 4901, upload-time = "2025-11-14T10:05:51.532Z" }, -] - -[[package]] -name = "pyobjc-framework-usernotifications" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/90/cd/e0253072f221fa89a42fe53f1a2650cc9bf415eb94ae455235bd010ee12e/pyobjc_framework_usernotifications-12.1.tar.gz", hash = "sha256:019ccdf2d400f9a428769df7dba4ea97c02453372bc5f8b75ce7ae54dfe130f9", size = 29749, upload-time = "2025-11-14T10:23:05.364Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/61/ad/c95053a475246464cba686e16269b0973821601910d1947d088b855a8dac/pyobjc_framework_usernotifications-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:412afb2bf5fe0049f9c4e732e81a8a35d5ebf97c30a5a6abd276259d020c82ac", size = 9644, upload-time = "2025-11-14T10:05:56.801Z" }, -] - -[[package]] -name = "pyobjc-framework-usernotificationsui" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-usernotifications" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0e/03/73e29fd5e5973cb3800c9d56107c1062547ef7524cbcc757c3cbbd5465c6/pyobjc_framework_usernotificationsui-12.1.tar.gz", hash = "sha256:51381c97c7344099377870e49ed0871fea85ba50efe50ab05ccffc06b43ec02e", size = 13125, upload-time = "2025-11-14T10:23:07.259Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/23/c8/52ac8a879079c1fbf25de8335ff506f7db87ff61e64838b20426f817f5d5/pyobjc_framework_usernotificationsui-12.1-py2.py3-none-any.whl", hash = "sha256:11af59dc5abfcb72c08769ab4d7ca32a628527a8ba341786431a0d2dacf31605", size = 3933, upload-time = "2025-11-14T10:06:05.478Z" }, -] - -[[package]] -name = "pyobjc-framework-videosubscriberaccount" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/10/f8/27927a9c125c622656ee5aada4596ccb8e5679da0260742360f193df6dcf/pyobjc_framework_videosubscriberaccount-12.1.tar.gz", hash = "sha256:750459fa88220ab83416f769f2d5d210a1f77b8938fa4d119aad0002fc32846b", size = 18793, upload-time = "2025-11-14T10:23:09.33Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/41/ca/e2f982916267508c1594f1e50d27bf223a24f55a5e175ab7d7822a00997c/pyobjc_framework_videosubscriberaccount-12.1-py2.py3-none-any.whl", hash = "sha256:381a5e8a3016676e52b88e38b706559fa09391d33474d8a8a52f20a883104a7b", size = 4825, upload-time = "2025-11-14T10:06:07.027Z" }, -] - -[[package]] -name = "pyobjc-framework-videotoolbox" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-coremedia" }, - { name = "pyobjc-framework-quartz" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b3/5f/6995ee40dc0d1a3460ee183f696e5254c0ad14a25b5bc5fd9bd7266c077b/pyobjc_framework_videotoolbox-12.1.tar.gz", hash = "sha256:7adc8670f3b94b086aed6e86c3199b388892edab4f02933c2e2d9b1657561bef", size = 57825, upload-time = "2025-11-14T10:23:13.825Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/94/a5/91c6c95416f41c412c2079950527cb746c0712ec319c51a6c728c8d6b231/pyobjc_framework_videotoolbox-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eb6ce6837344ee319122066c16ada4beb913e7bfd62188a8d14b1ecbb5a89234", size = 18908, upload-time = "2025-11-14T10:06:14.087Z" }, -] - -[[package]] -name = "pyobjc-framework-virtualization" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3b/6a/9d110b5521d9b898fad10928818c9f55d66a4af9ac097426c65a9878b095/pyobjc_framework_virtualization-12.1.tar.gz", hash = "sha256:e96afd8e801e92c6863da0921e40a3b68f724804f888bce43791330658abdb0f", size = 40682, upload-time = "2025-11-14T10:23:17.456Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/f2/0da47e91f3f8eeda9a8b4bb0d3a0c54a18925009e99b66a8226b9e06ce1e/pyobjc_framework_virtualization-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7d5724b38e64b39ab5ec3b45993afa29fc88b307d99ee2c7a1c0fd770e9b4b21", size = 13131, upload-time = "2025-11-14T10:06:29.337Z" }, -] - -[[package]] -name = "pyobjc-framework-vision" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-coreml" }, - { name = "pyobjc-framework-quartz" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c2/5a/08bb3e278f870443d226c141af14205ff41c0274da1e053b72b11dfc9fb2/pyobjc_framework_vision-12.1.tar.gz", hash = "sha256:a30959100e85dcede3a786c544e621ad6eb65ff6abf85721f805822b8c5fe9b0", size = 59538, upload-time = "2025-11-14T10:23:21.979Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/5a/23502935b3fc877d7573e743fc3e6c28748f33a45c43851d503bde52cde7/pyobjc_framework_vision-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:6b3211d84f3a12aad0cde752cfd43a80d0218960ac9e6b46b141c730e7d655bd", size = 16625, upload-time = "2025-11-14T10:06:44.422Z" }, -] - -[[package]] -name = "pyobjc-framework-webkit" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/14/10/110a50e8e6670765d25190ca7f7bfeecc47ec4a8c018cb928f4f82c56e04/pyobjc_framework_webkit-12.1.tar.gz", hash = "sha256:97a54dd05ab5266bd4f614e41add517ae62cdd5a30328eabb06792474b37d82a", size = 284531, upload-time = "2025-11-14T10:23:40.287Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/db/67/64920c8d201a7fc27962f467c636c4e763b43845baba2e091a50a97a5d52/pyobjc_framework_webkit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:af2c7197447638b92aafbe4847c063b6dd5e1ed83b44d3ce7e71e4c9b042ab5a", size = 50084, upload-time = "2025-11-14T10:07:05.868Z" }, -] - [[package]] name = "pyopenssl" version = "24.2.1" @@ -3626,27 +1238,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, ] -[[package]] -name = "pyperclip" -version = "1.11.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e8/52/d87eba7cb129b81563019d1679026e7a112ef76855d6159d24754dbd2a51/pyperclip-1.11.0.tar.gz", hash = "sha256:244035963e4428530d9e3a6101a1ef97209c6825edab1567beac148ccc1db1b6", size = 12185, upload-time = "2025-09-26T14:40:37.245Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/df/80/fc9d01d5ed37ba4c42ca2b55b4339ae6e200b456be3a1aaddf4a9fa99b8c/pyperclip-1.11.0-py3-none-any.whl", hash = "sha256:299403e9ff44581cb9ba2ffeed69c7aa96a008622ad0c46cb575ca75b5b84273", size = 11063, upload-time = "2025-09-26T14:40:36.069Z" }, -] - -[[package]] -name = "pyrect" -version = "0.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cb/04/2ba023d5f771b645f7be0c281cdacdcd939fe13d1deb331fc5ed1a6b3a98/PyRect-0.2.0.tar.gz", hash = "sha256:f65155f6df9b929b67caffbd57c0947c5ae5449d3b580d178074bffb47a09b78", size = 17219, upload-time = "2022-03-16T04:45:52.36Z" } - -[[package]] -name = "pyscreeze" -version = "1.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/f0/cb456ac4f1a73723d5b866933b7986f02bacea27516629c00f8e7da94c2d/pyscreeze-1.0.1.tar.gz", hash = "sha256:cf1662710f1b46aa5ff229ee23f367da9e20af4a78e6e365bee973cad0ead4be", size = 27826, upload-time = "2024-08-20T23:03:07.291Z" } - [[package]] name = "pyserial" version = "3.5" @@ -3755,72 +1346,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, ] -[[package]] -name = "python-xlib" -version = "0.33" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/86/f5/8c0653e5bb54e0cbdfe27bf32d41f27bc4e12faa8742778c17f2a71be2c0/python-xlib-0.33.tar.gz", hash = "sha256:55af7906a2c75ce6cb280a584776080602444f75815a7aff4d287bb2d7018b32", size = 269068, upload-time = "2022-12-25T18:53:00.824Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fc/b8/ff33610932e0ee81ae7f1269c890f697d56ff74b9f5b2ee5d9b7fa2c5355/python_xlib-0.33-py2.py3-none-any.whl", hash = "sha256:c3534038d42e0df2f1392a1b30a15a4ff5fdc2b86cfa94f072bf11b10a164398", size = 182185, upload-time = "2022-12-25T18:52:58.662Z" }, -] - -[[package]] -name = "python3-xlib" -version = "0.15" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ef/c6/2c5999de3bb1533521f1101e8fe56fd9c266732f4d48011c7c69b29d12ae/python3-xlib-0.15.tar.gz", hash = "sha256:dc4245f3ae4aa5949c1d112ee4723901ade37a96721ba9645f2bfa56e5b383f8", size = 132828, upload-time = "2014-05-31T12:28:59.603Z" } - -[[package]] -name = "pytweening" -version = "1.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/79/0c/c16bc93ac2755bac0066a8ecbd2a2931a1735a6fffd99a2b9681c7e83e90/pytweening-1.2.0.tar.gz", hash = "sha256:243318b7736698066c5f362ec5c2b6434ecf4297c3c8e7caa8abfe6af4cac71b", size = 171241, upload-time = "2024-02-20T03:37:56.809Z" } - -[[package]] -name = "pywin32" -version = "311" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, - { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, - { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, -] - -[[package]] -name = "pywinbox" -version = "0.7" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "ewmhlib", marker = "sys_platform == 'linux'" }, - { name = "pyobjc", marker = "sys_platform == 'darwin'" }, - { name = "python-xlib", marker = "sys_platform == 'linux'" }, - { name = "pywin32", marker = "sys_platform == 'win32'" }, - { name = "typing-extensions" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/b1/37/d59397221e15d2a7f38afaa4e8e6b8c244d818044f7daa0bdc5988df0a69/PyWinBox-0.7-py3-none-any.whl", hash = "sha256:8b2506a8dd7afa0a910b368762adfac885274132ef9151b0c81b0d2c6ffd6f83", size = 12274, upload-time = "2024-04-17T10:10:31.899Z" }, -] - -[[package]] -name = "pywinctl" -version = "0.4.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "ewmhlib", marker = "sys_platform == 'linux'" }, - { name = "pymonctl" }, - { name = "pyobjc", marker = "sys_platform == 'darwin'" }, - { name = "python-xlib", marker = "sys_platform == 'linux'" }, - { name = "pywin32", marker = "sys_platform == 'win32'" }, - { name = "pywinbox" }, - { name = "typing-extensions" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/be/33/8e4f632210b28fc9e998a9ab990e7ed97ecd2800cc50038e3800e1d85dbe/PyWinCtl-0.4.1-py3-none-any.whl", hash = "sha256:4d875e22969e1c6239d8c73156193630aaab876366167b8d97716f956384b089", size = 63158, upload-time = "2024-09-23T08:33:39.881Z" }, -] - [[package]] name = "pyyaml" version = "6.0.3" @@ -3926,15 +1451,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b8/0c/51f6841f1d84f404f92463fc2b1ba0da357ca1e3db6b7fbda26956c3b82a/ruamel_yaml-0.19.1-py3-none-any.whl", hash = "sha256:27592957fedf6e0b62f281e96effd28043345e0e66001f97683aa9a40c667c93", size = 118102, upload-time = "2026-01-02T16:50:29.201Z" }, ] -[[package]] -name = "rubicon-objc" -version = "0.5.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4f/d2/d39ecd205661a5c14c90dbd92a722a203848a3621785c9783716341de427/rubicon_objc-0.5.3.tar.gz", hash = "sha256:74c25920c5951a05db9d3a1aac31d23816ec7dacc841a5b124d911b99ea71b9a", size = 171512, upload-time = "2025-12-03T03:51:10.264Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/93/ab/e834c01138c272fb2e37d2f3c7cba708bc694dbc7b3f03b743f29ceb92d5/rubicon_objc-0.5.3-py3-none-any.whl", hash = "sha256:31dedcda9be38435f5ec067906e1eea5d0ddb790330e98a22e94ff424758b415", size = 64414, upload-time = "2025-12-03T03:51:09.082Z" }, -] - [[package]] name = "ruff" version = "0.15.1" From 8e13d8abd05a895a8308b4e317efbb9321cff66b Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sun, 15 Feb 2026 20:30:31 -0800 Subject: [PATCH 046/311] CI: build big UI report --- .github/workflows/tests.yaml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 2cca344423..03e818f422 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -255,8 +255,8 @@ jobs: source selfdrive/test/setup_vsound.sh && \ CI=1 pytest -s tools/sim/tests/test_metadrive_bridge.py" - create_mici_raylib_ui_report: - name: Create mici raylib UI Report + create_ui_report: + name: Create UI Report runs-on: ${{ (github.repository == 'commaai/openpilot') && ((github.event_name != 'pull_request') || @@ -270,13 +270,14 @@ jobs: - uses: ./.github/workflows/setup-with-retry - name: Build openpilot run: ${{ env.RUN }} "scons -j$(nproc)" - - name: Create mici raylib UI Report + - name: Create UI Report run: > ${{ env.RUN }} "PYTHONWARNINGS=ignore && source selfdrive/test/setup_xvfb.sh && - WINDOWED=1 python3 selfdrive/ui/tests/diff/replay.py" - - name: Upload Raylib UI Report + WINDOWED=1 python3 selfdrive/ui/tests/diff/replay.py && + WINDOWED=1 python3 selfdrive/ui/tests/diff/replay.py --big" + - name: Upload UI Report uses: actions/upload-artifact@v6 with: - name: mici-raylib-report-${{ inputs.run_number || '1' }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && 'master' || github.event.number }} + name: ui-report-${{ inputs.run_number || '1' }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && 'master' || github.event.number }} path: selfdrive/ui/tests/diff/report From 6704f63a3d74c25437167450ef5938f0071d24b3 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sun, 15 Feb 2026 20:43:56 -0800 Subject: [PATCH 047/311] update ui job name --- .github/workflows/mici_raylib_ui_preview.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/mici_raylib_ui_preview.yaml b/.github/workflows/mici_raylib_ui_preview.yaml index 5025d407cd..e5cfd34bdd 100644 --- a/.github/workflows/mici_raylib_ui_preview.yaml +++ b/.github/workflows/mici_raylib_ui_preview.yaml @@ -14,7 +14,7 @@ on: workflow_dispatch: env: - UI_JOB_NAME: "Create mici raylib UI Report" + UI_JOB_NAME: "Create UI Report" REPORT_NAME: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && 'master' || github.event.number }} SHA: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && github.sha || github.event.pull_request.head.sha }} BRANCH_NAME: "openpilot/pr-${{ github.event.number }}-mici-raylib-ui" @@ -58,7 +58,7 @@ jobs: github_token: ${{ secrets.GITHUB_TOKEN }} run_id: ${{ steps.get_run_id.outputs.run_id }} search_artifacts: true - name: mici-raylib-report-1-${{ env.REPORT_NAME }} + name: ui-report-1-${{ env.REPORT_NAME }} path: ${{ github.workspace }}/pr_ui - name: Getting master ui # filename: master_ui_raylib/mici_ui_replay.mp4 From a5f9c2fc23b4e9e8fe4aea3884fada99b1dc1b41 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sun, 15 Feb 2026 21:02:41 -0800 Subject: [PATCH 048/311] unified ui preview for mici and tizi (#37226) * unified ui preview for mici and tizi * lil more * variants * run it * trigger --- .github/workflows/mici_raylib_ui_preview.yaml | 151 --------------- .github/workflows/ui_preview.yaml | 176 ++++++++++++++++++ 2 files changed, 176 insertions(+), 151 deletions(-) delete mode 100644 .github/workflows/mici_raylib_ui_preview.yaml create mode 100644 .github/workflows/ui_preview.yaml diff --git a/.github/workflows/mici_raylib_ui_preview.yaml b/.github/workflows/mici_raylib_ui_preview.yaml deleted file mode 100644 index e5cfd34bdd..0000000000 --- a/.github/workflows/mici_raylib_ui_preview.yaml +++ /dev/null @@ -1,151 +0,0 @@ -name: "mici raylib ui preview" -on: - push: - branches: - - master - pull_request_target: - types: [assigned, opened, synchronize, reopened, edited] - branches: - - 'master' - paths: - - 'selfdrive/assets/**' - - 'selfdrive/ui/**' - - 'system/ui/**' - workflow_dispatch: - -env: - UI_JOB_NAME: "Create UI Report" - REPORT_NAME: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && 'master' || github.event.number }} - SHA: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && github.sha || github.event.pull_request.head.sha }} - BRANCH_NAME: "openpilot/pr-${{ github.event.number }}-mici-raylib-ui" - MASTER_BRANCH_NAME: "openpilot_master_ui_mici_raylib" - # All report files are pushed here - REPORT_FILES_BRANCH_NAME: "mici-raylib-ui-reports" - -jobs: - preview: - if: github.repository == 'commaai/openpilot' - name: preview - runs-on: ubuntu-latest - timeout-minutes: 20 - permissions: - contents: read - pull-requests: write - actions: read - steps: - - uses: actions/checkout@v6 - with: - submodules: true - - - name: Waiting for ui generation to end - uses: lewagon/wait-on-check-action@v1.3.4 - with: - ref: ${{ env.SHA }} - check-name: ${{ env.UI_JOB_NAME }} - repo-token: ${{ secrets.GITHUB_TOKEN }} - allowed-conclusions: success - wait-interval: 20 - - - name: Getting workflow run ID - id: get_run_id - run: | - echo "run_id=$(curl https://api.github.com/repos/${{ github.repository }}/commits/${{ env.SHA }}/check-runs | jq -r '.check_runs[] | select(.name == "${{ env.UI_JOB_NAME }}") | .html_url | capture("(?[0-9]+)") | .number')" >> $GITHUB_OUTPUT - - - name: Getting proposed ui # filename: pr_ui/mici_ui_replay.mp4 - id: download-artifact - uses: dawidd6/action-download-artifact@v6 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - run_id: ${{ steps.get_run_id.outputs.run_id }} - search_artifacts: true - name: ui-report-1-${{ env.REPORT_NAME }} - path: ${{ github.workspace }}/pr_ui - - - name: Getting master ui # filename: master_ui_raylib/mici_ui_replay.mp4 - uses: actions/checkout@v6 - with: - repository: commaai/ci-artifacts - ssh-key: ${{ secrets.CI_ARTIFACTS_DEPLOY_KEY }} - path: ${{ github.workspace }}/master_ui_raylib - ref: ${{ env.MASTER_BRANCH_NAME }} - - - name: Saving new master ui - if: github.ref == 'refs/heads/master' && github.event_name == 'push' - working-directory: ${{ github.workspace }}/master_ui_raylib - run: | - git checkout --orphan=new_master_ui_mici_raylib - git rm -rf * - git branch -D ${{ env.MASTER_BRANCH_NAME }} - git branch -m ${{ env.MASTER_BRANCH_NAME }} - git config user.name "GitHub Actions Bot" - git config user.email "<>" - mv ${{ github.workspace }}/pr_ui/* . - git add . - git commit -m "mici raylib video for commit ${{ env.SHA }}" - git push origin ${{ env.MASTER_BRANCH_NAME }} --force - - - name: Setup FFmpeg - uses: AnimMouse/setup-ffmpeg@ae28d57dabbb148eff63170b6bf7f2b60062cbae - - - name: Finding diff - if: github.event_name == 'pull_request_target' - id: find_diff - run: | - # Find the video file from PR - pr_video="${{ github.workspace }}/pr_ui/mici_ui_replay_proposed.mp4" - mv "${{ github.workspace }}/pr_ui/mici_ui_replay.mp4" "$pr_video" - - master_video="${{ github.workspace }}/pr_ui/mici_ui_replay_master.mp4" - mv "${{ github.workspace }}/master_ui_raylib/mici_ui_replay.mp4" "$master_video" - - # Run report - export PYTHONPATH=${{ github.workspace }} - baseurl="https://github.com/commaai/ci-artifacts/raw/refs/heads/${{ env.BRANCH_NAME }}" - diff_exit_code=0 - python3 ${{ github.workspace }}/selfdrive/ui/tests/diff/diff.py "${{ github.workspace }}/pr_ui/mici_ui_replay_master.mp4" "${{ github.workspace }}/pr_ui/mici_ui_replay_proposed.mp4" "diff.html" --basedir "$baseurl" --no-open || diff_exit_code=$? - - # Copy diff report files - cp ${{ github.workspace }}/selfdrive/ui/tests/diff/report/diff.html ${{ github.workspace }}/pr_ui/ - cp ${{ github.workspace }}/selfdrive/ui/tests/diff/report/diff.mp4 ${{ github.workspace }}/pr_ui/ - - REPORT_URL="https://commaai.github.io/ci-artifacts/diff_pr_${{ github.event.number }}.html" - if [ $diff_exit_code -eq 0 ]; then - DIFF="✅ Videos are identical! [View Diff Report]($REPORT_URL)" - else - DIFF="❌ Videos differ! [View Diff Report]($REPORT_URL)" - fi - echo "DIFF=$DIFF" >> "$GITHUB_OUTPUT" - - - name: Saving proposed ui - if: github.event_name == 'pull_request_target' - working-directory: ${{ github.workspace }}/master_ui_raylib - run: | - # Overwrite PR branch w/ proposed ui, and master ui at this point in time for future reference - git config user.name "GitHub Actions Bot" - git config user.email "<>" - git checkout --orphan=${{ env.BRANCH_NAME }} - git rm -rf * - mv ${{ github.workspace }}/pr_ui/* . - git add . - git commit -m "mici raylib video for PR #${{ github.event.number }}" - git push origin ${{ env.BRANCH_NAME }} --force - - # Append diff report to report files branch - git fetch origin ${{ env.REPORT_FILES_BRANCH_NAME }} - git checkout ${{ env.REPORT_FILES_BRANCH_NAME }} - cp ${{ github.workspace }}/selfdrive/ui/tests/diff/report/diff.html diff_pr_${{ github.event.number }}.html - git add diff_pr_${{ github.event.number }}.html - git commit -m "mici raylib ui diff report for PR #${{ github.event.number }}" || echo "No changes to commit" - git push origin ${{ env.REPORT_FILES_BRANCH_NAME }} - - - name: Comment Video on PR - if: github.event_name == 'pull_request_target' - uses: thollander/actions-comment-pull-request@v2 - with: - message: | - - ## mici raylib UI Preview - ${{ steps.find_diff.outputs.DIFF }} - comment_tag: run_id_video_mici_raylib - pr_number: ${{ github.event.number }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/ui_preview.yaml b/.github/workflows/ui_preview.yaml new file mode 100644 index 0000000000..3663257daf --- /dev/null +++ b/.github/workflows/ui_preview.yaml @@ -0,0 +1,176 @@ +name: "ui preview" +on: + push: + branches: + - master + pull_request: # TODO: change back to pull_request_target before merging + types: [assigned, opened, synchronize, reopened, edited] + branches: + - 'master' + paths: # TODO: remove workflow path before merging + - 'selfdrive/assets/**' + - 'selfdrive/ui/**' + - 'system/ui/**' + - '.github/workflows/ui_preview.yaml' + workflow_dispatch: + +env: + UI_JOB_NAME: "Create UI Report" + REPORT_NAME: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && 'master' || github.event.number }} + SHA: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && github.sha || github.event.pull_request.head.sha }} + BRANCH_NAME: "openpilot/pr-${{ github.event.number }}-ui-preview" + REPORT_FILES_BRANCH_NAME: "mici-raylib-ui-reports" + + # variant:video_prefix:master_branch + VARIANTS: "mici:mici_ui_replay:openpilot_master_ui_mici_raylib big:tizi_ui_replay:openpilot_master_ui_big_raylib" + +jobs: + preview: + if: github.repository == 'commaai/openpilot' + name: preview + runs-on: ubuntu-latest + timeout-minutes: 20 + permissions: + contents: read + pull-requests: write + actions: read + steps: + - uses: actions/checkout@v6 + with: + submodules: true + + - name: Waiting for ui generation to end + uses: lewagon/wait-on-check-action@v1.3.4 + with: + ref: ${{ env.SHA }} + check-name: ${{ env.UI_JOB_NAME }} + repo-token: ${{ secrets.GITHUB_TOKEN }} + allowed-conclusions: success + wait-interval: 20 + + - name: Getting workflow run ID + id: get_run_id + run: | + echo "run_id=$(curl https://api.github.com/repos/${{ github.repository }}/commits/${{ env.SHA }}/check-runs | jq -r '.check_runs[] | select(.name == "${{ env.UI_JOB_NAME }}") | .html_url | capture("(?[0-9]+)") | .number')" >> $GITHUB_OUTPUT + + - name: Getting proposed ui + uses: dawidd6/action-download-artifact@v6 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + run_id: ${{ steps.get_run_id.outputs.run_id }} + search_artifacts: true + name: ui-report-1-${{ env.REPORT_NAME }} + path: ${{ github.workspace }}/pr_ui + + - name: Getting mici master ui + uses: actions/checkout@v6 + with: + repository: commaai/ci-artifacts + ssh-key: ${{ secrets.CI_ARTIFACTS_DEPLOY_KEY }} + path: ${{ github.workspace }}/master_mici + ref: openpilot_master_ui_mici_raylib + + - name: Getting big master ui + uses: actions/checkout@v6 + with: + repository: commaai/ci-artifacts + ssh-key: ${{ secrets.CI_ARTIFACTS_DEPLOY_KEY }} + path: ${{ github.workspace }}/master_big + ref: openpilot_master_ui_big_raylib + + - name: Saving new master ui + if: github.ref == 'refs/heads/master' && github.event_name == 'push' + run: | + for variant in $VARIANTS; do + IFS=':' read -r name video branch <<< "$variant" + master_dir="${{ github.workspace }}/master_${name}" + cd "$master_dir" + git checkout --orphan=new_branch + git rm -rf * + git branch -D "$branch" + git branch -m "$branch" + git config user.name "GitHub Actions Bot" + git config user.email "<>" + cp "${{ github.workspace }}/pr_ui/${video}.mp4" . + git add . + git commit -m "${name} video for commit ${{ env.SHA }}" + git push origin "$branch" --force + done + + - name: Setup FFmpeg + uses: AnimMouse/setup-ffmpeg@ae28d57dabbb148eff63170b6bf7f2b60062cbae + + - name: Finding diffs + if: github.event_name == 'pull_request' + id: find_diff + run: | + export PYTHONPATH=${{ github.workspace }} + baseurl="https://github.com/commaai/ci-artifacts/raw/refs/heads/${{ env.BRANCH_NAME }}" + + COMMENT="" + for variant in $VARIANTS; do + IFS=':' read -r name video _ <<< "$variant" + diff_name="${name}_diff" + + mv "${{ github.workspace }}/pr_ui/${video}.mp4" "${{ github.workspace }}/pr_ui/${video}_proposed.mp4" + cp "${{ github.workspace }}/master_${name}/${video}.mp4" "${{ github.workspace }}/pr_ui/${video}_master.mp4" + + diff_exit_code=0 + python3 ${{ github.workspace }}/selfdrive/ui/tests/diff/diff.py \ + "${{ github.workspace }}/pr_ui/${video}_master.mp4" \ + "${{ github.workspace }}/pr_ui/${video}_proposed.mp4" \ + "${diff_name}.html" --basedir "$baseurl" --no-open || diff_exit_code=$? + + cp "${{ github.workspace }}/selfdrive/ui/tests/diff/report/${diff_name}.html" "${{ github.workspace }}/pr_ui/" + cp "${{ github.workspace }}/selfdrive/ui/tests/diff/report/${diff_name}.mp4" "${{ github.workspace }}/pr_ui/" + + REPORT_URL="https://commaai.github.io/ci-artifacts/${diff_name}_pr_${{ github.event.number }}.html" + if [ $diff_exit_code -eq 0 ]; then + COMMENT+="**${name}**: Videos are identical! [View Diff Report]($REPORT_URL)"$'\n' + else + COMMENT+="**${name}**: ⚠️ Videos differ! [View Diff Report]($REPORT_URL)"$'\n' + fi + done + + { + echo "COMMENT<> "$GITHUB_OUTPUT" + + - name: Saving proposed ui + if: github.event_name == 'pull_request' + working-directory: ${{ github.workspace }}/master_mici + run: | + git config user.name "GitHub Actions Bot" + git config user.email "<>" + git checkout --orphan=${{ env.BRANCH_NAME }} + git rm -rf * + mv ${{ github.workspace }}/pr_ui/* . + git add . + git commit -m "ui videos for PR #${{ github.event.number }}" + git push origin ${{ env.BRANCH_NAME }} --force + + # Append diff reports to report files branch + git fetch origin ${{ env.REPORT_FILES_BRANCH_NAME }} + git checkout ${{ env.REPORT_FILES_BRANCH_NAME }} + for variant in $VARIANTS; do + IFS=':' read -r name _ _ <<< "$variant" + diff_name="${name}_diff" + cp "${{ github.workspace }}/selfdrive/ui/tests/diff/report/${diff_name}.html" "${diff_name}_pr_${{ github.event.number }}.html" + git add "${diff_name}_pr_${{ github.event.number }}.html" + done + git commit -m "ui diff reports for PR #${{ github.event.number }}" || echo "No changes to commit" + git push origin ${{ env.REPORT_FILES_BRANCH_NAME }} + + - name: Comment on PR + if: github.event_name == 'pull_request' + uses: thollander/actions-comment-pull-request@v2 + with: + message: | + + ## UI Preview + ${{ steps.find_diff.outputs.COMMENT }} + comment_tag: run_id_ui_preview + pr_number: ${{ github.event.number }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From a48988ccb3e8c1a2010f989630d9149cd9756b6d Mon Sep 17 00:00:00 2001 From: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com> Date: Mon, 16 Feb 2026 00:01:59 -0800 Subject: [PATCH 049/311] chore: sync tinygrad (#1680) * chore: sync tinygrad Runs great in sim. now we need to rebuild some models * oops forgot to unblock this after testing * helpers * oh yeah * latest tg * this wont do anything empriically * reduce complexity * okay lint * Update tinygrad_runner.py * Update modeld.py * Update build-all-tinygrad-models.yaml * tinygrad bump * Update modeld.py * Update tinygrad_runner.py * bump * Update SConscript * Update SConscript * com * Update fetcher.py * Update helpers.py * Update model_runner.py * Update model_runner.py --------- Co-authored-by: Jason Wen --- .github/workflows/build-all-tinygrad-models.yaml | 3 ++- .gitmodules | 2 +- selfdrive/modeld/SConscript | 2 +- sunnypilot/modeld_v2/SConscript | 2 +- sunnypilot/modeld_v2/modeld.py | 7 +++++++ sunnypilot/models/fetcher.py | 2 +- sunnypilot/models/helpers.py | 4 ++-- sunnypilot/models/runners/model_runner.py | 11 ----------- sunnypilot/models/runners/tinygrad/tinygrad_runner.py | 4 ++-- tinygrad_repo | 2 +- 10 files changed, 18 insertions(+), 21 deletions(-) diff --git a/.github/workflows/build-all-tinygrad-models.yaml b/.github/workflows/build-all-tinygrad-models.yaml index 00864d38a0..e901baee0c 100644 --- a/.github/workflows/build-all-tinygrad-models.yaml +++ b/.github/workflows/build-all-tinygrad-models.yaml @@ -140,7 +140,7 @@ jobs: run: | echo '${{ needs.setup.outputs.model_matrix }}' > matrix.json built=(); while IFS= read -r line; do built+=("$line"); done < <( - ls output | sed -E 's/^model-//' | sed -E 's/-[0-9]+$//' | sed -E 's/ \([^)]*\)//' | awk '{gsub(/^ +| +$/, ""); print}' + find output -maxdepth 1 -name 'model-*' -printf "%f\n" | sed -E 's/^model-//' | sed -E 's/-[0-9]+$//' | sed -E 's/ \([^)]*\)//' | awk '{gsub(/^ +| +$/, ""); print}' ) jq -c --argjson built "$(printf '%s\n' "${built[@]}" | jq -R . | jq -s .)" \ 'map(select(.display_name as $n | ($built | index($n | gsub("^ +| +$"; "")) | not)))' matrix.json > retry_matrix.json @@ -168,6 +168,7 @@ jobs: if: ${{ !cancelled() && (needs.get_and_build.result != 'failure' || needs.retry_get_and_build.result == 'success' || (needs.retry_failed_models.outputs.retry_matrix != '[]' && needs.retry_failed_models.outputs.retry_matrix != '')) }} runs-on: ubuntu-latest strategy: + fail-fast: false max-parallel: 1 matrix: model: ${{ fromJson(needs.setup.outputs.model_matrix) }} diff --git a/.gitmodules b/.gitmodules index cd6cf2168f..5c5d72a7dc 100644 --- a/.gitmodules +++ b/.gitmodules @@ -15,7 +15,7 @@ url = https://github.com/commaai/teleoprtc [submodule "tinygrad"] path = tinygrad_repo - url = https://github.com/commaai/tinygrad.git + url = https://github.com/sunnypilot/tinygrad.git [submodule "sunnypilot/neural_network_data"] path = sunnypilot/neural_network_data url = https://github.com/sunnypilot/neural-network-data.git diff --git a/selfdrive/modeld/SConscript b/selfdrive/modeld/SConscript index 903cb5d389..7865e86f66 100644 --- a/selfdrive/modeld/SConscript +++ b/selfdrive/modeld/SConscript @@ -50,7 +50,7 @@ def tg_compile(flags, model_name): # Compile small models for model_name in ['driving_vision', 'driving_policy', 'dmonitoring_model']: flags = { - 'larch64': 'DEV=QCOM', + 'larch64': 'DEV=QCOM FLOAT16=1 NOLOCALS=1 IMAGE=2 JIT_BATCH_SIZE=0', 'Darwin': f'DEV=CPU HOME={os.path.expanduser("~")} IMAGE=0', # tinygrad calls brew which needs a $HOME in the env }.get(arch, 'DEV=CPU CPU_LLVM=1 IMAGE=0') tg_compile(flags, model_name) diff --git a/sunnypilot/modeld_v2/SConscript b/sunnypilot/modeld_v2/SConscript index 94033846b0..28d39a75f1 100644 --- a/sunnypilot/modeld_v2/SConscript +++ b/sunnypilot/modeld_v2/SConscript @@ -60,7 +60,7 @@ def tg_compile(flags, model_name): for model_name in ['supercombo', 'driving_vision', 'driving_off_policy', 'driving_policy']: if File(f"models/{model_name}.onnx").exists(): flags = { - 'larch64': 'DEV=QCOM', + 'larch64': 'DEV=QCOM FLOAT16=1 NOLOCALS=1 IMAGE=2 JIT_BATCH_SIZE=0', 'Darwin': f'DEV=CPU HOME={os.path.expanduser("~")} IMAGE=0', # tinygrad calls brew which needs a $HOME in the env }.get(arch, 'DEV=CPU CPU_LLVM=1 IMAGE=0') tg_compile(flags, model_name) diff --git a/sunnypilot/modeld_v2/modeld.py b/sunnypilot/modeld_v2/modeld.py index be0724db0d..e25078f52f 100755 --- a/sunnypilot/modeld_v2/modeld.py +++ b/sunnypilot/modeld_v2/modeld.py @@ -1,4 +1,11 @@ #!/usr/bin/env python3 +import os +from openpilot.system.hardware import TICI +os.environ['DEV'] = 'QCOM' if TICI else 'CPU' +USBGPU = "USBGPU" in os.environ +if USBGPU: + os.environ['DEV'] = 'AMD' + os.environ['AMD_IFACE'] = 'USB' import time import numpy as np import cereal.messaging as messaging diff --git a/sunnypilot/models/fetcher.py b/sunnypilot/models/fetcher.py index 1d8da083a9..0de7496578 100644 --- a/sunnypilot/models/fetcher.py +++ b/sunnypilot/models/fetcher.py @@ -116,7 +116,7 @@ class ModelCache: class ModelFetcher: """Handles fetching and caching of model data from remote source""" - MODEL_URL = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-docs/refs/heads/gh-pages/docs/driving_models_v11.json" + MODEL_URL = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-docs/refs/heads/gh-pages/docs/driving_models_v14.json" def __init__(self, params: Params): self.params = params diff --git a/sunnypilot/models/helpers.py b/sunnypilot/models/helpers.py index ce6625a1f0..7fcf7f85ef 100644 --- a/sunnypilot/models/helpers.py +++ b/sunnypilot/models/helpers.py @@ -19,8 +19,8 @@ from openpilot.system.hardware.hw import Paths from pathlib import Path # see the README.md for more details on the model selector versioning -CURRENT_SELECTOR_VERSION = 13 -REQUIRED_MIN_SELECTOR_VERSION = 12 +CURRENT_SELECTOR_VERSION = 14 +REQUIRED_MIN_SELECTOR_VERSION = 14 USE_ONNX = os.getenv('USE_ONNX', PC) diff --git a/sunnypilot/models/runners/model_runner.py b/sunnypilot/models/runners/model_runner.py index 6902ed09b8..a49ff4d206 100644 --- a/sunnypilot/models/runners/model_runner.py +++ b/sunnypilot/models/runners/model_runner.py @@ -1,9 +1,7 @@ -import os from abc import abstractmethod, ABC import numpy as np from openpilot.sunnypilot.models.helpers import get_active_bundle -from openpilot.system.hardware import TICI from openpilot.sunnypilot.models.runners.constants import NumpyDict, ShapeDict, CLMemDict, FrameDict, Model, SliceDict, SEND_RAW_PRED from openpilot.system.hardware.hw import Paths import pickle @@ -11,15 +9,6 @@ import pickle CUSTOM_MODEL_PATH = Paths.model_root() -# Set QCOM environment variable for TICI devices, potentially enabling hardware acceleration -USBGPU = "USBGPU" in os.environ -if USBGPU: - os.environ['DEV'] = 'AMD' - os.environ['AMD_IFACE'] = 'USB' -else: - os.environ['DEV'] = 'QCOM' if TICI else 'CPU' - - class ModelData: """ Stores metadata and configuration for a specific machine learning model. diff --git a/sunnypilot/models/runners/tinygrad/tinygrad_runner.py b/sunnypilot/models/runners/tinygrad/tinygrad_runner.py index 7b48e3086b..2df1c65e08 100644 --- a/sunnypilot/models/runners/tinygrad/tinygrad_runner.py +++ b/sunnypilot/models/runners/tinygrad/tinygrad_runner.py @@ -51,7 +51,7 @@ class TinygradRunner(ModelRunner, SupercomboTinygrad, PolicyTinygrad, VisionTiny self.input_to_dtype = {} self.input_to_device = {} for idx, name in enumerate(self.model_run.captured.expected_names): - info = self.model_run.captured.expected_st_vars_dtype_device[idx] + info = self.model_run.captured.expected_input_info[idx] self.input_to_dtype[name] = info[2] # dtype self.input_to_device[name] = info[3] # device @@ -84,7 +84,7 @@ class TinygradRunner(ModelRunner, SupercomboTinygrad, PolicyTinygrad, VisionTiny def _run_model(self) -> NumpyDict: """Runs the Tinygrad model inference and parses the outputs.""" - outputs = self.model_run(**self.inputs).contiguous().realize().uop.base.buffer.numpy() + outputs = self.model_run(**self.inputs).numpy().flatten() return self._parse_outputs(outputs) def _parse_outputs(self, model_outputs: np.ndarray) -> NumpyDict: diff --git a/tinygrad_repo b/tinygrad_repo index 7296c74cbd..3501a71478 160000 --- a/tinygrad_repo +++ b/tinygrad_repo @@ -1 +1 @@ -Subproject commit 7296c74cbd2666da7dce95d7ca6dab5340653a5c +Subproject commit 3501a714785ff370cffb966a45d5f9cdf6c9ea7a From a120053d124488342ff40e9d1e6ff3f3b153a31b Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Mon, 16 Feb 2026 03:32:08 -0500 Subject: [PATCH 050/311] docker: Dockerfile.sunnypilot uv run scons (#1700) * docker: Dockerfile.sunnypilot uv run scons * docker: Dockerfile.sunnypilot: don't set UV_PROJECT_ENVIRONMENT --- Dockerfile.sunnypilot | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Dockerfile.sunnypilot b/Dockerfile.sunnypilot index 88a226ad08..abc207171e 100644 --- a/Dockerfile.sunnypilot +++ b/Dockerfile.sunnypilot @@ -9,4 +9,6 @@ WORKDIR ${OPENPILOT_PATH} COPY . ${OPENPILOT_PATH}/ -RUN scons --cache-readonly -j$(nproc) +ENV UV_BIN="/home/batman/.local/bin/" +ENV PATH="$UV_BIN:$PATH" +RUN UV_PROJECT_ENVIRONMENT=$VIRTUAL_ENV uv run scons --cache-readonly -j$(nproc) From 4a869778a916ee0ef86f218229229d4c46d9fb50 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Mon, 16 Feb 2026 04:07:07 -0500 Subject: [PATCH 051/311] Revert "ci: skip uv lock upgrade on forks" (#1701) Revert "ci: skip uv lock upgrade on forks (#1213)" This reverts commit 220cfff04d28fceda9c2a258579240d679825918. --- .github/workflows/repo-maintenance.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/repo-maintenance.yaml b/.github/workflows/repo-maintenance.yaml index 3c652b6ce6..0e567a0a40 100644 --- a/.github/workflows/repo-maintenance.yaml +++ b/.github/workflows/repo-maintenance.yaml @@ -43,7 +43,6 @@ jobs: with: submodules: true - name: uv lock - if: github.repository == 'commaai/openpilot' run: | python3 -m ensurepip --upgrade pip3 install uv From 0cb6b7b807955e1f20b9c5e5d86341b03907ae57 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Mon, 16 Feb 2026 04:16:49 -0500 Subject: [PATCH 052/311] ci: exclude non-sunnypilot maintained submodules from repo maintenance (#1703) * ci: exclude non-sunnypilot maintained submodules from repo maintenance * match names --- .github/workflows/repo-maintenance.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/repo-maintenance.yaml b/.github/workflows/repo-maintenance.yaml index 0e567a0a40..9be42cd041 100644 --- a/.github/workflows/repo-maintenance.yaml +++ b/.github/workflows/repo-maintenance.yaml @@ -56,6 +56,9 @@ jobs: - name: bump submodules run: | git config --global --add safe.directory '*' + git config submodule.msgq.update none + git config submodule.rednose_repo.update none + git config submodule.teleoprtc_repo.update none git config submodule.tinygrad.update none git submodule update --remote git add . From f81e7245fef3b7269ef7b868e4d970d9177ee47c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 16 Feb 2026 04:18:44 -0500 Subject: [PATCH 053/311] [bot] Update Python packages (#1702) Update Python packages Co-authored-by: github-actions[bot] --- opendbc_repo | 2 +- uv.lock | 118 +++++++++++++++++++++++++-------------------------- 2 files changed, 60 insertions(+), 60 deletions(-) diff --git a/opendbc_repo b/opendbc_repo index f07b9b3f38..8289577096 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit f07b9b3f38ab94a367c4d576b8cdc0f73a81ee06 +Subproject commit 8289577096cf303e3f8c53a86d0e7355651b53b5 diff --git a/uv.lock b/uv.lock index 8909934e9a..782c20849c 100644 --- a/uv.lock +++ b/uv.lock @@ -415,11 +415,11 @@ wheels = [ [[package]] name = "filelock" -version = "3.20.3" +version = "3.24.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1d/65/ce7f1b70157833bf3cb851b556a37d4547ceafc158aa9b34b36782f23696/filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1", size = 19485, upload-time = "2026-01-09T17:55:05.421Z" } +sdist = { url = "https://files.pythonhosted.org/packages/02/a8/dae62680be63cbb3ff87cfa2f51cf766269514ea5488479d42fec5aa6f3a/filelock-3.24.2.tar.gz", hash = "sha256:c22803117490f156e59fafce621f0550a7a853e2bbf4f87f112b11d469b6c81b", size = 37601, upload-time = "2026-02-16T02:50:45.614Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701, upload-time = "2026-01-09T17:55:04.334Z" }, + { url = "https://files.pythonhosted.org/packages/e7/04/a94ebfb4eaaa08db56725a40de2887e95de4e8641b9e902c311bfa00aa39/filelock-3.24.2-py3-none-any.whl", hash = "sha256:667d7dc0b7d1e1064dd5f8f8e80bdac157a6482e8d2e02cd16fd3b6b33bd6556", size = 24152, upload-time = "2026-02-16T02:50:44Z" }, ] [[package]] @@ -1144,30 +1144,30 @@ wheels = [ [[package]] name = "pillow" -version = "12.1.0" +version = "12.1.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d0/02/d52c733a2452ef1ffcc123b68e6606d07276b0e358db70eabad7e40042b7/pillow-12.1.0.tar.gz", hash = "sha256:5c5ae0a06e9ea030ab786b0251b32c7e4ce10e58d983c0d5c56029455180b5b9", size = 46977283, upload-time = "2026-01-02T09:13:29.892Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/42/5c74462b4fd957fcd7b13b04fb3205ff8349236ea74c7c375766d6c82288/pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4", size = 46980264, upload-time = "2026-02-11T04:23:07.146Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/31/dc53fe21a2f2996e1b7d92bf671cdb157079385183ef7c1ae08b485db510/pillow-12.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a332ac4ccb84b6dde65dbace8431f3af08874bf9770719d32a635c4ef411b18b", size = 5262642, upload-time = "2026-01-02T09:11:10.138Z" }, - { url = "https://files.pythonhosted.org/packages/ab/c1/10e45ac9cc79419cedf5121b42dcca5a50ad2b601fa080f58c22fb27626e/pillow-12.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:907bfa8a9cb790748a9aa4513e37c88c59660da3bcfffbd24a7d9e6abf224551", size = 4657464, upload-time = "2026-01-02T09:11:12.319Z" }, - { url = "https://files.pythonhosted.org/packages/ad/26/7b82c0ab7ef40ebede7a97c72d473bda5950f609f8e0c77b04af574a0ddb/pillow-12.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:efdc140e7b63b8f739d09a99033aa430accce485ff78e6d311973a67b6bf3208", size = 6234878, upload-time = "2026-01-02T09:11:14.096Z" }, - { url = "https://files.pythonhosted.org/packages/76/25/27abc9792615b5e886ca9411ba6637b675f1b77af3104710ac7353fe5605/pillow-12.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bef9768cab184e7ae6e559c032e95ba8d07b3023c289f79a2bd36e8bf85605a5", size = 8044868, upload-time = "2026-01-02T09:11:15.903Z" }, - { url = "https://files.pythonhosted.org/packages/0a/ea/f200a4c36d836100e7bc738fc48cd963d3ba6372ebc8298a889e0cfc3359/pillow-12.1.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:742aea052cf5ab5034a53c3846165bc3ce88d7c38e954120db0ab867ca242661", size = 6349468, upload-time = "2026-01-02T09:11:17.631Z" }, - { url = "https://files.pythonhosted.org/packages/11/8f/48d0b77ab2200374c66d344459b8958c86693be99526450e7aee714e03e4/pillow-12.1.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a6dfc2af5b082b635af6e08e0d1f9f1c4e04d17d4e2ca0ef96131e85eda6eb17", size = 7041518, upload-time = "2026-01-02T09:11:19.389Z" }, - { url = "https://files.pythonhosted.org/packages/1d/23/c281182eb986b5d31f0a76d2a2c8cd41722d6fb8ed07521e802f9bba52de/pillow-12.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:609e89d9f90b581c8d16358c9087df76024cf058fa693dd3e1e1620823f39670", size = 6462829, upload-time = "2026-01-02T09:11:21.28Z" }, - { url = "https://files.pythonhosted.org/packages/25/ef/7018273e0faac099d7b00982abdcc39142ae6f3bd9ceb06de09779c4a9d6/pillow-12.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:43b4899cfd091a9693a1278c4982f3e50f7fb7cff5153b05174b4afc9593b616", size = 7166756, upload-time = "2026-01-02T09:11:23.559Z" }, - { url = "https://files.pythonhosted.org/packages/8f/c8/993d4b7ab2e341fe02ceef9576afcf5830cdec640be2ac5bee1820d693d4/pillow-12.1.0-cp312-cp312-win32.whl", hash = "sha256:aa0c9cc0b82b14766a99fbe6084409972266e82f459821cd26997a488a7261a7", size = 6328770, upload-time = "2026-01-02T09:11:25.661Z" }, - { url = "https://files.pythonhosted.org/packages/a7/87/90b358775a3f02765d87655237229ba64a997b87efa8ccaca7dd3e36e7a7/pillow-12.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:d70534cea9e7966169ad29a903b99fc507e932069a881d0965a1a84bb57f6c6d", size = 7033406, upload-time = "2026-01-02T09:11:27.474Z" }, - { url = "https://files.pythonhosted.org/packages/5d/cf/881b457eccacac9e5b2ddd97d5071fb6d668307c57cbf4e3b5278e06e536/pillow-12.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:65b80c1ee7e14a87d6a068dd3b0aea268ffcabfe0498d38661b00c5b4b22e74c", size = 2452612, upload-time = "2026-01-02T09:11:29.309Z" }, + { url = "https://files.pythonhosted.org/packages/07/d3/8df65da0d4df36b094351dce696f2989bec731d4f10e743b1c5f4da4d3bf/pillow-12.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab323b787d6e18b3d91a72fc99b1a2c28651e4358749842b8f8dfacd28ef2052", size = 5262803, upload-time = "2026-02-11T04:20:47.653Z" }, + { url = "https://files.pythonhosted.org/packages/d6/71/5026395b290ff404b836e636f51d7297e6c83beceaa87c592718747e670f/pillow-12.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:adebb5bee0f0af4909c30db0d890c773d1a92ffe83da908e2e9e720f8edf3984", size = 4657601, upload-time = "2026-02-11T04:20:49.328Z" }, + { url = "https://files.pythonhosted.org/packages/b1/2e/1001613d941c67442f745aff0f7cc66dd8df9a9c084eb497e6a543ee6f7e/pillow-12.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb66b7cc26f50977108790e2456b7921e773f23db5630261102233eb355a3b79", size = 6234995, upload-time = "2026-02-11T04:20:51.032Z" }, + { url = "https://files.pythonhosted.org/packages/07/26/246ab11455b2549b9233dbd44d358d033a2f780fa9007b61a913c5b2d24e/pillow-12.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee2810642b2898bb187ced9b349e95d2a7272930796e022efaf12e99dccd293", size = 8045012, upload-time = "2026-02-11T04:20:52.882Z" }, + { url = "https://files.pythonhosted.org/packages/b2/8b/07587069c27be7535ac1fe33874e32de118fbd34e2a73b7f83436a88368c/pillow-12.1.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0b1cd6232e2b618adcc54d9882e4e662a089d5768cd188f7c245b4c8c44a397", size = 6349638, upload-time = "2026-02-11T04:20:54.444Z" }, + { url = "https://files.pythonhosted.org/packages/ff/79/6df7b2ee763d619cda2fb4fea498e5f79d984dae304d45a8999b80d6cf5c/pillow-12.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7aac39bcf8d4770d089588a2e1dd111cbaa42df5a94be3114222057d68336bd0", size = 7041540, upload-time = "2026-02-11T04:20:55.97Z" }, + { url = "https://files.pythonhosted.org/packages/2c/5e/2ba19e7e7236d7529f4d873bdaf317a318896bac289abebd4bb00ef247f0/pillow-12.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ab174cd7d29a62dd139c44bf74b698039328f45cb03b4596c43473a46656b2f3", size = 6462613, upload-time = "2026-02-11T04:20:57.542Z" }, + { url = "https://files.pythonhosted.org/packages/03/03/31216ec124bb5c3dacd74ce8efff4cc7f52643653bad4825f8f08c697743/pillow-12.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:339ffdcb7cbeaa08221cd401d517d4b1fe7a9ed5d400e4a8039719238620ca35", size = 7166745, upload-time = "2026-02-11T04:20:59.196Z" }, + { url = "https://files.pythonhosted.org/packages/1f/e7/7c4552d80052337eb28653b617eafdef39adfb137c49dd7e831b8dc13bc5/pillow-12.1.1-cp312-cp312-win32.whl", hash = "sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a", size = 6328823, upload-time = "2026-02-11T04:21:01.385Z" }, + { url = "https://files.pythonhosted.org/packages/3d/17/688626d192d7261bbbf98846fc98995726bddc2c945344b65bec3a29d731/pillow-12.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6", size = 7033367, upload-time = "2026-02-11T04:21:03.536Z" }, + { url = "https://files.pythonhosted.org/packages/ed/fe/a0ef1f73f939b0eca03ee2c108d0043a87468664770612602c63266a43c4/pillow-12.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523", size = 2453811, upload-time = "2026-02-11T04:21:05.116Z" }, ] [[package]] name = "platformdirs" -version = "4.5.1" +version = "4.9.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/04/fea538adf7dbbd6d186f551d595961e564a3b6715bdf276b477460858672/platformdirs-4.9.2.tar.gz", hash = "sha256:9a33809944b9db043ad67ca0db94b14bf452cc6aeaac46a88ea55b26e2e9d291", size = 28394, upload-time = "2026-02-16T03:56:10.574Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, + { url = "https://files.pythonhosted.org/packages/48/31/05e764397056194206169869b50cf2fee4dbbbc71b344705b9c0d878d4d8/platformdirs-4.9.2-py3-none-any.whl", hash = "sha256:9170634f126f8efdae22fb58ae8a0eaa86f38365bc57897a6c4f781d1f5875bd", size = 21168, upload-time = "2026-02-16T03:56:08.891Z" }, ] [[package]] @@ -1328,14 +1328,14 @@ wheels = [ [[package]] name = "pyee" -version = "13.0.0" +version = "13.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/95/03/1fd98d5841cd7964a27d729ccf2199602fe05eb7a405c1462eb7277945ed/pyee-13.0.0.tar.gz", hash = "sha256:b391e3c5a434d1f5118a25615001dbc8f669cf410ab67d04c4d4e07c55481c37", size = 31250, upload-time = "2025-03-17T18:53:15.955Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8b/04/e7c1fe4dc78a6fdbfd6c337b1c3732ff543b8a397683ab38378447baa331/pyee-13.0.1.tar.gz", hash = "sha256:0b931f7c14535667ed4c7e0d531716368715e860b988770fc7eb8578d1f67fc8", size = 31655, upload-time = "2026-02-14T21:12:28.044Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/4d/b9add7c84060d4c1906abe9a7e5359f2a60f7a9a4f67268b2766673427d8/pyee-13.0.0-py3-none-any.whl", hash = "sha256:48195a3cddb3b1515ce0695ed76036b5ccc2ef3a9f963ff9f77aec0139845498", size = 15730, upload-time = "2025-03-17T18:53:14.532Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c4/b4d4827c93ef43c01f599ef31453ccc1c132b353284fc6c87d535c233129/pyee-13.0.1-py3-none-any.whl", hash = "sha256:af2f8fede4171ef667dfded53f96e2ed0d6e6bd7ee3bb46437f77e3b57689228", size = 15659, upload-time = "2026-02-14T21:12:26.263Z" }, ] [[package]] @@ -4055,27 +4055,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.15.0" +version = "0.15.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c8/39/5cee96809fbca590abea6b46c6d1c586b49663d1d2830a751cc8fc42c666/ruff-0.15.0.tar.gz", hash = "sha256:6bdea47cdbea30d40f8f8d7d69c0854ba7c15420ec75a26f463290949d7f7e9a", size = 4524893, upload-time = "2026-02-03T17:53:35.357Z" } +sdist = { url = "https://files.pythonhosted.org/packages/04/dc/4e6ac71b511b141cf626357a3946679abeba4cf67bc7cc5a17920f31e10d/ruff-0.15.1.tar.gz", hash = "sha256:c590fe13fb57c97141ae975c03a1aedb3d3156030cabd740d6ff0b0d601e203f", size = 4540855, upload-time = "2026-02-12T23:09:09.998Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/88/3fd1b0aa4b6330d6aaa63a285bc96c9f71970351579152d231ed90914586/ruff-0.15.0-py3-none-linux_armv6l.whl", hash = "sha256:aac4ebaa612a82b23d45964586f24ae9bc23ca101919f5590bdb368d74ad5455", size = 10354332, upload-time = "2026-02-03T17:52:54.892Z" }, - { url = "https://files.pythonhosted.org/packages/72/f6/62e173fbb7eb75cc29fe2576a1e20f0a46f671a2587b5f604bfb0eaf5f6f/ruff-0.15.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:dcd4be7cc75cfbbca24a98d04d0b9b36a270d0833241f776b788d59f4142b14d", size = 10767189, upload-time = "2026-02-03T17:53:19.778Z" }, - { url = "https://files.pythonhosted.org/packages/99/e4/968ae17b676d1d2ff101d56dc69cf333e3a4c985e1ec23803df84fc7bf9e/ruff-0.15.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d747e3319b2bce179c7c1eaad3d884dc0a199b5f4d5187620530adf9105268ce", size = 10075384, upload-time = "2026-02-03T17:53:29.241Z" }, - { url = "https://files.pythonhosted.org/packages/a2/bf/9843c6044ab9e20af879c751487e61333ca79a2c8c3058b15722386b8cae/ruff-0.15.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:650bd9c56ae03102c51a5e4b554d74d825ff3abe4db22b90fd32d816c2e90621", size = 10481363, upload-time = "2026-02-03T17:52:43.332Z" }, - { url = "https://files.pythonhosted.org/packages/55/d9/4ada5ccf4cd1f532db1c8d44b6f664f2208d3d93acbeec18f82315e15193/ruff-0.15.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6664b7eac559e3048223a2da77769c2f92b43a6dfd4720cef42654299a599c9", size = 10187736, upload-time = "2026-02-03T17:53:00.522Z" }, - { url = "https://files.pythonhosted.org/packages/86/e2/f25eaecd446af7bb132af0a1d5b135a62971a41f5366ff41d06d25e77a91/ruff-0.15.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f811f97b0f092b35320d1556f3353bf238763420ade5d9e62ebd2b73f2ff179", size = 10968415, upload-time = "2026-02-03T17:53:15.705Z" }, - { url = "https://files.pythonhosted.org/packages/e7/dc/f06a8558d06333bf79b497d29a50c3a673d9251214e0d7ec78f90b30aa79/ruff-0.15.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:761ec0a66680fab6454236635a39abaf14198818c8cdf691e036f4bc0f406b2d", size = 11809643, upload-time = "2026-02-03T17:53:23.031Z" }, - { url = "https://files.pythonhosted.org/packages/dd/45/0ece8db2c474ad7df13af3a6d50f76e22a09d078af63078f005057ca59eb/ruff-0.15.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:940f11c2604d317e797b289f4f9f3fa5555ffe4fb574b55ed006c3d9b6f0eb78", size = 11234787, upload-time = "2026-02-03T17:52:46.432Z" }, - { url = "https://files.pythonhosted.org/packages/8a/d9/0e3a81467a120fd265658d127db648e4d3acfe3e4f6f5d4ea79fac47e587/ruff-0.15.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcbca3d40558789126da91d7ef9a7c87772ee107033db7191edefa34e2c7f1b4", size = 11112797, upload-time = "2026-02-03T17:52:49.274Z" }, - { url = "https://files.pythonhosted.org/packages/b2/cb/8c0b3b0c692683f8ff31351dfb6241047fa873a4481a76df4335a8bff716/ruff-0.15.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9a121a96db1d75fa3eb39c4539e607f628920dd72ff1f7c5ee4f1b768ac62d6e", size = 11033133, upload-time = "2026-02-03T17:53:33.105Z" }, - { url = "https://files.pythonhosted.org/packages/f8/5e/23b87370cf0f9081a8c89a753e69a4e8778805b8802ccfe175cc410e50b9/ruff-0.15.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5298d518e493061f2eabd4abd067c7e4fb89e2f63291c94332e35631c07c3662", size = 10442646, upload-time = "2026-02-03T17:53:06.278Z" }, - { url = "https://files.pythonhosted.org/packages/e1/9a/3c94de5ce642830167e6d00b5c75aacd73e6347b4c7fc6828699b150a5ee/ruff-0.15.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:afb6e603d6375ff0d6b0cee563fa21ab570fd15e65c852cb24922cef25050cf1", size = 10195750, upload-time = "2026-02-03T17:53:26.084Z" }, - { url = "https://files.pythonhosted.org/packages/30/15/e396325080d600b436acc970848d69df9c13977942fb62bb8722d729bee8/ruff-0.15.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:77e515f6b15f828b94dc17d2b4ace334c9ddb7d9468c54b2f9ed2b9c1593ef16", size = 10676120, upload-time = "2026-02-03T17:53:09.363Z" }, - { url = "https://files.pythonhosted.org/packages/8d/c9/229a23d52a2983de1ad0fb0ee37d36e0257e6f28bfd6b498ee2c76361874/ruff-0.15.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6f6e80850a01eb13b3e42ee0ebdf6e4497151b48c35051aab51c101266d187a3", size = 11201636, upload-time = "2026-02-03T17:52:57.281Z" }, - { url = "https://files.pythonhosted.org/packages/6f/b0/69adf22f4e24f3677208adb715c578266842e6e6a3cc77483f48dd999ede/ruff-0.15.0-py3-none-win32.whl", hash = "sha256:238a717ef803e501b6d51e0bdd0d2c6e8513fe9eec14002445134d3907cd46c3", size = 10465945, upload-time = "2026-02-03T17:53:12.591Z" }, - { url = "https://files.pythonhosted.org/packages/51/ad/f813b6e2c97e9b4598be25e94a9147b9af7e60523b0cb5d94d307c15229d/ruff-0.15.0-py3-none-win_amd64.whl", hash = "sha256:dd5e4d3301dc01de614da3cdffc33d4b1b96fb89e45721f1598e5532ccf78b18", size = 11564657, upload-time = "2026-02-03T17:52:51.893Z" }, - { url = "https://files.pythonhosted.org/packages/f6/b0/2d823f6e77ebe560f4e397d078487e8d52c1516b331e3521bc75db4272ca/ruff-0.15.0-py3-none-win_arm64.whl", hash = "sha256:c480d632cc0ca3f0727acac8b7d053542d9e114a462a145d0b00e7cd658c515a", size = 10865753, upload-time = "2026-02-03T17:53:03.014Z" }, + { url = "https://files.pythonhosted.org/packages/23/bf/e6e4324238c17f9d9120a9d60aa99a7daaa21204c07fcd84e2ef03bb5fd1/ruff-0.15.1-py3-none-linux_armv6l.whl", hash = "sha256:b101ed7cf4615bda6ffe65bdb59f964e9f4a0d3f85cbf0e54f0ab76d7b90228a", size = 10367819, upload-time = "2026-02-12T23:09:03.598Z" }, + { url = "https://files.pythonhosted.org/packages/b3/ea/c8f89d32e7912269d38c58f3649e453ac32c528f93bb7f4219258be2e7ed/ruff-0.15.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:939c995e9277e63ea632cc8d3fae17aa758526f49a9a850d2e7e758bfef46602", size = 10798618, upload-time = "2026-02-12T23:09:22.928Z" }, + { url = "https://files.pythonhosted.org/packages/5e/0f/1d0d88bc862624247d82c20c10d4c0f6bb2f346559d8af281674cf327f15/ruff-0.15.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1d83466455fdefe60b8d9c8df81d3c1bbb2115cede53549d3b522ce2bc703899", size = 10148518, upload-time = "2026-02-12T23:08:58.339Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c8/291c49cefaa4a9248e986256df2ade7add79388fe179e0691be06fae6f37/ruff-0.15.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9457e3c3291024866222b96108ab2d8265b477e5b1534c7ddb1810904858d16", size = 10518811, upload-time = "2026-02-12T23:09:31.865Z" }, + { url = "https://files.pythonhosted.org/packages/c3/1a/f5707440e5ae43ffa5365cac8bbb91e9665f4a883f560893829cf16a606b/ruff-0.15.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:92c92b003e9d4f7fbd33b1867bb15a1b785b1735069108dfc23821ba045b29bc", size = 10196169, upload-time = "2026-02-12T23:09:17.306Z" }, + { url = "https://files.pythonhosted.org/packages/2a/ff/26ddc8c4da04c8fd3ee65a89c9fb99eaa5c30394269d424461467be2271f/ruff-0.15.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fe5c41ab43e3a06778844c586251eb5a510f67125427625f9eb2b9526535779", size = 10990491, upload-time = "2026-02-12T23:09:25.503Z" }, + { url = "https://files.pythonhosted.org/packages/fc/00/50920cb385b89413f7cdb4bb9bc8fc59c1b0f30028d8bccc294189a54955/ruff-0.15.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66a6dd6df4d80dc382c6484f8ce1bcceb55c32e9f27a8b94c32f6c7331bf14fb", size = 11843280, upload-time = "2026-02-12T23:09:19.88Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6d/2f5cad8380caf5632a15460c323ae326f1e1a2b5b90a6ee7519017a017ca/ruff-0.15.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a4a42cbb8af0bda9bcd7606b064d7c0bc311a88d141d02f78920be6acb5aa83", size = 11274336, upload-time = "2026-02-12T23:09:14.907Z" }, + { url = "https://files.pythonhosted.org/packages/a3/1d/5f56cae1d6c40b8a318513599b35ea4b075d7dc1cd1d04449578c29d1d75/ruff-0.15.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ab064052c31dddada35079901592dfba2e05f5b1e43af3954aafcbc1096a5b2", size = 11137288, upload-time = "2026-02-12T23:09:07.475Z" }, + { url = "https://files.pythonhosted.org/packages/cd/20/6f8d7d8f768c93b0382b33b9306b3b999918816da46537d5a61635514635/ruff-0.15.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:5631c940fe9fe91f817a4c2ea4e81f47bee3ca4aa646134a24374f3c19ad9454", size = 11070681, upload-time = "2026-02-12T23:08:55.43Z" }, + { url = "https://files.pythonhosted.org/packages/9a/67/d640ac76069f64cdea59dba02af2e00b1fa30e2103c7f8d049c0cff4cafd/ruff-0.15.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:68138a4ba184b4691ccdc39f7795c66b3c68160c586519e7e8444cf5a53e1b4c", size = 10486401, upload-time = "2026-02-12T23:09:27.927Z" }, + { url = "https://files.pythonhosted.org/packages/65/3d/e1429f64a3ff89297497916b88c32a5cc88eeca7e9c787072d0e7f1d3e1e/ruff-0.15.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:518f9af03bfc33c03bdb4cb63fabc935341bb7f54af500f92ac309ecfbba6330", size = 10197452, upload-time = "2026-02-12T23:09:12.147Z" }, + { url = "https://files.pythonhosted.org/packages/78/83/e2c3bade17dad63bf1e1c2ffaf11490603b760be149e1419b07049b36ef2/ruff-0.15.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:da79f4d6a826caaea95de0237a67e33b81e6ec2e25fc7e1993a4015dffca7c61", size = 10693900, upload-time = "2026-02-12T23:09:34.418Z" }, + { url = "https://files.pythonhosted.org/packages/a1/27/fdc0e11a813e6338e0706e8b39bb7a1d61ea5b36873b351acee7e524a72a/ruff-0.15.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3dd86dccb83cd7d4dcfac303ffc277e6048600dfc22e38158afa208e8bf94a1f", size = 11227302, upload-time = "2026-02-12T23:09:36.536Z" }, + { url = "https://files.pythonhosted.org/packages/f6/58/ac864a75067dcbd3b95be5ab4eb2b601d7fbc3d3d736a27e391a4f92a5c1/ruff-0.15.1-py3-none-win32.whl", hash = "sha256:660975d9cb49b5d5278b12b03bb9951d554543a90b74ed5d366b20e2c57c2098", size = 10462555, upload-time = "2026-02-12T23:09:29.899Z" }, + { url = "https://files.pythonhosted.org/packages/e0/5e/d4ccc8a27ecdb78116feac4935dfc39d1304536f4296168f91ed3ec00cd2/ruff-0.15.1-py3-none-win_amd64.whl", hash = "sha256:c820fef9dd5d4172a6570e5721704a96c6679b80cf7be41659ed439653f62336", size = 11599956, upload-time = "2026-02-12T23:09:01.157Z" }, + { url = "https://files.pythonhosted.org/packages/2a/07/5bda6a85b220c64c65686bc85bd0bbb23b29c62b3a9f9433fa55f17cda93/ruff-0.15.1-py3-none-win_arm64.whl", hash = "sha256:5ff7d5f0f88567850f45081fac8f4ec212be8d0b963e385c3f7d0d2eb4899416", size = 10874604, upload-time = "2026-02-12T23:09:05.515Z" }, ] [[package]] @@ -4212,26 +4212,26 @@ wheels = [ [[package]] name = "ty" -version = "0.0.15" +version = "0.0.17" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4e/25/257602d316b9333089b688a7a11b33ebc660b74e8dacf400dc3dfdea1594/ty-0.0.15.tar.gz", hash = "sha256:4f9a5b8df208c62dba56e91b93bed8b5bb714839691b8cff16d12c983bfa1174", size = 5101936, upload-time = "2026-02-05T01:06:34.922Z" } +sdist = { url = "https://files.pythonhosted.org/packages/66/c3/41ae6346443eedb65b96761abfab890a48ce2aa5a8a27af69c5c5d99064d/ty-0.0.17.tar.gz", hash = "sha256:847ed6c120913e280bf9b54d8eaa7a1049708acb8824ad234e71498e8ad09f97", size = 5167209, upload-time = "2026-02-13T13:26:36.835Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/c5/35626e732b79bf0e6213de9f79aff59b5f247c0a1e3ce0d93e675ab9b728/ty-0.0.15-py3-none-linux_armv6l.whl", hash = "sha256:68e092458516c61512dac541cde0a5e4e5842df00b4e81881ead8f745ddec794", size = 10138374, upload-time = "2026-02-05T01:07:03.804Z" }, - { url = "https://files.pythonhosted.org/packages/d5/8a/48fd81664604848f79d03879b3ca3633762d457a069b07e09fb1b87edd6e/ty-0.0.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:79f2e75289eae3cece94c51118b730211af4ba5762906f52a878041b67e54959", size = 9947858, upload-time = "2026-02-05T01:06:47.453Z" }, - { url = "https://files.pythonhosted.org/packages/b6/85/c1ac8e97bcd930946f4c94db85b675561d590b4e72703bf3733419fc3973/ty-0.0.15-py3-none-macosx_11_0_arm64.whl", hash = "sha256:112a7b26e63e48cc72c8c5b03227d1db280cfa57a45f2df0e264c3a016aa8c3c", size = 9443220, upload-time = "2026-02-05T01:06:44.98Z" }, - { url = "https://files.pythonhosted.org/packages/3c/d9/244bc02599d950f7a4298fbc0c1b25cc808646b9577bdf7a83470b2d1cec/ty-0.0.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71f62a2644972975a657d9dc867bf901235cde51e8d24c20311067e7afd44a56", size = 9949976, upload-time = "2026-02-05T01:07:01.515Z" }, - { url = "https://files.pythonhosted.org/packages/7e/ab/3a0daad66798c91a33867a3ececf17d314ac65d4ae2bbbd28cbfde94da63/ty-0.0.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9e48b42be2d257317c85b78559233273b655dd636fc61e7e1d69abd90fd3cba4", size = 9965918, upload-time = "2026-02-05T01:06:54.283Z" }, - { url = "https://files.pythonhosted.org/packages/39/4e/e62b01338f653059a7c0cd09d1a326e9a9eedc351a0f0de9db0601658c3d/ty-0.0.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27dd5b52a421e6871c5bfe9841160331b60866ed2040250cb161886478ab3e4f", size = 10424943, upload-time = "2026-02-05T01:07:08.777Z" }, - { url = "https://files.pythonhosted.org/packages/65/b5/7aa06655ce69c0d4f3e845d2d85e79c12994b6d84c71699cfb437e0bc8cf/ty-0.0.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76b85c9ec2219e11c358a7db8e21b7e5c6674a1fb9b6f633836949de98d12286", size = 10964692, upload-time = "2026-02-05T01:06:37.103Z" }, - { url = "https://files.pythonhosted.org/packages/13/04/36fdfe1f3c908b471e246e37ce3d011175584c26d3853e6c5d9a0364564c/ty-0.0.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9e8204c61d8ede4f21f2975dce74efdb80fafb2fae1915c666cceb33ea3c90b", size = 10692225, upload-time = "2026-02-05T01:06:49.714Z" }, - { url = "https://files.pythonhosted.org/packages/13/41/5bf882649bd8b64ded5fbce7fb8d77fb3b868de1a3b1a6c4796402b47308/ty-0.0.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af87c3be7c944bb4d6609d6c63e4594944b0028c7bd490a525a82b88fe010d6d", size = 10516776, upload-time = "2026-02-05T01:06:52.047Z" }, - { url = "https://files.pythonhosted.org/packages/56/75/66852d7e004f859839c17ffe1d16513c1e7cc04bcc810edb80ca022a9124/ty-0.0.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:50dccf7398505e5966847d366c9e4c650b8c225411c2a68c32040a63b9521eea", size = 9928828, upload-time = "2026-02-05T01:06:56.647Z" }, - { url = "https://files.pythonhosted.org/packages/65/72/96bc16c7b337a3ef358fd227b3c8ef0c77405f3bfbbfb59ee5915f0d9d71/ty-0.0.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:bd797b8f231a4f4715110259ad1ad5340a87b802307f3e06d92bfb37b858a8f3", size = 9978960, upload-time = "2026-02-05T01:06:29.567Z" }, - { url = "https://files.pythonhosted.org/packages/a0/18/d2e316a35b626de2227f832cd36d21205e4f5d96fd036a8af84c72ecec1b/ty-0.0.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9deb7f20e18b25440a9aa4884f934ba5628ef456dbde91819d5af1a73da48af3", size = 10135903, upload-time = "2026-02-05T01:06:59.256Z" }, - { url = "https://files.pythonhosted.org/packages/02/d3/b617a79c9dad10c888d7c15cd78859e0160b8772273637b9c4241a049491/ty-0.0.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7b31b3de031255b90a5f4d9cb3d050feae246067c87130e5a6861a8061c71754", size = 10615879, upload-time = "2026-02-05T01:07:06.661Z" }, - { url = "https://files.pythonhosted.org/packages/fb/b0/2652a73c71c77296a6343217063f05745da60c67b7e8a8e25f2064167fce/ty-0.0.15-py3-none-win32.whl", hash = "sha256:9362c528ceb62c89d65c216336d28d500bc9f4c10418413f63ebc16886e16cc1", size = 9578058, upload-time = "2026-02-05T01:06:42.928Z" }, - { url = "https://files.pythonhosted.org/packages/84/6e/08a4aedebd2a6ce2784b5bc3760e43d1861f1a184734a78215c2d397c1df/ty-0.0.15-py3-none-win_amd64.whl", hash = "sha256:4db040695ae67c5524f59cb8179a8fa277112e69042d7dfdac862caa7e3b0d9c", size = 10457112, upload-time = "2026-02-05T01:06:39.885Z" }, - { url = "https://files.pythonhosted.org/packages/b3/be/1991f2bc12847ae2d4f1e3ac5dcff8bb7bc1261390645c0755bb55616355/ty-0.0.15-py3-none-win_arm64.whl", hash = "sha256:e5a98d4119e77d6136461e16ae505f8f8069002874ab073de03fbcb1a5e8bf25", size = 9937490, upload-time = "2026-02-05T01:06:32.388Z" }, + { url = "https://files.pythonhosted.org/packages/c0/01/0ef15c22a1c54b0f728ceff3f62d478dbf8b0dcf8ff7b80b954f79584f3e/ty-0.0.17-py3-none-linux_armv6l.whl", hash = "sha256:64a9a16555cc8867d35c2647c2f1afbd3cae55f68fd95283a574d1bb04fe93e0", size = 10192793, upload-time = "2026-02-13T13:27:13.943Z" }, + { url = "https://files.pythonhosted.org/packages/0f/2c/f4c322d9cded56edc016b1092c14b95cf58c8a33b4787316ea752bb9418e/ty-0.0.17-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:eb2dbd8acd5c5a55f4af0d479523e7c7265a88542efe73ed3d696eb1ba7b6454", size = 10051977, upload-time = "2026-02-13T13:26:57.741Z" }, + { url = "https://files.pythonhosted.org/packages/4c/a5/43746c1ff81e784f5fc303afc61fe5bcd85d0fcf3ef65cb2cef78c7486c7/ty-0.0.17-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f18f5fd927bc628deb9ea2df40f06b5f79c5ccf355db732025a3e8e7152801f6", size = 9564639, upload-time = "2026-02-13T13:26:42.781Z" }, + { url = "https://files.pythonhosted.org/packages/d6/b8/280b04e14a9c0474af574f929fba2398b5e1c123c1e7735893b4cd73d13c/ty-0.0.17-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5383814d1d7a5cc53b3b07661856bab04bb2aac7a677c8d33c55169acdaa83df", size = 10061204, upload-time = "2026-02-13T13:27:00.152Z" }, + { url = "https://files.pythonhosted.org/packages/2a/d7/493e1607d8dfe48288d8a768a2adc38ee27ef50e57f0af41ff273987cda0/ty-0.0.17-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9c20423b8744b484f93e7bf2ef8a9724bca2657873593f9f41d08bd9f83444c9", size = 10013116, upload-time = "2026-02-13T13:26:34.543Z" }, + { url = "https://files.pythonhosted.org/packages/80/ef/22f3ed401520afac90dbdf1f9b8b7755d85b0d5c35c1cb35cf5bd11b59c2/ty-0.0.17-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e6f5b1aba97db9af86517b911674b02f5bc310750485dc47603a105bd0e83ddd", size = 10533623, upload-time = "2026-02-13T13:26:31.449Z" }, + { url = "https://files.pythonhosted.org/packages/75/ce/744b15279a11ac7138832e3a55595706b4a8a209c9f878e3ab8e571d9032/ty-0.0.17-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:488bce1a9bea80b851a97cd34c4d2ffcd69593d6c3f54a72ae02e5c6e47f3d0c", size = 11069750, upload-time = "2026-02-13T13:26:48.638Z" }, + { url = "https://files.pythonhosted.org/packages/f2/be/1133c91f15a0e00d466c24f80df486d630d95d1b2af63296941f7473812f/ty-0.0.17-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8df66b91ec84239420985ec215e7f7549bfda2ac036a3b3c065f119d1c06825a", size = 10870862, upload-time = "2026-02-13T13:26:54.715Z" }, + { url = "https://files.pythonhosted.org/packages/3e/4a/a2ed209ef215b62b2d3246e07e833081e07d913adf7e0448fc204be443d6/ty-0.0.17-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:002139e807c53002790dfefe6e2f45ab0e04012e76db3d7c8286f96ec121af8f", size = 10628118, upload-time = "2026-02-13T13:26:45.439Z" }, + { url = "https://files.pythonhosted.org/packages/b3/0c/87476004cb5228e9719b98afffad82c3ef1f84334bde8527bcacba7b18cb/ty-0.0.17-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6c4e01f05ce82e5d489ab3900ca0899a56c4ccb52659453780c83e5b19e2b64c", size = 10038185, upload-time = "2026-02-13T13:27:02.693Z" }, + { url = "https://files.pythonhosted.org/packages/46/4b/98f0b3ba9aef53c1f0305519536967a4aa793a69ed72677b0a625c5313ac/ty-0.0.17-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2b226dd1e99c0d2152d218c7e440150d1a47ce3c431871f0efa073bbf899e881", size = 10047644, upload-time = "2026-02-13T13:27:05.474Z" }, + { url = "https://files.pythonhosted.org/packages/93/e0/06737bb80aa1a9103b8651d2eb691a7e53f1ed54111152be25f4a02745db/ty-0.0.17-py3-none-musllinux_1_2_i686.whl", hash = "sha256:8b11f1da7859e0ad69e84b3c5ef9a7b055ceed376a432fad44231bdfc48061c2", size = 10231140, upload-time = "2026-02-13T13:27:10.844Z" }, + { url = "https://files.pythonhosted.org/packages/7c/79/e2a606bd8852383ba9abfdd578f4a227bd18504145381a10a5f886b4e751/ty-0.0.17-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:c04e196809ff570559054d3e011425fd7c04161529eb551b3625654e5f2434cb", size = 10718344, upload-time = "2026-02-13T13:26:51.66Z" }, + { url = "https://files.pythonhosted.org/packages/c5/2d/2663984ac11de6d78f74432b8b14ba64d170b45194312852b7543cf7fd56/ty-0.0.17-py3-none-win32.whl", hash = "sha256:305b6ed150b2740d00a817b193373d21f0767e10f94ac47abfc3b2e5a5aec809", size = 9672932, upload-time = "2026-02-13T13:27:08.522Z" }, + { url = "https://files.pythonhosted.org/packages/de/b5/39be78f30b31ee9f5a585969930c7248354db90494ff5e3d0756560fb731/ty-0.0.17-py3-none-win_amd64.whl", hash = "sha256:531828267527aee7a63e972f54e5eee21d9281b72baf18e5c2850c6b862add83", size = 10542138, upload-time = "2026-02-13T13:27:17.084Z" }, + { url = "https://files.pythonhosted.org/packages/40/b7/f875c729c5d0079640c75bad2c7e5d43edc90f16ba242f28a11966df8f65/ty-0.0.17-py3-none-win_arm64.whl", hash = "sha256:de9810234c0c8d75073457e10a84825b9cd72e6629826b7f01c7a0b266ae25b1", size = 10023068, upload-time = "2026-02-13T13:26:39.637Z" }, ] [[package]] From 136574fbcbb2ddcdd4aab3b97c98fbafceab5e5a Mon Sep 17 00:00:00 2001 From: David <49467229+TheSecurityDev@users.noreply.github.com> Date: Mon, 16 Feb 2026 11:47:20 -0600 Subject: [PATCH 054/311] ui replay: run with no window (#37229) run headless --- .github/workflows/tests.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 03e818f422..d892507319 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -274,8 +274,8 @@ jobs: run: > ${{ env.RUN }} "PYTHONWARNINGS=ignore && source selfdrive/test/setup_xvfb.sh && - WINDOWED=1 python3 selfdrive/ui/tests/diff/replay.py && - WINDOWED=1 python3 selfdrive/ui/tests/diff/replay.py --big" + python3 selfdrive/ui/tests/diff/replay.py && + python3 selfdrive/ui/tests/diff/replay.py --big" - name: Upload UI Report uses: actions/upload-artifact@v6 with: From 422885dce67732ccc847baef2917e81a0e01f2bd Mon Sep 17 00:00:00 2001 From: David <49467229+TheSecurityDev@users.noreply.github.com> Date: Mon, 16 Feb 2026 11:55:46 -0600 Subject: [PATCH 055/311] ui replay: cleanup and fix workflow todos (#37230) * fix: update pull request trigger and clean up workflow paths * fix other event names --- .github/workflows/ui_preview.yaml | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ui_preview.yaml b/.github/workflows/ui_preview.yaml index 3663257daf..72ced49852 100644 --- a/.github/workflows/ui_preview.yaml +++ b/.github/workflows/ui_preview.yaml @@ -3,15 +3,14 @@ on: push: branches: - master - pull_request: # TODO: change back to pull_request_target before merging + pull_request_target: types: [assigned, opened, synchronize, reopened, edited] branches: - 'master' - paths: # TODO: remove workflow path before merging + paths: - 'selfdrive/assets/**' - 'selfdrive/ui/**' - 'system/ui/**' - - '.github/workflows/ui_preview.yaml' workflow_dispatch: env: @@ -101,7 +100,7 @@ jobs: uses: AnimMouse/setup-ffmpeg@ae28d57dabbb148eff63170b6bf7f2b60062cbae - name: Finding diffs - if: github.event_name == 'pull_request' + if: github.event_name == 'pull_request_target' id: find_diff run: | export PYTHONPATH=${{ github.workspace }} @@ -139,7 +138,7 @@ jobs: } >> "$GITHUB_OUTPUT" - name: Saving proposed ui - if: github.event_name == 'pull_request' + if: github.event_name == 'pull_request_target' working-directory: ${{ github.workspace }}/master_mici run: | git config user.name "GitHub Actions Bot" @@ -164,7 +163,7 @@ jobs: git push origin ${{ env.REPORT_FILES_BRANCH_NAME }} - name: Comment on PR - if: github.event_name == 'pull_request' + if: github.event_name == 'pull_request_target' uses: thollander/actions-comment-pull-request@v2 with: message: | From 5d8e54ae3ed9444968152edc76355984131ab786 Mon Sep 17 00:00:00 2001 From: commaci-public <60409688+commaci-public@users.noreply.github.com> Date: Mon, 16 Feb 2026 10:17:46 -0800 Subject: [PATCH 056/311] [bot] Update Python packages (#37228) * Update Python packages * revert for now --------- Co-authored-by: Vehicle Researcher Co-authored-by: Adeeb Shihadeh --- opendbc_repo | 2 +- panda | 2 +- uv.lock | 12 ++++++------ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/opendbc_repo b/opendbc_repo index 245cb1f205..647441e42d 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit 245cb1f2056071a7625e2ad7e4515f57784515bd +Subproject commit 647441e42db5735e249344449ad857eeeeff7224 diff --git a/panda b/panda index 24fe11466d..b1191df619 160000 --- a/panda +++ b/panda @@ -1 +1 @@ -Subproject commit 24fe11466de37a1a761c16e35353ab39602e03e0 +Subproject commit b1191df619dbc201b9444634a29e2c016fee6619 diff --git a/uv.lock b/uv.lock index 51e71ce645..bd4632942a 100644 --- a/uv.lock +++ b/uv.lock @@ -1024,11 +1024,11 @@ wheels = [ [[package]] name = "platformdirs" -version = "4.9.1" +version = "4.9.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6c/d5/763666321efaded11112de8b7a7f2273dd8d1e205168e73c334e54b0ab9a/platformdirs-4.9.1.tar.gz", hash = "sha256:f310f16e89c4e29117805d8328f7c10876eeff36c94eac879532812110f7d39f", size = 28392, upload-time = "2026-02-14T21:02:44.973Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/04/fea538adf7dbbd6d186f551d595961e564a3b6715bdf276b477460858672/platformdirs-4.9.2.tar.gz", hash = "sha256:9a33809944b9db043ad67ca0db94b14bf452cc6aeaac46a88ea55b26e2e9d291", size = 28394, upload-time = "2026-02-16T03:56:10.574Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/70/77/e8c95e95f1d4cdd88c90a96e31980df7e709e51059fac150046ad67fac63/platformdirs-4.9.1-py3-none-any.whl", hash = "sha256:61d8b967d34791c162d30d60737369cbbd77debad5b981c4bfda1842e71e0d66", size = 21307, upload-time = "2026-02-14T21:02:43.492Z" }, + { url = "https://files.pythonhosted.org/packages/48/31/05e764397056194206169869b50cf2fee4dbbbc71b344705b9c0d878d4d8/platformdirs-4.9.2-py3-none-any.whl", hash = "sha256:9170634f126f8efdae22fb58ae8a0eaa86f38365bc57897a6c4f781d1f5875bd", size = 21168, upload-time = "2026-02-16T03:56:08.891Z" }, ] [[package]] @@ -1487,15 +1487,15 @@ wheels = [ [[package]] name = "sentry-sdk" -version = "2.52.0" +version = "2.53.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/59/eb/1b497650eb564701f9a7b8a95c51b2abe9347ed2c0b290ba78f027ebe4ea/sentry_sdk-2.52.0.tar.gz", hash = "sha256:fa0bec872cfec0302970b2996825723d67390cdd5f0229fb9efed93bd5384899", size = 410273, upload-time = "2026-02-04T15:03:54.706Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/06/66c8b705179bc54087845f28fd1b72f83751b6e9a195628e2e9af9926505/sentry_sdk-2.53.0.tar.gz", hash = "sha256:6520ef2c4acd823f28efc55e43eb6ce2e6d9f954a95a3aa96b6fd14871e92b77", size = 412369, upload-time = "2026-02-16T11:11:14.743Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ca/63/2c6daf59d86b1c30600bff679d039f57fd1932af82c43c0bde1cbc55e8d4/sentry_sdk-2.52.0-py2.py3-none-any.whl", hash = "sha256:931c8f86169fc6f2752cb5c4e6480f0d516112e78750c312e081ababecbaf2ed", size = 435547, upload-time = "2026-02-04T15:03:51.567Z" }, + { url = "https://files.pythonhosted.org/packages/47/d4/2fdf854bc3b9c7f55219678f812600a20a138af2dd847d99004994eada8f/sentry_sdk-2.53.0-py2.py3-none-any.whl", hash = "sha256:46e1ed8d84355ae54406c924f6b290c3d61f4048625989a723fd622aab838899", size = 437908, upload-time = "2026-02-16T11:11:13.227Z" }, ] [[package]] From 7fd131e01c0831a1c1789d3a0ca30e20028827e5 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Mon, 16 Feb 2026 11:00:12 -0800 Subject: [PATCH 057/311] mem_usage.py: switch to our tabulate --- selfdrive/debug/mem_usage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selfdrive/debug/mem_usage.py b/selfdrive/debug/mem_usage.py index 9bed566e19..3451bfc3d6 100755 --- a/selfdrive/debug/mem_usage.py +++ b/selfdrive/debug/mem_usage.py @@ -4,8 +4,8 @@ import os from collections import defaultdict import numpy as np -from tabulate import tabulate +from openpilot.common.utils import tabulate from openpilot.tools.lib.logreader import LogReader DEMO_ROUTE = "a2a0ccea32023010|2023-07-27--13-01-19" From 3661a014892ca8397c3e179ca520f5ffea749e16 Mon Sep 17 00:00:00 2001 From: David <49467229+TheSecurityDev@users.noreply.github.com> Date: Mon, 16 Feb 2026 16:05:43 -0600 Subject: [PATCH 058/311] ui diff: compare frame hashes instead of temp files (#37154) * refactor: streamline frame comparison by using frame hashes instead of extracting frames * add vsync Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- selfdrive/ui/tests/diff/diff.py | 62 ++++++++++++++------------------- 1 file changed, 27 insertions(+), 35 deletions(-) diff --git a/selfdrive/ui/tests/diff/diff.py b/selfdrive/ui/tests/diff/diff.py index bde6d44238..f2b88a3cad 100755 --- a/selfdrive/ui/tests/diff/diff.py +++ b/selfdrive/ui/tests/diff/diff.py @@ -2,7 +2,6 @@ import os import sys import subprocess -import tempfile import webbrowser import argparse from pathlib import Path @@ -11,17 +10,18 @@ from openpilot.common.basedir import BASEDIR DIFF_OUT_DIR = Path(BASEDIR) / "selfdrive" / "ui" / "tests" / "diff" / "report" -def extract_frames(video_path, output_dir): - output_pattern = str(output_dir / "frame_%04d.png") - cmd = ['ffmpeg', '-i', video_path, '-vsync', '0', output_pattern, '-y'] - subprocess.run(cmd, capture_output=True, check=True) - frames = sorted(output_dir.glob("frame_*.png")) - return frames - - -def compare_frames(frame1_path, frame2_path): - result = subprocess.run(['cmp', '-s', frame1_path, frame2_path]) - return result.returncode == 0 +def extract_framehashes(video_path): + cmd = ['ffmpeg', '-i', video_path, '-map', '0:v:0', '-vsync', '0', '-f', 'framehash', '-hash', 'md5', '-'] + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + hashes = [] + for line in result.stdout.splitlines(): + if not line or line.startswith('#'): + continue + parts = line.split(',') + if len(parts) < 4: + continue + hashes.append(parts[-1].strip()) + return hashes def create_diff_video(video1, video2, output_path): @@ -32,34 +32,26 @@ def create_diff_video(video1, video2, output_path): def find_differences(video1, video2): - with tempfile.TemporaryDirectory() as tmpdir: - tmpdir = Path(tmpdir) + print(f"Hashing frames from {video1}...") + hashes1 = extract_framehashes(video1) - print(f"Extracting frames from {video1}...") - frames1_dir = tmpdir / "frames1" - frames1_dir.mkdir() - frames1 = extract_frames(video1, frames1_dir) + print(f"Hashing frames from {video2}...") + hashes2 = extract_framehashes(video2) - print(f"Extracting frames from {video2}...") - frames2_dir = tmpdir / "frames2" - frames2_dir.mkdir() - frames2 = extract_frames(video2, frames2_dir) + if len(hashes1) != len(hashes2): + print(f"WARNING: Frame count mismatch: {len(hashes1)} vs {len(hashes2)}") + min_frames = min(len(hashes1), len(hashes2)) + hashes1 = hashes1[:min_frames] + hashes2 = hashes2[:min_frames] - if len(frames1) != len(frames2): - print(f"WARNING: Frame count mismatch: {len(frames1)} vs {len(frames2)}") - min_frames = min(len(frames1), len(frames2)) - frames1 = frames1[:min_frames] - frames2 = frames2[:min_frames] + print(f"Comparing {len(hashes1)} frames...") + different_frames = [] - print(f"Comparing {len(frames1)} frames...") - different_frames = [] + for i, (h1, h2) in enumerate(zip(hashes1, hashes2, strict=False)): + if h1 != h2: + different_frames.append(i) - for i, (f1, f2) in enumerate(zip(frames1, frames2, strict=False)): - is_different = not compare_frames(f1, f2) - if is_different: - different_frames.append(i) - - return different_frames, len(frames1) + return different_frames, len(hashes1) def generate_html_report(video1, video2, basedir, different_frames, total_frames, diff_video_name): From d984fb1bae75dacd503e7c1226cc606424107532 Mon Sep 17 00:00:00 2001 From: David <49467229+TheSecurityDev@users.noreply.github.com> Date: Mon, 16 Feb 2026 16:32:37 -0600 Subject: [PATCH 059/311] ui diff replay: better display replays of different lengths (#37116) * refactor: improve video synchronization logic in HTML report generation * feat: include description of which video is longer in report; refactor stuff and add types * refactor: simplify HTML report generation and remove extra formatting * reduce diff * fix video name * reduce diff * reduce diff * fix * parentheses * fix I guess --- selfdrive/ui/tests/diff/diff.py | 117 ++++++++++++++++---------------- 1 file changed, 58 insertions(+), 59 deletions(-) diff --git a/selfdrive/ui/tests/diff/diff.py b/selfdrive/ui/tests/diff/diff.py index f2b88a3cad..42efa381b2 100755 --- a/selfdrive/ui/tests/diff/diff.py +++ b/selfdrive/ui/tests/diff/diff.py @@ -31,19 +31,13 @@ def create_diff_video(video1, video2, output_path): subprocess.run(cmd, capture_output=True, check=True) -def find_differences(video1, video2): +def find_differences(video1, video2) -> tuple[list[int], tuple[int, int]]: print(f"Hashing frames from {video1}...") hashes1 = extract_framehashes(video1) print(f"Hashing frames from {video2}...") hashes2 = extract_framehashes(video2) - if len(hashes1) != len(hashes2): - print(f"WARNING: Frame count mismatch: {len(hashes1)} vs {len(hashes2)}") - min_frames = min(len(hashes1), len(hashes2)) - hashes1 = hashes1[:min_frames] - hashes2 = hashes2[:min_frames] - print(f"Comparing {len(hashes1)} frames...") different_frames = [] @@ -51,10 +45,10 @@ def find_differences(video1, video2): if h1 != h2: different_frames.append(i) - return different_frames, len(hashes1) + return different_frames, (len(hashes1), len(hashes2)) -def generate_html_report(video1, video2, basedir, different_frames, total_frames, diff_video_name): +def generate_html_report(videos: tuple[str, str], basedir: str, different_frames: list[int], frame_counts: tuple[int, int], diff_video_name): chunks = [] if different_frames: current_chunk = [different_frames[0]] @@ -66,66 +60,70 @@ def generate_html_report(video1, video2, basedir, different_frames, total_frames current_chunk = [different_frames[i]] chunks.append(current_chunk) + total_frames = max(frame_counts) + frame_delta = frame_counts[1] - frame_counts[0] + different_total = len(different_frames) + abs(frame_delta) + result_text = ( f"✅ Videos are identical! ({total_frames} frames)" - if len(different_frames) == 0 - else f"❌ Found {len(different_frames)} different frames out of {total_frames} total ({(len(different_frames) / total_frames * 100):.1f}%)" + if different_total == 0 + else f"❌ Found {different_total} different frames out of {total_frames} total ({different_total / total_frames * 100:.1f}%)." + + (f" Video {'2' if frame_delta > 0 else '1'} is longer by {abs(frame_delta)} frames." if frame_delta != 0 else "") ) + def render_video_cell(video_id: str, title: str, path: str, is_diff=False): + return f""" + +

{title}

+ + +""" + html = f"""

UI Diff

- - - +{render_video_cell("video1", "Video 1", videos[0])} +{render_video_cell("video2", "Video 2", videos[1])} +{render_video_cell("diffVideo", "Pixel Diff", diff_video_name, is_diff=True)}
-

Video 1

- -
-

Video 2

- -
-

Pixel Diff

- -

@@ -162,14 +160,14 @@ def main(): diff_video_path = str(DIFF_OUT_DIR / diff_video_name) create_diff_video(args.video1, args.video2, diff_video_path) - different_frames, total_frames = find_differences(args.video1, args.video2) + different_frames, frame_counts = find_differences(args.video1, args.video2) if different_frames is None: sys.exit(1) print() print("Generating HTML report...") - html = generate_html_report(args.video1, args.video2, args.basedir, different_frames, total_frames, diff_video_name) + html = generate_html_report((args.video1, args.video2), args.basedir, different_frames, frame_counts, diff_video_name) with open(DIFF_OUT_DIR / args.output, 'w') as f: f.write(html) @@ -179,7 +177,8 @@ def main(): print(f"Opening {args.output} in browser...") webbrowser.open(f'file://{os.path.abspath(DIFF_OUT_DIR / args.output)}') - return 0 if len(different_frames) == 0 else 1 + extra_frames = abs(frame_counts[0] - frame_counts[1]) + return 0 if (len(different_frames) + extra_frames) == 0 else 1 if __name__ == "__main__": From 037e6e749aacff472aebc1fbe71c276eafe97f03 Mon Sep 17 00:00:00 2001 From: Ahmed Harmouche Date: Tue, 17 Feb 2026 18:19:41 +0100 Subject: [PATCH 060/311] cabana: fix crash when zmq address is used (#37222) * Fix zmq support in cabana * Refactor to launch bridge, remove socketadapter * bridge_path should be camel_case --- tools/cabana/streams/devicestream.cc | 31 ++++++++++++++++++++++++++-- tools/cabana/streams/devicestream.h | 5 +++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/tools/cabana/streams/devicestream.cc b/tools/cabana/streams/devicestream.cc index 462dd7a361..20eaa70cd6 100644 --- a/tools/cabana/streams/devicestream.cc +++ b/tools/cabana/streams/devicestream.cc @@ -6,7 +6,9 @@ #include "cereal/services.h" #include +#include #include +#include #include #include #include @@ -17,12 +19,37 @@ DeviceStream::DeviceStream(QObject *parent, QString address) : zmq_address(address), LiveStream(parent) { } +DeviceStream::~DeviceStream() { + if (!bridge_process) + return; + + bridge_process->terminate(); + if (!bridge_process->waitForFinished(3000)) { + bridge_process->kill(); + bridge_process->waitForFinished(); + } +} + +void DeviceStream::start() { + if (!zmq_address.isEmpty()) { + bridge_process = new QProcess(this); + QString bridge_path = QCoreApplication::applicationDirPath() + "/../../cereal/messaging/bridge"; + bridge_process->start(QFileInfo(bridge_path).absoluteFilePath(), QStringList { zmq_address, "/\"can/\"" }); + + if (!bridge_process->waitForStarted()) { + QMessageBox::warning(nullptr, tr("Error"), tr("Failed to start bridge: %1").arg(bridge_process->errorString())); + return; + } + } + + LiveStream::start(); +} + void DeviceStream::streamThread() { zmq_address.isEmpty() ? unsetenv("ZMQ") : setenv("ZMQ", "1", 1); 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, false, true, services.at("can").queue_size)); + std::unique_ptr sock(SubSocket::create(context.get(), "can", "127.0.0.1", false, true, services.at("can").queue_size)); assert(sock != NULL); // run as fast as messages come in while (!QThread::currentThread()->isInterruptionRequested()) { diff --git a/tools/cabana/streams/devicestream.h b/tools/cabana/streams/devicestream.h index 3bdf224998..6beb300d7a 100644 --- a/tools/cabana/streams/devicestream.h +++ b/tools/cabana/streams/devicestream.h @@ -2,16 +2,21 @@ #include "tools/cabana/streams/livestream.h" +#include + class DeviceStream : public LiveStream { Q_OBJECT public: DeviceStream(QObject *parent, QString address = {}); + ~DeviceStream(); inline QString routeName() const override { return QString("Live Streaming From %1").arg(zmq_address.isEmpty() ? "127.0.0.1" : zmq_address); } protected: + void start() override; void streamThread() override; + QProcess *bridge_process = nullptr; const QString zmq_address; }; From e8f65bc6520e2af5472b085fb17040a8eea6880d Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Tue, 17 Feb 2026 13:17:03 -0500 Subject: [PATCH 061/311] Controls: Support for Torque Lateral Control v0 Tune (#1693) * init * no * more * tree it * final * comment * only with enforce torque * only with enforce torque * missed * no * lint * Apply suggestion from @sunnyhaibin * sunnylink metadata sync * Apply suggestion from @sunnyhaibin --------- Co-authored-by: nayan --- common/params_keys.h | 1 + selfdrive/controls/controlsd.py | 2 + .../steering_sub_layouts/torque_settings.py | 79 ++++++++++- .../selfdrive/controls/controlsd_ext.py | 12 ++ .../controls/lib/latcontrol_torque_v0.py | 128 ++++++++++++++++++ .../lib/latcontrol_torque_versions.json | 8 ++ sunnypilot/sunnylink/params_metadata.json | 18 +++ .../sunnylink/tests/test_params_sync.py | 82 +++++++++++ .../sunnylink/tools/update_params_metadata.py | 29 ++++ 9 files changed, 357 insertions(+), 2 deletions(-) create mode 100644 sunnypilot/selfdrive/controls/lib/latcontrol_torque_v0.py create mode 100644 sunnypilot/selfdrive/controls/lib/latcontrol_torque_versions.json diff --git a/common/params_keys.h b/common/params_keys.h index dba0742268..6f27d9582d 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -268,6 +268,7 @@ inline static std::unordered_map keys = { {"EnforceTorqueControl", {PERSISTENT | BACKUP, BOOL}}, {"LiveTorqueParamsToggle", {PERSISTENT | BACKUP , BOOL}}, {"LiveTorqueParamsRelaxedToggle", {PERSISTENT | BACKUP , BOOL}}, + {"TorqueControlTune", {PERSISTENT | BACKUP, FLOAT}}, {"TorqueParamsOverrideEnabled", {PERSISTENT | BACKUP, BOOL, "0"}}, {"TorqueParamsOverrideFriction", {PERSISTENT | BACKUP, FLOAT, "0.1"}}, {"TorqueParamsOverrideLatAccelFactor", {PERSISTENT | BACKUP, FLOAT, "2.5"}}, diff --git a/selfdrive/controls/controlsd.py b/selfdrive/controls/controlsd.py index 9d0e5c9f15..12b146e80b 100755 --- a/selfdrive/controls/controlsd.py +++ b/selfdrive/controls/controlsd.py @@ -64,6 +64,8 @@ class Controls(ControlsExt): elif self.CP.lateralTuning.which() == 'torque': self.LaC = LatControlTorque(self.CP, self.CP_SP, self.CI, DT_CTRL) + self.LaC = ControlsExt.initialize_lateral_control(self, self.LaC, self.CI, DT_CTRL) + def update(self): self.sm.update(15) if self.sm.updated["liveCalibration"]: diff --git a/selfdrive/ui/sunnypilot/layouts/settings/steering_sub_layouts/torque_settings.py b/selfdrive/ui/sunnypilot/layouts/settings/steering_sub_layouts/torque_settings.py index dceee8b518..b4302e1abd 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/steering_sub_layouts/torque_settings.py +++ b/selfdrive/ui/sunnypilot/layouts/settings/steering_sub_layouts/torque_settings.py @@ -4,15 +4,24 @@ Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. This file is part of sunnypilot and is licensed under the MIT License. See the LICENSE.md file in the root directory for more details. """ +import json +import math +import os from collections.abc import Callable import pyray as rl +from openpilot.common.basedir import BASEDIR from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.multilang import tr -from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp, option_item_sp +from openpilot.system.ui.sunnypilot.lib.utils import NoElideButtonAction +from openpilot.system.ui.sunnypilot.widgets.list_view import ListItemSP, toggle_item_sp, option_item_sp +from openpilot.system.ui.sunnypilot.widgets.tree_dialog import TreeOptionDialog, TreeFolder, TreeNode +from openpilot.system.ui.widgets import Widget, DialogResult from openpilot.system.ui.widgets.network import NavButton from openpilot.system.ui.widgets.scroller_tici import Scroller -from openpilot.system.ui.widgets import Widget + +TORQUE_VERSIONS_PATH = os.path.join(BASEDIR, "sunnypilot", "selfdrive", "controls", "lib", "latcontrol_torque_versions.json") class TorqueSettingsLayout(Widget): @@ -20,10 +29,23 @@ class TorqueSettingsLayout(Widget): super().__init__() self._back_button = NavButton(tr("Back")) self._back_button.set_click_callback(back_btn_callback) + self._torque_version_dialog: TreeOptionDialog | None = None + self.cached_torque_versions = {} + self._load_versions() items = self._initialize_items() self._scroller = Scroller(items, line_separator=True, spacing=0) + def _load_versions(self): + with open(TORQUE_VERSIONS_PATH) as f: + self.cached_torque_versions = json.load(f) + def _initialize_items(self): + self._torque_control_versions = ListItemSP( + title=tr("Torque Control Tune Version"), + description="Select the version of Torque Control Tune to use.", + action_item=NoElideButtonAction(tr("SELECT")), + callback=self._show_torque_version_dialog, + ) self._self_tune_toggle = toggle_item_sp( param="LiveTorqueParamsToggle", title=lambda: tr("Self-Tune"), @@ -73,6 +95,7 @@ class TorqueSettingsLayout(Widget): ) items = [ + self._torque_control_versions, self._self_tune_toggle, self._relaxed_tune_toggle, self._custom_tune_toggle, @@ -103,6 +126,7 @@ class TorqueSettingsLayout(Widget): title_text = tr("Real-Time & Offline") if ui_state.params.get("TorqueParamsOverrideEnabled") else tr("Offline Only") self._torque_lat_accel_factor.set_title(lambda: tr("Lateral Acceleration Factor") + " (" + title_text + ")") self._torque_friction.set_title(lambda: tr("Friction") + " (" + title_text + ")") + self._torque_control_versions.action_item.set_value(self._get_current_torque_version_label()) def _render(self, rect): self._back_button.set_position(self._rect.x, self._rect.y + 20) @@ -113,3 +137,54 @@ class TorqueSettingsLayout(Widget): def show_event(self): self._scroller.show_event() + + def _get_current_torque_version_label(self): + current_val_bytes = ui_state.params.get("TorqueControlTune") + if current_val_bytes is None: + return tr("Default") + + try: + current_val = float(current_val_bytes) + for label, info in self.cached_torque_versions.items(): + if math.isclose(float(info["version"]), current_val, rel_tol=1e-5): + return label + except (ValueError, KeyError): + pass + + return tr("Default") + + def _show_torque_version_dialog(self): + options_map = {} + for label, info in self.cached_torque_versions.items(): + try: + options_map[label] = float(info["version"]) + except (ValueError, KeyError): + pass + + # Sort options by label in descending order + sorted_labels = sorted(options_map.keys(), key=lambda k: options_map[k], reverse=True) + + nodes = [TreeNode(tr("Default"))] + for label in sorted_labels: + nodes.append(TreeNode(label)) + + folders = [TreeFolder("", nodes)] + + current_label = self._get_current_torque_version_label() + + def handle_selection(result: int): + if result == DialogResult.CONFIRM and self._torque_version_dialog: + selected_ref = self._torque_version_dialog.selection_ref + if selected_ref == tr("Default"): + ui_state.params.remove("TorqueControlTune") + elif selected_ref in options_map: + ui_state.params.put("TorqueControlTune", options_map[selected_ref]) + self._torque_version_dialog = None + + self._torque_version_dialog = TreeOptionDialog( + tr("Select Torque Control Tune Version"), + folders, + current_ref=current_label, + option_font_weight=FontWeight.UNIFONT, + ) + gui_app.set_modal_overlay(self._torque_version_dialog, callback=handle_selection) diff --git a/sunnypilot/selfdrive/controls/controlsd_ext.py b/sunnypilot/selfdrive/controls/controlsd_ext.py index 3f6053d158..3f6ef2b439 100644 --- a/sunnypilot/selfdrive/controls/controlsd_ext.py +++ b/sunnypilot/selfdrive/controls/controlsd_ext.py @@ -16,6 +16,7 @@ from openpilot.sunnypilot import PARAMS_UPDATE_PERIOD from openpilot.sunnypilot.livedelay.helpers import get_lat_delay from openpilot.sunnypilot.modeld.modeld_base import ModelStateBase from openpilot.sunnypilot.selfdrive.controls.lib.blinker_pause_lateral import BlinkerPauseLateral +from openpilot.sunnypilot.selfdrive.controls.lib.latcontrol_torque_v0 import LatControlTorque as LatControlTorqueV0 class ControlsExt(ModelStateBase): @@ -33,6 +34,17 @@ class ControlsExt(ModelStateBase): self.sm_services_ext = ['radarState', 'selfdriveStateSP'] self.pm_services_ext = ['carControlSP'] + def initialize_lateral_control(self, lac, CI, dt): + enforce_torque_control = self.params.get_bool("EnforceTorqueControl") + torque_versions = self.params.get("TorqueControlTune") + if not enforce_torque_control: + return lac + + if torque_versions == 0.0: # v0 + return LatControlTorqueV0(self.CP, self.CP_SP, CI, dt) + else: + return lac + def get_params_sp(self, sm: messaging.SubMaster) -> None: if time.monotonic() - self._param_update_time > PARAMS_UPDATE_PERIOD: self.blinker_pause_lateral.get_params() diff --git a/sunnypilot/selfdrive/controls/lib/latcontrol_torque_v0.py b/sunnypilot/selfdrive/controls/lib/latcontrol_torque_v0.py new file mode 100644 index 0000000000..f7874cc539 --- /dev/null +++ b/sunnypilot/selfdrive/controls/lib/latcontrol_torque_v0.py @@ -0,0 +1,128 @@ +import math +import numpy as np +from collections import deque + +from cereal import log +from opendbc.car.lateral import get_friction +from openpilot.common.constants import ACCELERATION_DUE_TO_GRAVITY +from openpilot.common.filter_simple import FirstOrderFilter +from openpilot.selfdrive.controls.lib.latcontrol import LatControl +from openpilot.common.pid import PIDController + +from openpilot.sunnypilot.selfdrive.controls.lib.latcontrol_torque_ext import LatControlTorqueExt + +# At higher speeds (25+mph) we can assume: +# Lateral acceleration achieved by a specific car correlates to +# torque applied to the steering rack. It does not correlate to +# wheel slip, or to speed. + +# This controller applies torque to achieve desired lateral +# accelerations. To compensate for the low speed effects the +# proportional gain is increased at low speeds by the PID controller. +# Additionally, there is friction in the steering wheel that needs +# to be overcome to move it at all, this is compensated for too. + +KP = 1.0 +KI = 0.3 +KD = 0.0 +INTERP_SPEEDS = [1, 1.5, 2.0, 3.0, 5, 7.5, 10, 15, 30] +KP_INTERP = [250, 120, 65, 30, 11.5, 5.5, 3.5, 2.0, KP] + +LP_FILTER_CUTOFF_HZ = 1.2 +LAT_ACCEL_REQUEST_BUFFER_SECONDS = 1.0 +FRICTION_THRESHOLD = 0.3 +VERSION = 0 + + +class LatControlTorque(LatControl): + def __init__(self, CP, CP_SP, CI, dt): + super().__init__(CP, CP_SP, CI, dt) + self.torque_params = CP.lateralTuning.torque.as_builder() + self.torque_from_lateral_accel = CI.torque_from_lateral_accel() + self.lateral_accel_from_torque = CI.lateral_accel_from_torque() + self.pid = PIDController([INTERP_SPEEDS, KP_INTERP], KI, KD, rate=1/self.dt) + self.update_limits() + self.steering_angle_deadzone_deg = self.torque_params.steeringAngleDeadzoneDeg + self.lat_accel_request_buffer_len = int(LAT_ACCEL_REQUEST_BUFFER_SECONDS / self.dt) + self.lat_accel_request_buffer = deque([0.] * self.lat_accel_request_buffer_len , maxlen=self.lat_accel_request_buffer_len) + self.previous_measurement = 0.0 + self.measurement_rate_filter = FirstOrderFilter(0.0, 1 / (2 * np.pi * LP_FILTER_CUTOFF_HZ), self.dt) + + self.extension = LatControlTorqueExt(self, CP, CP_SP, CI) + + def update_live_torque_params(self, latAccelFactor, latAccelOffset, friction): + self.torque_params.latAccelFactor = latAccelFactor + self.torque_params.latAccelOffset = latAccelOffset + self.torque_params.friction = friction + self.update_limits() + + def update_limits(self): + self.pid.set_limits(self.lateral_accel_from_torque(self.steer_max, self.torque_params), + self.lateral_accel_from_torque(-self.steer_max, self.torque_params)) + + def update(self, active, CS, VM, params, steer_limited_by_safety, desired_curvature, calibrated_pose, curvature_limited, lat_delay): + # Override torque params from extension + if self.extension.update_override_torque_params(self.torque_params): + self.update_limits() + + pid_log = log.ControlsState.LateralTorqueState.new_message() + pid_log.version = VERSION + if not active: + output_torque = 0.0 + pid_log.active = False + else: + measured_curvature = -VM.calc_curvature(math.radians(CS.steeringAngleDeg - params.angleOffsetDeg), CS.vEgo, params.roll) + roll_compensation = params.roll * ACCELERATION_DUE_TO_GRAVITY + curvature_deadzone = abs(VM.calc_curvature(math.radians(self.steering_angle_deadzone_deg), CS.vEgo, 0.0)) + lateral_accel_deadzone = curvature_deadzone * CS.vEgo ** 2 + + delay_frames = int(np.clip(lat_delay / self.dt, 1, self.lat_accel_request_buffer_len)) + expected_lateral_accel = self.lat_accel_request_buffer[-delay_frames] + # TODO factor out lateral jerk from error to later replace it with delay independent alternative + future_desired_lateral_accel = desired_curvature * CS.vEgo ** 2 + self.lat_accel_request_buffer.append(future_desired_lateral_accel) + gravity_adjusted_future_lateral_accel = future_desired_lateral_accel - roll_compensation + desired_lateral_jerk = (future_desired_lateral_accel - expected_lateral_accel) / lat_delay + + measurement = measured_curvature * CS.vEgo ** 2 + measurement_rate = self.measurement_rate_filter.update((measurement - self.previous_measurement) / self.dt) + self.previous_measurement = measurement + + setpoint = lat_delay * desired_lateral_jerk + expected_lateral_accel + error = setpoint - measurement + + # do error correction in lateral acceleration space, convert at end to handle non-linear torque responses correctly + pid_log.error = float(error) + ff = gravity_adjusted_future_lateral_accel + # latAccelOffset corrects roll compensation bias from device roll misalignment relative to car roll + ff -= self.torque_params.latAccelOffset + # TODO jerk is weighted by lat_delay for legacy reasons, but should be made independent of it + ff += get_friction(error, lateral_accel_deadzone, FRICTION_THRESHOLD, self.torque_params) + + freeze_integrator = steer_limited_by_safety or CS.steeringPressed or CS.vEgo < 5 + output_lataccel = self.pid.update(pid_log.error, + -measurement_rate, + feedforward=ff, + speed=CS.vEgo, + freeze_integrator=freeze_integrator) + output_torque = self.torque_from_lateral_accel(output_lataccel, self.torque_params) + + # Lateral acceleration torque controller extension updates + # Overrides pid_log.error and output_torque + pid_log, output_torque = self.extension.update(CS, VM, self.pid, params, ff, pid_log, setpoint, measurement, calibrated_pose, roll_compensation, + future_desired_lateral_accel, measurement, lateral_accel_deadzone, gravity_adjusted_future_lateral_accel, + desired_curvature, measured_curvature, steer_limited_by_safety, output_torque) + + pid_log.active = True + pid_log.p = float(self.pid.p) + pid_log.i = float(self.pid.i) + pid_log.d = float(self.pid.d) + pid_log.f = float(self.pid.f) + pid_log.output = float(-output_torque) # TODO: log lat accel? + pid_log.actualLateralAccel = float(measurement) + pid_log.desiredLateralAccel = float(setpoint) + pid_log.desiredLateralJerk = float(desired_lateral_jerk) + pid_log.saturated = bool(self._check_saturation(self.steer_max - abs(output_torque) < 1e-3, CS, steer_limited_by_safety, curvature_limited)) + + # TODO left is positive in this convention + return -output_torque, 0.0, pid_log diff --git a/sunnypilot/selfdrive/controls/lib/latcontrol_torque_versions.json b/sunnypilot/selfdrive/controls/lib/latcontrol_torque_versions.json new file mode 100644 index 0000000000..21b5884a01 --- /dev/null +++ b/sunnypilot/selfdrive/controls/lib/latcontrol_torque_versions.json @@ -0,0 +1,8 @@ +{ + "v1.0": { + "version": "1.0" + }, + "v0.0": { + "version": "0.0" + } +} diff --git a/sunnypilot/sunnylink/params_metadata.json b/sunnypilot/sunnylink/params_metadata.json index ad94ad35ff..67ce903aba 100644 --- a/sunnypilot/sunnylink/params_metadata.json +++ b/sunnypilot/sunnylink/params_metadata.json @@ -1247,6 +1247,24 @@ "title": "[TIZI/TICI only] Steering Arc", "description": "Display steering arc on the driving screen when lateral control is enabled." }, + "TorqueControlTune": { + "title": "Torque Control Tune Version", + "description": "Select the version of Torque Control Tune to use.", + "options": [ + { + "value": "", + "label": "Default" + }, + { + "value": 1.0, + "label": "v1.0" + }, + { + "value": 0.0, + "label": "v0.0" + } + ] + }, "TorqueParamsOverrideEnabled": { "title": "Manual Real-Time Tuning", "description": "" diff --git a/sunnypilot/sunnylink/tests/test_params_sync.py b/sunnypilot/sunnylink/tests/test_params_sync.py index 26bdca42d6..05114de49a 100644 --- a/sunnypilot/sunnylink/tests/test_params_sync.py +++ b/sunnypilot/sunnylink/tests/test_params_sync.py @@ -200,3 +200,85 @@ def test_known_params_metadata(): assert acc_long["min"] == 1 assert acc_long["max"] == 10 assert acc_long["step"] == 1 + + +def test_torque_control_tune_versions_in_sync(): + """ + Test that TorqueControlTune options in params_metadata.json match versions in latcontrol_torque_versions.json. + + Why: + The TorqueControlTune dropdown in the UI should always reflect the available torque tune versions. + If versions are added/removed from latcontrol_torque_versions.json, the metadata must be updated accordingly. + + Expected: + - TorqueControlTune should have a 'Default' option with empty string value + - All versions from latcontrol_torque_versions.json should be present in the options + - The version values and labels should match between both files + """ + from openpilot.common.basedir import BASEDIR + + versions_json_path = os.path.join(BASEDIR, "sunnypilot", "selfdrive", "controls", "lib", "latcontrol_torque_versions.json") + sync_script_path = "python3 sunnypilot/sunnylink/tools/sync_torque_versions.py" + + # Load both files + with open(METADATA_PATH) as f: + metadata = json.load(f) + + with open(versions_json_path) as f: + versions = json.load(f) + + # Get TorqueControlTune metadata + torque_tune = metadata.get("TorqueControlTune") + if torque_tune is None: + pytest.fail(f"TorqueControlTune not found in params_metadata.json. Please run '{sync_script_path}' to sync.") + + if "options" not in torque_tune: + pytest.fail(f"TorqueControlTune must have options. Please run '{sync_script_path}' to sync.") + + options = torque_tune["options"] + if not isinstance(options, list): + pytest.fail(f"TorqueControlTune options must be a list. Please run '{sync_script_path}' to sync.") + + if len(options) == 0: + pytest.fail(f"TorqueControlTune must have at least one option. Please run '{sync_script_path}' to sync.") + + # Check that Default option exists + default_option = next((opt for opt in options if opt.get("value") == ""), None) + if default_option is None: + pytest.fail(f"TorqueControlTune must have a 'Default' option with empty string value. Please run '{sync_script_path}' to sync.") + + if default_option.get("label") != "Default": + pytest.fail(f"Default option must have label 'Default'. Please run '{sync_script_path}' to sync.") + + # Build expected options from versions.json + expected_version_keys = set(versions.keys()) + actual_version_keys = set() + + for option in options: + if option.get("value") == "": + continue # Skip the default option + + label = option.get("label") + value = option.get("value") + + # Check that this option corresponds to a version + if label not in versions: + pytest.fail(f"Option label '{label}' not found in latcontrol_torque_versions.json. Please run '{sync_script_path}' to sync.") + + # Check that the value matches the version number + expected_value = float(versions[label]["version"]) + if value != expected_value: + pytest.fail(f"Option '{label}' has value {value}, expected {expected_value}. Please run '{sync_script_path}' to sync.") + + actual_version_keys.add(label) + + # Check that all versions are represented + missing_versions = expected_version_keys - actual_version_keys + if missing_versions: + pytest.fail(f"The following versions are missing from TorqueControlTune options: {missing_versions}. " + + f"Please run '{sync_script_path}' to sync.") + + extra_versions = actual_version_keys - expected_version_keys + if extra_versions: + pytest.fail("The following versions in TorqueControlTune options are not in latcontrol_torque_versions.json: " + + f"{extra_versions}. Please run '{sync_script_path}' to sync.") diff --git a/sunnypilot/sunnylink/tools/update_params_metadata.py b/sunnypilot/sunnylink/tools/update_params_metadata.py index ac2ef556e6..3ef91debe0 100755 --- a/sunnypilot/sunnylink/tools/update_params_metadata.py +++ b/sunnypilot/sunnylink/tools/update_params_metadata.py @@ -8,9 +8,11 @@ See the LICENSE.md file in the root directory for more details. import json import os +from openpilot.common.basedir import BASEDIR from openpilot.common.params import Params METADATA_PATH = os.path.join(os.path.dirname(__file__), "../params_metadata.json") +TORQUE_VERSIONS_JSON = os.path.join(BASEDIR, "sunnypilot", "selfdrive", "controls", "lib", "latcontrol_torque_versions.json") def main(): @@ -51,6 +53,33 @@ def main(): print(f"Updated {METADATA_PATH}") + # update torque versions param + update_torque_versions_param() + +def update_torque_versions_param(): + with open(TORQUE_VERSIONS_JSON) as f: + current_versions = json.load(f) + + try: + with open(METADATA_PATH) as f: + params_metadata = json.load(f) + + options = [{"value": "", "label": "Default"}] + for version_key, version_data in current_versions.items(): + version_value = float(version_data["version"]) + options.append({"value": version_value, "label": str(version_key)}) + + if "TorqueControlTune" in params_metadata: + params_metadata["TorqueControlTune"]["options"] = options + + with open(METADATA_PATH, 'w') as f: + json.dump(params_metadata, f, indent=2) + f.write('\n') + + print(f"Updated TorqueControlTune options in params_metadata.json with {len(options)} options: \n{options}") + + except Exception as e: + print(f"Failed to update TorqueControlTune versions in params_metadata.json: {e}") if __name__ == "__main__": main() From 43d162e8fbb8ab84b233e541e68113e9499120ae Mon Sep 17 00:00:00 2001 From: felsager <76905857+felsager@users.noreply.github.com> Date: Tue, 17 Feb 2026 11:49:26 -0800 Subject: [PATCH 062/311] mpc_longitudinal_tuning_report: use enum for axis (#37231) --- .../maneuver_helpers.py | 18 +++++ .../mpc_longitudinal_tuning_report.py | 75 +++++++------------ 2 files changed, 47 insertions(+), 46 deletions(-) create mode 100644 tools/longitudinal_maneuvers/maneuver_helpers.py diff --git a/tools/longitudinal_maneuvers/maneuver_helpers.py b/tools/longitudinal_maneuvers/maneuver_helpers.py new file mode 100644 index 0000000000..9fc65fb9e6 --- /dev/null +++ b/tools/longitudinal_maneuvers/maneuver_helpers.py @@ -0,0 +1,18 @@ +from enum import IntEnum + +class Axis(IntEnum): + TIME = 0 + EGO_POSITION = 1 + LEAD_DISTANCE= 2 + EGO_V = 3 + LEAD_V = 4 + EGO_A = 5 + D_REL = 6 + +axis_labels = {Axis.TIME: 'Time (s)', + Axis.EGO_POSITION: 'Ego position (m)', + Axis.LEAD_DISTANCE: 'Lead absolute position (m)', + Axis.EGO_V: 'Ego Velocity (m/s)', + Axis.LEAD_V: 'Lead Velocity (m/s)', + Axis.EGO_A: 'Ego acceleration (m/s^2)', + Axis.D_REL: 'Lead distance (m)'} diff --git a/tools/longitudinal_maneuvers/mpc_longitudinal_tuning_report.py b/tools/longitudinal_maneuvers/mpc_longitudinal_tuning_report.py index 7623f669cd..ae3fee7355 100644 --- a/tools/longitudinal_maneuvers/mpc_longitudinal_tuning_report.py +++ b/tools/longitudinal_maneuvers/mpc_longitudinal_tuning_report.py @@ -5,29 +5,16 @@ import numpy as np import matplotlib.pyplot as plt from openpilot.common.realtime import DT_MDL from openpilot.selfdrive.controls.tests.test_following_distance import desired_follow_distance +from openpilot.tools.longitudinal_maneuvers.maneuver_helpers import Axis, axis_labels from openpilot.selfdrive.test.longitudinal_maneuvers.maneuver import Maneuver -TIME = 0 -LEAD_DISTANCE= 2 -EGO_V = 3 -EGO_A = 5 -D_REL = 6 - -axis_labels = ['Time (s)', - 'Ego position (m)', - 'Lead absolute position (m)', - 'Ego Velocity (m/s)', - 'Lead Velocity (m/s)', - 'Ego acceleration (m/s^2)', - 'Lead distance (m)' - ] def get_html_from_results(results, labels, AXIS): fig, ax = plt.subplots(figsize=(16, 8)) - for idx, speed in enumerate(list(results.keys())): - ax.plot(results[speed][:, TIME], results[speed][:, AXIS], label=labels[idx]) + for idx, key in enumerate(results.keys()): + ax.plot(results[key][:, Axis.TIME], results[key][:, AXIS], label=labels[idx]) - ax.set_xlabel('Time (s)') + ax.set_xlabel(axis_labels[Axis.TIME]) ax.set_ylabel(axis_labels[AXIS]) ax.legend(bbox_to_anchor=(1.02, 1), loc='upper left', borderaxespad=0) ax.grid(True, linestyle='--', alpha=0.7) @@ -60,8 +47,8 @@ def generate_mpc_tuning_report(): labels.append(f'{lead_accel} m/s^2 lead acceleration') htmls.append(markdown.markdown('# ' + name)) - htmls.append(get_html_from_results(results, labels, EGO_V)) - htmls.append(get_html_from_results(results, labels, EGO_A)) + htmls.append(get_html_from_results(results, labels, Axis.EGO_V)) + htmls.append(get_html_from_results(results, labels, Axis.EGO_A)) results = {} @@ -78,12 +65,11 @@ def generate_mpc_tuning_report(): breakpoints=[0., 30.], ) valid, results[speed] = man.evaluate() - results[speed][:,2] = results[speed][:,2] - results[speed][:,1] labels.append(f'{speed} m/s approach speed') htmls.append(markdown.markdown('# ' + name)) - htmls.append(get_html_from_results(results, labels, EGO_A)) - htmls.append(get_html_from_results(results, labels, D_REL)) + htmls.append(get_html_from_results(results, labels, Axis.EGO_A)) + htmls.append(get_html_from_results(results, labels, Axis.D_REL)) results = {} @@ -104,9 +90,9 @@ def generate_mpc_tuning_report(): labels.append(f'{oscil} m/s oscillation size') htmls.append(markdown.markdown('# ' + name)) - htmls.append(get_html_from_results(results, labels, D_REL)) - htmls.append(get_html_from_results(results, labels, EGO_V)) - htmls.append(get_html_from_results(results, labels, EGO_A)) + htmls.append(get_html_from_results(results, labels, Axis.D_REL)) + htmls.append(get_html_from_results(results, labels, Axis.EGO_V)) + htmls.append(get_html_from_results(results, labels, Axis.EGO_A)) results = {} @@ -128,12 +114,12 @@ def generate_mpc_tuning_report(): breakpoints=bps, ) valid, results[oscil] = man.evaluate() - labels.append(f'{oscil} m/s oscilliation size') + labels.append(f'{oscil} m/s oscillation size') htmls.append(markdown.markdown('# ' + name)) - htmls.append(get_html_from_results(results, labels, D_REL)) - htmls.append(get_html_from_results(results, labels, EGO_V)) - htmls.append(get_html_from_results(results, labels, EGO_A)) + htmls.append(get_html_from_results(results, labels, Axis.D_REL)) + htmls.append(get_html_from_results(results, labels, Axis.EGO_V)) + htmls.append(get_html_from_results(results, labels, Axis.EGO_A)) results = {} @@ -150,12 +136,11 @@ def generate_mpc_tuning_report(): breakpoints=[0.], ) valid, results[distance] = man.evaluate() - results[distance][:,2] = results[distance][:,2] - results[distance][:,1] labels.append(f'{distance} m initial distance') htmls.append(markdown.markdown('# ' + name)) - htmls.append(get_html_from_results(results, labels, EGO_V)) - htmls.append(get_html_from_results(results, labels, D_REL)) + htmls.append(get_html_from_results(results, labels, Axis.EGO_V)) + htmls.append(get_html_from_results(results, labels, Axis.D_REL)) results = {} @@ -172,12 +157,11 @@ def generate_mpc_tuning_report(): breakpoints=[0.], ) valid, results[distance] = man.evaluate() - results[distance][:,2] = results[distance][:,2] - results[distance][:,1] labels.append(f'{distance} m initial distance') htmls.append(markdown.markdown('# ' + name)) - htmls.append(get_html_from_results(results, labels, EGO_V)) - htmls.append(get_html_from_results(results, labels, D_REL)) + htmls.append(get_html_from_results(results, labels, Axis.EGO_V)) + htmls.append(get_html_from_results(results, labels, Axis.D_REL)) results = {} @@ -195,12 +179,11 @@ def generate_mpc_tuning_report(): breakpoints=[0., 5., 5 + stop_time], ) valid, results[stop_time] = man.evaluate() - results[stop_time][:,2] = results[stop_time][:,2] - results[stop_time][:,1] labels.append(f'{stop_time} seconds stop time') htmls.append(markdown.markdown('# ' + name)) - htmls.append(get_html_from_results(results, labels, EGO_A)) - htmls.append(get_html_from_results(results, labels, D_REL)) + htmls.append(get_html_from_results(results, labels, Axis.EGO_A)) + htmls.append(get_html_from_results(results, labels, Axis.D_REL)) results = {} @@ -222,8 +205,8 @@ def generate_mpc_tuning_report(): labels.append(f'{speed} m/s speed') htmls.append(markdown.markdown('# ' + name)) - htmls.append(get_html_from_results(results, labels, EGO_A)) - htmls.append(get_html_from_results(results, labels, D_REL)) + htmls.append(get_html_from_results(results, labels, Axis.EGO_A)) + htmls.append(get_html_from_results(results, labels, Axis.D_REL)) results = {} @@ -244,8 +227,8 @@ def generate_mpc_tuning_report(): labels.append(f'{speed} m/s speed') htmls.append(markdown.markdown('# ' + name)) - htmls.append(get_html_from_results(results, labels, EGO_V)) - htmls.append(get_html_from_results(results, labels, EGO_A)) + htmls.append(get_html_from_results(results, labels, Axis.EGO_V)) + htmls.append(get_html_from_results(results, labels, Axis.EGO_A)) results = {} @@ -267,8 +250,8 @@ def generate_mpc_tuning_report(): labels.append(f'{speed} m/s speed') htmls.append(markdown.markdown('# ' + name)) - htmls.append(get_html_from_results(results, labels, EGO_V)) - htmls.append(get_html_from_results(results, labels, EGO_A)) + htmls.append(get_html_from_results(results, labels, Axis.EGO_V)) + htmls.append(get_html_from_results(results, labels, Axis.EGO_A)) results = {} @@ -290,8 +273,8 @@ def generate_mpc_tuning_report(): labels.append(f'{speed} m/s speed') htmls.append(markdown.markdown('# ' + name)) - htmls.append(get_html_from_results(results, labels, EGO_V)) - htmls.append(get_html_from_results(results, labels, EGO_A)) + htmls.append(get_html_from_results(results, labels, Axis.EGO_V)) + htmls.append(get_html_from_results(results, labels, Axis.EGO_A)) return htmls From 0a98ee9e4f2ff45e5129db38b40c9c5ae69b3395 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 17 Feb 2026 14:56:08 -0800 Subject: [PATCH 063/311] WifiUii: rm separate connecting status (#37233) rm connecting --- .../mici/layouts/settings/network/wifi_ui.py | 19 +------------------ system/ui/lib/wifi_manager.py | 18 +++++++++--------- 2 files changed, 10 insertions(+), 27 deletions(-) diff --git a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py index b27e03ed14..679372404a 100644 --- a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py +++ b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py @@ -343,20 +343,16 @@ class WifiUIMici(BigMultiOptionDialog): self.set_back_callback(back_callback) self._network_info_page = NetworkInfoPage(wifi_manager, self._connect_to_network, wifi_manager.forget_connection, self._open_network_manage_page) - self._network_info_page.set_connecting(lambda: self._connecting) + self._network_info_page.set_connecting(lambda: wifi_manager.connecting_to_ssid) self._loading_animation = LoadingAnimation() self._wifi_manager = wifi_manager - self._connecting: str | None = None self._networks: dict[str, Network] = {} self._wifi_manager.add_callbacks( need_auth=self._on_need_auth, - activated=self._on_activated, - forgotten=self._on_forgotten, networks_updated=self._on_network_updated, - disconnected=self._on_disconnected, ) def show_event(self): @@ -402,7 +398,6 @@ class WifiUIMici(BigMultiOptionDialog): 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() @@ -420,11 +415,9 @@ class WifiUIMici(BigMultiOptionDialog): return if network.is_saved: - self._connecting = network.ssid self._wifi_manager.activate_connection(network.ssid) self._update_buttons() elif network.security_type == SecurityType.OPEN: - self._connecting = network.ssid self._wifi_manager.connect_to_network(network.ssid, "") self._update_buttons() else: @@ -443,16 +436,6 @@ class WifiUIMici(BigMultiOptionDialog): gui_app.set_modal_overlay_tick(self._wifi_manager.process_callbacks) gui_app.set_modal_overlay(dlg, on_close) - def _on_activated(self): - self._connecting = None - - def _on_forgotten(self, ssid): - if self._connecting == ssid: - self._connecting = None - - def _on_disconnected(self): - self._connecting = None - def _render(self, _): super()._render(_) diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index 2a4a9ab711..e1343137ad 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -166,8 +166,8 @@ class WifiManager: # State self._connections: dict[str, str] = {} # ssid -> connection path, updated via NM signals - self._connecting_to_ssid: str = "" - self._prev_connecting_to_ssid: str = "" + self._connecting_to_ssid: str | None = None + self._prev_connecting_to_ssid: str | None = None self._ipv4_address: str = "" self._current_network_metered: MeteredType = MeteredType.UNKNOWN self._tethering_password: str = "" @@ -236,7 +236,7 @@ class WifiManager: return self._current_network_metered @property - def connecting_to_ssid(self) -> str: + def connecting_to_ssid(self) -> str | None: return self._connecting_to_ssid @property @@ -333,21 +333,21 @@ class WifiManager: if failed_ssid: self._enqueue_callbacks(self._need_auth, failed_ssid) self.forget_connection(failed_ssid, block=True) - self._prev_connecting_to_ssid = "" + self._prev_connecting_to_ssid = None if self._connecting_to_ssid == failed_ssid: - self._connecting_to_ssid = "" + self._connecting_to_ssid = None elif new_state == NMDeviceState.ACTIVATED: if len(self._activated): self._update_networks() self._enqueue_callbacks(self._activated) - self._prev_connecting_to_ssid = "" - self._connecting_to_ssid = "" + self._prev_connecting_to_ssid = None + self._connecting_to_ssid = None elif new_state == NMDeviceState.DISCONNECTED and change_reason != NM_DEVICE_STATE_REASON_NEW_ACTIVATION: self._enqueue_callbacks(self._forgotten, self._connecting_to_ssid) - self._prev_connecting_to_ssid = "" - self._connecting_to_ssid = "" + self._prev_connecting_to_ssid = None + self._connecting_to_ssid = None def _network_scanner(self): while not self._exit: From 7dc56dc064907fc2de4bcd3bd4bae6292c44d084 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 17 Feb 2026 15:01:07 -0800 Subject: [PATCH 064/311] draw black bg behind BigButton --- selfdrive/ui/mici/widgets/button.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/selfdrive/ui/mici/widgets/button.py b/selfdrive/ui/mici/widgets/button.py index 101f3bb56e..3fb5bce964 100644 --- a/selfdrive/ui/mici/widgets/button.py +++ b/selfdrive/ui/mici/widgets/button.py @@ -220,6 +220,10 @@ class BigButton(Widget): btn_x = self._rect.x + (self._rect.width * (1 - scale)) / 2 btn_y = self._rect.y + (self._rect.height * (1 - scale)) / 2 + # draw black background since images are transparent + scaled_rect = rl.Rectangle(btn_x, btn_y, self._rect.width * scale, self._rect.height * scale) + rl.draw_rectangle_rounded(scaled_rect, 0.4, 7, rl.Color(0, 0, 0, int(255 * 0.5))) + self._draw_content(btn_y) rl.draw_texture_ex(txt_bg, (btn_x, btn_y), 0, scale, rl.WHITE) From e527b463a5163ebbbd95e3f2ca8c43f6ebe36412 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Tue, 17 Feb 2026 15:02:06 -0800 Subject: [PATCH 065/311] Revert "Drop support for Intel macOS (#37215)" (#37234) This reverts commit eea07462fad217a2e98decf7392818dbb0f22478. --- SConstruct | 17 ++++++++-- scripts/platform.sh | 51 ---------------------------- third_party/acados/build.sh | 6 ++-- third_party/libyuv/build.sh | 10 ++++-- third_party/raylib/build.sh | 8 +++-- tools/install_python_dependencies.sh | 2 +- tools/mac_setup.sh | 1 - tools/op.sh | 28 ++++++++++++++- 8 files changed, 59 insertions(+), 64 deletions(-) delete mode 100755 scripts/platform.sh diff --git a/SConstruct b/SConstruct index 05885c0b5d..4f04be624c 100644 --- a/SConstruct +++ b/SConstruct @@ -2,6 +2,7 @@ import os import subprocess import sys import sysconfig +import platform import shlex import numpy as np @@ -23,8 +24,19 @@ AddOption('--minimal', default=os.path.exists(File('#.gitattributes').abspath), # minimal by default on release branch (where there's no LFS) help='the minimum build to run openpilot. no tests, tools, etc.') -# Detect platform (see scripts/platform.sh) -arch = subprocess.check_output(["bash", "-c", "source scripts/platform.sh >&2 && echo $OPENPILOT_ARCH"], encoding='utf8', stderr=subprocess.PIPE).rstrip() +# Detect platform +arch = subprocess.check_output(["uname", "-m"], encoding='utf8').rstrip() +if platform.system() == "Darwin": + arch = "Darwin" + brew_prefix = subprocess.check_output(['brew', '--prefix'], encoding='utf8').strip() +elif arch == "aarch64" and os.path.isfile('/TICI'): + arch = "larch64" +assert arch in [ + "larch64", # linux tici arm64 + "aarch64", # linux pc arm64 + "x86_64", # linux pc x64 + "Darwin", # macOS arm64 (x86 not supported) +] env = Environment( ENV={ @@ -91,7 +103,6 @@ if arch == "larch64": env.Append(CCFLAGS=arch_flags) env.Append(CXXFLAGS=arch_flags) elif arch == "Darwin": - brew_prefix = subprocess.check_output(['brew', '--prefix'], encoding='utf8').strip() env.Append(LIBPATH=[ f"{brew_prefix}/lib", f"{brew_prefix}/opt/openssl@3.0/lib", diff --git a/scripts/platform.sh b/scripts/platform.sh deleted file mode 100755 index 1cbd8aa607..0000000000 --- a/scripts/platform.sh +++ /dev/null @@ -1,51 +0,0 @@ -#!/usr/bin/env bash -# -# Centralized platform and architecture detection for openpilot. -# Source this script to get OPENPILOT_ARCH set to one of: -# larch64 - linux tici arm64 -# aarch64 - linux pc arm64 -# x86_64 - linux pc x64 -# Darwin - macOS arm64 -# - -RED='\033[0;31m' -GREEN='\033[0;32m' -NC='\033[0m' - -OPENPILOT_ARCH=$(uname -m) - -# ── check OS and normalize arch ────────────────────────────── -if [ -f /TICI ]; then - # TICI runs AGNOS — no OS validation needed - OPENPILOT_ARCH="larch64" - -elif [[ "$OSTYPE" == "darwin"* ]]; then - if [[ "$OPENPILOT_ARCH" == "x86_64" ]]; then - echo -e " ↳ [${RED}✗${NC}] Intel-based Macs are not supported!" - echo " openpilot requires an Apple Silicon Mac (M1 or newer)." - exit 1 - fi - echo -e " ↳ [${GREEN}✔${NC}] macOS detected." - OPENPILOT_ARCH="Darwin" - -elif [[ "$OSTYPE" == "linux-gnu"* ]]; then - if [ -f "/etc/os-release" ]; then - source /etc/os-release - case "$VERSION_CODENAME" in - "jammy" | "kinetic" | "noble" | "focal") - echo -e " ↳ [${GREEN}✔${NC}] Ubuntu $VERSION_CODENAME detected." - ;; - *) - echo -e " ↳ [${RED}✗${NC}] Incompatible Ubuntu version $VERSION_CODENAME detected!" - exit 1 - ;; - esac - else - echo -e " ↳ [${RED}✗${NC}] No /etc/os-release on your system. Make sure you're running on Ubuntu, or similar!" - exit 1 - fi - -else - echo -e " ↳ [${RED}✗${NC}] OS type $OSTYPE not supported!" - exit 1 -fi diff --git a/third_party/acados/build.sh b/third_party/acados/build.sh index cca1743506..95b3913c4a 100755 --- a/third_party/acados/build.sh +++ b/third_party/acados/build.sh @@ -6,11 +6,10 @@ export ZERO_AR_DATE=1 DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" -source "$DIR/../../scripts/platform.sh" -ARCHNAME="$OPENPILOT_ARCH" - +ARCHNAME="x86_64" BLAS_TARGET="X64_AUTOMATIC" if [ -f /TICI ]; then + ARCHNAME="larch64" BLAS_TARGET="ARMV8A_ARM_CORTEX_A57" fi @@ -18,6 +17,7 @@ ACADOS_FLAGS="-DACADOS_WITH_QPOASES=ON -UBLASFEO_TARGET -DBLASFEO_TARGET=$BLAS_T if [[ "$OSTYPE" == "darwin"* ]]; then ACADOS_FLAGS="$ACADOS_FLAGS -DCMAKE_OSX_ARCHITECTURES=arm64 -DCMAKE_MACOSX_RPATH=1" + ARCHNAME="Darwin" fi if [ ! -d acados_repo/ ]; then diff --git a/third_party/libyuv/build.sh b/third_party/libyuv/build.sh index 0e4e10133b..11f88ab46c 100755 --- a/third_party/libyuv/build.sh +++ b/third_party/libyuv/build.sh @@ -6,8 +6,14 @@ export ZERO_AR_DATE=1 DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" -source "$DIR/../../scripts/platform.sh" -ARCHNAME="$OPENPILOT_ARCH" +ARCHNAME=$(uname -m) +if [ -f /TICI ]; then + ARCHNAME="larch64" +fi + +if [[ "$OSTYPE" == "darwin"* ]]; then + ARCHNAME="Darwin" +fi cd $DIR if [ ! -d libyuv ]; then diff --git a/third_party/raylib/build.sh b/third_party/raylib/build.sh index b8408cd88c..d20f9d33af 100755 --- a/third_party/raylib/build.sh +++ b/third_party/raylib/build.sh @@ -20,9 +20,9 @@ cd $DIR RAYLIB_PLATFORM="PLATFORM_DESKTOP" -source "$DIR/../../scripts/platform.sh" -ARCHNAME="$OPENPILOT_ARCH" +ARCHNAME=$(uname -m) if [ -f /TICI ]; then + ARCHNAME="larch64" RAYLIB_PLATFORM="PLATFORM_COMMA" elif [[ "$OSTYPE" == "linux"* ]]; then # required dependencies on Linux PC @@ -33,6 +33,10 @@ elif [[ "$OSTYPE" == "linux"* ]]; then libxrandr-dev fi +if [[ "$OSTYPE" == "darwin"* ]]; then + ARCHNAME="Darwin" +fi + INSTALL_DIR="$DIR/$ARCHNAME" rm -rf $INSTALL_DIR mkdir -p $INSTALL_DIR diff --git a/tools/install_python_dependencies.sh b/tools/install_python_dependencies.sh index bf7accbbd5..4454845fcd 100755 --- a/tools/install_python_dependencies.sh +++ b/tools/install_python_dependencies.sh @@ -23,7 +23,7 @@ echo "installing python packages..." uv sync --frozen --all-extras source .venv/bin/activate -if [[ "$OSTYPE" == "darwin"* ]]; then +if [[ "$(uname)" == 'Darwin' ]]; then touch "$ROOT"/.env echo "export OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES" >> "$ROOT"/.env fi diff --git a/tools/mac_setup.sh b/tools/mac_setup.sh index 7e126d4f57..82748e9613 100755 --- a/tools/mac_setup.sh +++ b/tools/mac_setup.sh @@ -3,7 +3,6 @@ set -e DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" ROOT="$(cd $DIR/../ && pwd)" -source $ROOT/scripts/platform.sh # homebrew update is slow export HOMEBREW_NO_AUTO_UPDATE=1 diff --git a/tools/op.sh b/tools/op.sh index afee4cfb7e..8c41926e0c 100755 --- a/tools/op.sh +++ b/tools/op.sh @@ -122,7 +122,33 @@ function op_check_git() { function op_check_os() { echo "Checking for compatible os version..." - source "$OPENPILOT_ROOT/scripts/platform.sh" + if [[ "$OSTYPE" == "linux-gnu"* ]]; then + + if [ -f "/etc/os-release" ]; then + source /etc/os-release + case "$VERSION_CODENAME" in + "jammy" | "kinetic" | "noble" | "focal") + echo -e " ↳ [${GREEN}✔${NC}] Ubuntu $VERSION_CODENAME detected." + ;; + * ) + echo -e " ↳ [${RED}✗${NC}] Incompatible Ubuntu version $VERSION_CODENAME detected!" + loge "ERROR_INCOMPATIBLE_UBUNTU" "$VERSION_CODENAME" + return 1 + ;; + esac + else + echo -e " ↳ [${RED}✗${NC}] No /etc/os-release on your system. Make sure you're running on Ubuntu, or similar!" + loge "ERROR_UNKNOWN_UBUNTU" + return 1 + fi + + elif [[ "$OSTYPE" == "darwin"* ]]; then + echo -e " ↳ [${GREEN}✔${NC}] macOS detected." + else + echo -e " ↳ [${RED}✗${NC}] OS type $OSTYPE not supported!" + loge "ERROR_UNKNOWN_OS" "$OSTYPE" + return 1 + fi } function op_check_python() { From 14f3f6dd1a3989a9d79721cbd37669d59e132383 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 17 Feb 2026 15:02:20 -0800 Subject: [PATCH 066/311] WifiManager: fix forgotten callback signature --- system/ui/lib/wifi_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index e1343137ad..42432f4bd9 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -185,7 +185,7 @@ class WifiManager: # Callbacks self._need_auth: list[Callable[[str], None]] = [] self._activated: list[Callable[[], None]] = [] - self._forgotten: list[Callable[[str], None]] = [] + self._forgotten: list[Callable[[str | None], None]] = [] self._networks_updated: list[Callable[[list[Network]], None]] = [] self._disconnected: list[Callable[[], None]] = [] From 1f85860f7e8631892796c374319c61e3bb45a82a Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 17 Feb 2026 16:16:05 -0800 Subject: [PATCH 067/311] WifiManager: always update networks after activation --- system/ui/lib/wifi_manager.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index 42432f4bd9..fc3160e641 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -338,8 +338,7 @@ class WifiManager: self._connecting_to_ssid = None elif new_state == NMDeviceState.ACTIVATED: - if len(self._activated): - self._update_networks() + self._update_networks() self._enqueue_callbacks(self._activated) self._prev_connecting_to_ssid = None self._connecting_to_ssid = None From fd34659dc37924d404063723b6d2fbd75fa3ca7d Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 17 Feb 2026 16:25:44 -0800 Subject: [PATCH 068/311] NetworkManager: add more device states (#37235) * safe * missing states * add enum for nmdevicestatereason * rm for now * fix links --- system/ui/lib/networkmanager.py | 19 +++++++++++++++---- system/ui/lib/wifi_manager.py | 12 +++++------- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/system/ui/lib/networkmanager.py b/system/ui/lib/networkmanager.py index 19d54e6516..f16271a505 100644 --- a/system/ui/lib/networkmanager.py +++ b/system/ui/lib/networkmanager.py @@ -3,17 +3,31 @@ from enum import IntEnum # NetworkManager device states class NMDeviceState(IntEnum): + # https://networkmanager.dev/docs/api/1.46/nm-dbus-types.html#NMDeviceState UNKNOWN = 0 + UNMANAGED = 10 + UNAVAILABLE = 20 DISCONNECTED = 30 PREPARE = 40 - STATE_CONFIG = 50 + CONFIG = 50 NEED_AUTH = 60 IP_CONFIG = 70 + IP_CHECK = 80 + SECONDARIES = 90 ACTIVATED = 100 DEACTIVATING = 110 FAILED = 120 +class NMDeviceStateReason(IntEnum): + # https://networkmanager.dev/docs/api/1.46/nm-dbus-types.html#NMDeviceStateReason + NONE = 0 + UNKNOWN = 1 + NO_SECRETS = 7 + SUPPLICANT_DISCONNECT = 8 + NEW_ACTIVATION = 60 + + # NetworkManager constants NM = "org.freedesktop.NetworkManager" NM_PATH = '/org/freedesktop/NetworkManager' @@ -30,9 +44,6 @@ NM_IP4_CONFIG_IFACE = 'org.freedesktop.NetworkManager.IP4Config' NM_DEVICE_TYPE_WIFI = 2 NM_DEVICE_TYPE_MODEM = 8 -NM_DEVICE_STATE_REASON_NO_SECRETS = 7 -NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT = 8 -NM_DEVICE_STATE_REASON_NEW_ACTIVATION = 60 # https://developer.gnome.org/NetworkManager/1.26/nm-dbus-types.html#NM80211ApFlags NM_802_11_AP_FLAGS_NONE = 0x0 diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index fc3160e641..a0d684c789 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -23,10 +23,8 @@ from openpilot.system.ui.lib.networkmanager import (NM, NM_WIRELESS_IFACE, NM_80 NM_802_11_AP_FLAGS_PRIVACY, NM_802_11_AP_FLAGS_WPS, NM_PATH, NM_IFACE, NM_ACCESS_POINT_IFACE, NM_SETTINGS_PATH, NM_SETTINGS_IFACE, NM_CONNECTION_IFACE, NM_DEVICE_IFACE, - NM_DEVICE_TYPE_WIFI, NM_DEVICE_TYPE_MODEM, NM_DEVICE_STATE_REASON_NO_SECRETS, - NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT, - NM_DEVICE_STATE_REASON_NEW_ACTIVATION, NM_ACTIVE_CONNECTION_IFACE, - NM_IP4_CONFIG_IFACE, NM_PROPERTIES_IFACE, NMDeviceState) + NM_DEVICE_TYPE_WIFI, NM_DEVICE_TYPE_MODEM, NM_ACTIVE_CONNECTION_IFACE, + NM_IP4_CONFIG_IFACE, NM_PROPERTIES_IFACE, NMDeviceState, NMDeviceStateReason) try: from openpilot.common.params import Params @@ -327,8 +325,8 @@ class WifiManager: # BAD PASSWORD - use prev if current has already moved on to a new connection # - strong network rejects with NEED_AUTH+SUPPLICANT_DISCONNECT # - weak/gone network fails with FAILED+NO_SECRETS - if ((new_state == NMDeviceState.NEED_AUTH and change_reason == NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT) or - (new_state == NMDeviceState.FAILED and change_reason == NM_DEVICE_STATE_REASON_NO_SECRETS)): + if ((new_state == NMDeviceState.NEED_AUTH and change_reason == NMDeviceStateReason.SUPPLICANT_DISCONNECT) or + (new_state == NMDeviceState.FAILED and change_reason == NMDeviceStateReason.NO_SECRETS)): failed_ssid = self._prev_connecting_to_ssid or self._connecting_to_ssid if failed_ssid: self._enqueue_callbacks(self._need_auth, failed_ssid) @@ -343,7 +341,7 @@ class WifiManager: self._prev_connecting_to_ssid = None self._connecting_to_ssid = None - elif new_state == NMDeviceState.DISCONNECTED and change_reason != NM_DEVICE_STATE_REASON_NEW_ACTIVATION: + elif new_state == NMDeviceState.DISCONNECTED and change_reason != NMDeviceStateReason.NEW_ACTIVATION: self._enqueue_callbacks(self._forgotten, self._connecting_to_ssid) self._prev_connecting_to_ssid = None self._connecting_to_ssid = None From 4f407dabcd59a38618e2b20e732de43f4d2b6112 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Tue, 17 Feb 2026 19:36:01 -0500 Subject: [PATCH 069/311] ci: fix update translations by enable submodule checkout in repo maintenance (#37236) --- .github/workflows/repo-maintenance.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/repo-maintenance.yaml b/.github/workflows/repo-maintenance.yaml index ec361536d8..55d1c2052c 100644 --- a/.github/workflows/repo-maintenance.yaml +++ b/.github/workflows/repo-maintenance.yaml @@ -16,6 +16,8 @@ jobs: if: github.repository == 'commaai/openpilot' steps: - uses: actions/checkout@v6 + with: + submodules: true - uses: ./.github/workflows/setup-with-retry - name: Update translations run: | From c07ddcefb09f221aa878b8f99224808e8fb621ae Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Tue, 17 Feb 2026 19:48:37 -0500 Subject: [PATCH 070/311] version: bump to 2026.001.000 --- sunnypilot/common/version.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sunnypilot/common/version.h b/sunnypilot/common/version.h index 6833a508de..1c7fcd2e64 100644 --- a/sunnypilot/common/version.h +++ b/sunnypilot/common/version.h @@ -1 +1 @@ -#define SUNNYPILOT_VERSION "2025.003.000" +#define SUNNYPILOT_VERSION "2026.001.000" From 80e23509ba349c8b1b1492150257a21d4bfaf9ec Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Tue, 17 Feb 2026 22:33:28 -0500 Subject: [PATCH 071/311] Update CHANGELOG.md --- CHANGELOG.md | 100 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 99 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 61bc81bafc..d22324bcf9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,103 @@ -sunnypilot Version 2025.003.000 (20xx-xx-xx) +sunnypilot Version 2026.001.000 (2026-03-xx) ======================== +* What's Changed (sunnypilot/sunnypilot) + * Complete rewrite of the user interface from Qt C++ to Raylib Python + * comma four support + * ui: sunnypilot toggle style by @nayan8teen + * ui: fix scroll panel mouse wheel behavior by @nayan8teen + * ui: sunnypilot panels by @nayan8teen + * sunnylink: centralize key pair handling in sunnylink registration by @devtekve + * ui: reimplement sunnypilot branding with Raylib by @sunnyhaibin + * ui: Platform Selector by @Discountchubbs + * ui: vehicle brand settings by @Discountchubbs + * ui: sunnylink client-side implementation by @nayan8teen + * ui: `NetworkUISP` by @Discountchubbs + * ui: add sunnypilot font by @nayan8teen + * ui: sunnypilot sponsor tier color mapping by @sunnyhaibin + * ui: sunnylink panel by @nayan8teen + * ui: Models panel by @Discountchubbs + * ui: software panel by @Discountchubbs + * modeld_v2: support planplus outputs by @Discountchubbs + * ui: OSM panel by @Discountchubbs + * ui: Developer panel extension by @Discountchubbs + * sunnylink: Vehicle Selector support by @sunnyhaibin + * [TIZI/TICI] ui: Developer Metrics by @rav4kumar + * [comma 4] ui: sunnylink panel by @nayan8teen + * ui: lateral-only and longitudinal-only UI statuses support by @royjr + * sunnylink: elliptic curve keys support and improve key path handling by @nayan8teen + * sunnylink: block remote modification of SSH key parameters by @zikeji + * [TIZI/TICI] ui: rainbow path by @rav4kumar + * [TIZI/TICI] ui: chevron metrics by @rav4kumar + * ui: include MADS enabled state to `engaged` check by @sunnyhaibin + * Toyota: Enforce Factory Longitudinal Control by @sunnyhaibin + * ui: fix malformed dongle ID display on the PC if dongleID is not set by @dzid26 + * SL: Re enable and validate ingestion of swaglogs by @devtekve + * modeld_v2: planplus model tuning by @Discountchubbs + * ui: fix Always Offroad button visibility by @nayan8teen + * Reimplement sunnypilot Terms of Service & sunnylink Consent Screens by @sunnyhaibin + * [TIZI/TICI] ui: update dmoji position and Developer UI adjustments by @rav4kumar + * modeld: configurable camera offset by @Discountchubbs + * [TIZI/TICI] ui: sunnylink status on sidebar by @Copilot + * ui: Global Brightness Override by @nayan8teen + * ui: Customizable Interactive Timeout by @sunnyhaibin + * sunnylink: add units to param metadata by @nayan8teen + * ui: Customizable Onroad Brightness by @sunnyhaibin + * [TIZI/TICI] ui: Steering panel by @nayan8teen + * [TIZI/TICI] ui: Rocket Fuel by @rav4kumar + * [TIZI/TICI] ui: MICI style turn signals by @rav4kumar + * [TIZI/TICI] ui: MICI style blindspot indicators by @sunnyhaibin + * [MICI] ui: display blindspot indicators when available by @rav4kumar + * [TIZI/TICI] ui: Road Name by @rav4kumar + * [TIZI/TICI] ui: Blue "Exit Always Offroad" button by @dzid26 + * [TIZI/TICI] ui: Speed Limit by @rav4kumar + * Reapply "latcontrol_torque: lower kp and lower friction threshold (commaai/openpilot#36619)" by @sunnyhaibin + * [TIZI/TICI] ui: steering arc by @royjr + * [TIZI/TICI] ui: Smart Cruise Control elements by @sunnyhaibin + * [TIZI/TICI] ui: Green Light and Lead Departure elements by @sunnyhaibin + * [TIZI/TICI] ui: standstill timer by @sunnyhaibin + * [MICI] ui: driving models selector by @Discountchubbs + * [TIZI/TICI] ui: Hide vEgo and True vEgo by @sunnyhaibin + * [TIZI/TICI] ui: Visuals panel by @nayan8teen + * Device: Retain QuickBoot state after op switch by @nayan8teen + * [TIZI/TICI] ui: Trips panel by @sunnyhaibin + * [TIZI/TICI] ui: dynamic ICBM status by @sunnyhaibin + * [TIZI/TICI] ui: Cruise panel by @sunnyhaibin + * ui: better wake mode support by @nayan8teen + * Pause Lateral Control with Blinker: Post-Blinker Delay by @CHaucke89 + * SCC-V: Use p97 for predicted lateral accel by @yasu-oh + * Controls: Support for Torque Lateral Control v0 Tune by @sunnyhaibin +* What's Changed (sunnypilot/opendbc) + * Honda: DBC for Accord 9th Generation by @mvl-boston + * FCA: update tire stiffness values for `RAM_HD` by @dparring + * Honda: Nidec hybrid baseline brake support by @mvl-boston + * Subaru Global Gen2: bump steering limits and update tuning by @sunnyhaibin + * Toyota: Enforce Stock Longitudinal Control by @rav4kumar + * Nissan: use MADS enabled status for LKAS HUD logic by @downquark7 + * Reapply "Lateral: lower friction threshold (#2915)" (#378) by @sunnyhaibin + * HKG: add KIA_FORTE_2019_NON_SCC fingerprint by @royjr + * Nissan: Parse cruise control buttons by @downquark7 + * Rivian: Add stalk down ACC behavior to match stock Rivian by @lukasloetkolben + * Tesla: remove `TESLA_MODEL_X` from `dashcamOnly` by @ssysm + * Hyundai Longitudinal: refactor tuning by @Discountchubbs + * Tesla: add fingerprint for Model 3 Performance HW4 by @sunnyhaibin + * Toyota: do not disable radar when smartDSU or CAN Filter detected by @sunnyhaibin + * Honda: add missing `GasInterceptor` messages to Taiwan Odyssey DBC by @mvl-boston + * GM: remove `CHEVROLET_EQUINOX_NON_ACC_3RD_GEN` from `dashcamOnly` by @sunnyhaibin +* New Contributors (sunnypilot/sunnypilot) + * @TheSecurityDev made their first contribution in "ui: fix sidebar scroll in UI screenshots" + * @zikeji made their first contribution in "sunnylink: block remote modification of SSH key parameters" + * @Candy0707 made their first contribution in "[TIZI/TICI] ui: Fix misaligned turn signals and blindspot indicators with sidebar" + * @CHaucke89 made their first contribution in "Pause Lateral Control with Blinker: Post-Blinker Delay" + * @yasu-oh made their first contribution in "SCC-V: Use p97 for predicted lateral accel" +* New Contributors (sunnypilot/opendbc) + * @AmyJeanes made their first contribution in "Tesla: Fix stock LKAS being blocked when MADS is enabled" + * @mvl-boston made their first contribution in "Honda: Update Clarity brake to renamed DBC message name" + * @dzid26 made their first contribution in "Tesla: Parse speed limit from CAN" + * @firestar5683 made their first contribution in "GM: Non-ACC platforms with steering only support" + * @downquark7 made their first contribution in "Nissan: use MADS enabled status for LKAS HUD logic" + * @royjr made their first contribution in "HKG: add KIA_FORTE_2019_NON_SCC fingerprint" + * @ssysm made their first contribution in "Tesla: remove `TESLA_MODEL_X` from `dashcamOnly`" +* Full Changelog: https://github.com/sunnypilot/sunnypilot/compare/v2025.002.000...v2026.001.000 sunnypilot Version 2025.002.000 (2025-11-06) ======================== From d6238c285a5c7be6bc7620154a2624c49f1ebb4b Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 17 Feb 2026 19:41:31 -0800 Subject: [PATCH 072/311] ui: disable tethering password while updating (#37240) * setting completed * add back * try * try * only pass * just tehteringk --- selfdrive/ui/mici/layouts/settings/network/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/selfdrive/ui/mici/layouts/settings/network/__init__.py b/selfdrive/ui/mici/layouts/settings/network/__init__.py index bda619feef..3854e998e9 100644 --- a/selfdrive/ui/mici/layouts/settings/network/__init__.py +++ b/selfdrive/ui/mici/layouts/settings/network/__init__.py @@ -36,6 +36,7 @@ class NetworkLayoutMici(NavWidget): # ******** Tethering ******** def tethering_toggle_callback(checked: bool): self._tethering_toggle_btn.set_enabled(False) + self._tethering_password_btn.set_enabled(False) self._network_metered_btn.set_enabled(False) self._wifi_manager.set_tethering_active(checked) @@ -43,6 +44,8 @@ class NetworkLayoutMici(NavWidget): def tethering_password_callback(password: str): if password: + self._tethering_toggle_btn.set_enabled(False) + self._tethering_password_btn.set_enabled(False) self._wifi_manager.set_tethering_password(password) def tethering_password_clicked(): @@ -155,6 +158,7 @@ class NetworkLayoutMici(NavWidget): tethering_active = self._wifi_manager.is_tethering_active() # TODO: use real signals (like activated/settings changed, etc.) to speed up re-enabling buttons self._tethering_toggle_btn.set_enabled(True) + self._tethering_password_btn.set_enabled(True) self._network_metered_btn.set_enabled(lambda: not tethering_active and bool(self._wifi_manager.ipv4_address)) self._tethering_toggle_btn.set_checked(tethering_active) From 028f5ca1f4bd741e32ea1c2cd439ebbb09afad14 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 17 Feb 2026 19:52:37 -0800 Subject: [PATCH 073/311] WifiUi: fix flickering IP and network metered (#37242) fix flickering ip and network metered --- system/ui/lib/wifi_manager.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index a0d684c789..6737946f4d 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -705,8 +705,8 @@ class WifiManager: threading.Thread(target=worker, daemon=True).start() def _update_active_connection_info(self): - self._ipv4_address = "" - self._current_network_metered = MeteredType.UNKNOWN + ipv4_address = "" + metered = MeteredType.UNKNOWN for active_conn in self._get_active_connections(): conn_addr = DBusAddress(active_conn, bus_name=NM, interface=NM_ACTIVE_CONNECTION_IFACE) @@ -728,7 +728,7 @@ class WifiManager: for entry in address_data: if 'address' in entry: - self._ipv4_address = entry['address'][1] + ipv4_address = entry['address'][1] break # Metered status @@ -740,10 +740,13 @@ class WifiManager: metered_prop = settings['connection'].get('metered', ('i', 0))[1] if metered_prop == MeteredType.YES: - self._current_network_metered = MeteredType.YES + metered = MeteredType.YES elif metered_prop == MeteredType.NO: - self._current_network_metered = MeteredType.NO - return + metered = MeteredType.NO + break + + self._ipv4_address = ipv4_address + self._current_network_metered = metered def __del__(self): self.stop() From 735c2fb48e00cd63ce0b3c84e3a1a06ad9e3a801 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 17 Feb 2026 21:24:38 -0800 Subject: [PATCH 074/311] WifiManager: active WiFi connection helper (#37244) * short circuit * rename * move some usages over * clean up * cmt --- system/ui/lib/wifi_manager.py | 106 ++++++++++++++++++---------------- 1 file changed, 56 insertions(+), 50 deletions(-) diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index 6737946f4d..d403f91f8d 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -407,8 +407,27 @@ class WifiManager: self._connections = {ssid: path for ssid, path in self._connections.items() if path != conn_path} def _get_active_connections(self): + # Returns list of ActiveConnection return self._router_main.send_and_get_reply(Properties(self._nm).get('ActiveConnections')).body[0][1] + def _get_active_wifi_connection(self) -> tuple[str | None, dict | None]: + # Returns first Connection settings path and ActiveConnection props from ActiveConnections with Type 802-11-wireless + for active_conn in self._get_active_connections(): + conn_addr = DBusAddress(active_conn, bus_name=NM, interface=NM_ACTIVE_CONNECTION_IFACE) + reply = self._router_main.send_and_get_reply(Properties(conn_addr).get_all()) + + if reply.header.message_type == MessageType.error: + cloudlog.warning(f"Failed to get active connection properties for {active_conn}: {reply}") + continue + + props = reply.body[0] + + conn_path = props.get('Connection', ('o', '/'))[1] + if props.get('Type', ('s', ''))[1] == '802-11-wireless' and conn_path != '/': + return conn_path, props + + return None, None + def _get_connection_settings(self, conn_path: str) -> dict: conn_addr = DBusAddress(conn_path, bus_name=NM, interface=NM_CONNECTION_IFACE) reply = self._router_main.send_and_get_reply(new_method_call(conn_addr, 'GetSettings')) @@ -530,8 +549,8 @@ class WifiManager: threading.Thread(target=worker, daemon=True).start() def _deactivate_connection(self, ssid: str): - for conn_path in self._get_active_connections(): - conn_addr = DBusAddress(conn_path, bus_name=NM, interface=NM_ACTIVE_CONNECTION_IFACE) + for active_conn in self._get_active_connections(): + conn_addr = DBusAddress(active_conn, bus_name=NM, interface=NM_ACTIVE_CONNECTION_IFACE) specific_obj_path = self._router_main.send_and_get_reply(Properties(conn_addr).get('SpecificObject')).body[0][1] if specific_obj_path != "/": @@ -539,7 +558,7 @@ class WifiManager: ap_ssid = bytes(self._router_main.send_and_get_reply(Properties(ap_addr).get('Ssid')).body[0][1]).decode("utf-8", "replace") if ap_ssid == ssid: - self._router_main.send_and_get_reply(new_method_call(self._nm, 'DeactivateConnection', 'o', (conn_path,))) + self._router_main.send_and_get_reply(new_method_call(self._nm, 'DeactivateConnection', 'o', (active_conn,))) return def is_tethering_active(self) -> bool: @@ -614,28 +633,26 @@ class WifiManager: def set_current_network_metered(self, metered: MeteredType): def worker(): - for active_conn in self._get_active_connections(): - conn_addr = DBusAddress(active_conn, bus_name=NM, interface=NM_ACTIVE_CONNECTION_IFACE) - props = self._router_main.send_and_get_reply(Properties(conn_addr).get_all()).body[0] + if self.is_tethering_active(): + return - if props.get('Type', ('s', ''))[1] == '802-11-wireless' and not self.is_tethering_active(): - conn_path = props.get('Connection', ('o', '/'))[1] - if conn_path == "/": - continue + conn_path, _ = self._get_active_wifi_connection() + if conn_path is None: + cloudlog.warning('No active WiFi connection found') + return - settings = self._get_connection_settings(conn_path) + settings = self._get_connection_settings(conn_path) - if len(settings) == 0: - cloudlog.warning(f'Failed to get connection settings for {conn_path}') - return + if len(settings) == 0: + cloudlog.warning(f'Failed to get connection settings for {conn_path}') + return - settings['connection']['metered'] = ('i', int(metered)) + settings['connection']['metered'] = ('i', int(metered)) - conn_addr = DBusAddress(conn_path, bus_name=NM, interface=NM_CONNECTION_IFACE) - reply = self._router_main.send_and_get_reply(new_method_call(conn_addr, 'Update', 'a{sa{sv}}', (settings,))) - if reply.header.message_type == MessageType.error: - cloudlog.warning(f'Failed to update tethering settings: {reply}') - return + conn_addr = DBusAddress(conn_path, bus_name=NM, interface=NM_CONNECTION_IFACE) + reply = self._router_main.send_and_get_reply(new_method_call(conn_addr, 'Update', 'a{sa{sv}}', (settings,))) + if reply.header.message_type == MessageType.error: + cloudlog.warning(f'Failed to update metered settings: {reply}') threading.Thread(target=worker, daemon=True).start() @@ -708,42 +725,31 @@ class WifiManager: ipv4_address = "" metered = MeteredType.UNKNOWN - for active_conn in self._get_active_connections(): - conn_addr = DBusAddress(active_conn, bus_name=NM, interface=NM_ACTIVE_CONNECTION_IFACE) - reply = self._router_main.send_and_get_reply(Properties(conn_addr).get_all()) + conn_path, props = self._get_active_wifi_connection() - if reply.header.message_type == MessageType.error: - cloudlog.warning(f"Failed to get active connection properties for {active_conn}: {reply}") - continue + if conn_path is not None and props is not None: + # IPv4 address + ip4config_path = props.get('Ip4Config', ('o', '/'))[1] - props = reply.body[0] + if ip4config_path != "/": + ip4config_addr = DBusAddress(ip4config_path, bus_name=NM, interface=NM_IP4_CONFIG_IFACE) + address_data = self._router_main.send_and_get_reply(Properties(ip4config_addr).get('AddressData')).body[0][1] - if props.get('Type', ('s', ''))[1] == '802-11-wireless': - # IPv4 address - ip4config_path = props.get('Ip4Config', ('o', '/'))[1] + for entry in address_data: + if 'address' in entry: + ipv4_address = entry['address'][1] + break - if ip4config_path != "/": - ip4config_addr = DBusAddress(ip4config_path, bus_name=NM, interface=NM_IP4_CONFIG_IFACE) - address_data = self._router_main.send_and_get_reply(Properties(ip4config_addr).get('AddressData')).body[0][1] + # Metered status + settings = self._get_connection_settings(conn_path) - for entry in address_data: - if 'address' in entry: - ipv4_address = entry['address'][1] - break + if len(settings) > 0: + metered_prop = settings['connection'].get('metered', ('i', 0))[1] - # Metered status - conn_path = props.get('Connection', ('o', '/'))[1] - if conn_path != "/": - settings = self._get_connection_settings(conn_path) - - if len(settings) > 0: - metered_prop = settings['connection'].get('metered', ('i', 0))[1] - - if metered_prop == MeteredType.YES: - metered = MeteredType.YES - elif metered_prop == MeteredType.NO: - metered = MeteredType.NO - break + if metered_prop == MeteredType.YES: + metered = MeteredType.YES + elif metered_prop == MeteredType.NO: + metered = MeteredType.NO self._ipv4_address = ipv4_address self._current_network_metered = metered From 887ea25b6d1e66bbdff5fbccccedd519d960775f Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 17 Feb 2026 21:49:50 -0800 Subject: [PATCH 075/311] WifiManager: fix is_connected flicker while roaming on low strength networks (#37243) * temp * clean up * debug * clean up * fix * cmt * clean up --- system/ui/lib/wifi_manager.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index d403f91f8d..57a40895cd 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -94,10 +94,12 @@ class Network: ip_address: str = "" # TODO: implement @classmethod - def from_dbus(cls, ssid: str, aps: list["AccessPoint"], is_saved: bool) -> "Network": + def from_dbus(cls, ssid: str, aps: list["AccessPoint"], is_saved: bool, active_connection: bool) -> "Network": # we only want to show the strongest AP for each Network/SSID strongest_ap = max(aps, key=lambda ap: ap.strength) - is_connected = any(ap.is_connected for ap in aps) + # fall back to ActiveConnection during momentary AP roaming or low strength networks. matches GNOME shell behavior + # https://github.com/GNOME/gnome-shell/blob/3f8b174274fac7d69477523d4873ef8253e1ed49/js/ui/status/network.js#L810-L819 + is_connected = any(ap.is_connected for ap in aps) or active_connection security_type = get_security_type(strongest_ap.flags, strongest_ap.wpa_flags, strongest_ap.rsn_flags) return cls( @@ -707,7 +709,9 @@ class WifiManager: # catch all for parsing errors cloudlog.exception(f"Failed to parse AP properties for {ap_path}") - networks = [Network.from_dbus(ssid, ap_list, ssid in self._connections) for ssid, ap_list in aps.items()] + active_wifi_connection, _ = self._get_active_wifi_connection() + networks = [Network.from_dbus(ssid, ap_list, ssid in self._connections, + self._connections.get(ssid) == active_wifi_connection) for ssid, ap_list in aps.items()] # sort with quantized strength to reduce jumping networks.sort(key=lambda n: (-n.is_connected, -n.is_saved, -round(n.strength / 100 * 2), n.ssid.lower())) self._networks = networks From 57b461a186d838aeb2041c7947d94f7a8e9784ae Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 18 Feb 2026 01:32:49 -0500 Subject: [PATCH 076/311] [bot] Update Python packages (#1704) * Update Python packages * Update CHANGELOG.md --------- Co-authored-by: github-actions[bot] Co-authored-by: Jason Wen --- CHANGELOG.md | 1 + opendbc_repo | 2 +- uv.lock | 15 +++++++-------- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d22324bcf9..8ad026b12b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -83,6 +83,7 @@ sunnypilot Version 2026.001.000 (2026-03-xx) * Toyota: do not disable radar when smartDSU or CAN Filter detected by @sunnyhaibin * Honda: add missing `GasInterceptor` messages to Taiwan Odyssey DBC by @mvl-boston * GM: remove `CHEVROLET_EQUINOX_NON_ACC_3RD_GEN` from `dashcamOnly` by @sunnyhaibin + * GM: remove `CHEVROLET_BOLT_NON_ACC_2ND_GEN` from `dashcamOnly` by @sunnyhaibin * New Contributors (sunnypilot/sunnypilot) * @TheSecurityDev made their first contribution in "ui: fix sidebar scroll in UI screenshots" * @zikeji made their first contribution in "sunnylink: block remote modification of SSH key parameters" diff --git a/opendbc_repo b/opendbc_repo index 8289577096..a996ed701b 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit 8289577096cf303e3f8c53a86d0e7355651b53b5 +Subproject commit a996ed701bd0efe0ebcefa057373ba426dfb3a11 diff --git a/uv.lock b/uv.lock index 782c20849c..a6b43b6fe5 100644 --- a/uv.lock +++ b/uv.lock @@ -356,13 +356,12 @@ wheels = [ [[package]] name = "dearpygui" -version = "2.1.1" +version = "2.2" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/79/41/2146e8d03d28b5a66d5282beb26ffd9ab68a729a29d31e2fe91809271bf5/dearpygui-2.1.1-cp312-cp312-macosx_10_6_x86_64.whl", hash = "sha256:238aea7b4be7376f564dae8edd563b280ec1483a03786022969938507691e017", size = 2101529, upload-time = "2025-11-14T14:47:39.646Z" }, - { url = "https://files.pythonhosted.org/packages/b0/c5/fcc37ef834fe225241aa4f18d77aaa2903134f283077978d65a901c624c6/dearpygui-2.1.1-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:c27ca6ecd4913555b717f3bb341c0b6a27d6c9fdc9932f0b3c31ae2ef893ae35", size = 1895555, upload-time = "2025-11-14T14:47:48.149Z" }, - { url = "https://files.pythonhosted.org/packages/74/66/19f454ba02d5f03a847cc1dfee4a849cd2307d97add5ba26fecdca318adb/dearpygui-2.1.1-cp312-cp312-manylinux1_x86_64.whl", hash = "sha256:8c071e9c165d89217bdcdaf769c6069252fcaee50bf369489add524107932273", size = 2641509, upload-time = "2025-11-14T14:47:54.581Z" }, - { url = "https://files.pythonhosted.org/packages/5e/58/d01538556103d544a5a5b4cbcb00646ff92d8a97f0a6283a56bede4307c8/dearpygui-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:9f2291313d2035f8a4108e13f60d8c1a0e7c19af7554a7739a3fd15b3d5af8f7", size = 1808971, upload-time = "2025-11-14T14:47:28.15Z" }, + { url = "https://files.pythonhosted.org/packages/17/c8/b4afdac89c7bf458513366af3143f7383d7b09721637989c95788d93e24c/dearpygui-2.2-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:34ceae1ca1b65444e49012d6851312e44f08713da1b8cc0150cf41f1c207af9c", size = 1931443, upload-time = "2026-02-17T14:21:54.394Z" }, + { url = "https://files.pythonhosted.org/packages/43/93/a2d083b2e0edb095be815662cc41e40cf9ea7b65d6323e47bb30df7eb284/dearpygui-2.2-cp312-cp312-manylinux1_x86_64.whl", hash = "sha256:e1fae9ae59fec0e41773df64c80311a6ba67696219dde5506a2a4c013e8bcdfa", size = 2592645, upload-time = "2026-02-17T14:22:02.869Z" }, + { url = "https://files.pythonhosted.org/packages/80/ba/eae13acaad479f522db853e8b1ccd695a7bc8da2b9685c1d70a3b318df89/dearpygui-2.2-cp312-cp312-win_amd64.whl", hash = "sha256:7d399543b5a26ab6426ef3bbd776e55520b491b3e169647bde5e6b2de3701b35", size = 1830531, upload-time = "2026-02-17T14:21:43.386Z" }, ] [[package]] @@ -4089,15 +4088,15 @@ wheels = [ [[package]] name = "sentry-sdk" -version = "2.52.0" +version = "2.53.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/59/eb/1b497650eb564701f9a7b8a95c51b2abe9347ed2c0b290ba78f027ebe4ea/sentry_sdk-2.52.0.tar.gz", hash = "sha256:fa0bec872cfec0302970b2996825723d67390cdd5f0229fb9efed93bd5384899", size = 410273, upload-time = "2026-02-04T15:03:54.706Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/06/66c8b705179bc54087845f28fd1b72f83751b6e9a195628e2e9af9926505/sentry_sdk-2.53.0.tar.gz", hash = "sha256:6520ef2c4acd823f28efc55e43eb6ce2e6d9f954a95a3aa96b6fd14871e92b77", size = 412369, upload-time = "2026-02-16T11:11:14.743Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ca/63/2c6daf59d86b1c30600bff679d039f57fd1932af82c43c0bde1cbc55e8d4/sentry_sdk-2.52.0-py2.py3-none-any.whl", hash = "sha256:931c8f86169fc6f2752cb5c4e6480f0d516112e78750c312e081ababecbaf2ed", size = 435547, upload-time = "2026-02-04T15:03:51.567Z" }, + { url = "https://files.pythonhosted.org/packages/47/d4/2fdf854bc3b9c7f55219678f812600a20a138af2dd847d99004994eada8f/sentry_sdk-2.53.0-py2.py3-none-any.whl", hash = "sha256:46e1ed8d84355ae54406c924f6b290c3d61f4048625989a723fd622aab838899", size = 437908, upload-time = "2026-02-16T11:11:13.227Z" }, ] [[package]] From 966bb6cc549f1fa55c3f03379428d625e5bc1da9 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 17 Feb 2026 22:41:51 -0800 Subject: [PATCH 077/311] WifiUi: update wifi button in loop (#37246) * move to update_state * move back --- .../mici/layouts/settings/network/__init__.py | 46 +++++++++---------- system/ui/lib/wifi_manager.py | 4 ++ 2 files changed, 27 insertions(+), 23 deletions(-) diff --git a/selfdrive/ui/mici/layouts/settings/network/__init__.py b/selfdrive/ui/mici/layouts/settings/network/__init__.py index 3854e998e9..c14d8ad8e5 100644 --- a/selfdrive/ui/mici/layouts/settings/network/__init__.py +++ b/selfdrive/ui/mici/layouts/settings/network/__init__.py @@ -123,6 +123,29 @@ class NetworkLayoutMici(NavWidget): self._apn_btn.set_visible(show_cell_settings) self._cellular_metered_btn.set_visible(show_cell_settings) + # Update wi-fi button with ssid and ip address + # TODO: make sure we handle hidden ssids + connecting_ssid = self._wifi_manager.connecting_to_ssid + connected_network = next((network for network in self._wifi_manager.networks if network.is_connected), None) + if connecting_ssid: + display_network = next((n for n in self._wifi_manager.networks if n.ssid == connecting_ssid), None) + self._wifi_button.set_text(normalize_ssid(connecting_ssid)) + self._wifi_button.set_value("connecting...") + elif connected_network is not None: + display_network = connected_network + self._wifi_button.set_text(normalize_ssid(connected_network.ssid)) + self._wifi_button.set_value(self._wifi_manager.ipv4_address or "not connected") + else: + display_network = None + self._wifi_button.set_text("wi-fi") + self._wifi_button.set_value("not connected") + + if display_network is not None: + strength = WifiIcon.get_strength_icon_idx(display_network.strength) + self._wifi_button.set_icon(self._wifi_full_txt if strength == 2 else self._wifi_medium_txt if strength == 1 else self._wifi_low_txt) + else: + self._wifi_button.set_icon(self._wifi_slash_txt) + def show_event(self): super().show_event() self._current_panel = NetworkPanelType.NONE @@ -162,29 +185,6 @@ class NetworkLayoutMici(NavWidget): self._network_metered_btn.set_enabled(lambda: not tethering_active and bool(self._wifi_manager.ipv4_address)) self._tethering_toggle_btn.set_checked(tethering_active) - # Update wi-fi button with ssid and ip address - # TODO: make sure we handle hidden ssids - connecting_ssid = self._wifi_manager.connecting_to_ssid - connected_network = next((network for network in networks if network.is_connected), None) - if connecting_ssid: - display_network = next((n for n in networks if n.ssid == connecting_ssid), None) - self._wifi_button.set_text(normalize_ssid(connecting_ssid)) - self._wifi_button.set_value("connecting...") - elif connected_network is not None: - display_network = connected_network - self._wifi_button.set_text(normalize_ssid(connected_network.ssid)) - self._wifi_button.set_value(self._wifi_manager.ipv4_address or "not connected") - else: - display_network = None - self._wifi_button.set_text("wi-fi") - self._wifi_button.set_value("not connected") - - if display_network is not None: - strength = WifiIcon.get_strength_icon_idx(display_network.strength) - self._wifi_button.set_icon(self._wifi_full_txt if strength == 2 else self._wifi_medium_txt if strength == 1 else self._wifi_low_txt) - else: - self._wifi_button.set_icon(self._wifi_slash_txt) - # Update network metered self._network_metered_btn.set_value( { diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index 57a40895cd..f22c6c0d92 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -227,6 +227,10 @@ class WifiManager: if disconnected is not None: self._disconnected.append(disconnected) + @property + def networks(self) -> list[Network]: + return self._networks + @property def ipv4_address(self) -> str: return self._ipv4_address From c8e10139c2afb9137f90702ca03f718dffcfde8c Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 17 Feb 2026 22:53:49 -0800 Subject: [PATCH 078/311] WifiUi: if connected, don't show not connected (#37245) * obt * obt * debug * clean up --- selfdrive/ui/mici/layouts/settings/network/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selfdrive/ui/mici/layouts/settings/network/__init__.py b/selfdrive/ui/mici/layouts/settings/network/__init__.py index c14d8ad8e5..659cd5ff3a 100644 --- a/selfdrive/ui/mici/layouts/settings/network/__init__.py +++ b/selfdrive/ui/mici/layouts/settings/network/__init__.py @@ -134,7 +134,7 @@ class NetworkLayoutMici(NavWidget): elif connected_network is not None: display_network = connected_network self._wifi_button.set_text(normalize_ssid(connected_network.ssid)) - self._wifi_button.set_value(self._wifi_manager.ipv4_address or "not connected") + self._wifi_button.set_value(self._wifi_manager.ipv4_address or "obtaining IP...") else: display_network = None self._wifi_button.set_text("wi-fi") From 5eed9490c6ed92895d1c67636574b5b4854b2395 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Wed, 18 Feb 2026 02:02:34 -0500 Subject: [PATCH 079/311] ci: remove double prebuilt workflow runs --- .github/workflows/sunnypilot-master-dev-prep.yaml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/.github/workflows/sunnypilot-master-dev-prep.yaml b/.github/workflows/sunnypilot-master-dev-prep.yaml index 8e8e3c3d9d..36cfc2e7ee 100644 --- a/.github/workflows/sunnypilot-master-dev-prep.yaml +++ b/.github/workflows/sunnypilot-master-dev-prep.yaml @@ -242,9 +242,4 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Trigger prebuilt workflow - if: success() && steps.push-changes.outputs.has_changes == 'true' - run: | - gh workflow run sunnypilot-build-prebuilt.yaml --ref "${{ inputs.target_branch || env.DEFAULT_TARGET_BRANCH }}" - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + From 3a74f8c568ab429633c87761b9ab00a2808c4b23 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Wed, 18 Feb 2026 02:57:25 -0500 Subject: [PATCH 080/311] [TIZI/TICI] ui: ensure null checks for `CarParams` and `CarParamsSP` (#1706) * [TIZI/TICI] ui: ensure null checks for `CarParams` and `CarParamsSP` * space --- .github/workflows/sunnypilot-master-dev-prep.yaml | 2 -- selfdrive/ui/sunnypilot/layouts/settings/models.py | 2 +- .../settings/steering_sub_layouts/lane_change_settings.py | 2 +- .../layouts/settings/steering_sub_layouts/mads_settings.py | 4 ++-- selfdrive/ui/sunnypilot/layouts/settings/vehicle/__init__.py | 2 +- .../ui/sunnypilot/layouts/settings/vehicle/brands/hyundai.py | 2 +- .../ui/sunnypilot/layouts/settings/vehicle/brands/subaru.py | 2 +- .../sunnypilot/layouts/settings/vehicle/platform_selector.py | 2 +- 8 files changed, 8 insertions(+), 10 deletions(-) diff --git a/.github/workflows/sunnypilot-master-dev-prep.yaml b/.github/workflows/sunnypilot-master-dev-prep.yaml index 36cfc2e7ee..1cb574764b 100644 --- a/.github/workflows/sunnypilot-master-dev-prep.yaml +++ b/.github/workflows/sunnypilot-master-dev-prep.yaml @@ -241,5 +241,3 @@ jobs: gh run watch "$RUN_ID" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - diff --git a/selfdrive/ui/sunnypilot/layouts/settings/models.py b/selfdrive/ui/sunnypilot/layouts/settings/models.py index 5820b34cc0..4fb579a0f2 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/models.py +++ b/selfdrive/ui/sunnypilot/layouts/settings/models.py @@ -99,7 +99,7 @@ class ModelsLayout(Widget): "Keeping this on provides the stock openpilot experience.") if lagd_toggle: desc += f"
{tr('Live Steer Delay:')} {ui_state.sm['liveDelay'].lateralDelay:.3f} s" - elif ui_state.CP: + elif ui_state.CP is not None: sw = float(ui_state.params.get("LagdToggleDelay", "0.2")) cp = ui_state.CP.steerActuatorDelay desc += f"
{tr('Actuator Delay:')} {cp:.2f} s + {tr('Software Delay:')} {sw:.2f} s = {tr('Total Delay:')} {cp + sw:.2f} s" diff --git a/selfdrive/ui/sunnypilot/layouts/settings/steering_sub_layouts/lane_change_settings.py b/selfdrive/ui/sunnypilot/layouts/settings/steering_sub_layouts/lane_change_settings.py index 9a5d5202a5..fbb9ce7cf7 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/steering_sub_layouts/lane_change_settings.py +++ b/selfdrive/ui/sunnypilot/layouts/settings/steering_sub_layouts/lane_change_settings.py @@ -75,7 +75,7 @@ class LaneChangeSettingsLayout(Widget): self._scroller.show_event() def _update_toggles(self): - enable_bsm = ui_state.CP and ui_state.CP.enableBsm + enable_bsm = ui_state.CP is not None and ui_state.CP.enableBsm if not enable_bsm and ui_state.params.get_bool("AutoLaneChangeBsmDelay"): ui_state.params.remove("AutoLaneChangeBsmDelay") self._bsm_delay.action_item.set_enabled(enable_bsm and ui_state.params.get("AutoLaneChangeTimer", return_default=True) > AutoLaneChangeMode.NUDGE) diff --git a/selfdrive/ui/sunnypilot/layouts/settings/steering_sub_layouts/mads_settings.py b/selfdrive/ui/sunnypilot/layouts/settings/steering_sub_layouts/mads_settings.py index 3542adf285..098fcf8ce8 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/steering_sub_layouts/mads_settings.py +++ b/selfdrive/ui/sunnypilot/layouts/settings/steering_sub_layouts/mads_settings.py @@ -91,12 +91,12 @@ class MadsSettingsLayout(Widget): if bundle: brand = bundle.get("brand", "") if not brand: - brand = ui_state.CP.brand if ui_state.CP else "" + brand = ui_state.CP.brand if ui_state.CP is not None else "" if brand == "rivian": return True elif brand == "tesla": - return not (ui_state.CP_SP and ui_state.CP_SP.flags & TeslaFlagsSP.HAS_VEHICLE_BUS) + return not (ui_state.CP_SP is not None and ui_state.CP_SP.flags & TeslaFlagsSP.HAS_VEHICLE_BUS) return False def _update_steering_mode_description(self, button_index: int): diff --git a/selfdrive/ui/sunnypilot/layouts/settings/vehicle/__init__.py b/selfdrive/ui/sunnypilot/layouts/settings/vehicle/__init__.py index 0dd12d76f4..d8fdd61e72 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/vehicle/__init__.py +++ b/selfdrive/ui/sunnypilot/layouts/settings/vehicle/__init__.py @@ -35,7 +35,7 @@ class VehicleLayout(Widget): def get_brand(): if bundle := ui_state.params.get("CarPlatformBundle"): return bundle.get("brand", "") - elif ui_state.CP and ui_state.CP.carFingerprint != "MOCK": + elif ui_state.CP is not None and ui_state.CP.carFingerprint != "MOCK": return ui_state.CP.brand return "" diff --git a/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/hyundai.py b/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/hyundai.py index f6849eb201..351dacbbc2 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/hyundai.py +++ b/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/hyundai.py @@ -32,7 +32,7 @@ class HyundaiSettings(BrandSettings): if bundle: platform = bundle.get("platform") self.alpha_long_available = CAR[platform] not in (UNSUPPORTED_LONGITUDINAL_CAR | CANFD_UNSUPPORTED_LONGITUDINAL_CAR) - elif ui_state.CP: + elif ui_state.CP is not None: self.alpha_long_available = ui_state.CP.alphaLongitudinalAvailable tuning_param = int(ui_state.params.get("HyundaiLongitudinalTuning") or "0") diff --git a/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/subaru.py b/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/subaru.py index 66e7ec1d5a..5ef53a0e43 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/subaru.py +++ b/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/subaru.py @@ -39,7 +39,7 @@ class SubaruSettings(BrandSettings): platform = bundle.get("platform") config = CAR[platform].config self.has_stop_and_go = not (config.flags & (SubaruFlags.GLOBAL_GEN2 | SubaruFlags.HYBRID)) - elif ui_state.CP: + elif ui_state.CP is not None: self.has_stop_and_go = not (ui_state.CP.flags & (SubaruFlags.GLOBAL_GEN2 | SubaruFlags.HYBRID)) disabled_msg = self.stop_and_go_disabled_msg() diff --git a/selfdrive/ui/sunnypilot/layouts/settings/vehicle/platform_selector.py b/selfdrive/ui/sunnypilot/layouts/settings/vehicle/platform_selector.py index db90595274..517a7d9620 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/vehicle/platform_selector.py +++ b/selfdrive/ui/sunnypilot/layouts/settings/vehicle/platform_selector.py @@ -131,7 +131,7 @@ class PlatformSelector(Button): self._platform = bundle.get("name", "") self.set_text(self._platform) self.color = style.BLUE - elif ui_state.CP and ui_state.CP.carFingerprint != "MOCK": + elif ui_state.CP is not None and ui_state.CP.carFingerprint != "MOCK": self._platform = ui_state.CP.carFingerprint self.set_text(self._platform) self.color = style.GREEN From 80f4becabfad873a273dcce0fbcfb3c9cabc5971 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Wed, 18 Feb 2026 01:03:39 -0800 Subject: [PATCH 081/311] no need to guard connect with password --- selfdrive/ui/mici/layouts/settings/network/wifi_ui.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py index 679372404a..5bf811e26a 100644 --- a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py +++ b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py @@ -397,9 +397,8 @@ class WifiUIMici(BigMultiOptionDialog): btn.set_network_missing(True) def _connect_with_password(self, ssid: str, password: str): - if password: - self._wifi_manager.connect_to_network(ssid, password) - self._update_buttons() + self._wifi_manager.connect_to_network(ssid, password) + self._update_buttons() def _on_option_selected(self, option: str): super()._on_option_selected(option) From edafe139a41898f4dae4c445b97e582d93fb94ab Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Wed, 18 Feb 2026 01:14:40 -0800 Subject: [PATCH 082/311] WifiManager: set connecting status if NM auto connects (#37247) * set connecting if nm auto connects * good catch --- system/ui/lib/wifi_manager.py | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index f22c6c0d92..14a32e0a8a 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -10,7 +10,7 @@ from typing import Any from jeepney import DBusAddress, new_method_call from jeepney.bus_messages import MatchRule, message_bus -from jeepney.io.blocking import open_dbus_connection as open_dbus_connection_blocking +from jeepney.io.blocking import DBusConnection, open_dbus_connection as open_dbus_connection_blocking from jeepney.io.threading import DBusRouter, open_dbus_connection as open_dbus_connection_threading from jeepney.low_level import MessageType from jeepney.wrappers import Properties @@ -341,6 +341,17 @@ class WifiManager: if self._connecting_to_ssid == failed_ssid: self._connecting_to_ssid = None + elif new_state == NMDeviceState.PREPARE and self._connecting_to_ssid is None: + # Set connecting status when NetworkManager connects to known networks on its own + conn_path, _ = self._get_active_wifi_connection(self._conn_monitor) + if conn_path is None: + cloudlog.warning("Failed to get active wifi connection during PREPARE state") + continue + + ssid = next((s for s, p in self._connections.items() if p == conn_path), None) + if ssid: + self._set_connecting(ssid) + elif new_state == NMDeviceState.ACTIVATED: self._update_networks() self._enqueue_callbacks(self._activated) @@ -412,15 +423,21 @@ class WifiManager: def _connection_removed(self, conn_path: str): self._connections = {ssid: path for ssid, path in self._connections.items() if path != conn_path} - def _get_active_connections(self): + def _get_active_connections(self, router: DBusConnection | DBusRouter | None = None): # Returns list of ActiveConnection - return self._router_main.send_and_get_reply(Properties(self._nm).get('ActiveConnections')).body[0][1] + if router is None: + router = self._router_main - def _get_active_wifi_connection(self) -> tuple[str | None, dict | None]: + return router.send_and_get_reply(Properties(self._nm).get('ActiveConnections')).body[0][1] + + def _get_active_wifi_connection(self, router: DBusConnection | DBusRouter | None = None) -> tuple[str | None, dict | None]: # Returns first Connection settings path and ActiveConnection props from ActiveConnections with Type 802-11-wireless - for active_conn in self._get_active_connections(): + if router is None: + router = self._router_main + + for active_conn in self._get_active_connections(router): conn_addr = DBusAddress(active_conn, bus_name=NM, interface=NM_ACTIVE_CONNECTION_IFACE) - reply = self._router_main.send_and_get_reply(Properties(conn_addr).get_all()) + reply = router.send_and_get_reply(Properties(conn_addr).get_all()) if reply.header.message_type == MessageType.error: cloudlog.warning(f"Failed to get active connection properties for {active_conn}: {reply}") From 7aeb7085a3fd222d302eaeabe53fa96c53cd3184 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Wed, 18 Feb 2026 01:17:42 -0800 Subject: [PATCH 083/311] WifiUi: add hide Scroller event (#37248) * add show/hide scroller events * another good catch --- selfdrive/ui/mici/layouts/settings/network/wifi_ui.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py index 5bf811e26a..dd8b26e545 100644 --- a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py +++ b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py @@ -362,6 +362,10 @@ class WifiUIMici(BigMultiOptionDialog): self._scroller._items.clear() self._update_buttons() + def hide_event(self): + super().hide_event() + self._scroller.hide_event() + def _open_network_manage_page(self, result=None): if self._network_info_page._network is not None and self._network_info_page._network.ssid in self._networks: self._network_info_page.update_networks(self._networks) From 62b5fd54e64da9bee138b345ae7649e3e7343a25 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Wed, 18 Feb 2026 01:18:06 -0800 Subject: [PATCH 084/311] WifiUi: sort by real strength (#37249) sort by real strength --- system/ui/lib/wifi_manager.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index 14a32e0a8a..8f4d8bdaf0 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -733,8 +733,7 @@ class WifiManager: active_wifi_connection, _ = self._get_active_wifi_connection() networks = [Network.from_dbus(ssid, ap_list, ssid in self._connections, self._connections.get(ssid) == active_wifi_connection) for ssid, ap_list in aps.items()] - # sort with quantized strength to reduce jumping - networks.sort(key=lambda n: (-n.is_connected, -n.is_saved, -round(n.strength / 100 * 2), n.ssid.lower())) + networks.sort(key=lambda n: (-n.is_connected, -n.is_saved, -n.strength, n.ssid.lower())) self._networks = networks self._update_active_connection_info() From b5f86446d450c25b3e27d0a057a9b9c92885b04c Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Wed, 18 Feb 2026 01:19:31 -0800 Subject: [PATCH 085/311] WifiManager: check AddConnection was successful (#37250) check addconnect --- system/ui/lib/wifi_manager.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index 8f4d8bdaf0..61522ffdee 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -530,7 +530,12 @@ class WifiManager: } settings_addr = DBusAddress(NM_SETTINGS_PATH, bus_name=NM, interface=NM_SETTINGS_IFACE) - self._router_main.send_and_get_reply(new_method_call(settings_addr, 'AddConnection', 'a{sa{sv}}', (connection,))) + reply = self._router_main.send_and_get_reply(new_method_call(settings_addr, 'AddConnection', 'a{sa{sv}}', (connection,))) + + if reply.header.message_type == MessageType.error: + cloudlog.warning(f"Failed to add connection for {ssid}: {reply}") + self._connecting_to_ssid = None + self._prev_connecting_to_ssid = None threading.Thread(target=worker, daemon=True).start() From 489afc38421831b895042bbdb91ac7538ea04d8b Mon Sep 17 00:00:00 2001 From: Nick Date: Wed, 18 Feb 2026 02:34:57 -0700 Subject: [PATCH 086/311] four ui: edge shadows (#37239) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ui: add edge shadow effect to horizontal scrollers in settings Adds a black gradient falloff shadow (20x240, 100%→0% opacity) on the left and right edges of horizontal Scroller panels. Enabled via an opt-in `edge_shadows` parameter on Scroller for easy per-screen control. Enabled on: settings menu, toggles, network, device, developer. Not enabled on: home screen carousel, vertical scrollers, setup screens. Co-authored-by: Cursor * ui: reduce edge shadow opacity to 80% Co-authored-by: Cursor * what on earth is this * some lines are ok --------- Co-authored-by: Cursor Co-authored-by: Shane Smiskol --- selfdrive/ui/mici/layouts/main.py | 2 +- system/ui/widgets/scroller.py | 22 ++++++++++++++++++---- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/selfdrive/ui/mici/layouts/main.py b/selfdrive/ui/mici/layouts/main.py index f12c95eafb..b78a1d8eaf 100644 --- a/selfdrive/ui/mici/layouts/main.py +++ b/selfdrive/ui/mici/layouts/main.py @@ -47,7 +47,7 @@ class MiciMainLayout(Widget): self._alerts_layout, self._home_layout, self._onroad_layout, - ], spacing=0, pad_start=0, pad_end=0, scroll_indicator=False) + ], spacing=0, pad_start=0, pad_end=0, scroll_indicator=False, edge_shadows=False) self._scroller.set_reset_scroll_at_show(False) # Disable scrolling when onroad is interacting with bookmark diff --git a/system/ui/widgets/scroller.py b/system/ui/widgets/scroller.py index fcba1952ce..5930a2a6eb 100644 --- a/system/ui/widgets/scroller.py +++ b/system/ui/widgets/scroller.py @@ -12,6 +12,8 @@ LINE_COLOR = rl.GRAY LINE_PADDING = 40 ANIMATION_SCALE = 0.6 +EDGE_SHADOW_WIDTH = 20 + MIN_ZOOM_ANIMATION_TIME = 0.075 # seconds DO_ZOOM = False DO_JELLO = False @@ -77,7 +79,7 @@ class ScrollIndicator(Widget): class Scroller(Widget): def __init__(self, items: list[Widget], horizontal: bool = True, snap_items: bool = True, spacing: int = ITEM_SPACING, line_separator: bool = False, pad_start: int = ITEM_SPACING, pad_end: int = ITEM_SPACING, - scroll_indicator: bool = True): + scroll_indicator: bool = True, edge_shadows: bool = True): super().__init__() self._items: list[Widget] = [] self._horizontal = horizontal @@ -107,8 +109,9 @@ class Scroller(Widget): self.scroll_panel = GuiScrollPanel2(self._horizontal, handle_out_of_bounds=not self._snap_items) self._scroll_enabled: bool | Callable[[], bool] = True - self._show_scroll_indicator = scroll_indicator + self._show_scroll_indicator = scroll_indicator and self._horizontal self._scroll_indicator = ScrollIndicator() + self._edge_shadows = edge_shadows and self._horizontal for item in items: self.add_widget(item) @@ -286,8 +289,19 @@ class Scroller(Widget): rl.end_scissor_mode() - # Draw scroll indicator - if self._show_scroll_indicator and self._horizontal and len(self._visible_items) > 0: + # Draw edge shadows on top of scroller content + if self._edge_shadows: + rl.draw_rectangle_gradient_h(int(self._rect.x), int(self._rect.y), + EDGE_SHADOW_WIDTH, int(self._rect.y), + rl.Color(0, 0, 0, 166), rl.BLANK) + + right_x = int(self._rect.x + self._rect.width - EDGE_SHADOW_WIDTH) + rl.draw_rectangle_gradient_h(right_x, int(self._rect.y), + EDGE_SHADOW_WIDTH, int(self._rect.y), + rl.BLANK, rl.Color(0, 0, 0, 166)) + + # Draw scroll indicator on top of edge shadows + if self._show_scroll_indicator and len(self._visible_items) > 0: self._scroll_indicator.update(self._scroll_offset, self._content_size, self._rect) self._scroll_indicator.render() From c6db0cd4b6f61a4f4ce52b6ca941a83af386508f Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Wed, 18 Feb 2026 05:52:59 -0800 Subject: [PATCH 087/311] WifiManager: fix all networks showing as connected when no active connection (#37252) * WifiManager: fix all networks showing as connected when no active connection When there's no active WiFi connection, _get_active_wifi_connection() returns None. This caused `self._connections.get(ssid) == None` to be True for all unsaved networks, marking them all as connected. Co-authored-by: Cursor * ltl --------- Co-authored-by: Cursor --- system/ui/lib/wifi_manager.py | 1 + 1 file changed, 1 insertion(+) diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index 61522ffdee..52246beb7e 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -737,6 +737,7 @@ class WifiManager: active_wifi_connection, _ = self._get_active_wifi_connection() networks = [Network.from_dbus(ssid, ap_list, ssid in self._connections, + active_wifi_connection is not None and self._connections.get(ssid) == active_wifi_connection) for ssid, ap_list in aps.items()] networks.sort(key=lambda n: (-n.is_connected, -n.is_saved, -n.strength, n.ssid.lower())) self._networks = networks From d80cde6e41206813126304658c11351c88f77714 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Wed, 18 Feb 2026 18:17:06 -0500 Subject: [PATCH 088/311] tools: block `manage_athenad` in sim startup script (#37256) tools: block `manage_athenad` in Metadrive startup script --- tools/sim/launch_openpilot.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/sim/launch_openpilot.sh b/tools/sim/launch_openpilot.sh index fa5ac731bd..392f365d03 100755 --- a/tools/sim/launch_openpilot.sh +++ b/tools/sim/launch_openpilot.sh @@ -6,7 +6,7 @@ export SIMULATION="1" export SKIP_FW_QUERY="1" export FINGERPRINT="HONDA_CIVIC_2022" -export BLOCK="${BLOCK},camerad,loggerd,encoderd,micd,logmessaged" +export BLOCK="${BLOCK},camerad,loggerd,encoderd,micd,logmessaged,manage_athenad" if [[ "$CI" ]]; then # TODO: offscreen UI should work export BLOCK="${BLOCK},ui" From b80d3e113b071a5a1e4a7057184e12dfc93c06dc Mon Sep 17 00:00:00 2001 From: David <49467229+TheSecurityDev@users.noreply.github.com> Date: Wed, 18 Feb 2026 17:20:25 -0600 Subject: [PATCH 089/311] ui diff: better diff report on mobile (#37255) * Add HTML template for UI diff report * update .gitignore * empty line * use proper html tags * remove paragraph tags * simplify paths * use h3 * use simpler replace instead; dynamically generate videos * update diff html styling * remove unnecessary * fix * use h4 instead * padding on h4 * adjust heading margin * revert * use h4 again * remove viewport * Revert "remove viewport" This reverts commit 7636920e556fc06bbd65cff7ecb4c3d31b11024d. --- selfdrive/ui/tests/.gitignore | 5 -- selfdrive/ui/tests/diff/.gitignore | 2 + selfdrive/ui/tests/diff/diff.py | 68 +++--------------- selfdrive/ui/tests/diff/diff_template.html | 80 ++++++++++++++++++++++ 4 files changed, 93 insertions(+), 62 deletions(-) create mode 100644 selfdrive/ui/tests/diff/.gitignore create mode 100644 selfdrive/ui/tests/diff/diff_template.html diff --git a/selfdrive/ui/tests/.gitignore b/selfdrive/ui/tests/.gitignore index 39e2f8970c..74ab2675db 100644 --- a/selfdrive/ui/tests/.gitignore +++ b/selfdrive/ui/tests/.gitignore @@ -1,7 +1,2 @@ test test_translations -test_ui/report_1 - -diff/**/*.mp4 -diff/**/*.html -diff/.coverage diff --git a/selfdrive/ui/tests/diff/.gitignore b/selfdrive/ui/tests/diff/.gitignore new file mode 100644 index 0000000000..e21a8d896e --- /dev/null +++ b/selfdrive/ui/tests/diff/.gitignore @@ -0,0 +1,2 @@ +report +.coverage diff --git a/selfdrive/ui/tests/diff/diff.py b/selfdrive/ui/tests/diff/diff.py index 42efa381b2..974edb42a3 100755 --- a/selfdrive/ui/tests/diff/diff.py +++ b/selfdrive/ui/tests/diff/diff.py @@ -8,6 +8,7 @@ from pathlib import Path from openpilot.common.basedir import BASEDIR DIFF_OUT_DIR = Path(BASEDIR) / "selfdrive" / "ui" / "tests" / "diff" / "report" +HTML_TEMPLATE_PATH = Path(__file__).with_name("diff_template.html") def extract_framehashes(video_path): @@ -71,64 +72,17 @@ def generate_html_report(videos: tuple[str, str], basedir: str, different_frames + (f" Video {'2' if frame_delta > 0 else '1'} is longer by {abs(frame_delta)} frames." if frame_delta != 0 else "") ) - def render_video_cell(video_id: str, title: str, path: str, is_diff=False): - return f""" - -

{title}

- - -""" + # Load HTML template and replace placeholders + html = HTML_TEMPLATE_PATH.read_text() + placeholders = { + "VIDEO1_SRC": os.path.join(basedir, os.path.basename(videos[0])), + "VIDEO2_SRC": os.path.join(basedir, os.path.basename(videos[1])), + "DIFF_SRC": os.path.join(basedir, diff_video_name), + "RESULT_TEXT": result_text, + } + for key, value in placeholders.items(): + html = html.replace(f"${key}", value) - html = f"""

UI Diff

- - -{render_video_cell("video1", "Video 1", videos[0])} -{render_video_cell("video2", "Video 2", videos[1])} -{render_video_cell("diffVideo", "Pixel Diff", diff_video_name, is_diff=True)} - -
- -
-

Results: {result_text}

-""" return html diff --git a/selfdrive/ui/tests/diff/diff_template.html b/selfdrive/ui/tests/diff/diff_template.html new file mode 100644 index 0000000000..3f1de10512 --- /dev/null +++ b/selfdrive/ui/tests/diff/diff_template.html @@ -0,0 +1,80 @@ + + + + + + UI Diff Report + + + +

UI Diff

+
+
+

Results: $RESULT_TEXT

+ + + From a6f4cdb31943c3bbf3aceea616813a7df660a9d8 Mon Sep 17 00:00:00 2001 From: David <49467229+TheSecurityDev@users.noreply.github.com> Date: Wed, 18 Feb 2026 17:20:55 -0600 Subject: [PATCH 090/311] ui replay: remove fps limiting during headless replay (#37241) use OFFSCREEN during headless replay for no fps limiting --- selfdrive/ui/tests/diff/replay.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/selfdrive/ui/tests/diff/replay.py b/selfdrive/ui/tests/diff/replay.py index 1cb0b42ada..bb047b2015 100755 --- a/selfdrive/ui/tests/diff/replay.py +++ b/selfdrive/ui/tests/diff/replay.py @@ -27,15 +27,17 @@ def setup_state(): def run_replay(variant: LayoutVariant) -> None: - from openpilot.selfdrive.ui.ui_state import ui_state # Import within OpenpilotPrefix context so param values are setup correctly - from openpilot.system.ui.lib.application import gui_app # Import here for accurate coverage - from openpilot.selfdrive.ui.tests.diff.replay_script import build_script + if HEADLESS: + rl.set_config_flags(rl.ConfigFlags.FLAG_WINDOW_HIDDEN) + os.environ["OFFSCREEN"] = "1" # Run UI without FPS limit (set before importing gui_app) setup_state() os.makedirs(DIFF_OUT_DIR, exist_ok=True) - if HEADLESS: - rl.set_config_flags(rl.FLAG_WINDOW_HIDDEN) + from openpilot.selfdrive.ui.ui_state import ui_state # Import within OpenpilotPrefix context so param values are setup correctly + from openpilot.system.ui.lib.application import gui_app # Import here for accurate coverage + from openpilot.selfdrive.ui.tests.diff.replay_script import build_script + gui_app.init_window("ui diff test", fps=FPS) # Dynamically import main layout based on variant From b6a0c89dc57da60cffd387ad49005ed07a7f6950 Mon Sep 17 00:00:00 2001 From: David <49467229+TheSecurityDev@users.noreply.github.com> Date: Wed, 18 Feb 2026 17:21:17 -0600 Subject: [PATCH 091/311] ui replay: record lossless to fix big replay diff (#37237) * add RECORD_LOSSLESS and enable for replays * use RECORD_QUALITY instead * comment * clarify comment Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * clarify comment --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- selfdrive/ui/tests/diff/replay.py | 1 + system/ui/lib/application.py | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/selfdrive/ui/tests/diff/replay.py b/selfdrive/ui/tests/diff/replay.py index bb047b2015..e424d11f62 100755 --- a/selfdrive/ui/tests/diff/replay.py +++ b/selfdrive/ui/tests/diff/replay.py @@ -106,6 +106,7 @@ def main(): if args.big: os.environ["BIG"] = "1" os.environ["RECORD"] = "1" + os.environ["RECORD_QUALITY"] = "0" # Use CRF 0 ("lossless" encode) for deterministic output across different machines os.environ["RECORD_OUTPUT"] = os.path.join(DIFF_OUT_DIR, os.environ.get("RECORD_OUTPUT", f"{variant}_ui_replay.mp4")) print(f"Running {variant} UI replay...") diff --git a/system/ui/lib/application.py b/system/ui/lib/application.py index da314a394f..755de674de 100644 --- a/system/ui/lib/application.py +++ b/system/ui/lib/application.py @@ -41,7 +41,8 @@ PROFILE_RENDER = int(os.getenv("PROFILE_RENDER", "0")) PROFILE_STATS = int(os.getenv("PROFILE_STATS", "100")) # Number of functions to show in profile output RECORD = os.getenv("RECORD") == "1" RECORD_OUTPUT = str(Path(os.getenv("RECORD_OUTPUT", "output")).with_suffix(".mp4")) -RECORD_BITRATE = os.getenv("RECORD_BITRATE", "") # Target bitrate e.g. "2000k" +RECORD_QUALITY = int(os.getenv("RECORD_QUALITY", "23")) # Dynamic bitrate quality level (CRF); 0 is lossless (bigger size), max is 51, default is 23 for x264 +RECORD_BITRATE = os.getenv("RECORD_BITRATE", "") # Target bitrate e.g. "2000k" (overrides RECORD_QUALITY when set) RECORD_SPEED = int(os.getenv("RECORD_SPEED", "1")) # Speed multiplier OFFSCREEN = os.getenv("OFFSCREEN") == "1" # Disable FPS limiting for fast offline rendering @@ -298,8 +299,10 @@ class GuiApplication: '-r', str(output_fps), # Output frame rate (for speed multiplier) '-c:v', 'libx264', '-preset', 'ultrafast', + '-crf', str(RECORD_QUALITY) ] if RECORD_BITRATE: + # NOTE: custom bitrate overrides crf setting ffmpeg_args += ['-b:v', RECORD_BITRATE, '-maxrate', RECORD_BITRATE, '-bufsize', RECORD_BITRATE] ffmpeg_args += [ '-y', # Overwrite existing file From 488d84c6643b848bb3494bc7b4c5878956aaeda3 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Wed, 18 Feb 2026 16:40:09 -0800 Subject: [PATCH 092/311] mici updater: clean up unused signal strength (#37259) clean up --- system/ui/mici_updater.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/system/ui/mici_updater.py b/system/ui/mici_updater.py index 7ebb4262ff..5c8748783a 100755 --- a/system/ui/mici_updater.py +++ b/system/ui/mici_updater.py @@ -8,7 +8,7 @@ from enum import IntEnum from openpilot.system.hardware import HARDWARE from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.text_measure import measure_text_cached -from openpilot.system.ui.lib.wifi_manager import WifiManager, Network +from openpilot.system.ui.lib.wifi_manager import WifiManager from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.label import gui_text_box, gui_label, UnifiedLabel from openpilot.system.ui.widgets.button import FullRoundedButton @@ -28,7 +28,6 @@ class Updater(Widget): self.updater = updater_path self.manifest = manifest_path self.current_screen = Screen.PROMPT - self._current_network_strength = -1 self.progress_value = 0 self.progress_text = "loading" @@ -40,7 +39,6 @@ class Updater(Widget): self._network_setup_page = NetworkSetupPage(self._wifi_manager, self._network_setup_continue_callback, self._network_setup_back_callback) - self._wifi_manager.add_callbacks(networks_updated=self._on_network_updated) self._network_monitor = NetworkConnectivityMonitor() self._network_monitor.start() @@ -66,9 +64,6 @@ class Updater(Widget): def _update_failed_retry_callback(self): self.set_current_screen(Screen.PROMPT) - def _on_network_updated(self, networks: list[Network]): - self._current_network_strength = next((net.strength for net in networks if net.is_connected), -1) - def set_current_screen(self, screen: Screen): if self.current_screen != screen: if screen == Screen.PROGRESS: From 3c4ddba992b3ee52a9c881ee0eb9727d3a37af11 Mon Sep 17 00:00:00 2001 From: ZwX1616 Date: Wed, 18 Feb 2026 20:09:46 -0800 Subject: [PATCH 093/311] DM: Ford GT Le Mans Model (#37257) * b483cec4-7816-4570-a774-be3a2c100098/50 * shipfest * da4b8724-8998-45da-aa36-d8fb390492b9 * revert * typo * deprecates * 53a2718f-206b-4400-a70b-16915de18472/200 * bump * update --- cereal/log.capnp | 4 +-- .../modeld/models/dmonitoring_model.onnx | 4 +-- selfdrive/monitoring/helpers.py | 25 +++---------------- 3 files changed, 8 insertions(+), 25 deletions(-) diff --git a/cereal/log.capnp b/cereal/log.capnp index afb710e808..119cf29999 100644 --- a/cereal/log.capnp +++ b/cereal/log.capnp @@ -2232,9 +2232,9 @@ struct DriverMonitoringState @0xb83cda094a1da284 { isActiveMode @16 :Bool; isRHD @4 :Bool; uncertainCount @19 :UInt32; - phoneProbOffset @20 :Float32; - phoneProbValidCount @21 :UInt32; + phoneProbOffsetDEPRECATED @20 :Float32; + phoneProbValidCountDEPRECATED @21 :UInt32; isPreviewDEPRECATED @15 :Bool; rhdCheckedDEPRECATED @5 :Bool; eventsDEPRECATED @0 :List(Car.OnroadEventDEPRECATED); diff --git a/selfdrive/modeld/models/dmonitoring_model.onnx b/selfdrive/modeld/models/dmonitoring_model.onnx index 9b1c4a1834..24234d4c50 100644 --- a/selfdrive/modeld/models/dmonitoring_model.onnx +++ b/selfdrive/modeld/models/dmonitoring_model.onnx @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3446bf8b22e50e47669a25bf32460ae8baf8547037f346753e19ecbfcf6d4e59 -size 6954368 +oid sha256:5e8de9dc7df306700cce9c22b992e25b95a38f894c47adaea742a9cf8ba78e1a +size 7307246 diff --git a/selfdrive/monitoring/helpers.py b/selfdrive/monitoring/helpers.py index 3377ce6c68..91ddaaa9c1 100644 --- a/selfdrive/monitoring/helpers.py +++ b/selfdrive/monitoring/helpers.py @@ -35,14 +35,7 @@ class DRIVER_MONITOR_SETTINGS: self._EYE_THRESHOLD = 0.65 self._SG_THRESHOLD = 0.9 self._BLINK_THRESHOLD = 0.865 - - self._PHONE_THRESH = 0.75 if device_type == 'mici' else 0.4 - self._PHONE_THRESH2 = 15.0 - self._PHONE_MAX_OFFSET = 0.06 - self._PHONE_MIN_OFFSET = 0.025 - self._PHONE_DATA_AVG = 0.05 - self._PHONE_DATA_VAR = 3*0.005 - self._PHONE_MAX_COUNT = int(360 / self._DT_DMON) + self._PHONE_THRESH = 0.68 self._POSE_PITCH_THRESHOLD = 0.3133 self._POSE_PITCH_THRESHOLD_SLACK = 0.3237 @@ -152,11 +145,10 @@ class DriverMonitoring: # init driver status wheelpos_filter_raw_priors = (self.settings._WHEELPOS_DATA_AVG, self.settings._WHEELPOS_DATA_VAR, 2) - phone_filter_raw_priors = (self.settings._PHONE_DATA_AVG, self.settings._PHONE_DATA_VAR, 2) self.wheelpos = DriverProb(raw_priors=wheelpos_filter_raw_priors, max_trackable=self.settings._WHEELPOS_MAX_COUNT) - self.phone = DriverProb(raw_priors=phone_filter_raw_priors, max_trackable=self.settings._PHONE_MAX_COUNT) self.pose = DriverPose(settings=self.settings) self.blink = DriverBlink() + self.phone_prob = 0. self.always_on = always_on self.distracted_types = [] @@ -257,12 +249,7 @@ class DriverMonitoring: if (self.blink.left + self.blink.right)*0.5 > self.settings._BLINK_THRESHOLD: distracted_types.append(DistractedType.DISTRACTED_BLINK) - if self.phone.prob_calibrated: - using_phone = self.phone.prob > max(min(self.phone.prob_offseter.filtered_stat.M, self.settings._PHONE_MAX_OFFSET), self.settings._PHONE_MIN_OFFSET) \ - * self.settings._PHONE_THRESH2 - else: - using_phone = self.phone.prob > self.settings._PHONE_THRESH - if using_phone: + if self.phone_prob > self.settings._PHONE_THRESH: distracted_types.append(DistractedType.DISTRACTED_PHONE) return distracted_types @@ -301,7 +288,7 @@ class DriverMonitoring: * (driver_data.sunglassesProb < self.settings._SG_THRESHOLD) self.blink.right = driver_data.rightBlinkProb * (driver_data.rightEyeProb > self.settings._EYE_THRESHOLD) \ * (driver_data.sunglassesProb < self.settings._SG_THRESHOLD) - self.phone.prob = driver_data.phoneProb + self.phone_prob = driver_data.phoneProb self.distracted_types = self._get_distracted_types() self.driver_distracted = (DistractedType.DISTRACTED_PHONE in self.distracted_types @@ -315,11 +302,9 @@ class DriverMonitoring: if self.face_detected and car_speed > self.settings._POSE_CALIB_MIN_SPEED and self.pose.low_std and (not op_engaged or not self.driver_distracted): self.pose.pitch_offseter.push_and_update(self.pose.pitch) self.pose.yaw_offseter.push_and_update(self.pose.yaw) - self.phone.prob_offseter.push_and_update(self.phone.prob) self.pose.calibrated = self.pose.pitch_offseter.filtered_stat.n > self.settings._POSE_OFFSET_MIN_COUNT and \ self.pose.yaw_offseter.filtered_stat.n > self.settings._POSE_OFFSET_MIN_COUNT - self.phone.prob_calibrated = self.phone.prob_offseter.filtered_stat.n > self.settings._POSE_OFFSET_MIN_COUNT if self.face_detected and not self.driver_distracted: if model_std_max > self.settings._DCAM_UNCERTAIN_ALERT_THRESHOLD: @@ -425,8 +410,6 @@ class DriverMonitoring: "posePitchValidCount": self.pose.pitch_offseter.filtered_stat.n, "poseYawOffset": self.pose.yaw_offseter.filtered_stat.mean(), "poseYawValidCount": self.pose.yaw_offseter.filtered_stat.n, - "phoneProbOffset": self.phone.prob_offseter.filtered_stat.mean(), - "phoneProbValidCount": self.phone.prob_offseter.filtered_stat.n, "stepChange": self.step_change, "awarenessActive": self.awareness_active, "awarenessPassive": self.awareness_passive, From 0bb2f8c9d43eed52afd4fac6c8531965d968b382 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 19 Feb 2026 01:22:00 -0500 Subject: [PATCH 094/311] [bot] Update Python packages (#1707) Update Python packages Co-authored-by: github-actions[bot] --- opendbc_repo | 2 +- uv.lock | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/opendbc_repo b/opendbc_repo index a996ed701b..613a562bba 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit a996ed701bd0efe0ebcefa057373ba426dfb3a11 +Subproject commit 613a562bba80997b5c1c74cd799641d06fb2b38c diff --git a/uv.lock b/uv.lock index a6b43b6fe5..c2c8d3c8b6 100644 --- a/uv.lock +++ b/uv.lock @@ -414,11 +414,11 @@ wheels = [ [[package]] name = "filelock" -version = "3.24.2" +version = "3.24.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/02/a8/dae62680be63cbb3ff87cfa2f51cf766269514ea5488479d42fec5aa6f3a/filelock-3.24.2.tar.gz", hash = "sha256:c22803117490f156e59fafce621f0550a7a853e2bbf4f87f112b11d469b6c81b", size = 37601, upload-time = "2026-02-16T02:50:45.614Z" } +sdist = { url = "https://files.pythonhosted.org/packages/73/92/a8e2479937ff39185d20dd6a851c1a63e55849e447a55e798cc2e1f49c65/filelock-3.24.3.tar.gz", hash = "sha256:011a5644dc937c22699943ebbfc46e969cdde3e171470a6e40b9533e5a72affa", size = 37935, upload-time = "2026-02-19T00:48:20.543Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/04/a94ebfb4eaaa08db56725a40de2887e95de4e8641b9e902c311bfa00aa39/filelock-3.24.2-py3-none-any.whl", hash = "sha256:667d7dc0b7d1e1064dd5f8f8e80bdac157a6482e8d2e02cd16fd3b6b33bd6556", size = 24152, upload-time = "2026-02-16T02:50:44Z" }, + { url = "https://files.pythonhosted.org/packages/9c/0f/5d0c71a1aefeb08efff26272149e07ab922b64f46c63363756224bd6872e/filelock-3.24.3-py3-none-any.whl", hash = "sha256:426e9a4660391f7f8a810d71b0555bce9008b0a1cc342ab1f6947d37639e002d", size = 24331, upload-time = "2026-02-19T00:48:18.465Z" }, ] [[package]] From 5ccabb9d547d88794a3ee8808dfdf1bc88894aba Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Thu, 19 Feb 2026 01:36:08 -0500 Subject: [PATCH 095/311] [TIZI/TICI] ui: use `vCruiseCluster` and `vEgoCluster` for SLA `preActive` (#1708) --- selfdrive/ui/sunnypilot/onroad/speed_limit.py | 29 +++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/selfdrive/ui/sunnypilot/onroad/speed_limit.py b/selfdrive/ui/sunnypilot/onroad/speed_limit.py index a0b5ea3935..e8b5c5cfcd 100644 --- a/selfdrive/ui/sunnypilot/onroad/speed_limit.py +++ b/selfdrive/ui/sunnypilot/onroad/speed_limit.py @@ -22,6 +22,8 @@ from openpilot.system.ui.widgets import Widget METER_TO_FOOT = 3.28084 METER_TO_MILE = 0.000621371 AHEAD_THRESHOLD = 5 +SET_SPEED_NA = 255 +KM_TO_MILE = 0.621371 AssistState = custom.LongitudinalPlanSP.SpeedLimit.AssistState SpeedLimitSource = custom.LongitudinalPlanSP.SpeedLimit.Source @@ -58,8 +60,11 @@ class SpeedLimitRenderer(Widget): self.speed_limit_ahead_frame = 0 self.assist_frame = 0 - self.speed = 0.0 - self.set_speed = 0.0 + self.is_cruise_set: bool = False + self.is_cruise_available: bool = True + self.set_speed: float = SET_SPEED_NA + self.speed: float = 0.0 + self.v_ego_cluster_seen: bool = False self.font_bold = gui_app.font(FontWeight.BOLD) self.font_demi = gui_app.font(FontWeight.SEMI_BOLD) @@ -77,6 +82,8 @@ class SpeedLimitRenderer(Widget): def update(self): sm = ui_state.sm if sm.recv_frame["carState"] < ui_state.started_frame: + self.set_speed = SET_SPEED_NA + self.speed = 0.0 return if sm.updated["longitudinalPlanSP"]: @@ -106,9 +113,21 @@ class SpeedLimitRenderer(Widget): self.speed_limit_ahead_dist_prev = self.speed_limit_ahead_dist - cs = sm["carState"] - self.set_speed = cs.cruiseState.speed * self.speed_conv - v_ego = cs.vEgoCluster if cs.vEgoCluster != 0.0 else cs.vEgo + controls_state = sm['controlsState'] + car_state = sm["carState"] + + v_cruise_cluster = car_state.vCruiseCluster + self.set_speed = ( + controls_state.vCruiseDEPRECATED if v_cruise_cluster == 0.0 else v_cruise_cluster + ) + self.is_cruise_set = 0 < self.set_speed < SET_SPEED_NA + self.is_cruise_available = self.set_speed != -1 + + if self.is_cruise_set and not ui_state.is_metric: + self.set_speed *= KM_TO_MILE + + self.v_ego_cluster_seen = self.v_ego_cluster_seen or car_state.vEgoCluster != 0.0 + v_ego = car_state.vEgoCluster if self.v_ego_cluster_seen else car_state.vEgo self.speed = max(0.0, v_ego * self.speed_conv) @staticmethod From 612c518dd68b994186bf3d29970c44e673523afb Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Wed, 18 Feb 2026 22:42:05 -0800 Subject: [PATCH 096/311] WifiManager: signal-driven connection status (#37258) * signal driven wifi state * copy exactly * copy signal handler * remove is_connected * Revert "remove is_connected" This reverts commit f2246a70f4a29e9f3405947ca43d9404578c9d2d. * do 3 network * missing reason * do wifiui * clean up mici updater * rest * or not connecting * clean up is_connected * clean up wifiui * match wifiui state more exactly in network panel for wifi button * update active connection info after activation (used to do in _update_networks) * clean up prints * more * rm * not needed * clean up state machine a bit * more * more * indent * final clean up * debug * debug * wait for ip? * more * revert * just to see * ensure we emit activated even if we fail to get conn path from dbus * hmm * fine * back * back * Revert "back" This reverts commit 6464abe243c2a3bbf62b8f9a109b72ec3ddb3817. * debug flickering on forget then connect to another. commit before this is good * fix rare flicker when forgetting network and immediately connecting to another * clean up * clean up router stuff now * ugh wtf * stash -- wtf * Revert "stash -- wtf" This reverts commit 756a92a9c0530a16917303424e26447f258f17e4. * Revert "fix rare flicker when forgetting network and immediately connecting to" This reverts commit 90c5fc14551726765ab2524e7866ee8b3c5dee7c. * remove debug * fix * add issues * add flow * match previous behavior * it doesn't fix the flikcer * more atomic * Revert "more atomic" This reverts commit ead87c5a7a4030719b64138c12b9154ec82e73d9. * last test! last test! * really the race is here? * atomic wifi_state replace * not slow * clean up --- .../mici/layouts/settings/network/__init__.py | 16 +- .../mici/layouts/settings/network/wifi_ui.py | 40 ++-- system/ui/lib/networkmanager.py | 1 + system/ui/lib/wifi_manager.py | 196 ++++++++++++------ system/ui/widgets/network.py | 4 +- 5 files changed, 165 insertions(+), 92 deletions(-) diff --git a/selfdrive/ui/mici/layouts/settings/network/__init__.py b/selfdrive/ui/mici/layouts/settings/network/__init__.py index 659cd5ff3a..1940b680eb 100644 --- a/selfdrive/ui/mici/layouts/settings/network/__init__.py +++ b/selfdrive/ui/mici/layouts/settings/network/__init__.py @@ -10,7 +10,7 @@ from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.selfdrive.ui.lib.prime_state import PrimeType from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.widgets import NavWidget -from openpilot.system.ui.lib.wifi_manager import WifiManager, Network, MeteredType +from openpilot.system.ui.lib.wifi_manager import WifiManager, Network, MeteredType, ConnectStatus class NetworkPanelType(IntEnum): @@ -125,15 +125,13 @@ class NetworkLayoutMici(NavWidget): # Update wi-fi button with ssid and ip address # TODO: make sure we handle hidden ssids - connecting_ssid = self._wifi_manager.connecting_to_ssid - connected_network = next((network for network in self._wifi_manager.networks if network.is_connected), None) - if connecting_ssid: - display_network = next((n for n in self._wifi_manager.networks if n.ssid == connecting_ssid), None) - self._wifi_button.set_text(normalize_ssid(connecting_ssid)) + wifi_state = self._wifi_manager.wifi_state + display_network = next((n for n in self._wifi_manager.networks if n.ssid == wifi_state.ssid), None) + if wifi_state.status == ConnectStatus.CONNECTING: + self._wifi_button.set_text(normalize_ssid(wifi_state.ssid or "wi-fi")) self._wifi_button.set_value("connecting...") - elif connected_network is not None: - display_network = connected_network - self._wifi_button.set_text(normalize_ssid(connected_network.ssid)) + elif wifi_state.status == ConnectStatus.CONNECTED: + self._wifi_button.set_text(normalize_ssid(wifi_state.ssid or "wi-fi")) self._wifi_button.set_value(self._wifi_manager.ipv4_address or "obtaining IP...") else: display_network = None diff --git a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py index dd8b26e545..66b8e352f5 100644 --- a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py +++ b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py @@ -8,7 +8,7 @@ from openpilot.system.ui.widgets.label import UnifiedLabel from openpilot.selfdrive.ui.mici.widgets.dialog import BigMultiOptionDialog, BigInputDialog, BigDialogOptionButton, BigConfirmationDialogV2 from openpilot.system.ui.lib.application import gui_app, MousePos, FontWeight from openpilot.system.ui.widgets import Widget, NavWidget -from openpilot.system.ui.lib.wifi_manager import WifiManager, Network, SecurityType +from openpilot.system.ui.lib.wifi_manager import WifiManager, Network, SecurityType, WifiState def normalize_ssid(ssid: str) -> str: @@ -94,7 +94,7 @@ class WifiIcon(Widget): class WifiItem(BigDialogOptionButton): LEFT_MARGIN = 20 - def __init__(self, network: Network): + def __init__(self, network: Network, wifi_state_callback: Callable[[], WifiState]): super().__init__(network.ssid) self.set_rect(rl.Rectangle(0, 0, gui_app.width, self.HEIGHT)) @@ -102,6 +102,7 @@ class WifiItem(BigDialogOptionButton): self._selected_txt = gui_app.texture("icons_mici/settings/network/new/wifi_selected.png", 48, 96) self._network = network + self._wifi_state_callback = wifi_state_callback self._wifi_icon = WifiIcon() self._wifi_icon.set_current_network(network) @@ -119,7 +120,8 @@ class WifiItem(BigDialogOptionButton): def _render(self, _): disabled_alpha = 0.35 if not self.enabled else 1.0 - if self._network.is_connected: + # connecting or connected + if self._wifi_state_callback().ssid == self._network.ssid: selected_x = int(self._rect.x - self._selected_txt.width / 2) selected_y = int(self._rect.y + (self._rect.height - self._selected_txt.height) / 2) rl.draw_texture(self._selected_txt, selected_x, selected_y, rl.WHITE) @@ -214,7 +216,8 @@ class ForgetButton(Widget): class NetworkInfoPage(NavWidget): - def __init__(self, wifi_manager, connect_callback: Callable, forget_callback: Callable, open_network_manage_page: Callable): + def __init__(self, wifi_manager, connect_callback: Callable, forget_callback: Callable, open_network_manage_page: Callable, + connecting_callback: Callable[[], str | None], connected_callback: Callable[[], str | None]): super().__init__() self._wifi_manager = wifi_manager @@ -235,7 +238,8 @@ class NetworkInfoPage(NavWidget): # State self._network: Network | None = None - self._connecting: Callable[[], str | None] | None = None + self._connecting_callback = connecting_callback + self._connected_callback = connected_callback def show_event(self): super().show_event() @@ -263,7 +267,7 @@ class NetworkInfoPage(NavWidget): if self._is_connecting: self._connect_btn.set_label("connecting...") self._connect_btn.set_enabled(False) - elif self._network.is_connected: + elif self._is_connected: self._connect_btn.set_label("connected") self._connect_btn.set_enabled(False) elif self._network.security_type == SecurityType.UNSUPPORTED: @@ -285,16 +289,20 @@ class NetworkInfoPage(NavWidget): self._network = network self._wifi_icon.set_current_network(network) - def set_connecting(self, is_connecting: Callable[[], str | None]): - self._connecting = is_connecting - @property def _is_connecting(self): - if self._connecting is None or self._network is None: + if self._network is None: return False - is_connecting = self._connecting() == self._network.ssid + is_connecting = self._connecting_callback() == self._network.ssid return is_connecting + @property + def _is_connected(self): + if self._network is None: + return False + is_connected = self._connected_callback() == self._network.ssid + return is_connected + def _render(self, _): self._wifi_icon.render(rl.Rectangle( self._rect.x + 32, @@ -342,8 +350,8 @@ class WifiUIMici(BigMultiOptionDialog): # Set up back navigation self.set_back_callback(back_callback) - self._network_info_page = NetworkInfoPage(wifi_manager, self._connect_to_network, wifi_manager.forget_connection, self._open_network_manage_page) - self._network_info_page.set_connecting(lambda: wifi_manager.connecting_to_ssid) + self._network_info_page = NetworkInfoPage(wifi_manager, self._connect_to_network, wifi_manager.forget_connection, self._open_network_manage_page, + lambda: wifi_manager.connecting_to_ssid, lambda: wifi_manager.connected_ssid) self._loading_animation = LoadingAnimation() @@ -385,11 +393,11 @@ class WifiUIMici(BigMultiOptionDialog): # Update network on existing button self._scroller._items[network_button_idx].set_current_network(network) else: - network_button = WifiItem(network) + network_button = WifiItem(network, lambda: self._wifi_manager.wifi_state) self._scroller.add_widget(network_button) - # Move connected network to the start - connected_btn_idx = next((i for i, btn in enumerate(self._scroller._items) if btn._network.is_connected), None) + # Move connecting/connected network to the start + connected_btn_idx = next((i for i, btn in enumerate(self._scroller._items) if self._wifi_manager.wifi_state.ssid == btn._network.ssid), None) if connected_btn_idx is not None and connected_btn_idx > 0: self._scroller._items.insert(0, self._scroller._items.pop(connected_btn_idx)) self._scroller._layout() # fixes selected style single frame stutter diff --git a/system/ui/lib/networkmanager.py b/system/ui/lib/networkmanager.py index f16271a505..e04e3eeadc 100644 --- a/system/ui/lib/networkmanager.py +++ b/system/ui/lib/networkmanager.py @@ -25,6 +25,7 @@ class NMDeviceStateReason(IntEnum): UNKNOWN = 1 NO_SECRETS = 7 SUPPLICANT_DISCONNECT = 8 + CONNECTION_REMOVED = 38 NEW_ACTIVATION = 60 diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index 52246beb7e..479bad7ece 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -4,7 +4,7 @@ import time import uuid import subprocess from collections.abc import Callable -from dataclasses import dataclass +from dataclasses import dataclass, replace from enum import IntEnum from typing import Any @@ -88,24 +88,19 @@ def get_security_type(flags: int, wpa_flags: int, rsn_flags: int) -> SecurityTyp class Network: ssid: str strength: int - is_connected: bool security_type: SecurityType is_saved: bool ip_address: str = "" # TODO: implement @classmethod - def from_dbus(cls, ssid: str, aps: list["AccessPoint"], is_saved: bool, active_connection: bool) -> "Network": + def from_dbus(cls, ssid: str, aps: list["AccessPoint"], is_saved: bool) -> "Network": # we only want to show the strongest AP for each Network/SSID strongest_ap = max(aps, key=lambda ap: ap.strength) - # fall back to ActiveConnection during momentary AP roaming or low strength networks. matches GNOME shell behavior - # https://github.com/GNOME/gnome-shell/blob/3f8b174274fac7d69477523d4873ef8253e1ed49/js/ui/status/network.js#L810-L819 - is_connected = any(ap.is_connected for ap in aps) or active_connection security_type = get_security_type(strongest_ap.flags, strongest_ap.wpa_flags, strongest_ap.rsn_flags) return cls( ssid=ssid, strength=strongest_ap.strength, - is_connected=is_connected and is_saved, security_type=security_type, is_saved=is_saved, ) @@ -116,14 +111,13 @@ class AccessPoint: ssid: str bssid: str strength: int - is_connected: bool flags: int wpa_flags: int rsn_flags: int ap_path: str @classmethod - def from_dbus(cls, ap_props: dict[str, tuple[str, Any]], ap_path: str, active_ap_path: str) -> "AccessPoint": + def from_dbus(cls, ap_props: dict[str, tuple[str, Any]], ap_path: str) -> "AccessPoint": ssid = bytes(ap_props['Ssid'][1]).decode("utf-8", "replace") bssid = str(ap_props['HwAddress'][1]) strength = int(ap_props['Strength'][1]) @@ -135,7 +129,6 @@ class AccessPoint: ssid=ssid, bssid=bssid, strength=strength, - is_connected=ap_path == active_ap_path, flags=flags, wpa_flags=wpa_flags, rsn_flags=rsn_flags, @@ -143,6 +136,18 @@ class AccessPoint: ) +class ConnectStatus(IntEnum): + DISCONNECTED = 0 + CONNECTING = 1 + CONNECTED = 2 + + +@dataclass +class WifiState: + ssid: str | None = None + status: ConnectStatus = ConnectStatus.DISCONNECTED + + class WifiManager: def __init__(self): self._networks: list[Network] = [] # a network can be comprised of multiple APs @@ -166,8 +171,7 @@ class WifiManager: # State self._connections: dict[str, str] = {} # ssid -> connection path, updated via NM signals - self._connecting_to_ssid: str | None = None - self._prev_connecting_to_ssid: str | None = None + self._wifi_state: WifiState = WifiState() self._ipv4_address: str = "" self._current_network_metered: MeteredType = MeteredType.UNKNOWN self._tethering_password: str = "" @@ -199,18 +203,41 @@ class WifiManager: def worker(): self._wait_for_wifi_device() - self._scan_thread.start() - self._state_thread.start() - self._init_connections() if Params is not None and self._tethering_ssid not in self._connections: self._add_tethering_connection() + self._init_wifi_state() + + self._scan_thread.start() + self._state_thread.start() + self._tethering_password = self._get_tethering_password() cloudlog.debug("WifiManager initialized") threading.Thread(target=worker, daemon=True).start() + def _init_wifi_state(self, block: bool = True): + def worker(): + dev_addr = DBusAddress(self._wifi_device, bus_name=NM, interface=NM_DEVICE_IFACE) + dev_state = self._router_main.send_and_get_reply(Properties(dev_addr).get('State')).body[0][1] + + wifi_state = WifiState() + if NMDeviceState.PREPARE <= dev_state <= NMDeviceState.SECONDARIES and dev_state != NMDeviceState.NEED_AUTH: + wifi_state.status = ConnectStatus.CONNECTING + elif dev_state == NMDeviceState.ACTIVATED: + wifi_state.status = ConnectStatus.CONNECTED + + conn_path, _ = self._get_active_wifi_connection() + if conn_path: + wifi_state.ssid = next((s for s, p in self._connections.items() if p == conn_path), None) + self._wifi_state = wifi_state + + if block: + worker() + else: + threading.Thread(target=worker, daemon=True).start() + def add_callbacks(self, need_auth: Callable[[str], None] | None = None, activated: Callable[[], None] | None = None, forgotten: Callable[[str], None] | None = None, @@ -231,6 +258,10 @@ class WifiManager: def networks(self) -> list[Network]: return self._networks + @property + def wifi_state(self) -> WifiState: + return self._wifi_state + @property def ipv4_address(self) -> str: return self._ipv4_address @@ -241,15 +272,18 @@ class WifiManager: @property def connecting_to_ssid(self) -> str | None: - return self._connecting_to_ssid + return self._wifi_state.ssid if self._wifi_state.status == ConnectStatus.CONNECTING else None + + @property + def connected_ssid(self) -> str | None: + return self._wifi_state.ssid if self._wifi_state.status == ConnectStatus.CONNECTED else None @property def tethering_password(self) -> str: return self._tethering_password - def _set_connecting(self, ssid: str): - self._prev_connecting_to_ssid = self._connecting_to_ssid - self._connecting_to_ssid = ssid + def _set_connecting(self, ssid: str | None): + self._wifi_state = WifiState(ssid=ssid, status=ConnectStatus.DISCONNECTED if ssid is None else ConnectStatus.CONNECTING) def _enqueue_callbacks(self, cbs: list[Callable], *args): for cb in cbs: @@ -264,8 +298,9 @@ class WifiManager: def set_active(self, active: bool): self._active = active - # Update networks immediately when activating for UI + # Update networks and WiFi state (to self-heal) immediately when activating for UI if active: + self._init_wifi_state(block=False) self._update_networks(block=False) def _monitor_state(self): @@ -325,43 +360,77 @@ class WifiManager: self._update_networks() # Device state changes + # TODO: known race conditions when switching networks (e.g. forget A, connect to B): + # 1. DEACTIVATING/DISCONNECTED + CONNECTION_REMOVED: fires before NewConnection for B + # arrives, so _set_connecting(None) clears B's CONNECTING state causing UI flicker. + # DEACTIVATING(CONNECTION_REMOVED): wifi_state (B, CONNECTING) -> (None, DISCONNECTED) + # Fix: make DEACTIVATING a no-op, and guard DISCONNECTED with + # `if wifi_state.ssid not in _connections` (NewConnection arrives between the two). + # 2. PREPARE/CONFIG ssid lookup: DBus may return stale A's conn_path, overwriting B. + # PREPARE(0): wifi_state (B, CONNECTING) -> (A, CONNECTING) + # Fix: only do DBus lookup when wifi_state.ssid is None (auto-connections); + # user-initiated connections already have ssid set via _set_connecting. while len(state_q): new_state, previous_state, change_reason = state_q.popleft().body - # BAD PASSWORD - use prev if current has already moved on to a new connection - # - strong network rejects with NEED_AUTH+SUPPLICANT_DISCONNECT - # - weak/gone network fails with FAILED+NO_SECRETS - if ((new_state == NMDeviceState.NEED_AUTH and change_reason == NMDeviceStateReason.SUPPLICANT_DISCONNECT) or - (new_state == NMDeviceState.FAILED and change_reason == NMDeviceStateReason.NO_SECRETS)): - failed_ssid = self._prev_connecting_to_ssid or self._connecting_to_ssid - if failed_ssid: - self._enqueue_callbacks(self._need_auth, failed_ssid) - self.forget_connection(failed_ssid, block=True) - self._prev_connecting_to_ssid = None - if self._connecting_to_ssid == failed_ssid: - self._connecting_to_ssid = None + if new_state == NMDeviceState.DISCONNECTED: + if change_reason != NMDeviceStateReason.NEW_ACTIVATION: + # catches CONNECTION_REMOVED reason when connection is forgotten + self._set_connecting(None) - elif new_state == NMDeviceState.PREPARE and self._connecting_to_ssid is None: + elif new_state in (NMDeviceState.PREPARE, NMDeviceState.CONFIG): # Set connecting status when NetworkManager connects to known networks on its own + wifi_state = replace(self._wifi_state, status=ConnectStatus.CONNECTING) + conn_path, _ = self._get_active_wifi_connection(self._conn_monitor) if conn_path is None: - cloudlog.warning("Failed to get active wifi connection during PREPARE state") - continue + cloudlog.warning("Failed to get active wifi connection during PREPARE/CONFIG state") + else: + wifi_state.ssid = next((s for s, p in self._connections.items() if p == conn_path), None) - ssid = next((s for s, p in self._connections.items() if p == conn_path), None) - if ssid: - self._set_connecting(ssid) + self._wifi_state = wifi_state + + # BAD PASSWORD + # - strong network rejects with NEED_AUTH+SUPPLICANT_DISCONNECT + # - weak/gone network fails with FAILED+NO_SECRETS + elif ((new_state == NMDeviceState.NEED_AUTH and change_reason == NMDeviceStateReason.SUPPLICANT_DISCONNECT) or + (new_state == NMDeviceState.FAILED and change_reason == NMDeviceStateReason.NO_SECRETS)): + + if self._wifi_state.ssid: + self._enqueue_callbacks(self._need_auth, self._wifi_state.ssid) + + self._set_connecting(None) + + elif new_state in (NMDeviceState.NEED_AUTH, NMDeviceState.IP_CONFIG, NMDeviceState.IP_CHECK, + NMDeviceState.SECONDARIES, NMDeviceState.FAILED): + pass elif new_state == NMDeviceState.ACTIVATED: + # Note that IP address from Ip4Config may not be propagated immediately and could take until the next scan results self._update_networks() - self._enqueue_callbacks(self._activated) - self._prev_connecting_to_ssid = None - self._connecting_to_ssid = None - elif new_state == NMDeviceState.DISCONNECTED and change_reason != NMDeviceStateReason.NEW_ACTIVATION: - self._enqueue_callbacks(self._forgotten, self._connecting_to_ssid) - self._prev_connecting_to_ssid = None - self._connecting_to_ssid = None + wifi_state = replace(self._wifi_state, status=ConnectStatus.CONNECTED) + + conn_path, _ = self._get_active_wifi_connection(self._conn_monitor) + if conn_path is None: + cloudlog.warning("Failed to get active wifi connection during ACTIVATED state") + self._wifi_state = wifi_state + self._enqueue_callbacks(self._activated) + else: + wifi_state.ssid = next((s for s, p in self._connections.items() if p == conn_path), None) + self._wifi_state = wifi_state + self._enqueue_callbacks(self._activated) + + # Persist volatile connections (created by AddAndActivateConnection2) to disk + conn_addr = DBusAddress(conn_path, bus_name=NM, interface=NM_CONNECTION_IFACE) + save_reply = self._conn_monitor.send_and_get_reply(new_method_call(conn_addr, 'Save')) + if save_reply.header.message_type == MessageType.error: + cloudlog.warning(f"Failed to persist connection to disk: {save_reply}") + + elif new_state == NMDeviceState.DEACTIVATING: + if change_reason == NMDeviceStateReason.CONNECTION_REMOVED: + # When connection is forgotten + self._set_connecting(None) def _network_scanner(self): while not self._exit: @@ -417,8 +486,6 @@ class WifiManager: ssid = settings['802-11-wireless']['ssid'][1].decode("utf-8", "replace") if ssid != "": self._connections[ssid] = conn_path - if ssid != self._tethering_ssid: - self.activate_connection(ssid, block=True) def _connection_removed(self, conn_path: str): self._connections = {ssid: path for ssid, path in self._connections.items() if path != conn_path} @@ -529,13 +596,19 @@ class WifiManager: 'psk': ('s', password), } - settings_addr = DBusAddress(NM_SETTINGS_PATH, bus_name=NM, interface=NM_SETTINGS_IFACE) - reply = self._router_main.send_and_get_reply(new_method_call(settings_addr, 'AddConnection', 'a{sa{sv}}', (connection,))) + # Volatile connection auto-deletes on disconnect (wrong password, user switches networks) + # Persisted to disk on ACTIVATED via Save() + if self._wifi_device is None: + cloudlog.warning("No WiFi device found") + self._set_connecting(None) + return + + reply = self._router_main.send_and_get_reply(new_method_call(self._nm, 'AddAndActivateConnection2', 'a{sa{sv}}ooa{sv}', + (connection, self._wifi_device, "/", {'persist': ('s', 'volatile')}))) if reply.header.message_type == MessageType.error: - cloudlog.warning(f"Failed to add connection for {ssid}: {reply}") - self._connecting_to_ssid = None - self._prev_connecting_to_ssid = None + cloudlog.warning(f"Failed to add and activate connection for {ssid}: {reply}") + self._set_connecting(None) threading.Thread(target=worker, daemon=True).start() @@ -549,8 +622,7 @@ class WifiManager: conn_addr = DBusAddress(conn_path, bus_name=NM, interface=NM_CONNECTION_IFACE) self._router_main.send_and_get_reply(new_method_call(conn_addr, 'Delete')) - if len(self._forgotten): - self._update_networks() + self._update_networks() self._enqueue_callbacks(self._forgotten, ssid) if block: @@ -590,10 +662,8 @@ class WifiManager: return def is_tethering_active(self) -> bool: - for network in self._networks: - if network.is_connected: - return bool(network.ssid == self._tethering_ssid) - return False + # Check ssid, not connected_ssid, to also catch connecting state + return self._wifi_state.ssid == self._tethering_ssid def set_tethering_password(self, password: str): def worker(): @@ -708,7 +778,6 @@ class WifiManager: # NOTE: AccessPoints property may exclude hidden APs (use GetAllAccessPoints method if needed) wifi_addr = DBusAddress(self._wifi_device, NM, interface=NM_WIRELESS_IFACE) wifi_props = self._router_main.send_and_get_reply(Properties(wifi_addr).get_all()).body[0] - active_ap_path = wifi_props.get('ActiveAccessPoint', ('o', '/'))[1] ap_paths = wifi_props.get('AccessPoints', ('ao', []))[1] aps: dict[str, list[AccessPoint]] = {} @@ -723,7 +792,7 @@ class WifiManager: continue try: - ap = AccessPoint.from_dbus(ap_props.body[0], ap_path, active_ap_path) + ap = AccessPoint.from_dbus(ap_props.body[0], ap_path) if ap.ssid == "": continue @@ -735,11 +804,8 @@ class WifiManager: # catch all for parsing errors cloudlog.exception(f"Failed to parse AP properties for {ap_path}") - active_wifi_connection, _ = self._get_active_wifi_connection() - networks = [Network.from_dbus(ssid, ap_list, ssid in self._connections, - active_wifi_connection is not None and - self._connections.get(ssid) == active_wifi_connection) for ssid, ap_list in aps.items()] - networks.sort(key=lambda n: (-n.is_connected, -n.is_saved, -n.strength, n.ssid.lower())) + networks = [Network.from_dbus(ssid, ap_list, ssid in self._connections) for ssid, ap_list in aps.items()] + networks.sort(key=lambda n: (n.ssid != self._wifi_state.ssid, -n.is_saved, -n.strength, n.ssid.lower())) self._networks = networks self._update_active_connection_info() diff --git a/system/ui/widgets/network.py b/system/ui/widgets/network.py index 9de44c585c..186164916b 100644 --- a/system/ui/widgets/network.py +++ b/system/ui/widgets/network.py @@ -400,7 +400,7 @@ class WifiManagerUI(Widget): self.state = UIState.NEEDS_AUTH self._state_network = network self._password_retry = False - elif not network.is_connected: + elif self._wifi_manager.wifi_state.ssid != network.ssid: self.connect_to_network(network) def _forget_networks_buttons_callback(self, network): @@ -410,7 +410,7 @@ class WifiManagerUI(Widget): def _draw_status_icon(self, rect, network: Network): """Draw the status icon based on network's connection state""" icon_file = None - if network.is_connected and self.state != UIState.CONNECTING: + if self._wifi_manager.connected_ssid == network.ssid and self.state != UIState.CONNECTING: icon_file = "icons/checkmark.png" elif network.security_type == SecurityType.UNSUPPORTED: icon_file = "icons/circled_slash.png" From a28cc71b8b940383c0352960a16365a84b5fa518 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 19 Feb 2026 00:12:49 -0800 Subject: [PATCH 097/311] WifiManager: always emit forgot callback (#37261) * fixme * fixme * fixme --- system/ui/lib/wifi_manager.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index 479bad7ece..8f1382a52c 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -617,11 +617,12 @@ class WifiManager: conn_path = self._connections.get(ssid, None) if conn_path is None: cloudlog.warning(f"Trying to forget unknown connection: {ssid}") - return - - conn_addr = DBusAddress(conn_path, bus_name=NM, interface=NM_CONNECTION_IFACE) - self._router_main.send_and_get_reply(new_method_call(conn_addr, 'Delete')) + else: + conn_addr = DBusAddress(conn_path, bus_name=NM, interface=NM_CONNECTION_IFACE) + self._router_main.send_and_get_reply(new_method_call(conn_addr, 'Delete')) + # FIXME: race here where ConnectionRemoved signal may arrive after we update all Network is_saved + # and keep the old ssid's is_saved=True self._update_networks() self._enqueue_callbacks(self._forgotten, ssid) From c736d43cce2bc3c7fa62212a6b60bbbdf21e5a01 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 19 Feb 2026 00:40:20 -0800 Subject: [PATCH 098/311] Remove old TODO in WifiManager hell no --- system/ui/lib/wifi_manager.py | 1 - 1 file changed, 1 deletion(-) diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index 8f1382a52c..05eac7959a 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -90,7 +90,6 @@ class Network: strength: int security_type: SecurityType is_saved: bool - ip_address: str = "" # TODO: implement @classmethod def from_dbus(cls, ssid: str, aps: list["AccessPoint"], is_saved: bool) -> "Network": From a3f2452fa7e5cd5034a46175bf46232d5381579a Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 19 Feb 2026 00:49:35 -0800 Subject: [PATCH 099/311] WifiManager: single source for known connections (#37262) * temp * rev * reproduce race condition where connection removed signal takes a while to remove, then update networks keep is_saved true * fix * Revert "reproduce race condition where connection removed signal takes a while to remove, then update networks keep is_saved true" This reverts commit cf7044ee955777db16434ab81c520bbe798c9164. * not anymore * more clear * safe guards nl --- .../mici/layouts/settings/network/wifi_ui.py | 4 +-- system/ui/lib/wifi_manager.py | 29 ++++++++++--------- system/ui/widgets/network.py | 6 ++-- 3 files changed, 21 insertions(+), 18 deletions(-) diff --git a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py index 66b8e352f5..e8fefb82dd 100644 --- a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py +++ b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py @@ -263,7 +263,7 @@ class NetworkInfoPage(NavWidget): if self._network is None: return - self._connect_btn.set_full(not self._network.is_saved and not self._is_connecting) + self._connect_btn.set_full(not self._wifi_manager.is_connection_saved(self._network.ssid) and not self._is_connecting) if self._is_connecting: self._connect_btn.set_label("connecting...") self._connect_btn.set_enabled(False) @@ -425,7 +425,7 @@ class WifiUIMici(BigMultiOptionDialog): cloudlog.warning(f"Trying to connect to unknown network: {ssid}") return - if network.is_saved: + if self._wifi_manager.is_connection_saved(network.ssid): self._wifi_manager.activate_connection(network.ssid) self._update_buttons() elif network.security_type == SecurityType.OPEN: diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index 05eac7959a..a5b5833d7f 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -89,10 +89,9 @@ class Network: ssid: str strength: int security_type: SecurityType - is_saved: bool @classmethod - def from_dbus(cls, ssid: str, aps: list["AccessPoint"], is_saved: bool) -> "Network": + def from_dbus(cls, ssid: str, aps: list["AccessPoint"]) -> "Network": # we only want to show the strongest AP for each Network/SSID strongest_ap = max(aps, key=lambda ap: ap.strength) security_type = get_security_type(strongest_ap.flags, strongest_ap.wpa_flags, strongest_ap.rsn_flags) @@ -101,7 +100,6 @@ class Network: ssid=ssid, strength=strongest_ap.strength, security_type=security_type, - is_saved=is_saved, ) @@ -620,8 +618,6 @@ class WifiManager: conn_addr = DBusAddress(conn_path, bus_name=NM, interface=NM_CONNECTION_IFACE) self._router_main.send_and_get_reply(new_method_call(conn_addr, 'Delete')) - # FIXME: race here where ConnectionRemoved signal may arrive after we update all Network is_saved - # and keep the old ssid's is_saved=True self._update_networks() self._enqueue_callbacks(self._forgotten, ssid) @@ -635,13 +631,17 @@ class WifiManager: def worker(): conn_path = self._connections.get(ssid, None) - if conn_path is not None: - if self._wifi_device is None: - cloudlog.warning("No WiFi device found") - return + if conn_path is None or self._wifi_device is None: + cloudlog.warning(f"Failed to activate connection for {ssid}: conn_path={conn_path}, wifi_device={self._wifi_device}") + self._set_connecting(None) + return - self._router_main.send(new_method_call(self._nm, 'ActivateConnection', 'ooo', - (conn_path, self._wifi_device, "/"))) + reply = self._router_main.send_and_get_reply(new_method_call(self._nm, 'ActivateConnection', 'ooo', + (conn_path, self._wifi_device, "/"))) + + if reply.header.message_type == MessageType.error: + cloudlog.warning(f"Failed to activate connection for {ssid}: {reply}") + self._set_connecting(None) if block: worker() @@ -665,6 +665,9 @@ class WifiManager: # Check ssid, not connected_ssid, to also catch connecting state return self._wifi_state.ssid == self._tethering_ssid + def is_connection_saved(self, ssid: str) -> bool: + return ssid in self._connections + def set_tethering_password(self, password: str): def worker(): conn_path = self._connections.get(self._tethering_ssid, None) @@ -804,8 +807,8 @@ class WifiManager: # catch all for parsing errors cloudlog.exception(f"Failed to parse AP properties for {ap_path}") - networks = [Network.from_dbus(ssid, ap_list, ssid in self._connections) for ssid, ap_list in aps.items()] - networks.sort(key=lambda n: (n.ssid != self._wifi_state.ssid, -n.is_saved, -n.strength, n.ssid.lower())) + networks = [Network.from_dbus(ssid, ap_list) for ssid, ap_list in aps.items()] + networks.sort(key=lambda n: (n.ssid != self._wifi_state.ssid, not self.is_connection_saved(n.ssid), -n.strength, n.ssid.lower())) self._networks = networks self._update_active_connection_info() diff --git a/system/ui/widgets/network.py b/system/ui/widgets/network.py index 186164916b..3e5e4a1d55 100644 --- a/system/ui/widgets/network.py +++ b/system/ui/widgets/network.py @@ -383,7 +383,7 @@ class WifiManagerUI(Widget): gui_label(status_text_rect, status_text, font_size=48, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER) else: # If the network is saved, show the "Forget" button - if network.is_saved: + if self._wifi_manager.is_connection_saved(network.ssid): forget_btn_rect = rl.Rectangle( security_icon_rect.x - self.btn_width - spacing, rect.y + (ITEM_HEIGHT - 80) / 2, @@ -396,7 +396,7 @@ class WifiManagerUI(Widget): self._draw_signal_strength_icon(signal_icon_rect, network) def _networks_buttons_callback(self, network): - if not network.is_saved and network.security_type != SecurityType.OPEN: + if not self._wifi_manager.is_connection_saved(network.ssid) and network.security_type != SecurityType.OPEN: self.state = UIState.NEEDS_AUTH self._state_network = network self._password_retry = False @@ -432,7 +432,7 @@ class WifiManagerUI(Widget): def connect_to_network(self, network: Network, password=''): self.state = UIState.CONNECTING self._state_network = network - if network.is_saved and not password: + if self._wifi_manager.is_connection_saved(network.ssid) and not password: self._wifi_manager.activate_connection(network.ssid) else: self._wifi_manager.connect_to_network(network.ssid, password) From 69544c57fd09fbaad4951c7bbe2bde38d8041096 Mon Sep 17 00:00:00 2001 From: Trey Moen <50057480+greatgitsby@users.noreply.github.com> Date: Thu, 19 Feb 2026 10:28:04 -0700 Subject: [PATCH 100/311] refactor(esim): cleanup lpa (#37260) cleanup lpa --- system/hardware/esim.py | 1 - system/hardware/tici/esim.py | 106 ------------------------ system/hardware/tici/hardware.py | 2 +- system/hardware/tici/lpa.py | 24 ++++++ system/hardware/tici/tests/test_esim.py | 51 ------------ 5 files changed, 25 insertions(+), 159 deletions(-) delete mode 100644 system/hardware/tici/esim.py create mode 100644 system/hardware/tici/lpa.py delete mode 100644 system/hardware/tici/tests/test_esim.py diff --git a/system/hardware/esim.py b/system/hardware/esim.py index 58ead6593f..9b7d4f9ec0 100755 --- a/system/hardware/esim.py +++ b/system/hardware/esim.py @@ -7,7 +7,6 @@ from openpilot.system.hardware import HARDWARE if __name__ == '__main__': parser = argparse.ArgumentParser(prog='esim.py', description='manage eSIM profiles on your comma device', epilog='comma.ai') - parser.add_argument('--backend', choices=['qmi', 'at'], default='qmi', help='use the specified backend, defaults to qmi') parser.add_argument('--switch', metavar='iccid', help='switch to profile') parser.add_argument('--delete', metavar='iccid', help='delete profile (warning: this cannot be undone)') parser.add_argument('--download', nargs=2, metavar=('qr', 'name'), help='download a profile using QR code (format: LPA:1$rsp.truphone.com$QRF-SPEEDTEST)') diff --git a/system/hardware/tici/esim.py b/system/hardware/tici/esim.py deleted file mode 100644 index b489286f50..0000000000 --- a/system/hardware/tici/esim.py +++ /dev/null @@ -1,106 +0,0 @@ -import json -import os -import shutil -import subprocess -from typing import Literal - -from openpilot.system.hardware.base import LPABase, LPAError, LPAProfileNotFoundError, Profile - -class TiciLPA(LPABase): - def __init__(self, interface: Literal['qmi', 'at'] = 'qmi'): - self.env = os.environ.copy() - self.env['LPAC_APDU'] = interface - self.env['QMI_DEVICE'] = '/dev/cdc-wdm0' - self.env['AT_DEVICE'] = '/dev/ttyUSB2' - - self.timeout_sec = 45 - - if shutil.which('lpac') is None: - raise LPAError('lpac not found, must be installed!') - - def list_profiles(self) -> list[Profile]: - msgs = self._invoke('profile', 'list') - self._validate_successful(msgs) - return [Profile( - iccid=p['iccid'], - nickname=p['profileNickname'], - enabled=p['profileState'] == 'enabled', - provider=p['serviceProviderName'] - ) for p in msgs[-1]['payload']['data']] - - def get_active_profile(self) -> Profile | None: - return next((p for p in self.list_profiles() if p.enabled), None) - - def delete_profile(self, iccid: str) -> None: - self._validate_profile_exists(iccid) - latest = self.get_active_profile() - if latest is not None and latest.iccid == iccid: - raise LPAError('cannot delete active profile, switch to another profile first') - self._validate_successful(self._invoke('profile', 'delete', iccid)) - self._process_notifications() - - def download_profile(self, qr: str, nickname: str | None = None) -> None: - msgs = self._invoke('profile', 'download', '-a', qr) - self._validate_successful(msgs) - new_profile = next((m for m in msgs if m['payload']['message'] == 'es8p_meatadata_parse'), None) - if new_profile is None: - raise LPAError('no new profile found') - if nickname: - self.nickname_profile(new_profile['payload']['data']['iccid'], nickname) - self._process_notifications() - - def nickname_profile(self, iccid: str, nickname: str) -> None: - self._validate_profile_exists(iccid) - self._validate_successful(self._invoke('profile', 'nickname', iccid, nickname)) - - def switch_profile(self, iccid: str) -> None: - self._validate_profile_exists(iccid) - latest = self.get_active_profile() - if latest and latest.iccid == iccid: - return - self._validate_successful(self._invoke('profile', 'enable', iccid)) - self._process_notifications() - - def _invoke(self, *cmd: str): - proc = subprocess.Popen(['sudo', '-E', 'lpac'] + list(cmd), stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=self.env) - try: - out, err = proc.communicate(timeout=self.timeout_sec) - except subprocess.TimeoutExpired as e: - proc.kill() - raise LPAError(f"lpac {cmd} timed out after {self.timeout_sec} seconds") from e - - messages = [] - for line in out.decode().strip().splitlines(): - if line.startswith('{'): - message = json.loads(line) - - # lpac response format validations - assert 'type' in message, 'expected type in message' - assert message['type'] == 'lpa' or message['type'] == 'progress', 'expected lpa or progress message type' - assert 'payload' in message, 'expected payload in message' - assert 'code' in message['payload'], 'expected code in message payload' - assert 'data' in message['payload'], 'expected data in message payload' - - msg_ret_code = message['payload']['code'] - if msg_ret_code != 0: - raise LPAError(f"lpac {' '.join(cmd)} failed with code {msg_ret_code}: <{message['payload']['message']}> {message['payload']['data']}") - - messages.append(message) - - if len(messages) == 0: - raise LPAError(f"lpac {cmd} returned no messages") - - return messages - - def _process_notifications(self) -> None: - """ - Process notifications stored on the eUICC, typically to activate/deactivate the profile with the carrier. - """ - self._validate_successful(self._invoke('notification', 'process', '-a', '-r')) - - def _validate_profile_exists(self, iccid: str) -> None: - if not any(p.iccid == iccid for p in self.list_profiles()): - raise LPAProfileNotFoundError(f'profile {iccid} does not exist') - - def _validate_successful(self, msgs: list[dict]) -> None: - assert msgs[-1]['payload']['message'] == 'success', 'expected success notification' diff --git a/system/hardware/tici/hardware.py b/system/hardware/tici/hardware.py index 5a84afce03..2295ca3cba 100644 --- a/system/hardware/tici/hardware.py +++ b/system/hardware/tici/hardware.py @@ -12,7 +12,7 @@ from openpilot.common.utils import sudo_read, sudo_write from openpilot.common.gpio import gpio_set, gpio_init, get_irqs_for_action from openpilot.system.hardware.base import HardwareBase, LPABase, ThermalConfig, ThermalZone from openpilot.system.hardware.tici import iwlist -from openpilot.system.hardware.tici.esim import TiciLPA +from openpilot.system.hardware.tici.lpa import TiciLPA from openpilot.system.hardware.tici.pins import GPIO from openpilot.system.hardware.tici.amplifier import Amplifier diff --git a/system/hardware/tici/lpa.py b/system/hardware/tici/lpa.py new file mode 100644 index 0000000000..9bd9d8c7b0 --- /dev/null +++ b/system/hardware/tici/lpa.py @@ -0,0 +1,24 @@ +from openpilot.system.hardware.base import LPABase, Profile + + +class TiciLPA(LPABase): + def __init__(self): + pass + + def list_profiles(self) -> list[Profile]: + return [] + + def get_active_profile(self) -> Profile | None: + return None + + def delete_profile(self, iccid: str) -> None: + return None + + def download_profile(self, qr: str, nickname: str | None = None) -> None: + return None + + def nickname_profile(self, iccid: str, nickname: str) -> None: + return None + + def switch_profile(self, iccid: str) -> None: + return None diff --git a/system/hardware/tici/tests/test_esim.py b/system/hardware/tici/tests/test_esim.py deleted file mode 100644 index 6fab931cce..0000000000 --- a/system/hardware/tici/tests/test_esim.py +++ /dev/null @@ -1,51 +0,0 @@ -import pytest - -from openpilot.system.hardware import HARDWARE, TICI -from openpilot.system.hardware.base import LPAProfileNotFoundError - -# https://euicc-manual.osmocom.org/docs/rsp/known-test-profile -# iccid is always the same for the given activation code -TEST_ACTIVATION_CODE = 'LPA:1$rsp.truphone.com$QRF-BETTERROAMING-PMRDGIR2EARDEIT5' -TEST_ICCID = '8944476500001944011' - -TEST_NICKNAME = 'test_profile' - -def cleanup(): - lpa = HARDWARE.get_sim_lpa() - try: - lpa.delete_profile(TEST_ICCID) - except LPAProfileNotFoundError: - pass - lpa.process_notifications() - -class TestEsim: - - @classmethod - def setup_class(cls): - if not TICI: - pytest.skip() - cleanup() - - @classmethod - def teardown_class(cls): - cleanup() - - def test_provision_enable_disable(self): - lpa = HARDWARE.get_sim_lpa() - current_active = lpa.get_active_profile() - - lpa.download_profile(TEST_ACTIVATION_CODE, TEST_NICKNAME) - assert any(p.iccid == TEST_ICCID and p.nickname == TEST_NICKNAME for p in lpa.list_profiles()) - - lpa.enable_profile(TEST_ICCID) - new_active = lpa.get_active_profile() - assert new_active is not None - assert new_active.iccid == TEST_ICCID - assert new_active.nickname == TEST_NICKNAME - - lpa.disable_profile(TEST_ICCID) - new_active = lpa.get_active_profile() - assert new_active is None - - if current_active: - lpa.enable_profile(current_active.iccid) From 140aa95523060e39dcd5300a269e05dcfe2155ca Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Thu, 19 Feb 2026 09:33:03 -0800 Subject: [PATCH 101/311] add kia k7 to release notes --- RELEASES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASES.md b/RELEASES.md index 6191c6ba3d..6ea887efce 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,5 +1,6 @@ Version 0.10.4 (2026-02-17) ======================== +* Kia K7 2017 support thanks to royjr! * Lexus LS 2018 support thanks to Hacheoy! Version 0.10.3 (2025-12-17) From 6853f1db29a0cef6f97a4d0a7b8bc65afd37fe75 Mon Sep 17 00:00:00 2001 From: Daniel Koepping Date: Thu, 19 Feb 2026 14:42:28 -0800 Subject: [PATCH 102/311] bump panda (#37265) --- panda | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/panda b/panda index b1191df619..e1da7dc918 160000 --- a/panda +++ b/panda @@ -1 +1 @@ -Subproject commit b1191df619dbc201b9444634a29e2c016fee6619 +Subproject commit e1da7dc918c0bcda6fbbdd9ee6f89c5428ec5039 From 8650ca837ffc37f736473d1052f982455ef44594 Mon Sep 17 00:00:00 2001 From: Daniel Koepping Date: Thu, 19 Feb 2026 14:50:48 -0800 Subject: [PATCH 103/311] add power reduction to release notes (#37266) --- RELEASES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASES.md b/RELEASES.md index 6ea887efce..895dcbba7a 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -2,6 +2,7 @@ Version 0.10.4 (2026-02-17) ======================== * Kia K7 2017 support thanks to royjr! * Lexus LS 2018 support thanks to Hacheoy! +* Reduce comma four standby power usage by 77% to 52 mW Version 0.10.3 (2025-12-17) ======================== From 93977e2ee23421ad4dbda7a61d7a51ae2eaa8452 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 19 Feb 2026 16:35:51 -0800 Subject: [PATCH 104/311] ui: fix side gradients (#37268) fix --- system/ui/widgets/scroller.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/system/ui/widgets/scroller.py b/system/ui/widgets/scroller.py index 5930a2a6eb..69a50ed84b 100644 --- a/system/ui/widgets/scroller.py +++ b/system/ui/widgets/scroller.py @@ -291,14 +291,14 @@ class Scroller(Widget): # Draw edge shadows on top of scroller content if self._edge_shadows: - rl.draw_rectangle_gradient_h(int(self._rect.x), int(self._rect.y), - EDGE_SHADOW_WIDTH, int(self._rect.y), - rl.Color(0, 0, 0, 166), rl.BLANK) + rl.draw_rectangle_gradient_h(int(self._rect.x), 0, + EDGE_SHADOW_WIDTH, int(self._rect.height), + rl.Color(0, 0, 0, 204), rl.BLANK) right_x = int(self._rect.x + self._rect.width - EDGE_SHADOW_WIDTH) - rl.draw_rectangle_gradient_h(right_x, int(self._rect.y), - EDGE_SHADOW_WIDTH, int(self._rect.y), - rl.BLANK, rl.Color(0, 0, 0, 166)) + rl.draw_rectangle_gradient_h(right_x, 0, + EDGE_SHADOW_WIDTH, int(self._rect.height), + rl.BLANK, rl.Color(0, 0, 0, 204)) # Draw scroll indicator on top of edge shadows if self._show_scroll_indicator and len(self._visible_items) > 0: From 6ecb1060be9e69696461fe3190cf93915cda00e3 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 19 Feb 2026 16:36:35 -0800 Subject: [PATCH 105/311] ui: normalize ssids for 3X (#37269) * fix for tici * clean up --- selfdrive/ui/mici/layouts/settings/network/__init__.py | 4 ++-- selfdrive/ui/mici/layouts/settings/network/wifi_ui.py | 6 +----- system/ui/lib/wifi_manager.py | 4 ++++ system/ui/widgets/network.py | 9 +++++---- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/selfdrive/ui/mici/layouts/settings/network/__init__.py b/selfdrive/ui/mici/layouts/settings/network/__init__.py index 1940b680eb..cd0f4ee80f 100644 --- a/selfdrive/ui/mici/layouts/settings/network/__init__.py +++ b/selfdrive/ui/mici/layouts/settings/network/__init__.py @@ -3,14 +3,14 @@ from enum import IntEnum from collections.abc import Callable from openpilot.system.ui.widgets.scroller import Scroller -from openpilot.selfdrive.ui.mici.layouts.settings.network.wifi_ui import WifiUIMici, WifiIcon, normalize_ssid +from openpilot.selfdrive.ui.mici.layouts.settings.network.wifi_ui import WifiUIMici, WifiIcon from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigMultiToggle, BigParamControl, BigToggle from openpilot.selfdrive.ui.mici.widgets.dialog import BigInputDialog from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.selfdrive.ui.lib.prime_state import PrimeType from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.widgets import NavWidget -from openpilot.system.ui.lib.wifi_manager import WifiManager, Network, MeteredType, ConnectStatus +from openpilot.system.ui.lib.wifi_manager import WifiManager, Network, MeteredType, ConnectStatus, normalize_ssid class NetworkPanelType(IntEnum): diff --git a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py index e8fefb82dd..34e73e823e 100644 --- a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py +++ b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py @@ -8,11 +8,7 @@ from openpilot.system.ui.widgets.label import UnifiedLabel from openpilot.selfdrive.ui.mici.widgets.dialog import BigMultiOptionDialog, BigInputDialog, BigDialogOptionButton, BigConfirmationDialogV2 from openpilot.system.ui.lib.application import gui_app, MousePos, FontWeight from openpilot.system.ui.widgets import Widget, NavWidget -from openpilot.system.ui.lib.wifi_manager import WifiManager, Network, SecurityType, WifiState - - -def normalize_ssid(ssid: str) -> str: - return ssid.replace("’", "'") # for iPhone hotspots +from openpilot.system.ui.lib.wifi_manager import WifiManager, Network, SecurityType, WifiState, normalize_ssid class LoadingAnimation(Widget): diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index a5b5833d7f..25c4548e94 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -40,6 +40,10 @@ DEBUG = False _dbus_call_idx = 0 +def normalize_ssid(ssid: str) -> str: + return ssid.replace("’", "'") # for iPhone hotspots + + def _wrap_router(router): def _wrap(orig): def wrapper(msg, **kw): diff --git a/system/ui/widgets/network.py b/system/ui/widgets/network.py index 3e5e4a1d55..1f146fbdd1 100644 --- a/system/ui/widgets/network.py +++ b/system/ui/widgets/network.py @@ -6,7 +6,7 @@ import pyray as rl from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel -from openpilot.system.ui.lib.wifi_manager import WifiManager, SecurityType, Network, MeteredType +from openpilot.system.ui.lib.wifi_manager import WifiManager, SecurityType, Network, MeteredType, normalize_ssid from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.button import ButtonStyle, Button from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog @@ -311,12 +311,13 @@ class WifiManagerUI(Widget): return if self.state == UIState.NEEDS_AUTH and self._state_network: - self.keyboard.set_title(tr("Wrong password") if self._password_retry else tr("Enter password"), tr("for \"{}\"").format(self._state_network.ssid)) + self.keyboard.set_title(tr("Wrong password") if self._password_retry else tr("Enter password"), + tr("for \"{}\"").format(normalize_ssid(self._state_network.ssid))) self.keyboard.reset(min_text_size=MIN_PASSWORD_LENGTH) gui_app.set_modal_overlay(self.keyboard, lambda result: self._on_password_entered(cast(Network, self._state_network), result)) elif self.state == UIState.SHOW_FORGET_CONFIRM and self._state_network: confirm_dialog = ConfirmDialog("", tr("Forget"), tr("Cancel")) - confirm_dialog.set_text(tr("Forget Wi-Fi Network \"{}\"?").format(self._state_network.ssid)) + confirm_dialog.set_text(tr("Forget Wi-Fi Network \"{}\"?").format(normalize_ssid(self._state_network.ssid))) confirm_dialog.reset() gui_app.set_modal_overlay(confirm_dialog, callback=lambda result: self.on_forgot_confirm_finished(self._state_network, result)) else: @@ -445,7 +446,7 @@ class WifiManagerUI(Widget): def _on_network_updated(self, networks: list[Network]): self._networks = networks for n in self._networks: - self._networks_buttons[n.ssid] = Button(n.ssid, partial(self._networks_buttons_callback, n), font_size=55, + self._networks_buttons[n.ssid] = Button(normalize_ssid(n.ssid), partial(self._networks_buttons_callback, n), font_size=55, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT, button_style=ButtonStyle.TRANSPARENT_WHITE_TEXT) self._networks_buttons[n.ssid].set_touch_valid_callback(lambda: self.scroll_panel.is_touch_valid()) self._forget_networks_buttons[n.ssid] = Button(tr("Forget"), partial(self._forget_networks_buttons_callback, n), button_style=ButtonStyle.FORGET_WIFI, From 48568cba0bd24197c6f3a85f960d5250cc6ab20c Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 19 Feb 2026 22:32:26 -0800 Subject: [PATCH 106/311] mici training guide: fix memory leak each time you open dialog (#37270) * fix * meh * unclaud test is best * yess * try * works! * remove dict handling * clean up * more clean up * remove trash * fixup * fix up onboarding again * fix drivercameradialog * don't show test window * more widgets * fix all * Revert "fix all" This reverts commit 42d3537c9314af382961a16443a63faed202b157. * move and whitelist * clean up * more test + ignore * to fix * temp * Revert "temp" This reverts commit 215ecbb8a8fc0e6826d45b2c0d57999c7a19a400. --- selfdrive/ui/mici/layouts/onboarding.py | 7 +- selfdrive/ui/mici/tests/test_widget_leaks.py | 118 +++++++++++++++++++ 2 files changed, 123 insertions(+), 2 deletions(-) create mode 100755 selfdrive/ui/mici/tests/test_widget_leaks.py diff --git a/selfdrive/ui/mici/layouts/onboarding.py b/selfdrive/ui/mici/layouts/onboarding.py index 4248fef2ec..a399d7679c 100644 --- a/selfdrive/ui/mici/layouts/onboarding.py +++ b/selfdrive/ui/mici/layouts/onboarding.py @@ -124,8 +124,11 @@ class TrainingGuideDMTutorial(Widget): def __init__(self, continue_callback): super().__init__() + + self_ref = weakref.ref(self) + self._back_button = SmallCircleIconButton(gui_app.texture("icons_mici/setup/driver_monitoring/dm_question.png", 28, 48)) - self._back_button.set_click_callback(self._show_bad_face_page) + self._back_button.set_click_callback(lambda: self_ref() and self_ref()._show_bad_face_page()) self._good_button = SmallCircleIconButton(gui_app.texture("icons_mici/setup/driver_monitoring/dm_check.png", 42, 42)) # Wrap the continue callback to restore settings @@ -138,7 +141,7 @@ class TrainingGuideDMTutorial(Widget): 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._bad_face_page = DMBadFaceDetected(HARDWARE.shutdown, lambda: self_ref() and self_ref()._hide_bad_face_page()) self._should_show_bad_face_page = False # Disable driver monitoring model when device times out for inactivity diff --git a/selfdrive/ui/mici/tests/test_widget_leaks.py b/selfdrive/ui/mici/tests/test_widget_leaks.py new file mode 100755 index 0000000000..ffa256b716 --- /dev/null +++ b/selfdrive/ui/mici/tests/test_widget_leaks.py @@ -0,0 +1,118 @@ +import pyray as rl +rl.set_config_flags(rl.ConfigFlags.FLAG_WINDOW_HIDDEN) +import gc +import weakref +from openpilot.system.ui.lib.application import gui_app +from openpilot.system.ui.widgets import Widget + +# mici dialogs +from openpilot.selfdrive.ui.mici.layouts.onboarding import TrainingGuide as MiciTrainingGuide, OnboardingWindow as MiciOnboardingWindow +from openpilot.selfdrive.ui.mici.onroad.driver_camera_dialog import DriverCameraDialog as MiciDriverCameraDialog +from openpilot.selfdrive.ui.mici.widgets.pairing_dialog import PairingDialog as MiciPairingDialog +from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigConfirmationDialogV2, BigInputDialog, BigMultiOptionDialog +from openpilot.selfdrive.ui.mici.layouts.settings.device import MiciFccModal + +# tici dialogs +from openpilot.selfdrive.ui.onroad.driver_camera_dialog import DriverCameraDialog as TiciDriverCameraDialog +from openpilot.selfdrive.ui.layouts.onboarding import OnboardingWindow as TiciOnboardingWindow +from openpilot.selfdrive.ui.widgets.pairing_dialog import PairingDialog as TiciPairingDialog +from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog +from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog +from openpilot.system.ui.widgets.html_render import HtmlModal +from openpilot.system.ui.widgets.keyboard import Keyboard + +# FIXME: known small leaks not worth worrying about at the moment +KNOWN_LEAKS = { + "openpilot.selfdrive.ui.mici.onroad.driver_camera_dialog.DriverCameraView", + "openpilot.selfdrive.ui.mici.layouts.onboarding.TermsPage", + "openpilot.selfdrive.ui.mici.layouts.onboarding.TrainingGuide", + "openpilot.selfdrive.ui.mici.layouts.onboarding.DeclinePage", + "openpilot.selfdrive.ui.mici.layouts.onboarding.OnboardingWindow", + "openpilot.selfdrive.ui.onroad.driver_state.DriverStateRenderer", + "openpilot.selfdrive.ui.onroad.driver_camera_dialog.DriverCameraDialog", + "openpilot.selfdrive.ui.layouts.onboarding.TermsPage", + "openpilot.selfdrive.ui.layouts.onboarding.DeclinePage", + "openpilot.selfdrive.ui.layouts.onboarding.OnboardingWindow", + "openpilot.system.ui.widgets.confirm_dialog.ConfirmDialog", + "openpilot.system.ui.widgets.label.Label", + "openpilot.system.ui.widgets.button.Button", + "openpilot.selfdrive.ui.mici.widgets.dialog.BigDialog", + "openpilot.system.ui.widgets.html_render.HtmlRenderer", + "openpilot.system.ui.widgets.NavBar", + "openpilot.system.ui.widgets.inputbox.InputBox", + "openpilot.system.ui.widgets.scroller_tici.Scroller", + "openpilot.system.ui.widgets.scroller.Scroller", + "openpilot.system.ui.widgets.label.UnifiedLabel", + "openpilot.selfdrive.ui.mici.widgets.dialog.BigMultiOptionDialog", + "openpilot.system.ui.widgets.mici_keyboard.MiciKeyboard", + "openpilot.selfdrive.ui.mici.widgets.dialog.BigConfirmationDialogV2", + "openpilot.system.ui.widgets.keyboard.Keyboard", + "openpilot.system.ui.widgets.slider.BigSlider", + "openpilot.selfdrive.ui.mici.widgets.dialog.BigInputDialog", + "openpilot.system.ui.widgets.option_dialog.MultiOptionDialog", +} + + +def get_child_widgets(widget: Widget) -> list[Widget]: + children = [] + for val in widget.__dict__.values(): + items = val if isinstance(val, (list, tuple)) else (val,) + children.extend(w for w in items if isinstance(w, Widget)) + return children + + +def test_dialogs_do_not_leak(): + gui_app.init_window("ref-test") + + leaked_widgets = set() + + for ctor in ( + # mici + MiciDriverCameraDialog, MiciTrainingGuide, MiciOnboardingWindow, MiciPairingDialog, + lambda: BigDialog("test", "test"), + lambda: BigConfirmationDialogV2("test", "icons_mici/settings/network/new/trash.png"), + lambda: BigInputDialog("test"), + lambda: BigMultiOptionDialog(["a", "b"], "a"), + lambda: MiciFccModal(text="test"), + # tici + TiciDriverCameraDialog, TiciOnboardingWindow, TiciPairingDialog, Keyboard, + lambda: ConfirmDialog("test", "ok"), + lambda: MultiOptionDialog("test", ["a", "b"]), + lambda: HtmlModal(text="test"), + ): + widget = ctor() + all_refs = [weakref.ref(w) for w in get_child_widgets(widget) + [widget]] + + del widget + + for ref in all_refs: + if ref() is not None: + obj = ref() + name = f"{type(obj).__module__}.{type(obj).__qualname__}" + leaked_widgets.add(name) + + print(f"\n=== Widget {name} alive after del") + print(" Referrers:") + for r in gc.get_referrers(obj): + if r is obj: + continue + + if hasattr(r, '__self__') and r.__self__ is not obj: + print(f" bound method: {type(r.__self__).__qualname__}.{r.__name__}") + elif hasattr(r, '__func__'): + print(f" method: {r.__name__}") + else: + print(f" {type(r).__module__}.{type(r).__qualname__}") + del obj + + gui_app.close() + + unexpected = leaked_widgets - KNOWN_LEAKS + assert not unexpected, f"New leaked widgets: {unexpected}" + + fixed = KNOWN_LEAKS - leaked_widgets + assert not fixed, f"These leaks are fixed, remove from KNOWN_LEAKS: {fixed}" + + +if __name__ == "__main__": + test_dialogs_do_not_leak() From e54c0091bc4f8aaef8296f1b24ce10d00574c3b2 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 19 Feb 2026 22:49:23 -0800 Subject: [PATCH 107/311] tici ui: always show regulatory button (#37273) * i knew it * clean up --- selfdrive/ui/layouts/settings/device.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/selfdrive/ui/layouts/settings/device.py b/selfdrive/ui/layouts/settings/device.py index 00ae6a188e..0a2f2dacae 100644 --- a/selfdrive/ui/layouts/settings/device.py +++ b/selfdrive/ui/layouts/settings/device.py @@ -9,7 +9,6 @@ from openpilot.selfdrive.ui.onroad.driver_camera_dialog import DriverCameraDialo from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.selfdrive.ui.layouts.onboarding import TrainingGuide from openpilot.selfdrive.ui.widgets.pairing_dialog import PairingDialog -from openpilot.system.hardware import TICI from openpilot.system.ui.lib.application import FontWeight, gui_app from openpilot.system.ui.lib.multilang import multilang, tr, tr_noop from openpilot.system.ui.widgets import Widget, DialogResult @@ -64,11 +63,10 @@ class DeviceLayout(Widget): self._reset_calib_btn, button_item(lambda: tr("Review Training Guide"), lambda: tr("REVIEW"), lambda: tr(DESCRIPTIONS['review_guide']), self._on_review_training_guide, enabled=ui_state.is_offroad), - regulatory_btn := button_item(lambda: tr("Regulatory"), lambda: tr("VIEW"), callback=self._on_regulatory, enabled=ui_state.is_offroad), + button_item(lambda: tr("Regulatory"), lambda: tr("VIEW"), callback=self._on_regulatory, enabled=ui_state.is_offroad), button_item(lambda: tr("Change Language"), lambda: tr("CHANGE"), callback=self._show_language_dialog), self._power_off_btn, ] - regulatory_btn.set_visible(TICI) return items def _offroad_transition(self): From 6bd3cab8a8855b2714b169f5957eaab88f6fbbc9 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 19 Feb 2026 22:52:21 -0800 Subject: [PATCH 108/311] edge shadows should use widget y --- system/ui/widgets/scroller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/system/ui/widgets/scroller.py b/system/ui/widgets/scroller.py index 69a50ed84b..9bb7883215 100644 --- a/system/ui/widgets/scroller.py +++ b/system/ui/widgets/scroller.py @@ -291,12 +291,12 @@ class Scroller(Widget): # Draw edge shadows on top of scroller content if self._edge_shadows: - rl.draw_rectangle_gradient_h(int(self._rect.x), 0, + rl.draw_rectangle_gradient_h(int(self._rect.x), int(self._rect.y), EDGE_SHADOW_WIDTH, int(self._rect.height), rl.Color(0, 0, 0, 204), rl.BLANK) right_x = int(self._rect.x + self._rect.width - EDGE_SHADOW_WIDTH) - rl.draw_rectangle_gradient_h(right_x, 0, + rl.draw_rectangle_gradient_h(right_x, int(self._rect.y), EDGE_SHADOW_WIDTH, int(self._rect.height), rl.BLANK, rl.Color(0, 0, 0, 204)) From f829c90de6d62840dc2a6bb2180c639fd37c2f56 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 20 Feb 2026 02:40:41 -0800 Subject: [PATCH 109/311] skip widget leak test --- selfdrive/ui/mici/tests/test_widget_leaks.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/selfdrive/ui/mici/tests/test_widget_leaks.py b/selfdrive/ui/mici/tests/test_widget_leaks.py index ffa256b716..12fa608b36 100755 --- a/selfdrive/ui/mici/tests/test_widget_leaks.py +++ b/selfdrive/ui/mici/tests/test_widget_leaks.py @@ -2,6 +2,7 @@ import pyray as rl rl.set_config_flags(rl.ConfigFlags.FLAG_WINDOW_HIDDEN) import gc import weakref +import pytest from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.widgets import Widget @@ -61,6 +62,7 @@ def get_child_widgets(widget: Widget) -> list[Widget]: return children +@pytest.mark.skip(reason="segfaults") def test_dialogs_do_not_leak(): gui_app.init_window("ref-test") From cefddf4b9b6ea5eb40c9b3fd44dd331793a12e9a Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 20 Feb 2026 02:43:11 -0800 Subject: [PATCH 110/311] ui: add navigation stack for tici (#37275) * initial * start to support nav stack in settings panels + fix some navwidget bugs * add deprecation warning and move more to new nav stack * fix overriding NavWidget enabled and do developer panel * fix interactive timeout and do main * more device, not done yet * minor network fixes * dcam dialog * start onboarding * fix onboarding * do mici setup * remove now useless CUSTOM_SOFTWARE * support big ui with old modal overlay * reset can be old modal overlay, but updater needs new since it uses wifiui * flip name truthiness to inspire excitement * all *should* work, but will do pass later * clean up main * clean up settiings * clean up dialog and developer * cleanup mici setup some * rm one more * fix keyboard * revert * might as well but clarify * fix networkinfopage buttons * lint * nice clean up from cursor * animate background fade with position * fix device overlays * cursor fix pt1 cursor fix pt2 * rm print * capital * temp fix from cursor for onboarding not freeing space after reviewing training guide * fix home screen scroller snap not resetting * stash * nice gradient on top * 40 * 20 * no gradient * return unused returns and always show regulatory btn * nice! * revert selfdrive/ui * let's do tici first * bring back ui * not sure why __del__, SetupWidget was never deleted? * device "done" * network "done!!" * toggles "done" * software "done" * developer "done" * fix onboarding * use new modal for debug windows * and aug * setup "done" * clean up * updater "done" * reset "done" * pop first before callbacks in case callbacks push * fix cmt * not needed * remove two commented functions for mici * clean up application * typing * static * not sure what this means * fix big * more static * actually great catch * fix cmt --- selfdrive/ui/layouts/main.py | 6 +- selfdrive/ui/layouts/onboarding.py | 6 +- selfdrive/ui/layouts/settings/developer.py | 6 +- selfdrive/ui/layouts/settings/device.py | 67 ++++++++------------- selfdrive/ui/layouts/settings/software.py | 12 ++-- selfdrive/ui/layouts/settings/toggles.py | 6 +- selfdrive/ui/onroad/augmented_road_view.py | 4 +- selfdrive/ui/onroad/driver_camera_dialog.py | 8 +-- selfdrive/ui/tests/diff/replay.py | 2 +- selfdrive/ui/ui.py | 13 ++-- selfdrive/ui/widgets/pairing_dialog.py | 11 ++-- selfdrive/ui/widgets/setup.py | 14 ++--- selfdrive/ui/widgets/ssh_key.py | 5 +- system/ui/lib/application.py | 58 +++++++++++++++--- system/ui/tici_reset.py | 38 ++++++------ system/ui/tici_setup.py | 17 +++--- system/ui/tici_updater.py | 9 ++- system/ui/widgets/confirm_dialog.py | 22 +++---- system/ui/widgets/html_render.py | 2 +- system/ui/widgets/keyboard.py | 43 +++++++------ system/ui/widgets/network.py | 57 ++++++++++-------- system/ui/widgets/option_dialog.py | 13 ++-- 22 files changed, 231 insertions(+), 188 deletions(-) diff --git a/selfdrive/ui/layouts/main.py b/selfdrive/ui/layouts/main.py index 702854f98a..15d44e24da 100644 --- a/selfdrive/ui/layouts/main.py +++ b/selfdrive/ui/layouts/main.py @@ -36,10 +36,12 @@ class MainLayout(Widget): # Set callbacks self._setup_callbacks() - # Start onboarding if terms or training not completed + gui_app.push_widget(self) + + # Start onboarding if terms or training not completed, make sure to push after self self._onboarding_window = OnboardingWindow() if not self._onboarding_window.completed: - gui_app.set_modal_overlay(self._onboarding_window) + gui_app.push_widget(self._onboarding_window) def _render(self, _): self._handle_onroad_transition() diff --git a/selfdrive/ui/layouts/onboarding.py b/selfdrive/ui/layouts/onboarding.py index 5d61c1c95a..25294511d4 100644 --- a/selfdrive/ui/layouts/onboarding.py +++ b/selfdrive/ui/layouts/onboarding.py @@ -81,6 +81,9 @@ class TrainingGuide(Widget): if self._completed_callback: self._completed_callback() + # NOTE: this pops OnboardingWindow during real onboarding + gui_app.pop_widget() + def _update_state(self): if len(self._image_objs): self._textures.append(gui_app._load_texture_from_image(self._image_objs.pop(0))) @@ -194,11 +197,10 @@ class OnboardingWindow(Widget): ui_state.params.put("HasAcceptedTerms", terms_version) self._state = OnboardingState.ONBOARDING if self._training_done: - gui_app.set_modal_overlay(None) + gui_app.pop_widget() def _on_completed_training(self): ui_state.params.put("CompletedTrainingVersion", training_version) - gui_app.set_modal_overlay(None) def _render(self, _): if self._training_guide is None: diff --git a/selfdrive/ui/layouts/settings/developer.py b/selfdrive/ui/layouts/settings/developer.py index 646c817508..17ab60172a 100644 --- a/selfdrive/ui/layouts/settings/developer.py +++ b/selfdrive/ui/layouts/settings/developer.py @@ -164,7 +164,7 @@ class DeveloperLayout(Widget): def _on_alpha_long_enabled(self, state: bool): if state: - def confirm_callback(result: int): + def confirm_callback(result: DialogResult): if result == DialogResult.CONFIRM: self._params.put_bool("AlphaLongitudinalEnabled", True) self._params.put_bool("OnroadCycleRequested", True) @@ -176,8 +176,8 @@ class DeveloperLayout(Widget): content = (f"

{self._alpha_long_toggle.title}


" + f"

{self._alpha_long_toggle.description}

") - dlg = ConfirmDialog(content, tr("Enable"), rich=True) - gui_app.set_modal_overlay(dlg, callback=confirm_callback) + dlg = ConfirmDialog(content, tr("Enable"), rich=True, callback=confirm_callback) + gui_app.push_widget(dlg) else: self._params.put_bool("AlphaLongitudinalEnabled", False) diff --git a/selfdrive/ui/layouts/settings/device.py b/selfdrive/ui/layouts/settings/device.py index 0a2f2dacae..751373dba6 100644 --- a/selfdrive/ui/layouts/settings/device.py +++ b/selfdrive/ui/layouts/settings/device.py @@ -33,8 +33,6 @@ class DeviceLayout(Widget): self._params = Params() self._select_language_dialog: MultiOptionDialog | None = None - self._driver_camera: DriverCameraDialog | None = None - self._pair_device_dialog: PairingDialog | None = None self._fcc_dialog: HtmlModal | None = None self._training_guide: TrainingGuide | None = None @@ -44,7 +42,8 @@ class DeviceLayout(Widget): ui_state.add_offroad_transition_callback(self._offroad_transition) def _initialize_items(self): - self._pair_device_btn = button_item(lambda: tr("Pair Device"), lambda: tr("PAIR"), lambda: tr(DESCRIPTIONS['pair_device']), callback=self._pair_device) + self._pair_device_btn = button_item(lambda: tr("Pair Device"), lambda: tr("PAIR"), lambda: tr(DESCRIPTIONS['pair_device']), + callback=lambda: gui_app.push_widget(PairingDialog())) self._pair_device_btn.set_visible(lambda: not ui_state.prime_state.is_paired()) self._reset_calib_btn = button_item(lambda: tr("Reset Calibration"), lambda: tr("RESET"), lambda: tr(DESCRIPTIONS['reset_calibration']), @@ -59,7 +58,7 @@ class DeviceLayout(Widget): text_item(lambda: tr("Serial"), self._params.get("HardwareSerial") or (lambda: tr("N/A"))), self._pair_device_btn, button_item(lambda: tr("Driver Camera"), lambda: tr("PREVIEW"), lambda: tr(DESCRIPTIONS['driver_camera']), - callback=self._show_driver_camera, enabled=ui_state.is_offroad), + callback=lambda: gui_app.push_widget(DriverCameraDialog()), enabled=ui_state.is_offroad), self._reset_calib_btn, button_item(lambda: tr("Review Training Guide"), lambda: tr("REVIEW"), lambda: tr(DESCRIPTIONS['review_guide']), self._on_review_training_guide, enabled=ui_state.is_offroad), @@ -79,29 +78,23 @@ class DeviceLayout(Widget): self._scroller.render(rect) def _show_language_dialog(self): - def handle_language_selection(result: int): - if result == 1 and self._select_language_dialog: + def handle_language_selection(result: DialogResult): + if result == DialogResult.CONFIRM and self._select_language_dialog: selected_language = multilang.languages[self._select_language_dialog.selection] multilang.change_language(selected_language) self._update_calib_description() self._select_language_dialog = None self._select_language_dialog = MultiOptionDialog(tr("Select a language"), multilang.languages, multilang.codes[multilang.language], - option_font_weight=FontWeight.UNIFONT) - gui_app.set_modal_overlay(self._select_language_dialog, callback=handle_language_selection) - - def _show_driver_camera(self): - if not self._driver_camera: - self._driver_camera = DriverCameraDialog() - - gui_app.set_modal_overlay(self._driver_camera, callback=lambda result: setattr(self, '_driver_camera', None)) + option_font_weight=FontWeight.UNIFONT, callback=handle_language_selection) + gui_app.push_widget(self._select_language_dialog) def _reset_calibration_prompt(self): if ui_state.engaged: - gui_app.set_modal_overlay(alert_dialog(tr("Disengage to Reset Calibration"))) + gui_app.push_widget(alert_dialog(tr("Disengage to Reset Calibration"))) return - def reset_calibration(result: int): + def reset_calibration(result: DialogResult): # Check engaged again in case it changed while the dialog was open if ui_state.engaged or result != DialogResult.CONFIRM: return @@ -114,8 +107,8 @@ class DeviceLayout(Widget): self._params.put_bool("OnroadCycleRequested", True) self._update_calib_description() - dialog = ConfirmDialog(tr("Are you sure you want to reset calibration?"), tr("Reset")) - gui_app.set_modal_overlay(dialog, callback=reset_calibration) + dialog = ConfirmDialog(tr("Are you sure you want to reset calibration?"), tr("Reset"), callback=reset_calibration) + gui_app.push_widget(dialog) def _update_calib_description(self): desc = tr(DESCRIPTIONS['reset_calibration']) @@ -167,42 +160,34 @@ class DeviceLayout(Widget): def _reboot_prompt(self): if ui_state.engaged: - gui_app.set_modal_overlay(alert_dialog(tr("Disengage to Reboot"))) + gui_app.push_widget(alert_dialog(tr("Disengage to Reboot"))) return - dialog = ConfirmDialog(tr("Are you sure you want to reboot?"), tr("Reboot")) - gui_app.set_modal_overlay(dialog, callback=self._perform_reboot) + def perform_reboot(result: DialogResult): + if not ui_state.engaged and result == DialogResult.CONFIRM: + self._params.put_bool_nonblocking("DoReboot", True) - def _perform_reboot(self, result: int): - if not ui_state.engaged and result == DialogResult.CONFIRM: - self._params.put_bool_nonblocking("DoReboot", True) + dialog = ConfirmDialog(tr("Are you sure you want to reboot?"), tr("Reboot"), callback=perform_reboot) + gui_app.push_widget(dialog) def _power_off_prompt(self): if ui_state.engaged: - gui_app.set_modal_overlay(alert_dialog(tr("Disengage to Power Off"))) + gui_app.push_widget(alert_dialog(tr("Disengage to Power Off"))) return - dialog = ConfirmDialog(tr("Are you sure you want to power off?"), tr("Power Off")) - gui_app.set_modal_overlay(dialog, callback=self._perform_power_off) + def perform_power_off(result: DialogResult): + if not ui_state.engaged and result == DialogResult.CONFIRM: + self._params.put_bool_nonblocking("DoShutdown", True) - def _perform_power_off(self, result: int): - if not ui_state.engaged and result == DialogResult.CONFIRM: - self._params.put_bool_nonblocking("DoShutdown", True) - - def _pair_device(self): - if not self._pair_device_dialog: - self._pair_device_dialog = PairingDialog() - gui_app.set_modal_overlay(self._pair_device_dialog, callback=lambda result: setattr(self, '_pair_device_dialog', None)) + dialog = ConfirmDialog(tr("Are you sure you want to power off?"), tr("Power Off"), callback=perform_power_off) + gui_app.push_widget(dialog) def _on_regulatory(self): if not self._fcc_dialog: self._fcc_dialog = HtmlModal(os.path.join(BASEDIR, "selfdrive/assets/offroad/fcc.html")) - gui_app.set_modal_overlay(self._fcc_dialog) + gui_app.push_widget(self._fcc_dialog) def _on_review_training_guide(self): if not self._training_guide: - def completed_callback(): - gui_app.set_modal_overlay(None) - - self._training_guide = TrainingGuide(completed_callback=completed_callback) - gui_app.set_modal_overlay(self._training_guide) + self._training_guide = TrainingGuide() + gui_app.push_widget(self._training_guide) diff --git a/selfdrive/ui/layouts/settings/software.py b/selfdrive/ui/layouts/settings/software.py index e0df8f2705..c197b45453 100644 --- a/selfdrive/ui/layouts/settings/software.py +++ b/selfdrive/ui/layouts/settings/software.py @@ -165,12 +165,12 @@ class SoftwareLayout(Widget): os.system("pkill -SIGHUP -f system.updated.updated") def _on_uninstall(self): - def handle_uninstall_confirmation(result): + def handle_uninstall_confirmation(result: DialogResult): if result == DialogResult.CONFIRM: ui_state.params.put_bool("DoUninstall", True) - dialog = ConfirmDialog(tr("Are you sure you want to uninstall?"), tr("Uninstall")) - gui_app.set_modal_overlay(dialog, callback=handle_uninstall_confirmation) + dialog = ConfirmDialog(tr("Are you sure you want to uninstall?"), tr("Uninstall"), callback=handle_uninstall_confirmation) + gui_app.push_widget(dialog) def _on_install_update(self): # Trigger reboot to install update @@ -189,9 +189,8 @@ class SoftwareLayout(Widget): branches.insert(0, b) current_target = ui_state.params.get("UpdaterTargetBranch") or "" - self._branch_dialog = MultiOptionDialog(tr("Select a branch"), branches, current_target) - def handle_selection(result): + def handle_selection(result: DialogResult): # Confirmed selection if result == DialogResult.CONFIRM and self._branch_dialog is not None and self._branch_dialog.selection: selection = self._branch_dialog.selection @@ -200,4 +199,5 @@ class SoftwareLayout(Widget): os.system("pkill -SIGUSR1 -f system.updated.updated") self._branch_dialog = None - gui_app.set_modal_overlay(self._branch_dialog, callback=handle_selection) + self._branch_dialog = MultiOptionDialog(tr("Select a branch"), branches, current_target, callback=handle_selection) + gui_app.push_widget(self._branch_dialog) diff --git a/selfdrive/ui/layouts/settings/toggles.py b/selfdrive/ui/layouts/settings/toggles.py index 7fae2dfd24..dbe5e241aa 100644 --- a/selfdrive/ui/layouts/settings/toggles.py +++ b/selfdrive/ui/layouts/settings/toggles.py @@ -214,7 +214,7 @@ class TogglesLayout(Widget): def _handle_experimental_mode_toggle(self, state: bool): confirmed = self._params.get_bool("ExperimentalModeConfirmed") if state and not confirmed: - def confirm_callback(result: int): + def confirm_callback(result: DialogResult): if result == DialogResult.CONFIRM: self._params.put_bool("ExperimentalMode", True) self._params.put_bool("ExperimentalModeConfirmed", True) @@ -225,8 +225,8 @@ class TogglesLayout(Widget): # show confirmation dialog content = (f"

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


" + f"

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

") - dlg = ConfirmDialog(content, tr("Enable"), rich=True) - gui_app.set_modal_overlay(dlg, callback=confirm_callback) + dlg = ConfirmDialog(content, tr("Enable"), rich=True, callback=confirm_callback) + gui_app.push_widget(dlg) else: self._update_experimental_mode_icon() self._params.put_bool("ExperimentalMode", state) diff --git a/selfdrive/ui/onroad/augmented_road_view.py b/selfdrive/ui/onroad/augmented_road_view.py index 1f202141c3..f8fb589b61 100644 --- a/selfdrive/ui/onroad/augmented_road_view.py +++ b/selfdrive/ui/onroad/augmented_road_view.py @@ -219,8 +219,9 @@ class AugmentedRoadView(CameraView): if __name__ == "__main__": - gui_app.init_window("OnRoad Camera View") + gui_app.init_window("OnRoad Camera View", new_modal=True) road_camera_view = AugmentedRoadView(ROAD_CAM) + gui_app.push_widget(road_camera_view) print("***press space to switch camera view***") try: for _ in gui_app.render(): @@ -229,6 +230,5 @@ if __name__ == "__main__": if WIDE_CAM in road_camera_view.available_streams: stream = ROAD_CAM if road_camera_view.stream_type == WIDE_CAM else WIDE_CAM road_camera_view.switch_stream(stream) - road_camera_view.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) finally: road_camera_view.close() diff --git a/selfdrive/ui/onroad/driver_camera_dialog.py b/selfdrive/ui/onroad/driver_camera_dialog.py index f69ad8c49c..a4518a2520 100644 --- a/selfdrive/ui/onroad/driver_camera_dialog.py +++ b/selfdrive/ui/onroad/driver_camera_dialog.py @@ -14,7 +14,7 @@ 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(lambda: gui_app.set_modal_overlay(None)) + device.add_interactive_timeout_callback(gui_app.pop_widget) ui_state.params.put_bool("IsDriverViewEnabled", True) def hide_event(self): @@ -24,7 +24,7 @@ class DriverCameraDialog(CameraView): def _handle_mouse_release(self, _): super()._handle_mouse_release(_) - gui_app.set_modal_overlay(None) + gui_app.pop_widget() def __del__(self): self.close() @@ -100,12 +100,12 @@ class DriverCameraDialog(CameraView): if __name__ == "__main__": - gui_app.init_window("Driver Camera View") + gui_app.init_window("Driver Camera View", new_modal=True) driver_camera_view = DriverCameraDialog() + gui_app.push_widget(driver_camera_view) try: for _ in gui_app.render(): ui_state.update() - driver_camera_view.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) finally: driver_camera_view.close() diff --git a/selfdrive/ui/tests/diff/replay.py b/selfdrive/ui/tests/diff/replay.py index e424d11f62..62a808209a 100755 --- a/selfdrive/ui/tests/diff/replay.py +++ b/selfdrive/ui/tests/diff/replay.py @@ -38,7 +38,7 @@ def run_replay(variant: LayoutVariant) -> None: from openpilot.system.ui.lib.application import gui_app # Import here for accurate coverage from openpilot.selfdrive.ui.tests.diff.replay_script import build_script - gui_app.init_window("ui diff test", fps=FPS) + gui_app.init_window("ui diff test", fps=FPS, new_modal=variant == "tizi") # Dynamically import main layout based on variant if variant == "mici": diff --git a/selfdrive/ui/ui.py b/selfdrive/ui/ui.py index 7fe0dfbbc9..0e271c72db 100755 --- a/selfdrive/ui/ui.py +++ b/selfdrive/ui/ui.py @@ -9,21 +9,26 @@ from openpilot.selfdrive.ui.layouts.main import MainLayout from openpilot.selfdrive.ui.mici.layouts.main import MiciMainLayout from openpilot.selfdrive.ui.ui_state import ui_state +BIG_UI = gui_app.big_ui() + def main(): cores = {5, } config_realtime_process(0, 51) - gui_app.init_window("UI") - if gui_app.big_ui(): + if BIG_UI: + gui_app.init_window("UI", new_modal=True) main_layout = MainLayout() else: + gui_app.init_window("UI") main_layout = MiciMainLayout() - main_layout.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) + main_layout.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) + for should_render in gui_app.render(): ui_state.update() if should_render: - main_layout.render() + if not BIG_UI: + main_layout.render() # reaffine after power save offlines our core if TICI and os.sched_getaffinity(0) != cores: diff --git a/selfdrive/ui/widgets/pairing_dialog.py b/selfdrive/ui/widgets/pairing_dialog.py index f960cf723e..c07b2463f3 100644 --- a/selfdrive/ui/widgets/pairing_dialog.py +++ b/selfdrive/ui/widgets/pairing_dialog.py @@ -26,7 +26,7 @@ class PairingDialog(Widget): self.qr_texture: rl.Texture | None = None self.last_qr_generation = float('-inf') self._close_btn = IconButton(gui_app.texture("icons/close.png", 80, 80)) - self._close_btn.set_click_callback(lambda: gui_app.set_modal_overlay(None)) + self._close_btn.set_click_callback(gui_app.pop_widget) def _get_pairing_url(self) -> str: try: @@ -69,7 +69,7 @@ class PairingDialog(Widget): def _update_state(self): if ui_state.prime_state.is_paired(): - gui_app.set_modal_overlay(None) + gui_app.pop_widget() def _render(self, rect: rl.Rectangle) -> int: rl.clear_background(rl.Color(224, 224, 224, 255)) @@ -160,12 +160,11 @@ class PairingDialog(Widget): if __name__ == "__main__": - gui_app.init_window("pairing device") + gui_app.init_window("pairing device", new_modal=True) pairing = PairingDialog() + gui_app.push_widget(pairing) try: for _ in gui_app.render(): - result = pairing.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) - if result != -1: - break + pass finally: del pairing diff --git a/selfdrive/ui/widgets/setup.py b/selfdrive/ui/widgets/setup.py index 3c9406688f..c9452fc535 100644 --- a/selfdrive/ui/widgets/setup.py +++ b/selfdrive/ui/widgets/setup.py @@ -15,7 +15,6 @@ class SetupWidget(Widget): def __init__(self): super().__init__() self._open_settings_callback = None - self._pairing_dialog: PairingDialog | None = None self._pair_device_btn = Button(lambda: tr("Pair device"), self._show_pairing, button_style=ButtonStyle.PRIMARY) self._open_settings_btn = Button(lambda: tr("Open"), lambda: self._open_settings_callback() if self._open_settings_callback else None, button_style=ButtonStyle.PRIMARY) @@ -86,16 +85,11 @@ class SetupWidget(Widget): button_rect = rl.Rectangle(x, y, w, button_height) self._open_settings_btn.render(button_rect) - def _show_pairing(self): + @staticmethod + def _show_pairing(): if not system_time_valid(): dlg = alert_dialog(tr("Please connect to Wi-Fi to complete initial pairing")) - gui_app.set_modal_overlay(dlg) + gui_app.push_widget(dlg) return - if not self._pairing_dialog: - self._pairing_dialog = PairingDialog() - gui_app.set_modal_overlay(self._pairing_dialog, lambda result: setattr(self, '_pairing_dialog', None)) - - def __del__(self): - if self._pairing_dialog: - del self._pairing_dialog + gui_app.push_widget(PairingDialog()) diff --git a/selfdrive/ui/widgets/ssh_key.py b/selfdrive/ui/widgets/ssh_key.py index 88389cb053..b31a9eb3bd 100644 --- a/selfdrive/ui/widgets/ssh_key.py +++ b/selfdrive/ui/widgets/ssh_key.py @@ -59,7 +59,7 @@ class SshKeyAction(ItemAction): # Show error dialog if there's an error if self._error_message: message = copy.copy(self._error_message) - gui_app.set_modal_overlay(alert_dialog(message)) + gui_app.push_widget(alert_dialog(message)) self._username = "" self._error_message = "" @@ -87,7 +87,8 @@ class SshKeyAction(ItemAction): if self._state == SshKeyActionState.ADD: self._keyboard.reset() self._keyboard.set_title(tr("Enter your GitHub username")) - gui_app.set_modal_overlay(self._keyboard, callback=self._on_username_submit) + self._keyboard.set_callback(self._on_username_submit) + gui_app.push_widget(self._keyboard) elif self._state == SshKeyActionState.REMOVE: self._params.remove("GithubUsername") self._params.remove("GithubSshKeys") diff --git a/system/ui/lib/application.py b/system/ui/lib/application.py index 755de674de..f056fddbf0 100644 --- a/system/ui/lib/application.py +++ b/system/ui/lib/application.py @@ -230,6 +230,10 @@ class GuiApplication: self._modal_overlay_shown = False self._modal_overlay_tick: Callable[[], None] | None = None + # TODO: move over the entire ui and deprecate + self._new_modal = False + self._nav_stack: list[object] = [] + self._mouse = MouseState(self._scale) self._mouse_events: list[MouseEvent] = [] self._last_mouse_event: MouseEvent = MouseEvent(MousePos(0, 0), 0, False, False, False, 0.0) @@ -262,7 +266,7 @@ class GuiApplication: def request_close(self): self._window_close_requested = True - def init_window(self, title: str, fps: int = _DEFAULT_FPS): + def init_window(self, title: str, fps: int = _DEFAULT_FPS, new_modal: bool = False): with self._startup_profile_context(): def _close(sig, frame): self.close() @@ -270,6 +274,8 @@ class GuiApplication: signal.signal(signal.SIGINT, _close) atexit.register(self.close) + self._new_modal = new_modal + flags = rl.ConfigFlags.FLAG_MSAA_4X_HINT if ENABLE_VSYNC: flags |= rl.ConfigFlags.FLAG_VSYNC_HINT @@ -373,7 +379,34 @@ class GuiApplication: except Exception: break + def push_widget(self, widget: object): + assert self._new_modal + + # disable previous widget to prevent input processing + if len(self._nav_stack) > 0: + prev_widget = self._nav_stack[-1] + prev_widget.set_enabled(False) + + self._nav_stack.append(widget) + widget.show_event() + + def pop_widget(self): + assert self._new_modal + + if len(self._nav_stack) < 2: + cloudlog.warning("At least one widget should remain on the stack, ignoring pop") + return + + # re-enable previous widget and pop current + prev_widget = self._nav_stack[-2] + prev_widget.set_enabled(True) + + widget = self._nav_stack.pop() + widget.hide_event() + def set_modal_overlay(self, overlay, callback: Callable | None = None): + assert not self._new_modal, "set_modal_overlay is deprecated, use push_widget instead" + if self._modal_overlay.overlay is not None: if hasattr(self._modal_overlay.overlay, 'hide_event'): self._modal_overlay.overlay.hide_event() @@ -528,15 +561,24 @@ class GuiApplication: rl.begin_drawing() rl.clear_background(rl.BLACK) - # 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: + if self._new_modal: + # Only render last widget + for widget in self._nav_stack[-1:]: + widget.render(rl.Rectangle(0, 0, self.width, self.height)) + + # Yield to allow caller to run non-rendering related code yield True + else: + # 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 + if self._render_texture: rl.end_texture_mode() rl.begin_drawing() diff --git a/system/ui/tici_reset.py b/system/ui/tici_reset.py index 3922c27aac..b22b240850 100755 --- a/system/ui/tici_reset.py +++ b/system/ui/tici_reset.py @@ -36,13 +36,9 @@ class Reset(Widget): self._mode = mode self._previous_reset_state = None self._reset_state = ResetState.NONE - self._cancel_button = Button("Cancel", self._cancel_callback) + self._cancel_button = Button("Cancel", gui_app.request_close) self._confirm_button = Button("Confirm", self._confirm, button_style=ButtonStyle.PRIMARY) self._reboot_button = Button("Reboot", lambda: os.system("sudo reboot")) - self._render_status = True - - def _cancel_callback(self): - self._render_status = False def _do_erase(self): if PC: @@ -69,30 +65,30 @@ class Reset(Widget): elif self._reset_state != ResetState.RESETTING and (time.monotonic() - self._timeout_st) > TIMEOUT: exit(0) - def _render(self, rect: rl.Rectangle): - label_rect = rl.Rectangle(rect.x + 140, rect.y, rect.width - 280, 100 * FONT_SCALE) + def _render(self, _): + content_rect = rl.Rectangle(45, 200, self._rect.width - 90, self._rect.height - 245) + + label_rect = rl.Rectangle(content_rect.x + 140, content_rect.y, content_rect.width - 280, 100 * FONT_SCALE) gui_label(label_rect, "System Reset", 100, font_weight=FontWeight.BOLD) - text_rect = rl.Rectangle(rect.x + 140, rect.y + 140, rect.width - 280, rect.height - 90 - 100 * FONT_SCALE) + text_rect = rl.Rectangle(content_rect.x + 140, content_rect.y + 140, content_rect.width - 280, content_rect.height - 90 - 100 * FONT_SCALE) gui_text_box(text_rect, self._get_body_text(), 90) button_height = 160 button_spacing = 50 - button_top = rect.y + rect.height - button_height - button_width = (rect.width - button_spacing) / 2.0 + button_top = content_rect.y + content_rect.height - button_height + button_width = (content_rect.width - button_spacing) / 2.0 if self._reset_state != ResetState.RESETTING: if self._mode == ResetMode.RECOVER: - self._reboot_button.render(rl.Rectangle(rect.x, button_top, button_width, button_height)) + self._reboot_button.render(rl.Rectangle(content_rect.x, button_top, button_width, button_height)) elif self._mode == ResetMode.USER_RESET: - self._cancel_button.render(rl.Rectangle(rect.x, button_top, button_width, button_height)) + self._cancel_button.render(rl.Rectangle(content_rect.x, button_top, button_width, button_height)) if self._reset_state != ResetState.FAILED: - self._confirm_button.render(rl.Rectangle(rect.x + button_width + 50, button_top, button_width, button_height)) + self._confirm_button.render(rl.Rectangle(content_rect.x + button_width + 50, button_top, button_width, button_height)) else: - self._reboot_button.render(rl.Rectangle(rect.x, button_top, rect.width, button_height)) - - return self._render_status + self._reboot_button.render(rl.Rectangle(content_rect.x, button_top, content_rect.width, button_height)) def _confirm(self): if self._reset_state == ResetState.CONFIRM: @@ -120,16 +116,16 @@ def main(): elif sys.argv[1] == "--format": mode = ResetMode.FORMAT - gui_app.init_window("System Reset", 20) + gui_app.init_window("System Reset", 20, new_modal=True) reset = Reset(mode) if mode == ResetMode.FORMAT: reset.start_reset() - for should_render in gui_app.render(): - if should_render: - if not reset.render(rl.Rectangle(45, 200, gui_app.width - 90, gui_app.height - 245)): - break + gui_app.push_widget(reset) + + for _ in gui_app.render(): + pass if __name__ == "__main__": diff --git a/system/ui/tici_setup.py b/system/ui/tici_setup.py index bf64361bed..bb70b75b20 100755 --- a/system/ui/tici_setup.py +++ b/system/ui/tici_setup.py @@ -16,7 +16,7 @@ from openpilot.common.utils import run_cmd from openpilot.system.hardware import HARDWARE from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE -from openpilot.system.ui.widgets import Widget +from openpilot.system.ui.widgets import DialogResult, Widget from openpilot.system.ui.widgets.button import Button, ButtonStyle, ButtonRadio from openpilot.system.ui.widgets.keyboard import Keyboard from openpilot.system.ui.widgets.label import Label @@ -327,19 +327,20 @@ class Setup(Widget): def render_custom_software(self): def handle_keyboard_result(result): # Enter pressed - if result == 1: + if result == DialogResult.CONFIRM: url = self.keyboard.text self.keyboard.clear() if url: self.download(url) # Cancel pressed - elif result == 0: + elif result == DialogResult.CANCEL: self.state = SetupState.SOFTWARE_SELECTION self.keyboard.reset(min_text_size=1) self.keyboard.set_title("Enter URL", "for Custom Software") - gui_app.set_modal_overlay(self.keyboard, callback=handle_keyboard_result) + self.keyboard.set_callback(handle_keyboard_result) + gui_app.push_widget(self.keyboard) def use_openpilot(self): if os.path.isdir(INSTALL_PATH) and os.path.isfile(VALID_CACHE_PATH): @@ -435,11 +436,11 @@ class Setup(Widget): def main(): try: - gui_app.init_window("Setup", 20) + gui_app.init_window("Setup", 20, new_modal=True) setup = Setup() - for should_render in gui_app.render(): - if should_render: - setup.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) + gui_app.push_widget(setup) + for _ in gui_app.render(): + pass setup.close() except Exception as e: print(f"Setup error: {e}") diff --git a/system/ui/tici_updater.py b/system/ui/tici_updater.py index ebf4b3bec3..c040e1a404 100755 --- a/system/ui/tici_updater.py +++ b/system/ui/tici_updater.py @@ -160,11 +160,10 @@ def main(): manifest_path = sys.argv[2] try: - gui_app.init_window("System Update") - updater = Updater(updater_path, manifest_path) - for should_render in gui_app.render(): - if should_render: - updater.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) + gui_app.init_window("System Update", new_modal=True) + gui_app.push_widget(Updater(updater_path, manifest_path)) + for _ in gui_app.render(): + pass finally: # Make sure we clean up even if there's an error gui_app.close() diff --git a/system/ui/widgets/confirm_dialog.py b/system/ui/widgets/confirm_dialog.py index 97618660bd..3544836761 100644 --- a/system/ui/widgets/confirm_dialog.py +++ b/system/ui/widgets/confirm_dialog.py @@ -1,4 +1,5 @@ import pyray as rl +from collections.abc import Callable from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.widgets import DialogResult @@ -17,7 +18,7 @@ BACKGROUND_COLOR = rl.Color(27, 27, 27, 255) class ConfirmDialog(Widget): - def __init__(self, text: str, confirm_text: str, cancel_text: str | None = None, rich: bool = False): + def __init__(self, text: str, confirm_text: str, cancel_text: str | None = None, rich: bool = False, callback: Callable[[DialogResult], None] | None = None): super().__init__() if cancel_text is None: cancel_text = tr("Cancel") @@ -26,7 +27,7 @@ class ConfirmDialog(Widget): self._cancel_button = Button(cancel_text, self._cancel_button_callback) self._confirm_button = Button(confirm_text, self._confirm_button_callback, button_style=ButtonStyle.PRIMARY) self._rich = rich - self._dialog_result = DialogResult.NO_ACTION + self._callback = callback self._cancel_text = cancel_text self._scroller = Scroller([self._html_renderer], line_separator=False, spacing=0) @@ -36,14 +37,15 @@ class ConfirmDialog(Widget): else: self._html_renderer.parse_html_content(text) - def reset(self): - self._dialog_result = DialogResult.NO_ACTION - def _cancel_button_callback(self): - self._dialog_result = DialogResult.CANCEL + gui_app.pop_widget() + if self._callback: + self._callback(DialogResult.CANCEL) def _confirm_button_callback(self): - self._dialog_result = DialogResult.CONFIRM + gui_app.pop_widget() + if self._callback: + self._callback(DialogResult.CONFIRM) def _render(self, rect: rl.Rectangle): dialog_x = OUTER_MARGIN if not self._rich else RICH_OUTER_MARGIN @@ -73,9 +75,9 @@ class ConfirmDialog(Widget): self._scroller.render(text_rect) if rl.is_key_pressed(rl.KeyboardKey.KEY_ENTER): - self._dialog_result = DialogResult.CONFIRM + self._confirm_button_callback() elif rl.is_key_pressed(rl.KeyboardKey.KEY_ESCAPE): - self._dialog_result = DialogResult.CANCEL + self._cancel_button_callback() if self._cancel_text: self._confirm_button.render(confirm_button) @@ -85,8 +87,6 @@ class ConfirmDialog(Widget): full_confirm_button = rl.Rectangle(dialog_rect.x + MARGIN, button_y, full_button_width, BUTTON_HEIGHT) self._confirm_button.render(full_confirm_button) - return self._dialog_result - def alert_dialog(message: str, button_text: str | None = None): if button_text is None: diff --git a/system/ui/widgets/html_render.py b/system/ui/widgets/html_render.py index 7d90d56925..77fca9fe34 100644 --- a/system/ui/widgets/html_render.py +++ b/system/ui/widgets/html_render.py @@ -260,7 +260,7 @@ class HtmlModal(Widget): super().__init__() self._content = HtmlRenderer(file_path=file_path, text=text) self._scroll_panel = GuiScrollPanel() - self._ok_button = Button(tr("OK"), click_callback=lambda: gui_app.set_modal_overlay(None), button_style=ButtonStyle.PRIMARY) + self._ok_button = Button(tr("OK"), click_callback=gui_app.pop_widget, button_style=ButtonStyle.PRIMARY) def _render(self, rect: rl.Rectangle): margin = 50 diff --git a/system/ui/widgets/keyboard.py b/system/ui/widgets/keyboard.py index 4ec92f507a..531725688a 100644 --- a/system/ui/widgets/keyboard.py +++ b/system/ui/widgets/keyboard.py @@ -1,12 +1,13 @@ from functools import partial import time from typing import Literal +from collections.abc import Callable import pyray as rl from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.multilang import tr -from openpilot.system.ui.widgets import Widget +from openpilot.system.ui.widgets import DialogResult, Widget from openpilot.system.ui.widgets.button import ButtonStyle, Button from openpilot.system.ui.widgets.inputbox import InputBox from openpilot.system.ui.widgets.label import Label @@ -58,7 +59,8 @@ KEYBOARD_LAYOUTS = { class Keyboard(Widget): - def __init__(self, max_text_size: int = 255, min_text_size: int = 0, password_mode: bool = False, show_password_toggle: bool = False): + def __init__(self, max_text_size: int = 255, min_text_size: int = 0, password_mode: bool = False, show_password_toggle: bool = False, + callback: Callable[[DialogResult], None] | None = None): super().__init__() self._layout_name: Literal["lowercase", "uppercase", "numbers", "specials"] = "lowercase" self._caps_lock = False @@ -71,13 +73,13 @@ class Keyboard(Widget): self._input_box = InputBox(max_text_size) self._password_mode = password_mode self._show_password_toggle = show_password_toggle + self._callback = callback # Backspace key repeat tracking self._backspace_pressed: bool = False self._backspace_press_time: float = 0.0 self._backspace_last_repeat: float = 0.0 - self._render_return_status = -1 self._cancel_button = Button(lambda: tr("Cancel"), self._cancel_button_callback) self._eye_button = Button("", self._eye_button_callback, button_style=ButtonStyle.TRANSPARENT) @@ -122,16 +124,23 @@ class Keyboard(Widget): self._title.set_text(title) self._sub_title.set_text(sub_title) + def set_callback(self, callback: Callable[[DialogResult], None] | None): + self._callback = callback + def _eye_button_callback(self): self._password_mode = not self._password_mode def _cancel_button_callback(self): self.clear() - self._render_return_status = 0 + gui_app.pop_widget() + if self._callback: + self._callback(DialogResult.CANCEL) def _key_callback(self, k): if k == ENTER_KEY: - self._render_return_status = 1 + gui_app.pop_widget() + if self._callback: + self._callback(DialogResult.CONFIRM) else: self.handle_key_press(k) @@ -197,8 +206,6 @@ class Keyboard(Widget): self._all_keys[key].set_enabled(is_enabled) self._all_keys[key].render(key_rect) - return self._render_return_status - def _render_input_area(self, input_rect: rl.Rectangle): if self._show_password_toggle: self._input_box.set_password_mode(self._password_mode) @@ -250,7 +257,6 @@ class Keyboard(Widget): def reset(self, min_text_size: int | None = None): if min_text_size is not None: self._min_text_size = min_text_size - self._render_return_status = -1 self._last_shift_press_time = 0 self._backspace_pressed = False self._backspace_press_time = 0.0 @@ -259,15 +265,18 @@ class Keyboard(Widget): if __name__ == "__main__": - gui_app.init_window("Keyboard") - keyboard = Keyboard(min_text_size=8, show_password_toggle=True) - for _ in gui_app.render(): - keyboard.set_title("Keyboard Input", "Type your text below") - result = keyboard.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) - if result == 1: + def callback(result: DialogResult): + if result == DialogResult.CONFIRM: print(f"You typed: {keyboard.text}") - gui_app.request_close() - elif result == 0: + elif result == DialogResult.CANCEL: print("Canceled") - gui_app.request_close() + gui_app.request_close() + + gui_app.init_window("Keyboard", new_modal=True) + keyboard = Keyboard(min_text_size=8, show_password_toggle=True, callback=callback) + keyboard.set_title("Keyboard Input", "Type your text below") + + gui_app.push_widget(keyboard) + for _ in gui_app.render(): + pass gui_app.close() diff --git a/system/ui/widgets/network.py b/system/ui/widgets/network.py index 1f146fbdd1..fcd56607c0 100644 --- a/system/ui/widgets/network.py +++ b/system/ui/widgets/network.py @@ -7,7 +7,7 @@ from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel from openpilot.system.ui.lib.wifi_manager import WifiManager, SecurityType, Network, MeteredType, normalize_ssid -from openpilot.system.ui.widgets import Widget +from openpilot.system.ui.widgets import DialogResult, Widget from openpilot.system.ui.widgets.button import ButtonStyle, Button from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog from openpilot.system.ui.widgets.keyboard import Keyboard @@ -187,8 +187,8 @@ class AdvancedNetworkSettings(Widget): self._wifi_manager.update_gsm_settings(roaming_state, self._params.get("GsmApn") or "", self._params.get_bool("GsmMetered")) def _edit_apn(self): - def update_apn(result): - if result != 1: + def update_apn(result: DialogResult): + if result != DialogResult.CONFIRM: return apn = self._keyboard.text.strip() @@ -203,7 +203,8 @@ class AdvancedNetworkSettings(Widget): self._keyboard.reset(min_text_size=0) self._keyboard.set_title(tr("Enter APN"), tr("leave blank for automatic configuration")) self._keyboard.set_text(current_apn) - gui_app.set_modal_overlay(self._keyboard, update_apn) + self._keyboard.set_callback(update_apn) + gui_app.push_widget(self._keyboard) def _toggle_cellular_metered(self): metered = self._cellular_metered_action.get_state() @@ -216,15 +217,18 @@ class AdvancedNetworkSettings(Widget): self._wifi_manager.set_current_network_metered(metered_type) def _connect_to_hidden_network(self): - def connect_hidden(result): - if result != 1: + def connect_hidden(result: DialogResult): + if result != DialogResult.CONFIRM: return ssid = self._keyboard.text if not ssid: return - def enter_password(result): + def enter_password(result: DialogResult): + if result != DialogResult.CONFIRM: + return + password = self._keyboard.text if password == "": # connect without password @@ -235,15 +239,17 @@ class AdvancedNetworkSettings(Widget): self._keyboard.reset(min_text_size=0) self._keyboard.set_title(tr("Enter password"), tr("for \"{}\"").format(ssid)) - gui_app.set_modal_overlay(self._keyboard, enter_password) + self._keyboard.set_callback(enter_password) + gui_app.push_widget(self._keyboard) self._keyboard.reset(min_text_size=1) self._keyboard.set_title(tr("Enter SSID"), "") - gui_app.set_modal_overlay(self._keyboard, connect_hidden) + self._keyboard.set_callback(connect_hidden) + gui_app.push_widget(self._keyboard) def _edit_tethering_password(self): - def update_password(result): - if result != 1: + def update_password(result: DialogResult): + if result != DialogResult.CONFIRM: return password = self._keyboard.text @@ -253,7 +259,8 @@ class AdvancedNetworkSettings(Widget): self._keyboard.reset(min_text_size=MIN_PASSWORD_LENGTH) self._keyboard.set_title(tr("Enter new tethering password"), "") self._keyboard.set_text(self._wifi_manager.tethering_password) - gui_app.set_modal_overlay(self._keyboard, update_password) + self._keyboard.set_callback(update_password) + gui_app.push_widget(self._keyboard) def _update_state(self): self._wifi_manager.process_callbacks() @@ -314,29 +321,29 @@ class WifiManagerUI(Widget): self.keyboard.set_title(tr("Wrong password") if self._password_retry else tr("Enter password"), tr("for \"{}\"").format(normalize_ssid(self._state_network.ssid))) self.keyboard.reset(min_text_size=MIN_PASSWORD_LENGTH) - gui_app.set_modal_overlay(self.keyboard, lambda result: self._on_password_entered(cast(Network, self._state_network), result)) + self.keyboard.set_callback(lambda result: self._on_password_entered(cast(Network, self._state_network), result)) + gui_app.push_widget(self.keyboard) elif self.state == UIState.SHOW_FORGET_CONFIRM and self._state_network: - confirm_dialog = ConfirmDialog("", tr("Forget"), tr("Cancel")) + confirm_dialog = ConfirmDialog("", tr("Forget"), tr("Cancel"), callback=lambda result: self.on_forgot_confirm_finished(self._state_network, result)) confirm_dialog.set_text(tr("Forget Wi-Fi Network \"{}\"?").format(normalize_ssid(self._state_network.ssid))) - confirm_dialog.reset() - gui_app.set_modal_overlay(confirm_dialog, callback=lambda result: self.on_forgot_confirm_finished(self._state_network, result)) + gui_app.push_widget(confirm_dialog) else: self._draw_network_list(rect) - def _on_password_entered(self, network: Network, result: int): - if result == 1: + def _on_password_entered(self, network: Network, result: DialogResult): + if result == DialogResult.CONFIRM: password = self.keyboard.text self.keyboard.clear() if len(password) >= MIN_PASSWORD_LENGTH: self.connect_to_network(network, password) - elif result == 0: + elif result == DialogResult.CANCEL: self.state = UIState.IDLE - def on_forgot_confirm_finished(self, network, result: int): - if result == 1: + def on_forgot_confirm_finished(self, network, result: DialogResult): + if result == DialogResult.CONFIRM: self.forget_network(network) - elif result == 0: + elif result == DialogResult.CANCEL: self.state = UIState.IDLE def _draw_network_list(self, rect: rl.Rectangle): @@ -474,11 +481,11 @@ class WifiManagerUI(Widget): def main(): - gui_app.init_window("Wi-Fi Manager") - wifi_ui = WifiManagerUI(WifiManager()) + gui_app.init_window("Wi-Fi Manager", new_modal=True) + gui_app.push_widget(WifiManagerUI(WifiManager())) for _ in gui_app.render(): - wifi_ui.render(rl.Rectangle(50, 50, gui_app.width - 100, gui_app.height - 100)) + pass gui_app.close() diff --git a/system/ui/widgets/option_dialog.py b/system/ui/widgets/option_dialog.py index 62578d1cfb..206400a74f 100644 --- a/system/ui/widgets/option_dialog.py +++ b/system/ui/widgets/option_dialog.py @@ -1,5 +1,6 @@ import pyray as rl -from openpilot.system.ui.lib.application import FontWeight +from collections.abc import Callable +from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.widgets import Widget, DialogResult from openpilot.system.ui.widgets.button import Button, ButtonStyle @@ -17,13 +18,13 @@ LIST_ITEM_SPACING = 25 class MultiOptionDialog(Widget): - def __init__(self, title, options, current="", option_font_weight=FontWeight.MEDIUM): + def __init__(self, title, options, current="", option_font_weight=FontWeight.MEDIUM, callback: Callable[[DialogResult], None] | None = None): super().__init__() self.title = title self.options = options self.current = current self.selection = current - self._result: DialogResult = DialogResult.NO_ACTION + self._callback = callback # Create scroller with option buttons self.option_buttons = [Button(option, click_callback=lambda opt=option: self._on_option_clicked(opt), @@ -36,7 +37,9 @@ class MultiOptionDialog(Widget): self.select_button = Button(lambda: tr("Select"), click_callback=lambda: self._set_result(DialogResult.CONFIRM), button_style=ButtonStyle.PRIMARY) def _set_result(self, result: DialogResult): - self._result = result + gui_app.pop_widget() + if self._callback: + self._callback(result) def _on_option_clicked(self, option): self.selection = option @@ -74,5 +77,3 @@ class MultiOptionDialog(Widget): select_rect = rl.Rectangle(content_rect.x + button_w + BUTTON_SPACING, button_y, button_w, BUTTON_HEIGHT) self.select_button.set_enabled(self.selection != self.current) self.select_button.render(select_rect) - - return self._result From 8bca2ca7588bae62a860b1e90a835e1678367596 Mon Sep 17 00:00:00 2001 From: Trey Moen <50057480+greatgitsby@users.noreply.github.com> Date: Fri, 20 Feb 2026 11:26:50 -0700 Subject: [PATCH 111/311] feat(lpa): `at` client + list profiles (#37271) * feat(lpa): implement list_profiles in TiciLPA Add AT command serial interface, TLV parsing, and ES10x transport to support listing eSIM profiles (SGP.22 v2.3). TiciLPA is a singleton that maintains a persistent connection to the modem. * feat(lpa): close TiciLPA serial connection on exit Register atexit cleanup so the logical channel and serial port are released when the process exits, even on crashes or early exits. * feat(lpa): close stale logical channels on init to prevent timeouts * trying to brick it * Revert "trying to brick it" This reverts commit 46a0467314479c92d2cf331280521a1263f6cc43. * feat(lpa): remove ensure_capabilities check on init Target devices are known to support the required AT commands, so skip the capability probes and stale channel cleanup to speed up initialization. * feat(lpa): enable debug logging via DEBUG=1 env variable * muuuch better * revert * cleanup * constant --- system/hardware/tici/lpa.py | 229 +++++++++++++++++++++++++++++++++++- 1 file changed, 227 insertions(+), 2 deletions(-) diff --git a/system/hardware/tici/lpa.py b/system/hardware/tici/lpa.py index 9bd9d8c7b0..4d649fda8b 100644 --- a/system/hardware/tici/lpa.py +++ b/system/hardware/tici/lpa.py @@ -1,12 +1,237 @@ +# SGP.22 v2.3: https://www.gsma.com/solutions-and-impact/technologies/esim/wp-content/uploads/2021/07/SGP.22-v2.3.pdf + +import atexit +import base64 +import os +import serial +import sys + +from collections.abc import Generator + from openpilot.system.hardware.base import LPABase, Profile +DEFAULT_DEVICE = "/dev/ttyUSB2" +DEFAULT_BAUD = 9600 +DEFAULT_TIMEOUT = 5.0 +# https://euicc-manual.osmocom.org/docs/lpa/applet-id/ +ISDR_AID = "A0000005591010FFFFFFFF8900000100" +ES10X_MSS = 120 +DEBUG = os.environ.get("DEBUG") == "1" + +# TLV Tags +TAG_ICCID = 0x5A +TAG_PROFILE_INFO_LIST = 0xBF2D + +STATE_LABELS = {0: "disabled", 1: "enabled", 255: "unknown"} +ICON_LABELS = {0: "jpeg", 1: "png", 255: "unknown"} +CLASS_LABELS = {0: "test", 1: "provisioning", 2: "operational", 255: "unknown"} + + +def b64e(data: bytes) -> str: + return base64.b64encode(data).decode("ascii") + + +class AtClient: + def __init__(self, device: str, baud: int, timeout: float, debug: bool) -> None: + self.serial = serial.Serial(device, baudrate=baud, timeout=timeout) + self.debug = debug + self.channel: str | None = None + self.serial.reset_input_buffer() + + def close(self) -> None: + try: + if self.channel: + self.query(f"AT+CCHC={self.channel}") + self.channel = None + finally: + self.serial.close() + + def send(self, cmd: str) -> None: + if self.debug: + print(f">> {cmd}", file=sys.stderr) + self.serial.write((cmd + "\r").encode("ascii")) + + def expect(self) -> list[str]: + lines: list[str] = [] + while True: + raw = self.serial.readline() + if not raw: + raise TimeoutError("AT command timed out") + line = raw.decode(errors="ignore").strip() + if not line: + continue + if self.debug: + print(f"<< {line}", file=sys.stderr) + if line == "OK": + return lines + if line == "ERROR" or line.startswith("+CME ERROR"): + raise RuntimeError(f"AT command failed: {line}") + lines.append(line) + + def query(self, cmd: str) -> list[str]: + self.send(cmd) + return self.expect() + + def open_isdr(self) -> None: + # close any stale logical channel from a previous crashed session + try: + self.query("AT+CCHC=1") + except RuntimeError: + pass + for line in self.query(f'AT+CCHO="{ISDR_AID}"'): + if line.startswith("+CCHO:") and (ch := line.split(":", 1)[1].strip()): + self.channel = ch + return + raise RuntimeError("Failed to open ISD-R application") + + def send_apdu(self, apdu: bytes) -> tuple[bytes, int, int]: + if not self.channel: + raise RuntimeError("Logical channel is not open") + hex_payload = apdu.hex().upper() + for line in self.query(f'AT+CGLA={self.channel},{len(hex_payload)},"{hex_payload}"'): + if line.startswith("+CGLA:"): + parts = line.split(":", 1)[1].split(",", 1) + if len(parts) == 2: + data = bytes.fromhex(parts[1].strip().strip('"')) + if len(data) >= 2: + return data[:-2], data[-2], data[-1] + raise RuntimeError("Missing +CGLA response") + + +# --- TLV utilities --- + +def iter_tlv(data: bytes, with_positions: bool = False) -> Generator: + idx, length = 0, len(data) + while idx < length: + start_pos = idx + tag = data[idx] + idx += 1 + if tag & 0x1F == 0x1F: # Multi-byte tag + tag_value = tag + while idx < length: + next_byte = data[idx] + idx += 1 + tag_value = (tag_value << 8) | next_byte + if not (next_byte & 0x80): + break + else: + tag_value = tag + if idx >= length: + break + size = data[idx] + idx += 1 + if size & 0x80: # Multi-byte length + num_bytes = size & 0x7F + if idx + num_bytes > length: + break + size = int.from_bytes(data[idx : idx + num_bytes], "big") + idx += num_bytes + if idx + size > length: + break + value = data[idx : idx + size] + idx += size + yield (tag_value, value, start_pos, idx) if with_positions else (tag_value, value) + + +def find_tag(data: bytes, target: int) -> bytes | None: + return next((v for t, v in iter_tlv(data) if t == target), None) + + +def tbcd_to_string(raw: bytes) -> str: + return "".join(str(n) for b in raw for n in (b & 0x0F, b >> 4) if n <= 9) + + +# Profile field decoders: TLV tag -> (field_name, decoder) +_PROFILE_FIELDS = { + TAG_ICCID: ("iccid", tbcd_to_string), + 0x4F: ("isdpAid", lambda v: v.hex().upper()), + 0x9F70: ("profileState", lambda v: STATE_LABELS.get(v[0], "unknown")), + 0x90: ("profileNickname", lambda v: v.decode("utf-8", errors="ignore") or None), + 0x91: ("serviceProviderName", lambda v: v.decode("utf-8", errors="ignore") or None), + 0x92: ("profileName", lambda v: v.decode("utf-8", errors="ignore") or None), + 0x93: ("iconType", lambda v: ICON_LABELS.get(v[0], "unknown")), + 0x94: ("icon", b64e), + 0x95: ("profileClass", lambda v: CLASS_LABELS.get(v[0], "unknown")), +} + + +def _decode_profile_fields(data: bytes) -> dict: + """Parse known profile metadata TLV fields into a dict.""" + result = {} + for tag, value in iter_tlv(data): + if (field := _PROFILE_FIELDS.get(tag)): + result[field[0]] = field[1](value) + return result + + +# --- ES10x command transport --- + +def es10x_command(client: AtClient, data: bytes) -> bytes: + response = bytearray() + sequence = 0 + offset = 0 + while offset < len(data): + chunk = data[offset : offset + ES10X_MSS] + offset += len(chunk) + is_last = offset == len(data) + apdu = bytes([0x80, 0xE2, 0x91 if is_last else 0x11, sequence & 0xFF, len(chunk)]) + chunk + segment, sw1, sw2 = client.send_apdu(apdu) + response.extend(segment) + while True: + if sw1 == 0x61: # More data available + segment, sw1, sw2 = client.send_apdu(bytes([0x80, 0xC0, 0x00, 0x00, sw2 or 0])) + response.extend(segment) + continue + if (sw1 & 0xF0) == 0x90: + break + raise RuntimeError(f"APDU failed with SW={sw1:02X}{sw2:02X}") + sequence += 1 + return bytes(response) + + +# --- Profile operations --- + +def decode_profiles(blob: bytes) -> list[dict]: + root = find_tag(blob, TAG_PROFILE_INFO_LIST) + if root is None: + raise RuntimeError("Missing ProfileInfoList") + list_ok = find_tag(root, 0xA0) + if list_ok is None: + return [] + defaults = {name: None for name, _ in _PROFILE_FIELDS.values()} + return [{**defaults, **_decode_profile_fields(value)} for tag, value in iter_tlv(list_ok) if tag == 0xE3] + + +def list_profiles(client: AtClient) -> list[dict]: + return decode_profiles(es10x_command(client, TAG_PROFILE_INFO_LIST.to_bytes(2, "big") + b"\x00")) + + class TiciLPA(LPABase): + _instance = None + + def __new__(cls): + if cls._instance is None: + cls._instance = super().__new__(cls) + return cls._instance + def __init__(self): - pass + if hasattr(self, '_client'): + return + self._client = AtClient(DEFAULT_DEVICE, DEFAULT_BAUD, DEFAULT_TIMEOUT, debug=DEBUG) + self._client.open_isdr() + atexit.register(self._client.close) def list_profiles(self) -> list[Profile]: - return [] + return [ + Profile( + iccid=p.get("iccid", ""), + nickname=p.get("profileNickname") or "", + enabled=p.get("profileState") == "enabled", + provider=p.get("serviceProviderName") or "", + ) + for p in list_profiles(self._client) + ] def get_active_profile(self) -> Profile | None: return None From b27fa58444766c47f1ef980af79d324a669012c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Harald=20Sch=C3=A4fer?= Date: Fri, 20 Feb 2026 10:37:14 -0800 Subject: [PATCH 112/311] Simpler file chunker (#37276) * Chunk tinygrad pkl below GitHub max size * pull that out * rm glob * make work * Single name def * unused comment * more cleanups * revert that * 10MB overhead --------- Co-authored-by: Adeeb Shihadeh --- .gitignore | 3 +- common/file_chunker.py | 31 ++++++++++++++++++ selfdrive/modeld/SConscript | 45 +++++++++++++-------------- selfdrive/modeld/dmonitoringmodeld.py | 7 ++--- selfdrive/modeld/external_pickle.py | 38 ---------------------- selfdrive/modeld/modeld.py | 9 +++--- 6 files changed, 61 insertions(+), 72 deletions(-) create mode 100644 common/file_chunker.py delete mode 100755 selfdrive/modeld/external_pickle.py diff --git a/.gitignore b/.gitignore index af18f06628..062801d787 100644 --- a/.gitignore +++ b/.gitignore @@ -64,8 +64,7 @@ flycheck_* cppcheck_report.txt comma*.sh -selfdrive/modeld/models/*.pkl -selfdrive/modeld/models/*.pkl.* +selfdrive/modeld/models/*.pkl* # openpilot log files *.bz2 diff --git a/common/file_chunker.py b/common/file_chunker.py new file mode 100644 index 0000000000..f03d04a382 --- /dev/null +++ b/common/file_chunker.py @@ -0,0 +1,31 @@ +import math +import os +from pathlib import Path + +CHUNK_SIZE = 49 * 1024 * 1024 # 49MB, under GitHub's 50MB limit + +def get_chunk_name(name, idx, num_chunks): + return f"{name}.chunk{idx+1:02d}of{num_chunks:02d}" + +def get_chunk_paths(path, file_size): + num_chunks = math.ceil(file_size / CHUNK_SIZE) + return [get_chunk_name(path, i, num_chunks) for i in range(num_chunks)] + +def chunk_file(path, num_chunks): + with open(path, 'rb') as f: + data = f.read() + actual_num_chunks = max(1, math.ceil(len(data) / CHUNK_SIZE)) + assert num_chunks >= actual_num_chunks, f"Allowed {num_chunks} chunks but needs at least {actual_num_chunks}, for path {path}" + for i in range(num_chunks): + with open(get_chunk_name(path, i, num_chunks), 'wb') as f: + f.write(data[i * CHUNK_SIZE:(i + 1) * CHUNK_SIZE]) + + +def read_file_chunked(path): + for num_chunks in range(1, 100): + if os.path.isfile(get_chunk_name(path, 0, num_chunks)): + files = [get_chunk_name(path, i, num_chunks) for i in range(num_chunks)] + return b''.join(Path(f).read_bytes() for f in files) + if os.path.isfile(path): + return Path(path).read_bytes() + raise FileNotFoundError(path) diff --git a/selfdrive/modeld/SConscript b/selfdrive/modeld/SConscript index 1808cfec2f..35be2acc04 100644 --- a/selfdrive/modeld/SConscript +++ b/selfdrive/modeld/SConscript @@ -1,14 +1,18 @@ import os import glob +from openpilot.common.file_chunker import chunk_file, get_chunk_paths Import('env', 'arch') +chunker_file = File("#common/file_chunker.py") lenv = env.Clone() -CHUNK_BYTES = int(os.environ.get("TG_CHUNK_BYTES", str(45 * 1024 * 1024))) tinygrad_root = env.Dir("#").abspath tinygrad_files = ["#"+x for x in glob.glob(env.Dir("#tinygrad_repo").relpath + "/**", recursive=True, root_dir=tinygrad_root) if 'pycache' not in x and os.path.isfile(os.path.join(tinygrad_root, x))] +def estimate_pickle_max_size(onnx_size): + return 1.2 * onnx_size + 10 * 1024 * 1024 # 20% + 10MB is plenty + # Get model metadata for model_name in ['driving_vision', 'driving_policy', 'dmonitoring_model']: fn = File(f"models/{model_name}").abspath @@ -26,39 +30,34 @@ image_flag = { 'larch64': 'IMAGE=2', }.get(arch, 'IMAGE=0') script_files = [File(Dir("#selfdrive/modeld").File("compile_warp.py").abspath)] -cmd = f'{tg_flags} python3 {Dir("#selfdrive/modeld").abspath}/compile_warp.py ' +compile_warp_cmd = f'{tg_flags} python3 {Dir("#selfdrive/modeld").abspath}/compile_warp.py ' from openpilot.common.transformations.camera import _ar_ox_fisheye, _os_fisheye warp_targets = [] for cam in [_ar_ox_fisheye, _os_fisheye]: w, h = cam.width, cam.height warp_targets += [File(f"models/warp_{w}x{h}_tinygrad.pkl").abspath, File(f"models/dm_warp_{w}x{h}_tinygrad.pkl").abspath] -lenv.Command(warp_targets, tinygrad_files + script_files, cmd) +def chunk_warps(target, source, env): + for t in warp_targets: + chunk_file(t, 1) +chunk_targets = sum([get_chunk_paths(t, estimate_pickle_max_size(0)) for t in warp_targets], []) +lenv.Command(chunk_targets, tinygrad_files + script_files + [chunker_file], + [compile_warp_cmd, chunk_warps]) def tg_compile(flags, model_name): pythonpath_string = 'PYTHONPATH="${PYTHONPATH}:' + env.Dir("#tinygrad_repo").abspath + '"' fn = File(f"models/{model_name}").abspath - - out = fn + "_tinygrad.pkl" - full = out + ".full" - parts = out + ".parts" - - full_node = lenv.Command( - full, - [fn + ".onnx"] + tinygrad_files, - f'{pythonpath_string} {flags} {image_flag} python3 {Dir("#tinygrad_repo").abspath}/examples/openpilot/compile3.py {fn}.onnx {full}' + pkl = fn + "_tinygrad.pkl" + onnx_path = fn + ".onnx" + chunk_targets = get_chunk_paths(pkl, estimate_pickle_max_size(os.path.getsize(onnx_path))) + def do_chunk(target, source, env): + chunk_file(pkl, len(chunk_targets)) + return lenv.Command( + chunk_targets, + [onnx_path] + tinygrad_files + [chunker_file], + [f'{pythonpath_string} {flags} {image_flag} python3 {Dir("#tinygrad_repo").abspath}/examples/openpilot/compile3.py {fn}.onnx {pkl}', + do_chunk] ) - split_script = File(Dir("#selfdrive/modeld").File("external_pickle.py").abspath) - parts_node = lenv.Command( - parts, - [full_node, split_script, Value(str(CHUNK_BYTES))], - [f'python3 {split_script.abspath} {full} {out} {CHUNK_BYTES}', Delete(full)], - ) - - lenv.NoCache(parts_node) - lenv.AlwaysBuild(parts_node) - return parts_node - # Compile small models for model_name in ['driving_vision', 'driving_policy', 'dmonitoring_model']: tg_compile(tg_flags, model_name) diff --git a/selfdrive/modeld/dmonitoringmodeld.py b/selfdrive/modeld/dmonitoringmodeld.py index 956ea8a6a2..6befe210a4 100755 --- a/selfdrive/modeld/dmonitoringmodeld.py +++ b/selfdrive/modeld/dmonitoringmodeld.py @@ -16,8 +16,8 @@ from openpilot.common.realtime import config_realtime_process from openpilot.common.transformations.model import dmonitoringmodel_intrinsics from openpilot.common.transformations.camera import _ar_ox_fisheye, _os_fisheye from openpilot.system.camerad.cameras.nv12_info import get_nv12_info +from openpilot.common.file_chunker import read_file_chunked from openpilot.selfdrive.modeld.parse_model_outputs import sigmoid, safe_exp -from openpilot.selfdrive.modeld.external_pickle import load_external_pickle PROCESS_NAME = "selfdrive.modeld.dmonitoringmodeld" SEND_RAW_PRED = os.getenv('SEND_RAW_PRED') @@ -45,7 +45,7 @@ class ModelState: self.tensor_inputs = {k: Tensor(v, device='NPY').realize() for k,v in self.numpy_inputs.items()} self._blob_cache : dict[int, Tensor] = {} self.image_warp = None - self.model_run = load_external_pickle(MODEL_PKL_PATH) + self.model_run = pickle.loads(read_file_chunked(str(MODEL_PKL_PATH))) def run(self, buf: VisionBuf, calib: np.ndarray, transform: np.ndarray) -> tuple[np.ndarray, float]: self.numpy_inputs['calib'][0,:] = calib @@ -55,8 +55,7 @@ class ModelState: if self.image_warp is None: self.frame_buf_params = get_nv12_info(buf.width, buf.height) warp_path = MODELS_DIR / f'dm_warp_{buf.width}x{buf.height}_tinygrad.pkl' - with open(warp_path, "rb") as f: - self.image_warp = pickle.load(f) + self.image_warp = pickle.loads(read_file_chunked(str(warp_path))) ptr = buf.data.ctypes.data # There is a ringbuffer of imgs, just cache tensors pointing to all of them if ptr not in self._blob_cache: diff --git a/selfdrive/modeld/external_pickle.py b/selfdrive/modeld/external_pickle.py deleted file mode 100755 index d60a9632a6..0000000000 --- a/selfdrive/modeld/external_pickle.py +++ /dev/null @@ -1,38 +0,0 @@ -#!/usr/bin/env python3 -import hashlib -import pickle -import sys -from pathlib import Path - -def split_pickle(full_path: Path, out_prefix: Path, chunk_bytes: int) -> None: - data = full_path.read_bytes() - out_dir = out_prefix.parent - - for p in out_dir.glob(f"{out_prefix.name}.data-*"): - p.unlink() - - total = (len(data) + chunk_bytes - 1) // chunk_bytes - names = [] - for i in range(0, len(data), chunk_bytes): - name = f"{out_prefix.name}.data-{(i // chunk_bytes) + 1:04d}-of-{total:04d}" - (out_dir / name).write_bytes(data[i:i + chunk_bytes]) - names.append(name) - - manifest = hashlib.sha256(data).hexdigest() + "\n" + "\n".join(names) + "\n" - (out_dir / (out_prefix.name + ".parts")).write_text(manifest) - -def load_external_pickle(prefix: Path): - parts = prefix.parent / (prefix.name + ".parts") - lines = parts.read_text().splitlines() - expected_hash, chunk_names = lines[0], lines[1:] - - data = bytearray() - for name in chunk_names: - data += (prefix.parent / name).read_bytes() - - if hashlib.sha256(data).hexdigest() != expected_hash: - raise RuntimeError(f"hash mismatch loading {prefix}") - return pickle.loads(data) - -if __name__ == "__main__": - split_pickle(Path(sys.argv[1]), Path(sys.argv[2]), int(sys.argv[3])) diff --git a/selfdrive/modeld/modeld.py b/selfdrive/modeld/modeld.py index 3fe3e0e6d6..8cfbea02c8 100755 --- a/selfdrive/modeld/modeld.py +++ b/selfdrive/modeld/modeld.py @@ -27,8 +27,8 @@ from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper from openpilot.selfdrive.controls.lib.drive_helpers import get_accel_from_plan, smooth_value, get_curvature_from_plan from openpilot.selfdrive.modeld.parse_model_outputs import Parser from openpilot.selfdrive.modeld.fill_model_msg import fill_model_msg, fill_pose_msg, PublishState +from openpilot.common.file_chunker import read_file_chunked from openpilot.selfdrive.modeld.constants import ModelConstants, Plan -from openpilot.selfdrive.modeld.external_pickle import load_external_pickle PROCESS_NAME = "selfdrive.modeld.modeld" @@ -178,8 +178,8 @@ class ModelState: self.parser = Parser() self.frame_buf_params : dict[str, tuple[int, int, int, int]] = {} self.update_imgs = None - self.vision_run = load_external_pickle(VISION_PKL_PATH) - self.policy_run = load_external_pickle(POLICY_PKL_PATH) + self.vision_run = pickle.loads(read_file_chunked(str(VISION_PKL_PATH))) + self.policy_run = pickle.loads(read_file_chunked(str(POLICY_PKL_PATH))) def slice_outputs(self, model_outputs: np.ndarray, output_slices: dict[str, slice]) -> dict[str, np.ndarray]: parsed_model_outputs = {k: model_outputs[np.newaxis, v] for k,v in output_slices.items()} @@ -196,8 +196,7 @@ class ModelState: w, h = bufs[key].width, bufs[key].height self.frame_buf_params[key] = get_nv12_info(w, h) warp_path = MODELS_DIR / f'warp_{w}x{h}_tinygrad.pkl' - with open(warp_path, "rb") as f: - self.update_imgs = pickle.load(f) + self.update_imgs = pickle.loads(read_file_chunked(str(warp_path))) for key in bufs.keys(): ptr = bufs[key].data.ctypes.data From 66687746f990c7021db673df9eceb06e6850789e Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Fri, 20 Feb 2026 14:20:02 -0800 Subject: [PATCH 113/311] replace dictdiffer with native capnp differ (#37279) * replace dictdiffer with native capnp differ * capnp diff --- pyproject.toml | 1 - selfdrive/test/process_replay/compare_logs.py | 81 ++++++++++++++----- uv.lock | 11 --- 3 files changed, 60 insertions(+), 33 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8eb86101af..ac10630a9c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -96,7 +96,6 @@ testing = [ dev = [ "av", - "dictdiffer", "matplotlib", "opencv-python-headless", "parameterized >=0.8, <0.9", diff --git a/selfdrive/test/process_replay/compare_logs.py b/selfdrive/test/process_replay/compare_logs.py index 13d51a636f..e2d912a833 100755 --- a/selfdrive/test/process_replay/compare_logs.py +++ b/selfdrive/test/process_replay/compare_logs.py @@ -3,13 +3,16 @@ import sys import math import capnp import numbers -import dictdiffer from collections import Counter from openpilot.tools.lib.logreader import LogReader EPSILON = sys.float_info.epsilon +_DynamicStructReader = capnp.lib.capnp._DynamicStructReader +_DynamicListReader = capnp.lib.capnp._DynamicListReader +_DynamicEnum = capnp.lib.capnp._DynamicEnum + def remove_ignored_fields(msg, ignore): msg = msg.as_builder() @@ -39,6 +42,61 @@ def remove_ignored_fields(msg, ignore): return msg +def _diff_capnp(r1, r2, path, tolerance): + """Walk two capnp struct readers and yield (action, dotted_path, value) diffs. + + Floats are compared with the given tolerance (combined absolute+relative). + """ + schema = r1.schema + + for fname in schema.non_union_fields: + child_path = path + (fname,) + v1 = getattr(r1, fname) + v2 = getattr(r2, fname) + yield from _diff_capnp_values(v1, v2, child_path, tolerance) + + if schema.union_fields: + w1, w2 = r1.which(), r2.which() + if w1 != w2: + yield 'change', '.'.join(path), (w1, w2) + else: + child_path = path + (w1,) + v1, v2 = getattr(r1, w1), getattr(r2, w2) + yield from _diff_capnp_values(v1, v2, child_path, tolerance) + + +def _diff_capnp_values(v1, v2, path, tolerance): + if isinstance(v1, _DynamicStructReader): + yield from _diff_capnp(v1, v2, path, tolerance) + + elif isinstance(v1, _DynamicListReader): + dot = '.'.join(path) + n1, n2 = len(v1), len(v2) + n = min(n1, n2) + for i in range(n): + yield from _diff_capnp_values(v1[i], v2[i], path + (str(i),), tolerance) + if n2 > n: + yield 'add', dot, list(enumerate(v2[n:], n)) + if n1 > n: + yield 'remove', dot, list(reversed([(i, v1[i]) for i in range(n, n1)])) + + elif isinstance(v1, _DynamicEnum): + s1, s2 = str(v1), str(v2) + if s1 != s2: + yield 'change', '.'.join(path), (s1, s2) + + elif isinstance(v1, float): + if not (v1 == v2 or ( + math.isfinite(v1) and math.isfinite(v2) and + abs(v1 - v2) <= max(tolerance, tolerance * max(abs(v1), abs(v2))) + )): + yield 'change', '.'.join(path), (v1, v2) + + else: + if v1 != v2: + yield 'change', '.'.join(path), (v1, v2) + + def compare_logs(log1, log2, ignore_fields=None, ignore_msgs=None, tolerance=None,): if ignore_fields is None: ignore_fields = [] @@ -65,26 +123,7 @@ def compare_logs(log1, log2, ignore_fields=None, ignore_msgs=None, tolerance=Non msg2 = remove_ignored_fields(msg2, ignore_fields) if msg1.to_bytes() != msg2.to_bytes(): - msg1_dict = msg1.as_reader().to_dict(verbose=True) - msg2_dict = msg2.as_reader().to_dict(verbose=True) - - dd = dictdiffer.diff(msg1_dict, msg2_dict, ignore=ignore_fields) - - # Dictdiffer only supports relative tolerance, we also want to check for absolute - # TODO: add this to dictdiffer - def outside_tolerance(diff): - try: - if diff[0] == "change": - a, b = diff[2] - finite = math.isfinite(a) and math.isfinite(b) - if finite and isinstance(a, numbers.Number) and isinstance(b, numbers.Number): - return abs(a - b) > max(tolerance, tolerance * max(abs(a), abs(b))) - except TypeError: - pass - return True - - dd = list(filter(outside_tolerance, dd)) - + dd = list(_diff_capnp(msg1.as_reader(), msg2.as_reader(), (), tolerance)) diff.extend(dd) return diff diff --git a/uv.lock b/uv.lock index bd4632942a..4f1c957f3c 100644 --- a/uv.lock +++ b/uv.lock @@ -356,15 +356,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5e/58/d01538556103d544a5a5b4cbcb00646ff92d8a97f0a6283a56bede4307c8/dearpygui-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:9f2291313d2035f8a4108e13f60d8c1a0e7c19af7554a7739a3fd15b3d5af8f7", size = 1808971, upload-time = "2025-11-14T14:47:28.15Z" }, ] -[[package]] -name = "dictdiffer" -version = "0.9.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/61/7b/35cbccb7effc5d7e40f4c55e2b79399e1853041997fcda15c9ff160abba0/dictdiffer-0.9.0.tar.gz", hash = "sha256:17bacf5fbfe613ccf1b6d512bd766e6b21fb798822a133aa86098b8ac9997578", size = 31513, upload-time = "2021-07-22T13:24:29.276Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/47/ef/4cb333825d10317a36a1154341ba37e6e9c087bac99c1990ef07ffdb376f/dictdiffer-0.9.0-py2.py3-none-any.whl", hash = "sha256:442bfc693cfcadaf46674575d2eba1c53b42f5e404218ca2c2ff549f2df56595", size = 16754, upload-time = "2021-07-22T13:24:26.783Z" }, -] - [[package]] name = "dnspython" version = "2.8.0" @@ -846,7 +837,6 @@ dependencies = [ [package.optional-dependencies] dev = [ { name = "av" }, - { name = "dictdiffer" }, { name = "matplotlib" }, { name = "opencv-python-headless" }, { name = "parameterized" }, @@ -887,7 +877,6 @@ requires-dist = [ { name = "crcmod-plus" }, { name = "cython" }, { name = "dearpygui", marker = "(platform_machine != 'aarch64' and extra == 'tools') or (sys_platform != 'linux' and extra == 'tools')", specifier = ">=2.1.0" }, - { name = "dictdiffer", marker = "extra == 'dev'" }, { name = "hypothesis", marker = "extra == 'testing'", specifier = "==6.47.*" }, { name = "inputs" }, { name = "jeepney" }, From 07163f793b41b039f1b0cdb51a2f7d680b65f42e Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Fri, 20 Feb 2026 14:48:27 -0800 Subject: [PATCH 114/311] pytest timeout doesn't even work (#37281) --- pyproject.toml | 2 -- selfdrive/locationd/test/test_lagd.py | 1 - uv.lock | 14 -------------- 3 files changed, 17 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ac10630a9c..92d82e8330 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -86,7 +86,6 @@ testing = [ "pytest-subtests", # https://github.com/pytest-dev/pytest-xdist/pull/1229 "pytest-xdist @ git+https://github.com/sshane/pytest-xdist@2b4372bd62699fb412c4fe2f95bf9f01bd2018da", - "pytest-timeout", "pytest-asyncio", "pytest-mock", "ruff", @@ -126,7 +125,6 @@ cpp_files = "test_*" cpp_harness = "selfdrive/test/cpp_harness.py" python_files = "test_*.py" asyncio_default_fixture_loop_scope = "function" -#timeout = "30" # you get this long by default markers = [ "slow: tests that take awhile to run and can be skipped with -m 'not slow'", "tici: tests that are only meant to run on the C3/C3X", diff --git a/selfdrive/locationd/test/test_lagd.py b/selfdrive/locationd/test/test_lagd.py index a3dfce9c29..e9b5aff6d4 100644 --- a/selfdrive/locationd/test/test_lagd.py +++ b/selfdrive/locationd/test/test_lagd.py @@ -120,7 +120,6 @@ class TestLagd: assert msg.liveDelay.calPerc == 100 @pytest.mark.skipif(PC, reason="only on device") - @pytest.mark.timeout(60) def test_estimator_performance(self): mocked_CP = car.CarParams(steerActuatorDelay=0.8) estimator = LateralLagEstimator(mocked_CP, DT) diff --git a/uv.lock b/uv.lock index 4f1c957f3c..86d324ec4c 100644 --- a/uv.lock +++ b/uv.lock @@ -855,7 +855,6 @@ testing = [ { name = "pytest-cpp" }, { name = "pytest-mock" }, { name = "pytest-subtests" }, - { name = "pytest-timeout" }, { name = "pytest-xdist" }, { name = "ruff" }, { name = "ty" }, @@ -904,7 +903,6 @@ requires-dist = [ { name = "pytest-cpp", marker = "extra == 'testing'" }, { name = "pytest-mock", marker = "extra == 'testing'" }, { name = "pytest-subtests", marker = "extra == 'testing'" }, - { name = "pytest-timeout", marker = "extra == 'testing'" }, { name = "pytest-xdist", marker = "extra == 'testing'", git = "https://github.com/sshane/pytest-xdist?rev=2b4372bd62699fb412c4fe2f95bf9f01bd2018da" }, { name = "pyzmq" }, { name = "qrcode" }, @@ -1302,18 +1300,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/23/64/bba465299b37448b4c1b84c7a04178399ac22d47b3dc5db1874fe55a2bd3/pytest_subtests-0.15.0-py3-none-any.whl", hash = "sha256:da2d0ce348e1f8d831d5a40d81e3aeac439fec50bd5251cbb7791402696a9493", size = 9185, upload-time = "2025-10-20T16:26:17.239Z" }, ] -[[package]] -name = "pytest-timeout" -version = "2.4.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ac/82/4c9ecabab13363e72d880f2fb504c5f750433b2b6f16e99f4ec21ada284c/pytest_timeout-2.4.0.tar.gz", hash = "sha256:7e68e90b01f9eff71332b25001f85c75495fc4e3a836701876183c4bcfd0540a", size = 17973, upload-time = "2025-05-05T19:44:34.99Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2", size = 14382, upload-time = "2025-05-05T19:44:33.502Z" }, -] - [[package]] name = "pytest-xdist" version = "3.7.1.dev24+g2b4372bd6" From b28ff40d4ded866b59a14107ff972db41d4bed2a Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Fri, 20 Feb 2026 14:59:36 -0800 Subject: [PATCH 115/311] insource parameterized (#37280) * insource parameterized * lil more * fix --- cereal/messaging/tests/test_messaging.py | 2 +- cereal/messaging/tests/test_services.py | 2 +- common/parameterized.py | 47 +++++++++++++++++++ pyproject.toml | 1 - selfdrive/car/tests/test_car_interfaces.py | 2 +- selfdrive/car/tests/test_cruise_speed.py | 2 +- selfdrive/car/tests/test_models.py | 2 +- .../controls/tests/test_following_distance.py | 2 +- selfdrive/controls/tests/test_latcontrol.py | 2 +- .../tests/test_latcontrol_torque_buffer.py | 2 +- .../test_longitudinal.py | 2 +- selfdrive/test/process_replay/test_fuzzy.py | 2 +- selfdrive/test/process_replay/test_regen.py | 2 +- selfdrive/ui/tests/test_translations.py | 2 +- system/loggerd/tests/test_encoder.py | 2 +- system/webrtc/tests/test_webrtcd.py | 2 +- tools/lib/tests/test_logreader.py | 2 +- uv.lock | 11 ----- 18 files changed, 62 insertions(+), 27 deletions(-) create mode 100644 common/parameterized.py diff --git a/cereal/messaging/tests/test_messaging.py b/cereal/messaging/tests/test_messaging.py index 583eb8b0d8..afdab8a51f 100644 --- a/cereal/messaging/tests/test_messaging.py +++ b/cereal/messaging/tests/test_messaging.py @@ -5,7 +5,7 @@ import numbers import random import threading import time -from parameterized import parameterized +from openpilot.common.parameterized import parameterized import pytest from cereal import log, car diff --git a/cereal/messaging/tests/test_services.py b/cereal/messaging/tests/test_services.py index 8bfd2ea978..3320723fec 100644 --- a/cereal/messaging/tests/test_services.py +++ b/cereal/messaging/tests/test_services.py @@ -1,7 +1,7 @@ import os import tempfile from typing import Dict -from parameterized import parameterized +from openpilot.common.parameterized import parameterized import cereal.services as services from cereal.services import SERVICE_LIST diff --git a/common/parameterized.py b/common/parameterized.py new file mode 100644 index 0000000000..7cd21bb9c5 --- /dev/null +++ b/common/parameterized.py @@ -0,0 +1,47 @@ +import sys +import pytest +import inspect + + +class parameterized: + @staticmethod + def expand(cases): + cases = list(cases) + + if not cases: + return lambda func: pytest.mark.skip("no parameterized cases")(func) + + def decorator(func): + params = [p for p in inspect.signature(func).parameters if p != 'self'] + normalized = [c if isinstance(c, tuple) else (c,) for c in cases] + # Infer arg count from first case so extra params (e.g. from @given) are left untouched + expand_params = params[: len(normalized[0])] + if len(expand_params) == 1: + return pytest.mark.parametrize(expand_params[0], [c[0] for c in normalized])(func) + return pytest.mark.parametrize(', '.join(expand_params), normalized)(func) + + return decorator + + +def parameterized_class(attrs, input_list=None): + if isinstance(attrs, list) and (not attrs or isinstance(attrs[0], dict)): + params_list = attrs + else: + assert input_list is not None + attr_names = (attrs,) if isinstance(attrs, str) else tuple(attrs) + params_list = [dict(zip(attr_names, v if isinstance(v, (tuple, list)) else (v,), strict=False)) for v in input_list] + + def decorator(cls): + globs = sys._getframe(1).f_globals + for i, params in enumerate(params_list): + name = f"{cls.__name__}_{i}" + new_cls = type(name, (cls,), dict(params)) + new_cls.__module__ = cls.__module__ + new_cls.__test__ = True # override inherited False so pytest collects this subclass + globs[name] = new_cls + # Don't collect the un-parametrised base, but return it so outer decorators + # (e.g. @pytest.mark.skip) land on it and propagate to subclasses via MRO. + cls.__test__ = False + return cls + + return decorator diff --git a/pyproject.toml b/pyproject.toml index 92d82e8330..12134ef1d9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -97,7 +97,6 @@ dev = [ "av", "matplotlib", "opencv-python-headless", - "parameterized >=0.8, <0.9", ] tools = [ diff --git a/selfdrive/car/tests/test_car_interfaces.py b/selfdrive/car/tests/test_car_interfaces.py index 24d2faa0db..1bc59326a2 100644 --- a/selfdrive/car/tests/test_car_interfaces.py +++ b/selfdrive/car/tests/test_car_interfaces.py @@ -1,7 +1,7 @@ import os import hypothesis.strategies as st from hypothesis import Phase, given, settings -from parameterized import parameterized +from openpilot.common.parameterized import parameterized from cereal import car from opendbc.car import DT_CTRL diff --git a/selfdrive/car/tests/test_cruise_speed.py b/selfdrive/car/tests/test_cruise_speed.py index aa70e49f5d..05fef93b4e 100644 --- a/selfdrive/car/tests/test_cruise_speed.py +++ b/selfdrive/car/tests/test_cruise_speed.py @@ -2,7 +2,7 @@ import pytest import itertools import numpy as np -from parameterized import parameterized_class +from openpilot.common.parameterized import parameterized_class from cereal import log from openpilot.selfdrive.car.cruise import VCruiseHelper, V_CRUISE_MIN, V_CRUISE_MAX, V_CRUISE_INITIAL, IMPERIAL_INCREMENT from cereal import car diff --git a/selfdrive/car/tests/test_models.py b/selfdrive/car/tests/test_models.py index 94f5b33231..a7f3d68c14 100644 --- a/selfdrive/car/tests/test_models.py +++ b/selfdrive/car/tests/test_models.py @@ -6,7 +6,7 @@ import unittest # noqa: TID251 from collections import defaultdict, Counter import hypothesis.strategies as st from hypothesis import Phase, given, settings -from parameterized import parameterized_class +from openpilot.common.parameterized import parameterized_class from opendbc.car import DT_CTRL, gen_empty_fingerprint, structs from opendbc.car.can_definitions import CanData diff --git a/selfdrive/controls/tests/test_following_distance.py b/selfdrive/controls/tests/test_following_distance.py index ad1ff1a189..1eb88d7206 100644 --- a/selfdrive/controls/tests/test_following_distance.py +++ b/selfdrive/controls/tests/test_following_distance.py @@ -1,6 +1,6 @@ import pytest import itertools -from parameterized import parameterized_class +from openpilot.common.parameterized import parameterized_class from cereal import log diff --git a/selfdrive/controls/tests/test_latcontrol.py b/selfdrive/controls/tests/test_latcontrol.py index 354c7f00ad..5c3381edce 100644 --- a/selfdrive/controls/tests/test_latcontrol.py +++ b/selfdrive/controls/tests/test_latcontrol.py @@ -1,4 +1,4 @@ -from parameterized import parameterized +from openpilot.common.parameterized import parameterized from cereal import car, log from opendbc.car.car_helpers import interfaces diff --git a/selfdrive/controls/tests/test_latcontrol_torque_buffer.py b/selfdrive/controls/tests/test_latcontrol_torque_buffer.py index 76d0c28423..ab1d2c7b36 100644 --- a/selfdrive/controls/tests/test_latcontrol_torque_buffer.py +++ b/selfdrive/controls/tests/test_latcontrol_torque_buffer.py @@ -1,4 +1,4 @@ -from parameterized import parameterized +from openpilot.common.parameterized import parameterized from cereal import car, log from opendbc.car.car_helpers import interfaces diff --git a/selfdrive/test/longitudinal_maneuvers/test_longitudinal.py b/selfdrive/test/longitudinal_maneuvers/test_longitudinal.py index ab1800b4fb..90bc46b187 100644 --- a/selfdrive/test/longitudinal_maneuvers/test_longitudinal.py +++ b/selfdrive/test/longitudinal_maneuvers/test_longitudinal.py @@ -1,5 +1,5 @@ import itertools -from parameterized import parameterized_class +from openpilot.common.parameterized import parameterized_class from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import STOP_DISTANCE from openpilot.selfdrive.test.longitudinal_maneuvers.maneuver import Maneuver diff --git a/selfdrive/test/process_replay/test_fuzzy.py b/selfdrive/test/process_replay/test_fuzzy.py index 723112163e..6989f8957f 100644 --- a/selfdrive/test/process_replay/test_fuzzy.py +++ b/selfdrive/test/process_replay/test_fuzzy.py @@ -2,7 +2,7 @@ import copy import os from hypothesis import given, HealthCheck, Phase, settings import hypothesis.strategies as st -from parameterized import parameterized +from openpilot.common.parameterized import parameterized from cereal import log from opendbc.car.toyota.values import CAR as TOYOTA diff --git a/selfdrive/test/process_replay/test_regen.py b/selfdrive/test/process_replay/test_regen.py index 5f26daf786..f4942e486c 100644 --- a/selfdrive/test/process_replay/test_regen.py +++ b/selfdrive/test/process_replay/test_regen.py @@ -1,4 +1,4 @@ -from parameterized import parameterized +from openpilot.common.parameterized import parameterized from openpilot.selfdrive.test.process_replay.regen import regen_segment from openpilot.selfdrive.test.process_replay.process_replay import check_openpilot_enabled diff --git a/selfdrive/ui/tests/test_translations.py b/selfdrive/ui/tests/test_translations.py index 3177814f9f..599c99013c 100644 --- a/selfdrive/ui/tests/test_translations.py +++ b/selfdrive/ui/tests/test_translations.py @@ -5,7 +5,7 @@ import re import xml.etree.ElementTree as ET import string import requests -from parameterized import parameterized_class +from openpilot.common.parameterized import parameterized_class from openpilot.system.ui.lib.multilang import TRANSLATIONS_DIR, LANGUAGES_FILE with open(str(LANGUAGES_FILE)) as f: diff --git a/system/loggerd/tests/test_encoder.py b/system/loggerd/tests/test_encoder.py index e4dabd3df9..a9de0690aa 100644 --- a/system/loggerd/tests/test_encoder.py +++ b/system/loggerd/tests/test_encoder.py @@ -7,7 +7,7 @@ import subprocess import time from pathlib import Path -from parameterized import parameterized +from openpilot.common.parameterized import parameterized from tqdm import trange from openpilot.common.params import Params diff --git a/system/webrtc/tests/test_webrtcd.py b/system/webrtc/tests/test_webrtcd.py index 4fa6d8953f..e5f6ceaa37 100644 --- a/system/webrtc/tests/test_webrtcd.py +++ b/system/webrtc/tests/test_webrtcd.py @@ -10,7 +10,7 @@ from openpilot.system.webrtc.webrtcd import get_stream import aiortc from teleoprtc import WebRTCOfferBuilder -from parameterized import parameterized_class +from openpilot.common.parameterized import parameterized_class @parameterized_class(("in_services", "out_services"), [ diff --git a/tools/lib/tests/test_logreader.py b/tools/lib/tests/test_logreader.py index 0151940c44..123f142383 100644 --- a/tools/lib/tests/test_logreader.py +++ b/tools/lib/tests/test_logreader.py @@ -7,7 +7,7 @@ import os import pytest import requests -from parameterized import parameterized +from openpilot.common.parameterized import parameterized from cereal import log as capnp_log from openpilot.tools.lib.logreader import LogsUnavailable, LogIterable, LogReader, parse_indirect, ReadMode diff --git a/uv.lock b/uv.lock index 86d324ec4c..e14591deee 100644 --- a/uv.lock +++ b/uv.lock @@ -839,7 +839,6 @@ dev = [ { name = "av" }, { name = "matplotlib" }, { name = "opencv-python-headless" }, - { name = "parameterized" }, ] docs = [ { name = "jinja2" }, @@ -889,7 +888,6 @@ requires-dist = [ { name = "numpy", specifier = ">=2.0" }, { name = "onnx", specifier = ">=1.14.0" }, { name = "opencv-python-headless", marker = "extra == 'dev'" }, - { name = "parameterized", marker = "extra == 'dev'", specifier = ">=0.8,<0.9" }, { name = "pre-commit-hooks", marker = "extra == 'testing'" }, { name = "psutil" }, { name = "pyaudio" }, @@ -972,15 +970,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/11/5d/3744c6550dddf933785a37cdd4a9921fe13284e6d115b5a2637fe390f158/panda3d_simplepbr-0.13.1-py3-none-any.whl", hash = "sha256:cda41cb57cff035b851646956cfbdcc408bee42511dabd4f2d7bd4fbf48c57a9", size = 2457097, upload-time = "2025-03-30T16:57:39.729Z" }, ] -[[package]] -name = "parameterized" -version = "0.8.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c6/23/2288f308d238b4f261c039cafcd650435d624de97c6ffc903f06ea8af50f/parameterized-0.8.1.tar.gz", hash = "sha256:41bbff37d6186430f77f900d777e5bb6a24928a1c46fb1de692f8b52b8833b5c", size = 23936, upload-time = "2021-01-09T20:35:18.235Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/31/13/fe468c8c7400a8eca204e6e160a29bf7dcd45a76e20f1c030f3eaa690d93/parameterized-0.8.1-py2.py3-none-any.whl", hash = "sha256:9cbb0b69a03e8695d68b3399a8a5825200976536fe1cb79db60ed6a4c8c9efe9", size = 26354, upload-time = "2021-01-09T20:35:16.307Z" }, -] - [[package]] name = "pathspec" version = "1.0.4" From 5d54743d8ba5f377f4260d9573d532ecfcfaae93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Harald=20Sch=C3=A4fer?= Date: Fri, 20 Feb 2026 15:19:39 -0800 Subject: [PATCH 116/311] safer model pkl chunking (#37283) * safer chunking * rm unchunked --- common/file_chunker.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/common/file_chunker.py b/common/file_chunker.py index f03d04a382..139a7dcacc 100644 --- a/common/file_chunker.py +++ b/common/file_chunker.py @@ -1,8 +1,9 @@ +import glob import math import os from pathlib import Path -CHUNK_SIZE = 49 * 1024 * 1024 # 49MB, under GitHub's 50MB limit +CHUNK_SIZE = 19 * 1024 * 1024 # 49MB, under GitHub's 50MB limit def get_chunk_name(name, idx, num_chunks): return f"{name}.chunk{idx+1:02d}of{num_chunks:02d}" @@ -12,6 +13,8 @@ def get_chunk_paths(path, file_size): return [get_chunk_name(path, i, num_chunks) for i in range(num_chunks)] def chunk_file(path, num_chunks): + for old in glob.glob(f"{path}.chunk*"): + os.remove(old) with open(path, 'rb') as f: data = f.read() actual_num_chunks = max(1, math.ceil(len(data) / CHUNK_SIZE)) @@ -19,13 +22,15 @@ def chunk_file(path, num_chunks): for i in range(num_chunks): with open(get_chunk_name(path, i, num_chunks), 'wb') as f: f.write(data[i * CHUNK_SIZE:(i + 1) * CHUNK_SIZE]) + os.remove(path) def read_file_chunked(path): - for num_chunks in range(1, 100): - if os.path.isfile(get_chunk_name(path, 0, num_chunks)): - files = [get_chunk_name(path, i, num_chunks) for i in range(num_chunks)] - return b''.join(Path(f).read_bytes() for f in files) + chunks = sorted(glob.glob(f"{path}.chunk*")) + if chunks: + expected = [get_chunk_name(path, i, len(chunks)) for i in range(len(chunks))] + assert chunks == expected, f"Chunk mismatch: {chunks} != {expected}" + return b''.join(Path(f).read_bytes() for f in chunks) if os.path.isfile(path): return Path(path).read_bytes() raise FileNotFoundError(path) From 806655b052372afe830f6b0d2ba6dc73a222fcaf Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Fri, 20 Feb 2026 15:48:09 -0800 Subject: [PATCH 117/311] CI: replace docker with `op setup` (#37282) --- .github/workflows/auto-cache/action.yaml | 5 +- .github/workflows/badges.yaml | 6 +- .../workflows/compile-openpilot/action.yaml | 10 +-- .github/workflows/repo-maintenance.yaml | 17 +--- .../workflows/setup-with-retry/action.yaml | 4 - .github/workflows/setup/action.yaml | 20 ++--- .github/workflows/tests.yaml | 78 +++++++------------ system/webrtc/tests/test_webrtcd.py | 65 ---------------- 8 files changed, 49 insertions(+), 156 deletions(-) delete mode 100644 system/webrtc/tests/test_webrtcd.py diff --git a/.github/workflows/auto-cache/action.yaml b/.github/workflows/auto-cache/action.yaml index 377b1eedcd..42c8f8fd2d 100644 --- a/.github/workflows/auto-cache/action.yaml +++ b/.github/workflows/auto-cache/action.yaml @@ -52,7 +52,4 @@ runs: # make the directory manually in case we didn't get a hit, so it doesn't fail on future steps - id: scons-cache-setup shell: bash - run: | - mkdir -p ${{ inputs.path }} - sudo chmod -R 777 ${{ inputs.path }} - sudo chown -R $USER ${{ inputs.path }} + run: mkdir -p ${{ inputs.path }} diff --git a/.github/workflows/badges.yaml b/.github/workflows/badges.yaml index 3f9c9c1c59..23f2c135d5 100644 --- a/.github/workflows/badges.yaml +++ b/.github/workflows/badges.yaml @@ -5,9 +5,7 @@ on: workflow_dispatch: env: - BASE_IMAGE: openpilot-base - DOCKER_REGISTRY: ghcr.io/commaai - RUN: docker run --shm-size 2G -v $PWD:/tmp/openpilot -w /tmp/openpilot -e PYTHONPATH=/tmp/openpilot -e NUM_JOBS -e JOB_ID -e GITHUB_ACTION -e GITHUB_REF -e GITHUB_HEAD_REF -e GITHUB_SHA -e GITHUB_REPOSITORY -e GITHUB_RUN_ID -v $GITHUB_WORKSPACE/.ci_cache/scons_cache:/tmp/scons_cache -v $GITHUB_WORKSPACE/.ci_cache/comma_download_cache:/tmp/comma_download_cache -v $GITHUB_WORKSPACE/.ci_cache/openpilot_cache:/tmp/openpilot_cache $DOCKER_REGISTRY/$BASE_IMAGE:latest /bin/bash -c + PYTHONPATH: ${{ github.workspace }} jobs: badges: @@ -23,7 +21,7 @@ jobs: - uses: ./.github/workflows/setup-with-retry - name: Push badges run: | - ${{ env.RUN }} "python3 selfdrive/ui/translations/create_badges.py" + python3 selfdrive/ui/translations/create_badges.py rm .gitattributes diff --git a/.github/workflows/compile-openpilot/action.yaml b/.github/workflows/compile-openpilot/action.yaml index 4015746c0e..627b4845aa 100644 --- a/.github/workflows/compile-openpilot/action.yaml +++ b/.github/workflows/compile-openpilot/action.yaml @@ -6,16 +6,16 @@ runs: - shell: bash name: Build openpilot with all flags run: | - ${{ env.RUN }} "scons -j$(nproc)" - ${{ env.RUN }} "release/check-dirty.sh" + scons -j$(nproc) + release/check-dirty.sh - shell: bash name: Cleanup scons cache and rebuild run: | - ${{ env.RUN }} "rm -rf /tmp/scons_cache/* && \ - scons -j$(nproc) --cache-populate" + rm -rf /tmp/scons_cache/* + scons -j$(nproc) --cache-populate - name: Save scons cache uses: actions/cache/save@v4 if: github.ref == 'refs/heads/master' with: - path: .ci_cache/scons_cache + path: /tmp/scons_cache key: scons-${{ runner.arch }}-${{ env.CACHE_COMMIT_DATE }}-${{ github.sha }} diff --git a/.github/workflows/repo-maintenance.yaml b/.github/workflows/repo-maintenance.yaml index 55d1c2052c..10b2e8ab2f 100644 --- a/.github/workflows/repo-maintenance.yaml +++ b/.github/workflows/repo-maintenance.yaml @@ -6,9 +6,7 @@ on: workflow_dispatch: env: - BASE_IMAGE: openpilot-base - BUILD: selfdrive/test/docker_build.sh base - RUN: docker run --shm-size 2G -v $PWD:/tmp/openpilot -w /tmp/openpilot -e CI=1 -e PYTHONWARNINGS=error -e PYTHONPATH=/tmp/openpilot -e NUM_JOBS -e JOB_ID -e GITHUB_ACTION -e GITHUB_REF -e GITHUB_HEAD_REF -e GITHUB_SHA -e GITHUB_REPOSITORY -e GITHUB_RUN_ID -v $GITHUB_WORKSPACE/.ci_cache/scons_cache:/tmp/scons_cache -v $GITHUB_WORKSPACE/.ci_cache/comma_download_cache:/tmp/comma_download_cache -v $GITHUB_WORKSPACE/.ci_cache/openpilot_cache:/tmp/openpilot_cache $BASE_IMAGE /bin/bash -c + PYTHONPATH: ${{ github.workspace }} jobs: update_translations: @@ -20,8 +18,7 @@ jobs: submodules: true - uses: ./.github/workflows/setup-with-retry - name: Update translations - run: | - ${{ env.RUN }} "python3 selfdrive/ui/update_translations.py --vanish" + run: python3 selfdrive/ui/update_translations.py --vanish - name: Create Pull Request uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 with: @@ -37,18 +34,14 @@ jobs: package_updates: name: package_updates runs-on: ubuntu-latest - container: - image: ghcr.io/commaai/openpilot-base:latest if: github.repository == 'commaai/openpilot' steps: - uses: actions/checkout@v6 with: submodules: true + - uses: ./.github/workflows/setup-with-retry - name: uv lock - run: | - python3 -m ensurepip --upgrade - pip3 install uv - uv lock --upgrade + run: uv lock --upgrade - name: uv pip tree id: pip_tree run: | @@ -57,12 +50,10 @@ jobs: echo 'EOF' >> $GITHUB_OUTPUT - name: bump submodules run: | - git config --global --add safe.directory '*' git submodule update --remote git add . - name: update car docs run: | - export PYTHONPATH="$PWD" scons -j$(nproc) --minimal opendbc_repo python selfdrive/car/docs.py git add docs/CARS.md diff --git a/.github/workflows/setup-with-retry/action.yaml b/.github/workflows/setup-with-retry/action.yaml index 98a3913600..923cc3aadb 100644 --- a/.github/workflows/setup-with-retry/action.yaml +++ b/.github/workflows/setup-with-retry/action.yaml @@ -1,10 +1,6 @@ name: 'openpilot env setup, with retry on failure' inputs: - docker_hub_pat: - description: 'Auth token for Docker Hub, required for BuildJet jobs' - required: false - default: '' sleep_time: description: 'Time to sleep between retries' required: false diff --git a/.github/workflows/setup/action.yaml b/.github/workflows/setup/action.yaml index 818060c3b0..56f0096450 100644 --- a/.github/workflows/setup/action.yaml +++ b/.github/workflows/setup/action.yaml @@ -34,23 +34,19 @@ runs: - id: date shell: bash run: echo "CACHE_COMMIT_DATE=$(git log -1 --pretty='format:%cd' --date=format:'%Y-%m-%d-%H:%M')" >> $GITHUB_ENV - - shell: bash - run: echo "$CACHE_COMMIT_DATE" - id: scons-cache uses: ./.github/workflows/auto-cache with: - path: .ci_cache/scons_cache + path: /tmp/scons_cache key: scons-${{ runner.arch }}-${{ env.CACHE_COMMIT_DATE }}-${{ github.sha }} restore-keys: | scons-${{ runner.arch }}-${{ env.CACHE_COMMIT_DATE }} scons-${{ runner.arch }} - # as suggested here: https://github.com/moby/moby/issues/32816#issuecomment-910030001 - - id: normalize-file-permissions - shell: bash - name: Normalize file permissions to ensure a consistent docker build cache - run: | - find . -type f -executable -not -perm 755 -exec chmod 755 {} \; - find . -type f -not -executable -not -perm 644 -exec chmod 644 {} \; - # build our docker image - shell: bash - run: eval ${{ env.BUILD }} + name: Run setup + run: ./tools/op.sh setup + - shell: bash + name: Setup cache dirs + run: | + mkdir -p /tmp/comma_download_cache + echo "$GITHUB_WORKSPACE/.venv/bin" >> $GITHUB_PATH diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index d892507319..e1765eb562 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -18,13 +18,8 @@ concurrency: cancel-in-progress: true env: - PYTHONWARNINGS: error - BASE_IMAGE: openpilot-base - DOCKER_LOGIN: docker login ghcr.io -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }} - BUILD: selfdrive/test/docker_build.sh base - - RUN: docker run --shm-size 2G -v $PWD:/tmp/openpilot -w /tmp/openpilot -e CI=1 -e PYTHONWARNINGS=error -e PYTHONPATH=/tmp/openpilot -e NUM_JOBS -e JOB_ID -e GITHUB_ACTION -e GITHUB_REF -e GITHUB_HEAD_REF -e GITHUB_SHA -e GITHUB_REPOSITORY -e GITHUB_RUN_ID -v $GITHUB_WORKSPACE/.ci_cache/scons_cache:/tmp/scons_cache -v $GITHUB_WORKSPACE/.ci_cache/comma_download_cache:/tmp/comma_download_cache -v $GITHUB_WORKSPACE/.ci_cache/openpilot_cache:/tmp/openpilot_cache $BASE_IMAGE /bin/bash -c - + CI: 1 + PYTHONPATH: ${{ github.workspace }} PYTEST: pytest --continue-on-collection-errors --durations=0 -n logical jobs: @@ -38,6 +33,7 @@ jobs: || fromJSON('["ubuntu-24.04"]') }} env: STRIPPED_DIR: /tmp/releasepilot + PYTHONPATH: /tmp/releasepilot steps: - uses: actions/checkout@v6 with: @@ -53,15 +49,13 @@ jobs: run: TARGET_DIR=$STRIPPED_DIR release/build_stripped.sh - uses: ./.github/workflows/setup-with-retry - name: Build openpilot and run checks - timeout-minutes: ${{ ((steps.restore-scons-cache.outputs.cache-hit == 'true') && 10 || 30) }} # allow more time when we missed the scons cache - run: | - cd $STRIPPED_DIR - ${{ env.RUN }} "python3 system/manager/build.py" + timeout-minutes: 30 + working-directory: ${{ env.STRIPPED_DIR }} + run: python3 system/manager/build.py - name: Run tests timeout-minutes: 1 - run: | - cd $STRIPPED_DIR - ${{ env.RUN }} "release/check-dirty.sh" + working-directory: ${{ env.STRIPPED_DIR }} + run: release/check-dirty.sh - name: Check submodules if: github.repository == 'commaai/openpilot' timeout-minutes: 3 @@ -78,11 +72,6 @@ jobs: - uses: actions/checkout@v6 with: submodules: true - - name: Setup docker push - if: github.ref == 'refs/heads/master' && github.event_name != 'pull_request' && github.repository == 'commaai/openpilot' - run: | - echo "PUSH_IMAGE=true" >> "$GITHUB_ENV" - $DOCKER_LOGIN - uses: ./.github/workflows/setup-with-retry - uses: ./.github/workflows/compile-openpilot timeout-minutes: 30 @@ -106,7 +95,6 @@ jobs: - name: Install dependencies run: ./tools/mac_setup.sh env: - PYTHONWARNINGS: default # package install has DeprecationWarnings HOMEBREW_DISPLAY_INSTALL_TIMES: 1 - run: git lfs pull - name: Getting scons cache @@ -128,8 +116,6 @@ jobs: (github.event.pull_request.head.repo.full_name == 'commaai/openpilot')) && fromJSON('["namespace-profile-amd64-8x16", "namespace-experiments:docker.builds.local-cache=separate"]') || fromJSON('["ubuntu-24.04"]') }} - env: - PYTHONWARNINGS: default steps: - uses: actions/checkout@v6 with: @@ -137,7 +123,7 @@ jobs: - uses: ./.github/workflows/setup-with-retry - name: Static analysis timeout-minutes: 1 - run: ${{ env.RUN }} "scripts/lint/lint.sh" + run: scripts/lint/lint.sh unit_tests: name: unit tests @@ -154,15 +140,14 @@ jobs: - uses: ./.github/workflows/setup-with-retry id: setup-step - name: Build openpilot - run: ${{ env.RUN }} "scons -j$(nproc)" + run: scons -j$(nproc) - name: Run unit tests timeout-minutes: ${{ contains(runner.name, 'nsc') && ((steps.setup-step.outputs.duration < 18) && 1 || 2) || 20 }} run: | - ${{ env.RUN }} "source selfdrive/test/setup_xvfb.sh && \ - # Pre-compile Python bytecode so each pytest worker doesn't need to - $PYTEST --collect-only -m 'not slow' -qq && \ - MAX_EXAMPLES=1 $PYTEST -m 'not slow' && \ - chmod -R 777 /tmp/comma_download_cache" + source selfdrive/test/setup_xvfb.sh + # Pre-compile Python bytecode so each pytest worker doesn't need to + $PYTEST --collect-only -m 'not slow' -qq + MAX_EXAMPLES=1 $PYTEST -m 'not slow' process_replay: name: process replay @@ -182,17 +167,14 @@ jobs: id: dependency-cache uses: actions/cache@v5 with: - path: .ci_cache/comma_download_cache + path: /tmp/comma_download_cache key: proc-replay-${{ hashFiles('selfdrive/test/process_replay/test_processes.py') }} - name: Build openpilot - run: | - ${{ env.RUN }} "scons -j$(nproc)" + run: scons -j$(nproc) - name: Run replay timeout-minutes: ${{ contains(runner.name, 'nsc') && (steps.dependency-cache.outputs.cache-hit == 'true') && ((steps.setup-step.outputs.duration < 18) && 1 || 2) || 20 }} continue-on-error: ${{ github.ref == 'refs/heads/master' }} - run: | - ${{ env.RUN }} "selfdrive/test/process_replay/test_processes.py -j$(nproc) && \ - chmod -R 777 /tmp/comma_download_cache" + run: selfdrive/test/process_replay/test_processes.py -j$(nproc) - name: Print diff id: print-diff if: always() @@ -226,9 +208,9 @@ jobs: - name: Run regen if: false timeout-minutes: 4 - run: | - ${{ env.RUN }} "ONNXCPU=1 $PYTEST selfdrive/test/process_replay/test_regen.py && \ - chmod -R 777 /tmp/comma_download_cache" + env: + ONNXCPU: 1 + run: $PYTEST selfdrive/test/process_replay/test_regen.py simulator_driving: name: simulator driving @@ -246,14 +228,13 @@ jobs: - uses: ./.github/workflows/setup-with-retry id: setup-step - name: Build openpilot - run: | - ${{ env.RUN }} "scons -j$(nproc)" + run: scons -j$(nproc) - name: Driving test timeout-minutes: ${{ (steps.setup-step.outputs.duration < 18) && 1 || 2 }} run: | - ${{ env.RUN }} "source selfdrive/test/setup_xvfb.sh && \ - source selfdrive/test/setup_vsound.sh && \ - CI=1 pytest -s tools/sim/tests/test_metadrive_bridge.py" + source selfdrive/test/setup_xvfb.sh + source selfdrive/test/setup_vsound.sh + pytest -s tools/sim/tests/test_metadrive_bridge.py create_ui_report: name: Create UI Report @@ -269,13 +250,12 @@ jobs: submodules: true - uses: ./.github/workflows/setup-with-retry - name: Build openpilot - run: ${{ env.RUN }} "scons -j$(nproc)" + run: scons -j$(nproc) - name: Create UI Report - run: > - ${{ env.RUN }} "PYTHONWARNINGS=ignore && - source selfdrive/test/setup_xvfb.sh && - python3 selfdrive/ui/tests/diff/replay.py && - python3 selfdrive/ui/tests/diff/replay.py --big" + run: | + source selfdrive/test/setup_xvfb.sh + python3 selfdrive/ui/tests/diff/replay.py + python3 selfdrive/ui/tests/diff/replay.py --big - name: Upload UI Report uses: actions/upload-artifact@v6 with: diff --git a/system/webrtc/tests/test_webrtcd.py b/system/webrtc/tests/test_webrtcd.py deleted file mode 100644 index e5f6ceaa37..0000000000 --- a/system/webrtc/tests/test_webrtcd.py +++ /dev/null @@ -1,65 +0,0 @@ -import pytest -import asyncio -import json -# for aiortc and its dependencies -import warnings -warnings.filterwarnings("ignore", category=DeprecationWarning) -warnings.filterwarnings("ignore", category=RuntimeWarning) # TODO: remove this when google-crc32c publish a python3.12 wheel - -from openpilot.system.webrtc.webrtcd import get_stream - -import aiortc -from teleoprtc import WebRTCOfferBuilder -from openpilot.common.parameterized import parameterized_class - - -@parameterized_class(("in_services", "out_services"), [ - (["testJoystick"], ["carState"]), - ([], ["carState"]), - (["testJoystick"], []), - ([], []), -]) -@pytest.mark.asyncio -class TestWebrtcdProc: - async def assertCompletesWithTimeout(self, awaitable, timeout=1): - try: - async with asyncio.timeout(timeout): - await awaitable - except TimeoutError: - pytest.fail("Timeout while waiting for awaitable to complete") - - async def test_webrtcd(self, mocker): - mock_request = mocker.MagicMock() - async def connect(offer): - body = {'sdp': offer.sdp, 'cameras': offer.video, 'bridge_services_in': self.in_services, 'bridge_services_out': self.out_services} - mock_request.json.side_effect = mocker.AsyncMock(return_value=body) - response = await get_stream(mock_request) - response_json = json.loads(response.text) - return aiortc.RTCSessionDescription(**response_json) - - builder = WebRTCOfferBuilder(connect) - builder.offer_to_receive_video_stream("road") - builder.offer_to_receive_audio_stream() - if len(self.in_services) > 0 or len(self.out_services) > 0: - builder.add_messaging() - - stream = builder.stream() - - await self.assertCompletesWithTimeout(stream.start()) - await self.assertCompletesWithTimeout(stream.wait_for_connection()) - - assert stream.has_incoming_video_track("road") - assert stream.has_incoming_audio_track() - assert stream.has_messaging_channel() == (len(self.in_services) > 0 or len(self.out_services) > 0) - - video_track, audio_track = stream.get_incoming_video_track("road"), stream.get_incoming_audio_track() - await self.assertCompletesWithTimeout(video_track.recv()) - await self.assertCompletesWithTimeout(audio_track.recv()) - - await self.assertCompletesWithTimeout(stream.stop()) - - # cleanup, very implementation specific, test may break if it changes - assert mock_request.app["streams"].__setitem__.called, "Implementation changed, please update this test" - _, session = mock_request.app["streams"].__setitem__.call_args.args - await self.assertCompletesWithTimeout(session.post_run_cleanup()) - From 5fc6fe68f637189c84b53383f5c5d23ae12c63d1 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Fri, 20 Feb 2026 16:14:46 -0800 Subject: [PATCH 118/311] rm mapbox-earcut (#37284) --- pyproject.toml | 1 - uv.lock | 22 ---------------------- 2 files changed, 23 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 12134ef1d9..b17af33def 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,7 +67,6 @@ dependencies = [ # ui "raylib > 5.5.0.3", "qrcode", - "mapbox-earcut", "jeepney", ] diff --git a/uv.lock b/uv.lock index e14591deee..b3aa498e9a 100644 --- a/uv.lock +++ b/uv.lock @@ -552,26 +552,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/94/6d/344a164d32d65d503ffe9201cd74cf13a020099dc446554d1e50b07f167b/libusb1-3.3.1-py3-none-win_amd64.whl", hash = "sha256:6e21b772d80d6487fbb55d3d2141218536db302da82f1983754e96c72781c102", size = 141080, upload-time = "2025-03-24T05:36:46.594Z" }, ] -[[package]] -name = "mapbox-earcut" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/bc/7b/bbf6b00488662be5d2eb7a188222c264b6f713bac10dc4a77bf37a4cb4b6/mapbox_earcut-2.0.0.tar.gz", hash = "sha256:81eab6b86cf99551deb698b98e3f7502c57900e5c479df15e1bdaf1a57f0f9d6", size = 39934, upload-time = "2025-11-16T18:41:27.251Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8d/93/846804029d955c3c841d8efff77c2b0e8d9aab057d3a077dc8e3f88b5ea4/mapbox_earcut-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:db55ce18e698bc9d90914ee7d4f8c3e4d23827456ece7c5d7a1ec91e90c7122b", size = 55623, upload-time = "2025-11-16T18:40:32.113Z" }, - { url = "https://files.pythonhosted.org/packages/d3/f6/cc9ece104bc3876b350dba6fef7f34fb7b20ecc028d2cdbdbecb436b1ed1/mapbox_earcut-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:01dd6099d16123baf582a11b2bd1d59ce848498cf0cdca3812fd1f8b20ff33b7", size = 52028, upload-time = "2025-11-16T18:40:33.516Z" }, - { url = "https://files.pythonhosted.org/packages/88/6e/230da4aabcc56c99e9bddb4c43ce7d4ba3609c0caf2d316fb26535d7c60c/mapbox_earcut-2.0.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d5a098aae26a52282bc981a38e7bf6b889d2ea7442f2cd1903d2ba842f4ff07", size = 56351, upload-time = "2025-11-16T18:40:35.217Z" }, - { url = "https://files.pythonhosted.org/packages/1a/f7/5cdd3752526e91d91336c7263af7767b291d21e63c89d7190a60051f0f87/mapbox_earcut-2.0.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de35f241d0b9110ad9260f295acedd9d7cc0d7acfe30d36b1b3ee8419c2caba1", size = 59209, upload-time = "2025-11-16T18:40:36.634Z" }, - { url = "https://files.pythonhosted.org/packages/7b/a2/b7781416cb93b37b95d0444e03f87184de8815e57ff202ce4105fa921325/mapbox_earcut-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6cb63ab85e2e430c350f93e75c13f8b91cb8c8a045f3cd714c390b69a720368a", size = 152316, upload-time = "2025-11-16T18:40:38.147Z" }, - { url = "https://files.pythonhosted.org/packages/c1/74/396338e3d345e4e36fb23a0380921098b6a95ce7fb19c4777f4185a5974e/mapbox_earcut-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fb3c9f069fc3795306db87f8139f70c4f047532f897a3de05f54dc1faebc97f6", size = 157268, upload-time = "2025-11-16T18:40:39.753Z" }, - { url = "https://files.pythonhosted.org/packages/56/2c/66fd137ea86c508f6cd7247f7f6e2d1dabffc9f0e9ccf14c71406b197af1/mapbox_earcut-2.0.0-cp312-cp312-win32.whl", hash = "sha256:eb290e6676217707ed238dd55e07b0a8ca3ab928f6a27c4afefb2ff3af08d7cb", size = 51226, upload-time = "2025-11-16T18:40:41.018Z" }, - { url = "https://files.pythonhosted.org/packages/b8/84/7b78e37b0c2109243c0dad7d9ba9774b02fcee228bf61cf727a5aa1702e2/mapbox_earcut-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:5ef5b3319a43375272ad2cad9333ed16e569b5102e32a4241451358897e6f6ee", size = 56417, upload-time = "2025-11-16T18:40:42.173Z" }, - { url = "https://files.pythonhosted.org/packages/75/7f/cd7195aa27c1c8f2b9d38025a5a8663f32cd01c07b648a54b1308ab26c15/mapbox_earcut-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:a4a3706feb5cc8c782d8f68bb0110c8d551304043f680a87a54b0651a2c208c3", size = 50111, upload-time = "2025-11-16T18:40:43.334Z" }, -] - [[package]] name = "markdown" version = "3.10.2" @@ -807,7 +787,6 @@ dependencies = [ { name = "jeepney" }, { name = "json-rpc" }, { name = "libusb1" }, - { name = "mapbox-earcut" }, { name = "numpy" }, { name = "onnx" }, { name = "psutil" }, @@ -881,7 +860,6 @@ requires-dist = [ { name = "jinja2", marker = "extra == 'docs'" }, { name = "json-rpc" }, { name = "libusb1" }, - { name = "mapbox-earcut" }, { name = "matplotlib", marker = "extra == 'dev'" }, { name = "metadrive-simulator", marker = "platform_machine != 'aarch64' and extra == 'tools'", git = "https://github.com/commaai/metadrive.git?rev=minimal" }, { name = "mkdocs", marker = "extra == 'docs'" }, From f9f33c4dc4ab3fe366973cd833f415d7384a645b Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Fri, 20 Feb 2026 16:39:11 -0800 Subject: [PATCH 119/311] show venv size in package update job (#37286) * show venv size in package update job * lil more --- .github/workflows/repo-maintenance.yaml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/.github/workflows/repo-maintenance.yaml b/.github/workflows/repo-maintenance.yaml index 10b2e8ab2f..d2c2447d7a 100644 --- a/.github/workflows/repo-maintenance.yaml +++ b/.github/workflows/repo-maintenance.yaml @@ -48,6 +48,20 @@ jobs: echo 'PIP_TREE<> $GITHUB_OUTPUT uv pip tree >> $GITHUB_OUTPUT echo 'EOF' >> $GITHUB_OUTPUT + - name: venv size + id: venv_size + run: | + echo 'VENV_SIZE<> $GITHUB_OUTPUT + echo "Total: $(du -sh .venv | cut -f1)" >> $GITHUB_OUTPUT + echo "" >> $GITHUB_OUTPUT + echo "Top 10 by size:" >> $GITHUB_OUTPUT + du -sh .venv/lib/python*/site-packages/* 2>/dev/null \ + | grep -v '\.dist-info' \ + | grep -v '__pycache__' \ + | sort -rh \ + | head -10 \ + | while IFS=$'\t' read size path; do echo "$size ${path##*/}"; done >> $GITHUB_OUTPUT + echo 'EOF' >> $GITHUB_OUTPUT - name: bump submodules run: | git submodule update --remote @@ -71,6 +85,12 @@ jobs: Automatic PR from repo-maintenance -> package_updates ``` + $ du -sh .venv && du -sh .venv/lib/python*/site-packages/* | sort -rh | head -10 + ${{ steps.venv_size.outputs.VENV_SIZE }} + ``` + + ``` + $ uv pip tree ${{ steps.pip_tree.outputs.PIP_TREE }} ``` labels: bot From 09926bf5b5eaf9821a7c0fca3ac00800c2372119 Mon Sep 17 00:00:00 2001 From: Bruce Wayne Date: Fri, 20 Feb 2026 16:43:33 -0800 Subject: [PATCH 120/311] Revert "safer model pkl chunking (#37283)" This reverts commit 5d54743d8ba5f377f4260d9573d532ecfcfaae93. --- common/file_chunker.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/common/file_chunker.py b/common/file_chunker.py index 139a7dcacc..f03d04a382 100644 --- a/common/file_chunker.py +++ b/common/file_chunker.py @@ -1,9 +1,8 @@ -import glob import math import os from pathlib import Path -CHUNK_SIZE = 19 * 1024 * 1024 # 49MB, under GitHub's 50MB limit +CHUNK_SIZE = 49 * 1024 * 1024 # 49MB, under GitHub's 50MB limit def get_chunk_name(name, idx, num_chunks): return f"{name}.chunk{idx+1:02d}of{num_chunks:02d}" @@ -13,8 +12,6 @@ def get_chunk_paths(path, file_size): return [get_chunk_name(path, i, num_chunks) for i in range(num_chunks)] def chunk_file(path, num_chunks): - for old in glob.glob(f"{path}.chunk*"): - os.remove(old) with open(path, 'rb') as f: data = f.read() actual_num_chunks = max(1, math.ceil(len(data) / CHUNK_SIZE)) @@ -22,15 +19,13 @@ def chunk_file(path, num_chunks): for i in range(num_chunks): with open(get_chunk_name(path, i, num_chunks), 'wb') as f: f.write(data[i * CHUNK_SIZE:(i + 1) * CHUNK_SIZE]) - os.remove(path) def read_file_chunked(path): - chunks = sorted(glob.glob(f"{path}.chunk*")) - if chunks: - expected = [get_chunk_name(path, i, len(chunks)) for i in range(len(chunks))] - assert chunks == expected, f"Chunk mismatch: {chunks} != {expected}" - return b''.join(Path(f).read_bytes() for f in chunks) + for num_chunks in range(1, 100): + if os.path.isfile(get_chunk_name(path, 0, num_chunks)): + files = [get_chunk_name(path, i, num_chunks) for i in range(num_chunks)] + return b''.join(Path(f).read_bytes() for f in files) if os.path.isfile(path): return Path(path).read_bytes() raise FileNotFoundError(path) From d6af0e6eb50960c9bb7c4fffd44dfe87cb6888c1 Mon Sep 17 00:00:00 2001 From: Bruce Wayne Date: Fri, 20 Feb 2026 16:43:43 -0800 Subject: [PATCH 121/311] Revert "Simpler file chunker (#37276)" This reverts commit b27fa58444766c47f1ef980af79d324a669012c4. --- .gitignore | 3 +- common/file_chunker.py | 31 ------------------ selfdrive/modeld/SConscript | 45 ++++++++++++++------------- selfdrive/modeld/dmonitoringmodeld.py | 7 +++-- selfdrive/modeld/external_pickle.py | 38 ++++++++++++++++++++++ selfdrive/modeld/modeld.py | 9 +++--- 6 files changed, 72 insertions(+), 61 deletions(-) delete mode 100644 common/file_chunker.py create mode 100755 selfdrive/modeld/external_pickle.py diff --git a/.gitignore b/.gitignore index 062801d787..af18f06628 100644 --- a/.gitignore +++ b/.gitignore @@ -64,7 +64,8 @@ flycheck_* cppcheck_report.txt comma*.sh -selfdrive/modeld/models/*.pkl* +selfdrive/modeld/models/*.pkl +selfdrive/modeld/models/*.pkl.* # openpilot log files *.bz2 diff --git a/common/file_chunker.py b/common/file_chunker.py deleted file mode 100644 index f03d04a382..0000000000 --- a/common/file_chunker.py +++ /dev/null @@ -1,31 +0,0 @@ -import math -import os -from pathlib import Path - -CHUNK_SIZE = 49 * 1024 * 1024 # 49MB, under GitHub's 50MB limit - -def get_chunk_name(name, idx, num_chunks): - return f"{name}.chunk{idx+1:02d}of{num_chunks:02d}" - -def get_chunk_paths(path, file_size): - num_chunks = math.ceil(file_size / CHUNK_SIZE) - return [get_chunk_name(path, i, num_chunks) for i in range(num_chunks)] - -def chunk_file(path, num_chunks): - with open(path, 'rb') as f: - data = f.read() - actual_num_chunks = max(1, math.ceil(len(data) / CHUNK_SIZE)) - assert num_chunks >= actual_num_chunks, f"Allowed {num_chunks} chunks but needs at least {actual_num_chunks}, for path {path}" - for i in range(num_chunks): - with open(get_chunk_name(path, i, num_chunks), 'wb') as f: - f.write(data[i * CHUNK_SIZE:(i + 1) * CHUNK_SIZE]) - - -def read_file_chunked(path): - for num_chunks in range(1, 100): - if os.path.isfile(get_chunk_name(path, 0, num_chunks)): - files = [get_chunk_name(path, i, num_chunks) for i in range(num_chunks)] - return b''.join(Path(f).read_bytes() for f in files) - if os.path.isfile(path): - return Path(path).read_bytes() - raise FileNotFoundError(path) diff --git a/selfdrive/modeld/SConscript b/selfdrive/modeld/SConscript index 35be2acc04..1808cfec2f 100644 --- a/selfdrive/modeld/SConscript +++ b/selfdrive/modeld/SConscript @@ -1,18 +1,14 @@ import os import glob -from openpilot.common.file_chunker import chunk_file, get_chunk_paths Import('env', 'arch') -chunker_file = File("#common/file_chunker.py") lenv = env.Clone() +CHUNK_BYTES = int(os.environ.get("TG_CHUNK_BYTES", str(45 * 1024 * 1024))) tinygrad_root = env.Dir("#").abspath tinygrad_files = ["#"+x for x in glob.glob(env.Dir("#tinygrad_repo").relpath + "/**", recursive=True, root_dir=tinygrad_root) if 'pycache' not in x and os.path.isfile(os.path.join(tinygrad_root, x))] -def estimate_pickle_max_size(onnx_size): - return 1.2 * onnx_size + 10 * 1024 * 1024 # 20% + 10MB is plenty - # Get model metadata for model_name in ['driving_vision', 'driving_policy', 'dmonitoring_model']: fn = File(f"models/{model_name}").abspath @@ -30,34 +26,39 @@ image_flag = { 'larch64': 'IMAGE=2', }.get(arch, 'IMAGE=0') script_files = [File(Dir("#selfdrive/modeld").File("compile_warp.py").abspath)] -compile_warp_cmd = f'{tg_flags} python3 {Dir("#selfdrive/modeld").abspath}/compile_warp.py ' +cmd = f'{tg_flags} python3 {Dir("#selfdrive/modeld").abspath}/compile_warp.py ' from openpilot.common.transformations.camera import _ar_ox_fisheye, _os_fisheye warp_targets = [] for cam in [_ar_ox_fisheye, _os_fisheye]: w, h = cam.width, cam.height warp_targets += [File(f"models/warp_{w}x{h}_tinygrad.pkl").abspath, File(f"models/dm_warp_{w}x{h}_tinygrad.pkl").abspath] -def chunk_warps(target, source, env): - for t in warp_targets: - chunk_file(t, 1) -chunk_targets = sum([get_chunk_paths(t, estimate_pickle_max_size(0)) for t in warp_targets], []) -lenv.Command(chunk_targets, tinygrad_files + script_files + [chunker_file], - [compile_warp_cmd, chunk_warps]) +lenv.Command(warp_targets, tinygrad_files + script_files, cmd) def tg_compile(flags, model_name): pythonpath_string = 'PYTHONPATH="${PYTHONPATH}:' + env.Dir("#tinygrad_repo").abspath + '"' fn = File(f"models/{model_name}").abspath - pkl = fn + "_tinygrad.pkl" - onnx_path = fn + ".onnx" - chunk_targets = get_chunk_paths(pkl, estimate_pickle_max_size(os.path.getsize(onnx_path))) - def do_chunk(target, source, env): - chunk_file(pkl, len(chunk_targets)) - return lenv.Command( - chunk_targets, - [onnx_path] + tinygrad_files + [chunker_file], - [f'{pythonpath_string} {flags} {image_flag} python3 {Dir("#tinygrad_repo").abspath}/examples/openpilot/compile3.py {fn}.onnx {pkl}', - do_chunk] + + out = fn + "_tinygrad.pkl" + full = out + ".full" + parts = out + ".parts" + + full_node = lenv.Command( + full, + [fn + ".onnx"] + tinygrad_files, + f'{pythonpath_string} {flags} {image_flag} python3 {Dir("#tinygrad_repo").abspath}/examples/openpilot/compile3.py {fn}.onnx {full}' ) + split_script = File(Dir("#selfdrive/modeld").File("external_pickle.py").abspath) + parts_node = lenv.Command( + parts, + [full_node, split_script, Value(str(CHUNK_BYTES))], + [f'python3 {split_script.abspath} {full} {out} {CHUNK_BYTES}', Delete(full)], + ) + + lenv.NoCache(parts_node) + lenv.AlwaysBuild(parts_node) + return parts_node + # Compile small models for model_name in ['driving_vision', 'driving_policy', 'dmonitoring_model']: tg_compile(tg_flags, model_name) diff --git a/selfdrive/modeld/dmonitoringmodeld.py b/selfdrive/modeld/dmonitoringmodeld.py index 6befe210a4..956ea8a6a2 100755 --- a/selfdrive/modeld/dmonitoringmodeld.py +++ b/selfdrive/modeld/dmonitoringmodeld.py @@ -16,8 +16,8 @@ from openpilot.common.realtime import config_realtime_process from openpilot.common.transformations.model import dmonitoringmodel_intrinsics from openpilot.common.transformations.camera import _ar_ox_fisheye, _os_fisheye from openpilot.system.camerad.cameras.nv12_info import get_nv12_info -from openpilot.common.file_chunker import read_file_chunked from openpilot.selfdrive.modeld.parse_model_outputs import sigmoid, safe_exp +from openpilot.selfdrive.modeld.external_pickle import load_external_pickle PROCESS_NAME = "selfdrive.modeld.dmonitoringmodeld" SEND_RAW_PRED = os.getenv('SEND_RAW_PRED') @@ -45,7 +45,7 @@ class ModelState: self.tensor_inputs = {k: Tensor(v, device='NPY').realize() for k,v in self.numpy_inputs.items()} self._blob_cache : dict[int, Tensor] = {} self.image_warp = None - self.model_run = pickle.loads(read_file_chunked(str(MODEL_PKL_PATH))) + self.model_run = load_external_pickle(MODEL_PKL_PATH) def run(self, buf: VisionBuf, calib: np.ndarray, transform: np.ndarray) -> tuple[np.ndarray, float]: self.numpy_inputs['calib'][0,:] = calib @@ -55,7 +55,8 @@ class ModelState: if self.image_warp is None: self.frame_buf_params = get_nv12_info(buf.width, buf.height) warp_path = MODELS_DIR / f'dm_warp_{buf.width}x{buf.height}_tinygrad.pkl' - self.image_warp = pickle.loads(read_file_chunked(str(warp_path))) + with open(warp_path, "rb") as f: + self.image_warp = pickle.load(f) ptr = buf.data.ctypes.data # There is a ringbuffer of imgs, just cache tensors pointing to all of them if ptr not in self._blob_cache: diff --git a/selfdrive/modeld/external_pickle.py b/selfdrive/modeld/external_pickle.py new file mode 100755 index 0000000000..d60a9632a6 --- /dev/null +++ b/selfdrive/modeld/external_pickle.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +import hashlib +import pickle +import sys +from pathlib import Path + +def split_pickle(full_path: Path, out_prefix: Path, chunk_bytes: int) -> None: + data = full_path.read_bytes() + out_dir = out_prefix.parent + + for p in out_dir.glob(f"{out_prefix.name}.data-*"): + p.unlink() + + total = (len(data) + chunk_bytes - 1) // chunk_bytes + names = [] + for i in range(0, len(data), chunk_bytes): + name = f"{out_prefix.name}.data-{(i // chunk_bytes) + 1:04d}-of-{total:04d}" + (out_dir / name).write_bytes(data[i:i + chunk_bytes]) + names.append(name) + + manifest = hashlib.sha256(data).hexdigest() + "\n" + "\n".join(names) + "\n" + (out_dir / (out_prefix.name + ".parts")).write_text(manifest) + +def load_external_pickle(prefix: Path): + parts = prefix.parent / (prefix.name + ".parts") + lines = parts.read_text().splitlines() + expected_hash, chunk_names = lines[0], lines[1:] + + data = bytearray() + for name in chunk_names: + data += (prefix.parent / name).read_bytes() + + if hashlib.sha256(data).hexdigest() != expected_hash: + raise RuntimeError(f"hash mismatch loading {prefix}") + return pickle.loads(data) + +if __name__ == "__main__": + split_pickle(Path(sys.argv[1]), Path(sys.argv[2]), int(sys.argv[3])) diff --git a/selfdrive/modeld/modeld.py b/selfdrive/modeld/modeld.py index 8cfbea02c8..3fe3e0e6d6 100755 --- a/selfdrive/modeld/modeld.py +++ b/selfdrive/modeld/modeld.py @@ -27,8 +27,8 @@ from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper from openpilot.selfdrive.controls.lib.drive_helpers import get_accel_from_plan, smooth_value, get_curvature_from_plan from openpilot.selfdrive.modeld.parse_model_outputs import Parser from openpilot.selfdrive.modeld.fill_model_msg import fill_model_msg, fill_pose_msg, PublishState -from openpilot.common.file_chunker import read_file_chunked from openpilot.selfdrive.modeld.constants import ModelConstants, Plan +from openpilot.selfdrive.modeld.external_pickle import load_external_pickle PROCESS_NAME = "selfdrive.modeld.modeld" @@ -178,8 +178,8 @@ class ModelState: self.parser = Parser() self.frame_buf_params : dict[str, tuple[int, int, int, int]] = {} self.update_imgs = None - self.vision_run = pickle.loads(read_file_chunked(str(VISION_PKL_PATH))) - self.policy_run = pickle.loads(read_file_chunked(str(POLICY_PKL_PATH))) + self.vision_run = load_external_pickle(VISION_PKL_PATH) + self.policy_run = load_external_pickle(POLICY_PKL_PATH) def slice_outputs(self, model_outputs: np.ndarray, output_slices: dict[str, slice]) -> dict[str, np.ndarray]: parsed_model_outputs = {k: model_outputs[np.newaxis, v] for k,v in output_slices.items()} @@ -196,7 +196,8 @@ class ModelState: w, h = bufs[key].width, bufs[key].height self.frame_buf_params[key] = get_nv12_info(w, h) warp_path = MODELS_DIR / f'warp_{w}x{h}_tinygrad.pkl' - self.update_imgs = pickle.loads(read_file_chunked(str(warp_path))) + with open(warp_path, "rb") as f: + self.update_imgs = pickle.load(f) for key in bufs.keys(): ptr = bufs[key].data.ctypes.data From 23e1c4f49e63e550a44a85f0020b601bd9376a1d Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Fri, 20 Feb 2026 16:47:47 -0800 Subject: [PATCH 122/311] rm onnx (#37285) --- pyproject.toml | 3 -- scripts/reporter.py | 28 ++++++++++---- selfdrive/modeld/get_model_metadata.py | 40 +++++++++++++------ uv.lock | 53 -------------------------- 4 files changed, 49 insertions(+), 75 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b17af33def..f8614a47e1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,9 +37,6 @@ dependencies = [ "libusb1", "spidev; platform_system == 'Linux'", - # modeld - "onnx >= 1.14.0", - # logging "pyzmq", "sentry-sdk", diff --git a/scripts/reporter.py b/scripts/reporter.py index 903fcc8911..d894b8af48 100755 --- a/scripts/reporter.py +++ b/scripts/reporter.py @@ -1,17 +1,33 @@ #!/usr/bin/env python3 import os import glob -import onnx + +from tinygrad.nn.onnx import OnnxPBParser BASEDIR = os.path.abspath(os.path.join(os.path.dirname(os.path.realpath(__file__)), "../")) + MASTER_PATH = os.getenv("MASTER_PATH", BASEDIR) MODEL_PATH = "/selfdrive/modeld/models/" + +class MetadataOnnxPBParser(OnnxPBParser): + def _parse_ModelProto(self) -> dict: + obj = {"metadata_props": []} + for fid, wire_type in self._parse_message(self.reader.len): + match fid: + case 14: + obj["metadata_props"].append(self._parse_StringStringEntryProto()) + case _: + self.reader.skip_field(wire_type) + return obj + + def get_checkpoint(f): - model = onnx.load(f) - metadata = {prop.key: prop.value for prop in model.metadata_props} + model = MetadataOnnxPBParser(f).parse() + metadata = {prop["key"]: prop["value"] for prop in model["metadata_props"]} return metadata['model_checkpoint'].split('/')[0] + if __name__ == "__main__": print("| | master | PR branch |") print("|-| ----- | --------- |") @@ -24,8 +40,4 @@ if __name__ == "__main__": fn = os.path.basename(f) master = get_checkpoint(MASTER_PATH + MODEL_PATH + fn) pr = get_checkpoint(BASEDIR + MODEL_PATH + fn) - print( - "|", fn, "|", - f"[{master}](https://reporter.comma.life/experiment/{master})", "|", - f"[{pr}](https://reporter.comma.life/experiment/{pr})", "|" - ) + print("|", fn, "|", f"[{master}](https://reporter.comma.life/experiment/{master})", "|", f"[{pr}](https://reporter.comma.life/experiment/{pr})", "|") diff --git a/selfdrive/modeld/get_model_metadata.py b/selfdrive/modeld/get_model_metadata.py index 2001d23d75..838b1e9f40 100755 --- a/selfdrive/modeld/get_model_metadata.py +++ b/selfdrive/modeld/get_model_metadata.py @@ -1,33 +1,51 @@ #!/usr/bin/env python3 import sys import pathlib -import onnx import codecs import pickle from typing import Any -def get_name_and_shape(value_info:onnx.ValueInfoProto) -> tuple[str, tuple[int,...]]: - shape = tuple([int(dim.dim_value) for dim in value_info.type.tensor_type.shape.dim]) - name = value_info.name +from tinygrad.nn.onnx import OnnxPBParser + + +class MetadataOnnxPBParser(OnnxPBParser): + def _parse_ModelProto(self) -> dict: + obj: dict[str, Any] = {"graph": {"input": [], "output": []}, "metadata_props": []} + for fid, wire_type in self._parse_message(self.reader.len): + match fid: + case 7: + obj["graph"] = self._parse_GraphProto() + case 14: + obj["metadata_props"].append(self._parse_StringStringEntryProto()) + case _: + self.reader.skip_field(wire_type) + return obj + + +def get_name_and_shape(value_info: dict[str, Any]) -> tuple[str, tuple[int, ...]]: + shape = tuple(int(dim) if isinstance(dim, int) else 0 for dim in value_info["parsed_type"].shape) + name = value_info["name"] return name, shape -def get_metadata_value_by_name(model:onnx.ModelProto, name:str) -> str | Any: - for prop in model.metadata_props: - if prop.key == name: - return prop.value + +def get_metadata_value_by_name(model: dict[str, Any], name: str) -> str | Any: + for prop in model["metadata_props"]: + if prop["key"] == name: + return prop["value"] return None + if __name__ == "__main__": model_path = pathlib.Path(sys.argv[1]) - model = onnx.load(str(model_path)) + model = MetadataOnnxPBParser(model_path).parse() output_slices = get_metadata_value_by_name(model, 'output_slices') assert output_slices is not None, 'output_slices not found in metadata' metadata = { 'model_checkpoint': get_metadata_value_by_name(model, 'model_checkpoint'), 'output_slices': pickle.loads(codecs.decode(output_slices.encode(), "base64")), - 'input_shapes': dict([get_name_and_shape(x) for x in model.graph.input]), - 'output_shapes': dict([get_name_and_shape(x) for x in model.graph.output]) + 'input_shapes': dict(get_name_and_shape(x) for x in model["graph"]["input"]), + 'output_shapes': dict(get_name_and_shape(x) for x in model["graph"]["output"]), } metadata_path = model_path.parent / (model_path.stem + '_metadata.pkl') diff --git a/uv.lock b/uv.lock index b3aa498e9a..844b1db705 100644 --- a/uv.lock +++ b/uv.lock @@ -663,22 +663,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521, upload-time = "2023-11-20T17:51:08.587Z" }, ] -[[package]] -name = "ml-dtypes" -version = "0.5.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0e/4a/c27b42ed9b1c7d13d9ba8b6905dece787d6259152f2309338aed29b2447b/ml_dtypes-0.5.4.tar.gz", hash = "sha256:8ab06a50fb9bf9666dd0fe5dfb4676fa2b0ac0f31ecff72a6c3af8e22c063453", size = 692314, upload-time = "2025-11-17T22:32:31.031Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/b8/3c70881695e056f8a32f8b941126cf78775d9a4d7feba8abcb52cb7b04f2/ml_dtypes-0.5.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a174837a64f5b16cab6f368171a1a03a27936b31699d167684073ff1c4237dac", size = 676927, upload-time = "2025-11-17T22:31:48.182Z" }, - { url = "https://files.pythonhosted.org/packages/54/0f/428ef6881782e5ebb7eca459689448c0394fa0a80bea3aa9262cba5445ea/ml_dtypes-0.5.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a7f7c643e8b1320fd958bf098aa7ecf70623a42ec5154e3be3be673f4c34d900", size = 5028464, upload-time = "2025-11-17T22:31:50.135Z" }, - { url = "https://files.pythonhosted.org/packages/3a/cb/28ce52eb94390dda42599c98ea0204d74799e4d8047a0eb559b6fd648056/ml_dtypes-0.5.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ad459e99793fa6e13bd5b7e6792c8f9190b4e5a1b45c63aba14a4d0a7f1d5ff", size = 5009002, upload-time = "2025-11-17T22:31:52.001Z" }, - { url = "https://files.pythonhosted.org/packages/f5/f0/0cfadd537c5470378b1b32bd859cf2824972174b51b873c9d95cfd7475a5/ml_dtypes-0.5.4-cp312-cp312-win_amd64.whl", hash = "sha256:c1a953995cccb9e25a4ae19e34316671e4e2edaebe4cf538229b1fc7109087b7", size = 212222, upload-time = "2025-11-17T22:31:53.742Z" }, - { url = "https://files.pythonhosted.org/packages/16/2e/9acc86985bfad8f2c2d30291b27cd2bb4c74cea08695bd540906ed744249/ml_dtypes-0.5.4-cp312-cp312-win_arm64.whl", hash = "sha256:9bad06436568442575beb2d03389aa7456c690a5b05892c471215bfd8cf39460", size = 160793, upload-time = "2025-11-17T22:31:55.358Z" }, -] - [[package]] name = "mpmath" version = "1.3.0" @@ -734,26 +718,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2f/a3/56c5c604fae6dd40fa2ed3040d005fca97e91bd320d232ac9931d77ba13c/numpy-2.4.2-cp312-cp312-win_arm64.whl", hash = "sha256:fbde1b0c6e81d56f5dccd95dd4a711d9b95df1ae4009a60887e56b27e8d903fa", size = 10220171, upload-time = "2026-01-31T23:11:14.684Z" }, ] -[[package]] -name = "onnx" -version = "1.20.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "ml-dtypes" }, - { name = "numpy" }, - { name = "protobuf" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3b/8a/335c03a8683a88a32f9a6bb98899ea6df241a41df64b37b9696772414794/onnx-1.20.1.tar.gz", hash = "sha256:ded16de1df563d51fbc1ad885f2a426f814039d8b5f4feb77febe09c0295ad67", size = 12048980, upload-time = "2026-01-10T01:40:03.043Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/4c/4b17e82f91ab9aa07ff595771e935ca73547b035030dc5f5a76e63fbfea9/onnx-1.20.1-cp312-abi3-macosx_12_0_universal2.whl", hash = "sha256:1d923bb4f0ce1b24c6859222a7e6b2f123e7bfe7623683662805f2e7b9e95af2", size = 17903547, upload-time = "2026-01-10T01:39:31.015Z" }, - { url = "https://files.pythonhosted.org/packages/64/5e/1bfa100a9cb3f2d3d5f2f05f52f7e60323b0e20bb0abace1ae64dbc88f25/onnx-1.20.1-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ddc0b7d8b5a94627dc86c533d5e415af94cbfd103019a582669dad1f56d30281", size = 17412021, upload-time = "2026-01-10T01:39:33.885Z" }, - { url = "https://files.pythonhosted.org/packages/fb/71/d3fec0dcf9a7a99e7368112d9c765154e81da70fcba1e3121131a45c245b/onnx-1.20.1-cp312-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9336b6b8e6efcf5c490a845f6afd7e041c89a56199aeda384ed7d58fb953b080", size = 17510450, upload-time = "2026-01-10T01:39:36.589Z" }, - { url = "https://files.pythonhosted.org/packages/74/a7/edce1403e05a46e59b502fae8e3350ceeac5841f8e8f1561e98562ed9b09/onnx-1.20.1-cp312-abi3-win32.whl", hash = "sha256:564c35a94811979808ab5800d9eb4f3f32c12daedba7e33ed0845f7c61ef2431", size = 16238216, upload-time = "2026-01-10T01:39:39.46Z" }, - { url = "https://files.pythonhosted.org/packages/8b/c7/8690c81200ae652ac550c1df52f89d7795e6cc941f3cb38c9ef821419e80/onnx-1.20.1-cp312-abi3-win_amd64.whl", hash = "sha256:9fe7f9a633979d50984b94bda8ceb7807403f59a341d09d19342dc544d0ca1d5", size = 16389207, upload-time = "2026-01-10T01:39:41.955Z" }, - { url = "https://files.pythonhosted.org/packages/01/a0/4fb0e6d36eaf079af366b2c1f68bafe92df6db963e2295da84388af64abc/onnx-1.20.1-cp312-abi3-win_arm64.whl", hash = "sha256:21d747348b1c8207406fa2f3e12b82f53e0d5bb3958bcd0288bd27d3cb6ebb00", size = 16344155, upload-time = "2026-01-10T01:39:45.536Z" }, -] - [[package]] name = "opencv-python-headless" version = "4.13.0.92" @@ -788,7 +752,6 @@ dependencies = [ { name = "json-rpc" }, { name = "libusb1" }, { name = "numpy" }, - { name = "onnx" }, { name = "psutil" }, { name = "pyaudio" }, { name = "pycapnp" }, @@ -864,7 +827,6 @@ requires-dist = [ { name = "metadrive-simulator", marker = "platform_machine != 'aarch64' and extra == 'tools'", git = "https://github.com/commaai/metadrive.git?rev=minimal" }, { name = "mkdocs", marker = "extra == 'docs'" }, { name = "numpy", specifier = ">=2.0" }, - { name = "onnx", specifier = ">=1.14.0" }, { name = "opencv-python-headless", marker = "extra == 'dev'" }, { name = "pre-commit-hooks", marker = "extra == 'testing'" }, { name = "psutil" }, @@ -1030,21 +992,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, ] -[[package]] -name = "protobuf" -version = "6.33.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ba/25/7c72c307aafc96fa87062aa6291d9f7c94836e43214d43722e86037aac02/protobuf-6.33.5.tar.gz", hash = "sha256:6ddcac2a081f8b7b9642c09406bc6a4290128fce5f471cddd165960bb9119e5c", size = 444465, upload-time = "2026-01-29T21:51:33.494Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b1/79/af92d0a8369732b027e6d6084251dd8e782c685c72da161bd4a2e00fbabb/protobuf-6.33.5-cp310-abi3-win32.whl", hash = "sha256:d71b040839446bac0f4d162e758bea99c8251161dae9d0983a3b88dee345153b", size = 425769, upload-time = "2026-01-29T21:51:21.751Z" }, - { url = "https://files.pythonhosted.org/packages/55/75/bb9bc917d10e9ee13dee8607eb9ab963b7cf8be607c46e7862c748aa2af7/protobuf-6.33.5-cp310-abi3-win_amd64.whl", hash = "sha256:3093804752167bcab3998bec9f1048baae6e29505adaf1afd14a37bddede533c", size = 437118, upload-time = "2026-01-29T21:51:24.022Z" }, - { url = "https://files.pythonhosted.org/packages/a2/6b/e48dfc1191bc5b52950246275bf4089773e91cb5ba3592621723cdddca62/protobuf-6.33.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:a5cb85982d95d906df1e2210e58f8e4f1e3cdc088e52c921a041f9c9a0386de5", size = 427766, upload-time = "2026-01-29T21:51:25.413Z" }, - { url = "https://files.pythonhosted.org/packages/4e/b1/c79468184310de09d75095ed1314b839eb2f72df71097db9d1404a1b2717/protobuf-6.33.5-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:9b71e0281f36f179d00cbcb119cb19dec4d14a81393e5ea220f64b286173e190", size = 324638, upload-time = "2026-01-29T21:51:26.423Z" }, - { url = "https://files.pythonhosted.org/packages/c5/f5/65d838092fd01c44d16037953fd4c2cc851e783de9b8f02b27ec4ffd906f/protobuf-6.33.5-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:8afa18e1d6d20af15b417e728e9f60f3aa108ee76f23c3b2c07a2c3b546d3afd", size = 339411, upload-time = "2026-01-29T21:51:27.446Z" }, - { url = "https://files.pythonhosted.org/packages/9b/53/a9443aa3ca9ba8724fdfa02dd1887c1bcd8e89556b715cfbacca6b63dbec/protobuf-6.33.5-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:cbf16ba3350fb7b889fca858fb215967792dc125b35c7976ca4818bee3521cf0", size = 323465, upload-time = "2026-01-29T21:51:28.925Z" }, - { url = "https://files.pythonhosted.org/packages/57/bf/2086963c69bdac3d7cff1cc7ff79b8ce5ea0bec6797a017e1be338a46248/protobuf-6.33.5-py3-none-any.whl", hash = "sha256:69915a973dd0f60f31a08b8318b73eab2bd6a392c79184b3612226b0a3f8ec02", size = 170687, upload-time = "2026-01-29T21:51:32.557Z" }, -] - [[package]] name = "psutil" version = "7.2.2" From c46cf9f536ef3accbf4c8e9ecb3c2d1682887a91 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Fri, 20 Feb 2026 18:35:24 -0800 Subject: [PATCH 123/311] lil pyproject.toml cleanup --- pyproject.toml | 7 ++----- uv.lock | 8 +++----- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f8614a47e1..4becd17563 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,17 +20,15 @@ dependencies = [ # core "cffi", "scons", - "pycapnp==2.1.0", + "pycapnp", "Cython", "setuptools", "numpy >=2.0", # body / webrtcd + "av", "aiohttp", "aiortc", - # aiortc does not put an upper bound on pyopenssl and is now incompatible - # with the latest release - "pyopenssl < 24.3.0", "pyaudio", # panda @@ -90,7 +88,6 @@ testing = [ ] dev = [ - "av", "matplotlib", "opencv-python-headless", ] diff --git a/uv.lock b/uv.lock index 844b1db705..8b8bfd639b 100644 --- a/uv.lock +++ b/uv.lock @@ -743,6 +743,7 @@ source = { editable = "." } dependencies = [ { name = "aiohttp" }, { name = "aiortc" }, + { name = "av" }, { name = "casadi" }, { name = "cffi" }, { name = "crcmod-plus" }, @@ -757,7 +758,6 @@ dependencies = [ { name = "pycapnp" }, { name = "pycryptodome" }, { name = "pyjwt" }, - { name = "pyopenssl" }, { name = "pyserial" }, { name = "pyzmq" }, { name = "qrcode" }, @@ -778,7 +778,6 @@ dependencies = [ [package.optional-dependencies] dev = [ - { name = "av" }, { name = "matplotlib" }, { name = "opencv-python-headless" }, ] @@ -809,7 +808,7 @@ tools = [ requires-dist = [ { name = "aiohttp" }, { name = "aiortc" }, - { name = "av", marker = "extra == 'dev'" }, + { name = "av" }, { name = "casadi", specifier = ">=3.6.6" }, { name = "cffi" }, { name = "codespell", marker = "extra == 'testing'" }, @@ -831,10 +830,9 @@ requires-dist = [ { name = "pre-commit-hooks", marker = "extra == 'testing'" }, { name = "psutil" }, { name = "pyaudio" }, - { name = "pycapnp", specifier = "==2.1.0" }, + { name = "pycapnp" }, { name = "pycryptodome" }, { name = "pyjwt" }, - { name = "pyopenssl", specifier = "<24.3.0" }, { name = "pyserial" }, { name = "pytest", marker = "extra == 'testing'" }, { name = "pytest-asyncio", marker = "extra == 'testing'" }, From c98ba4ff4cf4da424add07d6f3c182f4924cb48e Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Fri, 20 Feb 2026 18:54:00 -0800 Subject: [PATCH 124/311] Qt is optional (#37295) * Qt is optional * cleanup --- SConstruct | 6 ++--- tools/cabana/.gitignore | 2 +- tools/cabana/SConscript | 26 ++++++++++++------- tools/cabana/cabana | 38 ++++++++++++++++++++++++++++ tools/install_ubuntu_dependencies.sh | 8 ------ tools/mac_setup.sh | 22 ---------------- 6 files changed, 58 insertions(+), 44 deletions(-) create mode 100755 tools/cabana/cabana diff --git a/SConstruct b/SConstruct index 4f04be624c..3b8aadf914 100644 --- a/SConstruct +++ b/SConstruct @@ -211,10 +211,8 @@ SConscript(['third_party/SConscript']) SConscript(['selfdrive/SConscript']) -if Dir('#tools/cabana/').exists() and GetOption('extras'): - SConscript(['tools/replay/SConscript']) - if arch != "larch64": - SConscript(['tools/cabana/SConscript']) +if Dir('#tools/cabana/').exists() and arch != "larch64": + SConscript(['tools/cabana/SConscript']) env.CompilationDatabase('compile_commands.json') diff --git a/tools/cabana/.gitignore b/tools/cabana/.gitignore index 362a51f5c9..3d64f83204 100644 --- a/tools/cabana/.gitignore +++ b/tools/cabana/.gitignore @@ -1,6 +1,6 @@ moc_* *.moc -cabana +_cabana dbc/car_fingerprint_to_dbc.json tests/test_cabana diff --git a/tools/cabana/SConscript b/tools/cabana/SConscript index b79d046fca..025797d1e3 100644 --- a/tools/cabana/SConscript +++ b/tools/cabana/SConscript @@ -1,14 +1,27 @@ import subprocess import os +import shutil -Import('env', 'arch', 'common', 'messaging', 'visionipc', 'replay_lib', 'cereal') +Import('env', 'arch', 'common', 'messaging', 'visionipc', 'cereal') + +# Detect Qt - skip build if not available +if arch == "Darwin": + brew_prefix = subprocess.check_output(['brew', '--prefix'], encoding='utf8').strip() + has_qt = os.path.isdir(os.path.join(brew_prefix, "opt/qt@5")) +else: + has_qt = shutil.which('qmake') is not None + +if not has_qt: + Return() + +SConscript(['#tools/replay/SConscript']) +Import('replay_lib') qt_env = env.Clone() qt_modules = ["Widgets", "Gui", "Core", "Network", "Concurrent", "DBus", "Xml"] qt_libs = [] if arch == "Darwin": - brew_prefix = subprocess.check_output(['brew', '--prefix'], encoding='utf8').strip() qt_env['QTDIR'] = f"{brew_prefix}/opt/qt@5" qt_dirs = [ os.path.join(qt_env['QTDIR'], "include"), @@ -31,12 +44,7 @@ else: qt_dirs += [f"{qt_install_headers}/QtGui/{qt_gui_dirs[0]}/QtGui", ] if qt_gui_dirs else [] qt_dirs += [f"{qt_install_headers}/Qt{m}" for m in qt_modules] - qt_libs = [f"Qt5{m}" for m in qt_modules] - if arch == "larch64": - qt_libs += ["GLESv2", "wayland-client"] - qt_env.PrependENVPath('PATH', Dir("#third_party/qt5/larch64/bin/").abspath) - elif arch != "Darwin": - qt_libs += ["GL"] + qt_libs = [f"Qt5{m}" for m in qt_modules] + ["GL"] qt_env['QT3DIR'] = qt_env['QTDIR'] qt_env.Tool('qt3') @@ -83,7 +91,7 @@ cabana_lib = cabana_env.Library("cabana_lib", ['mainwin.cc', 'streams/socketcans 'chart/chartswidget.cc', 'chart/chart.cc', 'chart/signalselector.cc', 'chart/tiplabel.cc', 'chart/sparkline.cc', 'commands.cc', 'messageswidget.cc', 'streamselector.cc', 'settings.cc', 'panda.cc', 'cameraview.cc', 'detailwidget.cc', 'tools/findsimilarbits.cc', 'tools/findsignal.cc', 'tools/routeinfo.cc'], LIBS=cabana_libs, FRAMEWORKS=base_frameworks) -cabana_env.Program('cabana', ['cabana.cc', cabana_lib, assets], LIBS=cabana_libs, FRAMEWORKS=base_frameworks) +cabana_env.Program('_cabana', ['cabana.cc', cabana_lib, assets], LIBS=cabana_libs, FRAMEWORKS=base_frameworks) if GetOption('extras'): cabana_env.Program('tests/test_cabana', ['tests/test_runner.cc', 'tests/test_cabana.cc', cabana_lib], LIBS=[cabana_libs]) diff --git a/tools/cabana/cabana b/tools/cabana/cabana new file mode 100755 index 0000000000..00709734a5 --- /dev/null +++ b/tools/cabana/cabana @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +set -e + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" +ROOT="$(cd "$DIR/../../" && pwd)" + +install_qt() { + if [[ "$(uname)" == "Darwin" ]]; then + brew install qt@5 + brew link qt@5 || true + else + SUDO="" + if [[ ! $(id -u) -eq 0 ]]; then + SUDO="sudo" + fi + $SUDO apt-get install -y --no-install-recommends \ + qtbase5-dev \ + qtbase5-dev-tools \ + qttools5-dev-tools \ + libqt5charts5-dev \ + libqt5svg5-dev \ + libqt5serialbus5-dev \ + libqt5x11extras5-dev \ + libqt5opengl5-dev + fi +} + +# Install Qt if not found +if ! command -v qmake &> /dev/null; then + echo "Qt not found, installing dependencies..." + install_qt +fi + +# Build _cabana +cd "$ROOT" +scons -j"$(nproc)" tools/cabana/_cabana + +exec "$DIR/_cabana" "$@" diff --git a/tools/install_ubuntu_dependencies.sh b/tools/install_ubuntu_dependencies.sh index f428e9972a..a20176419f 100755 --- a/tools/install_ubuntu_dependencies.sh +++ b/tools/install_ubuntu_dependencies.sh @@ -52,18 +52,12 @@ function install_ubuntu_common_requirements() { libglfw3-dev \ libglib2.0-0 \ libjpeg-dev \ - libqt5charts5-dev \ libncurses5-dev \ libusb-1.0-0-dev \ libzmq3-dev \ libzstd-dev \ libsqlite3-dev \ portaudio19-dev \ - qttools5-dev-tools \ - libqt5svg5-dev \ - libqt5serialbus5-dev \ - libqt5x11extras5-dev \ - libqt5opengl5-dev \ gettext } @@ -73,8 +67,6 @@ function install_ubuntu_lts_latest_requirements() { $SUDO apt-get install -y --no-install-recommends \ g++-12 \ - qtbase5-dev \ - qtbase5-dev-tools \ python3-dev \ python3-venv } diff --git a/tools/mac_setup.sh b/tools/mac_setup.sh index 82748e9613..a5a6eed040 100755 --- a/tools/mac_setup.sh +++ b/tools/mac_setup.sh @@ -36,7 +36,6 @@ brew "glfw" brew "libusb" brew "libtool" brew "llvm" -brew "qt@5" brew "zeromq" cask "gcc-arm-embedded" brew "portaudio" @@ -56,27 +55,6 @@ export CPPFLAGS="$CPPFLAGS -I${BREW_PREFIX}/opt/bzip2/include" $DIR/install_python_dependencies.sh echo "[ ] installed python dependencies t=$SECONDS" -# brew does not link qt5 by default -# check if qt5 can be linked, if not, prompt the user to link it -QT_BIN_LOCATION="$(command -v lupdate || :)" -if [ -n "$QT_BIN_LOCATION" ]; then - # if qt6 is linked, prompt the user to unlink it and link the right version - QT_BIN_VERSION="$(lupdate -version | awk '{print $NF}')" - if [[ ! "$QT_BIN_VERSION" =~ 5\.[0-9]+\.[0-9]+ ]]; then - echo - echo "lupdate/lrelease available at PATH is $QT_BIN_VERSION" - if [[ "$QT_BIN_LOCATION" == "$(brew --prefix)/"* ]]; then - echo "Run the following command to link qt5:" - echo "brew unlink qt@6 && brew link qt@5" - else - echo "Remove conflicting qt entries from PATH and run the following command to link qt5:" - echo "brew link qt@5" - fi - fi -else - brew link qt@5 -fi - echo echo "---- OPENPILOT SETUP DONE ----" echo "Open a new shell or configure your active shell env by running:" From 30350f4207b220b414b36b7ec4c9994050939b35 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 20 Feb 2026 19:00:27 -0800 Subject: [PATCH 125/311] ui: navigation stack (#37094) * initial * start to support nav stack in settings panels + fix some navwidget bugs * add deprecation warning and move more to new nav stack * fix overriding NavWidget enabled and do developer panel * fix interactive timeout and do main * more device, not done yet * minor network fixes * dcam dialog * start onboarding * fix onboarding * do mici setup * remove now useless CUSTOM_SOFTWARE * support big ui with old modal overlay * reset can be old modal overlay, but updater needs new since it uses wifiui * flip name truthiness to inspire excitement * all *should* work, but will do pass later * clean up main * clean up settiings * clean up dialog and developer * cleanup mici setup some * rm one more * fix keyboard * revert * might as well but clarify * fix networkinfopage buttons * lint * nice clean up from cursor * animate background fade with position * fix device overlays * cursor fix pt1 cursor fix pt2 * rm print * capital * temp fix from cursor for onboarding not freeing space after reviewing training guide * fix home screen scroller snap not resetting * stash * nice gradient on top * 40 * 20 * no gradient * return unused returns and always show regulatory btn * nice! * clean up * new_modal is always true! * more clean up * clean up * big only renders top 1 * fixup setup and updater * stash * Revert "stash" This reverts commit 3cfb226ccb51869ed1f7d630b5fdd6725ad094d5. * fix mici keys coming in from top * clean up * fix mici dialogs like tici, pop first incase call back pushes * clever way but not not * Revert "clever way but not not" This reverts commit f69d106df61262f049df20cc1a9064ca1e6feeb7. * more setup * mici keyboard: fix not disabling below * cmt * fix wifi callbacks not running in rare case * clean up network * clean up network * clean up dialog * pairing * rm * todo * fix replay * they push themselkves! * clean up ui_state * clean up application * clean up * stash * Revert "stash" This reverts commit 07d3f5f26c99ef891086b6fe03095d53a62b8631. * typing * lint --- selfdrive/ui/mici/layouts/main.py | 81 ++++------ selfdrive/ui/mici/layouts/onboarding.py | 14 +- .../ui/mici/layouts/settings/developer.py | 11 +- selfdrive/ui/mici/layouts/settings/device.py | 38 ++--- .../ui/mici/layouts/settings/firehose.py | 7 +- .../mici/layouts/settings/network/__init__.py | 42 ++---- .../mici/layouts/settings/network/wifi_ui.py | 45 ++---- .../ui/mici/layouts/settings/settings.py | 89 +++-------- selfdrive/ui/mici/layouts/settings/toggles.py | 5 +- .../ui/mici/onroad/augmented_road_view.py | 2 +- .../ui/mici/onroad/driver_camera_dialog.py | 10 +- selfdrive/ui/mici/tests/test_widget_leaks.py | 2 - selfdrive/ui/mici/widgets/dialog.py | 38 ++--- selfdrive/ui/mici/widgets/pairing_dialog.py | 11 +- selfdrive/ui/onroad/augmented_road_view.py | 2 +- selfdrive/ui/onroad/driver_camera_dialog.py | 2 +- selfdrive/ui/tests/diff/replay.py | 8 +- selfdrive/ui/tests/profile_onroad.py | 5 +- selfdrive/ui/ui.py | 12 +- selfdrive/ui/widgets/pairing_dialog.py | 2 +- system/ui/lib/application.py | 102 ++++--------- system/ui/mici_reset.py | 8 +- system/ui/mici_setup.py | 141 +++++++----------- system/ui/mici_updater.py | 7 +- system/ui/tici_reset.py | 2 +- system/ui/tici_setup.py | 2 +- system/ui/tici_updater.py | 2 +- system/ui/widgets/__init__.py | 31 +++- system/ui/widgets/keyboard.py | 2 +- system/ui/widgets/mici_keyboard.py | 7 +- system/ui/widgets/network.py | 2 +- 31 files changed, 260 insertions(+), 472 deletions(-) diff --git a/selfdrive/ui/mici/layouts/main.py b/selfdrive/ui/mici/layouts/main.py index b78a1d8eaf..be2c4747f3 100644 --- a/selfdrive/ui/mici/layouts/main.py +++ b/selfdrive/ui/mici/layouts/main.py @@ -1,5 +1,4 @@ import pyray as rl -from enum import IntEnum import cereal.messaging as messaging from openpilot.selfdrive.ui.mici.layouts.home import MiciHomeLayout from openpilot.selfdrive.ui.mici.layouts.settings.settings import SettingsLayout @@ -15,18 +14,12 @@ from openpilot.system.ui.lib.application import gui_app ONROAD_DELAY = 2.5 # seconds -class MainState(IntEnum): - MAIN = 0 - SETTINGS = 1 - - class MiciMainLayout(Widget): def __init__(self): super().__init__() self._pm = messaging.PubMaster(['bookmarkButton']) - self._current_mode: MainState | None = None self._prev_onroad = False self._prev_standstill = False self._onroad_time_delay: float | None = None @@ -49,38 +42,31 @@ class MiciMainLayout(Widget): self._onroad_layout, ], spacing=0, pad_start=0, pad_end=0, scroll_indicator=False, edge_shadows=False) self._scroller.set_reset_scroll_at_show(False) + self._scroller.set_enabled(lambda: self.enabled) # for nav stack # Disable scrolling when onroad is interacting with bookmark self._scroller.set_scrolling_enabled(lambda: not self._onroad_layout.is_swiping_left()) - self._layouts = { - MainState.MAIN: self._scroller, - MainState.SETTINGS: self._settings_layout, - } - # Set callbacks self._setup_callbacks() - # Start onboarding if terms or training not completed + gui_app.push_widget(self) + + # Start onboarding if terms or training not completed, make sure to push after self self._onboarding_window = OnboardingWindow() if not self._onboarding_window.completed: - gui_app.set_modal_overlay(self._onboarding_window) + gui_app.push_widget(self._onboarding_window) def _setup_callbacks(self): - self._home_layout.set_callbacks(on_settings=self._on_settings_clicked) - self._settings_layout.set_callbacks(on_close=self._on_settings_closed) + self._home_layout.set_callbacks(on_settings=lambda: gui_app.push_widget(self._settings_layout)) self._onroad_layout.set_click_callback(lambda: self._scroll_to(self._home_layout)) - device.add_interactive_timeout_callback(self._set_mode_for_started) + device.add_interactive_timeout_callback(self._on_interactive_timeout) def _scroll_to(self, layout: Widget): layout_x = int(layout.rect.x) self._scroller.scroll_to(layout_x, smooth=True) def _render(self, _): - # Initial show event - if self._current_mode is None: - self._set_mode(MainState.MAIN) - if not self._setup: if self._alerts_layout.active_alerts() > 0: self._scroller.scroll_to(self._alerts_layout.rect.x) @@ -89,59 +75,50 @@ class MiciMainLayout(Widget): self._setup = True # Render - if self._current_mode == MainState.MAIN: - self._scroller.render(self._rect) - - elif self._current_mode == MainState.SETTINGS: - self._settings_layout.render(self._rect) + self._scroller.render(self._rect) self._handle_transitions() - def _set_mode(self, mode: MainState): - if mode != self._current_mode: - if self._current_mode is not None: - self._layouts[self._current_mode].hide_event() - self._layouts[mode].show_event() - self._current_mode = mode - def _handle_transitions(self): + # Don't pop if onboarding + if gui_app.get_active_widget() == self._onboarding_window: + return + if ui_state.started != self._prev_onroad: self._prev_onroad = ui_state.started + # onroad: after delay, pop nav stack and scroll to onroad + # offroad: immediately scroll to home, but don't pop nav stack (can stay in settings) if ui_state.started: self._onroad_time_delay = rl.get_time() else: - self._set_mode_for_started(True) + self._scroll_to(self._home_layout) - # delay so we show home for a bit after starting if self._onroad_time_delay is not None and rl.get_time() - self._onroad_time_delay >= ONROAD_DELAY: - self._set_mode_for_started(True) + gui_app.pop_widgets_to(self) + self._scroll_to(self._onroad_layout) self._onroad_time_delay = None + # When car leaves standstill, pop nav stack and scroll to onroad CS = ui_state.sm["carState"] if not CS.standstill and self._prev_standstill: - self._set_mode(MainState.MAIN) + gui_app.pop_widgets_to(self) self._scroll_to(self._onroad_layout) self._prev_standstill = CS.standstill - def _set_mode_for_started(self, onroad_transition: bool = False): + def _on_interactive_timeout(self): + # Don't pop if onboarding + if gui_app.get_active_widget() == self._onboarding_window: + return + if ui_state.started: - CS = ui_state.sm["carState"] - # Only go onroad if car starts or is not at a standstill - if not CS.standstill or onroad_transition: - self._set_mode(MainState.MAIN) + # Don't pop if at standstill + if not ui_state.sm["carState"].standstill: + gui_app.pop_widgets_to(self) self._scroll_to(self._onroad_layout) else: - # Stay in settings if car turns off while in settings - if not onroad_transition or self._current_mode != MainState.SETTINGS: - self._set_mode(MainState.MAIN) - self._scroll_to(self._home_layout) - - def _on_settings_clicked(self): - self._set_mode(MainState.SETTINGS) - - def _on_settings_closed(self): - self._set_mode(MainState.MAIN) + gui_app.pop_widgets_to(self) + self._scroll_to(self._home_layout) def _on_bookmark_clicked(self): user_bookmark = messaging.new_message('bookmarkButton') diff --git a/selfdrive/ui/mici/layouts/onboarding.py b/selfdrive/ui/mici/layouts/onboarding.py index a399d7679c..539074453c 100644 --- a/selfdrive/ui/mici/layouts/onboarding.py +++ b/selfdrive/ui/mici/layouts/onboarding.py @@ -7,7 +7,7 @@ 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 import Widget, NavWidget 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 @@ -42,7 +42,7 @@ class DriverCameraSetupDialog(DriverCameraDialog): gui_label(rect, tr("camera starting"), font_size=64, font_weight=FontWeight.BOLD, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER) rl.end_scissor_mode() - return -1 + return # Position dmoji on opposite side from driver is_rhd = self.driver_state_renderer.is_rhd @@ -55,7 +55,6 @@ class DriverCameraSetupDialog(DriverCameraDialog): self._draw_face_detection(rect) rl.end_scissor_mode() - return -1 class TrainingGuidePreDMTutorial(SetupTermsPage): @@ -367,9 +366,9 @@ class TrainingGuide(Widget): self._completed_callback() def _render(self, _): + rl.draw_rectangle_rec(self._rect, rl.BLACK) if self._step < len(self._steps): self._steps[self._step].render(self._rect) - return -1 class DeclinePage(Widget): @@ -438,9 +437,11 @@ class TermsPage(SetupTermsPage): )) -class OnboardingWindow(Widget): +class OnboardingWindow(NavWidget): def __init__(self): super().__init__() + self.set_back_enabled(False) + self._accepted_terms: bool = ui_state.params.get("HasAcceptedTerms") == terms_version self._training_done: bool = ui_state.params.get("CompletedTrainingVersion") == training_version @@ -473,7 +474,7 @@ class OnboardingWindow(Widget): def close(self): ui_state.params.put_bool("IsDriverViewEnabled", False) - gui_app.set_modal_overlay(None) + gui_app.pop_widget() def _on_terms_accepted(self): ui_state.params.put("HasAcceptedTerms", terms_version) @@ -490,4 +491,3 @@ class OnboardingWindow(Widget): self._training_guide.render(self._rect) elif self._state == OnboardingState.DECLINE: self._decline_page.render(self._rect) - return -1 diff --git a/selfdrive/ui/mici/layouts/settings/developer.py b/selfdrive/ui/mici/layouts/settings/developer.py index ad68d6ee94..383c7ef9b9 100644 --- a/selfdrive/ui/mici/layouts/settings/developer.py +++ b/selfdrive/ui/mici/layouts/settings/developer.py @@ -1,5 +1,4 @@ import pyray as rl -from collections.abc import Callable from openpilot.common.time_helpers import system_time_valid from openpilot.system.ui.widgets.scroller import Scroller @@ -13,9 +12,9 @@ from openpilot.selfdrive.ui.widgets.ssh_key import SshKeyAction class DeveloperLayoutMici(NavWidget): - def __init__(self, back_callback: Callable): + def __init__(self): super().__init__() - self.set_back_callback(back_callback) + self.set_back_callback(gui_app.pop_widget) def github_username_callback(username: str): if username: @@ -25,16 +24,16 @@ class DeveloperLayoutMici(NavWidget): self._ssh_keys_btn.set_value(username) else: dlg = BigDialog("", ssh_keys._error_message) - gui_app.set_modal_overlay(dlg) + gui_app.push_widget(dlg) def ssh_keys_callback(): github_username = ui_state.params.get("GithubUsername") or "" dlg = BigInputDialog("enter GitHub username...", github_username, confirm_callback=github_username_callback) if not system_time_valid(): dlg = BigDialog("Please connect to Wi-Fi to fetch your key", "") - gui_app.set_modal_overlay(dlg) + gui_app.push_widget(dlg) return - gui_app.set_modal_overlay(dlg) + gui_app.push_widget(dlg) txt_ssh = gui_app.texture("icons_mici/settings/developer/ssh.png", 56, 64) github_username = ui_state.params.get("GithubUsername") or "" diff --git a/selfdrive/ui/mici/layouts/settings/device.py b/selfdrive/ui/mici/layouts/settings/device.py index 1346267465..e1beae4fe3 100644 --- a/selfdrive/ui/mici/layouts/settings/device.py +++ b/selfdrive/ui/mici/layouts/settings/device.py @@ -28,7 +28,7 @@ class MiciFccModal(NavWidget): def __init__(self, file_path: str | None = None, text: str | None = None): super().__init__() - self.set_back_callback(lambda: gui_app.set_modal_overlay(None)) + self.set_back_callback(gui_app.pop_widget) self._content = HtmlRenderer(file_path=file_path, text=text) self._scroll_panel = GuiScrollPanel2(horizontal=False) self._fcc_logo = gui_app.texture("icons_mici/settings/device/fcc_logo.png", 76, 64) @@ -47,8 +47,6 @@ class MiciFccModal(NavWidget): rl.draw_texture_ex(self._fcc_logo, fcc_pos, 0.0, 1.0, rl.WHITE) - return -1 - def _engaged_confirmation_callback(callback: Callable, action_text: str): if not ui_state.engaged: @@ -74,10 +72,10 @@ def _engaged_confirmation_callback(callback: Callable, action_text: str): dlg: BigConfirmationDialogV2 | BigDialog = BigConfirmationDialogV2(f"slide to\n{action_text.lower()}", icon, red=red, exit_on_confirm=action_text == "reset", confirm_callback=confirm_callback) - gui_app.set_modal_overlay(dlg) + gui_app.push_widget(dlg) else: dlg = BigDialog(f"Disengage to {action_text}", "") - gui_app.set_modal_overlay(dlg) + gui_app.push_widget(dlg) class DeviceInfoLayoutMici(Widget): @@ -147,7 +145,7 @@ class PairBigButton(BigButton): dlg = BigDialog(tr("Device must be registered with the comma.ai backend to pair"), "") else: dlg = PairingDialog() - gui_app.set_modal_overlay(dlg) + gui_app.push_widget(dlg) UPDATER_TIMEOUT = 10.0 # seconds to wait for updater to respond @@ -173,7 +171,7 @@ class UpdateOpenpilotBigButton(BigButton): def _handle_mouse_release(self, mouse_pos: MousePos): if not system_time_valid(): dlg = BigDialog(tr("Please connect to Wi-Fi to update"), "") - gui_app.set_modal_overlay(dlg) + gui_app.push_widget(dlg) return self.set_enabled(False) @@ -268,12 +266,10 @@ class UpdateOpenpilotBigButton(BigButton): class DeviceLayoutMici(NavWidget): - def __init__(self, back_callback: Callable): + def __init__(self): super().__init__() self._fcc_dialog: HtmlModal | None = None - self._driver_camera: DriverCameraDialog | None = None - self._training_guide: TrainingGuide | None = None def power_off_callback(): ui_state.params.put_bool("DoShutdown", True) @@ -309,11 +305,11 @@ class DeviceLayoutMici(NavWidget): regulatory_btn.set_click_callback(self._on_regulatory) driver_cam_btn = BigButton("driver\ncamera preview", "", "icons_mici/settings/device/cameras.png") - driver_cam_btn.set_click_callback(self._show_driver_camera) + driver_cam_btn.set_click_callback(lambda: gui_app.push_widget(DriverCameraDialog())) driver_cam_btn.set_enabled(lambda: ui_state.is_offroad()) review_training_guide_btn = BigButton("review\ntraining guide", "", "icons_mici/settings/device/info.png") - review_training_guide_btn.set_click_callback(self._on_review_training_guide) + review_training_guide_btn.set_click_callback(lambda: gui_app.push_widget(TrainingGuide(completed_callback=gui_app.pop_widget))) review_training_guide_btn.set_enabled(lambda: ui_state.is_offroad()) self._scroller = Scroller([ @@ -330,7 +326,8 @@ class DeviceLayoutMici(NavWidget): ], snap_items=False) # Set up back navigation - self.set_back_callback(back_callback) + # TODO: can this somehow be generic in widgets/__init__.py or application.py? + self.set_back_callback(gui_app.pop_widget) # Hide power off button when onroad ui_state.add_offroad_transition_callback(self._offroad_transition) @@ -338,24 +335,11 @@ class DeviceLayoutMici(NavWidget): def _on_regulatory(self): if not self._fcc_dialog: self._fcc_dialog = MiciFccModal(os.path.join(BASEDIR, "selfdrive/assets/offroad/mici_fcc.html")) - gui_app.set_modal_overlay(self._fcc_dialog) + gui_app.push_widget(self._fcc_dialog) def _offroad_transition(self): self._power_off_btn.set_visible(ui_state.is_offroad()) - def _show_driver_camera(self): - if not self._driver_camera: - self._driver_camera = DriverCameraDialog() - gui_app.set_modal_overlay(self._driver_camera, callback=lambda result: setattr(self, '_driver_camera', None)) - - def _on_review_training_guide(self): - if not self._training_guide: - def completed_callback(): - gui_app.set_modal_overlay(None) - - self._training_guide = TrainingGuide(completed_callback=completed_callback) - gui_app.set_modal_overlay(self._training_guide, callback=lambda result: setattr(self, '_training_guide', None)) - def show_event(self): super().show_event() self._scroller.show_event() diff --git a/selfdrive/ui/mici/layouts/settings/firehose.py b/selfdrive/ui/mici/layouts/settings/firehose.py index d305906e13..a2288752ea 100644 --- a/selfdrive/ui/mici/layouts/settings/firehose.py +++ b/selfdrive/ui/mici/layouts/settings/firehose.py @@ -132,9 +132,6 @@ class FirehoseLayoutBase(Widget): y = self._draw_wrapped_text(x, y, w, tr(answer), gui_app.font(FontWeight.ROMAN), 32, self.LIGHT_GRAY) y += 20 - # return value not used by NavWidget - return -1 - def _draw_wrapped_text(self, x, y, width, text, font, font_size, color): wrapped = wrap_text(font, text, font_size, width) for line in wrapped: @@ -223,6 +220,6 @@ class FirehoseLayoutBase(Widget): class FirehoseLayout(FirehoseLayoutBase, NavWidget): BACK_TOUCH_AREA_PERCENTAGE = 0.1 - def __init__(self, back_callback): + def __init__(self): super().__init__() - self.set_back_callback(back_callback) + self.set_back_callback(gui_app.pop_widget) diff --git a/selfdrive/ui/mici/layouts/settings/network/__init__.py b/selfdrive/ui/mici/layouts/settings/network/__init__.py index cd0f4ee80f..9f49521c36 100644 --- a/selfdrive/ui/mici/layouts/settings/network/__init__.py +++ b/selfdrive/ui/mici/layouts/settings/network/__init__.py @@ -1,6 +1,4 @@ import pyray as rl -from enum import IntEnum -from collections.abc import Callable from openpilot.system.ui.widgets.scroller import Scroller from openpilot.selfdrive.ui.mici.layouts.settings.network.wifi_ui import WifiUIMici, WifiIcon @@ -13,21 +11,13 @@ from openpilot.system.ui.widgets import NavWidget from openpilot.system.ui.lib.wifi_manager import WifiManager, Network, MeteredType, ConnectStatus, normalize_ssid -class NetworkPanelType(IntEnum): - NONE = 0 - WIFI = 1 - - class NetworkLayoutMici(NavWidget): - def __init__(self, back_callback: Callable): + def __init__(self): super().__init__() - self._current_panel = NetworkPanelType.WIFI - self.set_back_enabled(lambda: self._current_panel == NetworkPanelType.NONE) - self._wifi_manager = WifiManager() self._wifi_manager.set_active(False) - self._wifi_ui = WifiUIMici(self._wifi_manager, back_callback=lambda: self._switch_to_panel(NetworkPanelType.NONE)) + self._wifi_ui = WifiUIMici(self._wifi_manager) self._wifi_manager.add_callbacks( networks_updated=self._on_network_updated, @@ -52,7 +42,7 @@ class NetworkLayoutMici(NavWidget): tethering_password = self._wifi_manager.tethering_password dlg = BigInputDialog("enter password...", tethering_password, minimum_length=8, confirm_callback=tethering_password_callback) - gui_app.set_modal_overlay(dlg) + gui_app.push_widget(dlg) txt_tethering = gui_app.texture("icons_mici/settings/network/tethering.png", 64, 54) self._tethering_password_btn = BigButton("tethering password", "", txt_tethering) @@ -79,7 +69,7 @@ class NetworkLayoutMici(NavWidget): self._wifi_full_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_full.png", 64, 47) self._wifi_button = BigButton("wi-fi", "not connected", self._wifi_slash_txt, scroll=True) - self._wifi_button.set_click_callback(lambda: self._switch_to_panel(NetworkPanelType.WIFI)) + self._wifi_button.set_click_callback(lambda: gui_app.push_widget(self._wifi_ui)) # ******** Advanced settings ******** # ******** Roaming toggle ******** @@ -111,7 +101,7 @@ class NetworkLayoutMici(NavWidget): self._wifi_manager.update_gsm_settings(roaming_enabled, ui_state.params.get("GsmApn") or "", metered) # Set up back navigation - self.set_back_callback(back_callback) + self.set_back_callback(gui_app.pop_widget) def _update_state(self): super()._update_state() @@ -146,14 +136,18 @@ class NetworkLayoutMici(NavWidget): def show_event(self): super().show_event() - self._current_panel = NetworkPanelType.NONE self._wifi_manager.set_active(True) self._scroller.show_event() + # Process wifi callbacks while at any point in the nav stack + gui_app.set_nav_stack_tick(self._wifi_manager.process_callbacks) + def hide_event(self): super().hide_event() self._wifi_manager.set_active(False) + gui_app.set_nav_stack_tick(None) + def _toggle_roaming(self, checked: bool): self._wifi_manager.update_gsm_settings(checked, ui_state.params.get("GsmApn") or "", ui_state.params.get_bool("GsmMetered")) @@ -169,7 +163,7 @@ class NetworkLayoutMici(NavWidget): current_apn = ui_state.params.get("GsmApn") or "" dlg = BigInputDialog("enter APN...", current_apn, minimum_length=0, confirm_callback=update_apn) - gui_app.set_modal_overlay(dlg) + gui_app.push_widget(dlg) def _toggle_cellular_metered(self, checked: bool): self._wifi_manager.update_gsm_settings(ui_state.params.get_bool("GsmRoaming"), ui_state.params.get("GsmApn") or "", checked) @@ -191,17 +185,5 @@ class NetworkLayoutMici(NavWidget): MeteredType.NO: 'unmetered' }.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() - elif self._current_panel == NetworkPanelType.WIFI: - self._wifi_ui.hide_event() - self._current_panel = panel_type - def _render(self, rect: rl.Rectangle): - self._wifi_manager.process_callbacks() - - if self._current_panel == NetworkPanelType.WIFI: - self._wifi_ui.render(rect) - else: - self._scroller.render(rect) + self._scroller.render(rect) diff --git a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py index 34e73e823e..1918362527 100644 --- a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py +++ b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py @@ -186,10 +186,9 @@ class ConnectButton(Widget): class ForgetButton(Widget): HORIZONTAL_MARGIN = 8 - def __init__(self, forget_network: Callable, open_network_manage_page): + def __init__(self, forget_network: Callable): super().__init__() self._forget_network = forget_network - self._open_network_manage_page = open_network_manage_page self._bg_txt = gui_app.texture("icons_mici/settings/network/new/forget_button.png", 100, 100) self._bg_pressed_txt = gui_app.texture("icons_mici/settings/network/new/forget_button_pressed.png", 100, 100) @@ -200,7 +199,7 @@ class ForgetButton(Widget): super()._handle_mouse_release(mouse_pos) dlg = BigConfirmationDialogV2("slide to forget", "icons_mici/settings/network/new/trash.png", red=True, confirm_callback=self._forget_network) - gui_app.set_modal_overlay(dlg, callback=self._open_network_manage_page) + gui_app.push_widget(dlg) def _render(self, _): bg_txt = self._bg_pressed_txt if self.is_pressed else self._bg_txt @@ -212,7 +211,7 @@ class ForgetButton(Widget): class NetworkInfoPage(NavWidget): - def __init__(self, wifi_manager, connect_callback: Callable, forget_callback: Callable, open_network_manage_page: Callable, + def __init__(self, wifi_manager, connect_callback: Callable, forget_callback: Callable, connecting_callback: Callable[[], str | None], connected_callback: Callable[[], str | None]): super().__init__() self._wifi_manager = wifi_manager @@ -220,8 +219,8 @@ class NetworkInfoPage(NavWidget): self.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) self._wifi_icon = WifiIcon() - self._forget_btn = ForgetButton(lambda: forget_callback(self._network.ssid) if self._network is not None else None, - open_network_manage_page) + self._forget_btn = ForgetButton(lambda: forget_callback(self._network.ssid) if self._network is not None else None) + self._forget_btn.set_enabled(lambda: self.enabled) # for stack self._connect_btn = ConnectButton() self._connect_btn.set_click_callback(lambda: connect_callback(self._network.ssid) if self._network is not None else None) @@ -230,7 +229,7 @@ class NetworkInfoPage(NavWidget): self._subtitle = UnifiedLabel("", 36, FontWeight.ROMAN, rl.Color(255, 255, 255, int(255 * 0.9 * 0.65)), alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE) - self.set_back_callback(lambda: gui_app.set_modal_overlay(None)) + self.set_back_callback(gui_app.pop_widget) # State self._network: Network | None = None @@ -249,11 +248,13 @@ class NetworkInfoPage(NavWidget): break else: # network disappeared, close page - gui_app.set_modal_overlay(None) + # TODO: pop_widgets_to, to close potentially open keyboard too + if gui_app.get_active_widget() == self: + gui_app.pop_widget() def _update_state(self): super()._update_state() - # Modal overlays stop main UI rendering, so we need to call here + # TODO: remove? only left for potential compatibility with setup/updater self._wifi_manager.process_callbacks() if self._network is None: @@ -271,7 +272,7 @@ class NetworkInfoPage(NavWidget): self._connect_btn.set_enabled(False) else: # saved or unknown self._connect_btn.set_label("connect") - self._connect_btn.set_enabled(True) + self._connect_btn.set_enabled(self.enabled) self._title.set_text(normalize_ssid(self._network.ssid)) if self._network.security_type == SecurityType.OPEN: @@ -336,17 +337,15 @@ class NetworkInfoPage(NavWidget): self._forget_btn.rect.height, )) - return -1 - class WifiUIMici(BigMultiOptionDialog): - def __init__(self, wifi_manager: WifiManager, back_callback: Callable): + def __init__(self, wifi_manager: WifiManager): super().__init__([], None) # Set up back navigation - self.set_back_callback(back_callback) + self.set_back_callback(gui_app.pop_widget) - self._network_info_page = NetworkInfoPage(wifi_manager, self._connect_to_network, wifi_manager.forget_connection, self._open_network_manage_page, + self._network_info_page = NetworkInfoPage(wifi_manager, self._connect_to_network, wifi_manager.forget_connection, lambda: wifi_manager.connecting_to_ssid, lambda: wifi_manager.connected_ssid) self._loading_animation = LoadingAnimation() @@ -370,11 +369,6 @@ class WifiUIMici(BigMultiOptionDialog): super().hide_event() self._scroller.hide_event() - def _open_network_manage_page(self, result=None): - if self._network_info_page._network is not None and self._network_info_page._network.ssid in self._networks: - self._network_info_page.update_networks(self._networks) - gui_app.set_modal_overlay(self._network_info_page) - def _on_network_updated(self, networks: list[Network]): self._networks = {network.ssid: network for network in networks} self._update_buttons() @@ -413,7 +407,7 @@ class WifiUIMici(BigMultiOptionDialog): if option in self._networks: self._network_info_page.set_current_network(self._networks[option]) - self._open_network_manage_page() + gui_app.push_widget(self._network_info_page) def _connect_to_network(self, ssid: str): network = self._networks.get(ssid) @@ -434,14 +428,7 @@ class WifiUIMici(BigMultiOptionDialog): hint = "wrong password..." if incorrect_password else "enter password..." dlg = BigInputDialog(hint, "", minimum_length=8, confirm_callback=lambda _password: self._connect_with_password(ssid, _password)) - - def on_close(result=None): - gui_app.set_modal_overlay_tick(None) - self._open_network_manage_page(result) - - # Process wifi callbacks while the keyboard is shown so forgotten clears connecting state - gui_app.set_modal_overlay_tick(self._wifi_manager.process_callbacks) - gui_app.set_modal_overlay(dlg, on_close) + gui_app.push_widget(dlg) def _render(self, _): super()._render(_) diff --git a/selfdrive/ui/mici/layouts/settings/settings.py b/selfdrive/ui/mici/layouts/settings/settings.py index a6f59a0389..2372104a00 100644 --- a/selfdrive/ui/mici/layouts/settings/settings.py +++ b/selfdrive/ui/mici/layouts/settings/settings.py @@ -1,7 +1,4 @@ import pyray as rl -from dataclasses import dataclass -from enum import IntEnum -from collections.abc import Callable from openpilot.common.params import Params from openpilot.system.ui.widgets.scroller import Scroller @@ -12,22 +9,7 @@ from openpilot.selfdrive.ui.mici.layouts.settings.device import DeviceLayoutMici from openpilot.selfdrive.ui.mici.layouts.settings.developer import DeveloperLayoutMici from openpilot.selfdrive.ui.mici.layouts.settings.firehose import FirehoseLayout from openpilot.system.ui.lib.application import gui_app, FontWeight -from openpilot.system.ui.widgets import Widget, NavWidget - - -class PanelType(IntEnum): - TOGGLES = 0 - NETWORK = 1 - DEVICE = 2 - DEVELOPER = 3 - USER_MANUAL = 4 - FIREHOSE = 5 - - -@dataclass -class PanelInfo: - name: str - instance: Widget +from openpilot.system.ui.widgets import NavWidget class SettingsBigButton(BigButton): @@ -39,19 +21,26 @@ class SettingsLayout(NavWidget): def __init__(self): super().__init__() self._params = Params() - self._current_panel = None # PanelType.DEVICE + toggles_panel = TogglesLayoutMici() toggles_btn = SettingsBigButton("toggles", "", "icons_mici/settings.png") - toggles_btn.set_click_callback(lambda: self._set_current_panel(PanelType.TOGGLES)) - network_btn = SettingsBigButton("network", "", "icons_mici/settings/network/wifi_strength_full.png", icon_size=(76, 56)) - network_btn.set_click_callback(lambda: self._set_current_panel(PanelType.NETWORK)) - device_btn = SettingsBigButton("device", "", "icons_mici/settings/device_icon.png", icon_size=(74, 60)) - device_btn.set_click_callback(lambda: self._set_current_panel(PanelType.DEVICE)) - developer_btn = SettingsBigButton("developer", "", "icons_mici/settings/developer_icon.png", icon_size=(64, 60)) - developer_btn.set_click_callback(lambda: self._set_current_panel(PanelType.DEVELOPER)) + toggles_btn.set_click_callback(lambda: gui_app.push_widget(toggles_panel)) + network_panel = NetworkLayoutMici() + network_btn = SettingsBigButton("network", "", "icons_mici/settings/network/wifi_strength_full.png", icon_size=(76, 56)) + network_btn.set_click_callback(lambda: gui_app.push_widget(network_panel)) + + device_panel = DeviceLayoutMici() + device_btn = SettingsBigButton("device", "", "icons_mici/settings/device_icon.png", icon_size=(74, 60)) + device_btn.set_click_callback(lambda: gui_app.push_widget(device_panel)) + + developer_panel = DeveloperLayoutMici() + developer_btn = SettingsBigButton("developer", "", "icons_mici/settings/developer_icon.png", icon_size=(64, 60)) + developer_btn.set_click_callback(lambda: gui_app.push_widget(developer_panel)) + + firehose_panel = FirehoseLayout() firehose_btn = SettingsBigButton("firehose", "", "icons_mici/settings/firehose.png", icon_size=(52, 62)) - firehose_btn.set_click_callback(lambda: self._set_current_panel(PanelType.FIREHOSE)) + firehose_btn.set_click_callback(lambda: gui_app.push_widget(firehose_panel)) self._scroller = Scroller([ toggles_btn, @@ -64,55 +53,17 @@ class SettingsLayout(NavWidget): ], snap_items=False) # Set up back navigation - self.set_back_callback(self.close_settings) - self.set_back_enabled(lambda: self._current_panel is None) - - self._panels = { - PanelType.TOGGLES: PanelInfo("Toggles", TogglesLayoutMici(back_callback=lambda: self._set_current_panel(None))), - PanelType.NETWORK: PanelInfo("Network", NetworkLayoutMici(back_callback=lambda: self._set_current_panel(None))), - PanelType.DEVICE: PanelInfo("Device", DeviceLayoutMici(back_callback=lambda: self._set_current_panel(None))), - PanelType.DEVELOPER: PanelInfo("Developer", DeveloperLayoutMici(back_callback=lambda: self._set_current_panel(None))), - PanelType.FIREHOSE: PanelInfo("Firehose", FirehoseLayout(back_callback=lambda: self._set_current_panel(None))), - } + self.set_back_callback(gui_app.pop_widget) self._font_medium = gui_app.font(FontWeight.MEDIUM) - # Callbacks - self._close_callback: Callable | None = None - def show_event(self): super().show_event() - self._set_current_panel(None) self._scroller.show_event() - if self._current_panel is not None: - self._panels[self._current_panel].instance.show_event() def hide_event(self): super().hide_event() - if self._current_panel is not None: - self._panels[self._current_panel].instance.hide_event() - - def set_callbacks(self, on_close: Callable): - self._close_callback = on_close + self._scroller.hide_event() def _render(self, rect: rl.Rectangle): - if self._current_panel is not None: - self._draw_current_panel() - else: - self._scroller.render(rect) - - def _draw_current_panel(self): - panel = self._panels[self._current_panel] - panel.instance.render(self._rect) - - def _set_current_panel(self, panel_type: PanelType | None): - if panel_type != self._current_panel: - if self._current_panel is not None: - self._panels[self._current_panel].instance.hide_event() - self._current_panel = panel_type - if self._current_panel is not None: - self._panels[self._current_panel].instance.show_event() - - def close_settings(self): - if self._close_callback: - self._close_callback() + self._scroller.render(rect) diff --git a/selfdrive/ui/mici/layouts/settings/toggles.py b/selfdrive/ui/mici/layouts/settings/toggles.py index c16504fac8..d6fb75a4d8 100644 --- a/selfdrive/ui/mici/layouts/settings/toggles.py +++ b/selfdrive/ui/mici/layouts/settings/toggles.py @@ -1,5 +1,4 @@ import pyray as rl -from collections.abc import Callable from cereal import log from openpilot.system.ui.widgets.scroller import Scroller @@ -13,9 +12,9 @@ PERSONALITY_TO_INT = log.LongitudinalPersonality.schema.enumerants class TogglesLayoutMici(NavWidget): - def __init__(self, back_callback: Callable): + def __init__(self): super().__init__() - self.set_back_callback(back_callback) + self.set_back_callback(gui_app.pop_widget) self._personality_toggle = BigMultiParamToggle("driving personality", "LongitudinalPersonality", ["aggressive", "standard", "relaxed"]) self._experimental_btn = BigParamControl("experimental mode", "ExperimentalMode") diff --git a/selfdrive/ui/mici/onroad/augmented_road_view.py b/selfdrive/ui/mici/onroad/augmented_road_view.py index 69bcca401d..99e33e8644 100644 --- a/selfdrive/ui/mici/onroad/augmented_road_view.py +++ b/selfdrive/ui/mici/onroad/augmented_road_view.py @@ -363,7 +363,7 @@ class AugmentedRoadView(CameraView): if __name__ == "__main__": gui_app.init_window("OnRoad Camera View") - road_camera_view = AugmentedRoadView(ROAD_CAM) + road_camera_view = AugmentedRoadView(lambda: None, stream_type=ROAD_CAM) print("***press space to switch camera view***") try: for _ in gui_app.render(): diff --git a/selfdrive/ui/mici/onroad/driver_camera_dialog.py b/selfdrive/ui/mici/onroad/driver_camera_dialog.py index bab3d6e6f1..26a5d132c6 100644 --- a/selfdrive/ui/mici/onroad/driver_camera_dialog.py +++ b/selfdrive/ui/mici/onroad/driver_camera_dialog.py @@ -34,8 +34,8 @@ class DriverCameraDialog(NavWidget): self._pm: messaging.PubMaster | None = None if not no_escape: # TODO: this can grow unbounded, should be given some thought - device.add_interactive_timeout_callback(lambda: gui_app.set_modal_overlay(None)) - self.set_back_callback(lambda: gui_app.set_modal_overlay(None)) + device.add_interactive_timeout_callback(gui_app.pop_widget) + self.set_back_callback(gui_app.pop_widget) self.set_back_enabled(not no_escape) # Load eye icons @@ -87,7 +87,7 @@ class DriverCameraDialog(NavWidget): alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER) rl.end_scissor_mode() self._publish_alert_sound(None) - return -1 + return driver_data = self._draw_face_detection(rect) if driver_data is not None: @@ -105,7 +105,7 @@ class DriverCameraDialog(NavWidget): self._render_dm_alerts(rect) rl.end_scissor_mode() - return -1 + return def _publish_alert_sound(self, dm_state): """Publish selfdriveState with only alertSound field set""" @@ -235,9 +235,9 @@ if __name__ == "__main__": gui_app.init_window("Driver Camera View (mici)") driver_camera_view = DriverCameraDialog() + gui_app.push_widget(driver_camera_view) try: for _ in gui_app.render(): ui_state.update() - driver_camera_view.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) finally: driver_camera_view.close() diff --git a/selfdrive/ui/mici/tests/test_widget_leaks.py b/selfdrive/ui/mici/tests/test_widget_leaks.py index 12fa608b36..c7f21bb2f8 100755 --- a/selfdrive/ui/mici/tests/test_widget_leaks.py +++ b/selfdrive/ui/mici/tests/test_widget_leaks.py @@ -37,14 +37,12 @@ KNOWN_LEAKS = { "openpilot.system.ui.widgets.confirm_dialog.ConfirmDialog", "openpilot.system.ui.widgets.label.Label", "openpilot.system.ui.widgets.button.Button", - "openpilot.selfdrive.ui.mici.widgets.dialog.BigDialog", "openpilot.system.ui.widgets.html_render.HtmlRenderer", "openpilot.system.ui.widgets.NavBar", "openpilot.system.ui.widgets.inputbox.InputBox", "openpilot.system.ui.widgets.scroller_tici.Scroller", "openpilot.system.ui.widgets.scroller.Scroller", "openpilot.system.ui.widgets.label.UnifiedLabel", - "openpilot.selfdrive.ui.mici.widgets.dialog.BigMultiOptionDialog", "openpilot.system.ui.widgets.mici_keyboard.MiciKeyboard", "openpilot.selfdrive.ui.mici.widgets.dialog.BigConfirmationDialogV2", "openpilot.system.ui.widgets.keyboard.Keyboard", diff --git a/selfdrive/ui/mici/widgets/dialog.py b/selfdrive/ui/mici/widgets/dialog.py index 6b4cb92c16..612f31b753 100644 --- a/selfdrive/ui/mici/widgets/dialog.py +++ b/selfdrive/ui/mici/widgets/dialog.py @@ -4,7 +4,7 @@ import pyray as rl from typing import Union from collections.abc import Callable from typing import cast -from openpilot.system.ui.widgets import Widget, NavWidget, DialogResult +from openpilot.system.ui.widgets import Widget, NavWidget from openpilot.system.ui.widgets.label import UnifiedLabel, gui_label from openpilot.system.ui.widgets.mici_keyboard import MiciKeyboard from openpilot.system.ui.lib.text_measure import measure_text_cached @@ -23,17 +23,8 @@ PADDING = 20 class BigDialogBase(NavWidget, abc.ABC): def __init__(self): super().__init__() - self._ret = DialogResult.NO_ACTION self.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) - self.set_back_callback(lambda: setattr(self, '_ret', DialogResult.CANCEL)) - - def _render(self, _) -> DialogResult: - """ - Allows `gui_app.set_modal_overlay(BigDialog(...))`. - The overlay runner keeps calling until result != NO_ACTION. - """ - - return self._ret + self.set_back_callback(gui_app.pop_widget) class BigDialog(BigDialogBase): @@ -44,7 +35,7 @@ class BigDialog(BigDialogBase): self._title = title self._description = description - def _render(self, _) -> DialogResult: + def _render(self, _): super()._render(_) # draw title @@ -74,8 +65,6 @@ class BigDialog(BigDialogBase): gui_label(desc_rect, desc_wrapped, 30, font_weight=FontWeight.MEDIUM, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER) - return self._ret - class BigConfirmationDialogV2(BigDialogBase): def __init__(self, title: str, icon: str, red: bool = False, @@ -91,22 +80,21 @@ class BigConfirmationDialogV2(BigDialogBase): self._slider = RedBigSlider(title, icon_txt, confirm_callback=self._on_confirm) else: self._slider = BigSlider(title, icon_txt, confirm_callback=self._on_confirm) - self._slider.set_enabled(lambda: not self._swiping_away) + self._slider.set_enabled(lambda: self.enabled and not self._swiping_away) # self.enabled for nav stack def _on_confirm(self): + if self._exit_on_confirm: + gui_app.pop_widget() if self._confirm_callback: self._confirm_callback() - if self._exit_on_confirm: - self._ret = DialogResult.CONFIRM def _update_state(self): super()._update_state() if self._swiping_away and not self._slider.confirmed: self._slider.reset() - def _render(self, _) -> DialogResult: + def _render(self, _): self._slider.render(self._rect) - return self._ret class BigInputDialog(BigDialogBase): @@ -124,6 +112,7 @@ class BigInputDialog(BigDialogBase): font_weight=FontWeight.MEDIUM) self._keyboard = MiciKeyboard() self._keyboard.set_text(default_text) + self._keyboard.set_enabled(lambda: self.enabled) # for nav stack self._minimum_length = minimum_length self._backspace_held_time: float | None = None @@ -140,9 +129,10 @@ class BigInputDialog(BigDialogBase): self._top_right_button_rect = rl.Rectangle(0, 0, 0, 0) def confirm_callback_wrapper(): - self._ret = DialogResult.CONFIRM + text = self._keyboard.text() + gui_app.pop_widget() if confirm_callback: - confirm_callback(self._keyboard.text()) + confirm_callback(text) self._confirm_callback = confirm_callback_wrapper def _update_state(self): @@ -238,8 +228,6 @@ class BigInputDialog(BigDialogBase): rl.draw_rectangle_lines_ex(self._top_right_button_rect, 1, rl.Color(0, 255, 0, 255)) rl.draw_rectangle_lines_ex(self._top_left_button_rect, 1, rl.Color(0, 255, 0, 255)) - return self._ret - def _handle_mouse_press(self, mouse_pos: MousePos): super()._handle_mouse_press(mouse_pos) # TODO: need to track where press was so enter and back can activate on release rather than press @@ -392,8 +380,6 @@ class BigMultiOptionDialog(BigDialogBase): super()._render(_) self._scroller.render(self._rect) - return self._ret - class BigDialogButton(BigButton): def __init__(self, text: str, value: str = "", icon: Union[str, rl.Texture] = "", description: str = ""): @@ -404,4 +390,4 @@ class BigDialogButton(BigButton): super()._handle_mouse_release(mouse_pos) dlg = BigDialog(self.text, self._description) - gui_app.set_modal_overlay(dlg) + gui_app.push_widget(dlg) diff --git a/selfdrive/ui/mici/widgets/pairing_dialog.py b/selfdrive/ui/mici/widgets/pairing_dialog.py index 88bab2d001..088d2a0b6f 100644 --- a/selfdrive/ui/mici/widgets/pairing_dialog.py +++ b/selfdrive/ui/mici/widgets/pairing_dialog.py @@ -19,7 +19,7 @@ class PairingDialog(NavWidget): def __init__(self): super().__init__() - self.set_back_callback(lambda: gui_app.set_modal_overlay(None)) + self.set_back_callback(gui_app.pop_widget) self._params = Params() self._qr_texture: rl.Texture | None = None self._last_qr_generation = float("-inf") @@ -72,7 +72,7 @@ class PairingDialog(NavWidget): if ui_state.prime_state.is_paired(): self._playing_dismiss_animation = True - def _render(self, rect: rl.Rectangle) -> int: + def _render(self, rect: rl.Rectangle): self._check_qr_refresh() self._render_qr_code() @@ -85,8 +85,6 @@ class PairingDialog(NavWidget): rl.draw_texture_ex(self._txt_pair, rl.Vector2(label_x, self._rect.y + self._rect.height - self._txt_pair.height - 16), 0.0, 1.0, rl.Color(255, 255, 255, int(255 * 0.35))) - return -1 - def _render_qr_code(self) -> None: if not self._qr_texture: error_font = gui_app.font(FontWeight.BOLD) @@ -107,10 +105,9 @@ class PairingDialog(NavWidget): if __name__ == "__main__": gui_app.init_window("pairing device") pairing = PairingDialog() + gui_app.push_widget(pairing) try: for _ in gui_app.render(): - result = pairing.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) - if result != -1: - break + pass finally: del pairing diff --git a/selfdrive/ui/onroad/augmented_road_view.py b/selfdrive/ui/onroad/augmented_road_view.py index f8fb589b61..17d89fbd50 100644 --- a/selfdrive/ui/onroad/augmented_road_view.py +++ b/selfdrive/ui/onroad/augmented_road_view.py @@ -219,7 +219,7 @@ class AugmentedRoadView(CameraView): if __name__ == "__main__": - gui_app.init_window("OnRoad Camera View", new_modal=True) + gui_app.init_window("OnRoad Camera View") road_camera_view = AugmentedRoadView(ROAD_CAM) gui_app.push_widget(road_camera_view) print("***press space to switch camera view***") diff --git a/selfdrive/ui/onroad/driver_camera_dialog.py b/selfdrive/ui/onroad/driver_camera_dialog.py index a4518a2520..e66e04b824 100644 --- a/selfdrive/ui/onroad/driver_camera_dialog.py +++ b/selfdrive/ui/onroad/driver_camera_dialog.py @@ -100,7 +100,7 @@ class DriverCameraDialog(CameraView): if __name__ == "__main__": - gui_app.init_window("Driver Camera View", new_modal=True) + gui_app.init_window("Driver Camera View") driver_camera_view = DriverCameraDialog() gui_app.push_widget(driver_camera_view) diff --git a/selfdrive/ui/tests/diff/replay.py b/selfdrive/ui/tests/diff/replay.py index 62a808209a..7ed7ce9364 100755 --- a/selfdrive/ui/tests/diff/replay.py +++ b/selfdrive/ui/tests/diff/replay.py @@ -38,7 +38,7 @@ def run_replay(variant: LayoutVariant) -> None: from openpilot.system.ui.lib.application import gui_app # Import here for accurate coverage from openpilot.selfdrive.ui.tests.diff.replay_script import build_script - gui_app.init_window("ui diff test", fps=FPS, new_modal=variant == "tizi") + gui_app.init_window("ui diff test", fps=FPS) # Dynamically import main layout based on variant if variant == "mici": @@ -46,7 +46,6 @@ def run_replay(variant: LayoutVariant) -> None: else: from openpilot.selfdrive.ui.layouts.main import MainLayout main_layout = MainLayout() - main_layout.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) pm = PubMaster(["deviceState", "pandaStates", "driverStateV2", "selfdriveState"]) script = build_script(pm, main_layout, variant) @@ -59,7 +58,7 @@ def run_replay(variant: LayoutVariant) -> None: rl.get_time = lambda: frame / FPS # Main loop to replay events and render frames - for should_render in gui_app.render(): + for _ in gui_app.render(): # Handle all events for the current frame while script_index < len(script) and script[script_index][0] == frame: _, event = script[script_index] @@ -82,9 +81,6 @@ def run_replay(variant: LayoutVariant) -> None: ui_state.update() - if should_render: - main_layout.render() - frame += 1 if script_index >= len(script): diff --git a/selfdrive/ui/tests/profile_onroad.py b/selfdrive/ui/tests/profile_onroad.py index fde4f25ffe..18194d7363 100755 --- a/selfdrive/ui/tests/profile_onroad.py +++ b/selfdrive/ui/tests/profile_onroad.py @@ -83,7 +83,6 @@ if __name__ == "__main__": gui_app.init_window("UI Profiling", fps=600) main_layout = MiciMainLayout() - main_layout.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) print("Running...") patch_submaster(message_chunks) @@ -95,15 +94,13 @@ if __name__ == "__main__": yuv_buffer_size = W * H + (W // 2) * (H // 2) * 2 yuv_data = np.random.randint(0, 256, yuv_buffer_size, dtype=np.uint8).tobytes() with cProfile.Profile() as pr: - for should_render in gui_app.render(): + for _ in gui_app.render(): if ui_state.sm.frame >= len(message_chunks): break if ui_state.sm.frame % 3 == 0: eof = int((ui_state.sm.frame % 3) * 0.05 * 1e9) vipc.send(VisionStreamType.VISION_STREAM_ROAD, yuv_data, ui_state.sm.frame % 3, eof, eof) ui_state.update() - if should_render: - main_layout.render() pr.dump_stats(f'{args.output}_deterministic.stats') rl.close_window() diff --git a/selfdrive/ui/ui.py b/selfdrive/ui/ui.py index 0e271c72db..e3cac2618e 100755 --- a/selfdrive/ui/ui.py +++ b/selfdrive/ui/ui.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 import os -import pyray as rl from openpilot.system.hardware import TICI from openpilot.common.realtime import config_realtime_process, set_core_affinity @@ -16,20 +15,15 @@ def main(): cores = {5, } config_realtime_process(0, 51) + gui_app.init_window("UI") if BIG_UI: - gui_app.init_window("UI", new_modal=True) - main_layout = MainLayout() + MainLayout() else: - gui_app.init_window("UI") - main_layout = MiciMainLayout() - main_layout.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) + MiciMainLayout() for should_render in gui_app.render(): ui_state.update() if should_render: - if not BIG_UI: - main_layout.render() - # reaffine after power save offlines our core if TICI and os.sched_getaffinity(0) != cores: try: diff --git a/selfdrive/ui/widgets/pairing_dialog.py b/selfdrive/ui/widgets/pairing_dialog.py index c07b2463f3..1ff550e4b6 100644 --- a/selfdrive/ui/widgets/pairing_dialog.py +++ b/selfdrive/ui/widgets/pairing_dialog.py @@ -160,7 +160,7 @@ class PairingDialog(Widget): if __name__ == "__main__": - gui_app.init_window("pairing device", new_modal=True) + gui_app.init_window("pairing device") pairing = PairingDialog() gui_app.push_widget(pairing) try: diff --git a/system/ui/lib/application.py b/system/ui/lib/application.py index f056fddbf0..7c0bd2bf3c 100644 --- a/system/ui/lib/application.py +++ b/system/ui/lib/application.py @@ -12,7 +12,6 @@ import subprocess from contextlib import contextmanager from collections.abc import Callable from collections import deque -from dataclasses import dataclass from enum import StrEnum from pathlib import Path from typing import NamedTuple @@ -115,12 +114,6 @@ def font_fallback(font: rl.Font) -> rl.Font: return font -@dataclass -class ModalOverlay: - overlay: object = None - callback: Callable | None = None - - class MousePos(NamedTuple): x: float y: float @@ -226,13 +219,9 @@ class GuiApplication: self._last_fps_log_time: float = time.monotonic() self._frame = 0 self._window_close_requested = False - self._modal_overlay = ModalOverlay() - self._modal_overlay_shown = False - self._modal_overlay_tick: Callable[[], None] | None = None - - # TODO: move over the entire ui and deprecate - self._new_modal = False self._nav_stack: list[object] = [] + self._nav_stack_tick: Callable[[], None] | None = None + self._nav_stack_widgets_to_render = 1 if self.big_ui() else 2 self._mouse = MouseState(self._scale) self._mouse_events: list[MouseEvent] = [] @@ -266,7 +255,7 @@ class GuiApplication: def request_close(self): self._window_close_requested = True - def init_window(self, title: str, fps: int = _DEFAULT_FPS, new_modal: bool = False): + def init_window(self, title: str, fps: int = _DEFAULT_FPS): with self._startup_profile_context(): def _close(sig, frame): self.close() @@ -274,8 +263,6 @@ class GuiApplication: signal.signal(signal.SIGINT, _close) atexit.register(self.close) - self._new_modal = new_modal - flags = rl.ConfigFlags.FLAG_MSAA_4X_HINT if ENABLE_VSYNC: flags |= rl.ConfigFlags.FLAG_VSYNC_HINT @@ -380,44 +367,48 @@ class GuiApplication: break def push_widget(self, widget: object): - assert self._new_modal + if widget in self._nav_stack: + cloudlog.warning("Widget already in stack, cannot push again!") + return # disable previous widget to prevent input processing if len(self._nav_stack) > 0: prev_widget = self._nav_stack[-1] + # TODO: change these to touch_valid prev_widget.set_enabled(False) self._nav_stack.append(widget) widget.show_event() def pop_widget(self): - assert self._new_modal - if len(self._nav_stack) < 2: - cloudlog.warning("At least one widget should remain on the stack, ignoring pop") + cloudlog.warning("At least one widget should remain on the stack, ignoring pop!") return # re-enable previous widget and pop current + # TODO: switch to touch_valid prev_widget = self._nav_stack[-2] prev_widget.set_enabled(True) widget = self._nav_stack.pop() widget.hide_event() - def set_modal_overlay(self, overlay, callback: Callable | None = None): - assert not self._new_modal, "set_modal_overlay is deprecated, use push_widget instead" + def pop_widgets_to(self, widget): + if widget not in self._nav_stack: + cloudlog.warning("Widget not in stack, cannot pop to it!") + return - if self._modal_overlay.overlay is not None: - if hasattr(self._modal_overlay.overlay, 'hide_event'): - self._modal_overlay.overlay.hide_event() + # pops all widgets after specified widget + while len(self._nav_stack) > 0 and self._nav_stack[-1] != widget: + self.pop_widget() - if self._modal_overlay.callback is not None: - self._modal_overlay.callback(-1) + def get_active_widget(self): + if len(self._nav_stack) > 0: + return self._nav_stack[-1] + return None - 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_nav_stack_tick(self, tick_function: Callable | None): + self._nav_stack_tick = tick_function def set_should_render(self, should_render: bool): self._should_render = should_render @@ -561,23 +552,15 @@ class GuiApplication: rl.begin_drawing() rl.clear_background(rl.BLACK) - if self._new_modal: - # Only render last widget - for widget in self._nav_stack[-1:]: - widget.render(rl.Rectangle(0, 0, self.width, self.height)) + # Allow a Widget to still run a function regardless of the stack depth + if self._nav_stack_tick is not None: + self._nav_stack_tick() - # Yield to allow caller to run non-rendering related code - yield True + # Only render top widgets + for widget in self._nav_stack[-self._nav_stack_widgets_to_render:]: + widget.render(rl.Rectangle(0, 0, self.width, self.height)) - else: - # 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 + yield True if self._render_texture: rl.end_texture_mode() @@ -631,33 +614,6 @@ class GuiApplication: def height(self): return self._height - def _handle_modal_overlay(self) -> bool: - if self._modal_overlay.overlay: - if hasattr(self._modal_overlay.overlay, 'render'): - result = self._modal_overlay.overlay.render(rl.Rectangle(0, 0, self.width, self.height)) - elif callable(self._modal_overlay.overlay): - result = self._modal_overlay.overlay() - else: - raise Exception - - # Send show event to Widget - if not self._modal_overlay_shown and hasattr(self._modal_overlay.overlay, 'show_event'): - self._modal_overlay.overlay.show_event() - self._modal_overlay_shown = True - - if result >= 0: - # Clear the overlay and execute the callback - original_modal = self._modal_overlay - self._modal_overlay = ModalOverlay() - if hasattr(original_modal.overlay, 'hide_event'): - original_modal.overlay.hide_event() - if original_modal.callback is not None: - original_modal.callback(result) - return True - else: - self._modal_overlay_shown = False - return False - def _load_fonts(self): for font_weight_file in FontWeight: with as_file(FONT_DIR) as fspath: diff --git a/system/ui/mici_reset.py b/system/ui/mici_reset.py index 925afd7d10..357e672931 100755 --- a/system/ui/mici_reset.py +++ b/system/ui/mici_reset.py @@ -150,10 +150,10 @@ def main(): if mode == ResetMode.FORMAT: reset.start_reset() - for should_render in gui_app.render(): - if should_render: - if not reset.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height)): - break + gui_app.push_widget(reset) + + for _ in gui_app.render(): + pass if __name__ == "__main__": diff --git a/system/ui/mici_setup.py b/system/ui/mici_setup.py index b5c0d05281..aa6f54c508 100755 --- a/system/ui/mici_setup.py +++ b/system/ui/mici_setup.py @@ -19,7 +19,7 @@ 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.system.ui.lib.scroll_panel2 import GuiScrollPanel2 -from openpilot.system.ui.widgets import Widget, DialogResult +from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.button import (IconButton, SmallButton, WideRoundedButton, SmallerRoundedButton, SmallCircleIconButton, WidishRoundedButton, SmallRedPillButton, FullRoundedButton) @@ -96,10 +96,9 @@ class SetupState(IntEnum): NETWORK_SETUP = 1 NETWORK_SETUP_CUSTOM_SOFTWARE = 2 SOFTWARE_SELECTION = 3 - CUSTOM_SOFTWARE = 4 - DOWNLOADING = 5 - DOWNLOAD_FAILED = 6 - CUSTOM_SOFTWARE_WARNING = 7 + DOWNLOADING = 4 + DOWNLOAD_FAILED = 5 + CUSTOM_SOFTWARE_WARNING = 6 class StartPage(Widget): @@ -128,7 +127,9 @@ class SoftwareSelectionPage(Widget): super().__init__() self._openpilot_slider = LargerSlider("slide to use\nopenpilot", use_openpilot_callback) + self._openpilot_slider.set_enabled(lambda: self.enabled) self._custom_software_slider = LargerSlider("slide to use\ncustom software", use_custom_software_callback, green=False) + self._custom_software_slider.set_enabled(lambda: self.enabled) def reset(self): self._openpilot_slider.reset() @@ -390,9 +391,11 @@ class FailedPage(Widget): self._reboot_button = SmallRedPillButton("reboot") self._reboot_button.set_click_callback(reboot_callback) + self._reboot_button.set_enabled(lambda: self.enabled) # for nav stack self._retry_button = WideRoundedButton("retry") self._retry_button.set_click_callback(retry_callback) + self._retry_button.set_enabled(lambda: self.enabled) # for nav stack def set_reason(self, reason: str): self._reason_label.set_text(reason) @@ -427,15 +430,10 @@ class FailedPage(Widget): )) -class NetworkSetupState(IntEnum): - MAIN = 0 - WIFI_PANEL = 1 - - class NetworkSetupPage(Widget): def __init__(self, wifi_manager, continue_callback: Callable, back_callback: Callable): super().__init__() - self._wifi_ui = WifiUIMici(wifi_manager, back_callback=lambda: self.set_state(NetworkSetupState.MAIN)) + self._wifi_ui = WifiUIMici(wifi_manager) self._no_wifi_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_slash.png", 58, 50) self._wifi_full_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_full.png", 58, 50) @@ -445,78 +443,54 @@ class NetworkSetupPage(Widget): back_txt = gui_app.texture("icons_mici/setup/back_new.png", 37, 32) self._back_button = SmallCircleIconButton(back_txt) self._back_button.set_click_callback(back_callback) + self._back_button.set_enabled(lambda: self.enabled) # for nav stack self._wifi_button = SmallerRoundedButton("wifi") - self._wifi_button.set_click_callback(lambda: self.set_state(NetworkSetupState.WIFI_PANEL)) + self._wifi_button.set_click_callback(lambda: gui_app.push_widget(self._wifi_ui)) + self._wifi_button.set_enabled(lambda: self.enabled) self._continue_button = WidishRoundedButton("continue") self._continue_button.set_enabled(False) self._continue_button.set_click_callback(continue_callback) - self._state = NetworkSetupState.MAIN - self._prev_has_internet = False - - def set_state(self, state: NetworkSetupState): - if self._state == NetworkSetupState.WIFI_PANEL and state != NetworkSetupState.WIFI_PANEL: - self._wifi_ui.hide_event() - self._state = state - if state == NetworkSetupState.WIFI_PANEL: - self._wifi_ui.show_event() - def set_has_internet(self, has_internet: bool): if has_internet: self._network_header.set_title("connected to internet") self._network_header.set_icon(self._wifi_full_txt) - self._continue_button.set_enabled(True) + self._continue_button.set_enabled(self.enabled) else: self._network_header.set_title(self._waiting_text) 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 - - def hide_event(self): - super().hide_event() - if self._state == NetworkSetupState.WIFI_PANEL: - self._wifi_ui.hide_event() - def _render(self, _): - if self._state == NetworkSetupState.MAIN: - self._network_header.render(rl.Rectangle( - self._rect.x + 16, - self._rect.y + 16, - self._rect.width - 32, - self._network_header.rect.height, - )) + self._network_header.render(rl.Rectangle( + self._rect.x + 16, + self._rect.y + 16, + self._rect.width - 32, + self._network_header.rect.height, + )) - 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._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._wifi_button.render(rl.Rectangle( - self._rect.x + 8 + self._back_button.rect.width + 10, - self._rect.y + self._rect.height - self._wifi_button.rect.height, - self._wifi_button.rect.width, - self._wifi_button.rect.height, - )) + self._wifi_button.render(rl.Rectangle( + self._rect.x + 8 + self._back_button.rect.width + 10, + self._rect.y + self._rect.height - self._wifi_button.rect.height, + self._wifi_button.rect.width, + self._wifi_button.rect.height, + )) - self._continue_button.render(rl.Rectangle( - self._rect.x + self._rect.width - self._continue_button.rect.width - 8, - self._rect.y + self._rect.height - self._continue_button.rect.height, - self._continue_button.rect.width, - self._continue_button.rect.height, - )) - else: - self._wifi_ui.render(self._rect) + self._continue_button.render(rl.Rectangle( + self._rect.x + self._rect.width - self._continue_button.rect.width - 8, + self._rect.y + self._rect.height - self._continue_button.rect.height, + self._continue_button.rect.width, + self._continue_button.rect.height, + )) class Setup(Widget): @@ -533,28 +507,33 @@ class Setup(Widget): self._network_monitor = NetworkConnectivityMonitor() self._network_monitor.start() self._prev_has_internet = False - gui_app.set_modal_overlay_tick(self._modal_overlay_tick) + gui_app.set_nav_stack_tick(self._nav_stack_tick) self._start_page = StartPage() self._start_page.set_click_callback(self._getting_started_button_callback) self._network_setup_page = NetworkSetupPage(self._wifi_manager, self._network_setup_continue_button_callback, self._network_setup_back_button_callback) + # TODO: change these to touch_valid + self._network_setup_page.set_enabled(lambda: self.enabled) # for nav stack self._software_selection_page = SoftwareSelectionPage(self._software_selection_continue_button_callback, self._software_selection_custom_software_button_callback) + self._software_selection_page.set_enabled(lambda: self.enabled) # for nav stack self._download_failed_page = FailedPage(HARDWARE.reboot, self._download_failed_startover_button_callback) + self._download_failed_page.set_enabled(lambda: self.enabled) # for nav stack self._custom_software_warning_page = CustomSoftwareWarningPage(self._software_selection_custom_software_continue, self._custom_software_warning_back_button_callback) + self._custom_software_warning_page.set_enabled(lambda: self.enabled) # for nav stack self._downloading_page = DownloadingPage() - def _modal_overlay_tick(self): + def _nav_stack_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) + gui_app.pop_widgets_to(self) self._prev_has_internet = has_internet def _update_state(self): @@ -582,8 +561,6 @@ class Setup(Widget): self._software_selection_page.render(rect) elif self.state == SetupState.CUSTOM_SOFTWARE_WARNING: self._custom_software_warning_page.render(rect) - elif self.state == SetupState.CUSTOM_SOFTWARE: - self.render_custom_software() elif self.state == SetupState.DOWNLOADING: self.render_downloading(rect) elif self.state == SetupState.DOWNLOAD_FAILED: @@ -614,14 +591,19 @@ class Setup(Widget): if self.state == SetupState.NETWORK_SETUP: self.download(OPENPILOT_URL) elif self.state == SetupState.NETWORK_SETUP_CUSTOM_SOFTWARE: - self._set_state(SetupState.CUSTOM_SOFTWARE) + def handle_keyboard_result(text): + url = text.strip() + if url: + self.download(url) + + keyboard = BigInputDialog("custom software URL", confirm_callback=handle_keyboard_result) + gui_app.push_widget(keyboard) def close(self): self._network_monitor.stop() def render_network_setup(self, rect: rl.Rectangle): has_internet = self._network_monitor.network_connected.is_set() - self._prev_has_internet = has_internet self._network_setup_page.set_has_internet(has_internet) self._network_setup_page.render(rect) @@ -629,19 +611,6 @@ class Setup(Widget): self._downloading_page.set_progress(self.download_progress) self._downloading_page.render(rect) - def render_custom_software(self): - def handle_keyboard_result(text): - url = text.strip() - if url: - self.download(url) - - def handle_keyboard_exit(result): - if result == DialogResult.CANCEL: - self._set_state(SetupState.SOFTWARE_SELECTION) - - keyboard = BigInputDialog("custom software URL...", confirm_callback=handle_keyboard_result) - gui_app.set_modal_overlay(keyboard, callback=handle_keyboard_exit) - def use_openpilot(self): if os.path.isdir(INSTALL_PATH) and os.path.isfile(VALID_CACHE_PATH): os.remove(VALID_CACHE_PATH) @@ -738,9 +707,9 @@ def main(): try: gui_app.init_window("Setup") setup = Setup() - for should_render in gui_app.render(): - if should_render: - setup.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) + gui_app.push_widget(setup) + for _ in gui_app.render(): + pass setup.close() except Exception as e: print(f"Setup error: {e}") diff --git a/system/ui/mici_updater.py b/system/ui/mici_updater.py index 5c8748783a..5de72ac8c4 100755 --- a/system/ui/mici_updater.py +++ b/system/ui/mici_updater.py @@ -38,6 +38,7 @@ class Updater(Widget): self._network_setup_page = NetworkSetupPage(self._wifi_manager, self._network_setup_continue_callback, self._network_setup_back_callback) + self._network_setup_page.set_enabled(lambda: self.enabled) # for nav stack self._network_monitor = NetworkConnectivityMonitor() self._network_monitor.start() @@ -182,9 +183,9 @@ def main(): try: gui_app.init_window("System Update") updater = Updater(updater_path, manifest_path) - for should_render in gui_app.render(): - if should_render: - updater.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) + gui_app.push_widget(updater) + for _ in gui_app.render(): + pass updater.close() except Exception as e: print(f"Updater error: {e}") diff --git a/system/ui/tici_reset.py b/system/ui/tici_reset.py index b22b240850..23f6b344ec 100755 --- a/system/ui/tici_reset.py +++ b/system/ui/tici_reset.py @@ -116,7 +116,7 @@ def main(): elif sys.argv[1] == "--format": mode = ResetMode.FORMAT - gui_app.init_window("System Reset", 20, new_modal=True) + gui_app.init_window("System Reset", 20) reset = Reset(mode) if mode == ResetMode.FORMAT: diff --git a/system/ui/tici_setup.py b/system/ui/tici_setup.py index bb70b75b20..39f95cc8a0 100755 --- a/system/ui/tici_setup.py +++ b/system/ui/tici_setup.py @@ -436,7 +436,7 @@ class Setup(Widget): def main(): try: - gui_app.init_window("Setup", 20, new_modal=True) + gui_app.init_window("Setup", 20) setup = Setup() gui_app.push_widget(setup) for _ in gui_app.render(): diff --git a/system/ui/tici_updater.py b/system/ui/tici_updater.py index c040e1a404..9824638cd0 100755 --- a/system/ui/tici_updater.py +++ b/system/ui/tici_updater.py @@ -160,7 +160,7 @@ def main(): manifest_path = sys.argv[2] try: - gui_app.init_window("System Update", new_modal=True) + gui_app.init_window("System Update") gui_app.push_widget(Updater(updater_path, manifest_path)) for _ in gui_app.render(): pass diff --git a/system/ui/widgets/__init__.py b/system/ui/widgets/__init__.py index b3b1276a58..25c908c72c 100644 --- a/system/ui/widgets/__init__.py +++ b/system/ui/widgets/__init__.py @@ -108,6 +108,10 @@ class Widget(abc.ABC): # Keep track of whether mouse down started within the widget's rectangle if self.enabled and self.__was_awake: self._process_mouse_events() + else: + # TODO: ideally we emit release events when going disabled + self.__is_pressed = [False] * MAX_TOUCH_SLOTS + self.__tracking_is_pressed = [False] * MAX_TOUCH_SLOTS self.__was_awake = device.awake @@ -181,6 +185,7 @@ class Widget(abc.ABC): def show_event(self): """Optionally handle show event. Parent must manually call this""" + # TODO: iterate through all child objects, check for subclassing from Widget/Layout (Scroller) def hide_event(self): """Optionally handle hide event. Parent must manually call this""" @@ -261,6 +266,7 @@ class NavWidget(Widget, abc.ABC): self._back_callback = callback def _handle_mouse_event(self, mouse_event: MouseEvent) -> None: + # FIXME: disabling this widget on new push_widget still causes this widget to track mouse events without mouse down super()._handle_mouse_event(mouse_event) if not self.back_enabled: @@ -320,13 +326,14 @@ class NavWidget(Widget, abc.ABC): if not self._set_up: self._set_up = True if hasattr(self, '_scroller'): + # TODO: use touch_valid original_enabled = self._scroller._enabled - self._scroller.set_enabled(lambda: not self._swiping_away and (original_enabled() if callable(original_enabled) else - original_enabled)) + self._scroller.set_enabled(lambda: self.enabled and not self._swiping_away and (original_enabled() if callable(original_enabled) else + original_enabled)) elif hasattr(self, '_scroll_panel'): original_enabled = self._scroll_panel.enabled - self._scroll_panel.set_enabled(lambda: not self._swiping_away and (original_enabled() if callable(original_enabled) else - original_enabled)) + self._scroll_panel.set_enabled(lambda: self.enabled and not self._swiping_away and (original_enabled() if callable(original_enabled) else + original_enabled)) if self._trigger_animate_in: self._pos_filter.x = self._rect.height @@ -336,6 +343,10 @@ class NavWidget(Widget, abc.ABC): new_y = 0.0 + if not self.enabled: + self._back_button_start_pos = None + + # TODO: why is this not in handle_mouse_event? have to hack above if self._back_button_start_pos is not None: last_mouse_event = gui_app.last_mouse_event # push entire widget as user drags it away @@ -363,6 +374,14 @@ class NavWidget(Widget, abc.ABC): self.set_position(self._rect.x, new_y) + def _layout(self): + # Dim whatever is behind this widget, fading with position (runs after _update_state so position is correct) + overlay_alpha = int(200 * max(0.0, min(1.0, 1.0 - self._rect.y / self._rect.height))) if self._rect.height > 0 else 0 + rl.draw_rectangle(0, 0, int(self._rect.width), int(self._rect.height), rl.Color(0, 0, 0, overlay_alpha)) + + bounce_height = 20 + rl.draw_rectangle(int(self._rect.x), int(self._rect.y), int(self._rect.width), int(self._rect.height + bounce_height), rl.BLACK) + def render(self, rect: rl.Rectangle | None = None) -> bool | int | None: ret = super().render(rect) @@ -379,10 +398,6 @@ class NavWidget(Widget, abc.ABC): else: self._nav_bar_y_filter.update(NAV_BAR_MARGIN) - # draw black above widget when dismissing - if self._rect.y > 0: - rl.draw_rectangle(int(self._rect.x), 0, int(self._rect.width), int(self._rect.y), rl.BLACK) - self._nav_bar.set_position(bar_x, round(self._nav_bar_y_filter.x)) self._nav_bar.render() diff --git a/system/ui/widgets/keyboard.py b/system/ui/widgets/keyboard.py index 531725688a..49c59a431f 100644 --- a/system/ui/widgets/keyboard.py +++ b/system/ui/widgets/keyboard.py @@ -272,7 +272,7 @@ if __name__ == "__main__": print("Canceled") gui_app.request_close() - gui_app.init_window("Keyboard", new_modal=True) + gui_app.init_window("Keyboard") keyboard = Keyboard(min_text_size=8, show_password_toggle=True, callback=callback) keyboard.set_title("Keyboard Input", "Type your text below") diff --git a/system/ui/widgets/mici_keyboard.py b/system/ui/widgets/mici_keyboard.py index 6d2e08e053..59a2451387 100644 --- a/system/ui/widgets/mici_keyboard.py +++ b/system/ui/widgets/mici_keyboard.py @@ -61,7 +61,7 @@ class Key(Widget): self._x_filter.x = x self._y_filter.x = local_y # keep track of original position so dragging around feels consistent. also move touch area down a bit - self.original_position = rl.Vector2(x, y + KEY_TOUCH_AREA_OFFSET) + self.original_position = rl.Vector2(x, local_y + KEY_TOUCH_AREA_OFFSET) self._position_initialized = True if not smooth: @@ -227,6 +227,8 @@ class MiciKeyboard(Widget): for current_row, row in zip(self._current_keys, keys, strict=False): # not all layouts have the same number of keys for current_key, key in zip_repeat(current_row, row): + # reset parent rect for new keys + key.set_parent_rect(self._rect) current_pos = current_key.get_position() key.set_position(current_pos[0], current_pos[1], smooth=False) @@ -264,7 +266,8 @@ class MiciKeyboard(Widget): for key in row: mouse_pos = gui_app.last_mouse_event.pos # approximate distance for comparison is accurate enough - dist = abs(key.original_position.x - mouse_pos.x) + abs(key.original_position.y - mouse_pos.y) + # use local y coords so parent widget offset (e.g. during NavWidget animate-in) doesn't affect hit testing + dist = abs(key.original_position.x - mouse_pos.x) + abs(key.original_position.y - (mouse_pos.y - self._rect.y)) if dist < closest_key[1]: if self._closest_key[0] is None or key is self._closest_key[0] or dist < self._closest_key[1] - KEY_DRAG_HYSTERESIS: closest_key = (key, dist) diff --git a/system/ui/widgets/network.py b/system/ui/widgets/network.py index fcd56607c0..668565a033 100644 --- a/system/ui/widgets/network.py +++ b/system/ui/widgets/network.py @@ -481,7 +481,7 @@ class WifiManagerUI(Widget): def main(): - gui_app.init_window("Wi-Fi Manager", new_modal=True) + gui_app.init_window("Wi-Fi Manager") gui_app.push_widget(WifiManagerUI(WifiManager())) for _ in gui_app.render(): From 4e8a4f87f406f65d5f6e9a924a547a0f48f0f727 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Fri, 20 Feb 2026 22:36:32 -0800 Subject: [PATCH 126/311] pj: handle no qt --- tools/plotjuggler/test_plotjuggler.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tools/plotjuggler/test_plotjuggler.py b/tools/plotjuggler/test_plotjuggler.py index a2c509f943..26bad25c3e 100644 --- a/tools/plotjuggler/test_plotjuggler.py +++ b/tools/plotjuggler/test_plotjuggler.py @@ -1,9 +1,12 @@ import os import glob +import shutil import signal import subprocess import time +import pytest + from openpilot.common.basedir import BASEDIR from openpilot.common.timeout import Timeout from openpilot.tools.plotjuggler.juggle import DEMO_ROUTE, install @@ -12,6 +15,7 @@ PJ_DIR = os.path.join(BASEDIR, "tools/plotjuggler") class TestPlotJuggler: + @pytest.mark.skipif(not shutil.which('qmake'), reason="Qt not installed") def test_demo(self): install() From 468a50b6f6470c615e9f40a343f9b9f51034bba3 Mon Sep 17 00:00:00 2001 From: Trey Moen <50057480+greatgitsby@users.noreply.github.com> Date: Fri, 20 Feb 2026 23:38:51 -0700 Subject: [PATCH 127/311] fix: adb ssh on mac (#37298) * fix: adb ssh on mac * revert --- tools/scripts/adb_ssh.sh | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/tools/scripts/adb_ssh.sh b/tools/scripts/adb_ssh.sh index ad65693722..4527a0296d 100755 --- a/tools/scripts/adb_ssh.sh +++ b/tools/scripts/adb_ssh.sh @@ -2,7 +2,9 @@ set -euo pipefail # Forward all openpilot service ports -mapfile -t SERVICE_PORTS < <(python3 - <<'PY' +while IFS=' ' read -r name port; do + adb forward "tcp:${port}" "tcp:${port}" > /dev/null +done < <(python3 - <<'PY' from cereal.services import SERVICE_LIST FNV_PRIME = 0x100000001b3 @@ -29,12 +31,6 @@ for name, port in sorted(ports): PY ) -for entry in "${SERVICE_PORTS[@]}"; do - name="${entry% *}" - port="${entry##* }" - adb forward "tcp:${port}" "tcp:${port}" > /dev/null -done - # Forward SSH port first for interactive shell access. adb forward tcp:2222 tcp:22 From a694d051b390204ff8c9154bd930d2d8f7efd721 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Fri, 20 Feb 2026 22:39:03 -0800 Subject: [PATCH 128/311] trim unused ubuntu deps (#37297) * trim unused ubuntu deps * mac cleanup --- tools/install_ubuntu_dependencies.sh | 7 ------- tools/mac_setup.sh | 2 -- 2 files changed, 9 deletions(-) diff --git a/tools/install_ubuntu_dependencies.sh b/tools/install_ubuntu_dependencies.sh index a20176419f..aa98102d49 100755 --- a/tools/install_ubuntu_dependencies.sh +++ b/tools/install_ubuntu_dependencies.sh @@ -42,21 +42,15 @@ function install_ubuntu_common_requirements() { ffmpeg \ libavformat-dev \ libavcodec-dev \ - libavdevice-dev \ libavutil-dev \ - libavfilter-dev \ libbz2-dev \ libeigen3-dev \ - libffi-dev \ libgles2-mesa-dev \ - libglfw3-dev \ - libglib2.0-0 \ libjpeg-dev \ libncurses5-dev \ libusb-1.0-0-dev \ libzmq3-dev \ libzstd-dev \ - libsqlite3-dev \ portaudio19-dev \ gettext } @@ -66,7 +60,6 @@ function install_ubuntu_lts_latest_requirements() { install_ubuntu_common_requirements $SUDO apt-get install -y --no-install-recommends \ - g++-12 \ python3-dev \ python3-venv } diff --git a/tools/mac_setup.sh b/tools/mac_setup.sh index a5a6eed040..3f13cbe74a 100755 --- a/tools/mac_setup.sh +++ b/tools/mac_setup.sh @@ -32,9 +32,7 @@ brew "capnp" brew "coreutils" brew "eigen" brew "ffmpeg" -brew "glfw" brew "libusb" -brew "libtool" brew "llvm" brew "zeromq" cask "gcc-arm-embedded" From 06298b28f1c08aa3bdfb7cc91cc3c4f9a1c65d84 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sat, 21 Feb 2026 10:17:51 -0800 Subject: [PATCH 129/311] ty: fix unused warnings --- system/ubloxd/binary_struct.py | 4 ++-- tools/clip/run.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/system/ubloxd/binary_struct.py b/system/ubloxd/binary_struct.py index 7b229620a2..c144bd5696 100644 --- a/system/ubloxd/binary_struct.py +++ b/system/ubloxd/binary_struct.py @@ -174,7 +174,7 @@ class BinaryStruct: if not is_dataclass(cls): dataclass(init=False)(cls) fields = list(getattr(cls, '__annotations__', {}).items()) - cls.__binary_fields__ = fields # type: ignore[attr-defined] + cls.__binary_fields__ = fields @classmethod def _read(inner_cls, reader: BinaryReader): @@ -184,7 +184,7 @@ class BinaryStruct: setattr(obj, name, value) return obj - cls._read = _read # type: ignore[attr-defined] + cls._read = _read @classmethod def from_bytes(cls: type[T], data: bytes) -> T: diff --git a/tools/clip/run.py b/tools/clip/run.py index d338f097d6..5711cafa59 100755 --- a/tools/clip/run.py +++ b/tools/clip/run.py @@ -85,7 +85,7 @@ def _parse_and_chunk_segment(args: tuple) -> list[dict]: if not messages: return [] - dt_ns, chunks, current, next_time = 1e9 / fps, [], {}, messages[0].logMonoTime + 1e9 / fps # type: ignore[var-annotated] + dt_ns, chunks, current, next_time = 1e9 / fps, [], {}, messages[0].logMonoTime + 1e9 / fps for msg in messages: if msg.logMonoTime >= next_time: chunks.append(current) @@ -159,7 +159,7 @@ def iter_segment_frames(camera_paths, start_time, end_time, fps=20, use_qcam=Fal seg_frames = FrameReader(path, pix_fmt="nv12") assert seg_frames is not None - frame = seg_frames[local_idx] if use_qcam else seg_frames.get(local_idx) # type: ignore[index, union-attr] + frame = seg_frames[local_idx] if use_qcam else seg_frames.get(local_idx) yield global_idx, frame @@ -286,7 +286,7 @@ def clip(route: Route, output: str, start: int, end: int, headless: bool = True, if big: from openpilot.selfdrive.ui.onroad.augmented_road_view import AugmentedRoadView else: - from openpilot.selfdrive.ui.mici.onroad.augmented_road_view import AugmentedRoadView # type: ignore[assignment] + from openpilot.selfdrive.ui.mici.onroad.augmented_road_view import AugmentedRoadView from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.system.ui.lib.application import gui_app, FontWeight timer.lap("import") From 02e550e2cb7f83d8c822abb446d333e326d77303 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sat, 21 Feb 2026 11:32:51 -0800 Subject: [PATCH 130/311] remove setup_vsound (#37305) --- .github/workflows/tests.yaml | 1 - selfdrive/test/setup_vsound.sh | 10 ---------- 2 files changed, 11 deletions(-) delete mode 100755 selfdrive/test/setup_vsound.sh diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index e1765eb562..71ff03cba3 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -233,7 +233,6 @@ jobs: timeout-minutes: ${{ (steps.setup-step.outputs.duration < 18) && 1 || 2 }} run: | source selfdrive/test/setup_xvfb.sh - source selfdrive/test/setup_vsound.sh pytest -s tools/sim/tests/test_metadrive_bridge.py create_ui_report: diff --git a/selfdrive/test/setup_vsound.sh b/selfdrive/test/setup_vsound.sh deleted file mode 100755 index aab1499744..0000000000 --- a/selfdrive/test/setup_vsound.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env bash - -{ - #start pulseaudio daemon - sudo pulseaudio -D - - # create a virtual null audio and set it to default device - sudo pactl load-module module-null-sink sink_name=virtual_audio - sudo pactl set-default-sink virtual_audio -} > /dev/null 2>&1 From f45f239774bcb4f3ba01ee4192877ba17ea4480a Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sat, 21 Feb 2026 11:34:32 -0800 Subject: [PATCH 131/311] CI: remove redundant build job (#37306) --- .github/workflows/tests.yaml | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 71ff03cba3..35f04e61c0 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -61,21 +61,6 @@ jobs: timeout-minutes: 3 run: release/check-submodules.sh - build: - runs-on: ${{ - (github.repository == 'commaai/openpilot') && - ((github.event_name != 'pull_request') || - (github.event.pull_request.head.repo.full_name == 'commaai/openpilot')) - && fromJSON('["namespace-profile-amd64-8x16", "namespace-experiments:docker.builds.local-cache=separate"]') - || fromJSON('["ubuntu-24.04"]') }} - steps: - - uses: actions/checkout@v6 - with: - submodules: true - - uses: ./.github/workflows/setup-with-retry - - uses: ./.github/workflows/compile-openpilot - timeout-minutes: 30 - build_mac: name: build macOS 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' }} From f41d77b24fb897af23e25d00f709a2ae36352e4d Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sat, 21 Feb 2026 11:45:44 -0800 Subject: [PATCH 132/311] Actions cleanup (#37307) * rm those * more opt --- .github/workflows/tests.yaml | 8 +++----- pyproject.toml | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 35f04e61c0..40dfaaa801 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -19,8 +19,6 @@ concurrency: env: CI: 1 - PYTHONPATH: ${{ github.workspace }} - PYTEST: pytest --continue-on-collection-errors --durations=0 -n logical jobs: build_release: @@ -131,8 +129,8 @@ jobs: run: | source selfdrive/test/setup_xvfb.sh # Pre-compile Python bytecode so each pytest worker doesn't need to - $PYTEST --collect-only -m 'not slow' -qq - MAX_EXAMPLES=1 $PYTEST -m 'not slow' + pytest --collect-only -m 'not slow' -qq + MAX_EXAMPLES=1 pytest -m 'not slow' process_replay: name: process replay @@ -195,7 +193,7 @@ jobs: timeout-minutes: 4 env: ONNXCPU: 1 - run: $PYTEST selfdrive/test/process_replay/test_regen.py + run: pytest selfdrive/test/process_replay/test_regen.py simulator_driving: name: simulator driving diff --git a/pyproject.toml b/pyproject.toml index 4becd17563..5aeb2ffab4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -112,7 +112,7 @@ allow-direct-references = true [tool.pytest.ini_options] minversion = "6.0" -addopts = "--ignore=openpilot/ --ignore=opendbc/ --ignore=panda/ --ignore=rednose_repo/ --ignore=tinygrad_repo/ --ignore=teleoprtc_repo/ --ignore=msgq/ -Werror --strict-config --strict-markers --durations=10 -n auto --dist=loadgroup" +addopts = "--ignore=openpilot/ --ignore=opendbc/ --ignore=panda/ --ignore=rednose_repo/ --ignore=tinygrad_repo/ --ignore=teleoprtc_repo/ --ignore=msgq/ -Werror --strict-config --strict-markers --durations=20 --maxprocesses=8 -n auto --dist=loadgroup" cpp_files = "test_*" cpp_harness = "selfdrive/test/cpp_harness.py" python_files = "test_*.py" From 082cf39d736ad10a10ca33c7ba3f01f12e0a9f00 Mon Sep 17 00:00:00 2001 From: Christopher Haucke <132518562+CHaucke89@users.noreply.github.com> Date: Sat, 21 Feb 2026 20:01:27 -0500 Subject: [PATCH 133/311] UI: Fix option control display for floating point params (#1711) --- system/ui/sunnypilot/widgets/option_control.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/system/ui/sunnypilot/widgets/option_control.py b/system/ui/sunnypilot/widgets/option_control.py index 91e9650ebd..291d8f6ff0 100644 --- a/system/ui/sunnypilot/widgets/option_control.py +++ b/system/ui/sunnypilot/widgets/option_control.py @@ -44,7 +44,8 @@ class OptionControlSP(ItemAction): self.current_value = int(key) break else: - self.current_value = int(self.params.get(self.param_key, return_default=True)) + value = self.params.get(self.param_key, return_default=True) + self.current_value = int(float(value) * 100.0) if self.use_float_scaling else int(value) # Initialize font and button styles self._font = gui_app.font(FontWeight.MEDIUM) From ece999c54803655186244620112402698816af0f Mon Sep 17 00:00:00 2001 From: Andrey Litvitski Date: Sun, 22 Feb 2026 07:10:16 +0300 Subject: [PATCH 134/311] fix typos in contributing doc (#37309) --- docs/CONTRIBUTING.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 7583095eaf..62468c7448 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -13,13 +13,13 @@ Development is coordinated through [Discord](https://discord.comma.ai) and GitHu ## What contributions are we looking for? **openpilot's priorities are [safety](SAFETY.md), stability, quality, and features, in that order.** -openpilot is part of comma's mission to *solve self-driving cars while delivering shippable intermediaries*, and all development is towards that goal. +openpilot is part of comma's mission to *solve self-driving cars while delivering shippable intermediaries*, and all development is towards that goal. ### What gets merged? The probability of a pull request being merged is a function of its value to the project and the effort it will take us to get it merged. If a PR offers *some* value but will take lots of time to get merged, it will be closed. -Simple, well-tested bug fixes are the easiest to merge, and new features are the hardest to get merged. +Simple, well-tested bug fixes are the easiest to merge, and new features are the hardest to get merged. All of these are examples of good PRs: * typo fix: https://github.com/commaai/openpilot/pull/30678 @@ -29,17 +29,17 @@ All of these are examples of good PRs: ### What doesn't get merged? -* **style changes**: code is art, and it's up to the author to make it beautiful +* **style changes**: code is art, and it's up to the author to make it beautiful * **500+ line PRs**: clean it up, break it up into smaller PRs, or both * **PRs without a clear goal**: every PR must have a singular and clear goal * **UI design**: we do not have a good review process for this yet * **New features**: We believe openpilot is mostly feature-complete, and the rest is a matter of refinement and fixing bugs. As a result of this, most feature PRs will be immediately closed, however the beauty of open source is that forks can and do offer features that upstream openpilot doesn't. -* **Negative expected value**: This a class of PRs that makes an improvement, but the risk or validation costs more than the improvement. The risk can be mitigated by first getting a failing test merged. +* **Negative expected value**: This is a class of PRs that makes an improvement, but the risk or validation costs more than the improvement. The risk can be mitigated by first getting a failing test merged. ### First contribution [Projects / openpilot bounties](https://github.com/orgs/commaai/projects/26/views/1?pane=info) is the best place to get started and goes in-depth on what's expected when working on a bounty. -There's lot of bounties that don't require a comma 3X or a car. +There are a lot of bounties that don't require a comma 3X or a car. ## Pull Requests From 7cd9ab27e62444654bd0de19d4bb14a43003a5b1 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Sat, 21 Feb 2026 20:37:45 -0800 Subject: [PATCH 135/311] ui: split out NavWidget (#37312) * spliit * fix * fix imports --- selfdrive/ui/mici/layouts/onboarding.py | 3 +- .../ui/mici/layouts/settings/developer.py | 2 +- selfdrive/ui/mici/layouts/settings/device.py | 3 +- .../ui/mici/layouts/settings/firehose.py | 3 +- .../mici/layouts/settings/network/__init__.py | 2 +- .../mici/layouts/settings/network/wifi_ui.py | 3 +- .../ui/mici/layouts/settings/settings.py | 2 +- selfdrive/ui/mici/layouts/settings/toggles.py | 2 +- .../ui/mici/onroad/driver_camera_dialog.py | 2 +- selfdrive/ui/mici/widgets/dialog.py | 3 +- selfdrive/ui/mici/widgets/pairing_dialog.py | 2 +- system/ui/widgets/__init__.py | 221 ----------------- system/ui/widgets/nav_widget.py | 227 ++++++++++++++++++ 13 files changed, 243 insertions(+), 232 deletions(-) create mode 100644 system/ui/widgets/nav_widget.py diff --git a/selfdrive/ui/mici/layouts/onboarding.py b/selfdrive/ui/mici/layouts/onboarding.py index 539074453c..b7fafd894a 100644 --- a/selfdrive/ui/mici/layouts/onboarding.py +++ b/selfdrive/ui/mici/layouts/onboarding.py @@ -7,7 +7,8 @@ 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, NavWidget +from openpilot.system.ui.widgets import Widget +from openpilot.system.ui.widgets.nav_widget import NavWidget 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 diff --git a/selfdrive/ui/mici/layouts/settings/developer.py b/selfdrive/ui/mici/layouts/settings/developer.py index 383c7ef9b9..9b5db1c520 100644 --- a/selfdrive/ui/mici/layouts/settings/developer.py +++ b/selfdrive/ui/mici/layouts/settings/developer.py @@ -5,7 +5,7 @@ from openpilot.system.ui.widgets.scroller import Scroller from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigToggle, BigParamControl, BigCircleParamControl from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigInputDialog from openpilot.system.ui.lib.application import gui_app -from openpilot.system.ui.widgets import NavWidget +from openpilot.system.ui.widgets.nav_widget import NavWidget from openpilot.selfdrive.ui.layouts.settings.common import restart_needed_callback from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.selfdrive.ui.widgets.ssh_key import SshKeyAction diff --git a/selfdrive/ui/mici/layouts/settings/device.py b/selfdrive/ui/mici/layouts/settings/device.py index e1beae4fe3..4776a60425 100644 --- a/selfdrive/ui/mici/layouts/settings/device.py +++ b/selfdrive/ui/mici/layouts/settings/device.py @@ -16,7 +16,8 @@ from openpilot.selfdrive.ui.mici.onroad.driver_camera_dialog import DriverCamera from openpilot.selfdrive.ui.mici.layouts.onboarding import TrainingGuide from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos from openpilot.system.ui.lib.multilang import tr -from openpilot.system.ui.widgets import Widget, NavWidget +from openpilot.system.ui.widgets import Widget +from openpilot.system.ui.widgets.nav_widget import NavWidget from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.system.ui.widgets.label import MiciLabel from openpilot.system.ui.widgets.html_render import HtmlModal, HtmlRenderer diff --git a/selfdrive/ui/mici/layouts/settings/firehose.py b/selfdrive/ui/mici/layouts/settings/firehose.py index a2288752ea..eb3331c868 100644 --- a/selfdrive/ui/mici/layouts/settings/firehose.py +++ b/selfdrive/ui/mici/layouts/settings/firehose.py @@ -13,7 +13,8 @@ from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE from openpilot.system.ui.lib.wrap_text import wrap_text from openpilot.system.ui.lib.scroll_panel2 import GuiScrollPanel2 from openpilot.system.ui.lib.multilang import tr, trn, tr_noop -from openpilot.system.ui.widgets import Widget, NavWidget +from openpilot.system.ui.widgets import Widget +from openpilot.system.ui.widgets.nav_widget import NavWidget TITLE = tr_noop("Firehose Mode") DESCRIPTION = tr_noop( diff --git a/selfdrive/ui/mici/layouts/settings/network/__init__.py b/selfdrive/ui/mici/layouts/settings/network/__init__.py index 9f49521c36..e73383c0d4 100644 --- a/selfdrive/ui/mici/layouts/settings/network/__init__.py +++ b/selfdrive/ui/mici/layouts/settings/network/__init__.py @@ -7,7 +7,7 @@ from openpilot.selfdrive.ui.mici.widgets.dialog import BigInputDialog from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.selfdrive.ui.lib.prime_state import PrimeType from openpilot.system.ui.lib.application import gui_app -from openpilot.system.ui.widgets import NavWidget +from openpilot.system.ui.widgets.nav_widget import NavWidget from openpilot.system.ui.lib.wifi_manager import WifiManager, Network, MeteredType, ConnectStatus, normalize_ssid diff --git a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py index 1918362527..b974b502a1 100644 --- a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py +++ b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py @@ -7,7 +7,8 @@ from openpilot.common.swaglog import cloudlog from openpilot.system.ui.widgets.label import UnifiedLabel from openpilot.selfdrive.ui.mici.widgets.dialog import BigMultiOptionDialog, BigInputDialog, BigDialogOptionButton, BigConfirmationDialogV2 from openpilot.system.ui.lib.application import gui_app, MousePos, FontWeight -from openpilot.system.ui.widgets import Widget, NavWidget +from openpilot.system.ui.widgets import Widget +from openpilot.system.ui.widgets.nav_widget import NavWidget from openpilot.system.ui.lib.wifi_manager import WifiManager, Network, SecurityType, WifiState, normalize_ssid diff --git a/selfdrive/ui/mici/layouts/settings/settings.py b/selfdrive/ui/mici/layouts/settings/settings.py index 2372104a00..0dbe1b2204 100644 --- a/selfdrive/ui/mici/layouts/settings/settings.py +++ b/selfdrive/ui/mici/layouts/settings/settings.py @@ -9,7 +9,7 @@ from openpilot.selfdrive.ui.mici.layouts.settings.device import DeviceLayoutMici from openpilot.selfdrive.ui.mici.layouts.settings.developer import DeveloperLayoutMici from openpilot.selfdrive.ui.mici.layouts.settings.firehose import FirehoseLayout from openpilot.system.ui.lib.application import gui_app, FontWeight -from openpilot.system.ui.widgets import NavWidget +from openpilot.system.ui.widgets.nav_widget import NavWidget class SettingsBigButton(BigButton): diff --git a/selfdrive/ui/mici/layouts/settings/toggles.py b/selfdrive/ui/mici/layouts/settings/toggles.py index d6fb75a4d8..0e9be393ec 100644 --- a/selfdrive/ui/mici/layouts/settings/toggles.py +++ b/selfdrive/ui/mici/layouts/settings/toggles.py @@ -4,7 +4,7 @@ from cereal import log from openpilot.system.ui.widgets.scroller import Scroller from openpilot.selfdrive.ui.mici.widgets.button import BigParamControl, BigMultiParamToggle from openpilot.system.ui.lib.application import gui_app -from openpilot.system.ui.widgets import NavWidget +from openpilot.system.ui.widgets.nav_widget import NavWidget from openpilot.selfdrive.ui.layouts.settings.common import restart_needed_callback from openpilot.selfdrive.ui.ui_state import ui_state diff --git a/selfdrive/ui/mici/onroad/driver_camera_dialog.py b/selfdrive/ui/mici/onroad/driver_camera_dialog.py index 26a5d132c6..4fddc88f6d 100644 --- a/selfdrive/ui/mici/onroad/driver_camera_dialog.py +++ b/selfdrive/ui/mici/onroad/driver_camera_dialog.py @@ -7,7 +7,7 @@ from openpilot.selfdrive.ui.ui_state import ui_state, device from openpilot.selfdrive.selfdrived.events import EVENTS, ET from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.multilang import tr -from openpilot.system.ui.widgets import NavWidget +from openpilot.system.ui.widgets.nav_widget import NavWidget from openpilot.system.ui.widgets.label import gui_label EventName = log.OnroadEvent.EventName diff --git a/selfdrive/ui/mici/widgets/dialog.py b/selfdrive/ui/mici/widgets/dialog.py index 612f31b753..f57938c8ee 100644 --- a/selfdrive/ui/mici/widgets/dialog.py +++ b/selfdrive/ui/mici/widgets/dialog.py @@ -4,7 +4,8 @@ import pyray as rl from typing import Union from collections.abc import Callable from typing import cast -from openpilot.system.ui.widgets import Widget, NavWidget +from openpilot.system.ui.widgets import Widget +from openpilot.system.ui.widgets.nav_widget import NavWidget from openpilot.system.ui.widgets.label import UnifiedLabel, gui_label from openpilot.system.ui.widgets.mici_keyboard import MiciKeyboard from openpilot.system.ui.lib.text_measure import measure_text_cached diff --git a/selfdrive/ui/mici/widgets/pairing_dialog.py b/selfdrive/ui/mici/widgets/pairing_dialog.py index 088d2a0b6f..64b2c9a063 100644 --- a/selfdrive/ui/mici/widgets/pairing_dialog.py +++ b/selfdrive/ui/mici/widgets/pairing_dialog.py @@ -7,7 +7,7 @@ from openpilot.common.api import Api from openpilot.common.swaglog import cloudlog from openpilot.common.params import Params from openpilot.selfdrive.ui.ui_state import ui_state -from openpilot.system.ui.widgets import NavWidget +from openpilot.system.ui.widgets.nav_widget import NavWidget from openpilot.system.ui.lib.application import FontWeight, gui_app from openpilot.system.ui.widgets.label import MiciLabel diff --git a/system/ui/widgets/__init__.py b/system/ui/widgets/__init__.py index 25c908c72c..37254c8b35 100644 --- a/system/ui/widgets/__init__.py +++ b/system/ui/widgets/__init__.py @@ -4,7 +4,6 @@ import abc import pyray as rl from enum import IntEnum from collections.abc import Callable -from openpilot.common.filter_simple import BounceFilter, FirstOrderFilter from openpilot.system.ui.lib.application import gui_app, MousePos, MAX_TOUCH_SLOTS, MouseEvent try: @@ -189,223 +188,3 @@ class Widget(abc.ABC): def hide_event(self): """Optionally handle hide event. Parent must manually call this""" - - -SWIPE_AWAY_THRESHOLD = 80 # px to dismiss after releasing -START_DISMISSING_THRESHOLD = 40 # px to start dismissing while dragging -BLOCK_SWIPE_AWAY_THRESHOLD = 60 # px horizontal movement to block swipe away - -NAV_BAR_MARGIN = 6 -NAV_BAR_WIDTH = 205 -NAV_BAR_HEIGHT = 8 - -DISMISS_PUSH_OFFSET = 50 + NAV_BAR_MARGIN + NAV_BAR_HEIGHT # px extra to push down when dismissing -DISMISS_TIME_SECONDS = 2.0 - - -class NavBar(Widget): - def __init__(self): - super().__init__() - self.set_rect(rl.Rectangle(0, 0, NAV_BAR_WIDTH, NAV_BAR_HEIGHT)) - self._alpha = 1.0 - self._alpha_filter = FirstOrderFilter(1.0, 0.1, 1 / gui_app.target_fps) - self._fade_time = 0.0 - - def set_alpha(self, alpha: float) -> None: - self._alpha = alpha - self._fade_time = rl.get_time() - - def show_event(self): - super().show_event() - self._alpha = 1.0 - self._alpha_filter.x = 1.0 - self._fade_time = rl.get_time() - - def _render(self, _): - if rl.get_time() - self._fade_time > DISMISS_TIME_SECONDS: - self._alpha = 0.0 - alpha = self._alpha_filter.update(self._alpha) - - # white bar with black border - rl.draw_rectangle_rounded(self._rect, 1.0, 6, rl.Color(255, 255, 255, int(255 * 0.9 * alpha))) - rl.draw_rectangle_rounded_lines_ex(self._rect, 1.0, 6, 2, rl.Color(0, 0, 0, int(255 * 0.3 * alpha))) - - -class NavWidget(Widget, abc.ABC): - """ - A full screen widget that supports back navigation by swiping down from the top. - """ - BACK_TOUCH_AREA_PERCENTAGE = 0.65 - - def __init__(self): - super().__init__() - self._back_callback: Callable[[], None] | None = None - self._back_button_start_pos: MousePos | None = None - self._swiping_away = False # currently swiping away - self._can_swipe_away = True # swipe away is blocked after certain horizontal movement - - self._pos_filter = BounceFilter(0.0, 0.1, 1 / gui_app.target_fps, bounce=1) - self._playing_dismiss_animation = False - self._trigger_animate_in = False - self._nav_bar_show_time = 0.0 - self._back_enabled: bool | Callable[[], bool] = True - self._nav_bar = NavBar() - - self._nav_bar_y_filter = FirstOrderFilter(0.0, 0.1, 1 / gui_app.target_fps) - - self._set_up = False - - @property - def back_enabled(self) -> bool: - return self._back_enabled() if callable(self._back_enabled) else self._back_enabled - - def set_back_enabled(self, enabled: bool | Callable[[], bool]) -> None: - self._back_enabled = enabled - - def set_back_callback(self, callback: Callable[[], None]) -> None: - self._back_callback = callback - - def _handle_mouse_event(self, mouse_event: MouseEvent) -> None: - # FIXME: disabling this widget on new push_widget still causes this widget to track mouse events without mouse down - super()._handle_mouse_event(mouse_event) - - if not self.back_enabled: - self._back_button_start_pos = None - self._swiping_away = False - self._can_swipe_away = True - return - - if mouse_event.left_pressed: - # user is able to swipe away if starting near top of screen, or anywhere if scroller is at top - self._pos_filter.update_alpha(0.04) - 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 - - # 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 - - elif mouse_event.left_down: - if self._back_button_start_pos is not None: - # block swiping away if too much horizontal or upward movement - horizontal_movement = abs(mouse_event.pos.x - self._back_button_start_pos.x) > BLOCK_SWIPE_AWAY_THRESHOLD - upward_movement = mouse_event.pos.y - self._back_button_start_pos.y < -BLOCK_SWIPE_AWAY_THRESHOLD - if not self._swiping_away and (horizontal_movement or upward_movement): - self._can_swipe_away = False - self._back_button_start_pos = None - - # block horizontal swiping if now swiping away - if self._can_swipe_away: - if mouse_event.pos.y - self._back_button_start_pos.y > START_DISMISSING_THRESHOLD: - self._swiping_away = True - - elif mouse_event.left_released: - self._pos_filter.update_alpha(0.1) - # if far enough, trigger back navigation callback - if self._back_button_start_pos is not None: - if mouse_event.pos.y - self._back_button_start_pos.y > SWIPE_AWAY_THRESHOLD: - self._playing_dismiss_animation = True - - self._back_button_start_pos = None - self._swiping_away = False - - def _update_state(self): - super()._update_state() - - # Disable self's scroller while swiping away - if not self._set_up: - self._set_up = True - if hasattr(self, '_scroller'): - # TODO: use touch_valid - original_enabled = self._scroller._enabled - self._scroller.set_enabled(lambda: self.enabled and not self._swiping_away and (original_enabled() if callable(original_enabled) else - original_enabled)) - elif hasattr(self, '_scroll_panel'): - original_enabled = self._scroll_panel.enabled - self._scroll_panel.set_enabled(lambda: self.enabled and not self._swiping_away and (original_enabled() if callable(original_enabled) else - original_enabled)) - - if self._trigger_animate_in: - self._pos_filter.x = self._rect.height - self._nav_bar_y_filter.x = -NAV_BAR_MARGIN - NAV_BAR_HEIGHT - self._nav_bar_show_time = rl.get_time() - self._trigger_animate_in = False - - new_y = 0.0 - - if not self.enabled: - self._back_button_start_pos = None - - # TODO: why is this not in handle_mouse_event? have to hack above - if self._back_button_start_pos is not None: - last_mouse_event = gui_app.last_mouse_event - # push entire widget as user drags it away - new_y = max(last_mouse_event.pos.y - self._back_button_start_pos.y, 0) - if new_y < SWIPE_AWAY_THRESHOLD: - new_y /= 2 # resistance until mouse release would dismiss widget - - if self._swiping_away: - self._nav_bar.set_alpha(1.0) - - if self._playing_dismiss_animation: - new_y = self._rect.height + DISMISS_PUSH_OFFSET - - new_y = round(self._pos_filter.update(new_y)) - if abs(new_y) < 1 and self._pos_filter.velocity.x == 0.0: - new_y = self._pos_filter.x = 0.0 - - if new_y > self._rect.height + DISMISS_PUSH_OFFSET - 10: - if self._back_callback is not None: - self._back_callback() - - self._playing_dismiss_animation = False - self._back_button_start_pos = None - self._swiping_away = False - - self.set_position(self._rect.x, new_y) - - def _layout(self): - # Dim whatever is behind this widget, fading with position (runs after _update_state so position is correct) - overlay_alpha = int(200 * max(0.0, min(1.0, 1.0 - self._rect.y / self._rect.height))) if self._rect.height > 0 else 0 - rl.draw_rectangle(0, 0, int(self._rect.width), int(self._rect.height), rl.Color(0, 0, 0, overlay_alpha)) - - bounce_height = 20 - rl.draw_rectangle(int(self._rect.x), int(self._rect.y), int(self._rect.width), int(self._rect.height + bounce_height), rl.BLACK) - - def render(self, rect: rl.Rectangle | None = None) -> bool | int | None: - ret = super().render(rect) - - if self.back_enabled: - bar_x = self._rect.x + (self._rect.width - self._nav_bar.rect.width) / 2 - nav_bar_delayed = rl.get_time() - self._nav_bar_show_time < 0.4 - # User dragging or dismissing, nav bar follows NavWidget - if self._back_button_start_pos is not None or self._playing_dismiss_animation: - self._nav_bar_y_filter.x = NAV_BAR_MARGIN + self._pos_filter.x - # Waiting to show - elif nav_bar_delayed: - self._nav_bar_y_filter.x = -NAV_BAR_MARGIN - NAV_BAR_HEIGHT - # Animate back to top - else: - self._nav_bar_y_filter.update(NAV_BAR_MARGIN) - - self._nav_bar.set_position(bar_x, round(self._nav_bar_y_filter.x)) - self._nav_bar.render() - - return ret - - def show_event(self): - super().show_event() - # FIXME: we don't know the height of the rect at first show_event since it's before the first render :( - # so we need this hacky bool for now - self._trigger_animate_in = True - self._nav_bar.show_event() diff --git a/system/ui/widgets/nav_widget.py b/system/ui/widgets/nav_widget.py new file mode 100644 index 0000000000..c1b1df1292 --- /dev/null +++ b/system/ui/widgets/nav_widget.py @@ -0,0 +1,227 @@ +from __future__ import annotations + +import abc +import pyray as rl +from collections.abc import Callable +from openpilot.system.ui.widgets import Widget +from openpilot.common.filter_simple import BounceFilter, FirstOrderFilter +from openpilot.system.ui.lib.application import gui_app, MousePos, MouseEvent + +SWIPE_AWAY_THRESHOLD = 80 # px to dismiss after releasing +START_DISMISSING_THRESHOLD = 40 # px to start dismissing while dragging +BLOCK_SWIPE_AWAY_THRESHOLD = 60 # px horizontal movement to block swipe away + +NAV_BAR_MARGIN = 6 +NAV_BAR_WIDTH = 205 +NAV_BAR_HEIGHT = 8 + +DISMISS_PUSH_OFFSET = 50 + NAV_BAR_MARGIN + NAV_BAR_HEIGHT # px extra to push down when dismissing +DISMISS_TIME_SECONDS = 2.0 + + +class NavBar(Widget): + def __init__(self): + super().__init__() + self.set_rect(rl.Rectangle(0, 0, NAV_BAR_WIDTH, NAV_BAR_HEIGHT)) + self._alpha = 1.0 + self._alpha_filter = FirstOrderFilter(1.0, 0.1, 1 / gui_app.target_fps) + self._fade_time = 0.0 + + def set_alpha(self, alpha: float) -> None: + self._alpha = alpha + self._fade_time = rl.get_time() + + def show_event(self): + super().show_event() + self._alpha = 1.0 + self._alpha_filter.x = 1.0 + self._fade_time = rl.get_time() + + def _render(self, _): + if rl.get_time() - self._fade_time > DISMISS_TIME_SECONDS: + self._alpha = 0.0 + alpha = self._alpha_filter.update(self._alpha) + + # white bar with black border + rl.draw_rectangle_rounded(self._rect, 1.0, 6, rl.Color(255, 255, 255, int(255 * 0.9 * alpha))) + rl.draw_rectangle_rounded_lines_ex(self._rect, 1.0, 6, 2, rl.Color(0, 0, 0, int(255 * 0.3 * alpha))) + + +class NavWidget(Widget, abc.ABC): + """ + A full screen widget that supports back navigation by swiping down from the top. + """ + BACK_TOUCH_AREA_PERCENTAGE = 0.65 + + def __init__(self): + super().__init__() + self._back_callback: Callable[[], None] | None = None + self._back_button_start_pos: MousePos | None = None + self._swiping_away = False # currently swiping away + self._can_swipe_away = True # swipe away is blocked after certain horizontal movement + + self._pos_filter = BounceFilter(0.0, 0.1, 1 / gui_app.target_fps, bounce=1) + self._playing_dismiss_animation = False + self._trigger_animate_in = False + self._nav_bar_show_time = 0.0 + self._back_enabled: bool | Callable[[], bool] = True + self._nav_bar = NavBar() + + self._nav_bar_y_filter = FirstOrderFilter(0.0, 0.1, 1 / gui_app.target_fps) + + self._set_up = False + + @property + def back_enabled(self) -> bool: + return self._back_enabled() if callable(self._back_enabled) else self._back_enabled + + def set_back_enabled(self, enabled: bool | Callable[[], bool]) -> None: + self._back_enabled = enabled + + def set_back_callback(self, callback: Callable[[], None]) -> None: + self._back_callback = callback + + def _handle_mouse_event(self, mouse_event: MouseEvent) -> None: + # FIXME: disabling this widget on new push_widget still causes this widget to track mouse events without mouse down + super()._handle_mouse_event(mouse_event) + + if not self.back_enabled: + self._back_button_start_pos = None + self._swiping_away = False + self._can_swipe_away = True + return + + if mouse_event.left_pressed: + # user is able to swipe away if starting near top of screen, or anywhere if scroller is at top + self._pos_filter.update_alpha(0.04) + 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 + + # 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 + + elif mouse_event.left_down: + if self._back_button_start_pos is not None: + # block swiping away if too much horizontal or upward movement + horizontal_movement = abs(mouse_event.pos.x - self._back_button_start_pos.x) > BLOCK_SWIPE_AWAY_THRESHOLD + upward_movement = mouse_event.pos.y - self._back_button_start_pos.y < -BLOCK_SWIPE_AWAY_THRESHOLD + if not self._swiping_away and (horizontal_movement or upward_movement): + self._can_swipe_away = False + self._back_button_start_pos = None + + # block horizontal swiping if now swiping away + if self._can_swipe_away: + if mouse_event.pos.y - self._back_button_start_pos.y > START_DISMISSING_THRESHOLD: + self._swiping_away = True + + elif mouse_event.left_released: + self._pos_filter.update_alpha(0.1) + # if far enough, trigger back navigation callback + if self._back_button_start_pos is not None: + if mouse_event.pos.y - self._back_button_start_pos.y > SWIPE_AWAY_THRESHOLD: + self._playing_dismiss_animation = True + + self._back_button_start_pos = None + self._swiping_away = False + + def _update_state(self): + super()._update_state() + + # Disable self's scroller while swiping away + if not self._set_up: + self._set_up = True + if hasattr(self, '_scroller'): + # TODO: use touch_valid + original_enabled = self._scroller._enabled + self._scroller.set_enabled(lambda: self.enabled and not self._swiping_away and (original_enabled() if callable(original_enabled) else + original_enabled)) + elif hasattr(self, '_scroll_panel'): + original_enabled = self._scroll_panel.enabled + self._scroll_panel.set_enabled(lambda: self.enabled and not self._swiping_away and (original_enabled() if callable(original_enabled) else + original_enabled)) + + if self._trigger_animate_in: + self._pos_filter.x = self._rect.height + self._nav_bar_y_filter.x = -NAV_BAR_MARGIN - NAV_BAR_HEIGHT + self._nav_bar_show_time = rl.get_time() + self._trigger_animate_in = False + + new_y = 0.0 + + if not self.enabled: + self._back_button_start_pos = None + + # TODO: why is this not in handle_mouse_event? have to hack above + if self._back_button_start_pos is not None: + last_mouse_event = gui_app.last_mouse_event + # push entire widget as user drags it away + new_y = max(last_mouse_event.pos.y - self._back_button_start_pos.y, 0) + if new_y < SWIPE_AWAY_THRESHOLD: + new_y /= 2 # resistance until mouse release would dismiss widget + + if self._swiping_away: + self._nav_bar.set_alpha(1.0) + + if self._playing_dismiss_animation: + new_y = self._rect.height + DISMISS_PUSH_OFFSET + + new_y = round(self._pos_filter.update(new_y)) + if abs(new_y) < 1 and self._pos_filter.velocity.x == 0.0: + new_y = self._pos_filter.x = 0.0 + + if new_y > self._rect.height + DISMISS_PUSH_OFFSET - 10: + if self._back_callback is not None: + self._back_callback() + + self._playing_dismiss_animation = False + self._back_button_start_pos = None + self._swiping_away = False + + self.set_position(self._rect.x, new_y) + + def _layout(self): + # Dim whatever is behind this widget, fading with position (runs after _update_state so position is correct) + overlay_alpha = int(200 * max(0.0, min(1.0, 1.0 - self._rect.y / self._rect.height))) if self._rect.height > 0 else 0 + rl.draw_rectangle(0, 0, int(self._rect.width), int(self._rect.height), rl.Color(0, 0, 0, overlay_alpha)) + + bounce_height = 20 + rl.draw_rectangle(int(self._rect.x), int(self._rect.y), int(self._rect.width), int(self._rect.height + bounce_height), rl.BLACK) + + def render(self, rect: rl.Rectangle | None = None) -> bool | int | None: + ret = super().render(rect) + + if self.back_enabled: + bar_x = self._rect.x + (self._rect.width - self._nav_bar.rect.width) / 2 + nav_bar_delayed = rl.get_time() - self._nav_bar_show_time < 0.4 + # User dragging or dismissing, nav bar follows NavWidget + if self._back_button_start_pos is not None or self._playing_dismiss_animation: + self._nav_bar_y_filter.x = NAV_BAR_MARGIN + self._pos_filter.x + # Waiting to show + elif nav_bar_delayed: + self._nav_bar_y_filter.x = -NAV_BAR_MARGIN - NAV_BAR_HEIGHT + # Animate back to top + else: + self._nav_bar_y_filter.update(NAV_BAR_MARGIN) + + self._nav_bar.set_position(bar_x, round(self._nav_bar_y_filter.x)) + self._nav_bar.render() + + return ret + + def show_event(self): + super().show_event() + # FIXME: we don't know the height of the rect at first show_event since it's before the first render :( + # so we need this hacky bool for now + self._trigger_animate_in = True + self._nav_bar.show_event() From c4393277fb7ea776fd28dc6dd7474b069f8a94b3 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Sat, 21 Feb 2026 20:56:51 -0800 Subject: [PATCH 136/311] ui: draw debug rects (#37313) * debug * new --- system/ui/lib/application.py | 4 ++++ system/ui/widgets/__init__.py | 7 +++++++ 2 files changed, 11 insertions(+) diff --git a/system/ui/lib/application.py b/system/ui/lib/application.py index 7c0bd2bf3c..5d284bf454 100644 --- a/system/ui/lib/application.py +++ b/system/ui/lib/application.py @@ -248,6 +248,10 @@ class GuiApplication: def set_show_fps(self, show: bool): self._show_fps = show + @property + def show_touches(self) -> bool: + return self._show_touches + @property def target_fps(self): return self._target_fps diff --git a/system/ui/widgets/__init__.py b/system/ui/widgets/__init__.py index 37254c8b35..cc8d72959f 100644 --- a/system/ui/widgets/__init__.py +++ b/system/ui/widgets/__init__.py @@ -104,6 +104,9 @@ class Widget(abc.ABC): self._layout() ret = self._render(self._rect) + if gui_app.show_touches: + self._draw_debug_rect() + # Keep track of whether mouse down started within the widget's rectangle if self.enabled and self.__was_awake: self._process_mouse_events() @@ -116,6 +119,10 @@ class Widget(abc.ABC): return ret + def _draw_debug_rect(self) -> None: + rl.draw_rectangle_lines(int(self._rect.x), int(self._rect.y), + max(int(self._rect.width), 1), max(int(self._rect.height), 1), rl.RED) + def _process_mouse_events(self) -> None: hit_rect = self._hit_rect touch_valid = self._touch_valid() From 1304f959785e66393fc203cc66556bc0baeccc2f Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Sat, 21 Feb 2026 22:26:07 -0800 Subject: [PATCH 137/311] mici ui: remove line separators (#37314) remove --- system/ui/widgets/scroller.py | 24 +----------------------- 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/system/ui/widgets/scroller.py b/system/ui/widgets/scroller.py index 9bb7883215..f5d5738e9c 100644 --- a/system/ui/widgets/scroller.py +++ b/system/ui/widgets/scroller.py @@ -19,21 +19,6 @@ DO_ZOOM = False DO_JELLO = False -class LineSeparator(Widget): - def __init__(self, height: int = 1): - super().__init__() - self._rect = rl.Rectangle(0, 0, 0, height) - - def set_parent_rect(self, parent_rect: rl.Rectangle) -> None: - super().set_parent_rect(parent_rect) - self._rect.width = parent_rect.width - - def _render(self, _): - rl.draw_line(int(self._rect.x) + LINE_PADDING, int(self._rect.y), - int(self._rect.x + self._rect.width) - LINE_PADDING, int(self._rect.y), - LINE_COLOR) - - class ScrollIndicator(Widget): HORIZONTAL_MARGIN = 4 @@ -78,14 +63,13 @@ class ScrollIndicator(Widget): class Scroller(Widget): def __init__(self, items: list[Widget], horizontal: bool = True, snap_items: bool = True, spacing: int = ITEM_SPACING, - line_separator: bool = False, pad_start: int = ITEM_SPACING, pad_end: int = ITEM_SPACING, + pad_start: int = ITEM_SPACING, pad_end: int = ITEM_SPACING, scroll_indicator: bool = True, edge_shadows: bool = True): super().__init__() self._items: list[Widget] = [] self._horizontal = horizontal self._snap_items = snap_items self._spacing = spacing - self._line_separator = LineSeparator() if line_separator else None self._pad_start = pad_start self._pad_end = pad_end @@ -214,12 +198,6 @@ class Scroller(Widget): 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(self._visible_items) - for i in range(1, len(self._visible_items)): - self._visible_items.insert(l - i, self._line_separator) - 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 From cdcc2f676603467dd23b18d71272115a3873353a Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Sat, 21 Feb 2026 22:31:05 -0800 Subject: [PATCH 138/311] mici scroller: remove double pad args (#37315) * remove double pad args * fix --- selfdrive/ui/mici/layouts/main.py | 2 +- selfdrive/ui/mici/layouts/offroad_alerts.py | 2 +- selfdrive/ui/mici/widgets/dialog.py | 2 +- system/ui/widgets/scroller.py | 10 ++++------ 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/selfdrive/ui/mici/layouts/main.py b/selfdrive/ui/mici/layouts/main.py index be2c4747f3..928ea942da 100644 --- a/selfdrive/ui/mici/layouts/main.py +++ b/selfdrive/ui/mici/layouts/main.py @@ -40,7 +40,7 @@ class MiciMainLayout(Widget): self._alerts_layout, self._home_layout, self._onroad_layout, - ], spacing=0, pad_start=0, pad_end=0, scroll_indicator=False, edge_shadows=False) + ], spacing=0, pad=0, scroll_indicator=False, edge_shadows=False) self._scroller.set_reset_scroll_at_show(False) self._scroller.set_enabled(lambda: self.enabled) # for nav stack diff --git a/selfdrive/ui/mici/layouts/offroad_alerts.py b/selfdrive/ui/mici/layouts/offroad_alerts.py index 60f64b31b0..778d718552 100644 --- a/selfdrive/ui/mici/layouts/offroad_alerts.py +++ b/selfdrive/ui/mici/layouts/offroad_alerts.py @@ -197,7 +197,7 @@ class MiciOffroadAlerts(Widget): self._last_refresh = 0.0 # Create vertical scroller - self._scroller = Scroller([], horizontal=False, spacing=12, pad_start=0, pad_end=0, snap_items=False) + self._scroller = Scroller([], horizontal=False, spacing=12, pad=0, snap_items=False) # Create empty state label self._empty_label = UnifiedLabel(tr("no alerts"), 65, FontWeight.DISPLAY, rl.WHITE, diff --git a/selfdrive/ui/mici/widgets/dialog.py b/selfdrive/ui/mici/widgets/dialog.py index f57938c8ee..8a1c5df65f 100644 --- a/selfdrive/ui/mici/widgets/dialog.py +++ b/selfdrive/ui/mici/widgets/dialog.py @@ -297,7 +297,7 @@ class BigMultiOptionDialog(BigDialogBase): # Widget doesn't differentiate between click and drag self._can_click = True - self._scroller = Scroller([], horizontal=False, pad_start=100, pad_end=100, spacing=0, snap_items=True) + self._scroller = Scroller([], horizontal=False, pad=100, spacing=0, snap_items=True) for option in options: self._scroller.add_widget(BigDialogOptionButton(option)) diff --git a/system/ui/widgets/scroller.py b/system/ui/widgets/scroller.py index f5d5738e9c..94e00872d2 100644 --- a/system/ui/widgets/scroller.py +++ b/system/ui/widgets/scroller.py @@ -63,15 +63,13 @@ class ScrollIndicator(Widget): class Scroller(Widget): def __init__(self, items: list[Widget], horizontal: bool = True, snap_items: bool = True, spacing: int = ITEM_SPACING, - pad_start: int = ITEM_SPACING, pad_end: int = ITEM_SPACING, - scroll_indicator: bool = True, edge_shadows: bool = True): + pad: int = ITEM_SPACING, scroll_indicator: bool = True, edge_shadows: bool = True): super().__init__() self._items: list[Widget] = [] self._horizontal = horizontal self._snap_items = snap_items self._spacing = spacing - self._pad_start = pad_start - self._pad_end = pad_end + self._pad = pad self._reset_scroll_at_show = True @@ -200,7 +198,7 @@ class Scroller(Widget): 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 + self._content_size += self._pad * 2 self._scroll_offset = self._get_scroll(self._visible_items, self._content_size) @@ -208,7 +206,7 @@ class Scroller(Widget): cur_pos = 0 for idx, item in enumerate(self._visible_items): - spacing = self._spacing if (idx > 0) else self._pad_start + spacing = self._spacing if (idx > 0) else self._pad # Nicely lay out items horizontally/vertically if self._horizontal: x = self._rect.x + cur_pos + spacing From f99dc2eab27afdb494e1c410d24dac40dc68fcb2 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Sat, 21 Feb 2026 22:35:41 -0800 Subject: [PATCH 139/311] mici scroller: default no snapping (#37316) * change default * fix --- selfdrive/ui/mici/layouts/main.py | 2 +- selfdrive/ui/mici/layouts/offroad_alerts.py | 2 +- selfdrive/ui/mici/layouts/settings/developer.py | 2 +- selfdrive/ui/mici/layouts/settings/device.py | 2 +- selfdrive/ui/mici/layouts/settings/network/__init__.py | 2 +- selfdrive/ui/mici/layouts/settings/settings.py | 2 +- selfdrive/ui/mici/layouts/settings/toggles.py | 2 +- selfdrive/ui/mici/widgets/dialog.py | 2 +- system/ui/widgets/scroller.py | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/selfdrive/ui/mici/layouts/main.py b/selfdrive/ui/mici/layouts/main.py index 928ea942da..f6dbad89a1 100644 --- a/selfdrive/ui/mici/layouts/main.py +++ b/selfdrive/ui/mici/layouts/main.py @@ -40,7 +40,7 @@ class MiciMainLayout(Widget): self._alerts_layout, self._home_layout, self._onroad_layout, - ], spacing=0, pad=0, scroll_indicator=False, edge_shadows=False) + ], snap_items=True, spacing=0, pad=0, scroll_indicator=False, edge_shadows=False) self._scroller.set_reset_scroll_at_show(False) self._scroller.set_enabled(lambda: self.enabled) # for nav stack diff --git a/selfdrive/ui/mici/layouts/offroad_alerts.py b/selfdrive/ui/mici/layouts/offroad_alerts.py index 778d718552..565d27593d 100644 --- a/selfdrive/ui/mici/layouts/offroad_alerts.py +++ b/selfdrive/ui/mici/layouts/offroad_alerts.py @@ -197,7 +197,7 @@ class MiciOffroadAlerts(Widget): self._last_refresh = 0.0 # Create vertical scroller - self._scroller = Scroller([], horizontal=False, spacing=12, pad=0, snap_items=False) + self._scroller = Scroller([], horizontal=False, spacing=12, pad=0) # Create empty state label self._empty_label = UnifiedLabel(tr("no alerts"), 65, FontWeight.DISPLAY, rl.WHITE, diff --git a/selfdrive/ui/mici/layouts/settings/developer.py b/selfdrive/ui/mici/layouts/settings/developer.py index 9b5db1c520..f107df7207 100644 --- a/selfdrive/ui/mici/layouts/settings/developer.py +++ b/selfdrive/ui/mici/layouts/settings/developer.py @@ -65,7 +65,7 @@ class DeveloperLayoutMici(NavWidget): self._long_maneuver_toggle, self._alpha_long_toggle, self._debug_mode_toggle, - ], snap_items=False) + ]) # Toggle lists self._refresh_toggles = ( diff --git a/selfdrive/ui/mici/layouts/settings/device.py b/selfdrive/ui/mici/layouts/settings/device.py index 4776a60425..d919df1c16 100644 --- a/selfdrive/ui/mici/layouts/settings/device.py +++ b/selfdrive/ui/mici/layouts/settings/device.py @@ -324,7 +324,7 @@ class DeviceLayoutMici(NavWidget): regulatory_btn, reboot_btn, self._power_off_btn, - ], snap_items=False) + ]) # Set up back navigation # TODO: can this somehow be generic in widgets/__init__.py or application.py? diff --git a/selfdrive/ui/mici/layouts/settings/network/__init__.py b/selfdrive/ui/mici/layouts/settings/network/__init__.py index e73383c0d4..ad4e43a1ac 100644 --- a/selfdrive/ui/mici/layouts/settings/network/__init__.py +++ b/selfdrive/ui/mici/layouts/settings/network/__init__.py @@ -93,7 +93,7 @@ class NetworkLayoutMici(NavWidget): self._apn_btn, self._cellular_metered_btn, # */ - ], snap_items=False) + ]) # Set initial config roaming_enabled = ui_state.params.get_bool("GsmRoaming") diff --git a/selfdrive/ui/mici/layouts/settings/settings.py b/selfdrive/ui/mici/layouts/settings/settings.py index 0dbe1b2204..15fd681996 100644 --- a/selfdrive/ui/mici/layouts/settings/settings.py +++ b/selfdrive/ui/mici/layouts/settings/settings.py @@ -50,7 +50,7 @@ class SettingsLayout(NavWidget): #BigDialogButton("manual", "", "icons_mici/settings/manual_icon.png", "Check out the mici user\nmanual at comma.ai/setup"), firehose_btn, developer_btn, - ], snap_items=False) + ]) # Set up back navigation self.set_back_callback(gui_app.pop_widget) diff --git a/selfdrive/ui/mici/layouts/settings/toggles.py b/selfdrive/ui/mici/layouts/settings/toggles.py index 0e9be393ec..b0f7189230 100644 --- a/selfdrive/ui/mici/layouts/settings/toggles.py +++ b/selfdrive/ui/mici/layouts/settings/toggles.py @@ -34,7 +34,7 @@ class TogglesLayoutMici(NavWidget): record_front, record_mic, enable_openpilot, - ], snap_items=False) + ]) # Toggle lists self._refresh_toggles = ( diff --git a/selfdrive/ui/mici/widgets/dialog.py b/selfdrive/ui/mici/widgets/dialog.py index 8a1c5df65f..c695949e49 100644 --- a/selfdrive/ui/mici/widgets/dialog.py +++ b/selfdrive/ui/mici/widgets/dialog.py @@ -297,7 +297,7 @@ class BigMultiOptionDialog(BigDialogBase): # Widget doesn't differentiate between click and drag self._can_click = True - self._scroller = Scroller([], horizontal=False, pad=100, spacing=0, snap_items=True) + self._scroller = Scroller([], horizontal=False, snap_items=True, pad=100, spacing=0) for option in options: self._scroller.add_widget(BigDialogOptionButton(option)) diff --git a/system/ui/widgets/scroller.py b/system/ui/widgets/scroller.py index 94e00872d2..e50a48bbc5 100644 --- a/system/ui/widgets/scroller.py +++ b/system/ui/widgets/scroller.py @@ -62,7 +62,7 @@ class ScrollIndicator(Widget): class Scroller(Widget): - def __init__(self, items: list[Widget], horizontal: bool = True, snap_items: bool = True, spacing: int = ITEM_SPACING, + def __init__(self, items: list[Widget], horizontal: bool = True, snap_items: bool = False, spacing: int = ITEM_SPACING, pad: int = ITEM_SPACING, scroll_indicator: bool = True, edge_shadows: bool = True): super().__init__() self._items: list[Widget] = [] From a3f40dbac38214f2bfd3b5fda2f94cdde7f99a31 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Sat, 21 Feb 2026 22:50:59 -0800 Subject: [PATCH 140/311] ui: add Layout class (#37311) * split nav widget out * clean up * clean up * fix * work * small enough to not be function * nah we want intflag * clean up * always runs * more clean up * prep for scroller * opacity for settings * clean up layout * set enabled * rm --- selfdrive/ui/mici/layouts/home.py | 148 ++++++++++++++---------------- system/ui/widgets/icon_widget.py | 16 ++++ system/ui/widgets/layouts.py | 60 ++++++++++++ 3 files changed, 143 insertions(+), 81 deletions(-) create mode 100644 system/ui/widgets/icon_widget.py create mode 100644 system/ui/widgets/layouts.py diff --git a/selfdrive/ui/mici/layouts/home.py b/selfdrive/ui/mici/layouts/home.py index d4bbb74914..5e7e9bfea7 100644 --- a/selfdrive/ui/mici/layouts/home.py +++ b/selfdrive/ui/mici/layouts/home.py @@ -3,8 +3,10 @@ import time from cereal import log import pyray as rl from collections.abc import Callable -from openpilot.system.ui.widgets.label import gui_label, MiciLabel, UnifiedLabel from openpilot.system.ui.widgets import Widget +from openpilot.system.ui.widgets.layouts import HBoxLayout +from openpilot.system.ui.widgets.icon_widget import IconWidget +from openpilot.system.ui.widgets.label import gui_label, MiciLabel, UnifiedLabel from openpilot.system.ui.lib.application import gui_app, FontWeight, DEFAULT_TEXT_COLOR, MousePos from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.system.ui.text import wrap_text @@ -77,24 +79,11 @@ class DeviceStatus(Widget): font_weight=FontWeight.MEDIUM, alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT) -class MiciHomeLayout(Widget): +class NetworkIcon(Widget): def __init__(self): super().__init__() - 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 - - self._settings_txt = gui_app.texture("icons_mici/settings.png", 48, 48) - self._experimental_txt = gui_app.texture("icons_mici/experimental_mode.png", 48, 48) - self._mic_txt = gui_app.texture("icons_mici/microphone.png", 32, 46) - - self._net_type = NETWORK_TYPES.get(NetworkType.none) + self.set_rect(rl.Rectangle(0, 0, 54, 44)) # max size of all icons + self._net_type = NetworkType.none self._net_strength = 0 self._wifi_slash_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_slash.png", 50, 44) @@ -109,6 +98,63 @@ class MiciHomeLayout(Widget): self._cell_high_txt = gui_app.texture("icons_mici/settings/network/cell_strength_high.png", 54, 36) self._cell_full_txt = gui_app.texture("icons_mici/settings/network/cell_strength_full.png", 54, 36) + def _update_state(self): + device_state = ui_state.sm['deviceState'] + self._net_type = device_state.networkType + strength = device_state.networkStrength + self._net_strength = max(0, min(5, strength.raw + 1)) if strength.raw > 0 else 0 + + def _render(self, _): + # draw network + if self._net_type == NetworkType.wifi: + # There is no 1 + draw_net_txt = {0: self._wifi_none_txt, + 2: self._wifi_low_txt, + 3: self._wifi_medium_txt, + 4: self._wifi_full_txt, + 5: self._wifi_full_txt}.get(self._net_strength, self._wifi_low_txt) + elif self._net_type in (NetworkType.cell2G, NetworkType.cell3G, NetworkType.cell4G, NetworkType.cell5G): + draw_net_txt = {0: self._cell_none_txt, + 2: self._cell_low_txt, + 3: self._cell_medium_txt, + 4: self._cell_high_txt, + 5: self._cell_full_txt}.get(self._net_strength, self._cell_none_txt) + else: + draw_net_txt = self._wifi_slash_txt + + draw_x = self._rect.x + (self._rect.width - draw_net_txt.width) / 2 + draw_y = self._rect.y + (self._rect.height - draw_net_txt.height) / 2 + + if draw_net_txt == self._wifi_slash_txt: + # Offset by difference in height between slashless and slash icons to make center align match + draw_y -= (self._wifi_slash_txt.height - self._wifi_none_txt.height) / 2 + + rl.draw_texture(draw_net_txt, int(draw_x), int(draw_y), rl.Color(255, 255, 255, int(255 * 0.9))) + + +class MiciHomeLayout(Widget): + def __init__(self): + super().__init__() + 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 + + self._experimental_icon = IconWidget("icons_mici/experimental_mode.png", (48, 48)) + self._mic_icon = IconWidget("icons_mici/microphone.png", (32, 46)) + + self._status_bar_layout = HBoxLayout([ + IconWidget("icons_mici/settings.png", (48, 48), opacity=0.9), + NetworkIcon(), + self._experimental_icon, + self._mic_icon, + ], spacing=18) + self._openpilot_label = MiciLabel("openpilot", font_size=96, color=rl.Color(255, 255, 255, int(255 * 0.9)), font_weight=FontWeight.DISPLAY) self._version_label = MiciLabel("", font_size=36, font_weight=FontWeight.ROMAN) self._large_version_label = MiciLabel("", font_size=64, color=rl.GRAY, font_weight=FontWeight.ROMAN) @@ -118,7 +164,6 @@ class MiciHomeLayout(Widget): def show_event(self): self._version_text = self._get_version_text() - self._update_network_status(ui_state.sm['deviceState']) self._update_params() def _update_params(self): @@ -142,19 +187,11 @@ class MiciHomeLayout(Widget): 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) - # Update version text self._version_text = self._get_version_text() self._last_refresh = rl.get_time() self._update_params() - def _update_network_status(self, device_state): - self._net_type = device_state.networkType - strength = device_state.networkStrength - self._net_strength = max(0, min(5, strength.raw + 1)) if strength.raw > 0 else 0 - def set_callbacks(self, on_settings: Callable | None = None): self._on_settings_click = on_settings @@ -206,60 +243,9 @@ class MiciHomeLayout(Widget): self._version_commit_label.set_position(version_pos.x, version_pos.y + self._date_label.font_size + 7) self._version_commit_label.render() - self._render_bottom_status_bar() - - def _render_bottom_status_bar(self): # ***** Center-aligned bottom section icons ***** + self._experimental_icon.set_visible(self._experimental_mode) + self._mic_icon.set_visible(ui_state.recording_audio) - # TODO: refactor repeated icon drawing into a small loop - ITEM_SPACING = 18 - Y_CENTER = 24 - - last_x = self.rect.x + HOME_PADDING - - # Draw settings icon in bottom left corner - rl.draw_texture(self._settings_txt, int(last_x), int(self._rect.y + self.rect.height - self._settings_txt.height / 2 - Y_CENTER), - rl.Color(255, 255, 255, int(255 * 0.9))) - last_x = last_x + self._settings_txt.width + ITEM_SPACING - - # draw network - if self._net_type == NetworkType.wifi: - # There is no 1 - draw_net_txt = {0: self._wifi_none_txt, - 2: self._wifi_low_txt, - 3: self._wifi_medium_txt, - 4: self._wifi_full_txt, - 5: self._wifi_full_txt}.get(self._net_strength, self._wifi_low_txt) - rl.draw_texture(draw_net_txt, int(last_x), - int(self._rect.y + self.rect.height - draw_net_txt.height / 2 - Y_CENTER), rl.Color(255, 255, 255, int(255 * 0.9))) - last_x += draw_net_txt.width + ITEM_SPACING - - elif self._net_type in (NetworkType.cell2G, NetworkType.cell3G, NetworkType.cell4G, NetworkType.cell5G): - draw_net_txt = {0: self._cell_none_txt, - 2: self._cell_low_txt, - 3: self._cell_medium_txt, - 4: self._cell_high_txt, - 5: self._cell_full_txt}.get(self._net_strength, self._cell_none_txt) - rl.draw_texture(draw_net_txt, int(last_x), - int(self._rect.y + self.rect.height - draw_net_txt.height / 2 - Y_CENTER), rl.Color(255, 255, 255, int(255 * 0.9))) - last_x += draw_net_txt.width + ITEM_SPACING - - else: - # No network - # Offset by difference in height between slashless and slash icons to make center align match - rl.draw_texture(self._wifi_slash_txt, int(last_x), int(self._rect.y + self.rect.height - self._wifi_slash_txt.height / 2 - - (self._wifi_slash_txt.height - self._wifi_none_txt.height) / 2 - Y_CENTER), - rl.Color(255, 255, 255, int(255 * 0.9))) - last_x += self._wifi_slash_txt.width + ITEM_SPACING - - # draw experimental icon - if self._experimental_mode: - rl.draw_texture(self._experimental_txt, int(last_x), - int(self._rect.y + self.rect.height - self._experimental_txt.height / 2 - Y_CENTER), rl.Color(255, 255, 255, 255)) - last_x += self._experimental_txt.width + ITEM_SPACING - - # draw microphone icon when recording audio is enabled - if ui_state.recording_audio: - rl.draw_texture(self._mic_txt, int(last_x), - int(self._rect.y + self.rect.height - self._mic_txt.height / 2 - Y_CENTER), rl.Color(255, 255, 255, 255)) - last_x += self._mic_txt.width + ITEM_SPACING + footer_rect = rl.Rectangle(self.rect.x + HOME_PADDING, self.rect.y + self.rect.height - 48, self.rect.width - HOME_PADDING, 48) + self._status_bar_layout.render(footer_rect) diff --git a/system/ui/widgets/icon_widget.py b/system/ui/widgets/icon_widget.py new file mode 100644 index 0000000000..bf7790b937 --- /dev/null +++ b/system/ui/widgets/icon_widget.py @@ -0,0 +1,16 @@ +import pyray as rl +from openpilot.system.ui.lib.application import gui_app +from openpilot.system.ui.widgets import Widget + + +class IconWidget(Widget): + def __init__(self, image_path: str, size: tuple[int, int], opacity: float = 1.0): + super().__init__() + self._texture = gui_app.texture(image_path, size[0], size[1]) + self._opacity = opacity + self.set_rect(rl.Rectangle(0, 0, float(size[0]), float(size[1]))) + self.set_enabled(False) + + def _render(self, _) -> None: + color = rl.Color(255, 255, 255, int(self._opacity * 255)) + rl.draw_texture_ex(self._texture, rl.Vector2(self._rect.x, self._rect.y), 0.0, 1.0, color) diff --git a/system/ui/widgets/layouts.py b/system/ui/widgets/layouts.py new file mode 100644 index 0000000000..6f97fe5ed8 --- /dev/null +++ b/system/ui/widgets/layouts.py @@ -0,0 +1,60 @@ +from enum import IntFlag +from openpilot.system.ui.widgets import Widget + + +class Alignment(IntFlag): + LEFT = 0 + # TODO: implement + # H_CENTER = 2 + # RIGHT = 4 + + TOP = 8 + V_CENTER = 16 + BOTTOM = 32 + + +class HBoxLayout(Widget): + """ + A Widget that lays out child Widgets horizontally. + """ + + def __init__(self, widgets: list[Widget] | None = None, spacing: int = 0, + alignment: Alignment = Alignment.LEFT | Alignment.V_CENTER): + super().__init__() + self._widgets: list[Widget] = [] + self._spacing = spacing + self._alignment = alignment + + if widgets is not None: + for widget in widgets: + self.add_widget(widget) + + @property + def widgets(self) -> list[Widget]: + return self._widgets + + def add_widget(self, widget: Widget) -> None: + self._widgets.append(widget) + + def _render(self, _): + visible_widgets = [w for w in self._widgets if w.is_visible] + + cur_offset_x = 0 + + for idx, widget in enumerate(visible_widgets): + spacing = self._spacing if (idx > 0) else 0 + + x = self._rect.x + cur_offset_x + spacing + cur_offset_x += widget.rect.width + spacing + + if self._alignment & Alignment.TOP: + y = self._rect.y + elif self._alignment & Alignment.BOTTOM: + y = self._rect.y + self._rect.height - widget.rect.height + else: # center + y = self._rect.y + (self._rect.height - widget.rect.height) / 2 + + # Update widget position and render + widget.set_position(round(x), round(y)) + widget.set_parent_rect(self._rect) + widget.render() From 8fa3f60de731026fdbec7d2c27dd11704c1d5516 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Sat, 21 Feb 2026 22:54:18 -0800 Subject: [PATCH 141/311] mici ui: remove DeviceStatus (#37317) rm --- selfdrive/ui/mici/layouts/home.py | 59 ++----------------------------- 1 file changed, 3 insertions(+), 56 deletions(-) diff --git a/selfdrive/ui/mici/layouts/home.py b/selfdrive/ui/mici/layouts/home.py index 5e7e9bfea7..31884e5f18 100644 --- a/selfdrive/ui/mici/layouts/home.py +++ b/selfdrive/ui/mici/layouts/home.py @@ -6,11 +6,10 @@ from collections.abc import Callable from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.layouts import HBoxLayout from openpilot.system.ui.widgets.icon_widget import IconWidget -from openpilot.system.ui.widgets.label import gui_label, MiciLabel, UnifiedLabel -from openpilot.system.ui.lib.application import gui_app, FontWeight, DEFAULT_TEXT_COLOR, MousePos +from openpilot.system.ui.widgets.label import MiciLabel, UnifiedLabel +from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos from openpilot.selfdrive.ui.ui_state import ui_state -from openpilot.system.ui.text import wrap_text -from openpilot.system.version import training_version, RELEASE_BRANCHES +from openpilot.system.version import RELEASE_BRANCHES HEAD_BUTTON_FONT_SIZE = 40 HOME_PADDING = 8 @@ -28,57 +27,6 @@ NETWORK_TYPES = { } -class DeviceStatus(Widget): - def __init__(self): - super().__init__() - self.set_rect(rl.Rectangle(0, 0, 300, 175)) - self._update_state() - self._version_text = self._get_version_text() - - self._do_welcome() - - def _do_welcome(self): - ui_state.params.put("CompletedTrainingVersion", training_version) - - def refresh(self): - self._update_state() - self._version_text = self._get_version_text() - - def _get_version_text(self) -> str: - brand = "openpilot" - description = ui_state.params.get("UpdaterCurrentDescription") - return f"{brand} {description}" if description else brand - - def _update_state(self): - # TODO: refresh function that can be called periodically, not at 60 fps, so we can update version - # update system status - self._system_status = "SYSTEM READY ✓" if ui_state.panda_type != log.PandaState.PandaType.unknown else "BOOTING UP..." - - # update network status - strength = ui_state.sm['deviceState'].networkStrength.raw - strength_text = "● " * strength + "○ " * (4 - strength) # ◌ also works - network_type = NETWORK_TYPES[ui_state.sm['deviceState'].networkType.raw] - self._network_status = f"{network_type} {strength_text}" - - def _render(self, _): - # draw status - status_rect = rl.Rectangle(self._rect.x, self._rect.y, self._rect.width, 40) - gui_label(status_rect, self._system_status, font_size=HEAD_BUTTON_FONT_SIZE, color=DEFAULT_TEXT_COLOR, - font_weight=FontWeight.BOLD, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER) - - # draw network status - network_rect = rl.Rectangle(self._rect.x, self._rect.y + 60, self._rect.width, 40) - gui_label(network_rect, self._network_status, font_size=40, color=DEFAULT_TEXT_COLOR, - font_weight=FontWeight.MEDIUM, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER) - - # draw version - version_font_size = 30 - version_rect = rl.Rectangle(self._rect.x, self._rect.y + 140, self._rect.width + 20, 40) - wrapped_text = '\n'.join(wrap_text(self._version_text, version_font_size, version_rect.width)) - gui_label(version_rect, wrapped_text, font_size=version_font_size, color=DEFAULT_TEXT_COLOR, - font_weight=FontWeight.MEDIUM, alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT) - - class NetworkIcon(Widget): def __init__(self): super().__init__() @@ -105,7 +53,6 @@ class NetworkIcon(Widget): self._net_strength = max(0, min(5, strength.raw + 1)) if strength.raw > 0 else 0 def _render(self, _): - # draw network if self._net_type == NetworkType.wifi: # There is no 1 draw_net_txt = {0: self._wifi_none_txt, From 6fcd2187e18b295f36cceedc610bf3706fedb168 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Sat, 21 Feb 2026 23:33:47 -0800 Subject: [PATCH 142/311] scroller: items property --- .../ui/mici/layouts/settings/network/wifi_ui.py | 12 ++++++------ selfdrive/ui/mici/widgets/dialog.py | 8 ++++---- system/ui/widgets/scroller.py | 4 ++++ 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py index b974b502a1..1c5bd58871 100644 --- a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py +++ b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py @@ -363,7 +363,7 @@ class WifiUIMici(BigMultiOptionDialog): # Clear scroller items and update from latest scan results super().show_event() self._wifi_manager.set_active(True) - self._scroller._items.clear() + self._scroller.items.clear() self._update_buttons() def hide_event(self): @@ -379,22 +379,22 @@ class WifiUIMici(BigMultiOptionDialog): # Only add new buttons to the end. Update existing buttons without re-sorting so user can freely scroll around for network in self._networks.values(): - network_button_idx = next((i for i, btn in enumerate(self._scroller._items) if btn.option == network.ssid), None) + network_button_idx = next((i for i, btn in enumerate(self._scroller.items) if btn.option == network.ssid), None) if network_button_idx is not None: # Update network on existing button - self._scroller._items[network_button_idx].set_current_network(network) + self._scroller.items[network_button_idx].set_current_network(network) else: network_button = WifiItem(network, lambda: self._wifi_manager.wifi_state) self._scroller.add_widget(network_button) # Move connecting/connected network to the start - connected_btn_idx = next((i for i, btn in enumerate(self._scroller._items) if self._wifi_manager.wifi_state.ssid == btn._network.ssid), None) + connected_btn_idx = next((i for i, btn in enumerate(self._scroller.items) if self._wifi_manager.wifi_state.ssid == btn._network.ssid), None) if connected_btn_idx is not None and connected_btn_idx > 0: - self._scroller._items.insert(0, self._scroller._items.pop(connected_btn_idx)) + self._scroller.items.insert(0, self._scroller.items.pop(connected_btn_idx)) self._scroller._layout() # fixes selected style single frame stutter # Disable networks no longer present - for btn in self._scroller._items: + for btn in self._scroller.items: if btn.option not in self._networks: btn.set_enabled(False) btn.set_network_missing(True) diff --git a/selfdrive/ui/mici/widgets/dialog.py b/selfdrive/ui/mici/widgets/dialog.py index c695949e49..32624bbdd6 100644 --- a/selfdrive/ui/mici/widgets/dialog.py +++ b/selfdrive/ui/mici/widgets/dialog.py @@ -313,7 +313,7 @@ class BigMultiOptionDialog(BigDialogBase): def _on_option_selected(self, option: str): y_pos = 0.0 - for btn in self._scroller._items: + for btn in self._scroller.items: btn = cast(BigDialogOptionButton, btn) if btn.option == option: rect_center_y = self._rect.y + self._rect.height / 2 @@ -350,7 +350,7 @@ class BigMultiOptionDialog(BigDialogBase): return # select current option - for btn in self._scroller._items: + for btn in self._scroller.items: btn = cast(BigDialogOptionButton, btn) if btn.option == self._selected_option: self._on_option_selected(btn.option) @@ -362,13 +362,13 @@ class BigMultiOptionDialog(BigDialogBase): # get selection by whichever button is closest to center center_y = self._rect.y + self._rect.height / 2 closest_btn = (None, float('inf')) - for btn in self._scroller._items: + for btn in self._scroller.items: dist_y = abs((btn.rect.y + btn.rect.height / 2) - center_y) if dist_y < closest_btn[1]: closest_btn = (btn, dist_y) if closest_btn[0]: - for btn in self._scroller._items: + for btn in self._scroller.items: btn.set_selected(btn.option == closest_btn[0].option) self._selected_option = closest_btn[0].option diff --git a/system/ui/widgets/scroller.py b/system/ui/widgets/scroller.py index e50a48bbc5..8de86fca9e 100644 --- a/system/ui/widgets/scroller.py +++ b/system/ui/widgets/scroller.py @@ -117,6 +117,10 @@ class Scroller(Widget): def is_auto_scrolling(self) -> bool: return self._scrolling_to is not None + @property + def items(self) -> list[Widget]: + return self._items + def add_widget(self, item: Widget) -> None: self._items.append(item) item.set_touch_valid_callback(lambda: self.scroll_panel.is_touch_valid() and self.enabled) From 517289f3a5c1686801200337e8d762168c697d0b Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Sun, 22 Feb 2026 03:36:31 -0800 Subject: [PATCH 143/311] mici scroller: add move animation (#37319) * already 90% of the way there and not 144 lines * nice * lift properly * lift, wait, move, wait, drop! * some clean up * epic, he ran a simulation to turn opacity filter into pixels * scroll independant move animation without layout! * move into function * clean up * rm * overlay behind moving item * Revert "overlay behind moving item" This reverts commit 598e22363eb66af6496fe5f1eea8e643d4c2adbb. * simpler overlay under lifted item * support multiple animations at once * Revert "support multiple animations at once" This reverts commit 3ce6c8281053ee4831ceb88cacf66c343fc7d7ff. * clean up * cmt * clean up * kinda works * Revert "kinda works" This reverts commit ff050c6afc058788b3189a0acc202ada17353504. * clean up clean up * clear overlay * diff report * don't break more --- system/ui/widgets/scroller.py | 126 ++++++++++++++++++++++++++++++---- 1 file changed, 112 insertions(+), 14 deletions(-) diff --git a/system/ui/widgets/scroller.py b/system/ui/widgets/scroller.py index 8de86fca9e..8630b4703f 100644 --- a/system/ui/widgets/scroller.py +++ b/system/ui/widgets/scroller.py @@ -3,6 +3,7 @@ import numpy as np from collections.abc import Callable from openpilot.common.filter_simple import FirstOrderFilter, BounceFilter +from openpilot.common.swaglog import cloudlog from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.scroll_panel2 import GuiScrollPanel2, ScrollState from openpilot.system.ui.widgets import Widget @@ -12,6 +13,9 @@ LINE_COLOR = rl.GRAY LINE_PADDING = 40 ANIMATION_SCALE = 0.6 +MOVE_LIFT = 20 +MOVE_OVERLAY_ALPHA = 0.65 + EDGE_SHADOW_WIDTH = 20 MIN_ZOOM_ANIMATION_TIME = 0.075 # seconds @@ -95,6 +99,15 @@ class Scroller(Widget): self._scroll_indicator = ScrollIndicator() self._edge_shadows = edge_shadows and self._horizontal + # move animation state + # on move; lift src widget -> wait -> move all -> wait -> drop src widget + self._overlay_filter = FirstOrderFilter(0.0, 0.05, 1 / gui_app.target_fps) + self._move_animations: dict[Widget, FirstOrderFilter] = {} + self._move_lift: dict[Widget, FirstOrderFilter] = {} + # these are used to wait before moving/dropping, also to move onto next part of the animation earlier for timing + self._pending_lift: set[Widget] = set() + self._pending_move: set[Widget] = set() + for item in items: self.add_widget(item) @@ -123,7 +136,8 @@ class Scroller(Widget): def add_widget(self, item: Widget) -> None: self._items.append(item) - item.set_touch_valid_callback(lambda: self.scroll_panel.is_touch_valid() and self.enabled) + item.set_touch_valid_callback(lambda: self.scroll_panel.is_touch_valid() and self.enabled and self._scrolling_to is None + and not self.moving_items) def set_scrolling_enabled(self, enabled: bool | Callable[[], bool]) -> None: """Set whether scrolling is enabled (does not affect widget enabled state).""" @@ -197,6 +211,69 @@ class Scroller(Widget): return self.scroll_panel.get_offset() + @property + def moving_items(self) -> bool: + return len(self._move_animations) > 0 or len(self._move_lift) > 0 + + def move_item(self, from_idx: int, to_idx: int): + assert self._horizontal + if from_idx == to_idx: + return + + if self.moving_items: + cloudlog.warning(f"Already moving items, cannot move from {from_idx} to {to_idx}") + return + + item = self._items.pop(from_idx) + self._items.insert(to_idx, item) + + # store original position in content space of all affected widgets to animate from + for idx in range(min(from_idx, to_idx), max(from_idx, to_idx) + 1): + affected_item = self._items[idx] + self._move_animations[affected_item] = FirstOrderFilter(affected_item.rect.x - self._scroll_offset, 0.15, 1 / gui_app.target_fps) + self._pending_move.add(affected_item) + + # lift only src widget to make it more clear which one is moving + self._move_lift[item] = FirstOrderFilter(0.0, 0.15, 1 / gui_app.target_fps) + self._pending_lift.add(item) + + def _do_move_animation(self, item: Widget, target_x: float, target_y: float) -> tuple[float, float]: + if item in self._move_lift: + lift_filter = self._move_lift[item] + + # Animate lift + if len(self._pending_move) > 0: + lift_filter.update(MOVE_LIFT) + # start moving when almost lifted + if abs(lift_filter.x - MOVE_LIFT) < 2: + self._pending_lift.discard(item) + else: + # if done moving, animate down + lift_filter.update(0) + if abs(lift_filter.x) < 1: + del self._move_lift[item] + target_y -= lift_filter.x + + # Animate move + if item in self._move_animations: + move_filter = self._move_animations[item] + + # compare/update in content space to match filter + content_x = target_x - self._scroll_offset + if len(self._pending_lift) == 0: + move_filter.update(content_x) + + # drop when close to target + if abs(move_filter.x - content_x) < 10: + self._pending_move.discard(item) + + # finished moving + if abs(move_filter.x - content_x) < 1: + del self._move_animations[item] + target_x = move_filter.x + self._scroll_offset + + return target_x, target_y + def _layout(self): self._visible_items = [item for item in self._items if item.is_visible] @@ -242,30 +319,46 @@ class Scroller(Widget): [self._item_pos_filter.x, self._scroll_offset, self._item_pos_filter.x]) y -= np.clip(jello_offset, -20, 20) + # Animate moves if needed + x, y = self._do_move_animation(item, x, y) + # Update item state item.set_position(round(x), round(y)) # round to prevent jumping when settling item.set_parent_rect(self._rect) + def _render_item(self, item: Widget): + # Skip rendering if not in viewport + if not rl.check_collision_recs(item.rect, self._rect): + return + + # Scale each element around its own origin when scrolling + scale = self._zoom_filter.x + if scale != 1.0: + rl.rl_push_matrix() + rl.rl_scalef(scale, scale, 1.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() + def _render(self, _): rl.begin_scissor_mode(int(self._rect.x), int(self._rect.y), int(self._rect.width), int(self._rect.height)) for item in reversed(self._visible_items): - # Skip rendering if not in viewport - if not rl.check_collision_recs(item.rect, self._rect): + if item in self._move_lift: continue + self._render_item(item) - # Scale each element around its own origin when scrolling - scale = self._zoom_filter.x - if scale != 1.0: - rl.rl_push_matrix() - rl.rl_scalef(scale, scale, 1.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() + # Dim background if moving items, lifted items are above + self._overlay_filter.update(MOVE_OVERLAY_ALPHA if self.moving_items else 0.0) + if self._overlay_filter.x > 0.01: + rl.draw_rectangle_rec(self._rect, rl.Color(0, 0, 0, int(255 * self._overlay_filter.x))) + + for item in self._move_lift: + self._render_item(item) rl.end_scissor_mode() @@ -295,5 +388,10 @@ class Scroller(Widget): def hide_event(self): super().hide_event() + self._overlay_filter.x = 0.0 + self._move_animations.clear() + self._move_lift.clear() + self._pending_lift.clear() + self._pending_move.clear() for item in self._items: item.hide_event() From 1b262a5a52088bf8ea1b05e9cfc7c609a6841bcc Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Sun, 22 Feb 2026 03:55:13 -0800 Subject: [PATCH 144/311] Scroller: fix overlay --- 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 8630b4703f..9393dd07ea 100644 --- a/system/ui/widgets/scroller.py +++ b/system/ui/widgets/scroller.py @@ -353,7 +353,7 @@ class Scroller(Widget): self._render_item(item) # Dim background if moving items, lifted items are above - self._overlay_filter.update(MOVE_OVERLAY_ALPHA if self.moving_items else 0.0) + self._overlay_filter.update(MOVE_OVERLAY_ALPHA if len(self._pending_move) else 0.0) if self._overlay_filter.x > 0.01: rl.draw_rectangle_rec(self._rect, rl.Color(0, 0, 0, int(255 * self._overlay_filter.x))) From 31ac5a216d9016fd54f24085fa023622d6e28dd8 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Sun, 22 Feb 2026 05:49:27 -0800 Subject: [PATCH 145/311] WifiManager: fix NEED_AUTH for wrong network (#37320) * stash * test seemed to work * simplify * clean up * move under * Revert "move under" This reverts commit ce940cffb32378cbe5a69edaf6fc9d9cec202e54. * back * fix --- system/ui/lib/wifi_manager.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index 25c4548e94..f95ac7d886 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -146,6 +146,7 @@ class ConnectStatus(IntEnum): @dataclass class WifiState: ssid: str | None = None + prev_ssid: str | None = None status: ConnectStatus = ConnectStatus.DISCONNECTED @@ -284,7 +285,10 @@ class WifiManager: return self._tethering_password def _set_connecting(self, ssid: str | None): - self._wifi_state = WifiState(ssid=ssid, status=ConnectStatus.DISCONNECTED if ssid is None else ConnectStatus.CONNECTING) + # Track prev ssid so late NEED_AUTH signals target the right network + self._wifi_state = WifiState(ssid=ssid, + prev_ssid=self.connecting_to_ssid if ssid is not None else None, + status=ConnectStatus.DISCONNECTED if ssid is None else ConnectStatus.CONNECTING) def _enqueue_callbacks(self, cbs: list[Callable], *args): for cb in cbs: @@ -391,16 +395,18 @@ class WifiManager: self._wifi_state = wifi_state - # BAD PASSWORD + # BAD PASSWORD - use prev if current has already moved on to a new connection # - strong network rejects with NEED_AUTH+SUPPLICANT_DISCONNECT # - weak/gone network fails with FAILED+NO_SECRETS elif ((new_state == NMDeviceState.NEED_AUTH and change_reason == NMDeviceStateReason.SUPPLICANT_DISCONNECT) or (new_state == NMDeviceState.FAILED and change_reason == NMDeviceStateReason.NO_SECRETS)): - if self._wifi_state.ssid: - self._enqueue_callbacks(self._need_auth, self._wifi_state.ssid) - - self._set_connecting(None) + failed_ssid = self._wifi_state.prev_ssid or self._wifi_state.ssid + if failed_ssid: + self._enqueue_callbacks(self._need_auth, failed_ssid) + self._wifi_state.prev_ssid = None + if self._wifi_state.ssid == failed_ssid: + self._set_connecting(None) elif new_state in (NMDeviceState.NEED_AUTH, NMDeviceState.IP_CONFIG, NMDeviceState.IP_CHECK, NMDeviceState.SECONDARIES, NMDeviceState.FAILED): @@ -410,7 +416,7 @@ class WifiManager: # Note that IP address from Ip4Config may not be propagated immediately and could take until the next scan results self._update_networks() - wifi_state = replace(self._wifi_state, status=ConnectStatus.CONNECTED) + wifi_state = replace(self._wifi_state, prev_ssid=None, status=ConnectStatus.CONNECTED) conn_path, _ = self._get_active_wifi_connection(self._conn_monitor) if conn_path is None: From 5f722d2c93199ae22922a3cb2f8e8cbb6870892d Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Sun, 22 Feb 2026 07:08:48 -0800 Subject: [PATCH 146/311] four: new wifi ui design (#37152) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * start * start * lil more * add forget * fix forget button scrolling * push right a bit * fix forget press * add divider * fix scroll panel * better forget and overriding * revert this * check icon * cursor merge conflict fix * fix rounding and forget btn placement * scroll indicator * 65% * calibrate * try loading animation * push to device * top right * bottom right * no red * top left * bottom left * down 2px * WHY DOES NETWORK MANAGER KEEP CRASHING AHHH * reduce round trip calls in update_networks * clean up and combine getallaccesspoint and activeaccesspoint * cmt * animate big button over smoothly. super hacky, need to clean up * animate * remove old widgets and images * remove status label, tune loading animation opac back * connecting is a little buggy still * add back missing network and don't pop * some fixes * "clean up" * fix lag in animation * fix adding saved connection to start * remove saved network to start, divider * animate up, over, and down * revert for now * remove fancy complex animation for now, sorry nick * remove divider + clean up * more clean up * more clean up * fix forget button press * cmt * tweak loading animation behavior * new lock fix wifi * rm old lock * great catch by opus * clean up * debug * fix touch events that are down -> up in one frame (why it only bugged on mici) * clean up * eager forgetting * this SHOULD be full eager forget, more than i thought * fix wifi slash positioning * move forgotten networks after saved networks * temp keep * test on device * fix * see 65 * 5 best * fix double render double brightness * can click bottom right now * disable touch while animating * fix animation * can scroll while animating, not tap * not great yet * clean up * didn't work * always update networks after activation * stash * move to update_state * debug * debug * temp * fix ip and metered flickering when updating at high freq (or rare race condition) * fix * if you give it less than 8 chars it never clears connecting * lock no int * better wrong password handling * shake when wrong password * nm set connecting when it connects on its own * loading bottom right * sort connecting first * sort by unquantized to put strongest first * clean up * clean up nm * clean up nm * shorter * fix crash * 0.5s * debug * revert and try something else * stash * no * rev * use signals * more * not wrong password if ever connected after wrong * similar to gnome shell, don't save connection that never successfully activated. we do this by creating temporary memory connection with persist: volatile that deletes itself if failed, and then only write to disk when activated * clean up * cover all states * clear if connecting too * remove pritn * might need this for CoxWifi * whoops * save last pass * Revert "whoops" This reverts commit 83a133955246ce32dcf119ededd8b01b3162a866. * Revert "might need this for CoxWifi" This reverts commit cddb8b35be152ed154462b188283f9d5a844583d. * this may be less noisy for low strength networks, but less accurate as previous was reflecting nm state better * Revert "this may be less noisy for low strength networks, but less accurate as previous was reflecting nm state better" This reverts commit 740286c846556f32125a96bfe6ecf128300af0d8. * race condition with volotile not removing conn fast enough/update networks not firing fast enough * Revert "save last pass" This reverts commit 7249a58a18b11487fd0370cee36e40a17f7ac521. * revert some wifiman stuff to master * not needed * rm active ap * remove old dead code * do after * always send forgotten callback so we can't be stuck in forgetting state forever * reproduce race condition where connection removed signal takes a while to remove, then update networks keep is_saved true * fix from merge * nice, we can remove some eager code now for treating is_saved as not saved after forgot since it's live * more * rm * simplify passed in callbacks * clean up * need this one check back for wrong password to hide forget for a split second * opus says this is simpler 🤔 * Revert "opus says this is simpler 🤔" This reverts commit 71472e5b383d7f2083d95ba1188070f41ae14775. * another attempt * Revert "another attempt" This reverts commit 31f30babe656f9cad24399bc2196bb6e7ab79bbd. * fix from merge * some lcean up * fix * fixes to make work with new animation * clean up * this works too * simplify loading animation behavior for now, revert wifi scan time * clean up * temporary fix * stash * Revert "stash" This reverts commit 7471dbdc452807b33b4868a98dd8565681b2e44d. * stash * Revert "stash" This reverts commit e0e5e6e861734320ce5dea5626086784577cb334. * this check was because is_connected could have been stale from Network as the source * nm can show connected/connecting to network with 0 aps for a while if strength is low, move out of range under those states * stash * Revert "stash" This reverts commit 5ec3b454d54392523947f6477f551657d3863a6d. * todo * todo * order * don't need temporary fix anymore * cmt * order * unused i --- .../settings/network/new/connect_button.png | 3 - .../network/new/connect_button_pressed.png | 3 - .../network/new/full_connect_button.png | 3 - .../new/full_connect_button_pressed.png | 3 - .../icons_mici/settings/network/new/lock.png | 4 +- .../settings/network/new/wifi_selected.png | 3 - .../mici/layouts/settings/network/wifi_ui.py | 550 ++++++++---------- system/ui/lib/networkmanager.py | 1 + system/ui/lib/wifi_manager.py | 7 +- system/ui/widgets/scroller.py | 18 +- 10 files changed, 279 insertions(+), 316 deletions(-) delete mode 100644 selfdrive/assets/icons_mici/settings/network/new/connect_button.png delete mode 100644 selfdrive/assets/icons_mici/settings/network/new/connect_button_pressed.png delete mode 100644 selfdrive/assets/icons_mici/settings/network/new/full_connect_button.png delete mode 100644 selfdrive/assets/icons_mici/settings/network/new/full_connect_button_pressed.png delete mode 100644 selfdrive/assets/icons_mici/settings/network/new/wifi_selected.png diff --git a/selfdrive/assets/icons_mici/settings/network/new/connect_button.png b/selfdrive/assets/icons_mici/settings/network/new/connect_button.png deleted file mode 100644 index eae5af77f0..0000000000 --- a/selfdrive/assets/icons_mici/settings/network/new/connect_button.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:04236fa0f2759a01c6e321ac7b1c86c7a039215a7953b1a23d250ecf2ef1fa87 -size 8563 diff --git a/selfdrive/assets/icons_mici/settings/network/new/connect_button_pressed.png b/selfdrive/assets/icons_mici/settings/network/new/connect_button_pressed.png deleted file mode 100644 index 0da6c384d9..0000000000 --- a/selfdrive/assets/icons_mici/settings/network/new/connect_button_pressed.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4337098554af30c98ebd512e17ab08207db868ff34acca5f865fcbfc940286d3 -size 21123 diff --git a/selfdrive/assets/icons_mici/settings/network/new/full_connect_button.png b/selfdrive/assets/icons_mici/settings/network/new/full_connect_button.png deleted file mode 100644 index 905170fd10..0000000000 --- a/selfdrive/assets/icons_mici/settings/network/new/full_connect_button.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ffd37d5e5d5980efa98fee1cd0e8ebbf4139149b41c099e7dc3d5bd402cffb92 -size 9072 diff --git a/selfdrive/assets/icons_mici/settings/network/new/full_connect_button_pressed.png b/selfdrive/assets/icons_mici/settings/network/new/full_connect_button_pressed.png deleted file mode 100644 index 88eb4ac2a3..0000000000 --- a/selfdrive/assets/icons_mici/settings/network/new/full_connect_button_pressed.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1b1d58704f8808dcb5a7ce9d86bc4212477759e96ac2419475f16f9184ee6a42 -size 21892 diff --git a/selfdrive/assets/icons_mici/settings/network/new/lock.png b/selfdrive/assets/icons_mici/settings/network/new/lock.png index 9fc152d3db..65bd71f654 100644 --- a/selfdrive/assets/icons_mici/settings/network/new/lock.png +++ b/selfdrive/assets/icons_mici/settings/network/new/lock.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:782161f35b4925c7063c441b0c341331c814614cf241f21b4e70134280c630f0 -size 1182 +oid sha256:7488c1aa69b728387b2cf300a614cc64e3c2305d2b509c14cf44cad65d20d85c +size 2509 diff --git a/selfdrive/assets/icons_mici/settings/network/new/wifi_selected.png b/selfdrive/assets/icons_mici/settings/network/new/wifi_selected.png deleted file mode 100644 index 2a3e837138..0000000000 --- a/selfdrive/assets/icons_mici/settings/network/new/wifi_selected.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:160f67162e075436200d6719e614ddf96caaa2b7c0a3943f728c2afef10aa4ad -size 2489 diff --git a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py index 1c5bd58871..c43a294e40 100644 --- a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py +++ b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py @@ -3,67 +3,78 @@ import numpy as np import pyray as rl from collections.abc import Callable +from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.swaglog import cloudlog -from openpilot.system.ui.widgets.label import UnifiedLabel -from openpilot.selfdrive.ui.mici.widgets.dialog import BigMultiOptionDialog, BigInputDialog, BigDialogOptionButton, BigConfirmationDialogV2 +from openpilot.selfdrive.ui.mici.widgets.dialog import BigInputDialog, BigConfirmationDialogV2 +from openpilot.selfdrive.ui.mici.widgets.button import BigButton, LABEL_COLOR, LABEL_HORIZONTAL_PADDING, LABEL_VERTICAL_PADDING from openpilot.system.ui.lib.application import gui_app, MousePos, FontWeight from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.nav_widget import NavWidget -from openpilot.system.ui.lib.wifi_manager import WifiManager, Network, SecurityType, WifiState, normalize_ssid +from openpilot.system.ui.widgets.scroller import Scroller +from openpilot.system.ui.lib.wifi_manager import WifiManager, Network, SecurityType, normalize_ssid class LoadingAnimation(Widget): - def _render(self, _): - cx = int(self._rect.x + 70) - cy = int(self._rect.y + self._rect.height / 2 - 50) + HIDE_TIME = 4 - y_mag = 20 - anim_scale = 5 - spacing = 28 + def __init__(self): + super().__init__() + self._opacity_filter = FirstOrderFilter(0.0, 0.1, 1 / gui_app.target_fps) + self._opacity_target = 1.0 + self._hide_time = 0.0 + + def show_event(self): + self._opacity_target = 1.0 + self._hide_time = rl.get_time() + + def _render(self, _): + if rl.get_time() - self._hide_time > self.HIDE_TIME: + self._opacity_target = 0.0 + + self._opacity_filter.update(self._opacity_target) + + if self._opacity_filter.x < 0.01: + return + + cx = int(self._rect.x + self._rect.width / 2) + cy = int(self._rect.y + self._rect.height / 2) + + y_mag = 7 + anim_scale = 4 + spacing = 14 for i in range(3): x = cx - spacing + i * spacing y = int(cy + min(math.sin((rl.get_time() - i * 0.2) * anim_scale) * y_mag, 0)) - alpha = int(np.interp(cy - y, [0, y_mag], [255 * 0.45, 255 * 0.9])) - rl.draw_circle(x, y, 10, rl.Color(255, 255, 255, alpha)) + alpha = int(np.interp(cy - y, [0, y_mag], [255 * 0.45, 255 * 0.9]) * self._opacity_filter.x) + rl.draw_circle(x, y, 5, rl.Color(255, 255, 255, alpha)) class WifiIcon(Widget): - def __init__(self): + def __init__(self, network: Network): super().__init__() - self.set_rect(rl.Rectangle(0, 0, 86, 64)) + self.set_rect(rl.Rectangle(0, 0, 48 + 5, 36 + 5)) - self._wifi_slash_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_slash.png", 86, 64) - self._wifi_low_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_low.png", 86, 64) - self._wifi_medium_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_medium.png", 86, 64) - self._wifi_full_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_full.png", 86, 64) - self._lock_txt = gui_app.texture("icons_mici/settings/network/new/lock.png", 22, 32) + self._wifi_slash_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_slash.png", 48, 42) + self._wifi_low_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_low.png", 48, 36) + self._wifi_medium_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_medium.png", 48, 36) + self._wifi_full_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_full.png", 48, 36) + self._lock_txt = gui_app.texture("icons_mici/settings/network/new/lock.png", 21, 27) - self._network: Network | None = None + self._network: Network = network self._network_missing = False # if network disappeared from scan results - self._scale = 1.0 - self._opacity = 1.0 - def set_current_network(self, network: Network): + def update_network(self, network: Network): self._network = network def set_network_missing(self, missing: bool): self._network_missing = missing - def set_scale(self, scale: float): - self._scale = scale - - def set_opacity(self, opacity: float): - self._opacity = opacity - @staticmethod def get_strength_icon_idx(strength: int) -> int: return round(strength / 100 * 2) def _render(self, _): - if self._network is None: - return - # Determine which wifi strength icon to use strength = self.get_strength_icon_idx(self._network.strength) if self._network_missing: @@ -75,126 +86,186 @@ class WifiIcon(Widget): else: strength_icon = self._wifi_low_txt - tint = rl.Color(255, 255, 255, int(255 * self._opacity)) - icon_x = int(self._rect.x + (self._rect.width - strength_icon.width * self._scale) // 2) - icon_y = int(self._rect.y + (self._rect.height - strength_icon.height * self._scale) // 2) - rl.draw_texture_ex(strength_icon, (icon_x, icon_y), 0.0, self._scale, tint) + rl.draw_texture_ex(strength_icon, (self._rect.x, self._rect.y + self._rect.height - strength_icon.height), 0.0, 1.0, rl.WHITE) # Render lock icon at lower right of wifi icon if secured if self._network.security_type not in (SecurityType.OPEN, SecurityType.UNSUPPORTED): - lock_scale = self._scale * 1.1 - lock_x = int(icon_x + 1 + strength_icon.width * self._scale - self._lock_txt.width * lock_scale / 2) - lock_y = int(icon_y + 1 + strength_icon.height * self._scale - self._lock_txt.height * lock_scale / 2) - rl.draw_texture_ex(self._lock_txt, (lock_x, lock_y), 0.0, lock_scale, tint) + lock_x = self._rect.x + self._rect.width - self._lock_txt.width + lock_y = self._rect.y + self._rect.height - self._lock_txt.height + 6 + rl.draw_texture_ex(self._lock_txt, (lock_x, lock_y), 0.0, 1.0, rl.WHITE) -class WifiItem(BigDialogOptionButton): - LEFT_MARGIN = 20 +class WifiButton(BigButton): + LABEL_PADDING = 98 + LABEL_WIDTH = 402 - 98 - 28 # button width - left padding - right padding + SUB_LABEL_WIDTH = 402 - LABEL_HORIZONTAL_PADDING * 2 - def __init__(self, network: Network, wifi_state_callback: Callable[[], WifiState]): - super().__init__(network.ssid) - - self.set_rect(rl.Rectangle(0, 0, gui_app.width, self.HEIGHT)) - - self._selected_txt = gui_app.texture("icons_mici/settings/network/new/wifi_selected.png", 48, 96) + def __init__(self, network: Network, wifi_manager: WifiManager): + super().__init__(normalize_ssid(network.ssid), scroll=True) self._network = network - self._wifi_state_callback = wifi_state_callback - self._wifi_icon = WifiIcon() - self._wifi_icon.set_current_network(network) + self._wifi_manager = wifi_manager + + self._wifi_icon = WifiIcon(network) + self._forget_btn = ForgetButton(self._forget_network) + self._check_txt = gui_app.texture("icons_mici/setup/driver_monitoring/dm_check.png", 32, 32) + + # Eager state (not sourced from Network) + self._network_missing = False + self._network_forgetting = False + self._wrong_password = False + self._shake_start: float | None = None + + def update_network(self, network: Network): + self._network = network + self._wifi_icon.update_network(network) + + # We can assume network is not missing if got new Network + self._network_missing = False + self._wifi_icon.set_network_missing(False) + if self._is_connected or self._is_connecting: + self._wrong_password = False + + def _forget_network(self): + if self._network_forgetting: + return + + self._network_forgetting = True + self._forget_btn.set_visible(False) + self._wifi_manager.forget_connection(self._network.ssid) + + def on_forgotten(self): + self._network_forgetting = False + self._forget_btn.set_visible(True) def set_network_missing(self, missing: bool): + self._network_missing = missing self._wifi_icon.set_network_missing(missing) - def set_current_network(self, network: Network): - self._network = network - self._wifi_icon.set_current_network(network) - - # reset if we see the network again - self.set_enabled(True) - self.set_network_missing(False) - - def _render(self, _): - disabled_alpha = 0.35 if not self.enabled else 1.0 - - # connecting or connected - if self._wifi_state_callback().ssid == self._network.ssid: - selected_x = int(self._rect.x - self._selected_txt.width / 2) - selected_y = int(self._rect.y + (self._rect.height - self._selected_txt.height) / 2) - rl.draw_texture(self._selected_txt, selected_x, selected_y, rl.WHITE) - - self._wifi_icon.set_opacity(disabled_alpha) - self._wifi_icon.set_scale((1.0 if self._selected else 0.65) * 0.7) - self._wifi_icon.render(rl.Rectangle( - self._rect.x + self.LEFT_MARGIN, - self._rect.y, - self.SELECTED_HEIGHT, - self._rect.height - )) - - if self._selected: - self._label.set_font_size(self.SELECTED_HEIGHT) - self._label.set_color(rl.Color(255, 255, 255, int(255 * 0.9 * disabled_alpha))) - self._label.set_font_weight(FontWeight.DISPLAY) - else: - self._label.set_font_size(self.HEIGHT) - self._label.set_color(rl.Color(255, 255, 255, int(255 * 0.58 * disabled_alpha))) - self._label.set_font_weight(FontWeight.DISPLAY_REGULAR) - - label_offset = self.LEFT_MARGIN + self._wifi_icon.rect.width + 20 - label_rect = rl.Rectangle(self._rect.x + label_offset, self._rect.y, self._rect.width - label_offset, self._rect.height) - self._label.set_text(normalize_ssid(self._network.ssid)) - self._label.render(label_rect) - - -class ConnectButton(Widget): - def __init__(self): - super().__init__() - self._bg_txt = gui_app.texture("icons_mici/settings/network/new/connect_button.png", 410, 100) - self._bg_pressed_txt = gui_app.texture("icons_mici/settings/network/new/connect_button_pressed.png", 410, 100) - self._bg_full_txt = gui_app.texture("icons_mici/settings/network/new/full_connect_button.png", 520, 100) - self._bg_full_pressed_txt = gui_app.texture("icons_mici/settings/network/new/full_connect_button_pressed.png", 520, 100) - - self._full: bool = False - - self._label = UnifiedLabel("", 36, FontWeight.MEDIUM, rl.Color(255, 255, 255, int(255 * 0.9)), - alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER, - alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE) + def set_wrong_password(self): + self._wrong_password = True + self._shake_start = rl.get_time() @property - def full(self) -> bool: - return self._full + def network(self) -> Network: + return self._network - def set_full(self, full: bool): - self._full = full - self.set_rect(rl.Rectangle(0, 0, 520 if self._full else 410, 100)) + @property + def _show_forget_btn(self): + return (self._is_saved and not self._wrong_password) or self._is_connecting - def set_label(self, text: str): - self._label.set_text(text) + def _handle_mouse_release(self, mouse_pos: MousePos): + if self._show_forget_btn and rl.check_collision_point_rec(mouse_pos, self._forget_btn.rect): + return + super()._handle_mouse_release(mouse_pos) - def _render(self, _): - if self._full: - bg_txt = self._bg_full_pressed_txt if self.is_pressed and self.enabled else self._bg_full_txt - else: - bg_txt = self._bg_pressed_txt if self.is_pressed and self.enabled else self._bg_txt + def _get_label_font_size(self): + return 48 - rl.draw_texture(bg_txt, int(self._rect.x), int(self._rect.y), rl.WHITE) + @property + def _shake_offset(self) -> float: + SHAKE_DURATION = 0.5 + SHAKE_AMPLITUDE = 24.0 + SHAKE_FREQUENCY = 32.0 + t = rl.get_time() - (self._shake_start or 0.0) + if t > SHAKE_DURATION: + return 0.0 + decay = 1.0 - t / SHAKE_DURATION + return decay * SHAKE_AMPLITUDE * math.sin(t * SHAKE_FREQUENCY) - self._label.set_text_color(rl.Color(255, 255, 255, int(255 * 0.9) if self.enabled else int(255 * 0.9 * 0.65))) - self._label.render(self._rect) + def set_position(self, x: float, y: float) -> None: + super().set_position(x + self._shake_offset, y) + + def _draw_content(self, btn_y: float): + self._label.set_color(LABEL_COLOR) + label_rect = rl.Rectangle(self._rect.x + self.LABEL_PADDING, btn_y + LABEL_VERTICAL_PADDING, + self.LABEL_WIDTH, self._rect.height - LABEL_VERTICAL_PADDING * 2) + self._label.render(label_rect) + + if self.value: + sub_label_x = self._rect.x + LABEL_HORIZONTAL_PADDING + label_y = btn_y + self._rect.height - LABEL_VERTICAL_PADDING + sub_label_w = self.SUB_LABEL_WIDTH - (self._forget_btn.rect.width if self._show_forget_btn else 0) + sub_label_height = self._sub_label.get_content_height(sub_label_w) + + if self._is_connected and not self._network_forgetting: + check_y = int(label_y - sub_label_height + (sub_label_height - self._check_txt.height) / 2) + rl.draw_texture(self._check_txt, int(sub_label_x), check_y, rl.Color(255, 255, 255, int(255 * 0.9 * 0.65))) + sub_label_x += self._check_txt.width + 14 + + sub_label_rect = rl.Rectangle(sub_label_x, label_y - sub_label_height, sub_label_w, sub_label_height) + self._sub_label.render(sub_label_rect) + + # Wifi icon + self._wifi_icon.render(rl.Rectangle( + self._rect.x + 30, + btn_y + 30, + self._wifi_icon.rect.width, + self._wifi_icon.rect.height, + )) + + # Forget button + if self._show_forget_btn: + self._forget_btn.render(rl.Rectangle( + self._rect.x + self._rect.width - self._forget_btn.rect.width, + btn_y + self._rect.height - self._forget_btn.rect.height, + self._forget_btn.rect.width, + self._forget_btn.rect.height, + )) + + def set_touch_valid_callback(self, touch_callback: Callable[[], bool]) -> None: + super().set_touch_valid_callback(lambda: touch_callback() and not self._forget_btn.is_pressed) + self._forget_btn.set_touch_valid_callback(touch_callback) + + @property + def _is_saved(self): + return self._wifi_manager.is_connection_saved(self._network.ssid) + + @property + def _is_connecting(self): + return self._wifi_manager.connecting_to_ssid == self._network.ssid + + @property + def _is_connected(self): + return self._wifi_manager.connected_ssid == self._network.ssid + + def _update_state(self): + if any((self._network_missing, self._is_connecting, self._is_connected, self._network_forgetting, + self._network.security_type == SecurityType.UNSUPPORTED)): + self.set_enabled(False) + self._sub_label.set_color(rl.Color(255, 255, 255, int(255 * 0.585))) + self._sub_label.set_font_weight(FontWeight.ROMAN) + + if self._network_forgetting: + self.set_value("forgetting...") + elif self._is_connecting: + self.set_value("connecting...") + elif self._is_connected: + self.set_value("connected") + elif self._network_missing: + # after connecting/connected since NM will still attempt to connect/stay connected for a while + self.set_value("not in range") + else: + self.set_value("unsupported") + + else: # saved, wrong password, or unknown + self.set_value("wrong password" if self._wrong_password else "connect") + self.set_enabled(True) + self._sub_label.set_color(rl.Color(255, 255, 255, int(255 * 0.9))) + self._sub_label.set_font_weight(FontWeight.SEMI_BOLD) class ForgetButton(Widget): - HORIZONTAL_MARGIN = 8 + MARGIN = 12 # bottom and right def __init__(self, forget_network: Callable): super().__init__() self._forget_network = forget_network - self._bg_txt = gui_app.texture("icons_mici/settings/network/new/forget_button.png", 100, 100) - self._bg_pressed_txt = gui_app.texture("icons_mici/settings/network/new/forget_button_pressed.png", 100, 100) - self._trash_txt = gui_app.texture("icons_mici/settings/network/new/trash.png", 35, 42) - self.set_rect(rl.Rectangle(0, 0, 100 + self.HORIZONTAL_MARGIN * 2, 100)) + self._bg_txt = gui_app.texture("icons_mici/settings/network/new/forget_button.png", 84, 84) + self._bg_pressed_txt = gui_app.texture("icons_mici/settings/network/new/forget_button_pressed.png", 84, 84) + self._trash_txt = gui_app.texture("icons_mici/settings/network/new/trash.png", 29, 35) + self.set_rect(rl.Rectangle(0, 0, 84 + self.MARGIN * 2, 84 + self.MARGIN * 2)) def _handle_mouse_release(self, mouse_pos: MousePos): super()._handle_mouse_release(mouse_pos) @@ -204,150 +275,22 @@ class ForgetButton(Widget): def _render(self, _): bg_txt = self._bg_pressed_txt if self.is_pressed else self._bg_txt - rl.draw_texture(bg_txt, int(self._rect.x + self.HORIZONTAL_MARGIN), int(self._rect.y), rl.WHITE) + rl.draw_texture_ex(bg_txt, (self._rect.x + (self._rect.width - self._bg_txt.width) / 2, + self._rect.y + (self._rect.height - self._bg_txt.height) / 2), 0, 1.0, rl.WHITE) - trash_x = int(self._rect.x + (self._rect.width - self._trash_txt.width) // 2) - trash_y = int(self._rect.y + (self._rect.height - self._trash_txt.height) // 2) - rl.draw_texture(self._trash_txt, trash_x, trash_y, rl.WHITE) + trash_x = self._rect.x + (self._rect.width - self._trash_txt.width) / 2 + trash_y = self._rect.y + (self._rect.height - self._trash_txt.height) / 2 + rl.draw_texture_ex(self._trash_txt, (trash_x, trash_y), 0, 1.0, rl.WHITE) -class NetworkInfoPage(NavWidget): - def __init__(self, wifi_manager, connect_callback: Callable, forget_callback: Callable, - connecting_callback: Callable[[], str | None], connected_callback: Callable[[], str | None]): - super().__init__() - self._wifi_manager = wifi_manager - - self.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) - - self._wifi_icon = WifiIcon() - self._forget_btn = ForgetButton(lambda: forget_callback(self._network.ssid) if self._network is not None else None) - self._forget_btn.set_enabled(lambda: self.enabled) # for stack - self._connect_btn = ConnectButton() - self._connect_btn.set_click_callback(lambda: connect_callback(self._network.ssid) if self._network is not None else None) - - self._title = UnifiedLabel("", 64, FontWeight.DISPLAY, rl.Color(255, 255, 255, int(255 * 0.9)), - alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE, scroll=True) - self._subtitle = UnifiedLabel("", 36, FontWeight.ROMAN, rl.Color(255, 255, 255, int(255 * 0.9 * 0.65)), - alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE) - - self.set_back_callback(gui_app.pop_widget) - - # State - self._network: Network | None = None - self._connecting_callback = connecting_callback - self._connected_callback = connected_callback - - def show_event(self): - super().show_event() - self._title.reset_scroll() - - def update_networks(self, networks: dict[str, Network]): - # update current network from latest scan results - for ssid, network in networks.items(): - if self._network is not None and ssid == self._network.ssid: - self.set_current_network(network) - break - else: - # network disappeared, close page - # TODO: pop_widgets_to, to close potentially open keyboard too - if gui_app.get_active_widget() == self: - gui_app.pop_widget() - - def _update_state(self): - super()._update_state() - # TODO: remove? only left for potential compatibility with setup/updater - self._wifi_manager.process_callbacks() - - if self._network is None: - return - - self._connect_btn.set_full(not self._wifi_manager.is_connection_saved(self._network.ssid) and not self._is_connecting) - if self._is_connecting: - self._connect_btn.set_label("connecting...") - self._connect_btn.set_enabled(False) - elif self._is_connected: - self._connect_btn.set_label("connected") - self._connect_btn.set_enabled(False) - elif self._network.security_type == SecurityType.UNSUPPORTED: - self._connect_btn.set_label("connect") - self._connect_btn.set_enabled(False) - else: # saved or unknown - self._connect_btn.set_label("connect") - self._connect_btn.set_enabled(self.enabled) - - self._title.set_text(normalize_ssid(self._network.ssid)) - if self._network.security_type == SecurityType.OPEN: - self._subtitle.set_text("open") - elif self._network.security_type == SecurityType.UNSUPPORTED: - self._subtitle.set_text("unsupported") - else: - self._subtitle.set_text("secured") - - def set_current_network(self, network: Network): - self._network = network - self._wifi_icon.set_current_network(network) - - @property - def _is_connecting(self): - if self._network is None: - return False - is_connecting = self._connecting_callback() == self._network.ssid - return is_connecting - - @property - def _is_connected(self): - if self._network is None: - return False - is_connected = self._connected_callback() == self._network.ssid - return is_connected - - def _render(self, _): - self._wifi_icon.render(rl.Rectangle( - self._rect.x + 32, - self._rect.y + (self._rect.height - self._connect_btn.rect.height - self._wifi_icon.rect.height) / 2, - self._wifi_icon.rect.width, - self._wifi_icon.rect.height, - )) - - self._title.render(rl.Rectangle( - self._rect.x + self._wifi_icon.rect.width + 32 + 32, - self._rect.y + 32 - 16, - self._rect.width - (self._wifi_icon.rect.width + 32 + 32), - 64, - )) - - self._subtitle.render(rl.Rectangle( - self._rect.x + self._wifi_icon.rect.width + 32 + 32, - self._rect.y + 32 + 64 - 16, - self._rect.width - (self._wifi_icon.rect.width + 32 + 32), - 48, - )) - - self._connect_btn.render(rl.Rectangle( - self._rect.x + 8, - self._rect.y + self._rect.height - self._connect_btn.rect.height, - self._connect_btn.rect.width, - self._connect_btn.rect.height, - )) - - if not self._connect_btn.full: - self._forget_btn.render(rl.Rectangle( - self._rect.x + self._rect.width - self._forget_btn.rect.width, - self._rect.y + self._rect.height - self._forget_btn.rect.height, - self._forget_btn.rect.width, - self._forget_btn.rect.height, - )) - - -class WifiUIMici(BigMultiOptionDialog): +class WifiUIMici(NavWidget): def __init__(self, wifi_manager: WifiManager): - super().__init__([], None) + super().__init__() # Set up back navigation self.set_back_callback(gui_app.pop_widget) - self._network_info_page = NetworkInfoPage(wifi_manager, self._connect_to_network, wifi_manager.forget_connection, - lambda: wifi_manager.connecting_to_ssid, lambda: wifi_manager.connected_ssid) + self._scroller = Scroller([]) self._loading_animation = LoadingAnimation() @@ -356,12 +299,15 @@ class WifiUIMici(BigMultiOptionDialog): self._wifi_manager.add_callbacks( need_auth=self._on_need_auth, + forgotten=self._on_forgotten, networks_updated=self._on_network_updated, ) def show_event(self): # Clear scroller items and update from latest scan results super().show_event() + self._scroller.show_event() + self._loading_animation.show_event() self._wifi_manager.set_active(True) self._scroller.items.clear() self._update_buttons() @@ -373,43 +319,38 @@ class WifiUIMici(BigMultiOptionDialog): def _on_network_updated(self, networks: list[Network]): self._networks = {network.ssid: network for network in networks} self._update_buttons() - self._network_info_page.update_networks(self._networks) def _update_buttons(self): - # Only add new buttons to the end. Update existing buttons without re-sorting so user can freely scroll around + # Update existing buttons, add new ones to the end + existing = {btn.network.ssid: btn for btn in self._scroller.items if isinstance(btn, WifiButton)} for network in self._networks.values(): - network_button_idx = next((i for i, btn in enumerate(self._scroller.items) if btn.option == network.ssid), None) - if network_button_idx is not None: - # Update network on existing button - self._scroller.items[network_button_idx].set_current_network(network) + if network.ssid in existing: + existing[network.ssid].update_network(network) else: - network_button = WifiItem(network, lambda: self._wifi_manager.wifi_state) - self._scroller.add_widget(network_button) + btn = WifiButton(network, self._wifi_manager) + btn.set_click_callback(lambda ssid=network.ssid: self._connect_to_network(ssid)) + self._scroller.add_widget(btn) - # Move connecting/connected network to the start - connected_btn_idx = next((i for i, btn in enumerate(self._scroller.items) if self._wifi_manager.wifi_state.ssid == btn._network.ssid), None) - if connected_btn_idx is not None and connected_btn_idx > 0: - self._scroller.items.insert(0, self._scroller.items.pop(connected_btn_idx)) - self._scroller._layout() # fixes selected style single frame stutter - - # Disable networks no longer present + # Mark networks no longer in scan results (display handled by _update_state) for btn in self._scroller.items: - if btn.option not in self._networks: - btn.set_enabled(False) + if isinstance(btn, WifiButton) and btn.network.ssid not in self._networks: btn.set_network_missing(True) + # Move connecting/connected network to the front with animation + front_ssid = self._wifi_manager.wifi_state.ssid + front_btn_idx = next((i for i, btn in enumerate(self._scroller.items) + if isinstance(btn, WifiButton) and + btn.network.ssid == front_ssid), None) if front_ssid else None + + if front_btn_idx is not None and front_btn_idx > 0: + self._scroller.move_item(front_btn_idx, 0) + def _connect_with_password(self, ssid: str, password: str): self._wifi_manager.connect_to_network(ssid, password) + self._scroller.scroll_to(self._scroller.scroll_panel.get_offset(), smooth=True) self._update_buttons() - def _on_option_selected(self, option: str): - super()._on_option_selected(option) - - if option in self._networks: - self._network_info_page.set_current_network(self._networks[option]) - gui_app.push_widget(self._network_info_page) - def _connect_to_network(self, ssid: str): network = self._networks.get(ssid) if network is None: @@ -418,21 +359,46 @@ class WifiUIMici(BigMultiOptionDialog): if self._wifi_manager.is_connection_saved(network.ssid): self._wifi_manager.activate_connection(network.ssid) - self._update_buttons() elif network.security_type == SecurityType.OPEN: self._wifi_manager.connect_to_network(network.ssid, "") - self._update_buttons() else: self._on_need_auth(network.ssid, False) + return + + self._scroller.scroll_to(self._scroller.scroll_panel.get_offset(), smooth=True) + self._update_buttons() def _on_need_auth(self, ssid, incorrect_password=True): - hint = "wrong password..." if incorrect_password else "enter password..." - dlg = BigInputDialog(hint, "", minimum_length=8, + if incorrect_password: + for btn in self._scroller.items: + if isinstance(btn, WifiButton) and btn.network.ssid == ssid: + btn.set_wrong_password() + break + return + + dlg = BigInputDialog("enter password...", "", minimum_length=8, confirm_callback=lambda _password: self._connect_with_password(ssid, _password)) gui_app.push_widget(dlg) - def _render(self, _): - super()._render(_) + def _on_forgotten(self, ssid): + # For eager UI forget + for btn in self._scroller.items: + if isinstance(btn, WifiButton) and btn.network.ssid == ssid: + btn.on_forgotten() - if not self._networks: - self._loading_animation.render(self._rect) + def _update_state(self): + super()._update_state() + + # Show loading animation near end + max_scroll = max(self._scroller.content_size - self._scroller.rect.width, 1) + progress = -self._scroller.scroll_panel.get_offset() / max_scroll + if progress > 0.8 or len(self._scroller.items) <= 1: + self._loading_animation.show_event() + + def _render(self, _): + self._scroller.render(self._rect) + + anim_w = 90 + anim_x = self._rect.x + self._rect.width - anim_w + anim_y = self._rect.y + self._rect.height - 25 + 2 + self._loading_animation.render(rl.Rectangle(anim_x, anim_y, anim_w, 20)) diff --git a/system/ui/lib/networkmanager.py b/system/ui/lib/networkmanager.py index e04e3eeadc..c47928d8ba 100644 --- a/system/ui/lib/networkmanager.py +++ b/system/ui/lib/networkmanager.py @@ -26,6 +26,7 @@ class NMDeviceStateReason(IntEnum): NO_SECRETS = 7 SUPPLICANT_DISCONNECT = 8 CONNECTION_REMOVED = 38 + SSID_NOT_FOUND = 53 NEW_ACTIVATION = 60 diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index f95ac7d886..4ca0a382d4 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -378,6 +378,9 @@ class WifiManager: while len(state_q): new_state, previous_state, change_reason = state_q.popleft().body + # TODO: Handle (FAILED, SSID_NOT_FOUND) and emit for ui to show error + # Happens when network drops off after starting connection + if new_state == NMDeviceState.DISCONNECTED: if change_reason != NMDeviceStateReason.NEW_ACTIVATION: # catches CONNECTION_REMOVED reason when connection is forgotten @@ -414,8 +417,6 @@ class WifiManager: elif new_state == NMDeviceState.ACTIVATED: # Note that IP address from Ip4Config may not be propagated immediately and could take until the next scan results - self._update_networks() - wifi_state = replace(self._wifi_state, prev_ssid=None, status=ConnectStatus.CONNECTED) conn_path, _ = self._get_active_wifi_connection(self._conn_monitor) @@ -423,10 +424,12 @@ class WifiManager: cloudlog.warning("Failed to get active wifi connection during ACTIVATED state") self._wifi_state = wifi_state self._enqueue_callbacks(self._activated) + self._update_networks() else: wifi_state.ssid = next((s for s, p in self._connections.items() if p == conn_path), None) self._wifi_state = wifi_state self._enqueue_callbacks(self._activated) + self._update_networks() # Persist volatile connections (created by AddAndActivateConnection2) to disk conn_addr = DBusAddress(conn_path, bus_name=NM, interface=NM_CONNECTION_IFACE) diff --git a/system/ui/widgets/scroller.py b/system/ui/widgets/scroller.py index 9393dd07ea..0543b1395e 100644 --- a/system/ui/widgets/scroller.py +++ b/system/ui/widgets/scroller.py @@ -15,6 +15,7 @@ ANIMATION_SCALE = 0.6 MOVE_LIFT = 20 MOVE_OVERLAY_ALPHA = 0.65 +SCROLL_RC = 0.15 EDGE_SHADOW_WIDTH = 20 @@ -78,7 +79,7 @@ class Scroller(Widget): self._reset_scroll_at_show = True self._scrolling_to: float | None = None - self._scroll_filter = FirstOrderFilter(0.0, 0.1, 1 / gui_app.target_fps) + self._scroll_filter = FirstOrderFilter(0.0, SCROLL_RC, 1 / gui_app.target_fps) self._zoom_filter = FirstOrderFilter(1.0, 0.2, 1 / gui_app.target_fps) self._zoom_out_t: float = 0.0 @@ -134,6 +135,10 @@ class Scroller(Widget): def items(self) -> list[Widget]: return self._items + @property + def content_size(self) -> float: + return self._content_size + def add_widget(self, item: Widget) -> None: self._items.append(item) item.set_touch_valid_callback(lambda: self.scroll_panel.is_touch_valid() and self.enabled and self._scrolling_to is None @@ -159,7 +164,7 @@ class Scroller(Widget): if self._scrolling_to is not None and (self.scroll_panel.state == ScrollState.PRESSED or self.scroll_panel.state == ScrollState.MANUAL_SCROLL): self._scrolling_to = None - if self._scrolling_to is not None: + if self._scrolling_to is not None and len(self._pending_lift) == 0: self._scroll_filter.update(self._scrolling_to) self.scroll_panel.set_offset(self._scroll_filter.x) @@ -230,14 +235,17 @@ class Scroller(Widget): # store original position in content space of all affected widgets to animate from for idx in range(min(from_idx, to_idx), max(from_idx, to_idx) + 1): affected_item = self._items[idx] - self._move_animations[affected_item] = FirstOrderFilter(affected_item.rect.x - self._scroll_offset, 0.15, 1 / gui_app.target_fps) + self._move_animations[affected_item] = FirstOrderFilter(affected_item.rect.x - self._scroll_offset, SCROLL_RC, 1 / gui_app.target_fps) self._pending_move.add(affected_item) # lift only src widget to make it more clear which one is moving - self._move_lift[item] = FirstOrderFilter(0.0, 0.15, 1 / gui_app.target_fps) + self._move_lift[item] = FirstOrderFilter(0.0, SCROLL_RC, 1 / gui_app.target_fps) self._pending_lift.add(item) def _do_move_animation(self, item: Widget, target_x: float, target_y: float) -> tuple[float, float]: + # wait a frame before moving so we match potential pending scroll animation + can_start_move = len(self._pending_lift) == 0 + if item in self._move_lift: lift_filter = self._move_lift[item] @@ -260,7 +268,7 @@ class Scroller(Widget): # compare/update in content space to match filter content_x = target_x - self._scroll_offset - if len(self._pending_lift) == 0: + if can_start_move: move_filter.update(content_x) # drop when close to target From ddf8abc14a272d5e95ffd2569d1ddfd2195c56f5 Mon Sep 17 00:00:00 2001 From: Trey Moen <50057480+greatgitsby@users.noreply.github.com> Date: Sun, 22 Feb 2026 10:34:37 -0700 Subject: [PATCH 147/311] Revert "feat(lpa): `at` client + list profiles (#37271)" (#37322) This reverts commit 8bca2ca7588bae62a860b1e90a835e1678367596. --- system/hardware/tici/lpa.py | 229 +----------------------------------- 1 file changed, 2 insertions(+), 227 deletions(-) diff --git a/system/hardware/tici/lpa.py b/system/hardware/tici/lpa.py index 4d649fda8b..9bd9d8c7b0 100644 --- a/system/hardware/tici/lpa.py +++ b/system/hardware/tici/lpa.py @@ -1,237 +1,12 @@ -# SGP.22 v2.3: https://www.gsma.com/solutions-and-impact/technologies/esim/wp-content/uploads/2021/07/SGP.22-v2.3.pdf - -import atexit -import base64 -import os -import serial -import sys - -from collections.abc import Generator - from openpilot.system.hardware.base import LPABase, Profile -DEFAULT_DEVICE = "/dev/ttyUSB2" -DEFAULT_BAUD = 9600 -DEFAULT_TIMEOUT = 5.0 -# https://euicc-manual.osmocom.org/docs/lpa/applet-id/ -ISDR_AID = "A0000005591010FFFFFFFF8900000100" -ES10X_MSS = 120 -DEBUG = os.environ.get("DEBUG") == "1" - -# TLV Tags -TAG_ICCID = 0x5A -TAG_PROFILE_INFO_LIST = 0xBF2D - -STATE_LABELS = {0: "disabled", 1: "enabled", 255: "unknown"} -ICON_LABELS = {0: "jpeg", 1: "png", 255: "unknown"} -CLASS_LABELS = {0: "test", 1: "provisioning", 2: "operational", 255: "unknown"} - - -def b64e(data: bytes) -> str: - return base64.b64encode(data).decode("ascii") - - -class AtClient: - def __init__(self, device: str, baud: int, timeout: float, debug: bool) -> None: - self.serial = serial.Serial(device, baudrate=baud, timeout=timeout) - self.debug = debug - self.channel: str | None = None - self.serial.reset_input_buffer() - - def close(self) -> None: - try: - if self.channel: - self.query(f"AT+CCHC={self.channel}") - self.channel = None - finally: - self.serial.close() - - def send(self, cmd: str) -> None: - if self.debug: - print(f">> {cmd}", file=sys.stderr) - self.serial.write((cmd + "\r").encode("ascii")) - - def expect(self) -> list[str]: - lines: list[str] = [] - while True: - raw = self.serial.readline() - if not raw: - raise TimeoutError("AT command timed out") - line = raw.decode(errors="ignore").strip() - if not line: - continue - if self.debug: - print(f"<< {line}", file=sys.stderr) - if line == "OK": - return lines - if line == "ERROR" or line.startswith("+CME ERROR"): - raise RuntimeError(f"AT command failed: {line}") - lines.append(line) - - def query(self, cmd: str) -> list[str]: - self.send(cmd) - return self.expect() - - def open_isdr(self) -> None: - # close any stale logical channel from a previous crashed session - try: - self.query("AT+CCHC=1") - except RuntimeError: - pass - for line in self.query(f'AT+CCHO="{ISDR_AID}"'): - if line.startswith("+CCHO:") and (ch := line.split(":", 1)[1].strip()): - self.channel = ch - return - raise RuntimeError("Failed to open ISD-R application") - - def send_apdu(self, apdu: bytes) -> tuple[bytes, int, int]: - if not self.channel: - raise RuntimeError("Logical channel is not open") - hex_payload = apdu.hex().upper() - for line in self.query(f'AT+CGLA={self.channel},{len(hex_payload)},"{hex_payload}"'): - if line.startswith("+CGLA:"): - parts = line.split(":", 1)[1].split(",", 1) - if len(parts) == 2: - data = bytes.fromhex(parts[1].strip().strip('"')) - if len(data) >= 2: - return data[:-2], data[-2], data[-1] - raise RuntimeError("Missing +CGLA response") - - -# --- TLV utilities --- - -def iter_tlv(data: bytes, with_positions: bool = False) -> Generator: - idx, length = 0, len(data) - while idx < length: - start_pos = idx - tag = data[idx] - idx += 1 - if tag & 0x1F == 0x1F: # Multi-byte tag - tag_value = tag - while idx < length: - next_byte = data[idx] - idx += 1 - tag_value = (tag_value << 8) | next_byte - if not (next_byte & 0x80): - break - else: - tag_value = tag - if idx >= length: - break - size = data[idx] - idx += 1 - if size & 0x80: # Multi-byte length - num_bytes = size & 0x7F - if idx + num_bytes > length: - break - size = int.from_bytes(data[idx : idx + num_bytes], "big") - idx += num_bytes - if idx + size > length: - break - value = data[idx : idx + size] - idx += size - yield (tag_value, value, start_pos, idx) if with_positions else (tag_value, value) - - -def find_tag(data: bytes, target: int) -> bytes | None: - return next((v for t, v in iter_tlv(data) if t == target), None) - - -def tbcd_to_string(raw: bytes) -> str: - return "".join(str(n) for b in raw for n in (b & 0x0F, b >> 4) if n <= 9) - - -# Profile field decoders: TLV tag -> (field_name, decoder) -_PROFILE_FIELDS = { - TAG_ICCID: ("iccid", tbcd_to_string), - 0x4F: ("isdpAid", lambda v: v.hex().upper()), - 0x9F70: ("profileState", lambda v: STATE_LABELS.get(v[0], "unknown")), - 0x90: ("profileNickname", lambda v: v.decode("utf-8", errors="ignore") or None), - 0x91: ("serviceProviderName", lambda v: v.decode("utf-8", errors="ignore") or None), - 0x92: ("profileName", lambda v: v.decode("utf-8", errors="ignore") or None), - 0x93: ("iconType", lambda v: ICON_LABELS.get(v[0], "unknown")), - 0x94: ("icon", b64e), - 0x95: ("profileClass", lambda v: CLASS_LABELS.get(v[0], "unknown")), -} - - -def _decode_profile_fields(data: bytes) -> dict: - """Parse known profile metadata TLV fields into a dict.""" - result = {} - for tag, value in iter_tlv(data): - if (field := _PROFILE_FIELDS.get(tag)): - result[field[0]] = field[1](value) - return result - - -# --- ES10x command transport --- - -def es10x_command(client: AtClient, data: bytes) -> bytes: - response = bytearray() - sequence = 0 - offset = 0 - while offset < len(data): - chunk = data[offset : offset + ES10X_MSS] - offset += len(chunk) - is_last = offset == len(data) - apdu = bytes([0x80, 0xE2, 0x91 if is_last else 0x11, sequence & 0xFF, len(chunk)]) + chunk - segment, sw1, sw2 = client.send_apdu(apdu) - response.extend(segment) - while True: - if sw1 == 0x61: # More data available - segment, sw1, sw2 = client.send_apdu(bytes([0x80, 0xC0, 0x00, 0x00, sw2 or 0])) - response.extend(segment) - continue - if (sw1 & 0xF0) == 0x90: - break - raise RuntimeError(f"APDU failed with SW={sw1:02X}{sw2:02X}") - sequence += 1 - return bytes(response) - - -# --- Profile operations --- - -def decode_profiles(blob: bytes) -> list[dict]: - root = find_tag(blob, TAG_PROFILE_INFO_LIST) - if root is None: - raise RuntimeError("Missing ProfileInfoList") - list_ok = find_tag(root, 0xA0) - if list_ok is None: - return [] - defaults = {name: None for name, _ in _PROFILE_FIELDS.values()} - return [{**defaults, **_decode_profile_fields(value)} for tag, value in iter_tlv(list_ok) if tag == 0xE3] - - -def list_profiles(client: AtClient) -> list[dict]: - return decode_profiles(es10x_command(client, TAG_PROFILE_INFO_LIST.to_bytes(2, "big") + b"\x00")) - - class TiciLPA(LPABase): - _instance = None - - def __new__(cls): - if cls._instance is None: - cls._instance = super().__new__(cls) - return cls._instance - def __init__(self): - if hasattr(self, '_client'): - return - self._client = AtClient(DEFAULT_DEVICE, DEFAULT_BAUD, DEFAULT_TIMEOUT, debug=DEBUG) - self._client.open_isdr() - atexit.register(self._client.close) + pass def list_profiles(self) -> list[Profile]: - return [ - Profile( - iccid=p.get("iccid", ""), - nickname=p.get("profileNickname") or "", - enabled=p.get("profileState") == "enabled", - provider=p.get("serviceProviderName") or "", - ) - for p in list_profiles(self._client) - ] + return [] def get_active_profile(self) -> Profile | None: return None From afa9ec1138b398508ef9062f7a9c42a1d3c50acf Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sun, 22 Feb 2026 16:27:59 -0800 Subject: [PATCH 148/311] bump panda: vendored toolchain (#37324) * bump panda: vendored toolchain * add * bump panda --- Dockerfile.openpilot_base | 4 +--- panda | 2 +- pyproject.toml | 1 + tools/install_ubuntu_dependencies.sh | 1 - tools/mac_setup.sh | 1 - uv.lock | 7 +++++++ 6 files changed, 10 insertions(+), 6 deletions(-) diff --git a/Dockerfile.openpilot_base b/Dockerfile.openpilot_base index 8a60412993..6ea1450bee 100644 --- a/Dockerfile.openpilot_base +++ b/Dockerfile.openpilot_base @@ -14,9 +14,7 @@ ENV LC_ALL=en_US.UTF-8 COPY tools/install_ubuntu_dependencies.sh /tmp/tools/ RUN /tmp/tools/install_ubuntu_dependencies.sh && \ - rm -rf /var/lib/apt/lists/* /tmp/* && \ - cd /usr/lib/gcc/arm-none-eabi/* && \ - rm -rf arm/ thumb/nofp thumb/v6* thumb/v8* thumb/v7+fp thumb/v7-r+fp.sp + rm -rf /var/lib/apt/lists/* /tmp/* ENV NVIDIA_VISIBLE_DEVICES=all ENV NVIDIA_DRIVER_CAPABILITIES=graphics,utility,compute diff --git a/panda b/panda index e1da7dc918..49f72e931f 160000 --- a/panda +++ b/panda @@ -1 +1 @@ -Subproject commit e1da7dc918c0bcda6fbbdd9ee6f89c5428ec5039 +Subproject commit 49f72e931f09ceecb00c0c7937808fcaeecd3c17 diff --git a/pyproject.toml b/pyproject.toml index 5aeb2ffab4..cc56eb3594 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -90,6 +90,7 @@ testing = [ dev = [ "matplotlib", "opencv-python-headless", + "gcc-arm-none-eabi @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=gcc-arm-none-eabi", ] tools = [ diff --git a/tools/install_ubuntu_dependencies.sh b/tools/install_ubuntu_dependencies.sh index aa98102d49..06ab8fda3a 100755 --- a/tools/install_ubuntu_dependencies.sh +++ b/tools/install_ubuntu_dependencies.sh @@ -36,7 +36,6 @@ function install_ubuntu_common_requirements() { # TODO: vendor the rest of these in third_party/ $SUDO apt-get install -y --no-install-recommends \ - gcc-arm-none-eabi \ capnproto \ libcapnp-dev \ ffmpeg \ diff --git a/tools/mac_setup.sh b/tools/mac_setup.sh index 3f13cbe74a..dece439752 100755 --- a/tools/mac_setup.sh +++ b/tools/mac_setup.sh @@ -35,7 +35,6 @@ brew "ffmpeg" brew "libusb" brew "llvm" brew "zeromq" -cask "gcc-arm-embedded" brew "portaudio" EOS diff --git a/uv.lock b/uv.lock index 8b8bfd639b..aa03f6d5e2 100644 --- a/uv.lock +++ b/uv.lock @@ -416,6 +416,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, ] +[[package]] +name = "gcc-arm-none-eabi" +version = "13.2.1" +source = { git = "https://github.com/commaai/dependencies.git?subdirectory=gcc-arm-none-eabi&rev=releases#e769f658aad6ab46e98490bf0800e69b99e22f7a" } + [[package]] name = "ghp-import" version = "2.1.0" @@ -778,6 +783,7 @@ dependencies = [ [package.optional-dependencies] dev = [ + { name = "gcc-arm-none-eabi" }, { name = "matplotlib" }, { name = "opencv-python-headless" }, ] @@ -816,6 +822,7 @@ requires-dist = [ { name = "crcmod-plus" }, { name = "cython" }, { name = "dearpygui", marker = "(platform_machine != 'aarch64' and extra == 'tools') or (sys_platform != 'linux' and extra == 'tools')", specifier = ">=2.1.0" }, + { name = "gcc-arm-none-eabi", marker = "extra == 'dev'", git = "https://github.com/commaai/dependencies.git?subdirectory=gcc-arm-none-eabi&rev=releases" }, { name = "hypothesis", marker = "extra == 'testing'", specifier = "==6.47.*" }, { name = "inputs" }, { name = "jeepney" }, From f881a9ba681de3fddf478bfe781cd0a7c885a12f Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sun, 22 Feb 2026 19:00:29 -0800 Subject: [PATCH 149/311] rm vendor building workflow --- .github/workflows/vendor_third_party.yaml | 51 ----------------------- 1 file changed, 51 deletions(-) delete mode 100644 .github/workflows/vendor_third_party.yaml diff --git a/.github/workflows/vendor_third_party.yaml b/.github/workflows/vendor_third_party.yaml deleted file mode 100644 index df50cfad23..0000000000 --- a/.github/workflows/vendor_third_party.yaml +++ /dev/null @@ -1,51 +0,0 @@ -name: vendor third_party - -on: - workflow_dispatch: - -jobs: - build: - if: github.ref != 'refs/heads/master' - strategy: - matrix: - os: [ubuntu-24.04, macos-latest] - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v6 - with: - submodules: true - - name: Build - run: third_party/build.sh - - name: Package artifacts - run: | - git add -A third_party/ - git diff --cached --name-only -- third_party/ | tar -cf /tmp/third_party_build.tar -T - - - uses: actions/upload-artifact@v4 - with: - name: third-party-${{ runner.os }} - path: /tmp/third_party_build.tar - - commit: - needs: build - runs-on: ubuntu-24.04 - permissions: - contents: write - steps: - - uses: actions/checkout@v6 - - uses: actions/download-artifact@v4 - with: - path: /tmp/artifacts - - name: Commit vendored libraries - run: | - for f in /tmp/artifacts/*/third_party_build.tar; do - tar xf "$f" - done - git add third_party/ - if git diff --cached --quiet; then - echo "No changes to commit" - exit 0 - fi - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git commit -m "third_party: rebuild vendor libraries" - git push From 4bffe422e4304d590566947d353a548a14867b9e Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sun, 22 Feb 2026 19:15:11 -0800 Subject: [PATCH 150/311] vendor capnproto and ffmpeg via dependencies repo (#37327) --- SConstruct | 10 ++++++++++ pyproject.toml | 4 ++++ system/loggerd/SConscript | 4 ++-- tools/cabana/SConscript | 2 +- tools/install_ubuntu_dependencies.sh | 7 ------- tools/mac_setup.sh | 2 -- tools/replay/SConscript | 2 +- uv.lock | 16 +++++++++++++++- 8 files changed, 33 insertions(+), 14 deletions(-) diff --git a/SConstruct b/SConstruct index 3b8aadf914..3d611af0b2 100644 --- a/SConstruct +++ b/SConstruct @@ -38,6 +38,14 @@ assert arch in [ "Darwin", # macOS arm64 (x86 not supported) ] +if arch != "larch64": + import capnproto + import ffmpeg as ffmpeg_pkg + pkgs = [capnproto, ffmpeg_pkg] +else: + # TODO: remove when AGNOS has our new vendor pkgs + pkgs = [] + env = Environment( ENV={ "PATH": os.environ['PATH'], @@ -74,6 +82,7 @@ env = Environment( "#third_party/acados/include/hpipm/include", "#third_party/catch2/include", "#third_party/libyuv/include", + [x.INCLUDE_DIR for x in pkgs], ], LIBPATH=[ "#common", @@ -83,6 +92,7 @@ env = Environment( "#rednose/helpers", f"#third_party/libyuv/{arch}/lib", f"#third_party/acados/{arch}/lib", + [x.LIB_DIR for x in pkgs], ], RPATH=[], CYTHONCFILESUFFIX=".cpp", diff --git a/pyproject.toml b/pyproject.toml index cc56eb3594..93d7b380b1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,10 @@ dependencies = [ "setuptools", "numpy >=2.0", + # vendored native dependencies + "capnproto @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=capnproto", + "ffmpeg @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=ffmpeg", + # body / webrtcd "av", "aiohttp", diff --git a/system/loggerd/SConscript b/system/loggerd/SConscript index cc8ef7c88f..fecf448855 100644 --- a/system/loggerd/SConscript +++ b/system/loggerd/SConscript @@ -1,8 +1,8 @@ Import('env', 'arch', 'messaging', 'common', 'visionipc') libs = [common, messaging, visionipc, - 'avformat', 'avcodec', 'avutil', - 'yuv', 'pthread', 'zstd'] + 'avformat', 'avcodec', 'swresample', 'avutil', 'x264', + 'yuv', 'pthread', 'z', 'm', 'zstd'] src = ['logger.cc', 'zstd_writer.cc', 'video_writer.cc', 'encoder/encoder.cc', 'encoder/v4l_encoder.cc', 'encoder/jpeg_encoder.cc'] if arch != "larch64": diff --git a/tools/cabana/SConscript b/tools/cabana/SConscript index 025797d1e3..8ea59d0766 100644 --- a/tools/cabana/SConscript +++ b/tools/cabana/SConscript @@ -75,7 +75,7 @@ qt_libs = base_libs cabana_env = qt_env.Clone() -cabana_libs = [cereal, messaging, visionipc, replay_lib, 'avutil', 'avcodec', 'avformat', 'bz2', 'zstd', 'curl', 'yuv', 'usb-1.0'] + qt_libs +cabana_libs = [cereal, messaging, visionipc, replay_lib, 'avformat', 'avcodec', 'swresample', 'avutil', 'x264', 'z', 'bz2', 'zstd', 'curl', 'yuv', 'usb-1.0'] + qt_libs opendbc_path = '-DOPENDBC_FILE_PATH=\'"%s"\'' % (cabana_env.Dir("../../opendbc/dbc").abspath) cabana_env['CXXFLAGS'] += [opendbc_path] diff --git a/tools/install_ubuntu_dependencies.sh b/tools/install_ubuntu_dependencies.sh index 06ab8fda3a..e4b8a64559 100755 --- a/tools/install_ubuntu_dependencies.sh +++ b/tools/install_ubuntu_dependencies.sh @@ -34,14 +34,7 @@ function install_ubuntu_common_requirements() { git-lfs \ xvfb - # TODO: vendor the rest of these in third_party/ $SUDO apt-get install -y --no-install-recommends \ - capnproto \ - libcapnp-dev \ - ffmpeg \ - libavformat-dev \ - libavcodec-dev \ - libavutil-dev \ libbz2-dev \ libeigen3-dev \ libgles2-mesa-dev \ diff --git a/tools/mac_setup.sh b/tools/mac_setup.sh index dece439752..e2ede7fbb8 100755 --- a/tools/mac_setup.sh +++ b/tools/mac_setup.sh @@ -28,10 +28,8 @@ fi brew bundle --file=- <<-EOS brew "git-lfs" -brew "capnp" brew "coreutils" brew "eigen" -brew "ffmpeg" brew "libusb" brew "llvm" brew "zeromq" diff --git a/tools/replay/SConscript b/tools/replay/SConscript index 698ab9885d..b39cf6dab1 100644 --- a/tools/replay/SConscript +++ b/tools/replay/SConscript @@ -12,7 +12,7 @@ if arch != "Darwin": replay_lib_src.append("qcom_decoder.cc") replay_lib = replay_env.Library("replay", replay_lib_src, LIBS=base_libs, FRAMEWORKS=base_frameworks) Export('replay_lib') -replay_libs = [replay_lib, 'avutil', 'avcodec', 'avformat', 'bz2', 'zstd', 'curl', 'yuv', 'ncurses'] + base_libs +replay_libs = [replay_lib, 'avformat', 'avcodec', 'swresample', 'avutil', 'x264', 'z', 'bz2', 'zstd', 'curl', 'yuv', 'ncurses'] + base_libs replay_env.Program("replay", ["main.cc"], LIBS=replay_libs, FRAMEWORKS=base_frameworks) if GetOption('extras'): diff --git a/uv.lock b/uv.lock index aa03f6d5e2..9bfbd05dd5 100644 --- a/uv.lock +++ b/uv.lock @@ -119,6 +119,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7b/ff/48fa68888b8d5bae36d915556ff18f9e5fdc6b5ff5ae23dc4904c9713168/av-13.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:5ea0deab0e6a739cb742fba2a3983d8102f7516a3cdf3c46669f3cac0ed1f351", size = 25781343, upload-time = "2024-10-06T04:53:29.577Z" }, ] +[[package]] +name = "capnproto" +version = "1.0.1" +source = { git = "https://github.com/commaai/dependencies.git?subdirectory=capnproto&rev=releases#acf53363042d92d3206b8ce6b9da4bfb75b200c7" } + [[package]] name = "casadi" version = "3.7.2" @@ -374,6 +379,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, ] +[[package]] +name = "ffmpeg" +version = "7.1.0" +source = { git = "https://github.com/commaai/dependencies.git?subdirectory=ffmpeg&rev=releases#acf53363042d92d3206b8ce6b9da4bfb75b200c7" } + [[package]] name = "fonttools" version = "4.61.1" @@ -419,7 +429,7 @@ wheels = [ [[package]] name = "gcc-arm-none-eabi" version = "13.2.1" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=gcc-arm-none-eabi&rev=releases#e769f658aad6ab46e98490bf0800e69b99e22f7a" } +source = { git = "https://github.com/commaai/dependencies.git?subdirectory=gcc-arm-none-eabi&rev=releases#acf53363042d92d3206b8ce6b9da4bfb75b200c7" } [[package]] name = "ghp-import" @@ -749,10 +759,12 @@ dependencies = [ { name = "aiohttp" }, { name = "aiortc" }, { name = "av" }, + { name = "capnproto" }, { name = "casadi" }, { name = "cffi" }, { name = "crcmod-plus" }, { name = "cython" }, + { name = "ffmpeg" }, { name = "inputs" }, { name = "jeepney" }, { name = "json-rpc" }, @@ -815,6 +827,7 @@ requires-dist = [ { name = "aiohttp" }, { name = "aiortc" }, { name = "av" }, + { name = "capnproto", git = "https://github.com/commaai/dependencies.git?subdirectory=capnproto&rev=releases" }, { name = "casadi", specifier = ">=3.6.6" }, { name = "cffi" }, { name = "codespell", marker = "extra == 'testing'" }, @@ -822,6 +835,7 @@ requires-dist = [ { name = "crcmod-plus" }, { name = "cython" }, { name = "dearpygui", marker = "(platform_machine != 'aarch64' and extra == 'tools') or (sys_platform != 'linux' and extra == 'tools')", specifier = ">=2.1.0" }, + { name = "ffmpeg", git = "https://github.com/commaai/dependencies.git?subdirectory=ffmpeg&rev=releases" }, { name = "gcc-arm-none-eabi", marker = "extra == 'dev'", git = "https://github.com/commaai/dependencies.git?subdirectory=gcc-arm-none-eabi&rev=releases" }, { name = "hypothesis", marker = "extra == 'testing'", specifier = "==6.47.*" }, { name = "inputs" }, From fa2050ab1a34fcaeeced00ba723be41b1b3a4b06 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sun, 22 Feb 2026 19:21:56 -0800 Subject: [PATCH 151/311] rm unused dependencies (#37329) ok just libusb --- tools/install_ubuntu_dependencies.sh | 1 - tools/mac_setup.sh | 1 - 2 files changed, 2 deletions(-) diff --git a/tools/install_ubuntu_dependencies.sh b/tools/install_ubuntu_dependencies.sh index e4b8a64559..0c9fada757 100755 --- a/tools/install_ubuntu_dependencies.sh +++ b/tools/install_ubuntu_dependencies.sh @@ -40,7 +40,6 @@ function install_ubuntu_common_requirements() { libgles2-mesa-dev \ libjpeg-dev \ libncurses5-dev \ - libusb-1.0-0-dev \ libzmq3-dev \ libzstd-dev \ portaudio19-dev \ diff --git a/tools/mac_setup.sh b/tools/mac_setup.sh index e2ede7fbb8..4a66beeec9 100755 --- a/tools/mac_setup.sh +++ b/tools/mac_setup.sh @@ -30,7 +30,6 @@ brew bundle --file=- <<-EOS brew "git-lfs" brew "coreutils" brew "eigen" -brew "libusb" brew "llvm" brew "zeromq" brew "portaudio" From f9114931772772d8496df8981b038b198c93b586 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sun, 22 Feb 2026 19:30:24 -0800 Subject: [PATCH 152/311] rm pyaudio (#37331) * rm pyaudio * those too --- pyproject.toml | 1 - system/webrtc/device/audio.py | 109 --------------------- system/webrtc/tests/test_stream_session.py | 17 ---- system/webrtc/webrtcd.py | 21 +--- tools/bodyteleop/web.py | 43 +------- tools/install_ubuntu_dependencies.sh | 1 - tools/mac_setup.sh | 1 - uv.lock | 12 --- 8 files changed, 4 insertions(+), 201 deletions(-) delete mode 100644 system/webrtc/device/audio.py diff --git a/pyproject.toml b/pyproject.toml index 93d7b380b1..508a6230b2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,6 @@ dependencies = [ "av", "aiohttp", "aiortc", - "pyaudio", # panda "libusb1", diff --git a/system/webrtc/device/audio.py b/system/webrtc/device/audio.py deleted file mode 100644 index b1859518a1..0000000000 --- a/system/webrtc/device/audio.py +++ /dev/null @@ -1,109 +0,0 @@ -import asyncio -import io - -import aiortc -import av -import numpy as np -import pyaudio - - -class AudioInputStreamTrack(aiortc.mediastreams.AudioStreamTrack): - PYAUDIO_TO_AV_FORMAT_MAP = { - pyaudio.paUInt8: 'u8', - pyaudio.paInt16: 's16', - pyaudio.paInt24: 's24', - pyaudio.paInt32: 's32', - pyaudio.paFloat32: 'flt', - } - - def __init__(self, audio_format: int = pyaudio.paInt16, rate: int = 16000, channels: int = 1, packet_time: float = 0.020, device_index: int | None = None): - super().__init__() - - self.p = pyaudio.PyAudio() - chunk_size = int(packet_time * rate) - self.stream = self.p.open(format=audio_format, - channels=channels, - rate=rate, - frames_per_buffer=chunk_size, - input=True, - input_device_index=device_index) - self.format = audio_format - self.rate = rate - self.channels = channels - self.packet_time = packet_time - self.chunk_size = chunk_size - self.pts = 0 - - async def recv(self): - mic_data = self.stream.read(self.chunk_size) - mic_array = np.frombuffer(mic_data, dtype=np.int16) - mic_array = np.expand_dims(mic_array, axis=0) - layout = 'stereo' if self.channels > 1 else 'mono' - frame = av.AudioFrame.from_ndarray(mic_array, format=self.PYAUDIO_TO_AV_FORMAT_MAP[self.format], layout=layout) - frame.rate = self.rate - frame.pts = self.pts - self.pts += frame.samples - - return frame - - -class AudioOutputSpeaker: - def __init__(self, audio_format: int = pyaudio.paInt16, rate: int = 48000, channels: int = 2, packet_time: float = 0.2, device_index: int | None = None): - - chunk_size = int(packet_time * rate) - self.p = pyaudio.PyAudio() - self.buffer = io.BytesIO() - self.channels = channels - self.stream = self.p.open(format=audio_format, - channels=channels, - rate=rate, - frames_per_buffer=chunk_size, - output=True, - output_device_index=device_index, - stream_callback=self.__pyaudio_callback) - self.tracks_and_tasks: list[tuple[aiortc.MediaStreamTrack, asyncio.Task | None]] = [] - - def __pyaudio_callback(self, in_data, frame_count, time_info, status): - if self.buffer.getbuffer().nbytes < frame_count * self.channels * 2: - buff = b'\x00\x00' * frame_count * self.channels - elif self.buffer.getbuffer().nbytes > 115200: # 3x the usual read size - self.buffer.seek(0) - buff = self.buffer.read(frame_count * self.channels * 4) - buff = buff[:frame_count * self.channels * 2] - self.buffer.seek(2) - else: - self.buffer.seek(0) - buff = self.buffer.read(frame_count * self.channels * 2) - self.buffer.seek(2) - return (buff, pyaudio.paContinue) - - async def __consume(self, track): - while True: - try: - frame = await track.recv() - except aiortc.MediaStreamError: - return - - self.buffer.write(bytes(frame.planes[0])) - - def hasTrack(self, track: aiortc.MediaStreamTrack) -> bool: - return any(t == track for t, _ in self.tracks_and_tasks) - - def addTrack(self, track: aiortc.MediaStreamTrack): - if not self.hasTrack(track): - self.tracks_and_tasks.append((track, None)) - - def start(self): - for index, (track, task) in enumerate(self.tracks_and_tasks): - if task is None: - self.tracks_and_tasks[index] = (track, asyncio.create_task(self.__consume(track))) - - def stop(self): - for _, task in self.tracks_and_tasks: - if task is not None: - task.cancel() - - self.tracks_and_tasks = [] - self.stream.stop_stream() - self.stream.close() - self.p.terminate() diff --git a/system/webrtc/tests/test_stream_session.py b/system/webrtc/tests/test_stream_session.py index e31fda3728..f44d217d58 100644 --- a/system/webrtc/tests/test_stream_session.py +++ b/system/webrtc/tests/test_stream_session.py @@ -9,12 +9,10 @@ warnings.filterwarnings("ignore", category=RuntimeWarning) # TODO: remove this w from aiortc import RTCDataChannel from aiortc.mediastreams import VIDEO_CLOCK_RATE, VIDEO_TIME_BASE import capnp -import pyaudio from cereal import messaging, log from openpilot.system.webrtc.webrtcd import CerealOutgoingMessageProxy, CerealIncomingMessageProxy from openpilot.system.webrtc.device.video import LiveStreamVideoStreamTrack -from openpilot.system.webrtc.device.audio import AudioInputStreamTrack class TestStreamSession: @@ -87,18 +85,3 @@ class TestStreamSession: assert abs(i + packet.pts - (start_pts + (((time.monotonic_ns() - start_ns) * VIDEO_CLOCK_RATE) // 1_000_000_000))) < 450 #5ms assert packet.size == 0 - def test_input_audio_track(self, mocker): - packet_time, rate = 0.02, 16000 - sample_count = int(packet_time * rate) - mocked_stream = mocker.MagicMock(spec=pyaudio.Stream) - mocked_stream.read.return_value = b"\x00" * 2 * sample_count - - config = {"open.side_effect": lambda *args, **kwargs: mocked_stream} - mocker.patch("pyaudio.PyAudio", spec=True, **config) - track = AudioInputStreamTrack(audio_format=pyaudio.paInt16, packet_time=packet_time, rate=rate) - - for i in range(5): - frame = self.loop.run_until_complete(track.recv()) - assert frame.rate == rate - assert frame.samples == sample_count - assert frame.pts == i * sample_count diff --git a/system/webrtc/webrtcd.py b/system/webrtc/webrtcd.py index c19f1bf9dd..d2c90cafb5 100755 --- a/system/webrtc/webrtcd.py +++ b/system/webrtc/webrtcd.py @@ -119,10 +119,8 @@ class StreamSession: shared_pub_master = DynamicPubMaster([]) def __init__(self, sdp: str, cameras: list[str], incoming_services: list[str], outgoing_services: list[str], debug_mode: bool = False): - from aiortc.mediastreams import VideoStreamTrack, AudioStreamTrack - from aiortc.contrib.media import MediaBlackhole + from aiortc.mediastreams import VideoStreamTrack from openpilot.system.webrtc.device.video import LiveStreamVideoStreamTrack - from openpilot.system.webrtc.device.audio import AudioInputStreamTrack, AudioOutputSpeaker from teleoprtc import WebRTCAnswerBuilder from teleoprtc.info import parse_info_from_offer @@ -132,11 +130,6 @@ class StreamSession: assert len(cameras) == config.n_expected_camera_tracks, "Incoming stream has misconfigured number of video tracks" for cam in cameras: builder.add_video_stream(cam, LiveStreamVideoStreamTrack(cam) if not debug_mode else VideoStreamTrack()) - if config.expected_audio_track: - builder.add_audio_stream(AudioInputStreamTrack() if not debug_mode else AudioStreamTrack()) - if config.incoming_audio_track: - self.audio_output_cls = AudioOutputSpeaker if not debug_mode else MediaBlackhole - builder.offer_to_receive_audio_stream() self.stream = builder.stream() self.identifier = str(uuid.uuid4()) @@ -151,11 +144,10 @@ class StreamSession: self.outgoing_bridge = CerealOutgoingMessageProxy(messaging.SubMaster(outgoing_services)) self.outgoing_bridge_runner = CerealProxyRunner(self.outgoing_bridge) - self.audio_output: AudioOutputSpeaker | MediaBlackhole | None = None self.run_task: asyncio.Task | None = None self.logger = logging.getLogger("webrtcd") - self.logger.info("New stream session (%s), cameras %s, audio in %s out %s, incoming services %s, outgoing services %s", - self.identifier, cameras, config.incoming_audio_track, config.expected_audio_track, incoming_services, outgoing_services) + self.logger.info("New stream session (%s), cameras %s, incoming services %s, outgoing services %s", + self.identifier, cameras, incoming_services, outgoing_services) def start(self): self.run_task = asyncio.create_task(self.run()) @@ -188,11 +180,6 @@ class StreamSession: channel = self.stream.get_messaging_channel() self.outgoing_bridge_runner.proxy.add_channel(channel) self.outgoing_bridge_runner.start() - if self.stream.has_incoming_audio_track(): - track = self.stream.get_incoming_audio_track(buffered=False) - self.audio_output = self.audio_output_cls() - self.audio_output.addTrack(track) - self.audio_output.start() self.logger.info("Stream session (%s) connected", self.identifier) await self.stream.wait_for_disconnection() @@ -206,8 +193,6 @@ class StreamSession: await self.stream.stop() if self.outgoing_bridge is not None: self.outgoing_bridge_runner.stop() - if self.audio_output: - self.audio_output.stop() @dataclass diff --git a/tools/bodyteleop/web.py b/tools/bodyteleop/web.py index fd8f691d19..f91d6a1441 100644 --- a/tools/bodyteleop/web.py +++ b/tools/bodyteleop/web.py @@ -1,4 +1,3 @@ -import asyncio import dataclasses import json import logging @@ -6,8 +5,6 @@ import os import ssl import subprocess -import pyaudio -import wave from aiohttp import web from aiohttp import ClientSession @@ -22,35 +19,6 @@ TELEOPDIR = f"{BASEDIR}/tools/bodyteleop" WEBRTCD_HOST, WEBRTCD_PORT = "localhost", 5001 -## UTILS -async def play_sound(sound: str): - SOUNDS = { - "engage": "selfdrive/assets/sounds/engage.wav", - "disengage": "selfdrive/assets/sounds/disengage.wav", - "error": "selfdrive/assets/sounds/warning_immediate.wav", - } - assert sound in SOUNDS - - chunk = 5120 - with wave.open(os.path.join(BASEDIR, SOUNDS[sound]), "rb") as wf: - def callback(in_data, frame_count, time_info, status): - data = wf.readframes(frame_count) - return data, pyaudio.paContinue - - p = pyaudio.PyAudio() - stream = p.open(format=p.get_format_from_width(wf.getsampwidth()), - channels=wf.getnchannels(), - rate=wf.getframerate(), - output=True, - frames_per_buffer=chunk, - stream_callback=callback) - stream.start_stream() - while stream.is_active(): - await asyncio.sleep(0) - stream.stop_stream() - stream.close() - p.terminate() - ## SSL def create_ssl_cert(cert_path: str, key_path: str): try: @@ -86,14 +54,6 @@ async def ping(request: 'web.Request'): return web.Response(text="pong") -async def sound(request: 'web.Request'): - params = await request.json() - sound_to_play = params["sound"] - - await play_sound(sound_to_play) - return web.json_response({"status": "ok"}) - - async def offer(request: 'web.Request'): params = await request.json() body = StreamRequestBody(params["sdp"], ["driver"], ["testJoystick"], ["carState"]) @@ -111,14 +71,13 @@ def main(): # Enable joystick debug mode Params().put_bool("JoystickDebugMode", True) - # App needs to be HTTPS for microphone and audio autoplay to work on the browser + # App needs to be HTTPS for WebRTC to work on the browser ssl_context = create_ssl_context() app = web.Application() app.router.add_get("/", index) app.router.add_get("/ping", ping, allow_head=True) app.router.add_post("/offer", offer) - app.router.add_post("/sound", sound) app.router.add_static('/static', os.path.join(TELEOPDIR, 'static')) web.run_app(app, access_log=None, host="0.0.0.0", port=5000, ssl_context=ssl_context) diff --git a/tools/install_ubuntu_dependencies.sh b/tools/install_ubuntu_dependencies.sh index 0c9fada757..26fb45f052 100755 --- a/tools/install_ubuntu_dependencies.sh +++ b/tools/install_ubuntu_dependencies.sh @@ -42,7 +42,6 @@ function install_ubuntu_common_requirements() { libncurses5-dev \ libzmq3-dev \ libzstd-dev \ - portaudio19-dev \ gettext } diff --git a/tools/mac_setup.sh b/tools/mac_setup.sh index 4a66beeec9..3fe5cc59bf 100755 --- a/tools/mac_setup.sh +++ b/tools/mac_setup.sh @@ -32,7 +32,6 @@ brew "coreutils" brew "eigen" brew "llvm" brew "zeromq" -brew "portaudio" EOS echo "[ ] finished brew install t=$SECONDS" diff --git a/uv.lock b/uv.lock index 9bfbd05dd5..fa1c4f61b6 100644 --- a/uv.lock +++ b/uv.lock @@ -771,7 +771,6 @@ dependencies = [ { name = "libusb1" }, { name = "numpy" }, { name = "psutil" }, - { name = "pyaudio" }, { name = "pycapnp" }, { name = "pycryptodome" }, { name = "pyjwt" }, @@ -850,7 +849,6 @@ requires-dist = [ { name = "opencv-python-headless", marker = "extra == 'dev'" }, { name = "pre-commit-hooks", marker = "extra == 'testing'" }, { name = "psutil" }, - { name = "pyaudio" }, { name = "pycapnp" }, { name = "pycryptodome" }, { name = "pyjwt" }, @@ -1027,16 +1025,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, ] -[[package]] -name = "pyaudio" -version = "0.2.14" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/26/1d/8878c7752febb0f6716a7e1a52cb92ac98871c5aa522cba181878091607c/PyAudio-0.2.14.tar.gz", hash = "sha256:78dfff3879b4994d1f4fc6485646a57755c6ee3c19647a491f790a0895bd2f87", size = 47066, upload-time = "2023-11-07T07:11:48.806Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8d/45/8d2b76e8f6db783f9326c1305f3f816d4a12c8eda5edc6a2e1d03c097c3b/PyAudio-0.2.14-cp312-cp312-win32.whl", hash = "sha256:5fce4bcdd2e0e8c063d835dbe2860dac46437506af509353c7f8114d4bacbd5b", size = 144750, upload-time = "2023-11-07T07:11:40.142Z" }, - { url = "https://files.pythonhosted.org/packages/b0/6a/d25812e5f79f06285767ec607b39149d02aa3b31d50c2269768f48768930/PyAudio-0.2.14-cp312-cp312-win_amd64.whl", hash = "sha256:12f2f1ba04e06ff95d80700a78967897a489c05e093e3bffa05a84ed9c0a7fa3", size = 164126, upload-time = "2023-11-07T07:11:41.539Z" }, -] - [[package]] name = "pycapnp" version = "2.1.0" From f4a36f7f743f2fb3499af4ab6769a8819150917f Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sun, 22 Feb 2026 19:37:14 -0800 Subject: [PATCH 153/311] rm cpp bz2 (#37332) --- tools/cabana/SConscript | 2 +- tools/cabana/tests/test_cabana.cc | 2 +- tools/replay/SConscript | 2 +- tools/replay/logreader.cc | 4 +-- tools/replay/route.cc | 4 +-- tools/replay/tests/test_replay.cc | 4 +-- tools/replay/util.cc | 42 ------------------------------- tools/replay/util.h | 2 -- 8 files changed, 8 insertions(+), 54 deletions(-) diff --git a/tools/cabana/SConscript b/tools/cabana/SConscript index 8ea59d0766..90de212655 100644 --- a/tools/cabana/SConscript +++ b/tools/cabana/SConscript @@ -75,7 +75,7 @@ qt_libs = base_libs cabana_env = qt_env.Clone() -cabana_libs = [cereal, messaging, visionipc, replay_lib, 'avformat', 'avcodec', 'swresample', 'avutil', 'x264', 'z', 'bz2', 'zstd', 'curl', 'yuv', 'usb-1.0'] + qt_libs +cabana_libs = [cereal, messaging, visionipc, replay_lib, 'avformat', 'avcodec', 'swresample', 'avutil', 'x264', 'z', 'zstd', 'curl', 'yuv', 'usb-1.0'] + qt_libs opendbc_path = '-DOPENDBC_FILE_PATH=\'"%s"\'' % (cabana_env.Dir("../../opendbc/dbc").abspath) cabana_env['CXXFLAGS'] += [opendbc_path] diff --git a/tools/cabana/tests/test_cabana.cc b/tools/cabana/tests/test_cabana.cc index d9fcae6f21..4c11bfc8b8 100644 --- a/tools/cabana/tests/test_cabana.cc +++ b/tools/cabana/tests/test_cabana.cc @@ -5,7 +5,7 @@ #include "catch2/catch.hpp" #include "tools/cabana/dbc/dbcmanager.h" -const std::string TEST_RLOG_URL = "https://commadataci.blob.core.windows.net/openpilotci/0c94aa1e1296d7c6/2021-05-05--19-48-37/0/rlog.bz2"; +const std::string TEST_RLOG_URL = "https://commadataci.blob.core.windows.net/openpilotci/0c94aa1e1296d7c6/2021-05-05--19-48-37/0/rlog.zst"; TEST_CASE("DBCFile::generateDBC") { QString fn = QString("%1/%2.dbc").arg(OPENDBC_FILE_PATH, "tesla_can"); diff --git a/tools/replay/SConscript b/tools/replay/SConscript index b39cf6dab1..47b25df166 100644 --- a/tools/replay/SConscript +++ b/tools/replay/SConscript @@ -12,7 +12,7 @@ if arch != "Darwin": replay_lib_src.append("qcom_decoder.cc") replay_lib = replay_env.Library("replay", replay_lib_src, LIBS=base_libs, FRAMEWORKS=base_frameworks) Export('replay_lib') -replay_libs = [replay_lib, 'avformat', 'avcodec', 'swresample', 'avutil', 'x264', 'z', 'bz2', 'zstd', 'curl', 'yuv', 'ncurses'] + base_libs +replay_libs = [replay_lib, 'avformat', 'avcodec', 'swresample', 'avutil', 'x264', 'z', 'zstd', 'curl', 'yuv', 'ncurses'] + base_libs replay_env.Program("replay", ["main.cc"], LIBS=replay_libs, FRAMEWORKS=base_frameworks) if GetOption('extras'): diff --git a/tools/replay/logreader.cc b/tools/replay/logreader.cc index 75abb8417b..0d9e053aba 100644 --- a/tools/replay/logreader.cc +++ b/tools/replay/logreader.cc @@ -9,9 +9,7 @@ bool LogReader::load(const std::string &url, std::atomic *abort, bool local_cache, int chunk_size, int retries) { std::string data = FileReader(local_cache, chunk_size, retries).read(url, abort); if (!data.empty()) { - if (url.find(".bz2") != std::string::npos || util::starts_with(data, "BZh9")) { - data = decompressBZ2(data, abort); - } else if (url.find(".zst") != std::string::npos || util::starts_with(data, "\x28\xB5\x2F\xFD")) { + if (url.find(".zst") != std::string::npos || util::starts_with(data, "\x28\xB5\x2F\xFD")) { data = decompressZST(data, abort); } } diff --git a/tools/replay/route.cc b/tools/replay/route.cc index ba00828267..663c4b43cb 100644 --- a/tools/replay/route.cc +++ b/tools/replay/route.cc @@ -174,9 +174,9 @@ void Route::addFileToSegment(int n, const std::string &file) { auto pos = name.find_last_of("--"); name = pos != std::string::npos ? name.substr(pos + 2) : name; - if (name == "rlog.bz2" || name == "rlog.zst" || name == "rlog") { + if (name == "rlog.zst" || name == "rlog") { segments_[n].rlog = file; - } else if (name == "qlog.bz2" || name == "qlog.zst" || name == "qlog") { + } else if (name == "qlog.zst" || name == "qlog") { segments_[n].qlog = file; } else if (name == "fcamera.hevc") { segments_[n].road_cam = file; diff --git a/tools/replay/tests/test_replay.cc b/tools/replay/tests/test_replay.cc index aed3de59a8..f4afc29968 100644 --- a/tools/replay/tests/test_replay.cc +++ b/tools/replay/tests/test_replay.cc @@ -2,14 +2,14 @@ #include "catch2/catch.hpp" #include "tools/replay/replay.h" -const std::string TEST_RLOG_URL = "https://commadataci.blob.core.windows.net/openpilotci/0c94aa1e1296d7c6/2021-05-05--19-48-37/0/rlog.bz2"; +const std::string TEST_RLOG_URL = "https://commadataci.blob.core.windows.net/openpilotci/0c94aa1e1296d7c6/2021-05-05--19-48-37/0/rlog.zst"; TEST_CASE("LogReader") { SECTION("corrupt log") { FileReader reader(true); std::string corrupt_content = reader.read(TEST_RLOG_URL); corrupt_content.resize(corrupt_content.length() / 2); - corrupt_content = decompressBZ2(corrupt_content); + corrupt_content = decompressZST(corrupt_content); LogReader log; REQUIRE(log.load(corrupt_content.data(), corrupt_content.size())); REQUIRE(log.events.size() > 0); diff --git a/tools/replay/util.cc b/tools/replay/util.cc index 481564322e..cc37c19ecf 100644 --- a/tools/replay/util.cc +++ b/tools/replay/util.cc @@ -1,6 +1,5 @@ #include "tools/replay/util.h" -#include #include #include @@ -280,47 +279,6 @@ bool httpDownload(const std::string &url, const std::string &file, size_t chunk_ return httpDownload(url, of, chunk_size, size, abort); } -std::string decompressBZ2(const std::string &in, std::atomic *abort) { - return decompressBZ2((std::byte *)in.data(), in.size(), abort); -} - -std::string decompressBZ2(const std::byte *in, size_t in_size, std::atomic *abort) { - if (in_size == 0) return {}; - - bz_stream strm = {}; - int bzerror = BZ2_bzDecompressInit(&strm, 0, 0); - assert(bzerror == BZ_OK); - - strm.next_in = (char *)in; - strm.avail_in = in_size; - std::string out(in_size * 5, '\0'); - do { - strm.next_out = (char *)(&out[strm.total_out_lo32]); - strm.avail_out = out.size() - strm.total_out_lo32; - - const char *prev_write_pos = strm.next_out; - bzerror = BZ2_bzDecompress(&strm); - if (bzerror == BZ_OK && prev_write_pos == strm.next_out) { - // content is corrupt - bzerror = BZ_STREAM_END; - rWarning("decompressBZ2 error: content is corrupt"); - break; - } - - if (bzerror == BZ_OK && strm.avail_in > 0 && strm.avail_out == 0) { - out.resize(out.size() * 2); - } - } while (bzerror == BZ_OK && !(abort && *abort)); - - BZ2_bzDecompressEnd(&strm); - if (bzerror == BZ_STREAM_END && !(abort && *abort)) { - out.resize(strm.total_out_lo32); - out.shrink_to_fit(); - return out; - } - return {}; -} - std::string decompressZST(const std::string &in, std::atomic *abort) { return decompressZST((std::byte *)in.data(), in.size(), abort); } diff --git a/tools/replay/util.h b/tools/replay/util.h index 1f61951d21..fc4d2d54f9 100644 --- a/tools/replay/util.h +++ b/tools/replay/util.h @@ -48,8 +48,6 @@ private: std::string sha256(const std::string &str); void precise_nano_sleep(int64_t nanoseconds, std::atomic &interrupt_requested); -std::string decompressBZ2(const std::string &in, std::atomic *abort = nullptr); -std::string decompressBZ2(const std::byte *in, size_t in_size, std::atomic *abort = nullptr); std::string decompressZST(const std::string &in, std::atomic *abort = nullptr); std::string decompressZST(const std::byte *in, size_t in_size, std::atomic *abort = nullptr); std::string getUrlWithoutQuery(const std::string &url); From cef81da1e936a6ef88f2bb3d1241b081b2666def Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sun, 22 Feb 2026 19:59:05 -0800 Subject: [PATCH 154/311] use vendored zeromq from dependencies repo (#37333) * use vendored zeromq from dependencies repo Co-Authored-By: Claude Opus 4.6 * lock * rm more crap --------- Co-authored-by: Claude Opus 4.6 --- SConstruct | 3 +- pyproject.toml | 1 + tools/install_ubuntu_dependencies.sh | 1 - tools/mac_setup.sh | 9 -- uv.lock | 179 ++++++++++++++------------- 5 files changed, 99 insertions(+), 94 deletions(-) diff --git a/SConstruct b/SConstruct index 3d611af0b2..148c119066 100644 --- a/SConstruct +++ b/SConstruct @@ -41,7 +41,8 @@ assert arch in [ if arch != "larch64": import capnproto import ffmpeg as ffmpeg_pkg - pkgs = [capnproto, ffmpeg_pkg] + import zeromq + pkgs = [capnproto, ffmpeg_pkg, zeromq] else: # TODO: remove when AGNOS has our new vendor pkgs pkgs = [] diff --git a/pyproject.toml b/pyproject.toml index 508a6230b2..71e40e3914 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,7 @@ dependencies = [ # vendored native dependencies "capnproto @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=capnproto", "ffmpeg @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=ffmpeg", + "zeromq @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=zeromq", # body / webrtcd "av", diff --git a/tools/install_ubuntu_dependencies.sh b/tools/install_ubuntu_dependencies.sh index 26fb45f052..7d24b6c09a 100755 --- a/tools/install_ubuntu_dependencies.sh +++ b/tools/install_ubuntu_dependencies.sh @@ -40,7 +40,6 @@ function install_ubuntu_common_requirements() { libgles2-mesa-dev \ libjpeg-dev \ libncurses5-dev \ - libzmq3-dev \ libzstd-dev \ gettext } diff --git a/tools/mac_setup.sh b/tools/mac_setup.sh index 3fe5cc59bf..c4eee0f67d 100755 --- a/tools/mac_setup.sh +++ b/tools/mac_setup.sh @@ -31,19 +31,10 @@ brew "git-lfs" brew "coreutils" brew "eigen" brew "llvm" -brew "zeromq" EOS echo "[ ] finished brew install t=$SECONDS" -BREW_PREFIX=$(brew --prefix) - -# archive backend tools for pip dependencies -export LDFLAGS="$LDFLAGS -L${BREW_PREFIX}/opt/zlib/lib" -export LDFLAGS="$LDFLAGS -L${BREW_PREFIX}/opt/bzip2/lib" -export CPPFLAGS="$CPPFLAGS -I${BREW_PREFIX}/opt/zlib/include" -export CPPFLAGS="$CPPFLAGS -I${BREW_PREFIX}/opt/bzip2/include" - # install python dependencies $DIR/install_python_dependencies.sh echo "[ ] installed python dependencies t=$SECONDS" diff --git a/uv.lock b/uv.lock index fa1c4f61b6..3327ba4dbe 100644 --- a/uv.lock +++ b/uv.lock @@ -60,27 +60,20 @@ wheels = [ [[package]] name = "aiortc" -version = "1.10.1" +version = "1.14.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aioice" }, { name = "av" }, - { name = "cffi" }, { name = "cryptography" }, { name = "google-crc32c" }, { name = "pyee" }, { name = "pylibsrtp" }, { name = "pyopenssl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8a/f8/408e092748521889c9d33dddcef920afd9891cf6db4615ba6b6bfe114ff8/aiortc-1.10.1.tar.gz", hash = "sha256:64926ad86bde20c1a4dacb7c3a164e57b522606b70febe261fada4acf79641b5", size = 1179406, upload-time = "2025-02-02T17:36:38.684Z" } +sdist = { url = "https://files.pythonhosted.org/packages/51/9c/4e027bfe0195de0442da301e2389329496745d40ae44d2d7c4571c4290ce/aiortc-1.14.0.tar.gz", hash = "sha256:adc8a67ace10a085721e588e06a00358ed8eaf5f6b62f0a95358ff45628dd762", size = 1180864, upload-time = "2025-10-13T21:40:37.905Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0a/6b/74547a30d1ddcc81f905ef4ff7fcc2c89b7482cb2045688f2aaa4fa918aa/aiortc-1.10.1-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:3bef536f38394b518aefae9dbf9cdd08f39e4c425f316f9692f0d8dc724810bd", size = 1218457, upload-time = "2025-02-02T17:36:23.172Z" }, - { url = "https://files.pythonhosted.org/packages/46/92/b4ccf39cd18e366ace2a11dc7d98ed55967b4b325707386b5788149db15e/aiortc-1.10.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:8842c02e38513d9432ef22982572833487bb015f23348fa10a690616dbf55143", size = 898855, upload-time = "2025-02-02T17:36:25.9Z" }, - { url = "https://files.pythonhosted.org/packages/a4/e9/2676de48b493787d8b03129713e6bb2dfbacca2a565090f2a89cbad71f96/aiortc-1.10.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:954a420de01c0bf6b07a0c58b662029b1c4204ddbd8f5c4162bbdebd43f882b1", size = 1750403, upload-time = "2025-02-02T17:36:28.446Z" }, - { url = "https://files.pythonhosted.org/packages/c3/9d/ab6d09183cdaf5df060923d9bd5c9ed5fb1802661d9401dba35f3c85a57b/aiortc-1.10.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7c0d46fb30307a9d7deb4b7d66f0b0e73b77a7221b063fb6dc78821a5d2aa1e", size = 1867886, upload-time = "2025-02-02T17:36:30.209Z" }, - { url = "https://files.pythonhosted.org/packages/c2/71/0b5666e6b965dbd9a7f331aa827a6c3ab3eb4d582fefb686a7f4227b7954/aiortc-1.10.1-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:89582f6923046f79f15d9045f432bc78191eacc95f6bed18714e86ec935188d9", size = 1893709, upload-time = "2025-02-02T17:36:32.342Z" }, - { url = "https://files.pythonhosted.org/packages/9d/0a/8c0c78fad79ef595a0ed6e2ab413900e6bd0eac65fc5c31c9d8736bff909/aiortc-1.10.1-cp39-abi3-win32.whl", hash = "sha256:d1cbe87f740b33ffaa8e905f21092773e74916be338b64b81c8b79af4c3847eb", size = 923265, upload-time = "2025-02-02T17:36:34.685Z" }, - { url = "https://files.pythonhosted.org/packages/73/12/a27dd588a4988021da88cb4d338d8ee65ac097afc14e9193ab0be4a48790/aiortc-1.10.1-cp39-abi3-win_amd64.whl", hash = "sha256:c9a5a0b23f8a77540068faec8837fa0a65b0396c20f09116bdb874b75e0b6abe", size = 1009488, upload-time = "2025-02-02T17:36:36.317Z" }, + { url = "https://files.pythonhosted.org/packages/57/ab/31646a49209568cde3b97eeade0d28bb78b400e6645c56422c101df68932/aiortc-1.14.0-py3-none-any.whl", hash = "sha256:4b244d7e482f4e1f67e685b3468269628eca1ec91fa5b329ab517738cfca086e", size = 93183, upload-time = "2025-10-13T21:40:36.59Z" }, ] [[package]] @@ -107,22 +100,23 @@ wheels = [ [[package]] name = "av" -version = "13.1.0" +version = "16.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0c/9d/486d31e76784cc0ad943f420c5e05867263b32b37e2f4b0f7f22fdc1ca3a/av-13.1.0.tar.gz", hash = "sha256:d3da736c55847d8596eb8c26c60e036f193001db3bc5c10da8665622d906c17e", size = 3957908, upload-time = "2024-10-06T04:54:57.507Z" } +sdist = { url = "https://files.pythonhosted.org/packages/78/cd/3a83ffbc3cc25b39721d174487fb0d51a76582f4a1703f98e46170ce83d4/av-16.1.0.tar.gz", hash = "sha256:a094b4fd87a3721dacf02794d3d2c82b8d712c85b9534437e82a8a978c175ffd", size = 4285203, upload-time = "2026-01-11T07:31:33.772Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/aa/4bdd8ce59173574fc6e0c282c71ee6f96fca82643d97bf172bc4cb5a5674/av-13.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:261dbc3f4b55f4f8f3375b10b2258fca7f2ab7a6365c01bc65e77a0d5327a195", size = 24268674, upload-time = "2024-10-06T04:53:11.251Z" }, - { url = "https://files.pythonhosted.org/packages/17/b4/b267dd5bad99eed49ec6731827c6bcb5ab03864bf732a7ebb81e3df79911/av-13.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83d259ef86b9054eb914bc7c6a7f6092a6d75cb939295e70ee979cfd92a67b99", size = 19475617, upload-time = "2024-10-06T04:53:13.832Z" }, - { url = "https://files.pythonhosted.org/packages/68/32/4209e51f54d7b54a1feb576d309c671ed1ff437b54fcc4ec68c239199e0a/av-13.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3b4d3ca159eceab97e3c0fb08fe756520fb95508417f76e48198fda2a5b0806", size = 32468873, upload-time = "2024-10-06T04:53:17.639Z" }, - { url = "https://files.pythonhosted.org/packages/b6/d8/c174da5f06b24f3c9e36f91fd02a7411c39da9ce792c17964260d4be675e/av-13.1.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40e8f757e373b73a2dc4640852a00cce4a4a92ef19b2e642a96d6994cd1fffbf", size = 31818484, upload-time = "2024-10-06T04:53:21.509Z" }, - { url = "https://files.pythonhosted.org/packages/7f/22/0dd8d1d5cad415772bb707d16aea8b81cf75d340d11d3668eea43468c730/av-13.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8aaec2c0bfd024359db3821d679009d4e637e1bee0321d20f61c54ed6b20f41", size = 34398652, upload-time = "2024-10-06T04:53:25.798Z" }, - { url = "https://files.pythonhosted.org/packages/7b/ff/48fa68888b8d5bae36d915556ff18f9e5fdc6b5ff5ae23dc4904c9713168/av-13.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:5ea0deab0e6a739cb742fba2a3983d8102f7516a3cdf3c46669f3cac0ed1f351", size = 25781343, upload-time = "2024-10-06T04:53:29.577Z" }, + { url = "https://files.pythonhosted.org/packages/9c/84/2535f55edcd426cebec02eb37b811b1b0c163f26b8d3f53b059e2ec32665/av-16.1.0-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:640f57b93f927fba8689f6966c956737ee95388a91bd0b8c8b5e0481f73513d6", size = 26945785, upload-time = "2026-01-09T20:18:34.486Z" }, + { url = "https://files.pythonhosted.org/packages/b6/17/ffb940c9e490bf42e86db4db1ff426ee1559cd355a69609ec1efe4d3a9eb/av-16.1.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:ae3fb658eec00852ebd7412fdc141f17f3ddce8afee2d2e1cf366263ad2a3b35", size = 21481147, upload-time = "2026-01-09T20:18:36.716Z" }, + { url = "https://files.pythonhosted.org/packages/15/c1/e0d58003d2d83c3921887d5c8c9b8f5f7de9b58dc2194356a2656a45cfdc/av-16.1.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:27ee558d9c02a142eebcbe55578a6d817fedfde42ff5676275504e16d07a7f86", size = 39517197, upload-time = "2026-01-11T09:57:31.937Z" }, + { url = "https://files.pythonhosted.org/packages/32/77/787797b43475d1b90626af76f80bfb0c12cfec5e11eafcfc4151b8c80218/av-16.1.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:7ae547f6d5fa31763f73900d43901e8c5fa6367bb9a9840978d57b5a7ae14ed2", size = 41174337, upload-time = "2026-01-11T09:57:35.792Z" }, + { url = "https://files.pythonhosted.org/packages/8e/ac/d90df7f1e3b97fc5554cf45076df5045f1e0a6adf13899e10121229b826c/av-16.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8cf065f9d438e1921dc31fc7aa045790b58aee71736897866420d80b5450f62a", size = 40817720, upload-time = "2026-01-11T09:57:39.039Z" }, + { url = "https://files.pythonhosted.org/packages/80/6f/13c3a35f9dbcebafd03fe0c4cbd075d71ac8968ec849a3cfce406c35a9d2/av-16.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a345877a9d3cc0f08e2bc4ec163ee83176864b92587afb9d08dff50f37a9a829", size = 42267396, upload-time = "2026-01-11T09:57:42.115Z" }, + { url = "https://files.pythonhosted.org/packages/c8/b9/275df9607f7fb44317ccb1d4be74827185c0d410f52b6e2cd770fe209118/av-16.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:f49243b1d27c91cd8c66fdba90a674e344eb8eb917264f36117bf2b6879118fd", size = 31752045, upload-time = "2026-01-11T09:57:45.106Z" }, ] [[package]] name = "capnproto" version = "1.0.1" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=capnproto&rev=releases#acf53363042d92d3206b8ce6b9da4bfb75b200c7" } +source = { git = "https://github.com/commaai/dependencies.git?subdirectory=capnproto&rev=releases#5fc3994af75440c9222d627d14a24550f27a1e7f" } [[package]] name = "casadi" @@ -292,31 +286,41 @@ wheels = [ [[package]] name = "cryptography" -version = "43.0.3" +version = "46.0.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0d/05/07b55d1fa21ac18c3a8c79f764e2514e6f6a9698f1be44994f5adf0d29db/cryptography-43.0.3.tar.gz", hash = "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805", size = 686989, upload-time = "2024-10-18T15:58:32.918Z" } +sdist = { url = "https://files.pythonhosted.org/packages/60/04/ee2a9e8542e4fa2773b81771ff8349ff19cdd56b7258a0cc442639052edb/cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d", size = 750064, upload-time = "2026-02-10T19:18:38.255Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1f/f3/01fdf26701a26f4b4dbc337a26883ad5bccaa6f1bbbdd29cd89e22f18a1c/cryptography-43.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e", size = 6225303, upload-time = "2024-10-18T15:57:36.753Z" }, - { url = "https://files.pythonhosted.org/packages/a3/01/4896f3d1b392025d4fcbecf40fdea92d3df8662123f6835d0af828d148fd/cryptography-43.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e", size = 3760905, upload-time = "2024-10-18T15:57:39.166Z" }, - { url = "https://files.pythonhosted.org/packages/0a/be/f9a1f673f0ed4b7f6c643164e513dbad28dd4f2dcdf5715004f172ef24b6/cryptography-43.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f", size = 3977271, upload-time = "2024-10-18T15:57:41.227Z" }, - { url = "https://files.pythonhosted.org/packages/4e/49/80c3a7b5514d1b416d7350830e8c422a4d667b6d9b16a9392ebfd4a5388a/cryptography-43.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6", size = 3746606, upload-time = "2024-10-18T15:57:42.903Z" }, - { url = "https://files.pythonhosted.org/packages/0e/16/a28ddf78ac6e7e3f25ebcef69ab15c2c6be5ff9743dd0709a69a4f968472/cryptography-43.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18", size = 3986484, upload-time = "2024-10-18T15:57:45.434Z" }, - { url = "https://files.pythonhosted.org/packages/01/f5/69ae8da70c19864a32b0315049866c4d411cce423ec169993d0434218762/cryptography-43.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd", size = 3852131, upload-time = "2024-10-18T15:57:47.267Z" }, - { url = "https://files.pythonhosted.org/packages/fd/db/e74911d95c040f9afd3612b1f732e52b3e517cb80de8bf183be0b7d413c6/cryptography-43.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73", size = 4075647, upload-time = "2024-10-18T15:57:49.684Z" }, - { url = "https://files.pythonhosted.org/packages/56/48/7b6b190f1462818b324e674fa20d1d5ef3e24f2328675b9b16189cbf0b3c/cryptography-43.0.3-cp37-abi3-win32.whl", hash = "sha256:cbeb489927bd7af4aa98d4b261af9a5bc025bd87f0e3547e11584be9e9427be2", size = 2623873, upload-time = "2024-10-18T15:57:51.822Z" }, - { url = "https://files.pythonhosted.org/packages/eb/b1/0ebff61a004f7f89e7b65ca95f2f2375679d43d0290672f7713ee3162aff/cryptography-43.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:f46304d6f0c6ab8e52770addfa2fc41e6629495548862279641972b6215451cd", size = 3068039, upload-time = "2024-10-18T15:57:54.426Z" }, - { url = "https://files.pythonhosted.org/packages/30/d5/c8b32c047e2e81dd172138f772e81d852c51f0f2ad2ae8a24f1122e9e9a7/cryptography-43.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8ac43ae87929a5982f5948ceda07001ee5e83227fd69cf55b109144938d96984", size = 6222984, upload-time = "2024-10-18T15:57:56.174Z" }, - { url = "https://files.pythonhosted.org/packages/2f/78/55356eb9075d0be6e81b59f45c7b48df87f76a20e73893872170471f3ee8/cryptography-43.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5", size = 3762968, upload-time = "2024-10-18T15:57:58.206Z" }, - { url = "https://files.pythonhosted.org/packages/2a/2c/488776a3dc843f95f86d2f957ca0fc3407d0242b50bede7fad1e339be03f/cryptography-43.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4", size = 3977754, upload-time = "2024-10-18T15:58:00.683Z" }, - { url = "https://files.pythonhosted.org/packages/7c/04/2345ca92f7a22f601a9c62961741ef7dd0127c39f7310dffa0041c80f16f/cryptography-43.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7", size = 3749458, upload-time = "2024-10-18T15:58:02.225Z" }, - { url = "https://files.pythonhosted.org/packages/ac/25/e715fa0bc24ac2114ed69da33adf451a38abb6f3f24ec207908112e9ba53/cryptography-43.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405", size = 3988220, upload-time = "2024-10-18T15:58:04.331Z" }, - { url = "https://files.pythonhosted.org/packages/21/ce/b9c9ff56c7164d8e2edfb6c9305045fbc0df4508ccfdb13ee66eb8c95b0e/cryptography-43.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16", size = 3853898, upload-time = "2024-10-18T15:58:06.113Z" }, - { url = "https://files.pythonhosted.org/packages/2a/33/b3682992ab2e9476b9c81fff22f02c8b0a1e6e1d49ee1750a67d85fd7ed2/cryptography-43.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73", size = 4076592, upload-time = "2024-10-18T15:58:08.673Z" }, - { url = "https://files.pythonhosted.org/packages/81/1e/ffcc41b3cebd64ca90b28fd58141c5f68c83d48563c88333ab660e002cd3/cryptography-43.0.3-cp39-abi3-win32.whl", hash = "sha256:d56e96520b1020449bbace2b78b603442e7e378a9b3bd68de65c782db1507995", size = 2623145, upload-time = "2024-10-18T15:58:10.264Z" }, - { url = "https://files.pythonhosted.org/packages/87/5c/3dab83cc4aba1f4b0e733e3f0c3e7d4386440d660ba5b1e3ff995feb734d/cryptography-43.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362", size = 3068026, upload-time = "2024-10-18T15:58:11.916Z" }, + { url = "https://files.pythonhosted.org/packages/f7/81/b0bb27f2ba931a65409c6b8a8b358a7f03c0e46eceacddff55f7c84b1f3b/cryptography-46.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad", size = 7176289, upload-time = "2026-02-10T19:17:08.274Z" }, + { url = "https://files.pythonhosted.org/packages/ff/9e/6b4397a3e3d15123de3b1806ef342522393d50736c13b20ec4c9ea6693a6/cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b", size = 4275637, upload-time = "2026-02-10T19:17:10.53Z" }, + { url = "https://files.pythonhosted.org/packages/63/e7/471ab61099a3920b0c77852ea3f0ea611c9702f651600397ac567848b897/cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b", size = 4424742, upload-time = "2026-02-10T19:17:12.388Z" }, + { url = "https://files.pythonhosted.org/packages/37/53/a18500f270342d66bf7e4d9f091114e31e5ee9e7375a5aba2e85a91e0044/cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263", size = 4277528, upload-time = "2026-02-10T19:17:13.853Z" }, + { url = "https://files.pythonhosted.org/packages/22/29/c2e812ebc38c57b40e7c583895e73c8c5adb4d1e4a0cc4c5a4fdab2b1acc/cryptography-46.0.5-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d", size = 4947993, upload-time = "2026-02-10T19:17:15.618Z" }, + { url = "https://files.pythonhosted.org/packages/6b/e7/237155ae19a9023de7e30ec64e5d99a9431a567407ac21170a046d22a5a3/cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed", size = 4456855, upload-time = "2026-02-10T19:17:17.221Z" }, + { url = "https://files.pythonhosted.org/packages/2d/87/fc628a7ad85b81206738abbd213b07702bcbdada1dd43f72236ef3cffbb5/cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2", size = 3984635, upload-time = "2026-02-10T19:17:18.792Z" }, + { url = "https://files.pythonhosted.org/packages/84/29/65b55622bde135aedf4565dc509d99b560ee4095e56989e815f8fd2aa910/cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2", size = 4277038, upload-time = "2026-02-10T19:17:20.256Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/45e76c68d7311432741faf1fbf7fac8a196a0a735ca21f504c75d37e2558/cryptography-46.0.5-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0", size = 4912181, upload-time = "2026-02-10T19:17:21.825Z" }, + { url = "https://files.pythonhosted.org/packages/6d/1a/c1ba8fead184d6e3d5afcf03d569acac5ad063f3ac9fb7258af158f7e378/cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731", size = 4456482, upload-time = "2026-02-10T19:17:25.133Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e5/3fb22e37f66827ced3b902cf895e6a6bc1d095b5b26be26bd13c441fdf19/cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82", size = 4405497, upload-time = "2026-02-10T19:17:26.66Z" }, + { url = "https://files.pythonhosted.org/packages/1a/df/9d58bb32b1121a8a2f27383fabae4d63080c7ca60b9b5c88be742be04ee7/cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1", size = 4667819, upload-time = "2026-02-10T19:17:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/ea/ed/325d2a490c5e94038cdb0117da9397ece1f11201f425c4e9c57fe5b9f08b/cryptography-46.0.5-cp311-abi3-win32.whl", hash = "sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48", size = 3028230, upload-time = "2026-02-10T19:17:30.518Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5a/ac0f49e48063ab4255d9e3b79f5def51697fce1a95ea1370f03dc9db76f6/cryptography-46.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4", size = 3480909, upload-time = "2026-02-10T19:17:32.083Z" }, + { url = "https://files.pythonhosted.org/packages/e2/fa/a66aa722105ad6a458bebd64086ca2b72cdd361fed31763d20390f6f1389/cryptography-46.0.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31", size = 7170514, upload-time = "2026-02-10T19:17:56.267Z" }, + { url = "https://files.pythonhosted.org/packages/0f/04/c85bdeab78c8bc77b701bf0d9bdcf514c044e18a46dcff330df5448631b0/cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18", size = 4275349, upload-time = "2026-02-10T19:17:58.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/32/9b87132a2f91ee7f5223b091dc963055503e9b442c98fc0b8a5ca765fab0/cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235", size = 4420667, upload-time = "2026-02-10T19:18:00.619Z" }, + { url = "https://files.pythonhosted.org/packages/a1/a6/a7cb7010bec4b7c5692ca6f024150371b295ee1c108bdc1c400e4c44562b/cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a", size = 4276980, upload-time = "2026-02-10T19:18:02.379Z" }, + { url = "https://files.pythonhosted.org/packages/8e/7c/c4f45e0eeff9b91e3f12dbd0e165fcf2a38847288fcfd889deea99fb7b6d/cryptography-46.0.5-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76", size = 4939143, upload-time = "2026-02-10T19:18:03.964Z" }, + { url = "https://files.pythonhosted.org/packages/37/19/e1b8f964a834eddb44fa1b9a9976f4e414cbb7aa62809b6760c8803d22d1/cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614", size = 4453674, upload-time = "2026-02-10T19:18:05.588Z" }, + { url = "https://files.pythonhosted.org/packages/db/ed/db15d3956f65264ca204625597c410d420e26530c4e2943e05a0d2f24d51/cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229", size = 3978801, upload-time = "2026-02-10T19:18:07.167Z" }, + { url = "https://files.pythonhosted.org/packages/41/e2/df40a31d82df0a70a0daf69791f91dbb70e47644c58581d654879b382d11/cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1", size = 4276755, upload-time = "2026-02-10T19:18:09.813Z" }, + { url = "https://files.pythonhosted.org/packages/33/45/726809d1176959f4a896b86907b98ff4391a8aa29c0aaaf9450a8a10630e/cryptography-46.0.5-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d", size = 4901539, upload-time = "2026-02-10T19:18:11.263Z" }, + { url = "https://files.pythonhosted.org/packages/99/0f/a3076874e9c88ecb2ecc31382f6e7c21b428ede6f55aafa1aa272613e3cd/cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c", size = 4452794, upload-time = "2026-02-10T19:18:12.914Z" }, + { url = "https://files.pythonhosted.org/packages/02/ef/ffeb542d3683d24194a38f66ca17c0a4b8bf10631feef44a7ef64e631b1a/cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4", size = 4404160, upload-time = "2026-02-10T19:18:14.375Z" }, + { url = "https://files.pythonhosted.org/packages/96/93/682d2b43c1d5f1406ed048f377c0fc9fc8f7b0447a478d5c65ab3d3a66eb/cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9", size = 4667123, upload-time = "2026-02-10T19:18:15.886Z" }, + { url = "https://files.pythonhosted.org/packages/45/2d/9c5f2926cb5300a8eefc3f4f0b3f3df39db7f7ce40c8365444c49363cbda/cryptography-46.0.5-cp38-abi3-win32.whl", hash = "sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72", size = 3010220, upload-time = "2026-02-10T19:18:17.361Z" }, + { url = "https://files.pythonhosted.org/packages/48/ef/0c2f4a8e31018a986949d34a01115dd057bf536905dca38897bacd21fac3/cryptography-46.0.5-cp38-abi3-win_amd64.whl", hash = "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595", size = 3467050, upload-time = "2026-02-10T19:18:18.899Z" }, ] [[package]] @@ -352,13 +356,12 @@ wheels = [ [[package]] name = "dearpygui" -version = "2.1.1" +version = "2.2" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/79/41/2146e8d03d28b5a66d5282beb26ffd9ab68a729a29d31e2fe91809271bf5/dearpygui-2.1.1-cp312-cp312-macosx_10_6_x86_64.whl", hash = "sha256:238aea7b4be7376f564dae8edd563b280ec1483a03786022969938507691e017", size = 2101529, upload-time = "2025-11-14T14:47:39.646Z" }, - { url = "https://files.pythonhosted.org/packages/b0/c5/fcc37ef834fe225241aa4f18d77aaa2903134f283077978d65a901c624c6/dearpygui-2.1.1-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:c27ca6ecd4913555b717f3bb341c0b6a27d6c9fdc9932f0b3c31ae2ef893ae35", size = 1895555, upload-time = "2025-11-14T14:47:48.149Z" }, - { url = "https://files.pythonhosted.org/packages/74/66/19f454ba02d5f03a847cc1dfee4a849cd2307d97add5ba26fecdca318adb/dearpygui-2.1.1-cp312-cp312-manylinux1_x86_64.whl", hash = "sha256:8c071e9c165d89217bdcdaf769c6069252fcaee50bf369489add524107932273", size = 2641509, upload-time = "2025-11-14T14:47:54.581Z" }, - { url = "https://files.pythonhosted.org/packages/5e/58/d01538556103d544a5a5b4cbcb00646ff92d8a97f0a6283a56bede4307c8/dearpygui-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:9f2291313d2035f8a4108e13f60d8c1a0e7c19af7554a7739a3fd15b3d5af8f7", size = 1808971, upload-time = "2025-11-14T14:47:28.15Z" }, + { url = "https://files.pythonhosted.org/packages/17/c8/b4afdac89c7bf458513366af3143f7383d7b09721637989c95788d93e24c/dearpygui-2.2-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:34ceae1ca1b65444e49012d6851312e44f08713da1b8cc0150cf41f1c207af9c", size = 1931443, upload-time = "2026-02-17T14:21:54.394Z" }, + { url = "https://files.pythonhosted.org/packages/43/93/a2d083b2e0edb095be815662cc41e40cf9ea7b65d6323e47bb30df7eb284/dearpygui-2.2-cp312-cp312-manylinux1_x86_64.whl", hash = "sha256:e1fae9ae59fec0e41773df64c80311a6ba67696219dde5506a2a4c013e8bcdfa", size = 2592645, upload-time = "2026-02-17T14:22:02.869Z" }, + { url = "https://files.pythonhosted.org/packages/80/ba/eae13acaad479f522db853e8b1ccd695a7bc8da2b9685c1d70a3b318df89/dearpygui-2.2-cp312-cp312-win_amd64.whl", hash = "sha256:7d399543b5a26ab6426ef3bbd776e55520b491b3e169647bde5e6b2de3701b35", size = 1830531, upload-time = "2026-02-17T14:21:43.386Z" }, ] [[package]] @@ -382,7 +385,7 @@ wheels = [ [[package]] name = "ffmpeg" version = "7.1.0" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=ffmpeg&rev=releases#acf53363042d92d3206b8ce6b9da4bfb75b200c7" } +source = { git = "https://github.com/commaai/dependencies.git?subdirectory=ffmpeg&rev=releases#5fc3994af75440c9222d627d14a24550f27a1e7f" } [[package]] name = "fonttools" @@ -429,7 +432,7 @@ wheels = [ [[package]] name = "gcc-arm-none-eabi" version = "13.2.1" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=gcc-arm-none-eabi&rev=releases#acf53363042d92d3206b8ce6b9da4bfb75b200c7" } +source = { git = "https://github.com/commaai/dependencies.git?subdirectory=gcc-arm-none-eabi&rev=releases#5fc3994af75440c9222d627d14a24550f27a1e7f" } [[package]] name = "ghp-import" @@ -789,6 +792,7 @@ dependencies = [ { name = "tqdm" }, { name = "websocket-client" }, { name = "xattr" }, + { name = "zeromq" }, { name = "zstandard" }, ] @@ -872,9 +876,10 @@ requires-dist = [ { name = "spidev", marker = "sys_platform == 'linux'" }, { name = "sympy" }, { name = "tqdm" }, - { name = "ty", marker = "extra == 'testing'" }, + { name = "ty", marker = "extra == 'testing'", specifier = "==0.0.17" }, { name = "websocket-client" }, { name = "xattr" }, + { name = "zeromq", git = "https://github.com/commaai/dependencies.git?subdirectory=zeromq&rev=releases" }, { name = "zstandard" }, ] provides-extras = ["docs", "testing", "dev", "tools"] @@ -1027,22 +1032,24 @@ wheels = [ [[package]] name = "pycapnp" -version = "2.1.0" +version = "2.2.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/15/86/a57e3c92acd3e1d2fc3dcad683ada191f722e4ac927e1a384b228ec2780a/pycapnp-2.1.0.tar.gz", hash = "sha256:69cc3d861fee1c9b26c73ad2e8a5d51e76ad87e4ff9be33a4fd2fc72f5846aec", size = 689734, upload-time = "2025-09-05T03:50:40.851Z" } +sdist = { url = "https://files.pythonhosted.org/packages/85/7b/b2f356bc24220068beffc03e94062e8059a1383addb837303794398aec36/pycapnp-2.2.2.tar.gz", hash = "sha256:7f6c23c2283173a3cb6f1a5086dd0114779d508a7cd1b138d25a6357857d02b6", size = 730142, upload-time = "2026-01-21T01:22:13.73Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/74/0e/66b41ba600e5f2523e900b7cc0d2e8496b397a1f2d6a5b7b323ab83418b7/pycapnp-2.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d2ec561bc948d11f64f43bf9601bede5d6a603d105ae311bd5583c7130624a4", size = 1619223, upload-time = "2025-09-05T03:48:54.64Z" }, - { url = "https://files.pythonhosted.org/packages/40/6e/9bcb30180bd40cb0534124ff7f8ba8746a735018d593f608bf40c97821c0/pycapnp-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:132cd97f57f6b6636323ca9b68d389dd90b96e87af38cde31e2b5c5a064f277e", size = 1484321, upload-time = "2025-09-05T03:48:55.85Z" }, - { url = "https://files.pythonhosted.org/packages/14/0a/9ee1c9ecaff499e4fd1df2f0335bc20f666ec6ce5cd80f8ab055007f3c9b/pycapnp-2.1.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:568e79268ba7c02a71fe558a8aec1ae3c0f0e6aff809ff618a46afe4964957d2", size = 5143502, upload-time = "2025-09-05T03:48:57.733Z" }, - { url = "https://files.pythonhosted.org/packages/4d/50/65837e1416f7a8861ca1e8fe4582a5aef37192d7ef5e2ecfe46880bfdf9c/pycapnp-2.1.0-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:bcbf6f882d78d368c8e4bb792295392f5c4d71ddffa13a48da27e7bd47b99e37", size = 5508134, upload-time = "2025-09-05T03:48:59.383Z" }, - { url = "https://files.pythonhosted.org/packages/a1/59/46df6db800e77dbc3cc940723fb3fd7bc837327c858edf464a0f904bf547/pycapnp-2.1.0-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:dc25b96e393410dde25c61c1df3ce644700ef94826c829426d58c2c6b3e2d2f5", size = 5631794, upload-time = "2025-09-05T03:49:03.511Z" }, - { url = "https://files.pythonhosted.org/packages/63/9d/18e978500d5f6bd8d152f4d6919e3cfb83ead8a71c14613bbb54322df8b9/pycapnp-2.1.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:48938e0436ab1be615fc0a41434119a2065490a6212b9a5e56949e89b0588b76", size = 5369378, upload-time = "2025-09-05T03:49:05.539Z" }, - { url = "https://files.pythonhosted.org/packages/96/dc/726f1917e9996dc29f9fd1cf30674a14546cdbdfa0777e1982b6bd1ad628/pycapnp-2.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0c20de0f6e0b3fa9fa1df3864cf46051db3511b63bc29514d1092af65f2b82a0", size = 5999140, upload-time = "2025-09-05T03:49:07.341Z" }, - { url = "https://files.pythonhosted.org/packages/fd/3a/3bbc4c5776fc32fbf8a59df5c7c5810efd292b933cd6545eb4b16d896268/pycapnp-2.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:18caca6527862475167c10ea0809531130585aa8a86cc76cd1629eb87ee30637", size = 6454308, upload-time = "2025-09-05T03:49:08.998Z" }, - { url = "https://files.pythonhosted.org/packages/bf/dd/17e2d7808424f10ffddc47329b980488ed83ec716c504791787e593a7a93/pycapnp-2.1.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9dcc11237697007b66e3bfc500d2ad892bd79672c9b50d61fbf728c6aaf936de", size = 6544212, upload-time = "2025-09-05T03:49:10.675Z" }, - { url = "https://files.pythonhosted.org/packages/6a/5b/68090013128d7853f34c43828dd4dc80a7c8516fd1b56057b134e1e4c2c0/pycapnp-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c151edf78155b6416e7cb31e2e333d302d742ba52bb37d4dbdf71e75cc999d46", size = 6295279, upload-time = "2025-09-05T03:49:12.712Z" }, - { url = "https://files.pythonhosted.org/packages/5b/52/7d85212b4fcf127588888f71d3dbf5558ee7dc302eba760b12b1b325f9a3/pycapnp-2.1.0-cp312-cp312-win32.whl", hash = "sha256:c09b28419321dafafc644d60c57ff8ccaf3c3e686801b6060c612a7a3c580944", size = 1038995, upload-time = "2025-09-05T03:49:14.165Z" }, - { url = "https://files.pythonhosted.org/packages/f2/12/25d283ebf5c28717364647672e7494dc46196ca7a662f5420e4866f45687/pycapnp-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:560cb69cc02b0347e85b0629e4c2f0a316240900aa905392f9df6bab0a359989", size = 1176620, upload-time = "2025-09-05T03:49:15.545Z" }, + { url = "https://files.pythonhosted.org/packages/8a/76/f8f81d32ddf950e934ec144facbc112e5acbef31a63ba5be0c5f34a00fd5/pycapnp-2.2.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b86cb8ea5b8011b562c4e022325a826a62f91196ceb5aa33a766c0bea0b8fd3", size = 1605194, upload-time = "2026-01-21T01:20:29.604Z" }, + { url = "https://files.pythonhosted.org/packages/50/dd/a31be782d56a8648fef899f39aeeab867cf544a6b170871e3f4cbfc58af6/pycapnp-2.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2353531cfa669e3eeb99be9f993573341650276abec46676d687cc12b3e6b6d9", size = 1486613, upload-time = "2026-01-21T01:20:31.415Z" }, + { url = "https://files.pythonhosted.org/packages/aa/bf/8da830dda94eb7327c6508d6c26fbd964897d742f8c1c0ec48623f0c515b/pycapnp-2.2.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:ee27bdc78c7ccd8eaa0fe31e09f0ec4ef31deda3f475fc9373bb4b0de8083053", size = 5186701, upload-time = "2026-01-21T01:20:32.836Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a1/13d0baa2f337f4f6fe8c2142646ba437a26b9c433f5d7ce016a912bad052/pycapnp-2.2.2-cp312-cp312-manylinux_2_28_i686.whl", hash = "sha256:a8ded808911d1d7a9a2197626c09eea6e269e74dc1276760789538b1efcf6cd5", size = 5239464, upload-time = "2026-01-21T01:20:34.793Z" }, + { url = "https://files.pythonhosted.org/packages/82/76/0451c64b5f0132e4b75a0afe8cec957c8bf8fa981264a7c0b264cb94663e/pycapnp-2.2.2-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:59e92e1db40041d82a95eab0bd8de2676ce50c6b97c1457e2edde4d134b6d046", size = 5542887, upload-time = "2026-01-21T01:20:36.463Z" }, + { url = "https://files.pythonhosted.org/packages/04/00/d025d68d9a5330d55cbe2d018091cacfef0835c3ad422eb6778c4525041f/pycapnp-2.2.2-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:ee1e9ac2f0b80fa892b922b60e36efc925d072ecf1204ba3e59d8d9ac7c3dc83", size = 5659696, upload-time = "2026-01-21T01:20:38.069Z" }, + { url = "https://files.pythonhosted.org/packages/58/b7/28f7c539a5f4cbaa12e55ec27d081d11473464230f2e801e4714606d3453/pycapnp-2.2.2-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:53273b385be78ed8ac997ff8697f2a4c760e93c190b509822a937de5531f4861", size = 5413827, upload-time = "2026-01-21T01:20:39.781Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a7/83bc13d90675f0cee8a38d4ad8401bb2f8662c543b3a6622aeffb7b56b1e/pycapnp-2.2.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:812cbdd002bc542b63f969b85c6b9041dfdaf4185613635a6d4feea84c9092fa", size = 6046815, upload-time = "2026-01-21T01:20:42.172Z" }, + { url = "https://files.pythonhosted.org/packages/0d/8a/80f46baa1684bbcc4754ce22c5a44693a1209a64de6df2b256b85b8b8a97/pycapnp-2.2.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9c330218a44bd649b96f565dbf5326d183fdd20f9887bdedfeabd73f0366c2e1", size = 6367625, upload-time = "2026-01-21T01:20:44.004Z" }, + { url = "https://files.pythonhosted.org/packages/02/00/60e82eaf6b4e78d887157bf9f18234c852771cc575355e63d1114c4a5d79/pycapnp-2.2.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:796aa0ba18bcd4e6b2815471bbed059ad7ee8a815a30e81ac8a9aa030ec7818d", size = 6487265, upload-time = "2026-01-21T01:20:46.137Z" }, + { url = "https://files.pythonhosted.org/packages/57/6e/2dedd8f95dc22357c50a775ee2b8711b3d711f30344d244141e0e1962c3e/pycapnp-2.2.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:251a6abdd64b9b11d2a8e16fc365b922ef6ba6c968959b72a3a3d9d8ec8cc8d7", size = 6576699, upload-time = "2026-01-21T01:20:47.987Z" }, + { url = "https://files.pythonhosted.org/packages/2f/53/f7f69ed1d11ea30ea4f0f6d8319fbc18bc8781c480c118005e0a394492a7/pycapnp-2.2.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6aab811e0fcc27ae8bf5f04dedaa7e0af47e0d4db51d9c85ab0d2dad26a46bd7", size = 6344114, upload-time = "2026-01-21T01:20:50.367Z" }, + { url = "https://files.pythonhosted.org/packages/ab/78/ab78ee42797ff44c7e1fc0d1aa9396c6742cb05ff01a7cdf9c8f19e0defe/pycapnp-2.2.2-cp312-cp312-win32.whl", hash = "sha256:5061c85dd8f843b2656720ca6976d2a9b418845580c6f6d9602f7119fc2208d5", size = 1047207, upload-time = "2026-01-21T01:20:51.842Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fb/6edf56d5144c476270fa8b2e6a660ef5a188fb0097193e342618fbcb0210/pycapnp-2.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:700eb8c77405222903af3fb5a371c0d766f86139c3d51f4bff41ccd6403b51f9", size = 1185178, upload-time = "2026-01-21T01:20:53.429Z" }, ] [[package]] @@ -1127,14 +1134,15 @@ wheels = [ [[package]] name = "pyopenssl" -version = "24.2.1" +version = "25.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5d/70/ff56a63248562e77c0c8ee4aefc3224258f1856977e0c1472672b62dadb8/pyopenssl-24.2.1.tar.gz", hash = "sha256:4247f0dbe3748d560dcbb2ff3ea01af0f9a1a001ef5f7c4c647956ed8cbf0e95", size = 184323, upload-time = "2024-07-20T17:26:31.252Z" } +sdist = { url = "https://files.pythonhosted.org/packages/80/be/97b83a464498a79103036bc74d1038df4a7ef0e402cfaf4d5e113fb14759/pyopenssl-25.3.0.tar.gz", hash = "sha256:c981cb0a3fd84e8602d7afc209522773b94c1c2446a3c710a75b06fe1beae329", size = 184073, upload-time = "2025-09-17T00:32:21.037Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/dd/e0aa7ebef5168c75b772eda64978c597a9129b46be17779054652a7999e4/pyOpenSSL-24.2.1-py3-none-any.whl", hash = "sha256:967d5719b12b243588573f39b0c677637145c7a1ffedcd495a487e58177fbb8d", size = 58390, upload-time = "2024-07-20T17:26:29.057Z" }, + { url = "https://files.pythonhosted.org/packages/d1/81/ef2b1dfd1862567d573a4fdbc9f969067621764fbb74338496840a1d2977/pyopenssl-25.3.0-py3-none-any.whl", hash = "sha256:1fda6fc034d5e3d179d39e59c1895c9faeaf40a79de5fc4cbbfbe0d36f4a77b6", size = 57268, upload-time = "2025-09-17T00:32:19.474Z" }, ] [[package]] @@ -1349,27 +1357,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.15.1" +version = "0.15.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/04/dc/4e6ac71b511b141cf626357a3946679abeba4cf67bc7cc5a17920f31e10d/ruff-0.15.1.tar.gz", hash = "sha256:c590fe13fb57c97141ae975c03a1aedb3d3156030cabd740d6ff0b0d601e203f", size = 4540855, upload-time = "2026-02-12T23:09:09.998Z" } +sdist = { url = "https://files.pythonhosted.org/packages/06/04/eab13a954e763b0606f460443fcbf6bb5a0faf06890ea3754ff16523dce5/ruff-0.15.2.tar.gz", hash = "sha256:14b965afee0969e68bb871eba625343b8673375f457af4abe98553e8bbb98342", size = 4558148, upload-time = "2026-02-19T22:32:20.271Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/23/bf/e6e4324238c17f9d9120a9d60aa99a7daaa21204c07fcd84e2ef03bb5fd1/ruff-0.15.1-py3-none-linux_armv6l.whl", hash = "sha256:b101ed7cf4615bda6ffe65bdb59f964e9f4a0d3f85cbf0e54f0ab76d7b90228a", size = 10367819, upload-time = "2026-02-12T23:09:03.598Z" }, - { url = "https://files.pythonhosted.org/packages/b3/ea/c8f89d32e7912269d38c58f3649e453ac32c528f93bb7f4219258be2e7ed/ruff-0.15.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:939c995e9277e63ea632cc8d3fae17aa758526f49a9a850d2e7e758bfef46602", size = 10798618, upload-time = "2026-02-12T23:09:22.928Z" }, - { url = "https://files.pythonhosted.org/packages/5e/0f/1d0d88bc862624247d82c20c10d4c0f6bb2f346559d8af281674cf327f15/ruff-0.15.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1d83466455fdefe60b8d9c8df81d3c1bbb2115cede53549d3b522ce2bc703899", size = 10148518, upload-time = "2026-02-12T23:08:58.339Z" }, - { url = "https://files.pythonhosted.org/packages/f5/c8/291c49cefaa4a9248e986256df2ade7add79388fe179e0691be06fae6f37/ruff-0.15.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9457e3c3291024866222b96108ab2d8265b477e5b1534c7ddb1810904858d16", size = 10518811, upload-time = "2026-02-12T23:09:31.865Z" }, - { url = "https://files.pythonhosted.org/packages/c3/1a/f5707440e5ae43ffa5365cac8bbb91e9665f4a883f560893829cf16a606b/ruff-0.15.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:92c92b003e9d4f7fbd33b1867bb15a1b785b1735069108dfc23821ba045b29bc", size = 10196169, upload-time = "2026-02-12T23:09:17.306Z" }, - { url = "https://files.pythonhosted.org/packages/2a/ff/26ddc8c4da04c8fd3ee65a89c9fb99eaa5c30394269d424461467be2271f/ruff-0.15.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fe5c41ab43e3a06778844c586251eb5a510f67125427625f9eb2b9526535779", size = 10990491, upload-time = "2026-02-12T23:09:25.503Z" }, - { url = "https://files.pythonhosted.org/packages/fc/00/50920cb385b89413f7cdb4bb9bc8fc59c1b0f30028d8bccc294189a54955/ruff-0.15.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66a6dd6df4d80dc382c6484f8ce1bcceb55c32e9f27a8b94c32f6c7331bf14fb", size = 11843280, upload-time = "2026-02-12T23:09:19.88Z" }, - { url = "https://files.pythonhosted.org/packages/5d/6d/2f5cad8380caf5632a15460c323ae326f1e1a2b5b90a6ee7519017a017ca/ruff-0.15.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a4a42cbb8af0bda9bcd7606b064d7c0bc311a88d141d02f78920be6acb5aa83", size = 11274336, upload-time = "2026-02-12T23:09:14.907Z" }, - { url = "https://files.pythonhosted.org/packages/a3/1d/5f56cae1d6c40b8a318513599b35ea4b075d7dc1cd1d04449578c29d1d75/ruff-0.15.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ab064052c31dddada35079901592dfba2e05f5b1e43af3954aafcbc1096a5b2", size = 11137288, upload-time = "2026-02-12T23:09:07.475Z" }, - { url = "https://files.pythonhosted.org/packages/cd/20/6f8d7d8f768c93b0382b33b9306b3b999918816da46537d5a61635514635/ruff-0.15.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:5631c940fe9fe91f817a4c2ea4e81f47bee3ca4aa646134a24374f3c19ad9454", size = 11070681, upload-time = "2026-02-12T23:08:55.43Z" }, - { url = "https://files.pythonhosted.org/packages/9a/67/d640ac76069f64cdea59dba02af2e00b1fa30e2103c7f8d049c0cff4cafd/ruff-0.15.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:68138a4ba184b4691ccdc39f7795c66b3c68160c586519e7e8444cf5a53e1b4c", size = 10486401, upload-time = "2026-02-12T23:09:27.927Z" }, - { url = "https://files.pythonhosted.org/packages/65/3d/e1429f64a3ff89297497916b88c32a5cc88eeca7e9c787072d0e7f1d3e1e/ruff-0.15.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:518f9af03bfc33c03bdb4cb63fabc935341bb7f54af500f92ac309ecfbba6330", size = 10197452, upload-time = "2026-02-12T23:09:12.147Z" }, - { url = "https://files.pythonhosted.org/packages/78/83/e2c3bade17dad63bf1e1c2ffaf11490603b760be149e1419b07049b36ef2/ruff-0.15.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:da79f4d6a826caaea95de0237a67e33b81e6ec2e25fc7e1993a4015dffca7c61", size = 10693900, upload-time = "2026-02-12T23:09:34.418Z" }, - { url = "https://files.pythonhosted.org/packages/a1/27/fdc0e11a813e6338e0706e8b39bb7a1d61ea5b36873b351acee7e524a72a/ruff-0.15.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3dd86dccb83cd7d4dcfac303ffc277e6048600dfc22e38158afa208e8bf94a1f", size = 11227302, upload-time = "2026-02-12T23:09:36.536Z" }, - { url = "https://files.pythonhosted.org/packages/f6/58/ac864a75067dcbd3b95be5ab4eb2b601d7fbc3d3d736a27e391a4f92a5c1/ruff-0.15.1-py3-none-win32.whl", hash = "sha256:660975d9cb49b5d5278b12b03bb9951d554543a90b74ed5d366b20e2c57c2098", size = 10462555, upload-time = "2026-02-12T23:09:29.899Z" }, - { url = "https://files.pythonhosted.org/packages/e0/5e/d4ccc8a27ecdb78116feac4935dfc39d1304536f4296168f91ed3ec00cd2/ruff-0.15.1-py3-none-win_amd64.whl", hash = "sha256:c820fef9dd5d4172a6570e5721704a96c6679b80cf7be41659ed439653f62336", size = 11599956, upload-time = "2026-02-12T23:09:01.157Z" }, - { url = "https://files.pythonhosted.org/packages/2a/07/5bda6a85b220c64c65686bc85bd0bbb23b29c62b3a9f9433fa55f17cda93/ruff-0.15.1-py3-none-win_arm64.whl", hash = "sha256:5ff7d5f0f88567850f45081fac8f4ec212be8d0b963e385c3f7d0d2eb4899416", size = 10874604, upload-time = "2026-02-12T23:09:05.515Z" }, + { url = "https://files.pythonhosted.org/packages/2f/70/3a4dc6d09b13cb3e695f28307e5d889b2e1a66b7af9c5e257e796695b0e6/ruff-0.15.2-py3-none-linux_armv6l.whl", hash = "sha256:120691a6fdae2f16d65435648160f5b81a9625288f75544dc40637436b5d3c0d", size = 10430565, upload-time = "2026-02-19T22:32:41.824Z" }, + { url = "https://files.pythonhosted.org/packages/71/0b/bb8457b56185ece1305c666dc895832946d24055be90692381c31d57466d/ruff-0.15.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a89056d831256099658b6bba4037ac6dd06f49d194199215befe2bb10457ea5e", size = 10820354, upload-time = "2026-02-19T22:32:07.366Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c1/e0532d7f9c9e0b14c46f61b14afd563298b8b83f337b6789ddd987e46121/ruff-0.15.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e36dee3a64be0ebd23c86ffa3aa3fd3ac9a712ff295e192243f814a830b6bd87", size = 10170767, upload-time = "2026-02-19T22:32:13.188Z" }, + { url = "https://files.pythonhosted.org/packages/47/e8/da1aa341d3af017a21c7a62fb5ec31d4e7ad0a93ab80e3a508316efbcb23/ruff-0.15.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9fb47b6d9764677f8c0a193c0943ce9a05d6763523f132325af8a858eadc2b9", size = 10529591, upload-time = "2026-02-19T22:32:02.547Z" }, + { url = "https://files.pythonhosted.org/packages/93/74/184fbf38e9f3510231fbc5e437e808f0b48c42d1df9434b208821efcd8d6/ruff-0.15.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f376990f9d0d6442ea9014b19621d8f2aaf2b8e39fdbfc79220b7f0c596c9b80", size = 10260771, upload-time = "2026-02-19T22:32:36.938Z" }, + { url = "https://files.pythonhosted.org/packages/05/ac/605c20b8e059a0bc4b42360414baa4892ff278cec1c91fff4be0dceedefd/ruff-0.15.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2dcc987551952d73cbf5c88d9fdee815618d497e4df86cd4c4824cc59d5dd75f", size = 11045791, upload-time = "2026-02-19T22:32:31.642Z" }, + { url = "https://files.pythonhosted.org/packages/fd/52/db6e419908f45a894924d410ac77d64bdd98ff86901d833364251bd08e22/ruff-0.15.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:42a47fd785cbe8c01b9ff45031af875d101b040ad8f4de7bbb716487c74c9a77", size = 11879271, upload-time = "2026-02-19T22:32:29.305Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d8/7992b18f2008bdc9231d0f10b16df7dda964dbf639e2b8b4c1b4e91b83af/ruff-0.15.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cbe9f49354866e575b4c6943856989f966421870e85cd2ac94dccb0a9dcb2fea", size = 11303707, upload-time = "2026-02-19T22:32:22.492Z" }, + { url = "https://files.pythonhosted.org/packages/d7/02/849b46184bcfdd4b64cde61752cc9a146c54759ed036edd11857e9b8443b/ruff-0.15.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7a672c82b5f9887576087d97be5ce439f04bbaf548ee987b92d3a7dede41d3a", size = 11149151, upload-time = "2026-02-19T22:32:44.234Z" }, + { url = "https://files.pythonhosted.org/packages/70/04/f5284e388bab60d1d3b99614a5a9aeb03e0f333847e2429bebd2aaa1feec/ruff-0.15.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:72ecc64f46f7019e2bcc3cdc05d4a7da958b629a5ab7033195e11a438403d956", size = 11091132, upload-time = "2026-02-19T22:32:24.691Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ae/88d844a21110e14d92cf73d57363fab59b727ebeabe78009b9ccb23500af/ruff-0.15.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:8dcf243b15b561c655c1ef2f2b0050e5d50db37fe90115507f6ff37d865dc8b4", size = 10504717, upload-time = "2026-02-19T22:32:26.75Z" }, + { url = "https://files.pythonhosted.org/packages/64/27/867076a6ada7f2b9c8292884ab44d08fd2ba71bd2b5364d4136f3cd537e1/ruff-0.15.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dab6941c862c05739774677c6273166d2510d254dac0695c0e3f5efa1b5585de", size = 10263122, upload-time = "2026-02-19T22:32:10.036Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ef/faf9321d550f8ebf0c6373696e70d1758e20ccdc3951ad7af00c0956be7c/ruff-0.15.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1b9164f57fc36058e9a6806eb92af185b0697c9fe4c7c52caa431c6554521e5c", size = 10735295, upload-time = "2026-02-19T22:32:39.227Z" }, + { url = "https://files.pythonhosted.org/packages/2f/55/e8089fec62e050ba84d71b70e7834b97709ca9b7aba10c1a0b196e493f97/ruff-0.15.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:80d24fcae24d42659db7e335b9e1531697a7102c19185b8dc4a028b952865fd8", size = 11241641, upload-time = "2026-02-19T22:32:34.617Z" }, + { url = "https://files.pythonhosted.org/packages/23/01/1c30526460f4d23222d0fabd5888868262fd0e2b71a00570ca26483cd993/ruff-0.15.2-py3-none-win32.whl", hash = "sha256:fd5ff9e5f519a7e1bd99cbe8daa324010a74f5e2ebc97c6242c08f26f3714f6f", size = 10507885, upload-time = "2026-02-19T22:32:15.635Z" }, + { url = "https://files.pythonhosted.org/packages/5c/10/3d18e3bbdf8fc50bbb4ac3cc45970aa5a9753c5cb51bf9ed9a3cd8b79fa3/ruff-0.15.2-py3-none-win_amd64.whl", hash = "sha256:d20014e3dfa400f3ff84830dfb5755ece2de45ab62ecea4af6b7262d0fb4f7c5", size = 11623725, upload-time = "2026-02-19T22:32:04.947Z" }, + { url = "https://files.pythonhosted.org/packages/6d/78/097c0798b1dab9f8affe73da9642bb4500e098cb27fd8dc9724816ac747b/ruff-0.15.2-py3-none-win_arm64.whl", hash = "sha256:cabddc5822acdc8f7b5527b36ceac55cc51eec7b1946e60181de8fe83ca8876e", size = 10941649, upload-time = "2026-02-19T22:32:18.108Z" }, ] [[package]] @@ -1605,6 +1613,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/73/ae/b48f95715333080afb75a4504487cbe142cae1268afc482d06692d605ae6/yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", size = 46814, upload-time = "2025-10-06T14:12:53.872Z" }, ] +[[package]] +name = "zeromq" +version = "4.3.5" +source = { git = "https://github.com/commaai/dependencies.git?subdirectory=zeromq&rev=releases#5fc3994af75440c9222d627d14a24550f27a1e7f" } + [[package]] name = "zstandard" version = "0.25.0" From 08b76d3de66458ba7bd74645b669c8fa70bd334a Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sun, 22 Feb 2026 20:14:12 -0800 Subject: [PATCH 155/311] Use built-in clang on macOS (#37335) * rm extra LLVM install on macOS * update that * rm brew cache * no cache * Revert "no cache" This reverts commit a3f8eff234935d4bb27d4bd785ad8a710496a159. --- .github/workflows/tests.yaml | 8 -------- SConstruct | 1 - tools/mac_setup.sh | 1 - 3 files changed, 10 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 40dfaaa801..79520105a4 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -67,14 +67,6 @@ jobs: with: submodules: true - run: echo "CACHE_COMMIT_DATE=$(git log -1 --pretty='format:%cd' --date=format:'%Y-%m-%d-%H:%M')" >> $GITHUB_ENV - - name: Homebrew cache - uses: ./.github/workflows/auto-cache - with: - path: ~/Library/Caches/Homebrew - key: brew-macos-${{ env.CACHE_COMMIT_DATE }}-${{ github.sha }} - restore-keys: | - brew-macos-${{ env.CACHE_COMMIT_DATE }} - brew-macos - name: Install dependencies run: ./tools/mac_setup.sh env: diff --git a/SConstruct b/SConstruct index 148c119066..5f450e6b57 100644 --- a/SConstruct +++ b/SConstruct @@ -117,7 +117,6 @@ elif arch == "Darwin": env.Append(LIBPATH=[ f"{brew_prefix}/lib", f"{brew_prefix}/opt/openssl@3.0/lib", - f"{brew_prefix}/opt/llvm/lib/c++", "/System/Library/Frameworks/OpenGL.framework/Libraries", ]) env.Append(CCFLAGS=["-DGL_SILENCE_DEPRECATION"]) diff --git a/tools/mac_setup.sh b/tools/mac_setup.sh index c4eee0f67d..1ab9ca02e7 100755 --- a/tools/mac_setup.sh +++ b/tools/mac_setup.sh @@ -30,7 +30,6 @@ brew bundle --file=- <<-EOS brew "git-lfs" brew "coreutils" brew "eigen" -brew "llvm" EOS echo "[ ] finished brew install t=$SECONDS" From 0738c05d9f7ea910747b6f56dc290da922c7bd70 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sun, 22 Feb 2026 21:29:23 -0800 Subject: [PATCH 156/311] vendored git-lfs (#37338) * use vendored zeromq from dependencies repo Co-Authored-By: Claude Opus 4.6 * lock * rm more crap * use vendored git-lfs from dependencies repo Co-Authored-By: Claude Opus 4.6 * from releases --------- Co-authored-by: Claude Opus 4.6 --- pyproject.toml | 1 + tools/install_ubuntu_dependencies.sh | 1 - tools/mac_setup.sh | 1 - uv.lock | 15 +++++++++++---- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 71e40e3914..3697b004c7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,6 +29,7 @@ dependencies = [ "capnproto @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=capnproto", "ffmpeg @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=ffmpeg", "zeromq @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=zeromq", + "git-lfs @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=git-lfs", # body / webrtcd "av", diff --git a/tools/install_ubuntu_dependencies.sh b/tools/install_ubuntu_dependencies.sh index 7d24b6c09a..d14a2a5350 100755 --- a/tools/install_ubuntu_dependencies.sh +++ b/tools/install_ubuntu_dependencies.sh @@ -31,7 +31,6 @@ function install_ubuntu_common_requirements() { libcurl4-openssl-dev \ locales \ git \ - git-lfs \ xvfb $SUDO apt-get install -y --no-install-recommends \ diff --git a/tools/mac_setup.sh b/tools/mac_setup.sh index 1ab9ca02e7..f9f7732051 100755 --- a/tools/mac_setup.sh +++ b/tools/mac_setup.sh @@ -27,7 +27,6 @@ else fi brew bundle --file=- <<-EOS -brew "git-lfs" brew "coreutils" brew "eigen" EOS diff --git a/uv.lock b/uv.lock index 3327ba4dbe..9432df071c 100644 --- a/uv.lock +++ b/uv.lock @@ -116,7 +116,7 @@ wheels = [ [[package]] name = "capnproto" version = "1.0.1" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=capnproto&rev=releases#5fc3994af75440c9222d627d14a24550f27a1e7f" } +source = { git = "https://github.com/commaai/dependencies.git?subdirectory=capnproto&rev=releases#d9eaf5b01ed0b0f036a0463e13d6e101352ffdd6" } [[package]] name = "casadi" @@ -385,7 +385,7 @@ wheels = [ [[package]] name = "ffmpeg" version = "7.1.0" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=ffmpeg&rev=releases#5fc3994af75440c9222d627d14a24550f27a1e7f" } +source = { git = "https://github.com/commaai/dependencies.git?subdirectory=ffmpeg&rev=releases#d9eaf5b01ed0b0f036a0463e13d6e101352ffdd6" } [[package]] name = "fonttools" @@ -432,7 +432,7 @@ wheels = [ [[package]] name = "gcc-arm-none-eabi" version = "13.2.1" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=gcc-arm-none-eabi&rev=releases#5fc3994af75440c9222d627d14a24550f27a1e7f" } +source = { git = "https://github.com/commaai/dependencies.git?subdirectory=gcc-arm-none-eabi&rev=releases#d9eaf5b01ed0b0f036a0463e13d6e101352ffdd6" } [[package]] name = "ghp-import" @@ -446,6 +446,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, ] +[[package]] +name = "git-lfs" +version = "3.6.1" +source = { git = "https://github.com/commaai/dependencies.git?subdirectory=git-lfs&rev=releases#d9eaf5b01ed0b0f036a0463e13d6e101352ffdd6" } + [[package]] name = "google-crc32c" version = "1.8.0" @@ -768,6 +773,7 @@ dependencies = [ { name = "crcmod-plus" }, { name = "cython" }, { name = "ffmpeg" }, + { name = "git-lfs" }, { name = "inputs" }, { name = "jeepney" }, { name = "json-rpc" }, @@ -840,6 +846,7 @@ requires-dist = [ { name = "dearpygui", marker = "(platform_machine != 'aarch64' and extra == 'tools') or (sys_platform != 'linux' and extra == 'tools')", specifier = ">=2.1.0" }, { name = "ffmpeg", git = "https://github.com/commaai/dependencies.git?subdirectory=ffmpeg&rev=releases" }, { name = "gcc-arm-none-eabi", marker = "extra == 'dev'", git = "https://github.com/commaai/dependencies.git?subdirectory=gcc-arm-none-eabi&rev=releases" }, + { name = "git-lfs", git = "https://github.com/commaai/dependencies.git?subdirectory=git-lfs&rev=releases" }, { name = "hypothesis", marker = "extra == 'testing'", specifier = "==6.47.*" }, { name = "inputs" }, { name = "jeepney" }, @@ -1616,7 +1623,7 @@ wheels = [ [[package]] name = "zeromq" version = "4.3.5" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=zeromq&rev=releases#5fc3994af75440c9222d627d14a24550f27a1e7f" } +source = { git = "https://github.com/commaai/dependencies.git?subdirectory=zeromq&rev=releases#d9eaf5b01ed0b0f036a0463e13d6e101352ffdd6" } [[package]] name = "zstandard" From f96406b13f7a55e6187b895a1c2610e538ece8de Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sun, 22 Feb 2026 21:48:00 -0800 Subject: [PATCH 157/311] use vendored eigen from dependencies repo (#37339) * use vendored eigen from dependencies repo Co-Authored-By: Claude Opus 4.6 * lock --------- Co-authored-by: Claude Opus 4.6 --- SConstruct | 3 ++- pyproject.toml | 1 + tools/install_ubuntu_dependencies.sh | 1 - tools/mac_setup.sh | 1 - uv.lock | 17 ++++++++++++----- 5 files changed, 15 insertions(+), 8 deletions(-) diff --git a/SConstruct b/SConstruct index 5f450e6b57..cb1c97d158 100644 --- a/SConstruct +++ b/SConstruct @@ -40,9 +40,10 @@ assert arch in [ if arch != "larch64": import capnproto + import eigen import ffmpeg as ffmpeg_pkg import zeromq - pkgs = [capnproto, ffmpeg_pkg, zeromq] + pkgs = [capnproto, eigen, ffmpeg_pkg, zeromq] else: # TODO: remove when AGNOS has our new vendor pkgs pkgs = [] diff --git a/pyproject.toml b/pyproject.toml index 3697b004c7..03ab5902ff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,6 +27,7 @@ dependencies = [ # vendored native dependencies "capnproto @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=capnproto", + "eigen @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=eigen", "ffmpeg @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=ffmpeg", "zeromq @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=zeromq", "git-lfs @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=git-lfs", diff --git a/tools/install_ubuntu_dependencies.sh b/tools/install_ubuntu_dependencies.sh index d14a2a5350..effa68b9ca 100755 --- a/tools/install_ubuntu_dependencies.sh +++ b/tools/install_ubuntu_dependencies.sh @@ -35,7 +35,6 @@ function install_ubuntu_common_requirements() { $SUDO apt-get install -y --no-install-recommends \ libbz2-dev \ - libeigen3-dev \ libgles2-mesa-dev \ libjpeg-dev \ libncurses5-dev \ diff --git a/tools/mac_setup.sh b/tools/mac_setup.sh index f9f7732051..1a8a17bfeb 100755 --- a/tools/mac_setup.sh +++ b/tools/mac_setup.sh @@ -28,7 +28,6 @@ fi brew bundle --file=- <<-EOS brew "coreutils" -brew "eigen" EOS echo "[ ] finished brew install t=$SECONDS" diff --git a/uv.lock b/uv.lock index 9432df071c..33c210a90f 100644 --- a/uv.lock +++ b/uv.lock @@ -116,7 +116,7 @@ wheels = [ [[package]] name = "capnproto" version = "1.0.1" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=capnproto&rev=releases#d9eaf5b01ed0b0f036a0463e13d6e101352ffdd6" } +source = { git = "https://github.com/commaai/dependencies.git?subdirectory=capnproto&rev=releases#96208e8726374ab5229366102a17401edb68076c" } [[package]] name = "casadi" @@ -373,6 +373,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, ] +[[package]] +name = "eigen" +version = "3.4.0" +source = { git = "https://github.com/commaai/dependencies.git?subdirectory=eigen&rev=releases#96208e8726374ab5229366102a17401edb68076c" } + [[package]] name = "execnet" version = "2.1.2" @@ -385,7 +390,7 @@ wheels = [ [[package]] name = "ffmpeg" version = "7.1.0" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=ffmpeg&rev=releases#d9eaf5b01ed0b0f036a0463e13d6e101352ffdd6" } +source = { git = "https://github.com/commaai/dependencies.git?subdirectory=ffmpeg&rev=releases#96208e8726374ab5229366102a17401edb68076c" } [[package]] name = "fonttools" @@ -432,7 +437,7 @@ wheels = [ [[package]] name = "gcc-arm-none-eabi" version = "13.2.1" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=gcc-arm-none-eabi&rev=releases#d9eaf5b01ed0b0f036a0463e13d6e101352ffdd6" } +source = { git = "https://github.com/commaai/dependencies.git?subdirectory=gcc-arm-none-eabi&rev=releases#96208e8726374ab5229366102a17401edb68076c" } [[package]] name = "ghp-import" @@ -449,7 +454,7 @@ wheels = [ [[package]] name = "git-lfs" version = "3.6.1" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=git-lfs&rev=releases#d9eaf5b01ed0b0f036a0463e13d6e101352ffdd6" } +source = { git = "https://github.com/commaai/dependencies.git?subdirectory=git-lfs&rev=releases#96208e8726374ab5229366102a17401edb68076c" } [[package]] name = "google-crc32c" @@ -772,6 +777,7 @@ dependencies = [ { name = "cffi" }, { name = "crcmod-plus" }, { name = "cython" }, + { name = "eigen" }, { name = "ffmpeg" }, { name = "git-lfs" }, { name = "inputs" }, @@ -844,6 +850,7 @@ requires-dist = [ { name = "crcmod-plus" }, { name = "cython" }, { name = "dearpygui", marker = "(platform_machine != 'aarch64' and extra == 'tools') or (sys_platform != 'linux' and extra == 'tools')", specifier = ">=2.1.0" }, + { name = "eigen", git = "https://github.com/commaai/dependencies.git?subdirectory=eigen&rev=releases" }, { name = "ffmpeg", git = "https://github.com/commaai/dependencies.git?subdirectory=ffmpeg&rev=releases" }, { name = "gcc-arm-none-eabi", marker = "extra == 'dev'", git = "https://github.com/commaai/dependencies.git?subdirectory=gcc-arm-none-eabi&rev=releases" }, { name = "git-lfs", git = "https://github.com/commaai/dependencies.git?subdirectory=git-lfs&rev=releases" }, @@ -1623,7 +1630,7 @@ wheels = [ [[package]] name = "zeromq" version = "4.3.5" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=zeromq&rev=releases#d9eaf5b01ed0b0f036a0463e13d6e101352ffdd6" } +source = { git = "https://github.com/commaai/dependencies.git?subdirectory=zeromq&rev=releases#96208e8726374ab5229366102a17401edb68076c" } [[package]] name = "zstandard" From ca058bcc8182de7dc70d4918f2def39e54ae9775 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sun, 22 Feb 2026 21:52:11 -0800 Subject: [PATCH 158/311] bye bye brew (#37340) * bye bye brew * drop the nproc it's simpler --- .github/workflows/tests.yaml | 2 +- tools/mac_setup.sh | 22 ---------------------- 2 files changed, 1 insertion(+), 23 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 79520105a4..92cdd23768 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -81,7 +81,7 @@ jobs: scons-${{ runner.arch }}-macos-${{ env.CACHE_COMMIT_DATE }} scons-${{ runner.arch }}-macos - name: Building openpilot - run: . .venv/bin/activate && scons -j$(nproc) + run: . .venv/bin/activate && scons static_analysis: name: static analysis diff --git a/tools/mac_setup.sh b/tools/mac_setup.sh index 1a8a17bfeb..9607f70cf5 100755 --- a/tools/mac_setup.sh +++ b/tools/mac_setup.sh @@ -4,34 +4,12 @@ set -e DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" ROOT="$(cd $DIR/../ && pwd)" -# homebrew update is slow -export HOMEBREW_NO_AUTO_UPDATE=1 - if [[ $SHELL == "/bin/zsh" ]]; then RC_FILE="$HOME/.zshrc" elif [[ $SHELL == "/bin/bash" ]]; then RC_FILE="$HOME/.bash_profile" fi -# Install brew if required -if [[ $(command -v brew) == "" ]]; then - echo "Installing Homebrew" - /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" - echo "[ ] installed brew t=$SECONDS" - - # make brew available now - echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> $RC_FILE - eval "$(/opt/homebrew/bin/brew shellenv)" -else - brew up -fi - -brew bundle --file=- <<-EOS -brew "coreutils" -EOS - -echo "[ ] finished brew install t=$SECONDS" - # install python dependencies $DIR/install_python_dependencies.sh echo "[ ] installed python dependencies t=$SECONDS" From 2a0ac63fa578780daf88d2dfcb1dba9cd22f2199 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sun, 22 Feb 2026 22:17:15 -0800 Subject: [PATCH 159/311] remove libbz2 from ubuntu setup (#37342) --- tools/install_ubuntu_dependencies.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/tools/install_ubuntu_dependencies.sh b/tools/install_ubuntu_dependencies.sh index effa68b9ca..4a1cf9a8cb 100755 --- a/tools/install_ubuntu_dependencies.sh +++ b/tools/install_ubuntu_dependencies.sh @@ -34,7 +34,6 @@ function install_ubuntu_common_requirements() { xvfb $SUDO apt-get install -y --no-install-recommends \ - libbz2-dev \ libgles2-mesa-dev \ libjpeg-dev \ libncurses5-dev \ From 35e38f5fe4cbab410067f05aaf0b3be2ca26c83c Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Mon, 23 Feb 2026 01:21:40 -0800 Subject: [PATCH 160/311] mici ui: show lock in network panel (#37345) * disable forget for tethering * nets * put in wifiman * batch * revert * draw --- .../mici/layouts/settings/network/__init__.py | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/selfdrive/ui/mici/layouts/settings/network/__init__.py b/selfdrive/ui/mici/layouts/settings/network/__init__.py index ad4e43a1ac..299df5fea0 100644 --- a/selfdrive/ui/mici/layouts/settings/network/__init__.py +++ b/selfdrive/ui/mici/layouts/settings/network/__init__.py @@ -8,7 +8,27 @@ from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.selfdrive.ui.lib.prime_state import PrimeType from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.widgets.nav_widget import NavWidget -from openpilot.system.ui.lib.wifi_manager import WifiManager, Network, MeteredType, ConnectStatus, normalize_ssid +from openpilot.system.ui.lib.wifi_manager import WifiManager, Network, MeteredType, ConnectStatus, SecurityType, normalize_ssid + + +class WifiNetworkButton(BigButton): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._lock_txt = gui_app.texture("icons_mici/settings/network/new/lock.png", 28, 36) + self._draw_lock = False + + def set_draw_lock(self, draw: bool): + self._draw_lock = draw + + def _draw_content(self, btn_y: float): + super()._draw_content(btn_y) + # Render lock icon at lower right of wifi icon if secured + if self._draw_lock: + icon_x = self._rect.x + self._rect.width - 30 - self._txt_icon.width + icon_y = btn_y + 30 + lock_x = icon_x + self._txt_icon.width - self._lock_txt.width + 7 + lock_y = icon_y + self._txt_icon.height - self._lock_txt.height + 8 + rl.draw_texture_ex(self._lock_txt, (lock_x, lock_y), 0.0, 1.0, rl.WHITE) class NetworkLayoutMici(NavWidget): @@ -68,7 +88,7 @@ class NetworkLayoutMici(NavWidget): self._wifi_medium_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_medium.png", 64, 47) self._wifi_full_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_full.png", 64, 47) - self._wifi_button = BigButton("wi-fi", "not connected", self._wifi_slash_txt, scroll=True) + self._wifi_button = WifiNetworkButton("wi-fi", "not connected", self._wifi_slash_txt, scroll=True) self._wifi_button.set_click_callback(lambda: gui_app.push_widget(self._wifi_ui)) # ******** Advanced settings ******** @@ -131,8 +151,10 @@ class NetworkLayoutMici(NavWidget): if display_network is not None: strength = WifiIcon.get_strength_icon_idx(display_network.strength) self._wifi_button.set_icon(self._wifi_full_txt if strength == 2 else self._wifi_medium_txt if strength == 1 else self._wifi_low_txt) + self._wifi_button.set_draw_lock(display_network.security_type not in (SecurityType.OPEN, SecurityType.UNSUPPORTED)) else: self._wifi_button.set_icon(self._wifi_slash_txt) + self._wifi_button.set_draw_lock(False) def show_event(self): super().show_event() From 2ecdd2810c6475dd40f9a981e2e10c67a4645a5d Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Mon, 23 Feb 2026 01:22:59 -0800 Subject: [PATCH 161/311] mici ui: disable forget on tethering and show full strength (#37344) * disable forget for tethering * nets * put in wifiman * batch * Revert "batch" This reverts commit 9af20c1c7513c22bf9283b2f02514373fa981f50. * clean up * more * more --- selfdrive/ui/mici/layouts/settings/network/wifi_ui.py | 3 +++ system/ui/lib/wifi_manager.py | 8 +++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py index c43a294e40..c9bc54a8b5 100644 --- a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py +++ b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py @@ -152,6 +152,9 @@ class WifiButton(BigButton): @property def _show_forget_btn(self): + if self._network.is_tethering: + return False + return (self._is_saved and not self._wrong_password) or self._is_connecting def _handle_mouse_release(self, mouse_pos: MousePos): diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index 4ca0a382d4..4f02b70625 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -93,17 +93,19 @@ class Network: ssid: str strength: int security_type: SecurityType + is_tethering: bool @classmethod - def from_dbus(cls, ssid: str, aps: list["AccessPoint"]) -> "Network": + def from_dbus(cls, ssid: str, aps: list["AccessPoint"], is_tethering: bool) -> "Network": # we only want to show the strongest AP for each Network/SSID strongest_ap = max(aps, key=lambda ap: ap.strength) security_type = get_security_type(strongest_ap.flags, strongest_ap.wpa_flags, strongest_ap.rsn_flags) return cls( ssid=ssid, - strength=strongest_ap.strength, + strength=100 if is_tethering else strongest_ap.strength, security_type=security_type, + is_tethering=is_tethering, ) @@ -820,7 +822,7 @@ class WifiManager: # catch all for parsing errors cloudlog.exception(f"Failed to parse AP properties for {ap_path}") - networks = [Network.from_dbus(ssid, ap_list) for ssid, ap_list in aps.items()] + networks = [Network.from_dbus(ssid, ap_list, ssid == self._tethering_ssid) for ssid, ap_list in aps.items()] networks.sort(key=lambda n: (n.ssid != self._wifi_state.ssid, not self.is_connection_saved(n.ssid), -n.strength, n.ssid.lower())) self._networks = networks From 8d0cb9c8cfd9efd03e8b9cd8f851fcfe5d844200 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Mon, 23 Feb 2026 01:31:06 -0800 Subject: [PATCH 162/311] Unified label fix scroll fade (#37346) * disable forget for tethering * nets * put in wifiman * batch * revert * clean up --- system/ui/widgets/label.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/system/ui/widgets/label.py b/system/ui/widgets/label.py index cb0cf66b14..865c8b38c3 100644 --- a/system/ui/widgets/label.py +++ b/system/ui/widgets/label.py @@ -753,7 +753,12 @@ class UnifiedLabel(Widget): # draw black fade on left and right fade_width = 20 rl.draw_rectangle_gradient_h(int(self._rect.x + self._rect.width - fade_width), int(self._rect.y), fade_width, int(self._rect.height), rl.BLANK, rl.BLACK) - if self._scroll_state != ScrollState.STARTING: + + # stop drawing left fade once text scrolls past + text_width = visible_sizes[0].x if visible_sizes else 0 + first_copy_in_view = self._scroll_offset + text_width > 0 + draw_left_fade = self._scroll_state != ScrollState.STARTING and first_copy_in_view + if draw_left_fade: rl.draw_rectangle_gradient_h(int(self._rect.x), int(self._rect.y), fade_width, int(self._rect.height), rl.BLACK, rl.BLANK) rl.end_scissor_mode() From c16879f2b82178742a3cec8def3ee45faebf833c Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Mon, 23 Feb 2026 01:40:56 -0800 Subject: [PATCH 163/311] mici ui: fix missing home show event (#37347) fix missing --- selfdrive/ui/mici/layouts/main.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/selfdrive/ui/mici/layouts/main.py b/selfdrive/ui/mici/layouts/main.py index f6dbad89a1..dc01aba56f 100644 --- a/selfdrive/ui/mici/layouts/main.py +++ b/selfdrive/ui/mici/layouts/main.py @@ -62,6 +62,12 @@ class MiciMainLayout(Widget): self._onroad_layout.set_click_callback(lambda: self._scroll_to(self._home_layout)) device.add_interactive_timeout_callback(self._on_interactive_timeout) + def show_event(self): + self._scroller.show_event() + + def hide_event(self): + self._scroller.hide_event() + def _scroll_to(self, layout: Widget): layout_x = int(layout.rect.x) self._scroller.scroll_to(layout_x, smooth=True) From 0a14e198083b00763c3f20702da8655ccb322d66 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Mon, 23 Feb 2026 09:45:29 -0800 Subject: [PATCH 164/311] CI: use setup action on macOS (#37352) --- .github/workflows/setup/action.yaml | 14 +++++++++++--- .github/workflows/tests.yaml | 17 ++--------------- 2 files changed, 13 insertions(+), 18 deletions(-) diff --git a/.github/workflows/setup/action.yaml b/.github/workflows/setup/action.yaml index 56f0096450..958f60d2ea 100644 --- a/.github/workflows/setup/action.yaml +++ b/.github/workflows/setup/action.yaml @@ -38,10 +38,18 @@ runs: uses: ./.github/workflows/auto-cache with: path: /tmp/scons_cache - key: scons-${{ runner.arch }}-${{ env.CACHE_COMMIT_DATE }}-${{ github.sha }} + key: scons-${{ runner.os }}-${{ runner.arch }}-${{ env.CACHE_COMMIT_DATE }}-${{ github.sha }} restore-keys: | - scons-${{ runner.arch }}-${{ env.CACHE_COMMIT_DATE }} - scons-${{ runner.arch }} + scons-${{ runner.os }}-${{ runner.arch }}-${{ env.CACHE_COMMIT_DATE }} + scons-${{ runner.os }}-${{ runner.arch }} + # venv cache + - id: venv-cache + uses: ./.github/workflows/auto-cache + with: + path: ${{ github.workspace }}/.venv + key: venv-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('uv.lock') }} + restore-keys: | + venv-${{ runner.os }}-${{ runner.arch }} - shell: bash name: Run setup run: ./tools/op.sh setup diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 92cdd23768..ff4fc39094 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -66,22 +66,9 @@ jobs: - uses: actions/checkout@v6 with: submodules: true - - run: echo "CACHE_COMMIT_DATE=$(git log -1 --pretty='format:%cd' --date=format:'%Y-%m-%d-%H:%M')" >> $GITHUB_ENV - - name: Install dependencies - run: ./tools/mac_setup.sh - env: - HOMEBREW_DISPLAY_INSTALL_TIMES: 1 - - run: git lfs pull - - name: Getting scons cache - uses: ./.github/workflows/auto-cache - with: - path: /tmp/scons_cache - key: scons-${{ runner.arch }}-macos-${{ env.CACHE_COMMIT_DATE }}-${{ github.sha }} - restore-keys: | - scons-${{ runner.arch }}-macos-${{ env.CACHE_COMMIT_DATE }} - scons-${{ runner.arch }}-macos + - uses: ./.github/workflows/setup-with-retry - name: Building openpilot - run: . .venv/bin/activate && scons + run: scons static_analysis: name: static analysis From 7cc237aa4c54bc0f7442fdbfa90cbd20ed123679 Mon Sep 17 00:00:00 2001 From: commaci-public <60409688+commaci-public@users.noreply.github.com> Date: Mon, 23 Feb 2026 11:58:58 -0800 Subject: [PATCH 165/311] [bot] Update Python packages (#37351) * Update Python packages * fix --------- Co-authored-by: Vehicle Researcher Co-authored-by: Adeeb Shihadeh --- docs/CARS.md | 3 +- opendbc_repo | 2 +- selfdrive/selfdrived/events.py | 2 +- selfdrive/ui/widgets/offroad_alerts.py | 2 +- uv.lock | 38 +++++++++++++------------- 5 files changed, 24 insertions(+), 23 deletions(-) diff --git a/docs/CARS.md b/docs/CARS.md index 65f79cdba4..5487ef1ee1 100644 --- a/docs/CARS.md +++ b/docs/CARS.md @@ -4,7 +4,7 @@ A supported vehicle is one that just works when you install a comma device. All supported cars provide a better experience than any stock system. Supported vehicles reference the US market unless otherwise specified. -# 328 Supported Cars +# 329 Supported Cars |Make|Model|Supported Package|ACC|No ACC accel below|No ALC below|Steering Torque|Resume from stop|Hardware Needed
 |Video|Setup Video| |---|---|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:| @@ -165,6 +165,7 @@ A supported vehicle is one that just works when you install a comma device. All |Kia|Forte 2022-23|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai E connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Kia|K5 2021-24|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Kia|K5 Hybrid 2020-22|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Kia|K7 2017|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|K8 Hybrid (with HDA II) 2023|Highway Driving Assist II|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai Q connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Kia|Niro EV 2019|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai H connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Kia|Niro EV 2020|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai F connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| diff --git a/opendbc_repo b/opendbc_repo index 647441e42d..b10b43105b 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit 647441e42db5735e249344449ad857eeeeff7224 +Subproject commit b10b43105bcd891f95920aac29d01177e0be4940 diff --git a/selfdrive/selfdrived/events.py b/selfdrive/selfdrived/events.py index 6fb96b6e5a..e10f67fa45 100755 --- a/selfdrive/selfdrived/events.py +++ b/selfdrive/selfdrived/events.py @@ -1107,7 +1107,7 @@ if __name__ == '__main__': for i, alerts in EVENTS.items(): for et, alert in alerts.items(): - if callable(alert): + if not isinstance(alert, Alert): alert = alert(CP, CS, sm, False, 1, log.LongitudinalPersonality.standard) alerts_by_type[et][alert.priority].append(event_names[i]) diff --git a/selfdrive/ui/widgets/offroad_alerts.py b/selfdrive/ui/widgets/offroad_alerts.py index 802243ff3e..a87727e27f 100644 --- a/selfdrive/ui/widgets/offroad_alerts.py +++ b/selfdrive/ui/widgets/offroad_alerts.py @@ -63,7 +63,7 @@ class ActionButton(Widget): @property def text(self) -> str: - return self._text() if callable(self._text) else self._text + return self._text if isinstance(self._text, str) else self._text() def _render(self, _): text_size = measure_text_cached(gui_app.font(FontWeight.MEDIUM), self.text, AlertConstants.FONT_SIZE) diff --git a/uv.lock b/uv.lock index 33c210a90f..11ff989278 100644 --- a/uv.lock +++ b/uv.lock @@ -890,7 +890,7 @@ requires-dist = [ { name = "spidev", marker = "sys_platform == 'linux'" }, { name = "sympy" }, { name = "tqdm" }, - { name = "ty", marker = "extra == 'testing'", specifier = "==0.0.17" }, + { name = "ty", marker = "extra == 'testing'" }, { name = "websocket-client" }, { name = "xattr" }, { name = "zeromq", git = "https://github.com/commaai/dependencies.git?subdirectory=zeromq&rev=releases" }, @@ -1509,26 +1509,26 @@ wheels = [ [[package]] name = "ty" -version = "0.0.17" +version = "0.0.18" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/66/c3/41ae6346443eedb65b96761abfab890a48ce2aa5a8a27af69c5c5d99064d/ty-0.0.17.tar.gz", hash = "sha256:847ed6c120913e280bf9b54d8eaa7a1049708acb8824ad234e71498e8ad09f97", size = 5167209, upload-time = "2026-02-13T13:26:36.835Z" } +sdist = { url = "https://files.pythonhosted.org/packages/74/15/9682700d8d60fdca7afa4febc83a2354b29cdcd56e66e19c92b521db3b39/ty-0.0.18.tar.gz", hash = "sha256:04ab7c3db5dcbcdac6ce62e48940d3a0124f377c05499d3f3e004e264ae94b83", size = 5214774, upload-time = "2026-02-20T21:51:31.173Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/01/0ef15c22a1c54b0f728ceff3f62d478dbf8b0dcf8ff7b80b954f79584f3e/ty-0.0.17-py3-none-linux_armv6l.whl", hash = "sha256:64a9a16555cc8867d35c2647c2f1afbd3cae55f68fd95283a574d1bb04fe93e0", size = 10192793, upload-time = "2026-02-13T13:27:13.943Z" }, - { url = "https://files.pythonhosted.org/packages/0f/2c/f4c322d9cded56edc016b1092c14b95cf58c8a33b4787316ea752bb9418e/ty-0.0.17-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:eb2dbd8acd5c5a55f4af0d479523e7c7265a88542efe73ed3d696eb1ba7b6454", size = 10051977, upload-time = "2026-02-13T13:26:57.741Z" }, - { url = "https://files.pythonhosted.org/packages/4c/a5/43746c1ff81e784f5fc303afc61fe5bcd85d0fcf3ef65cb2cef78c7486c7/ty-0.0.17-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f18f5fd927bc628deb9ea2df40f06b5f79c5ccf355db732025a3e8e7152801f6", size = 9564639, upload-time = "2026-02-13T13:26:42.781Z" }, - { url = "https://files.pythonhosted.org/packages/d6/b8/280b04e14a9c0474af574f929fba2398b5e1c123c1e7735893b4cd73d13c/ty-0.0.17-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5383814d1d7a5cc53b3b07661856bab04bb2aac7a677c8d33c55169acdaa83df", size = 10061204, upload-time = "2026-02-13T13:27:00.152Z" }, - { url = "https://files.pythonhosted.org/packages/2a/d7/493e1607d8dfe48288d8a768a2adc38ee27ef50e57f0af41ff273987cda0/ty-0.0.17-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9c20423b8744b484f93e7bf2ef8a9724bca2657873593f9f41d08bd9f83444c9", size = 10013116, upload-time = "2026-02-13T13:26:34.543Z" }, - { url = "https://files.pythonhosted.org/packages/80/ef/22f3ed401520afac90dbdf1f9b8b7755d85b0d5c35c1cb35cf5bd11b59c2/ty-0.0.17-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e6f5b1aba97db9af86517b911674b02f5bc310750485dc47603a105bd0e83ddd", size = 10533623, upload-time = "2026-02-13T13:26:31.449Z" }, - { url = "https://files.pythonhosted.org/packages/75/ce/744b15279a11ac7138832e3a55595706b4a8a209c9f878e3ab8e571d9032/ty-0.0.17-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:488bce1a9bea80b851a97cd34c4d2ffcd69593d6c3f54a72ae02e5c6e47f3d0c", size = 11069750, upload-time = "2026-02-13T13:26:48.638Z" }, - { url = "https://files.pythonhosted.org/packages/f2/be/1133c91f15a0e00d466c24f80df486d630d95d1b2af63296941f7473812f/ty-0.0.17-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8df66b91ec84239420985ec215e7f7549bfda2ac036a3b3c065f119d1c06825a", size = 10870862, upload-time = "2026-02-13T13:26:54.715Z" }, - { url = "https://files.pythonhosted.org/packages/3e/4a/a2ed209ef215b62b2d3246e07e833081e07d913adf7e0448fc204be443d6/ty-0.0.17-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:002139e807c53002790dfefe6e2f45ab0e04012e76db3d7c8286f96ec121af8f", size = 10628118, upload-time = "2026-02-13T13:26:45.439Z" }, - { url = "https://files.pythonhosted.org/packages/b3/0c/87476004cb5228e9719b98afffad82c3ef1f84334bde8527bcacba7b18cb/ty-0.0.17-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6c4e01f05ce82e5d489ab3900ca0899a56c4ccb52659453780c83e5b19e2b64c", size = 10038185, upload-time = "2026-02-13T13:27:02.693Z" }, - { url = "https://files.pythonhosted.org/packages/46/4b/98f0b3ba9aef53c1f0305519536967a4aa793a69ed72677b0a625c5313ac/ty-0.0.17-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2b226dd1e99c0d2152d218c7e440150d1a47ce3c431871f0efa073bbf899e881", size = 10047644, upload-time = "2026-02-13T13:27:05.474Z" }, - { url = "https://files.pythonhosted.org/packages/93/e0/06737bb80aa1a9103b8651d2eb691a7e53f1ed54111152be25f4a02745db/ty-0.0.17-py3-none-musllinux_1_2_i686.whl", hash = "sha256:8b11f1da7859e0ad69e84b3c5ef9a7b055ceed376a432fad44231bdfc48061c2", size = 10231140, upload-time = "2026-02-13T13:27:10.844Z" }, - { url = "https://files.pythonhosted.org/packages/7c/79/e2a606bd8852383ba9abfdd578f4a227bd18504145381a10a5f886b4e751/ty-0.0.17-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:c04e196809ff570559054d3e011425fd7c04161529eb551b3625654e5f2434cb", size = 10718344, upload-time = "2026-02-13T13:26:51.66Z" }, - { url = "https://files.pythonhosted.org/packages/c5/2d/2663984ac11de6d78f74432b8b14ba64d170b45194312852b7543cf7fd56/ty-0.0.17-py3-none-win32.whl", hash = "sha256:305b6ed150b2740d00a817b193373d21f0767e10f94ac47abfc3b2e5a5aec809", size = 9672932, upload-time = "2026-02-13T13:27:08.522Z" }, - { url = "https://files.pythonhosted.org/packages/de/b5/39be78f30b31ee9f5a585969930c7248354db90494ff5e3d0756560fb731/ty-0.0.17-py3-none-win_amd64.whl", hash = "sha256:531828267527aee7a63e972f54e5eee21d9281b72baf18e5c2850c6b862add83", size = 10542138, upload-time = "2026-02-13T13:27:17.084Z" }, - { url = "https://files.pythonhosted.org/packages/40/b7/f875c729c5d0079640c75bad2c7e5d43edc90f16ba242f28a11966df8f65/ty-0.0.17-py3-none-win_arm64.whl", hash = "sha256:de9810234c0c8d75073457e10a84825b9cd72e6629826b7f01c7a0b266ae25b1", size = 10023068, upload-time = "2026-02-13T13:26:39.637Z" }, + { url = "https://files.pythonhosted.org/packages/ae/d8/920460d4c22ea68fcdeb0b2fb53ea2aeb9c6d7875bde9278d84f2ac767b6/ty-0.0.18-py3-none-linux_armv6l.whl", hash = "sha256:4e5e91b0a79857316ef893c5068afc4b9872f9d257627d9bc8ac4d2715750d88", size = 10280825, upload-time = "2026-02-20T21:51:25.03Z" }, + { url = "https://files.pythonhosted.org/packages/83/56/62587de582d3d20d78fcdddd0594a73822ac5a399a12ef512085eb7a4de6/ty-0.0.18-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ee0e578b3f8416e2d5416da9553b78fd33857868aa1384cb7fefeceee5ff102d", size = 10118324, upload-time = "2026-02-20T21:51:22.27Z" }, + { url = "https://files.pythonhosted.org/packages/2f/2d/dbdace8d432a0755a7417f659bfd5b8a4261938ecbdfd7b42f4c454f5aa9/ty-0.0.18-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3f7a0487d36b939546a91d141f7fc3dbea32fab4982f618d5b04dc9d5b6da21e", size = 9605861, upload-time = "2026-02-20T21:51:16.066Z" }, + { url = "https://files.pythonhosted.org/packages/6b/d9/de11c0280f778d5fc571393aada7fe9b8bc1dd6a738f2e2c45702b8b3150/ty-0.0.18-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5e2fa8d45f57ca487a470e4bf66319c09b561150e98ae2a6b1a97ef04c1a4eb", size = 10092701, upload-time = "2026-02-20T21:51:26.862Z" }, + { url = "https://files.pythonhosted.org/packages/0f/94/068d4d591d791041732171e7b63c37a54494b2e7d28e88d2167eaa9ad875/ty-0.0.18-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d75652e9e937f7044b1aca16091193e7ef11dac1c7ec952b7fb8292b7ba1f5f2", size = 10109203, upload-time = "2026-02-20T21:51:11.59Z" }, + { url = "https://files.pythonhosted.org/packages/34/e4/526a4aa56dc0ca2569aaa16880a1ab105c3b416dd70e87e25a05688999f3/ty-0.0.18-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:563c868edceb8f6ddd5e91113c17d3676b028f0ed380bdb3829b06d9beb90e58", size = 10614200, upload-time = "2026-02-20T21:51:20.298Z" }, + { url = "https://files.pythonhosted.org/packages/fd/3d/b68ab20a34122a395880922587fbfc3adf090d22e0fb546d4d20fe8c2621/ty-0.0.18-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:502e2a1f948bec563a0454fc25b074bf5cf041744adba8794d024277e151d3b0", size = 11153232, upload-time = "2026-02-20T21:51:14.121Z" }, + { url = "https://files.pythonhosted.org/packages/68/ea/678243c042343fcda7e6af36036c18676c355878dcdcd517639586d2cf9e/ty-0.0.18-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc881dea97021a3aa29134a476937fd8054775c4177d01b94db27fcfb7aab65b", size = 10832934, upload-time = "2026-02-20T21:51:32.92Z" }, + { url = "https://files.pythonhosted.org/packages/d8/bd/7f8d647cef8b7b346c0163230a37e903c7461c7248574840b977045c77df/ty-0.0.18-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:421fcc3bc64cab56f48edb863c7c1c43649ec4d78ff71a1acb5366ad723b6021", size = 10700888, upload-time = "2026-02-20T21:51:09.673Z" }, + { url = "https://files.pythonhosted.org/packages/6e/06/cb3620dc48c5d335ba7876edfef636b2f4498eff4a262ff90033b9e88408/ty-0.0.18-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0fe5038a7136a0e638a2fb1ad06e3d3c4045314c6ba165c9c303b9aeb4623d6c", size = 10078965, upload-time = "2026-02-20T21:51:07.678Z" }, + { url = "https://files.pythonhosted.org/packages/60/27/c77a5a84533fa3b685d592de7b4b108eb1f38851c40fac4e79cc56ec7350/ty-0.0.18-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d123600a52372677613a719bbb780adeb9b68f47fb5f25acb09171de390e0035", size = 10134659, upload-time = "2026-02-20T21:51:18.311Z" }, + { url = "https://files.pythonhosted.org/packages/43/6e/60af6b88c73469e628ba5253a296da6984e0aa746206f3034c31f1a04ed1/ty-0.0.18-py3-none-musllinux_1_2_i686.whl", hash = "sha256:bb4bc11d32a1bf96a829bf6b9696545a30a196ac77bbc07cc8d3dfee35e03723", size = 10297494, upload-time = "2026-02-20T21:51:39.631Z" }, + { url = "https://files.pythonhosted.org/packages/33/90/612dc0b68224c723faed6adac2bd3f930a750685db76dfe17e6b9e534a83/ty-0.0.18-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:dda2efbf374ba4cd704053d04e32f2f784e85c2ddc2400006b0f96f5f7e4b667", size = 10791944, upload-time = "2026-02-20T21:51:37.13Z" }, + { url = "https://files.pythonhosted.org/packages/0d/da/f4ada0fd08a9e4138fe3fd2bcd3797753593f423f19b1634a814b9b2a401/ty-0.0.18-py3-none-win32.whl", hash = "sha256:c5768607c94977dacddc2f459ace6a11a408a0f57888dd59abb62d28d4fee4f7", size = 9677964, upload-time = "2026-02-20T21:51:42.039Z" }, + { url = "https://files.pythonhosted.org/packages/5e/fa/090ed9746e5c59fc26d8f5f96dc8441825171f1f47752f1778dad690b08b/ty-0.0.18-py3-none-win_amd64.whl", hash = "sha256:b78d0fa1103d36fc2fce92f2092adace52a74654ab7884d54cdaec8eb5016a4d", size = 10636576, upload-time = "2026-02-20T21:51:29.159Z" }, + { url = "https://files.pythonhosted.org/packages/92/4f/5dd60904c8105cda4d0be34d3a446c180933c76b84ae0742e58f02133713/ty-0.0.18-py3-none-win_arm64.whl", hash = "sha256:01770c3c82137c6b216aa3251478f0b197e181054ee92243772de553d3586398", size = 10095449, upload-time = "2026-02-20T21:51:34.914Z" }, ] [[package]] From b32227e69f8d62bc7f47a9d4c7685f2dcce843c4 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Mon, 23 Feb 2026 15:03:48 -0800 Subject: [PATCH 166/311] BigCircleButton: remove press_state_enabled --- selfdrive/ui/mici/widgets/button.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/selfdrive/ui/mici/widgets/button.py b/selfdrive/ui/mici/widgets/button.py index 3fb5bce964..f81b7f0df5 100644 --- a/selfdrive/ui/mici/widgets/button.py +++ b/selfdrive/ui/mici/widgets/button.py @@ -36,7 +36,6 @@ class BigCircleButton(Widget): # State self.set_rect(rl.Rectangle(0, 0, 180, 180)) - self._press_state_enabled = True self._scale_filter = BounceFilter(1.0, 0.1, 1 / gui_app.target_fps) # Icons @@ -49,9 +48,6 @@ class BigCircleButton(Widget): self._txt_btn_red_bg = gui_app.texture("icons_mici/buttons/button_circle_red.png", 180, 180) self._txt_btn_red_pressed_bg = gui_app.texture("icons_mici/buttons/button_circle_red_hover.png", 180, 180) - def set_enable_pressed_state(self, pressed: bool): - self._press_state_enabled = pressed - def _draw_content(self, btn_y: float): # draw icon icon_color = rl.Color(255, 255, 255, int(255 * 0.9)) if self.enabled else rl.Color(255, 255, 255, int(255 * 0.35)) @@ -63,10 +59,10 @@ class BigCircleButton(Widget): txt_bg = self._txt_btn_bg if not self._red else self._txt_btn_red_bg if not self.enabled: txt_bg = self._txt_btn_disabled_bg - elif self.is_pressed and self._press_state_enabled: + elif self.is_pressed: txt_bg = self._txt_btn_pressed_bg if not self._red else self._txt_btn_red_pressed_bg - scale = self._scale_filter.update(PRESSED_SCALE if self.is_pressed and self._press_state_enabled else 1.0) + scale = self._scale_filter.update(PRESSED_SCALE if self.is_pressed else 1.0) btn_x = self._rect.x + (self._rect.width * (1 - scale)) / 2 btn_y = self._rect.y + (self._rect.height * (1 - scale)) / 2 rl.draw_texture_ex(txt_bg, (btn_x, btn_y), 0, scale, rl.WHITE) From 90a9ef277c3253d8f2ffe77ffb824231411a8dda Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Mon, 23 Feb 2026 15:17:21 -0800 Subject: [PATCH 167/311] ui: remove multiple option dialog (#37356) * rm * from here too --- selfdrive/ui/mici/tests/test_widget_leaks.py | 3 +- selfdrive/ui/mici/widgets/dialog.py | 146 +------------------ 2 files changed, 2 insertions(+), 147 deletions(-) diff --git a/selfdrive/ui/mici/tests/test_widget_leaks.py b/selfdrive/ui/mici/tests/test_widget_leaks.py index c7f21bb2f8..7441ed5a22 100755 --- a/selfdrive/ui/mici/tests/test_widget_leaks.py +++ b/selfdrive/ui/mici/tests/test_widget_leaks.py @@ -10,7 +10,7 @@ from openpilot.system.ui.widgets import Widget from openpilot.selfdrive.ui.mici.layouts.onboarding import TrainingGuide as MiciTrainingGuide, OnboardingWindow as MiciOnboardingWindow from openpilot.selfdrive.ui.mici.onroad.driver_camera_dialog import DriverCameraDialog as MiciDriverCameraDialog from openpilot.selfdrive.ui.mici.widgets.pairing_dialog import PairingDialog as MiciPairingDialog -from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigConfirmationDialogV2, BigInputDialog, BigMultiOptionDialog +from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigConfirmationDialogV2, BigInputDialog from openpilot.selfdrive.ui.mici.layouts.settings.device import MiciFccModal # tici dialogs @@ -72,7 +72,6 @@ def test_dialogs_do_not_leak(): lambda: BigDialog("test", "test"), lambda: BigConfirmationDialogV2("test", "icons_mici/settings/network/new/trash.png"), lambda: BigInputDialog("test"), - lambda: BigMultiOptionDialog(["a", "b"], "a"), lambda: MiciFccModal(text="test"), # tici TiciDriverCameraDialog, TiciOnboardingWindow, TiciPairingDialog, Keyboard, diff --git a/selfdrive/ui/mici/widgets/dialog.py b/selfdrive/ui/mici/widgets/dialog.py index 32624bbdd6..8e978066c6 100644 --- a/selfdrive/ui/mici/widgets/dialog.py +++ b/selfdrive/ui/mici/widgets/dialog.py @@ -3,15 +3,12 @@ import math import pyray as rl from typing import Union from collections.abc import Callable -from typing import cast -from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.nav_widget import NavWidget from openpilot.system.ui.widgets.label import UnifiedLabel, gui_label from openpilot.system.ui.widgets.mici_keyboard import MiciKeyboard from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.lib.wrap_text import wrap_text -from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos, MouseEvent -from openpilot.system.ui.widgets.scroller import Scroller +from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos from openpilot.system.ui.widgets.slider import RedBigSlider, BigSlider from openpilot.common.filter_simple import FirstOrderFilter from openpilot.selfdrive.ui.mici.widgets.button import BigButton @@ -241,147 +238,6 @@ class BigInputDialog(BigDialogBase): self._confirm_callback() -class BigDialogOptionButton(Widget): - HEIGHT = 64 - 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), self.HEIGHT)) - - self._selected = False - - self._label = UnifiedLabel(option, font_size=70, text_color=rl.Color(255, 255, 255, int(255 * 0.58)), - font_weight=FontWeight.DISPLAY_REGULAR, alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE, - scroll=True) - - def show_event(self): - super().show_event() - self._label.reset_scroll() - - 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: - rl.draw_rectangle_lines_ex(self._rect, 1, rl.Color(0, 255, 0, 255)) - - # FIXME: offset x by -45 because scroller centers horizontally - if self._selected: - 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(self.HEIGHT) - self._label.set_color(rl.Color(255, 255, 255, int(255 * 0.58))) - self._label.set_font_weight(FontWeight.DISPLAY_REGULAR) - - self._label.render(self._rect) - - -class BigMultiOptionDialog(BigDialogBase): - BACK_TOUCH_AREA_PERCENTAGE = 0.1 - - def __init__(self, options: list[str], default: str | None): - super().__init__() - self._options = options - if default is not None: - assert default in options - - self._default_option: str | None = default - self._selected_option: str = self._default_option or (options[0] if len(options) > 0 else "") - self._last_selected_option: str = self._selected_option - - # Widget doesn't differentiate between click and drag - self._can_click = True - - self._scroller = Scroller([], horizontal=False, snap_items=True, pad=100, spacing=0) - - for option in options: - self._scroller.add_widget(BigDialogOptionButton(option)) - - def show_event(self): - super().show_event() - self._scroller.show_event() - if self._default_option is not None: - self._on_option_selected(self._default_option) - - def get_selected_option(self) -> str: - return self._selected_option - - def _on_option_selected(self, option: str): - y_pos = 0.0 - for btn in self._scroller.items: - 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) - - def _selected_option_changed(self): - pass - - def _handle_mouse_press(self, mouse_pos: MousePos): - super()._handle_mouse_press(mouse_pos) - self._can_click = True - - def _handle_mouse_event(self, mouse_event: MouseEvent) -> None: - super()._handle_mouse_event(mouse_event) - - # # TODO: add generic _handle_mouse_click handler to Widget - if not self._scroller.scroll_panel.is_touch_valid(): - self._can_click = False - - def _handle_mouse_release(self, mouse_pos: MousePos): - super()._handle_mouse_release(mouse_pos) - - if not self._can_click: - return - - # select current option - for btn in self._scroller.items: - btn = cast(BigDialogOptionButton, btn) - if btn.option == self._selected_option: - self._on_option_selected(btn.option) - break - - def _update_state(self): - super()._update_state() - - # get selection by whichever button is closest to center - center_y = self._rect.y + self._rect.height / 2 - closest_btn = (None, float('inf')) - for btn in self._scroller.items: - dist_y = abs((btn.rect.y + btn.rect.height / 2) - center_y) - if dist_y < closest_btn[1]: - closest_btn = (btn, dist_y) - - if closest_btn[0]: - for btn in self._scroller.items: - btn.set_selected(btn.option == closest_btn[0].option) - self._selected_option = closest_btn[0].option - - # Signal to subclasses if selection changed - if self._selected_option != self._last_selected_option: - self._selected_option_changed() - self._last_selected_option = self._selected_option - - def _render(self, _): - super()._render(_) - self._scroller.render(self._rect) - - class BigDialogButton(BigButton): def __init__(self, text: str, value: str = "", icon: Union[str, rl.Texture] = "", description: str = ""): super().__init__(text, value, icon) From 76d084d87706372563a8094196fc3ab16451c662 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Mon, 23 Feb 2026 16:34:42 -0800 Subject: [PATCH 168/311] switch to system compilers (GCC on Linux, Apple Clang on macOS) (#37355) --- SConstruct | 8 ++++---- common/prefix.h | 8 ++++---- common/ratekeeper.cc | 6 +++--- common/tests/test_util.cc | 10 +++++----- common/util.cc | 8 ++++---- common/util.h | 7 +++++++ msgq_repo | 2 +- opendbc_repo | 2 +- rednose_repo | 2 +- selfdrive/pandad/spi.cc | 2 +- selfdrive/pandad/tests/test_pandad_canprotocol.cc | 4 ++-- selfdrive/ui/SConscript | 2 +- system/loggerd/encoder/v4l_encoder.cc | 10 +++++----- system/loggerd/loggerd.cc | 4 ++-- system/loggerd/loggerd.h | 4 ++-- system/loggerd/tests/test_logger.cc | 2 +- tools/cabana/chart/chart.cc | 2 +- tools/cabana/signalview.cc | 4 ++-- tools/cabana/utils/util.cc | 4 ++-- tools/cabana/videowidget.cc | 13 +++++++------ tools/install_ubuntu_dependencies.sh | 1 - tools/replay/qcom_decoder.cc | 2 +- 22 files changed, 57 insertions(+), 50 deletions(-) diff --git a/SConstruct b/SConstruct index cb1c97d158..446fcc417d 100644 --- a/SConstruct +++ b/SConstruct @@ -56,15 +56,13 @@ env = Environment( "ACADOS_PYTHON_INTERFACE_PATH": Dir("#third_party/acados/acados_template").abspath, "TERA_PATH": Dir("#").abspath + f"/third_party/acados/{arch}/t_renderer" }, - CC='clang', - CXX='clang++', CCFLAGS=[ "-g", "-fPIC", "-O2", "-Wunused", "-Werror", - "-Wshadow", + "-Wshadow" if arch in ("Darwin", "larch64") else "-Wshadow=local", "-Wno-unknown-warning-option", "-Wno-inconsistent-missing-override", "-Wno-c99-designator", @@ -106,6 +104,8 @@ env = Environment( # Arch-specific flags and paths if arch == "larch64": + env["CC"] = "clang" + env["CXX"] = "clang++" env.Append(LIBPATH=[ "/usr/local/lib", "/system/vendor/lib64", @@ -162,7 +162,7 @@ if os.environ.get('SCONS_PROGRESS'): py_include = sysconfig.get_paths()['include'] envCython = env.Clone() envCython["CPPPATH"] += [py_include, np.get_include()] -envCython["CCFLAGS"] += ["-Wno-#warnings", "-Wno-shadow", "-Wno-deprecated-declarations"] +envCython["CCFLAGS"] += ["-Wno-#warnings", "-Wno-cpp", "-Wno-shadow", "-Wno-deprecated-declarations"] envCython["CCFLAGS"].remove("-Werror") envCython["LIBS"] = [] diff --git a/common/prefix.h b/common/prefix.h index 30ee18f637..de3a94d71f 100644 --- a/common/prefix.h +++ b/common/prefix.h @@ -27,14 +27,14 @@ public: auto param_path = Params().getParamPath(); if (util::file_exists(param_path)) { std::string real_path = util::readlink(param_path); - system(util::string_format("rm %s -rf", real_path.c_str()).c_str()); + util::check_system(util::string_format("rm %s -rf", real_path.c_str())); unlink(param_path.c_str()); } if (getenv("COMMA_CACHE") == nullptr) { - system(util::string_format("rm %s -rf", Path::download_cache_root().c_str()).c_str()); + util::check_system(util::string_format("rm %s -rf", Path::download_cache_root().c_str())); } - system(util::string_format("rm %s -rf", Path::comma_home().c_str()).c_str()); - system(util::string_format("rm %s -rf", msgq_path.c_str()).c_str()); + util::check_system(util::string_format("rm %s -rf", Path::comma_home().c_str())); + util::check_system(util::string_format("rm %s -rf", msgq_path.c_str())); unsetenv("OPENPILOT_PREFIX"); } diff --git a/common/ratekeeper.cc b/common/ratekeeper.cc index 7e63815168..a79acd7d51 100644 --- a/common/ratekeeper.cc +++ b/common/ratekeeper.cc @@ -6,9 +6,9 @@ #include "common/timing.h" #include "common/util.h" -RateKeeper::RateKeeper(const std::string &name, float rate, float print_delay_threshold) - : name(name), - print_delay_threshold(std::max(0.f, print_delay_threshold)) { +RateKeeper::RateKeeper(const std::string &name_, float rate, float print_delay_threshold_) + : name(name_), + print_delay_threshold(std::max(0.f, print_delay_threshold_)) { interval = 1 / rate; last_monitor_time = seconds_since_boot(); next_frame_time = last_monitor_time + interval; diff --git a/common/tests/test_util.cc b/common/tests/test_util.cc index de87fa3e06..d927b98a4d 100644 --- a/common/tests/test_util.cc +++ b/common/tests/test_util.cc @@ -36,7 +36,7 @@ TEST_CASE("util::read_file") { REQUIRE(util::read_file(filename).empty()); std::string content = random_bytes(64 * 1024); - write(fd, content.c_str(), content.size()); + REQUIRE(write(fd, content.c_str(), content.size()) == (ssize_t)content.size()); std::string ret = util::read_file(filename); bool equal = (ret == content); REQUIRE(equal); @@ -114,12 +114,12 @@ TEST_CASE("util::safe_fwrite") { } TEST_CASE("util::create_directories") { - system("rm /tmp/test_create_directories -rf"); + REQUIRE(system("rm /tmp/test_create_directories -rf") == 0); std::string dir = "/tmp/test_create_directories/a/b/c/d/e/f"; - auto check_dir_permissions = [](const std::string &dir, mode_t mode) -> bool { + auto check_dir_permissions = [](const std::string &path, mode_t mode) -> bool { struct stat st = {}; - return stat(dir.c_str(), &st) == 0 && (st.st_mode & S_IFMT) == S_IFDIR && (st.st_mode & (S_IRWXU | S_IRWXG | S_IRWXO)) == mode; + return stat(path.c_str(), &st) == 0 && (st.st_mode & S_IFMT) == S_IFDIR && (st.st_mode & (S_IRWXU | S_IRWXG | S_IRWXO)) == mode; }; SECTION("create_directories") { @@ -132,7 +132,7 @@ TEST_CASE("util::create_directories") { } SECTION("a file exists with the same name") { REQUIRE(util::create_directories(dir, 0755)); - int f = open((dir + "/file").c_str(), O_RDWR | O_CREAT); + int f = open((dir + "/file").c_str(), O_RDWR | O_CREAT, 0644); REQUIRE(f != -1); close(f); REQUIRE(util::create_directories(dir + "/file", 0755) == false); diff --git a/common/util.cc b/common/util.cc index 26a2bd60bc..84b47e187e 100644 --- a/common/util.cc +++ b/common/util.cc @@ -181,9 +181,9 @@ bool file_exists(const std::string& fn) { } static bool createDirectory(std::string dir, mode_t mode) { - auto verify_dir = [](const std::string& dir) -> bool { + auto verify_dir = [](const std::string& path) -> bool { struct stat st = {}; - return (stat(dir.c_str(), &st) == 0 && (st.st_mode & S_IFMT) == S_IFDIR); + return (stat(path.c_str(), &st) == 0 && (st.st_mode & S_IFMT) == S_IFDIR); }; // remove trailing /'s while (dir.size() > 1 && dir.back() == '/') { @@ -288,7 +288,7 @@ std::string strip(const std::string &str) { std::string check_output(const std::string& command) { char buffer[128]; std::string result; - std::unique_ptr pipe(popen(command.c_str(), "r"), pclose); + std::unique_ptr pipe(popen(command.c_str(), "r"), pclose); if (!pipe) { return ""; @@ -303,7 +303,7 @@ std::string check_output(const std::string& command) { bool system_time_valid() { // Default to August 26, 2024 - tm min_tm = {.tm_year = 2024 - 1900, .tm_mon = 7, .tm_mday = 26}; + tm min_tm = {.tm_mday = 26, .tm_mon = 7, .tm_year = 2024 - 1900}; time_t min_date = mktime(&min_tm); struct stat st; diff --git a/common/util.h b/common/util.h index f46db4d9fa..e4483ee7a5 100644 --- a/common/util.h +++ b/common/util.h @@ -96,6 +96,13 @@ bool create_directories(const std::string &dir, mode_t mode); std::string check_output(const std::string& command); +inline void check_system(const std::string& cmd) { + int ret = std::system(cmd.c_str()); + if (ret != 0) { + fprintf(stderr, "system command failed (%d): %s\n", ret, cmd.c_str()); + } +} + bool system_time_valid(); inline void sleep_for(const int milliseconds) { diff --git a/msgq_repo b/msgq_repo index fa514e7dd7..ed2777747d 160000 --- a/msgq_repo +++ b/msgq_repo @@ -1 +1 @@ -Subproject commit fa514e7dd77025952d94e893c622a5006e260321 +Subproject commit ed2777747d60de5a399b74ef1d4be4c1fb406ae1 diff --git a/opendbc_repo b/opendbc_repo index b10b43105b..ffe10c0c80 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit b10b43105bcd891f95920aac29d01177e0be4940 +Subproject commit ffe10c0c809cc48726292c832b109a14fee603be diff --git a/rednose_repo b/rednose_repo index 7fddc8e6d4..6ccb8d0556 160000 --- a/rednose_repo +++ b/rednose_repo @@ -1 +1 @@ -Subproject commit 7fddc8e6d49def83c952a78673179bdc62789214 +Subproject commit 6ccb8d055652cd9769b5e418edf116272fde4e09 diff --git a/selfdrive/pandad/spi.cc b/selfdrive/pandad/spi.cc index 25682e6b17..f54c26e506 100644 --- a/selfdrive/pandad/spi.cc +++ b/selfdrive/pandad/spi.cc @@ -32,7 +32,7 @@ const std::string SPI_DEVICE = "/dev/spidev0.0"; class LockEx { public: - LockEx(int fd, std::recursive_mutex &m) : fd(fd), m(m) { + LockEx(int fd_, std::recursive_mutex &m_) : fd(fd_), m(m_) { m.lock(); flock(fd, LOCK_EX); } diff --git a/selfdrive/pandad/tests/test_pandad_canprotocol.cc b/selfdrive/pandad/tests/test_pandad_canprotocol.cc index 9b53392f6b..8499a1aab8 100644 --- a/selfdrive/pandad/tests/test_pandad_canprotocol.cc +++ b/selfdrive/pandad/tests/test_pandad_canprotocol.cc @@ -21,8 +21,8 @@ struct PandaTest : public Panda { capnp::List::Reader can_data_list; }; -PandaTest::PandaTest(int can_list_size, cereal::PandaState::PandaType hw_type) : can_list_size(can_list_size), Panda() { - this->hw_type = hw_type; +PandaTest::PandaTest(int can_list_size_, cereal::PandaState::PandaType hw_type_) : can_list_size(can_list_size_), Panda() { + this->hw_type = hw_type_; int data_limit = ((hw_type == cereal::PandaState::PandaType::RED_PANDA) ? std::size(dlc_to_len) : 8); // prepare test data for (int i = 0; i < data_limit; ++i) { diff --git a/selfdrive/ui/SConscript b/selfdrive/ui/SConscript index 0de3e13c01..a03a26bea8 100644 --- a/selfdrive/ui/SConscript +++ b/selfdrive/ui/SConscript @@ -68,4 +68,4 @@ if GetOption('extras'): obj = raylib_env.Object(f"installer/installers/installer_{name}.o", ["installer/installer.cc"], CPPDEFINES=d) f = raylib_env.Program(f"installer/installers/installer_{name}", [obj, cont, inter, inter_bold, inter_light], LIBS=raylib_libs) # keep installers small - assert f[0].get_size() < 1900*1e3, f[0].get_size() + assert f[0].get_size() < 2500*1e3, f[0].get_size() diff --git a/system/loggerd/encoder/v4l_encoder.cc b/system/loggerd/encoder/v4l_encoder.cc index 6ee3af13b0..383fa2f0f5 100644 --- a/system/loggerd/encoder/v4l_encoder.cc +++ b/system/loggerd/encoder/v4l_encoder.cc @@ -43,29 +43,29 @@ static void dequeue_buffer(int fd, v4l2_buf_type buf_type, unsigned int *index=N static void queue_buffer(int fd, v4l2_buf_type buf_type, unsigned int index, VisionBuf *buf, struct timeval timestamp={}) { v4l2_plane plane = { + .bytesused = (uint32_t)buf->len, .length = (unsigned int)buf->len, .m = { .userptr = (unsigned long)buf->addr, }, - .bytesused = (uint32_t)buf->len, .reserved = {(unsigned int)buf->fd} }; v4l2_buffer v4l_buf = { - .type = buf_type, .index = index, + .type = buf_type, + .flags = V4L2_BUF_FLAG_TIMESTAMP_COPY, + .timestamp = timestamp, .memory = V4L2_MEMORY_USERPTR, .m = { .planes = &plane, }, .length = 1, - .flags = V4L2_BUF_FLAG_TIMESTAMP_COPY, - .timestamp = timestamp }; util::safe_ioctl(fd, VIDIOC_QBUF, &v4l_buf, "VIDIOC_QBUF failed"); } static void request_buffers(int fd, v4l2_buf_type buf_type, unsigned int count) { struct v4l2_requestbuffers reqbuf = { + .count = count, .type = buf_type, .memory = V4L2_MEMORY_USERPTR, - .count = count }; util::safe_ioctl(fd, VIDIOC_REQBUFS, &reqbuf, "VIDIOC_REQBUFS failed"); } diff --git a/system/loggerd/loggerd.cc b/system/loggerd/loggerd.cc index 47da321024..3755929619 100644 --- a/system/loggerd/loggerd.cc +++ b/system/loggerd/loggerd.cc @@ -219,11 +219,11 @@ void handle_preserve_segment(LoggerdState *s) { void loggerd_thread() { // setup messaging - typedef struct ServiceState { + struct ServiceState { std::string name; int counter, freq; bool encoder, preserve_segment, record_audio; - } ServiceState; + }; std::unordered_map service_state; std::unordered_map remote_encoders; diff --git a/system/loggerd/loggerd.h b/system/loggerd/loggerd.h index 8e3a74d2d9..6aa0c8be40 100644 --- a/system/loggerd/loggerd.h +++ b/system/loggerd/loggerd.h @@ -125,10 +125,10 @@ const EncoderInfo stream_driver_encoder_info = { const EncoderInfo qcam_encoder_info = { .publish_name = "qRoadEncodeData", .filename = "qcamera.ts", - .get_settings = [](int){return EncoderSettings::QcamEncoderSettings();}, + .include_audio = Params().getBool("RecordAudio"), .frame_width = 526, .frame_height = 330, - .include_audio = Params().getBool("RecordAudio"), + .get_settings = [](int){return EncoderSettings::QcamEncoderSettings();}, INIT_ENCODE_FUNCTIONS(QRoadEncode), }; diff --git a/system/loggerd/tests/test_logger.cc b/system/loggerd/tests/test_logger.cc index 40a45a68d5..61509c256c 100644 --- a/system/loggerd/tests/test_logger.cc +++ b/system/loggerd/tests/test_logger.cc @@ -56,7 +56,7 @@ void write_msg(LoggerState *logger) { TEST_CASE("logger") { const int segment_cnt = 100; const std::string log_root = "/tmp/test_logger"; - system(("rm " + log_root + " -rf").c_str()); + REQUIRE(system(("rm " + log_root + " -rf").c_str()) == 0); std::string route_name; { LoggerState logger(log_root); diff --git a/tools/cabana/chart/chart.cc b/tools/cabana/chart/chart.cc index bc2380e550..14491b1eff 100644 --- a/tools/cabana/chart/chart.cc +++ b/tools/cabana/chart/chart.cc @@ -571,7 +571,7 @@ void ChartView::showTip(double sec) { if (s.series->isVisible()) { QString value = "--"; // use reverse iterator to find last item <= sec. - auto it = std::lower_bound(s.vals.crbegin(), s.vals.crend(), sec, [](auto &p, double x) { return p.x() > x; }); + auto it = std::lower_bound(s.vals.crbegin(), s.vals.crend(), sec, [](auto &p, double v) { return p.x() > v; }); if (it != s.vals.crend() && it->x() >= axis_x->min()) { value = s.sig->formatValue(it->y(), false); s.track_pt = *it; diff --git a/tools/cabana/signalview.cc b/tools/cabana/signalview.cc index 0a0c07a155..a9ceacd806 100644 --- a/tools/cabana/signalview.cc +++ b/tools/cabana/signalview.cc @@ -34,12 +34,12 @@ SignalModel::SignalModel(QObject *parent) : root(new Item), QAbstractItemModel(p } void SignalModel::insertItem(SignalModel::Item *root_item, int pos, const cabana::Signal *sig) { - Item *parent_item = new Item{.sig = sig, .parent = root_item, .title = sig->name, .type = Item::Sig}; + Item *parent_item = new Item{.type = Item::Sig, .parent = root_item, .sig = sig, .title = sig->name}; root_item->children.insert(pos, parent_item); QString titles[]{"Name", "Size", "Receiver Nodes", "Little Endian", "Signed", "Offset", "Factor", "Type", "Multiplex Value", "Extra Info", "Unit", "Comment", "Minimum Value", "Maximum Value", "Value Table"}; for (int i = 0; i < std::size(titles); ++i) { - auto item = new Item{.sig = sig, .parent = parent_item, .title = titles[i], .type = (Item::Type)(i + Item::Name)}; + auto item = new Item{.type = (Item::Type)(i + Item::Name), .parent = parent_item, .sig = sig, .title = titles[i]}; parent_item->children.push_back(item); if (item->type == Item::ExtraInfo) { parent_item = item; diff --git a/tools/cabana/utils/util.cc b/tools/cabana/utils/util.cc index ca069b358d..61e4ee0514 100644 --- a/tools/cabana/utils/util.cc +++ b/tools/cabana/utils/util.cc @@ -166,13 +166,13 @@ UnixSignalHandler::~UnixSignalHandler() { } void UnixSignalHandler::signalHandler(int s) { - ::write(sig_fd[0], &s, sizeof(s)); + (void)!::write(sig_fd[0], &s, sizeof(s)); } void UnixSignalHandler::handleSigTerm() { sn->setEnabled(false); int tmp; - ::read(sig_fd[1], &tmp, sizeof(tmp)); + (void)!::read(sig_fd[1], &tmp, sizeof(tmp)); printf("\nexiting...\n"); qApp->closeAllWindows(); diff --git a/tools/cabana/videowidget.cc b/tools/cabana/videowidget.cc index 55018f28f0..a05bf24b4a 100644 --- a/tools/cabana/videowidget.cc +++ b/tools/cabana/videowidget.cc @@ -16,13 +16,14 @@ const int MIN_VIDEO_HEIGHT = 100; const int THUMBNAIL_MARGIN = 3; +// Indexed by TimelineType: None, Engaged, AlertInfo, AlertWarning, AlertCritical, UserBookmark static const QColor timeline_colors[] = { - [(int)TimelineType::None] = QColor(111, 143, 175), - [(int)TimelineType::Engaged] = QColor(0, 163, 108), - [(int)TimelineType::UserBookmark] = Qt::magenta, - [(int)TimelineType::AlertInfo] = Qt::green, - [(int)TimelineType::AlertWarning] = QColor(255, 195, 0), - [(int)TimelineType::AlertCritical] = QColor(199, 0, 57), + QColor(111, 143, 175), + QColor(0, 163, 108), + Qt::green, + QColor(255, 195, 0), + QColor(199, 0, 57), + Qt::magenta, }; static Replay *getReplay() { diff --git a/tools/install_ubuntu_dependencies.sh b/tools/install_ubuntu_dependencies.sh index 4a1cf9a8cb..6793cf2d19 100755 --- a/tools/install_ubuntu_dependencies.sh +++ b/tools/install_ubuntu_dependencies.sh @@ -24,7 +24,6 @@ function install_ubuntu_common_requirements() { # normal stuff, mostly for the bare docker image $SUDO apt-get install -y --no-install-recommends \ ca-certificates \ - clang \ build-essential \ curl \ libssl-dev \ diff --git a/tools/replay/qcom_decoder.cc b/tools/replay/qcom_decoder.cc index eb5409daa3..44ff16ce4f 100644 --- a/tools/replay/qcom_decoder.cc +++ b/tools/replay/qcom_decoder.cc @@ -175,8 +175,8 @@ bool MsmVidc::setPlaneFormat(enum v4l2_buf_type type, uint32_t fourcc) { bool MsmVidc::setFPS(uint32_t fps) { struct v4l2_streamparm streamparam = { .type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE, - .parm.output.timeperframe = {1, fps} }; + streamparam.parm.output.timeperframe = {1, fps}; util::safe_ioctl(fd, VIDIOC_S_PARM, &streamparam, "VIDIOC_S_PARM failed"); return true; } From 16dda06a0c040040b154ab8ea0360421fb5857ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Harald=20Sch=C3=A4fer?= Date: Mon, 23 Feb 2026 16:49:48 -0800 Subject: [PATCH 169/311] Reapply chunker (#37292) * Reapply chunker * good size * rm glob * cleaner * back to 45mb * warp need not be fixed * add manifest path * lil cleaner --- .gitignore | 3 +- common/file_chunker.py | 37 +++++++++++++++++++++++++ selfdrive/modeld/SConscript | 40 ++++++++++++--------------- selfdrive/modeld/dmonitoringmodeld.py | 4 +-- selfdrive/modeld/external_pickle.py | 38 ------------------------- selfdrive/modeld/modeld.py | 6 ++-- 6 files changed, 60 insertions(+), 68 deletions(-) create mode 100644 common/file_chunker.py delete mode 100755 selfdrive/modeld/external_pickle.py diff --git a/.gitignore b/.gitignore index af18f06628..062801d787 100644 --- a/.gitignore +++ b/.gitignore @@ -64,8 +64,7 @@ flycheck_* cppcheck_report.txt comma*.sh -selfdrive/modeld/models/*.pkl -selfdrive/modeld/models/*.pkl.* +selfdrive/modeld/models/*.pkl* # openpilot log files *.bz2 diff --git a/common/file_chunker.py b/common/file_chunker.py new file mode 100644 index 0000000000..ac9ddbb384 --- /dev/null +++ b/common/file_chunker.py @@ -0,0 +1,37 @@ +import math +import os +from pathlib import Path + +CHUNK_SIZE = 45 * 1024 * 1024 # 45MB, under GitHub's 50MB limit + +def get_chunk_name(name, idx, num_chunks): + return f"{name}.chunk{idx+1:02d}of{num_chunks:02d}" + +def get_manifest_path(name): + return f"{name}.chunkmanifest" + +def get_chunk_paths(path, file_size): + num_chunks = math.ceil(file_size / CHUNK_SIZE) + return [get_manifest_path(path)] + [get_chunk_name(path, i, num_chunks) for i in range(num_chunks)] + +def chunk_file(path, targets): + manifest_path, *chunk_paths = targets + with open(path, 'rb') as f: + data = f.read() + actual_num_chunks = max(1, math.ceil(len(data) / CHUNK_SIZE)) + assert len(chunk_paths) >= actual_num_chunks, f"Allowed {len(chunk_paths)} chunks but needs at least {actual_num_chunks}, for path {path}" + for i, chunk_path in enumerate(chunk_paths): + with open(chunk_path, 'wb') as f: + f.write(data[i * CHUNK_SIZE:(i + 1) * CHUNK_SIZE]) + Path(manifest_path).write_text(str(len(chunk_paths))) + os.remove(path) + + +def read_file_chunked(path): + manifest_path = get_manifest_path(path) + if os.path.isfile(manifest_path): + num_chunks = int(Path(manifest_path).read_text().strip()) + return b''.join(Path(get_chunk_name(path, i, num_chunks)).read_bytes() for i in range(num_chunks)) + if os.path.isfile(path): + return Path(path).read_bytes() + raise FileNotFoundError(path) diff --git a/selfdrive/modeld/SConscript b/selfdrive/modeld/SConscript index 1808cfec2f..ed5c50beb3 100644 --- a/selfdrive/modeld/SConscript +++ b/selfdrive/modeld/SConscript @@ -1,14 +1,18 @@ import os import glob +from openpilot.common.file_chunker import chunk_file, get_chunk_paths Import('env', 'arch') +chunker_file = File("#common/file_chunker.py") lenv = env.Clone() -CHUNK_BYTES = int(os.environ.get("TG_CHUNK_BYTES", str(45 * 1024 * 1024))) tinygrad_root = env.Dir("#").abspath tinygrad_files = ["#"+x for x in glob.glob(env.Dir("#tinygrad_repo").relpath + "/**", recursive=True, root_dir=tinygrad_root) if 'pycache' not in x and os.path.isfile(os.path.join(tinygrad_root, x))] +def estimate_pickle_max_size(onnx_size): + return 1.2 * onnx_size + 10 * 1024 * 1024 # 20% + 10MB is plenty + # Get model metadata for model_name in ['driving_vision', 'driving_policy', 'dmonitoring_model']: fn = File(f"models/{model_name}").abspath @@ -26,39 +30,29 @@ image_flag = { 'larch64': 'IMAGE=2', }.get(arch, 'IMAGE=0') script_files = [File(Dir("#selfdrive/modeld").File("compile_warp.py").abspath)] -cmd = f'{tg_flags} python3 {Dir("#selfdrive/modeld").abspath}/compile_warp.py ' +compile_warp_cmd = f'{tg_flags} python3 {Dir("#selfdrive/modeld").abspath}/compile_warp.py ' from openpilot.common.transformations.camera import _ar_ox_fisheye, _os_fisheye warp_targets = [] for cam in [_ar_ox_fisheye, _os_fisheye]: w, h = cam.width, cam.height warp_targets += [File(f"models/warp_{w}x{h}_tinygrad.pkl").abspath, File(f"models/dm_warp_{w}x{h}_tinygrad.pkl").abspath] -lenv.Command(warp_targets, tinygrad_files + script_files, cmd) +lenv.Command(warp_targets, tinygrad_files + script_files, compile_warp_cmd) def tg_compile(flags, model_name): pythonpath_string = 'PYTHONPATH="${PYTHONPATH}:' + env.Dir("#tinygrad_repo").abspath + '"' fn = File(f"models/{model_name}").abspath - - out = fn + "_tinygrad.pkl" - full = out + ".full" - parts = out + ".parts" - - full_node = lenv.Command( - full, - [fn + ".onnx"] + tinygrad_files, - f'{pythonpath_string} {flags} {image_flag} python3 {Dir("#tinygrad_repo").abspath}/examples/openpilot/compile3.py {fn}.onnx {full}' + pkl = fn + "_tinygrad.pkl" + onnx_path = fn + ".onnx" + chunk_targets = get_chunk_paths(pkl, estimate_pickle_max_size(os.path.getsize(onnx_path))) + def do_chunk(target, source, env): + chunk_file(pkl, chunk_targets) + return lenv.Command( + chunk_targets, + [onnx_path] + tinygrad_files + [chunker_file], + [f'{pythonpath_string} {flags} {image_flag} python3 {Dir("#tinygrad_repo").abspath}/examples/openpilot/compile3.py {fn}.onnx {pkl}', + do_chunk] ) - split_script = File(Dir("#selfdrive/modeld").File("external_pickle.py").abspath) - parts_node = lenv.Command( - parts, - [full_node, split_script, Value(str(CHUNK_BYTES))], - [f'python3 {split_script.abspath} {full} {out} {CHUNK_BYTES}', Delete(full)], - ) - - lenv.NoCache(parts_node) - lenv.AlwaysBuild(parts_node) - return parts_node - # Compile small models for model_name in ['driving_vision', 'driving_policy', 'dmonitoring_model']: tg_compile(tg_flags, model_name) diff --git a/selfdrive/modeld/dmonitoringmodeld.py b/selfdrive/modeld/dmonitoringmodeld.py index 956ea8a6a2..28190db3e6 100755 --- a/selfdrive/modeld/dmonitoringmodeld.py +++ b/selfdrive/modeld/dmonitoringmodeld.py @@ -16,8 +16,8 @@ from openpilot.common.realtime import config_realtime_process from openpilot.common.transformations.model import dmonitoringmodel_intrinsics from openpilot.common.transformations.camera import _ar_ox_fisheye, _os_fisheye from openpilot.system.camerad.cameras.nv12_info import get_nv12_info +from openpilot.common.file_chunker import read_file_chunked from openpilot.selfdrive.modeld.parse_model_outputs import sigmoid, safe_exp -from openpilot.selfdrive.modeld.external_pickle import load_external_pickle PROCESS_NAME = "selfdrive.modeld.dmonitoringmodeld" SEND_RAW_PRED = os.getenv('SEND_RAW_PRED') @@ -45,7 +45,7 @@ class ModelState: self.tensor_inputs = {k: Tensor(v, device='NPY').realize() for k,v in self.numpy_inputs.items()} self._blob_cache : dict[int, Tensor] = {} self.image_warp = None - self.model_run = load_external_pickle(MODEL_PKL_PATH) + self.model_run = pickle.loads(read_file_chunked(str(MODEL_PKL_PATH))) def run(self, buf: VisionBuf, calib: np.ndarray, transform: np.ndarray) -> tuple[np.ndarray, float]: self.numpy_inputs['calib'][0,:] = calib diff --git a/selfdrive/modeld/external_pickle.py b/selfdrive/modeld/external_pickle.py deleted file mode 100755 index d60a9632a6..0000000000 --- a/selfdrive/modeld/external_pickle.py +++ /dev/null @@ -1,38 +0,0 @@ -#!/usr/bin/env python3 -import hashlib -import pickle -import sys -from pathlib import Path - -def split_pickle(full_path: Path, out_prefix: Path, chunk_bytes: int) -> None: - data = full_path.read_bytes() - out_dir = out_prefix.parent - - for p in out_dir.glob(f"{out_prefix.name}.data-*"): - p.unlink() - - total = (len(data) + chunk_bytes - 1) // chunk_bytes - names = [] - for i in range(0, len(data), chunk_bytes): - name = f"{out_prefix.name}.data-{(i // chunk_bytes) + 1:04d}-of-{total:04d}" - (out_dir / name).write_bytes(data[i:i + chunk_bytes]) - names.append(name) - - manifest = hashlib.sha256(data).hexdigest() + "\n" + "\n".join(names) + "\n" - (out_dir / (out_prefix.name + ".parts")).write_text(manifest) - -def load_external_pickle(prefix: Path): - parts = prefix.parent / (prefix.name + ".parts") - lines = parts.read_text().splitlines() - expected_hash, chunk_names = lines[0], lines[1:] - - data = bytearray() - for name in chunk_names: - data += (prefix.parent / name).read_bytes() - - if hashlib.sha256(data).hexdigest() != expected_hash: - raise RuntimeError(f"hash mismatch loading {prefix}") - return pickle.loads(data) - -if __name__ == "__main__": - split_pickle(Path(sys.argv[1]), Path(sys.argv[2]), int(sys.argv[3])) diff --git a/selfdrive/modeld/modeld.py b/selfdrive/modeld/modeld.py index 3fe3e0e6d6..bff59366d6 100755 --- a/selfdrive/modeld/modeld.py +++ b/selfdrive/modeld/modeld.py @@ -27,8 +27,8 @@ from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper from openpilot.selfdrive.controls.lib.drive_helpers import get_accel_from_plan, smooth_value, get_curvature_from_plan from openpilot.selfdrive.modeld.parse_model_outputs import Parser from openpilot.selfdrive.modeld.fill_model_msg import fill_model_msg, fill_pose_msg, PublishState +from openpilot.common.file_chunker import read_file_chunked from openpilot.selfdrive.modeld.constants import ModelConstants, Plan -from openpilot.selfdrive.modeld.external_pickle import load_external_pickle PROCESS_NAME = "selfdrive.modeld.modeld" @@ -178,8 +178,8 @@ class ModelState: self.parser = Parser() self.frame_buf_params : dict[str, tuple[int, int, int, int]] = {} self.update_imgs = None - self.vision_run = load_external_pickle(VISION_PKL_PATH) - self.policy_run = load_external_pickle(POLICY_PKL_PATH) + self.vision_run = pickle.loads(read_file_chunked(str(VISION_PKL_PATH))) + self.policy_run = pickle.loads(read_file_chunked(str(POLICY_PKL_PATH))) def slice_outputs(self, model_outputs: np.ndarray, output_slices: dict[str, slice]) -> dict[str, np.ndarray]: parsed_model_outputs = {k: model_outputs[np.newaxis, v] for k,v in output_slices.items()} From 5af3f3215737a7741941232f51d4bd7b6b010cfa Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Mon, 23 Feb 2026 16:56:58 -0800 Subject: [PATCH 170/311] simplify setup (#37358) * simplify setup * lil more * simplify dockedr * just run setup there: * don't need that junk * lil more --- .github/workflows/prebuilt.yaml | 2 +- .github/workflows/release.yaml | 13 +-- Dockerfile.openpilot | 36 +++++-- Dockerfile.openpilot_base | 42 -------- selfdrive/test/docker_build.sh | 16 ++-- selfdrive/test/docker_common.sh | 18 ---- tools/install_python_dependencies.sh | 29 ------ tools/install_ubuntu_dependencies.sh | 99 ------------------- tools/mac_setup.sh | 20 ---- tools/op.sh | 6 +- tools/setup_dependencies.sh | 137 +++++++++++++++++++++++++++ tools/ubuntu_setup.sh | 10 -- 12 files changed, 180 insertions(+), 248 deletions(-) delete mode 100644 Dockerfile.openpilot_base delete mode 100644 selfdrive/test/docker_common.sh delete mode 100755 tools/install_python_dependencies.sh delete mode 100755 tools/install_ubuntu_dependencies.sh delete mode 100755 tools/mac_setup.sh create mode 100755 tools/setup_dependencies.sh delete mode 100755 tools/ubuntu_setup.sh diff --git a/.github/workflows/prebuilt.yaml b/.github/workflows/prebuilt.yaml index c0a2baa8e3..ecf1e8503a 100644 --- a/.github/workflows/prebuilt.yaml +++ b/.github/workflows/prebuilt.yaml @@ -6,7 +6,7 @@ on: env: DOCKER_LOGIN: docker login ghcr.io -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }} - BUILD: selfdrive/test/docker_build.sh prebuilt + BUILD: selfdrive/test/docker_build.sh jobs: build_prebuilt: diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 159902d519..4d731965d7 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -7,20 +7,12 @@ on: jobs: build_masterci: name: build master-ci - env: - ImageOS: ubuntu24 - container: - image: ghcr.io/commaai/openpilot-base:latest runs-on: ubuntu-latest if: github.repository == 'commaai/openpilot' permissions: checks: read contents: write steps: - - name: Install wait-on-check-action dependencies - run: | - sudo apt-get update - sudo apt-get install -y libyaml-dev - name: Wait for green check mark if: ${{ github.event_name == 'schedule' }} uses: lewagon/wait-on-check-action@ccfb013c15c8afb7bf2b7c028fb74dc5a068cccc @@ -34,9 +26,6 @@ jobs: with: submodules: true fetch-depth: 0 - - name: Pull LFS - run: | - git config --global --add safe.directory '*' - git lfs pull + - uses: ./.github/workflows/setup-with-retry - name: Push master-ci run: BRANCH=__nightly release/build_stripped.sh diff --git a/Dockerfile.openpilot b/Dockerfile.openpilot index 106a06e3a2..72d874b022 100644 --- a/Dockerfile.openpilot +++ b/Dockerfile.openpilot @@ -1,14 +1,38 @@ -FROM ghcr.io/commaai/openpilot-base:latest +FROM ubuntu:24.04 ENV PYTHONUNBUFFERED=1 -ENV OPENPILOT_PATH=/home/batman/openpilot +ENV DEBIAN_FRONTEND=noninteractive +RUN apt-get update && \ + apt-get install -y --no-install-recommends sudo tzdata locales && \ + rm -rf /var/lib/apt/lists/* +RUN sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && locale-gen +ENV LANG=en_US.UTF-8 +ENV LANGUAGE=en_US:en +ENV LC_ALL=en_US.UTF-8 + +ENV NVIDIA_VISIBLE_DEVICES=all +ENV NVIDIA_DRIVER_CAPABILITIES=graphics,utility,compute + +ARG USER=batman +ARG USER_UID=1001 +RUN useradd -m -s /bin/bash -u $USER_UID $USER +RUN usermod -aG sudo $USER +RUN echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers +USER $USER + +ENV OPENPILOT_PATH=/home/$USER/openpilot RUN mkdir -p ${OPENPILOT_PATH} WORKDIR ${OPENPILOT_PATH} -COPY . ${OPENPILOT_PATH}/ +COPY --chown=$USER . ${OPENPILOT_PATH}/ -ENV UV_BIN="/home/batman/.local/bin/" -ENV PATH="$UV_BIN:$PATH" -RUN UV_PROJECT_ENVIRONMENT=$VIRTUAL_ENV uv run scons --cache-readonly -j$(nproc) +ENV UV_BIN="/home/$USER/.local/bin/" +ENV VIRTUAL_ENV=${OPENPILOT_PATH}/.venv +ENV PATH="$UV_BIN:$VIRTUAL_ENV/bin:$PATH" +RUN tools/setup_dependencies.sh && \ + sudo rm -rf /var/lib/apt/lists/* + +USER root +RUN git config --global --add safe.directory '*' diff --git a/Dockerfile.openpilot_base b/Dockerfile.openpilot_base deleted file mode 100644 index 6ea1450bee..0000000000 --- a/Dockerfile.openpilot_base +++ /dev/null @@ -1,42 +0,0 @@ -FROM ubuntu:24.04 - -ENV PYTHONUNBUFFERED=1 - -ENV DEBIAN_FRONTEND=noninteractive -RUN apt-get update && \ - apt-get install -y --no-install-recommends sudo tzdata locales ssh pulseaudio xvfb x11-xserver-utils gnome-screenshot python3-tk python3-dev && \ - rm -rf /var/lib/apt/lists/* - -RUN sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && locale-gen -ENV LANG=en_US.UTF-8 -ENV LANGUAGE=en_US:en -ENV LC_ALL=en_US.UTF-8 - -COPY tools/install_ubuntu_dependencies.sh /tmp/tools/ -RUN /tmp/tools/install_ubuntu_dependencies.sh && \ - rm -rf /var/lib/apt/lists/* /tmp/* - -ENV NVIDIA_VISIBLE_DEVICES=all -ENV NVIDIA_DRIVER_CAPABILITIES=graphics,utility,compute -ENV QTWEBENGINE_DISABLE_SANDBOX=1 - -RUN dbus-uuidgen > /etc/machine-id - -ARG USER=batman -ARG USER_UID=1001 -RUN useradd -m -s /bin/bash -u $USER_UID $USER -RUN usermod -aG sudo $USER -RUN echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers -USER $USER - -COPY --chown=$USER pyproject.toml uv.lock /home/$USER -COPY --chown=$USER tools/install_python_dependencies.sh /home/$USER/tools/ - -ENV VIRTUAL_ENV=/home/$USER/.venv -ENV PATH="$VIRTUAL_ENV/bin:$PATH" -RUN cd /home/$USER && \ - tools/install_python_dependencies.sh && \ - rm -rf tools/ pyproject.toml uv.lock .cache - -USER root -RUN sudo git config --global --add safe.directory /tmp/openpilot diff --git a/selfdrive/test/docker_build.sh b/selfdrive/test/docker_build.sh index 4d58a1507c..8d1fa82249 100755 --- a/selfdrive/test/docker_build.sh +++ b/selfdrive/test/docker_build.sh @@ -1,12 +1,14 @@ #!/usr/bin/env bash set -e -# To build sim and docs, you can run the following to mount the scons cache to the same place as in CI: -# mkdir -p .ci_cache/scons_cache -# sudo mount --bind /tmp/scons_cache/ .ci_cache/scons_cache - SCRIPT_DIR=$(dirname "$0") OPENPILOT_DIR=$SCRIPT_DIR/../../ + +DOCKER_IMAGE=openpilot +DOCKER_FILE=Dockerfile.openpilot +DOCKER_REGISTRY=ghcr.io/commaai +COMMIT_SHA=$(git rev-parse HEAD) + if [ -n "$TARGET_ARCHITECTURE" ]; then PLATFORM="linux/$TARGET_ARCHITECTURE" TAG_SUFFIX="-$TARGET_ARCHITECTURE" @@ -15,9 +17,11 @@ else TAG_SUFFIX="" fi -source $SCRIPT_DIR/docker_common.sh $1 "$TAG_SUFFIX" +LOCAL_TAG=$DOCKER_IMAGE$TAG_SUFFIX +REMOTE_TAG=$DOCKER_REGISTRY/$LOCAL_TAG +REMOTE_SHA_TAG=$DOCKER_REGISTRY/$LOCAL_TAG:$COMMIT_SHA -DOCKER_BUILDKIT=1 docker buildx build --provenance false --pull --platform $PLATFORM --load --cache-to type=inline --cache-from type=registry,ref=$REMOTE_TAG -t $DOCKER_IMAGE:latest -t $REMOTE_TAG -t $LOCAL_TAG -f $OPENPILOT_DIR/$DOCKER_FILE $OPENPILOT_DIR +DOCKER_BUILDKIT=1 docker buildx build --provenance false --pull --platform $PLATFORM --load -t $DOCKER_IMAGE:latest -t $REMOTE_TAG -t $LOCAL_TAG -f $OPENPILOT_DIR/$DOCKER_FILE $OPENPILOT_DIR if [ -n "$PUSH_IMAGE" ]; then docker push $REMOTE_TAG diff --git a/selfdrive/test/docker_common.sh b/selfdrive/test/docker_common.sh deleted file mode 100644 index 2887fff74b..0000000000 --- a/selfdrive/test/docker_common.sh +++ /dev/null @@ -1,18 +0,0 @@ -if [ "$1" = "base" ]; then - export DOCKER_IMAGE=openpilot-base - export DOCKER_FILE=Dockerfile.openpilot_base -elif [ "$1" = "prebuilt" ]; then - export DOCKER_IMAGE=openpilot-prebuilt - export DOCKER_FILE=Dockerfile.openpilot -else - echo "Invalid docker build image: '$1'" - exit 1 -fi - -export DOCKER_REGISTRY=ghcr.io/commaai -export COMMIT_SHA=$(git rev-parse HEAD) - -TAG_SUFFIX=$2 -LOCAL_TAG=$DOCKER_IMAGE$TAG_SUFFIX -REMOTE_TAG=$DOCKER_REGISTRY/$LOCAL_TAG -REMOTE_SHA_TAG=$DOCKER_REGISTRY/$LOCAL_TAG:$COMMIT_SHA diff --git a/tools/install_python_dependencies.sh b/tools/install_python_dependencies.sh deleted file mode 100755 index 4454845fcd..0000000000 --- a/tools/install_python_dependencies.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# Increase the pip timeout to handle TimeoutError -export PIP_DEFAULT_TIMEOUT=200 - -DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" -ROOT="$DIR"/../ -cd "$ROOT" - -if ! command -v "uv" > /dev/null 2>&1; then - echo "installing uv..." - curl -LsSf --retry 5 --retry-delay 5 --retry-all-errors https://astral.sh/uv/install.sh | sh - UV_BIN="$HOME/.local/bin" - PATH="$UV_BIN:$PATH" -fi - -echo "updating uv..." -# ok to fail, can also fail due to installing with brew -uv self update || true - -echo "installing python packages..." -uv sync --frozen --all-extras -source .venv/bin/activate - -if [[ "$(uname)" == 'Darwin' ]]; then - touch "$ROOT"/.env - echo "export OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES" >> "$ROOT"/.env -fi diff --git a/tools/install_ubuntu_dependencies.sh b/tools/install_ubuntu_dependencies.sh deleted file mode 100755 index 6793cf2d19..0000000000 --- a/tools/install_ubuntu_dependencies.sh +++ /dev/null @@ -1,99 +0,0 @@ -#!/usr/bin/env bash -set -e - -SUDO="" - -# Use sudo if not root -if [[ ! $(id -u) -eq 0 ]]; then - if [[ -z $(which sudo) ]]; then - echo "Please install sudo or run as root" - exit 1 - fi - SUDO="sudo" -fi - -# Check if stdin is open -if [ -t 0 ]; then - INTERACTIVE=1 -fi - -# Install common packages -function install_ubuntu_common_requirements() { - $SUDO apt-get update - - # normal stuff, mostly for the bare docker image - $SUDO apt-get install -y --no-install-recommends \ - ca-certificates \ - build-essential \ - curl \ - libssl-dev \ - libcurl4-openssl-dev \ - locales \ - git \ - xvfb - - $SUDO apt-get install -y --no-install-recommends \ - libgles2-mesa-dev \ - libjpeg-dev \ - libncurses5-dev \ - libzstd-dev \ - gettext -} - -# Install Ubuntu 24.04 LTS packages -function install_ubuntu_lts_latest_requirements() { - install_ubuntu_common_requirements - - $SUDO apt-get install -y --no-install-recommends \ - python3-dev \ - python3-venv -} - -# Detect OS using /etc/os-release file -if [ -f "/etc/os-release" ]; then - source /etc/os-release - case "$VERSION_CODENAME" in - "jammy" | "kinetic" | "noble") - install_ubuntu_lts_latest_requirements - ;; - *) - echo "$ID $VERSION_ID is unsupported. This setup script is written for Ubuntu 24.04." - read -p "Would you like to attempt installation anyway? " -n 1 -r - echo "" - if [[ ! $REPLY =~ ^[Yy]$ ]]; then - exit 1 - fi - install_ubuntu_lts_latest_requirements - esac - - if [[ -d "/etc/udev/rules.d/" ]]; then - # Setup jungle udev rules - $SUDO tee /etc/udev/rules.d/12-panda_jungle.rules > /dev/null < /dev/null < /dev/null </dev/null && pwd )" -ROOT="$(cd $DIR/../ && pwd)" - -if [[ $SHELL == "/bin/zsh" ]]; then - RC_FILE="$HOME/.zshrc" -elif [[ $SHELL == "/bin/bash" ]]; then - RC_FILE="$HOME/.bash_profile" -fi - -# install python dependencies -$DIR/install_python_dependencies.sh -echo "[ ] installed python dependencies t=$SECONDS" - -echo -echo "---- OPENPILOT SETUP DONE ----" -echo "Open a new shell or configure your active shell env by running:" -echo "source $RC_FILE" diff --git a/tools/op.sh b/tools/op.sh index 8c41926e0c..966d4ba433 100755 --- a/tools/op.sh +++ b/tools/op.sh @@ -216,11 +216,7 @@ function op_setup() { echo "Installing dependencies..." st="$(date +%s)" - if [[ "$OSTYPE" == "linux-gnu"* ]]; then - SETUP_SCRIPT="tools/ubuntu_setup.sh" - elif [[ "$OSTYPE" == "darwin"* ]]; then - SETUP_SCRIPT="tools/mac_setup.sh" - fi + SETUP_SCRIPT="tools/setup_dependencies.sh" if ! $OPENPILOT_ROOT/$SETUP_SCRIPT; then echo -e " ↳ [${RED}✗${NC}] Dependencies installation failed!" loge "ERROR_DEPENDENCIES_INSTALLATION" diff --git a/tools/setup_dependencies.sh b/tools/setup_dependencies.sh new file mode 100755 index 0000000000..6dd1f294c1 --- /dev/null +++ b/tools/setup_dependencies.sh @@ -0,0 +1,137 @@ +#!/usr/bin/env bash +set -e + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" +ROOT="$(cd "$DIR/../" && pwd)" + +function install_ubuntu_deps() { + SUDO="" + + if [[ ! $(id -u) -eq 0 ]]; then + if [[ -z $(which sudo) ]]; then + echo "Please install sudo or run as root" + exit 1 + fi + SUDO="sudo" + fi + + # Detect OS using /etc/os-release file + if [ -f "/etc/os-release" ]; then + source /etc/os-release + case "$VERSION_CODENAME" in + "jammy" | "kinetic" | "noble") + ;; + *) + echo "$ID $VERSION_ID is unsupported. This setup script is written for Ubuntu 24.04." + read -p "Would you like to attempt installation anyway? " -n 1 -r + echo "" + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + exit 1 + fi + ;; + esac + else + echo "No /etc/os-release in the system. Make sure you're running on Ubuntu, or similar." + exit 1 + fi + + $SUDO apt-get update + + # normal stuff, mostly for the bare docker image + $SUDO apt-get install -y --no-install-recommends \ + ca-certificates \ + build-essential \ + curl \ + libssl-dev \ + libcurl4-openssl-dev \ + locales \ + git \ + xvfb + + $SUDO apt-get install -y --no-install-recommends \ + python3-dev \ + libgles2-mesa-dev \ + libjpeg-dev \ + libncurses5-dev \ + libzstd-dev \ + gettext + + if [[ -d "/etc/udev/rules.d/" ]]; then + # Setup jungle udev rules + $SUDO tee /etc/udev/rules.d/12-panda_jungle.rules > /dev/null < /dev/null < /dev/null < /dev/null 2>&1; then + echo "installing uv..." + curl -LsSf --retry 5 --retry-delay 5 --retry-all-errors https://astral.sh/uv/install.sh | sh + UV_BIN="$HOME/.local/bin" + PATH="$UV_BIN:$PATH" + fi + + echo "updating uv..." + # ok to fail, can also fail due to installing with brew + uv self update || true + + echo "installing python packages..." + uv sync --frozen --all-extras + source .venv/bin/activate + + if [[ "$(uname)" == 'Darwin' ]]; then + touch "$ROOT"/.env + echo "export OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES" >> "$ROOT"/.env + fi +} + +# --- Main --- + +if [[ "$OSTYPE" == "linux-gnu"* ]]; then + install_ubuntu_deps + echo "[ ] installed system dependencies t=$SECONDS" +elif [[ "$OSTYPE" == "darwin"* ]]; then + if [[ $SHELL == "/bin/zsh" ]]; then + RC_FILE="$HOME/.zshrc" + elif [[ $SHELL == "/bin/bash" ]]; then + RC_FILE="$HOME/.bash_profile" + fi +fi + +if [ -f "$ROOT/pyproject.toml" ]; then + install_python_deps + echo "[ ] installed python dependencies t=$SECONDS" +fi + +if [[ "$OSTYPE" == "darwin"* ]] && [[ -n "${RC_FILE:-}" ]]; then + echo + echo "---- OPENPILOT SETUP DONE ----" + echo "Open a new shell or configure your active shell env by running:" + echo "source $RC_FILE" +fi diff --git a/tools/ubuntu_setup.sh b/tools/ubuntu_setup.sh deleted file mode 100755 index be4cfb68fa..0000000000 --- a/tools/ubuntu_setup.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env bash - -set -e - -DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" - -# NOTE: this is used in a docker build, so do not run any scripts here. - -"$DIR"/install_ubuntu_dependencies.sh -"$DIR"/install_python_dependencies.sh From 19459d2b2e608324c2c010c1486325644530e535 Mon Sep 17 00:00:00 2001 From: Trey Moen <50057480+greatgitsby@users.noreply.github.com> Date: Mon, 23 Feb 2026 19:25:52 -0700 Subject: [PATCH 171/311] feat(lpa): at client + list profiles (#37337) * Reapply "feat(lpa): `at` client + list profiles (#37271)" (#37322) This reverts commit ddf8abc14a272d5e95ffd2569d1ddfd2195c56f5. * lpa: fall back to ModemManager D-Bus when serial port unavailable On older devices, ModemManager still claims /dev/ttyUSB2, so the direct serial open fails. Try serial first; if it can't be acquired, transparently route AT commands through MM's D-Bus Command() interface. Co-authored-by: Cursor * lpa: add serial/dbus transport labels to debug logs Co-authored-by: Cursor * no * lint * here * const --------- Co-authored-by: Cursor --- pyproject.toml | 2 +- system/hardware/tici/lpa.py | 261 +++++++++++++++++++++++++++++++++++- 2 files changed, 260 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 03ab5902ff..da36ea9df4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -140,7 +140,7 @@ testpaths = [ [tool.codespell] quiet-level = 3 # if you've got a short variable name that's getting flagged, add it here -ignore-words-list = "bu,ro,te,ue,alo,hda,ois,nam,nams,ned,som,parm,setts,inout,warmup,bumb,nd,sie,preints,whit,indexIn,ws,uint,grey,deque,stdio,amin,BA,LITE,atEnd,UIs,errorString,arange,FocusIn,od,tim,relA,hist,copyable,jupyter,thead,TGE,abl,lite" +ignore-words-list = "bu,ro,te,ue,alo,hda,ois,nam,nams,ned,som,parm,setts,inout,warmup,bumb,nd,sie,preints,whit,indexIn,ws,uint,grey,deque,stdio,amin,BA,LITE,atEnd,UIs,errorString,arange,FocusIn,od,tim,relA,hist,copyable,jupyter,thead,TGE,abl,lite,ser" builtin = "clear,rare,informal,code,names,en-GB_to_en-US" skip = "./third_party/*, ./tinygrad/*, ./tinygrad_repo/*, ./msgq/*, ./panda/*, ./opendbc/*, ./opendbc_repo/*, ./rednose/*, ./rednose_repo/*, ./teleoprtc/*, ./teleoprtc_repo/*, *.po, uv.lock, *.onnx, ./cereal/gen/*, */c_generated_code/*, docs/assets/*, tools/plotjuggler/layouts/*, selfdrive/assets/offroad/mici_fcc.html" diff --git a/system/hardware/tici/lpa.py b/system/hardware/tici/lpa.py index 9bd9d8c7b0..2e7e6a0ba9 100644 --- a/system/hardware/tici/lpa.py +++ b/system/hardware/tici/lpa.py @@ -1,12 +1,269 @@ +# SGP.22 v2.3: https://www.gsma.com/solutions-and-impact/technologies/esim/wp-content/uploads/2021/07/SGP.22-v2.3.pdf + +import atexit +import base64 +import math +import os +import serial +import sys + +from collections.abc import Generator + from openpilot.system.hardware.base import LPABase, Profile +DEFAULT_DEVICE = "/dev/ttyUSB2" +DEFAULT_BAUD = 9600 +DEFAULT_TIMEOUT = 5.0 +# https://euicc-manual.osmocom.org/docs/lpa/applet-id/ +ISDR_AID = "A0000005591010FFFFFFFF8900000100" +MM = "org.freedesktop.ModemManager1" +MM_MODEM = MM + ".Modem" +ES10X_MSS = 120 +DEBUG = os.environ.get("DEBUG") == "1" + +# TLV Tags +TAG_ICCID = 0x5A +TAG_PROFILE_INFO_LIST = 0xBF2D + +STATE_LABELS = {0: "disabled", 1: "enabled", 255: "unknown"} +ICON_LABELS = {0: "jpeg", 1: "png", 255: "unknown"} +CLASS_LABELS = {0: "test", 1: "provisioning", 2: "operational", 255: "unknown"} + + +def b64e(data: bytes) -> str: + return base64.b64encode(data).decode("ascii") + + +class AtClient: + def __init__(self, device: str, baud: int, timeout: float, debug: bool) -> None: + self.debug = debug + self.channel: str | None = None + self._timeout = timeout + self._serial: serial.Serial | None = None + try: + self._serial = serial.Serial(device, baudrate=baud, timeout=timeout) + self._serial.reset_input_buffer() + except (serial.SerialException, PermissionError, OSError): + pass + + def close(self) -> None: + try: + if self.channel: + self.query(f"AT+CCHC={self.channel}") + self.channel = None + finally: + if self._serial: + self._serial.close() + + def _send(self, cmd: str) -> None: + if self.debug: + print(f"SER >> {cmd}", file=sys.stderr) + self._serial.write((cmd + "\r").encode("ascii")) + + def _expect(self) -> list[str]: + lines: list[str] = [] + while True: + raw = self._serial.readline() + if not raw: + raise TimeoutError("AT command timed out") + line = raw.decode(errors="ignore").strip() + if not line: + continue + if self.debug: + print(f"SER << {line}", file=sys.stderr) + if line == "OK": + return lines + if line == "ERROR" or line.startswith("+CME ERROR"): + raise RuntimeError(f"AT command failed: {line}") + lines.append(line) + + def _get_modem(self): + import dbus + bus = dbus.SystemBus() + mm = bus.get_object(MM, '/org/freedesktop/ModemManager1') + objects = mm.GetManagedObjects(dbus_interface="org.freedesktop.DBus.ObjectManager", timeout=self._timeout) + modem_path = list(objects.keys())[0] + return bus.get_object(MM, modem_path) + + def _dbus_query(self, cmd: str) -> list[str]: + if self.debug: + print(f"DBUS >> {cmd}", file=sys.stderr) + try: + result = str(self._get_modem().Command(cmd, math.ceil(self._timeout), dbus_interface=MM_MODEM, timeout=self._timeout)) + except Exception as e: + raise RuntimeError(f"AT command failed: {e}") from e + lines = [line.strip() for line in result.splitlines() if line.strip()] + if self.debug: + for line in lines: + print(f"DBUS << {line}", file=sys.stderr) + return lines + + def query(self, cmd: str) -> list[str]: + if self._serial: + self._send(cmd) + return self._expect() + return self._dbus_query(cmd) + + def open_isdr(self) -> None: + # close any stale logical channel from a previous crashed session + try: + self.query("AT+CCHC=1") + except RuntimeError: + pass + for line in self.query(f'AT+CCHO="{ISDR_AID}"'): + if line.startswith("+CCHO:") and (ch := line.split(":", 1)[1].strip()): + self.channel = ch + return + raise RuntimeError("Failed to open ISD-R application") + + def send_apdu(self, apdu: bytes) -> tuple[bytes, int, int]: + if not self.channel: + raise RuntimeError("Logical channel is not open") + hex_payload = apdu.hex().upper() + for line in self.query(f'AT+CGLA={self.channel},{len(hex_payload)},"{hex_payload}"'): + if line.startswith("+CGLA:"): + parts = line.split(":", 1)[1].split(",", 1) + if len(parts) == 2: + data = bytes.fromhex(parts[1].strip().strip('"')) + if len(data) >= 2: + return data[:-2], data[-2], data[-1] + raise RuntimeError("Missing +CGLA response") + + +# --- TLV utilities --- + +def iter_tlv(data: bytes, with_positions: bool = False) -> Generator: + idx, length = 0, len(data) + while idx < length: + start_pos = idx + tag = data[idx] + idx += 1 + if tag & 0x1F == 0x1F: # Multi-byte tag + tag_value = tag + while idx < length: + next_byte = data[idx] + idx += 1 + tag_value = (tag_value << 8) | next_byte + if not (next_byte & 0x80): + break + else: + tag_value = tag + if idx >= length: + break + size = data[idx] + idx += 1 + if size & 0x80: # Multi-byte length + num_bytes = size & 0x7F + if idx + num_bytes > length: + break + size = int.from_bytes(data[idx : idx + num_bytes], "big") + idx += num_bytes + if idx + size > length: + break + value = data[idx : idx + size] + idx += size + yield (tag_value, value, start_pos, idx) if with_positions else (tag_value, value) + + +def find_tag(data: bytes, target: int) -> bytes | None: + return next((v for t, v in iter_tlv(data) if t == target), None) + + +def tbcd_to_string(raw: bytes) -> str: + return "".join(str(n) for b in raw for n in (b & 0x0F, b >> 4) if n <= 9) + + +# Profile field decoders: TLV tag -> (field_name, decoder) +_PROFILE_FIELDS = { + TAG_ICCID: ("iccid", tbcd_to_string), + 0x4F: ("isdpAid", lambda v: v.hex().upper()), + 0x9F70: ("profileState", lambda v: STATE_LABELS.get(v[0], "unknown")), + 0x90: ("profileNickname", lambda v: v.decode("utf-8", errors="ignore") or None), + 0x91: ("serviceProviderName", lambda v: v.decode("utf-8", errors="ignore") or None), + 0x92: ("profileName", lambda v: v.decode("utf-8", errors="ignore") or None), + 0x93: ("iconType", lambda v: ICON_LABELS.get(v[0], "unknown")), + 0x94: ("icon", b64e), + 0x95: ("profileClass", lambda v: CLASS_LABELS.get(v[0], "unknown")), +} + + +def _decode_profile_fields(data: bytes) -> dict: + """Parse known profile metadata TLV fields into a dict.""" + result = {} + for tag, value in iter_tlv(data): + if (field := _PROFILE_FIELDS.get(tag)): + result[field[0]] = field[1](value) + return result + + +# --- ES10x command transport --- + +def es10x_command(client: AtClient, data: bytes) -> bytes: + response = bytearray() + sequence = 0 + offset = 0 + while offset < len(data): + chunk = data[offset : offset + ES10X_MSS] + offset += len(chunk) + is_last = offset == len(data) + apdu = bytes([0x80, 0xE2, 0x91 if is_last else 0x11, sequence & 0xFF, len(chunk)]) + chunk + segment, sw1, sw2 = client.send_apdu(apdu) + response.extend(segment) + while True: + if sw1 == 0x61: # More data available + segment, sw1, sw2 = client.send_apdu(bytes([0x80, 0xC0, 0x00, 0x00, sw2 or 0])) + response.extend(segment) + continue + if (sw1 & 0xF0) == 0x90: + break + raise RuntimeError(f"APDU failed with SW={sw1:02X}{sw2:02X}") + sequence += 1 + return bytes(response) + + +# --- Profile operations --- + +def decode_profiles(blob: bytes) -> list[dict]: + root = find_tag(blob, TAG_PROFILE_INFO_LIST) + if root is None: + raise RuntimeError("Missing ProfileInfoList") + list_ok = find_tag(root, 0xA0) + if list_ok is None: + return [] + defaults = {name: None for name, _ in _PROFILE_FIELDS.values()} + return [{**defaults, **_decode_profile_fields(value)} for tag, value in iter_tlv(list_ok) if tag == 0xE3] + + +def list_profiles(client: AtClient) -> list[dict]: + return decode_profiles(es10x_command(client, TAG_PROFILE_INFO_LIST.to_bytes(2, "big") + b"\x00")) + + class TiciLPA(LPABase): + _instance = None + + def __new__(cls): + if cls._instance is None: + cls._instance = super().__new__(cls) + return cls._instance + def __init__(self): - pass + if hasattr(self, '_client'): + return + self._client = AtClient(DEFAULT_DEVICE, DEFAULT_BAUD, DEFAULT_TIMEOUT, debug=DEBUG) + self._client.open_isdr() + atexit.register(self._client.close) def list_profiles(self) -> list[Profile]: - return [] + return [ + Profile( + iccid=p.get("iccid", ""), + nickname=p.get("profileNickname") or "", + enabled=p.get("profileState") == "enabled", + provider=p.get("serviceProviderName") or "", + ) + for p in list_profiles(self._client) + ] def get_active_profile(self) -> Profile | None: return None From 111e8897be79f4c99734e77d0c389612f14a8e50 Mon Sep 17 00:00:00 2001 From: Nayan Date: Mon, 23 Feb 2026 22:23:08 -0500 Subject: [PATCH 172/311] models: fix default & index "0" (#1718) * default be gone * zero is the most significant number, don't ignore it. * thanks sentry!! --- common/params_keys.h | 2 +- sunnypilot/models/manager.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/common/params_keys.h b/common/params_keys.h index 6f27d9582d..8f3ee858f0 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -190,7 +190,7 @@ inline static std::unordered_map keys = { // Model Manager params {"ModelManager_ActiveBundle", {PERSISTENT, JSON}}, {"ModelManager_ClearCache", {CLEAR_ON_MANAGER_START, BOOL}}, - {"ModelManager_DownloadIndex", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, INT, "0"}}, + {"ModelManager_DownloadIndex", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, INT}}, {"ModelManager_Favs", {PERSISTENT | BACKUP, STRING}}, {"ModelManager_LastSyncTime", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, INT, "0"}}, {"ModelManager_ModelsCache", {PERSISTENT | BACKUP, JSON}}, diff --git a/sunnypilot/models/manager.py b/sunnypilot/models/manager.py index 2d3d670bfd..8fee0798b6 100644 --- a/sunnypilot/models/manager.py +++ b/sunnypilot/models/manager.py @@ -171,7 +171,7 @@ class ModelManagerSP: self.available_models = self.model_fetcher.get_available_bundles() self.active_bundle = get_active_bundle(self.params) - if index_to_download := self.params.get("ModelManager_DownloadIndex"): + if (index_to_download := self.params.get("ModelManager_DownloadIndex")) is not None: if model_to_download := next((model for model in self.available_models if model.index == index_to_download), None): try: self.download(model_to_download, Paths.model_root()) From 91930c2d0deb0e9d49e874ff8a49c8e04c69896e Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Mon, 23 Feb 2026 19:37:59 -0800 Subject: [PATCH 173/311] UnifiedLabel: add set_line_height --- system/ui/widgets/label.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/system/ui/widgets/label.py b/system/ui/widgets/label.py index 865c8b38c3..8ed9ec62f5 100644 --- a/system/ui/widgets/label.py +++ b/system/ui/widgets/label.py @@ -489,6 +489,13 @@ class UnifiedLabel(Widget): self._spacing_pixels = self._font_size * letter_spacing self._cached_text = None # Invalidate cache + def set_line_height(self, line_height: float): + """Update line height (multiplier, e.g., 1.0 = default).""" + new_line_height = line_height * 0.9 + if self._line_height != new_line_height: + self._line_height = new_line_height + self._cached_text = None # Invalidate cache (affects total height) + def set_font_weight(self, font_weight: FontWeight): """Update the font weight.""" if self._font_weight != font_weight: From ed8d1a65c3a86b237af4ecad3626c1379aec6dad Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Mon, 23 Feb 2026 19:39:30 -0800 Subject: [PATCH 174/311] BigCircleButton: new pressed image --- selfdrive/assets/icons_mici/buttons/button_circle_hover.png | 3 --- selfdrive/assets/icons_mici/buttons/button_circle_pressed.png | 3 +++ selfdrive/ui/mici/widgets/button.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) delete mode 100644 selfdrive/assets/icons_mici/buttons/button_circle_hover.png create mode 100644 selfdrive/assets/icons_mici/buttons/button_circle_pressed.png diff --git a/selfdrive/assets/icons_mici/buttons/button_circle_hover.png b/selfdrive/assets/icons_mici/buttons/button_circle_hover.png deleted file mode 100644 index 5cae152106..0000000000 --- a/selfdrive/assets/icons_mici/buttons/button_circle_hover.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:20024203288f144633014422e16119278477099f24fba5c155a804a1864a26b4 -size 7511 diff --git a/selfdrive/assets/icons_mici/buttons/button_circle_pressed.png b/selfdrive/assets/icons_mici/buttons/button_circle_pressed.png new file mode 100644 index 0000000000..9db0c2cd81 --- /dev/null +++ b/selfdrive/assets/icons_mici/buttons/button_circle_pressed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:70d4236bcfd3aa8f100b81179c1e0f193c6ffbd84769c4a516be4381e62b270a +size 18666 diff --git a/selfdrive/ui/mici/widgets/button.py b/selfdrive/ui/mici/widgets/button.py index f81b7f0df5..5f76535ef7 100644 --- a/selfdrive/ui/mici/widgets/button.py +++ b/selfdrive/ui/mici/widgets/button.py @@ -43,7 +43,7 @@ class BigCircleButton(Widget): self._txt_btn_disabled_bg = gui_app.texture("icons_mici/buttons/button_circle_disabled.png", 180, 180) self._txt_btn_bg = gui_app.texture("icons_mici/buttons/button_circle.png", 180, 180) - self._txt_btn_pressed_bg = gui_app.texture("icons_mici/buttons/button_circle_hover.png", 180, 180) + self._txt_btn_pressed_bg = gui_app.texture("icons_mici/buttons/button_circle_pressed.png", 180, 180) self._txt_btn_red_bg = gui_app.texture("icons_mici/buttons/button_circle_red.png", 180, 180) self._txt_btn_red_pressed_bg = gui_app.texture("icons_mici/buttons/button_circle_red_hover.png", 180, 180) From c5b65d072d9935dfaa4c17f43cb7ad6e70139974 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Mon, 23 Feb 2026 19:53:07 -0800 Subject: [PATCH 175/311] no more xset --- selfdrive/test/setup_xvfb.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/selfdrive/test/setup_xvfb.sh b/selfdrive/test/setup_xvfb.sh index b185e2b431..c1b74a850e 100755 --- a/selfdrive/test/setup_xvfb.sh +++ b/selfdrive/test/setup_xvfb.sh @@ -19,5 +19,4 @@ do done touch ~/.Xauthority -export XDG_SESSION_TYPE="x11" -xset -q \ No newline at end of file +export XDG_SESSION_TYPE="x11" \ No newline at end of file From 0e127cf88b03d18d5e9869022109989b32a9b0fa Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Mon, 23 Feb 2026 20:13:17 -0800 Subject: [PATCH 176/311] WifiManager: guard init wifi state (#37366) guard init wifi state --- system/ui/lib/wifi_manager.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index 4f02b70625..110fd72181 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -223,6 +223,10 @@ class WifiManager: def _init_wifi_state(self, block: bool = True): def worker(): + if self._wifi_device is None: + cloudlog.warning("No WiFi device found") + return + dev_addr = DBusAddress(self._wifi_device, bus_name=NM, interface=NM_DEVICE_IFACE) dev_state = self._router_main.send_and_get_reply(Properties(dev_addr).get('State')).body[0][1] From 12f923445b4b4d61069d19b74b38881b106d2ce5 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Mon, 23 Feb 2026 20:14:06 -0800 Subject: [PATCH 177/311] Slider: call confirm callback after set state in case confirm callback resets the state immediately --- system/ui/widgets/slider.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/ui/widgets/slider.py b/system/ui/widgets/slider.py index 455cdeef71..0cdc513a03 100644 --- a/system/ui/widgets/slider.py +++ b/system/ui/widgets/slider.py @@ -107,8 +107,8 @@ class SmallSlider(Widget): # activate once animation completes, small threshold for small floats if self._scroll_x_circle_filter.x < (activated_pos + 1): if not self._confirm_callback_called and (rl.get_time() - self._confirmed_time) >= self.CONFIRM_DELAY: - self._on_confirm() self._confirm_callback_called = True + self._on_confirm() elif not self._is_dragging_circle: # reset back to right From 8543afc78ab883d3c2d3aba5e757cc94cb3f22b4 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Mon, 23 Feb 2026 20:19:41 -0800 Subject: [PATCH 178/311] Slider: add pressed state (#37365) * sliders have pressed state * more * new and pressed setup sliders --- .../assets/icons_mici/buttons/button_circle_red_hover.png | 3 --- .../icons_mici/buttons/button_circle_red_pressed.png | 3 +++ .../slider_black_rounded_rectangle_pressed.png | 3 +++ .../setup/small_slider/slider_green_rounded_rectangle.png | 4 ++-- .../slider_green_rounded_rectangle_pressed.png | 3 +++ .../setup/small_slider/slider_red_circle_pressed.png | 3 +++ selfdrive/ui/mici/widgets/button.py | 2 +- system/ui/widgets/slider.py | 7 ++++++- 8 files changed, 21 insertions(+), 7 deletions(-) delete mode 100644 selfdrive/assets/icons_mici/buttons/button_circle_red_hover.png create mode 100644 selfdrive/assets/icons_mici/buttons/button_circle_red_pressed.png create mode 100644 selfdrive/assets/icons_mici/setup/small_slider/slider_black_rounded_rectangle_pressed.png create mode 100644 selfdrive/assets/icons_mici/setup/small_slider/slider_green_rounded_rectangle_pressed.png create mode 100644 selfdrive/assets/icons_mici/setup/small_slider/slider_red_circle_pressed.png diff --git a/selfdrive/assets/icons_mici/buttons/button_circle_red_hover.png b/selfdrive/assets/icons_mici/buttons/button_circle_red_hover.png deleted file mode 100644 index 3696334d5e..0000000000 --- a/selfdrive/assets/icons_mici/buttons/button_circle_red_hover.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:279c1d8f95eb9f4a3058dff76b0f316ce9eef7bc8f4296936ad25fd08703ce13 -size 10380 diff --git a/selfdrive/assets/icons_mici/buttons/button_circle_red_pressed.png b/selfdrive/assets/icons_mici/buttons/button_circle_red_pressed.png new file mode 100644 index 0000000000..e61a678c1c --- /dev/null +++ b/selfdrive/assets/icons_mici/buttons/button_circle_red_pressed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ed07f72339cf1c3926a2cb7314f9baa099bcdb3f8bc89a9084661b71334b0526 +size 32599 diff --git a/selfdrive/assets/icons_mici/setup/small_slider/slider_black_rounded_rectangle_pressed.png b/selfdrive/assets/icons_mici/setup/small_slider/slider_black_rounded_rectangle_pressed.png new file mode 100644 index 0000000000..470bfc50c0 --- /dev/null +++ b/selfdrive/assets/icons_mici/setup/small_slider/slider_black_rounded_rectangle_pressed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d1dd642ae4708cc7a837e8ef8b4c75f578654d241f8c854249c2b1ade640ceca +size 14058 diff --git a/selfdrive/assets/icons_mici/setup/small_slider/slider_green_rounded_rectangle.png b/selfdrive/assets/icons_mici/setup/small_slider/slider_green_rounded_rectangle.png index 9ebff76b50..88e6985f12 100644 --- a/selfdrive/assets/icons_mici/setup/small_slider/slider_green_rounded_rectangle.png +++ b/selfdrive/assets/icons_mici/setup/small_slider/slider_green_rounded_rectangle.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bcd08444c77b3e559876eeb88d17808f72496adc26e27c3c21c00ff410879447 -size 10966 +oid sha256:5ba98ab2b75f0c1f8fdffb9eab0a742645b80ef4ca404c007f374a5e0fd48d8c +size 10254 diff --git a/selfdrive/assets/icons_mici/setup/small_slider/slider_green_rounded_rectangle_pressed.png b/selfdrive/assets/icons_mici/setup/small_slider/slider_green_rounded_rectangle_pressed.png new file mode 100644 index 0000000000..999cdafcc7 --- /dev/null +++ b/selfdrive/assets/icons_mici/setup/small_slider/slider_green_rounded_rectangle_pressed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4545fdbaf67402b28b18644a7353a0620250ece6416c1b0ce0e27c758817b042 +size 26729 diff --git a/selfdrive/assets/icons_mici/setup/small_slider/slider_red_circle_pressed.png b/selfdrive/assets/icons_mici/setup/small_slider/slider_red_circle_pressed.png new file mode 100644 index 0000000000..eea6eded86 --- /dev/null +++ b/selfdrive/assets/icons_mici/setup/small_slider/slider_red_circle_pressed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a804da77b268f0a625f93949642ae74cdfe5b5caa5baea1c52c4605ae25c80e4 +size 12916 diff --git a/selfdrive/ui/mici/widgets/button.py b/selfdrive/ui/mici/widgets/button.py index 5f76535ef7..20da5a06cc 100644 --- a/selfdrive/ui/mici/widgets/button.py +++ b/selfdrive/ui/mici/widgets/button.py @@ -46,7 +46,7 @@ class BigCircleButton(Widget): self._txt_btn_pressed_bg = gui_app.texture("icons_mici/buttons/button_circle_pressed.png", 180, 180) self._txt_btn_red_bg = gui_app.texture("icons_mici/buttons/button_circle_red.png", 180, 180) - self._txt_btn_red_pressed_bg = gui_app.texture("icons_mici/buttons/button_circle_red_hover.png", 180, 180) + self._txt_btn_red_pressed_bg = gui_app.texture("icons_mici/buttons/button_circle_red_pressed.png", 180, 180) def _draw_content(self, btn_y: float): # draw icon diff --git a/system/ui/widgets/slider.py b/system/ui/widgets/slider.py index 0cdc513a03..734c5b5d65 100644 --- a/system/ui/widgets/slider.py +++ b/system/ui/widgets/slider.py @@ -42,6 +42,7 @@ class SmallSlider(Widget): self._bg_txt = gui_app.texture("icons_mici/setup/small_slider/slider_bg.png", 316, 100) self._circle_bg_txt = gui_app.texture("icons_mici/setup/small_slider/slider_red_circle.png", 100, 100) + self._circle_bg_pressed_txt = gui_app.texture("icons_mici/setup/small_slider/slider_red_circle_pressed.png", 100, 100) self._circle_arrow_txt = gui_app.texture("icons_mici/setup/small_slider/slider_arrow.png", 37, 32) @property @@ -140,7 +141,8 @@ class SmallSlider(Widget): self._label.render(label_rect) # circle and arrow - rl.draw_texture_ex(self._circle_bg_txt, rl.Vector2(btn_x, btn_y), 0.0, 1.0, white) + circle_bg_txt = self._circle_bg_pressed_txt if self._is_dragging_circle or self._confirmed_time > 0 else self._circle_bg_txt + rl.draw_texture_ex(circle_bg_txt, rl.Vector2(btn_x, btn_y), 0.0, 1.0, white) arrow_x = btn_x + (self._circle_bg_txt.width - self._circle_arrow_txt.width) / 2 arrow_y = btn_y + (self._circle_bg_txt.height - self._circle_arrow_txt.height) / 2 @@ -158,6 +160,7 @@ class LargerSlider(SmallSlider): self._bg_txt = gui_app.texture("icons_mici/setup/small_slider/slider_bg_larger.png", 520, 115) circle_fn = "slider_green_rounded_rectangle" if self._green else "slider_black_rounded_rectangle" self._circle_bg_txt = gui_app.texture(f"icons_mici/setup/small_slider/{circle_fn}.png", 180, 115) + self._circle_bg_pressed_txt = gui_app.texture(f"icons_mici/setup/small_slider/{circle_fn}_pressed.png", 180, 115) self._circle_arrow_txt = gui_app.texture("icons_mici/setup/small_slider/slider_arrow.png", 64, 55) @@ -174,6 +177,7 @@ class BigSlider(SmallSlider): self._bg_txt = gui_app.texture("icons_mici/buttons/slider_bg.png", 520, 180) self._circle_bg_txt = gui_app.texture("icons_mici/buttons/button_circle.png", 180, 180) + self._circle_bg_pressed_txt = gui_app.texture("icons_mici/buttons/button_circle_pressed.png", 180, 180) self._circle_arrow_txt = self._icon @@ -183,4 +187,5 @@ class RedBigSlider(BigSlider): self._bg_txt = gui_app.texture("icons_mici/buttons/slider_bg.png", 520, 180) self._circle_bg_txt = gui_app.texture("icons_mici/buttons/button_circle_red.png", 180, 180) + self._circle_bg_pressed_txt = gui_app.texture("icons_mici/buttons/button_circle_red_pressed.png", 180, 180) self._circle_arrow_txt = self._icon From bd3b7a1d87da44c360e40c9a35b9af4347a99332 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Mon, 23 Feb 2026 20:20:44 -0800 Subject: [PATCH 179/311] Scroller: preserve original touch valid callback (#37363) preserve --- system/ui/widgets/scroller.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/system/ui/widgets/scroller.py b/system/ui/widgets/scroller.py index 0543b1395e..7eff8ae594 100644 --- a/system/ui/widgets/scroller.py +++ b/system/ui/widgets/scroller.py @@ -141,8 +141,12 @@ class Scroller(Widget): def add_widget(self, item: Widget) -> None: self._items.append(item) + + # preserve original touch valid callback + original_touch_valid_callback = item._touch_valid_callback item.set_touch_valid_callback(lambda: self.scroll_panel.is_touch_valid() and self.enabled and self._scrolling_to is None - and not self.moving_items) + and not self.moving_items and (original_touch_valid_callback() if + original_touch_valid_callback else True)) def set_scrolling_enabled(self, enabled: bool | Callable[[], bool]) -> None: """Set whether scrolling is enabled (does not affect widget enabled state).""" From 76a552715f65fa394fc8b542326e22845aa80c6c Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Mon, 23 Feb 2026 20:45:22 -0800 Subject: [PATCH 180/311] ui: move shake into BigButton (#37364) * move * fix --- .../mici/layouts/settings/network/wifi_ui.py | 17 +---------------- selfdrive/ui/mici/widgets/button.py | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py index c9bc54a8b5..2efc45f46a 100644 --- a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py +++ b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py @@ -114,7 +114,6 @@ class WifiButton(BigButton): self._network_missing = False self._network_forgetting = False self._wrong_password = False - self._shake_start: float | None = None def update_network(self, network: Network): self._network = network @@ -144,7 +143,7 @@ class WifiButton(BigButton): def set_wrong_password(self): self._wrong_password = True - self._shake_start = rl.get_time() + self.trigger_shake() @property def network(self) -> Network: @@ -165,20 +164,6 @@ class WifiButton(BigButton): def _get_label_font_size(self): return 48 - @property - def _shake_offset(self) -> float: - SHAKE_DURATION = 0.5 - SHAKE_AMPLITUDE = 24.0 - SHAKE_FREQUENCY = 32.0 - t = rl.get_time() - (self._shake_start or 0.0) - if t > SHAKE_DURATION: - return 0.0 - decay = 1.0 - t / SHAKE_DURATION - return decay * SHAKE_AMPLITUDE * math.sin(t * SHAKE_FREQUENCY) - - def set_position(self, x: float, y: float) -> None: - super().set_position(x + self._shake_offset, y) - def _draw_content(self, btn_y: float): self._label.set_color(LABEL_COLOR) label_rect = rl.Rectangle(self._rect.x + self.LABEL_PADDING, btn_y + LABEL_VERTICAL_PADDING, diff --git a/selfdrive/ui/mici/widgets/button.py b/selfdrive/ui/mici/widgets/button.py index 20da5a06cc..14786245d7 100644 --- a/selfdrive/ui/mici/widgets/button.py +++ b/selfdrive/ui/mici/widgets/button.py @@ -1,3 +1,4 @@ +import math import pyray as rl from typing import Union from enum import Enum @@ -115,6 +116,7 @@ class BigButton(Widget): self.set_icon(icon) self._scale_filter = BounceFilter(1.0, 0.1, 1 / gui_app.target_fps) + self._shake_start: float | None = None self._rotate_icon_t: float | None = None @@ -174,6 +176,23 @@ class BigButton(Widget): def get_text(self): return self.text + def trigger_shake(self): + self._shake_start = rl.get_time() + + @property + def _shake_offset(self) -> float: + SHAKE_DURATION = 0.5 + SHAKE_AMPLITUDE = 24.0 + SHAKE_FREQUENCY = 32.0 + t = rl.get_time() - (self._shake_start or 0.0) + if t > SHAKE_DURATION: + return 0.0 + decay = 1.0 - t / SHAKE_DURATION + return decay * SHAKE_AMPLITUDE * math.sin(t * SHAKE_FREQUENCY) + + def set_position(self, x: float, y: float) -> None: + super().set_position(x + self._shake_offset, y) + def _draw_content(self, btn_y: float): # LABEL ------------------------------------------------------------------ label_x = self._rect.x + LABEL_HORIZONTAL_PADDING From 21b8189a45a653e1df7c54a81a6176510b45019d Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Mon, 23 Feb 2026 20:45:41 -0800 Subject: [PATCH 181/311] ui: support asset flip (#37367) * support asset flip * clean up --- .../assets/icons_mici/onroad/blind_spot_right.png | 3 --- .../assets/icons_mici/onroad/turn_signal_right.png | 3 --- selfdrive/assets/icons_mici/turn_intent_right.png | 3 --- selfdrive/ui/mici/onroad/alert_renderer.py | 4 ++-- selfdrive/ui/mici/onroad/hud_renderer.py | 2 +- system/ui/lib/application.py | 12 ++++++++---- 6 files changed, 11 insertions(+), 16 deletions(-) delete mode 100644 selfdrive/assets/icons_mici/onroad/blind_spot_right.png delete mode 100644 selfdrive/assets/icons_mici/onroad/turn_signal_right.png delete mode 100644 selfdrive/assets/icons_mici/turn_intent_right.png diff --git a/selfdrive/assets/icons_mici/onroad/blind_spot_right.png b/selfdrive/assets/icons_mici/onroad/blind_spot_right.png deleted file mode 100644 index b6cd7834ef..0000000000 --- a/selfdrive/assets/icons_mici/onroad/blind_spot_right.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:584cea202afff6dd20d67ae1a9cd6d2b8cc07598bccb91a8d1bac0142567308e -size 45489 diff --git a/selfdrive/assets/icons_mici/onroad/turn_signal_right.png b/selfdrive/assets/icons_mici/onroad/turn_signal_right.png deleted file mode 100644 index 6bcb68dac5..0000000000 --- a/selfdrive/assets/icons_mici/onroad/turn_signal_right.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7fae4872ab3c24d5e4c2be6150127a844f89bbdcadfccdff2dfed180e125d577 -size 45699 diff --git a/selfdrive/assets/icons_mici/turn_intent_right.png b/selfdrive/assets/icons_mici/turn_intent_right.png deleted file mode 100644 index e342778731..0000000000 --- a/selfdrive/assets/icons_mici/turn_intent_right.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7b7e0194a8b9009e493cdce35cd15711596a54227c740e9d6419a3891c6c4037 -size 912 diff --git a/selfdrive/ui/mici/onroad/alert_renderer.py b/selfdrive/ui/mici/onroad/alert_renderer.py index 64dd04c310..1c1fd8404f 100644 --- a/selfdrive/ui/mici/onroad/alert_renderer.py +++ b/selfdrive/ui/mici/onroad/alert_renderer.py @@ -112,9 +112,9 @@ class AlertRenderer(Widget): def _load_icons(self): self._txt_turn_signal_left = gui_app.texture('icons_mici/onroad/turn_signal_left.png', 104, 96) - self._txt_turn_signal_right = gui_app.texture('icons_mici/onroad/turn_signal_right.png', 104, 96) + self._txt_turn_signal_right = gui_app.texture('icons_mici/onroad/turn_signal_left.png', 104, 96, flip_x=True) self._txt_blind_spot_left = gui_app.texture('icons_mici/onroad/blind_spot_left.png', 134, 150) - self._txt_blind_spot_right = gui_app.texture('icons_mici/onroad/blind_spot_right.png', 134, 150) + self._txt_blind_spot_right = gui_app.texture('icons_mici/onroad/blind_spot_left.png', 134, 150, flip_x=True) def get_alert(self, sm: messaging.SubMaster) -> Alert | None: """Generate the current alert based on selfdrive state.""" diff --git a/selfdrive/ui/mici/onroad/hud_renderer.py b/selfdrive/ui/mici/onroad/hud_renderer.py index a6fa1a62bb..3b056c3e0f 100644 --- a/selfdrive/ui/mici/onroad/hud_renderer.py +++ b/selfdrive/ui/mici/onroad/hud_renderer.py @@ -50,7 +50,7 @@ class TurnIntent(Widget): self._turn_intent_rotation_filter = FirstOrderFilter(0, 0.1, 1 / gui_app.target_fps) self._txt_turn_intent_left: rl.Texture = gui_app.texture('icons_mici/turn_intent_left.png', 50, 20) - self._txt_turn_intent_right: rl.Texture = gui_app.texture('icons_mici/turn_intent_right.png', 50, 20) + self._txt_turn_intent_right: rl.Texture = gui_app.texture('icons_mici/turn_intent_left.png', 50, 20, flip_x=True) def _render(self, _): if self._turn_intent_alpha_filter.x > 1e-2: diff --git a/system/ui/lib/application.py b/system/ui/lib/application.py index 5d284bf454..1640b0d077 100644 --- a/system/ui/lib/application.py +++ b/system/ui/lib/application.py @@ -418,19 +418,19 @@ class GuiApplication: self._should_render = should_render def texture(self, asset_path: str, width: int | None = None, height: int | None = None, - alpha_premultiply=False, keep_aspect_ratio=True): - cache_key = f"{asset_path}_{width}_{height}_{alpha_premultiply}{keep_aspect_ratio}" + alpha_premultiply=False, keep_aspect_ratio=True, flip_x: bool = False) -> rl.Texture: + cache_key = f"{asset_path}_{width}_{height}_{alpha_premultiply}_{keep_aspect_ratio}_{flip_x}" if cache_key in self._textures: return self._textures[cache_key] with as_file(ASSETS_DIR.joinpath(asset_path)) as fspath: - image_obj = self._load_image_from_path(fspath.as_posix(), width, height, alpha_premultiply, keep_aspect_ratio) + image_obj = self._load_image_from_path(fspath.as_posix(), width, height, alpha_premultiply, keep_aspect_ratio, flip_x) texture_obj = self._load_texture_from_image(image_obj) self._textures[cache_key] = texture_obj return texture_obj def _load_image_from_path(self, image_path: str, width: int | None = None, height: int | None = None, - alpha_premultiply: bool = False, keep_aspect_ratio: bool = True) -> rl.Image: + alpha_premultiply: bool = False, keep_aspect_ratio: bool = True, flip_x: bool = False) -> rl.Image: """Load and resize an image, storing it for later automatic unloading.""" image = rl.load_image(image_path) @@ -459,6 +459,10 @@ class GuiApplication: rl.image_resize(image, width, height) else: assert keep_aspect_ratio, "Cannot resize without specifying width and height" + + if flip_x: + rl.image_flip_horizontal(image) + return image def _load_texture_from_image(self, image: rl.Image) -> rl.Texture: From ded5e5c8d04aae88e2d202a2d3b9f70b7f5a2ab6 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Mon, 23 Feb 2026 20:46:17 -0800 Subject: [PATCH 182/311] BigButton: normal draw order if not scrolling (#37368) no scroll normal drawing --- selfdrive/ui/mici/widgets/button.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/selfdrive/ui/mici/widgets/button.py b/selfdrive/ui/mici/widgets/button.py index 14786245d7..3ea650ece3 100644 --- a/selfdrive/ui/mici/widgets/button.py +++ b/selfdrive/ui/mici/widgets/button.py @@ -235,12 +235,16 @@ class BigButton(Widget): btn_x = self._rect.x + (self._rect.width * (1 - scale)) / 2 btn_y = self._rect.y + (self._rect.height * (1 - scale)) / 2 - # draw black background since images are transparent - scaled_rect = rl.Rectangle(btn_x, btn_y, self._rect.width * scale, self._rect.height * scale) - rl.draw_rectangle_rounded(scaled_rect, 0.4, 7, rl.Color(0, 0, 0, int(255 * 0.5))) + if self._scroll: + # draw black background since images are transparent + scaled_rect = rl.Rectangle(btn_x, btn_y, self._rect.width * scale, self._rect.height * scale) + rl.draw_rectangle_rounded(scaled_rect, 0.4, 7, rl.Color(0, 0, 0, int(255 * 0.5))) - self._draw_content(btn_y) - rl.draw_texture_ex(txt_bg, (btn_x, btn_y), 0, scale, rl.WHITE) + self._draw_content(btn_y) + rl.draw_texture_ex(txt_bg, (btn_x, btn_y), 0, scale, rl.WHITE) + else: + rl.draw_texture_ex(txt_bg, (btn_x, btn_y), 0, scale, rl.WHITE) + self._draw_content(btn_y) class BigToggle(BigButton): From 44cf6b358e6a3b99c0b507dc0d1e0b3622ce96c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Harald=20Sch=C3=A4fer?= Date: Mon, 23 Feb 2026 20:57:21 -0800 Subject: [PATCH 183/311] ffmpeg: pipe (#37359) spec pipe --- tools/lib/framereader.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/lib/framereader.py b/tools/lib/framereader.py index c13f7fd313..c923651563 100644 --- a/tools/lib/framereader.py +++ b/tools/lib/framereader.py @@ -51,10 +51,10 @@ def decompress_video_data(rawdat, w, h, pix_fmt="rgb24", vid_fmt='hevc', hwaccel "-vsync", "0", "-f", vid_fmt, "-flags2", "showall", - "-i", "-", + "-i", "pipe:0", "-f", "rawvideo", "-pix_fmt", pix_fmt, - "-"] + "pipe:1"] dat = subprocess.check_output(args, input=rawdat) ret: np.ndarray @@ -71,7 +71,7 @@ def ffprobe(fn, fmt=None): cmd = ["ffprobe", "-v", "quiet", "-print_format", "json", "-show_format", "-show_streams"] if fmt: cmd += ["-f", fmt] - cmd += ["-i", "-"] + cmd += ["-i", "pipe:0"] try: with FileReader(fn) as f: From 8bd806658960e0d820849408d59460e7bd594779 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Mon, 23 Feb 2026 21:11:41 -0800 Subject: [PATCH 184/311] rm libjpeg (#37371) --- tools/setup_dependencies.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/tools/setup_dependencies.sh b/tools/setup_dependencies.sh index 6dd1f294c1..cae869f3c0 100755 --- a/tools/setup_dependencies.sh +++ b/tools/setup_dependencies.sh @@ -51,7 +51,6 @@ function install_ubuntu_deps() { $SUDO apt-get install -y --no-install-recommends \ python3-dev \ libgles2-mesa-dev \ - libjpeg-dev \ libncurses5-dev \ libzstd-dev \ gettext From 2ddf95d47f258eab323c19e2dbdff1cc83e4ca66 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Mon, 23 Feb 2026 21:18:29 -0800 Subject: [PATCH 185/311] rm libgles2-mesa-dev (#37373) * rm libjpeg * rm-libgles2-mesa-dev --- tools/setup_dependencies.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/tools/setup_dependencies.sh b/tools/setup_dependencies.sh index cae869f3c0..79f56333cf 100755 --- a/tools/setup_dependencies.sh +++ b/tools/setup_dependencies.sh @@ -50,7 +50,6 @@ function install_ubuntu_deps() { $SUDO apt-get install -y --no-install-recommends \ python3-dev \ - libgles2-mesa-dev \ libncurses5-dev \ libzstd-dev \ gettext From 8a64cc57a96845c1ea67b537d648441631f97a6d Mon Sep 17 00:00:00 2001 From: Angus Dippenaar Date: Tue, 24 Feb 2026 06:22:40 +0100 Subject: [PATCH 186/311] [TIZI/TICI] visuals: Improved speed limit (#1713) improved speed limit Co-authored-by: royjr Co-authored-by: Jason Wen --- selfdrive/ui/sunnypilot/onroad/speed_limit.py | 47 +++++++++---------- 1 file changed, 22 insertions(+), 25 deletions(-) diff --git a/selfdrive/ui/sunnypilot/onroad/speed_limit.py b/selfdrive/ui/sunnypilot/onroad/speed_limit.py index e8b5c5cfcd..39a9848d37 100644 --- a/selfdrive/ui/sunnypilot/onroad/speed_limit.py +++ b/selfdrive/ui/sunnypilot/onroad/speed_limit.py @@ -4,6 +4,7 @@ Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. This file is part of sunnypilot and is licensed under the MIT License. See the LICENSE.md file in the root directory for more details. """ + from dataclasses import dataclass import math import pyray as rl @@ -33,7 +34,7 @@ SpeedLimitSource = custom.LongitudinalPlanSP.SpeedLimit.Source class Colors: WHITE = rl.WHITE BLACK = rl.BLACK - RED = rl.RED + RED = rl.Color(235, 32, 32, 255) GREY = rl.Color(145, 155, 149, 255) DARK_GREY = rl.Color(77, 77, 77, 255) SUB_BG = rl.Color(0, 0, 0, 180) @@ -201,22 +202,17 @@ class SpeedLimitRenderer(Widget): center = rl.Vector2(rect.x + rect.width / 2, rect.y + rect.height / 2) radius = (rect.width + 18) / 2 - white = rl.Color(255, 255, 255, int(255 * alpha)) - red = rl.Color(255, 0, 0, int(255 * alpha)) - - if hasattr(color, 'r'): - text_color = rl.Color(color.r, color.g, color.b, int(255 * alpha)) - else: - text_color = rl.Color(color[0], color[1], color[2], int(255 * alpha)) - - black = rl.Color(0, 0, 0, int(255 * alpha)) - dark_grey = rl.Color(77, 77, 77, int(255 * alpha)) + white = rl.color_alpha(Colors.WHITE, alpha) + red = rl.color_alpha(Colors.RED, alpha) + black = rl.color_alpha(Colors.BLACK, alpha) + dark_grey = rl.color_alpha(Colors.DARK_GREY, alpha) + text_color = rl.color_alpha(color, alpha) rl.draw_circle_v(center, radius, white) - rl.draw_ring(center, radius * 0.80, radius, 0, 360, 36, red) + rl.draw_ring(center, radius * 0.75, radius, 0, 360, 36, red) - f_size = 70 if len(val) >= 3 else 85 - self._draw_text_centered(self.font_bold, val, f_size, center, text_color) + font_size = 70 if len(val) >= 3 else 85 + self._draw_text_centered(self.font_bold, val, font_size, center, text_color) if sub and has_limit: s_radius = radius * 0.4 @@ -225,22 +221,23 @@ class SpeedLimitRenderer(Widget): rl.draw_circle_v(s_center, s_radius, black) rl.draw_ring(s_center, s_radius - 3, s_radius, 0, 360, 36, dark_grey) - f_scale = 0.5 if len(sub) < 3 else 0.45 - self._draw_text_centered(self.font_bold, sub, int(s_radius * 2 * f_scale), s_center, white) + font_scale = 0.5 if len(sub) < 3 else 0.45 + self._draw_text_centered(self.font_bold, sub, int(s_radius * 2 * font_scale), s_center, white) def _render_mutcd(self, rect, val, sub, color, has_limit, alpha=1.0): - white = rl.Color(255, 255, 255, int(255 * alpha)) - black = rl.Color(0, 0, 0, int(255 * alpha)) - dark_grey = rl.Color(77, 77, 77, int(255 * alpha)) - - if hasattr(color, 'r'): - text_color = rl.Color(color.r, color.g, color.b, int(255 * alpha)) - else: - text_color = rl.Color(color[0], color[1], color[2], int(255 * alpha)) + white = rl.color_alpha(Colors.WHITE, alpha) + black = rl.color_alpha(Colors.BLACK, alpha) + dark_grey = rl.color_alpha(Colors.DARK_GREY, alpha) + text_color = rl.color_alpha(color, alpha) rl.draw_rectangle_rounded(rect, 0.35, 10, white) + inner = rl.Rectangle(rect.x + 10, rect.y + 10, rect.width - 20, rect.height - 20) - rl.draw_rectangle_rounded_lines_ex(inner, 0.35, 10, 4, black) + outer_radius = 0.35 * rect.width / 2.0 + inner_radius = outer_radius - 10.0 + inner_roundness = inner_radius / (inner.width / 2.0) + + rl.draw_rectangle_rounded_lines_ex(inner, inner_roundness, 10, 4, black) self._draw_text_centered(self.font_demi, "SPEED", 40, rl.Vector2(rect.x + rect.width / 2, rect.y + 40), black) self._draw_text_centered(self.font_demi, "LIMIT", 40, rl.Vector2(rect.x + rect.width / 2, rect.y + 80), black) From a1e9cf9df91b2ce1e4aca6174a6f4eb979f08375 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Mon, 23 Feb 2026 21:42:24 -0800 Subject: [PATCH 187/311] translations: replace gettext apt dependency with pure Python tools (#37372) --- selfdrive/ui/SConscript | 14 -- selfdrive/ui/translations/potools.py | 362 +++++++++++++++++++++++++++ selfdrive/ui/update_translations.py | 22 +- system/ui/lib/multilang.py | 154 ++++++++++-- tools/setup_dependencies.sh | 3 +- 5 files changed, 511 insertions(+), 44 deletions(-) create mode 100644 selfdrive/ui/translations/potools.py diff --git a/selfdrive/ui/SConscript b/selfdrive/ui/SConscript index a03a26bea8..20ba2e44d0 100644 --- a/selfdrive/ui/SConscript +++ b/selfdrive/ui/SConscript @@ -1,6 +1,4 @@ -import os import re -import json from pathlib import Path Import('env', 'arch', 'common') @@ -18,18 +16,6 @@ env.Command( action=f"python3 {generator}", ) -# compile gettext .po -> .mo translations -with open(File("translations/languages.json").abspath) as f: - languages = json.loads(f.read()) - -po_sources = [f"#selfdrive/ui/translations/app_{l}.po" for l in languages.values()] -po_sources = [src for src in po_sources if os.path.exists(File(src).abspath)] -mo_targets = [src.replace(".po", ".mo") for src in po_sources] -mo_build = [] -for src, tgt in zip(po_sources, mo_targets): - mo_build.append(env.Command(tgt, src, "msgfmt -o $TARGET $SOURCE")) -mo_alias = env.Alias('mo', mo_build) -env.AlwaysBuild(mo_alias) if GetOption('extras'): # build installers diff --git a/selfdrive/ui/translations/potools.py b/selfdrive/ui/translations/potools.py new file mode 100644 index 0000000000..7571cccdd6 --- /dev/null +++ b/selfdrive/ui/translations/potools.py @@ -0,0 +1,362 @@ +"""Pure Python tools for managing .po translation files. + +Replaces GNU gettext CLI tools (xgettext, msginit, msgmerge) with Python +implementations for extracting, creating, and updating .po files. +""" + +import ast +import os +import re +from dataclasses import dataclass, field +from datetime import UTC, datetime +from pathlib import Path + + +@dataclass +class POEntry: + msgid: str = "" + msgstr: str = "" + msgid_plural: str = "" + msgstr_plural: dict[int, str] = field(default_factory=dict) + comments: list[str] = field(default_factory=list) + source_refs: list[str] = field(default_factory=list) + flags: list[str] = field(default_factory=list) + + @property + def is_plural(self) -> bool: + return bool(self.msgid_plural) + + +# ──── PO file parsing ──── + +def _parse_quoted(s: str) -> str: + """Parse a PO-format quoted string, handling escape sequences.""" + s = s.strip() + if not (s.startswith('"') and s.endswith('"')): + raise ValueError(f"Expected quoted string: {s!r}") + s = s[1:-1] + result = [] + i = 0 + while i < len(s): + if s[i] == '\\' and i + 1 < len(s): + c = s[i + 1] + if c == 'n': + result.append('\n') + elif c == 't': + result.append('\t') + elif c == '"': + result.append('"') + elif c == '\\': + result.append('\\') + else: + result.append(s[i:i + 2]) + i += 2 + else: + result.append(s[i]) + i += 1 + return ''.join(result) + + +def parse_po(path: str | Path) -> tuple[POEntry | None, list[POEntry]]: + """Parse a .po/.pot file. Returns (header_entry, entries).""" + with open(path, encoding='utf-8') as f: + lines = f.readlines() + + entries: list[POEntry] = [] + header: POEntry | None = None + cur: POEntry | None = None + cur_field: str | None = None + plural_idx = 0 + + def finish(): + nonlocal cur, header + if cur is None: + return + if cur.msgid == "" and cur.msgstr: + header = cur + elif cur.msgid != "" or cur.is_plural: + entries.append(cur) + cur = None + + for raw in lines: + line = raw.rstrip('\n') + stripped = line.strip() + + if not stripped: + finish() + cur_field = None + continue + + # Skip obsolete entries + if stripped.startswith('#~'): + continue + + if stripped.startswith('#'): + if cur is None: + cur = POEntry() + if stripped.startswith('#:'): + cur.source_refs.append(stripped[2:].strip()) + elif stripped.startswith('#,'): + cur.flags.extend(f.strip() for f in stripped[2:].split(',') if f.strip()) + else: + cur.comments.append(line) + continue + + if stripped.startswith('msgid_plural '): + if cur is None: + cur = POEntry() + cur.msgid_plural = _parse_quoted(stripped[len('msgid_plural '):]) + cur_field = 'msgid_plural' + continue + + if stripped.startswith('msgid '): + if cur is None: + cur = POEntry() + cur.msgid = _parse_quoted(stripped[len('msgid '):]) + cur_field = 'msgid' + continue + + m = re.match(r'msgstr\[(\d+)]\s+(.*)', stripped) + if m: + plural_idx = int(m.group(1)) + cur.msgstr_plural[plural_idx] = _parse_quoted(m.group(2)) + cur_field = 'msgstr_plural' + continue + + if stripped.startswith('msgstr '): + cur.msgstr = _parse_quoted(stripped[len('msgstr '):]) + cur_field = 'msgstr' + continue + + if stripped.startswith('"'): + val = _parse_quoted(stripped) + if cur_field == 'msgid': + cur.msgid += val + elif cur_field == 'msgid_plural': + cur.msgid_plural += val + elif cur_field == 'msgstr': + cur.msgstr += val + elif cur_field == 'msgstr_plural': + cur.msgstr_plural[plural_idx] += val + + finish() + return header, entries + + +# ──── PO file writing ──── + +def _quote(s: str) -> str: + """Quote a string for .po file output.""" + s = s.replace('\\', '\\\\').replace('"', '\\"').replace('\t', '\\t') + if '\n' in s and s != '\n': + parts = s.split('\n') + lines = ['""'] + for i, part in enumerate(parts): + text = part + ('\\n' if i < len(parts) - 1 else '') + if text: + lines.append(f'"{text}"') + return '\n'.join(lines) + return f'"{s}"'.replace('\n', '\\n') + + +def write_po(path: str | Path, header: POEntry | None, entries: list[POEntry]) -> None: + """Write a .po/.pot file.""" + with open(path, 'w', encoding='utf-8') as f: + if header: + for c in header.comments: + f.write(c + '\n') + if header.flags: + f.write('#, ' + ', '.join(header.flags) + '\n') + f.write(f'msgid {_quote("")}\n') + f.write(f'msgstr {_quote(header.msgstr)}\n\n') + + for entry in entries: + for c in entry.comments: + f.write(c + '\n') + for ref in entry.source_refs: + f.write(f'#: {ref}\n') + if entry.flags: + f.write('#, ' + ', '.join(entry.flags) + '\n') + f.write(f'msgid {_quote(entry.msgid)}\n') + if entry.is_plural: + f.write(f'msgid_plural {_quote(entry.msgid_plural)}\n') + for idx in sorted(entry.msgstr_plural): + f.write(f'msgstr[{idx}] {_quote(entry.msgstr_plural[idx])}\n') + else: + f.write(f'msgstr {_quote(entry.msgstr)}\n') + f.write('\n') + + +# ──── String extraction (replaces xgettext) ──── + +def extract_strings(files: list[str], basedir: str) -> list[POEntry]: + """Extract tr/trn/tr_noop calls from Python source files.""" + seen: dict[str, POEntry] = {} + + for filepath in files: + full = os.path.join(basedir, filepath) + with open(full, encoding='utf-8') as f: + source = f.read() + try: + tree = ast.parse(source, filename=filepath) + except SyntaxError: + continue + + for node in ast.walk(tree): + if not isinstance(node, ast.Call): + continue + + func = node.func + if isinstance(func, ast.Name): + name = func.id + elif isinstance(func, ast.Attribute): + name = func.attr + else: + continue + + if name not in ('tr', 'trn', 'tr_noop'): + continue + + ref = f'{filepath}:{node.lineno}' + is_flagged = name in ('tr', 'trn') + + if name in ('tr', 'tr_noop'): + if not node.args or not isinstance(node.args[0], ast.Constant) or not isinstance(node.args[0].value, str): + continue + msgid = node.args[0].value + if msgid in seen: + if ref not in seen[msgid].source_refs: + seen[msgid].source_refs.append(ref) + else: + flags = ['python-format'] if is_flagged else [] + seen[msgid] = POEntry(msgid=msgid, source_refs=[ref], flags=flags) + + elif name == 'trn': + if len(node.args) < 2: + continue + a1, a2 = node.args[0], node.args[1] + if not (isinstance(a1, ast.Constant) and isinstance(a1.value, str)): + continue + if not (isinstance(a2, ast.Constant) and isinstance(a2.value, str)): + continue + msgid, msgid_plural = a1.value, a2.value + if msgid in seen: + if ref not in seen[msgid].source_refs: + seen[msgid].source_refs.append(ref) + else: + flags = ['python-format'] if is_flagged else [] + seen[msgid] = POEntry( + msgid=msgid, msgid_plural=msgid_plural, + source_refs=[ref], flags=flags, + msgstr_plural={0: '', 1: ''}, + ) + + return list(seen.values()) + + +# ──── POT generation ──── + +def generate_pot(entries: list[POEntry], pot_path: str | Path) -> None: + """Generate a .pot template file from extracted entries.""" + now = datetime.now(UTC).strftime('%Y-%m-%d %H:%M%z') + header = POEntry( + comments=[ + '# SOME DESCRIPTIVE TITLE.', + "# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER", + '# This file is distributed under the same license as the PACKAGE package.', + '# FIRST AUTHOR , YEAR.', + '#', + ], + flags=['fuzzy'], + msgstr='Project-Id-Version: PACKAGE VERSION\n' + + 'Report-Msgid-Bugs-To: \n' + + f'POT-Creation-Date: {now}\n' + + 'PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n' + + 'Last-Translator: FULL NAME \n' + + 'Language-Team: LANGUAGE \n' + + 'Language: \n' + + 'MIME-Version: 1.0\n' + + 'Content-Type: text/plain; charset=UTF-8\n' + + 'Content-Transfer-Encoding: 8bit\n' + + 'Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n', + ) + write_po(pot_path, header, entries) + + +# ──── PO init (replaces msginit) ──── + +PLURAL_FORMS: dict[str, str] = { + 'en': 'nplurals=2; plural=(n != 1);', + 'de': 'nplurals=2; plural=(n != 1);', + 'fr': 'nplurals=2; plural=(n > 1);', + 'es': 'nplurals=2; plural=(n != 1);', + 'pt-BR': 'nplurals=2; plural=(n > 1);', + 'tr': 'nplurals=2; plural=(n != 1);', + 'uk': 'nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : 2);', + 'th': 'nplurals=1; plural=0;', + 'zh-CHT': 'nplurals=1; plural=0;', + 'zh-CHS': 'nplurals=1; plural=0;', + 'ko': 'nplurals=1; plural=0;', + 'ja': 'nplurals=1; plural=0;', +} + + +def init_po(pot_path: str | Path, po_path: str | Path, language: str) -> None: + """Create a new .po file from a .pot template (replaces msginit).""" + _, entries = parse_po(pot_path) + plural_forms = PLURAL_FORMS.get(language, 'nplurals=2; plural=(n != 1);') + now = datetime.now(UTC).strftime('%Y-%m-%d %H:%M%z') + + header = POEntry( + comments=[ + f'# {language} translations for PACKAGE package.', + "# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER", + '# This file is distributed under the same license as the PACKAGE package.', + '# Automatically generated.', + '#', + ], + msgstr='Project-Id-Version: PACKAGE VERSION\n' + + 'Report-Msgid-Bugs-To: \n' + + f'POT-Creation-Date: {now}\n' + + f'PO-Revision-Date: {now}\n' + + 'Last-Translator: Automatically generated\n' + + 'Language-Team: none\n' + + f'Language: {language}\n' + + 'MIME-Version: 1.0\n' + + 'Content-Type: text/plain; charset=UTF-8\n' + + 'Content-Transfer-Encoding: 8bit\n' + + f'Plural-Forms: {plural_forms}\n', + ) + + nplurals = int(re.search(r'nplurals=(\d+)', plural_forms).group(1)) + for e in entries: + if e.is_plural: + e.msgstr_plural = dict.fromkeys(range(nplurals), '') + + write_po(po_path, header, entries) + + +# ──── PO merge (replaces msgmerge) ──── + +def merge_po(po_path: str | Path, pot_path: str | Path) -> None: + """Update a .po file with entries from a .pot template (replaces msgmerge --update).""" + po_header, po_entries = parse_po(po_path) + _, pot_entries = parse_po(pot_path) + + existing = {e.msgid: e for e in po_entries} + merged = [] + + for pot_e in pot_entries: + if pot_e.msgid in existing: + old = existing[pot_e.msgid] + old.source_refs = pot_e.source_refs + old.flags = pot_e.flags + old.comments = pot_e.comments + if pot_e.is_plural: + old.msgid_plural = pot_e.msgid_plural + merged.append(old) + else: + merged.append(pot_e) + + merged.sort(key=lambda e: e.msgid) + write_po(po_path, po_header, merged) diff --git a/selfdrive/ui/update_translations.py b/selfdrive/ui/update_translations.py index bded80b2e5..6ff3667d8a 100755 --- a/selfdrive/ui/update_translations.py +++ b/selfdrive/ui/update_translations.py @@ -3,6 +3,7 @@ from itertools import chain import os from openpilot.common.basedir import BASEDIR from openpilot.system.ui.lib.multilang import SYSTEM_UI_DIR, UI_DIR, TRANSLATIONS_DIR, multilang +from openpilot.selfdrive.ui.translations.potools import extract_strings, generate_pot, merge_po, init_po LANGUAGES_FILE = os.path.join(str(TRANSLATIONS_DIR), "languages.json") POT_FILE = os.path.join(str(TRANSLATIONS_DIR), "app.pot") @@ -18,24 +19,17 @@ def update_translations(): if filename.endswith(".py"): files.append(os.path.relpath(os.path.join(root, filename), BASEDIR)) - # Create main translation file - cmd = ("xgettext -L Python --keyword=tr --keyword=trn:1,2 --keyword=tr_noop --from-code=UTF-8 " + - "--flag=tr:1:python-brace-format --flag=trn:1:python-brace-format --flag=trn:2:python-brace-format " + - f"-D {BASEDIR} -o {POT_FILE} {' '.join(files)}") - - ret = os.system(cmd) - assert ret == 0 + # Extract translatable strings and generate .pot template + entries = extract_strings(files, BASEDIR) + generate_pot(entries, POT_FILE) # Generate/update translation files for each language for name in multilang.languages.values(): - if os.path.exists(os.path.join(TRANSLATIONS_DIR, f"app_{name}.po")): - cmd = f"msgmerge --update --no-fuzzy-matching --backup=none --sort-output {TRANSLATIONS_DIR}/app_{name}.po {POT_FILE}" - ret = os.system(cmd) - assert ret == 0 + po_file = os.path.join(TRANSLATIONS_DIR, f"app_{name}.po") + if os.path.exists(po_file): + merge_po(po_file, POT_FILE) else: - cmd = f"msginit -l {name} --no-translator --input {POT_FILE} --output-file {TRANSLATIONS_DIR}/app_{name}.po" - ret = os.system(cmd) - assert ret == 0 + init_po(POT_FILE, po_file, name) if __name__ == "__main__": diff --git a/system/ui/lib/multilang.py b/system/ui/lib/multilang.py index ad2a5f9399..343c06a1e8 100644 --- a/system/ui/lib/multilang.py +++ b/system/ui/lib/multilang.py @@ -1,7 +1,7 @@ from importlib.resources import files -import os import json -import gettext +import os +import re from openpilot.common.basedir import BASEDIR from openpilot.common.swaglog import cloudlog @@ -23,14 +23,137 @@ UNIFONT_LANGUAGES = [ "ja", ] +# Plural form selectors for supported languages +PLURAL_SELECTORS = { + 'en': lambda n: 0 if n == 1 else 1, + 'de': lambda n: 0 if n == 1 else 1, + 'fr': lambda n: 0 if n <= 1 else 1, + 'pt-BR': lambda n: 0 if n <= 1 else 1, + 'es': lambda n: 0 if n == 1 else 1, + 'tr': lambda n: 0 if n == 1 else 1, + 'uk': lambda n: 0 if n % 10 == 1 and n % 100 != 11 else (1 if 2 <= n % 10 <= 4 and not 12 <= n % 100 <= 14 else 2), + 'th': lambda n: 0, + 'zh-CHT': lambda n: 0, + 'zh-CHS': lambda n: 0, + 'ko': lambda n: 0, + 'ja': lambda n: 0, +} + + +def _parse_quoted(s: str) -> str: + """Parse a PO-format quoted string.""" + s = s.strip() + if not (s.startswith('"') and s.endswith('"')): + raise ValueError(f"Expected quoted string: {s!r}") + s = s[1:-1] + result: list[str] = [] + i = 0 + while i < len(s): + if s[i] == '\\' and i + 1 < len(s): + c = s[i + 1] + if c == 'n': + result.append('\n') + elif c == 't': + result.append('\t') + elif c == '"': + result.append('"') + elif c == '\\': + result.append('\\') + else: + result.append(s[i:i + 2]) + i += 2 + else: + result.append(s[i]) + i += 1 + return ''.join(result) + + +def load_translations(path) -> tuple[dict[str, str], dict[str, list[str]]]: + """Parse a .po file and return (translations, plurals) dicts. + + translations: msgid -> msgstr + plurals: msgid -> [msgstr[0], msgstr[1], ...] + """ + with open(str(path), encoding='utf-8') as f: + lines = f.readlines() + + translations: dict[str, str] = {} + plurals: dict[str, list[str]] = {} + + # Parser state + msgid = msgid_plural = msgstr = "" + msgstr_plurals: dict[int, str] = {} + field: str | None = None + plural_idx = 0 + + def finish(): + nonlocal msgid, msgid_plural, msgstr, msgstr_plurals, field + if msgid: # skip header (empty msgid) + if msgid_plural: + max_idx = max(msgstr_plurals.keys()) if msgstr_plurals else 0 + plurals[msgid] = [msgstr_plurals.get(i, '') for i in range(max_idx + 1)] + else: + translations[msgid] = msgstr + msgid = msgid_plural = msgstr = "" + msgstr_plurals = {} + field = None + + for raw in lines: + line = raw.strip() + + if not line: + finish() + continue + + if line.startswith('#'): + continue + + if line.startswith('msgid_plural '): + msgid_plural = _parse_quoted(line[len('msgid_plural '):]) + field = 'msgid_plural' + continue + + if line.startswith('msgid '): + msgid = _parse_quoted(line[len('msgid '):]) + field = 'msgid' + continue + + m = re.match(r'msgstr\[(\d+)]\s+(.*)', line) + if m: + plural_idx = int(m.group(1)) + msgstr_plurals[plural_idx] = _parse_quoted(m.group(2)) + field = 'msgstr_plural' + continue + + if line.startswith('msgstr '): + msgstr = _parse_quoted(line[len('msgstr '):]) + field = 'msgstr' + continue + + if line.startswith('"'): + val = _parse_quoted(line) + if field == 'msgid': + msgid += val + elif field == 'msgid_plural': + msgid_plural += val + elif field == 'msgstr': + msgstr += val + elif field == 'msgstr_plural': + msgstr_plurals[plural_idx] += val + + finish() + return translations, plurals + class Multilang: def __init__(self): self._params = Params() if Params is not None else None self._language: str = "en" - self.languages = {} - self.codes = {} - self._translation: gettext.NullTranslations | gettext.GNUTranslations = gettext.NullTranslations() + self.languages: dict[str, str] = {} + self.codes: dict[str, str] = {} + self._translations: dict[str, str] = {} + self._plurals: dict[str, list[str]] = {} + self._plural_selector = PLURAL_SELECTORS.get('en', lambda n: 0) self._load_languages() @property @@ -43,27 +166,30 @@ class Multilang: def setup(self): try: - with TRANSLATIONS_DIR.joinpath(f'app_{self._language}.mo').open('rb') as fh: - translation = gettext.GNUTranslations(fh) - translation.install() - self._translation = translation + po_path = TRANSLATIONS_DIR.joinpath(f'app_{self._language}.po') + self._translations, self._plurals = load_translations(po_path) + self._plural_selector = PLURAL_SELECTORS.get(self._language, lambda n: 0) cloudlog.debug(f"Loaded translations for language: {self._language}") except FileNotFoundError: cloudlog.error(f"No translation file found for language: {self._language}, using default.") - gettext.install('app') - self._translation = gettext.NullTranslations() + self._translations = {} + self._plurals = {} def change_language(self, language_code: str) -> None: - # Reinstall gettext with the selected language self._params.put("LanguageSetting", language_code) self._language = language_code self.setup() def tr(self, text: str) -> str: - return self._translation.gettext(text) + return self._translations.get(text, text) def trn(self, singular: str, plural: str, n: int) -> str: - return self._translation.ngettext(singular, plural, n) + if singular in self._plurals: + idx = self._plural_selector(n) + forms = self._plurals[singular] + if idx < len(forms) and forms[idx]: + return forms[idx] + return singular if n == 1 else plural def _load_languages(self): with LANGUAGES_FILE.open(encoding='utf-8') as f: diff --git a/tools/setup_dependencies.sh b/tools/setup_dependencies.sh index 79f56333cf..98f1d878f8 100755 --- a/tools/setup_dependencies.sh +++ b/tools/setup_dependencies.sh @@ -51,8 +51,7 @@ function install_ubuntu_deps() { $SUDO apt-get install -y --no-install-recommends \ python3-dev \ libncurses5-dev \ - libzstd-dev \ - gettext + libzstd-dev if [[ -d "/etc/udev/rules.d/" ]]; then # Setup jungle udev rules From 8952c947d15dd460849af14bdfc1f74985f1680c Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Mon, 23 Feb 2026 21:48:13 -0800 Subject: [PATCH 188/311] only build installer on device --- selfdrive/ui/SConscript | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selfdrive/ui/SConscript b/selfdrive/ui/SConscript index 20ba2e44d0..5556883ef8 100644 --- a/selfdrive/ui/SConscript +++ b/selfdrive/ui/SConscript @@ -17,7 +17,7 @@ env.Command( ) -if GetOption('extras'): +if GetOption('extras') and arch == "larch64": # build installers if arch != "Darwin": raylib_env = env.Clone() From 79bc6c3a52d5f86746b51e2ddc20799e82589300 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Mon, 23 Feb 2026 21:59:39 -0800 Subject: [PATCH 189/311] replace python3-dev apt install with vendored package (#37374) * replace python3-dev apt install with vendored package Co-Authored-By: Claude Opus 4.6 * keep for agnos * cleaner --------- Co-authored-by: Claude Opus 4.6 --- SConstruct | 4 +++- pyproject.toml | 1 + tools/setup_dependencies.sh | 1 - uv.lock | 19 +++++++++++++------ 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/SConstruct b/SConstruct index 446fcc417d..bee043c545 100644 --- a/SConstruct +++ b/SConstruct @@ -42,11 +42,14 @@ if arch != "larch64": import capnproto import eigen import ffmpeg as ffmpeg_pkg + import python3_dev import zeromq pkgs = [capnproto, eigen, ffmpeg_pkg, zeromq] + py_include = python3_dev.INCLUDE_DIR else: # TODO: remove when AGNOS has our new vendor pkgs pkgs = [] + py_include = sysconfig.get_paths()['include'] env = Environment( ENV={ @@ -159,7 +162,6 @@ if os.environ.get('SCONS_PROGRESS'): Progress(progress_function, interval=node_interval) # ********** Cython build environment ********** -py_include = sysconfig.get_paths()['include'] envCython = env.Clone() envCython["CPPPATH"] += [py_include, np.get_include()] envCython["CCFLAGS"] += ["-Wno-#warnings", "-Wno-cpp", "-Wno-shadow", "-Wno-deprecated-declarations"] diff --git a/pyproject.toml b/pyproject.toml index da36ea9df4..3c4b1897a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,6 +29,7 @@ dependencies = [ "capnproto @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=capnproto", "eigen @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=eigen", "ffmpeg @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=ffmpeg", + "python3-dev @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=python3-dev", "zeromq @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=zeromq", "git-lfs @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=git-lfs", diff --git a/tools/setup_dependencies.sh b/tools/setup_dependencies.sh index 98f1d878f8..368c6f2008 100755 --- a/tools/setup_dependencies.sh +++ b/tools/setup_dependencies.sh @@ -49,7 +49,6 @@ function install_ubuntu_deps() { xvfb $SUDO apt-get install -y --no-install-recommends \ - python3-dev \ libncurses5-dev \ libzstd-dev diff --git a/uv.lock b/uv.lock index 11ff989278..2a03eb740d 100644 --- a/uv.lock +++ b/uv.lock @@ -116,7 +116,7 @@ wheels = [ [[package]] name = "capnproto" version = "1.0.1" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=capnproto&rev=releases#96208e8726374ab5229366102a17401edb68076c" } +source = { git = "https://github.com/commaai/dependencies.git?subdirectory=capnproto&rev=releases#7c17c720197a770e2ff9754c4aaebd75ba17b95c" } [[package]] name = "casadi" @@ -376,7 +376,7 @@ wheels = [ [[package]] name = "eigen" version = "3.4.0" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=eigen&rev=releases#96208e8726374ab5229366102a17401edb68076c" } +source = { git = "https://github.com/commaai/dependencies.git?subdirectory=eigen&rev=releases#7c17c720197a770e2ff9754c4aaebd75ba17b95c" } [[package]] name = "execnet" @@ -390,7 +390,7 @@ wheels = [ [[package]] name = "ffmpeg" version = "7.1.0" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=ffmpeg&rev=releases#96208e8726374ab5229366102a17401edb68076c" } +source = { git = "https://github.com/commaai/dependencies.git?subdirectory=ffmpeg&rev=releases#7c17c720197a770e2ff9754c4aaebd75ba17b95c" } [[package]] name = "fonttools" @@ -437,7 +437,7 @@ wheels = [ [[package]] name = "gcc-arm-none-eabi" version = "13.2.1" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=gcc-arm-none-eabi&rev=releases#96208e8726374ab5229366102a17401edb68076c" } +source = { git = "https://github.com/commaai/dependencies.git?subdirectory=gcc-arm-none-eabi&rev=releases#7c17c720197a770e2ff9754c4aaebd75ba17b95c" } [[package]] name = "ghp-import" @@ -454,7 +454,7 @@ wheels = [ [[package]] name = "git-lfs" version = "3.6.1" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=git-lfs&rev=releases#96208e8726374ab5229366102a17401edb68076c" } +source = { git = "https://github.com/commaai/dependencies.git?subdirectory=git-lfs&rev=releases#7c17c720197a770e2ff9754c4aaebd75ba17b95c" } [[package]] name = "google-crc32c" @@ -790,6 +790,7 @@ dependencies = [ { name = "pycryptodome" }, { name = "pyjwt" }, { name = "pyserial" }, + { name = "python3-dev" }, { name = "pyzmq" }, { name = "qrcode" }, { name = "raylib" }, @@ -877,6 +878,7 @@ requires-dist = [ { name = "pytest-mock", marker = "extra == 'testing'" }, { name = "pytest-subtests", marker = "extra == 'testing'" }, { name = "pytest-xdist", marker = "extra == 'testing'", git = "https://github.com/sshane/pytest-xdist?rev=2b4372bd62699fb412c4fe2f95bf9f01bd2018da" }, + { name = "python3-dev", git = "https://github.com/commaai/dependencies.git?subdirectory=python3-dev&rev=releases" }, { name = "pyzmq" }, { name = "qrcode" }, { name = "raylib", specifier = ">5.5.0.3" }, @@ -1264,6 +1266,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, ] +[[package]] +name = "python3-dev" +version = "3.12.8" +source = { git = "https://github.com/commaai/dependencies.git?subdirectory=python3-dev&rev=releases#7c17c720197a770e2ff9754c4aaebd75ba17b95c" } + [[package]] name = "pyyaml" version = "6.0.3" @@ -1630,7 +1637,7 @@ wheels = [ [[package]] name = "zeromq" version = "4.3.5" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=zeromq&rev=releases#96208e8726374ab5229366102a17401edb68076c" } +source = { git = "https://github.com/commaai/dependencies.git?subdirectory=zeromq&rev=releases#7c17c720197a770e2ff9754c4aaebd75ba17b95c" } [[package]] name = "zstandard" From 542e14306ec95b18a735a2b0cd73791106166b1a Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Mon, 23 Feb 2026 23:02:53 -0800 Subject: [PATCH 190/311] vendor zstd and ncurses (#37376) --- SConstruct | 4 +++- pyproject.toml | 2 ++ tools/setup_dependencies.sh | 4 ---- uv.lock | 28 +++++++++++++++++++++------- 4 files changed, 26 insertions(+), 12 deletions(-) diff --git a/SConstruct b/SConstruct index bee043c545..16c1d8ee73 100644 --- a/SConstruct +++ b/SConstruct @@ -42,9 +42,11 @@ if arch != "larch64": import capnproto import eigen import ffmpeg as ffmpeg_pkg + import ncurses import python3_dev import zeromq - pkgs = [capnproto, eigen, ffmpeg_pkg, zeromq] + import zstd + pkgs = [capnproto, eigen, ffmpeg_pkg, ncurses, zeromq, zstd] py_include = python3_dev.INCLUDE_DIR else: # TODO: remove when AGNOS has our new vendor pkgs diff --git a/pyproject.toml b/pyproject.toml index 3c4b1897a4..f983aca8ae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,6 +30,8 @@ dependencies = [ "eigen @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=eigen", "ffmpeg @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=ffmpeg", "python3-dev @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=python3-dev", + "zstd @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=zstd", + "ncurses @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=ncurses", "zeromq @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=zeromq", "git-lfs @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=git-lfs", diff --git a/tools/setup_dependencies.sh b/tools/setup_dependencies.sh index 368c6f2008..df06d4ab1d 100755 --- a/tools/setup_dependencies.sh +++ b/tools/setup_dependencies.sh @@ -48,10 +48,6 @@ function install_ubuntu_deps() { git \ xvfb - $SUDO apt-get install -y --no-install-recommends \ - libncurses5-dev \ - libzstd-dev - if [[ -d "/etc/udev/rules.d/" ]]; then # Setup jungle udev rules $SUDO tee /etc/udev/rules.d/12-panda_jungle.rules > /dev/null < Date: Tue, 24 Feb 2026 02:38:19 -0500 Subject: [PATCH 191/311] ICBM: ensure button timers update on disable to clear stale presses (#1688) * ICBM: ensure button timers update on disable to clear stale presses * fix race condition --------- Co-authored-by: Jason Wen --- selfdrive/car/cruise.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/selfdrive/car/cruise.py b/selfdrive/car/cruise.py index 9973862b85..572dfabc02 100644 --- a/selfdrive/car/cruise.py +++ b/selfdrive/car/cruise.py @@ -48,14 +48,14 @@ class VCruiseHelper(VCruiseHelperSP): self.get_minimum_set_speed(is_metric) + _enabled = self.update_enabled_state(CS, enabled) + if CS.cruiseState.available: - _enabled = self.update_enabled_state(CS, enabled) if not self.CP.pcmCruise or (not self.CP_SP.pcmCruiseSpeed and _enabled): # if stock cruise is completely disabled, then we can use our own set speed logic self._update_v_cruise_non_pcm(CS, _enabled, is_metric) self.update_speed_limit_assist_v_cruise_non_pcm() self.v_cruise_cluster_kph = self.v_cruise_kph - self.update_button_timers(CS, enabled) else: self.v_cruise_kph = CS.cruiseState.speed * CV.MS_TO_KPH self.v_cruise_cluster_kph = CS.cruiseState.speedCluster * CV.MS_TO_KPH @@ -69,6 +69,9 @@ class VCruiseHelper(VCruiseHelperSP): self.v_cruise_kph = V_CRUISE_UNSET self.v_cruise_cluster_kph = V_CRUISE_UNSET + if not self.CP.pcmCruise or not self.CP_SP.pcmCruiseSpeed: + self.update_button_timers(CS, enabled) + def _update_v_cruise_non_pcm(self, CS, enabled, is_metric): # handle button presses. TODO: this should be in state_control, but a decelCruise press # would have the effect of both enabling and changing speed is checked after the state transition From 03a14ff864af684f9f894a4f8b207e76e690c3b6 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Tue, 24 Feb 2026 03:02:36 -0500 Subject: [PATCH 192/311] [TIZI/TICI] ui: simplify Smart Cruise Control text rendering (#1719) --- .../sunnypilot/onroad/smart_cruise_control.py | 35 ++++--------------- 1 file changed, 7 insertions(+), 28 deletions(-) diff --git a/selfdrive/ui/sunnypilot/onroad/smart_cruise_control.py b/selfdrive/ui/sunnypilot/onroad/smart_cruise_control.py index ca71fcac4a..b4848141e9 100644 --- a/selfdrive/ui/sunnypilot/onroad/smart_cruise_control.py +++ b/selfdrive/ui/sunnypilot/onroad/smart_cruise_control.py @@ -25,7 +25,6 @@ class SmartCruiseControlRenderer(Widget): self.long_override = False self.font = gui_app.font(FontWeight.BOLD) - self.scc_tex = rl.load_render_texture(256, 128) def update(self): sm = ui_state.sm @@ -65,44 +64,24 @@ class SmartCruiseControlRenderer(Widget): sz = measure_text_cached(self.font, text, font_size) box_height = int(sz.y + padding_v * 2) - texture_width = 256 - texture_height = 128 - - rl.begin_texture_mode(self.scc_tex) - rl.clear_background(rl.Color(0, 0, 0, 0)) - if self.long_override: box_color = COLORS.OVERRIDE else: box_color = rl.Color(0, 255, 0, 255) - # Center box in texture - box_x = (texture_width - box_width) // 2 - box_y = (texture_height - box_height) // 2 + screen_y = rect_height / 4 + y_offset + box_x = rect_center_x + x_offset - box_width / 2 + box_y = screen_y - box_height / 2 + + # Draw rounded background box rl.draw_rectangle_rounded(rl.Rectangle(box_x, box_y, box_width, box_height), 0.2, 10, box_color) - # Draw text with custom blend mode to punch hole - rl.rl_set_blend_factors(rl.RL_ZERO, rl.RL_ONE_MINUS_SRC_ALPHA, 0x8006) - rl.rl_set_blend_mode(rl.BLEND_CUSTOM) - + # Draw text centered in the box (black color for contrast against bright green/grey) text_pos_x = box_x + (box_width - sz.x) / 2 text_pos_y = box_y + (box_height - sz.y) / 2 - rl.draw_text_ex(self.font, text, rl.Vector2(text_pos_x, text_pos_y), font_size, 0, rl.WHITE) - - rl.rl_set_blend_mode(rl.BLEND_ALPHA) # Reset - rl.end_texture_mode() - - screen_y = rect_height / 4 + y_offset - - dest_x = rect_center_x + x_offset - texture_width / 2 - dest_y = screen_y - texture_height / 2 - - src_rect = rl.Rectangle(0, 0, texture_width, -texture_height) - dst_rect = rl.Rectangle(dest_x, dest_y, texture_width, texture_height) - - rl.draw_texture_pro(self.scc_tex.texture, src_rect, dst_rect, rl.Vector2(0, 0), 0, rl.WHITE) + rl.draw_text_ex(self.font, text, rl.Vector2(text_pos_x, text_pos_y), font_size, 0, rl.BLACK) def _render(self, rect: rl.Rectangle): x_offset = -260 From 761c34949073317f0d363e2ba244e907106dc11a Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 24 Feb 2026 00:29:20 -0800 Subject: [PATCH 193/311] Make WifiNetworkButton self-contained --- .../mici/layouts/settings/network/__init__.py | 68 ++++++++++--------- 1 file changed, 35 insertions(+), 33 deletions(-) diff --git a/selfdrive/ui/mici/layouts/settings/network/__init__.py b/selfdrive/ui/mici/layouts/settings/network/__init__.py index 299df5fea0..8f9b18b9a0 100644 --- a/selfdrive/ui/mici/layouts/settings/network/__init__.py +++ b/selfdrive/ui/mici/layouts/settings/network/__init__.py @@ -12,13 +12,43 @@ from openpilot.system.ui.lib.wifi_manager import WifiManager, Network, MeteredTy class WifiNetworkButton(BigButton): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + def __init__(self, wifi_manager: WifiManager): + self._wifi_manager = wifi_manager self._lock_txt = gui_app.texture("icons_mici/settings/network/new/lock.png", 28, 36) self._draw_lock = False - def set_draw_lock(self, draw: bool): - self._draw_lock = draw + self._wifi_slash_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_slash.png", 64, 56) + self._wifi_low_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_low.png", 64, 47) + self._wifi_medium_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_medium.png", 64, 47) + self._wifi_full_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_full.png", 64, 47) + + super().__init__("wi-fi", "not connected", self._wifi_slash_txt, scroll=True) + + def _update_state(self): + super()._update_state() + + # Update wi-fi button with ssid and ip address + # TODO: make sure we handle hidden ssids + wifi_state = self._wifi_manager.wifi_state + display_network = next((n for n in self._wifi_manager.networks if n.ssid == wifi_state.ssid), None) + if wifi_state.status == ConnectStatus.CONNECTING: + self.set_text(normalize_ssid(wifi_state.ssid or "wi-fi")) + self.set_value("connecting...") + elif wifi_state.status == ConnectStatus.CONNECTED: + self.set_text(normalize_ssid(wifi_state.ssid or "wi-fi")) + self.set_value(self._wifi_manager.ipv4_address or "obtaining IP...") + else: + display_network = None + self.set_text("wi-fi") + self.set_value("not connected") + + if display_network is not None: + strength = WifiIcon.get_strength_icon_idx(display_network.strength) + self.set_icon(self._wifi_full_txt if strength == 2 else self._wifi_medium_txt if strength == 1 else self._wifi_low_txt) + self._draw_lock = display_network.security_type not in (SecurityType.OPEN, SecurityType.UNSUPPORTED) + else: + self.set_icon(self._wifi_slash_txt) + self._draw_lock = False def _draw_content(self, btn_y: float): super()._draw_content(btn_y) @@ -83,12 +113,7 @@ class NetworkLayoutMici(NavWidget): self._network_metered_btn = BigMultiToggle("network usage", ["default", "metered", "unmetered"], select_callback=network_metered_callback) self._network_metered_btn.set_enabled(False) - self._wifi_slash_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_slash.png", 64, 56) - self._wifi_low_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_low.png", 64, 47) - self._wifi_medium_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_medium.png", 64, 47) - self._wifi_full_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_full.png", 64, 47) - - self._wifi_button = WifiNetworkButton("wi-fi", "not connected", self._wifi_slash_txt, scroll=True) + self._wifi_button = WifiNetworkButton(self._wifi_manager) self._wifi_button.set_click_callback(lambda: gui_app.push_widget(self._wifi_ui)) # ******** Advanced settings ******** @@ -133,29 +158,6 @@ class NetworkLayoutMici(NavWidget): self._apn_btn.set_visible(show_cell_settings) self._cellular_metered_btn.set_visible(show_cell_settings) - # Update wi-fi button with ssid and ip address - # TODO: make sure we handle hidden ssids - wifi_state = self._wifi_manager.wifi_state - display_network = next((n for n in self._wifi_manager.networks if n.ssid == wifi_state.ssid), None) - if wifi_state.status == ConnectStatus.CONNECTING: - self._wifi_button.set_text(normalize_ssid(wifi_state.ssid or "wi-fi")) - self._wifi_button.set_value("connecting...") - elif wifi_state.status == ConnectStatus.CONNECTED: - self._wifi_button.set_text(normalize_ssid(wifi_state.ssid or "wi-fi")) - self._wifi_button.set_value(self._wifi_manager.ipv4_address or "obtaining IP...") - else: - display_network = None - self._wifi_button.set_text("wi-fi") - self._wifi_button.set_value("not connected") - - if display_network is not None: - strength = WifiIcon.get_strength_icon_idx(display_network.strength) - self._wifi_button.set_icon(self._wifi_full_txt if strength == 2 else self._wifi_medium_txt if strength == 1 else self._wifi_low_txt) - self._wifi_button.set_draw_lock(display_network.security_type not in (SecurityType.OPEN, SecurityType.UNSUPPORTED)) - else: - self._wifi_button.set_icon(self._wifi_slash_txt) - self._wifi_button.set_draw_lock(False) - def show_event(self): super().show_event() self._wifi_manager.set_active(True) From 9f7002fdf1519d9c1c30556ccef908e8d3ce1c44 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 24 Feb 2026 00:30:40 -0800 Subject: [PATCH 194/311] mici setup: set core affinity --- system/ui/mici_setup.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/system/ui/mici_setup.py b/system/ui/mici_setup.py index aa6f54c508..51feb63c05 100755 --- a/system/ui/mici_setup.py +++ b/system/ui/mici_setup.py @@ -14,8 +14,10 @@ from collections.abc import Callable import pyray as rl from cereal import log +from openpilot.common.realtime import config_realtime_process, set_core_affinity +from openpilot.common.swaglog import cloudlog from openpilot.common.utils import run_cmd -from openpilot.system.hardware import HARDWARE +from openpilot.system.hardware import HARDWARE, TICI from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.wifi_manager import WifiManager from openpilot.system.ui.lib.scroll_panel2 import GuiScrollPanel2 @@ -704,6 +706,14 @@ class Setup(Widget): def main(): + config_realtime_process(0, 51) + # attempt to affine. AGNOS will start setup with all cores, should only fail when manually launching with screen off + if TICI: + try: + set_core_affinity([5]) + except OSError: + cloudlog.exception("Failed to set core affinity for setup process") + try: gui_app.init_window("Setup") setup = Setup() From cf083711bbe12babad1425b802b48bcd3c3ee2ec Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 24 Feb 2026 00:34:03 -0800 Subject: [PATCH 195/311] mici setup: match tici network timeout --- system/ui/mici_setup.py | 7 +++---- system/ui/tici_setup.py | 4 ++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/system/ui/mici_setup.py b/system/ui/mici_setup.py index 51feb63c05..6c73f14e3a 100755 --- a/system/ui/mici_setup.py +++ b/system/ui/mici_setup.py @@ -51,11 +51,10 @@ exec ./launch_openpilot.sh class NetworkConnectivityMonitor: - def __init__(self, should_check: Callable[[], bool] | None = None, check_interval: float = 1.0): + def __init__(self, should_check: Callable[[], bool] | None = None): self.network_connected = threading.Event() self.wifi_connected = threading.Event() self._should_check = should_check or (lambda: True) - self._check_interval = check_interval self._stop_event = threading.Event() self._thread: threading.Thread | None = None @@ -80,7 +79,7 @@ class NetworkConnectivityMonitor: if self._should_check(): try: request = urllib.request.Request(OPENPILOT_URL, method="HEAD") - urllib.request.urlopen(request, timeout=1.0) + urllib.request.urlopen(request, timeout=2.0) self.network_connected.set() if HARDWARE.get_network_type() == NetworkType.wifi: self.wifi_connected.set() @@ -89,7 +88,7 @@ class NetworkConnectivityMonitor: else: self.reset() - if self._stop_event.wait(timeout=self._check_interval): + if self._stop_event.wait(timeout=1.0): break diff --git a/system/ui/tici_setup.py b/system/ui/tici_setup.py index 39f95cc8a0..8098e9ea27 100755 --- a/system/ui/tici_setup.py +++ b/system/ui/tici_setup.py @@ -218,7 +218,7 @@ class Setup(Widget): while not self.stop_network_check_thread.is_set(): if self.state == SetupState.NETWORK_SETUP: try: - urllib.request.urlopen(OPENPILOT_URL, timeout=2) + urllib.request.urlopen(OPENPILOT_URL, timeout=2.0) self.network_connected.set() if HARDWARE.get_network_type() == NetworkType.wifi: self.wifi_connected.set() @@ -226,7 +226,7 @@ class Setup(Widget): self.wifi_connected.clear() except Exception: self.network_connected.clear() - time.sleep(1) + time.sleep(1.0) def start_network_check(self): if self.network_check_thread is None or not self.network_check_thread.is_alive(): From faa23595af7e589a0581a4353f84b6f29f95b35b Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 24 Feb 2026 00:35:51 -0800 Subject: [PATCH 196/311] mici buttons and sliders: use semi bold --- system/ui/widgets/button.py | 2 +- system/ui/widgets/slider.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/system/ui/widgets/button.py b/system/ui/widgets/button.py index 9c0ea75b42..67125d7091 100644 --- a/system/ui/widgets/button.py +++ b/system/ui/widgets/button.py @@ -232,7 +232,7 @@ class SmallButton(Widget): self._load_assets() - self._label = UnifiedLabel(text, 36, font_weight=FontWeight.MEDIUM, + self._label = UnifiedLabel(text, 36, font_weight=FontWeight.SEMI_BOLD, text_color=rl.Color(255, 255, 255, int(255 * 0.9)), alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER, alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE) diff --git a/system/ui/widgets/slider.py b/system/ui/widgets/slider.py index 734c5b5d65..8f4bbfc011 100644 --- a/system/ui/widgets/slider.py +++ b/system/ui/widgets/slider.py @@ -33,7 +33,7 @@ class SmallSlider(Widget): self._is_dragging_circle = False - self._label = UnifiedLabel(title, font_size=36, font_weight=FontWeight.MEDIUM, text_color=rl.Color(255, 255, 255, int(255 * 0.65)), + self._label = UnifiedLabel(title, font_size=36, font_weight=FontWeight.SEMI_BOLD, text_color=rl.Color(255, 255, 255, int(255 * 0.65)), alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT, alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE, line_height=0.9) From 081bb51e586a4f48ad00d5eb05b9b0616b0c3fe7 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 24 Feb 2026 00:50:32 -0800 Subject: [PATCH 197/311] mici: add missing Scroller hide events --- selfdrive/ui/mici/layouts/main.py | 2 ++ selfdrive/ui/mici/layouts/offroad_alerts.py | 5 +++++ selfdrive/ui/mici/layouts/settings/developer.py | 4 ++++ selfdrive/ui/mici/layouts/settings/device.py | 4 ++++ selfdrive/ui/mici/layouts/settings/network/__init__.py | 1 + selfdrive/ui/mici/layouts/settings/toggles.py | 4 ++++ 6 files changed, 20 insertions(+) diff --git a/selfdrive/ui/mici/layouts/main.py b/selfdrive/ui/mici/layouts/main.py index dc01aba56f..3e3948eeab 100644 --- a/selfdrive/ui/mici/layouts/main.py +++ b/selfdrive/ui/mici/layouts/main.py @@ -63,9 +63,11 @@ class MiciMainLayout(Widget): device.add_interactive_timeout_callback(self._on_interactive_timeout) def show_event(self): + super().show_event() self._scroller.show_event() def hide_event(self): + super().hide_event() self._scroller.hide_event() def _scroll_to(self, layout: Widget): diff --git a/selfdrive/ui/mici/layouts/offroad_alerts.py b/selfdrive/ui/mici/layouts/offroad_alerts.py index 565d27593d..bc1cd02c5d 100644 --- a/selfdrive/ui/mici/layouts/offroad_alerts.py +++ b/selfdrive/ui/mici/layouts/offroad_alerts.py @@ -289,10 +289,15 @@ class MiciOffroadAlerts(Widget): def show_event(self): """Reset scroll position when shown and refresh alerts.""" + super().show_event() self._scroller.show_event() self._last_refresh = time.monotonic() self.refresh() + def hide_event(self): + super().hide_event() + self._scroller.hide_event() + def _update_state(self): """Periodically refresh alerts.""" # Refresh alerts periodically, not every frame diff --git a/selfdrive/ui/mici/layouts/settings/developer.py b/selfdrive/ui/mici/layouts/settings/developer.py index f107df7207..b04d696823 100644 --- a/selfdrive/ui/mici/layouts/settings/developer.py +++ b/selfdrive/ui/mici/layouts/settings/developer.py @@ -104,6 +104,10 @@ class DeveloperLayoutMici(NavWidget): self._scroller.show_event() self._update_toggles() + def hide_event(self): + super().hide_event() + self._scroller.hide_event() + def _render(self, rect: rl.Rectangle): self._scroller.render(rect) diff --git a/selfdrive/ui/mici/layouts/settings/device.py b/selfdrive/ui/mici/layouts/settings/device.py index d919df1c16..cd7172455f 100644 --- a/selfdrive/ui/mici/layouts/settings/device.py +++ b/selfdrive/ui/mici/layouts/settings/device.py @@ -345,5 +345,9 @@ class DeviceLayoutMici(NavWidget): super().show_event() self._scroller.show_event() + def hide_event(self): + super().hide_event() + self._scroller.hide_event() + def _render(self, rect: rl.Rectangle): self._scroller.render(rect) diff --git a/selfdrive/ui/mici/layouts/settings/network/__init__.py b/selfdrive/ui/mici/layouts/settings/network/__init__.py index 8f9b18b9a0..bdae924566 100644 --- a/selfdrive/ui/mici/layouts/settings/network/__init__.py +++ b/selfdrive/ui/mici/layouts/settings/network/__init__.py @@ -168,6 +168,7 @@ class NetworkLayoutMici(NavWidget): def hide_event(self): super().hide_event() + self._scroller.hide_event() self._wifi_manager.set_active(False) gui_app.set_nav_stack_tick(None) diff --git a/selfdrive/ui/mici/layouts/settings/toggles.py b/selfdrive/ui/mici/layouts/settings/toggles.py index b0f7189230..d6a91b40f7 100644 --- a/selfdrive/ui/mici/layouts/settings/toggles.py +++ b/selfdrive/ui/mici/layouts/settings/toggles.py @@ -71,6 +71,10 @@ class TogglesLayoutMici(NavWidget): self._scroller.show_event() self._update_toggles() + def hide_event(self): + super().hide_event() + self._scroller.hide_event() + def _update_toggles(self): ui_state.update_params() From 3352e48c517f8e8f19df95cb3a4bf88031f31cb2 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 24 Feb 2026 00:50:47 -0800 Subject: [PATCH 198/311] Scroller: add blocking scroll to (#37378) * rename * make tuple * blocking --- system/ui/widgets/scroller.py | 43 +++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/system/ui/widgets/scroller.py b/system/ui/widgets/scroller.py index 7eff8ae594..b01b34be14 100644 --- a/system/ui/widgets/scroller.py +++ b/system/ui/widgets/scroller.py @@ -78,8 +78,8 @@ class Scroller(Widget): self._reset_scroll_at_show = True - self._scrolling_to: float | None = None - self._scroll_filter = FirstOrderFilter(0.0, SCROLL_RC, 1 / gui_app.target_fps) + self._scrolling_to: tuple[float | None, bool] = (None, False) # target offset, block user scrolling + self._scrolling_to_filter = FirstOrderFilter(0.0, SCROLL_RC, 1 / gui_app.target_fps) self._zoom_filter = FirstOrderFilter(1.0, 0.2, 1 / gui_app.target_fps) self._zoom_out_t: float = 0.0 @@ -115,7 +115,9 @@ class Scroller(Widget): def set_reset_scroll_at_show(self, scroll: bool): self._reset_scroll_at_show = scroll - def scroll_to(self, pos: float, smooth: bool = False): + def scroll_to(self, pos: float, smooth: bool = False, block: bool = False): + assert not block or smooth, "Instant scroll cannot be blocking" + # already there if abs(pos) < 1: return @@ -123,13 +125,14 @@ class Scroller(Widget): # FIXME: the padding correction doesn't seem correct scroll_offset = self.scroll_panel.get_offset() - pos if smooth: - self._scrolling_to = scroll_offset + self._scrolling_to_filter.x = self.scroll_panel.get_offset() + self._scrolling_to = scroll_offset, block else: self.scroll_panel.set_offset(scroll_offset) @property def is_auto_scrolling(self) -> bool: - return self._scrolling_to is not None + return self._scrolling_to[0] is not None @property def items(self) -> list[Widget]: @@ -144,7 +147,7 @@ class Scroller(Widget): # preserve original touch valid callback original_touch_valid_callback = item._touch_valid_callback - item.set_touch_valid_callback(lambda: self.scroll_panel.is_touch_valid() and self.enabled and self._scrolling_to is None + item.set_touch_valid_callback(lambda: self.scroll_panel.is_touch_valid() and self.enabled and self._scrolling_to[0] is None and not self.moving_items and (original_touch_valid_callback() if original_touch_valid_callback else True)) @@ -154,7 +157,7 @@ class Scroller(Widget): def _update_state(self): if DO_ZOOM: - if self._scrolling_to is not None or self.scroll_panel.state != ScrollState.STEADY: + if self._scrolling_to[0] is not None or self.scroll_panel.state != ScrollState.STEADY: self._zoom_out_t = rl.get_time() + MIN_ZOOM_ANIMATION_TIME self._zoom_filter.update(0.85) else: @@ -164,24 +167,22 @@ class Scroller(Widget): else: self._zoom_filter.update(0.85) - # Cancel auto-scroll if user starts manually scrolling - if self._scrolling_to is not None and (self.scroll_panel.state == ScrollState.PRESSED or self.scroll_panel.state == ScrollState.MANUAL_SCROLL): - self._scrolling_to = None + # Cancel auto-scroll if user starts manually scrolling (unless blocking) + if (self.scroll_panel.state in (ScrollState.PRESSED, ScrollState.MANUAL_SCROLL) and + self._scrolling_to[0] is not None and not self._scrolling_to[1]): + self._scrolling_to = None, False - if self._scrolling_to is not None and len(self._pending_lift) == 0: - self._scroll_filter.update(self._scrolling_to) - self.scroll_panel.set_offset(self._scroll_filter.x) + if self._scrolling_to[0] is not None and len(self._pending_lift) == 0: + self._scrolling_to_filter.update(self._scrolling_to[0]) + self.scroll_panel.set_offset(self._scrolling_to_filter.x) - if abs(self._scroll_filter.x - self._scrolling_to) < 1: - self.scroll_panel.set_offset(self._scrolling_to) - self._scrolling_to = None - else: - # keep current scroll position up to date - self._scroll_filter.x = self.scroll_panel.get_offset() + if abs(self._scrolling_to_filter.x - self._scrolling_to[0]) < 1: + self.scroll_panel.set_offset(self._scrolling_to[0]) + self._scrolling_to = None, False def _get_scroll(self, visible_items: list[Widget], content_size: float) -> float: scroll_enabled = self._scroll_enabled() if callable(self._scroll_enabled) else self._scroll_enabled - self.scroll_panel.set_enabled(scroll_enabled and self.enabled) + self.scroll_panel.set_enabled(scroll_enabled and self.enabled and not self._scrolling_to[1]) self.scroll_panel.update(self._rect, content_size) if not self._snap_items: return round(self.scroll_panel.get_offset()) @@ -405,5 +406,7 @@ class Scroller(Widget): self._move_lift.clear() self._pending_lift.clear() self._pending_move.clear() + self._scrolling_to = None, False + self._scrolling_to_filter.x = 0.0 for item in self._items: item.hide_event() From c787507449024fe0dae3cb0f19f1a481613ea63e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Harald=20Sch=C3=A4fer?= Date: Tue, 24 Feb 2026 09:43:47 -0800 Subject: [PATCH 199/311] Revert "rm onnx (#37285)" (#37379) This reverts commit 23e1c4f49e63e550a44a85f0020b601bd9376a1d. --- pyproject.toml | 3 ++ scripts/reporter.py | 28 ++++---------- selfdrive/modeld/get_model_metadata.py | 40 ++++++------------- uv.lock | 53 ++++++++++++++++++++++++++ 4 files changed, 75 insertions(+), 49 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f983aca8ae..b40bed23d1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,6 +44,9 @@ dependencies = [ "libusb1", "spidev; platform_system == 'Linux'", + # modeld + "onnx >= 1.14.0", + # logging "pyzmq", "sentry-sdk", diff --git a/scripts/reporter.py b/scripts/reporter.py index d894b8af48..903fcc8911 100755 --- a/scripts/reporter.py +++ b/scripts/reporter.py @@ -1,33 +1,17 @@ #!/usr/bin/env python3 import os import glob - -from tinygrad.nn.onnx import OnnxPBParser +import onnx BASEDIR = os.path.abspath(os.path.join(os.path.dirname(os.path.realpath(__file__)), "../")) - MASTER_PATH = os.getenv("MASTER_PATH", BASEDIR) MODEL_PATH = "/selfdrive/modeld/models/" - -class MetadataOnnxPBParser(OnnxPBParser): - def _parse_ModelProto(self) -> dict: - obj = {"metadata_props": []} - for fid, wire_type in self._parse_message(self.reader.len): - match fid: - case 14: - obj["metadata_props"].append(self._parse_StringStringEntryProto()) - case _: - self.reader.skip_field(wire_type) - return obj - - def get_checkpoint(f): - model = MetadataOnnxPBParser(f).parse() - metadata = {prop["key"]: prop["value"] for prop in model["metadata_props"]} + model = onnx.load(f) + metadata = {prop.key: prop.value for prop in model.metadata_props} return metadata['model_checkpoint'].split('/')[0] - if __name__ == "__main__": print("| | master | PR branch |") print("|-| ----- | --------- |") @@ -40,4 +24,8 @@ if __name__ == "__main__": fn = os.path.basename(f) master = get_checkpoint(MASTER_PATH + MODEL_PATH + fn) pr = get_checkpoint(BASEDIR + MODEL_PATH + fn) - print("|", fn, "|", f"[{master}](https://reporter.comma.life/experiment/{master})", "|", f"[{pr}](https://reporter.comma.life/experiment/{pr})", "|") + print( + "|", fn, "|", + f"[{master}](https://reporter.comma.life/experiment/{master})", "|", + f"[{pr}](https://reporter.comma.life/experiment/{pr})", "|" + ) diff --git a/selfdrive/modeld/get_model_metadata.py b/selfdrive/modeld/get_model_metadata.py index 838b1e9f40..2001d23d75 100755 --- a/selfdrive/modeld/get_model_metadata.py +++ b/selfdrive/modeld/get_model_metadata.py @@ -1,51 +1,33 @@ #!/usr/bin/env python3 import sys import pathlib +import onnx import codecs import pickle from typing import Any -from tinygrad.nn.onnx import OnnxPBParser - - -class MetadataOnnxPBParser(OnnxPBParser): - def _parse_ModelProto(self) -> dict: - obj: dict[str, Any] = {"graph": {"input": [], "output": []}, "metadata_props": []} - for fid, wire_type in self._parse_message(self.reader.len): - match fid: - case 7: - obj["graph"] = self._parse_GraphProto() - case 14: - obj["metadata_props"].append(self._parse_StringStringEntryProto()) - case _: - self.reader.skip_field(wire_type) - return obj - - -def get_name_and_shape(value_info: dict[str, Any]) -> tuple[str, tuple[int, ...]]: - shape = tuple(int(dim) if isinstance(dim, int) else 0 for dim in value_info["parsed_type"].shape) - name = value_info["name"] +def get_name_and_shape(value_info:onnx.ValueInfoProto) -> tuple[str, tuple[int,...]]: + shape = tuple([int(dim.dim_value) for dim in value_info.type.tensor_type.shape.dim]) + name = value_info.name return name, shape - -def get_metadata_value_by_name(model: dict[str, Any], name: str) -> str | Any: - for prop in model["metadata_props"]: - if prop["key"] == name: - return prop["value"] +def get_metadata_value_by_name(model:onnx.ModelProto, name:str) -> str | Any: + for prop in model.metadata_props: + if prop.key == name: + return prop.value return None - if __name__ == "__main__": model_path = pathlib.Path(sys.argv[1]) - model = MetadataOnnxPBParser(model_path).parse() + model = onnx.load(str(model_path)) output_slices = get_metadata_value_by_name(model, 'output_slices') assert output_slices is not None, 'output_slices not found in metadata' metadata = { 'model_checkpoint': get_metadata_value_by_name(model, 'model_checkpoint'), 'output_slices': pickle.loads(codecs.decode(output_slices.encode(), "base64")), - 'input_shapes': dict(get_name_and_shape(x) for x in model["graph"]["input"]), - 'output_shapes': dict(get_name_and_shape(x) for x in model["graph"]["output"]), + 'input_shapes': dict([get_name_and_shape(x) for x in model.graph.input]), + 'output_shapes': dict([get_name_and_shape(x) for x in model.graph.output]) } metadata_path = model_path.parent / (model_path.stem + '_metadata.pkl') diff --git a/uv.lock b/uv.lock index 1dcc489227..b27732b18b 100644 --- a/uv.lock +++ b/uv.lock @@ -691,6 +691,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521, upload-time = "2023-11-20T17:51:08.587Z" }, ] +[[package]] +name = "ml-dtypes" +version = "0.5.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/4a/c27b42ed9b1c7d13d9ba8b6905dece787d6259152f2309338aed29b2447b/ml_dtypes-0.5.4.tar.gz", hash = "sha256:8ab06a50fb9bf9666dd0fe5dfb4676fa2b0ac0f31ecff72a6c3af8e22c063453", size = 692314, upload-time = "2025-11-17T22:32:31.031Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/b8/3c70881695e056f8a32f8b941126cf78775d9a4d7feba8abcb52cb7b04f2/ml_dtypes-0.5.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a174837a64f5b16cab6f368171a1a03a27936b31699d167684073ff1c4237dac", size = 676927, upload-time = "2025-11-17T22:31:48.182Z" }, + { url = "https://files.pythonhosted.org/packages/54/0f/428ef6881782e5ebb7eca459689448c0394fa0a80bea3aa9262cba5445ea/ml_dtypes-0.5.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a7f7c643e8b1320fd958bf098aa7ecf70623a42ec5154e3be3be673f4c34d900", size = 5028464, upload-time = "2025-11-17T22:31:50.135Z" }, + { url = "https://files.pythonhosted.org/packages/3a/cb/28ce52eb94390dda42599c98ea0204d74799e4d8047a0eb559b6fd648056/ml_dtypes-0.5.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ad459e99793fa6e13bd5b7e6792c8f9190b4e5a1b45c63aba14a4d0a7f1d5ff", size = 5009002, upload-time = "2025-11-17T22:31:52.001Z" }, + { url = "https://files.pythonhosted.org/packages/f5/f0/0cfadd537c5470378b1b32bd859cf2824972174b51b873c9d95cfd7475a5/ml_dtypes-0.5.4-cp312-cp312-win_amd64.whl", hash = "sha256:c1a953995cccb9e25a4ae19e34316671e4e2edaebe4cf538229b1fc7109087b7", size = 212222, upload-time = "2025-11-17T22:31:53.742Z" }, + { url = "https://files.pythonhosted.org/packages/16/2e/9acc86985bfad8f2c2d30291b27cd2bb4c74cea08695bd540906ed744249/ml_dtypes-0.5.4-cp312-cp312-win_arm64.whl", hash = "sha256:9bad06436568442575beb2d03389aa7456c690a5b05892c471215bfd8cf39460", size = 160793, upload-time = "2025-11-17T22:31:55.358Z" }, +] + [[package]] name = "mpmath" version = "1.3.0" @@ -751,6 +767,26 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2f/a3/56c5c604fae6dd40fa2ed3040d005fca97e91bd320d232ac9931d77ba13c/numpy-2.4.2-cp312-cp312-win_arm64.whl", hash = "sha256:fbde1b0c6e81d56f5dccd95dd4a711d9b95df1ae4009a60887e56b27e8d903fa", size = 10220171, upload-time = "2026-01-31T23:11:14.684Z" }, ] +[[package]] +name = "onnx" +version = "1.20.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ml-dtypes" }, + { name = "numpy" }, + { name = "protobuf" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3b/8a/335c03a8683a88a32f9a6bb98899ea6df241a41df64b37b9696772414794/onnx-1.20.1.tar.gz", hash = "sha256:ded16de1df563d51fbc1ad885f2a426f814039d8b5f4feb77febe09c0295ad67", size = 12048980, upload-time = "2026-01-10T01:40:03.043Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/4c/4b17e82f91ab9aa07ff595771e935ca73547b035030dc5f5a76e63fbfea9/onnx-1.20.1-cp312-abi3-macosx_12_0_universal2.whl", hash = "sha256:1d923bb4f0ce1b24c6859222a7e6b2f123e7bfe7623683662805f2e7b9e95af2", size = 17903547, upload-time = "2026-01-10T01:39:31.015Z" }, + { url = "https://files.pythonhosted.org/packages/64/5e/1bfa100a9cb3f2d3d5f2f05f52f7e60323b0e20bb0abace1ae64dbc88f25/onnx-1.20.1-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ddc0b7d8b5a94627dc86c533d5e415af94cbfd103019a582669dad1f56d30281", size = 17412021, upload-time = "2026-01-10T01:39:33.885Z" }, + { url = "https://files.pythonhosted.org/packages/fb/71/d3fec0dcf9a7a99e7368112d9c765154e81da70fcba1e3121131a45c245b/onnx-1.20.1-cp312-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9336b6b8e6efcf5c490a845f6afd7e041c89a56199aeda384ed7d58fb953b080", size = 17510450, upload-time = "2026-01-10T01:39:36.589Z" }, + { url = "https://files.pythonhosted.org/packages/74/a7/edce1403e05a46e59b502fae8e3350ceeac5841f8e8f1561e98562ed9b09/onnx-1.20.1-cp312-abi3-win32.whl", hash = "sha256:564c35a94811979808ab5800d9eb4f3f32c12daedba7e33ed0845f7c61ef2431", size = 16238216, upload-time = "2026-01-10T01:39:39.46Z" }, + { url = "https://files.pythonhosted.org/packages/8b/c7/8690c81200ae652ac550c1df52f89d7795e6cc941f3cb38c9ef821419e80/onnx-1.20.1-cp312-abi3-win_amd64.whl", hash = "sha256:9fe7f9a633979d50984b94bda8ceb7807403f59a341d09d19342dc544d0ca1d5", size = 16389207, upload-time = "2026-01-10T01:39:41.955Z" }, + { url = "https://files.pythonhosted.org/packages/01/a0/4fb0e6d36eaf079af366b2c1f68bafe92df6db963e2295da84388af64abc/onnx-1.20.1-cp312-abi3-win_arm64.whl", hash = "sha256:21d747348b1c8207406fa2f3e12b82f53e0d5bb3958bcd0288bd27d3cb6ebb00", size = 16344155, upload-time = "2026-01-10T01:39:45.536Z" }, +] + [[package]] name = "opencv-python-headless" version = "4.13.0.92" @@ -791,6 +827,7 @@ dependencies = [ { name = "libusb1" }, { name = "ncurses" }, { name = "numpy" }, + { name = "onnx" }, { name = "psutil" }, { name = "pycapnp" }, { name = "pycryptodome" }, @@ -873,6 +910,7 @@ requires-dist = [ { name = "mkdocs", marker = "extra == 'docs'" }, { name = "ncurses", git = "https://github.com/commaai/dependencies.git?subdirectory=ncurses&rev=releases" }, { name = "numpy", specifier = ">=2.0" }, + { name = "onnx", specifier = ">=1.14.0" }, { name = "opencv-python-headless", marker = "extra == 'dev'" }, { name = "pre-commit-hooks", marker = "extra == 'testing'" }, { name = "psutil" }, @@ -1039,6 +1077,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, ] +[[package]] +name = "protobuf" +version = "6.33.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/25/7c72c307aafc96fa87062aa6291d9f7c94836e43214d43722e86037aac02/protobuf-6.33.5.tar.gz", hash = "sha256:6ddcac2a081f8b7b9642c09406bc6a4290128fce5f471cddd165960bb9119e5c", size = 444465, upload-time = "2026-01-29T21:51:33.494Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/79/af92d0a8369732b027e6d6084251dd8e782c685c72da161bd4a2e00fbabb/protobuf-6.33.5-cp310-abi3-win32.whl", hash = "sha256:d71b040839446bac0f4d162e758bea99c8251161dae9d0983a3b88dee345153b", size = 425769, upload-time = "2026-01-29T21:51:21.751Z" }, + { url = "https://files.pythonhosted.org/packages/55/75/bb9bc917d10e9ee13dee8607eb9ab963b7cf8be607c46e7862c748aa2af7/protobuf-6.33.5-cp310-abi3-win_amd64.whl", hash = "sha256:3093804752167bcab3998bec9f1048baae6e29505adaf1afd14a37bddede533c", size = 437118, upload-time = "2026-01-29T21:51:24.022Z" }, + { url = "https://files.pythonhosted.org/packages/a2/6b/e48dfc1191bc5b52950246275bf4089773e91cb5ba3592621723cdddca62/protobuf-6.33.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:a5cb85982d95d906df1e2210e58f8e4f1e3cdc088e52c921a041f9c9a0386de5", size = 427766, upload-time = "2026-01-29T21:51:25.413Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b1/c79468184310de09d75095ed1314b839eb2f72df71097db9d1404a1b2717/protobuf-6.33.5-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:9b71e0281f36f179d00cbcb119cb19dec4d14a81393e5ea220f64b286173e190", size = 324638, upload-time = "2026-01-29T21:51:26.423Z" }, + { url = "https://files.pythonhosted.org/packages/c5/f5/65d838092fd01c44d16037953fd4c2cc851e783de9b8f02b27ec4ffd906f/protobuf-6.33.5-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:8afa18e1d6d20af15b417e728e9f60f3aa108ee76f23c3b2c07a2c3b546d3afd", size = 339411, upload-time = "2026-01-29T21:51:27.446Z" }, + { url = "https://files.pythonhosted.org/packages/9b/53/a9443aa3ca9ba8724fdfa02dd1887c1bcd8e89556b715cfbacca6b63dbec/protobuf-6.33.5-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:cbf16ba3350fb7b889fca858fb215967792dc125b35c7976ca4818bee3521cf0", size = 323465, upload-time = "2026-01-29T21:51:28.925Z" }, + { url = "https://files.pythonhosted.org/packages/57/bf/2086963c69bdac3d7cff1cc7ff79b8ce5ea0bec6797a017e1be338a46248/protobuf-6.33.5-py3-none-any.whl", hash = "sha256:69915a973dd0f60f31a08b8318b73eab2bd6a392c79184b3612226b0a3f8ec02", size = 170687, upload-time = "2026-01-29T21:51:32.557Z" }, +] + [[package]] name = "psutil" version = "7.2.2" From a064de7ceb483360fafba22f6bd05cc7081827e7 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Tue, 24 Feb 2026 12:00:39 -0800 Subject: [PATCH 200/311] use vendored libjpeg-turbo (#37381) --- SConstruct | 11 +++-------- pyproject.toml | 2 ++ tools/setup_dependencies.sh | 1 - uv.lock | 32 +++++++++++++++++++++++--------- 4 files changed, 28 insertions(+), 18 deletions(-) diff --git a/SConstruct b/SConstruct index 16c1d8ee73..8d58cc012a 100644 --- a/SConstruct +++ b/SConstruct @@ -28,7 +28,6 @@ AddOption('--minimal', arch = subprocess.check_output(["uname", "-m"], encoding='utf8').rstrip() if platform.system() == "Darwin": arch = "Darwin" - brew_prefix = subprocess.check_output(['brew', '--prefix'], encoding='utf8').strip() elif arch == "aarch64" and os.path.isfile('/TICI'): arch = "larch64" assert arch in [ @@ -42,11 +41,13 @@ if arch != "larch64": import capnproto import eigen import ffmpeg as ffmpeg_pkg + import libjpeg import ncurses + import openssl3 import python3_dev import zeromq import zstd - pkgs = [capnproto, eigen, ffmpeg_pkg, ncurses, zeromq, zstd] + pkgs = [capnproto, eigen, ffmpeg_pkg, libjpeg, ncurses, openssl3, zeromq, zstd] py_include = python3_dev.INCLUDE_DIR else: # TODO: remove when AGNOS has our new vendor pkgs @@ -121,16 +122,10 @@ if arch == "larch64": env.Append(CXXFLAGS=arch_flags) elif arch == "Darwin": env.Append(LIBPATH=[ - f"{brew_prefix}/lib", - f"{brew_prefix}/opt/openssl@3.0/lib", "/System/Library/Frameworks/OpenGL.framework/Libraries", ]) env.Append(CCFLAGS=["-DGL_SILENCE_DEPRECATION"]) env.Append(CXXFLAGS=["-DGL_SILENCE_DEPRECATION"]) - env.Append(CPPPATH=[ - f"{brew_prefix}/include", - f"{brew_prefix}/opt/openssl@3.0/include", - ]) else: env.Append(LIBPATH=[ "/usr/lib", diff --git a/pyproject.toml b/pyproject.toml index b40bed23d1..e7debf78dc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,6 +29,8 @@ dependencies = [ "capnproto @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=capnproto", "eigen @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=eigen", "ffmpeg @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=ffmpeg", + "libjpeg @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=libjpeg", + "openssl3 @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=openssl3", "python3-dev @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=python3-dev", "zstd @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=zstd", "ncurses @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=ncurses", diff --git a/tools/setup_dependencies.sh b/tools/setup_dependencies.sh index df06d4ab1d..7c0bf7d708 100755 --- a/tools/setup_dependencies.sh +++ b/tools/setup_dependencies.sh @@ -42,7 +42,6 @@ function install_ubuntu_deps() { ca-certificates \ build-essential \ curl \ - libssl-dev \ libcurl4-openssl-dev \ locales \ git \ diff --git a/uv.lock b/uv.lock index b27732b18b..658f62a331 100644 --- a/uv.lock +++ b/uv.lock @@ -116,7 +116,7 @@ wheels = [ [[package]] name = "capnproto" version = "1.0.1" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=capnproto&rev=releases#736268e60bb617876ebaa05c5a4db9b0d70a4793" } +source = { git = "https://github.com/commaai/dependencies.git?subdirectory=capnproto&rev=releases#31af284805d0787a689e129311d992bec14a2400" } [[package]] name = "casadi" @@ -376,7 +376,7 @@ wheels = [ [[package]] name = "eigen" version = "3.4.0" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=eigen&rev=releases#736268e60bb617876ebaa05c5a4db9b0d70a4793" } +source = { git = "https://github.com/commaai/dependencies.git?subdirectory=eigen&rev=releases#31af284805d0787a689e129311d992bec14a2400" } [[package]] name = "execnet" @@ -390,7 +390,7 @@ wheels = [ [[package]] name = "ffmpeg" version = "7.1.0" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=ffmpeg&rev=releases#736268e60bb617876ebaa05c5a4db9b0d70a4793" } +source = { git = "https://github.com/commaai/dependencies.git?subdirectory=ffmpeg&rev=releases#31af284805d0787a689e129311d992bec14a2400" } [[package]] name = "fonttools" @@ -437,7 +437,7 @@ wheels = [ [[package]] name = "gcc-arm-none-eabi" version = "13.2.1" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=gcc-arm-none-eabi&rev=releases#736268e60bb617876ebaa05c5a4db9b0d70a4793" } +source = { git = "https://github.com/commaai/dependencies.git?subdirectory=gcc-arm-none-eabi&rev=releases#31af284805d0787a689e129311d992bec14a2400" } [[package]] name = "ghp-import" @@ -454,7 +454,7 @@ wheels = [ [[package]] name = "git-lfs" version = "3.6.1" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=git-lfs&rev=releases#736268e60bb617876ebaa05c5a4db9b0d70a4793" } +source = { git = "https://github.com/commaai/dependencies.git?subdirectory=git-lfs&rev=releases#31af284805d0787a689e129311d992bec14a2400" } [[package]] name = "google-crc32c" @@ -569,6 +569,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e2/92/5f3068cf15ee5cb624a0c7596e67e2a0bb2adee33f71c379054a491d07da/kiwisolver-1.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:2c1a4f57df73965f3f14df20b80ee29e6a7930a57d2d9e8491a25f676e197c60", size = 64992, upload-time = "2025-08-10T21:26:25.732Z" }, ] +[[package]] +name = "libjpeg" +version = "3.1.0" +source = { git = "https://github.com/commaai/dependencies.git?subdirectory=libjpeg&rev=releases#31af284805d0787a689e129311d992bec14a2400" } + [[package]] name = "libusb1" version = "3.3.1" @@ -746,7 +751,7 @@ wheels = [ [[package]] name = "ncurses" version = "6.5" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=ncurses&rev=releases#736268e60bb617876ebaa05c5a4db9b0d70a4793" } +source = { git = "https://github.com/commaai/dependencies.git?subdirectory=ncurses&rev=releases#31af284805d0787a689e129311d992bec14a2400" } [[package]] name = "numpy" @@ -824,10 +829,12 @@ dependencies = [ { name = "inputs" }, { name = "jeepney" }, { name = "json-rpc" }, + { name = "libjpeg" }, { name = "libusb1" }, { name = "ncurses" }, { name = "numpy" }, { name = "onnx" }, + { name = "openssl3" }, { name = "psutil" }, { name = "pycapnp" }, { name = "pycryptodome" }, @@ -904,6 +911,7 @@ requires-dist = [ { name = "jeepney" }, { name = "jinja2", marker = "extra == 'docs'" }, { name = "json-rpc" }, + { name = "libjpeg", git = "https://github.com/commaai/dependencies.git?subdirectory=libjpeg&rev=releases" }, { name = "libusb1" }, { name = "matplotlib", marker = "extra == 'dev'" }, { name = "metadrive-simulator", marker = "platform_machine != 'aarch64' and extra == 'tools'", git = "https://github.com/commaai/metadrive.git?rev=minimal" }, @@ -912,6 +920,7 @@ requires-dist = [ { name = "numpy", specifier = ">=2.0" }, { name = "onnx", specifier = ">=1.14.0" }, { name = "opencv-python-headless", marker = "extra == 'dev'" }, + { name = "openssl3", git = "https://github.com/commaai/dependencies.git?subdirectory=openssl3&rev=releases" }, { name = "pre-commit-hooks", marker = "extra == 'testing'" }, { name = "psutil" }, { name = "pycapnp" }, @@ -947,6 +956,11 @@ requires-dist = [ ] provides-extras = ["docs", "testing", "dev", "tools"] +[[package]] +name = "openssl3" +version = "3.4.1" +source = { git = "https://github.com/commaai/dependencies.git?subdirectory=openssl3&rev=releases#31af284805d0787a689e129311d992bec14a2400" } + [[package]] name = "packaging" version = "26.0" @@ -1331,7 +1345,7 @@ wheels = [ [[package]] name = "python3-dev" version = "3.12.8" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=python3-dev&rev=releases#736268e60bb617876ebaa05c5a4db9b0d70a4793" } +source = { git = "https://github.com/commaai/dependencies.git?subdirectory=python3-dev&rev=releases#31af284805d0787a689e129311d992bec14a2400" } [[package]] name = "pyyaml" @@ -1699,7 +1713,7 @@ wheels = [ [[package]] name = "zeromq" version = "4.3.5" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=zeromq&rev=releases#736268e60bb617876ebaa05c5a4db9b0d70a4793" } +source = { git = "https://github.com/commaai/dependencies.git?subdirectory=zeromq&rev=releases#31af284805d0787a689e129311d992bec14a2400" } [[package]] name = "zstandard" @@ -1729,4 +1743,4 @@ wheels = [ [[package]] name = "zstd" version = "1.5.6" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=zstd&rev=releases#736268e60bb617876ebaa05c5a4db9b0d70a4793" } +source = { git = "https://github.com/commaai/dependencies.git?subdirectory=zstd&rev=releases#31af284805d0787a689e129311d992bec14a2400" } From 6db6d792111c523455d9bdc0ac817e2f534f50e7 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 24 Feb 2026 15:34:48 -0800 Subject: [PATCH 201/311] WifiUi: decouple button update from move/scroll (#37383) * meh * hmm * can also do this * keep behavior * rm --- .../mici/layouts/settings/network/wifi_ui.py | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py index 2efc45f46a..ec6b0c9dc5 100644 --- a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py +++ b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py @@ -325,19 +325,11 @@ class WifiUIMici(NavWidget): if isinstance(btn, WifiButton) and btn.network.ssid not in self._networks: btn.set_network_missing(True) - # Move connecting/connected network to the front with animation - front_ssid = self._wifi_manager.wifi_state.ssid - front_btn_idx = next((i for i, btn in enumerate(self._scroller.items) - if isinstance(btn, WifiButton) and - btn.network.ssid == front_ssid), None) if front_ssid else None - - if front_btn_idx is not None and front_btn_idx > 0: - self._scroller.move_item(front_btn_idx, 0) + self._move_network_to_front(self._wifi_manager.wifi_state.ssid) def _connect_with_password(self, ssid: str, password: str): self._wifi_manager.connect_to_network(ssid, password) - self._scroller.scroll_to(self._scroller.scroll_panel.get_offset(), smooth=True) - self._update_buttons() + self._move_network_to_front(ssid, scroll=True) def _connect_to_network(self, ssid: str): network = self._networks.get(ssid) @@ -353,8 +345,7 @@ class WifiUIMici(NavWidget): self._on_need_auth(network.ssid, False) return - self._scroller.scroll_to(self._scroller.scroll_panel.get_offset(), smooth=True) - self._update_buttons() + self._move_network_to_front(ssid, scroll=True) def _on_need_auth(self, ssid, incorrect_password=True): if incorrect_password: @@ -374,6 +365,19 @@ class WifiUIMici(NavWidget): if isinstance(btn, WifiButton) and btn.network.ssid == ssid: btn.on_forgotten() + def _move_network_to_front(self, ssid: str | None, scroll: bool = False): + # Move connecting/connected network to the front with animation + front_btn_idx = next((i for i, btn in enumerate(self._scroller.items) + if isinstance(btn, WifiButton) and + btn.network.ssid == ssid), None) if ssid else None + + if front_btn_idx is not None and front_btn_idx > 0: + self._scroller.move_item(front_btn_idx, 0) + + if scroll: + # Scroll to the new position of the network + self._scroller.scroll_to(self._scroller.scroll_panel.get_offset(), smooth=True) + def _update_state(self): super()._update_state() From 159d3a30e318c65cef66fd8ff5ff2345e44a9ee6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Harald=20Sch=C3=A4fer?= Date: Tue, 24 Feb 2026 15:35:52 -0800 Subject: [PATCH 202/311] RM onnx (#37377) * Give tf flags to onnx parse * rm onnx again * update lock --- pyproject.toml | 3 -- scripts/reporter.py | 28 ++++++++++---- selfdrive/modeld/SConscript | 15 ++++---- selfdrive/modeld/get_model_metadata.py | 40 +++++++++++++------ uv.lock | 53 -------------------------- 5 files changed, 57 insertions(+), 82 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e7debf78dc..bdcbd77801 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,9 +46,6 @@ dependencies = [ "libusb1", "spidev; platform_system == 'Linux'", - # modeld - "onnx >= 1.14.0", - # logging "pyzmq", "sentry-sdk", diff --git a/scripts/reporter.py b/scripts/reporter.py index 903fcc8911..d894b8af48 100755 --- a/scripts/reporter.py +++ b/scripts/reporter.py @@ -1,17 +1,33 @@ #!/usr/bin/env python3 import os import glob -import onnx + +from tinygrad.nn.onnx import OnnxPBParser BASEDIR = os.path.abspath(os.path.join(os.path.dirname(os.path.realpath(__file__)), "../")) + MASTER_PATH = os.getenv("MASTER_PATH", BASEDIR) MODEL_PATH = "/selfdrive/modeld/models/" + +class MetadataOnnxPBParser(OnnxPBParser): + def _parse_ModelProto(self) -> dict: + obj = {"metadata_props": []} + for fid, wire_type in self._parse_message(self.reader.len): + match fid: + case 14: + obj["metadata_props"].append(self._parse_StringStringEntryProto()) + case _: + self.reader.skip_field(wire_type) + return obj + + def get_checkpoint(f): - model = onnx.load(f) - metadata = {prop.key: prop.value for prop in model.metadata_props} + model = MetadataOnnxPBParser(f).parse() + metadata = {prop["key"]: prop["value"] for prop in model["metadata_props"]} return metadata['model_checkpoint'].split('/')[0] + if __name__ == "__main__": print("| | master | PR branch |") print("|-| ----- | --------- |") @@ -24,8 +40,4 @@ if __name__ == "__main__": fn = os.path.basename(f) master = get_checkpoint(MASTER_PATH + MODEL_PATH + fn) pr = get_checkpoint(BASEDIR + MODEL_PATH + fn) - print( - "|", fn, "|", - f"[{master}](https://reporter.comma.life/experiment/{master})", "|", - f"[{pr}](https://reporter.comma.life/experiment/{pr})", "|" - ) + print("|", fn, "|", f"[{master}](https://reporter.comma.life/experiment/{master})", "|", f"[{pr}](https://reporter.comma.life/experiment/{pr})", "|") diff --git a/selfdrive/modeld/SConscript b/selfdrive/modeld/SConscript index ed5c50beb3..95ac06bb1a 100644 --- a/selfdrive/modeld/SConscript +++ b/selfdrive/modeld/SConscript @@ -13,19 +13,20 @@ tinygrad_files = ["#"+x for x in glob.glob(env.Dir("#tinygrad_repo").relpath + " def estimate_pickle_max_size(onnx_size): return 1.2 * onnx_size + 10 * 1024 * 1024 # 20% + 10MB is plenty -# Get model metadata -for model_name in ['driving_vision', 'driving_policy', 'dmonitoring_model']: - fn = File(f"models/{model_name}").abspath - script_files = [File(Dir("#selfdrive/modeld").File("get_model_metadata.py").abspath)] - cmd = f'python3 {Dir("#selfdrive/modeld").abspath}/get_model_metadata.py {fn}.onnx' - lenv.Command(fn + "_metadata.pkl", [fn + ".onnx"] + tinygrad_files + script_files, cmd) - # compile warp # THREADS=0 is need to prevent bug: https://github.com/tinygrad/tinygrad/issues/14689 tg_flags = { 'larch64': 'DEV=QCOM FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0', 'Darwin': f'DEV=CPU THREADS=0 HOME={os.path.expanduser("~")}', # tinygrad calls brew which needs a $HOME in the env }.get(arch, 'DEV=CPU CPU_LLVM=1 THREADS=0') + +# Get model metadata +for model_name in ['driving_vision', 'driving_policy', 'dmonitoring_model']: + fn = File(f"models/{model_name}").abspath + script_files = [File(Dir("#selfdrive/modeld").File("get_model_metadata.py").abspath)] + cmd = f'{tg_flags} python3 {Dir("#selfdrive/modeld").abspath}/get_model_metadata.py {fn}.onnx' + lenv.Command(fn + "_metadata.pkl", [fn + ".onnx"] + tinygrad_files + script_files, cmd) + image_flag = { 'larch64': 'IMAGE=2', }.get(arch, 'IMAGE=0') diff --git a/selfdrive/modeld/get_model_metadata.py b/selfdrive/modeld/get_model_metadata.py index 2001d23d75..838b1e9f40 100755 --- a/selfdrive/modeld/get_model_metadata.py +++ b/selfdrive/modeld/get_model_metadata.py @@ -1,33 +1,51 @@ #!/usr/bin/env python3 import sys import pathlib -import onnx import codecs import pickle from typing import Any -def get_name_and_shape(value_info:onnx.ValueInfoProto) -> tuple[str, tuple[int,...]]: - shape = tuple([int(dim.dim_value) for dim in value_info.type.tensor_type.shape.dim]) - name = value_info.name +from tinygrad.nn.onnx import OnnxPBParser + + +class MetadataOnnxPBParser(OnnxPBParser): + def _parse_ModelProto(self) -> dict: + obj: dict[str, Any] = {"graph": {"input": [], "output": []}, "metadata_props": []} + for fid, wire_type in self._parse_message(self.reader.len): + match fid: + case 7: + obj["graph"] = self._parse_GraphProto() + case 14: + obj["metadata_props"].append(self._parse_StringStringEntryProto()) + case _: + self.reader.skip_field(wire_type) + return obj + + +def get_name_and_shape(value_info: dict[str, Any]) -> tuple[str, tuple[int, ...]]: + shape = tuple(int(dim) if isinstance(dim, int) else 0 for dim in value_info["parsed_type"].shape) + name = value_info["name"] return name, shape -def get_metadata_value_by_name(model:onnx.ModelProto, name:str) -> str | Any: - for prop in model.metadata_props: - if prop.key == name: - return prop.value + +def get_metadata_value_by_name(model: dict[str, Any], name: str) -> str | Any: + for prop in model["metadata_props"]: + if prop["key"] == name: + return prop["value"] return None + if __name__ == "__main__": model_path = pathlib.Path(sys.argv[1]) - model = onnx.load(str(model_path)) + model = MetadataOnnxPBParser(model_path).parse() output_slices = get_metadata_value_by_name(model, 'output_slices') assert output_slices is not None, 'output_slices not found in metadata' metadata = { 'model_checkpoint': get_metadata_value_by_name(model, 'model_checkpoint'), 'output_slices': pickle.loads(codecs.decode(output_slices.encode(), "base64")), - 'input_shapes': dict([get_name_and_shape(x) for x in model.graph.input]), - 'output_shapes': dict([get_name_and_shape(x) for x in model.graph.output]) + 'input_shapes': dict(get_name_and_shape(x) for x in model["graph"]["input"]), + 'output_shapes': dict(get_name_and_shape(x) for x in model["graph"]["output"]), } metadata_path = model_path.parent / (model_path.stem + '_metadata.pkl') diff --git a/uv.lock b/uv.lock index 658f62a331..9094417693 100644 --- a/uv.lock +++ b/uv.lock @@ -696,22 +696,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521, upload-time = "2023-11-20T17:51:08.587Z" }, ] -[[package]] -name = "ml-dtypes" -version = "0.5.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0e/4a/c27b42ed9b1c7d13d9ba8b6905dece787d6259152f2309338aed29b2447b/ml_dtypes-0.5.4.tar.gz", hash = "sha256:8ab06a50fb9bf9666dd0fe5dfb4676fa2b0ac0f31ecff72a6c3af8e22c063453", size = 692314, upload-time = "2025-11-17T22:32:31.031Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/b8/3c70881695e056f8a32f8b941126cf78775d9a4d7feba8abcb52cb7b04f2/ml_dtypes-0.5.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a174837a64f5b16cab6f368171a1a03a27936b31699d167684073ff1c4237dac", size = 676927, upload-time = "2025-11-17T22:31:48.182Z" }, - { url = "https://files.pythonhosted.org/packages/54/0f/428ef6881782e5ebb7eca459689448c0394fa0a80bea3aa9262cba5445ea/ml_dtypes-0.5.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a7f7c643e8b1320fd958bf098aa7ecf70623a42ec5154e3be3be673f4c34d900", size = 5028464, upload-time = "2025-11-17T22:31:50.135Z" }, - { url = "https://files.pythonhosted.org/packages/3a/cb/28ce52eb94390dda42599c98ea0204d74799e4d8047a0eb559b6fd648056/ml_dtypes-0.5.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ad459e99793fa6e13bd5b7e6792c8f9190b4e5a1b45c63aba14a4d0a7f1d5ff", size = 5009002, upload-time = "2025-11-17T22:31:52.001Z" }, - { url = "https://files.pythonhosted.org/packages/f5/f0/0cfadd537c5470378b1b32bd859cf2824972174b51b873c9d95cfd7475a5/ml_dtypes-0.5.4-cp312-cp312-win_amd64.whl", hash = "sha256:c1a953995cccb9e25a4ae19e34316671e4e2edaebe4cf538229b1fc7109087b7", size = 212222, upload-time = "2025-11-17T22:31:53.742Z" }, - { url = "https://files.pythonhosted.org/packages/16/2e/9acc86985bfad8f2c2d30291b27cd2bb4c74cea08695bd540906ed744249/ml_dtypes-0.5.4-cp312-cp312-win_arm64.whl", hash = "sha256:9bad06436568442575beb2d03389aa7456c690a5b05892c471215bfd8cf39460", size = 160793, upload-time = "2025-11-17T22:31:55.358Z" }, -] - [[package]] name = "mpmath" version = "1.3.0" @@ -772,26 +756,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2f/a3/56c5c604fae6dd40fa2ed3040d005fca97e91bd320d232ac9931d77ba13c/numpy-2.4.2-cp312-cp312-win_arm64.whl", hash = "sha256:fbde1b0c6e81d56f5dccd95dd4a711d9b95df1ae4009a60887e56b27e8d903fa", size = 10220171, upload-time = "2026-01-31T23:11:14.684Z" }, ] -[[package]] -name = "onnx" -version = "1.20.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "ml-dtypes" }, - { name = "numpy" }, - { name = "protobuf" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3b/8a/335c03a8683a88a32f9a6bb98899ea6df241a41df64b37b9696772414794/onnx-1.20.1.tar.gz", hash = "sha256:ded16de1df563d51fbc1ad885f2a426f814039d8b5f4feb77febe09c0295ad67", size = 12048980, upload-time = "2026-01-10T01:40:03.043Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/4c/4b17e82f91ab9aa07ff595771e935ca73547b035030dc5f5a76e63fbfea9/onnx-1.20.1-cp312-abi3-macosx_12_0_universal2.whl", hash = "sha256:1d923bb4f0ce1b24c6859222a7e6b2f123e7bfe7623683662805f2e7b9e95af2", size = 17903547, upload-time = "2026-01-10T01:39:31.015Z" }, - { url = "https://files.pythonhosted.org/packages/64/5e/1bfa100a9cb3f2d3d5f2f05f52f7e60323b0e20bb0abace1ae64dbc88f25/onnx-1.20.1-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ddc0b7d8b5a94627dc86c533d5e415af94cbfd103019a582669dad1f56d30281", size = 17412021, upload-time = "2026-01-10T01:39:33.885Z" }, - { url = "https://files.pythonhosted.org/packages/fb/71/d3fec0dcf9a7a99e7368112d9c765154e81da70fcba1e3121131a45c245b/onnx-1.20.1-cp312-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9336b6b8e6efcf5c490a845f6afd7e041c89a56199aeda384ed7d58fb953b080", size = 17510450, upload-time = "2026-01-10T01:39:36.589Z" }, - { url = "https://files.pythonhosted.org/packages/74/a7/edce1403e05a46e59b502fae8e3350ceeac5841f8e8f1561e98562ed9b09/onnx-1.20.1-cp312-abi3-win32.whl", hash = "sha256:564c35a94811979808ab5800d9eb4f3f32c12daedba7e33ed0845f7c61ef2431", size = 16238216, upload-time = "2026-01-10T01:39:39.46Z" }, - { url = "https://files.pythonhosted.org/packages/8b/c7/8690c81200ae652ac550c1df52f89d7795e6cc941f3cb38c9ef821419e80/onnx-1.20.1-cp312-abi3-win_amd64.whl", hash = "sha256:9fe7f9a633979d50984b94bda8ceb7807403f59a341d09d19342dc544d0ca1d5", size = 16389207, upload-time = "2026-01-10T01:39:41.955Z" }, - { url = "https://files.pythonhosted.org/packages/01/a0/4fb0e6d36eaf079af366b2c1f68bafe92df6db963e2295da84388af64abc/onnx-1.20.1-cp312-abi3-win_arm64.whl", hash = "sha256:21d747348b1c8207406fa2f3e12b82f53e0d5bb3958bcd0288bd27d3cb6ebb00", size = 16344155, upload-time = "2026-01-10T01:39:45.536Z" }, -] - [[package]] name = "opencv-python-headless" version = "4.13.0.92" @@ -833,7 +797,6 @@ dependencies = [ { name = "libusb1" }, { name = "ncurses" }, { name = "numpy" }, - { name = "onnx" }, { name = "openssl3" }, { name = "psutil" }, { name = "pycapnp" }, @@ -918,7 +881,6 @@ requires-dist = [ { name = "mkdocs", marker = "extra == 'docs'" }, { name = "ncurses", git = "https://github.com/commaai/dependencies.git?subdirectory=ncurses&rev=releases" }, { name = "numpy", specifier = ">=2.0" }, - { name = "onnx", specifier = ">=1.14.0" }, { name = "opencv-python-headless", marker = "extra == 'dev'" }, { name = "openssl3", git = "https://github.com/commaai/dependencies.git?subdirectory=openssl3&rev=releases" }, { name = "pre-commit-hooks", marker = "extra == 'testing'" }, @@ -1091,21 +1053,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, ] -[[package]] -name = "protobuf" -version = "6.33.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ba/25/7c72c307aafc96fa87062aa6291d9f7c94836e43214d43722e86037aac02/protobuf-6.33.5.tar.gz", hash = "sha256:6ddcac2a081f8b7b9642c09406bc6a4290128fce5f471cddd165960bb9119e5c", size = 444465, upload-time = "2026-01-29T21:51:33.494Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b1/79/af92d0a8369732b027e6d6084251dd8e782c685c72da161bd4a2e00fbabb/protobuf-6.33.5-cp310-abi3-win32.whl", hash = "sha256:d71b040839446bac0f4d162e758bea99c8251161dae9d0983a3b88dee345153b", size = 425769, upload-time = "2026-01-29T21:51:21.751Z" }, - { url = "https://files.pythonhosted.org/packages/55/75/bb9bc917d10e9ee13dee8607eb9ab963b7cf8be607c46e7862c748aa2af7/protobuf-6.33.5-cp310-abi3-win_amd64.whl", hash = "sha256:3093804752167bcab3998bec9f1048baae6e29505adaf1afd14a37bddede533c", size = 437118, upload-time = "2026-01-29T21:51:24.022Z" }, - { url = "https://files.pythonhosted.org/packages/a2/6b/e48dfc1191bc5b52950246275bf4089773e91cb5ba3592621723cdddca62/protobuf-6.33.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:a5cb85982d95d906df1e2210e58f8e4f1e3cdc088e52c921a041f9c9a0386de5", size = 427766, upload-time = "2026-01-29T21:51:25.413Z" }, - { url = "https://files.pythonhosted.org/packages/4e/b1/c79468184310de09d75095ed1314b839eb2f72df71097db9d1404a1b2717/protobuf-6.33.5-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:9b71e0281f36f179d00cbcb119cb19dec4d14a81393e5ea220f64b286173e190", size = 324638, upload-time = "2026-01-29T21:51:26.423Z" }, - { url = "https://files.pythonhosted.org/packages/c5/f5/65d838092fd01c44d16037953fd4c2cc851e783de9b8f02b27ec4ffd906f/protobuf-6.33.5-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:8afa18e1d6d20af15b417e728e9f60f3aa108ee76f23c3b2c07a2c3b546d3afd", size = 339411, upload-time = "2026-01-29T21:51:27.446Z" }, - { url = "https://files.pythonhosted.org/packages/9b/53/a9443aa3ca9ba8724fdfa02dd1887c1bcd8e89556b715cfbacca6b63dbec/protobuf-6.33.5-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:cbf16ba3350fb7b889fca858fb215967792dc125b35c7976ca4818bee3521cf0", size = 323465, upload-time = "2026-01-29T21:51:28.925Z" }, - { url = "https://files.pythonhosted.org/packages/57/bf/2086963c69bdac3d7cff1cc7ff79b8ce5ea0bec6797a017e1be338a46248/protobuf-6.33.5-py3-none-any.whl", hash = "sha256:69915a973dd0f60f31a08b8318b73eab2bd6a392c79184b3612226b0a3f8ec02", size = 170687, upload-time = "2026-01-29T21:51:32.557Z" }, -] - [[package]] name = "psutil" version = "7.2.2" From 0b6da2077f70540e952ac98cf01a04c42e830021 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Harald=20Sch=C3=A4fer?= Date: Tue, 24 Feb 2026 15:41:00 -0800 Subject: [PATCH 203/311] parse planplus (#37386) --- selfdrive/modeld/parse_model_outputs.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/selfdrive/modeld/parse_model_outputs.py b/selfdrive/modeld/parse_model_outputs.py index 038f51ca9c..5c11e8ca18 100644 --- a/selfdrive/modeld/parse_model_outputs.py +++ b/selfdrive/modeld/parse_model_outputs.py @@ -113,6 +113,8 @@ class Parser: plan_mhp = self.is_mhp(outs, 'plan', ModelConstants.IDX_N * ModelConstants.PLAN_WIDTH) plan_in_N, plan_out_N = (ModelConstants.PLAN_MHP_N, ModelConstants.PLAN_MHP_SELECTION) if plan_mhp else (0, 0) self.parse_mdn('plan', outs, in_N=plan_in_N, out_N=plan_out_N, out_shape=(ModelConstants.IDX_N, ModelConstants.PLAN_WIDTH)) + if 'planplus' in outs: + self.parse_mdn('planplus', outs, in_N=0, out_N=0, out_shape=(ModelConstants.IDX_N, ModelConstants.PLAN_WIDTH)) self.parse_categorical_crossentropy('desire_state', outs, out_shape=(ModelConstants.DESIRE_PRED_WIDTH,)) return outs From 8810948eca11f39d2497429f82ffdd9dc2b15e0c Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Tue, 24 Feb 2026 18:49:59 -0800 Subject: [PATCH 204/311] CI: ensure no brew (#37387) --- .github/workflows/setup/action.yaml | 4 ---- .github/workflows/tests.yaml | 4 ++++ tools/cabana/SConscript | 7 +++++-- tools/op.sh | 2 ++ 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/.github/workflows/setup/action.yaml b/.github/workflows/setup/action.yaml index 958f60d2ea..f3a1a39509 100644 --- a/.github/workflows/setup/action.yaml +++ b/.github/workflows/setup/action.yaml @@ -26,10 +26,6 @@ runs: exit 1 fi - # do this after checkout to ensure our custom LFS config is used to pull from GitLab - - shell: bash - run: git lfs pull - # build cache - id: date shell: bash diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index ff4fc39094..fa327ea74b 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -66,6 +66,10 @@ jobs: - uses: actions/checkout@v6 with: submodules: true + - name: Remove Homebrew from environment + run: | + FILTERED=$(echo "$PATH" | tr ':' '\n' | grep -v '/opt/homebrew' | tr '\n' ':') + echo "PATH=${FILTERED}/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin" >> $GITHUB_ENV - uses: ./.github/workflows/setup-with-retry - name: Building openpilot run: scons diff --git a/tools/cabana/SConscript b/tools/cabana/SConscript index 90de212655..137e77d85b 100644 --- a/tools/cabana/SConscript +++ b/tools/cabana/SConscript @@ -6,8 +6,11 @@ Import('env', 'arch', 'common', 'messaging', 'visionipc', 'cereal') # Detect Qt - skip build if not available if arch == "Darwin": - brew_prefix = subprocess.check_output(['brew', '--prefix'], encoding='utf8').strip() - has_qt = os.path.isdir(os.path.join(brew_prefix, "opt/qt@5")) + try: + brew_prefix = subprocess.check_output(['brew', '--prefix'], encoding='utf8').strip() + has_qt = os.path.isdir(os.path.join(brew_prefix, "opt/qt@5")) + except (FileNotFoundError, subprocess.CalledProcessError): + has_qt = False else: has_qt = shutil.which('qmake') is not None diff --git a/tools/op.sh b/tools/op.sh index 966d4ba433..f5c5b6082a 100755 --- a/tools/op.sh +++ b/tools/op.sh @@ -225,6 +225,8 @@ function op_setup() { et="$(date +%s)" echo -e " ↳ [${GREEN}✔${NC}] Dependencies installed successfully in $((et - st)) seconds." + op_activate_venv + echo "Getting git submodules..." st="$(date +%s)" if ! git submodule update --jobs 4 --init --recursive; then From ed34c4cfd63561dbc856daae281431e917f7cc06 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 24 Feb 2026 20:42:50 -0800 Subject: [PATCH 205/311] NavWidget: reset some state on show --- system/ui/widgets/nav_widget.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/system/ui/widgets/nav_widget.py b/system/ui/widgets/nav_widget.py index c1b1df1292..2944f47a76 100644 --- a/system/ui/widgets/nav_widget.py +++ b/system/ui/widgets/nav_widget.py @@ -225,3 +225,8 @@ class NavWidget(Widget, abc.ABC): # so we need this hacky bool for now self._trigger_animate_in = True self._nav_bar.show_event() + + # Reset state + self._pos_filter.update_alpha(0.1) + self._back_button_start_pos = None + self._swiping_away = False From 6442752486e49d67373a52631d8e4b01ca6fa89d Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 24 Feb 2026 22:29:25 -0800 Subject: [PATCH 206/311] Scroller: reset state on show (#37391) * one time test * fix! * cleanm up * cleanm up --- system/ui/widgets/scroller.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/system/ui/widgets/scroller.py b/system/ui/widgets/scroller.py index b01b34be14..b2aeb55744 100644 --- a/system/ui/widgets/scroller.py +++ b/system/ui/widgets/scroller.py @@ -393,14 +393,12 @@ class Scroller(Widget): def show_event(self): super().show_event() - if self._reset_scroll_at_show: - self.scroll_panel.set_offset(0.0) - for item in self._items: item.show_event() - def hide_event(self): - super().hide_event() + if self._reset_scroll_at_show: + self.scroll_panel.set_offset(0.0) + self._overlay_filter.x = 0.0 self._move_animations.clear() self._move_lift.clear() @@ -408,5 +406,8 @@ class Scroller(Widget): self._pending_move.clear() self._scrolling_to = None, False self._scrolling_to_filter.x = 0.0 + + def hide_event(self): + super().hide_event() for item in self._items: item.hide_event() From 571937da84805a4500737c5e26d55402868b100e Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 24 Feb 2026 22:42:09 -0800 Subject: [PATCH 207/311] WifiUi: sort networks on show event (#37390) * should fail * this works but i think i know a better way * something like this * hmm * this works * rm useless test * good stash * Revert "good stash" This reverts commit c2dddf0810286cb56e2418dd6f7085c2239e5109. --- .../ui/mici/layouts/settings/network/wifi_ui.py | 3 ++- system/ui/lib/wifi_manager.py | 13 +++++-------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py index ec6b0c9dc5..c69dc8b225 100644 --- a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py +++ b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py @@ -298,7 +298,8 @@ class WifiUIMici(NavWidget): self._loading_animation.show_event() self._wifi_manager.set_active(True) self._scroller.items.clear() - self._update_buttons() + # trigger button update on latest sorted networks + self._on_network_updated(self._wifi_manager.networks) def hide_event(self): super().hide_event() diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index 110fd72181..00d7e45897 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -154,7 +154,7 @@ class WifiState: class WifiManager: def __init__(self): - self._networks: list[Network] = [] # a network can be comprised of multiple APs + self._networks: list[Network] = [] # an unsorted list of available Networks. a Network can be comprised of multiple APs self._active = True # used to not run when not in settings self._exit = False @@ -264,7 +264,8 @@ class WifiManager: @property def networks(self) -> list[Network]: - return self._networks + # Sort by connected/connecting, then known, then strength, then alphabetically. This is a pure UI ordering and should not affect underlying state. + return sorted(self._networks, key=lambda n: (n.ssid != self._wifi_state.ssid, not self.is_connection_saved(n.ssid), -n.strength, n.ssid.lower())) @property def wifi_state(self) -> WifiState: @@ -826,13 +827,9 @@ class WifiManager: # catch all for parsing errors cloudlog.exception(f"Failed to parse AP properties for {ap_path}") - networks = [Network.from_dbus(ssid, ap_list, ssid == self._tethering_ssid) for ssid, ap_list in aps.items()] - networks.sort(key=lambda n: (n.ssid != self._wifi_state.ssid, not self.is_connection_saved(n.ssid), -n.strength, n.ssid.lower())) - self._networks = networks - + self._networks = [Network.from_dbus(ssid, ap_list, ssid == self._tethering_ssid) for ssid, ap_list in aps.items()] self._update_active_connection_info() - - self._enqueue_callbacks(self._networks_updated, self._networks) + self._enqueue_callbacks(self._networks_updated, self.networks) # sorted if block: worker() From 7a43e2cb6736421e45b4b5e2e51cb381ac3ecd9e Mon Sep 17 00:00:00 2001 From: Zeph Date: Wed, 25 Feb 2026 00:52:48 -0600 Subject: [PATCH 208/311] controlsd: fix steer_limited_by_safety not updating under MADS (#1716) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `steer_limited_by_safety` update in `publish()` is gated by `selfdriveState.active`, which is False during MADS lateral-only control. This causes the flag to never update once cruise deactivates — it stays stuck at whatever value it had during the last ramp-up (typically True), permanently suppressing the saturation timer in `_check_saturation` and preventing the "Turn Exceeds Steering Limit" alert from firing. Use `CC.latActive` instead, which already accounts for MADS via `get_lat_active()` in ControlsExt. Bug was introduced in #446 (MADS), which updated `CC.latActive` to use `mads.active` but missed updating the `steer_limited_by_safety` gate in `publish()`. --- selfdrive/controls/controlsd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selfdrive/controls/controlsd.py b/selfdrive/controls/controlsd.py index 12b146e80b..1c8e9f76b1 100755 --- a/selfdrive/controls/controlsd.py +++ b/selfdrive/controls/controlsd.py @@ -185,7 +185,7 @@ class Controls(ControlsExt): hudControl.leftLaneDepart = self.sm['driverAssistance'].leftLaneDeparture hudControl.rightLaneDepart = self.sm['driverAssistance'].rightLaneDeparture - if self.sm['selfdriveState'].active: + if self.get_lat_active(self.sm): CO = self.sm['carOutput'] if self.CP.steerControlType == car.CarParams.SteerControlType.angle: self.steer_limited_by_safety = abs(CC.actuators.steeringAngleDeg - CO.actuatorsOutput.steeringAngleDeg) > \ From 538e1e8a9a77ababf8492108867b8adccc59e7d3 Mon Sep 17 00:00:00 2001 From: Zeph Date: Wed, 25 Feb 2026 01:00:07 -0600 Subject: [PATCH 209/311] soundd: trigger timeout warning during MADS lateral-only (#1717) * soundd: trigger timeout warning during MADS lateral-only The selfdrive timeout alert (warningImmediate) only fires when selfdriveState.enabled is True. During MADS lateral-only mode, enabled is False even though the system is actively steering. If selfdrived stops publishing while MADS lateral is active, the driver gets no audible warning that steering has become unresponsive. Add selfdriveStateSP to the SubMaster and check mads.active alongside selfdriveState.enabled so the timeout alert fires whenever the system is actuating steering. * test_soundd: add MADS lateral-only timeout test Test that the selfdrive timeout warning fires when selfdriveState.enabled is False but selfdriveStateSP.mads.active is True. --------- Co-authored-by: Jason Wen --- selfdrive/ui/soundd.py | 4 ++-- selfdrive/ui/tests/test_soundd.py | 30 ++++++++++++++++++++++++++++-- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/selfdrive/ui/soundd.py b/selfdrive/ui/soundd.py index c41fec2676..edf28b7128 100644 --- a/selfdrive/ui/soundd.py +++ b/selfdrive/ui/soundd.py @@ -65,7 +65,7 @@ def check_selfdrive_timeout_alert(sm): ss_missing = time.monotonic() - sm.recv_time['selfdriveState'] if ss_missing > SELFDRIVE_STATE_TIMEOUT: - if sm['selfdriveState'].enabled and (ss_missing - SELFDRIVE_STATE_TIMEOUT) < 10: + if (sm['selfdriveState'].enabled or sm['selfdriveStateSP'].mads.enabled) and (ss_missing - SELFDRIVE_STATE_TIMEOUT) < 10: return True return False @@ -158,7 +158,7 @@ class Soundd(QuietMode): # sounddevice must be imported after forking processes import sounddevice as sd - sm = messaging.SubMaster(['selfdriveState', 'soundPressure']) + sm = messaging.SubMaster(['selfdriveState', 'selfdriveStateSP', 'soundPressure']) with self.get_stream(sd) as stream: rk = Ratekeeper(20) diff --git a/selfdrive/ui/tests/test_soundd.py b/selfdrive/ui/tests/test_soundd.py index a9da8455eb..226117ae8f 100644 --- a/selfdrive/ui/tests/test_soundd.py +++ b/selfdrive/ui/tests/test_soundd.py @@ -10,8 +10,8 @@ AudibleAlert = car.CarControl.HUDControl.AudibleAlert class TestSoundd: def test_check_selfdrive_timeout_alert(self): - sm = SubMaster(['selfdriveState']) - pm = PubMaster(['selfdriveState']) + sm = SubMaster(['selfdriveState', 'selfdriveStateSP']) + pm = PubMaster(['selfdriveState', 'selfdriveStateSP']) for _ in range(100): cs = messaging.new_message('selfdriveState') @@ -31,5 +31,31 @@ class TestSoundd: assert check_selfdrive_timeout_alert(sm) + def test_check_selfdrive_timeout_alert_mads_lateral_only(self): + sm = SubMaster(['selfdriveState', 'selfdriveStateSP']) + pm = PubMaster(['selfdriveState', 'selfdriveStateSP']) + + for _ in range(100): + cs = messaging.new_message('selfdriveState') + cs.selfdriveState.enabled = False + + ss_sp = messaging.new_message('selfdriveStateSP') + ss_sp.selfdriveStateSP.mads.enabled = True + + pm.send("selfdriveState", cs) + pm.send("selfdriveStateSP", ss_sp) + + time.sleep(0.01) + + sm.update(0) + + assert not check_selfdrive_timeout_alert(sm) + + for _ in range(SELFDRIVE_STATE_TIMEOUT * 110): + sm.update(0) + time.sleep(0.01) + + assert check_selfdrive_timeout_alert(sm) + # TODO: add test with micd for checking that soundd actually outputs sounds From 1792a600535de37e238fe958cd0c8f115343f23a Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 24 Feb 2026 23:24:08 -0800 Subject: [PATCH 210/311] WifiManager: split out state machine (#37395) split out state machine --- system/ui/lib/wifi_manager.py | 128 ++++++++++++++++++---------------- 1 file changed, 66 insertions(+), 62 deletions(-) diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index 00d7e45897..ed8d65bae9 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -372,82 +372,86 @@ class WifiManager: self._update_networks() # Device state changes - # TODO: known race conditions when switching networks (e.g. forget A, connect to B): - # 1. DEACTIVATING/DISCONNECTED + CONNECTION_REMOVED: fires before NewConnection for B - # arrives, so _set_connecting(None) clears B's CONNECTING state causing UI flicker. - # DEACTIVATING(CONNECTION_REMOVED): wifi_state (B, CONNECTING) -> (None, DISCONNECTED) - # Fix: make DEACTIVATING a no-op, and guard DISCONNECTED with - # `if wifi_state.ssid not in _connections` (NewConnection arrives between the two). - # 2. PREPARE/CONFIG ssid lookup: DBus may return stale A's conn_path, overwriting B. - # PREPARE(0): wifi_state (B, CONNECTING) -> (A, CONNECTING) - # Fix: only do DBus lookup when wifi_state.ssid is None (auto-connections); - # user-initiated connections already have ssid set via _set_connecting. while len(state_q): new_state, previous_state, change_reason = state_q.popleft().body - # TODO: Handle (FAILED, SSID_NOT_FOUND) and emit for ui to show error - # Happens when network drops off after starting connection + self._handle_state_change(new_state, previous_state, change_reason) - if new_state == NMDeviceState.DISCONNECTED: - if change_reason != NMDeviceStateReason.NEW_ACTIVATION: - # catches CONNECTION_REMOVED reason when connection is forgotten - self._set_connecting(None) + def _handle_state_change(self, new_state: int, _: int, change_reason: int): + # TODO: known race conditions when switching networks (e.g. forget A, connect to B): + # 1. DEACTIVATING/DISCONNECTED + CONNECTION_REMOVED: fires before NewConnection for B + # arrives, so _set_connecting(None) clears B's CONNECTING state causing UI flicker. + # DEACTIVATING(CONNECTION_REMOVED): wifi_state (B, CONNECTING) -> (None, DISCONNECTED) + # Fix: make DEACTIVATING a no-op, and guard DISCONNECTED with + # `if wifi_state.ssid not in _connections` (NewConnection arrives between the two). + # 2. PREPARE/CONFIG ssid lookup: DBus may return stale A's conn_path, overwriting B. + # PREPARE(0): wifi_state (B, CONNECTING) -> (A, CONNECTING) + # Fix: only do DBus lookup when wifi_state.ssid is None (auto-connections); + # user-initiated connections already have ssid set via _set_connecting. - elif new_state in (NMDeviceState.PREPARE, NMDeviceState.CONFIG): - # Set connecting status when NetworkManager connects to known networks on its own - wifi_state = replace(self._wifi_state, status=ConnectStatus.CONNECTING) + # TODO: Handle (FAILED, SSID_NOT_FOUND) and emit for ui to show error + # Happens when network drops off after starting connection - conn_path, _ = self._get_active_wifi_connection(self._conn_monitor) - if conn_path is None: - cloudlog.warning("Failed to get active wifi connection during PREPARE/CONFIG state") - else: - wifi_state.ssid = next((s for s, p in self._connections.items() if p == conn_path), None) + if new_state == NMDeviceState.DISCONNECTED: + if change_reason != NMDeviceStateReason.NEW_ACTIVATION: + # catches CONNECTION_REMOVED reason when connection is forgotten + self._set_connecting(None) - self._wifi_state = wifi_state + elif new_state in (NMDeviceState.PREPARE, NMDeviceState.CONFIG): + # Set connecting status when NetworkManager connects to known networks on its own + wifi_state = replace(self._wifi_state, status=ConnectStatus.CONNECTING) - # BAD PASSWORD - use prev if current has already moved on to a new connection - # - strong network rejects with NEED_AUTH+SUPPLICANT_DISCONNECT - # - weak/gone network fails with FAILED+NO_SECRETS - elif ((new_state == NMDeviceState.NEED_AUTH and change_reason == NMDeviceStateReason.SUPPLICANT_DISCONNECT) or - (new_state == NMDeviceState.FAILED and change_reason == NMDeviceStateReason.NO_SECRETS)): + conn_path, _ = self._get_active_wifi_connection(self._conn_monitor) + if conn_path is None: + cloudlog.warning("Failed to get active wifi connection during PREPARE/CONFIG state") + else: + wifi_state.ssid = next((s for s, p in self._connections.items() if p == conn_path), None) - failed_ssid = self._wifi_state.prev_ssid or self._wifi_state.ssid - if failed_ssid: - self._enqueue_callbacks(self._need_auth, failed_ssid) - self._wifi_state.prev_ssid = None - if self._wifi_state.ssid == failed_ssid: - self._set_connecting(None) + self._wifi_state = wifi_state - elif new_state in (NMDeviceState.NEED_AUTH, NMDeviceState.IP_CONFIG, NMDeviceState.IP_CHECK, - NMDeviceState.SECONDARIES, NMDeviceState.FAILED): - pass + # BAD PASSWORD - use prev if current has already moved on to a new connection + # - strong network rejects with NEED_AUTH+SUPPLICANT_DISCONNECT + # - weak/gone network fails with FAILED+NO_SECRETS + elif ((new_state == NMDeviceState.NEED_AUTH and change_reason == NMDeviceStateReason.SUPPLICANT_DISCONNECT) or + (new_state == NMDeviceState.FAILED and change_reason == NMDeviceStateReason.NO_SECRETS)): - elif new_state == NMDeviceState.ACTIVATED: - # Note that IP address from Ip4Config may not be propagated immediately and could take until the next scan results - wifi_state = replace(self._wifi_state, prev_ssid=None, status=ConnectStatus.CONNECTED) + failed_ssid = self._wifi_state.prev_ssid or self._wifi_state.ssid + if failed_ssid: + self._enqueue_callbacks(self._need_auth, failed_ssid) + self._wifi_state.prev_ssid = None + if self._wifi_state.ssid == failed_ssid: + self._set_connecting(None) - conn_path, _ = self._get_active_wifi_connection(self._conn_monitor) - if conn_path is None: - cloudlog.warning("Failed to get active wifi connection during ACTIVATED state") - self._wifi_state = wifi_state - self._enqueue_callbacks(self._activated) - self._update_networks() - else: - wifi_state.ssid = next((s for s, p in self._connections.items() if p == conn_path), None) - self._wifi_state = wifi_state - self._enqueue_callbacks(self._activated) - self._update_networks() + elif new_state in (NMDeviceState.NEED_AUTH, NMDeviceState.IP_CONFIG, NMDeviceState.IP_CHECK, + NMDeviceState.SECONDARIES, NMDeviceState.FAILED): + pass - # Persist volatile connections (created by AddAndActivateConnection2) to disk - conn_addr = DBusAddress(conn_path, bus_name=NM, interface=NM_CONNECTION_IFACE) - save_reply = self._conn_monitor.send_and_get_reply(new_method_call(conn_addr, 'Save')) - if save_reply.header.message_type == MessageType.error: - cloudlog.warning(f"Failed to persist connection to disk: {save_reply}") + elif new_state == NMDeviceState.ACTIVATED: + # Note that IP address from Ip4Config may not be propagated immediately and could take until the next scan results + wifi_state = replace(self._wifi_state, prev_ssid=None, status=ConnectStatus.CONNECTED) - elif new_state == NMDeviceState.DEACTIVATING: - if change_reason == NMDeviceStateReason.CONNECTION_REMOVED: - # When connection is forgotten - self._set_connecting(None) + conn_path, _ = self._get_active_wifi_connection(self._conn_monitor) + if conn_path is None: + cloudlog.warning("Failed to get active wifi connection during ACTIVATED state") + self._wifi_state = wifi_state + self._enqueue_callbacks(self._activated) + self._update_networks() + else: + wifi_state.ssid = next((s for s, p in self._connections.items() if p == conn_path), None) + self._wifi_state = wifi_state + self._enqueue_callbacks(self._activated) + self._update_networks() + + # Persist volatile connections (created by AddAndActivateConnection2) to disk + conn_addr = DBusAddress(conn_path, bus_name=NM, interface=NM_CONNECTION_IFACE) + save_reply = self._conn_monitor.send_and_get_reply(new_method_call(conn_addr, 'Save')) + if save_reply.header.message_type == MessageType.error: + cloudlog.warning(f"Failed to persist connection to disk: {save_reply}") + + elif new_state == NMDeviceState.DEACTIVATING: + if change_reason == NMDeviceStateReason.CONNECTION_REMOVED: + # When connection is forgotten + self._set_connecting(None) def _network_scanner(self): while not self._exit: From a4166563e1fe28d9f64637f7049ebe65cc59bfed Mon Sep 17 00:00:00 2001 From: Lukas Heintz <61192133+lukasloetkolben@users.noreply.github.com> Date: Wed, 25 Feb 2026 08:39:50 +0100 Subject: [PATCH 211/311] pandad: flasher for Rivian long upgrade module (#1712) * flasher for Rivian long upgrade * self-contained no dependency on Panda.F4_DEVICES * standalone flasher * move to sp module * use brand field directly * use brand field directly * use brand field directly * bump * add some logging --------- Co-authored-by: Jason Wen --- opendbc_repo | 2 +- selfdrive/pandad/pandad.py | 5 + sunnypilot/selfdrive/pandad/__init__.py | 0 .../selfdrive/pandad/rivian_long_flasher.py | 96 ++++++++++++++++++ .../pandad/rivian_long_fw.bin.signed | Bin 0 -> 59500 bytes 5 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 sunnypilot/selfdrive/pandad/__init__.py create mode 100755 sunnypilot/selfdrive/pandad/rivian_long_flasher.py create mode 100644 sunnypilot/selfdrive/pandad/rivian_long_fw.bin.signed diff --git a/opendbc_repo b/opendbc_repo index 613a562bba..cbcd126951 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit 613a562bba80997b5c1c74cd799641d06fb2b38c +Subproject commit cbcd126951094311ab5a43dd384a495cd881c080 diff --git a/selfdrive/pandad/pandad.py b/selfdrive/pandad/pandad.py index d21e60f838..c9ddfd6338 100755 --- a/selfdrive/pandad/pandad.py +++ b/selfdrive/pandad/pandad.py @@ -12,6 +12,8 @@ from openpilot.common.params import Params from openpilot.system.hardware import HARDWARE from openpilot.common.swaglog import cloudlog +from openpilot.sunnypilot.selfdrive.pandad.rivian_long_flasher import flash_rivian_long + def get_expected_signature() -> bytes: try: @@ -129,6 +131,9 @@ def main() -> None: for serial in panda_serials: pandas.append(flash_panda(serial)) + # flash Rivian longitudinal upgrade panda + flash_rivian_long(pandas) + # Ensure internal panda is present if expected internal_pandas = [panda for panda in pandas if panda.is_internal()] if HARDWARE.has_internal_panda() and len(internal_pandas) == 0: diff --git a/sunnypilot/selfdrive/pandad/__init__.py b/sunnypilot/selfdrive/pandad/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/sunnypilot/selfdrive/pandad/rivian_long_flasher.py b/sunnypilot/selfdrive/pandad/rivian_long_flasher.py new file mode 100755 index 0000000000..58ff0683be --- /dev/null +++ b/sunnypilot/selfdrive/pandad/rivian_long_flasher.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +""" +Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + +This file is part of sunnypilot and is licensed under the MIT License. +See the LICENSE.md file in the root directory for more details. +""" +import os +from itertools import accumulate + +from cereal import car, messaging +from panda import Panda +from openpilot.common.params import Params +from openpilot.common.swaglog import cloudlog + +FW_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "rivian_long_fw.bin.signed") +SECTOR_SIZES = [0x4000] * 4 + [0x10000] + [0x20000] * 11 + + +def _is_rivian() -> bool: + params = Params() + + # check fixed fingerprint + if bundle := params.get("CarPlatformBundle"): + if bundle.get("brand") == "rivian": + return True + + # check cached fingerprint + CP_bytes = params.get("CarParamsPersistent") + if CP_bytes is not None: + CP = messaging.log_from_bytes(CP_bytes, car.CarParams) + if CP.brand == "rivian": + return True + + return False + + +def _flash_static(handle, code): + assert Panda.flasher_present(handle) + last_sector = next((i + 1 for i, v in enumerate(accumulate(SECTOR_SIZES[1:])) if v > len(code)), -1) + assert 1 <= last_sector < 7, "Invalid firmware size" + + handle.controlWrite(Panda.REQUEST_IN, 0xb1, 0, 0, b'') + for i in range(1, last_sector + 1): + handle.controlWrite(Panda.REQUEST_IN, 0xb2, i, 0, b'') + for i in range(0, len(code), 0x10): + handle.bulkWrite(2, code[i:i + 0x10]) + try: + handle.controlWrite(Panda.REQUEST_IN, 0xd8, 0, 0, b'', expect_disconnect=True) + except Exception: + pass + + +def _flash_panda(panda: Panda) -> None: + expected_sig = Panda.get_signature_from_firmware(FW_PATH) + if not panda.bootstub and panda.get_signature() == expected_sig: + cloudlog.info(f"F4 panda {panda.get_usb_serial()} already up to date") + return + + cloudlog.info(f"Flashing F4 panda {panda.get_usb_serial()}") + with open(FW_PATH, "rb") as f: + code = f.read() + + if not panda.bootstub: + # enter bootstub directly, panda.reset() rejects deprecated hw types + try: + panda._handle.controlWrite(Panda.REQUEST_IN, 0xd1, 1, 0, b'', timeout=15000, expect_disconnect=True) + except Exception: + pass + panda.close() + panda.reconnect() + + _flash_static(panda._handle, code) + panda.reconnect() + + +def flash_rivian_long(pandas: list[Panda]) -> None: + if not os.path.isfile(FW_PATH): + cloudlog.error(f"Rivian longitudinal upgrade firmware not found at {FW_PATH}") + return + + if not _is_rivian(): + cloudlog.info("Not a Rivian, skipping longitudinal upgrade...") + return + + for panda in pandas: + # only flash external black pandas (HW_TYPE_BLACK = 0x03) + if panda.get_type() == b'\x03' and not panda.is_internal(): + try: + _flash_panda(panda) + except Exception: + cloudlog.exception(f"Failed to flash F4 panda {panda.get_usb_serial()}") + + +if __name__ == '__main__': + flash_rivian_long([Panda(s) for s in Panda.list()]) diff --git a/sunnypilot/selfdrive/pandad/rivian_long_fw.bin.signed b/sunnypilot/selfdrive/pandad/rivian_long_fw.bin.signed new file mode 100644 index 0000000000000000000000000000000000000000..bdbd237ba99589813bdda2199ad71f42bfc2937a GIT binary patch literal 59500 zcmafc33yXg`uDl_W^ZWI1t@KSECot~5-4h+sA*Cz>4IgO5rNUNsA2h!m1Up-r-8x% zq9agH77-LwTxM*GRjRZp6j#tmC@?4>Rn%x|JLyKQ-SYj;O-qZ;JRdyox##Ad^*!%) z-m~-i`=7TL8}jiUIoohXAhv-U5^Y-Un0yY5<=B{sp)QxB~bVF#0i~7z^+s z-T-I;{0ayHr1(7y5D6Fz7z!`|?gFF%(g9Ney{Mgr_$j_b2gO19+svJ)1{= zA+9bjaghd6Cn4?9{HgRm(S|^gHEYSn63#E-m}2I@JbAiHk{Py@NeOdG46_!nB}g@y zG1EFE*|AHiTg=4iX9xk{i;ED}%!I(t68ai7#iu z9~rfE@`6ZbJjKmSI?%9`VGuh3-S0XrwbBeT%JJNONf>bVfdz! zXMc(y*BZ#u*2e0kO^urN|Y;Oad|z(Vkjs5&aIVX)wFQYwew>$%mu_Bfp3>TC5rrVZf&&q z<>GvSP`QA&*y}iEb7Df}QPbhylGj}~RR^b}(l!PDTdcS>3+h#u_{XbjbqgJmQBB^@ zpCZX;Jd9qo>XbKZmCI{xeqLW}Iq5O1t!c4vMV9wHM+%-^GEd1FmF`!XIQ7*gLd^AQ zA~CWaqUY*}cJOeA#gYEpQ=>|UR*(u+DJ^U6Fedp$7eQ?Mg6F-opD%l~>t1mf6XYy>!22$5qDg zjF-}ZI-BU{uG+tF|C%BpQ5&}=ZuS0EU$4rRXS?KC@+^r#N-57E%Pz{38fx;0NPQx2 zf>#zZM4P}7trnE%FQ2=L==ZNV)l$vwUv&yN{u-fK!l0Fr{@4h{uaGl-jVx{r<7cG_ zs|UFES48JXbcaeWmPVl-(6&LO<8*G7zjA3sIKFKz4bM^;n!E#Yq&lCM+h`Y8|uKjv`x#=7IhfWPF4_Yr;=#TNdS0mk`e8Tc)qA2S`FwsT%>Q$ ze71nI&y-qxwR)yL$`GAl8Cu;G)RMMBa;1!!*kRff`?7_(8p~@cqd?z=j4NerJ+#dA zxB6`HF)T;aL>h6b~%$kY5>=Gf+p8{ccs36nIeP$aKGYm07<4fDf_Hf`({QKPQUC z;Y9lr_$&wg-->t10HQq;L9~55Y;LXqJ%ZNOnU(odkGJi7g7XkZi1W?O58_)mDE*nj zFt@q+An0C>=il%vhi4?-8BcYA7gYU+s8A_HEM1niAnmBjmj zxfd5j`q>B$9EqITZ@ol(%6RjHYdyHroF$5+NZ>D;Xs09IA6Ek{=sECH-BJX}xYMY1 zRXb=Of18H!m>r?-&j0#d?I|iaRlqvt#FF}5n$s;stR|zn%;^t`rG|S>7ZeplXRr=X zAK)~AUB`(jnW&#dtp2t}8CvC3a!E0+kd!w!vlu0b2le~&NM2I2cc)Q;F(G~vF(uWD zWjrm$+;k0i{!E z_EFp%Z+ldvfnPuyI$%xaLV9o;bY*!Thaqpue#>x3I;km|e{cD4;#qmbyNkoE&g zrK2ORPHkseC4NT2j8lv@WVCvi@rq=hr^v75plOJ|k|Z{HiB4t;<53e|hI(Zl`RI7J zWUXxU0FQr!s;&&}m8@m(>rHPN#!o()@kERmV}@>$N}-#WafS@V=qqKUi6P&3=2^~r zV}C>|`MAs|L7GPOA(a4;0;DmdK+q806uhs)v?2EIQ`oCT7&AYW1ci!`&cwk!36tAi z)S2tEN(Z=4Hj#*TU5p{YFa^C?W`_K@FV{C)Qtzqr(r+hRLv>5cXUked`GK(k??n2U zi2U*a*)m@3CVF*Fk#MgzVX+qUeHS0`k-=^hdE;qFV%Q3LX^t!>$3@dn@QM6Vhp~z| zM-(!6PUqEI9k^rdy8=yZu?RWxI1nC@`L20J% ziyM{?C)+F(p=hB{X`S=UbbOB(kR9-ZUVcbsDShPIg{}j^oB8fEAnzv z%9YqA@^vif4i4|@nYo#POlDoIXruSqA7?5H-g0Uaf?Ovl11hDMo^eOg#M`f$-jX~ z;f%7T7@6C{nC6T-BjS=tQhLnIaVh&O3U_JS?EF|4ZAbppP)PgLr%%{6=ggWG6=(IG zHgI`0Epo%m0anZ@*(a^}`BOFJlAH^IZCw1ay;BtxyM-J^QyJoUF6+Fo)nskW=T75^ zk)dS7;g{a*MWF@rZ5U`2UGUKri13iuPQtvQ>x>7aVkfN94!@~fCeP?9v;&M178HK z34>fwu|&J0Kv9|6Pm43W20g6A_ZT-@AEmD?i-hD;@EyG;T z&Yw2~dT>0r(aNQOAN-D+M&h5c(JF&f=Roylv=7gElT`4 z&Q#%37BKxIv8fBkYO^G?i6x}rgZyq%+#(i*+d(QSvDQ3wWVy53Zpf< zkYbn8t=h$C`_n9)g%3oJ9~#FUXF=ViZO+1Y^m%I`O%)3Z4zjO8=hu)Dd{K+^&f+&l zqylFl<%6yNg%3h)Wl{3naa`%b_d@N-FW10|86eJ0F3l%~1pXxpcU2kBh_iKATVq+j zUn(^!T**8uT0~J%F&Rt>Y(!CBD$0K^T_n0-pf;$$TsS0$eus$|HpRY*nu~l4!N`fTr$w-dTIM&4bl?Vl``1P75zbSJnl7LwYH2W@ z{W_`%0^YtB_1dB#-@z?g)KQIjZGT&o>J+mmIjJ?;-+FDrJFHX4Rf_k%l0$QvkR)XY z)kbvwBlB>MC;Upn`8h5&9hT>9dBoPZoCQz#+@f!aehvNaZx?MWfu?|$tH{FCQqLB1 zfJwROWivRw7``X8L15y4^>WGna_MD`XC}sF6kAO2Nu~RUY>=OmwR+S%#}DK+b+ZM{ zIxAqdpk8kU%oY@QD)5xxDOs<%H{y<8F6;df5?uy2BqsiqAzZTba-yGOBQv5iUWs#i zpN=1B-ybizZ(yF^8;SWW>dyYeYbqxOI=3TQQ{_kV&&_`oaVDkBOX;)C%Rzq?33GLn z!g^_r&ZFk)R`k#um8bK^8GHotbpE&y9h&2)?Z4tZ&OSWny0E@_w~#jjv>+Q~e%Q?M zR)JZ@)mvpX^&fLZK8>}eMah|)gLe;S<_mIB&qTTZHbvvUvtlSDcZk0ImCzUUR(VbR zCmeGh&GBB@kMIaIxYhM?E=Q1)IremOq&eOEP~JX!q;+3juTMZx%4k)PS?|La^_evo zI1b${%^Jd+Z+7NOQi?2*c{}r9iLnAkgQiVIyTfy6i zf|ov78_~6TF+%W#Qvax;TxP>O3;gs!u<|E`8Q=BccthV^G)>?4H*bAcz=UA4w^ug{J$M@^;_XuflcF$D%rSmBcYyps(B?zyPR z__Ukt!RheZ>gmI&(4+rh8Su6bX>VfCdPPvrN7)9TpB!QlHV(1wZaroqwC}%mhuM@O z9?OCX#ANvGA3?b-Os_!f7;vXCb9jru+siKWLK4D1rJ;zN>4N$Zas0*6&n6t&{#z*P~#<1O7Y^7R-%9oXB`v86rERA5`t?u#sXkH5{`ZO4lF~{4Sk3?M2 zBhOpNqvc^Iv!i+B9Z5z81LZ*z4+&IBC6L#vDUZsE&WW}W!=eSOZq|YudC?1JE#y#c zVYil7>BG>Y)F6#fx=0Bs^tt~0m65#Eb|fztFp52AZ3?{-UlU!u{SMJqFyIaj@4?Za zU_uXmB7^ZgG@0KWix@3;S@&SXSi~~A2O>uCcz<`ajS*w>gW>p%V*0LbHGZS9nL%Yw zI%BuIC!I~CVJUuRkRrV*M2{jbs4*TG6H|8FGHQ9nEuv4h=PBuVDnidzCw(r5O*S)b z@!aK>R;Oid=s6QlGxJLK5j96I`uuzDDmkhD?(4_!k%|L#d226rRn`7R4&>P zdPjT2-_aiNiS~$3v`2hu@D%fppSu{moywXSn+84HTWLl-*YC7GqTjy|YXEt37f>77 zX`Qn)e`;!ZFi;;Hm_0D7Mp!$N)z=7{MmiC09Z3@J9ua9P5?1{&_hxW~x%vGWHNtwn zMsR|LlDfidQ*J)8f?`nLrL^5I;&N!0JUfX`SU_c4FtGdsD%<|_IOcg8VpanPK%TWx zXjREUNo-r$3bww8k)<4O8&YV?6TtN+L?30vBC7AzHd^m{sG+G-b*r}ePDM{`KXyv9 zh#`7Gtd`0wNQMuR%#2wLAOJgP`sp;kAl?doI9dI zoOjsc*2Y1e-<+ESYnagwd1{V*lhBn+Iqb{MPzp}}&{KQu!NA8o@3DVHZ-VsOQ9)3} z+vniDMtpx)5RxC-epF~zmDSIPU~5Yda_4tgcDtAn?1*?f5zB`R>M4ozl)SGKmYC2r z>12;>i!nfd_65TXbF860b#!t1NPaRfQ`E^ImWJ5ju8B6e$d&hW-GdmFiQ~H_*ra+Y z6YuDfBHcKoOX?bn->6J9blriN9I;_tMsOwN?Wej%gCnWdS=%|Xhi6xICgLki_vg;x zkRCLhv`dQ^l>ww{D8ACzBV9ufqj~3c4nzx7h&6RaBSz)YFKB6)E&?sc?g|F>1WVf5 zA9)7)3Npn;bDKMr$W3K>T4%2wyj!G_UOhPWWJnKEy(P*n$O$?xjt|WlF?Ljc>yKmn zlG##4#eJn@1f$y|bjIx#y5b^ByS;buzj~SE%l3Uw{SCh7NY5njkfaCuSiNGudMb8F zFwoI4up+zy`IgcdFZ~_;=%6D!iI2DcBId_8OVmZOU*REjTvWrq`MK_yWB-~U{dZx6g zDIeI=Ge*DfsmpeZT~AvXeW<0{TkoOoFNv*;xA%U3z9Sg;dp7|NZmOu(Q=q?(iDk=R z|JlYZPKw{2dy>uE+!6^L z*bW-Qh@WX6Axeen_C#9@-cPn0#OH~2z4&~$T_@`Jo9)^jsq{4oeo;R+Qv8MF~CQAlr0axKHVM($wbnFfz9m{H&VQ9U4XWH zkkr%eSUk0W{{2zoorn;k!34xJ0QplT*&HnF?qsYnfd+cty0V(0v7PGM~KP#cWun3hh+O+15z)NUP#JB_(vGN<(|FrewjbW5 z9fDRnsr2-=UGO=Sb+p}-b_~*=-ay~?TZXzeZj&zARfhFS*Bi5oHqg?zreMmIGFZA= zYGd|Uius}qzv4X^X4iQ-tav)D&Y}%98&nm+0Dq!)JQ$u@za_3v|1*Zg2+aLlg{D#k zpC;8B{|Rc>g1)JBTdw9!6&zC6!=efXehPNO;w|}>`i>*Bx0FbE@W-1Ryuq>dhwOuR z?qfyDhnDg12FI^W$v;XYlW!z2U(4YG9?>poq5k7o(Mueo27L$N6LGmv7NyyLyHd-; z!0GeH#ag6%n1^y$X^_q|{IFVKGX@#xgWx2Z!e~8%Ff?CrBmdXmQ5`Kos=c8(0s3$m zwtWM>(J`RfjTnKq84D|@t#Lt6EPH6_1woQ_Mo`3uWgLhU5a0I7ll>hFi-gEqOg{M=7D*DTGCKUWN21I(iZjB7B&~P zscCJ-=#2QdGl&oAH)Eo5j-btNU_66?*IX>ZBQ6f%VV4BqL6;QaewPg43obdr-7W>f zoh~K9zq?cjx4Xg+Zgo*Dx!FZWaic37VWBGm;d++_VSy_W;a^-)2v@qI5$3yM5dNQQ zAi|}tK?omr4Mw=w6^n3zi^`XWU2zEKxu}$x<4QpI2iH)9GhM?F=DLO>%yDTEX1a6; zd6yoc)n!0vb|oUb*EIs+B-coU_qawOoZw1AILLbh z7R+LTfvzqy;*&9t2?hdPsrF#tZp>t$>$(_&VI`zC>M?cGQYmHA^5QG@^hi6&-S9@N ze90nd(nzXA?>KQ$Uj(J;2=HY*U@U<0s3ALk30B#S_qz2tWaQDdvekRQO~s}9B}eT0 zE#c>fqR#ibOlZr^u6V>tyAGo*eppoC|E?qU8+pv##PHaiNAe_1v64E^ML})62+pmI z9qy7h?npk-__zblNdle&vemBZ4Fi^b(l}~q4R%8j!$(N@5jc;9XEkm?KGhkR17uiX z|DtVE0CNHU9PlFkS%=ZtHfJ1i`T9?spH+oNiyjPJIJCUR+#OifZEyb82745b>)a3wt4jD^y5}xw>M?u8!JQZZdq+K>Z zhTojDf7mR`Caf#ZN~f#KM=W$^JlH+drq*=_H1gu#ZVm{1Dm9SyE14n`?_z-CNmylkf{n1OMZEGiOdy3e0 zDp!9wDat6!NB@pC)3H1PFY1c-hGeo^cM5swixM_~Dy&;~L32W5xi>C}w#Ul`T(oYT zVn0w;KYv!9b?autsQwhSTZ+{=Wh=M!A4q@DQfrTKS^ZAV0I1DFN$Cj{JA1vuJ$4|i zZD`JBd!H`lw)WSh7QFS>r53cMPnTMp^quNb7A=A<9YJ*|=e$jqj<{WyLb5{tP&%P~ zzX$%E0Znp-YNQ`J6ct>Fsq&y8Hz)Br{8AEhjFEV_Y)MvJURR*DW9^uqJni1jApHJS z|M+GbQD!N#%Q~8xseQRv$3XYTGvlc(A?ie{kBu(MgLaO8pO9z%HkESpR(>$QL1)U6 z@WXhFWmj%%`-(;OujHdHPHi^LDD4oMn2G6r)ssnC>8<8*lBNWLvFsXKn(H^Ghu*&H zx)X1t=j}?@(5z~m7G>?|DeHLc)?@za9BizzHO^Z4!d+Q;~3Fg3kACrNWv_>=I8DD z+}5#AY7u)jN|DH6%`VXr4E!^g+nT>{0A@huC%Xgx>QKW++Z~AP3=^LN;8UXSN=V(1 zc8L*jGu%QHL;^Za#e56|B%K~O}~2Sb23&(=+kU!g6BO@{s5g}GxF_ZEb;Gl>bEY~DlI%+&qkdm zkfHVIOk~D31u-me8k{e0#qQ40tKDS8opogNpWVq7hILFzw=v1pZMahQ*gC_?J!+xY%Tnxam&&x!TVkgQOZ2JPcmJbRN?JG}G-_T>Ck1Bros}-+o1wF2FVvU*6awU*-=Gdtit(8YM z+*lV@%B0;`d8k3|AR|7;T3RD^sQ%*RFslja20|uSC;`*4VtH0vTbL-WHDK(@y92Lv z4ibIJ-GN^^qD7x_cfi{bWn(d0?+$#|LA5fA*zgV({wcMM(i^J}v&r#WZ3nBPbD?Lu z2`mFQq(gV$*$(PAB#2#biGCouc0&0<0pDh}=3VOPONdqR->#onN|H4v6*jeNvHlVG z?;5MPdMBq_KgX^)xe?q98{)&BKG3!&k;~tJBJ#`Qr;)^qC}qeHu6`4zM0x@Xp{e8c z-x&P1K>Q7*0HY2gbxR6M}(rL2iuO18+Xk zyw=_V*%h)ahP(y53ji8O6xwJ+|F!s)y^UKq6(x$<)HffKLF+#@g781kuS{`+i zG*FFUQ0Z=AM3|zK7$0^shRs68UAu*mX-8lwXWYdYH6BMr3}&#S(#i$xUGEA>MrtYD z;ig{BQEAdTVz}{R-GW%=HzBpF{d$qEk{{W98hQ1s2wJ-UJN;km_|;&43p;I( zTcqyzEbRJmkI3r`{2@35<3`tw&CQD}H7#LWv4zUWSl7tB{m^fbUEqRlgZ(&m*kC-m z|BW`zTe^2DqoaPg!`+Y^d12WP*})CJZk>8jzJGL1WhiQ-v%u ziC2N&^Ud9XeJ4~{Wg;#eXrZnkINbduQEV)MHI}=j;6x$KJJ-?psHd|TYN zwb1gWC$iunfzTF}`2cpKw|D#ZykW z7W11d$6ONBf6TSVA~}FvqR?^C*0>iv!j3S2@syA=kJfpno@EzWNYXJ^B6d$eDkU`d8dTk%cKX05qsfA$>D`Qi2Y2&nC2m4=eu+L zjB+yOXf?uNV~wyEkOSBO*uAVqcyXDH&#fDV7HkEt(3&~aykZLNfdy^0@Xzxe;Tfak z07*XPIvV@ieypDIE_SLg;joOut-f$>uQ!#i@M^Qj*s<>O!rtr&AD$OpOyl;o#hn)p zr){_B_JuJ2hGnaWcg}jb3~QDXPUGuov}X2{3$D}12Y8Dl9rjQvIOLX74U$WDP0U8i z%znC;@!$2zK;p>Q;pPY^ZfaUC-Q)3Z!R!p3V!15DYKtwe zfx;j2yntP_Y%;v+*lo{cyjXChAlD~Jl(g0@&hT-PqixQ`ac+{FgMB?q;=)Jf`rlL9 z^h6hPtSg`me(`hsV}EvhA>@UEWPk=v0>(3zBJ;=S-!Fqy7Wd*|LLAb&L7SdqnE@u-qU}Vi&bB2 z8RgkjP*X54Gm*{EOIq_6+xY}*SD>mp5;8kDPJQ-h8-auw`Mb6pG%8?4$FDb5(AI$F zwDinLG-hy&wjTAt_t$FDyq{ME!2Z{$fb@!!AUvx<*} zqhVOqH`2EczpL1C5?+A6NYqM`#FyBH$Lj9gN;Pfbisxhgmm;fq(oL{K;G5+{gQBW z`w37VO+zJ!xux8xsF%nSAdj9h9jli}i18u!C1FfjZ|$hrajjt}J+~!VUs=~K3AQvU zD|%D(v~N;x`+|)mE2VM7q>@G}+V@#!sC|q7OZ%*}efv8D?{}i@S(k(}#<>|?jo&+% z@jIsAOWd++LwrffRM$5CSiMsI0%XNt{#DCIO@#Z)l^5}rz*kyUUO9x=aI}9tq{Cm) zcAMDtcN>40ORdKfX!m7TN#mn)KWQ{LMl?$29%}f5Q(cHEQ+fyi1ljt(_RUG6LPA` z8_S>cQraVF;;AMNX|r3+gtk+ z_}2C>=>Mz1skgVU8GGP%mW=T5>$r8%IiEP*Q}Di`LH%13YDuAjw&T4v^2qBrXq)7E zte&V^j?ST~ zon-U`_yUfGT@cQs61IwGK7h;~0tG4>rV!GcbSStjoc1$P)7_0Zg6 zQ;FyYKIZ%>oM8SGj4j2vx+||(bP0_QzHI z!l*XkP1O`MQM$bPGiUgwfv8?=VB=nIbDHUT`95L1DaHCh6B7}#P|Q9~wii3sFAGE) zYD4Js9PimHDDwAVU-Did?C?IpL;pZC zuTSt1pU_m4(`bt6NHIGovtsuFEC!8e5157@??%gkapb-!+7~jj ziI0X9Vn$x^G9v}=9JFyd;25C4EIQs+Tv!ZAd5UluIeb0z79KuA+Te{^|dM5E+c(!T(aFzzLUJ&gkVr=!KI5?bX6D{`<^z<9uf9*z} zKLuz#p>gWp|MNy_%}o`@<>LZlW&&E%oe^WbqRjOzj-eQk`Q5T!99;hOwWjGrpG|2! zgo*npCM4hYvn-Fd%`Mb<-f~<|96S0sVQk`RE`1 zhUbeIH);K5@NS$qZW6OLG;WC(aZZSy+_~wI!!{(wW{(jksJRHb&Vro+i9W9l{W16S zDF)BmWx-EfsTBkMiww9u)y1x)Qq_SY7WPUQLisiI|2M};^KlBZPa(zLB2YS8ccSV3h}XXA<4JS#C5(;* z=i2oS+5;WveAciBe5b<~u2cK-)iaeiov60>POO&*yLsYn~GOT1Su(+mL5N!A1vqm5}2*rH*GA2iiR=%AXJcU50aFz4KJM zJ9Y#(Xr|KQt6|H~r(quXx*CTIlB!7F+anE58Hzpn0zCrIaicx@1A6o%|F+npUB8-r za_JN=Gs+8C58mAix>lSI@on|*X#6jo$^#SwWQ@9J)ELfU-Q0W@7T`3` zb>XCQk07r$Zo4d`)`5O==6!IWEF3(qZmk=WYfe9+o}tPcs5{WNu@I+WyZUpB%FtrQRx&3`JCj!%yMyO zU|TRk)FN~ht)=^QFOzr-GUVH+V#{TZ413`wW0q3uu7`xo=khS)t$m6lCYV1L?T+cd zu?-NNP2hJe5XGW}0pnmesjoukIPTwjNK3&IT7yowx^ z`2Dh2uNix&j@Cl&#S(4e1<+9((XPUKYy#2lMqi!72+kA8#I_X$<Y(LleXTL# ziwU5E-c$6%m5Tgy?yV4e5LKb6h+TDgb^g?^aWh4`rx<;f<1Pt$Ye+>C{2>o z+71TRG*)kIo8`15Q{ja)Kz;daW|b z4b}AAB$+`_GPIkvdfw)c5o;IA_I&#lpq3?H}g0Ru|;@IfZ;w$g|WL zxZcI;$LqfMd3e-A2=!6d1TLMdd==#M{8K2=h zpE%;fciUoMgDo63+cGrR{K+10|=?D~d9$!Bev}CmB z`ldcO)mJ-8?Q-H=1gm(z&03h|G4|ZdV)ln^2nJsH)wlI;IlzsQB-4|4M5#nP;#|ne zqoYf0yoAZricNB0<)fo-Byrb^;Si|rpfd$HGW)-DhRU{1G5bGtz6E{7QlFVfjjp=) zalTXEul93PGS|IT@K{c+@1AU# zUynXy(YoJn`@V36`}?ubcM{^?<$S;G^FrePyYq*!C7xa$xu-i<f`3&w_0lSG#0#75TA3mlJ{{vo(Vcm@I^dvjQSz|*f;N>l=Y!NDgXO2 zEw2z~OmwUS&)Ws{1qs&gdnAk>l#I2>#9sjAfXE&i+XT9n5T~XD&!&>EV=1juT9dCG z><^bvZEf}&H&89U_krFSqZN8Q&$B0|M36Zn&h^gW_O|a8hPQ7OP5@iST*um+sR@&} zpw)DA&1j`vcwV^EWG$s$FSB31VVUEKwNK}s>1V}1FN}f5l=we(nw!T#gPQ%PKx?77 zh1vh3)3{BXV~AAOUvm1Mh`fd8JG%ZX(uAjnCPwtbbdPr5Vs0*i4IzcCwiC5zAURJ$ za?w3I;Yj@+<`I-nE&2Gd%UtDDl#~)Tb>sC^(+n+^ce8ae{MP0s+80E-%Q>DUFJio?48i#ulc}Fy_;_0x zd>s-#p=Z?o)PY_ZPP3s=Q)NEtpj0#8(i!;vS9vzP2MnF#cL%1?IX>_eT9N7SozUT4C=y6LD8TM?mNvh|!SI#DC0*DV_gSr-|P?mHxH?x_c|Us8`p$ zQBN3}*V9Clne~hk>+d_h7GgC?%W1ynP*03f*+J&NC`7^5C;q>oj8IORYZITee2*|q z`^Ivld59UmPl&49EKI8oZ(_pBI=X)i?--&#U$z^3&frX9aC~Hj&iCz;($U9UQr#Pi zW9WYK>0e{rtY$q)Ug##qJdcUrA+~EXPVuMnso=OsK?)CYopB=C+!6Ro_r0ZzN$Q@6 z(_MS-H}UiAd#Bzg+$6Zu78RyoKZ4(=DqZ-Ls+8#Ut2POK>`re=qrW%b_IopaZ%K>C z@2y6*Lci*HLE_vbw5FxN_OQ*d`D&BMs0+eRX&+h=t{T=GN|^)E70iJ%0-2n|i+g3B z1D?oIZw$r#%$aDt8v6@~{>{a^y=b}VWiK=0sP}^Ky^*b#M^9v8*X=ExIma?3$}@iL zJp1Vu3%fsV7H&7WAP7d>`S%rSz`2Ibu+BhRkh$j+?qCR=r=WWg1o1|Pgq(y-=~}dD zS?CTl4%QGC{ZWfWiu;eMQBxYx{oOL~Dlxn(NXA4avu-Y#xQ(S65l;tzB@X_LP9xm| z!}(ayo=`a)w}D7KkJu}5f5{>H$tkRx8K+5scMo?RhHN!zqtQ0Rf@3?4`Q?*f<^Ezk zR=NpijxnAS7FN^>Kh3K}?RzZhtD(BC*yBraj|X;Q1}9K!kWH-j4qJC$>BH;ptQcnh zJL-Da{+i`KZf3kD1?!+mJ~yMk)Ked=i{45W%_|ZgE}Ms&-HLofn&KPDCAo||Gr85X zeKz*E;`W6G?5EL)H<&Sy1~Q&&vN2<|efLJl#uN#1z?}48;8JjMscaf%U;S<|af%B0-TuXUrzMqJw4VjVodQ*FpDbO%m@;iN;-$ zOHVZNfX5vlH~!6`GKhPw8h6b7lNG7v(tX+VZpprTC42XM<%sJ58-&>=)rCmV^-CdN zos}8C(q3%&_-eIRnu(o&tC++#+=X@>^~Asyo}>7W*ku-<+2-x$ znz$X%l(BsNRBnl;+`6E~&Mhdi*W_94MR}aP`e#`r?yHMtdD0Za%oHT|Z(4H1a%Ra1 zSJ8qa*ke(%;Au+>P8Oy1lW#q9Fh1IYdnU0?o$HwiN~leX|Bza#)O-59plPkn1%GSl zZmoTSZf&GLoUwTNd$ndjPOBiNY5H?Qb&HC5xy`x2jD1OO1^cHHZ+!3^E`WD{A$~bz z_GY2~_b0~}<4&@1@H=?Yic60;7C0z{%<_4nLpuX%*U0K5xJ_-e*hXS}+ml+kOE{02 zQM-H5y3n4bUvS@=%%epam+*TdP6sr@28b<;@WdmR$4HXD$K3loo^_su>vk!rBqKTkHQkb% zV@ziRVcrjGAn)cqiqJoAlh6cc2DAWL=l!ty(bcc6es#ea>{B^|eJ5Be0-OUJ1?&dA z2sjMbJCE+Wm$coJNAHM{wB3bgMNo3{80O98K}I_Q^0RZ^G0^7s-J|(Uf@1y|;VSag z0lo+Pg!+D&CwnUybQ!!mcB^fQ)Ke=+4xiws@r;g)WX2Cl=9&~-lV@CMT=r2xG9P|6ktf}&+cH@^e zuQXd*`t?(B71Sd_>O!8Ekjw-9i|Z|PwdGHt)s|_!UguccYFZ+MX&%g;W4p36^Mh?= zrJ|h(dLH+Xb+KV*M@tjV0Ciu4bvoU~;Pl1Tw2Q(sY4kkRGimP%a?R~|f`R1jBL(sD zunX@BO3f78Z@1{`J8lxDzEhnp#=)YiM12ggZ2$-p270aqrJa-FAz@ z6Y^hwgWO8o_Hv{k;C(asUmi)50=L;7F8Hp%;=^r1ZM8Gm2@#W?u|(rGKq?~-HAG<_ zSd^;0@n4R(VUJ^d^H4*CgFfMp){km*s+O(}21erqTdJAi>7LIcJ^M@P7)}RoQ2y8o zQ2pC5nXY4gpZfRv)OgqQ8>7j$t%#j29N=NrS=Xi8lq}O)w_u1BxA;7fHPrDdxPBA*iDzuljE`}ibu!{!&@gI5Z4j(8>RkB(mGh*GlkOjI;InT?U{ zy>I4@p9OIa_Hb^m7hniR^93OzPr{G(FrqFq`=5c|vODlZ7c^Hou5}P;X_oC!!z@)M zcDiR`=dwgRXYz1YQ5h?tw!brl(m&n*L8+Xc@nU!z=sTseubs!+G=*WfZy0;hSc>0b z%Y&Xeibv?ZlCJ*g4i;wJmd@(o^o&^Jh%Vgnp`zgjduEs zHN~gI&1(vPo2zM2b04*+rJuE^l_YIi#-}Z61xD6T-Exjh$ym4A7#rf;Vc^waBIkzq zHbLauP;aKAH}r(sxsLv=dbf{!zyw?U`2Z&lTl*DtPJa_|4*_51qqy1AL!1CgE$@CtuMnEF|fBcZ{06Xo|~;uvwI znHD|Tx%Repz5@BFT^GW0XTR~dV2l!HaTI3-og_?&Ss{(D>?EnckTyJf&T0+z&$W9! zM)j?fi#rwBn%2UI5FgH6(*SNhT<8PUvbVVTMo2f^Cb!~0?Bl`^#m0NMZNswYI*+PX zzO3%UZzzopn5(b2;fUx-5tUU78AK^bg7qAYtq@*3=CwPZ8s5p0HI%K}9-HQP?d`xZVyv7#otW^=m5d zy$aZ*x3p6l+l-dI1Kubqd|Uj*$!$g}U+ZTF_qV%3J7Fi@vb)@c$>M&BE6gn4Oso@A z84mdmEfSL3z!q-awqmVl#}gww1}b;hH&@Sxs6xEk|~DF zPtK92`&cHyW)rbA|JN_j3%XN=nerJ1LyKU}8?-M))EmoFT@>5Eopr;6S{3Z0rrF*J)ciO6%U!h zM)&sqb)0Xw-Z00J*EkAaA5?|3WaY06KN~V@;%#`O_pxh0q2J+bShl!tBY!1!k8-3@ zGwFlI|ERS1yA#2{>f7x4XW)rF;+Xs!3Xm8hZ=(QuUwkHdLT^Bvi!;=-(oN{eI1_F( zGVJym`CXu8dQ+kTX+~$!RQrZyTe8z99 zCER5)RMb8N513)2Fu_E_yG%CkClJZ_y&(Y}q<_Y`<41<#Q6WOSQ{(KUG~olx6V z_3tCF2ehZD?N7)0cKiQ!JsOQ(#DiO=qkplSqNgo=V|l!dDO?L(@m&GkMP*(YZRL9` z=&wavp57xL>!|^ir(-13p{Wj(^^S5#H{ILM4mu|={3M)Rv-KOTke=3fjzHU)P{z35 zl@X$jP5rF-oyLE760@#1j97Xed({s$j9l7Vr(qlAh5oJhwNbUy+xWMo*RlKf_I<6` zzX~cG;s{aDU(q%ebNO!Ei}zQIFV*3=Ae8Xicb_hW7sFFr|;M|tDg(X(W31fWCqSc_rnssX*23t;6P6DE{W8cL&bnZb3Rv4o*eA9*x{wa3?AkHpRGtF&4TNPgc6yJF)xOVSa4{&)2}nV#Iu`r2P1sd$GRLVZ+5>w# zaq?Vj$MgzTkH0t~yckg=90Z&kTO?GD#qG~pqHji9>0BOvA%f1OB2K~cJqffT5%u;3 zgHkymXTf~zWQaKeEu*T1s{c(X*U&(ycY{Sn}fjryY>UOCwf?i-{*5b z|K@z=%*>jZHEY(aJ+D2pHgxpPbrR2ANHM3|w&VSz!Ij#H;yreA_zDv9e+D{Z=_@It z1;^A%oY{y+Ki^pA4&AZY!ns}x4Z654qcrubJMPW0XM`8~WC>^8uy*e5JgP=m-)Dv3 z5hBgMnxRXwK`+&l>We$t^$WV!2>BR?04A3Pw$0&)!YXb8canl%Xo zKQ0vO4+x3#4+tqCukSb}^bX-74&eWL^HFE*iDN=vG2A!g2T?!454jxPIW_Dhy+aLq z>6{eRm+DG-Mxc)dD;C=IehRJ@yTFfELBn`D-5B;Y`_6SqdK0ttYr`;8t8(be_2tC- z#hc@9s~SqDyg43sgV#r>4I_Ds98 zre|`qDX7UCN#a)iDnDLC3>tcT@e|6?IH;@I7616>{jA#&CO^PqJD@VDE!g$yUq6 z7JY$xDo%Oh`$3Leg>au2a6Da@TuW>?Z34f)KXHs~B|o5kGsR_AKa; z>aiQTw__m}tTIjmEWN97$_*`Ybf+c=X8=QdPBMI%=RRPsO^e+@buDPqCrS4atd^^| zuVl4+FP2BxCtx(8Tpo~rr5vX~_Y{|E=Rl8Y7uYVT`-Lx~p2YhVSIF*14#$LEF$D27 z`|t1`$@%?Ir@?ta<$T;(>@CxC0pVPT!V~Ro(nO}JrJdl8lg54%+}Vm{ zxJhzcsKgFhGEOje#jcsg3q0o0OKH5~mx=h6gfA1Ek!ne}uWlJ`Z-_p(W%y!(=K}+#d_&?o?9_HFc?9 z;!=<_PIwPm-Sr2c2RQ zobWV(_JgReA}t4n%J9>u!$H*HG^jiXYKb3I9;L&*^)|6(p#O^6#O2N@hW=Z3dK>TzW)jfhTGeVP7g*q#ny%;zmqb%}OQQ4U`eW@vbmkRPA5aPnqI9}x z4zw@a;NBr{Z^#Eqe@$aC(NzQ*${pzB4aTChnW;0=Nz#^XCs}hz!WiZAjd`t$rp8q~ z$8u5_5OrMG4vpZSz{f$K)|1g{`6Ai6S+0=Uc%KVfn^tvAJKJ6Q;>E}Q?AcQHP@X1mIpDa~-WvSXLrXs0=PnN2^;WwnJ4*d8sDm=-6_gH_bo&Zg0 z6RKB@jO*o`G^m=SgslPX^1B|AZ}i=qhpVQcRTKB^0zc_#Scdm-0jZ8F+^;xuJtw+f zxr13Pj|zm>cgI>YzUhN^caB@2x2&1-yRwsf&TUThkKtL35TDhED7rVj67R#7x6Ab* zVYxfW?^P>}qRo~*tzAXwf5ED5zi-Ucg7Nsi-#58~>Tu_|?G^M*ij2{iL46aoDUHNl z*qf#sMJ=-$gud>FAU@058ik0Fu)$?%G+S zW59t+|GNFarLU$O5XNUi_j0zO?}&g3A#GfRkU#C7^m~vpYuo|BJWU%A*B6>N;GaA0 zz@>ix+j0GYOJ^~o7!Q3y6MGyGX230mE$dMsteh5voU5j#BF`4!nqeLl&^Mz($Qh3` z=74nM8lT?R10}s^ZVU87+86sdCP4=P@^1pJiGe5!Hs2d%BTpIpb7AKrT#oR1Gtwcv z1@tVWP2jC1~vy`wDoJIXXGB}QYn__>!x?{?bTgO8@-ijjqo3aKN0@@ za3>)=7=Ge2orQ4Aa8ExY3@T@L)skk7N9uMu?0GZyUcnK%MBH1rL3H{n8QM*pu6Q_I zmUZC`LEe>^!%`b*c-QkB?{c2;PUnFyb2!Gqu=GW^8Q3i;?VZnrL6Zh0EQ@ozHY6vp z)&p;I%}Gs`k!?Xqv6xddVV;cCp-7ePd~L{RzXUT**@lHSSz_8CuO7?~s|k)I>C+av zc^(%1u`nq5jIeV3-nh`C-jHx#*Penr9=h&H3~Vpq4wQg<;>g=%@wUOWxg{;>t2%rM zQ>4HZ6byyzN4URJtU#Y($I<&>ySQuPtr+o zKIfe&$!anBLgNn2Tn~)2VJ*&hQK1>e1v#8F^>RzV?PZ*2=|7iFI5{-2z@}<$a3h_$ zk9AI3f+y%&8yDtYb5F7r`koo)nk|;g;_N;$#o%hSyxPM0Mx;e#vViUb(4SAjd;dw9 z)tKE(t(K&l;^-V%oBv4RTog{n;DpVkeG-O9N~H(>NnX&Py=s#0To(Lg z^*GE=N&EMInvPAUlb$trOIeLK5tYK~Xi7^d_tzC+zPTBe!ySaT<4C^8oBMF--}FFVS+#x!1Yf+`qVMfct6!HIbTOnh`gZP9>#j z%$hkO^fAqH&1%i7qW_;7I%zavx__~^&d051*WBV)ivL~g7|`|X2dUcyj*%{-QxANI zb%Enq3V*oMigVX2ATUYzU{~t>OLWB(miX#Yi!}jP>!78w`AXf}c<&=4%Zbgkf&SgiW$P%I#U<^==D= z7&q=(_t_}ma|#{tuNOt#)%I}H_sas8^+-jPGyRRi0R%IkASD^ zXl?M^bw1#6F@E?C4!hU$A@TjL&rh8{%JD4u_1EX6J`9b6f@MwfLkRm^FR3fS+~CFN zF1fVknSd1!xw*M(W#9T-U-+KGK0qbD6%{Tbab_a5Z$JG@dD z8a6Dzy)G%$$87NPNf=p|kkZ9$h!pDLQ@lhT9lY)t&~jmr$VKrf4jR#TbsO>d)O0Qo z`>Fhe3)=+;4}!E={@rrnf`@;XVn_efVkiG6iX8&ZAePJ33>b0k3*>Sz3`o%eZ>yyd zH*ZEXR&VQrk;B;wqe|wp;@oS}^+OkYM(q?FG2}K`F0+U! zUBr~x`22(S#lI~|p;6v*UnqD)L67kD;q5%OS_+y5iJXA`6=5Lx_;KK&#tYo#rLznb z!j^%46ca|e=SZ_dCS43az1!wtGH0AKiOY+^E=cF3ma6X%2E_8o>+|dM$xc1^!cNNCxw(5Sw zu{Q_qyWozRhFAmVS}hM)yCip!^>UDm_(Be7&KCif#)uTS<*-qZiRa;xo>rP?Y)9SKWqC`tuPxwKcsG=lS8ZhDSishui;zcVY$y3YmD8E zqm6Bt(P@1akPvFZo z$2XRjX6G(RbufB2cy#ND5_(Q|TH&+??^NP6L4`F=Yn)au%-H{fuKPcF;j3Pnt@!$} z1HN;)^Fl6J~X77d%yY;D}IyeSGW-&8$4D@a!O&eYd2%7BsHy7=R2U9RJ z^*tsSWF2va;A7Bu^|_0~)w}Z&PfQo@9^Mw*W(g7cL9dc__-U7fs;Q}m1&`qr@^*2# z8aCB%Nxw(f645sv|MR&%<|M)KVJ_@7dpVQv1?nh+AQ6gH`X}^y;4K9Ngd^5{z%x;+ZX?D#k^3r#{9hF_^P$$zt-_M zLq2*zSa4GHGQk^t;2SYp1s}L?#TZhy3cg{^TXqZE0J{_GVz=w?_g;WAtsK6hGbDNG zPQh|ktP2|s9iMqb|6yn#|MS_1QHKS-e`pH6bm4bc2<(3td>NQ>STOh=7LtPxLzCWN zAs2pdM=&6-V?5%(gVgfZM~mPPLy#b>QB@W zX*PG zA$h0_8LFRvy+4#b0A&u)=+7bkjhI71LyYADNu*Qo9n~(-<>eUM1@9`gNlqfq(vn~5 zA6WI0Ua)-D8Y^N?FU%@=%-jTn;_u>~htkV8DspLXPoI@0^6*v&?HBmiCMbs5T6UOgbTCy|Sgs;NQnu z(aikrOUg=ec-CT>*ygZybA1=*?1XoB9ul&`Uq#-+J)h%il?WOAuvc{$en_(Tq|j4x z61!8MzmnNALS{51_9U_dK3fX#T=^dppf9wQ#*(KOK>@bmsEkOp7Cp8{|UPp_6^v*uq4MG7|)Nue-_rqkMVzjJp*`YsLa z8dAt>-x66{I%}|$FUA)P8oSb(h2AeunBiKzQrH+u8pke|XJcYjIpL-p*~YV&7iC9SOHxg05E^ zZsHslj=Nz5h&+fHUMs#wgL3h_7#g;roF}n9VIz)XeyN{-r^`xVQ?#}UJ7qI{;y>Am zlVFzpEr`1l--hNKtAVFia&OY(p+U?p#H^eoKpXmQ>!K?uSf$+u`c_6$$@ll_RmGva zmC@KcZSvSb8ZlQ#qgFAb-x4RhWBli@#3;^!=TRQcSBzDpp`PP!uZ!3r*BO+us3&lX2K5iNRCB|jIn}_4?+Y$K_T0|n&J^COVtug> zdM&G=y9(Om9nR`fCybBJattT^+;QG(;;tQY9i@S`(Ku+scS&6n*K`cMpin%L=9rEb zr2d%XnXJ%&+SW1Lg@%qii`gnz{?yiF=?Mwj)zoBhg_L~@t;$s9LAdv)(Hn=V>rIx& ztmDDsqb)134!bu+IxpB{IoXl}=wb2ypcvO=(X@^R>;tqI-V#9zD7oOP?4$QaU3N5C zTABsR0^t1y(n;|Xpwm>$@kEy$R-9*Swd`%i9admk)e?x6+2<|Jp;Kz25nhh(K{Z*H zVV9sg+U=`0u5TdD?ysKNYFW^HcM4XXxmJsFi#OVPQ)Fc7L19az4*Hv!7YjIe>9YU9 zOZ8)$EX-=Pn5@!@KI?iIzFcRuq+Rb1jr~?jGQJPhFQWd= zD?_h1v6Z+o==!0nEQ`5vg9S@=>u%^9tH-Gh;#@yDU9y;tm(vM2HP1y~t~pWX61uLY zTk`6%9i|J=og!oY)fwnf4<5q$9P4TwHW= zbaHmlx_R8@&4(GD;V|;T&;U$cPe%&f2p?6&6a1Wp{uy(ErKorZ3Q6h7t&rngC;)Ro z4ikPdbvEG`Ilq!V!3LknCVr|-`0pxu34b7nBD$O@023>RkYX1puh?O!pi%-VKXxe7*U6;s^@ z>Fe9t$S#-hD{eLak-y?`I{=aQ)xeY&`KTB1;YeQrI}dgp>}psm>}J>JGeXm=LA4O7S^W9 zGgj;`wW9$riX(F|LZT&C#TNw@rh?T1AGLF}3ZDX7u7a^UhxIEf$jH8U_2|D|T(J55 ze^gt(-Lt9UrGO!QG^5iVnElwA57z(ipHGe#m5fdHb=F6X8dp3s#^2FrL)EH9pZ)#2 zD_&ju51RJSBt!Fs{a-%4V)vG>4xDRx<1fpX-5u=KJ#O+`)5@jW-Z|Uy#o_tJqzT&F zBL?1=koV`%upYV{Z$0t+q1v{8_Z{Ag8fKf}%oGKKNB;DlIE0oE9Dt$=JrttG^?j^NJu)paVnCvJY$s z8j0Ge8?4e^@ZMx{zop=jI0?AkfZ0PVo+`sXK2003X0;E_ERwkaM{j|+Pi43iOaD<+ z2;-5L45dE;KM(iA5x}LwLvV^`2Iz475uSv*y#I=**aKIG7(BWnRXa}d-W2_NUA*l}pbomHQyK9o*AMOSmzp7DZQ)~5?i@;%_{|iV;5cRvJRI=+5+8^MB;w_; zRION8YLkwPL2ejjEXN^L(J1K`sltSdm%~bWy-ipr4-iWHQXBjmp!*!4)ce z6ejr~L-@#aL@(izvoTwGJ4#P@ZYBNC;kP$GNY5F`aRPLm$*f^VhDtEff7T8|nqT|8 zQhgQkvEqT-(ZhZOa0Rcz`|8I#?iZA2SB3H?z4K71ch@}uQCx43`r9M1)TdIvE%n_u z;NOjYFXp}8i$rUS@JnR^VjCfcN$zPpv3;JYuk-|*WLQ|M5nXuOO|m=Ym|$*C$lG`{ zxYVDwN3%9r4sSqi_EGIRp1vIOaAh#Wo;2XG&X)ldwe^kY>htBd5 zzH^vkQ=J>(CM4b)nm^=jV|f0cu+W6WxUnOpv591l=D!zF!GGfcJ&1}d0kIza5CQW& zRUC%uNj%OXhQLsmj1s2u$c&cb6@F44GD`ZRQc$Fm;`Z6}U60G5;WVM?O#?{S%a1^?ec= zO?a_Q`p<0Q2|pPHKZSo!d7X{lwm!#f>QgS)m%0h^y2||p$qN~sP5AFN{4bXADgDzc z$U}IP{LYA{u);4Ih}H2Pa#%*oZbz8>%J@@GiD4A_D4vWWk3UBbWv#@Y(moWn#rL%w zuh46oeu-Rvv3#W6qpCmA@rWEIvarlxKbM~3ZPQb}aLfU^WlJnt1pl3|L$jE60xXq5 zb*;!`+NB$rcF)5SY!bqIkY_MlO5+2!DFgGE$E7gA)FwNgX-C5@MH)3+@h`<3Wa$Q+ zwOk;@J0V>>Y#%xO`zx6?r$Ea2iSU8D#L_24BY}8k4oVe01p72 zv*7NA?FV~)Ez>TVEyZsg#k6n3>H(+xqaOqP&m-LuDZD=g{6u^@a8Ug2#XleKhBVJ2 z&+g6G`(G&KIh_oeVfVmhA}$v0?lq|6A}OBms!h{KraiQYX-E7Sd_;YAjAq(@!7c;* zHPCT22{f$6obgF1|JMkwgC!iN5Pu~RXG{{nV_6PfMVo&Bdpls1W~(Yb8hCBvr^#}% z2ri|!HHsYN{X~3hc6Q>J96l*FB`q;Qhwajc$U%eezGui#_Vl0`* zPc_b(QN|bL<;^N7G35{A!{)NFg|kXc`FufPnW>~?R&g0`HWud3FqQCm#u+nA`Fb52 zW13b_idag`=NHU2l}s}gmg)I)W3h;VM*`84(vE{tPAxzYd|6QuUo^GMRLBPcIIy%o z5T+E)HqI!>=i_5D__7jXVQHR;u=0Vd|1jQEXq++wyK|tm0OjjgPDz1?hc7ji@v{m` zXBDF|WdKq3nML`gVLXF3lK#T!54)#GI=QsiMEMG4;=gEC86OxY-uKxf&3GG1qm}1O(0PN(m(aIS)IwogJ64^xAXirm)75OrNPc{}#15n38 zW58E5p@^rbsBA`&G2c{jGrSbdq`>3a(}LTP#!m%u65~=63d=7Yz-LSCQ&1{4QF}v3tkyA|P8akOhEI=8 z$>6hds5U>5BuIw`@rGzmDKeHocIOqFSbS08)PiZlAh~>8J5+k^_>smk(;VYGkEp<(m`vT`K)ZtTdSi%3YO5O=SeXm1}&$d42DG`j4pZG!P zZR@9$Z_6K|hl~0o|K$Jj`)pEE;Tv~(JA)3 zcEUSbRUe)TuoK=pRCt33t)1`!k7yJ=*bDFfVY^8c_?`bpReaDBE<5qJuS(7W39X&* z{vYyJ=FIP7@E`n@_}5wePG|A=u1=EQe6WMwU67%^M+a-KXJZsi%3x?rvM1iJWqRq| zi#_q~BVu9bos@!CfvL+KM$dpf$Ijwd z#^a=wL9wnb%G=;`cqNw4TJFO;xp8=xW@g$_*jbaA7PrH+J>qb#3ifu`X1u#L+Q>^g+2#qk;(bTl&hy zHaKvCR3U{5`ORkKkMO$K1+Q|pi|R~s^xsnFjw+=-|0?3O6Ca+D3;R87)DgXZ$b-a{ z|228|quu^D+l|h<)3jIVckIjfQ}zV z(2-9EqR+)?fyRKNn@$?ZaUch=oD%C`U`{v3>YQb?*x`0?b@X(|V?}Hxenu#DF|q>Y z=1>f`5OZuJb9M-0;mp$^on_oC+fb(Q_HbfD-qj9z-F5xQCtB9eUUK`p zCdP$1^aA`$?&;d4iHFl(JZf6^QvTyO!|&jX`iYBs$$uMO9F0rK7@Hl(+D$pu4jdLb z3CzT1gOohDDIlc`ZV8(Tf(SB_jRiCTxiZ+pfDmjjrpH4;ULFfYs=MKz3f~~OLlH-? zL5PchF9Lo_e-GTj$YDf9O-S_!@C6bj9MPZ{TM8xkQ{-??NLNW7*ju5Q*_mn-KL!Fp zWY8)OSN&k@^W3QKtx4x~?1R@yUN6zQHXqhr>zbkWQY&q_-Chg zCF(m~y~Psj^BnIKaY^*r^SPiqUt|<{u$P=UtK`g{^9#O{@aj7W?>?3MD*Npc$A90y z`fi_IL}ed2lOJR8eTE`u1S9s)I}QaV7I_PQi#q5Z2u667XYjW?+ugulLQQN|_)9Rt ztA-(XOJ*V=fKk&%Fk5i z5b;5w?Z>HZ9b!t z?{S$)_U3PmjF+Axd(vxA(M#=SuW{4IhQId0JFJuNMs*Tib^l<`b(TSFEBnuPIkPcqZVFy|wwrogD9tBD1xa#i{&pveh=rl`u|4rB>z3a$XA%js`Fols zH88X;v}b=tjYdN!$(cRJp>W_Mqu{ld{8mV4?fzWM>mHK+O1b7@u4%@tuvmtT$P+j^U2ORhx1$3 z*pj5){BQdn&0cu_*UnvZ)_b(uX~6=c>USi-@BCNylk)uGcVd*X+rD4!SR34}7o}cz z+jIZH(dK!xm;8FE@XGv^d$?P#yE+HyRw2|R57PS~`<#0toh*cx#Lb@L@i-ek*lYeU z_Q&G|DF6LN(bZ$`d8&9J3gp=v?>c!MK>< ziDkTUww7Gwd~LqZIXBKBpN%etP8ghuyKX#m2BADU>-)*XpFZn*Z{nD2 zYLYpnujH?Ov?g}dxD|hkUh&AQ%UgyVu5&v5Ws0ls*^`?xL*Bl&d_(obrM;qC&J6qZ zlOb0RU&&ZCWW>RBg?$HhxpaTTlMg**FkBpb#CU4&qzg~gUdTSCtKL0f#k)aY8; Date: Wed, 25 Feb 2026 02:51:39 -0500 Subject: [PATCH 212/311] bump --- panda | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/panda b/panda index a95e060e85..7b9eb4b247 160000 --- a/panda +++ b/panda @@ -1 +1 @@ -Subproject commit a95e060e855dc59898dd3bf2274a598dacdd2571 +Subproject commit 7b9eb4b247e09e4aa40b78394e45d92f5b42a569 From f43dc93bd91ef16fb374954b4c106d2aa1bb9cc8 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Wed, 25 Feb 2026 03:34:05 -0500 Subject: [PATCH 213/311] Revert "bump" This reverts commit 06a5a380df70244a2a7c82218ca7748095aa9d6d. --- panda | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/panda b/panda index 7b9eb4b247..a95e060e85 160000 --- a/panda +++ b/panda @@ -1 +1 @@ -Subproject commit 7b9eb4b247e09e4aa40b78394e45d92f5b42a569 +Subproject commit a95e060e855dc59898dd3bf2274a598dacdd2571 From d9b5a1e30b7a29c6146d6ebb85f09eb860dcc227 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Wed, 25 Feb 2026 01:59:19 -0800 Subject: [PATCH 214/311] WifiManager: add test for state machine (#37396) * test wifi state machine * clean up and another few tests * no unittest :(( * clean up * clean up * try to repro on device * try to repro on device * nice, the flicker is covered by test_user_initiated_skips_dbus_lookup! * add todo soon to be all fixed * documentaiton * test the thread races too * _fire -> fire * duplication * new state * fix some tests * format * combine similar tests * use process_callbacks * clean up * collapse two tests * rm nl * previous messy test * delete old * asked another to ask questions --- system/ui/lib/networkmanager.py | 2 + .../ui/lib/tests/test_handle_state_change.py | 678 ++++++++++++++++++ system/ui/lib/wifi_manager.py | 15 + 3 files changed, 695 insertions(+) create mode 100644 system/ui/lib/tests/test_handle_state_change.py diff --git a/system/ui/lib/networkmanager.py b/system/ui/lib/networkmanager.py index c47928d8ba..c0e9fd289a 100644 --- a/system/ui/lib/networkmanager.py +++ b/system/ui/lib/networkmanager.py @@ -23,9 +23,11 @@ class NMDeviceStateReason(IntEnum): # https://networkmanager.dev/docs/api/1.46/nm-dbus-types.html#NMDeviceStateReason NONE = 0 UNKNOWN = 1 + IP_CONFIG_UNAVAILABLE = 5 NO_SECRETS = 7 SUPPLICANT_DISCONNECT = 8 CONNECTION_REMOVED = 38 + USER_REQUESTED = 39 SSID_NOT_FOUND = 53 NEW_ACTIVATION = 60 diff --git a/system/ui/lib/tests/test_handle_state_change.py b/system/ui/lib/tests/test_handle_state_change.py new file mode 100644 index 0000000000..eefd83c628 --- /dev/null +++ b/system/ui/lib/tests/test_handle_state_change.py @@ -0,0 +1,678 @@ +"""Tests for WifiManager._handle_state_change. + +Tests the state machine in isolation by constructing a WifiManager with mocked +DBus, then calling _handle_state_change directly with NM state transitions. + +Many tests assert *desired* behavior that the current code doesn't implement yet. +These are marked with pytest.mark.xfail and document the intended fix. +""" +import pytest +from pytest_mock import MockerFixture + +from openpilot.system.ui.lib.networkmanager import NMDeviceState, NMDeviceStateReason +from openpilot.system.ui.lib.wifi_manager import WifiManager, WifiState, ConnectStatus + + +def _make_wm(mocker: MockerFixture, connections=None): + """Create a WifiManager with only the fields _handle_state_change touches.""" + mocker.patch.object(WifiManager, '_initialize') + wm = WifiManager.__new__(WifiManager) + wm._exit = True # prevent stop() from doing anything in __del__ + wm._conn_monitor = mocker.MagicMock() + wm._connections = dict(connections or {}) + wm._wifi_state = WifiState() + wm._callback_queue = [] + wm._need_auth = [] + wm._activated = [] + wm._update_networks = mocker.MagicMock() + wm._get_active_wifi_connection = mocker.MagicMock(return_value=(None, None)) + return wm + + +def fire(wm: WifiManager, new_state: int, prev_state: int = NMDeviceState.UNKNOWN, + reason: int = NMDeviceStateReason.NONE) -> None: + """Feed a state change into the handler.""" + wm._handle_state_change(new_state, prev_state, reason) + + +def fire_wpa_connect(wm: WifiManager) -> None: + """WPA handshake then IP negotiation through ACTIVATED, as seen on device.""" + fire(wm, NMDeviceState.NEED_AUTH) + fire(wm, NMDeviceState.PREPARE, prev_state=NMDeviceState.NEED_AUTH) + fire(wm, NMDeviceState.CONFIG) + fire(wm, NMDeviceState.IP_CONFIG) + fire(wm, NMDeviceState.IP_CHECK) + fire(wm, NMDeviceState.SECONDARIES) + fire(wm, NMDeviceState.ACTIVATED) + + +# --------------------------------------------------------------------------- +# Basic transitions +# --------------------------------------------------------------------------- + +class TestDisconnected: + def test_generic_disconnect_clears_state(self, mocker): + wm = _make_wm(mocker) + wm._wifi_state = WifiState(ssid="Net", status=ConnectStatus.CONNECTED) + + fire(wm, NMDeviceState.DISCONNECTED, reason=NMDeviceStateReason.UNKNOWN) + + assert wm._wifi_state.ssid is None + assert wm._wifi_state.status == ConnectStatus.DISCONNECTED + wm._update_networks.assert_not_called() + + def test_new_activation_is_noop(self, mocker): + """NEW_ACTIVATION means NM is about to connect to another network — don't clear.""" + wm = _make_wm(mocker) + wm._wifi_state = WifiState(ssid="OldNet", status=ConnectStatus.CONNECTED) + + fire(wm, NMDeviceState.DISCONNECTED, reason=NMDeviceStateReason.NEW_ACTIVATION) + + assert wm._wifi_state.ssid == "OldNet" + assert wm._wifi_state.status == ConnectStatus.CONNECTED + + @pytest.mark.xfail(reason="TODO: CONNECTION_REMOVED should only clear if ssid not in _connections") + def test_connection_removed_keeps_other_connecting(self, mocker): + """Forget A while connecting to B: CONNECTION_REMOVED for A must not clear B.""" + wm = _make_wm(mocker, connections={"B": "/path/B"}) + wm._set_connecting("B") + + fire(wm, NMDeviceState.DISCONNECTED, reason=NMDeviceStateReason.CONNECTION_REMOVED) + + assert wm._wifi_state.ssid == "B" + assert wm._wifi_state.status == ConnectStatus.CONNECTING + + def test_connection_removed_clears_when_forgotten(self, mocker): + """Forget A: A is no longer in _connections, so state should clear.""" + wm = _make_wm(mocker, connections={}) + wm._wifi_state = WifiState(ssid="A", status=ConnectStatus.CONNECTED) + + fire(wm, NMDeviceState.DISCONNECTED, reason=NMDeviceStateReason.CONNECTION_REMOVED) + + assert wm._wifi_state.ssid is None + assert wm._wifi_state.status == ConnectStatus.DISCONNECTED + + +class TestDeactivating: + @pytest.mark.xfail(reason="TODO: DEACTIVATING should be a no-op") + def test_deactivating_is_noop(self, mocker): + """DEACTIVATING should be a no-op — DISCONNECTED follows with correct state. + + Fix: remove the entire DEACTIVATING elif block — do nothing for any reason. + """ + wm = _make_wm(mocker) + wm._wifi_state = WifiState(ssid="Net", status=ConnectStatus.CONNECTED) + + fire(wm, NMDeviceState.DEACTIVATING, reason=NMDeviceStateReason.CONNECTION_REMOVED) + + assert wm._wifi_state.ssid == "Net" + assert wm._wifi_state.status == ConnectStatus.CONNECTED + + +class TestPrepareConfig: + @pytest.mark.xfail(reason="TODO: should skip DBus lookup when ssid already set") + def test_user_initiated_skips_dbus_lookup(self, mocker): + """User called _set_connecting('B') — PREPARE must not overwrite via DBus. + + Reproduced on device: rapidly tap A then B. PREPARE's DBus lookup returns A's + stale conn_path, overwriting ssid to A for 1-2 frames. UI shows the "connecting" + indicator briefly jump to the wrong network row then back. + """ + wm = _make_wm(mocker, connections={"A": "/path/A", "B": "/path/B"}) + wm._set_connecting("B") + wm._get_active_wifi_connection.return_value = ("/path/A", {}) + + fire(wm, NMDeviceState.PREPARE) + + assert wm._wifi_state.ssid == "B" + assert wm._wifi_state.status == ConnectStatus.CONNECTING + wm._get_active_wifi_connection.assert_not_called() + + @pytest.mark.parametrize("state", [NMDeviceState.PREPARE, NMDeviceState.CONFIG]) + def test_auto_connect_looks_up_ssid(self, mocker, state): + """Auto-connection (ssid=None): PREPARE and CONFIG must look up ssid from NM.""" + wm = _make_wm(mocker, connections={"AutoNet": "/path/auto"}) + wm._get_active_wifi_connection.return_value = ("/path/auto", {}) + + fire(wm, state) + + assert wm._wifi_state.ssid == "AutoNet" + assert wm._wifi_state.status == ConnectStatus.CONNECTING + + def test_auto_connect_dbus_fails(self, mocker): + """Auto-connection but DBus returns None: ssid stays None, status CONNECTING.""" + wm = _make_wm(mocker) + + fire(wm, NMDeviceState.PREPARE) + + assert wm._wifi_state.ssid is None + assert wm._wifi_state.status == ConnectStatus.CONNECTING + + def test_auto_connect_conn_path_not_in_connections(self, mocker): + """DBus returns a conn_path that doesn't match any known connection.""" + wm = _make_wm(mocker, connections={"Other": "/path/other"}) + wm._get_active_wifi_connection.return_value = ("/path/unknown", {}) + + fire(wm, NMDeviceState.PREPARE) + + assert wm._wifi_state.ssid is None + assert wm._wifi_state.status == ConnectStatus.CONNECTING + + +class TestNeedAuth: + def test_wrong_password_fires_callback(self, mocker): + """NEED_AUTH+SUPPLICANT_DISCONNECT from CONFIG = real wrong password.""" + wm = _make_wm(mocker) + cb = mocker.MagicMock() + wm.add_callbacks(need_auth=cb) + wm._set_connecting("SecNet") + + fire(wm, NMDeviceState.NEED_AUTH, prev_state=NMDeviceState.CONFIG, + reason=NMDeviceStateReason.SUPPLICANT_DISCONNECT) + + assert wm._wifi_state.status == ConnectStatus.DISCONNECTED + assert len(wm._callback_queue) == 1 + wm.process_callbacks() + cb.assert_called_once_with("SecNet") + + def test_failed_no_secrets_fires_callback(self, mocker): + """FAILED+NO_SECRETS = wrong password (weak/gone network).""" + wm = _make_wm(mocker) + cb = mocker.MagicMock() + wm.add_callbacks(need_auth=cb) + wm._set_connecting("WeakNet") + + fire(wm, NMDeviceState.FAILED, reason=NMDeviceStateReason.NO_SECRETS) + + assert wm._wifi_state.status == ConnectStatus.DISCONNECTED + assert len(wm._callback_queue) == 1 + wm.process_callbacks() + cb.assert_called_once_with("WeakNet") + + def test_need_auth_then_failed_no_double_fire(self, mocker): + """Real device sends NEED_AUTH(SUPPLICANT_DISCONNECT) then FAILED(NO_SECRETS) back-to-back. + + The first clears ssid, so the second must not fire a duplicate callback. + Real device sequence: NEED_AUTH(CONFIG, SUPPLICANT_DISCONNECT) → FAILED(NEED_AUTH, NO_SECRETS) + """ + wm = _make_wm(mocker) + cb = mocker.MagicMock() + wm.add_callbacks(need_auth=cb) + wm._set_connecting("BadPass") + + fire(wm, NMDeviceState.NEED_AUTH, prev_state=NMDeviceState.CONFIG, + reason=NMDeviceStateReason.SUPPLICANT_DISCONNECT) + assert len(wm._callback_queue) == 1 + + fire(wm, NMDeviceState.FAILED, prev_state=NMDeviceState.NEED_AUTH, + reason=NMDeviceStateReason.NO_SECRETS) + assert len(wm._callback_queue) == 1 # no duplicate + + wm.process_callbacks() + cb.assert_called_once_with("BadPass") + + def test_no_ssid_no_callback(self, mocker): + """If ssid is None when NEED_AUTH fires, no callback enqueued.""" + wm = _make_wm(mocker) + cb = mocker.MagicMock() + wm.add_callbacks(need_auth=cb) + + fire(wm, NMDeviceState.NEED_AUTH, reason=NMDeviceStateReason.SUPPLICANT_DISCONNECT) + + assert len(wm._callback_queue) == 0 + + @pytest.mark.xfail(reason="TODO: interrupted auth (prev=DISCONNECTED) should be ignored") + def test_interrupted_auth_ignored(self, mocker): + """Switching A->B: NEED_AUTH from A (prev=DISCONNECTED) must not fire callback. + + Reproduced on device: rapidly switching between two saved networks can trigger a + rare false "wrong password" dialog for the previous network, even though both have + correct passwords. The stale NEED_AUTH has prev_state=DISCONNECTED (not CONFIG). + + Fix: the handler's second param is currently `_` (unused). Rename to `prev_state` + and only fire the NEED_AUTH callback when prev_state indicates a real auth failure + (e.g. prev_state == CONFIG), not a stale signal from a prior connection. + """ + wm = _make_wm(mocker) + cb = mocker.MagicMock() + wm.add_callbacks(need_auth=cb) + wm._set_connecting("A") + wm._set_connecting("B") + + fire(wm, NMDeviceState.NEED_AUTH, prev_state=NMDeviceState.DISCONNECTED, + reason=NMDeviceStateReason.SUPPLICANT_DISCONNECT) + + assert wm._wifi_state.ssid == "B" + assert wm._wifi_state.status == ConnectStatus.CONNECTING + assert len(wm._callback_queue) == 0 + + def test_need_auth_targets_previous_ssid_via_prev_ssid(self, mocker): + """Switch A→B, late NEED_AUTH arrives: prev_ssid mechanism fires callback for A. + + This tests current prev_ssid behavior which we plan to remove. + Migration: (1) add prev_state guard to NEED_AUTH (see test_interrupted_auth_ignored), + (2) remove prev_ssid from WifiState and handler, (3) delete this test — it becomes + redundant with test_interrupted_auth_ignored once prev_state is the guard mechanism. + """ + wm = _make_wm(mocker) + cb = mocker.MagicMock() + wm.add_callbacks(need_auth=cb) + wm._set_connecting("A") + wm._set_connecting("B") + + fire(wm, NMDeviceState.NEED_AUTH, reason=NMDeviceStateReason.SUPPLICANT_DISCONNECT) + + assert len(wm._callback_queue) == 1 + wm.process_callbacks() + cb.assert_called_once_with("A") + + +class TestPassthroughStates: + """NEED_AUTH (generic), IP_CONFIG, IP_CHECK, SECONDARIES, FAILED (generic) are no-ops.""" + + @pytest.mark.parametrize("state", [ + NMDeviceState.NEED_AUTH, + NMDeviceState.IP_CONFIG, + NMDeviceState.IP_CHECK, + NMDeviceState.SECONDARIES, + NMDeviceState.FAILED, + ]) + def test_passthrough_is_noop(self, mocker, state): + wm = _make_wm(mocker) + wm._set_connecting("Net") + + fire(wm, state, reason=NMDeviceStateReason.NONE) + + assert wm._wifi_state.ssid == "Net" + assert wm._wifi_state.status == ConnectStatus.CONNECTING + assert len(wm._callback_queue) == 0 + + +class TestActivated: + def test_sets_connected(self, mocker): + """ACTIVATED sets status to CONNECTED and fires callback.""" + wm = _make_wm(mocker, connections={"MyNet": "/path/mynet"}) + cb = mocker.MagicMock() + wm.add_callbacks(activated=cb) + wm._set_connecting("MyNet") + wm._get_active_wifi_connection.return_value = ("/path/mynet", {}) + + fire(wm, NMDeviceState.ACTIVATED) + + assert wm._wifi_state.status == ConnectStatus.CONNECTED + assert wm._wifi_state.ssid == "MyNet" + assert len(wm._callback_queue) == 1 + wm.process_callbacks() + cb.assert_called_once() + + def test_conn_path_none_still_connected(self, mocker): + """ACTIVATED but DBus returns None: status CONNECTED, ssid unchanged.""" + wm = _make_wm(mocker) + wm._set_connecting("MyNet") + + fire(wm, NMDeviceState.ACTIVATED) + + assert wm._wifi_state.status == ConnectStatus.CONNECTED + assert wm._wifi_state.ssid == "MyNet" + + def test_activated_side_effects(self, mocker): + """ACTIVATED persists the volatile connection to disk and triggers _update_networks.""" + wm = _make_wm(mocker, connections={"Net": "/path/net"}) + wm._set_connecting("Net") + wm._get_active_wifi_connection.return_value = ("/path/net", {}) + + fire(wm, NMDeviceState.ACTIVATED) + + wm._conn_monitor.send_and_get_reply.assert_called_once() + wm._update_networks.assert_called_once() + + +# --------------------------------------------------------------------------- +# Thread races: _set_connecting on main thread vs _handle_state_change on monitor thread. +# Uses side_effect on the DBus mock to simulate _set_connecting running mid-handler. +# --------------------------------------------------------------------------- +# The deterministic fixes (skip DBus lookup when ssid already set, prev_state guard +# on NEED_AUTH) also shrink these race windows to near-zero. If races are still +# visible after, make WifiState frozen (replace() + single atomic assignment) and/or +# add a narrow lock around _wifi_state reads/writes (not around DBus calls). +# The NEED_AUTH prev_ssid mutation race is eliminated by removing prev_ssid entirely. + +class TestThreadRaces: + @pytest.mark.xfail(reason="TODO: PREPARE overwrites _set_connecting via stale DBus lookup") + def test_prepare_race_user_tap_during_dbus(self, mocker): + """User taps B while PREPARE's DBus call is in flight for auto-connect. + + Monitor thread reads wifi_state (ssid=None), starts DBus call. + Main thread: _set_connecting("B"). Monitor thread writes back stale ssid from DBus. + """ + wm = _make_wm(mocker, connections={"A": "/path/A", "B": "/path/B"}) + + def user_taps_b_during_dbus(*args, **kwargs): + wm._set_connecting("B") + return ("/path/A", {}) + + wm._get_active_wifi_connection.side_effect = user_taps_b_during_dbus + + fire(wm, NMDeviceState.PREPARE) + + assert wm._wifi_state.ssid == "B" + assert wm._wifi_state.status == ConnectStatus.CONNECTING + + @pytest.mark.xfail(reason="TODO: ACTIVATED overwrites _set_connecting with stale CONNECTED state") + def test_activated_race_user_tap_during_dbus(self, mocker): + """User taps B right as A finishes connecting (ACTIVATED handler running). + + Monitor thread reads wifi_state (A, CONNECTING), starts DBus call. + Main thread: _set_connecting("B"). Monitor thread writes (A, CONNECTED), losing B. + """ + wm = _make_wm(mocker, connections={"A": "/path/A", "B": "/path/B"}) + wm._set_connecting("A") + + def user_taps_b_during_dbus(*args, **kwargs): + wm._set_connecting("B") + return ("/path/A", {}) + + wm._get_active_wifi_connection.side_effect = user_taps_b_during_dbus + + fire(wm, NMDeviceState.ACTIVATED) + + assert wm._wifi_state.ssid == "B" + assert wm._wifi_state.status == ConnectStatus.CONNECTING + + +# --------------------------------------------------------------------------- +# Full sequences (NM signal order from real devices) +# --------------------------------------------------------------------------- + +class TestFullSequences: + def test_normal_connect(self, mocker): + """User connects to saved network: full happy path. + + Real device sequence (switching from another connected network): + DEACTIVATING(ACTIVATED, NEW_ACTIVATION) → DISCONNECTED(DEACTIVATING, NEW_ACTIVATION) + PREPARE → CONFIG → NEED_AUTH(CONFIG, NONE) → PREPARE(NEED_AUTH, NONE) → CONFIG + → IP_CONFIG → IP_CHECK → SECONDARIES → ACTIVATED + """ + wm = _make_wm(mocker, connections={"Home": "/path/home"}) + wm._get_active_wifi_connection.return_value = ("/path/home", {}) + + wm._set_connecting("Home") + fire(wm, NMDeviceState.PREPARE) + fire(wm, NMDeviceState.CONFIG) + fire(wm, NMDeviceState.NEED_AUTH) # WPA handshake (reason=NONE) + fire(wm, NMDeviceState.PREPARE, prev_state=NMDeviceState.NEED_AUTH) + fire(wm, NMDeviceState.CONFIG) + assert wm._wifi_state.status == ConnectStatus.CONNECTING + + fire(wm, NMDeviceState.IP_CONFIG) + fire(wm, NMDeviceState.IP_CHECK) + fire(wm, NMDeviceState.SECONDARIES) + fire(wm, NMDeviceState.ACTIVATED) + + assert wm._wifi_state.status == ConnectStatus.CONNECTED + assert wm._wifi_state.ssid == "Home" + + def test_wrong_password_then_retry(self, mocker): + """Wrong password → NEED_AUTH → FAILED → NM auto-reconnects to saved network. + + Real device sequence: + PREPARE → CONFIG → NEED_AUTH(CONFIG, NONE) ← WPA handshake + → PREPARE(NEED_AUTH, NONE) → CONFIG + → NEED_AUTH(CONFIG, SUPPLICANT_DISCONNECT) ← wrong password + → FAILED(NEED_AUTH, NO_SECRETS) ← NM gives up + → DISCONNECTED(FAILED, NONE) + → PREPARE → CONFIG → NEED_AUTH(CONFIG, NONE) → PREPARE ← auto-reconnect + → CONFIG → IP_CONFIG → ... → ACTIVATED + """ + wm = _make_wm(mocker, connections={"Sec": "/path/sec"}) + cb = mocker.MagicMock() + wm.add_callbacks(need_auth=cb) + + wm._set_connecting("Sec") + fire(wm, NMDeviceState.PREPARE) + fire(wm, NMDeviceState.CONFIG) + fire(wm, NMDeviceState.NEED_AUTH) # WPA handshake (reason=NONE) + fire(wm, NMDeviceState.PREPARE, prev_state=NMDeviceState.NEED_AUTH) + fire(wm, NMDeviceState.CONFIG) + + fire(wm, NMDeviceState.NEED_AUTH, prev_state=NMDeviceState.CONFIG, + reason=NMDeviceStateReason.SUPPLICANT_DISCONNECT) + assert wm._wifi_state.status == ConnectStatus.DISCONNECTED + assert len(wm._callback_queue) == 1 + + # FAILED(NO_SECRETS) follows but ssid is already cleared — no double-fire + fire(wm, NMDeviceState.FAILED, reason=NMDeviceStateReason.NO_SECRETS) + assert len(wm._callback_queue) == 1 + + fire(wm, NMDeviceState.DISCONNECTED, prev_state=NMDeviceState.FAILED) + + # Retry + wm._callback_queue.clear() + wm._set_connecting("Sec") + wm._get_active_wifi_connection.return_value = ("/path/sec", {}) + fire(wm, NMDeviceState.PREPARE) + fire(wm, NMDeviceState.CONFIG) + fire_wpa_connect(wm) + assert wm._wifi_state.status == ConnectStatus.CONNECTED + + def test_switch_saved_networks(self, mocker): + """Switch from A to B (both saved): NM signal sequence from real device. + + Real device sequence: + DEACTIVATING(ACTIVATED, NEW_ACTIVATION) → DISCONNECTED(DEACTIVATING, NEW_ACTIVATION) + → PREPARE → CONFIG → NEED_AUTH(CONFIG, NONE) → PREPARE(NEED_AUTH, NONE) → CONFIG + → IP_CONFIG → IP_CHECK → SECONDARIES → ACTIVATED + """ + wm = _make_wm(mocker, connections={"A": "/path/A", "B": "/path/B"}) + wm._wifi_state = WifiState(ssid="A", status=ConnectStatus.CONNECTED) + wm._get_active_wifi_connection.return_value = ("/path/B", {}) + + wm._set_connecting("B") + + fire(wm, NMDeviceState.DEACTIVATING, prev_state=NMDeviceState.ACTIVATED, + reason=NMDeviceStateReason.NEW_ACTIVATION) + fire(wm, NMDeviceState.DISCONNECTED, prev_state=NMDeviceState.DEACTIVATING, + reason=NMDeviceStateReason.NEW_ACTIVATION) + assert wm._wifi_state.ssid == "B" + assert wm._wifi_state.status == ConnectStatus.CONNECTING + + fire(wm, NMDeviceState.PREPARE) + fire(wm, NMDeviceState.CONFIG) + fire_wpa_connect(wm) + assert wm._wifi_state.status == ConnectStatus.CONNECTED + assert wm._wifi_state.ssid == "B" + + @pytest.mark.xfail(reason="TODO: interrupted auth from switching should not fire need_auth") + def test_rapid_switch_no_false_wrong_password(self, mocker): + """Switch A→B quickly: A's interrupted NEED_AUTH must NOT show wrong password. + + NOTE: The late NEED_AUTH(DISCONNECTED, SUPPLICANT_DISCONNECT) is common when rapidly + switching between networks with wrong/new passwords. Less common when switching between + saved networks with correct passwords. Not guaranteed — some switches skip it and go + straight from DISCONNECTED to PREPARE. The prev_state is consistently DISCONNECTED + for stale signals, so the prev_state guard reliably distinguishes them. + + Worst-case signal sequence this protects against: + DEACTIVATING(NEW_ACTIVATION) → DISCONNECTED(NEW_ACTIVATION) + → NEED_AUTH(DISCONNECTED, SUPPLICANT_DISCONNECT) ← A's stale auth failure + → PREPARE → CONFIG → ... → ACTIVATED ← B connects + """ + wm = _make_wm(mocker, connections={"A": "/path/A", "B": "/path/B"}) + cb = mocker.MagicMock() + wm.add_callbacks(need_auth=cb) + wm._wifi_state = WifiState(ssid="A", status=ConnectStatus.CONNECTED) + wm._get_active_wifi_connection.return_value = ("/path/B", {}) + + wm._set_connecting("B") + + fire(wm, NMDeviceState.DEACTIVATING, prev_state=NMDeviceState.ACTIVATED, + reason=NMDeviceStateReason.NEW_ACTIVATION) + fire(wm, NMDeviceState.DISCONNECTED, prev_state=NMDeviceState.DEACTIVATING, + reason=NMDeviceStateReason.NEW_ACTIVATION) + fire(wm, NMDeviceState.NEED_AUTH, prev_state=NMDeviceState.DISCONNECTED, + reason=NMDeviceStateReason.SUPPLICANT_DISCONNECT) + + assert wm._wifi_state.ssid == "B" + assert wm._wifi_state.status == ConnectStatus.CONNECTING + assert len(wm._callback_queue) == 0 + + fire(wm, NMDeviceState.PREPARE) + fire(wm, NMDeviceState.CONFIG) + fire_wpa_connect(wm) + assert wm._wifi_state.status == ConnectStatus.CONNECTED + + @pytest.mark.xfail(reason="TODO: forget A while connecting to B should not clear B") + def test_forget_A_connect_B(self, mocker): + """Forget A while connecting to B: full signal sequence. + + Real device sequence: + DEACTIVATING(ACTIVATED, CONNECTION_REMOVED) → DISCONNECTED(DEACTIVATING, CONNECTION_REMOVED) + → PREPARE → CONFIG → NEED_AUTH(CONFIG, NONE) → PREPARE(NEED_AUTH, NONE) → CONFIG + → IP_CONFIG → IP_CHECK → SECONDARIES → ACTIVATED + + Signal order: + 1. User: _set_connecting("B"), forget("A") removes A from _connections + 2. NewConnection for B arrives → _connections["B"] = ... + 3. DEACTIVATING(CONNECTION_REMOVED) — should be no-op + 4. DISCONNECTED(CONNECTION_REMOVED) — B is in _connections, must not clear + 5. PREPARE → CONFIG → NEED_AUTH → PREPARE → CONFIG → ... → ACTIVATED + """ + wm = _make_wm(mocker, connections={"A": "/path/A"}) + wm._wifi_state = WifiState(ssid="A", status=ConnectStatus.CONNECTED) + + wm._set_connecting("B") + del wm._connections["A"] + wm._connections["B"] = "/path/B" + + fire(wm, NMDeviceState.DEACTIVATING, prev_state=NMDeviceState.ACTIVATED, + reason=NMDeviceStateReason.CONNECTION_REMOVED) + assert wm._wifi_state.ssid == "B" + assert wm._wifi_state.status == ConnectStatus.CONNECTING + + fire(wm, NMDeviceState.DISCONNECTED, prev_state=NMDeviceState.DEACTIVATING, + reason=NMDeviceStateReason.CONNECTION_REMOVED) + assert wm._wifi_state.ssid == "B" + assert wm._wifi_state.status == ConnectStatus.CONNECTING + + wm._get_active_wifi_connection.return_value = ("/path/B", {}) + fire(wm, NMDeviceState.PREPARE) + fire(wm, NMDeviceState.CONFIG) + fire_wpa_connect(wm) + assert wm._wifi_state.status == ConnectStatus.CONNECTED + assert wm._wifi_state.ssid == "B" + + @pytest.mark.xfail(reason="TODO: forget A while connecting to B should not clear B") + def test_forget_A_connect_B_late_new_connection(self, mocker): + """Forget A, connect B: NewConnection for B arrives AFTER DISCONNECTED. + + This is the worst-case race: B isn't in _connections when DISCONNECTED fires, + so the guard can't protect it and state clears. PREPARE must recover by doing + the DBus lookup (ssid is None at that point). + + Signal order: + 1. User: _set_connecting("B"), forget("A") removes A from _connections + 2. DEACTIVATING(CONNECTION_REMOVED) — B NOT in _connections, should be no-op + 3. DISCONNECTED(CONNECTION_REMOVED) — B STILL NOT in _connections, clears state + 4. NewConnection for B arrives late → _connections["B"] = ... + 5. PREPARE (ssid=None, so DBus lookup recovers) → CONFIG → ACTIVATED + """ + wm = _make_wm(mocker, connections={"A": "/path/A"}) + wm._wifi_state = WifiState(ssid="A", status=ConnectStatus.CONNECTED) + + wm._set_connecting("B") + del wm._connections["A"] + + fire(wm, NMDeviceState.DEACTIVATING, prev_state=NMDeviceState.ACTIVATED, + reason=NMDeviceStateReason.CONNECTION_REMOVED) + assert wm._wifi_state.ssid == "B" + assert wm._wifi_state.status == ConnectStatus.CONNECTING + + fire(wm, NMDeviceState.DISCONNECTED, prev_state=NMDeviceState.DEACTIVATING, + reason=NMDeviceStateReason.CONNECTION_REMOVED) + # B not in _connections yet, so state clears — this is the known edge case + assert wm._wifi_state.ssid is None + assert wm._wifi_state.status == ConnectStatus.DISCONNECTED + + # NewConnection arrives late + wm._connections["B"] = "/path/B" + wm._get_active_wifi_connection.return_value = ("/path/B", {}) + + # PREPARE recovers: ssid is None so it looks up from DBus + fire(wm, NMDeviceState.PREPARE) + assert wm._wifi_state.ssid == "B" + assert wm._wifi_state.status == ConnectStatus.CONNECTING + + fire(wm, NMDeviceState.CONFIG) + fire_wpa_connect(wm) + assert wm._wifi_state.status == ConnectStatus.CONNECTED + assert wm._wifi_state.ssid == "B" + + def test_auto_connect(self, mocker): + """NM auto-connects (no user action, ssid starts None).""" + wm = _make_wm(mocker, connections={"AutoNet": "/path/auto"}) + wm._get_active_wifi_connection.return_value = ("/path/auto", {}) + + fire(wm, NMDeviceState.PREPARE) + assert wm._wifi_state.ssid == "AutoNet" + assert wm._wifi_state.status == ConnectStatus.CONNECTING + + fire(wm, NMDeviceState.CONFIG) + fire_wpa_connect(wm) + assert wm._wifi_state.status == ConnectStatus.CONNECTED + assert wm._wifi_state.ssid == "AutoNet" + + @pytest.mark.xfail(reason="TODO: FAILED(SSID_NOT_FOUND) should emit error for UI") + def test_ssid_not_found(self, mocker): + """Network drops off after connection starts. + + NM docs: SSID_NOT_FOUND (53) = "The WiFi network could not be found" + Expected sequence: PREPARE → CONFIG → FAILED(SSID_NOT_FOUND) → DISCONNECTED + + NOTE: SSID_NOT_FOUND is rare. On-device testing with a disappearing hotspot typically + produces FAILED(NO_SECRETS) instead. May be driver-specific or require the network + to vanish from scan results mid-connection. + + The UI error callback mechanism is intentionally deferred — for now just clear state. + """ + wm = _make_wm(mocker, connections={"GoneNet": "/path/gone"}) + cb = mocker.MagicMock() + wm.add_callbacks(need_auth=cb) + + wm._set_connecting("GoneNet") + fire(wm, NMDeviceState.PREPARE) + fire(wm, NMDeviceState.CONFIG) + fire(wm, NMDeviceState.FAILED, reason=NMDeviceStateReason.SSID_NOT_FOUND) + + assert wm._wifi_state.status == ConnectStatus.DISCONNECTED + assert wm._wifi_state.ssid is None + + def test_failed_then_disconnected_clears_state(self, mocker): + """After FAILED, NM always transitions to DISCONNECTED to clean up. + + NM docs: FAILED (120) = "failed to connect, cleaning up the connection request" + Full sequence: ... → FAILED(reason) → DISCONNECTED(NONE) + """ + wm = _make_wm(mocker) + wm._set_connecting("Net") + + fire(wm, NMDeviceState.FAILED, reason=NMDeviceStateReason.NONE) + assert wm._wifi_state.status == ConnectStatus.CONNECTING # FAILED(NONE) is a no-op + + fire(wm, NMDeviceState.DISCONNECTED, reason=NMDeviceStateReason.NONE) + assert wm._wifi_state.ssid is None + assert wm._wifi_state.status == ConnectStatus.DISCONNECTED + + def test_user_requested_disconnect(self, mocker): + """User explicitly disconnects from the network. + + NM docs: USER_REQUESTED (39) = "Device disconnected by user or client" + Expected sequence: DEACTIVATING(USER_REQUESTED) → DISCONNECTED(USER_REQUESTED) + """ + wm = _make_wm(mocker) + wm._wifi_state = WifiState(ssid="MyNet", status=ConnectStatus.CONNECTED) + + fire(wm, NMDeviceState.DEACTIVATING, reason=NMDeviceStateReason.USER_REQUESTED) + fire(wm, NMDeviceState.DISCONNECTED, reason=NMDeviceStateReason.USER_REQUESTED) + + assert wm._wifi_state.ssid is None + assert wm._wifi_state.status == ConnectStatus.DISCONNECTED diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index ed8d65bae9..5d6cd8a785 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -389,6 +389,21 @@ class WifiManager: # Fix: only do DBus lookup when wifi_state.ssid is None (auto-connections); # user-initiated connections already have ssid set via _set_connecting. + # TODO: Thread safety — _wifi_state is read/written by both the monitor thread (this + # handler) and the main thread (_set_connecting via connect/activate). The GIL makes + # individual assignments atomic, but read-then-write patterns can lose main thread writes: + # PREPARE/CONFIG: reads _wifi_state, does slow DBus call, writes back — if + # _set_connecting runs in between, the handler overwrites it with stale state. + # This is both a deterministic bug (stale DBus data) and a thread race. The + # deterministic fix (skip DBus lookup when ssid is set) also shrinks the race + # window to near-zero since the read/write happen back-to-back under the GIL. + # ACTIVATED: same read-then-write pattern with a DBus call in between. + # NEED_AUTH: mutates _wifi_state.prev_ssid in place, which can corrupt a new + # WifiState created by _set_connecting on the main thread. + # The deterministic fixes (skip DBus lookup when ssid set, prev_state guard) shrink + # the race windows significantly. If still visible, add a narrow lock around + # _wifi_state reads/writes (not around DBus calls, to avoid blocking the UI thread). + # TODO: Handle (FAILED, SSID_NOT_FOUND) and emit for ui to show error # Happens when network drops off after starting connection From 72ecc330e2083c2a6e639ff08d5404ae5e77f74f Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Wed, 25 Feb 2026 02:54:15 -0800 Subject: [PATCH 215/311] WifiManager: don't emit need auth for partially connected networks (#37397) * fix a few * document * now remove unused prev_ssid * more --- .../ui/lib/tests/test_handle_state_change.py | 27 ----------------- system/ui/lib/wifi_manager.py | 29 ++++++++----------- 2 files changed, 12 insertions(+), 44 deletions(-) diff --git a/system/ui/lib/tests/test_handle_state_change.py b/system/ui/lib/tests/test_handle_state_change.py index eefd83c628..9e7c39be55 100644 --- a/system/ui/lib/tests/test_handle_state_change.py +++ b/system/ui/lib/tests/test_handle_state_change.py @@ -221,17 +221,12 @@ class TestNeedAuth: assert len(wm._callback_queue) == 0 - @pytest.mark.xfail(reason="TODO: interrupted auth (prev=DISCONNECTED) should be ignored") def test_interrupted_auth_ignored(self, mocker): """Switching A->B: NEED_AUTH from A (prev=DISCONNECTED) must not fire callback. Reproduced on device: rapidly switching between two saved networks can trigger a rare false "wrong password" dialog for the previous network, even though both have correct passwords. The stale NEED_AUTH has prev_state=DISCONNECTED (not CONFIG). - - Fix: the handler's second param is currently `_` (unused). Rename to `prev_state` - and only fire the NEED_AUTH callback when prev_state indicates a real auth failure - (e.g. prev_state == CONFIG), not a stale signal from a prior connection. """ wm = _make_wm(mocker) cb = mocker.MagicMock() @@ -246,26 +241,6 @@ class TestNeedAuth: assert wm._wifi_state.status == ConnectStatus.CONNECTING assert len(wm._callback_queue) == 0 - def test_need_auth_targets_previous_ssid_via_prev_ssid(self, mocker): - """Switch A→B, late NEED_AUTH arrives: prev_ssid mechanism fires callback for A. - - This tests current prev_ssid behavior which we plan to remove. - Migration: (1) add prev_state guard to NEED_AUTH (see test_interrupted_auth_ignored), - (2) remove prev_ssid from WifiState and handler, (3) delete this test — it becomes - redundant with test_interrupted_auth_ignored once prev_state is the guard mechanism. - """ - wm = _make_wm(mocker) - cb = mocker.MagicMock() - wm.add_callbacks(need_auth=cb) - wm._set_connecting("A") - wm._set_connecting("B") - - fire(wm, NMDeviceState.NEED_AUTH, reason=NMDeviceStateReason.SUPPLICANT_DISCONNECT) - - assert len(wm._callback_queue) == 1 - wm.process_callbacks() - cb.assert_called_once_with("A") - class TestPassthroughStates: """NEED_AUTH (generic), IP_CONFIG, IP_CHECK, SECONDARIES, FAILED (generic) are no-ops.""" @@ -335,7 +310,6 @@ class TestActivated: # on NEED_AUTH) also shrink these race windows to near-zero. If races are still # visible after, make WifiState frozen (replace() + single atomic assignment) and/or # add a narrow lock around _wifi_state reads/writes (not around DBus calls). -# The NEED_AUTH prev_ssid mutation race is eliminated by removing prev_ssid entirely. class TestThreadRaces: @pytest.mark.xfail(reason="TODO: PREPARE overwrites _set_connecting via stale DBus lookup") @@ -482,7 +456,6 @@ class TestFullSequences: assert wm._wifi_state.status == ConnectStatus.CONNECTED assert wm._wifi_state.ssid == "B" - @pytest.mark.xfail(reason="TODO: interrupted auth from switching should not fire need_auth") def test_rapid_switch_no_false_wrong_password(self, mocker): """Switch A→B quickly: A's interrupted NEED_AUTH must NOT show wrong password. diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index 5d6cd8a785..4b5711428b 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -148,7 +148,6 @@ class ConnectStatus(IntEnum): @dataclass class WifiState: ssid: str | None = None - prev_ssid: str | None = None status: ConnectStatus = ConnectStatus.DISCONNECTED @@ -292,10 +291,7 @@ class WifiManager: return self._tethering_password def _set_connecting(self, ssid: str | None): - # Track prev ssid so late NEED_AUTH signals target the right network - self._wifi_state = WifiState(ssid=ssid, - prev_ssid=self.connecting_to_ssid if ssid is not None else None, - status=ConnectStatus.DISCONNECTED if ssid is None else ConnectStatus.CONNECTING) + self._wifi_state = WifiState(ssid=ssid, status=ConnectStatus.DISCONNECTED if ssid is None else ConnectStatus.CONNECTING) def _enqueue_callbacks(self, cbs: list[Callable], *args): for cb in cbs: @@ -377,7 +373,7 @@ class WifiManager: self._handle_state_change(new_state, previous_state, change_reason) - def _handle_state_change(self, new_state: int, _: int, change_reason: int): + def _handle_state_change(self, new_state: int, prev_state: int, change_reason: int): # TODO: known race conditions when switching networks (e.g. forget A, connect to B): # 1. DEACTIVATING/DISCONNECTED + CONNECTION_REMOVED: fires before NewConnection for B # arrives, so _set_connecting(None) clears B's CONNECTING state causing UI flicker. @@ -398,8 +394,6 @@ class WifiManager: # deterministic fix (skip DBus lookup when ssid is set) also shrinks the race # window to near-zero since the read/write happen back-to-back under the GIL. # ACTIVATED: same read-then-write pattern with a DBus call in between. - # NEED_AUTH: mutates _wifi_state.prev_ssid in place, which can corrupt a new - # WifiState created by _set_connecting on the main thread. # The deterministic fixes (skip DBus lookup when ssid set, prev_state guard) shrink # the race windows significantly. If still visible, add a narrow lock around # _wifi_state reads/writes (not around DBus calls, to avoid blocking the UI thread). @@ -424,18 +418,19 @@ class WifiManager: self._wifi_state = wifi_state - # BAD PASSWORD - use prev if current has already moved on to a new connection + # BAD PASSWORD # - strong network rejects with NEED_AUTH+SUPPLICANT_DISCONNECT # - weak/gone network fails with FAILED+NO_SECRETS - elif ((new_state == NMDeviceState.NEED_AUTH and change_reason == NMDeviceStateReason.SUPPLICANT_DISCONNECT) or + # prev_state guard: real auth failures come from CONFIG (supplicant handshake). + # Stale NEED_AUTH from a prior connection during network switching arrives with + # prev_state=DISCONNECTED and must be ignored to avoid a false wrong-password callback. + elif ((new_state == NMDeviceState.NEED_AUTH and change_reason == NMDeviceStateReason.SUPPLICANT_DISCONNECT + and prev_state == NMDeviceState.CONFIG) or (new_state == NMDeviceState.FAILED and change_reason == NMDeviceStateReason.NO_SECRETS)): - failed_ssid = self._wifi_state.prev_ssid or self._wifi_state.ssid - if failed_ssid: - self._enqueue_callbacks(self._need_auth, failed_ssid) - self._wifi_state.prev_ssid = None - if self._wifi_state.ssid == failed_ssid: - self._set_connecting(None) + if self._wifi_state.ssid: + self._enqueue_callbacks(self._need_auth, self._wifi_state.ssid) + self._set_connecting(None) elif new_state in (NMDeviceState.NEED_AUTH, NMDeviceState.IP_CONFIG, NMDeviceState.IP_CHECK, NMDeviceState.SECONDARIES, NMDeviceState.FAILED): @@ -443,7 +438,7 @@ class WifiManager: elif new_state == NMDeviceState.ACTIVATED: # Note that IP address from Ip4Config may not be propagated immediately and could take until the next scan results - wifi_state = replace(self._wifi_state, prev_ssid=None, status=ConnectStatus.CONNECTED) + wifi_state = replace(self._wifi_state, status=ConnectStatus.CONNECTED) conn_path, _ = self._get_active_wifi_connection(self._conn_monitor) if conn_path is None: From 238fca23348bb6f14453fb487fc210e3066b892e Mon Sep 17 00:00:00 2001 From: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com> Date: Wed, 25 Feb 2026 08:03:08 -0800 Subject: [PATCH 216/311] tools: fix darwin compile errors (#37399) --- tools/cabana/SConscript | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tools/cabana/SConscript b/tools/cabana/SConscript index 137e77d85b..89e69e7dd4 100644 --- a/tools/cabana/SConscript +++ b/tools/cabana/SConscript @@ -77,6 +77,9 @@ else: qt_libs = base_libs cabana_env = qt_env.Clone() +if arch == "Darwin": + cabana_env['CPPPATH'] += [f"{brew_prefix}/include"] + cabana_env['LIBPATH'] += [f"{brew_prefix}/lib"] cabana_libs = [cereal, messaging, visionipc, replay_lib, 'avformat', 'avcodec', 'swresample', 'avutil', 'x264', 'z', 'zstd', 'curl', 'yuv', 'usb-1.0'] + qt_libs opendbc_path = '-DOPENDBC_FILE_PATH=\'"%s"\'' % (cabana_env.Dir("../../opendbc/dbc").abspath) From f2c47494205c08408543260630846cd638308ee5 Mon Sep 17 00:00:00 2001 From: Alexandre Nobuharu Sato <66435071+AlexandreSato@users.noreply.github.com> Date: Wed, 25 Feb 2026 17:40:13 -0300 Subject: [PATCH 217/311] update docs (#37293) * update docs * Update paths for brand-specific safety files --- docs/README.md | 2 +- docs/car-porting/what-is-a-car-port.md | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/README.md b/docs/README.md index 08dd4fa8bc..12d0b6f5dd 100644 --- a/docs/README.md +++ b/docs/README.md @@ -8,7 +8,7 @@ NOTE: Those commands must be run in the root directory of openpilot, **not /docs **1. Install the docs dependencies** ``` bash -pip install .[docs] +uv pip install .[docs] ``` **2. Build the new site** diff --git a/docs/car-porting/what-is-a-car-port.md b/docs/car-porting/what-is-a-car-port.md index 55cce94da1..3480e4e5d5 100644 --- a/docs/car-porting/what-is-a-car-port.md +++ b/docs/car-porting/what-is-a-car-port.md @@ -21,10 +21,10 @@ Each car brand is supported by a standard interface structure in `opendbc/car/[b * `values.py`: Limits for actuation, general constants for cars, and supported car documentation * `radar_interface.py`: Interface for parsing radar points from the car, if applicable -## panda +## safety -* `board/safety/safety_[brand].h`: Brand-specific safety logic -* `tests/safety/test_[brand].py`: Brand-specific safety CI tests +* `opendbc_repo/opendbc/safety/modes/[brand].h`: Brand-specific safety logic +* `opendbc_repo/opendbc/safety/tests/test_[brand].py`: Brand-specific safety CI tests ## openpilot From 7835b9aa17d6cc0fa7ef694bbec54c6f57a632d1 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Wed, 25 Feb 2026 15:24:33 -0800 Subject: [PATCH 218/311] WifiManager: no need to update networks in as many places v2 (#37405) * debug * todo * clean up * clean up * fix test --- system/ui/lib/tests/test_handle_state_change.py | 6 ++++-- system/ui/lib/wifi_manager.py | 6 +++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/system/ui/lib/tests/test_handle_state_change.py b/system/ui/lib/tests/test_handle_state_change.py index 9e7c39be55..a714eab88b 100644 --- a/system/ui/lib/tests/test_handle_state_change.py +++ b/system/ui/lib/tests/test_handle_state_change.py @@ -25,6 +25,7 @@ def _make_wm(mocker: MockerFixture, connections=None): wm._need_auth = [] wm._activated = [] wm._update_networks = mocker.MagicMock() + wm._update_active_connection_info = mocker.MagicMock() wm._get_active_wifi_connection = mocker.MagicMock(return_value=(None, None)) return wm @@ -291,7 +292,7 @@ class TestActivated: assert wm._wifi_state.ssid == "MyNet" def test_activated_side_effects(self, mocker): - """ACTIVATED persists the volatile connection to disk and triggers _update_networks.""" + """ACTIVATED persists the volatile connection to disk and updates active connection info.""" wm = _make_wm(mocker, connections={"Net": "/path/net"}) wm._set_connecting("Net") wm._get_active_wifi_connection.return_value = ("/path/net", {}) @@ -299,7 +300,8 @@ class TestActivated: fire(wm, NMDeviceState.ACTIVATED) wm._conn_monitor.send_and_get_reply.assert_called_once() - wm._update_networks.assert_called_once() + wm._update_active_connection_info.assert_called_once() + wm._update_networks.assert_not_called() # --------------------------------------------------------------------------- diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index 4b5711428b..4433114b19 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -424,6 +424,7 @@ class WifiManager: # prev_state guard: real auth failures come from CONFIG (supplicant handshake). # Stale NEED_AUTH from a prior connection during network switching arrives with # prev_state=DISCONNECTED and must be ignored to avoid a false wrong-password callback. + # TODO: sometimes on PC it's observed no future signals are fired if mouse is held down blocking wrong password dialog elif ((new_state == NMDeviceState.NEED_AUTH and change_reason == NMDeviceStateReason.SUPPLICANT_DISCONNECT and prev_state == NMDeviceState.CONFIG) or (new_state == NMDeviceState.FAILED and change_reason == NMDeviceStateReason.NO_SECRETS)): @@ -445,12 +446,12 @@ class WifiManager: cloudlog.warning("Failed to get active wifi connection during ACTIVATED state") self._wifi_state = wifi_state self._enqueue_callbacks(self._activated) - self._update_networks() + self._update_active_connection_info() else: wifi_state.ssid = next((s for s, p in self._connections.items() if p == conn_path), None) self._wifi_state = wifi_state self._enqueue_callbacks(self._activated) - self._update_networks() + self._update_active_connection_info() # Persist volatile connections (created by AddAndActivateConnection2) to disk conn_addr = DBusAddress(conn_path, bus_name=NM, interface=NM_CONNECTION_IFACE) @@ -652,7 +653,6 @@ class WifiManager: conn_addr = DBusAddress(conn_path, bus_name=NM, interface=NM_CONNECTION_IFACE) self._router_main.send_and_get_reply(new_method_call(conn_addr, 'Delete')) - self._update_networks() self._enqueue_callbacks(self._forgotten, ssid) if block: From bcb4a6a3e36d2c36cb9164cec6fe50732f7961e2 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Wed, 25 Feb 2026 17:25:31 -0800 Subject: [PATCH 219/311] WifiManager: fix deterministic state mismatches (#37407) * hmm * revert to master * context too big * fresh context * early return * early return * tests * restore cmts * lester nester * note * add * final review * cmt --- .../ui/lib/tests/test_handle_state_change.py | 22 ++----- system/ui/lib/wifi_manager.py | 66 +++++++++---------- 2 files changed, 37 insertions(+), 51 deletions(-) diff --git a/system/ui/lib/tests/test_handle_state_change.py b/system/ui/lib/tests/test_handle_state_change.py index a714eab88b..66e0ff26ea 100644 --- a/system/ui/lib/tests/test_handle_state_change.py +++ b/system/ui/lib/tests/test_handle_state_change.py @@ -3,8 +3,8 @@ Tests the state machine in isolation by constructing a WifiManager with mocked DBus, then calling _handle_state_change directly with NM state transitions. -Many tests assert *desired* behavior that the current code doesn't implement yet. -These are marked with pytest.mark.xfail and document the intended fix. +Remaining xfail tests cover thread races (monitor vs main thread) and deferred +features (SSID_NOT_FOUND UI error). """ import pytest from pytest_mock import MockerFixture @@ -72,7 +72,6 @@ class TestDisconnected: assert wm._wifi_state.ssid == "OldNet" assert wm._wifi_state.status == ConnectStatus.CONNECTED - @pytest.mark.xfail(reason="TODO: CONNECTION_REMOVED should only clear if ssid not in _connections") def test_connection_removed_keeps_other_connecting(self, mocker): """Forget A while connecting to B: CONNECTION_REMOVED for A must not clear B.""" wm = _make_wm(mocker, connections={"B": "/path/B"}) @@ -95,12 +94,8 @@ class TestDisconnected: class TestDeactivating: - @pytest.mark.xfail(reason="TODO: DEACTIVATING should be a no-op") def test_deactivating_is_noop(self, mocker): - """DEACTIVATING should be a no-op — DISCONNECTED follows with correct state. - - Fix: remove the entire DEACTIVATING elif block — do nothing for any reason. - """ + """DEACTIVATING is a no-op — DISCONNECTED follows with the correct reason.""" wm = _make_wm(mocker) wm._wifi_state = WifiState(ssid="Net", status=ConnectStatus.CONNECTED) @@ -111,7 +106,6 @@ class TestDeactivating: class TestPrepareConfig: - @pytest.mark.xfail(reason="TODO: should skip DBus lookup when ssid already set") def test_user_initiated_skips_dbus_lookup(self, mocker): """User called _set_connecting('B') — PREPARE must not overwrite via DBus. @@ -309,9 +303,9 @@ class TestActivated: # Uses side_effect on the DBus mock to simulate _set_connecting running mid-handler. # --------------------------------------------------------------------------- # The deterministic fixes (skip DBus lookup when ssid already set, prev_state guard -# on NEED_AUTH) also shrink these race windows to near-zero. If races are still -# visible after, make WifiState frozen (replace() + single atomic assignment) and/or -# add a narrow lock around _wifi_state reads/writes (not around DBus calls). +# on NEED_AUTH, DEACTIVATING no-op, CONNECTION_REMOVED guard) shrink these race +# windows to near-zero. If still visible, make WifiState frozen (replace() + single +# atomic assignment) and/or add a narrow lock around _wifi_state reads/writes. class TestThreadRaces: @pytest.mark.xfail(reason="TODO: PREPARE overwrites _set_connecting via stale DBus lookup") @@ -496,7 +490,6 @@ class TestFullSequences: fire_wpa_connect(wm) assert wm._wifi_state.status == ConnectStatus.CONNECTED - @pytest.mark.xfail(reason="TODO: forget A while connecting to B should not clear B") def test_forget_A_connect_B(self, mocker): """Forget A while connecting to B: full signal sequence. @@ -508,7 +501,7 @@ class TestFullSequences: Signal order: 1. User: _set_connecting("B"), forget("A") removes A from _connections 2. NewConnection for B arrives → _connections["B"] = ... - 3. DEACTIVATING(CONNECTION_REMOVED) — should be no-op + 3. DEACTIVATING(CONNECTION_REMOVED) — no-op 4. DISCONNECTED(CONNECTION_REMOVED) — B is in _connections, must not clear 5. PREPARE → CONFIG → NEED_AUTH → PREPARE → CONFIG → ... → ACTIVATED """ @@ -536,7 +529,6 @@ class TestFullSequences: assert wm._wifi_state.status == ConnectStatus.CONNECTED assert wm._wifi_state.ssid == "B" - @pytest.mark.xfail(reason="TODO: forget A while connecting to B should not clear B") def test_forget_A_connect_B_late_new_connection(self, mocker): """Forget A, connect B: NewConnection for B arrives AFTER DISCONNECTED. diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index 4433114b19..67325fb479 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -374,40 +374,36 @@ class WifiManager: self._handle_state_change(new_state, previous_state, change_reason) def _handle_state_change(self, new_state: int, prev_state: int, change_reason: int): - # TODO: known race conditions when switching networks (e.g. forget A, connect to B): - # 1. DEACTIVATING/DISCONNECTED + CONNECTION_REMOVED: fires before NewConnection for B - # arrives, so _set_connecting(None) clears B's CONNECTING state causing UI flicker. - # DEACTIVATING(CONNECTION_REMOVED): wifi_state (B, CONNECTING) -> (None, DISCONNECTED) - # Fix: make DEACTIVATING a no-op, and guard DISCONNECTED with - # `if wifi_state.ssid not in _connections` (NewConnection arrives between the two). - # 2. PREPARE/CONFIG ssid lookup: DBus may return stale A's conn_path, overwriting B. - # PREPARE(0): wifi_state (B, CONNECTING) -> (A, CONNECTING) - # Fix: only do DBus lookup when wifi_state.ssid is None (auto-connections); - # user-initiated connections already have ssid set via _set_connecting. - # TODO: Thread safety — _wifi_state is read/written by both the monitor thread (this # handler) and the main thread (_set_connecting via connect/activate). The GIL makes - # individual assignments atomic, but read-then-write patterns can lose main thread writes: - # PREPARE/CONFIG: reads _wifi_state, does slow DBus call, writes back — if - # _set_connecting runs in between, the handler overwrites it with stale state. - # This is both a deterministic bug (stale DBus data) and a thread race. The - # deterministic fix (skip DBus lookup when ssid is set) also shrinks the race - # window to near-zero since the read/write happen back-to-back under the GIL. - # ACTIVATED: same read-then-write pattern with a DBus call in between. - # The deterministic fixes (skip DBus lookup when ssid set, prev_state guard) shrink - # the race windows significantly. If still visible, add a narrow lock around - # _wifi_state reads/writes (not around DBus calls, to avoid blocking the UI thread). + # individual assignments atomic, but ACTIVATED still has a read-then-write pattern with + # a DBus call in between: if _set_connecting runs mid-call, the handler overwrites it. + # The deterministic fixes (skip DBus lookup when ssid set, prev_state guard, DEACTIVATING + # no-op, CONNECTION_REMOVED guard) shrink the race windows significantly. If still + # visible, add a narrow lock around _wifi_state reads/writes (not around DBus calls). - # TODO: Handle (FAILED, SSID_NOT_FOUND) and emit for ui to show error + # TODO: Handle (FAILED, SSID_NOT_FOUND) and emit for UI to show error # Happens when network drops off after starting connection if new_state == NMDeviceState.DISCONNECTED: - if change_reason != NMDeviceStateReason.NEW_ACTIVATION: - # catches CONNECTION_REMOVED reason when connection is forgotten - self._set_connecting(None) + if change_reason == NMDeviceStateReason.NEW_ACTIVATION: + return + + # Guard: forget A while connecting to B fires CONNECTION_REMOVED. Don't clear B's state + # if B is still a known connection. If B hasn't arrived in _connections yet (late + # NewConnection), state clears here but PREPARE recovers via DBus lookup. + if (change_reason == NMDeviceStateReason.CONNECTION_REMOVED and self._wifi_state.ssid and + self._wifi_state.ssid in self._connections): + return + + self._set_connecting(None) elif new_state in (NMDeviceState.PREPARE, NMDeviceState.CONFIG): - # Set connecting status when NetworkManager connects to known networks on its own + if self._wifi_state.ssid is not None: + self._wifi_state = replace(self._wifi_state, status=ConnectStatus.CONNECTING) + return + + # Auto-connection when NetworkManager connects to known networks on its own (ssid=None): look up ssid from NM wifi_state = replace(self._wifi_state, status=ConnectStatus.CONNECTING) conn_path, _ = self._get_active_wifi_connection(self._conn_monitor) @@ -444,25 +440,23 @@ class WifiManager: conn_path, _ = self._get_active_wifi_connection(self._conn_monitor) if conn_path is None: cloudlog.warning("Failed to get active wifi connection during ACTIVATED state") - self._wifi_state = wifi_state - self._enqueue_callbacks(self._activated) - self._update_active_connection_info() else: wifi_state.ssid = next((s for s, p in self._connections.items() if p == conn_path), None) - self._wifi_state = wifi_state - self._enqueue_callbacks(self._activated) - self._update_active_connection_info() - # Persist volatile connections (created by AddAndActivateConnection2) to disk + self._wifi_state = wifi_state + self._enqueue_callbacks(self._activated) + self._update_active_connection_info() + + # Persist volatile connections (created by AddAndActivateConnection2) to disk + if conn_path is not None: conn_addr = DBusAddress(conn_path, bus_name=NM, interface=NM_CONNECTION_IFACE) save_reply = self._conn_monitor.send_and_get_reply(new_method_call(conn_addr, 'Save')) if save_reply.header.message_type == MessageType.error: cloudlog.warning(f"Failed to persist connection to disk: {save_reply}") elif new_state == NMDeviceState.DEACTIVATING: - if change_reason == NMDeviceStateReason.CONNECTION_REMOVED: - # When connection is forgotten - self._set_connecting(None) + # no-op — DISCONNECTED always follows with the correct reason + pass def _network_scanner(self): while not self._exit: From 1550520b631422fd1db19fcbb1832841ea0b828b Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Wed, 25 Feb 2026 18:41:28 -0800 Subject: [PATCH 220/311] WifiManager: connect/activate failure resets ssid (#37410) fix connect/activate failure resetting connected/connecting ssid --- .../ui/lib/tests/test_handle_state_change.py | 70 +++++++++++++++++++ system/ui/lib/wifi_manager.py | 12 ++-- 2 files changed, 78 insertions(+), 4 deletions(-) diff --git a/system/ui/lib/tests/test_handle_state_change.py b/system/ui/lib/tests/test_handle_state_change.py index 66e0ff26ea..a6a6240149 100644 --- a/system/ui/lib/tests/test_handle_state_change.py +++ b/system/ui/lib/tests/test_handle_state_change.py @@ -7,6 +7,7 @@ Remaining xfail tests cover thread races (monitor vs main thread) and deferred features (SSID_NOT_FOUND UI error). """ import pytest +from jeepney.low_level import MessageType from pytest_mock import MockerFixture from openpilot.system.ui.lib.networkmanager import NMDeviceState, NMDeviceStateReason @@ -643,3 +644,72 @@ class TestFullSequences: assert wm._wifi_state.ssid is None assert wm._wifi_state.status == ConnectStatus.DISCONNECTED + + +# --------------------------------------------------------------------------- +# Worker error recovery: DBus errors in activate/connect re-sync with NM +# --------------------------------------------------------------------------- +# Verified on device: when ActivateConnection returns UnknownConnection error, +# NM emits no state signals. The worker error path is the only recovery point. + +class TestWorkerErrorRecovery: + """Worker threads re-sync with NM via _init_wifi_state on DBus errors, + preserving actual NM state instead of blindly clearing to DISCONNECTED.""" + + def _mock_init_restores(self, wm, mocker, ssid, status): + """Replace _init_wifi_state with a mock that simulates NM reporting the given state.""" + mock = mocker.MagicMock( + side_effect=lambda: setattr(wm, '_wifi_state', WifiState(ssid=ssid, status=status)) + ) + wm._init_wifi_state = mock + return mock + + def test_activate_dbus_error_resyncs(self, mocker): + """ActivateConnection returns DBus error while A is connected. + NM rejects the request — no state signals emitted. Worker must re-read NM + state to discover A is still connected, not clear to DISCONNECTED. + """ + wm = _make_wm(mocker, connections={"A": "/path/A", "B": "/path/B"}) + wm._wifi_device = "/dev/wifi0" + wm._nm = mocker.MagicMock() + wm._wifi_state = WifiState(ssid="A", status=ConnectStatus.CONNECTED) + wm._router_main = mocker.MagicMock() + + error_reply = mocker.MagicMock() + error_reply.header.message_type = MessageType.error + wm._router_main.send_and_get_reply.return_value = error_reply + + mock_init = self._mock_init_restores(wm, mocker, "A", ConnectStatus.CONNECTED) + + wm.activate_connection("B", block=True) + + mock_init.assert_called_once() + assert wm._wifi_state.ssid == "A" + assert wm._wifi_state.status == ConnectStatus.CONNECTED + + def test_connect_to_network_dbus_error_resyncs(self, mocker): + """AddAndActivateConnection2 returns DBus error while A is connected.""" + wm = _make_wm(mocker, connections={"A": "/path/A"}) + wm._wifi_device = "/dev/wifi0" + wm._nm = mocker.MagicMock() + wm._wifi_state = WifiState(ssid="A", status=ConnectStatus.CONNECTED) + wm._router_main = mocker.MagicMock() + wm._forgotten = [] + + error_reply = mocker.MagicMock() + error_reply.header.message_type = MessageType.error + wm._router_main.send_and_get_reply.return_value = error_reply + + mock_init = self._mock_init_restores(wm, mocker, "A", ConnectStatus.CONNECTED) + + # Run worker thread synchronously + workers = [] + mocker.patch('openpilot.system.ui.lib.wifi_manager.threading.Thread', + side_effect=lambda target, **kw: type('T', (), {'start': lambda self: workers.append(target)})()) + + wm.connect_to_network("B", "password123") + workers[-1]() + + mock_init.assert_called_once() + assert wm._wifi_state.ssid == "A" + assert wm._wifi_state.status == ConnectStatus.CONNECTED diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index 67325fb479..7c7f2d8280 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -626,7 +626,8 @@ class WifiManager: # Persisted to disk on ACTIVATED via Save() if self._wifi_device is None: cloudlog.warning("No WiFi device found") - self._set_connecting(None) + # TODO: expose a failed connection state in the UI + self._init_wifi_state() return reply = self._router_main.send_and_get_reply(new_method_call(self._nm, 'AddAndActivateConnection2', 'a{sa{sv}}ooa{sv}', @@ -634,7 +635,8 @@ class WifiManager: if reply.header.message_type == MessageType.error: cloudlog.warning(f"Failed to add and activate connection for {ssid}: {reply}") - self._set_connecting(None) + # TODO: expose a failed connection state in the UI + self._init_wifi_state() threading.Thread(target=worker, daemon=True).start() @@ -661,7 +663,8 @@ class WifiManager: conn_path = self._connections.get(ssid, None) if conn_path is None or self._wifi_device is None: cloudlog.warning(f"Failed to activate connection for {ssid}: conn_path={conn_path}, wifi_device={self._wifi_device}") - self._set_connecting(None) + # TODO: expose a failed connection state in the UI + self._init_wifi_state() return reply = self._router_main.send_and_get_reply(new_method_call(self._nm, 'ActivateConnection', 'ooo', @@ -669,7 +672,8 @@ class WifiManager: if reply.header.message_type == MessageType.error: cloudlog.warning(f"Failed to activate connection for {ssid}: {reply}") - self._set_connecting(None) + # TODO: expose a failed connection state in the UI + self._init_wifi_state() if block: worker() From c2a7437972fa25a5046673b54daf32b5dabdf727 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Wed, 25 Feb 2026 19:09:11 -0800 Subject: [PATCH 221/311] WifiManager: fix some threading race conditions (#37406) * interesting epoch approach * repro * determ fix * cmts * new issue * test * clean up * cmt * add back * reorg cmt * cmt * clean up * cmt --- .../ui/lib/tests/test_handle_state_change.py | 11 ++---- system/ui/lib/wifi_manager.py | 37 ++++++++++++++----- 2 files changed, 31 insertions(+), 17 deletions(-) diff --git a/system/ui/lib/tests/test_handle_state_change.py b/system/ui/lib/tests/test_handle_state_change.py index a6a6240149..1365b84a83 100644 --- a/system/ui/lib/tests/test_handle_state_change.py +++ b/system/ui/lib/tests/test_handle_state_change.py @@ -2,9 +2,6 @@ Tests the state machine in isolation by constructing a WifiManager with mocked DBus, then calling _handle_state_change directly with NM state transitions. - -Remaining xfail tests cover thread races (monitor vs main thread) and deferred -features (SSID_NOT_FOUND UI error). """ import pytest from jeepney.low_level import MessageType @@ -22,6 +19,7 @@ def _make_wm(mocker: MockerFixture, connections=None): wm._conn_monitor = mocker.MagicMock() wm._connections = dict(connections or {}) wm._wifi_state = WifiState() + wm._user_epoch = 0 wm._callback_queue = [] wm._need_auth = [] wm._activated = [] @@ -302,14 +300,14 @@ class TestActivated: # --------------------------------------------------------------------------- # Thread races: _set_connecting on main thread vs _handle_state_change on monitor thread. # Uses side_effect on the DBus mock to simulate _set_connecting running mid-handler. +# The epoch counter detects that a user action occurred during the slow DBus call +# and discards the stale update. # --------------------------------------------------------------------------- # The deterministic fixes (skip DBus lookup when ssid already set, prev_state guard # on NEED_AUTH, DEACTIVATING no-op, CONNECTION_REMOVED guard) shrink these race -# windows to near-zero. If still visible, make WifiState frozen (replace() + single -# atomic assignment) and/or add a narrow lock around _wifi_state reads/writes. +# windows significantly. The epoch counter closes the remaining gaps. class TestThreadRaces: - @pytest.mark.xfail(reason="TODO: PREPARE overwrites _set_connecting via stale DBus lookup") def test_prepare_race_user_tap_during_dbus(self, mocker): """User taps B while PREPARE's DBus call is in flight for auto-connect. @@ -329,7 +327,6 @@ class TestThreadRaces: assert wm._wifi_state.ssid == "B" assert wm._wifi_state.status == ConnectStatus.CONNECTING - @pytest.mark.xfail(reason="TODO: ACTIVATED overwrites _set_connecting with stale CONNECTED state") def test_activated_race_user_tap_during_dbus(self, mocker): """User taps B right as A finishes connecting (ACTIVATED handler running). diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index 7c7f2d8280..81c4ec0515 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -175,6 +175,7 @@ class WifiManager: # State self._connections: dict[str, str] = {} # ssid -> connection path, updated via NM signals self._wifi_state: WifiState = WifiState() + self._user_epoch: int = 0 self._ipv4_address: str = "" self._current_network_metered: MeteredType = MeteredType.UNKNOWN self._tethering_password: str = "" @@ -291,6 +292,8 @@ class WifiManager: return self._tethering_password def _set_connecting(self, ssid: str | None): + # Called by user action, or sequentially from state change handler + self._user_epoch += 1 self._wifi_state = WifiState(ssid=ssid, status=ConnectStatus.DISCONNECTED if ssid is None else ConnectStatus.CONNECTING) def _enqueue_callbacks(self, cbs: list[Callable], *args): @@ -374,13 +377,16 @@ class WifiManager: self._handle_state_change(new_state, previous_state, change_reason) def _handle_state_change(self, new_state: int, prev_state: int, change_reason: int): - # TODO: Thread safety — _wifi_state is read/written by both the monitor thread (this - # handler) and the main thread (_set_connecting via connect/activate). The GIL makes - # individual assignments atomic, but ACTIVATED still has a read-then-write pattern with - # a DBus call in between: if _set_connecting runs mid-call, the handler overwrites it. - # The deterministic fixes (skip DBus lookup when ssid set, prev_state guard, DEACTIVATING - # no-op, CONNECTION_REMOVED guard) shrink the race windows significantly. If still - # visible, add a narrow lock around _wifi_state reads/writes (not around DBus calls). + # Thread safety: _wifi_state is read/written by both the monitor thread (this handler) + # and the main thread (_set_connecting via connect/activate). PREPARE/CONFIG and ACTIVATED + # have a read-then-write pattern with a slow DBus call in between — if _set_connecting + # runs mid-call, the handler would overwrite the user's newer state with stale data. + # + # The _user_epoch counter solves this without locks. _set_connecting increments the epoch + # on every user action. Handlers snapshot the epoch before their DBus call and compare + # after: if it changed, a user action occurred during the call and the stale result is + # discarded. Combined with deterministic fixes (skip DBus lookup when ssid already set, + # DEACTIVATING no-op, CONNECTION_REMOVED guard), all known race windows are closed. # TODO: Handle (FAILED, SSID_NOT_FOUND) and emit for UI to show error # Happens when network drops off after starting connection @@ -399,6 +405,8 @@ class WifiManager: self._set_connecting(None) elif new_state in (NMDeviceState.PREPARE, NMDeviceState.CONFIG): + epoch = self._user_epoch + if self._wifi_state.ssid is not None: self._wifi_state = replace(self._wifi_state, status=ConnectStatus.CONNECTING) return @@ -407,6 +415,10 @@ class WifiManager: wifi_state = replace(self._wifi_state, status=ConnectStatus.CONNECTING) conn_path, _ = self._get_active_wifi_connection(self._conn_monitor) + + if self._user_epoch != epoch: + return + if conn_path is None: cloudlog.warning("Failed to get active wifi connection during PREPARE/CONFIG state") else: @@ -417,14 +429,14 @@ class WifiManager: # BAD PASSWORD # - strong network rejects with NEED_AUTH+SUPPLICANT_DISCONNECT # - weak/gone network fails with FAILED+NO_SECRETS - # prev_state guard: real auth failures come from CONFIG (supplicant handshake). - # Stale NEED_AUTH from a prior connection during network switching arrives with - # prev_state=DISCONNECTED and must be ignored to avoid a false wrong-password callback. # TODO: sometimes on PC it's observed no future signals are fired if mouse is held down blocking wrong password dialog elif ((new_state == NMDeviceState.NEED_AUTH and change_reason == NMDeviceStateReason.SUPPLICANT_DISCONNECT and prev_state == NMDeviceState.CONFIG) or (new_state == NMDeviceState.FAILED and change_reason == NMDeviceStateReason.NO_SECRETS)): + # prev_state guard: real auth failures come from CONFIG (supplicant handshake). + # Stale NEED_AUTH from a prior connection during network switching arrives with + # prev_state=DISCONNECTED and must be ignored to avoid a false wrong-password callback. if self._wifi_state.ssid: self._enqueue_callbacks(self._need_auth, self._wifi_state.ssid) self._set_connecting(None) @@ -435,9 +447,14 @@ class WifiManager: elif new_state == NMDeviceState.ACTIVATED: # Note that IP address from Ip4Config may not be propagated immediately and could take until the next scan results + epoch = self._user_epoch wifi_state = replace(self._wifi_state, status=ConnectStatus.CONNECTED) conn_path, _ = self._get_active_wifi_connection(self._conn_monitor) + + if self._user_epoch != epoch: + return + if conn_path is None: cloudlog.warning("Failed to get active wifi connection during ACTIVATED state") else: From 5c630b20a9ce75975170f5dc9eb65b63957d91f0 Mon Sep 17 00:00:00 2001 From: Robbe Derks Date: Wed, 25 Feb 2026 19:29:55 -0800 Subject: [PATCH 222/311] panda sound output level (#37408) parse sound output level --- cereal/log.capnp | 1 + panda | 2 +- selfdrive/pandad/pandad.cc | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/cereal/log.capnp b/cereal/log.capnp index 119cf29999..80a604d860 100644 --- a/cereal/log.capnp +++ b/cereal/log.capnp @@ -592,6 +592,7 @@ struct PandaState @0xa7649e2575e4591e { harnessStatus @21 :HarnessStatus; sbu1Voltage @35 :Float32; sbu2Voltage @36 :Float32; + soundOutputLevel @37 :UInt16; # can health canState0 @29 :PandaCanState; diff --git a/panda b/panda index 49f72e931f..3ffe9591a7 160000 --- a/panda +++ b/panda @@ -1 +1 @@ -Subproject commit 49f72e931f09ceecb00c0c7937808fcaeecd3c17 +Subproject commit 3ffe9591a7305c71f67a70355f8098c9b5d2a611 diff --git a/selfdrive/pandad/pandad.cc b/selfdrive/pandad/pandad.cc index d048dbd0c3..28d459f458 100644 --- a/selfdrive/pandad/pandad.cc +++ b/selfdrive/pandad/pandad.cc @@ -130,6 +130,7 @@ void fill_panda_state(cereal::PandaState::Builder &ps, cereal::PandaState::Panda ps.setSpiErrorCount(health.spi_error_count_pkt); ps.setSbu1Voltage(health.sbu1_voltage_mV / 1000.0f); ps.setSbu2Voltage(health.sbu2_voltage_mV / 1000.0f); + ps.setSoundOutputLevel(health.sound_output_level_pkt); } void fill_panda_can_state(cereal::PandaState::PandaCanState::Builder &cs, const can_health_t &can_health) { From 496ae85f6763757b0f122400da408c45a99ab3b3 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Wed, 25 Feb 2026 19:30:02 -0800 Subject: [PATCH 223/311] WifiManager: guard init_wifi_state (#37413) * failing test * fix * rename * better --- .../ui/lib/tests/test_handle_state_change.py | 27 +++++++++++++++++++ system/ui/lib/wifi_manager.py | 16 ++++++++--- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/system/ui/lib/tests/test_handle_state_change.py b/system/ui/lib/tests/test_handle_state_change.py index 1365b84a83..30d8385428 100644 --- a/system/ui/lib/tests/test_handle_state_change.py +++ b/system/ui/lib/tests/test_handle_state_change.py @@ -347,6 +347,33 @@ class TestThreadRaces: assert wm._wifi_state.ssid == "B" assert wm._wifi_state.status == ConnectStatus.CONNECTING + def test_init_wifi_state_race_user_tap_during_dbus(self, mocker): + """User taps B while _init_wifi_state's DBus calls are in flight. + + _init_wifi_state runs from set_active(True) or worker error paths. It does + 2 DBus calls (device State property + _get_active_wifi_connection) then + unconditionally writes _wifi_state. If the user taps a network during those + calls, _set_connecting("B") is overwritten with stale NM ground truth. + """ + wm = _make_wm(mocker, connections={"A": "/path/A", "B": "/path/B"}) + wm._wifi_device = "/dev/wifi0" + wm._router_main = mocker.MagicMock() + + state_reply = mocker.MagicMock() + state_reply.body = [('u', NMDeviceState.ACTIVATED)] + wm._router_main.send_and_get_reply.return_value = state_reply + + def user_taps_b_during_dbus(*args, **kwargs): + wm._set_connecting("B") + return ("/path/A", {}) + + wm._get_active_wifi_connection.side_effect = user_taps_b_during_dbus + + wm._init_wifi_state() + + assert wm._wifi_state.ssid == "B" + assert wm._wifi_state.status == ConnectStatus.CONNECTING + # --------------------------------------------------------------------------- # Full sequences (NM signal order from real devices) diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index 81c4ec0515..6e39fdf523 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -197,7 +197,7 @@ class WifiManager: self._networks_updated: list[Callable[[list[Network]], None]] = [] self._disconnected: list[Callable[[], None]] = [] - self._lock = threading.Lock() + self._scan_lock = threading.Lock() self._scan_thread = threading.Thread(target=self._network_scanner, daemon=True) self._state_thread = threading.Thread(target=self._monitor_state, daemon=True) self._initialize() @@ -227,6 +227,8 @@ class WifiManager: cloudlog.warning("No WiFi device found") return + epoch = self._user_epoch + dev_addr = DBusAddress(self._wifi_device, bus_name=NM, interface=NM_DEVICE_IFACE) dev_state = self._router_main.send_and_get_reply(Properties(dev_addr).get('State')).body[0][1] @@ -239,6 +241,10 @@ class WifiManager: conn_path, _ = self._get_active_wifi_connection() if conn_path: wifi_state.ssid = next((s for s, p in self._connections.items() if p == conn_path), None) + + if self._user_epoch != epoch: + return + self._wifi_state = wifi_state if block: @@ -281,11 +287,13 @@ class WifiManager: @property def connecting_to_ssid(self) -> str | None: - return self._wifi_state.ssid if self._wifi_state.status == ConnectStatus.CONNECTING else None + wifi_state = self._wifi_state + return wifi_state.ssid if wifi_state.status == ConnectStatus.CONNECTING else None @property def connected_ssid(self) -> str | None: - return self._wifi_state.ssid if self._wifi_state.status == ConnectStatus.CONNECTED else None + wifi_state = self._wifi_state + return wifi_state.ssid if wifi_state.status == ConnectStatus.CONNECTED else None @property def tethering_password(self) -> str: @@ -822,7 +830,7 @@ class WifiManager: return def worker(): - with self._lock: + with self._scan_lock: if self._wifi_device is None: cloudlog.warning("No WiFi device found") return From 561c490b2ad04727fce7774746b28061b4660ac5 Mon Sep 17 00:00:00 2001 From: Daniel Koepping Date: Wed, 25 Feb 2026 20:32:44 -0800 Subject: [PATCH 224/311] Replay: keep ref history (#37357) keep history --- .github/workflows/tests.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index fa327ea74b..f95fe7d2e2 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -162,15 +162,15 @@ jobs: if: github.repository == 'commaai/openpilot' && github.ref == 'refs/heads/master' working-directory: ${{ github.workspace }}/ci-artifacts run: | - git checkout --orphan process-replay - git rm -rf . git config user.name "GitHub Actions Bot" git config user.email "<>" + git fetch origin process-replay || true + git checkout process-replay 2>/dev/null || git checkout --orphan process-replay cp ${{ github.workspace }}/selfdrive/test/process_replay/fakedata/*.zst . echo "${{ github.sha }}" > ref_commit git add . - git commit -m "process-replay refs for ${{ github.repository }}@${{ github.sha }}" - git push origin process-replay --force + git commit -m "process-replay refs for ${{ github.repository }}@${{ github.sha }}" || echo "No changes to commit" + git push origin process-replay - name: Run regen if: false timeout-minutes: 4 From cf5ae3cbca1b3cda30832cd681aa59e9f7cd659e Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 26 Feb 2026 01:10:35 -0800 Subject: [PATCH 225/311] WifiManager: fix connect flash while forgetting (#37416) * real traces for some tests combine and new test for low strength/turn off hotspot while connecting revert wifiui * stupid llm * clean up --- system/ui/lib/networkmanager.py | 1 + .../ui/lib/tests/test_handle_state_change.py | 197 ++++++++++++++++-- system/ui/lib/wifi_manager.py | 11 +- 3 files changed, 191 insertions(+), 18 deletions(-) diff --git a/system/ui/lib/networkmanager.py b/system/ui/lib/networkmanager.py index c0e9fd289a..d2d6b30b10 100644 --- a/system/ui/lib/networkmanager.py +++ b/system/ui/lib/networkmanager.py @@ -26,6 +26,7 @@ class NMDeviceStateReason(IntEnum): IP_CONFIG_UNAVAILABLE = 5 NO_SECRETS = 7 SUPPLICANT_DISCONNECT = 8 + SUPPLICANT_TIMEOUT = 11 CONNECTION_REMOVED = 38 USER_REQUESTED = 39 SSID_NOT_FOUND = 53 diff --git a/system/ui/lib/tests/test_handle_state_change.py b/system/ui/lib/tests/test_handle_state_change.py index 30d8385428..69aae6fdf3 100644 --- a/system/ui/lib/tests/test_handle_state_change.py +++ b/system/ui/lib/tests/test_handle_state_change.py @@ -93,16 +93,42 @@ class TestDisconnected: class TestDeactivating: - def test_deactivating_is_noop(self, mocker): - """DEACTIVATING is a no-op — DISCONNECTED follows with the correct reason.""" + def test_deactivating_noop_for_non_connection_removed(self, mocker): + """DEACTIVATING with non-CONNECTION_REMOVED reason is a no-op.""" wm = _make_wm(mocker) wm._wifi_state = WifiState(ssid="Net", status=ConnectStatus.CONNECTED) - fire(wm, NMDeviceState.DEACTIVATING, reason=NMDeviceStateReason.CONNECTION_REMOVED) + fire(wm, NMDeviceState.DEACTIVATING, reason=NMDeviceStateReason.USER_REQUESTED) assert wm._wifi_state.ssid == "Net" assert wm._wifi_state.status == ConnectStatus.CONNECTED + @pytest.mark.parametrize("status, expected_clears", [ + (ConnectStatus.CONNECTED, True), + (ConnectStatus.CONNECTING, False), + ]) + def test_deactivating_connection_removed(self, mocker, status, expected_clears): + """DEACTIVATING(CONNECTION_REMOVED) clears CONNECTED but preserves CONNECTING. + + CONNECTED: forgetting the current network. The forgotten callback fires between + DEACTIVATING and DISCONNECTED — must clear here so the UI doesn't flash "connected" + after the eager _network_forgetting flag resets. + + CONNECTING: forget A while connecting to B. DEACTIVATING fires for A's removal, + but B's CONNECTING state must be preserved. + """ + wm = _make_wm(mocker, connections={"B": "/path/B"}) + wm._wifi_state = WifiState(ssid="B" if status == ConnectStatus.CONNECTING else "A", status=status) + + fire(wm, NMDeviceState.DEACTIVATING, reason=NMDeviceStateReason.CONNECTION_REMOVED) + + if expected_clears: + assert wm._wifi_state.ssid is None + assert wm._wifi_state.status == ConnectStatus.DISCONNECTED + else: + assert wm._wifi_state.ssid == "B" + assert wm._wifi_state.status == ConnectStatus.CONNECTING + class TestPrepareConfig: def test_user_initiated_skips_dbus_lookup(self, mocker): @@ -170,7 +196,17 @@ class TestNeedAuth: cb.assert_called_once_with("SecNet") def test_failed_no_secrets_fires_callback(self, mocker): - """FAILED+NO_SECRETS = wrong password (weak/gone network).""" + """FAILED+NO_SECRETS = wrong password (weak/gone network). + + Confirmed on device: also fires when a hotspot turns off during connection. + NM can't complete the WPA handshake (AP vanished) and reports NO_SECRETS + rather than SSID_NOT_FOUND. The need_auth callback fires, so the UI shows + "wrong password" — a false positive, but same signal path. + + Real device sequence (new connection, hotspot turned off immediately): + PREPARE → CONFIG → NEED_AUTH(CONFIG, NONE) → PREPARE(NEED_AUTH) → CONFIG + → NEED_AUTH(CONFIG, NONE) → FAILED(NEED_AUTH, NO_SECRETS) → DISCONNECTED(FAILED, NONE) + """ wm = _make_wm(mocker) cb = mocker.MagicMock() wm.add_callbacks(need_auth=cb) @@ -304,8 +340,9 @@ class TestActivated: # and discards the stale update. # --------------------------------------------------------------------------- # The deterministic fixes (skip DBus lookup when ssid already set, prev_state guard -# on NEED_AUTH, DEACTIVATING no-op, CONNECTION_REMOVED guard) shrink these race -# windows significantly. The epoch counter closes the remaining gaps. +# on NEED_AUTH, DEACTIVATING clears CONNECTED on CONNECTION_REMOVED, CONNECTION_REMOVED +# guard) shrink these race windows significantly. The epoch counter closes the +# remaining gaps. class TestThreadRaces: def test_prepare_race_user_tap_during_dbus(self, mocker): @@ -410,14 +447,17 @@ class TestFullSequences: def test_wrong_password_then_retry(self, mocker): """Wrong password → NEED_AUTH → FAILED → NM auto-reconnects to saved network. - Real device sequence: - PREPARE → CONFIG → NEED_AUTH(CONFIG, NONE) ← WPA handshake + Confirmed on device: wrong password for Shane's iPhone, NM auto-connected to unifi. + + Real device sequence (switching from a connected network): + DEACTIVATING(ACTIVATED, NEW_ACTIVATION) → DISCONNECTED(DEACTIVATING, NEW_ACTIVATION) + → PREPARE → CONFIG → NEED_AUTH(CONFIG, NONE) ← WPA handshake → PREPARE(NEED_AUTH, NONE) → CONFIG → NEED_AUTH(CONFIG, SUPPLICANT_DISCONNECT) ← wrong password → FAILED(NEED_AUTH, NO_SECRETS) ← NM gives up → DISCONNECTED(FAILED, NONE) - → PREPARE → CONFIG → NEED_AUTH(CONFIG, NONE) → PREPARE ← auto-reconnect - → CONFIG → IP_CONFIG → ... → ACTIVATED + → PREPARE → CONFIG → NEED_AUTH(CONFIG, NONE) → PREPARE(NEED_AUTH) → CONFIG + → IP_CONFIG → IP_CHECK → SECONDARIES → ACTIVATED ← auto-reconnect to other saved network """ wm = _make_wm(mocker, connections={"Sec": "/path/sec"}) cb = mocker.MagicMock() @@ -515,6 +555,80 @@ class TestFullSequences: fire_wpa_connect(wm) assert wm._wifi_state.status == ConnectStatus.CONNECTED + def test_forget_while_connecting(self, mocker): + """Forget the network we're currently connecting to (not yet ACTIVATED). + + Confirmed on device: connected to unifi, tapped Shane's iPhone, then forgot + Shane's iPhone while at CONFIG. NM auto-connected to unifi afterward. + + Real device sequence (switching then forgetting mid-connection): + DEACTIVATING(ACTIVATED, NEW_ACTIVATION) → DISCONNECTED(DEACTIVATING, NEW_ACTIVATION) + → PREPARE → CONFIG → NEED_AUTH(CONFIG, NONE) → PREPARE(NEED_AUTH) → CONFIG + → DEACTIVATING(CONFIG, CONNECTION_REMOVED) ← forget at CONFIG + → DISCONNECTED(DEACTIVATING, CONNECTION_REMOVED) + → PREPARE → CONFIG → ... → ACTIVATED ← NM auto-connects to other saved network + + Note: DEACTIVATING fires from CONFIG (not ACTIVATED). wifi_state.status is + CONNECTING, so the DEACTIVATING handler is a no-op. DISCONNECTED clears state + (ssid removed from _connections by ConnectionRemoved), then PREPARE recovers + via DBus lookup for the auto-connect. + """ + wm = _make_wm(mocker, connections={"A": "/path/A", "Other": "/path/other"}) + wm._get_active_wifi_connection.return_value = ("/path/other", {}) + + wm._set_connecting("A") + + fire(wm, NMDeviceState.PREPARE) + fire(wm, NMDeviceState.CONFIG) + assert wm._wifi_state.ssid == "A" + assert wm._wifi_state.status == ConnectStatus.CONNECTING + + # User forgets A: ConnectionRemoved processed first, then state changes + del wm._connections["A"] + + fire(wm, NMDeviceState.DEACTIVATING, prev_state=NMDeviceState.CONFIG, + reason=NMDeviceStateReason.CONNECTION_REMOVED) + assert wm._wifi_state.ssid == "A" + assert wm._wifi_state.status == ConnectStatus.CONNECTING # DEACTIVATING preserves CONNECTING + + fire(wm, NMDeviceState.DISCONNECTED, prev_state=NMDeviceState.DEACTIVATING, + reason=NMDeviceStateReason.CONNECTION_REMOVED) + assert wm._wifi_state.ssid is None + assert wm._wifi_state.status == ConnectStatus.DISCONNECTED + + # NM auto-connects to another saved network + fire(wm, NMDeviceState.PREPARE) + assert wm._wifi_state.ssid == "Other" + assert wm._wifi_state.status == ConnectStatus.CONNECTING + + fire(wm, NMDeviceState.CONFIG) + fire_wpa_connect(wm) + assert wm._wifi_state.status == ConnectStatus.CONNECTED + assert wm._wifi_state.ssid == "Other" + + def test_forget_connected_network(self, mocker): + """Forget the currently connected network (not switching to another). + + Real device sequence: + DEACTIVATING(ACTIVATED, CONNECTION_REMOVED) → DISCONNECTED(DEACTIVATING, CONNECTION_REMOVED) + + ConnectionRemoved signal may or may not have been processed before state changes. + Either way, state must clear — we're forgetting what we're connected to, not switching. + """ + wm = _make_wm(mocker, connections={"A": "/path/A"}) + wm._wifi_state = WifiState(ssid="A", status=ConnectStatus.CONNECTED) + + fire(wm, NMDeviceState.DEACTIVATING, prev_state=NMDeviceState.ACTIVATED, + reason=NMDeviceStateReason.CONNECTION_REMOVED) + assert wm._wifi_state.ssid is None + assert wm._wifi_state.status == ConnectStatus.DISCONNECTED + + # DISCONNECTED follows — harmless since state is already cleared + fire(wm, NMDeviceState.DISCONNECTED, prev_state=NMDeviceState.DEACTIVATING, + reason=NMDeviceStateReason.CONNECTION_REMOVED) + assert wm._wifi_state.ssid is None + assert wm._wifi_state.status == ConnectStatus.DISCONNECTED + def test_forget_A_connect_B(self, mocker): """Forget A while connecting to B: full signal sequence. @@ -613,16 +727,69 @@ class TestFullSequences: assert wm._wifi_state.status == ConnectStatus.CONNECTED assert wm._wifi_state.ssid == "AutoNet" + def test_network_lost_during_connection(self, mocker): + """Hotspot turned off while connecting (before ACTIVATED). + + Confirmed on device: started new connection to Shane's iPhone, immediately + turned off the hotspot. NM can't complete WPA handshake and reports + FAILED(NO_SECRETS) — same signal as wrong password (false positive). + + Real device sequence: + PREPARE → CONFIG → NEED_AUTH(CONFIG, NONE) → PREPARE(NEED_AUTH) → CONFIG + → NEED_AUTH(CONFIG, NONE) → FAILED(NEED_AUTH, NO_SECRETS) → DISCONNECTED(FAILED, NONE) + + Note: no DEACTIVATING, no SUPPLICANT_DISCONNECT. The NEED_AUTH(CONFIG, NONE) is the + normal WPA handshake (not an error). NM gives up with NO_SECRETS because the AP + vanished mid-handshake. + """ + wm = _make_wm(mocker, connections={"Hotspot": "/path/hs"}) + cb = mocker.MagicMock() + wm.add_callbacks(need_auth=cb) + + wm._set_connecting("Hotspot") + fire(wm, NMDeviceState.PREPARE) + fire(wm, NMDeviceState.CONFIG) + fire(wm, NMDeviceState.NEED_AUTH) # WPA handshake (reason=NONE) + fire(wm, NMDeviceState.PREPARE, prev_state=NMDeviceState.NEED_AUTH) + fire(wm, NMDeviceState.CONFIG) + assert wm._wifi_state.status == ConnectStatus.CONNECTING + + # Second NEED_AUTH(CONFIG, NONE) — NM retries handshake, AP vanishing + fire(wm, NMDeviceState.NEED_AUTH) + assert wm._wifi_state.status == ConnectStatus.CONNECTING + + # NM gives up — reports NO_SECRETS (same as wrong password) + fire(wm, NMDeviceState.FAILED, prev_state=NMDeviceState.NEED_AUTH, + reason=NMDeviceStateReason.NO_SECRETS) + assert wm._wifi_state.status == ConnectStatus.DISCONNECTED + assert len(wm._callback_queue) == 1 + + fire(wm, NMDeviceState.DISCONNECTED, prev_state=NMDeviceState.FAILED) + assert wm._wifi_state.ssid is None + assert wm._wifi_state.status == ConnectStatus.DISCONNECTED + + wm.process_callbacks() + cb.assert_called_once_with("Hotspot") + @pytest.mark.xfail(reason="TODO: FAILED(SSID_NOT_FOUND) should emit error for UI") def test_ssid_not_found(self, mocker): - """Network drops off after connection starts. + """Network drops off while connected — hotspot turned off. NM docs: SSID_NOT_FOUND (53) = "The WiFi network could not be found" - Expected sequence: PREPARE → CONFIG → FAILED(SSID_NOT_FOUND) → DISCONNECTED - NOTE: SSID_NOT_FOUND is rare. On-device testing with a disappearing hotspot typically - produces FAILED(NO_SECRETS) instead. May be driver-specific or require the network - to vanish from scan results mid-connection. + Confirmed on device: connected to Shane's iPhone, then turned off the hotspot. + No DEACTIVATING fires — NM goes straight from ACTIVATED to FAILED(SSID_NOT_FOUND). + NM retries connecting (PREPARE → CONFIG → ... → FAILED(CONFIG, SSID_NOT_FOUND)) + before finally giving up with DISCONNECTED. + + NOTE: turning off a hotspot during initial connection (before ACTIVATED) typically + produces FAILED(NO_SECRETS) instead of SSID_NOT_FOUND (see test_failed_no_secrets). + + Real device sequence (hotspot turned off while connected): + FAILED(ACTIVATED, SSID_NOT_FOUND) → DISCONNECTED(FAILED, NONE) + → PREPARE → CONFIG → NEED_AUTH(CONFIG, NONE) → PREPARE(NEED_AUTH) → CONFIG + → NEED_AUTH(CONFIG, NONE) → PREPARE(NEED_AUTH) → CONFIG + → FAILED(CONFIG, SSID_NOT_FOUND) → DISCONNECTED(FAILED, NONE) The UI error callback mechanism is intentionally deferred — for now just clear state. """ diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index 6e39fdf523..1beaeb736c 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -394,7 +394,8 @@ class WifiManager: # on every user action. Handlers snapshot the epoch before their DBus call and compare # after: if it changed, a user action occurred during the call and the stale result is # discarded. Combined with deterministic fixes (skip DBus lookup when ssid already set, - # DEACTIVATING no-op, CONNECTION_REMOVED guard), all known race windows are closed. + # DEACTIVATING clears CONNECTED on CONNECTION_REMOVED, CONNECTION_REMOVED guard), + # all known race windows are closed. # TODO: Handle (FAILED, SSID_NOT_FOUND) and emit for UI to show error # Happens when network drops off after starting connection @@ -480,8 +481,12 @@ class WifiManager: cloudlog.warning(f"Failed to persist connection to disk: {save_reply}") elif new_state == NMDeviceState.DEACTIVATING: - # no-op — DISCONNECTED always follows with the correct reason - pass + # Must clear state when forgetting the currently connected network so the UI + # doesn't flash "connected" after the eager "forgetting..." state resets + # (the forgotten callback fires between DEACTIVATING and DISCONNECTED). + # Only clear CONNECTED — CONNECTING must be preserved for forget-A-connect-B. + if change_reason == NMDeviceStateReason.CONNECTION_REMOVED and self._wifi_state.status == ConnectStatus.CONNECTED: + self._set_connecting(None) def _network_scanner(self): while not self._exit: From b2e94548b9d29ec64c5be43d9cce49ab3b7bf399 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 26 Feb 2026 01:20:06 -0800 Subject: [PATCH 226/311] ui: move connected wifi buttons to front independent of scan results (#37417) * move items * clean up * wtf * debg --- selfdrive/ui/mici/layouts/settings/network/wifi_ui.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py index c69dc8b225..8b8522ba19 100644 --- a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py +++ b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py @@ -1,4 +1,6 @@ import math +import time + import numpy as np import pyray as rl from collections.abc import Callable @@ -326,8 +328,6 @@ class WifiUIMici(NavWidget): if isinstance(btn, WifiButton) and btn.network.ssid not in self._networks: btn.set_network_missing(True) - self._move_network_to_front(self._wifi_manager.wifi_state.ssid) - def _connect_with_password(self, ssid: str, password: str): self._wifi_manager.connect_to_network(ssid, password) self._move_network_to_front(ssid, scroll=True) @@ -382,6 +382,10 @@ class WifiUIMici(NavWidget): def _update_state(self): super()._update_state() + t = time.monotonic() + self._move_network_to_front(self._wifi_manager.wifi_state.ssid) + print('took', (time.monotonic() - t) * 1000, 'ms to move network to front') + # Show loading animation near end max_scroll = max(self._scroller.content_size - self._scroller.rect.width, 1) progress = -self._scroller.scroll_panel.get_offset() / max_scroll From 811363cab9ec23c82a7932398249392f7a7e7cb3 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 26 Feb 2026 01:21:32 -0800 Subject: [PATCH 227/311] clean up --- selfdrive/ui/mici/layouts/settings/network/wifi_ui.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py index 8b8522ba19..e1548e6a6b 100644 --- a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py +++ b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py @@ -382,9 +382,7 @@ class WifiUIMici(NavWidget): def _update_state(self): super()._update_state() - t = time.monotonic() self._move_network_to_front(self._wifi_manager.wifi_state.ssid) - print('took', (time.monotonic() - t) * 1000, 'ms to move network to front') # Show loading animation near end max_scroll = max(self._scroller.content_size - self._scroller.rect.width, 1) From 4cd5c1b3c2abbee1832cbd3a23bcd1b1a513abf4 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 26 Feb 2026 02:24:12 -0800 Subject: [PATCH 228/311] clean up --- selfdrive/ui/mici/layouts/settings/network/wifi_ui.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py index e1548e6a6b..9604db5362 100644 --- a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py +++ b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py @@ -1,6 +1,4 @@ import math -import time - import numpy as np import pyray as rl from collections.abc import Callable From 146c64b0f197a54d81e0efab10193594f294a36e Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 26 Feb 2026 02:24:59 -0800 Subject: [PATCH 229/311] mici ui: improve tethering a bit (#37418) * try this * deactivate * faiilures! * starting * try * ... * starting * fix strength * revert * debug * more * override for display network * try * nvm it fixes a few things * cmt * clean up --- .../ui/mici/layouts/settings/network/__init__.py | 6 +++++- .../ui/mici/layouts/settings/network/wifi_ui.py | 4 ++-- system/ui/lib/wifi_manager.py | 12 ++++++++++-- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/selfdrive/ui/mici/layouts/settings/network/__init__.py b/selfdrive/ui/mici/layouts/settings/network/__init__.py index bdae924566..e5049a9ee7 100644 --- a/selfdrive/ui/mici/layouts/settings/network/__init__.py +++ b/selfdrive/ui/mici/layouts/settings/network/__init__.py @@ -33,7 +33,7 @@ class WifiNetworkButton(BigButton): display_network = next((n for n in self._wifi_manager.networks if n.ssid == wifi_state.ssid), None) if wifi_state.status == ConnectStatus.CONNECTING: self.set_text(normalize_ssid(wifi_state.ssid or "wi-fi")) - self.set_value("connecting...") + self.set_value("starting" if self._wifi_manager.is_tethering_active() else "connecting...") elif wifi_state.status == ConnectStatus.CONNECTED: self.set_text(normalize_ssid(wifi_state.ssid or "wi-fi")) self.set_value(self._wifi_manager.ipv4_address or "obtaining IP...") @@ -46,6 +46,10 @@ class WifiNetworkButton(BigButton): strength = WifiIcon.get_strength_icon_idx(display_network.strength) self.set_icon(self._wifi_full_txt if strength == 2 else self._wifi_medium_txt if strength == 1 else self._wifi_low_txt) self._draw_lock = display_network.security_type not in (SecurityType.OPEN, SecurityType.UNSUPPORTED) + elif self._wifi_manager.is_tethering_active(): + # takes a while to get Network + self.set_icon(self._wifi_full_txt) + self._draw_lock = True else: self.set_icon(self._wifi_slash_txt) self._draw_lock = False diff --git a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py index 9604db5362..062e28d3b1 100644 --- a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py +++ b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py @@ -227,9 +227,9 @@ class WifiButton(BigButton): if self._network_forgetting: self.set_value("forgetting...") elif self._is_connecting: - self.set_value("connecting...") + self.set_value("starting..." if self._network.is_tethering else "connecting...") elif self._is_connected: - self.set_value("connected") + self.set_value("tethering" if self._network.is_tethering else "connected") elif self._network_missing: # after connecting/connected since NM will still attempt to connect/stay connected for a while self.set_value("not in range") diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index 1beaeb736c..1a997f3c70 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -713,11 +713,19 @@ class WifiManager: def _deactivate_connection(self, ssid: str): for active_conn in self._get_active_connections(): conn_addr = DBusAddress(active_conn, bus_name=NM, interface=NM_ACTIVE_CONNECTION_IFACE) - specific_obj_path = self._router_main.send_and_get_reply(Properties(conn_addr).get('SpecificObject')).body[0][1] + reply = self._router_main.send_and_get_reply(Properties(conn_addr).get('SpecificObject')) + if reply.header.message_type == MessageType.error: + continue # object gone (e.g. rapid connect/disconnect) + + specific_obj_path = reply.body[0][1] if specific_obj_path != "/": ap_addr = DBusAddress(specific_obj_path, bus_name=NM, interface=NM_ACCESS_POINT_IFACE) - ap_ssid = bytes(self._router_main.send_and_get_reply(Properties(ap_addr).get('Ssid')).body[0][1]).decode("utf-8", "replace") + ap_reply = self._router_main.send_and_get_reply(Properties(ap_addr).get('Ssid')) + if ap_reply.header.message_type == MessageType.error: + continue # AP gone (e.g. mode switch) + + ap_ssid = bytes(ap_reply.body[0][1]).decode("utf-8", "replace") if ap_ssid == ssid: self._router_main.send_and_get_reply(new_method_call(self._nm, 'DeactivateConnection', 'o', (active_conn,))) From 93a96695eabf4f0f07f3853813217ce4c74f24b7 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 26 Feb 2026 03:46:40 -0800 Subject: [PATCH 230/311] WifiManager: frozen WifiState (#37420) froze --- system/ui/lib/wifi_manager.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index 1a997f3c70..0fcff70abd 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -145,7 +145,7 @@ class ConnectStatus(IntEnum): CONNECTED = 2 -@dataclass +@dataclass(frozen=True) class WifiState: ssid: str | None = None status: ConnectStatus = ConnectStatus.DISCONNECTED @@ -232,20 +232,21 @@ class WifiManager: dev_addr = DBusAddress(self._wifi_device, bus_name=NM, interface=NM_DEVICE_IFACE) dev_state = self._router_main.send_and_get_reply(Properties(dev_addr).get('State')).body[0][1] - wifi_state = WifiState() + ssid: str | None = None + status = ConnectStatus.DISCONNECTED if NMDeviceState.PREPARE <= dev_state <= NMDeviceState.SECONDARIES and dev_state != NMDeviceState.NEED_AUTH: - wifi_state.status = ConnectStatus.CONNECTING + status = ConnectStatus.CONNECTING elif dev_state == NMDeviceState.ACTIVATED: - wifi_state.status = ConnectStatus.CONNECTED + status = ConnectStatus.CONNECTED conn_path, _ = self._get_active_wifi_connection() if conn_path: - wifi_state.ssid = next((s for s, p in self._connections.items() if p == conn_path), None) + ssid = next((s for s, p in self._connections.items() if p == conn_path), None) if self._user_epoch != epoch: return - self._wifi_state = wifi_state + self._wifi_state = WifiState(ssid=ssid, status=status) if block: worker() @@ -431,7 +432,7 @@ class WifiManager: if conn_path is None: cloudlog.warning("Failed to get active wifi connection during PREPARE/CONFIG state") else: - wifi_state.ssid = next((s for s, p in self._connections.items() if p == conn_path), None) + wifi_state = replace(wifi_state, ssid=next((s for s, p in self._connections.items() if p == conn_path), None)) self._wifi_state = wifi_state @@ -467,7 +468,7 @@ class WifiManager: if conn_path is None: cloudlog.warning("Failed to get active wifi connection during ACTIVATED state") else: - wifi_state.ssid = next((s for s, p in self._connections.items() if p == conn_path), None) + wifi_state = replace(wifi_state, ssid=next((s for s, p in self._connections.items() if p == conn_path), None)) self._wifi_state = wifi_state self._enqueue_callbacks(self._activated) From 608a1c2baae6ef1df8551b3cf14cf4bd10b581c1 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 26 Feb 2026 03:47:40 -0800 Subject: [PATCH 231/311] Add comment about epoch guard --- system/ui/lib/wifi_manager.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index 0fcff70abd..4820c7aaba 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -243,6 +243,7 @@ class WifiManager: if conn_path: ssid = next((s for s, p in self._connections.items() if p == conn_path), None) + # Discard if user acted during DBus calls if self._user_epoch != epoch: return @@ -426,6 +427,7 @@ class WifiManager: conn_path, _ = self._get_active_wifi_connection(self._conn_monitor) + # Discard if user acted during DBus call if self._user_epoch != epoch: return @@ -462,6 +464,7 @@ class WifiManager: conn_path, _ = self._get_active_wifi_connection(self._conn_monitor) + # Discard if user acted during DBus call if self._user_epoch != epoch: return From 91696ba6c8298fbaf7d21969dca66599eefbf3d1 Mon Sep 17 00:00:00 2001 From: ZwX1616 Date: Thu, 26 Feb 2026 15:58:52 -0800 Subject: [PATCH 232/311] fix module for model_review (#37428) * install tg instead of onnx * fix python path --------- Co-authored-by: Bruce Wayne --- .github/workflows/model_review.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/model_review.yaml b/.github/workflows/model_review.yaml index 6b8ce143db..2775dbc574 100644 --- a/.github/workflows/model_review.yaml +++ b/.github/workflows/model_review.yaml @@ -17,6 +17,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v6 + with: + submodules: true - name: Checkout master uses: actions/checkout@v6 with: @@ -25,14 +27,12 @@ jobs: - run: git lfs pull - run: cd base && git lfs pull - - run: pip install onnx - - name: scripts/reporter.py id: report run: | echo "content<> $GITHUB_OUTPUT echo "## Model Review" >> $GITHUB_OUTPUT - MASTER_PATH=${{ github.workspace }}/base python scripts/reporter.py >> $GITHUB_OUTPUT + PYTHONPATH=${{ github.workspace }} MASTER_PATH=${{ github.workspace }}/base python scripts/reporter.py >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT - name: Post model report comment From 94ee6b0f435bdc8a006fa5cf9b5d2a3cf2ac6b55 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 26 Feb 2026 16:01:30 -0800 Subject: [PATCH 233/311] BigButton: move parameters into class (#37429) * BigButton: move parameters into class * fix --- .../ui/mici/layouts/settings/network/wifi_ui.py | 12 ++++++------ selfdrive/ui/mici/widgets/button.py | 17 +++++++++-------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py index 062e28d3b1..dda1d12220 100644 --- a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py +++ b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py @@ -6,7 +6,7 @@ from collections.abc import Callable from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.swaglog import cloudlog from openpilot.selfdrive.ui.mici.widgets.dialog import BigInputDialog, BigConfirmationDialogV2 -from openpilot.selfdrive.ui.mici.widgets.button import BigButton, LABEL_COLOR, LABEL_HORIZONTAL_PADDING, LABEL_VERTICAL_PADDING +from openpilot.selfdrive.ui.mici.widgets.button import BigButton, LABEL_COLOR from openpilot.system.ui.lib.application import gui_app, MousePos, FontWeight from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.nav_widget import NavWidget @@ -98,7 +98,7 @@ class WifiIcon(Widget): class WifiButton(BigButton): LABEL_PADDING = 98 LABEL_WIDTH = 402 - 98 - 28 # button width - left padding - right padding - SUB_LABEL_WIDTH = 402 - LABEL_HORIZONTAL_PADDING * 2 + SUB_LABEL_WIDTH = 402 - BigButton.LABEL_HORIZONTAL_PADDING * 2 def __init__(self, network: Network, wifi_manager: WifiManager): super().__init__(normalize_ssid(network.ssid), scroll=True) @@ -166,13 +166,13 @@ class WifiButton(BigButton): def _draw_content(self, btn_y: float): self._label.set_color(LABEL_COLOR) - label_rect = rl.Rectangle(self._rect.x + self.LABEL_PADDING, btn_y + LABEL_VERTICAL_PADDING, - self.LABEL_WIDTH, self._rect.height - LABEL_VERTICAL_PADDING * 2) + label_rect = rl.Rectangle(self._rect.x + self.LABEL_PADDING, btn_y + self.LABEL_VERTICAL_PADDING, + self.LABEL_WIDTH, self._rect.height - self.LABEL_VERTICAL_PADDING * 2) self._label.render(label_rect) if self.value: - sub_label_x = self._rect.x + LABEL_HORIZONTAL_PADDING - label_y = btn_y + self._rect.height - LABEL_VERTICAL_PADDING + sub_label_x = self._rect.x + self.LABEL_HORIZONTAL_PADDING + label_y = btn_y + self._rect.height - self.LABEL_VERTICAL_PADDING sub_label_w = self.SUB_LABEL_WIDTH - (self._forget_btn.rect.width if self._show_forget_btn else 0) sub_label_height = self._sub_label.get_content_height(sub_label_w) diff --git a/selfdrive/ui/mici/widgets/button.py b/selfdrive/ui/mici/widgets/button.py index 3ea650ece3..2855e2e4e1 100644 --- a/selfdrive/ui/mici/widgets/button.py +++ b/selfdrive/ui/mici/widgets/button.py @@ -17,8 +17,6 @@ except ImportError: SCROLLING_SPEED_PX_S = 50 COMPLICATION_SIZE = 36 LABEL_COLOR = rl.Color(255, 255, 255, int(255 * 0.9)) -LABEL_HORIZONTAL_PADDING = 40 -LABEL_VERTICAL_PADDING = 23 # visually matches 30 in figma COMPLICATION_GREY = rl.Color(0xAA, 0xAA, 0xAA, 255) PRESSED_SCALE = 1.15 if DO_ZOOM else 1.07 @@ -103,6 +101,9 @@ class BigCircleToggle(BigCircleButton): class BigButton(Widget): + LABEL_HORIZONTAL_PADDING = 40 + LABEL_VERTICAL_PADDING = 23 # visually matches 30 in figma + """A lightweight stand-in for the Qt BigButton, drawn & updated each frame.""" def __init__(self, text: str, value: str = "", icon: Union[str, rl.Texture] = "", icon_size: tuple[int, int] = (64, 64), @@ -145,7 +146,7 @@ class BigButton(Widget): def _width_hint(self) -> int: # Single line if scrolling, so hide behind icon if exists icon_size = self._icon_size[0] if self._txt_icon and self._scroll and self.value else 0 - return int(self._rect.width - LABEL_HORIZONTAL_PADDING * 2 - icon_size) + return int(self._rect.width - self.LABEL_HORIZONTAL_PADDING * 2 - icon_size) def _get_label_font_size(self): if len(self.text) <= 18: @@ -195,16 +196,16 @@ class BigButton(Widget): def _draw_content(self, btn_y: float): # LABEL ------------------------------------------------------------------ - label_x = self._rect.x + LABEL_HORIZONTAL_PADDING + label_x = self._rect.x + self.LABEL_HORIZONTAL_PADDING label_color = LABEL_COLOR if self.enabled else rl.Color(255, 255, 255, int(255 * 0.35)) self._label.set_color(label_color) - label_rect = rl.Rectangle(label_x, btn_y + LABEL_VERTICAL_PADDING, self._width_hint(), - self._rect.height - LABEL_VERTICAL_PADDING * 2) + label_rect = rl.Rectangle(label_x, btn_y + self.LABEL_VERTICAL_PADDING, self._width_hint(), + self._rect.height - self.LABEL_VERTICAL_PADDING * 2) self._label.render(label_rect) if self.value: - label_y = btn_y + self._rect.height - LABEL_VERTICAL_PADDING + label_y = btn_y + self._rect.height - self.LABEL_VERTICAL_PADDING sub_label_height = self._sub_label.get_content_height(self._width_hint()) sub_label_rect = rl.Rectangle(label_x, label_y - sub_label_height, self._width_hint(), sub_label_height) self._sub_label.render(sub_label_rect) @@ -293,7 +294,7 @@ class BigMultiToggle(BigToggle): self.set_value(self._options[0]) def _width_hint(self) -> int: - return int(self._rect.width - LABEL_HORIZONTAL_PADDING * 2 - self._txt_enabled_toggle.width) + return int(self._rect.width - self.LABEL_HORIZONTAL_PADDING * 2 - self._txt_enabled_toggle.width) def _handle_mouse_release(self, mouse_pos: MousePos): super()._handle_mouse_release(mouse_pos) From 7f1def00b227e567e836ccd9457fe5252ec84c64 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 26 Feb 2026 16:04:53 -0800 Subject: [PATCH 234/311] BigButton: handle background function (#37430) * move * fix --- selfdrive/ui/mici/widgets/button.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/selfdrive/ui/mici/widgets/button.py b/selfdrive/ui/mici/widgets/button.py index 2855e2e4e1..1faf3222d8 100644 --- a/selfdrive/ui/mici/widgets/button.py +++ b/selfdrive/ui/mici/widgets/button.py @@ -194,6 +194,19 @@ class BigButton(Widget): def set_position(self, x: float, y: float) -> None: super().set_position(x + self._shake_offset, y) + def _handle_background(self) -> tuple[rl.Texture, float, float, float]: + # draw _txt_default_bg + txt_bg = self._txt_default_bg + if not self.enabled: + txt_bg = self._txt_disabled_bg + elif self.is_pressed: + txt_bg = self._txt_pressed_bg + + scale = self._scale_filter.update(PRESSED_SCALE if self.is_pressed else 1.0) + btn_x = self._rect.x + (self._rect.width * (1 - scale)) / 2 + btn_y = self._rect.y + (self._rect.height * (1 - scale)) / 2 + return txt_bg, btn_x, btn_y, scale + def _draw_content(self, btn_y: float): # LABEL ------------------------------------------------------------------ label_x = self._rect.x + self.LABEL_HORIZONTAL_PADDING @@ -225,16 +238,7 @@ class BigButton(Widget): rl.draw_texture_pro(self._txt_icon, source_rec, dest_rec, origin, rotation, rl.Color(255, 255, 255, int(255 * 0.9))) def _render(self, _): - # draw _txt_default_bg - txt_bg = self._txt_default_bg - if not self.enabled: - txt_bg = self._txt_disabled_bg - elif self.is_pressed: - txt_bg = self._txt_pressed_bg - - scale = self._scale_filter.update(PRESSED_SCALE if self.is_pressed else 1.0) - btn_x = self._rect.x + (self._rect.width * (1 - scale)) / 2 - btn_y = self._rect.y + (self._rect.height * (1 - scale)) / 2 + txt_bg, btn_x, btn_y, scale = self._handle_background() if self._scroll: # draw black background since images are transparent From 04dcdf46bc7c3d84f8a20f6e81cfb5b38dab8689 Mon Sep 17 00:00:00 2001 From: ZwX1616 Date: Thu, 26 Feb 2026 16:10:57 -0800 Subject: [PATCH 235/311] DM: Le Mans GT3 Model (#37425) * 81248b12-6592-4a5c-9b59-a44c64123b2b * install tg instead of onnx * fix python path --------- Co-authored-by: Bruce Wayne --- selfdrive/modeld/models/dmonitoring_model.onnx | 2 +- selfdrive/monitoring/helpers.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/selfdrive/modeld/models/dmonitoring_model.onnx b/selfdrive/modeld/models/dmonitoring_model.onnx index 24234d4c50..dc621bed03 100644 --- a/selfdrive/modeld/models/dmonitoring_model.onnx +++ b/selfdrive/modeld/models/dmonitoring_model.onnx @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5e8de9dc7df306700cce9c22b992e25b95a38f894c47adaea742a9cf8ba78e1a +oid sha256:7aff7ff1dc08bbaf562a8f77380ab5e5914f8557dba2f760d87e4d953f5604b0 size 7307246 diff --git a/selfdrive/monitoring/helpers.py b/selfdrive/monitoring/helpers.py index 91ddaaa9c1..0b54504b64 100644 --- a/selfdrive/monitoring/helpers.py +++ b/selfdrive/monitoring/helpers.py @@ -35,7 +35,7 @@ class DRIVER_MONITOR_SETTINGS: self._EYE_THRESHOLD = 0.65 self._SG_THRESHOLD = 0.9 self._BLINK_THRESHOLD = 0.865 - self._PHONE_THRESH = 0.68 + self._PHONE_THRESH = 0.5 self._POSE_PITCH_THRESHOLD = 0.3133 self._POSE_PITCH_THRESHOLD_SLACK = 0.3237 From ac08c79139413ff703fc6fdcdeeb5fb9c7c0008e Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 26 Feb 2026 16:19:12 -0800 Subject: [PATCH 236/311] BigButton: sublabel takes all available space (#37431) change --- selfdrive/ui/mici/widgets/button.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/selfdrive/ui/mici/widgets/button.py b/selfdrive/ui/mici/widgets/button.py index 1faf3222d8..231dafa8eb 100644 --- a/selfdrive/ui/mici/widgets/button.py +++ b/selfdrive/ui/mici/widgets/button.py @@ -218,9 +218,9 @@ class BigButton(Widget): self._label.render(label_rect) if self.value: - label_y = btn_y + self._rect.height - self.LABEL_VERTICAL_PADDING - sub_label_height = self._sub_label.get_content_height(self._width_hint()) - sub_label_rect = rl.Rectangle(label_x, label_y - sub_label_height, self._width_hint(), sub_label_height) + label_y = btn_y + self.LABEL_VERTICAL_PADDING + self._label.get_content_height(self._width_hint()) + sub_label_height = btn_y + self._rect.height - self.LABEL_VERTICAL_PADDING - label_y + sub_label_rect = rl.Rectangle(label_x, label_y, self._width_hint(), sub_label_height) self._sub_label.render(sub_label_rect) # ICON ------------------------------------------------------------------- From 3cc4683eb737718b9b80554e40dc2c019182efe4 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 26 Feb 2026 17:34:26 -0800 Subject: [PATCH 237/311] mici reset: fix cancel closes application (#37434) * fix * match tici * rm --- system/ui/mici_reset.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/system/ui/mici_reset.py b/system/ui/mici_reset.py index 357e672931..a459927eeb 100755 --- a/system/ui/mici_reset.py +++ b/system/ui/mici_reset.py @@ -38,18 +38,13 @@ class Reset(Widget): self._reset_state = ResetState.NONE self._cancel_button = SmallButton("cancel") - self._cancel_button.set_click_callback(self._cancel_callback) + self._cancel_button.set_click_callback(gui_app.request_close) self._reboot_button = FullRoundedButton("reboot") self._reboot_button.set_click_callback(self._do_reboot) self._confirm_slider = SmallSlider("reset", self._confirm) - self._render_status = True - - def _cancel_callback(self): - self._render_status = False - def _do_reboot(self): if PC: return @@ -121,8 +116,6 @@ class Reset(Widget): self._reboot_button.rect.width, self._reboot_button.rect.height)) - return self._render_status - def _confirm(self): self.start_reset() From 6d559c4219695afd45d04a590b696b5412d63d61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20R=C4=85czy?= Date: Thu, 26 Feb 2026 19:47:07 -0800 Subject: [PATCH 238/311] lagd: min_lag (#37402) * Add min_lag * Split line * Clip lag * Test should run with 3 lag frames too * Update selfdrive/locationd/lagd.py --- selfdrive/locationd/lagd.py | 16 +++++++++------- selfdrive/locationd/test/test_lagd.py | 4 ++-- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/selfdrive/locationd/lagd.py b/selfdrive/locationd/lagd.py index d7834f7f1f..361bb79cce 100755 --- a/selfdrive/locationd/lagd.py +++ b/selfdrive/locationd/lagd.py @@ -24,6 +24,7 @@ MIN_ABS_YAW_RATE = 0.0 MAX_YAW_RATE_SANITY_CHECK = 1.0 MIN_NCC = 0.95 MAX_LAG = 1.0 +MIN_LAG = 0.15 MAX_LAG_STD = 0.1 MAX_LAT_ACCEL = 2.0 MAX_LAT_ACCEL_DIFF = 0.6 @@ -215,7 +216,7 @@ class LateralLagEstimator: liveDelay.status = log.LiveDelayData.Status.unestimated if liveDelay.status == log.LiveDelayData.Status.estimated: - liveDelay.lateralDelay = valid_mean_lag + liveDelay.lateralDelay = min(MAX_LAG, max(MIN_LAG, valid_mean_lag)) else: liveDelay.lateralDelay = self.initial_lag @@ -298,7 +299,7 @@ class LateralLagEstimator: new_values_start_idx = next(-i for i, t in enumerate(reversed(times)) if t <= self.last_estimate_t) is_valid = is_valid and not (new_values_start_idx == 0 or not np.any(okay[new_values_start_idx:])) - delay, corr, confidence = self.actuator_delay(desired, actual, okay, self.dt, MAX_LAG) + delay, corr, confidence = self.actuator_delay(desired, actual, okay, self.dt, MIN_LAG, MAX_LAG) if corr < self.min_ncc or confidence < self.min_confidence or not is_valid: return @@ -306,22 +307,23 @@ class LateralLagEstimator: self.last_estimate_t = self.t @staticmethod - def actuator_delay(expected_sig: np.ndarray, actual_sig: np.ndarray, mask: np.ndarray, dt: float, max_lag: float) -> tuple[float, float, float]: + def actuator_delay(expected_sig: np.ndarray, actual_sig: np.ndarray, mask: np.ndarray, + dt: float, min_lag: float, max_lag: float) -> tuple[float, float, float]: assert len(expected_sig) == len(actual_sig) - max_lag_samples = int(max_lag / dt) + min_lag_samples, max_lag_samples = int(round(min_lag / dt)), int(round(max_lag / dt)) padded_size = fft_next_good_size(len(expected_sig) + max_lag_samples) ncc = masked_normalized_cross_correlation(expected_sig, actual_sig, mask, padded_size) - # only consider lags from 0 to max_lag - roi = np.s_[len(expected_sig) - 1: len(expected_sig) - 1 + max_lag_samples] + # only consider lags from min_lag to max_lag + roi = np.s_[len(expected_sig) - 1 + min_lag_samples: len(expected_sig) - 1 + max_lag_samples] extended_roi = np.s_[roi.start - CORR_BORDER_OFFSET: roi.stop + CORR_BORDER_OFFSET] roi_ncc = ncc[roi] extended_roi_ncc = ncc[extended_roi] max_corr_index = np.argmax(roi_ncc) corr = roi_ncc[max_corr_index] - lag = parabolic_peak_interp(roi_ncc, max_corr_index) * dt + lag = parabolic_peak_interp(roi_ncc, max_corr_index) * dt + min_lag # to estimate lag confidence, gather all high-correlation candidates and see how spread they are # if e.g. 0.8 and 0.4 are both viable, this is an ambiguous case diff --git a/selfdrive/locationd/test/test_lagd.py b/selfdrive/locationd/test/test_lagd.py index e9b5aff6d4..4728413d9d 100644 --- a/selfdrive/locationd/test/test_lagd.py +++ b/selfdrive/locationd/test/test_lagd.py @@ -97,7 +97,7 @@ class TestLagd: assert msg.liveDelay.calPerc == 0 def test_estimator_basics(self, subtests): - for lag_frames in range(5): + for lag_frames in range(3, 10): with subtests.test(msg=f"lag_frames={lag_frames}"): mocked_CP = car.CarParams(steerActuatorDelay=0.8) estimator = LateralLagEstimator(mocked_CP, DT, min_recovery_buffer_sec=0.0, min_yr=0.0) @@ -111,7 +111,7 @@ class TestLagd: assert msg.liveDelay.calPerc == 100 def test_estimator_masking(self): - mocked_CP, lag_frames = car.CarParams(steerActuatorDelay=0.8), random.randint(1, 19) + mocked_CP, lag_frames = car.CarParams(steerActuatorDelay=0.8), random.randint(3, 19) estimator = LateralLagEstimator(mocked_CP, DT, min_recovery_buffer_sec=0.0, min_yr=0.0, min_valid_block_count=1) process_messages(estimator, lag_frames, (int(MIN_OKAY_WINDOW_SEC / DT) + BLOCK_SIZE) * 2, rejection_threshold=0.4) msg = estimator.get_msg(True) From 2ef29967e8dfa25b73c85e50f92d5b4f286c9f28 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Thu, 26 Feb 2026 20:42:18 -0800 Subject: [PATCH 239/311] tici: rm cavli modem config --- system/hardware/tici/hardware.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/system/hardware/tici/hardware.py b/system/hardware/tici/hardware.py index 2295ca3cba..2080341c88 100644 --- a/system/hardware/tici/hardware.py +++ b/system/hardware/tici/hardware.py @@ -464,6 +464,7 @@ class Tici(HardwareBase): cmds = [] + # Quectel EG25 if self.get_device_type() in ("tizi", ): # clear out old blue prime initial APN os.system('mmcli -m any --3gpp-set-initial-eps-bearer-settings="apn="') @@ -478,16 +479,8 @@ class Tici(HardwareBase): 'AT+QNVFW="/nv/item_files/ims/IMS_enable",00', 'AT+QNVFW="/nv/item_files/modem/mmode/ue_usage_setting",01', ] - elif manufacturer == 'Cavli Inc.': - cmds += [ - 'AT^SIMSWAP=1', # use SIM slot, instead of internal eSIM - 'AT$QCSIMSLEEP=0', # disable SIM sleep - 'AT$QCSIMCFG=SimPowerSave,0', # more sleep disable - # ethernet config - 'AT$QCPCFG=usbNet,0', - 'AT$QCNETDEVCTL=3,1', - ] + # Quectel EG916 else: # this modem gets upset with too many AT commands if sim_id is None or len(sim_id) == 0: From 245d5bba9c325ad4f662c491aa6f124b6f900466 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Thu, 26 Feb 2026 20:49:18 -0800 Subject: [PATCH 240/311] make ruff happy --- system/hardware/tici/hardware.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/system/hardware/tici/hardware.py b/system/hardware/tici/hardware.py index 2080341c88..8219f0a587 100644 --- a/system/hardware/tici/hardware.py +++ b/system/hardware/tici/hardware.py @@ -456,13 +456,8 @@ class Tici(HardwareBase): def configure_modem(self): sim_id = self.get_sim_info().get('sim_id', '') - modem = self.get_modem() - try: - manufacturer = str(modem.Get(MM_MODEM, 'Manufacturer', dbus_interface=DBUS_PROPS, timeout=TIMEOUT)) - except Exception: - manufacturer = None - cmds = [] + modem = self.get_modem() # Quectel EG25 if self.get_device_type() in ("tizi", ): From 0977a91d656660c22874bdf6e91267e8d0a4edd6 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Thu, 26 Feb 2026 21:17:00 -0800 Subject: [PATCH 241/311] CI for the people: no cache (#37437) * sympathize with our first time cloners * venv * rm compile openpilot * retry for all * rm setup action --- .github/workflows/auto-cache/action.yaml | 55 ------------------ .github/workflows/badges.yaml | 2 +- .../workflows/compile-openpilot/action.yaml | 21 ------- .github/workflows/release.yaml | 2 +- .github/workflows/repo-maintenance.yaml | 4 +- .../workflows/setup-with-retry/action.yaml | 48 ---------------- .github/workflows/setup/action.yaml | 56 ------------------- .github/workflows/tests.yaml | 41 ++++++-------- tools/op.sh | 24 +++++++- 9 files changed, 42 insertions(+), 211 deletions(-) delete mode 100644 .github/workflows/auto-cache/action.yaml delete mode 100644 .github/workflows/compile-openpilot/action.yaml delete mode 100644 .github/workflows/setup-with-retry/action.yaml delete mode 100644 .github/workflows/setup/action.yaml diff --git a/.github/workflows/auto-cache/action.yaml b/.github/workflows/auto-cache/action.yaml deleted file mode 100644 index 42c8f8fd2d..0000000000 --- a/.github/workflows/auto-cache/action.yaml +++ /dev/null @@ -1,55 +0,0 @@ -name: 'automatically cache based on current runner' - -inputs: - path: - description: 'path to cache' - required: true - key: - description: 'key' - required: true - restore-keys: - description: 'restore-keys' - required: true - save: - description: 'whether to save the cache' - default: 'true' - required: false -outputs: - cache-hit: - description: 'cache hit occurred' - value: ${{ (contains(runner.name, 'nsc') && steps.ns-cache.outputs.cache-hit) || - (!contains(runner.name, 'nsc') && inputs.save != 'false' && steps.gha-cache.outputs.cache-hit) || - (!contains(runner.name, 'nsc') && inputs.save == 'false' && steps.gha-cache-ro.outputs.cache-hit) }} - -runs: - using: "composite" - steps: - - name: setup namespace cache - id: ns-cache - if: ${{ contains(runner.name, 'nsc') }} - uses: namespacelabs/nscloud-cache-action@v1 - with: - path: ${{ inputs.path }} - - - name: setup github cache - id: gha-cache - if: ${{ !contains(runner.name, 'nsc') && inputs.save != 'false' }} - uses: 'actions/cache@v4' - with: - path: ${{ inputs.path }} - key: ${{ inputs.key }} - restore-keys: ${{ inputs.restore-keys }} - - - name: setup github cache - id: gha-cache-ro - if: ${{ !contains(runner.name, 'nsc') && inputs.save == 'false' }} - uses: 'actions/cache/restore@v4' - with: - path: ${{ inputs.path }} - key: ${{ inputs.key }} - restore-keys: ${{ inputs.restore-keys }} - - # make the directory manually in case we didn't get a hit, so it doesn't fail on future steps - - id: scons-cache-setup - shell: bash - run: mkdir -p ${{ inputs.path }} diff --git a/.github/workflows/badges.yaml b/.github/workflows/badges.yaml index 23f2c135d5..9b99c4f1fe 100644 --- a/.github/workflows/badges.yaml +++ b/.github/workflows/badges.yaml @@ -18,7 +18,7 @@ jobs: - uses: actions/checkout@v6 with: submodules: true - - uses: ./.github/workflows/setup-with-retry + - run: ./tools/op.sh setup - name: Push badges run: | python3 selfdrive/ui/translations/create_badges.py diff --git a/.github/workflows/compile-openpilot/action.yaml b/.github/workflows/compile-openpilot/action.yaml deleted file mode 100644 index 627b4845aa..0000000000 --- a/.github/workflows/compile-openpilot/action.yaml +++ /dev/null @@ -1,21 +0,0 @@ -name: 'compile openpilot' - -runs: - using: "composite" - steps: - - shell: bash - name: Build openpilot with all flags - run: | - scons -j$(nproc) - release/check-dirty.sh - - shell: bash - name: Cleanup scons cache and rebuild - run: | - rm -rf /tmp/scons_cache/* - scons -j$(nproc) --cache-populate - - name: Save scons cache - uses: actions/cache/save@v4 - if: github.ref == 'refs/heads/master' - with: - path: /tmp/scons_cache - key: scons-${{ runner.arch }}-${{ env.CACHE_COMMIT_DATE }}-${{ github.sha }} diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 4d731965d7..db0e12234b 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -26,6 +26,6 @@ jobs: with: submodules: true fetch-depth: 0 - - uses: ./.github/workflows/setup-with-retry + - run: ./tools/op.sh setup - name: Push master-ci run: BRANCH=__nightly release/build_stripped.sh diff --git a/.github/workflows/repo-maintenance.yaml b/.github/workflows/repo-maintenance.yaml index d2c2447d7a..2c5d049e4c 100644 --- a/.github/workflows/repo-maintenance.yaml +++ b/.github/workflows/repo-maintenance.yaml @@ -16,7 +16,7 @@ jobs: - uses: actions/checkout@v6 with: submodules: true - - uses: ./.github/workflows/setup-with-retry + - run: ./tools/op.sh setup - name: Update translations run: python3 selfdrive/ui/update_translations.py --vanish - name: Create Pull Request @@ -39,7 +39,7 @@ jobs: - uses: actions/checkout@v6 with: submodules: true - - uses: ./.github/workflows/setup-with-retry + - run: ./tools/op.sh setup - name: uv lock run: uv lock --upgrade - name: uv pip tree diff --git a/.github/workflows/setup-with-retry/action.yaml b/.github/workflows/setup-with-retry/action.yaml deleted file mode 100644 index 923cc3aadb..0000000000 --- a/.github/workflows/setup-with-retry/action.yaml +++ /dev/null @@ -1,48 +0,0 @@ -name: 'openpilot env setup, with retry on failure' - -inputs: - sleep_time: - description: 'Time to sleep between retries' - required: false - default: 30 - -outputs: - duration: - description: 'Duration of the setup process in seconds' - value: ${{ steps.get_duration.outputs.duration }} - -runs: - using: "composite" - steps: - - id: start_time - shell: bash - run: echo "START_TIME=$(date +%s)" >> $GITHUB_ENV - - id: setup1 - uses: ./.github/workflows/setup - continue-on-error: true - with: - is_retried: true - - if: steps.setup1.outcome == 'failure' - shell: bash - run: sleep ${{ inputs.sleep_time }} - - id: setup2 - if: steps.setup1.outcome == 'failure' - uses: ./.github/workflows/setup - continue-on-error: true - with: - is_retried: true - - if: steps.setup2.outcome == 'failure' - shell: bash - run: sleep ${{ inputs.sleep_time }} - - id: setup3 - if: steps.setup2.outcome == 'failure' - uses: ./.github/workflows/setup - with: - is_retried: true - - id: get_duration - shell: bash - run: | - END_TIME=$(date +%s) - DURATION=$((END_TIME - START_TIME)) - echo "Total duration: $DURATION seconds" - echo "duration=$DURATION" >> $GITHUB_OUTPUT diff --git a/.github/workflows/setup/action.yaml b/.github/workflows/setup/action.yaml deleted file mode 100644 index f3a1a39509..0000000000 --- a/.github/workflows/setup/action.yaml +++ /dev/null @@ -1,56 +0,0 @@ -name: 'openpilot env setup' - -inputs: - is_retried: - description: 'A mock param that asserts that we use the setup-with-retry instead of this action directly' - required: false - default: 'false' - -runs: - using: "composite" - steps: - # assert that this action is retried using the setup-with-retry - - shell: bash - if: ${{ inputs.is_retried == 'false' }} - run: | - echo "You should not run this action directly. Use setup-with-retry instead" - exit 1 - - - shell: bash - name: No retries! - run: | - if [ "${{ github.run_attempt }}" -gt 1 ]; then - echo -e "\033[0;31m##################################################" - echo -e "\033[0;31m Retries not allowed! Fix the flaky test! " - echo -e "\033[0;31m##################################################\033[0m" - exit 1 - fi - - # build cache - - id: date - shell: bash - run: echo "CACHE_COMMIT_DATE=$(git log -1 --pretty='format:%cd' --date=format:'%Y-%m-%d-%H:%M')" >> $GITHUB_ENV - - id: scons-cache - uses: ./.github/workflows/auto-cache - with: - path: /tmp/scons_cache - key: scons-${{ runner.os }}-${{ runner.arch }}-${{ env.CACHE_COMMIT_DATE }}-${{ github.sha }} - restore-keys: | - scons-${{ runner.os }}-${{ runner.arch }}-${{ env.CACHE_COMMIT_DATE }} - scons-${{ runner.os }}-${{ runner.arch }} - # venv cache - - id: venv-cache - uses: ./.github/workflows/auto-cache - with: - path: ${{ github.workspace }}/.venv - key: venv-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('uv.lock') }} - restore-keys: | - venv-${{ runner.os }}-${{ runner.arch }} - - shell: bash - name: Run setup - run: ./tools/op.sh setup - - shell: bash - name: Setup cache dirs - run: | - mkdir -p /tmp/comma_download_cache - echo "$GITHUB_WORKSPACE/.venv/bin" >> $GITHUB_PATH diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index f95fe7d2e2..00fdceda0b 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -27,7 +27,7 @@ jobs: (github.repository == 'commaai/openpilot') && ((github.event_name != 'pull_request') || (github.event.pull_request.head.repo.full_name == 'commaai/openpilot')) - && fromJSON('["namespace-profile-amd64-8x16", "namespace-experiments:docker.builds.local-cache=separate"]') + && fromJSON('["namespace-profile-amd64-8x16"]') || fromJSON('["ubuntu-24.04"]') }} env: STRIPPED_DIR: /tmp/releasepilot @@ -45,7 +45,7 @@ jobs: - name: Build devel timeout-minutes: 1 run: TARGET_DIR=$STRIPPED_DIR release/build_stripped.sh - - uses: ./.github/workflows/setup-with-retry + - run: ./tools/op.sh setup - name: Build openpilot and run checks timeout-minutes: 30 working-directory: ${{ env.STRIPPED_DIR }} @@ -70,7 +70,7 @@ jobs: run: | FILTERED=$(echo "$PATH" | tr ':' '\n' | grep -v '/opt/homebrew' | tr '\n' ':') echo "PATH=${FILTERED}/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin" >> $GITHUB_ENV - - uses: ./.github/workflows/setup-with-retry + - run: ./tools/op.sh setup - name: Building openpilot run: scons @@ -80,13 +80,13 @@ jobs: (github.repository == 'commaai/openpilot') && ((github.event_name != 'pull_request') || (github.event.pull_request.head.repo.full_name == 'commaai/openpilot')) - && fromJSON('["namespace-profile-amd64-8x16", "namespace-experiments:docker.builds.local-cache=separate"]') + && fromJSON('["namespace-profile-amd64-8x16"]') || fromJSON('["ubuntu-24.04"]') }} steps: - uses: actions/checkout@v6 with: submodules: true - - uses: ./.github/workflows/setup-with-retry + - run: ./tools/op.sh setup - name: Static analysis timeout-minutes: 1 run: scripts/lint/lint.sh @@ -97,18 +97,17 @@ jobs: (github.repository == 'commaai/openpilot') && ((github.event_name != 'pull_request') || (github.event.pull_request.head.repo.full_name == 'commaai/openpilot')) - && fromJSON('["namespace-profile-amd64-8x16", "namespace-experiments:docker.builds.local-cache=separate"]') + && fromJSON('["namespace-profile-amd64-8x16"]') || fromJSON('["ubuntu-24.04"]') }} steps: - uses: actions/checkout@v6 with: submodules: true - - uses: ./.github/workflows/setup-with-retry - id: setup-step + - run: ./tools/op.sh setup - name: Build openpilot run: scons -j$(nproc) - name: Run unit tests - timeout-minutes: ${{ contains(runner.name, 'nsc') && ((steps.setup-step.outputs.duration < 18) && 1 || 2) || 20 }} + timeout-minutes: ${{ contains(runner.name, 'nsc') && 2 || 20 }} run: | source selfdrive/test/setup_xvfb.sh # Pre-compile Python bytecode so each pytest worker doesn't need to @@ -121,24 +120,17 @@ jobs: (github.repository == 'commaai/openpilot') && ((github.event_name != 'pull_request') || (github.event.pull_request.head.repo.full_name == 'commaai/openpilot')) - && fromJSON('["namespace-profile-amd64-8x16", "namespace-experiments:docker.builds.local-cache=separate"]') + && fromJSON('["namespace-profile-amd64-8x16"]') || fromJSON('["ubuntu-24.04"]') }} steps: - uses: actions/checkout@v6 with: submodules: true - - uses: ./.github/workflows/setup-with-retry - id: setup-step - - name: Cache test routes - id: dependency-cache - uses: actions/cache@v5 - with: - path: /tmp/comma_download_cache - key: proc-replay-${{ hashFiles('selfdrive/test/process_replay/test_processes.py') }} + - run: ./tools/op.sh setup - name: Build openpilot run: scons -j$(nproc) - name: Run replay - timeout-minutes: ${{ contains(runner.name, 'nsc') && (steps.dependency-cache.outputs.cache-hit == 'true') && ((steps.setup-step.outputs.duration < 18) && 1 || 2) || 20 }} + timeout-minutes: ${{ contains(runner.name, 'nsc') && 2 || 20 }} continue-on-error: ${{ github.ref == 'refs/heads/master' }} run: selfdrive/test/process_replay/test_processes.py -j$(nproc) - name: Print diff @@ -184,19 +176,18 @@ jobs: (github.repository == 'commaai/openpilot') && ((github.event_name != 'pull_request') || (github.event.pull_request.head.repo.full_name == 'commaai/openpilot')) - && fromJSON('["namespace-profile-amd64-8x16", "namespace-experiments:docker.builds.local-cache=separate"]') + && fromJSON('["namespace-profile-amd64-8x16"]') || fromJSON('["ubuntu-24.04"]') }} if: false # FIXME: Started to timeout recently steps: - uses: actions/checkout@v6 with: submodules: true - - uses: ./.github/workflows/setup-with-retry - id: setup-step + - run: ./tools/op.sh setup - name: Build openpilot run: scons -j$(nproc) - name: Driving test - timeout-minutes: ${{ (steps.setup-step.outputs.duration < 18) && 1 || 2 }} + timeout-minutes: 2 run: | source selfdrive/test/setup_xvfb.sh pytest -s tools/sim/tests/test_metadrive_bridge.py @@ -207,13 +198,13 @@ jobs: (github.repository == 'commaai/openpilot') && ((github.event_name != 'pull_request') || (github.event.pull_request.head.repo.full_name == 'commaai/openpilot')) - && fromJSON('["namespace-profile-amd64-8x16", "namespace-experiments:docker.builds.local-cache=separate"]') + && fromJSON('["namespace-profile-amd64-8x16"]') || fromJSON('["ubuntu-24.04"]') }} steps: - uses: actions/checkout@v6 with: submodules: true - - uses: ./.github/workflows/setup-with-retry + - run: ./tools/op.sh setup - name: Build openpilot run: scons -j$(nproc) - name: Create UI Report diff --git a/tools/op.sh b/tools/op.sh index f5c5b6082a..9f3d4ee13b 100755 --- a/tools/op.sh +++ b/tools/op.sh @@ -35,6 +35,21 @@ function loge() { fi } +function retry() { + local attempts=$1 + shift + for i in $(seq 1 "$attempts"); do + if "$@"; then + return 0 + fi + if [ "$i" -lt "$attempts" ]; then + echo " Attempt $i/$attempts failed, retrying in 5s..." + sleep 5 + fi + done + return 1 +} + function op_run_command() { CMD="$@" @@ -229,7 +244,7 @@ function op_setup() { echo "Getting git submodules..." st="$(date +%s)" - if ! git submodule update --jobs 4 --init --recursive; then + if ! retry 3 git submodule update --jobs 4 --init --recursive; then echo -e " ↳ [${RED}✗${NC}] Getting git submodules failed!" loge "ERROR_GIT_SUBMODULES" return 1 @@ -239,7 +254,7 @@ function op_setup() { echo "Pulling git lfs files..." st="$(date +%s)" - if ! git lfs pull; then + if ! retry 3 git lfs pull; then echo -e " ↳ [${RED}✗${NC}] Pulling git lfs files failed!" loge "ERROR_GIT_LFS" return 1 @@ -260,6 +275,11 @@ function op_activate_venv() { set +e source $OPENPILOT_ROOT/.venv/bin/activate &> /dev/null || true set -e + + # persist venv on PATH across GitHub Actions steps + if [ -n "$GITHUB_PATH" ]; then + echo "$OPENPILOT_ROOT/.venv/bin" >> "$GITHUB_PATH" + fi } function op_venv() { From 286c4f8403901beb7c7aac3ff12b396791207e14 Mon Sep 17 00:00:00 2001 From: Andi Radulescu Date: Fri, 27 Feb 2026 07:24:51 +0200 Subject: [PATCH 242/311] op.sh: fallback to script's own location for openpilot root (#37326) op: fallback to script's own location for openpilot root --- tools/op.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tools/op.sh b/tools/op.sh index 9f3d4ee13b..2d833d6896 100755 --- a/tools/op.sh +++ b/tools/op.sh @@ -77,7 +77,8 @@ function op_get_openpilot_dir() { done # Fallback to hardcoded directories if not found - for dir in "$HOME/openpilot" "/data/openpilot"; do + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" + for dir in "${SCRIPT_DIR%/tools}" "$HOME/openpilot" "/data/openpilot"; do if [[ -f "$dir/launch_openpilot.sh" ]]; then OPENPILOT_ROOT="$dir" return 0 From de8f7c45842abaf431807ebc94c578bbc3af08da Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 26 Feb 2026 23:24:21 -0800 Subject: [PATCH 243/311] Scroller: rename scroll_to(block) --- system/ui/widgets/scroller.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/system/ui/widgets/scroller.py b/system/ui/widgets/scroller.py index b2aeb55744..2c10ff432a 100644 --- a/system/ui/widgets/scroller.py +++ b/system/ui/widgets/scroller.py @@ -78,7 +78,7 @@ class Scroller(Widget): self._reset_scroll_at_show = True - self._scrolling_to: tuple[float | None, bool] = (None, False) # target offset, block user scrolling + self._scrolling_to: tuple[float | None, bool] = (None, False) # target offset, block_interaction self._scrolling_to_filter = FirstOrderFilter(0.0, SCROLL_RC, 1 / gui_app.target_fps) self._zoom_filter = FirstOrderFilter(1.0, 0.2, 1 / gui_app.target_fps) self._zoom_out_t: float = 0.0 @@ -115,8 +115,8 @@ class Scroller(Widget): def set_reset_scroll_at_show(self, scroll: bool): self._reset_scroll_at_show = scroll - def scroll_to(self, pos: float, smooth: bool = False, block: bool = False): - assert not block or smooth, "Instant scroll cannot be blocking" + def scroll_to(self, pos: float, smooth: bool = False, block_interaction: bool = False): + assert not block_interaction or smooth, "Instant scroll cannot block user interaction" # already there if abs(pos) < 1: @@ -126,7 +126,7 @@ class Scroller(Widget): scroll_offset = self.scroll_panel.get_offset() - pos if smooth: self._scrolling_to_filter.x = self.scroll_panel.get_offset() - self._scrolling_to = scroll_offset, block + self._scrolling_to = scroll_offset, block_interaction else: self.scroll_panel.set_offset(scroll_offset) @@ -167,7 +167,7 @@ class Scroller(Widget): else: self._zoom_filter.update(0.85) - # Cancel auto-scroll if user starts manually scrolling (unless blocking) + # Cancel auto-scroll if user starts manually scrolling (unless block_interaction) if (self.scroll_panel.state in (ScrollState.PRESSED, ScrollState.MANUAL_SCROLL) and self._scrolling_to[0] is not None and not self._scrolling_to[1]): self._scrolling_to = None, False From 0437998bcef43c19b4769058f03aae22218b825c Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 26 Feb 2026 23:25:48 -0800 Subject: [PATCH 244/311] Scroller: add_widgets helper --- system/ui/widgets/scroller.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/system/ui/widgets/scroller.py b/system/ui/widgets/scroller.py index 2c10ff432a..65378688e6 100644 --- a/system/ui/widgets/scroller.py +++ b/system/ui/widgets/scroller.py @@ -109,8 +109,7 @@ class Scroller(Widget): self._pending_lift: set[Widget] = set() self._pending_move: set[Widget] = set() - for item in items: - self.add_widget(item) + self.add_widgets(items) def set_reset_scroll_at_show(self, scroll: bool): self._reset_scroll_at_show = scroll @@ -151,6 +150,10 @@ class Scroller(Widget): and not self.moving_items and (original_touch_valid_callback() if original_touch_valid_callback else True)) + def add_widgets(self, items: list[Widget]) -> None: + for item in items: + self.add_widget(item) + def set_scrolling_enabled(self, enabled: bool | Callable[[], bool]) -> None: """Set whether scrolling is enabled (does not affect widget enabled state).""" self._scroll_enabled = enabled From fe39ffa55ae735c3e18043b677887a4376676e4f Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 27 Feb 2026 00:56:41 -0800 Subject: [PATCH 245/311] mici ui: clear ssh key (#37449) * clear ssh * rev --- selfdrive/ui/mici/layouts/settings/developer.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/selfdrive/ui/mici/layouts/settings/developer.py b/selfdrive/ui/mici/layouts/settings/developer.py index b04d696823..ee0856a20e 100644 --- a/selfdrive/ui/mici/layouts/settings/developer.py +++ b/selfdrive/ui/mici/layouts/settings/developer.py @@ -25,10 +25,14 @@ class DeveloperLayoutMici(NavWidget): else: dlg = BigDialog("", ssh_keys._error_message) gui_app.push_widget(dlg) + else: + ui_state.params.remove("GithubUsername") + ui_state.params.remove("GithubSshKeys") + self._ssh_keys_btn.set_value("Not set") def ssh_keys_callback(): github_username = ui_state.params.get("GithubUsername") or "" - dlg = BigInputDialog("enter GitHub username...", github_username, confirm_callback=github_username_callback) + dlg = BigInputDialog("enter GitHub username...", github_username, minimum_length=0, confirm_callback=github_username_callback) if not system_time_valid(): dlg = BigDialog("Please connect to Wi-Fi to fetch your key", "") gui_app.push_widget(dlg) From 1bf0fb385149796b994186ac56b05f3868c67aa6 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 27 Feb 2026 02:37:02 -0800 Subject: [PATCH 246/311] mici ui: Scroller widget helpers (#37451) * it's so dumb * niceeee * oh this is interesting * this is actually epic * clean up * more clean up * cmt * super * forgot * top --- selfdrive/ui/mici/layouts/main.py | 19 +++-------- selfdrive/ui/mici/layouts/offroad_alerts.py | 13 ++------ .../ui/mici/layouts/settings/developer.py | 17 ++-------- selfdrive/ui/mici/layouts/settings/device.py | 18 +++-------- .../ui/mici/layouts/settings/firehose.py | 1 + .../mici/layouts/settings/network/__init__.py | 12 ++----- .../mici/layouts/settings/network/wifi_ui.py | 14 ++------ .../ui/mici/layouts/settings/settings.py | 20 ++---------- selfdrive/ui/mici/layouts/settings/toggles.py | 16 ++-------- system/ui/widgets/nav_widget.py | 16 +--------- system/ui/widgets/scroller.py | 32 ++++++++++++++++++- 11 files changed, 60 insertions(+), 118 deletions(-) diff --git a/selfdrive/ui/mici/layouts/main.py b/selfdrive/ui/mici/layouts/main.py index 3e3948eeab..e39a228daf 100644 --- a/selfdrive/ui/mici/layouts/main.py +++ b/selfdrive/ui/mici/layouts/main.py @@ -14,9 +14,9 @@ from openpilot.system.ui.lib.application import gui_app ONROAD_DELAY = 2.5 # seconds -class MiciMainLayout(Widget): +class MiciMainLayout(Scroller): def __init__(self): - super().__init__() + super().__init__(snap_items=True, spacing=0, pad=0, scroll_indicator=False, edge_shadows=False) self._pm = messaging.PubMaster(['bookmarkButton']) @@ -36,13 +36,12 @@ class MiciMainLayout(Widget): # TODO: set parent rect and use it if never passed rect from render (like in Scroller) widget.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) - self._scroller = Scroller([ + self._scroller.add_widgets([ self._alerts_layout, self._home_layout, self._onroad_layout, - ], snap_items=True, spacing=0, pad=0, scroll_indicator=False, edge_shadows=False) + ]) self._scroller.set_reset_scroll_at_show(False) - self._scroller.set_enabled(lambda: self.enabled) # for nav stack # Disable scrolling when onroad is interacting with bookmark self._scroller.set_scrolling_enabled(lambda: not self._onroad_layout.is_swiping_left()) @@ -62,14 +61,6 @@ class MiciMainLayout(Widget): self._onroad_layout.set_click_callback(lambda: self._scroll_to(self._home_layout)) device.add_interactive_timeout_callback(self._on_interactive_timeout) - def show_event(self): - super().show_event() - self._scroller.show_event() - - def hide_event(self): - super().hide_event() - self._scroller.hide_event() - def _scroll_to(self, layout: Widget): layout_x = int(layout.rect.x) self._scroller.scroll_to(layout_x, smooth=True) @@ -83,7 +74,7 @@ class MiciMainLayout(Widget): self._setup = True # Render - self._scroller.render(self._rect) + super()._render(self._rect) self._handle_transitions() diff --git a/selfdrive/ui/mici/layouts/offroad_alerts.py b/selfdrive/ui/mici/layouts/offroad_alerts.py index bc1cd02c5d..5ccb815da6 100644 --- a/selfdrive/ui/mici/layouts/offroad_alerts.py +++ b/selfdrive/ui/mici/layouts/offroad_alerts.py @@ -186,19 +186,17 @@ class AlertItem(Widget): rl.draw_texture(icon_texture, int(icon_x), int(icon_y), rl.WHITE) -class MiciOffroadAlerts(Widget): +class MiciOffroadAlerts(Scroller): """Offroad alerts layout with vertical scrolling.""" def __init__(self): - super().__init__() + # Create vertical scroller + super().__init__(horizontal=False, spacing=12, pad=0) self.params = Params() self.sorted_alerts: list[AlertData] = [] self.alert_items: list[AlertItem] = [] self._last_refresh = 0.0 - # Create vertical scroller - self._scroller = Scroller([], horizontal=False, spacing=12, pad=0) - # Create empty state label self._empty_label = UnifiedLabel(tr("no alerts"), 65, FontWeight.DISPLAY, rl.WHITE, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER, @@ -290,14 +288,9 @@ class MiciOffroadAlerts(Widget): def show_event(self): """Reset scroll position when shown and refresh alerts.""" super().show_event() - self._scroller.show_event() self._last_refresh = time.monotonic() self.refresh() - def hide_event(self): - super().hide_event() - self._scroller.hide_event() - def _update_state(self): """Periodically refresh alerts.""" # Refresh alerts periodically, not every frame diff --git a/selfdrive/ui/mici/layouts/settings/developer.py b/selfdrive/ui/mici/layouts/settings/developer.py index ee0856a20e..4d6bdfc3bb 100644 --- a/selfdrive/ui/mici/layouts/settings/developer.py +++ b/selfdrive/ui/mici/layouts/settings/developer.py @@ -1,17 +1,14 @@ -import pyray as rl - from openpilot.common.time_helpers import system_time_valid -from openpilot.system.ui.widgets.scroller import Scroller +from openpilot.system.ui.widgets.scroller import NavScroller from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigToggle, BigParamControl, BigCircleParamControl from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigInputDialog from openpilot.system.ui.lib.application import gui_app -from openpilot.system.ui.widgets.nav_widget import NavWidget from openpilot.selfdrive.ui.layouts.settings.common import restart_needed_callback from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.selfdrive.ui.widgets.ssh_key import SshKeyAction -class DeveloperLayoutMici(NavWidget): +class DeveloperLayoutMici(NavScroller): def __init__(self): super().__init__() self.set_back_callback(gui_app.pop_widget) @@ -61,7 +58,7 @@ class DeveloperLayoutMici(NavWidget): toggle_callback=lambda checked: (gui_app.set_show_touches(checked), gui_app.set_show_fps(checked))) - self._scroller = Scroller([ + self._scroller.add_widgets([ self._adb_toggle, self._ssh_toggle, self._ssh_keys_btn, @@ -105,16 +102,8 @@ class DeveloperLayoutMici(NavWidget): def show_event(self): super().show_event() - self._scroller.show_event() self._update_toggles() - def hide_event(self): - super().hide_event() - self._scroller.hide_event() - - def _render(self, rect: rl.Rectangle): - self._scroller.render(rect) - def _update_toggles(self): ui_state.update_params() diff --git a/selfdrive/ui/mici/layouts/settings/device.py b/selfdrive/ui/mici/layouts/settings/device.py index cd7172455f..7383393542 100644 --- a/selfdrive/ui/mici/layouts/settings/device.py +++ b/selfdrive/ui/mici/layouts/settings/device.py @@ -7,7 +7,7 @@ from collections.abc import Callable from openpilot.common.basedir import BASEDIR from openpilot.common.params import Params from openpilot.common.time_helpers import system_time_valid -from openpilot.system.ui.widgets.scroller import Scroller +from openpilot.system.ui.widgets.scroller import NavScroller from openpilot.system.ui.lib.scroll_panel2 import GuiScrollPanel2 from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigCircleButton from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigConfirmationDialogV2 @@ -32,6 +32,7 @@ class MiciFccModal(NavWidget): self.set_back_callback(gui_app.pop_widget) self._content = HtmlRenderer(file_path=file_path, text=text) self._scroll_panel = GuiScrollPanel2(horizontal=False) + self._scroll_panel.set_enabled(lambda: self.enabled and not self._swiping_away) self._fcc_logo = gui_app.texture("icons_mici/settings/device/fcc_logo.png", 76, 64) def _render(self, rect: rl.Rectangle): @@ -266,7 +267,7 @@ class UpdateOpenpilotBigButton(BigButton): self._waiting_for_updater_t = None -class DeviceLayoutMici(NavWidget): +class DeviceLayoutMici(NavScroller): def __init__(self): super().__init__() @@ -313,7 +314,7 @@ class DeviceLayoutMici(NavWidget): review_training_guide_btn.set_click_callback(lambda: gui_app.push_widget(TrainingGuide(completed_callback=gui_app.pop_widget))) review_training_guide_btn.set_enabled(lambda: ui_state.is_offroad()) - self._scroller = Scroller([ + self._scroller.add_widgets([ DeviceInfoLayoutMici(), UpdateOpenpilotBigButton(), PairBigButton(), @@ -340,14 +341,3 @@ class DeviceLayoutMici(NavWidget): def _offroad_transition(self): self._power_off_btn.set_visible(ui_state.is_offroad()) - - def show_event(self): - super().show_event() - self._scroller.show_event() - - def hide_event(self): - super().hide_event() - self._scroller.hide_event() - - def _render(self, rect: rl.Rectangle): - self._scroller.render(rect) diff --git a/selfdrive/ui/mici/layouts/settings/firehose.py b/selfdrive/ui/mici/layouts/settings/firehose.py index eb3331c868..b2c06d7299 100644 --- a/selfdrive/ui/mici/layouts/settings/firehose.py +++ b/selfdrive/ui/mici/layouts/settings/firehose.py @@ -224,3 +224,4 @@ class FirehoseLayout(FirehoseLayoutBase, NavWidget): def __init__(self): super().__init__() self.set_back_callback(gui_app.pop_widget) + self._scroll_panel.set_enabled(lambda: self.enabled and not self._swiping_away) diff --git a/selfdrive/ui/mici/layouts/settings/network/__init__.py b/selfdrive/ui/mici/layouts/settings/network/__init__.py index e5049a9ee7..ce3f1a817e 100644 --- a/selfdrive/ui/mici/layouts/settings/network/__init__.py +++ b/selfdrive/ui/mici/layouts/settings/network/__init__.py @@ -1,13 +1,12 @@ import pyray as rl -from openpilot.system.ui.widgets.scroller import Scroller +from openpilot.system.ui.widgets.scroller import NavScroller from openpilot.selfdrive.ui.mici.layouts.settings.network.wifi_ui import WifiUIMici, WifiIcon from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigMultiToggle, BigParamControl, BigToggle from openpilot.selfdrive.ui.mici.widgets.dialog import BigInputDialog from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.selfdrive.ui.lib.prime_state import PrimeType from openpilot.system.ui.lib.application import gui_app -from openpilot.system.ui.widgets.nav_widget import NavWidget from openpilot.system.ui.lib.wifi_manager import WifiManager, Network, MeteredType, ConnectStatus, SecurityType, normalize_ssid @@ -65,7 +64,7 @@ class WifiNetworkButton(BigButton): rl.draw_texture_ex(self._lock_txt, (lock_x, lock_y), 0.0, 1.0, rl.WHITE) -class NetworkLayoutMici(NavWidget): +class NetworkLayoutMici(NavScroller): def __init__(self): super().__init__() @@ -132,7 +131,7 @@ class NetworkLayoutMici(NavWidget): self._cellular_metered_btn = BigParamControl("cellular metered", "GsmMetered", toggle_callback=self._toggle_cellular_metered) # Main scroller ---------------------------------- - self._scroller = Scroller([ + self._scroller.add_widgets([ self._wifi_button, self._network_metered_btn, self._tethering_toggle_btn, @@ -165,14 +164,12 @@ class NetworkLayoutMici(NavWidget): def show_event(self): super().show_event() self._wifi_manager.set_active(True) - self._scroller.show_event() # Process wifi callbacks while at any point in the nav stack gui_app.set_nav_stack_tick(self._wifi_manager.process_callbacks) def hide_event(self): super().hide_event() - self._scroller.hide_event() self._wifi_manager.set_active(False) gui_app.set_nav_stack_tick(None) @@ -213,6 +210,3 @@ class NetworkLayoutMici(NavWidget): MeteredType.YES: 'metered', MeteredType.NO: 'unmetered' }.get(self._wifi_manager.current_network_metered, 'default')) - - def _render(self, rect: rl.Rectangle): - self._scroller.render(rect) diff --git a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py index dda1d12220..2fbe23c191 100644 --- a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py +++ b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py @@ -9,8 +9,7 @@ from openpilot.selfdrive.ui.mici.widgets.dialog import BigInputDialog, BigConfir from openpilot.selfdrive.ui.mici.widgets.button import BigButton, LABEL_COLOR from openpilot.system.ui.lib.application import gui_app, MousePos, FontWeight from openpilot.system.ui.widgets import Widget -from openpilot.system.ui.widgets.nav_widget import NavWidget -from openpilot.system.ui.widgets.scroller import Scroller +from openpilot.system.ui.widgets.scroller import NavScroller from openpilot.system.ui.lib.wifi_manager import WifiManager, Network, SecurityType, normalize_ssid @@ -271,15 +270,13 @@ class ForgetButton(Widget): rl.draw_texture_ex(self._trash_txt, (trash_x, trash_y), 0, 1.0, rl.WHITE) -class WifiUIMici(NavWidget): +class WifiUIMici(NavScroller): def __init__(self, wifi_manager: WifiManager): super().__init__() # Set up back navigation self.set_back_callback(gui_app.pop_widget) - self._scroller = Scroller([]) - self._loading_animation = LoadingAnimation() self._wifi_manager = wifi_manager @@ -294,17 +291,12 @@ class WifiUIMici(NavWidget): def show_event(self): # Clear scroller items and update from latest scan results super().show_event() - self._scroller.show_event() self._loading_animation.show_event() self._wifi_manager.set_active(True) self._scroller.items.clear() # trigger button update on latest sorted networks self._on_network_updated(self._wifi_manager.networks) - def hide_event(self): - super().hide_event() - self._scroller.hide_event() - def _on_network_updated(self, networks: list[Network]): self._networks = {network.ssid: network for network in networks} self._update_buttons() @@ -389,7 +381,7 @@ class WifiUIMici(NavWidget): self._loading_animation.show_event() def _render(self, _): - self._scroller.render(self._rect) + super()._render(self._rect) anim_w = 90 anim_x = self._rect.x + self._rect.width - anim_w diff --git a/selfdrive/ui/mici/layouts/settings/settings.py b/selfdrive/ui/mici/layouts/settings/settings.py index 15fd681996..d996e01fed 100644 --- a/selfdrive/ui/mici/layouts/settings/settings.py +++ b/selfdrive/ui/mici/layouts/settings/settings.py @@ -1,7 +1,5 @@ -import pyray as rl - from openpilot.common.params import Params -from openpilot.system.ui.widgets.scroller import Scroller +from openpilot.system.ui.widgets.scroller import NavScroller from openpilot.selfdrive.ui.mici.widgets.button import BigButton from openpilot.selfdrive.ui.mici.layouts.settings.toggles import TogglesLayoutMici from openpilot.selfdrive.ui.mici.layouts.settings.network import NetworkLayoutMici @@ -9,7 +7,6 @@ from openpilot.selfdrive.ui.mici.layouts.settings.device import DeviceLayoutMici from openpilot.selfdrive.ui.mici.layouts.settings.developer import DeveloperLayoutMici from openpilot.selfdrive.ui.mici.layouts.settings.firehose import FirehoseLayout from openpilot.system.ui.lib.application import gui_app, FontWeight -from openpilot.system.ui.widgets.nav_widget import NavWidget class SettingsBigButton(BigButton): @@ -17,7 +14,7 @@ class SettingsBigButton(BigButton): return 64 -class SettingsLayout(NavWidget): +class SettingsLayout(NavScroller): def __init__(self): super().__init__() self._params = Params() @@ -42,7 +39,7 @@ class SettingsLayout(NavWidget): firehose_btn = SettingsBigButton("firehose", "", "icons_mici/settings/firehose.png", icon_size=(52, 62)) firehose_btn.set_click_callback(lambda: gui_app.push_widget(firehose_panel)) - self._scroller = Scroller([ + self._scroller.add_widgets([ toggles_btn, network_btn, device_btn, @@ -56,14 +53,3 @@ class SettingsLayout(NavWidget): self.set_back_callback(gui_app.pop_widget) self._font_medium = gui_app.font(FontWeight.MEDIUM) - - def show_event(self): - super().show_event() - self._scroller.show_event() - - def hide_event(self): - super().hide_event() - self._scroller.hide_event() - - def _render(self, rect: rl.Rectangle): - self._scroller.render(rect) diff --git a/selfdrive/ui/mici/layouts/settings/toggles.py b/selfdrive/ui/mici/layouts/settings/toggles.py index d6a91b40f7..a7a7bff6e2 100644 --- a/selfdrive/ui/mici/layouts/settings/toggles.py +++ b/selfdrive/ui/mici/layouts/settings/toggles.py @@ -1,17 +1,15 @@ -import pyray as rl from cereal import log -from openpilot.system.ui.widgets.scroller import Scroller +from openpilot.system.ui.widgets.scroller import NavScroller from openpilot.selfdrive.ui.mici.widgets.button import BigParamControl, BigMultiParamToggle from openpilot.system.ui.lib.application import gui_app -from openpilot.system.ui.widgets.nav_widget import NavWidget from openpilot.selfdrive.ui.layouts.settings.common import restart_needed_callback from openpilot.selfdrive.ui.ui_state import ui_state PERSONALITY_TO_INT = log.LongitudinalPersonality.schema.enumerants -class TogglesLayoutMici(NavWidget): +class TogglesLayoutMici(NavScroller): def __init__(self): super().__init__() self.set_back_callback(gui_app.pop_widget) @@ -25,7 +23,7 @@ class TogglesLayoutMici(NavWidget): record_mic = BigParamControl("record & upload mic audio", "RecordAudio", toggle_callback=restart_needed_callback) enable_openpilot = BigParamControl("enable openpilot", "OpenpilotEnabledToggle", toggle_callback=restart_needed_callback) - self._scroller = Scroller([ + self._scroller.add_widgets([ self._personality_toggle, self._experimental_btn, is_metric_toggle, @@ -68,13 +66,8 @@ class TogglesLayoutMici(NavWidget): def show_event(self): super().show_event() - self._scroller.show_event() self._update_toggles() - def hide_event(self): - super().hide_event() - self._scroller.hide_event() - def _update_toggles(self): ui_state.update_params() @@ -93,6 +86,3 @@ class TogglesLayoutMici(NavWidget): # Refresh toggles from params to mirror external changes for key, item in self._refresh_toggles: item.set_checked(ui_state.params.get_bool(key)) - - def _render(self, rect: rl.Rectangle): - self._scroller.render(rect) diff --git a/system/ui/widgets/nav_widget.py b/system/ui/widgets/nav_widget.py index 2944f47a76..02afc911b2 100644 --- a/system/ui/widgets/nav_widget.py +++ b/system/ui/widgets/nav_widget.py @@ -69,8 +69,6 @@ class NavWidget(Widget, abc.ABC): self._nav_bar_y_filter = FirstOrderFilter(0.0, 0.1, 1 / gui_app.target_fps) - self._set_up = False - @property def back_enabled(self) -> bool: return self._back_enabled() if callable(self._back_enabled) else self._back_enabled @@ -96,6 +94,7 @@ class NavWidget(Widget, abc.ABC): self._pos_filter.update_alpha(0.04) in_dismiss_area = mouse_event.pos.y < self._rect.height * self.BACK_TOUCH_AREA_PERCENTAGE + # TODO: remove vertical scrolling and then this hacky logic to check if scroller is at top scroller_at_top = False vertical_scroller = False # TODO: -20? snapping in WiFi dialog can make offset not be positive at the top @@ -138,19 +137,6 @@ class NavWidget(Widget, abc.ABC): def _update_state(self): super()._update_state() - # Disable self's scroller while swiping away - if not self._set_up: - self._set_up = True - if hasattr(self, '_scroller'): - # TODO: use touch_valid - original_enabled = self._scroller._enabled - self._scroller.set_enabled(lambda: self.enabled and not self._swiping_away and (original_enabled() if callable(original_enabled) else - original_enabled)) - elif hasattr(self, '_scroll_panel'): - original_enabled = self._scroll_panel.enabled - self._scroll_panel.set_enabled(lambda: self.enabled and not self._swiping_away and (original_enabled() if callable(original_enabled) else - original_enabled)) - if self._trigger_animate_in: self._pos_filter.x = self._rect.height self._nav_bar_y_filter.x = -NAV_BAR_MARGIN - NAV_BAR_HEIGHT diff --git a/system/ui/widgets/scroller.py b/system/ui/widgets/scroller.py index 65378688e6..9d9f5663b8 100644 --- a/system/ui/widgets/scroller.py +++ b/system/ui/widgets/scroller.py @@ -7,6 +7,7 @@ from openpilot.common.swaglog import cloudlog from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.scroll_panel2 import GuiScrollPanel2, ScrollState from openpilot.system.ui.widgets import Widget +from openpilot.system.ui.widgets.nav_widget import NavWidget ITEM_SPACING = 20 LINE_COLOR = rl.GRAY @@ -66,7 +67,8 @@ class ScrollIndicator(Widget): rl.Color(255, 255, 255, int(255 * 0.45))) -class Scroller(Widget): +class _Scroller(Widget): + """Should use wrapper below to reduce boilerplate""" def __init__(self, items: list[Widget], horizontal: bool = True, snap_items: bool = False, spacing: int = ITEM_SPACING, pad: int = ITEM_SPACING, scroll_indicator: bool = True, edge_shadows: bool = True): super().__init__() @@ -414,3 +416,31 @@ class Scroller(Widget): super().hide_event() for item in self._items: item.hide_event() + + +class Scroller(Widget): + """Wrapper for _Scroller so that children do not need to call events or pass down enabled for nav stack.""" + def __init__(self, **kwargs): + super().__init__() + self._scroller = _Scroller([], **kwargs) + # pass down enabled to child widget for nav stack + self._scroller.set_enabled(lambda: self.enabled) + + def show_event(self): + super().show_event() + self._scroller.show_event() + + def hide_event(self): + super().hide_event() + self._scroller.hide_event() + + def _render(self, _): + self._scroller.render(self._rect) + + +class NavScroller(NavWidget, Scroller): + """Full screen Scroller that properly supports nav stack w/ animations""" + def __init__(self, **kwargs): + super().__init__(**kwargs) + # pass down enabled to child widget for nav stack + disable while swiping away NavWidget + self._scroller.set_enabled(lambda: self.enabled and not self._swiping_away) From c5372e904127a44aed4fd01749b7bf5dfefa667b Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Fri, 27 Feb 2026 08:04:24 -0800 Subject: [PATCH 247/311] new demo route (#37456) --- selfdrive/debug/mem_usage.py | 9 ++--- tools/clip/run.py | 68 ++++++++++++++++++++++++++++-------- tools/jotpluggler/pluggle.py | 29 ++++++++++----- tools/plotjuggler/juggle.py | 5 ++- tools/replay/replay.h | 2 +- 5 files changed, 81 insertions(+), 32 deletions(-) diff --git a/selfdrive/debug/mem_usage.py b/selfdrive/debug/mem_usage.py index 3451bfc3d6..66e742f3e6 100755 --- a/selfdrive/debug/mem_usage.py +++ b/selfdrive/debug/mem_usage.py @@ -8,13 +8,14 @@ import numpy as np from openpilot.common.utils import tabulate from openpilot.tools.lib.logreader import LogReader -DEMO_ROUTE = "a2a0ccea32023010|2023-07-27--13-01-19" +DEMO_ROUTE = "5beb9b58bd12b691/0000010a--a51155e496" MB = 1024 * 1024 TABULATE_OPTS = dict(tablefmt="simple_grid", stralign="center", numalign="center") def _get_procs(): from openpilot.selfdrive.test.test_onroad import PROCS + return PROCS @@ -137,9 +138,9 @@ def print_process_tables(op_procs, other_procs, total_mb, use_pss): op_rows, op_total = process_table_rows(op_procs, total_mb, use_pss, show_detail) # filter other: >5MB avg and not bare interpreter paths (test infra noise) - other_filtered = {n: v for n, v in other_procs.items() - if np.mean(v['pss' if use_pss else 'rss']) > 5.0 - and os.path.basename(n.split()[0]) not in ('python', 'python3')} + other_filtered = { + n: v for n, v in other_procs.items() if np.mean(v['pss' if use_pss else 'rss']) > 5.0 and os.path.basename(n.split()[0]) not in ('python', 'python3') + } other_rows, other_total = process_table_rows(other_filtered, total_mb, use_pss, show_detail) rows = op_rows diff --git a/tools/clip/run.py b/tools/clip/run.py index 5711cafa59..0aa90ec0a2 100755 --- a/tools/clip/run.py +++ b/tools/clip/run.py @@ -24,7 +24,7 @@ from openpilot.common.utils import Timer from msgq.visionipc import VisionIpcServer, VisionStreamType FRAMERATE = 20 -DEMO_ROUTE, DEMO_START, DEMO_END = 'a2a0ccea32023010/2023-07-27--13-01-19', 90, 105 +DEMO_ROUTE, DEMO_START, DEMO_END = '5beb9b58bd12b691/0000010a--a51155e496', 90, 105 logger = logging.getLogger('clip') @@ -81,6 +81,7 @@ def _download_segment(path: str) -> bytes: def _parse_and_chunk_segment(args: tuple) -> list[dict]: raw_data, fps = args from openpilot.tools.lib.logreader import _LogFileReader + messages = migrate_all(list(_LogFileReader("", dat=raw_data, sort_by_time=True))) if not messages: return [] @@ -122,6 +123,7 @@ def patch_submaster(message_chunks, ui_state): sm.data[svc] = getattr(msg.as_builder(), svc) sm.logMonoTime[svc], sm.recv_time[svc], sm.recv_frame[svc] = msg.logMonoTime, t, sm.frame sm.frame += 1 + ui_state.sm.update = mock_update @@ -150,8 +152,7 @@ def iter_segment_frames(camera_paths, start_time, end_time, fps=20, use_qcam=Fal if use_qcam: w, h = frame_size or get_frame_dimensions(path) with FileReader(path) as f: - result = subprocess.run(["ffmpeg", "-v", "quiet", "-i", "-", "-f", "rawvideo", "-pix_fmt", "nv12", "-"], - input=f.read(), capture_output=True) + result = subprocess.run(["ffmpeg", "-v", "quiet", "-i", "-", "-f", "rawvideo", "-pix_fmt", "nv12", "-"], input=f.read(), capture_output=True) if result.returncode != 0: raise RuntimeError(f"ffmpeg failed: {result.stderr.decode()}") seg_frames = np.frombuffer(result.stdout, dtype=np.uint8).reshape(-1, w * h * 3 // 2) @@ -172,8 +173,7 @@ class FrameQueue: self.frame_w, self.frame_h = get_frame_dimensions(first_path) self._queue, self._stop, self._error = queue.Queue(maxsize=prefetch_count), threading.Event(), None - self._thread = threading.Thread(target=self._worker, - args=(camera_paths, start_time, end_time, fps, use_qcam, (self.frame_w, self.frame_h)), daemon=True) + self._thread = threading.Thread(target=self._worker, args=(camera_paths, start_time, end_time, fps, use_qcam, (self.frame_w, self.frame_h)), daemon=True) self._thread.start() def _worker(self, camera_paths, start_time, end_time, fps, use_qcam, frame_size): @@ -208,6 +208,7 @@ class FrameQueue: def load_route_metadata(route): from openpilot.common.params import Params, UnknownKeyName + path = next((item for item in route.log_paths() if item), None) if not path: raise Exception('error getting route metadata: cannot find any uploaded logs') @@ -223,15 +224,20 @@ def load_route_metadata(route): origin = init_data.gitRemote.split('/')[3] if len(init_data.gitRemote.split('/')) > 3 else 'unknown' return { - 'version': init_data.version, 'route': route.name.canonical_name, - 'car': car_params.carFingerprint if car_params else 'unknown', 'origin': origin, - 'branch': init_data.gitBranch, 'commit': init_data.gitCommit[:7], 'modified': str(init_data.dirty).lower(), + 'version': init_data.version, + 'route': route.name.canonical_name, + 'car': car_params.carFingerprint if car_params else 'unknown', + 'origin': origin, + 'branch': init_data.gitBranch, + 'commit': init_data.gitCommit[:7], + 'modified': str(init_data.dirty).lower(), } def draw_text_box(text, x, y, size, gui_app, font, color=None, center=False): import pyray as rl from openpilot.system.ui.lib.text_measure import measure_text_cached + box_color, text_color = rl.Color(0, 0, 0, 85), color or rl.WHITE text_size = measure_text_cached(font, text, size) text_width, text_height = int(text_size.x), int(text_size.y) @@ -244,6 +250,7 @@ def draw_text_box(text, x, y, size, gui_app, font, color=None, center=False): def render_overlays(gui_app, font, big, metadata, title, start_time, frame_idx, show_metadata, show_time): from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.lib.wrap_text import wrap_text + metadata_size = 16 if big else 12 title_size = 32 if big else 24 time_size = 24 if big else 16 @@ -259,8 +266,17 @@ def render_overlays(gui_app, font, big, metadata, title, start_time, frame_idx, # Metadata overlay (first 5 seconds) if show_metadata and metadata and frame_idx < FRAMERATE * 5: m = metadata - text = ", ".join([f"openpilot v{m['version']}", f"route: {m['route']}", f"car: {m['car']}", f"origin: {m['origin']}", - f"branch: {m['branch']}", f"commit: {m['commit']}", f"modified: {m['modified']}"]) + text = ", ".join( + [ + f"openpilot v{m['version']}", + f"route: {m['route']}", + f"car: {m['car']}", + f"origin: {m['origin']}", + f"branch: {m['branch']}", + f"commit: {m['commit']}", + f"modified: {m['modified']}", + ] + ) # Wrap text if too wide (leave margin on each side) margin = 2 * (time_width + 10 if show_time else 20) # leave enough margin for time overlay max_width = gui_app.width - margin @@ -278,17 +294,29 @@ def render_overlays(gui_app, font, big, metadata, title, start_time, frame_idx, draw_text_box(title, 0, 60, title_size, gui_app, font, center=True) -def clip(route: Route, output: str, start: int, end: int, headless: bool = True, big: bool = False, - title: str | None = None, show_metadata: bool = True, show_time: bool = True, use_qcam: bool = False): +def clip( + route: Route, + output: str, + start: int, + end: int, + headless: bool = True, + big: bool = False, + title: str | None = None, + show_metadata: bool = True, + show_time: bool = True, + use_qcam: bool = False, +): timer, duration = Timer(), end - start import pyray as rl + if big: from openpilot.selfdrive.ui.onroad.augmented_road_view import AugmentedRoadView else: from openpilot.selfdrive.ui.mici.onroad.augmented_road_view import AugmentedRoadView from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.system.ui.lib.application import gui_app, FontWeight + timer.lap("import") logger.info(f"Clipping {route.name.canonical_name}, {start}s-{end}s ({duration}s)") @@ -297,7 +325,7 @@ def clip(route: Route, output: str, start: int, end: int, headless: bool = True, timer.lap("logs") frame_start = (start - seg_start * 60) * FRAMERATE - message_chunks = all_chunks[frame_start:frame_start + duration * FRAMERATE] + message_chunks = all_chunks[frame_start : frame_start + duration * FRAMERATE] if not message_chunks: logger.error("No messages to render") sys.exit(1) @@ -350,8 +378,18 @@ def main(): args = parse_args() setup_env(args.output, big=args.big, speed=args.speed, target_mb=args.file_size, duration=args.end - args.start) - clip(Route(args.route, data_dir=args.data_dir), args.output, args.start, args.end, not args.windowed, - args.big, args.title, not args.no_metadata, not args.no_time_overlay, args.qcam) + clip( + Route(args.route, data_dir=args.data_dir), + args.output, + args.start, + args.end, + not args.windowed, + args.big, + args.title, + not args.no_metadata, + not args.no_time_overlay, + args.qcam, + ) if __name__ == "__main__": diff --git a/tools/jotpluggler/pluggle.py b/tools/jotpluggler/pluggle.py index 92664ae5b3..67531ce90f 100755 --- a/tools/jotpluggler/pluggle.py +++ b/tools/jotpluggler/pluggle.py @@ -12,7 +12,7 @@ from openpilot.tools.jotpluggler.data import DataManager from openpilot.tools.jotpluggler.datatree import DataTree from openpilot.tools.jotpluggler.layout import LayoutManager -DEMO_ROUTE = "a2a0ccea32023010|2023-07-27--13-01-19" +DEMO_ROUTE = "5beb9b58bd12b691/0000010a--a51155e496" class WorkerManager: @@ -120,6 +120,7 @@ class PlaybackManager: if callback in self.x_axis_observers: self.x_axis_observers.remove(callback) + class MainController: def __init__(self, scale: float = 1.0): self.scale = scale @@ -197,8 +198,12 @@ class MainController: if dpg.does_item_exist("save_layout_dialog"): dpg.delete_item("save_layout_dialog") with dpg.file_dialog( - callback=self._save_layout_callback, tag="save_layout_dialog", width=int(700 * self.scale), height=int(400 * self.scale), - default_filename="layout", default_path=os.path.join(os.path.dirname(os.path.realpath(__file__)), "layouts") + callback=self._save_layout_callback, + tag="save_layout_dialog", + width=int(700 * self.scale), + height=int(400 * self.scale), + default_filename="layout", + default_path=os.path.join(os.path.dirname(os.path.realpath(__file__)), "layouts"), ): dpg.add_file_extension(".yaml") @@ -206,8 +211,11 @@ class MainController: if dpg.does_item_exist("load_layout_dialog"): dpg.delete_item("load_layout_dialog") with dpg.file_dialog( - callback=self._load_layout_callback, tag="load_layout_dialog", width=int(700 * self.scale), height=int(400 * self.scale), - default_path=os.path.join(os.path.dirname(os.path.realpath(__file__)), "layouts") + callback=self._load_layout_callback, + tag="load_layout_dialog", + width=int(700 * self.scale), + height=int(400 * self.scale), + default_path=os.path.join(os.path.dirname(os.path.realpath(__file__)), "layouts"), ): dpg.add_file_extension(".yaml") @@ -314,21 +322,23 @@ def main(route_to_load=None, layout_to_load=None): dpg.create_context() # TODO: find better way of calculating display scaling - #try: + # try: # w, h = next(tuple(map(int, l.split()[0].split('x'))) for l in subprocess.check_output(['xrandr']).decode().split('\n') if '*' in l) # actual resolution # scale = pyautogui.size()[0] / w # scaled resolution - #except Exception: + # except Exception: # scale = 1 scale = 1 with dpg.font_registry(): - default_font = dpg.add_font(os.path.join(BASEDIR, "selfdrive/assets/fonts/JetBrainsMono-Medium.ttf"), int(13 * scale * 2)) # 2x then scale for hidpi + default_font = dpg.add_font(os.path.join(BASEDIR, "selfdrive/assets/fonts/JetBrainsMono-Medium.ttf"), int(13 * scale * 2)) # 2x then scale for hidpi dpg.bind_font(default_font) dpg.set_global_font_scale(0.5) viewport_width, viewport_height = int(1200 * scale), int(800 * scale) dpg.create_viewport( - title='JotPluggler', width=viewport_width, height=viewport_height, + title='JotPluggler', + width=viewport_width, + height=viewport_height, ) dpg.setup_dearpygui() @@ -358,6 +368,7 @@ def main(route_to_load=None, layout_to_load=None): controller.shutdown() dpg.destroy_context() + if __name__ == "__main__": parser = argparse.ArgumentParser(description="A tool for visualizing openpilot logs.") parser.add_argument("--demo", action="store_true", help="Use the demo route instead of providing one") diff --git a/tools/plotjuggler/juggle.py b/tools/plotjuggler/juggle.py index c04efd50b4..0cab39bc69 100755 --- a/tools/plotjuggler/juggle.py +++ b/tools/plotjuggler/juggle.py @@ -21,7 +21,7 @@ juggle_dir = os.path.dirname(os.path.realpath(__file__)) os.environ['LD_LIBRARY_PATH'] = os.environ.get('LD_LIBRARY_PATH', '') + f":{juggle_dir}/bin/" -DEMO_ROUTE = "a2a0ccea32023010|2023-07-27--13-01-19" +DEMO_ROUTE = "5beb9b58bd12b691/0000010a--a51155e496" RELEASES_URL = "https://github.com/commaai/PlotJuggler/releases/download/latest" INSTALL_DIR = os.path.join(juggle_dir, "bin") PLOTJUGGLER_BIN = os.path.join(juggle_dir, "bin/plotjuggler") @@ -105,8 +105,7 @@ def juggle_route(route_or_segment_name, can, layout, dbc, should_migrate): if __name__ == "__main__": - parser = argparse.ArgumentParser(description="A helper to run PlotJuggler on openpilot routes", - formatter_class=argparse.ArgumentDefaultsHelpFormatter) + parser = argparse.ArgumentParser(description="A helper to run PlotJuggler on openpilot routes", formatter_class=argparse.ArgumentDefaultsHelpFormatter) parser.add_argument("--demo", action="store_true", help="Use the demo route instead of providing one") parser.add_argument("--can", action="store_true", help="Parse CAN data") diff --git a/tools/replay/replay.h b/tools/replay/replay.h index 58c1b71b8a..3e2bc7c00e 100644 --- a/tools/replay/replay.h +++ b/tools/replay/replay.h @@ -12,7 +12,7 @@ #include "tools/replay/seg_mgr.h" #include "tools/replay/timeline.h" -#define DEMO_ROUTE "a2a0ccea32023010|2023-07-27--13-01-19" +#define DEMO_ROUTE "5beb9b58bd12b691/0000010a--a51155e496" enum REPLAY_FLAGS { REPLAY_FLAG_NONE = 0x0000, From d899834b63b452c4f7e87b04368bc8ee106c23d2 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Fri, 27 Feb 2026 08:04:45 -0800 Subject: [PATCH 248/311] Revert "new demo route (#37456)" This reverts commit c5372e904127a44aed4fd01749b7bf5dfefa667b. --- selfdrive/debug/mem_usage.py | 9 +++-- tools/clip/run.py | 68 ++++++++---------------------------- tools/jotpluggler/pluggle.py | 29 +++++---------- tools/plotjuggler/juggle.py | 5 +-- tools/replay/replay.h | 2 +- 5 files changed, 32 insertions(+), 81 deletions(-) diff --git a/selfdrive/debug/mem_usage.py b/selfdrive/debug/mem_usage.py index 66e742f3e6..3451bfc3d6 100755 --- a/selfdrive/debug/mem_usage.py +++ b/selfdrive/debug/mem_usage.py @@ -8,14 +8,13 @@ import numpy as np from openpilot.common.utils import tabulate from openpilot.tools.lib.logreader import LogReader -DEMO_ROUTE = "5beb9b58bd12b691/0000010a--a51155e496" +DEMO_ROUTE = "a2a0ccea32023010|2023-07-27--13-01-19" MB = 1024 * 1024 TABULATE_OPTS = dict(tablefmt="simple_grid", stralign="center", numalign="center") def _get_procs(): from openpilot.selfdrive.test.test_onroad import PROCS - return PROCS @@ -138,9 +137,9 @@ def print_process_tables(op_procs, other_procs, total_mb, use_pss): op_rows, op_total = process_table_rows(op_procs, total_mb, use_pss, show_detail) # filter other: >5MB avg and not bare interpreter paths (test infra noise) - other_filtered = { - n: v for n, v in other_procs.items() if np.mean(v['pss' if use_pss else 'rss']) > 5.0 and os.path.basename(n.split()[0]) not in ('python', 'python3') - } + other_filtered = {n: v for n, v in other_procs.items() + if np.mean(v['pss' if use_pss else 'rss']) > 5.0 + and os.path.basename(n.split()[0]) not in ('python', 'python3')} other_rows, other_total = process_table_rows(other_filtered, total_mb, use_pss, show_detail) rows = op_rows diff --git a/tools/clip/run.py b/tools/clip/run.py index 0aa90ec0a2..5711cafa59 100755 --- a/tools/clip/run.py +++ b/tools/clip/run.py @@ -24,7 +24,7 @@ from openpilot.common.utils import Timer from msgq.visionipc import VisionIpcServer, VisionStreamType FRAMERATE = 20 -DEMO_ROUTE, DEMO_START, DEMO_END = '5beb9b58bd12b691/0000010a--a51155e496', 90, 105 +DEMO_ROUTE, DEMO_START, DEMO_END = 'a2a0ccea32023010/2023-07-27--13-01-19', 90, 105 logger = logging.getLogger('clip') @@ -81,7 +81,6 @@ def _download_segment(path: str) -> bytes: def _parse_and_chunk_segment(args: tuple) -> list[dict]: raw_data, fps = args from openpilot.tools.lib.logreader import _LogFileReader - messages = migrate_all(list(_LogFileReader("", dat=raw_data, sort_by_time=True))) if not messages: return [] @@ -123,7 +122,6 @@ def patch_submaster(message_chunks, ui_state): sm.data[svc] = getattr(msg.as_builder(), svc) sm.logMonoTime[svc], sm.recv_time[svc], sm.recv_frame[svc] = msg.logMonoTime, t, sm.frame sm.frame += 1 - ui_state.sm.update = mock_update @@ -152,7 +150,8 @@ def iter_segment_frames(camera_paths, start_time, end_time, fps=20, use_qcam=Fal if use_qcam: w, h = frame_size or get_frame_dimensions(path) with FileReader(path) as f: - result = subprocess.run(["ffmpeg", "-v", "quiet", "-i", "-", "-f", "rawvideo", "-pix_fmt", "nv12", "-"], input=f.read(), capture_output=True) + result = subprocess.run(["ffmpeg", "-v", "quiet", "-i", "-", "-f", "rawvideo", "-pix_fmt", "nv12", "-"], + input=f.read(), capture_output=True) if result.returncode != 0: raise RuntimeError(f"ffmpeg failed: {result.stderr.decode()}") seg_frames = np.frombuffer(result.stdout, dtype=np.uint8).reshape(-1, w * h * 3 // 2) @@ -173,7 +172,8 @@ class FrameQueue: self.frame_w, self.frame_h = get_frame_dimensions(first_path) self._queue, self._stop, self._error = queue.Queue(maxsize=prefetch_count), threading.Event(), None - self._thread = threading.Thread(target=self._worker, args=(camera_paths, start_time, end_time, fps, use_qcam, (self.frame_w, self.frame_h)), daemon=True) + self._thread = threading.Thread(target=self._worker, + args=(camera_paths, start_time, end_time, fps, use_qcam, (self.frame_w, self.frame_h)), daemon=True) self._thread.start() def _worker(self, camera_paths, start_time, end_time, fps, use_qcam, frame_size): @@ -208,7 +208,6 @@ class FrameQueue: def load_route_metadata(route): from openpilot.common.params import Params, UnknownKeyName - path = next((item for item in route.log_paths() if item), None) if not path: raise Exception('error getting route metadata: cannot find any uploaded logs') @@ -224,20 +223,15 @@ def load_route_metadata(route): origin = init_data.gitRemote.split('/')[3] if len(init_data.gitRemote.split('/')) > 3 else 'unknown' return { - 'version': init_data.version, - 'route': route.name.canonical_name, - 'car': car_params.carFingerprint if car_params else 'unknown', - 'origin': origin, - 'branch': init_data.gitBranch, - 'commit': init_data.gitCommit[:7], - 'modified': str(init_data.dirty).lower(), + 'version': init_data.version, 'route': route.name.canonical_name, + 'car': car_params.carFingerprint if car_params else 'unknown', 'origin': origin, + 'branch': init_data.gitBranch, 'commit': init_data.gitCommit[:7], 'modified': str(init_data.dirty).lower(), } def draw_text_box(text, x, y, size, gui_app, font, color=None, center=False): import pyray as rl from openpilot.system.ui.lib.text_measure import measure_text_cached - box_color, text_color = rl.Color(0, 0, 0, 85), color or rl.WHITE text_size = measure_text_cached(font, text, size) text_width, text_height = int(text_size.x), int(text_size.y) @@ -250,7 +244,6 @@ def draw_text_box(text, x, y, size, gui_app, font, color=None, center=False): def render_overlays(gui_app, font, big, metadata, title, start_time, frame_idx, show_metadata, show_time): from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.lib.wrap_text import wrap_text - metadata_size = 16 if big else 12 title_size = 32 if big else 24 time_size = 24 if big else 16 @@ -266,17 +259,8 @@ def render_overlays(gui_app, font, big, metadata, title, start_time, frame_idx, # Metadata overlay (first 5 seconds) if show_metadata and metadata and frame_idx < FRAMERATE * 5: m = metadata - text = ", ".join( - [ - f"openpilot v{m['version']}", - f"route: {m['route']}", - f"car: {m['car']}", - f"origin: {m['origin']}", - f"branch: {m['branch']}", - f"commit: {m['commit']}", - f"modified: {m['modified']}", - ] - ) + text = ", ".join([f"openpilot v{m['version']}", f"route: {m['route']}", f"car: {m['car']}", f"origin: {m['origin']}", + f"branch: {m['branch']}", f"commit: {m['commit']}", f"modified: {m['modified']}"]) # Wrap text if too wide (leave margin on each side) margin = 2 * (time_width + 10 if show_time else 20) # leave enough margin for time overlay max_width = gui_app.width - margin @@ -294,29 +278,17 @@ def render_overlays(gui_app, font, big, metadata, title, start_time, frame_idx, draw_text_box(title, 0, 60, title_size, gui_app, font, center=True) -def clip( - route: Route, - output: str, - start: int, - end: int, - headless: bool = True, - big: bool = False, - title: str | None = None, - show_metadata: bool = True, - show_time: bool = True, - use_qcam: bool = False, -): +def clip(route: Route, output: str, start: int, end: int, headless: bool = True, big: bool = False, + title: str | None = None, show_metadata: bool = True, show_time: bool = True, use_qcam: bool = False): timer, duration = Timer(), end - start import pyray as rl - if big: from openpilot.selfdrive.ui.onroad.augmented_road_view import AugmentedRoadView else: from openpilot.selfdrive.ui.mici.onroad.augmented_road_view import AugmentedRoadView from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.system.ui.lib.application import gui_app, FontWeight - timer.lap("import") logger.info(f"Clipping {route.name.canonical_name}, {start}s-{end}s ({duration}s)") @@ -325,7 +297,7 @@ def clip( timer.lap("logs") frame_start = (start - seg_start * 60) * FRAMERATE - message_chunks = all_chunks[frame_start : frame_start + duration * FRAMERATE] + message_chunks = all_chunks[frame_start:frame_start + duration * FRAMERATE] if not message_chunks: logger.error("No messages to render") sys.exit(1) @@ -378,18 +350,8 @@ def main(): args = parse_args() setup_env(args.output, big=args.big, speed=args.speed, target_mb=args.file_size, duration=args.end - args.start) - clip( - Route(args.route, data_dir=args.data_dir), - args.output, - args.start, - args.end, - not args.windowed, - args.big, - args.title, - not args.no_metadata, - not args.no_time_overlay, - args.qcam, - ) + clip(Route(args.route, data_dir=args.data_dir), args.output, args.start, args.end, not args.windowed, + args.big, args.title, not args.no_metadata, not args.no_time_overlay, args.qcam) if __name__ == "__main__": diff --git a/tools/jotpluggler/pluggle.py b/tools/jotpluggler/pluggle.py index 67531ce90f..92664ae5b3 100755 --- a/tools/jotpluggler/pluggle.py +++ b/tools/jotpluggler/pluggle.py @@ -12,7 +12,7 @@ from openpilot.tools.jotpluggler.data import DataManager from openpilot.tools.jotpluggler.datatree import DataTree from openpilot.tools.jotpluggler.layout import LayoutManager -DEMO_ROUTE = "5beb9b58bd12b691/0000010a--a51155e496" +DEMO_ROUTE = "a2a0ccea32023010|2023-07-27--13-01-19" class WorkerManager: @@ -120,7 +120,6 @@ class PlaybackManager: if callback in self.x_axis_observers: self.x_axis_observers.remove(callback) - class MainController: def __init__(self, scale: float = 1.0): self.scale = scale @@ -198,12 +197,8 @@ class MainController: if dpg.does_item_exist("save_layout_dialog"): dpg.delete_item("save_layout_dialog") with dpg.file_dialog( - callback=self._save_layout_callback, - tag="save_layout_dialog", - width=int(700 * self.scale), - height=int(400 * self.scale), - default_filename="layout", - default_path=os.path.join(os.path.dirname(os.path.realpath(__file__)), "layouts"), + callback=self._save_layout_callback, tag="save_layout_dialog", width=int(700 * self.scale), height=int(400 * self.scale), + default_filename="layout", default_path=os.path.join(os.path.dirname(os.path.realpath(__file__)), "layouts") ): dpg.add_file_extension(".yaml") @@ -211,11 +206,8 @@ class MainController: if dpg.does_item_exist("load_layout_dialog"): dpg.delete_item("load_layout_dialog") with dpg.file_dialog( - callback=self._load_layout_callback, - tag="load_layout_dialog", - width=int(700 * self.scale), - height=int(400 * self.scale), - default_path=os.path.join(os.path.dirname(os.path.realpath(__file__)), "layouts"), + callback=self._load_layout_callback, tag="load_layout_dialog", width=int(700 * self.scale), height=int(400 * self.scale), + default_path=os.path.join(os.path.dirname(os.path.realpath(__file__)), "layouts") ): dpg.add_file_extension(".yaml") @@ -322,23 +314,21 @@ def main(route_to_load=None, layout_to_load=None): dpg.create_context() # TODO: find better way of calculating display scaling - # try: + #try: # w, h = next(tuple(map(int, l.split()[0].split('x'))) for l in subprocess.check_output(['xrandr']).decode().split('\n') if '*' in l) # actual resolution # scale = pyautogui.size()[0] / w # scaled resolution - # except Exception: + #except Exception: # scale = 1 scale = 1 with dpg.font_registry(): - default_font = dpg.add_font(os.path.join(BASEDIR, "selfdrive/assets/fonts/JetBrainsMono-Medium.ttf"), int(13 * scale * 2)) # 2x then scale for hidpi + default_font = dpg.add_font(os.path.join(BASEDIR, "selfdrive/assets/fonts/JetBrainsMono-Medium.ttf"), int(13 * scale * 2)) # 2x then scale for hidpi dpg.bind_font(default_font) dpg.set_global_font_scale(0.5) viewport_width, viewport_height = int(1200 * scale), int(800 * scale) dpg.create_viewport( - title='JotPluggler', - width=viewport_width, - height=viewport_height, + title='JotPluggler', width=viewport_width, height=viewport_height, ) dpg.setup_dearpygui() @@ -368,7 +358,6 @@ def main(route_to_load=None, layout_to_load=None): controller.shutdown() dpg.destroy_context() - if __name__ == "__main__": parser = argparse.ArgumentParser(description="A tool for visualizing openpilot logs.") parser.add_argument("--demo", action="store_true", help="Use the demo route instead of providing one") diff --git a/tools/plotjuggler/juggle.py b/tools/plotjuggler/juggle.py index 0cab39bc69..c04efd50b4 100755 --- a/tools/plotjuggler/juggle.py +++ b/tools/plotjuggler/juggle.py @@ -21,7 +21,7 @@ juggle_dir = os.path.dirname(os.path.realpath(__file__)) os.environ['LD_LIBRARY_PATH'] = os.environ.get('LD_LIBRARY_PATH', '') + f":{juggle_dir}/bin/" -DEMO_ROUTE = "5beb9b58bd12b691/0000010a--a51155e496" +DEMO_ROUTE = "a2a0ccea32023010|2023-07-27--13-01-19" RELEASES_URL = "https://github.com/commaai/PlotJuggler/releases/download/latest" INSTALL_DIR = os.path.join(juggle_dir, "bin") PLOTJUGGLER_BIN = os.path.join(juggle_dir, "bin/plotjuggler") @@ -105,7 +105,8 @@ def juggle_route(route_or_segment_name, can, layout, dbc, should_migrate): if __name__ == "__main__": - parser = argparse.ArgumentParser(description="A helper to run PlotJuggler on openpilot routes", formatter_class=argparse.ArgumentDefaultsHelpFormatter) + parser = argparse.ArgumentParser(description="A helper to run PlotJuggler on openpilot routes", + formatter_class=argparse.ArgumentDefaultsHelpFormatter) parser.add_argument("--demo", action="store_true", help="Use the demo route instead of providing one") parser.add_argument("--can", action="store_true", help="Parse CAN data") diff --git a/tools/replay/replay.h b/tools/replay/replay.h index 3e2bc7c00e..58c1b71b8a 100644 --- a/tools/replay/replay.h +++ b/tools/replay/replay.h @@ -12,7 +12,7 @@ #include "tools/replay/seg_mgr.h" #include "tools/replay/timeline.h" -#define DEMO_ROUTE "5beb9b58bd12b691/0000010a--a51155e496" +#define DEMO_ROUTE "a2a0ccea32023010|2023-07-27--13-01-19" enum REPLAY_FLAGS { REPLAY_FLAG_NONE = 0x0000, From a1f4ba55bf55a2d5565d500d703ef0f0e58e833b Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Fri, 27 Feb 2026 08:05:06 -0800 Subject: [PATCH 249/311] nicer scons output (#37455) --- SConstruct | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/SConstruct b/SConstruct index 8d58cc012a..9b16dddccd 100644 --- a/SConstruct +++ b/SConstruct @@ -18,6 +18,7 @@ AddOption('--asan', action='store_true', help='turn on ASAN') AddOption('--ubsan', action='store_true', help='turn on UBSan') AddOption('--mutation', action='store_true', help='generate mutation-ready code') AddOption('--ccflags', action='store', type='string', default='', help='pass arbitrary flags over the command line') +AddOption('--verbose', action='store_true', default=False, help='show full build commands') AddOption('--minimal', action='store_false', dest='extras', @@ -148,6 +149,22 @@ if _extra_cc: if arch != "Darwin": env.Append(LINKFLAGS=["-Wl,--as-needed", "-Wl,--no-undefined"]) +# Shorter build output: show brief descriptions instead of full commands. +# Full command lines are still printed on failure by scons. +if not GetOption('verbose'): + for action, short in ( + ("CC", "CC"), + ("CXX", "CXX"), + ("LINK", "LINK"), + ("SHCC", "CC"), + ("SHCXX", "CXX"), + ("SHLINK", "LINK"), + ("AR", "AR"), + ("RANLIB", "RANLIB"), + ("AS", "AS"), + ): + env[f"{action}COMSTR"] = f" [{short}] $TARGET" + # progress output node_interval = 5 node_count = 0 From e1a4189c1fbf9cd023ac0eb259f2af40c407ce9f Mon Sep 17 00:00:00 2001 From: Andi Radulescu Date: Fri, 27 Feb 2026 21:51:01 +0200 Subject: [PATCH 250/311] op.sh: add 'op script' subcommand with som-debug (#37325) * op: add som-debug command for SOM serial debug via panda * op: namespace som-debug under 'op script' subcommand --- tools/op.sh | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tools/op.sh b/tools/op.sh index 2d833d6896..7c20403a27 100755 --- a/tools/op.sh +++ b/tools/op.sh @@ -311,6 +311,19 @@ function op_ssh() { op_run_command tools/scripts/ssh.py "$@" } +function op_script() { + op_before_cmd + + case $1 in + som-debug ) op_run_command panda/scripts/som_debug.sh "${@:2}" ;; + * ) + echo -e "Unknown script '$1'. Available scripts:" + echo -e " ${BOLD}som-debug${NC} SOM serial debug console via panda" + return 1 + ;; + esac +} + function op_check() { VERBOSE=1 op_before_cmd @@ -441,6 +454,9 @@ function op_default() { echo -e " ${BOLD}adb${NC} Run adb shell" echo -e " ${BOLD}ssh${NC} comma prime SSH helper" echo "" + echo -e "${BOLD}${UNDERLINE}Commands [Scripts]:${NC}" + echo -e " ${BOLD}script${NC} Run a script (e.g. op script som-debug)" + echo "" echo -e "${BOLD}${UNDERLINE}Commands [Testing]:${NC}" echo -e " ${BOLD}sim${NC} Run openpilot in a simulator" echo -e " ${BOLD}lint${NC} Run the linter" @@ -500,6 +516,7 @@ function _op() { post-commit ) shift 1; op_install_post_commit "$@" ;; adb ) shift 1; op_adb "$@" ;; ssh ) shift 1; op_ssh "$@" ;; + script ) shift 1; op_script "$@" ;; * ) op_default "$@" ;; esac } From 276713ddf9ee3e4fd7d2c3c0402fa0d7450547e8 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Fri, 27 Feb 2026 12:10:38 -0800 Subject: [PATCH 251/311] add back bz2 support with vendored bzip2 (#37459) * add back bz2 support with vendored bzip2 Reverts f4a36f7f7 ("rm cpp bz2") to restore bzip2 decompression support in replay/cabana tools, and replaces the system libbz2-dev with a vendored bzip2 package from commaai/dependencies. Co-Authored-By: Claude Opus 4.6 * relock bzip2 from releases branch Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: Claude Opus 4.6 --- SConstruct | 3 +- pyproject.toml | 1 + tools/cabana/SConscript | 2 +- tools/cabana/tests/test_cabana.cc | 2 +- tools/replay/SConscript | 2 +- tools/replay/logreader.cc | 4 +- tools/replay/route.cc | 4 +- tools/replay/tests/test_replay.cc | 4 +- tools/replay/util.cc | 42 ++++++++++++ tools/replay/util.h | 2 + uv.lock | 109 ++++++++++++++++-------------- 11 files changed, 115 insertions(+), 60 deletions(-) diff --git a/SConstruct b/SConstruct index 9b16dddccd..59ffaf4c76 100644 --- a/SConstruct +++ b/SConstruct @@ -39,6 +39,7 @@ assert arch in [ ] if arch != "larch64": + import bzip2 import capnproto import eigen import ffmpeg as ffmpeg_pkg @@ -48,7 +49,7 @@ if arch != "larch64": import python3_dev import zeromq import zstd - pkgs = [capnproto, eigen, ffmpeg_pkg, libjpeg, ncurses, openssl3, zeromq, zstd] + pkgs = [bzip2, capnproto, eigen, ffmpeg_pkg, libjpeg, ncurses, openssl3, zeromq, zstd] py_include = python3_dev.INCLUDE_DIR else: # TODO: remove when AGNOS has our new vendor pkgs diff --git a/pyproject.toml b/pyproject.toml index bdcbd77801..699c3af6f0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,7 @@ dependencies = [ "numpy >=2.0", # vendored native dependencies + "bzip2 @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=bzip2", "capnproto @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=capnproto", "eigen @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=eigen", "ffmpeg @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=ffmpeg", diff --git a/tools/cabana/SConscript b/tools/cabana/SConscript index 89e69e7dd4..e172278d91 100644 --- a/tools/cabana/SConscript +++ b/tools/cabana/SConscript @@ -81,7 +81,7 @@ if arch == "Darwin": cabana_env['CPPPATH'] += [f"{brew_prefix}/include"] cabana_env['LIBPATH'] += [f"{brew_prefix}/lib"] -cabana_libs = [cereal, messaging, visionipc, replay_lib, 'avformat', 'avcodec', 'swresample', 'avutil', 'x264', 'z', 'zstd', 'curl', 'yuv', 'usb-1.0'] + qt_libs +cabana_libs = [cereal, messaging, visionipc, replay_lib, 'avformat', 'avcodec', 'swresample', 'avutil', 'x264', 'z', 'bz2', 'zstd', 'curl', 'yuv', 'usb-1.0'] + qt_libs opendbc_path = '-DOPENDBC_FILE_PATH=\'"%s"\'' % (cabana_env.Dir("../../opendbc/dbc").abspath) cabana_env['CXXFLAGS'] += [opendbc_path] diff --git a/tools/cabana/tests/test_cabana.cc b/tools/cabana/tests/test_cabana.cc index 4c11bfc8b8..d9fcae6f21 100644 --- a/tools/cabana/tests/test_cabana.cc +++ b/tools/cabana/tests/test_cabana.cc @@ -5,7 +5,7 @@ #include "catch2/catch.hpp" #include "tools/cabana/dbc/dbcmanager.h" -const std::string TEST_RLOG_URL = "https://commadataci.blob.core.windows.net/openpilotci/0c94aa1e1296d7c6/2021-05-05--19-48-37/0/rlog.zst"; +const std::string TEST_RLOG_URL = "https://commadataci.blob.core.windows.net/openpilotci/0c94aa1e1296d7c6/2021-05-05--19-48-37/0/rlog.bz2"; TEST_CASE("DBCFile::generateDBC") { QString fn = QString("%1/%2.dbc").arg(OPENDBC_FILE_PATH, "tesla_can"); diff --git a/tools/replay/SConscript b/tools/replay/SConscript index 47b25df166..b39cf6dab1 100644 --- a/tools/replay/SConscript +++ b/tools/replay/SConscript @@ -12,7 +12,7 @@ if arch != "Darwin": replay_lib_src.append("qcom_decoder.cc") replay_lib = replay_env.Library("replay", replay_lib_src, LIBS=base_libs, FRAMEWORKS=base_frameworks) Export('replay_lib') -replay_libs = [replay_lib, 'avformat', 'avcodec', 'swresample', 'avutil', 'x264', 'z', 'zstd', 'curl', 'yuv', 'ncurses'] + base_libs +replay_libs = [replay_lib, 'avformat', 'avcodec', 'swresample', 'avutil', 'x264', 'z', 'bz2', 'zstd', 'curl', 'yuv', 'ncurses'] + base_libs replay_env.Program("replay", ["main.cc"], LIBS=replay_libs, FRAMEWORKS=base_frameworks) if GetOption('extras'): diff --git a/tools/replay/logreader.cc b/tools/replay/logreader.cc index 0d9e053aba..75abb8417b 100644 --- a/tools/replay/logreader.cc +++ b/tools/replay/logreader.cc @@ -9,7 +9,9 @@ bool LogReader::load(const std::string &url, std::atomic *abort, bool local_cache, int chunk_size, int retries) { std::string data = FileReader(local_cache, chunk_size, retries).read(url, abort); if (!data.empty()) { - if (url.find(".zst") != std::string::npos || util::starts_with(data, "\x28\xB5\x2F\xFD")) { + if (url.find(".bz2") != std::string::npos || util::starts_with(data, "BZh9")) { + data = decompressBZ2(data, abort); + } else if (url.find(".zst") != std::string::npos || util::starts_with(data, "\x28\xB5\x2F\xFD")) { data = decompressZST(data, abort); } } diff --git a/tools/replay/route.cc b/tools/replay/route.cc index 663c4b43cb..ba00828267 100644 --- a/tools/replay/route.cc +++ b/tools/replay/route.cc @@ -174,9 +174,9 @@ void Route::addFileToSegment(int n, const std::string &file) { auto pos = name.find_last_of("--"); name = pos != std::string::npos ? name.substr(pos + 2) : name; - if (name == "rlog.zst" || name == "rlog") { + if (name == "rlog.bz2" || name == "rlog.zst" || name == "rlog") { segments_[n].rlog = file; - } else if (name == "qlog.zst" || name == "qlog") { + } else if (name == "qlog.bz2" || name == "qlog.zst" || name == "qlog") { segments_[n].qlog = file; } else if (name == "fcamera.hevc") { segments_[n].road_cam = file; diff --git a/tools/replay/tests/test_replay.cc b/tools/replay/tests/test_replay.cc index f4afc29968..aed3de59a8 100644 --- a/tools/replay/tests/test_replay.cc +++ b/tools/replay/tests/test_replay.cc @@ -2,14 +2,14 @@ #include "catch2/catch.hpp" #include "tools/replay/replay.h" -const std::string TEST_RLOG_URL = "https://commadataci.blob.core.windows.net/openpilotci/0c94aa1e1296d7c6/2021-05-05--19-48-37/0/rlog.zst"; +const std::string TEST_RLOG_URL = "https://commadataci.blob.core.windows.net/openpilotci/0c94aa1e1296d7c6/2021-05-05--19-48-37/0/rlog.bz2"; TEST_CASE("LogReader") { SECTION("corrupt log") { FileReader reader(true); std::string corrupt_content = reader.read(TEST_RLOG_URL); corrupt_content.resize(corrupt_content.length() / 2); - corrupt_content = decompressZST(corrupt_content); + corrupt_content = decompressBZ2(corrupt_content); LogReader log; REQUIRE(log.load(corrupt_content.data(), corrupt_content.size())); REQUIRE(log.events.size() > 0); diff --git a/tools/replay/util.cc b/tools/replay/util.cc index cc37c19ecf..481564322e 100644 --- a/tools/replay/util.cc +++ b/tools/replay/util.cc @@ -1,5 +1,6 @@ #include "tools/replay/util.h" +#include #include #include @@ -279,6 +280,47 @@ bool httpDownload(const std::string &url, const std::string &file, size_t chunk_ return httpDownload(url, of, chunk_size, size, abort); } +std::string decompressBZ2(const std::string &in, std::atomic *abort) { + return decompressBZ2((std::byte *)in.data(), in.size(), abort); +} + +std::string decompressBZ2(const std::byte *in, size_t in_size, std::atomic *abort) { + if (in_size == 0) return {}; + + bz_stream strm = {}; + int bzerror = BZ2_bzDecompressInit(&strm, 0, 0); + assert(bzerror == BZ_OK); + + strm.next_in = (char *)in; + strm.avail_in = in_size; + std::string out(in_size * 5, '\0'); + do { + strm.next_out = (char *)(&out[strm.total_out_lo32]); + strm.avail_out = out.size() - strm.total_out_lo32; + + const char *prev_write_pos = strm.next_out; + bzerror = BZ2_bzDecompress(&strm); + if (bzerror == BZ_OK && prev_write_pos == strm.next_out) { + // content is corrupt + bzerror = BZ_STREAM_END; + rWarning("decompressBZ2 error: content is corrupt"); + break; + } + + if (bzerror == BZ_OK && strm.avail_in > 0 && strm.avail_out == 0) { + out.resize(out.size() * 2); + } + } while (bzerror == BZ_OK && !(abort && *abort)); + + BZ2_bzDecompressEnd(&strm); + if (bzerror == BZ_STREAM_END && !(abort && *abort)) { + out.resize(strm.total_out_lo32); + out.shrink_to_fit(); + return out; + } + return {}; +} + std::string decompressZST(const std::string &in, std::atomic *abort) { return decompressZST((std::byte *)in.data(), in.size(), abort); } diff --git a/tools/replay/util.h b/tools/replay/util.h index fc4d2d54f9..1f61951d21 100644 --- a/tools/replay/util.h +++ b/tools/replay/util.h @@ -48,6 +48,8 @@ private: std::string sha256(const std::string &str); void precise_nano_sleep(int64_t nanoseconds, std::atomic &interrupt_requested); +std::string decompressBZ2(const std::string &in, std::atomic *abort = nullptr); +std::string decompressBZ2(const std::byte *in, size_t in_size, std::atomic *abort = nullptr); std::string decompressZST(const std::string &in, std::atomic *abort = nullptr); std::string decompressZST(const std::byte *in, size_t in_size, std::atomic *abort = nullptr); std::string getUrlWithoutQuery(const std::string &url); diff --git a/uv.lock b/uv.lock index 9094417693..63c8c4cffa 100644 --- a/uv.lock +++ b/uv.lock @@ -113,10 +113,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c8/b9/275df9607f7fb44317ccb1d4be74827185c0d410f52b6e2cd770fe209118/av-16.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:f49243b1d27c91cd8c66fdba90a674e344eb8eb917264f36117bf2b6879118fd", size = 31752045, upload-time = "2026-01-11T09:57:45.106Z" }, ] +[[package]] +name = "bzip2" +version = "1.0.8" +source = { git = "https://github.com/commaai/dependencies.git?subdirectory=bzip2&rev=releases#11895d3f6bc62a229e84c87505948f15f9018ce0" } + [[package]] name = "capnproto" version = "1.0.1" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=capnproto&rev=releases#31af284805d0787a689e129311d992bec14a2400" } +source = { git = "https://github.com/commaai/dependencies.git?subdirectory=capnproto&rev=releases#11895d3f6bc62a229e84c87505948f15f9018ce0" } [[package]] name = "casadi" @@ -137,11 +142,11 @@ wheels = [ [[package]] name = "certifi" -version = "2026.1.4" +version = "2026.2.25" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, ] [[package]] @@ -376,7 +381,7 @@ wheels = [ [[package]] name = "eigen" version = "3.4.0" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=eigen&rev=releases#31af284805d0787a689e129311d992bec14a2400" } +source = { git = "https://github.com/commaai/dependencies.git?subdirectory=eigen&rev=releases#11895d3f6bc62a229e84c87505948f15f9018ce0" } [[package]] name = "execnet" @@ -390,7 +395,7 @@ wheels = [ [[package]] name = "ffmpeg" version = "7.1.0" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=ffmpeg&rev=releases#31af284805d0787a689e129311d992bec14a2400" } +source = { git = "https://github.com/commaai/dependencies.git?subdirectory=ffmpeg&rev=releases#11895d3f6bc62a229e84c87505948f15f9018ce0" } [[package]] name = "fonttools" @@ -437,7 +442,7 @@ wheels = [ [[package]] name = "gcc-arm-none-eabi" version = "13.2.1" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=gcc-arm-none-eabi&rev=releases#31af284805d0787a689e129311d992bec14a2400" } +source = { git = "https://github.com/commaai/dependencies.git?subdirectory=gcc-arm-none-eabi&rev=releases#11895d3f6bc62a229e84c87505948f15f9018ce0" } [[package]] name = "ghp-import" @@ -454,7 +459,7 @@ wheels = [ [[package]] name = "git-lfs" version = "3.6.1" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=git-lfs&rev=releases#31af284805d0787a689e129311d992bec14a2400" } +source = { git = "https://github.com/commaai/dependencies.git?subdirectory=git-lfs&rev=releases#11895d3f6bc62a229e84c87505948f15f9018ce0" } [[package]] name = "google-crc32c" @@ -572,7 +577,7 @@ wheels = [ [[package]] name = "libjpeg" version = "3.1.0" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=libjpeg&rev=releases#31af284805d0787a689e129311d992bec14a2400" } +source = { git = "https://github.com/commaai/dependencies.git?subdirectory=libjpeg&rev=releases#11895d3f6bc62a229e84c87505948f15f9018ce0" } [[package]] name = "libusb1" @@ -735,7 +740,7 @@ wheels = [ [[package]] name = "ncurses" version = "6.5" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=ncurses&rev=releases#31af284805d0787a689e129311d992bec14a2400" } +source = { git = "https://github.com/commaai/dependencies.git?subdirectory=ncurses&rev=releases#11895d3f6bc62a229e84c87505948f15f9018ce0" } [[package]] name = "numpy" @@ -782,6 +787,7 @@ dependencies = [ { name = "aiohttp" }, { name = "aiortc" }, { name = "av" }, + { name = "bzip2" }, { name = "capnproto" }, { name = "casadi" }, { name = "cffi" }, @@ -857,6 +863,7 @@ requires-dist = [ { name = "aiohttp" }, { name = "aiortc" }, { name = "av" }, + { name = "bzip2", git = "https://github.com/commaai/dependencies.git?subdirectory=bzip2&rev=releases" }, { name = "capnproto", git = "https://github.com/commaai/dependencies.git?subdirectory=capnproto&rev=releases" }, { name = "casadi", specifier = ">=3.6.6" }, { name = "cffi" }, @@ -921,7 +928,7 @@ provides-extras = ["docs", "testing", "dev", "tools"] [[package]] name = "openssl3" version = "3.4.1" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=openssl3&rev=releases#31af284805d0787a689e129311d992bec14a2400" } +source = { git = "https://github.com/commaai/dependencies.git?subdirectory=openssl3&rev=releases#11895d3f6bc62a229e84c87505948f15f9018ce0" } [[package]] name = "packaging" @@ -1292,7 +1299,7 @@ wheels = [ [[package]] name = "python3-dev" version = "3.12.8" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=python3-dev&rev=releases#31af284805d0787a689e129311d992bec14a2400" } +source = { git = "https://github.com/commaai/dependencies.git?subdirectory=python3-dev&rev=releases#11895d3f6bc62a229e84c87505948f15f9018ce0" } [[package]] name = "pyyaml" @@ -1401,27 +1408,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.15.2" +version = "0.15.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/06/04/eab13a954e763b0606f460443fcbf6bb5a0faf06890ea3754ff16523dce5/ruff-0.15.2.tar.gz", hash = "sha256:14b965afee0969e68bb871eba625343b8673375f457af4abe98553e8bbb98342", size = 4558148, upload-time = "2026-02-19T22:32:20.271Z" } +sdist = { url = "https://files.pythonhosted.org/packages/da/31/d6e536cdebb6568ae75a7f00e4b4819ae0ad2640c3604c305a0428680b0c/ruff-0.15.4.tar.gz", hash = "sha256:3412195319e42d634470cc97aa9803d07e9d5c9223b99bcb1518f0c725f26ae1", size = 4569550, upload-time = "2026-02-26T20:04:14.959Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2f/70/3a4dc6d09b13cb3e695f28307e5d889b2e1a66b7af9c5e257e796695b0e6/ruff-0.15.2-py3-none-linux_armv6l.whl", hash = "sha256:120691a6fdae2f16d65435648160f5b81a9625288f75544dc40637436b5d3c0d", size = 10430565, upload-time = "2026-02-19T22:32:41.824Z" }, - { url = "https://files.pythonhosted.org/packages/71/0b/bb8457b56185ece1305c666dc895832946d24055be90692381c31d57466d/ruff-0.15.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a89056d831256099658b6bba4037ac6dd06f49d194199215befe2bb10457ea5e", size = 10820354, upload-time = "2026-02-19T22:32:07.366Z" }, - { url = "https://files.pythonhosted.org/packages/2d/c1/e0532d7f9c9e0b14c46f61b14afd563298b8b83f337b6789ddd987e46121/ruff-0.15.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e36dee3a64be0ebd23c86ffa3aa3fd3ac9a712ff295e192243f814a830b6bd87", size = 10170767, upload-time = "2026-02-19T22:32:13.188Z" }, - { url = "https://files.pythonhosted.org/packages/47/e8/da1aa341d3af017a21c7a62fb5ec31d4e7ad0a93ab80e3a508316efbcb23/ruff-0.15.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9fb47b6d9764677f8c0a193c0943ce9a05d6763523f132325af8a858eadc2b9", size = 10529591, upload-time = "2026-02-19T22:32:02.547Z" }, - { url = "https://files.pythonhosted.org/packages/93/74/184fbf38e9f3510231fbc5e437e808f0b48c42d1df9434b208821efcd8d6/ruff-0.15.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f376990f9d0d6442ea9014b19621d8f2aaf2b8e39fdbfc79220b7f0c596c9b80", size = 10260771, upload-time = "2026-02-19T22:32:36.938Z" }, - { url = "https://files.pythonhosted.org/packages/05/ac/605c20b8e059a0bc4b42360414baa4892ff278cec1c91fff4be0dceedefd/ruff-0.15.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2dcc987551952d73cbf5c88d9fdee815618d497e4df86cd4c4824cc59d5dd75f", size = 11045791, upload-time = "2026-02-19T22:32:31.642Z" }, - { url = "https://files.pythonhosted.org/packages/fd/52/db6e419908f45a894924d410ac77d64bdd98ff86901d833364251bd08e22/ruff-0.15.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:42a47fd785cbe8c01b9ff45031af875d101b040ad8f4de7bbb716487c74c9a77", size = 11879271, upload-time = "2026-02-19T22:32:29.305Z" }, - { url = "https://files.pythonhosted.org/packages/3e/d8/7992b18f2008bdc9231d0f10b16df7dda964dbf639e2b8b4c1b4e91b83af/ruff-0.15.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cbe9f49354866e575b4c6943856989f966421870e85cd2ac94dccb0a9dcb2fea", size = 11303707, upload-time = "2026-02-19T22:32:22.492Z" }, - { url = "https://files.pythonhosted.org/packages/d7/02/849b46184bcfdd4b64cde61752cc9a146c54759ed036edd11857e9b8443b/ruff-0.15.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7a672c82b5f9887576087d97be5ce439f04bbaf548ee987b92d3a7dede41d3a", size = 11149151, upload-time = "2026-02-19T22:32:44.234Z" }, - { url = "https://files.pythonhosted.org/packages/70/04/f5284e388bab60d1d3b99614a5a9aeb03e0f333847e2429bebd2aaa1feec/ruff-0.15.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:72ecc64f46f7019e2bcc3cdc05d4a7da958b629a5ab7033195e11a438403d956", size = 11091132, upload-time = "2026-02-19T22:32:24.691Z" }, - { url = "https://files.pythonhosted.org/packages/fa/ae/88d844a21110e14d92cf73d57363fab59b727ebeabe78009b9ccb23500af/ruff-0.15.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:8dcf243b15b561c655c1ef2f2b0050e5d50db37fe90115507f6ff37d865dc8b4", size = 10504717, upload-time = "2026-02-19T22:32:26.75Z" }, - { url = "https://files.pythonhosted.org/packages/64/27/867076a6ada7f2b9c8292884ab44d08fd2ba71bd2b5364d4136f3cd537e1/ruff-0.15.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dab6941c862c05739774677c6273166d2510d254dac0695c0e3f5efa1b5585de", size = 10263122, upload-time = "2026-02-19T22:32:10.036Z" }, - { url = "https://files.pythonhosted.org/packages/e7/ef/faf9321d550f8ebf0c6373696e70d1758e20ccdc3951ad7af00c0956be7c/ruff-0.15.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1b9164f57fc36058e9a6806eb92af185b0697c9fe4c7c52caa431c6554521e5c", size = 10735295, upload-time = "2026-02-19T22:32:39.227Z" }, - { url = "https://files.pythonhosted.org/packages/2f/55/e8089fec62e050ba84d71b70e7834b97709ca9b7aba10c1a0b196e493f97/ruff-0.15.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:80d24fcae24d42659db7e335b9e1531697a7102c19185b8dc4a028b952865fd8", size = 11241641, upload-time = "2026-02-19T22:32:34.617Z" }, - { url = "https://files.pythonhosted.org/packages/23/01/1c30526460f4d23222d0fabd5888868262fd0e2b71a00570ca26483cd993/ruff-0.15.2-py3-none-win32.whl", hash = "sha256:fd5ff9e5f519a7e1bd99cbe8daa324010a74f5e2ebc97c6242c08f26f3714f6f", size = 10507885, upload-time = "2026-02-19T22:32:15.635Z" }, - { url = "https://files.pythonhosted.org/packages/5c/10/3d18e3bbdf8fc50bbb4ac3cc45970aa5a9753c5cb51bf9ed9a3cd8b79fa3/ruff-0.15.2-py3-none-win_amd64.whl", hash = "sha256:d20014e3dfa400f3ff84830dfb5755ece2de45ab62ecea4af6b7262d0fb4f7c5", size = 11623725, upload-time = "2026-02-19T22:32:04.947Z" }, - { url = "https://files.pythonhosted.org/packages/6d/78/097c0798b1dab9f8affe73da9642bb4500e098cb27fd8dc9724816ac747b/ruff-0.15.2-py3-none-win_arm64.whl", hash = "sha256:cabddc5822acdc8f7b5527b36ceac55cc51eec7b1946e60181de8fe83ca8876e", size = 10941649, upload-time = "2026-02-19T22:32:18.108Z" }, + { url = "https://files.pythonhosted.org/packages/f2/82/c11a03cfec3a4d26a0ea1e571f0f44be5993b923f905eeddfc397c13d360/ruff-0.15.4-py3-none-linux_armv6l.whl", hash = "sha256:a1810931c41606c686bae8b5b9a8072adac2f611bb433c0ba476acba17a332e0", size = 10453333, upload-time = "2026-02-26T20:04:20.093Z" }, + { url = "https://files.pythonhosted.org/packages/ce/5d/6a1f271f6e31dffb31855996493641edc3eef8077b883eaf007a2f1c2976/ruff-0.15.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5a1632c66672b8b4d3e1d1782859e98d6e0b4e70829530666644286600a33992", size = 10853356, upload-time = "2026-02-26T20:04:05.808Z" }, + { url = "https://files.pythonhosted.org/packages/b1/d8/0fab9f8842b83b1a9c2bf81b85063f65e93fb512e60effa95b0be49bfc54/ruff-0.15.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a4386ba2cd6c0f4ff75252845906acc7c7c8e1ac567b7bc3d373686ac8c222ba", size = 10187434, upload-time = "2026-02-26T20:03:54.656Z" }, + { url = "https://files.pythonhosted.org/packages/85/cc/cc220fd9394eff5db8d94dec199eec56dd6c9f3651d8869d024867a91030/ruff-0.15.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2496488bdfd3732747558b6f95ae427ff066d1fcd054daf75f5a50674411e75", size = 10535456, upload-time = "2026-02-26T20:03:52.738Z" }, + { url = "https://files.pythonhosted.org/packages/fa/0f/bced38fa5cf24373ec767713c8e4cadc90247f3863605fb030e597878661/ruff-0.15.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3f1c4893841ff2d54cbda1b2860fa3260173df5ddd7b95d370186f8a5e66a4ac", size = 10287772, upload-time = "2026-02-26T20:04:08.138Z" }, + { url = "https://files.pythonhosted.org/packages/2b/90/58a1802d84fed15f8f281925b21ab3cecd813bde52a8ca033a4de8ab0e7a/ruff-0.15.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:820b8766bd65503b6c30aaa6331e8ef3a6e564f7999c844e9a547c40179e440a", size = 11049051, upload-time = "2026-02-26T20:04:03.53Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ac/b7ad36703c35f3866584564dc15f12f91cb1a26a897dc2fd13d7cb3ae1af/ruff-0.15.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9fb74bab47139c1751f900f857fa503987253c3ef89129b24ed375e72873e85", size = 11890494, upload-time = "2026-02-26T20:04:10.497Z" }, + { url = "https://files.pythonhosted.org/packages/93/3d/3eb2f47a39a8b0da99faf9c54d3eb24720add1e886a5309d4d1be73a6380/ruff-0.15.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f80c98765949c518142b3a50a5db89343aa90f2c2bf7799de9986498ae6176db", size = 11326221, upload-time = "2026-02-26T20:04:12.84Z" }, + { url = "https://files.pythonhosted.org/packages/ff/90/bf134f4c1e5243e62690e09d63c55df948a74084c8ac3e48a88468314da6/ruff-0.15.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:451a2e224151729b3b6c9ffb36aed9091b2996fe4bdbd11f47e27d8f2e8888ec", size = 11168459, upload-time = "2026-02-26T20:04:00.969Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e5/a64d27688789b06b5d55162aafc32059bb8c989c61a5139a36e1368285eb/ruff-0.15.4-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a8f157f2e583c513c4f5f896163a93198297371f34c04220daf40d133fdd4f7f", size = 11104366, upload-time = "2026-02-26T20:03:48.099Z" }, + { url = "https://files.pythonhosted.org/packages/f1/f6/32d1dcb66a2559763fc3027bdd65836cad9eb09d90f2ed6a63d8e9252b02/ruff-0.15.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:917cc68503357021f541e69b35361c99387cdbbf99bd0ea4aa6f28ca99ff5338", size = 10510887, upload-time = "2026-02-26T20:03:45.771Z" }, + { url = "https://files.pythonhosted.org/packages/ff/92/22d1ced50971c5b6433aed166fcef8c9343f567a94cf2b9d9089f6aa80fe/ruff-0.15.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e9737c8161da79fd7cfec19f1e35620375bd8b2a50c3e77fa3d2c16f574105cc", size = 10285939, upload-time = "2026-02-26T20:04:22.42Z" }, + { url = "https://files.pythonhosted.org/packages/e6/f4/7c20aec3143837641a02509a4668fb146a642fd1211846634edc17eb5563/ruff-0.15.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:291258c917539e18f6ba40482fe31d6f5ac023994ee11d7bdafd716f2aab8a68", size = 10765471, upload-time = "2026-02-26T20:03:58.924Z" }, + { url = "https://files.pythonhosted.org/packages/d0/09/6d2f7586f09a16120aebdff8f64d962d7c4348313c77ebb29c566cefc357/ruff-0.15.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3f83c45911da6f2cd5936c436cf86b9f09f09165f033a99dcf7477e34041cbc3", size = 11263382, upload-time = "2026-02-26T20:04:24.424Z" }, + { url = "https://files.pythonhosted.org/packages/1b/fa/2ef715a1cd329ef47c1a050e10dee91a9054b7ce2fcfdd6a06d139afb7ec/ruff-0.15.4-py3-none-win32.whl", hash = "sha256:65594a2d557d4ee9f02834fcdf0a28daa8b3b9f6cb2cb93846025a36db47ef22", size = 10506664, upload-time = "2026-02-26T20:03:50.56Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a8/c688ef7e29983976820d18710f955751d9f4d4eb69df658af3d006e2ba3e/ruff-0.15.4-py3-none-win_amd64.whl", hash = "sha256:04196ad44f0df220c2ece5b0e959c2f37c777375ec744397d21d15b50a75264f", size = 11651048, upload-time = "2026-02-26T20:04:17.191Z" }, + { url = "https://files.pythonhosted.org/packages/3e/0a/9e1be9035b37448ce2e68c978f0591da94389ade5a5abafa4cf99985d1b2/ruff-0.15.4-py3-none-win_arm64.whl", hash = "sha256:60d5177e8cfc70e51b9c5fad936c634872a74209f934c1e79107d11787ad5453", size = 10966776, upload-time = "2026-02-26T20:03:56.908Z" }, ] [[package]] @@ -1539,26 +1546,26 @@ wheels = [ [[package]] name = "ty" -version = "0.0.18" +version = "0.0.19" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/74/15/9682700d8d60fdca7afa4febc83a2354b29cdcd56e66e19c92b521db3b39/ty-0.0.18.tar.gz", hash = "sha256:04ab7c3db5dcbcdac6ce62e48940d3a0124f377c05499d3f3e004e264ae94b83", size = 5214774, upload-time = "2026-02-20T21:51:31.173Z" } +sdist = { url = "https://files.pythonhosted.org/packages/84/5e/da108b9eeb392e02ff0478a34e9651490b36af295881cb56575b83f0cc3a/ty-0.0.19.tar.gz", hash = "sha256:ee3d9ed4cb586e77f6efe3d0fe5a855673ca438a3d533a27598e1d3502a2948a", size = 5220026, upload-time = "2026-02-26T12:13:15.215Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ae/d8/920460d4c22ea68fcdeb0b2fb53ea2aeb9c6d7875bde9278d84f2ac767b6/ty-0.0.18-py3-none-linux_armv6l.whl", hash = "sha256:4e5e91b0a79857316ef893c5068afc4b9872f9d257627d9bc8ac4d2715750d88", size = 10280825, upload-time = "2026-02-20T21:51:25.03Z" }, - { url = "https://files.pythonhosted.org/packages/83/56/62587de582d3d20d78fcdddd0594a73822ac5a399a12ef512085eb7a4de6/ty-0.0.18-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ee0e578b3f8416e2d5416da9553b78fd33857868aa1384cb7fefeceee5ff102d", size = 10118324, upload-time = "2026-02-20T21:51:22.27Z" }, - { url = "https://files.pythonhosted.org/packages/2f/2d/dbdace8d432a0755a7417f659bfd5b8a4261938ecbdfd7b42f4c454f5aa9/ty-0.0.18-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3f7a0487d36b939546a91d141f7fc3dbea32fab4982f618d5b04dc9d5b6da21e", size = 9605861, upload-time = "2026-02-20T21:51:16.066Z" }, - { url = "https://files.pythonhosted.org/packages/6b/d9/de11c0280f778d5fc571393aada7fe9b8bc1dd6a738f2e2c45702b8b3150/ty-0.0.18-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5e2fa8d45f57ca487a470e4bf66319c09b561150e98ae2a6b1a97ef04c1a4eb", size = 10092701, upload-time = "2026-02-20T21:51:26.862Z" }, - { url = "https://files.pythonhosted.org/packages/0f/94/068d4d591d791041732171e7b63c37a54494b2e7d28e88d2167eaa9ad875/ty-0.0.18-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d75652e9e937f7044b1aca16091193e7ef11dac1c7ec952b7fb8292b7ba1f5f2", size = 10109203, upload-time = "2026-02-20T21:51:11.59Z" }, - { url = "https://files.pythonhosted.org/packages/34/e4/526a4aa56dc0ca2569aaa16880a1ab105c3b416dd70e87e25a05688999f3/ty-0.0.18-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:563c868edceb8f6ddd5e91113c17d3676b028f0ed380bdb3829b06d9beb90e58", size = 10614200, upload-time = "2026-02-20T21:51:20.298Z" }, - { url = "https://files.pythonhosted.org/packages/fd/3d/b68ab20a34122a395880922587fbfc3adf090d22e0fb546d4d20fe8c2621/ty-0.0.18-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:502e2a1f948bec563a0454fc25b074bf5cf041744adba8794d024277e151d3b0", size = 11153232, upload-time = "2026-02-20T21:51:14.121Z" }, - { url = "https://files.pythonhosted.org/packages/68/ea/678243c042343fcda7e6af36036c18676c355878dcdcd517639586d2cf9e/ty-0.0.18-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc881dea97021a3aa29134a476937fd8054775c4177d01b94db27fcfb7aab65b", size = 10832934, upload-time = "2026-02-20T21:51:32.92Z" }, - { url = "https://files.pythonhosted.org/packages/d8/bd/7f8d647cef8b7b346c0163230a37e903c7461c7248574840b977045c77df/ty-0.0.18-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:421fcc3bc64cab56f48edb863c7c1c43649ec4d78ff71a1acb5366ad723b6021", size = 10700888, upload-time = "2026-02-20T21:51:09.673Z" }, - { url = "https://files.pythonhosted.org/packages/6e/06/cb3620dc48c5d335ba7876edfef636b2f4498eff4a262ff90033b9e88408/ty-0.0.18-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0fe5038a7136a0e638a2fb1ad06e3d3c4045314c6ba165c9c303b9aeb4623d6c", size = 10078965, upload-time = "2026-02-20T21:51:07.678Z" }, - { url = "https://files.pythonhosted.org/packages/60/27/c77a5a84533fa3b685d592de7b4b108eb1f38851c40fac4e79cc56ec7350/ty-0.0.18-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d123600a52372677613a719bbb780adeb9b68f47fb5f25acb09171de390e0035", size = 10134659, upload-time = "2026-02-20T21:51:18.311Z" }, - { url = "https://files.pythonhosted.org/packages/43/6e/60af6b88c73469e628ba5253a296da6984e0aa746206f3034c31f1a04ed1/ty-0.0.18-py3-none-musllinux_1_2_i686.whl", hash = "sha256:bb4bc11d32a1bf96a829bf6b9696545a30a196ac77bbc07cc8d3dfee35e03723", size = 10297494, upload-time = "2026-02-20T21:51:39.631Z" }, - { url = "https://files.pythonhosted.org/packages/33/90/612dc0b68224c723faed6adac2bd3f930a750685db76dfe17e6b9e534a83/ty-0.0.18-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:dda2efbf374ba4cd704053d04e32f2f784e85c2ddc2400006b0f96f5f7e4b667", size = 10791944, upload-time = "2026-02-20T21:51:37.13Z" }, - { url = "https://files.pythonhosted.org/packages/0d/da/f4ada0fd08a9e4138fe3fd2bcd3797753593f423f19b1634a814b9b2a401/ty-0.0.18-py3-none-win32.whl", hash = "sha256:c5768607c94977dacddc2f459ace6a11a408a0f57888dd59abb62d28d4fee4f7", size = 9677964, upload-time = "2026-02-20T21:51:42.039Z" }, - { url = "https://files.pythonhosted.org/packages/5e/fa/090ed9746e5c59fc26d8f5f96dc8441825171f1f47752f1778dad690b08b/ty-0.0.18-py3-none-win_amd64.whl", hash = "sha256:b78d0fa1103d36fc2fce92f2092adace52a74654ab7884d54cdaec8eb5016a4d", size = 10636576, upload-time = "2026-02-20T21:51:29.159Z" }, - { url = "https://files.pythonhosted.org/packages/92/4f/5dd60904c8105cda4d0be34d3a446c180933c76b84ae0742e58f02133713/ty-0.0.18-py3-none-win_arm64.whl", hash = "sha256:01770c3c82137c6b216aa3251478f0b197e181054ee92243772de553d3586398", size = 10095449, upload-time = "2026-02-20T21:51:34.914Z" }, + { url = "https://files.pythonhosted.org/packages/5a/31/fd8c6067abb275bea11523d21ecf64e1d870b1ce80cac529cf6636df1471/ty-0.0.19-py3-none-linux_armv6l.whl", hash = "sha256:29bed05d34c8a7597567b8e327c53c1aed4a07dcfbe6c81e6d60c7444936ad77", size = 10268470, upload-time = "2026-02-26T12:13:42.881Z" }, + { url = "https://files.pythonhosted.org/packages/15/de/16a11bbf7d98c75849fc41f5d008b89bb5d080a4b10dc8ea851ee2bd371b/ty-0.0.19-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:79140870c688c97ec68e723c28935ddef9d91a76d48c68e665fe7c851e628b8a", size = 10098562, upload-time = "2026-02-26T12:13:31.618Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4f/086d6ff6686eadf903913c45b53ab96694b62bbfee1d8cf3e55a9b5aa4b2/ty-0.0.19-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6e9c1f9cfa6a26f7881d14d75cf963af743f6c4189e6aa3e3b4056a65f22e730", size = 9604073, upload-time = "2026-02-26T12:13:24.645Z" }, + { url = "https://files.pythonhosted.org/packages/95/13/888a6b6c7ed4a880fee91bec997f775153ce86215ee4c56b868516314734/ty-0.0.19-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbca43b050edf1db2e64ae7b79add233c2aea2855b8a876081bbd032edcd0610", size = 10106295, upload-time = "2026-02-26T12:13:40.584Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e8/05a372cae8da482de73b8246fb43236bf11e24ac28c879804568108759db/ty-0.0.19-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8acaa88ab1955ca6b15a0ccc274011c4961377fe65c3948e5d2b212f2517b87c", size = 10098234, upload-time = "2026-02-26T12:13:33.725Z" }, + { url = "https://files.pythonhosted.org/packages/c5/f1/5b0958e9e9576e7662192fe689bbb3dc88e631a4e073db3047793a547d58/ty-0.0.19-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a901b6a6dd9d17d5b3b2e7bafc3057294e88da3f5de507347316687d7f191a1", size = 10607218, upload-time = "2026-02-26T12:13:17.576Z" }, + { url = "https://files.pythonhosted.org/packages/fb/ab/358c78b77844f58ff5aca368550ab16c719f1ab0ec892ceb1114d7500f4e/ty-0.0.19-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8deafdaaaee65fd121c66064da74a922d8501be4a2d50049c71eab521a23eff7", size = 11160593, upload-time = "2026-02-26T12:13:36.008Z" }, + { url = "https://files.pythonhosted.org/packages/95/59/827fc346d66a59fe48e9689a5ceb67dbbd5b4de2e8d4625371af39a2e8b7/ty-0.0.19-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:035e56071af280897441018f74f921b97d53aec0856f8af85f4f949df8eda07d", size = 10822392, upload-time = "2026-02-26T12:13:29.415Z" }, + { url = "https://files.pythonhosted.org/packages/81/f9/3bbfbbe35478de9bcd63848f4bc9bffda72278dd9732dbad3efc3978432e/ty-0.0.19-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abdf5885130393ce74501dba792f48ce0a515756ec81c33a4b324bdf3509df6e", size = 10707139, upload-time = "2026-02-26T12:13:20.148Z" }, + { url = "https://files.pythonhosted.org/packages/12/9e/597023b183ec4ade83a36a0cea5c103f3bffa34f70813d46386c61447fb8/ty-0.0.19-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:877e89005c8f9d1dbff5ad14cbac9f35c528406fde38926f9b44f24830de8d6a", size = 10096933, upload-time = "2026-02-26T12:13:45.266Z" }, + { url = "https://files.pythonhosted.org/packages/1e/76/d0d2f6e674db2a17c8efa5e26682b9dfa8d34774705f35902a7b45ebd3bd/ty-0.0.19-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:39bd1da051c1e4d316efaf79dbed313255633f7c6ad6e24d29f4d9c6ffaf4de6", size = 10109547, upload-time = "2026-02-26T12:13:22.17Z" }, + { url = "https://files.pythonhosted.org/packages/a4/b0/76026c06b852a3aa4fdb5bd329fdc2175aaf3c64a3fafece9cc4df167cee/ty-0.0.19-py3-none-musllinux_1_2_i686.whl", hash = "sha256:87df8415a6c9cb27b8f1382fcdc6052e59f5b9f50f78bc14663197eb5c8d3699", size = 10289110, upload-time = "2026-02-26T12:13:38.29Z" }, + { url = "https://files.pythonhosted.org/packages/14/6c/f3b3a189816b4f079b20fe5d0d7ee38e38a472f53cc6770bb6571147e3de/ty-0.0.19-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:89b6bb23c332ed5c38dd859eb5793f887abcc936f681a40d4ea68e35eac1af33", size = 10796479, upload-time = "2026-02-26T12:13:10.992Z" }, + { url = "https://files.pythonhosted.org/packages/3d/18/caee33d1ce9dd50bd94c26cde7cda4f6971e22e474e7d72a5c86d745ad58/ty-0.0.19-py3-none-win32.whl", hash = "sha256:19b33df3aa7af7b1a9eaa4e1175c3b4dec0f5f2e140243e3492c8355c37418f3", size = 9677215, upload-time = "2026-02-26T12:13:08.519Z" }, + { url = "https://files.pythonhosted.org/packages/81/41/18fc0771d0b1da7d7cc2fc9af278d3122b754fe8b521a748734f4e16ecfd/ty-0.0.19-py3-none-win_amd64.whl", hash = "sha256:b9052c61464cdd76bc8e6796f2588c08700f25d0dcbc225bb165e390ea9d96a4", size = 10651252, upload-time = "2026-02-26T12:13:13.035Z" }, + { url = "https://files.pythonhosted.org/packages/8b/8c/26f7ce8863eb54510082747b3dfb1046ba24f16fc11de18c0e5feb36ff18/ty-0.0.19-py3-none-win_arm64.whl", hash = "sha256:9329804b66dcbae8e7af916ef4963221ed53b8ec7d09b0793591c5ae8a0f3270", size = 10093195, upload-time = "2026-02-26T12:13:26.816Z" }, ] [[package]] @@ -1660,7 +1667,7 @@ wheels = [ [[package]] name = "zeromq" version = "4.3.5" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=zeromq&rev=releases#31af284805d0787a689e129311d992bec14a2400" } +source = { git = "https://github.com/commaai/dependencies.git?subdirectory=zeromq&rev=releases#11895d3f6bc62a229e84c87505948f15f9018ce0" } [[package]] name = "zstandard" @@ -1690,4 +1697,4 @@ wheels = [ [[package]] name = "zstd" version = "1.5.6" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=zstd&rev=releases#31af284805d0787a689e129311d992bec14a2400" } +source = { git = "https://github.com/commaai/dependencies.git?subdirectory=zstd&rev=releases#11895d3f6bc62a229e84c87505948f15f9018ce0" } From ddf05d7126e8e20a018bb45788130f6998b9da62 Mon Sep 17 00:00:00 2001 From: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com> Date: Fri, 27 Feb 2026 13:15:59 -0800 Subject: [PATCH 252/311] modeld_v2: tinygrad transformation warp (#1698) * chore: sync tinygrad Runs great in sim. now we need to rebuild some models * oops forgot to unblock this after testing * helpers * oh yeah * latest tg * this wont do anything empriically * reduce complexity * okay lint * Update tinygrad_runner.py * Update modeld.py * Update build-all-tinygrad-models.yaml * tinygrad bump * Update modeld.py * Update tinygrad_runner.py * bump * Update SConscript * Update SConscript * com * Update fetcher.py * Update helpers.py * life is froughtless, when you're thoughtless * lint * ozdust ballroom * shiz * Update tinygrad_runner.py * Update tinygrad_runner.py * slough it off as i do * try old support one last time * support mixed input dtypes * use internal * dont need that shiz * Update fill_model_msg.py * Update onnx_runner.py * Update onnx_runner.py * Update model_runner.py * see if this speeds up execution if not, revert me * no * Update helpers.py * rebase * more * planplus --------- Co-authored-by: Jason Wen --- .github/workflows/sunnypilot-build-model.yaml | 2 +- selfdrive/debug/uiview.py | 6 +- sunnypilot/SConscript | 1 - sunnypilot/modeld_v2/SConscript | 63 ++++---- sunnypilot/modeld_v2/get_model_metadata.py | 37 +++-- sunnypilot/modeld_v2/install_models_pc.py | 38 ++--- sunnypilot/modeld_v2/modeld.py | 41 +++--- sunnypilot/modeld_v2/models/commonmodel.cc | 62 -------- sunnypilot/modeld_v2/models/commonmodel.h | 97 ------------- sunnypilot/modeld_v2/models/commonmodel.pxd | 27 ---- .../modeld_v2/models/commonmodel_pyx.pxd | 13 -- .../modeld_v2/models/commonmodel_pyx.pyx | 74 ---------- .../modeld_v2/parse_model_outputs_split.py | 4 +- sunnypilot/modeld_v2/runners/ort_helpers.py | 36 ----- .../modeld_v2/runners/tinygrad_helpers.py | 8 - .../tests/test_buffer_logic_inspect.py | 6 +- sunnypilot/modeld_v2/tests/test_warp.py | 102 +++++++++++++ sunnypilot/modeld_v2/transforms/loadyuv.cc | 76 ---------- sunnypilot/modeld_v2/transforms/loadyuv.cl | 47 ------ sunnypilot/modeld_v2/transforms/loadyuv.h | 20 --- sunnypilot/modeld_v2/transforms/transform.cc | 97 ------------- sunnypilot/modeld_v2/transforms/transform.cl | 54 ------- sunnypilot/modeld_v2/transforms/transform.h | 25 ---- sunnypilot/modeld_v2/warp.py | 137 ++++++++++++++++++ sunnypilot/models/fetcher.py | 2 +- sunnypilot/models/helpers.py | 15 +- sunnypilot/models/runners/constants.py | 3 - sunnypilot/models/runners/model_runner.py | 6 +- sunnypilot/models/runners/onnx/onnx_runner.py | 62 -------- .../runners/tinygrad/tinygrad_runner.py | 44 +++--- system/manager/process_config.py | 5 - 31 files changed, 355 insertions(+), 855 deletions(-) delete mode 100644 sunnypilot/modeld_v2/models/commonmodel.cc delete mode 100644 sunnypilot/modeld_v2/models/commonmodel.h delete mode 100644 sunnypilot/modeld_v2/models/commonmodel.pxd delete mode 100644 sunnypilot/modeld_v2/models/commonmodel_pyx.pxd delete mode 100644 sunnypilot/modeld_v2/models/commonmodel_pyx.pyx delete mode 100644 sunnypilot/modeld_v2/runners/ort_helpers.py delete mode 100644 sunnypilot/modeld_v2/runners/tinygrad_helpers.py create mode 100644 sunnypilot/modeld_v2/tests/test_warp.py delete mode 100644 sunnypilot/modeld_v2/transforms/loadyuv.cc delete mode 100644 sunnypilot/modeld_v2/transforms/loadyuv.cl delete mode 100644 sunnypilot/modeld_v2/transforms/loadyuv.h delete mode 100644 sunnypilot/modeld_v2/transforms/transform.cc delete mode 100644 sunnypilot/modeld_v2/transforms/transform.cl delete mode 100644 sunnypilot/modeld_v2/transforms/transform.h create mode 100644 sunnypilot/modeld_v2/warp.py delete mode 100644 sunnypilot/models/runners/onnx/onnx_runner.py diff --git a/.github/workflows/sunnypilot-build-model.yaml b/.github/workflows/sunnypilot-build-model.yaml index bc4ecfe04b..293c0f2a01 100644 --- a/.github/workflows/sunnypilot-build-model.yaml +++ b/.github/workflows/sunnypilot-build-model.yaml @@ -173,7 +173,7 @@ jobs: echo "Compiling: $onnx_file -> $output_file" QCOM=1 python3 "${{ env.TINYGRAD_PATH }}/examples/openpilot/compile3.py" "$onnx_file" "$output_file" - QCOM=1 python3 "${{ env.MODELS_DIR }}/../get_model_metadata.py" "$onnx_file" || true + DEV=QCOM FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 python3 "${{ env.MODELS_DIR }}/../get_model_metadata.py" "$onnx_file" || true done - name: Prepare Output diff --git a/selfdrive/debug/uiview.py b/selfdrive/debug/uiview.py index ad3ccea036..eac1f8fbf4 100755 --- a/selfdrive/debug/uiview.py +++ b/selfdrive/debug/uiview.py @@ -3,7 +3,7 @@ import time from cereal import car, log, messaging from openpilot.common.params import Params -from openpilot.system.manager.process_config import managed_processes, is_snpe_model, is_tinygrad_model, is_stock_model +from openpilot.system.manager.process_config import managed_processes, is_tinygrad_model, is_stock_model from openpilot.system.hardware import HARDWARE if __name__ == "__main__": @@ -11,8 +11,6 @@ if __name__ == "__main__": params = Params() params.put("CarParams", CP.to_bytes()) - if use_snpe_modeld := is_snpe_model(False, params, CP): - print("Using SNPE modeld") if use_tinygrad_modeld := is_tinygrad_model(False, params, CP): print("Using TinyGrad modeld") if use_stock_modeld := is_stock_model(False, params, CP): @@ -21,7 +19,7 @@ if __name__ == "__main__": HARDWARE.set_power_save(False) procs = ['camerad', 'ui', 'calibrationd', 'plannerd', 'dmonitoringmodeld', 'dmonitoringd'] - procs += ["modeld_snpe" if use_snpe_modeld else "modeld_tinygrad" if use_tinygrad_modeld else "modeld"] + procs += ["modeld_tinygrad" if use_tinygrad_modeld else "modeld"] for p in procs: managed_processes[p].start() diff --git a/sunnypilot/SConscript b/sunnypilot/SConscript index eb3698f9d0..09ad39ab43 100644 --- a/sunnypilot/SConscript +++ b/sunnypilot/SConscript @@ -1,4 +1,3 @@ SConscript(['common/transformations/SConscript']) -SConscript(['modeld/SConscript']) SConscript(['modeld_v2/SConscript']) SConscript(['selfdrive/locationd/SConscript']) diff --git a/sunnypilot/modeld_v2/SConscript b/sunnypilot/modeld_v2/SConscript index 28d39a75f1..ddf889c0c0 100644 --- a/sunnypilot/modeld_v2/SConscript +++ b/sunnypilot/modeld_v2/SConscript @@ -1,34 +1,8 @@ import os import glob -Import('env', 'envCython', 'arch', 'cereal', 'messaging', 'common', 'visionipc') +Import('env', 'arch') lenv = env.Clone() -lenvCython = envCython.Clone() - -libs = [cereal, messaging, visionipc, common, 'capnp', 'kj', 'pthread'] -frameworks = [] - -common_src = [ - "models/commonmodel.cc", - "transforms/loadyuv.cc", - "transforms/transform.cc", -] - -# OpenCL is a framework on Mac -if arch == "Darwin": - frameworks += ['OpenCL'] -else: - libs += ['OpenCL'] - -# Set path definitions -for pathdef, fn in {'TRANSFORM': 'transforms/transform.cl', 'LOADYUV': 'transforms/loadyuv.cl'}.items(): - for xenv in (lenv, lenvCython): - xenv['CXXFLAGS'].append(f'-D{pathdef}_PATH=\\"{File(fn).abspath}\\"') - -# Compile cython -cython_libs = envCython["LIBS"] + libs -commonmodel_lib = lenv.Library('commonmodel', common_src) -lenvCython.Program('models/commonmodel_pyx.so', 'models/commonmodel_pyx.pyx', LIBS=[commonmodel_lib, *cython_libs], FRAMEWORKS=frameworks) tinygrad_files = ["#"+x for x in glob.glob(env.Dir("#tinygrad_repo").relpath + "/**", recursive=True, root_dir=env.Dir("#").abspath) if 'pycache' not in x] # Get model metadata @@ -47,20 +21,39 @@ if PC: if outputs: lenv.Command(outputs, inputs, cmd) +tg_flags = { + 'larch64': 'DEV=QCOM FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0', + 'Darwin': f'DEV=CPU THREADS=0 HOME={os.path.expanduser("~")}', +}.get(arch, 'DEV=CPU CPU_LLVM=1 THREADS=0') + +image_flag = { + 'larch64': 'IMAGE=2', +}.get(arch, 'IMAGE=0') + def tg_compile(flags, model_name): pythonpath_string = 'PYTHONPATH="${PYTHONPATH}:' + env.Dir("#tinygrad_repo").abspath + '"' fn = File(f"models/{model_name}").abspath + out = fn + "_tinygrad.pkl" + return lenv.Command( - fn + "_tinygrad.pkl", + out, [fn + ".onnx"] + tinygrad_files, - f'{pythonpath_string} {flags} python3 {Dir("#tinygrad_repo").abspath}/examples/openpilot/compile3.py {fn}.onnx {fn}_tinygrad.pkl' + f'{pythonpath_string} {flags} {image_flag} python3 {Dir("#tinygrad_repo").abspath}/examples/openpilot/compile3.py {fn}.onnx {out}' ) -# Compile small models +# Compile models for model_name in ['supercombo', 'driving_vision', 'driving_off_policy', 'driving_policy']: if File(f"models/{model_name}.onnx").exists(): - flags = { - 'larch64': 'DEV=QCOM FLOAT16=1 NOLOCALS=1 IMAGE=2 JIT_BATCH_SIZE=0', - 'Darwin': f'DEV=CPU HOME={os.path.expanduser("~")} IMAGE=0', # tinygrad calls brew which needs a $HOME in the env - }.get(arch, 'DEV=CPU CPU_LLVM=1 IMAGE=0') - tg_compile(flags, model_name) + tg_compile(tg_flags, model_name) + +script_files = [File("warp.py"), File(Dir("#selfdrive/modeld").File("compile_warp.py").abspath)] +pythonpath_string = 'PYTHONPATH="${PYTHONPATH}:' + env.Dir("#tinygrad_repo").abspath + ':' + env.Dir("#").abspath + '"' +compile_warp_cmd = f'{pythonpath_string} {tg_flags} python3 -m sunnypilot.modeld_v2.warp' + +from openpilot.common.transformations.camera import _ar_ox_fisheye, _os_fisheye +warp_targets = [] +for cam in [_ar_ox_fisheye, _os_fisheye]: + w, h = cam.width, cam.height + for bl in [2, 5]: + warp_targets.append(File(f"models/warp_{w}x{h}_b{bl}_tinygrad.pkl").abspath) +lenv.Command(warp_targets, tinygrad_files + script_files, compile_warp_cmd) diff --git a/sunnypilot/modeld_v2/get_model_metadata.py b/sunnypilot/modeld_v2/get_model_metadata.py index e0b5adc51b..838b1e9f40 100755 --- a/sunnypilot/modeld_v2/get_model_metadata.py +++ b/sunnypilot/modeld_v2/get_model_metadata.py @@ -1,36 +1,51 @@ #!/usr/bin/env python3 import sys import pathlib -import onnx import codecs import pickle from typing import Any +from tinygrad.nn.onnx import OnnxPBParser -def get_name_and_shape(value_info:onnx.ValueInfoProto) -> tuple[str, tuple[int,...]]: - shape = tuple([int(dim.dim_value) for dim in value_info.type.tensor_type.shape.dim]) - name = value_info.name + +class MetadataOnnxPBParser(OnnxPBParser): + def _parse_ModelProto(self) -> dict: + obj: dict[str, Any] = {"graph": {"input": [], "output": []}, "metadata_props": []} + for fid, wire_type in self._parse_message(self.reader.len): + match fid: + case 7: + obj["graph"] = self._parse_GraphProto() + case 14: + obj["metadata_props"].append(self._parse_StringStringEntryProto()) + case _: + self.reader.skip_field(wire_type) + return obj + + +def get_name_and_shape(value_info: dict[str, Any]) -> tuple[str, tuple[int, ...]]: + shape = tuple(int(dim) if isinstance(dim, int) else 0 for dim in value_info["parsed_type"].shape) + name = value_info["name"] return name, shape -def get_metadata_value_by_name(model:onnx.ModelProto, name:str) -> str | Any: - for prop in model.metadata_props: - if prop.key == name: - return prop.value +def get_metadata_value_by_name(model: dict[str, Any], name: str) -> str | Any: + for prop in model["metadata_props"]: + if prop["key"] == name: + return prop["value"] return None if __name__ == "__main__": model_path = pathlib.Path(sys.argv[1]) - model = onnx.load(str(model_path)) + model = MetadataOnnxPBParser(model_path).parse() output_slices = get_metadata_value_by_name(model, 'output_slices') assert output_slices is not None, 'output_slices not found in metadata' metadata = { 'model_checkpoint': get_metadata_value_by_name(model, 'model_checkpoint'), 'output_slices': pickle.loads(codecs.decode(output_slices.encode(), "base64")), - 'input_shapes': dict([get_name_and_shape(x) for x in model.graph.input]), - 'output_shapes': dict([get_name_and_shape(x) for x in model.graph.output]) + 'input_shapes': dict(get_name_and_shape(x) for x in model["graph"]["input"]), + 'output_shapes': dict(get_name_and_shape(x) for x in model["graph"]["output"]), } metadata_path = model_path.parent / (model_path.stem + '_metadata.pkl') diff --git a/sunnypilot/modeld_v2/install_models_pc.py b/sunnypilot/modeld_v2/install_models_pc.py index a378d90b11..d203de3487 100755 --- a/sunnypilot/modeld_v2/install_models_pc.py +++ b/sunnypilot/modeld_v2/install_models_pc.py @@ -3,41 +3,27 @@ import sys import shutil import pickle import codecs -import onnx from pathlib import Path from openpilot.system.hardware.hw import Paths - - -def get_name_and_shape(value_info): - shape = tuple([int(dim.dim_value) for dim in value_info.type.tensor_type.shape.dim]) - return value_info.name, shape - - -def get_metadata_value_by_name(model, name): - for prop in model.metadata_props: - if prop.key == name: - return prop.value - return None +from sunnypilot.modeld_v2.get_model_metadata import MetadataOnnxPBParser, get_name_and_shape, get_metadata_value_by_name def generate_metadata_pkl(model_path, output_path): try: - model = onnx.load(str(model_path)) + model = MetadataOnnxPBParser(model_path).parse() output_slices = get_metadata_value_by_name(model, 'output_slices') - - if output_slices: - metadata = { - 'model_checkpoint': get_metadata_value_by_name(model, 'model_checkpoint'), - 'output_slices': pickle.loads(codecs.decode(output_slices.encode(), "base64")), - 'input_shapes': dict([get_name_and_shape(x) for x in model.graph.input]), - 'output_shapes': dict([get_name_and_shape(x) for x in model.graph.output]) - } - with open(output_path, 'wb') as f: - pickle.dump(metadata, f) - return True - else: + if not output_slices: return False + metadata = { + 'model_checkpoint': get_metadata_value_by_name(model, 'model_checkpoint'), + 'output_slices': pickle.loads(codecs.decode(output_slices.encode(), "base64")), + 'input_shapes': dict(get_name_and_shape(x) for x in model["graph"]["input"]), + 'output_shapes': dict(get_name_and_shape(x) for x in model["graph"]["output"]), + } + with open(output_path, 'wb') as f: + pickle.dump(metadata, f) + return True except Exception: return False diff --git a/sunnypilot/modeld_v2/modeld.py b/sunnypilot/modeld_v2/modeld.py index e25078f52f..c38ba40d4f 100755 --- a/sunnypilot/modeld_v2/modeld.py +++ b/sunnypilot/modeld_v2/modeld.py @@ -26,7 +26,7 @@ from openpilot.selfdrive.controls.lib.drive_helpers import get_accel_from_plan, from openpilot.sunnypilot.modeld_v2.fill_model_msg import fill_model_msg, fill_pose_msg, PublishState, get_curvature_from_output from openpilot.sunnypilot.modeld_v2.constants import Plan -from openpilot.sunnypilot.modeld_v2.models.commonmodel_pyx import DrivingModelFrame, CLContext +from openpilot.sunnypilot.modeld_v2.warp import Warp from openpilot.sunnypilot.modeld_v2.meta_helper import load_meta_constants from openpilot.sunnypilot.modeld_v2.camera_offset_helper import CameraOffsetHelper @@ -49,12 +49,12 @@ class FrameMeta: class ModelState(ModelStateBase): - frames: dict[str, DrivingModelFrame] + frames: dict[str, Warp] inputs: dict[str, np.ndarray] prev_desire: np.ndarray # for tracking the rising edge of the pulse temporal_idxs: slice | np.ndarray - def __init__(self, context: CLContext): + def __init__(self): ModelStateBase.__init__(self) try: self.model_runner = get_model_runner() @@ -73,17 +73,16 @@ class ModelState(ModelStateBase): self.PLANPLUS_CONTROL: float = 1.0 buffer_length = 5 if self.model_runner.is_20hz else 2 - self.frames = {name: DrivingModelFrame(context, buffer_length) for name in self.model_runner.vision_input_names} + self.warp = Warp(buffer_length) self.prev_desire = np.zeros(self.constants.DESIRE_LEN, dtype=np.float32) - - # img buffers are managed in openCL transform code self.numpy_inputs = {} self.temporal_buffers = {} self.temporal_idxs_map = {} for key, shape in self.model_runner.input_shapes.items(): - if key not in self.frames: # Managed by opencl + if key not in self.model_runner.vision_input_names: # Policy inputs self.numpy_inputs[key] = np.zeros(shape, dtype=np.float32) + # Temporal input: shape is [batch, history, features] if len(shape) == 3 and shape[1] > 1: buffer_history_len = shape[1] * 4 if shape[1] < 99 else shape[1] # Allow for higher history buffers in the future @@ -129,10 +128,10 @@ class ModelState(ModelStateBase): if key in inputs and key not in [self.desire_key]: self.numpy_inputs[key][:] = inputs[key] - imgs_cl = {name: self.frames[name].prepare(bufs[name], transforms[name].flatten()) for name in self.model_runner.vision_input_names} - - # Prepare inputs using the model runner - self.model_runner.prepare_inputs(imgs_cl, self.numpy_inputs, self.frames) + imgs_tensors = self.warp.process(bufs, transforms) + for name, tensor in imgs_tensors.items(): + self.model_runner.inputs[name] = tensor + self.model_runner.prepare_inputs(self.numpy_inputs) if prepare_only: return None @@ -147,12 +146,11 @@ class ModelState(ModelStateBase): if "desired_curvature" in outputs: input_name_prev = None - if "prev_desired_curvs" in self.numpy_inputs.keys(): - input_name_prev = 'prev_desired_curvs' - elif "prev_desired_curv" in self.numpy_inputs.keys(): + if "prev_desired_curv" in self.numpy_inputs.keys(): input_name_prev = 'prev_desired_curv' if input_name_prev and input_name_prev in self.temporal_buffers: self.process_desired_curvature(outputs, input_name_prev) + return outputs def process_desired_curvature(self, outputs, input_name_prev): @@ -165,9 +163,8 @@ class ModelState(ModelStateBase): def get_action_from_model(self, model_output: dict[str, np.ndarray], prev_action: log.ModelDataV2.Action, lat_action_t: float, long_action_t: float, v_ego: float) -> log.ModelDataV2.Action: plan = model_output['plan'][0] - if 'planplus' in model_output: - recovery_power = self.PLANPLUS_CONTROL * (0.75 if v_ego > 20.0 else 1.0) - plan = plan + recovery_power * model_output['planplus'][0] + if 'planplus' in model_output and self.PLANPLUS_CONTROL != 1.0: + plan = plan + (self.PLANPLUS_CONTROL - 1.0) * model_output['planplus'][0] desired_accel, should_stop = get_accel_from_plan(plan[:, Plan.VELOCITY][:, 0], plan[:, Plan.ACCELERATION][:, 0], self.constants.T_IDXS, action_t=long_action_t) desired_accel = smooth_value(desired_accel, prev_action.desiredAcceleration, self.LONG_SMOOTH_SECONDS) @@ -190,10 +187,8 @@ def main(demo=False): setproctitle(PROCESS_NAME) config_realtime_process(7, 54) - cloudlog.warning("setting up CL context") - cl_context = CLContext() - cloudlog.warning("CL context ready; loading model") - model = ModelState(cl_context) + cloudlog.warning("loading model") + model = ModelState() cloudlog.warning("models loaded, modeld starting") # visionipc clients @@ -206,8 +201,8 @@ def main(demo=False): time.sleep(.1) vipc_client_main_stream = VisionStreamType.VISION_STREAM_WIDE_ROAD if main_wide_camera else VisionStreamType.VISION_STREAM_ROAD - vipc_client_main = VisionIpcClient("camerad", vipc_client_main_stream, True, cl_context) - vipc_client_extra = VisionIpcClient("camerad", VisionStreamType.VISION_STREAM_WIDE_ROAD, False, cl_context) + vipc_client_main = VisionIpcClient("camerad", vipc_client_main_stream, True) + vipc_client_extra = VisionIpcClient("camerad", VisionStreamType.VISION_STREAM_WIDE_ROAD, False) cloudlog.warning(f"vision stream set up, main_wide_camera: {main_wide_camera}, use_extra_client: {use_extra_client}") while not vipc_client_main.connect(False): diff --git a/sunnypilot/modeld_v2/models/commonmodel.cc b/sunnypilot/modeld_v2/models/commonmodel.cc deleted file mode 100644 index 5cd3a84fcc..0000000000 --- a/sunnypilot/modeld_v2/models/commonmodel.cc +++ /dev/null @@ -1,62 +0,0 @@ -#include "sunnypilot/modeld_v2/models/commonmodel.h" - -#include -#include - -#include "common/clutil.h" - -DrivingModelFrame::DrivingModelFrame(cl_device_id device_id, cl_context context, uint8_t buffer_length) : ModelFrame(device_id, context), buffer_length(buffer_length) { - input_frames = std::make_unique(buf_size); - input_frames_cl = CL_CHECK_ERR(clCreateBuffer(context, CL_MEM_READ_WRITE, buf_size, NULL, &err)); - img_buffer_20hz_cl = CL_CHECK_ERR(clCreateBuffer(context, CL_MEM_READ_WRITE, buffer_length*frame_size_bytes, NULL, &err)); - region.origin = (buffer_length - 1) * frame_size_bytes; - region.size = frame_size_bytes; - last_img_cl = CL_CHECK_ERR(clCreateSubBuffer(img_buffer_20hz_cl, CL_MEM_READ_WRITE, CL_BUFFER_CREATE_TYPE_REGION, ®ion, &err)); - // printf("Buffer length: %d, region origin: %lu, region size: %lu\n", buffer_length, region.origin, region.size); - - loadyuv_init(&loadyuv, context, device_id, MODEL_WIDTH, MODEL_HEIGHT); - init_transform(device_id, context, MODEL_WIDTH, MODEL_HEIGHT); -} - -cl_mem* DrivingModelFrame::prepare(cl_mem yuv_cl, int frame_width, int frame_height, int frame_stride, int frame_uv_offset, const mat3& projection) { - run_transform(yuv_cl, MODEL_WIDTH, MODEL_HEIGHT, frame_width, frame_height, frame_stride, frame_uv_offset, projection); - - for (int i = 0; i < (buffer_length - 1); i++) { - CL_CHECK(clEnqueueCopyBuffer(q, img_buffer_20hz_cl, img_buffer_20hz_cl, (i+1)*frame_size_bytes, i*frame_size_bytes, frame_size_bytes, 0, nullptr, nullptr)); - } - loadyuv_queue(&loadyuv, q, y_cl, u_cl, v_cl, last_img_cl); - - copy_queue(&loadyuv, q, img_buffer_20hz_cl, input_frames_cl, 0, 0, frame_size_bytes); - copy_queue(&loadyuv, q, last_img_cl, input_frames_cl, 0, frame_size_bytes, frame_size_bytes); - - // NOTE: Since thneed is using a different command queue, this clFinish is needed to ensure the image is ready. - clFinish(q); - return &input_frames_cl; -} - -DrivingModelFrame::~DrivingModelFrame() { - deinit_transform(); - loadyuv_destroy(&loadyuv); - CL_CHECK(clReleaseMemObject(img_buffer_20hz_cl)); - CL_CHECK(clReleaseMemObject(last_img_cl)); - CL_CHECK(clReleaseCommandQueue(q)); -} - - -MonitoringModelFrame::MonitoringModelFrame(cl_device_id device_id, cl_context context) : ModelFrame(device_id, context) { - input_frames = std::make_unique(buf_size); - input_frame_cl = CL_CHECK_ERR(clCreateBuffer(context, CL_MEM_READ_WRITE, buf_size, NULL, &err)); - - init_transform(device_id, context, MODEL_WIDTH, MODEL_HEIGHT); -} - -cl_mem* MonitoringModelFrame::prepare(cl_mem yuv_cl, int frame_width, int frame_height, int frame_stride, int frame_uv_offset, const mat3& projection) { - run_transform(yuv_cl, MODEL_WIDTH, MODEL_HEIGHT, frame_width, frame_height, frame_stride, frame_uv_offset, projection); - clFinish(q); - return &y_cl; -} - -MonitoringModelFrame::~MonitoringModelFrame() { - deinit_transform(); - CL_CHECK(clReleaseCommandQueue(q)); -} diff --git a/sunnypilot/modeld_v2/models/commonmodel.h b/sunnypilot/modeld_v2/models/commonmodel.h deleted file mode 100644 index 8203e064e0..0000000000 --- a/sunnypilot/modeld_v2/models/commonmodel.h +++ /dev/null @@ -1,97 +0,0 @@ -#pragma once - -#include -#include -#include - -#include - -#define CL_USE_DEPRECATED_OPENCL_1_2_APIS -#ifdef __APPLE__ -#include -#else -#include -#endif - -#include "common/mat.h" -#include "sunnypilot/modeld_v2/transforms/loadyuv.h" -#include "sunnypilot/modeld_v2/transforms/transform.h" - -class ModelFrame { -public: - ModelFrame(cl_device_id device_id, cl_context context) { - q = CL_CHECK_ERR(clCreateCommandQueue(context, device_id, 0, &err)); - } - virtual ~ModelFrame() {} - virtual cl_mem* prepare(cl_mem yuv_cl, int frame_width, int frame_height, int frame_stride, int frame_uv_offset, const mat3& projection) { return NULL; } - uint8_t* buffer_from_cl(cl_mem *in_frames, int buffer_size) { - CL_CHECK(clEnqueueReadBuffer(q, *in_frames, CL_TRUE, 0, buffer_size, input_frames.get(), 0, nullptr, nullptr)); - clFinish(q); - return &input_frames[0]; - } - - int MODEL_WIDTH; - int MODEL_HEIGHT; - int MODEL_FRAME_SIZE; - int buf_size; - -protected: - cl_mem y_cl, u_cl, v_cl; - Transform transform; - cl_command_queue q; - std::unique_ptr input_frames; - - void init_transform(cl_device_id device_id, cl_context context, int model_width, int model_height) { - y_cl = CL_CHECK_ERR(clCreateBuffer(context, CL_MEM_READ_WRITE, model_width * model_height, NULL, &err)); - u_cl = CL_CHECK_ERR(clCreateBuffer(context, CL_MEM_READ_WRITE, (model_width / 2) * (model_height / 2), NULL, &err)); - v_cl = CL_CHECK_ERR(clCreateBuffer(context, CL_MEM_READ_WRITE, (model_width / 2) * (model_height / 2), NULL, &err)); - transform_init(&transform, context, device_id); - } - - void deinit_transform() { - transform_destroy(&transform); - CL_CHECK(clReleaseMemObject(v_cl)); - CL_CHECK(clReleaseMemObject(u_cl)); - CL_CHECK(clReleaseMemObject(y_cl)); - } - - void run_transform(cl_mem yuv_cl, int model_width, int model_height, int frame_width, int frame_height, int frame_stride, int frame_uv_offset, const mat3& projection) { - transform_queue(&transform, q, - yuv_cl, frame_width, frame_height, frame_stride, frame_uv_offset, - y_cl, u_cl, v_cl, model_width, model_height, projection); - } -}; - -class DrivingModelFrame : public ModelFrame { -public: - DrivingModelFrame(cl_device_id device_id, cl_context context, uint8_t buffer_length); - ~DrivingModelFrame(); - cl_mem* prepare(cl_mem yuv_cl, int frame_width, int frame_height, int frame_stride, int frame_uv_offset, const mat3& projection); - - const int MODEL_WIDTH = 512; - const int MODEL_HEIGHT = 256; - const int MODEL_FRAME_SIZE = MODEL_WIDTH * MODEL_HEIGHT * 3 / 2; - const int buf_size = MODEL_FRAME_SIZE * 2; - const size_t frame_size_bytes = MODEL_FRAME_SIZE * sizeof(uint8_t); - const uint8_t buffer_length; - -private: - LoadYUVState loadyuv; - cl_mem img_buffer_20hz_cl, last_img_cl, input_frames_cl; - cl_buffer_region region; -}; - -class MonitoringModelFrame : public ModelFrame { -public: - MonitoringModelFrame(cl_device_id device_id, cl_context context); - ~MonitoringModelFrame(); - cl_mem* prepare(cl_mem yuv_cl, int frame_width, int frame_height, int frame_stride, int frame_uv_offset, const mat3& projection); - - const int MODEL_WIDTH = 1440; - const int MODEL_HEIGHT = 960; - const int MODEL_FRAME_SIZE = MODEL_WIDTH * MODEL_HEIGHT; - const int buf_size = MODEL_FRAME_SIZE; - -private: - cl_mem input_frame_cl; -}; diff --git a/sunnypilot/modeld_v2/models/commonmodel.pxd b/sunnypilot/modeld_v2/models/commonmodel.pxd deleted file mode 100644 index 55023ac4b9..0000000000 --- a/sunnypilot/modeld_v2/models/commonmodel.pxd +++ /dev/null @@ -1,27 +0,0 @@ -# distutils: language = c++ - -from msgq.visionipc.visionipc cimport cl_device_id, cl_context, cl_mem - -cdef extern from "common/mat.h": - cdef struct mat3: - float v[9] - -cdef extern from "common/clutil.h": - cdef unsigned long CL_DEVICE_TYPE_DEFAULT - cl_device_id cl_get_device_id(unsigned long) - cl_context cl_create_context(cl_device_id) - void cl_release_context(cl_context) - -cdef extern from "sunnypilot/modeld_v2/models/commonmodel.h": - cppclass ModelFrame: - int buf_size - unsigned char * buffer_from_cl(cl_mem*, int); - cl_mem * prepare(cl_mem, int, int, int, int, mat3) - - cppclass DrivingModelFrame: - int buf_size - DrivingModelFrame(cl_device_id, cl_context, unsigned char) - - cppclass MonitoringModelFrame: - int buf_size - MonitoringModelFrame(cl_device_id, cl_context) diff --git a/sunnypilot/modeld_v2/models/commonmodel_pyx.pxd b/sunnypilot/modeld_v2/models/commonmodel_pyx.pxd deleted file mode 100644 index 0bb798625b..0000000000 --- a/sunnypilot/modeld_v2/models/commonmodel_pyx.pxd +++ /dev/null @@ -1,13 +0,0 @@ -# distutils: language = c++ - -from msgq.visionipc.visionipc cimport cl_mem -from msgq.visionipc.visionipc_pyx cimport CLContext as BaseCLContext - -cdef class CLContext(BaseCLContext): - pass - -cdef class CLMem: - cdef cl_mem * mem - - @staticmethod - cdef create(void*) diff --git a/sunnypilot/modeld_v2/models/commonmodel_pyx.pyx b/sunnypilot/modeld_v2/models/commonmodel_pyx.pyx deleted file mode 100644 index 78a891f031..0000000000 --- a/sunnypilot/modeld_v2/models/commonmodel_pyx.pyx +++ /dev/null @@ -1,74 +0,0 @@ -# distutils: language = c++ -# cython: c_string_encoding=ascii, language_level=3 - -import numpy as np -cimport numpy as cnp -from libc.string cimport memcpy -from libc.stdint cimport uintptr_t, uint8_t - -from msgq.visionipc.visionipc cimport cl_mem -from msgq.visionipc.visionipc_pyx cimport VisionBuf, CLContext as BaseCLContext -from .commonmodel cimport CL_DEVICE_TYPE_DEFAULT, cl_get_device_id, cl_create_context, cl_release_context -from .commonmodel cimport mat3, ModelFrame as cppModelFrame, DrivingModelFrame as cppDrivingModelFrame, MonitoringModelFrame as cppMonitoringModelFrame - - -cdef class CLContext(BaseCLContext): - def __cinit__(self): - self.device_id = cl_get_device_id(CL_DEVICE_TYPE_DEFAULT) - self.context = cl_create_context(self.device_id) - - def __dealloc__(self): - if self.context: - cl_release_context(self.context) - -cdef class CLMem: - @staticmethod - cdef create(void * cmem): - mem = CLMem() - mem.mem = cmem - return mem - - @property - def mem_address(self): - return (self.mem) - -def cl_from_visionbuf(VisionBuf buf): - return CLMem.create(&buf.buf.buf_cl) - - -cdef class ModelFrame: - cdef cppModelFrame * frame - cdef int buf_size - - def __dealloc__(self): - del self.frame - - def prepare(self, VisionBuf buf, float[:] projection): - cdef mat3 cprojection - memcpy(cprojection.v, &projection[0], 9*sizeof(float)) - cdef cl_mem * data - data = self.frame.prepare(buf.buf.buf_cl, buf.width, buf.height, buf.stride, buf.uv_offset, cprojection) - return CLMem.create(data) - - def buffer_from_cl(self, CLMem in_frames): - cdef unsigned char * data2 - data2 = self.frame.buffer_from_cl(in_frames.mem, self.buf_size) - return np.asarray( data2) - - -cdef class DrivingModelFrame(ModelFrame): - cdef cppDrivingModelFrame * _frame - - def __cinit__(self, CLContext context, int buffer_length=2): - self._frame = new cppDrivingModelFrame(context.device_id, context.context, buffer_length) - self.frame = (self._frame) - self.buf_size = self._frame.buf_size - -cdef class MonitoringModelFrame(ModelFrame): - cdef cppMonitoringModelFrame * _frame - - def __cinit__(self, CLContext context): - self._frame = new cppMonitoringModelFrame(context.device_id, context.context) - self.frame = (self._frame) - self.buf_size = self._frame.buf_size - diff --git a/sunnypilot/modeld_v2/parse_model_outputs_split.py b/sunnypilot/modeld_v2/parse_model_outputs_split.py index a099facd15..831649e3c1 100644 --- a/sunnypilot/modeld_v2/parse_model_outputs_split.py +++ b/sunnypilot/modeld_v2/parse_model_outputs_split.py @@ -108,8 +108,8 @@ class Parser: plan_in_N, plan_out_N = (SplitModelConstants.PLAN_MHP_N, SplitModelConstants.PLAN_MHP_SELECTION) if plan_mhp else (0, 0) self.parse_mdn('plan', outs, in_N=plan_in_N, out_N=plan_out_N, out_shape=(SplitModelConstants.IDX_N, SplitModelConstants.PLAN_WIDTH)) - if 'planplus' in outs: - self.parse_mdn('planplus', outs, in_N=plan_in_N, out_N=plan_out_N, out_shape=(SplitModelConstants.IDX_N, SplitModelConstants.PLAN_WIDTH)) + if 'planplus' in outs: + self.parse_mdn('planplus', outs, in_N=0, out_N=0, out_shape=(SplitModelConstants.IDX_N, SplitModelConstants.PLAN_WIDTH)) def split_outputs(self, outs: dict[str, np.ndarray]) -> None: if 'desired_curvature' in outs: diff --git a/sunnypilot/modeld_v2/runners/ort_helpers.py b/sunnypilot/modeld_v2/runners/ort_helpers.py deleted file mode 100644 index 26afb03562..0000000000 --- a/sunnypilot/modeld_v2/runners/ort_helpers.py +++ /dev/null @@ -1,36 +0,0 @@ -import onnx -import onnxruntime as ort -import numpy as np -import itertools - -ORT_TYPES_TO_NP_TYPES = {'tensor(float16)': np.float16, 'tensor(float)': np.float32, 'tensor(uint8)': np.uint8} - -def attributeproto_fp16_to_fp32(attr): - float32_list = np.frombuffer(attr.raw_data, dtype=np.float16) - attr.data_type = 1 - attr.raw_data = float32_list.astype(np.float32).tobytes() - -def convert_fp16_to_fp32(model): - for i in model.graph.initializer: - if i.data_type == 10: - attributeproto_fp16_to_fp32(i) - for i in itertools.chain(model.graph.input, model.graph.output): - if i.type.tensor_type.elem_type == 10: - i.type.tensor_type.elem_type = 1 - for i in model.graph.node: - if i.op_type == 'Cast' and i.attribute[0].i == 10: - i.attribute[0].i = 1 - for a in i.attribute: - if hasattr(a, 't'): - if a.t.data_type == 10: - attributeproto_fp16_to_fp32(a.t) - return model.SerializeToString() - - -def make_onnx_cpu_runner(model_path): - options = ort.SessionOptions() - options.intra_op_num_threads = 4 - options.execution_mode = ort.ExecutionMode.ORT_SEQUENTIAL - options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL - model_data = convert_fp16_to_fp32(onnx.load(model_path)) - return ort.InferenceSession(model_data, options, providers=['CPUExecutionProvider']) diff --git a/sunnypilot/modeld_v2/runners/tinygrad_helpers.py b/sunnypilot/modeld_v2/runners/tinygrad_helpers.py deleted file mode 100644 index 776381341c..0000000000 --- a/sunnypilot/modeld_v2/runners/tinygrad_helpers.py +++ /dev/null @@ -1,8 +0,0 @@ - -from tinygrad.tensor import Tensor -from tinygrad.helpers import to_mv - -def qcom_tensor_from_opencl_address(opencl_address, shape, dtype): - cl_buf_desc_ptr = to_mv(opencl_address, 8).cast('Q')[0] - rawbuf_ptr = to_mv(cl_buf_desc_ptr, 0x100).cast('Q')[20] # offset 0xA0 is a raw gpu pointer. - return Tensor.from_blob(rawbuf_ptr, shape, dtype=dtype, device='QCOM') diff --git a/sunnypilot/modeld_v2/tests/test_buffer_logic_inspect.py b/sunnypilot/modeld_v2/tests/test_buffer_logic_inspect.py index f664db31b3..15009c94d9 100644 --- a/sunnypilot/modeld_v2/tests/test_buffer_logic_inspect.py +++ b/sunnypilot/modeld_v2/tests/test_buffer_logic_inspect.py @@ -53,7 +53,7 @@ class DummyModelRunner: self.is_20hz = False # Minimal prepare/run methods so ModelState can be run without actually running the model - def prepare_inputs(self, imgs_cl, numpy_inputs, frames): + def prepare_inputs(self, numpy_inputs): return None def run_model(self): @@ -105,7 +105,7 @@ def get_expected_indices(shape, constants, mode, key=None): @pytest.mark.parametrize("shapes,mode", SHAPE_MODE_PARAMS, indirect=["shapes"]) def test_buffer_shapes_and_indices(shapes, mode, apply_patches): - state = ModelState(None) + state = ModelState() constants = DummyModelRunner(shapes).constants for key in shapes: buf = state.temporal_buffers.get(key, None) @@ -236,7 +236,7 @@ def dynamic_buffer_update(state, key, new_val, mode): @pytest.mark.parametrize("shapes,mode", SHAPE_MODE_PARAMS, indirect=["shapes"]) @pytest.mark.parametrize("key", ["desire", "features_buffer", "prev_desired_curv"]) def test_buffer_update_equivalence(shapes, mode, key, apply_patches): - state = ModelState(None) + state = ModelState() if key == "desire": desire_keys = [k for k in shapes.keys() if k.startswith('desire')] if desire_keys: diff --git a/sunnypilot/modeld_v2/tests/test_warp.py b/sunnypilot/modeld_v2/tests/test_warp.py new file mode 100644 index 0000000000..daf0dd528c --- /dev/null +++ b/sunnypilot/modeld_v2/tests/test_warp.py @@ -0,0 +1,102 @@ +import os +os.environ['DEV'] = 'CPU' +import pytest +import numpy as np +from openpilot.selfdrive.modeld.compile_warp import get_nv12_info, CAMERA_CONFIGS +from openpilot.sunnypilot.modeld_v2.warp import Warp, MODEL_W, MODEL_H + +VISION_NAME_PAIRS = [ # needed to account for supercombos input_imgs + ('img', 'big_img'), + ('input_imgs', 'big_input_imgs'), +] + + +class MockVisionBuf: + def __init__(self, w, h): + self.width = w + self.height = h + _, _, _, yuv_size = get_nv12_info(w, h) + self.data = np.zeros(yuv_size, dtype=np.uint8) + + +@pytest.mark.parametrize("buffer_length", [2, 5]) +def test_warp_initialization(buffer_length): + warp = Warp(buffer_length) + assert warp.buffer_length == buffer_length + assert warp.img_buffer_shape == (buffer_length * 6, MODEL_H // 2, MODEL_W // 2) + + +@pytest.mark.parametrize("buffer_length", [2, 5]) +@pytest.mark.parametrize("cam_w, cam_h", CAMERA_CONFIGS) +@pytest.mark.parametrize("road, wide", VISION_NAME_PAIRS) +def test_warp_process(buffer_length, cam_w, cam_h, road, wide): + warp = Warp(buffer_length) + mock_buf = MockVisionBuf(cam_w, cam_h) + transform = np.eye(3, dtype=np.float32).flatten() + bufs = {road: mock_buf, wide: mock_buf} + transforms = {road: transform, wide: transform} + + out = warp.process(bufs, transforms) + assert isinstance(out, dict) + assert road in out and wide in out + assert out[road].shape == (1, 12, MODEL_H // 2, MODEL_W // 2) + assert out[wide].shape == (1, 12, MODEL_H // 2, MODEL_W // 2) + + key = (cam_w, cam_h) + assert key in warp.jit_cache + + out2 = warp.process(bufs, transforms) + assert out2[road].shape == out[road].shape + + +@pytest.mark.parametrize("road, wide", VISION_NAME_PAIRS) +def test_warp_buffer_shift(road, wide): + warp = Warp(2) + cam_w, cam_h = CAMERA_CONFIGS[1] + transform = np.eye(3, dtype=np.float32).flatten() + + buf1 = MockVisionBuf(cam_w, cam_h) + buf1.data[0] = 255 + bufs1 = {road: buf1, wide: buf1} + transforms = {road: transform, wide: transform} + out1 = warp.process(bufs1, transforms) + road1 = out1[road].numpy().copy() + + buf2 = MockVisionBuf(cam_w, cam_h) + buf2.data[0] = 128 + bufs2 = {road: buf2, wide: buf2} + out2 = warp.process(bufs2, transforms) + assert not np.array_equal(road1, out2[road].numpy()) + + +@pytest.mark.parametrize("buffer_length", [2, 5]) +@pytest.mark.parametrize("road, wide", VISION_NAME_PAIRS) +def test_warp_buffer_accumulation(buffer_length, road, wide): + warp = Warp(buffer_length) + cam_w, cam_h = CAMERA_CONFIGS[0] + transform = np.eye(3, dtype=np.float32).flatten() + transforms = {road: transform, wide: transform} + outputs = [] + + for i in range(buffer_length + 1): + buf = MockVisionBuf(cam_w, cam_h) + buf.data[:] = i * 10 + out = warp.process({road: buf, wide: buf}, transforms) + outputs.append(out[road].numpy().copy()) + + assert warp.full_buffers['img'].shape == (buffer_length * 6, MODEL_H // 2, MODEL_W // 2) + for i in range(1, len(outputs)): + assert not np.array_equal(outputs[i - 1], outputs[i]) + + +def test_warp_different_cameras_same_instance(): + warp = Warp(2) + transform = np.eye(3, dtype=np.float32).flatten() + + buf1 = MockVisionBuf(*CAMERA_CONFIGS[0]) + warp.process({'img': buf1, 'big_img': buf1}, {'img': transform, 'big_img': transform}) + assert len(warp.jit_cache) == 1 + + buf2 = MockVisionBuf(*CAMERA_CONFIGS[1]) + warp.process({'img': buf2, 'big_img': buf2}, {'img': transform, 'big_img': transform}) + assert len(warp.jit_cache) == 2 diff --git a/sunnypilot/modeld_v2/transforms/loadyuv.cc b/sunnypilot/modeld_v2/transforms/loadyuv.cc deleted file mode 100644 index eb669a5929..0000000000 --- a/sunnypilot/modeld_v2/transforms/loadyuv.cc +++ /dev/null @@ -1,76 +0,0 @@ -#include "sunnypilot/modeld_v2/transforms/loadyuv.h" - -#include -#include -#include - -void loadyuv_init(LoadYUVState* s, cl_context ctx, cl_device_id device_id, int width, int height) { - memset(s, 0, sizeof(*s)); - - s->width = width; - s->height = height; - - char args[1024]; - snprintf(args, sizeof(args), - "-cl-fast-relaxed-math -cl-denorms-are-zero " - "-DTRANSFORMED_WIDTH=%d -DTRANSFORMED_HEIGHT=%d", - width, height); - cl_program prg = cl_program_from_file(ctx, device_id, LOADYUV_PATH, args); - - s->loadys_krnl = CL_CHECK_ERR(clCreateKernel(prg, "loadys", &err)); - s->loaduv_krnl = CL_CHECK_ERR(clCreateKernel(prg, "loaduv", &err)); - s->copy_krnl = CL_CHECK_ERR(clCreateKernel(prg, "copy", &err)); - - // done with this - CL_CHECK(clReleaseProgram(prg)); -} - -void loadyuv_destroy(LoadYUVState* s) { - CL_CHECK(clReleaseKernel(s->loadys_krnl)); - CL_CHECK(clReleaseKernel(s->loaduv_krnl)); - CL_CHECK(clReleaseKernel(s->copy_krnl)); -} - -void loadyuv_queue(LoadYUVState* s, cl_command_queue q, - cl_mem y_cl, cl_mem u_cl, cl_mem v_cl, - cl_mem out_cl) { - cl_int global_out_off = 0; - - CL_CHECK(clSetKernelArg(s->loadys_krnl, 0, sizeof(cl_mem), &y_cl)); - CL_CHECK(clSetKernelArg(s->loadys_krnl, 1, sizeof(cl_mem), &out_cl)); - CL_CHECK(clSetKernelArg(s->loadys_krnl, 2, sizeof(cl_int), &global_out_off)); - - const size_t loadys_work_size = (s->width*s->height)/8; - CL_CHECK(clEnqueueNDRangeKernel(q, s->loadys_krnl, 1, NULL, - &loadys_work_size, NULL, 0, 0, NULL)); - - const size_t loaduv_work_size = ((s->width/2)*(s->height/2))/8; - global_out_off += (s->width*s->height); - - CL_CHECK(clSetKernelArg(s->loaduv_krnl, 0, sizeof(cl_mem), &u_cl)); - CL_CHECK(clSetKernelArg(s->loaduv_krnl, 1, sizeof(cl_mem), &out_cl)); - CL_CHECK(clSetKernelArg(s->loaduv_krnl, 2, sizeof(cl_int), &global_out_off)); - - CL_CHECK(clEnqueueNDRangeKernel(q, s->loaduv_krnl, 1, NULL, - &loaduv_work_size, NULL, 0, 0, NULL)); - - global_out_off += (s->width/2)*(s->height/2); - - CL_CHECK(clSetKernelArg(s->loaduv_krnl, 0, sizeof(cl_mem), &v_cl)); - CL_CHECK(clSetKernelArg(s->loaduv_krnl, 1, sizeof(cl_mem), &out_cl)); - CL_CHECK(clSetKernelArg(s->loaduv_krnl, 2, sizeof(cl_int), &global_out_off)); - - CL_CHECK(clEnqueueNDRangeKernel(q, s->loaduv_krnl, 1, NULL, - &loaduv_work_size, NULL, 0, 0, NULL)); -} - -void copy_queue(LoadYUVState* s, cl_command_queue q, cl_mem src, cl_mem dst, - size_t src_offset, size_t dst_offset, size_t size) { - CL_CHECK(clSetKernelArg(s->copy_krnl, 0, sizeof(cl_mem), &src)); - CL_CHECK(clSetKernelArg(s->copy_krnl, 1, sizeof(cl_mem), &dst)); - CL_CHECK(clSetKernelArg(s->copy_krnl, 2, sizeof(cl_int), &src_offset)); - CL_CHECK(clSetKernelArg(s->copy_krnl, 3, sizeof(cl_int), &dst_offset)); - const size_t copy_work_size = size/8; - CL_CHECK(clEnqueueNDRangeKernel(q, s->copy_krnl, 1, NULL, - ©_work_size, NULL, 0, 0, NULL)); -} \ No newline at end of file diff --git a/sunnypilot/modeld_v2/transforms/loadyuv.cl b/sunnypilot/modeld_v2/transforms/loadyuv.cl deleted file mode 100644 index 970187a6d7..0000000000 --- a/sunnypilot/modeld_v2/transforms/loadyuv.cl +++ /dev/null @@ -1,47 +0,0 @@ -#define UV_SIZE ((TRANSFORMED_WIDTH/2)*(TRANSFORMED_HEIGHT/2)) - -__kernel void loadys(__global uchar8 const * const Y, - __global uchar * out, - int out_offset) -{ - const int gid = get_global_id(0); - const int ois = gid * 8; - const int oy = ois / TRANSFORMED_WIDTH; - const int ox = ois % TRANSFORMED_WIDTH; - - const uchar8 ys = Y[gid]; - - // 02 - // 13 - - __global uchar* outy0; - __global uchar* outy1; - if ((oy & 1) == 0) { - outy0 = out + out_offset; //y0 - outy1 = out + out_offset + UV_SIZE*2; //y2 - } else { - outy0 = out + out_offset + UV_SIZE; //y1 - outy1 = out + out_offset + UV_SIZE*3; //y3 - } - - vstore4(ys.s0246, 0, outy0 + (oy/2) * (TRANSFORMED_WIDTH/2) + ox/2); - vstore4(ys.s1357, 0, outy1 + (oy/2) * (TRANSFORMED_WIDTH/2) + ox/2); -} - -__kernel void loaduv(__global uchar8 const * const in, - __global uchar8 * out, - int out_offset) -{ - const int gid = get_global_id(0); - const uchar8 inv = in[gid]; - out[gid + out_offset / 8] = inv; -} - -__kernel void copy(__global uchar8 * in, - __global uchar8 * out, - int in_offset, - int out_offset) -{ - const int gid = get_global_id(0); - out[gid + out_offset / 8] = in[gid + in_offset / 8]; -} diff --git a/sunnypilot/modeld_v2/transforms/loadyuv.h b/sunnypilot/modeld_v2/transforms/loadyuv.h deleted file mode 100644 index 659059cd25..0000000000 --- a/sunnypilot/modeld_v2/transforms/loadyuv.h +++ /dev/null @@ -1,20 +0,0 @@ -#pragma once - -#include "common/clutil.h" - -typedef struct { - int width, height; - cl_kernel loadys_krnl, loaduv_krnl, copy_krnl; -} LoadYUVState; - -void loadyuv_init(LoadYUVState* s, cl_context ctx, cl_device_id device_id, int width, int height); - -void loadyuv_destroy(LoadYUVState* s); - -void loadyuv_queue(LoadYUVState* s, cl_command_queue q, - cl_mem y_cl, cl_mem u_cl, cl_mem v_cl, - cl_mem out_cl); - - -void copy_queue(LoadYUVState* s, cl_command_queue q, cl_mem src, cl_mem dst, - size_t src_offset, size_t dst_offset, size_t size); \ No newline at end of file diff --git a/sunnypilot/modeld_v2/transforms/transform.cc b/sunnypilot/modeld_v2/transforms/transform.cc deleted file mode 100644 index adc9bcebf4..0000000000 --- a/sunnypilot/modeld_v2/transforms/transform.cc +++ /dev/null @@ -1,97 +0,0 @@ -#include "sunnypilot/modeld_v2/transforms/transform.h" - -#include -#include - -#include "common/clutil.h" - -void transform_init(Transform* s, cl_context ctx, cl_device_id device_id) { - memset(s, 0, sizeof(*s)); - - cl_program prg = cl_program_from_file(ctx, device_id, TRANSFORM_PATH, ""); - s->krnl = CL_CHECK_ERR(clCreateKernel(prg, "warpPerspective", &err)); - // done with this - CL_CHECK(clReleaseProgram(prg)); - - s->m_y_cl = CL_CHECK_ERR(clCreateBuffer(ctx, CL_MEM_READ_WRITE, 3*3*sizeof(float), NULL, &err)); - s->m_uv_cl = CL_CHECK_ERR(clCreateBuffer(ctx, CL_MEM_READ_WRITE, 3*3*sizeof(float), NULL, &err)); -} - -void transform_destroy(Transform* s) { - CL_CHECK(clReleaseMemObject(s->m_y_cl)); - CL_CHECK(clReleaseMemObject(s->m_uv_cl)); - CL_CHECK(clReleaseKernel(s->krnl)); -} - -void transform_queue(Transform* s, - cl_command_queue q, - cl_mem in_yuv, int in_width, int in_height, int in_stride, int in_uv_offset, - cl_mem out_y, cl_mem out_u, cl_mem out_v, - int out_width, int out_height, - const mat3& projection) { - const int zero = 0; - - // sampled using pixel center origin - // (because that's how fastcv and opencv does it) - - mat3 projection_y = projection; - - // in and out uv is half the size of y. - mat3 projection_uv = transform_scale_buffer(projection, 0.5); - - CL_CHECK(clEnqueueWriteBuffer(q, s->m_y_cl, CL_TRUE, 0, 3*3*sizeof(float), (void*)projection_y.v, 0, NULL, NULL)); - CL_CHECK(clEnqueueWriteBuffer(q, s->m_uv_cl, CL_TRUE, 0, 3*3*sizeof(float), (void*)projection_uv.v, 0, NULL, NULL)); - - const int in_y_width = in_width; - const int in_y_height = in_height; - const int in_y_px_stride = 1; - const int in_uv_width = in_width/2; - const int in_uv_height = in_height/2; - const int in_uv_px_stride = 2; - const int in_u_offset = in_uv_offset; - const int in_v_offset = in_uv_offset + 1; - - const int out_y_width = out_width; - const int out_y_height = out_height; - const int out_uv_width = out_width/2; - const int out_uv_height = out_height/2; - - CL_CHECK(clSetKernelArg(s->krnl, 0, sizeof(cl_mem), &in_yuv)); // src - CL_CHECK(clSetKernelArg(s->krnl, 1, sizeof(cl_int), &in_stride)); // src_row_stride - CL_CHECK(clSetKernelArg(s->krnl, 2, sizeof(cl_int), &in_y_px_stride)); // src_px_stride - CL_CHECK(clSetKernelArg(s->krnl, 3, sizeof(cl_int), &zero)); // src_offset - CL_CHECK(clSetKernelArg(s->krnl, 4, sizeof(cl_int), &in_y_height)); // src_rows - CL_CHECK(clSetKernelArg(s->krnl, 5, sizeof(cl_int), &in_y_width)); // src_cols - CL_CHECK(clSetKernelArg(s->krnl, 6, sizeof(cl_mem), &out_y)); // dst - CL_CHECK(clSetKernelArg(s->krnl, 7, sizeof(cl_int), &out_y_width)); // dst_row_stride - CL_CHECK(clSetKernelArg(s->krnl, 8, sizeof(cl_int), &zero)); // dst_offset - CL_CHECK(clSetKernelArg(s->krnl, 9, sizeof(cl_int), &out_y_height)); // dst_rows - CL_CHECK(clSetKernelArg(s->krnl, 10, sizeof(cl_int), &out_y_width)); // dst_cols - CL_CHECK(clSetKernelArg(s->krnl, 11, sizeof(cl_mem), &s->m_y_cl)); // M - - const size_t work_size_y[2] = {(size_t)out_y_width, (size_t)out_y_height}; - - CL_CHECK(clEnqueueNDRangeKernel(q, s->krnl, 2, NULL, - (const size_t*)&work_size_y, NULL, 0, 0, NULL)); - - const size_t work_size_uv[2] = {(size_t)out_uv_width, (size_t)out_uv_height}; - - CL_CHECK(clSetKernelArg(s->krnl, 2, sizeof(cl_int), &in_uv_px_stride)); // src_px_stride - CL_CHECK(clSetKernelArg(s->krnl, 3, sizeof(cl_int), &in_u_offset)); // src_offset - CL_CHECK(clSetKernelArg(s->krnl, 4, sizeof(cl_int), &in_uv_height)); // src_rows - CL_CHECK(clSetKernelArg(s->krnl, 5, sizeof(cl_int), &in_uv_width)); // src_cols - CL_CHECK(clSetKernelArg(s->krnl, 6, sizeof(cl_mem), &out_u)); // dst - CL_CHECK(clSetKernelArg(s->krnl, 7, sizeof(cl_int), &out_uv_width)); // dst_row_stride - CL_CHECK(clSetKernelArg(s->krnl, 8, sizeof(cl_int), &zero)); // dst_offset - CL_CHECK(clSetKernelArg(s->krnl, 9, sizeof(cl_int), &out_uv_height)); // dst_rows - CL_CHECK(clSetKernelArg(s->krnl, 10, sizeof(cl_int), &out_uv_width)); // dst_cols - CL_CHECK(clSetKernelArg(s->krnl, 11, sizeof(cl_mem), &s->m_uv_cl)); // M - - CL_CHECK(clEnqueueNDRangeKernel(q, s->krnl, 2, NULL, - (const size_t*)&work_size_uv, NULL, 0, 0, NULL)); - CL_CHECK(clSetKernelArg(s->krnl, 3, sizeof(cl_int), &in_v_offset)); // src_ofset - CL_CHECK(clSetKernelArg(s->krnl, 6, sizeof(cl_mem), &out_v)); // dst - - CL_CHECK(clEnqueueNDRangeKernel(q, s->krnl, 2, NULL, - (const size_t*)&work_size_uv, NULL, 0, 0, NULL)); -} diff --git a/sunnypilot/modeld_v2/transforms/transform.cl b/sunnypilot/modeld_v2/transforms/transform.cl deleted file mode 100644 index 2ca25920cd..0000000000 --- a/sunnypilot/modeld_v2/transforms/transform.cl +++ /dev/null @@ -1,54 +0,0 @@ -#define INTER_BITS 5 -#define INTER_TAB_SIZE (1 << INTER_BITS) -#define INTER_SCALE 1.f / INTER_TAB_SIZE - -#define INTER_REMAP_COEF_BITS 15 -#define INTER_REMAP_COEF_SCALE (1 << INTER_REMAP_COEF_BITS) - -__kernel void warpPerspective(__global const uchar * src, - int src_row_stride, int src_px_stride, int src_offset, int src_rows, int src_cols, - __global uchar * dst, - int dst_row_stride, int dst_offset, int dst_rows, int dst_cols, - __constant float * M) -{ - int dx = get_global_id(0); - int dy = get_global_id(1); - - if (dx < dst_cols && dy < dst_rows) - { - float X0 = M[0] * dx + M[1] * dy + M[2]; - float Y0 = M[3] * dx + M[4] * dy + M[5]; - float W = M[6] * dx + M[7] * dy + M[8]; - W = W != 0.0f ? INTER_TAB_SIZE / W : 0.0f; - int X = rint(X0 * W), Y = rint(Y0 * W); - - int sx = convert_short_sat(X >> INTER_BITS); - int sy = convert_short_sat(Y >> INTER_BITS); - - short sx_clamp = clamp(sx, 0, src_cols - 1); - short sx_p1_clamp = clamp(sx + 1, 0, src_cols - 1); - short sy_clamp = clamp(sy, 0, src_rows - 1); - short sy_p1_clamp = clamp(sy + 1, 0, src_rows - 1); - int v0 = convert_int(src[mad24(sy_clamp, src_row_stride, src_offset + sx_clamp*src_px_stride)]); - int v1 = convert_int(src[mad24(sy_clamp, src_row_stride, src_offset + sx_p1_clamp*src_px_stride)]); - int v2 = convert_int(src[mad24(sy_p1_clamp, src_row_stride, src_offset + sx_clamp*src_px_stride)]); - int v3 = convert_int(src[mad24(sy_p1_clamp, src_row_stride, src_offset + sx_p1_clamp*src_px_stride)]); - - short ay = (short)(Y & (INTER_TAB_SIZE - 1)); - short ax = (short)(X & (INTER_TAB_SIZE - 1)); - float taby = 1.f/INTER_TAB_SIZE*ay; - float tabx = 1.f/INTER_TAB_SIZE*ax; - - int dst_index = mad24(dy, dst_row_stride, dst_offset + dx); - - int itab0 = convert_short_sat_rte( (1.0f-taby)*(1.0f-tabx) * INTER_REMAP_COEF_SCALE ); - int itab1 = convert_short_sat_rte( (1.0f-taby)*tabx * INTER_REMAP_COEF_SCALE ); - int itab2 = convert_short_sat_rte( taby*(1.0f-tabx) * INTER_REMAP_COEF_SCALE ); - int itab3 = convert_short_sat_rte( taby*tabx * INTER_REMAP_COEF_SCALE ); - - int val = v0 * itab0 + v1 * itab1 + v2 * itab2 + v3 * itab3; - - uchar pix = convert_uchar_sat((val + (1 << (INTER_REMAP_COEF_BITS-1))) >> INTER_REMAP_COEF_BITS); - dst[dst_index] = pix; - } -} diff --git a/sunnypilot/modeld_v2/transforms/transform.h b/sunnypilot/modeld_v2/transforms/transform.h deleted file mode 100644 index 771a7054b3..0000000000 --- a/sunnypilot/modeld_v2/transforms/transform.h +++ /dev/null @@ -1,25 +0,0 @@ -#pragma once - -#define CL_USE_DEPRECATED_OPENCL_1_2_APIS -#ifdef __APPLE__ -#include -#else -#include -#endif - -#include "common/mat.h" - -typedef struct { - cl_kernel krnl; - cl_mem m_y_cl, m_uv_cl; -} Transform; - -void transform_init(Transform* s, cl_context ctx, cl_device_id device_id); - -void transform_destroy(Transform* transform); - -void transform_queue(Transform* s, cl_command_queue q, - cl_mem yuv, int in_width, int in_height, int in_stride, int in_uv_offset, - cl_mem out_y, cl_mem out_u, cl_mem out_v, - int out_width, int out_height, - const mat3& projection); diff --git a/sunnypilot/modeld_v2/warp.py b/sunnypilot/modeld_v2/warp.py new file mode 100644 index 0000000000..829cbcca49 --- /dev/null +++ b/sunnypilot/modeld_v2/warp.py @@ -0,0 +1,137 @@ +import pickle +import time +import numpy as np +from pathlib import Path +from tinygrad.tensor import Tensor +from tinygrad.engine.jit import TinyJit +from tinygrad.device import Device + +from openpilot.system.camerad.cameras.nv12_info import get_nv12_info +from openpilot.selfdrive.modeld.compile_warp import ( + CAMERA_CONFIGS, MEDMODEL_INPUT_SIZE, make_frame_prepare, make_update_both_imgs, + warp_pkl_path, +) + +MODELS_DIR = Path(__file__).parent / 'models' +MODEL_W, MODEL_H = MEDMODEL_INPUT_SIZE +UPSTREAM_BUFFER_LENGTH = 5 + + +def v2_warp_pkl_path(cam_w, cam_h, buffer_length): + return MODELS_DIR / f'warp_{cam_w}x{cam_h}_b{buffer_length}_tinygrad.pkl' + + +def compile_v2_warp(cam_w, cam_h, buffer_length): + _, _, _, yuv_size = get_nv12_info(cam_w, cam_h) + img_buffer_shape = (buffer_length * 6, MODEL_H // 2, MODEL_W // 2) + + print(f"Compiling v2 warp for {cam_w}x{cam_h} buffer_length={buffer_length}...") + + frame_prepare = make_frame_prepare(cam_w, cam_h, MODEL_W, MODEL_H) + update_both_imgs = make_update_both_imgs(frame_prepare, MODEL_W, MODEL_H) + update_img_jit = TinyJit(update_both_imgs, prune=True) + + full_buffer = Tensor.zeros(img_buffer_shape, dtype='uint8').contiguous().realize() + big_full_buffer = Tensor.zeros(img_buffer_shape, dtype='uint8').contiguous().realize() + full_buffer_np = np.zeros(img_buffer_shape, dtype=np.uint8) + big_full_buffer_np = np.zeros(img_buffer_shape, dtype=np.uint8) + + for i in range(10): + new_frame_np = (32 * np.random.randn(yuv_size).astype(np.float32) + 128).clip(0, 255).astype(np.uint8) + img_inputs = [full_buffer, + Tensor.from_blob(new_frame_np.ctypes.data, (yuv_size,), dtype='uint8').realize(), + Tensor(Tensor.randn(3, 3).mul(8).realize().numpy(), device='NPY')] + new_big_frame_np = (32 * np.random.randn(yuv_size).astype(np.float32) + 128).clip(0, 255).astype(np.uint8) + big_img_inputs = [big_full_buffer, + Tensor.from_blob(new_big_frame_np.ctypes.data, (yuv_size,), dtype='uint8').realize(), + Tensor(Tensor.randn(3, 3).mul(8).realize().numpy(), device='NPY')] + inputs = img_inputs + big_img_inputs + Device.default.synchronize() + + inputs_np = [x.numpy() for x in inputs] + inputs_np[0] = full_buffer_np + inputs_np[3] = big_full_buffer_np + + st = time.perf_counter() + out = update_img_jit(*inputs) + full_buffer = out[0].contiguous().realize().clone() + big_full_buffer = out[2].contiguous().realize().clone() + mt = time.perf_counter() + Device.default.synchronize() + et = time.perf_counter() + print(f" [{i+1}/10] enqueue {(mt-st)*1e3:6.2f} ms -- total {(et-st)*1e3:6.2f} ms") + + pkl_path = v2_warp_pkl_path(cam_w, cam_h, buffer_length) + with open(pkl_path, "wb") as f: + pickle.dump(update_img_jit, f) + print(f" Saved to {pkl_path}") + + jit = pickle.load(open(pkl_path, "rb")) + jit(*inputs) + + +class Warp: + def __init__(self, buffer_length=2): + self.buffer_length = buffer_length + self.img_buffer_shape = (buffer_length * 6, MODEL_H // 2, MODEL_W // 2) + + self.jit_cache = {} + self.full_buffers = {k: Tensor.zeros(self.img_buffer_shape, dtype='uint8').contiguous().realize() for k in ['img', 'big_img']} + self._blob_cache: dict[int, Tensor] = {} + self._nv12_cache: dict[tuple[int, int], int] = {} + self.transforms_np = {k: np.zeros((3, 3), dtype=np.float32) for k in ['img', 'big_img']} + self.transforms = {k: Tensor(v, device='NPY').realize() for k, v in self.transforms_np.items()} + + def process(self, bufs, transforms): + if not bufs: + return {} + road = next(n for n in bufs if 'big' not in n) + wide = next(n for n in bufs if 'big' in n) + cam_w, cam_h = bufs[road].width, bufs[road].height + key = (cam_w, cam_h) + + if key not in self.jit_cache: + v2_pkl = v2_warp_pkl_path(cam_w, cam_h, self.buffer_length) + if v2_pkl.exists(): + with open(v2_pkl, 'rb') as f: + self.jit_cache[key] = pickle.load(f) + elif self.buffer_length == UPSTREAM_BUFFER_LENGTH: + upstream_pkl = warp_pkl_path(cam_w, cam_h) + if upstream_pkl.exists(): + with open(upstream_pkl, 'rb') as f: + self.jit_cache[key] = pickle.load(f) + if key not in self.jit_cache: + frame_prepare = make_frame_prepare(cam_w, cam_h, MODEL_W, MODEL_H) + update_both_imgs = make_update_both_imgs(frame_prepare, MODEL_W, MODEL_H) + self.jit_cache[key] = TinyJit(update_both_imgs, prune=True) + + if key not in self._nv12_cache: + self._nv12_cache[key] = get_nv12_info(cam_w, cam_h)[3] + yuv_size = self._nv12_cache[key] + + road_ptr = bufs[road].data.ctypes.data + wide_ptr = bufs[wide].data.ctypes.data + if road_ptr not in self._blob_cache: + self._blob_cache[road_ptr] = Tensor.from_blob(road_ptr, (yuv_size,), dtype='uint8') + if wide_ptr not in self._blob_cache: + self._blob_cache[wide_ptr] = Tensor.from_blob(wide_ptr, (yuv_size,), dtype='uint8') + road_blob = self._blob_cache[road_ptr] + wide_blob = self._blob_cache[wide_ptr] if wide_ptr != road_ptr else Tensor.from_blob(wide_ptr, (yuv_size,), dtype='uint8') + np.copyto(self.transforms_np['img'], transforms[road].reshape(3, 3)) + np.copyto(self.transforms_np['big_img'], transforms[wide].reshape(3, 3)) + + Device.default.synchronize() + res = self.jit_cache[key]( + self.full_buffers['img'], road_blob, self.transforms['img'], + self.full_buffers['big_img'], wide_blob, self.transforms['big_img'], + ) + self.full_buffers['img'], out_road = res[0].realize(), res[1].realize() + self.full_buffers['big_img'], out_wide = res[2].realize(), res[3].realize() + + return {road: out_road, wide: out_wide} + + +if __name__ == "__main__": + for cam_w, cam_h in CAMERA_CONFIGS: + for bl in [2, 5]: + compile_v2_warp(cam_w, cam_h, bl) diff --git a/sunnypilot/models/fetcher.py b/sunnypilot/models/fetcher.py index 0de7496578..5990ee2e41 100644 --- a/sunnypilot/models/fetcher.py +++ b/sunnypilot/models/fetcher.py @@ -116,7 +116,7 @@ class ModelCache: class ModelFetcher: """Handles fetching and caching of model data from remote source""" - MODEL_URL = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-docs/refs/heads/gh-pages/docs/driving_models_v14.json" + MODEL_URL = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-docs/refs/heads/gh-pages/docs/driving_models_v15.json" def __init__(self, params: Params): self.params = params diff --git a/sunnypilot/models/helpers.py b/sunnypilot/models/helpers.py index 7fcf7f85ef..98f7d9e385 100644 --- a/sunnypilot/models/helpers.py +++ b/sunnypilot/models/helpers.py @@ -13,16 +13,13 @@ import numpy as np from openpilot.common.params import Params from cereal import custom from openpilot.sunnypilot.modeld.constants import Meta, MetaTombRaider, MetaSimPose -from openpilot.sunnypilot.modeld.runners import ModelRunner -from openpilot.system.hardware import PC from openpilot.system.hardware.hw import Paths from pathlib import Path # see the README.md for more details on the model selector versioning -CURRENT_SELECTOR_VERSION = 14 +CURRENT_SELECTOR_VERSION = 15 REQUIRED_MIN_SELECTOR_VERSION = 14 -USE_ONNX = os.getenv('USE_ONNX', PC) CUSTOM_MODEL_PATH = Paths.model_root() METADATA_PATH = Path(__file__).parent / '../models/supercombo_metadata.pkl' @@ -122,16 +119,6 @@ def _get_model(): return None -def get_model_path(): - if USE_ONNX: - return {ModelRunner.ONNX: Path(__file__).parent / '../models/supercombo.onnx'} - - if model := _get_model(): - return {ModelRunner.THNEED: f"{CUSTOM_MODEL_PATH}/{model.artifact.fileName}"} - - return {ModelRunner.THNEED: Path(__file__).parent / '../models/supercombo.thneed'} - - def load_metadata(): metadata_path = METADATA_PATH diff --git a/sunnypilot/models/runners/constants.py b/sunnypilot/models/runners/constants.py index cbd1fdb376..acb316888c 100644 --- a/sunnypilot/models/runners/constants.py +++ b/sunnypilot/models/runners/constants.py @@ -1,6 +1,5 @@ import os import numpy as np -from openpilot.sunnypilot.modeld_v2.models.commonmodel_pyx import DrivingModelFrame, CLMem from openpilot.system.hardware.hw import Paths from cereal import custom @@ -8,8 +7,6 @@ from cereal import custom NumpyDict = dict[str, np.ndarray] ShapeDict = dict[str, tuple[int, ...]] SliceDict = dict[str, slice] -CLMemDict = dict[str, CLMem] -FrameDict = dict[str, DrivingModelFrame] ModelType = custom.ModelManagerSP.Model.Type Model = custom.ModelManagerSP.Model diff --git a/sunnypilot/models/runners/model_runner.py b/sunnypilot/models/runners/model_runner.py index a49ff4d206..051fa349db 100644 --- a/sunnypilot/models/runners/model_runner.py +++ b/sunnypilot/models/runners/model_runner.py @@ -2,7 +2,7 @@ from abc import abstractmethod, ABC import numpy as np from openpilot.sunnypilot.models.helpers import get_active_bundle -from openpilot.sunnypilot.models.runners.constants import NumpyDict, ShapeDict, CLMemDict, FrameDict, Model, SliceDict, SEND_RAW_PRED +from openpilot.sunnypilot.models.runners.constants import NumpyDict, ShapeDict, Model, SliceDict, SEND_RAW_PRED from openpilot.system.hardware.hw import Paths import pickle @@ -133,13 +133,11 @@ class ModelRunner(ModularRunner): raise ValueError("Model data is not available. Ensure the model is loaded correctly.") @abstractmethod - def prepare_inputs(self, imgs_cl: CLMemDict, numpy_inputs: NumpyDict, frames: FrameDict) -> dict: + def prepare_inputs(self, numpy_inputs: NumpyDict) -> dict: """ Abstract method to prepare inputs for model inference. - :param imgs_cl: Dictionary of OpenCL memory objects for image inputs. :param numpy_inputs: Dictionary of numpy arrays for non-image inputs. - :param frames: Dictionary of DrivingModelFrame objects for context. :return: Dictionary of prepared inputs ready for the model. """ raise NotImplementedError diff --git a/sunnypilot/models/runners/onnx/onnx_runner.py b/sunnypilot/models/runners/onnx/onnx_runner.py deleted file mode 100644 index 1ffead4560..0000000000 --- a/sunnypilot/models/runners/onnx/onnx_runner.py +++ /dev/null @@ -1,62 +0,0 @@ -import numpy as np - -from openpilot.sunnypilot.modeld_v2 import MODEL_PATH -from openpilot.sunnypilot.modeld_v2.runners.ort_helpers import make_onnx_cpu_runner, ORT_TYPES_TO_NP_TYPES -from openpilot.sunnypilot.models.runners.constants import ModelType, ShapeDict, CLMemDict, NumpyDict, FrameDict -from openpilot.sunnypilot.models.runners.model_runner import ModelRunner -from openpilot.sunnypilot.modeld_v2.constants import ModelConstants - - -class ONNXRunner(ModelRunner): - """ - A ModelRunner implementation for executing ONNX models using ONNX Runtime CPU. - - Handles loading the ONNX model, preparing inputs as numpy arrays, running - inference, and parsing outputs. This runner is typically used on non-TICI platforms. - """ - def __init__(self): - super().__init__() - # Initialize ONNX Runtime session for the model at MODEL_PATH - self.runner = make_onnx_cpu_runner(MODEL_PATH) - # Map expected input names to numpy dtypes - self.input_to_nptype = { - model_input.name: ORT_TYPES_TO_NP_TYPES[model_input.type] - for model_input in self.runner.get_inputs() - } - # For ONNX, _model_data isn't strictly necessary as shapes/types come from the runner - # However, we might still need output_slices if custom models define them. - # We assume supercombo type for potentially loading output_slices metadata if available. - self._model_data = self.models.get(ModelType.supercombo) - self._constants = ModelConstants # Constants for ONNX models, if needed - - @property - def input_shapes(self) -> ShapeDict: - """Returns the input shapes defined in the ONNX model.""" - # ONNX shapes are derived directly from the model definition via the runner - return {runner_input.name: runner_input.shape for runner_input in self.runner.get_inputs()} - - def prepare_inputs(self, imgs_cl: CLMemDict, numpy_inputs: NumpyDict, frames: FrameDict) -> dict: - """Prepares inputs for the ONNX model as numpy arrays.""" - self.inputs = numpy_inputs # Start with non-image numpy inputs - # Convert image inputs from OpenCL buffers to numpy arrays - for key in imgs_cl: - buffer = frames[key].buffer_from_cl(imgs_cl[key]) - reshaped_buffer = buffer.reshape(self.input_shapes[key]) - self.inputs[key] = reshaped_buffer.astype(dtype=self.input_to_nptype[key]) - return self.inputs - - def _parse_outputs(self, model_outputs: np.ndarray) -> NumpyDict: - """Parses the raw ONNX model outputs using the standard Parser.""" - # Use slicing if metadata is available, otherwise pass raw outputs - if self._model_data is None: - raise ValueError("Model data is not available. Ensure the model is loaded correctly.") - - outputs_to_parse = self._slice_outputs(model_outputs) if self._model_data else {'raw_pred': model_outputs} - result: NumpyDict = self.parser_method_dict[self._model_data.model.type.raw](outputs_to_parse) - return result - - def _run_model(self) -> NumpyDict: - """Runs the ONNX model inference and parses the outputs.""" - # Execute the ONNX Runtime session - outputs = self.runner.run(None, self.inputs)[0].flatten() - return self._parse_outputs(outputs) diff --git a/sunnypilot/models/runners/tinygrad/tinygrad_runner.py b/sunnypilot/models/runners/tinygrad/tinygrad_runner.py index 2df1c65e08..9033c892eb 100644 --- a/sunnypilot/models/runners/tinygrad/tinygrad_runner.py +++ b/sunnypilot/models/runners/tinygrad/tinygrad_runner.py @@ -1,11 +1,9 @@ import pickle import numpy as np -from openpilot.sunnypilot.modeld_v2.runners.tinygrad_helpers import qcom_tensor_from_opencl_address -from openpilot.sunnypilot.models.runners.constants import CLMemDict, FrameDict, NumpyDict, ModelType, ShapeDict, CUSTOM_MODEL_PATH, SliceDict +from openpilot.sunnypilot.models.runners.constants import NumpyDict, ModelType, ShapeDict, CUSTOM_MODEL_PATH, SliceDict from openpilot.sunnypilot.models.runners.model_runner import ModelRunner from openpilot.sunnypilot.models.runners.tinygrad.model_types import PolicyTinygrad, VisionTinygrad, SupercomboTinygrad, OffPolicyTinygrad -from openpilot.system.hardware import TICI from openpilot.sunnypilot.models.split_model_constants import SplitModelConstants from openpilot.sunnypilot.modeld_v2.constants import ModelConstants @@ -54,37 +52,31 @@ class TinygradRunner(ModelRunner, SupercomboTinygrad, PolicyTinygrad, VisionTiny info = self.model_run.captured.expected_input_info[idx] self.input_to_dtype[name] = info[2] # dtype self.input_to_device[name] = info[3] # device + self._policy_cached = False @property def vision_input_names(self) -> list[str]: """Returns the list of vision input names from the input shapes.""" return [name for name in self.input_shapes.keys() if 'img' in name] - def prepare_vision_inputs(self, imgs_cl: CLMemDict, frames: FrameDict): - """Prepares vision (image) inputs as Tinygrad Tensors.""" - for key in imgs_cl: - if TICI and key not in self.inputs: - # On TICI, directly use OpenCL memory address for efficiency via QCOM extensions - self.inputs[key] = qcom_tensor_from_opencl_address(imgs_cl[key].mem_address, self.input_shapes[key], dtype=self.input_to_dtype[key]) - elif not TICI: - # On other platforms, copy data from CL buffer to a numpy array first - shape = frames[key].buffer_from_cl(imgs_cl[key]).reshape(self.input_shapes[key]) - self.inputs[key] = Tensor(shape, device=self.input_to_device[key], dtype=self.input_to_dtype[key]).realize() def prepare_policy_inputs(self, numpy_inputs: NumpyDict): - """Prepares non-image (policy) inputs as Tinygrad Tensors.""" - for key, value in numpy_inputs.items(): - self.inputs[key] = Tensor(value, device=self.input_to_device[key], dtype=self.input_to_dtype[key]).realize() + if not self._policy_cached: + for key, value in numpy_inputs.items(): + self.inputs[key] = Tensor(value, device='NPY').realize() + self._policy_cached = True - def prepare_inputs(self, imgs_cl: CLMemDict, numpy_inputs: NumpyDict, frames: FrameDict) -> dict: + def prepare_inputs(self, numpy_inputs: NumpyDict) -> dict: """Prepares all vision and policy inputs for the model.""" - self.prepare_vision_inputs(imgs_cl, frames) self.prepare_policy_inputs(numpy_inputs) + for key in self.vision_input_names: + if key in self.inputs: + self.inputs[key] = self.inputs[key].cast(self.input_to_dtype[key]) return self.inputs def _run_model(self) -> NumpyDict: """Runs the Tinygrad model inference and parses the outputs.""" - outputs = self.model_run(**self.inputs).numpy().flatten() + outputs = self.model_run(**self.inputs).contiguous().realize().uop.base.buffer.numpy().flatten() return self._parse_outputs(outputs) def _parse_outputs(self, model_outputs: np.ndarray) -> NumpyDict: @@ -120,6 +112,9 @@ class TinygradSplitRunner(ModelRunner): off_policy_output = self.off_policy_runner.run_model() outputs.update(off_policy_output) + if 'planplus' in outputs and 'plan' in outputs: + outputs['plan'] = outputs['plan'] + outputs['planplus'] + return outputs @property @@ -143,17 +138,18 @@ class TinygradSplitRunner(ModelRunner): slices.update(self.off_policy_runner.output_slices) return slices - def prepare_inputs(self, imgs_cl: CLMemDict, numpy_inputs: NumpyDict, frames: FrameDict) -> dict: + def prepare_inputs(self, numpy_inputs: NumpyDict) -> dict: """Prepares inputs for both vision and policy models.""" # Policy inputs only depend on numpy_inputs self.policy_runner.prepare_policy_inputs(numpy_inputs) - # Vision inputs depend on imgs_cl and frames - self.vision_runner.prepare_vision_inputs(imgs_cl, frames) + + for key in self.vision_input_names: + if key in self.inputs: + self.vision_runner.inputs[key] = self.inputs[key].cast(self.vision_runner.input_to_dtype[key]) + inputs = {**self.policy_runner.inputs, **self.vision_runner.inputs} if self.off_policy_runner: self.off_policy_runner.prepare_policy_inputs(numpy_inputs) inputs.update(self.off_policy_runner.inputs) - - # Return combined inputs (though they are stored within respective runners) return inputs diff --git a/system/manager/process_config.py b/system/manager/process_config.py index 793fbc07fa..2e43dfada6 100644 --- a/system/manager/process_config.py +++ b/system/manager/process_config.py @@ -80,10 +80,6 @@ def use_sunnylink_uploader_shim(started, params, CP: car.CarParams) -> bool: """Shim for use_sunnylink_uploader to match the process manager signature.""" return use_sunnylink_uploader(params) -def is_snpe_model(started, params, CP: car.CarParams) -> bool: - """Check if the active model runner is SNPE.""" - return bool(get_active_model_runner(params, not started) == custom.ModelManagerSP.Runner.snpe) - def is_tinygrad_model(started, params, CP: car.CarParams) -> bool: """Check if the active model runner is SNPE.""" return bool(get_active_model_runner(params, not started) == custom.ModelManagerSP.Runner.tinygrad) @@ -170,7 +166,6 @@ procs = [ procs += [ # Models PythonProcess("models_manager", "sunnypilot.models.manager", only_offroad), - NativeProcess("modeld_snpe", "sunnypilot/modeld", ["./modeld"], and_(only_onroad, is_snpe_model)), NativeProcess("modeld_tinygrad", "sunnypilot/modeld_v2", ["./modeld"], and_(only_onroad, is_tinygrad_model)), # Backup From 05e331736d2943c4e2fdec2c86ed53e862e6d012 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 27 Feb 2026 16:27:16 -0500 Subject: [PATCH 253/311] [bot] Update Python packages (#1715) Update Python packages Co-authored-by: github-actions[bot] --- docs/CARS.md | 3 +- opendbc_repo | 2 +- panda | 2 +- uv.lock | 102 +++++++++++++++++++++++++-------------------------- 4 files changed, 55 insertions(+), 54 deletions(-) diff --git a/docs/CARS.md b/docs/CARS.md index ab1eabeb13..dc8f1be4f1 100644 --- a/docs/CARS.md +++ b/docs/CARS.md @@ -4,7 +4,7 @@ A supported vehicle is one that just works when you install a comma device. All supported cars provide a better experience than any stock system. Supported vehicles reference the US market unless otherwise specified. -# 335 Supported Cars +# 336 Supported Cars |Make|Model|Supported Package|ACC|No ACC accel below|No ALC below|Steering Torque|Resume from stop|Hardware Needed
 |Video|Setup Video| |---|---|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:| @@ -166,6 +166,7 @@ A supported vehicle is one that just works when you install a comma device. All |Kia|Forte 2022-23|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai E connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Kia|K5 2021-24|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Kia|K5 Hybrid 2020-22|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Kia|K7 2017|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|K8 Hybrid (with HDA II) 2023|Highway Driving Assist II|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai Q connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Kia|Niro EV 2019|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai H connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Kia|Niro EV 2020|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai F connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| diff --git a/opendbc_repo b/opendbc_repo index a4182dab12..c5ad506330 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit a4182dab12d5e0971360973c8cceca5af2e3f79c +Subproject commit c5ad5063304987896fa0502553cb79f95acb1149 diff --git a/panda b/panda index 2b9ee96761..a0d3a4abe1 160000 --- a/panda +++ b/panda @@ -1 +1 @@ -Subproject commit 2b9ee96761ccfb2270ff8aeb9ac186a4e13cdb87 +Subproject commit a0d3a4abe132358a2889929469d794182a582fb1 diff --git a/uv.lock b/uv.lock index 9094417693..b6dc99e79a 100644 --- a/uv.lock +++ b/uv.lock @@ -116,7 +116,7 @@ wheels = [ [[package]] name = "capnproto" version = "1.0.1" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=capnproto&rev=releases#31af284805d0787a689e129311d992bec14a2400" } +source = { git = "https://github.com/commaai/dependencies.git?subdirectory=capnproto&rev=releases#11895d3f6bc62a229e84c87505948f15f9018ce0" } [[package]] name = "casadi" @@ -137,11 +137,11 @@ wheels = [ [[package]] name = "certifi" -version = "2026.1.4" +version = "2026.2.25" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, ] [[package]] @@ -376,7 +376,7 @@ wheels = [ [[package]] name = "eigen" version = "3.4.0" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=eigen&rev=releases#31af284805d0787a689e129311d992bec14a2400" } +source = { git = "https://github.com/commaai/dependencies.git?subdirectory=eigen&rev=releases#11895d3f6bc62a229e84c87505948f15f9018ce0" } [[package]] name = "execnet" @@ -390,7 +390,7 @@ wheels = [ [[package]] name = "ffmpeg" version = "7.1.0" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=ffmpeg&rev=releases#31af284805d0787a689e129311d992bec14a2400" } +source = { git = "https://github.com/commaai/dependencies.git?subdirectory=ffmpeg&rev=releases#11895d3f6bc62a229e84c87505948f15f9018ce0" } [[package]] name = "fonttools" @@ -437,7 +437,7 @@ wheels = [ [[package]] name = "gcc-arm-none-eabi" version = "13.2.1" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=gcc-arm-none-eabi&rev=releases#31af284805d0787a689e129311d992bec14a2400" } +source = { git = "https://github.com/commaai/dependencies.git?subdirectory=gcc-arm-none-eabi&rev=releases#11895d3f6bc62a229e84c87505948f15f9018ce0" } [[package]] name = "ghp-import" @@ -454,7 +454,7 @@ wheels = [ [[package]] name = "git-lfs" version = "3.6.1" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=git-lfs&rev=releases#31af284805d0787a689e129311d992bec14a2400" } +source = { git = "https://github.com/commaai/dependencies.git?subdirectory=git-lfs&rev=releases#11895d3f6bc62a229e84c87505948f15f9018ce0" } [[package]] name = "google-crc32c" @@ -572,7 +572,7 @@ wheels = [ [[package]] name = "libjpeg" version = "3.1.0" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=libjpeg&rev=releases#31af284805d0787a689e129311d992bec14a2400" } +source = { git = "https://github.com/commaai/dependencies.git?subdirectory=libjpeg&rev=releases#11895d3f6bc62a229e84c87505948f15f9018ce0" } [[package]] name = "libusb1" @@ -735,7 +735,7 @@ wheels = [ [[package]] name = "ncurses" version = "6.5" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=ncurses&rev=releases#31af284805d0787a689e129311d992bec14a2400" } +source = { git = "https://github.com/commaai/dependencies.git?subdirectory=ncurses&rev=releases#11895d3f6bc62a229e84c87505948f15f9018ce0" } [[package]] name = "numpy" @@ -921,7 +921,7 @@ provides-extras = ["docs", "testing", "dev", "tools"] [[package]] name = "openssl3" version = "3.4.1" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=openssl3&rev=releases#31af284805d0787a689e129311d992bec14a2400" } +source = { git = "https://github.com/commaai/dependencies.git?subdirectory=openssl3&rev=releases#11895d3f6bc62a229e84c87505948f15f9018ce0" } [[package]] name = "packaging" @@ -1292,7 +1292,7 @@ wheels = [ [[package]] name = "python3-dev" version = "3.12.8" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=python3-dev&rev=releases#31af284805d0787a689e129311d992bec14a2400" } +source = { git = "https://github.com/commaai/dependencies.git?subdirectory=python3-dev&rev=releases#11895d3f6bc62a229e84c87505948f15f9018ce0" } [[package]] name = "pyyaml" @@ -1401,27 +1401,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.15.2" +version = "0.15.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/06/04/eab13a954e763b0606f460443fcbf6bb5a0faf06890ea3754ff16523dce5/ruff-0.15.2.tar.gz", hash = "sha256:14b965afee0969e68bb871eba625343b8673375f457af4abe98553e8bbb98342", size = 4558148, upload-time = "2026-02-19T22:32:20.271Z" } +sdist = { url = "https://files.pythonhosted.org/packages/da/31/d6e536cdebb6568ae75a7f00e4b4819ae0ad2640c3604c305a0428680b0c/ruff-0.15.4.tar.gz", hash = "sha256:3412195319e42d634470cc97aa9803d07e9d5c9223b99bcb1518f0c725f26ae1", size = 4569550, upload-time = "2026-02-26T20:04:14.959Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2f/70/3a4dc6d09b13cb3e695f28307e5d889b2e1a66b7af9c5e257e796695b0e6/ruff-0.15.2-py3-none-linux_armv6l.whl", hash = "sha256:120691a6fdae2f16d65435648160f5b81a9625288f75544dc40637436b5d3c0d", size = 10430565, upload-time = "2026-02-19T22:32:41.824Z" }, - { url = "https://files.pythonhosted.org/packages/71/0b/bb8457b56185ece1305c666dc895832946d24055be90692381c31d57466d/ruff-0.15.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a89056d831256099658b6bba4037ac6dd06f49d194199215befe2bb10457ea5e", size = 10820354, upload-time = "2026-02-19T22:32:07.366Z" }, - { url = "https://files.pythonhosted.org/packages/2d/c1/e0532d7f9c9e0b14c46f61b14afd563298b8b83f337b6789ddd987e46121/ruff-0.15.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e36dee3a64be0ebd23c86ffa3aa3fd3ac9a712ff295e192243f814a830b6bd87", size = 10170767, upload-time = "2026-02-19T22:32:13.188Z" }, - { url = "https://files.pythonhosted.org/packages/47/e8/da1aa341d3af017a21c7a62fb5ec31d4e7ad0a93ab80e3a508316efbcb23/ruff-0.15.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9fb47b6d9764677f8c0a193c0943ce9a05d6763523f132325af8a858eadc2b9", size = 10529591, upload-time = "2026-02-19T22:32:02.547Z" }, - { url = "https://files.pythonhosted.org/packages/93/74/184fbf38e9f3510231fbc5e437e808f0b48c42d1df9434b208821efcd8d6/ruff-0.15.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f376990f9d0d6442ea9014b19621d8f2aaf2b8e39fdbfc79220b7f0c596c9b80", size = 10260771, upload-time = "2026-02-19T22:32:36.938Z" }, - { url = "https://files.pythonhosted.org/packages/05/ac/605c20b8e059a0bc4b42360414baa4892ff278cec1c91fff4be0dceedefd/ruff-0.15.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2dcc987551952d73cbf5c88d9fdee815618d497e4df86cd4c4824cc59d5dd75f", size = 11045791, upload-time = "2026-02-19T22:32:31.642Z" }, - { url = "https://files.pythonhosted.org/packages/fd/52/db6e419908f45a894924d410ac77d64bdd98ff86901d833364251bd08e22/ruff-0.15.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:42a47fd785cbe8c01b9ff45031af875d101b040ad8f4de7bbb716487c74c9a77", size = 11879271, upload-time = "2026-02-19T22:32:29.305Z" }, - { url = "https://files.pythonhosted.org/packages/3e/d8/7992b18f2008bdc9231d0f10b16df7dda964dbf639e2b8b4c1b4e91b83af/ruff-0.15.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cbe9f49354866e575b4c6943856989f966421870e85cd2ac94dccb0a9dcb2fea", size = 11303707, upload-time = "2026-02-19T22:32:22.492Z" }, - { url = "https://files.pythonhosted.org/packages/d7/02/849b46184bcfdd4b64cde61752cc9a146c54759ed036edd11857e9b8443b/ruff-0.15.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7a672c82b5f9887576087d97be5ce439f04bbaf548ee987b92d3a7dede41d3a", size = 11149151, upload-time = "2026-02-19T22:32:44.234Z" }, - { url = "https://files.pythonhosted.org/packages/70/04/f5284e388bab60d1d3b99614a5a9aeb03e0f333847e2429bebd2aaa1feec/ruff-0.15.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:72ecc64f46f7019e2bcc3cdc05d4a7da958b629a5ab7033195e11a438403d956", size = 11091132, upload-time = "2026-02-19T22:32:24.691Z" }, - { url = "https://files.pythonhosted.org/packages/fa/ae/88d844a21110e14d92cf73d57363fab59b727ebeabe78009b9ccb23500af/ruff-0.15.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:8dcf243b15b561c655c1ef2f2b0050e5d50db37fe90115507f6ff37d865dc8b4", size = 10504717, upload-time = "2026-02-19T22:32:26.75Z" }, - { url = "https://files.pythonhosted.org/packages/64/27/867076a6ada7f2b9c8292884ab44d08fd2ba71bd2b5364d4136f3cd537e1/ruff-0.15.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dab6941c862c05739774677c6273166d2510d254dac0695c0e3f5efa1b5585de", size = 10263122, upload-time = "2026-02-19T22:32:10.036Z" }, - { url = "https://files.pythonhosted.org/packages/e7/ef/faf9321d550f8ebf0c6373696e70d1758e20ccdc3951ad7af00c0956be7c/ruff-0.15.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1b9164f57fc36058e9a6806eb92af185b0697c9fe4c7c52caa431c6554521e5c", size = 10735295, upload-time = "2026-02-19T22:32:39.227Z" }, - { url = "https://files.pythonhosted.org/packages/2f/55/e8089fec62e050ba84d71b70e7834b97709ca9b7aba10c1a0b196e493f97/ruff-0.15.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:80d24fcae24d42659db7e335b9e1531697a7102c19185b8dc4a028b952865fd8", size = 11241641, upload-time = "2026-02-19T22:32:34.617Z" }, - { url = "https://files.pythonhosted.org/packages/23/01/1c30526460f4d23222d0fabd5888868262fd0e2b71a00570ca26483cd993/ruff-0.15.2-py3-none-win32.whl", hash = "sha256:fd5ff9e5f519a7e1bd99cbe8daa324010a74f5e2ebc97c6242c08f26f3714f6f", size = 10507885, upload-time = "2026-02-19T22:32:15.635Z" }, - { url = "https://files.pythonhosted.org/packages/5c/10/3d18e3bbdf8fc50bbb4ac3cc45970aa5a9753c5cb51bf9ed9a3cd8b79fa3/ruff-0.15.2-py3-none-win_amd64.whl", hash = "sha256:d20014e3dfa400f3ff84830dfb5755ece2de45ab62ecea4af6b7262d0fb4f7c5", size = 11623725, upload-time = "2026-02-19T22:32:04.947Z" }, - { url = "https://files.pythonhosted.org/packages/6d/78/097c0798b1dab9f8affe73da9642bb4500e098cb27fd8dc9724816ac747b/ruff-0.15.2-py3-none-win_arm64.whl", hash = "sha256:cabddc5822acdc8f7b5527b36ceac55cc51eec7b1946e60181de8fe83ca8876e", size = 10941649, upload-time = "2026-02-19T22:32:18.108Z" }, + { url = "https://files.pythonhosted.org/packages/f2/82/c11a03cfec3a4d26a0ea1e571f0f44be5993b923f905eeddfc397c13d360/ruff-0.15.4-py3-none-linux_armv6l.whl", hash = "sha256:a1810931c41606c686bae8b5b9a8072adac2f611bb433c0ba476acba17a332e0", size = 10453333, upload-time = "2026-02-26T20:04:20.093Z" }, + { url = "https://files.pythonhosted.org/packages/ce/5d/6a1f271f6e31dffb31855996493641edc3eef8077b883eaf007a2f1c2976/ruff-0.15.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5a1632c66672b8b4d3e1d1782859e98d6e0b4e70829530666644286600a33992", size = 10853356, upload-time = "2026-02-26T20:04:05.808Z" }, + { url = "https://files.pythonhosted.org/packages/b1/d8/0fab9f8842b83b1a9c2bf81b85063f65e93fb512e60effa95b0be49bfc54/ruff-0.15.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a4386ba2cd6c0f4ff75252845906acc7c7c8e1ac567b7bc3d373686ac8c222ba", size = 10187434, upload-time = "2026-02-26T20:03:54.656Z" }, + { url = "https://files.pythonhosted.org/packages/85/cc/cc220fd9394eff5db8d94dec199eec56dd6c9f3651d8869d024867a91030/ruff-0.15.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2496488bdfd3732747558b6f95ae427ff066d1fcd054daf75f5a50674411e75", size = 10535456, upload-time = "2026-02-26T20:03:52.738Z" }, + { url = "https://files.pythonhosted.org/packages/fa/0f/bced38fa5cf24373ec767713c8e4cadc90247f3863605fb030e597878661/ruff-0.15.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3f1c4893841ff2d54cbda1b2860fa3260173df5ddd7b95d370186f8a5e66a4ac", size = 10287772, upload-time = "2026-02-26T20:04:08.138Z" }, + { url = "https://files.pythonhosted.org/packages/2b/90/58a1802d84fed15f8f281925b21ab3cecd813bde52a8ca033a4de8ab0e7a/ruff-0.15.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:820b8766bd65503b6c30aaa6331e8ef3a6e564f7999c844e9a547c40179e440a", size = 11049051, upload-time = "2026-02-26T20:04:03.53Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ac/b7ad36703c35f3866584564dc15f12f91cb1a26a897dc2fd13d7cb3ae1af/ruff-0.15.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9fb74bab47139c1751f900f857fa503987253c3ef89129b24ed375e72873e85", size = 11890494, upload-time = "2026-02-26T20:04:10.497Z" }, + { url = "https://files.pythonhosted.org/packages/93/3d/3eb2f47a39a8b0da99faf9c54d3eb24720add1e886a5309d4d1be73a6380/ruff-0.15.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f80c98765949c518142b3a50a5db89343aa90f2c2bf7799de9986498ae6176db", size = 11326221, upload-time = "2026-02-26T20:04:12.84Z" }, + { url = "https://files.pythonhosted.org/packages/ff/90/bf134f4c1e5243e62690e09d63c55df948a74084c8ac3e48a88468314da6/ruff-0.15.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:451a2e224151729b3b6c9ffb36aed9091b2996fe4bdbd11f47e27d8f2e8888ec", size = 11168459, upload-time = "2026-02-26T20:04:00.969Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e5/a64d27688789b06b5d55162aafc32059bb8c989c61a5139a36e1368285eb/ruff-0.15.4-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a8f157f2e583c513c4f5f896163a93198297371f34c04220daf40d133fdd4f7f", size = 11104366, upload-time = "2026-02-26T20:03:48.099Z" }, + { url = "https://files.pythonhosted.org/packages/f1/f6/32d1dcb66a2559763fc3027bdd65836cad9eb09d90f2ed6a63d8e9252b02/ruff-0.15.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:917cc68503357021f541e69b35361c99387cdbbf99bd0ea4aa6f28ca99ff5338", size = 10510887, upload-time = "2026-02-26T20:03:45.771Z" }, + { url = "https://files.pythonhosted.org/packages/ff/92/22d1ced50971c5b6433aed166fcef8c9343f567a94cf2b9d9089f6aa80fe/ruff-0.15.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e9737c8161da79fd7cfec19f1e35620375bd8b2a50c3e77fa3d2c16f574105cc", size = 10285939, upload-time = "2026-02-26T20:04:22.42Z" }, + { url = "https://files.pythonhosted.org/packages/e6/f4/7c20aec3143837641a02509a4668fb146a642fd1211846634edc17eb5563/ruff-0.15.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:291258c917539e18f6ba40482fe31d6f5ac023994ee11d7bdafd716f2aab8a68", size = 10765471, upload-time = "2026-02-26T20:03:58.924Z" }, + { url = "https://files.pythonhosted.org/packages/d0/09/6d2f7586f09a16120aebdff8f64d962d7c4348313c77ebb29c566cefc357/ruff-0.15.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3f83c45911da6f2cd5936c436cf86b9f09f09165f033a99dcf7477e34041cbc3", size = 11263382, upload-time = "2026-02-26T20:04:24.424Z" }, + { url = "https://files.pythonhosted.org/packages/1b/fa/2ef715a1cd329ef47c1a050e10dee91a9054b7ce2fcfdd6a06d139afb7ec/ruff-0.15.4-py3-none-win32.whl", hash = "sha256:65594a2d557d4ee9f02834fcdf0a28daa8b3b9f6cb2cb93846025a36db47ef22", size = 10506664, upload-time = "2026-02-26T20:03:50.56Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a8/c688ef7e29983976820d18710f955751d9f4d4eb69df658af3d006e2ba3e/ruff-0.15.4-py3-none-win_amd64.whl", hash = "sha256:04196ad44f0df220c2ece5b0e959c2f37c777375ec744397d21d15b50a75264f", size = 11651048, upload-time = "2026-02-26T20:04:17.191Z" }, + { url = "https://files.pythonhosted.org/packages/3e/0a/9e1be9035b37448ce2e68c978f0591da94389ade5a5abafa4cf99985d1b2/ruff-0.15.4-py3-none-win_arm64.whl", hash = "sha256:60d5177e8cfc70e51b9c5fad936c634872a74209f934c1e79107d11787ad5453", size = 10966776, upload-time = "2026-02-26T20:03:56.908Z" }, ] [[package]] @@ -1539,26 +1539,26 @@ wheels = [ [[package]] name = "ty" -version = "0.0.18" +version = "0.0.19" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/74/15/9682700d8d60fdca7afa4febc83a2354b29cdcd56e66e19c92b521db3b39/ty-0.0.18.tar.gz", hash = "sha256:04ab7c3db5dcbcdac6ce62e48940d3a0124f377c05499d3f3e004e264ae94b83", size = 5214774, upload-time = "2026-02-20T21:51:31.173Z" } +sdist = { url = "https://files.pythonhosted.org/packages/84/5e/da108b9eeb392e02ff0478a34e9651490b36af295881cb56575b83f0cc3a/ty-0.0.19.tar.gz", hash = "sha256:ee3d9ed4cb586e77f6efe3d0fe5a855673ca438a3d533a27598e1d3502a2948a", size = 5220026, upload-time = "2026-02-26T12:13:15.215Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ae/d8/920460d4c22ea68fcdeb0b2fb53ea2aeb9c6d7875bde9278d84f2ac767b6/ty-0.0.18-py3-none-linux_armv6l.whl", hash = "sha256:4e5e91b0a79857316ef893c5068afc4b9872f9d257627d9bc8ac4d2715750d88", size = 10280825, upload-time = "2026-02-20T21:51:25.03Z" }, - { url = "https://files.pythonhosted.org/packages/83/56/62587de582d3d20d78fcdddd0594a73822ac5a399a12ef512085eb7a4de6/ty-0.0.18-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ee0e578b3f8416e2d5416da9553b78fd33857868aa1384cb7fefeceee5ff102d", size = 10118324, upload-time = "2026-02-20T21:51:22.27Z" }, - { url = "https://files.pythonhosted.org/packages/2f/2d/dbdace8d432a0755a7417f659bfd5b8a4261938ecbdfd7b42f4c454f5aa9/ty-0.0.18-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3f7a0487d36b939546a91d141f7fc3dbea32fab4982f618d5b04dc9d5b6da21e", size = 9605861, upload-time = "2026-02-20T21:51:16.066Z" }, - { url = "https://files.pythonhosted.org/packages/6b/d9/de11c0280f778d5fc571393aada7fe9b8bc1dd6a738f2e2c45702b8b3150/ty-0.0.18-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5e2fa8d45f57ca487a470e4bf66319c09b561150e98ae2a6b1a97ef04c1a4eb", size = 10092701, upload-time = "2026-02-20T21:51:26.862Z" }, - { url = "https://files.pythonhosted.org/packages/0f/94/068d4d591d791041732171e7b63c37a54494b2e7d28e88d2167eaa9ad875/ty-0.0.18-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d75652e9e937f7044b1aca16091193e7ef11dac1c7ec952b7fb8292b7ba1f5f2", size = 10109203, upload-time = "2026-02-20T21:51:11.59Z" }, - { url = "https://files.pythonhosted.org/packages/34/e4/526a4aa56dc0ca2569aaa16880a1ab105c3b416dd70e87e25a05688999f3/ty-0.0.18-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:563c868edceb8f6ddd5e91113c17d3676b028f0ed380bdb3829b06d9beb90e58", size = 10614200, upload-time = "2026-02-20T21:51:20.298Z" }, - { url = "https://files.pythonhosted.org/packages/fd/3d/b68ab20a34122a395880922587fbfc3adf090d22e0fb546d4d20fe8c2621/ty-0.0.18-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:502e2a1f948bec563a0454fc25b074bf5cf041744adba8794d024277e151d3b0", size = 11153232, upload-time = "2026-02-20T21:51:14.121Z" }, - { url = "https://files.pythonhosted.org/packages/68/ea/678243c042343fcda7e6af36036c18676c355878dcdcd517639586d2cf9e/ty-0.0.18-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc881dea97021a3aa29134a476937fd8054775c4177d01b94db27fcfb7aab65b", size = 10832934, upload-time = "2026-02-20T21:51:32.92Z" }, - { url = "https://files.pythonhosted.org/packages/d8/bd/7f8d647cef8b7b346c0163230a37e903c7461c7248574840b977045c77df/ty-0.0.18-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:421fcc3bc64cab56f48edb863c7c1c43649ec4d78ff71a1acb5366ad723b6021", size = 10700888, upload-time = "2026-02-20T21:51:09.673Z" }, - { url = "https://files.pythonhosted.org/packages/6e/06/cb3620dc48c5d335ba7876edfef636b2f4498eff4a262ff90033b9e88408/ty-0.0.18-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0fe5038a7136a0e638a2fb1ad06e3d3c4045314c6ba165c9c303b9aeb4623d6c", size = 10078965, upload-time = "2026-02-20T21:51:07.678Z" }, - { url = "https://files.pythonhosted.org/packages/60/27/c77a5a84533fa3b685d592de7b4b108eb1f38851c40fac4e79cc56ec7350/ty-0.0.18-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d123600a52372677613a719bbb780adeb9b68f47fb5f25acb09171de390e0035", size = 10134659, upload-time = "2026-02-20T21:51:18.311Z" }, - { url = "https://files.pythonhosted.org/packages/43/6e/60af6b88c73469e628ba5253a296da6984e0aa746206f3034c31f1a04ed1/ty-0.0.18-py3-none-musllinux_1_2_i686.whl", hash = "sha256:bb4bc11d32a1bf96a829bf6b9696545a30a196ac77bbc07cc8d3dfee35e03723", size = 10297494, upload-time = "2026-02-20T21:51:39.631Z" }, - { url = "https://files.pythonhosted.org/packages/33/90/612dc0b68224c723faed6adac2bd3f930a750685db76dfe17e6b9e534a83/ty-0.0.18-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:dda2efbf374ba4cd704053d04e32f2f784e85c2ddc2400006b0f96f5f7e4b667", size = 10791944, upload-time = "2026-02-20T21:51:37.13Z" }, - { url = "https://files.pythonhosted.org/packages/0d/da/f4ada0fd08a9e4138fe3fd2bcd3797753593f423f19b1634a814b9b2a401/ty-0.0.18-py3-none-win32.whl", hash = "sha256:c5768607c94977dacddc2f459ace6a11a408a0f57888dd59abb62d28d4fee4f7", size = 9677964, upload-time = "2026-02-20T21:51:42.039Z" }, - { url = "https://files.pythonhosted.org/packages/5e/fa/090ed9746e5c59fc26d8f5f96dc8441825171f1f47752f1778dad690b08b/ty-0.0.18-py3-none-win_amd64.whl", hash = "sha256:b78d0fa1103d36fc2fce92f2092adace52a74654ab7884d54cdaec8eb5016a4d", size = 10636576, upload-time = "2026-02-20T21:51:29.159Z" }, - { url = "https://files.pythonhosted.org/packages/92/4f/5dd60904c8105cda4d0be34d3a446c180933c76b84ae0742e58f02133713/ty-0.0.18-py3-none-win_arm64.whl", hash = "sha256:01770c3c82137c6b216aa3251478f0b197e181054ee92243772de553d3586398", size = 10095449, upload-time = "2026-02-20T21:51:34.914Z" }, + { url = "https://files.pythonhosted.org/packages/5a/31/fd8c6067abb275bea11523d21ecf64e1d870b1ce80cac529cf6636df1471/ty-0.0.19-py3-none-linux_armv6l.whl", hash = "sha256:29bed05d34c8a7597567b8e327c53c1aed4a07dcfbe6c81e6d60c7444936ad77", size = 10268470, upload-time = "2026-02-26T12:13:42.881Z" }, + { url = "https://files.pythonhosted.org/packages/15/de/16a11bbf7d98c75849fc41f5d008b89bb5d080a4b10dc8ea851ee2bd371b/ty-0.0.19-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:79140870c688c97ec68e723c28935ddef9d91a76d48c68e665fe7c851e628b8a", size = 10098562, upload-time = "2026-02-26T12:13:31.618Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4f/086d6ff6686eadf903913c45b53ab96694b62bbfee1d8cf3e55a9b5aa4b2/ty-0.0.19-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6e9c1f9cfa6a26f7881d14d75cf963af743f6c4189e6aa3e3b4056a65f22e730", size = 9604073, upload-time = "2026-02-26T12:13:24.645Z" }, + { url = "https://files.pythonhosted.org/packages/95/13/888a6b6c7ed4a880fee91bec997f775153ce86215ee4c56b868516314734/ty-0.0.19-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbca43b050edf1db2e64ae7b79add233c2aea2855b8a876081bbd032edcd0610", size = 10106295, upload-time = "2026-02-26T12:13:40.584Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e8/05a372cae8da482de73b8246fb43236bf11e24ac28c879804568108759db/ty-0.0.19-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8acaa88ab1955ca6b15a0ccc274011c4961377fe65c3948e5d2b212f2517b87c", size = 10098234, upload-time = "2026-02-26T12:13:33.725Z" }, + { url = "https://files.pythonhosted.org/packages/c5/f1/5b0958e9e9576e7662192fe689bbb3dc88e631a4e073db3047793a547d58/ty-0.0.19-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a901b6a6dd9d17d5b3b2e7bafc3057294e88da3f5de507347316687d7f191a1", size = 10607218, upload-time = "2026-02-26T12:13:17.576Z" }, + { url = "https://files.pythonhosted.org/packages/fb/ab/358c78b77844f58ff5aca368550ab16c719f1ab0ec892ceb1114d7500f4e/ty-0.0.19-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8deafdaaaee65fd121c66064da74a922d8501be4a2d50049c71eab521a23eff7", size = 11160593, upload-time = "2026-02-26T12:13:36.008Z" }, + { url = "https://files.pythonhosted.org/packages/95/59/827fc346d66a59fe48e9689a5ceb67dbbd5b4de2e8d4625371af39a2e8b7/ty-0.0.19-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:035e56071af280897441018f74f921b97d53aec0856f8af85f4f949df8eda07d", size = 10822392, upload-time = "2026-02-26T12:13:29.415Z" }, + { url = "https://files.pythonhosted.org/packages/81/f9/3bbfbbe35478de9bcd63848f4bc9bffda72278dd9732dbad3efc3978432e/ty-0.0.19-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abdf5885130393ce74501dba792f48ce0a515756ec81c33a4b324bdf3509df6e", size = 10707139, upload-time = "2026-02-26T12:13:20.148Z" }, + { url = "https://files.pythonhosted.org/packages/12/9e/597023b183ec4ade83a36a0cea5c103f3bffa34f70813d46386c61447fb8/ty-0.0.19-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:877e89005c8f9d1dbff5ad14cbac9f35c528406fde38926f9b44f24830de8d6a", size = 10096933, upload-time = "2026-02-26T12:13:45.266Z" }, + { url = "https://files.pythonhosted.org/packages/1e/76/d0d2f6e674db2a17c8efa5e26682b9dfa8d34774705f35902a7b45ebd3bd/ty-0.0.19-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:39bd1da051c1e4d316efaf79dbed313255633f7c6ad6e24d29f4d9c6ffaf4de6", size = 10109547, upload-time = "2026-02-26T12:13:22.17Z" }, + { url = "https://files.pythonhosted.org/packages/a4/b0/76026c06b852a3aa4fdb5bd329fdc2175aaf3c64a3fafece9cc4df167cee/ty-0.0.19-py3-none-musllinux_1_2_i686.whl", hash = "sha256:87df8415a6c9cb27b8f1382fcdc6052e59f5b9f50f78bc14663197eb5c8d3699", size = 10289110, upload-time = "2026-02-26T12:13:38.29Z" }, + { url = "https://files.pythonhosted.org/packages/14/6c/f3b3a189816b4f079b20fe5d0d7ee38e38a472f53cc6770bb6571147e3de/ty-0.0.19-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:89b6bb23c332ed5c38dd859eb5793f887abcc936f681a40d4ea68e35eac1af33", size = 10796479, upload-time = "2026-02-26T12:13:10.992Z" }, + { url = "https://files.pythonhosted.org/packages/3d/18/caee33d1ce9dd50bd94c26cde7cda4f6971e22e474e7d72a5c86d745ad58/ty-0.0.19-py3-none-win32.whl", hash = "sha256:19b33df3aa7af7b1a9eaa4e1175c3b4dec0f5f2e140243e3492c8355c37418f3", size = 9677215, upload-time = "2026-02-26T12:13:08.519Z" }, + { url = "https://files.pythonhosted.org/packages/81/41/18fc0771d0b1da7d7cc2fc9af278d3122b754fe8b521a748734f4e16ecfd/ty-0.0.19-py3-none-win_amd64.whl", hash = "sha256:b9052c61464cdd76bc8e6796f2588c08700f25d0dcbc225bb165e390ea9d96a4", size = 10651252, upload-time = "2026-02-26T12:13:13.035Z" }, + { url = "https://files.pythonhosted.org/packages/8b/8c/26f7ce8863eb54510082747b3dfb1046ba24f16fc11de18c0e5feb36ff18/ty-0.0.19-py3-none-win_arm64.whl", hash = "sha256:9329804b66dcbae8e7af916ef4963221ed53b8ec7d09b0793591c5ae8a0f3270", size = 10093195, upload-time = "2026-02-26T12:13:26.816Z" }, ] [[package]] @@ -1660,7 +1660,7 @@ wheels = [ [[package]] name = "zeromq" version = "4.3.5" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=zeromq&rev=releases#31af284805d0787a689e129311d992bec14a2400" } +source = { git = "https://github.com/commaai/dependencies.git?subdirectory=zeromq&rev=releases#11895d3f6bc62a229e84c87505948f15f9018ce0" } [[package]] name = "zstandard" @@ -1690,4 +1690,4 @@ wheels = [ [[package]] name = "zstd" version = "1.5.6" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=zstd&rev=releases#31af284805d0787a689e129311d992bec14a2400" } +source = { git = "https://github.com/commaai/dependencies.git?subdirectory=zstd&rev=releases#11895d3f6bc62a229e84c87505948f15f9018ce0" } From 6f6dfa6bba41b447c32d37eb19236bda0171b2b0 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Fri, 27 Feb 2026 17:53:42 -0500 Subject: [PATCH 254/311] tools: block `manage_sunnylinkd` in sim startup script (#1725) * ui sync conflicts with upstream * md * ref * ci * lint * more * more ci * new new * tools: block `manage_sunnylinkd` in sim startup script * try this out * unbump --- tools/sim/launch_openpilot.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/sim/launch_openpilot.sh b/tools/sim/launch_openpilot.sh index 392f365d03..ea3c4cb8f1 100755 --- a/tools/sim/launch_openpilot.sh +++ b/tools/sim/launch_openpilot.sh @@ -6,7 +6,7 @@ export SIMULATION="1" export SKIP_FW_QUERY="1" export FINGERPRINT="HONDA_CIVIC_2022" -export BLOCK="${BLOCK},camerad,loggerd,encoderd,micd,logmessaged,manage_athenad" +export BLOCK="${BLOCK},camerad,loggerd,encoderd,micd,logmessaged,manage_athenad,manage_sunnylinkd" if [[ "$CI" ]]; then # TODO: offscreen UI should work export BLOCK="${BLOCK},ui" From 010a32bb9bbf0889cdda27e157c4d17ce76b2827 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 27 Feb 2026 14:56:01 -0800 Subject: [PATCH 255/311] WifiUi: single source for forget btn visible (#37450) single --- selfdrive/ui/mici/layouts/settings/network/wifi_ui.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py index 2fbe23c191..a73a397d1a 100644 --- a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py +++ b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py @@ -129,12 +129,10 @@ class WifiButton(BigButton): return self._network_forgetting = True - self._forget_btn.set_visible(False) self._wifi_manager.forget_connection(self._network.ssid) def on_forgotten(self): self._network_forgetting = False - self._forget_btn.set_visible(True) def set_network_missing(self, missing: bool): self._network_missing = missing @@ -150,7 +148,7 @@ class WifiButton(BigButton): @property def _show_forget_btn(self): - if self._network.is_tethering: + if self._network.is_tethering or self._network_forgetting: return False return (self._is_saved and not self._wrong_password) or self._is_connecting From cc21fd3ac347e9e0e8c04b3e5b685adad26447a6 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Fri, 27 Feb 2026 15:04:37 -0800 Subject: [PATCH 256/311] ci: remove weekly eval jobs --- .github/workflows/ci_weekly_report.yaml | 101 ------------------------ .github/workflows/ci_weekly_run.yaml | 17 ---- 2 files changed, 118 deletions(-) delete mode 100644 .github/workflows/ci_weekly_report.yaml delete mode 100644 .github/workflows/ci_weekly_run.yaml diff --git a/.github/workflows/ci_weekly_report.yaml b/.github/workflows/ci_weekly_report.yaml deleted file mode 100644 index c7f5ec34f0..0000000000 --- a/.github/workflows/ci_weekly_report.yaml +++ /dev/null @@ -1,101 +0,0 @@ -name: weekly CI test report -on: - schedule: - - cron: '37 9 * * 1' # 9:37AM UTC -> 2:37AM PST every monday - workflow_dispatch: - inputs: - ci_runs: - description: 'The amount of runs to trigger in CI test report' -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -env: - CI_RUNS: ${{ github.event.inputs.ci_runs || '50' }} - -jobs: - setup: - if: github.repository == 'commaai/openpilot' - runs-on: ubuntu-latest - outputs: - ci_runs: ${{ steps.ci_runs_setup.outputs.matrix }} - steps: - - id: ci_runs_setup - name: CI_RUNS=${{ env.CI_RUNS }} - run: | - matrix=$(python3 -c "import json; print(json.dumps({ 'run_number' : list(range(${{ env.CI_RUNS }})) }))") - echo "matrix=$matrix" >> $GITHUB_OUTPUT - - ci_matrix_run: - needs: [ setup ] - strategy: - fail-fast: false - matrix: ${{fromJSON(needs.setup.outputs.ci_runs)}} - uses: commaai/openpilot/.github/workflows/ci_weekly_run.yaml@master - with: - run_number: ${{ matrix.run_number }} - - report: - needs: [ci_matrix_run] - runs-on: ubuntu-latest - if: always() && github.repository == 'commaai/openpilot' - steps: - - name: Get job results - uses: actions/github-script@v8 - id: get-job-results - with: - script: | - const jobs = await github - .paginate("GET /repos/{owner}/{repo}/actions/runs/{run_id}/attempts/{attempt}/jobs", { - owner: "commaai", - repo: "${{ github.event.repository.name }}", - run_id: "${{ github.run_id }}", - attempt: "${{ github.run_attempt }}", - }) - var report = {} - jobs.slice(1, jobs.length-1).forEach(job => { - if (job.conclusion === "skipped") return; - const jobName = job.name.split(" / ")[2]; - const runRegex = /\((.*?)\)/; - const run = job.name.match(runRegex)[1]; - report[jobName] = report[jobName] || { successes: [], failures: [], canceled: [] }; - switch (job.conclusion) { - case "success": - report[jobName].successes.push({ "run_number": run, "link": job.html_url}); break; - case "failure": - report[jobName].failures.push({ "run_number": run, "link": job.html_url }); break; - case "canceled": - report[jobName].canceled.push({ "run_number": run, "link": job.html_url }); break; - } - }); - return JSON.stringify({"jobs": report}); - - - name: Add job results to summary - env: - JOB_RESULTS: ${{ fromJSON(steps.get-job-results.outputs.result) }} - run: | - cat <> template.html - - - - - - - - - - - {% for key in jobs.keys() %} - - - - - - {% endfor %} -
Job✅ Passing❌ Failure Details
{% for i in range(5) %}{% if i+1 <= (5 * jobs[key]["successes"]|length // ${{ env.CI_RUNS }}) %}🟩{% else %}🟥{% endif %}{% endfor%}{{ key }}{{ 100 * jobs[key]["successes"]|length // ${{ env.CI_RUNS }} }}%{% if jobs[key]["failures"]|length > 0 %}
{% for failure in jobs[key]["failures"] %}Log for run #{{ failure['run_number'] }}
{% endfor %}
{% else %}{% endif %}
- EOF - - pip install jinja2-cli - echo $JOB_RESULTS | jinja2 template.html > report.html - echo "# CI Test Report - ${{ env.CI_RUNS }} Runs" >> $GITHUB_STEP_SUMMARY - cat report.html >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/ci_weekly_run.yaml b/.github/workflows/ci_weekly_run.yaml deleted file mode 100644 index acd24de163..0000000000 --- a/.github/workflows/ci_weekly_run.yaml +++ /dev/null @@ -1,17 +0,0 @@ -name: weekly CI test run -on: - workflow_call: - inputs: - run_number: - required: true - type: string - -concurrency: - group: ci-run-${{ inputs.run_number }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - tests: - uses: commaai/openpilot/.github/workflows/tests.yaml@master - with: - run_number: ${{ inputs.run_number }} From 6e8f32502412245b8e8963c6bec1b04cea43c512 Mon Sep 17 00:00:00 2001 From: Robbe Derks Date: Fri, 27 Feb 2026 15:05:01 -0800 Subject: [PATCH 257/311] Fix mic clipping on comma four (#37461) * 6dB reduction on four * wrong submodule --- panda | 2 +- selfdrive/ui/soundd.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/panda b/panda index 3ffe9591a7..d1410f7f7b 160000 --- a/panda +++ b/panda @@ -1 +1 @@ -Subproject commit 3ffe9591a7305c71f67a70355f8098c9b5d2a611 +Subproject commit d1410f7f7b061171c3702d84d975a3da3afce109 diff --git a/selfdrive/ui/soundd.py b/selfdrive/ui/soundd.py index d88410ada3..6a203d3afc 100644 --- a/selfdrive/ui/soundd.py +++ b/selfdrive/ui/soundd.py @@ -21,11 +21,12 @@ MIN_VOLUME = 0.1 SELFDRIVE_STATE_TIMEOUT = 5 # 5 seconds FILTER_DT = 1. / (micd.SAMPLE_RATE / micd.FFT_SAMPLES) -AMBIENT_DB = 30 # DB where MIN_VOLUME is applied +AMBIENT_DB = 24 # DB where MIN_VOLUME is applied DB_SCALE = 30 # AMBIENT_DB + DB_SCALE is where MAX_VOLUME is applied VOLUME_BASE = 20 if HARDWARE.get_device_type() == "tizi": + AMBIENT_DB = 30 VOLUME_BASE = 10 AudibleAlert = car.CarControl.HUDControl.AudibleAlert From 1b17bf40cd9a2af58415b03913b999080fd19c83 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 27 Feb 2026 15:47:54 -0800 Subject: [PATCH 258/311] Revert "UI: only show `onroad_fade.png` when engaged" (#37466) Revert "UI: only show `onroad_fade.png` when engaged (#37051)" This reverts commit 39dcc883301f47c4061e3206cba6d5d50ab72778. --- selfdrive/ui/mici/onroad/augmented_road_view.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/selfdrive/ui/mici/onroad/augmented_road_view.py b/selfdrive/ui/mici/onroad/augmented_road_view.py index 99e33e8644..dbd2bae886 100644 --- a/selfdrive/ui/mici/onroad/augmented_road_view.py +++ b/selfdrive/ui/mici/onroad/augmented_road_view.py @@ -14,7 +14,7 @@ from openpilot.selfdrive.ui.mici.onroad.cameraview import CameraView from openpilot.system.ui.lib.application import FontWeight, gui_app, MousePos, MouseEvent from openpilot.system.ui.widgets.label import UnifiedLabel from openpilot.system.ui.widgets import Widget -from openpilot.common.filter_simple import BounceFilter, FirstOrderFilter +from openpilot.common.filter_simple import BounceFilter from openpilot.common.transformations.camera import DEVICE_CAMERAS, DeviceCameraConfig, view_frame_from_device_frame from openpilot.common.transformations.orientation import rot_from_euler from enum import IntEnum @@ -165,7 +165,6 @@ class AugmentedRoadView(CameraView): alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE) self._fade_texture = gui_app.texture("icons_mici/onroad/onroad_fade.png") - self._fade_alpha_filter = FirstOrderFilter(0, 0.1, 1 / gui_app.target_fps) # debug self._pm = messaging.PubMaster(['uiDebug']) @@ -218,11 +217,8 @@ class AugmentedRoadView(CameraView): # Draw all UI overlays self._model_renderer.render(self._content_rect) - # Fade out bottom of overlays for looks (only when engaged) - fade_alpha = self._fade_alpha_filter.update(ui_state.status != UIStatus.DISENGAGED) - if fade_alpha > 1e-2: - rl.draw_texture_ex(self._fade_texture, rl.Vector2(self._content_rect.x, self._content_rect.y), 0.0, 1.0, - rl.Color(255, 255, 255, int(255 * fade_alpha))) + # Fade out bottom of overlays for looks + rl.draw_texture_ex(self._fade_texture, rl.Vector2(self._content_rect.x, self._content_rect.y), 0.0, 1.0, rl.WHITE) alert_to_render, not_animating_out = self._alert_renderer.will_render() From 3a958b882ab6e13948e0375406f9d65282a81627 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 27 Feb 2026 15:47:56 -0800 Subject: [PATCH 259/311] Revert "onroad: fill bookmark icon when activated" (#37465) Revert "onroad: fill bookmark icon when activated (#37034)" This reverts commit 0b958f7c9ae682e0ab95d0dc9f45f605be0dfce0. --- selfdrive/ui/mici/onroad/augmented_road_view.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/selfdrive/ui/mici/onroad/augmented_road_view.py b/selfdrive/ui/mici/onroad/augmented_road_view.py index dbd2bae886..9287406821 100644 --- a/selfdrive/ui/mici/onroad/augmented_road_view.py +++ b/selfdrive/ui/mici/onroad/augmented_road_view.py @@ -46,8 +46,6 @@ class BookmarkIcon(Widget): super().__init__() self._bookmark_callback = bookmark_callback self._icon = gui_app.texture("icons_mici/onroad/bookmark.png", 180, 180) - self._icon_fill = gui_app.texture("icons_mici/onroad/bookmark_fill.png", 180, 180) - self._active_icon = self._icon self._offset_filter = BounceFilter(0.0, 0.1, 1 / gui_app.target_fps) # State @@ -86,7 +84,6 @@ class BookmarkIcon(Widget): if self._offset_filter.x < 1e-3: self._interacting = False - self._active_icon = self._icon def _handle_mouse_event(self, mouse_event: MouseEvent): if not ui_state.started: @@ -99,7 +96,6 @@ class BookmarkIcon(Widget): self._is_swiping = True self._is_swiping_left = False self._state = BookmarkState.DRAGGING - self._active_icon = self._icon elif mouse_event.left_down and self._is_swiping: self._swipe_current_x = mouse_event.pos.x @@ -116,7 +112,6 @@ class BookmarkIcon(Widget): if swipe_distance > self.PEEK_THRESHOLD: self._state = BookmarkState.TRIGGERED self._triggered_time = rl.get_time() - self._active_icon = self._icon_fill self._bookmark_callback() else: # Otherwise, transition back to hidden @@ -130,8 +125,8 @@ class BookmarkIcon(Widget): """Render the bookmark icon.""" if self._offset_filter.x > 0: icon_x = self.rect.x + self.rect.width - round(self._offset_filter.x) - icon_y = self.rect.y + (self.rect.height - self._active_icon.height) / 2 # Vertically centered - rl.draw_texture(self._active_icon, int(icon_x), int(icon_y), rl.WHITE) + icon_y = self.rect.y + (self.rect.height - self._icon.height) / 2 # Vertically centered + rl.draw_texture(self._icon, int(icon_x), int(icon_y), rl.WHITE) class AugmentedRoadView(CameraView): From 3beee1b80e94180f8bed9c317713eb0ecb6ac745 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Fri, 27 Feb 2026 20:01:06 -0500 Subject: [PATCH 260/311] [MICI] ui: need superclass `_render` in `HudRendererSP` (#1728) --- selfdrive/ui/sunnypilot/mici/onroad/hud_renderer.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/selfdrive/ui/sunnypilot/mici/onroad/hud_renderer.py b/selfdrive/ui/sunnypilot/mici/onroad/hud_renderer.py index c079c5a0ef..9d39d01727 100644 --- a/selfdrive/ui/sunnypilot/mici/onroad/hud_renderer.py +++ b/selfdrive/ui/sunnypilot/mici/onroad/hud_renderer.py @@ -20,7 +20,9 @@ class HudRendererSP(HudRenderer): self.blind_spot_indicators.update() def _render(self, rect: rl.Rectangle) -> None: + super()._render(rect) self.blind_spot_indicators.render(rect) def _has_blind_spot_detected(self) -> bool: + return self.blind_spot_indicators.detected From 2e42bf9fa4ac298ce84de7bac3b5449933892d5f Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 27 Feb 2026 18:32:28 -0800 Subject: [PATCH 261/311] mici ui: fix onroad transitions if in settings (#37467) * fix * type --- selfdrive/ui/mici/layouts/main.py | 3 +-- .../ui/mici/layouts/settings/network/__init__.py | 4 ++-- system/ui/lib/application.py | 15 ++++++++++----- system/ui/mici_setup.py | 2 +- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/selfdrive/ui/mici/layouts/main.py b/selfdrive/ui/mici/layouts/main.py index e39a228daf..1a0c6bc094 100644 --- a/selfdrive/ui/mici/layouts/main.py +++ b/selfdrive/ui/mici/layouts/main.py @@ -49,6 +49,7 @@ class MiciMainLayout(Scroller): # Set callbacks self._setup_callbacks() + gui_app.add_nav_stack_tick(self._handle_transitions) gui_app.push_widget(self) # Start onboarding if terms or training not completed, make sure to push after self @@ -76,8 +77,6 @@ class MiciMainLayout(Scroller): # Render super()._render(self._rect) - self._handle_transitions() - def _handle_transitions(self): # Don't pop if onboarding if gui_app.get_active_widget() == self._onboarding_window: diff --git a/selfdrive/ui/mici/layouts/settings/network/__init__.py b/selfdrive/ui/mici/layouts/settings/network/__init__.py index ce3f1a817e..ae7d085e01 100644 --- a/selfdrive/ui/mici/layouts/settings/network/__init__.py +++ b/selfdrive/ui/mici/layouts/settings/network/__init__.py @@ -166,13 +166,13 @@ class NetworkLayoutMici(NavScroller): self._wifi_manager.set_active(True) # Process wifi callbacks while at any point in the nav stack - gui_app.set_nav_stack_tick(self._wifi_manager.process_callbacks) + gui_app.add_nav_stack_tick(self._wifi_manager.process_callbacks) def hide_event(self): super().hide_event() self._wifi_manager.set_active(False) - gui_app.set_nav_stack_tick(None) + gui_app.remove_nav_stack_tick(self._wifi_manager.process_callbacks) def _toggle_roaming(self, checked: bool): self._wifi_manager.update_gsm_settings(checked, ui_state.params.get("GsmApn") or "", ui_state.params.get_bool("GsmMetered")) diff --git a/system/ui/lib/application.py b/system/ui/lib/application.py index 1640b0d077..34aed4f6a6 100644 --- a/system/ui/lib/application.py +++ b/system/ui/lib/application.py @@ -220,7 +220,7 @@ class GuiApplication: self._frame = 0 self._window_close_requested = False self._nav_stack: list[object] = [] - self._nav_stack_tick: Callable[[], None] | None = None + self._nav_stack_ticks: list[Callable[[], None]] = [] self._nav_stack_widgets_to_render = 1 if self.big_ui() else 2 self._mouse = MouseState(self._scale) @@ -411,8 +411,13 @@ class GuiApplication: return self._nav_stack[-1] return None - def set_nav_stack_tick(self, tick_function: Callable | None): - self._nav_stack_tick = tick_function + def add_nav_stack_tick(self, tick_function: Callable[[], None]): + if tick_function not in self._nav_stack_ticks: + self._nav_stack_ticks.append(tick_function) + + def remove_nav_stack_tick(self, tick_function: Callable[[], None]): + if tick_function in self._nav_stack_ticks: + self._nav_stack_ticks.remove(tick_function) def set_should_render(self, should_render: bool): self._should_render = should_render @@ -561,8 +566,8 @@ class GuiApplication: rl.clear_background(rl.BLACK) # Allow a Widget to still run a function regardless of the stack depth - if self._nav_stack_tick is not None: - self._nav_stack_tick() + for tick in self._nav_stack_ticks: + tick() # Only render top widgets for widget in self._nav_stack[-self._nav_stack_widgets_to_render:]: diff --git a/system/ui/mici_setup.py b/system/ui/mici_setup.py index 6c73f14e3a..ca63544439 100755 --- a/system/ui/mici_setup.py +++ b/system/ui/mici_setup.py @@ -508,7 +508,7 @@ class Setup(Widget): self._network_monitor = NetworkConnectivityMonitor() self._network_monitor.start() self._prev_has_internet = False - gui_app.set_nav_stack_tick(self._nav_stack_tick) + gui_app.add_nav_stack_tick(self._nav_stack_tick) self._start_page = StartPage() self._start_page.set_click_callback(self._getting_started_button_callback) From 7eb65e878be32bed443fb3ba4f801cee8a875fd3 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Fri, 27 Feb 2026 22:56:26 -0500 Subject: [PATCH 262/311] [TIZI/TICI] ui: Speed Limit Assist active status (#1729) [TIZI/TICI] ui: display Speed Limit Assist active status --- .../ui/sunnypilot/onroad/hud_renderer.py | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/selfdrive/ui/sunnypilot/onroad/hud_renderer.py b/selfdrive/ui/sunnypilot/onroad/hud_renderer.py index 86fcf3c693..b96a921a8b 100644 --- a/selfdrive/ui/sunnypilot/onroad/hud_renderer.py +++ b/selfdrive/ui/sunnypilot/onroad/hud_renderer.py @@ -22,6 +22,8 @@ from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.lib.text_measure import measure_text_cached +SLA_ACTIVE_COLOR = rl.Color(0x91, 0x9b, 0x95, 0xff) + class HudRendererSP(HudRenderer): def __init__(self): @@ -71,6 +73,8 @@ class HudRendererSP(HudRenderer): self.show_icbm_status = self.icbm_active_counter > 0 def _draw_set_speed(self, rect: rl.Rectangle) -> None: + long_plan_sp = ui_state.sm['longitudinalPlanSP'] + long_override = ui_state.sm['carControl'].cruiseControl.override self._get_icbm_status() set_speed_width = UI_CONFIG.set_speed_width_metric if ui_state.is_metric else UI_CONFIG.set_speed_width_imperial @@ -85,12 +89,16 @@ class HudRendererSP(HudRenderer): set_speed_color = COLORS.DARK_GREY if self.is_cruise_set: set_speed_color = COLORS.WHITE - if ui_state.status == UIStatus.ENGAGED: - max_color = COLORS.ENGAGED - elif ui_state.status == UIStatus.DISENGAGED: - max_color = COLORS.DISENGAGED - elif ui_state.status == UIStatus.OVERRIDE: - max_color = COLORS.OVERRIDE + if long_plan_sp.speedLimit.assist.active: + set_speed_color = SLA_ACTIVE_COLOR if long_override else rl.Color(0, 0xff, 0, 0xff) + max_color = SLA_ACTIVE_COLOR if long_override else rl.Color(0x80, 0xd8, 0xa6, 0xff) + else: + if ui_state.status == UIStatus.ENGAGED: + max_color = COLORS.ENGAGED + elif ui_state.status == UIStatus.DISENGAGED: + max_color = COLORS.DISENGAGED + elif ui_state.status == UIStatus.OVERRIDE: + max_color = COLORS.OVERRIDE max_str_size = 60 if self.show_icbm_status else 40 max_str_y = 15 if self.show_icbm_status else 27 From 10f3f5680102a5a1310666db6e0aa411a2fd59cb Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 27 Feb 2026 20:20:50 -0800 Subject: [PATCH 263/311] mici ui: get version from build metadata (#37470) * get version from build * fix test --- selfdrive/ui/mici/layouts/home.py | 24 +++++++++++++++--------- selfdrive/ui/tests/diff/replay.py | 7 +++++++ 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/selfdrive/ui/mici/layouts/home.py b/selfdrive/ui/mici/layouts/home.py index 31884e5f18..730a7ca7b7 100644 --- a/selfdrive/ui/mici/layouts/home.py +++ b/selfdrive/ui/mici/layouts/home.py @@ -1,3 +1,4 @@ +import datetime import time from cereal import log @@ -149,17 +150,22 @@ class MiciHomeLayout(Widget): self._did_long_press = False def _get_version_text(self) -> tuple[str, str, str, str] | None: - description = ui_state.params.get("UpdaterCurrentDescription") + version = ui_state.params.get("Version") + branch = ui_state.params.get("GitBranch") + commit = ui_state.params.get("GitCommit") - if description is not None and len(description) > 0: - # Expect "version / branch / commit / date"; be tolerant of other formats - try: - version, branch, commit, date = description.split(" / ") - return version, branch, commit, date - except Exception: - return None + if not all((version, branch, commit)): + return None - return None + commit_date_raw = ui_state.params.get("GitCommitDate") + try: + # GitCommitDate format from get_commit_date(): '%ct %ci' e.g. "'1708012345 2024-02-15 ...'" + unix_ts = int(commit_date_raw.strip("'").split()[0]) + date_str = datetime.datetime.fromtimestamp(unix_ts).strftime("%b %d") + except (ValueError, IndexError, TypeError, AttributeError): + date_str = "" + + return version, branch, commit[:7], date_str def _render(self, _): # TODO: why is there extra space here to get it to be flush? diff --git a/selfdrive/ui/tests/diff/replay.py b/selfdrive/ui/tests/diff/replay.py index 7ed7ce9364..9860969efb 100755 --- a/selfdrive/ui/tests/diff/replay.py +++ b/selfdrive/ui/tests/diff/replay.py @@ -23,8 +23,15 @@ def setup_state(): params.put("HasAcceptedTerms", terms_version) params.put("CompletedTrainingVersion", training_version) params.put("DongleId", "test123456789") + # Combined description for layouts that still use it (BIG home, settings/software) params.put("UpdaterCurrentDescription", "0.10.1 / test-branch / abc1234 / Nov 30") + # Params for mici home + params.put("Version", "0.10.1") + params.put("GitBranch", "test-branch") + params.put("GitCommit", "abc12340ff9131237ba23a1d0fbd8edf9c80e87") + params.put("GitCommitDate", "'1732924800 2024-11-30 00:00:00 +0000'") + def run_replay(variant: LayoutVariant) -> None: if HEADLESS: From 29a3b3315ff4448c650c442feb887318acf4ebe1 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sat, 28 Feb 2026 00:18:35 -0500 Subject: [PATCH 264/311] ui: reimplement "Screen Off" option to Onroad Brightness (#1732) * ui: Add "Screen Off" option to Onroad Brightness * migrate old value * bruh * fix algo * comment * no --- common/params_keys.h | 1 + .../ui/sunnypilot/layouts/settings/display.py | 8 +++- selfdrive/ui/sunnypilot/ui_state.py | 10 ++-- sunnypilot/sunnylink/params_metadata.json | 46 +++++++++++-------- .../sunnylink/tools/update_params_metadata.py | 26 +++++++++++ sunnypilot/system/__init__.py | 0 sunnypilot/system/params_migration.py | 27 +++++++++++ system/manager/manager.py | 4 ++ 8 files changed, 97 insertions(+), 25 deletions(-) create mode 100644 sunnypilot/system/__init__.py create mode 100644 sunnypilot/system/params_migration.py diff --git a/common/params_keys.h b/common/params_keys.h index 8f3ee858f0..65402b5b35 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -170,6 +170,7 @@ inline static std::unordered_map keys = { {"OffroadMode", {CLEAR_ON_MANAGER_START, BOOL}}, {"Offroad_TiciSupport", {CLEAR_ON_MANAGER_START, JSON}}, {"OnroadScreenOffBrightness", {PERSISTENT | BACKUP, INT, "0"}}, + {"OnroadScreenOffBrightnessMigrated", {PERSISTENT | BACKUP, STRING, "0.0"}}, {"OnroadScreenOffTimer", {PERSISTENT | BACKUP, INT, "15"}}, {"OnroadUploads", {PERSISTENT | BACKUP, BOOL, "1"}}, {"QuickBootToggle", {PERSISTENT | BACKUP, BOOL, "0"}}, diff --git a/selfdrive/ui/sunnypilot/layouts/settings/display.py b/selfdrive/ui/sunnypilot/layouts/settings/display.py index e5f1eba181..d44118d8f4 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/display.py +++ b/selfdrive/ui/sunnypilot/layouts/settings/display.py @@ -19,6 +19,7 @@ ONROAD_BRIGHTNESS_TIMER_VALUES = {0: 15, 1: 30, **{i: (i - 1) * 60 for i in rang class OnroadBrightness(IntEnum): AUTO = 0 AUTO_DARK = 1 + SCREEN_OFF = 2 class DisplayLayout(Widget): @@ -35,7 +36,7 @@ class DisplayLayout(Widget): title=lambda: tr("Onroad Brightness"), description="", min_value=0, - max_value=21, + max_value=22, value_change_step=1, label_callback=lambda value: self.update_onroad_brightness(value), inline=True @@ -79,7 +80,10 @@ class DisplayLayout(Widget): if val == OnroadBrightness.AUTO_DARK: return tr("Auto (Dark)") - return f"{(val - 1) * 5} %" + if val == OnroadBrightness.SCREEN_OFF: + return tr("Screen Off") + + return f"{(val - 2) * 5} %" def _update_state(self): super()._update_state() diff --git a/selfdrive/ui/sunnypilot/ui_state.py b/selfdrive/ui/sunnypilot/ui_state.py index 4d26e138b1..050df6d26d 100644 --- a/selfdrive/ui/sunnypilot/ui_state.py +++ b/selfdrive/ui/sunnypilot/ui_state.py @@ -162,14 +162,16 @@ class DeviceSP: # For AUTO (Default) and Manual modes (while timer running), use standard brightness return cur_brightness - # 0: Auto (Default), 1: Auto (Dark) + # 0: Auto (Default), 1: Auto (Dark), 2: Screen Off if _ui_state.onroad_brightness == OnroadBrightness.AUTO: return cur_brightness - elif _ui_state.onroad_brightness == OnroadBrightness.AUTO_DARK: + if _ui_state.onroad_brightness == OnroadBrightness.AUTO_DARK: return cur_brightness + if _ui_state.onroad_brightness == OnroadBrightness.SCREEN_OFF: + return 0.0 - # 2-21: 5% - 100% - return float((_ui_state.onroad_brightness - 1) * 5) + # 3-22: 5% - 100% + return float((_ui_state.onroad_brightness - 2) * 5) @staticmethod def set_min_onroad_brightness(_ui_state, min_brightness: int) -> int: diff --git a/sunnypilot/sunnylink/params_metadata.json b/sunnypilot/sunnylink/params_metadata.json index 67ce903aba..59c473b973 100644 --- a/sunnypilot/sunnylink/params_metadata.json +++ b/sunnypilot/sunnylink/params_metadata.json @@ -845,86 +845,94 @@ }, { "value": 2, - "label": "5 %" + "label": "Screen Off" }, { "value": 3, - "label": "10 %" + "label": "5 %" }, { "value": 4, - "label": "15 %" + "label": "10 %" }, { "value": 5, - "label": "20 %" + "label": "15 %" }, { "value": 6, - "label": "25 %" + "label": "20 %" }, { "value": 7, - "label": "30 %" + "label": "25 %" }, { "value": 8, - "label": "35 %" + "label": "30 %" }, { "value": 9, - "label": "40 %" + "label": "35 %" }, { "value": 10, - "label": "45 %" + "label": "40 %" }, { "value": 11, - "label": "50 %" + "label": "45 %" }, { "value": 12, - "label": "55 %" + "label": "50 %" }, { "value": 13, - "label": "60 %" + "label": "55 %" }, { "value": 14, - "label": "65 %" + "label": "60 %" }, { "value": 15, - "label": "70 %" + "label": "65 %" }, { "value": 16, - "label": "75 %" + "label": "70 %" }, { "value": 17, - "label": "80 %" + "label": "75 %" }, { "value": 18, - "label": "85 %" + "label": "80 %" }, { "value": 19, - "label": "90 %" + "label": "85 %" }, { "value": 20, - "label": "95 %" + "label": "90 %" }, { "value": 21, + "label": "95 %" + }, + { + "value": 22, "label": "100 %" } ] }, + "OnroadScreenOffBrightnessMigrated": { + "title": "Onroad Brightness Migration Version", + "description": "This param is to track whether OnroadScreenOffBrightness needs to be migrated." + }, "OnroadScreenOffControl": { "title": "Onroad Brightness", "description": "Adjusts the screen brightness while it's in onroad state." diff --git a/sunnypilot/sunnylink/tools/update_params_metadata.py b/sunnypilot/sunnylink/tools/update_params_metadata.py index 3ef91debe0..013544005b 100755 --- a/sunnypilot/sunnylink/tools/update_params_metadata.py +++ b/sunnypilot/sunnylink/tools/update_params_metadata.py @@ -53,9 +53,34 @@ def main(): print(f"Updated {METADATA_PATH}") + # update onroad screen brightness params + update_onroad_brightness_param() + # update torque versions param update_torque_versions_param() + +def update_onroad_brightness_param(): + try: + with open(METADATA_PATH) as f: + params_metadata = json.load(f) + if "OnroadScreenOffBrightness" in params_metadata: + options = [ + {"value": 0, "label": "Auto (Default)"}, + {"value": 1, "label": "Auto (Dark)"}, + {"value": 2, "label": "Screen Off"}, + ] + for i in range(3, 23): + options.append({"value": i, "label": f"{(i - 2) * 5} %"}) + params_metadata["OnroadScreenOffBrightness"]["options"] = options + with open(METADATA_PATH, 'w') as f: + json.dump(params_metadata, f, indent=2) + f.write('\n') + print(f"Updated OnroadScreenOffBrightness options in params_metadata.json with {len(options)} options.") + except Exception as e: + print(f"Failed to update OnroadScreenOffBrightness versions in params_metadata.json: {e}") + + def update_torque_versions_param(): with open(TORQUE_VERSIONS_JSON) as f: current_versions = json.load(f) @@ -81,5 +106,6 @@ def update_torque_versions_param(): except Exception as e: print(f"Failed to update TorqueControlTune versions in params_metadata.json: {e}") + if __name__ == "__main__": main() diff --git a/sunnypilot/system/__init__.py b/sunnypilot/system/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/sunnypilot/system/params_migration.py b/sunnypilot/system/params_migration.py new file mode 100644 index 0000000000..6a82f1866d --- /dev/null +++ b/sunnypilot/system/params_migration.py @@ -0,0 +1,27 @@ +""" +Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + +This file is part of sunnypilot and is licensed under the MIT License. +See the LICENSE.md file in the root directory for more details. +""" +from openpilot.common.swaglog import cloudlog + +ONROAD_BRIGHTNESS_MIGRATION_VERSION: str = "1.0" + + +def run_migration(_params): + # migrate OnroadScreenOffBrightness + if _params.get("OnroadScreenOffBrightnessMigrated") != ONROAD_BRIGHTNESS_MIGRATION_VERSION: + try: + val = _params.get("OnroadScreenOffBrightness") + if val >= 2: # old: 5%, new: Screen Off + new_val = val + 1 + _params.put("OnroadScreenOffBrightness", new_val) + log_str = f"Successfully migrated OnroadScreenOffBrightness from {val} to {new_val}." + else: + log_str = "Migration not required for OnroadScreenOffBrightness." + + _params.put("OnroadScreenOffBrightnessMigrated", ONROAD_BRIGHTNESS_MIGRATION_VERSION) + cloudlog.info(log_str + f" Setting OnroadScreenOffBrightnessMigrated to {ONROAD_BRIGHTNESS_MIGRATION_VERSION}") + except Exception as e: + cloudlog.exception(f"Error migrating OnroadScreenOffBrightness: {e}") diff --git a/system/manager/manager.py b/system/manager/manager.py index 8c5d35d072..8c219909e4 100755 --- a/system/manager/manager.py +++ b/system/manager/manager.py @@ -22,6 +22,8 @@ from openpilot.system.version import get_build_metadata from openpilot.system.hardware.hw import Paths from openpilot.system.hardware import PC +from openpilot.sunnypilot.system.params_migration import run_migration + def manager_init() -> None: save_bootlog() @@ -49,6 +51,8 @@ def manager_init() -> None: if params.get_bool("RecordFrontLock"): params.put_bool("RecordFront", True) + run_migration(params) + # set unset params to their default value for k in params.all_keys(): default_value = params.get_default_value(k) From 47ca2c93817e265f6784bb40695e59ab9f742790 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 27 Feb 2026 21:21:33 -0800 Subject: [PATCH 265/311] ui: widgets animate out (#37321) * stash * widgets animate out * Revert "stash" This reverts commit eac3493509cff6f2c64111d803c7fef21a1aa2dd. * abstract * works also * works also * support pop_widget * only animate top * callback in request pop * tune it * fix * fix * try this * Revert "try this" This reverts commit 191373a1b35917ee3a361afe73b16eeb60d0a20e. * debug * debug * clean up * simple test * clean up * clean up * clean up * clean up * clean up * clean up * clkean up * re sort * fine * yes --- selfdrive/ui/mici/layouts/main.py | 7 ++-- selfdrive/ui/mici/widgets/dialog.py | 8 ++--- system/ui/lib/application.py | 43 ++++++++++++++++++++++--- system/ui/mici_setup.py | 2 +- system/ui/tests/test_nav_stack.py | 50 +++++++++++++++++++++++++++++ system/ui/widgets/nav_widget.py | 16 ++++++++- 6 files changed, 112 insertions(+), 14 deletions(-) create mode 100644 system/ui/tests/test_nav_stack.py diff --git a/selfdrive/ui/mici/layouts/main.py b/selfdrive/ui/mici/layouts/main.py index 1a0c6bc094..b8958993f2 100644 --- a/selfdrive/ui/mici/layouts/main.py +++ b/selfdrive/ui/mici/layouts/main.py @@ -93,14 +93,14 @@ class MiciMainLayout(Scroller): self._scroll_to(self._home_layout) if self._onroad_time_delay is not None and rl.get_time() - self._onroad_time_delay >= ONROAD_DELAY: - gui_app.pop_widgets_to(self) + gui_app.request_pop_widgets_to(self) self._scroll_to(self._onroad_layout) self._onroad_time_delay = None # When car leaves standstill, pop nav stack and scroll to onroad CS = ui_state.sm["carState"] if not CS.standstill and self._prev_standstill: - gui_app.pop_widgets_to(self) + gui_app.request_pop_widgets_to(self) self._scroll_to(self._onroad_layout) self._prev_standstill = CS.standstill @@ -112,9 +112,10 @@ class MiciMainLayout(Scroller): if ui_state.started: # Don't pop if at standstill if not ui_state.sm["carState"].standstill: - gui_app.pop_widgets_to(self) + gui_app.request_pop_widgets_to(self) self._scroll_to(self._onroad_layout) else: + # Screen turns off on timeout offroad, so pop immediately without animation gui_app.pop_widgets_to(self) self._scroll_to(self._home_layout) diff --git a/selfdrive/ui/mici/widgets/dialog.py b/selfdrive/ui/mici/widgets/dialog.py index 8e978066c6..1191f031be 100644 --- a/selfdrive/ui/mici/widgets/dialog.py +++ b/selfdrive/ui/mici/widgets/dialog.py @@ -82,8 +82,8 @@ class BigConfirmationDialogV2(BigDialogBase): def _on_confirm(self): if self._exit_on_confirm: - gui_app.pop_widget() - if self._confirm_callback: + gui_app.request_pop_widget(self._confirm_callback) + elif self._confirm_callback: self._confirm_callback() def _update_state(self): @@ -128,9 +128,7 @@ class BigInputDialog(BigDialogBase): def confirm_callback_wrapper(): text = self._keyboard.text() - gui_app.pop_widget() - if confirm_callback: - confirm_callback(text) + gui_app.request_pop_widget(lambda: confirm_callback(text) if confirm_callback else None) self._confirm_callback = confirm_callback_wrapper def _update_state(self): diff --git a/system/ui/lib/application.py b/system/ui/lib/application.py index 34aed4f6a6..ebfa385ea0 100644 --- a/system/ui/lib/application.py +++ b/system/ui/lib/application.py @@ -384,17 +384,25 @@ class GuiApplication: self._nav_stack.append(widget) widget.show_event() - def pop_widget(self): + # pop_widget and pop_widgets_to are immediate (no animation). Use request_* variants for animated dismiss. + def pop_widget(self, idx: int | None = None): if len(self._nav_stack) < 2: cloudlog.warning("At least one widget should remain on the stack, ignoring pop!") return - # re-enable previous widget and pop current + if idx is None: + idx = -1 + else: + if idx < 1 or idx >= len(self._nav_stack): + return + + # re-enable widget below, and re-enable popped widget so it's clean if pushed again # TODO: switch to touch_valid - prev_widget = self._nav_stack[-2] + prev_widget = self._nav_stack[idx - 1] prev_widget.set_enabled(True) - widget = self._nav_stack.pop() + widget = self._nav_stack.pop(idx) + widget.set_enabled(True) widget.hide_event() def pop_widgets_to(self, widget): @@ -406,6 +414,33 @@ class GuiApplication: while len(self._nav_stack) > 0 and self._nav_stack[-1] != widget: self.pop_widget() + def request_pop_widget(self, callback: Callable | None = None): + """Request the top widget to close. NavWidgets dismiss (animate then pop); others pop immediately. Callback runs after pop.""" + if len(self._nav_stack) < 2: + cloudlog.warning("At least one widget should remain on the stack, ignoring pop!") + return + top = self._nav_stack[-1] + if hasattr(top, "dismiss"): + top.dismiss(callback) + else: + self.pop_widget() + if callback: + callback() + + def request_pop_widgets_to(self, widget): + """Request to close widgets down to the given widget. Middle widgets are removed via pop_widget logic; only the top animates down.""" + if widget not in self._nav_stack: + cloudlog.warning("Widget not in stack, cannot pop to it!") + return + + if len(self._nav_stack) < 2 or self._nav_stack[-1] == widget: + return + + # Pop second-from-top repeatedly until stack is [target, top]; each goes through re-enable + hide_event + while len(self._nav_stack) > 2 and self._nav_stack[-2] != widget: + self.pop_widget(len(self._nav_stack) - 2) + self.request_pop_widget() + def get_active_widget(self): if len(self._nav_stack) > 0: return self._nav_stack[-1] diff --git a/system/ui/mici_setup.py b/system/ui/mici_setup.py index ca63544439..961cbe7531 100755 --- a/system/ui/mici_setup.py +++ b/system/ui/mici_setup.py @@ -534,7 +534,7 @@ class Setup(Widget): def _nav_stack_tick(self): has_internet = self._network_monitor.network_connected.is_set() if has_internet and not self._prev_has_internet: - gui_app.pop_widgets_to(self) + gui_app.request_pop_widgets_to(self) self._prev_has_internet = has_internet def _update_state(self): diff --git a/system/ui/tests/test_nav_stack.py b/system/ui/tests/test_nav_stack.py new file mode 100644 index 0000000000..eae5d78c1a --- /dev/null +++ b/system/ui/tests/test_nav_stack.py @@ -0,0 +1,50 @@ +import pytest +from openpilot.system.ui.lib.application import gui_app + + +class Widget: + def __init__(self): + self.enabled, self.shown, self.hidden = True, False, False + + def set_enabled(self, e): self.enabled = e + def show_event(self): self.shown = True + def hide_event(self): self.hidden = True + + +@pytest.fixture(autouse=True) +def clean_stack(): + gui_app._nav_stack = [] + yield + gui_app._nav_stack = [] + + +def test_push(): + a, b = Widget(), Widget() + gui_app.push_widget(a) + gui_app.push_widget(b) + assert not a.enabled and not a.hidden + assert b.enabled and b.shown + + +def test_pop_re_enables(): + widgets = [Widget() for _ in range(4)] + for w in widgets: + gui_app.push_widget(w) + assert all(not w.enabled for w in widgets[:-1]) + gui_app.pop_widget() + assert widgets[-2].enabled + + +@pytest.mark.parametrize("pop_fn", [gui_app.pop_widgets_to, gui_app.request_pop_widgets_to]) +def test_pop_widgets_to(pop_fn): + widgets = [Widget() for _ in range(4)] + for w in widgets: + gui_app.push_widget(w) + + root = widgets[0] + pop_fn(root) + + assert gui_app._nav_stack == [root] + assert root.enabled and not root.hidden + for w in widgets[1:]: + assert w.enabled and w.hidden and w.shown diff --git a/system/ui/widgets/nav_widget.py b/system/ui/widgets/nav_widget.py index 02afc911b2..ad8d5d313a 100644 --- a/system/ui/widgets/nav_widget.py +++ b/system/ui/widgets/nav_widget.py @@ -17,6 +17,7 @@ NAV_BAR_HEIGHT = 8 DISMISS_PUSH_OFFSET = 50 + NAV_BAR_MARGIN + NAV_BAR_HEIGHT # px extra to push down when dismissing DISMISS_TIME_SECONDS = 2.0 +DISMISS_ANIMATION_RC = 0.2 # time constant for non-user triggered dismiss animation class NavBar(Widget): @@ -62,6 +63,7 @@ class NavWidget(Widget, abc.ABC): self._pos_filter = BounceFilter(0.0, 0.1, 1 / gui_app.target_fps, bounce=1) self._playing_dismiss_animation = False + self._dismiss_callback: Callable | None = None self._trigger_animate_in = False self._nav_bar_show_time = 0.0 self._back_enabled: bool | Callable[[], bool] = True @@ -83,7 +85,8 @@ class NavWidget(Widget, abc.ABC): # FIXME: disabling this widget on new push_widget still causes this widget to track mouse events without mouse down super()._handle_mouse_event(mouse_event) - if not self.back_enabled: + # Don't let touch events change filter state during dismiss animation + if not self.back_enabled or self._playing_dismiss_animation: self._back_button_start_pos = None self._swiping_away = False self._can_swipe_away = True @@ -170,6 +173,10 @@ class NavWidget(Widget, abc.ABC): if self._back_callback is not None: self._back_callback() + if self._dismiss_callback is not None: + self._dismiss_callback() + self._dismiss_callback = None + self._playing_dismiss_animation = False self._back_button_start_pos = None self._swiping_away = False @@ -205,6 +212,13 @@ class NavWidget(Widget, abc.ABC): return ret + def dismiss(self, callback: Callable | None = None): + """Programmatically trigger the dismiss animation. Calls pop_widget when done, then callback.""" + if not self._playing_dismiss_animation: + self._pos_filter.update_alpha(DISMISS_ANIMATION_RC) + self._playing_dismiss_animation = True + self._dismiss_callback = callback + def show_event(self): super().show_event() # FIXME: we don't know the height of the rect at first show_event since it's before the first render :( From 16047e3c3d3bffab6b05025985b0b6a0538dbdd0 Mon Sep 17 00:00:00 2001 From: royjr Date: Sat, 28 Feb 2026 00:28:08 -0500 Subject: [PATCH 266/311] ui: dont hide steering wheel when blindspot disabled (#1709) Update blind_spot_indicators.py Co-authored-by: Jason Wen --- selfdrive/ui/sunnypilot/onroad/blind_spot_indicators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selfdrive/ui/sunnypilot/onroad/blind_spot_indicators.py b/selfdrive/ui/sunnypilot/onroad/blind_spot_indicators.py index f2f482525d..4e1d748117 100644 --- a/selfdrive/ui/sunnypilot/onroad/blind_spot_indicators.py +++ b/selfdrive/ui/sunnypilot/onroad/blind_spot_indicators.py @@ -28,7 +28,7 @@ class BlindSpotIndicators: @property def detected(self) -> bool: - return self._blind_spot_left_alpha_filter.x > 0.01 or self._blind_spot_right_alpha_filter.x > 0.01 + return ui_state.blindspot and (self._blind_spot_left_alpha_filter.x > 0.01 or self._blind_spot_right_alpha_filter.x > 0.01) def render(self, rect: rl.Rectangle) -> None: if not ui_state.blindspot: From 876ac69047403705400e48406ad319c9f6fc3a25 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 27 Feb 2026 23:48:56 -0800 Subject: [PATCH 267/311] mici ui: power button visible on ignition (#37475) visilbe when not ignition --- selfdrive/ui/mici/layouts/settings/device.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/selfdrive/ui/mici/layouts/settings/device.py b/selfdrive/ui/mici/layouts/settings/device.py index 7383393542..077411c95a 100644 --- a/selfdrive/ui/mici/layouts/settings/device.py +++ b/selfdrive/ui/mici/layouts/settings/device.py @@ -302,6 +302,7 @@ class DeviceLayoutMici(NavScroller): self._power_off_btn = BigCircleButton("icons_mici/settings/device/power.png", red=True, icon_size=(64, 66)) self._power_off_btn.set_click_callback(lambda: _engaged_confirmation_callback(power_off_callback, "power off")) + self._power_off_btn.set_visible(lambda: not ui_state.ignition) regulatory_btn = BigButton("regulatory info", "", "icons_mici/settings/device/info.png") regulatory_btn.set_click_callback(self._on_regulatory) @@ -331,13 +332,7 @@ class DeviceLayoutMici(NavScroller): # TODO: can this somehow be generic in widgets/__init__.py or application.py? self.set_back_callback(gui_app.pop_widget) - # Hide power off button when onroad - ui_state.add_offroad_transition_callback(self._offroad_transition) - def _on_regulatory(self): if not self._fcc_dialog: self._fcc_dialog = MiciFccModal(os.path.join(BASEDIR, "selfdrive/assets/offroad/mici_fcc.html")) gui_app.push_widget(self._fcc_dialog) - - def _offroad_transition(self): - self._power_off_btn.set_visible(ui_state.is_offroad()) From 6266feeed2c314e48b1fa5f735b1cc56f2a03284 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Sat, 28 Feb 2026 00:13:34 -0800 Subject: [PATCH 268/311] Revert "ui: widgets animate out" (#37478) Revert "ui: widgets animate out (#37321)" This reverts commit 47ca2c93817e265f6784bb40695e59ab9f742790. --- selfdrive/ui/mici/layouts/main.py | 7 ++-- selfdrive/ui/mici/widgets/dialog.py | 8 +++-- system/ui/lib/application.py | 43 +++---------------------- system/ui/mici_setup.py | 2 +- system/ui/tests/test_nav_stack.py | 50 ----------------------------- system/ui/widgets/nav_widget.py | 16 +-------- 6 files changed, 14 insertions(+), 112 deletions(-) delete mode 100644 system/ui/tests/test_nav_stack.py diff --git a/selfdrive/ui/mici/layouts/main.py b/selfdrive/ui/mici/layouts/main.py index b8958993f2..1a0c6bc094 100644 --- a/selfdrive/ui/mici/layouts/main.py +++ b/selfdrive/ui/mici/layouts/main.py @@ -93,14 +93,14 @@ class MiciMainLayout(Scroller): self._scroll_to(self._home_layout) if self._onroad_time_delay is not None and rl.get_time() - self._onroad_time_delay >= ONROAD_DELAY: - gui_app.request_pop_widgets_to(self) + gui_app.pop_widgets_to(self) self._scroll_to(self._onroad_layout) self._onroad_time_delay = None # When car leaves standstill, pop nav stack and scroll to onroad CS = ui_state.sm["carState"] if not CS.standstill and self._prev_standstill: - gui_app.request_pop_widgets_to(self) + gui_app.pop_widgets_to(self) self._scroll_to(self._onroad_layout) self._prev_standstill = CS.standstill @@ -112,10 +112,9 @@ class MiciMainLayout(Scroller): if ui_state.started: # Don't pop if at standstill if not ui_state.sm["carState"].standstill: - gui_app.request_pop_widgets_to(self) + gui_app.pop_widgets_to(self) self._scroll_to(self._onroad_layout) else: - # Screen turns off on timeout offroad, so pop immediately without animation gui_app.pop_widgets_to(self) self._scroll_to(self._home_layout) diff --git a/selfdrive/ui/mici/widgets/dialog.py b/selfdrive/ui/mici/widgets/dialog.py index 1191f031be..8e978066c6 100644 --- a/selfdrive/ui/mici/widgets/dialog.py +++ b/selfdrive/ui/mici/widgets/dialog.py @@ -82,8 +82,8 @@ class BigConfirmationDialogV2(BigDialogBase): def _on_confirm(self): if self._exit_on_confirm: - gui_app.request_pop_widget(self._confirm_callback) - elif self._confirm_callback: + gui_app.pop_widget() + if self._confirm_callback: self._confirm_callback() def _update_state(self): @@ -128,7 +128,9 @@ class BigInputDialog(BigDialogBase): def confirm_callback_wrapper(): text = self._keyboard.text() - gui_app.request_pop_widget(lambda: confirm_callback(text) if confirm_callback else None) + gui_app.pop_widget() + if confirm_callback: + confirm_callback(text) self._confirm_callback = confirm_callback_wrapper def _update_state(self): diff --git a/system/ui/lib/application.py b/system/ui/lib/application.py index ebfa385ea0..34aed4f6a6 100644 --- a/system/ui/lib/application.py +++ b/system/ui/lib/application.py @@ -384,25 +384,17 @@ class GuiApplication: self._nav_stack.append(widget) widget.show_event() - # pop_widget and pop_widgets_to are immediate (no animation). Use request_* variants for animated dismiss. - def pop_widget(self, idx: int | None = None): + def pop_widget(self): if len(self._nav_stack) < 2: cloudlog.warning("At least one widget should remain on the stack, ignoring pop!") return - if idx is None: - idx = -1 - else: - if idx < 1 or idx >= len(self._nav_stack): - return - - # re-enable widget below, and re-enable popped widget so it's clean if pushed again + # re-enable previous widget and pop current # TODO: switch to touch_valid - prev_widget = self._nav_stack[idx - 1] + prev_widget = self._nav_stack[-2] prev_widget.set_enabled(True) - widget = self._nav_stack.pop(idx) - widget.set_enabled(True) + widget = self._nav_stack.pop() widget.hide_event() def pop_widgets_to(self, widget): @@ -414,33 +406,6 @@ class GuiApplication: while len(self._nav_stack) > 0 and self._nav_stack[-1] != widget: self.pop_widget() - def request_pop_widget(self, callback: Callable | None = None): - """Request the top widget to close. NavWidgets dismiss (animate then pop); others pop immediately. Callback runs after pop.""" - if len(self._nav_stack) < 2: - cloudlog.warning("At least one widget should remain on the stack, ignoring pop!") - return - top = self._nav_stack[-1] - if hasattr(top, "dismiss"): - top.dismiss(callback) - else: - self.pop_widget() - if callback: - callback() - - def request_pop_widgets_to(self, widget): - """Request to close widgets down to the given widget. Middle widgets are removed via pop_widget logic; only the top animates down.""" - if widget not in self._nav_stack: - cloudlog.warning("Widget not in stack, cannot pop to it!") - return - - if len(self._nav_stack) < 2 or self._nav_stack[-1] == widget: - return - - # Pop second-from-top repeatedly until stack is [target, top]; each goes through re-enable + hide_event - while len(self._nav_stack) > 2 and self._nav_stack[-2] != widget: - self.pop_widget(len(self._nav_stack) - 2) - self.request_pop_widget() - def get_active_widget(self): if len(self._nav_stack) > 0: return self._nav_stack[-1] diff --git a/system/ui/mici_setup.py b/system/ui/mici_setup.py index 961cbe7531..ca63544439 100755 --- a/system/ui/mici_setup.py +++ b/system/ui/mici_setup.py @@ -534,7 +534,7 @@ class Setup(Widget): def _nav_stack_tick(self): has_internet = self._network_monitor.network_connected.is_set() if has_internet and not self._prev_has_internet: - gui_app.request_pop_widgets_to(self) + gui_app.pop_widgets_to(self) self._prev_has_internet = has_internet def _update_state(self): diff --git a/system/ui/tests/test_nav_stack.py b/system/ui/tests/test_nav_stack.py deleted file mode 100644 index eae5d78c1a..0000000000 --- a/system/ui/tests/test_nav_stack.py +++ /dev/null @@ -1,50 +0,0 @@ -import pytest -from openpilot.system.ui.lib.application import gui_app - - -class Widget: - def __init__(self): - self.enabled, self.shown, self.hidden = True, False, False - - def set_enabled(self, e): self.enabled = e - def show_event(self): self.shown = True - def hide_event(self): self.hidden = True - - -@pytest.fixture(autouse=True) -def clean_stack(): - gui_app._nav_stack = [] - yield - gui_app._nav_stack = [] - - -def test_push(): - a, b = Widget(), Widget() - gui_app.push_widget(a) - gui_app.push_widget(b) - assert not a.enabled and not a.hidden - assert b.enabled and b.shown - - -def test_pop_re_enables(): - widgets = [Widget() for _ in range(4)] - for w in widgets: - gui_app.push_widget(w) - assert all(not w.enabled for w in widgets[:-1]) - gui_app.pop_widget() - assert widgets[-2].enabled - - -@pytest.mark.parametrize("pop_fn", [gui_app.pop_widgets_to, gui_app.request_pop_widgets_to]) -def test_pop_widgets_to(pop_fn): - widgets = [Widget() for _ in range(4)] - for w in widgets: - gui_app.push_widget(w) - - root = widgets[0] - pop_fn(root) - - assert gui_app._nav_stack == [root] - assert root.enabled and not root.hidden - for w in widgets[1:]: - assert w.enabled and w.hidden and w.shown diff --git a/system/ui/widgets/nav_widget.py b/system/ui/widgets/nav_widget.py index ad8d5d313a..02afc911b2 100644 --- a/system/ui/widgets/nav_widget.py +++ b/system/ui/widgets/nav_widget.py @@ -17,7 +17,6 @@ NAV_BAR_HEIGHT = 8 DISMISS_PUSH_OFFSET = 50 + NAV_BAR_MARGIN + NAV_BAR_HEIGHT # px extra to push down when dismissing DISMISS_TIME_SECONDS = 2.0 -DISMISS_ANIMATION_RC = 0.2 # time constant for non-user triggered dismiss animation class NavBar(Widget): @@ -63,7 +62,6 @@ class NavWidget(Widget, abc.ABC): self._pos_filter = BounceFilter(0.0, 0.1, 1 / gui_app.target_fps, bounce=1) self._playing_dismiss_animation = False - self._dismiss_callback: Callable | None = None self._trigger_animate_in = False self._nav_bar_show_time = 0.0 self._back_enabled: bool | Callable[[], bool] = True @@ -85,8 +83,7 @@ class NavWidget(Widget, abc.ABC): # FIXME: disabling this widget on new push_widget still causes this widget to track mouse events without mouse down super()._handle_mouse_event(mouse_event) - # Don't let touch events change filter state during dismiss animation - if not self.back_enabled or self._playing_dismiss_animation: + if not self.back_enabled: self._back_button_start_pos = None self._swiping_away = False self._can_swipe_away = True @@ -173,10 +170,6 @@ class NavWidget(Widget, abc.ABC): if self._back_callback is not None: self._back_callback() - if self._dismiss_callback is not None: - self._dismiss_callback() - self._dismiss_callback = None - self._playing_dismiss_animation = False self._back_button_start_pos = None self._swiping_away = False @@ -212,13 +205,6 @@ class NavWidget(Widget, abc.ABC): return ret - def dismiss(self, callback: Callable | None = None): - """Programmatically trigger the dismiss animation. Calls pop_widget when done, then callback.""" - if not self._playing_dismiss_animation: - self._pos_filter.update_alpha(DISMISS_ANIMATION_RC) - self._playing_dismiss_animation = True - self._dismiss_callback = callback - def show_event(self): super().show_event() # FIXME: we don't know the height of the rect at first show_event since it's before the first render :( From b6f3692b56890e09428f9ffd9f2388fcea6d0dee Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Sat, 28 Feb 2026 00:29:15 -0800 Subject: [PATCH 269/311] NavWidget: standardize back callback (#37479) clean this up --- selfdrive/ui/mici/layouts/settings/developer.py | 1 - selfdrive/ui/mici/layouts/settings/device.py | 5 ----- selfdrive/ui/mici/layouts/settings/firehose.py | 1 - selfdrive/ui/mici/layouts/settings/network/__init__.py | 3 --- selfdrive/ui/mici/layouts/settings/network/wifi_ui.py | 3 --- selfdrive/ui/mici/layouts/settings/settings.py | 3 --- selfdrive/ui/mici/layouts/settings/toggles.py | 1 - selfdrive/ui/mici/onroad/driver_camera_dialog.py | 1 - selfdrive/ui/mici/widgets/dialog.py | 1 - selfdrive/ui/mici/widgets/pairing_dialog.py | 1 - system/ui/widgets/nav_widget.py | 7 +------ 11 files changed, 1 insertion(+), 26 deletions(-) diff --git a/selfdrive/ui/mici/layouts/settings/developer.py b/selfdrive/ui/mici/layouts/settings/developer.py index 4d6bdfc3bb..4e7796814e 100644 --- a/selfdrive/ui/mici/layouts/settings/developer.py +++ b/selfdrive/ui/mici/layouts/settings/developer.py @@ -11,7 +11,6 @@ from openpilot.selfdrive.ui.widgets.ssh_key import SshKeyAction class DeveloperLayoutMici(NavScroller): def __init__(self): super().__init__() - self.set_back_callback(gui_app.pop_widget) def github_username_callback(username: str): if username: diff --git a/selfdrive/ui/mici/layouts/settings/device.py b/selfdrive/ui/mici/layouts/settings/device.py index 077411c95a..d754b9f980 100644 --- a/selfdrive/ui/mici/layouts/settings/device.py +++ b/selfdrive/ui/mici/layouts/settings/device.py @@ -29,7 +29,6 @@ class MiciFccModal(NavWidget): def __init__(self, file_path: str | None = None, text: str | None = None): super().__init__() - self.set_back_callback(gui_app.pop_widget) self._content = HtmlRenderer(file_path=file_path, text=text) self._scroll_panel = GuiScrollPanel2(horizontal=False) self._scroll_panel.set_enabled(lambda: self.enabled and not self._swiping_away) @@ -328,10 +327,6 @@ class DeviceLayoutMici(NavScroller): self._power_off_btn, ]) - # Set up back navigation - # TODO: can this somehow be generic in widgets/__init__.py or application.py? - self.set_back_callback(gui_app.pop_widget) - def _on_regulatory(self): if not self._fcc_dialog: self._fcc_dialog = MiciFccModal(os.path.join(BASEDIR, "selfdrive/assets/offroad/mici_fcc.html")) diff --git a/selfdrive/ui/mici/layouts/settings/firehose.py b/selfdrive/ui/mici/layouts/settings/firehose.py index b2c06d7299..d16a04019f 100644 --- a/selfdrive/ui/mici/layouts/settings/firehose.py +++ b/selfdrive/ui/mici/layouts/settings/firehose.py @@ -223,5 +223,4 @@ class FirehoseLayout(FirehoseLayoutBase, NavWidget): def __init__(self): super().__init__() - self.set_back_callback(gui_app.pop_widget) self._scroll_panel.set_enabled(lambda: self.enabled and not self._swiping_away) diff --git a/selfdrive/ui/mici/layouts/settings/network/__init__.py b/selfdrive/ui/mici/layouts/settings/network/__init__.py index ae7d085e01..553a74fc60 100644 --- a/selfdrive/ui/mici/layouts/settings/network/__init__.py +++ b/selfdrive/ui/mici/layouts/settings/network/__init__.py @@ -148,9 +148,6 @@ class NetworkLayoutMici(NavScroller): metered = ui_state.params.get_bool("GsmMetered") self._wifi_manager.update_gsm_settings(roaming_enabled, ui_state.params.get("GsmApn") or "", metered) - # Set up back navigation - self.set_back_callback(gui_app.pop_widget) - def _update_state(self): super()._update_state() diff --git a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py index a73a397d1a..cc45daf24b 100644 --- a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py +++ b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py @@ -272,9 +272,6 @@ class WifiUIMici(NavScroller): def __init__(self, wifi_manager: WifiManager): super().__init__() - # Set up back navigation - self.set_back_callback(gui_app.pop_widget) - self._loading_animation = LoadingAnimation() self._wifi_manager = wifi_manager diff --git a/selfdrive/ui/mici/layouts/settings/settings.py b/selfdrive/ui/mici/layouts/settings/settings.py index d996e01fed..c7fb3201f5 100644 --- a/selfdrive/ui/mici/layouts/settings/settings.py +++ b/selfdrive/ui/mici/layouts/settings/settings.py @@ -49,7 +49,4 @@ class SettingsLayout(NavScroller): developer_btn, ]) - # Set up back navigation - self.set_back_callback(gui_app.pop_widget) - self._font_medium = gui_app.font(FontWeight.MEDIUM) diff --git a/selfdrive/ui/mici/layouts/settings/toggles.py b/selfdrive/ui/mici/layouts/settings/toggles.py index a7a7bff6e2..acb502fda0 100644 --- a/selfdrive/ui/mici/layouts/settings/toggles.py +++ b/selfdrive/ui/mici/layouts/settings/toggles.py @@ -12,7 +12,6 @@ PERSONALITY_TO_INT = log.LongitudinalPersonality.schema.enumerants class TogglesLayoutMici(NavScroller): def __init__(self): super().__init__() - self.set_back_callback(gui_app.pop_widget) self._personality_toggle = BigMultiParamToggle("driving personality", "LongitudinalPersonality", ["aggressive", "standard", "relaxed"]) self._experimental_btn = BigParamControl("experimental mode", "ExperimentalMode") diff --git a/selfdrive/ui/mici/onroad/driver_camera_dialog.py b/selfdrive/ui/mici/onroad/driver_camera_dialog.py index 4fddc88f6d..dfa8beeb77 100644 --- a/selfdrive/ui/mici/onroad/driver_camera_dialog.py +++ b/selfdrive/ui/mici/onroad/driver_camera_dialog.py @@ -35,7 +35,6 @@ class DriverCameraDialog(NavWidget): if not no_escape: # TODO: this can grow unbounded, should be given some thought device.add_interactive_timeout_callback(gui_app.pop_widget) - self.set_back_callback(gui_app.pop_widget) self.set_back_enabled(not no_escape) # Load eye icons diff --git a/selfdrive/ui/mici/widgets/dialog.py b/selfdrive/ui/mici/widgets/dialog.py index 8e978066c6..dbd3ae0d75 100644 --- a/selfdrive/ui/mici/widgets/dialog.py +++ b/selfdrive/ui/mici/widgets/dialog.py @@ -22,7 +22,6 @@ class BigDialogBase(NavWidget, abc.ABC): def __init__(self): super().__init__() self.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) - self.set_back_callback(gui_app.pop_widget) class BigDialog(BigDialogBase): diff --git a/selfdrive/ui/mici/widgets/pairing_dialog.py b/selfdrive/ui/mici/widgets/pairing_dialog.py index 64b2c9a063..7476a3b659 100644 --- a/selfdrive/ui/mici/widgets/pairing_dialog.py +++ b/selfdrive/ui/mici/widgets/pairing_dialog.py @@ -19,7 +19,6 @@ class PairingDialog(NavWidget): def __init__(self): super().__init__() - self.set_back_callback(gui_app.pop_widget) self._params = Params() self._qr_texture: rl.Texture | None = None self._last_qr_generation = float("-inf") diff --git a/system/ui/widgets/nav_widget.py b/system/ui/widgets/nav_widget.py index 02afc911b2..fdbea275af 100644 --- a/system/ui/widgets/nav_widget.py +++ b/system/ui/widgets/nav_widget.py @@ -55,7 +55,6 @@ class NavWidget(Widget, abc.ABC): def __init__(self): super().__init__() - self._back_callback: Callable[[], None] | None = None self._back_button_start_pos: MousePos | None = None self._swiping_away = False # currently swiping away self._can_swipe_away = True # swipe away is blocked after certain horizontal movement @@ -76,9 +75,6 @@ class NavWidget(Widget, abc.ABC): def set_back_enabled(self, enabled: bool | Callable[[], bool]) -> None: self._back_enabled = enabled - def set_back_callback(self, callback: Callable[[], None]) -> None: - self._back_callback = callback - def _handle_mouse_event(self, mouse_event: MouseEvent) -> None: # FIXME: disabling this widget on new push_widget still causes this widget to track mouse events without mouse down super()._handle_mouse_event(mouse_event) @@ -167,8 +163,7 @@ class NavWidget(Widget, abc.ABC): new_y = self._pos_filter.x = 0.0 if new_y > self._rect.height + DISMISS_PUSH_OFFSET - 10: - if self._back_callback is not None: - self._back_callback() + gui_app.pop_widget() self._playing_dismiss_animation = False self._back_button_start_pos = None From 8f328f17fcb99efd4e779a6080e3e1ad522e4c66 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Sat, 28 Feb 2026 02:44:27 -0800 Subject: [PATCH 270/311] NavWidget: rm useless state variable --- system/ui/widgets/nav_widget.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/system/ui/widgets/nav_widget.py b/system/ui/widgets/nav_widget.py index fdbea275af..edbaf7a98b 100644 --- a/system/ui/widgets/nav_widget.py +++ b/system/ui/widgets/nav_widget.py @@ -55,9 +55,8 @@ class NavWidget(Widget, abc.ABC): def __init__(self): super().__init__() - self._back_button_start_pos: MousePos | None = None + self._back_button_start_pos: MousePos | None = None # cleared after certain amount of horizontal movement self._swiping_away = False # currently swiping away - self._can_swipe_away = True # swipe away is blocked after certain horizontal movement self._pos_filter = BounceFilter(0.0, 0.1, 1 / gui_app.target_fps, bounce=1) self._playing_dismiss_animation = False @@ -82,7 +81,6 @@ class NavWidget(Widget, abc.ABC): if not self.back_enabled: self._back_button_start_pos = None self._swiping_away = False - self._can_swipe_away = True return if mouse_event.left_pressed: @@ -103,7 +101,6 @@ class NavWidget(Widget, abc.ABC): # 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 elif mouse_event.left_down: @@ -111,14 +108,14 @@ class NavWidget(Widget, abc.ABC): # block swiping away if too much horizontal or upward movement horizontal_movement = abs(mouse_event.pos.x - self._back_button_start_pos.x) > BLOCK_SWIPE_AWAY_THRESHOLD upward_movement = mouse_event.pos.y - self._back_button_start_pos.y < -BLOCK_SWIPE_AWAY_THRESHOLD - if not self._swiping_away and (horizontal_movement or upward_movement): - self._can_swipe_away = False - self._back_button_start_pos = None - # block horizontal swiping if now swiping away - if self._can_swipe_away: + if not (horizontal_movement or upward_movement): + # no blocking movement, check if we should start dismissing if mouse_event.pos.y - self._back_button_start_pos.y > START_DISMISSING_THRESHOLD: self._swiping_away = True + else: + if not self._swiping_away: + self._back_button_start_pos = None elif mouse_event.left_released: self._pos_filter.update_alpha(0.1) From b5855bcadee95c5fb2805b767388887cf7c699d0 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Sat, 28 Feb 2026 02:53:27 -0800 Subject: [PATCH 271/311] NavWidget: clean up names (#37481) * better names * better names * fix * order * rm! --- selfdrive/ui/mici/layouts/settings/device.py | 2 +- .../ui/mici/layouts/settings/firehose.py | 2 +- selfdrive/ui/mici/widgets/dialog.py | 4 +- system/ui/widgets/nav_widget.py | 74 +++++++++---------- system/ui/widgets/scroller.py | 2 +- 5 files changed, 42 insertions(+), 42 deletions(-) diff --git a/selfdrive/ui/mici/layouts/settings/device.py b/selfdrive/ui/mici/layouts/settings/device.py index d754b9f980..d7313cb5a9 100644 --- a/selfdrive/ui/mici/layouts/settings/device.py +++ b/selfdrive/ui/mici/layouts/settings/device.py @@ -31,7 +31,7 @@ class MiciFccModal(NavWidget): super().__init__() self._content = HtmlRenderer(file_path=file_path, text=text) self._scroll_panel = GuiScrollPanel2(horizontal=False) - self._scroll_panel.set_enabled(lambda: self.enabled and not self._swiping_away) + self._scroll_panel.set_enabled(lambda: self.enabled and not self._dragging_down) self._fcc_logo = gui_app.texture("icons_mici/settings/device/fcc_logo.png", 76, 64) def _render(self, rect: rl.Rectangle): diff --git a/selfdrive/ui/mici/layouts/settings/firehose.py b/selfdrive/ui/mici/layouts/settings/firehose.py index d16a04019f..9ad43a6a46 100644 --- a/selfdrive/ui/mici/layouts/settings/firehose.py +++ b/selfdrive/ui/mici/layouts/settings/firehose.py @@ -223,4 +223,4 @@ class FirehoseLayout(FirehoseLayoutBase, NavWidget): def __init__(self): super().__init__() - self._scroll_panel.set_enabled(lambda: self.enabled and not self._swiping_away) + self._scroll_panel.set_enabled(lambda: self.enabled and not self._dragging_down) diff --git a/selfdrive/ui/mici/widgets/dialog.py b/selfdrive/ui/mici/widgets/dialog.py index dbd3ae0d75..9ccc37e253 100644 --- a/selfdrive/ui/mici/widgets/dialog.py +++ b/selfdrive/ui/mici/widgets/dialog.py @@ -77,7 +77,7 @@ class BigConfirmationDialogV2(BigDialogBase): self._slider = RedBigSlider(title, icon_txt, confirm_callback=self._on_confirm) else: self._slider = BigSlider(title, icon_txt, confirm_callback=self._on_confirm) - self._slider.set_enabled(lambda: self.enabled and not self._swiping_away) # self.enabled for nav stack + self._slider.set_enabled(lambda: self.enabled and not self._dragging_down) # self.enabled for nav stack def _on_confirm(self): if self._exit_on_confirm: @@ -87,7 +87,7 @@ class BigConfirmationDialogV2(BigDialogBase): def _update_state(self): super()._update_state() - if self._swiping_away and not self._slider.confirmed: + if self._dragging_down and not self._slider.confirmed: self._slider.reset() def _render(self, _): diff --git a/system/ui/widgets/nav_widget.py b/system/ui/widgets/nav_widget.py index edbaf7a98b..6daf723ba3 100644 --- a/system/ui/widgets/nav_widget.py +++ b/system/ui/widgets/nav_widget.py @@ -55,16 +55,16 @@ class NavWidget(Widget, abc.ABC): def __init__(self): super().__init__() - self._back_button_start_pos: MousePos | None = None # cleared after certain amount of horizontal movement - self._swiping_away = False # currently swiping away + self._drag_start_pos: MousePos | None = None # cleared after certain amount of horizontal movement + self._dragging_down = False # swiped down enough to trigger dismissing on release + self._playing_dismiss_animation = False # released and animating away + self._y_pos_filter = BounceFilter(0.0, 0.1, 1 / gui_app.target_fps, bounce=1) - self._pos_filter = BounceFilter(0.0, 0.1, 1 / gui_app.target_fps, bounce=1) - self._playing_dismiss_animation = False self._trigger_animate_in = False - self._nav_bar_show_time = 0.0 self._back_enabled: bool | Callable[[], bool] = True - self._nav_bar = NavBar() + self._nav_bar = NavBar() + self._nav_bar_show_time = 0.0 self._nav_bar_y_filter = FirstOrderFilter(0.0, 0.1, 1 / gui_app.target_fps) @property @@ -79,13 +79,13 @@ class NavWidget(Widget, abc.ABC): super()._handle_mouse_event(mouse_event) if not self.back_enabled: - self._back_button_start_pos = None - self._swiping_away = False + self._drag_start_pos = None + self._dragging_down = False return if mouse_event.left_pressed: # user is able to swipe away if starting near top of screen, or anywhere if scroller is at top - self._pos_filter.update_alpha(0.04) + self._y_pos_filter.update_alpha(0.04) in_dismiss_area = mouse_event.pos.y < self._rect.height * self.BACK_TOUCH_AREA_PERCENTAGE # TODO: remove vertical scrolling and then this hacky logic to check if scroller is at top @@ -101,37 +101,37 @@ class NavWidget(Widget, abc.ABC): # 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._back_button_start_pos = mouse_event.pos + self._drag_start_pos = mouse_event.pos elif mouse_event.left_down: - if self._back_button_start_pos is not None: + if self._drag_start_pos is not None: # block swiping away if too much horizontal or upward movement - horizontal_movement = abs(mouse_event.pos.x - self._back_button_start_pos.x) > BLOCK_SWIPE_AWAY_THRESHOLD - upward_movement = mouse_event.pos.y - self._back_button_start_pos.y < -BLOCK_SWIPE_AWAY_THRESHOLD + horizontal_movement = abs(mouse_event.pos.x - self._drag_start_pos.x) > BLOCK_SWIPE_AWAY_THRESHOLD + upward_movement = mouse_event.pos.y - self._drag_start_pos.y < -BLOCK_SWIPE_AWAY_THRESHOLD if not (horizontal_movement or upward_movement): # no blocking movement, check if we should start dismissing - if mouse_event.pos.y - self._back_button_start_pos.y > START_DISMISSING_THRESHOLD: - self._swiping_away = True + if mouse_event.pos.y - self._drag_start_pos.y > START_DISMISSING_THRESHOLD: + self._dragging_down = True else: - if not self._swiping_away: - self._back_button_start_pos = None + if not self._dragging_down: + self._drag_start_pos = None elif mouse_event.left_released: - self._pos_filter.update_alpha(0.1) + self._y_pos_filter.update_alpha(0.1) # if far enough, trigger back navigation callback - if self._back_button_start_pos is not None: - if mouse_event.pos.y - self._back_button_start_pos.y > SWIPE_AWAY_THRESHOLD: + if self._drag_start_pos is not None: + if mouse_event.pos.y - self._drag_start_pos.y > SWIPE_AWAY_THRESHOLD: self._playing_dismiss_animation = True - self._back_button_start_pos = None - self._swiping_away = False + self._drag_start_pos = None + self._dragging_down = False def _update_state(self): super()._update_state() if self._trigger_animate_in: - self._pos_filter.x = self._rect.height + self._y_pos_filter.x = self._rect.height self._nav_bar_y_filter.x = -NAV_BAR_MARGIN - NAV_BAR_HEIGHT self._nav_bar_show_time = rl.get_time() self._trigger_animate_in = False @@ -139,32 +139,32 @@ class NavWidget(Widget, abc.ABC): new_y = 0.0 if not self.enabled: - self._back_button_start_pos = None + self._drag_start_pos = None # TODO: why is this not in handle_mouse_event? have to hack above - if self._back_button_start_pos is not None: + if self._drag_start_pos is not None: last_mouse_event = gui_app.last_mouse_event # push entire widget as user drags it away - new_y = max(last_mouse_event.pos.y - self._back_button_start_pos.y, 0) + new_y = max(last_mouse_event.pos.y - self._drag_start_pos.y, 0) if new_y < SWIPE_AWAY_THRESHOLD: new_y /= 2 # resistance until mouse release would dismiss widget - if self._swiping_away: + if self._dragging_down: self._nav_bar.set_alpha(1.0) if self._playing_dismiss_animation: new_y = self._rect.height + DISMISS_PUSH_OFFSET - new_y = round(self._pos_filter.update(new_y)) - if abs(new_y) < 1 and self._pos_filter.velocity.x == 0.0: - new_y = self._pos_filter.x = 0.0 + new_y = round(self._y_pos_filter.update(new_y)) + if abs(new_y) < 1 and self._y_pos_filter.velocity.x == 0.0: + new_y = self._y_pos_filter.x = 0.0 if new_y > self._rect.height + DISMISS_PUSH_OFFSET - 10: gui_app.pop_widget() self._playing_dismiss_animation = False - self._back_button_start_pos = None - self._swiping_away = False + self._drag_start_pos = None + self._dragging_down = False self.set_position(self._rect.x, new_y) @@ -183,8 +183,8 @@ class NavWidget(Widget, abc.ABC): bar_x = self._rect.x + (self._rect.width - self._nav_bar.rect.width) / 2 nav_bar_delayed = rl.get_time() - self._nav_bar_show_time < 0.4 # User dragging or dismissing, nav bar follows NavWidget - if self._back_button_start_pos is not None or self._playing_dismiss_animation: - self._nav_bar_y_filter.x = NAV_BAR_MARGIN + self._pos_filter.x + if self._drag_start_pos is not None or self._playing_dismiss_animation: + self._nav_bar_y_filter.x = NAV_BAR_MARGIN + self._y_pos_filter.x # Waiting to show elif nav_bar_delayed: self._nav_bar_y_filter.x = -NAV_BAR_MARGIN - NAV_BAR_HEIGHT @@ -205,6 +205,6 @@ class NavWidget(Widget, abc.ABC): self._nav_bar.show_event() # Reset state - self._pos_filter.update_alpha(0.1) - self._back_button_start_pos = None - self._swiping_away = False + self._y_pos_filter.update_alpha(0.1) + self._drag_start_pos = None + self._dragging_down = False diff --git a/system/ui/widgets/scroller.py b/system/ui/widgets/scroller.py index 9d9f5663b8..ca6492ae18 100644 --- a/system/ui/widgets/scroller.py +++ b/system/ui/widgets/scroller.py @@ -443,4 +443,4 @@ class NavScroller(NavWidget, Scroller): def __init__(self, **kwargs): super().__init__(**kwargs) # pass down enabled to child widget for nav stack + disable while swiping away NavWidget - self._scroller.set_enabled(lambda: self.enabled and not self._swiping_away) + self._scroller.set_enabled(lambda: self.enabled and not self._dragging_down) From 256ee6cf6fb26662af58b637a2a77cb5307bde7d Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Sat, 28 Feb 2026 03:03:49 -0800 Subject: [PATCH 272/311] rm hacky trigger --- system/ui/widgets/nav_widget.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/system/ui/widgets/nav_widget.py b/system/ui/widgets/nav_widget.py index 6daf723ba3..55eea04fe1 100644 --- a/system/ui/widgets/nav_widget.py +++ b/system/ui/widgets/nav_widget.py @@ -60,7 +60,6 @@ class NavWidget(Widget, abc.ABC): self._playing_dismiss_animation = False # released and animating away self._y_pos_filter = BounceFilter(0.0, 0.1, 1 / gui_app.target_fps, bounce=1) - self._trigger_animate_in = False self._back_enabled: bool | Callable[[], bool] = True self._nav_bar = NavBar() @@ -130,12 +129,6 @@ class NavWidget(Widget, abc.ABC): def _update_state(self): super()._update_state() - if self._trigger_animate_in: - self._y_pos_filter.x = self._rect.height - self._nav_bar_y_filter.x = -NAV_BAR_MARGIN - NAV_BAR_HEIGHT - self._nav_bar_show_time = rl.get_time() - self._trigger_animate_in = False - new_y = 0.0 if not self.enabled: @@ -199,12 +192,14 @@ class NavWidget(Widget, abc.ABC): def show_event(self): super().show_event() - # FIXME: we don't know the height of the rect at first show_event since it's before the first render :( - # so we need this hacky bool for now - self._trigger_animate_in = True self._nav_bar.show_event() # Reset state - self._y_pos_filter.update_alpha(0.1) self._drag_start_pos = None self._dragging_down = False + # Start NavWidget off-screen, no matter how tall it is + self._y_pos_filter.update_alpha(0.1) + self._y_pos_filter.x = gui_app.height + + self._nav_bar_y_filter.x = -NAV_BAR_MARGIN - NAV_BAR_HEIGHT + self._nav_bar_show_time = rl.get_time() From 940c5b3b3ff5ba3f03b458e73bb38eda489ea5d3 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Sat, 28 Feb 2026 03:17:57 -0800 Subject: [PATCH 273/311] NavWidget: remove back enabled (#37482) * free navwidget! * clean up * clean up --- selfdrive/ui/mici/layouts/onboarding.py | 12 +++--- .../ui/mici/onroad/driver_camera_dialog.py | 17 +++++--- system/ui/widgets/nav_widget.py | 42 ++++++------------- 3 files changed, 29 insertions(+), 42 deletions(-) diff --git a/selfdrive/ui/mici/layouts/onboarding.py b/selfdrive/ui/mici/layouts/onboarding.py index b7fafd894a..7b619f31ec 100644 --- a/selfdrive/ui/mici/layouts/onboarding.py +++ b/selfdrive/ui/mici/layouts/onboarding.py @@ -8,14 +8,13 @@ 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.nav_widget import NavWidget 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 from openpilot.selfdrive.ui.ui_state import ui_state, device from openpilot.selfdrive.ui.mici.onroad.driver_state import DriverStateRenderer -from openpilot.selfdrive.ui.mici.onroad.driver_camera_dialog import DriverCameraDialog +from openpilot.selfdrive.ui.mici.onroad.driver_camera_dialog import BaseDriverCameraDialog from openpilot.system.ui.widgets.label import gui_label from openpilot.system.ui.lib.multilang import tr from openpilot.system.version import terms_version, training_version @@ -27,9 +26,9 @@ class OnboardingState(IntEnum): DECLINE = 2 -class DriverCameraSetupDialog(DriverCameraDialog): +class DriverCameraSetupDialog(BaseDriverCameraDialog): def __init__(self): - super().__init__(no_escape=True) + super().__init__() 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() @@ -438,11 +437,9 @@ class TermsPage(SetupTermsPage): )) -class OnboardingWindow(NavWidget): +class OnboardingWindow(Widget): def __init__(self): super().__init__() - self.set_back_enabled(False) - self._accepted_terms: bool = ui_state.params.get("HasAcceptedTerms") == terms_version self._training_done: bool = ui_state.params.get("CompletedTrainingVersion") == training_version @@ -486,6 +483,7 @@ class OnboardingWindow(NavWidget): self.close() def _render(self, _): + rl.draw_rectangle_rec(self._rect, rl.BLACK) if self._state == OnboardingState.TERMS: self._terms.render(self._rect) elif self._state == OnboardingState.ONBOARDING: diff --git a/selfdrive/ui/mici/onroad/driver_camera_dialog.py b/selfdrive/ui/mici/onroad/driver_camera_dialog.py index dfa8beeb77..e8321b099c 100644 --- a/selfdrive/ui/mici/onroad/driver_camera_dialog.py +++ b/selfdrive/ui/mici/onroad/driver_camera_dialog.py @@ -7,6 +7,7 @@ from openpilot.selfdrive.ui.ui_state import ui_state, device from openpilot.selfdrive.selfdrived.events import EVENTS, ET from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.multilang import tr +from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.nav_widget import NavWidget from openpilot.system.ui.widgets.label import gui_label @@ -24,18 +25,15 @@ class DriverCameraView(CameraView): return base -class DriverCameraDialog(NavWidget): - def __init__(self, no_escape=False): +class BaseDriverCameraDialog(Widget): + # Not a NavWidget so training guide can use this without back navigation + def __init__(self): super().__init__() self._camera_view = DriverCameraView("camerad", VisionStreamType.VISION_STREAM_DRIVER) self.driver_state_renderer = DriverStateRenderer(lines=True) self.driver_state_renderer.set_rect(rl.Rectangle(0, 0, 200, 200)) self.driver_state_renderer.load_icons() self._pm: messaging.PubMaster | None = None - if not no_escape: - # TODO: this can grow unbounded, should be given some thought - device.add_interactive_timeout_callback(gui_app.pop_widget) - self.set_back_enabled(not no_escape) # Load eye icons self._eye_fill_texture = None @@ -230,6 +228,13 @@ class DriverCameraDialog(NavWidget): rl.draw_texture_v(self._glasses_texture, glasses_pos, rl.Color(70, 80, 161, int(255 * glasses_prob))) +class DriverCameraDialog(NavWidget, BaseDriverCameraDialog): + def __init__(self): + super().__init__() + # TODO: this can grow unbounded, should be given some thought + device.add_interactive_timeout_callback(gui_app.pop_widget) + + if __name__ == "__main__": gui_app.init_window("Driver Camera View (mici)") diff --git a/system/ui/widgets/nav_widget.py b/system/ui/widgets/nav_widget.py index 55eea04fe1..d7d3942157 100644 --- a/system/ui/widgets/nav_widget.py +++ b/system/ui/widgets/nav_widget.py @@ -2,7 +2,6 @@ from __future__ import annotations import abc import pyray as rl -from collections.abc import Callable from openpilot.system.ui.widgets import Widget from openpilot.common.filter_simple import BounceFilter, FirstOrderFilter from openpilot.system.ui.lib.application import gui_app, MousePos, MouseEvent @@ -60,28 +59,14 @@ class NavWidget(Widget, abc.ABC): self._playing_dismiss_animation = False # released and animating away self._y_pos_filter = BounceFilter(0.0, 0.1, 1 / gui_app.target_fps, bounce=1) - self._back_enabled: bool | Callable[[], bool] = True - self._nav_bar = NavBar() self._nav_bar_show_time = 0.0 self._nav_bar_y_filter = FirstOrderFilter(0.0, 0.1, 1 / gui_app.target_fps) - @property - def back_enabled(self) -> bool: - return self._back_enabled() if callable(self._back_enabled) else self._back_enabled - - def set_back_enabled(self, enabled: bool | Callable[[], bool]) -> None: - self._back_enabled = enabled - def _handle_mouse_event(self, mouse_event: MouseEvent) -> None: # FIXME: disabling this widget on new push_widget still causes this widget to track mouse events without mouse down super()._handle_mouse_event(mouse_event) - if not self.back_enabled: - self._drag_start_pos = None - self._dragging_down = False - return - if mouse_event.left_pressed: # user is able to swipe away if starting near top of screen, or anywhere if scroller is at top self._y_pos_filter.update_alpha(0.04) @@ -172,21 +157,20 @@ class NavWidget(Widget, abc.ABC): def render(self, rect: rl.Rectangle | None = None) -> bool | int | None: ret = super().render(rect) - if self.back_enabled: - bar_x = self._rect.x + (self._rect.width - self._nav_bar.rect.width) / 2 - nav_bar_delayed = rl.get_time() - self._nav_bar_show_time < 0.4 - # User dragging or dismissing, nav bar follows NavWidget - if self._drag_start_pos is not None or self._playing_dismiss_animation: - self._nav_bar_y_filter.x = NAV_BAR_MARGIN + self._y_pos_filter.x - # Waiting to show - elif nav_bar_delayed: - self._nav_bar_y_filter.x = -NAV_BAR_MARGIN - NAV_BAR_HEIGHT - # Animate back to top - else: - self._nav_bar_y_filter.update(NAV_BAR_MARGIN) + bar_x = self._rect.x + (self._rect.width - self._nav_bar.rect.width) / 2 + nav_bar_delayed = rl.get_time() - self._nav_bar_show_time < 0.4 + # User dragging or dismissing, nav bar follows NavWidget + if self._drag_start_pos is not None or self._playing_dismiss_animation: + self._nav_bar_y_filter.x = NAV_BAR_MARGIN + self._y_pos_filter.x + # Waiting to show + elif nav_bar_delayed: + self._nav_bar_y_filter.x = -NAV_BAR_MARGIN - NAV_BAR_HEIGHT + # Animate back to top + else: + self._nav_bar_y_filter.update(NAV_BAR_MARGIN) - self._nav_bar.set_position(bar_x, round(self._nav_bar_y_filter.x)) - self._nav_bar.render() + self._nav_bar.set_position(bar_x, round(self._nav_bar_y_filter.x)) + self._nav_bar.render() return ret From d016071df366eba47a74d4a0761494ed97469997 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Sat, 28 Feb 2026 03:26:18 -0800 Subject: [PATCH 274/311] NavWidget: clean up scroller access (#37480) * clean up * more * great clean ups * better name * remove useless _can_swipe_away * reorder * rename * state machine is nice but might be too much * Revert "state machine is nice but might be too much" This reverts commit f8952969243a2eac3ed5f84793ba7b0c0cdf24bf. * got a better name out of it though * clean up * clean up * rm! * rm * and this * and * clean up --- selfdrive/ui/mici/layouts/settings/device.py | 10 +---- .../ui/mici/layouts/settings/firehose.py | 10 ++--- system/ui/widgets/nav_widget.py | 42 +++++++++---------- system/ui/widgets/scroller.py | 23 ++++++++++ 4 files changed, 48 insertions(+), 37 deletions(-) diff --git a/selfdrive/ui/mici/layouts/settings/device.py b/selfdrive/ui/mici/layouts/settings/device.py index d7313cb5a9..b6f6f71d69 100644 --- a/selfdrive/ui/mici/layouts/settings/device.py +++ b/selfdrive/ui/mici/layouts/settings/device.py @@ -7,8 +7,7 @@ from collections.abc import Callable from openpilot.common.basedir import BASEDIR from openpilot.common.params import Params from openpilot.common.time_helpers import system_time_valid -from openpilot.system.ui.widgets.scroller import NavScroller -from openpilot.system.ui.lib.scroll_panel2 import GuiScrollPanel2 +from openpilot.system.ui.widgets.scroller import NavRawScrollPanel, NavScroller from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigCircleButton from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigConfirmationDialogV2 from openpilot.selfdrive.ui.mici.widgets.pairing_dialog import PairingDialog @@ -17,21 +16,16 @@ from openpilot.selfdrive.ui.mici.layouts.onboarding import TrainingGuide from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.widgets import Widget -from openpilot.system.ui.widgets.nav_widget import NavWidget from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.system.ui.widgets.label import MiciLabel from openpilot.system.ui.widgets.html_render import HtmlModal, HtmlRenderer from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID -class MiciFccModal(NavWidget): - BACK_TOUCH_AREA_PERCENTAGE = 0.1 - +class MiciFccModal(NavRawScrollPanel): def __init__(self, file_path: str | None = None, text: str | None = None): super().__init__() self._content = HtmlRenderer(file_path=file_path, text=text) - self._scroll_panel = GuiScrollPanel2(horizontal=False) - self._scroll_panel.set_enabled(lambda: self.enabled and not self._dragging_down) self._fcc_logo = gui_app.texture("icons_mici/settings/device/fcc_logo.png", 76, 64) def _render(self, rect: rl.Rectangle): diff --git a/selfdrive/ui/mici/layouts/settings/firehose.py b/selfdrive/ui/mici/layouts/settings/firehose.py index 9ad43a6a46..e5b6301acf 100644 --- a/selfdrive/ui/mici/layouts/settings/firehose.py +++ b/selfdrive/ui/mici/layouts/settings/firehose.py @@ -14,7 +14,7 @@ from openpilot.system.ui.lib.wrap_text import wrap_text from openpilot.system.ui.lib.scroll_panel2 import GuiScrollPanel2 from openpilot.system.ui.lib.multilang import tr, trn, tr_noop from openpilot.system.ui.widgets import Widget -from openpilot.system.ui.widgets.nav_widget import NavWidget +from openpilot.system.ui.widgets.scroller import NavRawScrollPanel TITLE = tr_noop("Firehose Mode") DESCRIPTION = tr_noop( @@ -218,9 +218,5 @@ class FirehoseLayoutBase(Widget): time.sleep(self.UPDATE_INTERVAL) -class FirehoseLayout(FirehoseLayoutBase, NavWidget): - BACK_TOUCH_AREA_PERCENTAGE = 0.1 - - def __init__(self): - super().__init__() - self._scroll_panel.set_enabled(lambda: self.enabled and not self._dragging_down) +class FirehoseLayout(NavRawScrollPanel, FirehoseLayoutBase): + pass diff --git a/system/ui/widgets/nav_widget.py b/system/ui/widgets/nav_widget.py index d7d3942157..acbec5eb62 100644 --- a/system/ui/widgets/nav_widget.py +++ b/system/ui/widgets/nav_widget.py @@ -14,11 +14,12 @@ NAV_BAR_MARGIN = 6 NAV_BAR_WIDTH = 205 NAV_BAR_HEIGHT = 8 -DISMISS_PUSH_OFFSET = 50 + NAV_BAR_MARGIN + NAV_BAR_HEIGHT # px extra to push down when dismissing -DISMISS_TIME_SECONDS = 2.0 +DISMISS_PUSH_OFFSET = NAV_BAR_MARGIN + NAV_BAR_HEIGHT + 50 # px extra to push down when dismissing class NavBar(Widget): + FADE_AFTER_SECONDS = 2.0 + def __init__(self): super().__init__() self.set_rect(rl.Rectangle(0, 0, NAV_BAR_WIDTH, NAV_BAR_HEIGHT)) @@ -37,7 +38,7 @@ class NavBar(Widget): self._fade_time = rl.get_time() def _render(self, _): - if rl.get_time() - self._fade_time > DISMISS_TIME_SECONDS: + if rl.get_time() - self._fade_time > self.FADE_AFTER_SECONDS: self._alpha = 0.0 alpha = self._alpha_filter.update(self._alpha) @@ -54,42 +55,37 @@ class NavWidget(Widget, abc.ABC): def __init__(self): super().__init__() + # State self._drag_start_pos: MousePos | None = None # cleared after certain amount of horizontal movement self._dragging_down = False # swiped down enough to trigger dismissing on release self._playing_dismiss_animation = False # released and animating away self._y_pos_filter = BounceFilter(0.0, 0.1, 1 / gui_app.target_fps, bounce=1) + # TODO: move this state into NavBar self._nav_bar = NavBar() self._nav_bar_show_time = 0.0 self._nav_bar_y_filter = FirstOrderFilter(0.0, 0.1, 1 / gui_app.target_fps) + def _back_enabled(self) -> bool: + # Children can override this to block swipe away, like when not at + # the top of a vertical scroll panel to prevent erroneous swipes + return True + def _handle_mouse_event(self, mouse_event: MouseEvent) -> None: - # FIXME: disabling this widget on new push_widget still causes this widget to track mouse events without mouse down super()._handle_mouse_event(mouse_event) if mouse_event.left_pressed: - # user is able to swipe away if starting near top of screen, or anywhere if scroller is at top + # user is able to swipe away if starting near top of screen self._y_pos_filter.update_alpha(0.04) in_dismiss_area = mouse_event.pos.y < self._rect.height * self.BACK_TOUCH_AREA_PERCENTAGE - # TODO: remove vertical scrolling and then this hacky logic to check if scroller is at top - 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 - - # 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: + if in_dismiss_area and self._back_enabled(): self._drag_start_pos = mouse_event.pos elif mouse_event.left_down: if self._drag_start_pos is not None: # block swiping away if too much horizontal or upward movement + # block (lock-in) threshold is higher than start dismissing horizontal_movement = abs(mouse_event.pos.x - self._drag_start_pos.x) > BLOCK_SWIPE_AWAY_THRESHOLD upward_movement = mouse_event.pos.y - self._drag_start_pos.y < -BLOCK_SWIPE_AWAY_THRESHOLD @@ -102,7 +98,9 @@ class NavWidget(Widget, abc.ABC): self._drag_start_pos = None elif mouse_event.left_released: + # reset rc for either slide up or down animation self._y_pos_filter.update_alpha(0.1) + # if far enough, trigger back navigation callback if self._drag_start_pos is not None: if mouse_event.pos.y - self._drag_start_pos.y > SWIPE_AWAY_THRESHOLD: @@ -116,10 +114,13 @@ class NavWidget(Widget, abc.ABC): new_y = 0.0 + if self._dragging_down: + self._nav_bar.set_alpha(1.0) + + # FIXME: disabling this widget on new push_widget still causes this widget to track mouse events without mouse down if not self.enabled: self._drag_start_pos = None - # TODO: why is this not in handle_mouse_event? have to hack above if self._drag_start_pos is not None: last_mouse_event = gui_app.last_mouse_event # push entire widget as user drags it away @@ -127,9 +128,6 @@ class NavWidget(Widget, abc.ABC): if new_y < SWIPE_AWAY_THRESHOLD: new_y /= 2 # resistance until mouse release would dismiss widget - if self._dragging_down: - self._nav_bar.set_alpha(1.0) - if self._playing_dismiss_animation: new_y = self._rect.height + DISMISS_PUSH_OFFSET diff --git a/system/ui/widgets/scroller.py b/system/ui/widgets/scroller.py index ca6492ae18..fb47f690b2 100644 --- a/system/ui/widgets/scroller.py +++ b/system/ui/widgets/scroller.py @@ -444,3 +444,26 @@ class NavScroller(NavWidget, Scroller): super().__init__(**kwargs) # pass down enabled to child widget for nav stack + disable while swiping away NavWidget self._scroller.set_enabled(lambda: self.enabled and not self._dragging_down) + + def _back_enabled(self) -> bool: + # Vertical scrollers need to be at the top to swipe away to prevent erroneous swipes + # TODO: only used for offroad alerts, remove when horizontal + return self._scroller._horizontal or self._scroller.scroll_panel.get_offset() >= -20 # some tolerance + + +# TODO: only used for a few vertical scrollers, remove when horizontal +class NavRawScrollPanel(NavWidget): + # can swipe anywhere, only when at top + BACK_TOUCH_AREA_PERCENTAGE = 1.0 + + def __init__(self): + super().__init__() + self._scroll_panel = GuiScrollPanel2(horizontal=False) + self._scroll_panel.set_enabled(lambda: self.enabled and not self._dragging_down) + + def show_event(self): + super().show_event() + self._scroll_panel.set_offset(0) + + def _back_enabled(self) -> bool: + return self._scroll_panel.get_offset() >= -20 From 87c495b7610cd1212017264dc88cb813ba6a6cac Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Sat, 28 Feb 2026 06:16:03 -0800 Subject: [PATCH 275/311] Update test_widget_leaks.py --- selfdrive/ui/mici/tests/test_widget_leaks.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/selfdrive/ui/mici/tests/test_widget_leaks.py b/selfdrive/ui/mici/tests/test_widget_leaks.py index 7441ed5a22..be12839cd7 100755 --- a/selfdrive/ui/mici/tests/test_widget_leaks.py +++ b/selfdrive/ui/mici/tests/test_widget_leaks.py @@ -38,10 +38,10 @@ KNOWN_LEAKS = { "openpilot.system.ui.widgets.label.Label", "openpilot.system.ui.widgets.button.Button", "openpilot.system.ui.widgets.html_render.HtmlRenderer", - "openpilot.system.ui.widgets.NavBar", + "openpilot.system.ui.widgets.nav_widget.NavBar", + "openpilot.selfdrive.ui.mici.layouts.settings.device.MiciFccModal", "openpilot.system.ui.widgets.inputbox.InputBox", "openpilot.system.ui.widgets.scroller_tici.Scroller", - "openpilot.system.ui.widgets.scroller.Scroller", "openpilot.system.ui.widgets.label.UnifiedLabel", "openpilot.system.ui.widgets.mici_keyboard.MiciKeyboard", "openpilot.selfdrive.ui.mici.widgets.dialog.BigConfirmationDialogV2", From 2af7b3441e2fac013b0c25fcdc6957e6a8135006 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Sat, 28 Feb 2026 06:19:43 -0800 Subject: [PATCH 276/311] Nav stack: clean up (#37484) guards --- selfdrive/ui/mici/widgets/dialog.py | 14 +++++++++++--- system/ui/widgets/mici_keyboard.py | 2 +- system/ui/widgets/nav_widget.py | 5 +++++ system/ui/widgets/scroller.py | 4 ++-- 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/selfdrive/ui/mici/widgets/dialog.py b/selfdrive/ui/mici/widgets/dialog.py index 9ccc37e253..6cfb6455bf 100644 --- a/selfdrive/ui/mici/widgets/dialog.py +++ b/selfdrive/ui/mici/widgets/dialog.py @@ -77,7 +77,7 @@ class BigConfirmationDialogV2(BigDialogBase): self._slider = RedBigSlider(title, icon_txt, confirm_callback=self._on_confirm) else: self._slider = BigSlider(title, icon_txt, confirm_callback=self._on_confirm) - self._slider.set_enabled(lambda: self.enabled and not self._dragging_down) # self.enabled for nav stack + self._slider.set_enabled(lambda: self.enabled and not self.is_dismissing) # for nav stack + NavWidget def _on_confirm(self): if self._exit_on_confirm: @@ -87,7 +87,7 @@ class BigConfirmationDialogV2(BigDialogBase): def _update_state(self): super()._update_state() - if self._dragging_down and not self._slider.confirmed: + if self.is_dismissing and not self._slider.confirmed: self._slider.reset() def _render(self, _): @@ -109,7 +109,7 @@ class BigInputDialog(BigDialogBase): font_weight=FontWeight.MEDIUM) self._keyboard = MiciKeyboard() self._keyboard.set_text(default_text) - self._keyboard.set_enabled(lambda: self.enabled) # for nav stack + self._keyboard.set_enabled(lambda: self.enabled and not self.is_dismissing) # for nav stack + NavWidget self._minimum_length = minimum_length self._backspace_held_time: float | None = None @@ -135,6 +135,10 @@ class BigInputDialog(BigDialogBase): def _update_state(self): super()._update_state() + if self.is_dismissing: + self._backspace_held_time = None + return + last_mouse_event = gui_app.last_mouse_event if last_mouse_event.left_down and rl.check_collision_point_rec(last_mouse_event.pos, self._top_right_button_rect) and self._backspace_img_alpha.x > 1: if self._backspace_held_time is None: @@ -229,6 +233,10 @@ class BigInputDialog(BigDialogBase): super()._handle_mouse_press(mouse_pos) # TODO: need to track where press was so enter and back can activate on release rather than press # or turn into icon widgets :eyes_open: + + if self.is_dismissing: + return + # handle backspace icon click if rl.check_collision_point_rec(mouse_pos, self._top_right_button_rect) and self._backspace_img_alpha.x > 254: self._keyboard.backspace() diff --git a/system/ui/widgets/mici_keyboard.py b/system/ui/widgets/mici_keyboard.py index 59a2451387..18384fd905 100644 --- a/system/ui/widgets/mici_keyboard.py +++ b/system/ui/widgets/mici_keyboard.py @@ -322,7 +322,7 @@ class MiciKeyboard(Widget): self._selected_key_filter.update(self._closest_key[0] is not None) # unselect key after animation plays - if self._unselect_key_t is not None and rl.get_time() > self._unselect_key_t: + if (self._unselect_key_t is not None and rl.get_time() > self._unselect_key_t) or not self.enabled: self._closest_key = (None, float('inf')) self._unselect_key_t = None self._selected_key_t = None diff --git a/system/ui/widgets/nav_widget.py b/system/ui/widgets/nav_widget.py index acbec5eb62..fb680a0b5a 100644 --- a/system/ui/widgets/nav_widget.py +++ b/system/ui/widgets/nav_widget.py @@ -172,6 +172,10 @@ class NavWidget(Widget, abc.ABC): return ret + @property + def is_dismissing(self) -> bool: + return self._dragging_down or self._playing_dismiss_animation + def show_event(self): super().show_event() self._nav_bar.show_event() @@ -179,6 +183,7 @@ class NavWidget(Widget, abc.ABC): # Reset state self._drag_start_pos = None self._dragging_down = False + self._playing_dismiss_animation = False # Start NavWidget off-screen, no matter how tall it is self._y_pos_filter.update_alpha(0.1) self._y_pos_filter.x = gui_app.height diff --git a/system/ui/widgets/scroller.py b/system/ui/widgets/scroller.py index fb47f690b2..c48be6b80b 100644 --- a/system/ui/widgets/scroller.py +++ b/system/ui/widgets/scroller.py @@ -443,7 +443,7 @@ class NavScroller(NavWidget, Scroller): def __init__(self, **kwargs): super().__init__(**kwargs) # pass down enabled to child widget for nav stack + disable while swiping away NavWidget - self._scroller.set_enabled(lambda: self.enabled and not self._dragging_down) + self._scroller.set_enabled(lambda: self.enabled and not self.is_dismissing) def _back_enabled(self) -> bool: # Vertical scrollers need to be at the top to swipe away to prevent erroneous swipes @@ -459,7 +459,7 @@ class NavRawScrollPanel(NavWidget): def __init__(self): super().__init__() self._scroll_panel = GuiScrollPanel2(horizontal=False) - self._scroll_panel.set_enabled(lambda: self.enabled and not self._dragging_down) + self._scroll_panel.set_enabled(lambda: self.enabled and not self.is_dismissing) def show_event(self): super().show_event() From b15390d351b059511b8f972e7a98565fc648ed84 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Sat, 28 Feb 2026 06:33:51 -0800 Subject: [PATCH 277/311] mici ui: add interaction timeout fixme + fix navwidget guard --- selfdrive/ui/mici/layouts/main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/selfdrive/ui/mici/layouts/main.py b/selfdrive/ui/mici/layouts/main.py index 1a0c6bc094..bd9167e828 100644 --- a/selfdrive/ui/mici/layouts/main.py +++ b/selfdrive/ui/mici/layouts/main.py @@ -92,6 +92,7 @@ class MiciMainLayout(Scroller): else: self._scroll_to(self._home_layout) + # FIXME: these two pops can interrupt user interacting in the settings if self._onroad_time_delay is not None and rl.get_time() - self._onroad_time_delay >= ONROAD_DELAY: gui_app.pop_widgets_to(self) self._scroll_to(self._onroad_layout) From e244aabe88cbfff302befdc823dfd72535615e80 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Sat, 28 Feb 2026 06:38:47 -0800 Subject: [PATCH 278/311] mici ui: fix navwidget guard --- system/ui/widgets/nav_widget.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/system/ui/widgets/nav_widget.py b/system/ui/widgets/nav_widget.py index fb680a0b5a..3ac4c903ba 100644 --- a/system/ui/widgets/nav_widget.py +++ b/system/ui/widgets/nav_widget.py @@ -74,6 +74,10 @@ class NavWidget(Widget, abc.ABC): def _handle_mouse_event(self, mouse_event: MouseEvent) -> None: super()._handle_mouse_event(mouse_event) + # Don't let touch events change filter state during dismiss animation + if self._playing_dismiss_animation: + return + if mouse_event.left_pressed: # user is able to swipe away if starting near top of screen self._y_pos_filter.update_alpha(0.04) From 9cc0d7a1c96ea27dcc2247a66c0f3e930c075aaf Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Sat, 28 Feb 2026 07:16:06 -0800 Subject: [PATCH 279/311] NavWidget: support programmatic dismiss (#37486) * add dismiss support * add to widget * use in dialogs * good catch * fix!! * fix!! * it works eitehr way * frick * good catch --- selfdrive/ui/mici/widgets/dialog.py | 8 +++----- selfdrive/ui/mici/widgets/pairing_dialog.py | 4 ++-- system/ui/widgets/__init__.py | 6 ++++++ system/ui/widgets/nav_widget.py | 15 +++++++++++++++ 4 files changed, 26 insertions(+), 7 deletions(-) diff --git a/selfdrive/ui/mici/widgets/dialog.py b/selfdrive/ui/mici/widgets/dialog.py index 6cfb6455bf..619c1ca28f 100644 --- a/selfdrive/ui/mici/widgets/dialog.py +++ b/selfdrive/ui/mici/widgets/dialog.py @@ -81,8 +81,8 @@ class BigConfirmationDialogV2(BigDialogBase): def _on_confirm(self): if self._exit_on_confirm: - gui_app.pop_widget() - if self._confirm_callback: + self.dismiss(self._confirm_callback) + elif self._confirm_callback: self._confirm_callback() def _update_state(self): @@ -127,9 +127,7 @@ class BigInputDialog(BigDialogBase): def confirm_callback_wrapper(): text = self._keyboard.text() - gui_app.pop_widget() - if confirm_callback: - confirm_callback(text) + self.dismiss((lambda: confirm_callback(text)) if confirm_callback else None) self._confirm_callback = confirm_callback_wrapper def _update_state(self): diff --git a/selfdrive/ui/mici/widgets/pairing_dialog.py b/selfdrive/ui/mici/widgets/pairing_dialog.py index 7476a3b659..991cb05a8c 100644 --- a/selfdrive/ui/mici/widgets/pairing_dialog.py +++ b/selfdrive/ui/mici/widgets/pairing_dialog.py @@ -68,8 +68,8 @@ class PairingDialog(NavWidget): def _update_state(self): super()._update_state() - if ui_state.prime_state.is_paired(): - self._playing_dismiss_animation = True + if ui_state.prime_state.is_paired() and not self.is_dismissing: + self.dismiss() def _render(self, rect: rl.Rectangle): self._check_qr_refresh() diff --git a/system/ui/widgets/__init__.py b/system/ui/widgets/__init__.py index cc8d72959f..1e2f783811 100644 --- a/system/ui/widgets/__init__.py +++ b/system/ui/widgets/__init__.py @@ -195,3 +195,9 @@ class Widget(abc.ABC): def hide_event(self): """Optionally handle hide event. Parent must manually call this""" + + def dismiss(self, callback: Callable[[], None] | None = None): + """Immediately dismiss the widget, firing the callback after.""" + gui_app.pop_widget() + if callback: + callback() diff --git a/system/ui/widgets/nav_widget.py b/system/ui/widgets/nav_widget.py index 3ac4c903ba..fe17f12a8f 100644 --- a/system/ui/widgets/nav_widget.py +++ b/system/ui/widgets/nav_widget.py @@ -2,6 +2,7 @@ from __future__ import annotations import abc import pyray as rl +from collections.abc import Callable from openpilot.system.ui.widgets import Widget from openpilot.common.filter_simple import BounceFilter, FirstOrderFilter from openpilot.system.ui.lib.application import gui_app, MousePos, MouseEvent @@ -15,6 +16,7 @@ NAV_BAR_WIDTH = 205 NAV_BAR_HEIGHT = 8 DISMISS_PUSH_OFFSET = NAV_BAR_MARGIN + NAV_BAR_HEIGHT + 50 # px extra to push down when dismissing +DISMISS_ANIMATION_RC = 0.2 # slightly slower for non-user triggered dismiss animation class NavBar(Widget): @@ -61,6 +63,8 @@ class NavWidget(Widget, abc.ABC): self._playing_dismiss_animation = False # released and animating away self._y_pos_filter = BounceFilter(0.0, 0.1, 1 / gui_app.target_fps, bounce=1) + self._dismiss_callback: Callable[[], None] | None = None + # TODO: move this state into NavBar self._nav_bar = NavBar() self._nav_bar_show_time = 0.0 @@ -141,6 +145,9 @@ class NavWidget(Widget, abc.ABC): if new_y > self._rect.height + DISMISS_PUSH_OFFSET - 10: gui_app.pop_widget() + if self._dismiss_callback is not None: + self._dismiss_callback() + self._dismiss_callback = None self._playing_dismiss_animation = False self._drag_start_pos = None @@ -180,6 +187,13 @@ class NavWidget(Widget, abc.ABC): def is_dismissing(self) -> bool: return self._dragging_down or self._playing_dismiss_animation + def dismiss(self, callback: Callable[[], None] | None = None): + """Programmatically trigger the dismiss animation. Calls pop_widget when done, then callback.""" + if not self._playing_dismiss_animation: + self._playing_dismiss_animation = True + self._y_pos_filter.update_alpha(DISMISS_ANIMATION_RC) + self._dismiss_callback = callback + def show_event(self): super().show_event() self._nav_bar.show_event() @@ -188,6 +202,7 @@ class NavWidget(Widget, abc.ABC): self._drag_start_pos = None self._dragging_down = False self._playing_dismiss_animation = False + self._dismiss_callback = None # Start NavWidget off-screen, no matter how tall it is self._y_pos_filter.update_alpha(0.1) self._y_pos_filter.x = gui_app.height From 6cbef7bc13d393ee71c6aa06f5506deef67b1925 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Sat, 28 Feb 2026 08:00:07 -0800 Subject: [PATCH 280/311] ui: widgets animate out v2 (#37483) * i like this better * clean up * debug * fix able to click navwidgets that are closing (tested at rc 10) * add dismiss guards * fix keyboard so it unselects * pairing: use dismiss * main todo * rm pop_widgets_to! * reset dismiss state on show event * debug pop animation bugs * Revert "debug pop animation bugs" This reverts commit 9239f2e12cf79b1f75d15d39262fdd15ff5a5200. * revert * cmt * type * clean up * now do the todo * treat using widgets, not idxs, as a separate clean up for later * actually if not navwidget this is buggy * fix * short * simpler --- selfdrive/ui/mici/layouts/main.py | 12 ++++------ system/ui/lib/application.py | 38 +++++++++++++++++++++++-------- 2 files changed, 34 insertions(+), 16 deletions(-) diff --git a/selfdrive/ui/mici/layouts/main.py b/selfdrive/ui/mici/layouts/main.py index bd9167e828..2c3fea0d32 100644 --- a/selfdrive/ui/mici/layouts/main.py +++ b/selfdrive/ui/mici/layouts/main.py @@ -94,15 +94,13 @@ class MiciMainLayout(Scroller): # FIXME: these two pops can interrupt user interacting in the settings if self._onroad_time_delay is not None and rl.get_time() - self._onroad_time_delay >= ONROAD_DELAY: - gui_app.pop_widgets_to(self) - self._scroll_to(self._onroad_layout) + gui_app.pop_widgets_to(self, lambda: self._scroll_to(self._onroad_layout)) self._onroad_time_delay = None # When car leaves standstill, pop nav stack and scroll to onroad CS = ui_state.sm["carState"] if not CS.standstill and self._prev_standstill: - gui_app.pop_widgets_to(self) - self._scroll_to(self._onroad_layout) + gui_app.pop_widgets_to(self, lambda: self._scroll_to(self._onroad_layout)) self._prev_standstill = CS.standstill def _on_interactive_timeout(self): @@ -113,10 +111,10 @@ class MiciMainLayout(Scroller): if ui_state.started: # Don't pop if at standstill if not ui_state.sm["carState"].standstill: - gui_app.pop_widgets_to(self) - self._scroll_to(self._onroad_layout) + gui_app.pop_widgets_to(self, lambda: self._scroll_to(self._onroad_layout)) else: - gui_app.pop_widgets_to(self) + # Screen turns off on timeout offroad, so pop immediately without animation + gui_app.pop_widgets_to(self, instant=True) self._scroll_to(self._home_layout) def _on_bookmark_clicked(self): diff --git a/system/ui/lib/application.py b/system/ui/lib/application.py index 34aed4f6a6..98e05e1127 100644 --- a/system/ui/lib/application.py +++ b/system/ui/lib/application.py @@ -383,27 +383,47 @@ class GuiApplication: self._nav_stack.append(widget) widget.show_event() + widget.set_enabled(True) - def pop_widget(self): + def pop_widget(self, idx: int | None = None): + # Pops widget instantly without animation if len(self._nav_stack) < 2: cloudlog.warning("At least one widget should remain on the stack, ignoring pop!") return - # re-enable previous widget and pop current - # TODO: switch to touch_valid - prev_widget = self._nav_stack[-2] - prev_widget.set_enabled(True) + idx_to_pop = len(self._nav_stack) - 1 if idx is None else idx + if idx_to_pop <= 0 or idx_to_pop >= len(self._nav_stack): + cloudlog.warning(f"Invalid index {idx_to_pop} to pop, ignoring!") + return - widget = self._nav_stack.pop() + # only re-enable previous widget if popping top widget + if idx_to_pop == len(self._nav_stack) - 1: + prev_widget = self._nav_stack[idx_to_pop - 1] + prev_widget.set_enabled(True) + + widget = self._nav_stack.pop(idx_to_pop) widget.hide_event() - def pop_widgets_to(self, widget): + def pop_widgets_to(self, widget: object, callback: Callable[[], None] | None = None, instant: bool = False): + # Pops middle widgets instantly without animation then dismisses top, animated out if NavWidget if widget not in self._nav_stack: cloudlog.warning("Widget not in stack, cannot pop to it!") return - # pops all widgets after specified widget - while len(self._nav_stack) > 0 and self._nav_stack[-1] != widget: + # Nothing to pop, ensure we still run callback + top_widget = self._nav_stack[-1] + if top_widget == widget: + if callback: + callback() + return + + # instantly pop widgets in between, then dismiss top widget for animation + while len(self._nav_stack) > 1 and self._nav_stack[-2] != widget: + self.pop_widget(len(self._nav_stack) - 2) + + if not instant: + top_widget.dismiss(callback) + else: self.pop_widget() def get_active_widget(self): From 870430e19fb686ee09f8bfad79e7d354e7fc435b Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Sat, 28 Feb 2026 08:11:25 -0800 Subject: [PATCH 281/311] Revert "Actions cleanup" (#37463) Revert "Actions cleanup (#37307)" This reverts commit f41d77b24fb897af23e25d00f709a2ae36352e4d. --- .github/workflows/tests.yaml | 8 +++++--- pyproject.toml | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 00fdceda0b..8add04c465 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -19,6 +19,8 @@ concurrency: env: CI: 1 + PYTHONPATH: ${{ github.workspace }} + PYTEST: pytest --continue-on-collection-errors --durations=0 -n logical jobs: build_release: @@ -111,8 +113,8 @@ jobs: run: | source selfdrive/test/setup_xvfb.sh # Pre-compile Python bytecode so each pytest worker doesn't need to - pytest --collect-only -m 'not slow' -qq - MAX_EXAMPLES=1 pytest -m 'not slow' + $PYTEST --collect-only -m 'not slow' -qq + MAX_EXAMPLES=1 $PYTEST -m 'not slow' process_replay: name: process replay @@ -168,7 +170,7 @@ jobs: timeout-minutes: 4 env: ONNXCPU: 1 - run: pytest selfdrive/test/process_replay/test_regen.py + run: $PYTEST selfdrive/test/process_replay/test_regen.py simulator_driving: name: simulator driving diff --git a/pyproject.toml b/pyproject.toml index 699c3af6f0..c3467342b6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -125,7 +125,7 @@ allow-direct-references = true [tool.pytest.ini_options] minversion = "6.0" -addopts = "--ignore=openpilot/ --ignore=opendbc/ --ignore=panda/ --ignore=rednose_repo/ --ignore=tinygrad_repo/ --ignore=teleoprtc_repo/ --ignore=msgq/ -Werror --strict-config --strict-markers --durations=20 --maxprocesses=8 -n auto --dist=loadgroup" +addopts = "--ignore=openpilot/ --ignore=opendbc/ --ignore=panda/ --ignore=rednose_repo/ --ignore=tinygrad_repo/ --ignore=teleoprtc_repo/ --ignore=msgq/ -Werror --strict-config --strict-markers --durations=10 -n auto --dist=loadgroup" cpp_files = "test_*" cpp_harness = "selfdrive/test/cpp_harness.py" python_files = "test_*.py" From a27efe5796e31f77e97efb33762522f2ddc3b0d0 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sat, 28 Feb 2026 10:39:13 -0800 Subject: [PATCH 282/311] setup: add retry for transient network fails on uv install (#37490) --- tools/setup_dependencies.sh | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/tools/setup_dependencies.sh b/tools/setup_dependencies.sh index 7c0bf7d708..a3b2f68bfd 100755 --- a/tools/setup_dependencies.sh +++ b/tools/setup_dependencies.sh @@ -4,6 +4,21 @@ set -e DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" ROOT="$(cd "$DIR/../" && pwd)" +function retry() { + local attempts=$1 + shift + for i in $(seq 1 "$attempts"); do + if "$@"; then + return 0 + fi + if [ "$i" -lt "$attempts" ]; then + echo " Attempt $i/$attempts failed, retrying in 5s..." + sleep 5 + fi + done + return 1 +} + function install_ubuntu_deps() { SUDO="" @@ -83,7 +98,8 @@ function install_python_deps() { if ! command -v "uv" > /dev/null 2>&1; then echo "installing uv..." - curl -LsSf --retry 5 --retry-delay 5 --retry-all-errors https://astral.sh/uv/install.sh | sh + # TODO: outer retry can be removed once https://github.com/axodotdev/cargo-dist/pull/2311 is merged + retry 3 sh -c 'curl --retry 5 --retry-delay 5 --retry-all-errors -LsSf https://astral.sh/uv/install.sh | UV_GITHUB_TOKEN="${GITHUB_TOKEN:-}" sh' UV_BIN="$HOME/.local/bin" PATH="$UV_BIN:$PATH" fi From fd1937c6d49e73328b503d4bc5d43d805cb216f4 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sat, 28 Feb 2026 15:23:04 -0500 Subject: [PATCH 283/311] ui: Speed Limit Assist `preActive` improvements (#1743) * mici init * obv * hybrid * too soon junior * make them all flash the same pls * abstract * shorter * also too soon junior * not so fast --- selfdrive/ui/sunnypilot/onroad/speed_limit.py | 61 +++++++++++-------- .../selfdrive/assets/img_minus_arrow_down.png | 4 +- .../selfdrive/assets/img_plus_arrow_up.png | 4 +- sunnypilot/selfdrive/selfdrived/events.py | 18 +++--- 4 files changed, 51 insertions(+), 36 deletions(-) diff --git a/selfdrive/ui/sunnypilot/onroad/speed_limit.py b/selfdrive/ui/sunnypilot/onroad/speed_limit.py index 39a9848d37..962c96e17a 100644 --- a/selfdrive/ui/sunnypilot/onroad/speed_limit.py +++ b/selfdrive/ui/sunnypilot/onroad/speed_limit.py @@ -6,7 +6,6 @@ See the LICENSE.md file in the root directory for more details. """ from dataclasses import dataclass -import math import pyray as rl from cereal import custom @@ -41,9 +40,32 @@ class Colors: MUTCD_LINES = rl.Color(255, 255, 255, 100) -class SpeedLimitRenderer(Widget): +class SpeedLimitAlertRenderer: def __init__(self): - super().__init__() + arrow_size = 200 + self.arrow_up = gui_app.texture("../../sunnypilot/selfdrive/assets/img_plus_arrow_up.png", arrow_size, arrow_size) + self.arrow_down = gui_app.texture("../../sunnypilot/selfdrive/assets/img_minus_arrow_down.png", arrow_size, arrow_size) + + self._pre_active_alpha_filter = FirstOrderFilter(1.0, 0.05, 1 / gui_app.target_fps) + self._pre_active_alert_frame = 0 + + def update(self): + assist_state = ui_state.sm['longitudinalPlanSP'].speedLimit.assist.state + if assist_state == AssistState.preActive: + self._pre_active_alert_frame += 1 + if (self._pre_active_alert_frame % gui_app.target_fps) < (gui_app.target_fps * 0.75): + self._pre_active_alpha_filter.x = 1.0 + else: + self._pre_active_alpha_filter.update(0.0) + else: + self._pre_active_alert_frame = 0 + self._pre_active_alpha_filter.update(1.0) + + +class SpeedLimitRenderer(Widget, SpeedLimitAlertRenderer): + def __init__(self): + Widget.__init__(self) + SpeedLimitAlertRenderer.__init__(self) self.speed_limit = 0.0 self.speed_limit_last = 0.0 @@ -60,7 +82,6 @@ class SpeedLimitRenderer(Widget): self.speed_limit_ahead_valid = False self.speed_limit_ahead_frame = 0 - self.assist_frame = 0 self.is_cruise_set: bool = False self.is_cruise_available: bool = True self.set_speed: float = SET_SPEED_NA @@ -70,17 +91,13 @@ class SpeedLimitRenderer(Widget): self.font_bold = gui_app.font(FontWeight.BOLD) self.font_demi = gui_app.font(FontWeight.SEMI_BOLD) self.font_norm = gui_app.font(FontWeight.NORMAL) - self._sign_alpha_filter = FirstOrderFilter(1.0, 0.5, 1 / gui_app.target_fps) - - arrow_size = 90 - self._arrow_up = gui_app.texture("../../sunnypilot/selfdrive/assets/img_plus_arrow_up.png", arrow_size, arrow_size) - self._arrow_down = gui_app.texture("../../sunnypilot/selfdrive/assets/img_minus_arrow_down.png", arrow_size, arrow_size) @property def speed_conv(self): return CV.MS_TO_KPH if ui_state.is_metric else CV.MS_TO_MPH def update(self): + SpeedLimitAlertRenderer.update(self) sm = ui_state.sm if sm.recv_frame["carState"] < ui_state.started_frame: self.set_speed = SET_SPEED_NA @@ -143,13 +160,7 @@ class SpeedLimitRenderer(Widget): sign_rect = rl.Rectangle(x, y, width, UI_CONFIG.set_speed_height + 6 * 2) - if self.speed_limit_assist_state == AssistState.preActive: - self.assist_frame += 1 - pulse_value = 0.65 + 0.35 * math.sin(self.assist_frame * math.pi / gui_app.target_fps) - alpha = self._sign_alpha_filter.update(pulse_value) - else: - self.assist_frame = 0 - alpha = self._sign_alpha_filter.update(1.0) + alpha = self._pre_active_alpha_filter.x if ui_state.speed_limit_mode != SpeedLimitMode.off: self._draw_sign_main(sign_rect, alpha) @@ -184,19 +195,19 @@ class SpeedLimitRenderer(Widget): set_speed_rounded = round(self.set_speed) limit_rounded = round(self.speed_limit_final_last) - bounce_frequency = 2.0 * math.pi / (gui_app.target_fps * 2.5) - bounce_offset = int(20 * math.sin(self.assist_frame * bounce_frequency)) - sign_margin = 12 arrow_spacing = int(sign_margin * 1.4) arrow_x = sign_rect.x + sign_rect.width + arrow_spacing - if set_speed_rounded < limit_rounded: - arrow_y = sign_rect.y + (sign_rect.height - self._arrow_up.height) / 2 + bounce_offset - rl.draw_texture(self._arrow_up, int(arrow_x), int(arrow_y), rl.WHITE) - elif set_speed_rounded > limit_rounded: - arrow_y = sign_rect.y + (sign_rect.height - self._arrow_down.height) / 2 - bounce_offset - rl.draw_texture(self._arrow_down, int(arrow_x), int(arrow_y), rl.WHITE) + icon_alpha = max(0.0, min(self._pre_active_alpha_filter.x * 255.0, 255.0)) + if icon_alpha > 0: + color = rl.Color(255, 255, 255, int(icon_alpha)) + if set_speed_rounded < limit_rounded: + arrow_y = sign_rect.y + (sign_rect.height - self.arrow_up.height) / 2 + rl.draw_texture(self.arrow_up, int(arrow_x), int(arrow_y), color) + elif set_speed_rounded > limit_rounded: + arrow_y = sign_rect.y + (sign_rect.height - self.arrow_down.height) / 2 + rl.draw_texture(self.arrow_down, int(arrow_x), int(arrow_y), color) def _render_vienna(self, rect, val, sub, color, has_limit, alpha=1.0): center = rl.Vector2(rect.x + rect.width / 2, rect.y + rect.height / 2) diff --git a/sunnypilot/selfdrive/assets/img_minus_arrow_down.png b/sunnypilot/selfdrive/assets/img_minus_arrow_down.png index 250f640585..2dc99789a5 100644 --- a/sunnypilot/selfdrive/assets/img_minus_arrow_down.png +++ b/sunnypilot/selfdrive/assets/img_minus_arrow_down.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0f4a0bf26cfd2c64939759d43bee839aadfd017a6f74065095a27b03615e55a4 -size 26627 +oid sha256:ad42ecaeff96a0a6c1a6db67b01e3c0452566906dd3a7f0336a1010ae854d27b +size 60924 diff --git a/sunnypilot/selfdrive/assets/img_plus_arrow_up.png b/sunnypilot/selfdrive/assets/img_plus_arrow_up.png index 14c1529da3..4247827340 100644 --- a/sunnypilot/selfdrive/assets/img_plus_arrow_up.png +++ b/sunnypilot/selfdrive/assets/img_plus_arrow_up.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3e451ff31e9b3f144822ab282b8e47cd5a603ca59ff94bd1849271181b86c6b1 -size 29220 +oid sha256:1fdea39873f60a0c4b216b75792e826f3633091a72ea2abd05740bd6e4e72089 +size 63058 diff --git a/sunnypilot/selfdrive/selfdrived/events.py b/sunnypilot/selfdrive/selfdrived/events.py index 4edc0bd470..d7507ab2e3 100644 --- a/sunnypilot/selfdrive/selfdrived/events.py +++ b/sunnypilot/selfdrive/selfdrived/events.py @@ -1,3 +1,9 @@ +""" +Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + +This file is part of sunnypilot and is licensed under the MIT License. +See the LICENSE.md file in the root directory for more details. +""" import cereal.messaging as messaging from cereal import log, car, custom from openpilot.common.constants import CV @@ -34,7 +40,6 @@ def speed_limit_pre_active_alert(CP: car.CarParams, CS: car.CarState, sm: messag speed_limit_final_last = sm['longitudinalPlanSP'].speedLimit.resolver.speedLimitFinalLast speed_limit_final_last_conv = round(speed_limit_final_last * speed_conv) alert_1_str = "" - alert_2_str = "" alert_size = AlertSize.none if CP.openpilotLongitudinalControl and CP.pcmCruise: @@ -44,13 +49,12 @@ def speed_limit_pre_active_alert(CP: car.CarParams, CS: car.CarState, sm: messag pcm_long_required_max_set_speed_conv = round(pcm_long_required_max * speed_conv) speed_unit = "km/h" if metric else "mph" - alert_1_str = "Speed Limit Assist: Activation Required" - alert_2_str = f"Manually change set speed to {pcm_long_required_max_set_speed_conv} {speed_unit} to activate" - alert_size = AlertSize.mid + alert_1_str = f"Speed Limit Assist: set to {pcm_long_required_max_set_speed_conv} {speed_unit} to engage" + alert_size = AlertSize.small return Alert( alert_1_str, - alert_2_str, + "", AlertStatus.normal, alert_size, Priority.LOW, VisualAlert.none, AudibleAlertSP.promptSingleLow, .1) @@ -193,7 +197,7 @@ EVENTS_SP: dict[int, dict[str, Alert | AlertCallbackType]] = { EventNameSP.speedLimitActive: { ET.WARNING: Alert( - "Automatically adjusting to the posted speed limit", + "Auto adjusting to speed limit", "", AlertStatus.normal, AlertSize.small, Priority.LOW, VisualAlert.none, AudibleAlertSP.promptSingleHigh, 5.), @@ -213,7 +217,7 @@ EVENTS_SP: dict[int, dict[str, Alert | AlertCallbackType]] = { EventNameSP.speedLimitPending: { ET.WARNING: Alert( - "Automatically adjusting to the last speed limit", + "Auto adjusting to last speed limit", "", AlertStatus.normal, AlertSize.small, Priority.LOW, VisualAlert.none, AudibleAlertSP.promptSingleHigh, 5.), From ca5234a32f9b0331b11ed33f418a82669c5ac8e1 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sat, 28 Feb 2026 16:44:00 -0800 Subject: [PATCH 284/311] tools/setup: remove vestigial mac .env file --- tools/setup_dependencies.sh | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tools/setup_dependencies.sh b/tools/setup_dependencies.sh index a3b2f68bfd..8132cd16dc 100755 --- a/tools/setup_dependencies.sh +++ b/tools/setup_dependencies.sh @@ -111,11 +111,6 @@ function install_python_deps() { echo "installing python packages..." uv sync --frozen --all-extras source .venv/bin/activate - - if [[ "$(uname)" == 'Darwin' ]]; then - touch "$ROOT"/.env - echo "export OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES" >> "$ROOT"/.env - fi } # --- Main --- From 5a0c06434609b762b4d84da594ff567f0910df07 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sat, 28 Feb 2026 20:06:38 -0500 Subject: [PATCH 285/311] ui: consolidate Speed Limit Assist `preActive` status rendering (#1745) * mici init * obv * hybrid * adapt * less * consolidate * oops Refactor speed limit alert function to use car state directly. * no event border for tizi/tici * abstract it * too soon junior * refactor --- selfdrive/ui/sunnypilot/onroad/speed_limit.py | 65 ++++++++++++++----- 1 file changed, 47 insertions(+), 18 deletions(-) diff --git a/selfdrive/ui/sunnypilot/onroad/speed_limit.py b/selfdrive/ui/sunnypilot/onroad/speed_limit.py index 962c96e17a..02df292cd6 100644 --- a/selfdrive/ui/sunnypilot/onroad/speed_limit.py +++ b/selfdrive/ui/sunnypilot/onroad/speed_limit.py @@ -6,6 +6,7 @@ See the LICENSE.md file in the root directory for more details. """ from dataclasses import dataclass +from enum import StrEnum import pyray as rl from cereal import custom @@ -14,6 +15,7 @@ from openpilot.common.filter_simple import FirstOrderFilter from openpilot.selfdrive.ui.onroad.hud_renderer import UI_CONFIG from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.common import Mode as SpeedLimitMode +from openpilot.system.hardware import HARDWARE from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.lib.text_measure import measure_text_cached @@ -40,11 +42,21 @@ class Colors: MUTCD_LINES = rl.Color(255, 255, 255, 100) +class IconSide(StrEnum): + left = 'left' + right = 'right' + + class SpeedLimitAlertRenderer: + ARROW_SIZE = 90 if HARDWARE.get_device_type() == 'mici' else 200 + def __init__(self): - arrow_size = 200 - self.arrow_up = gui_app.texture("../../sunnypilot/selfdrive/assets/img_plus_arrow_up.png", arrow_size, arrow_size) - self.arrow_down = gui_app.texture("../../sunnypilot/selfdrive/assets/img_minus_arrow_down.png", arrow_size, arrow_size) + self.arrow_up = gui_app.texture("../../sunnypilot/selfdrive/assets/img_plus_arrow_up.png", self.ARROW_SIZE, self.ARROW_SIZE) + self.arrow_down = gui_app.texture("../../sunnypilot/selfdrive/assets/img_minus_arrow_down.png", self.ARROW_SIZE, self.ARROW_SIZE) + + blank_image = rl.gen_image_color(self.ARROW_SIZE, self.ARROW_SIZE, rl.Color(0, 0, 0, 0)) + self.arrow_blank = rl.load_texture_from_image(blank_image) + rl.unload_image(blank_image) self._pre_active_alpha_filter = FirstOrderFilter(1.0, 0.05, 1 / gui_app.target_fps) self._pre_active_alert_frame = 0 @@ -61,6 +73,31 @@ class SpeedLimitAlertRenderer: self._pre_active_alert_frame = 0 self._pre_active_alpha_filter.update(1.0) + def speed_limit_pre_active_icon_helper(self): + icon_alpha = max(0.0, min(self._pre_active_alpha_filter.x * 255.0, 255.0)) + txt_icon = self.arrow_blank + icon_margin_x = 10 + icon_margin_y = 18 + + if icon_alpha > 0: + speed_conv = CV.MS_TO_KPH if ui_state.is_metric else CV.MS_TO_MPH + speed_limit_final_last = ui_state.sm['longitudinalPlanSP'].speedLimit.resolver.speedLimitFinalLast + + v_cruise_cluster = ui_state.sm['carState'].vCruiseCluster + set_speed = ui_state.sm['controlsState'].vCruiseDEPRECATED if v_cruise_cluster == 0.0 else v_cruise_cluster + if not ui_state.is_metric: + set_speed *= KM_TO_MILE + + set_speed_round = round(set_speed) + speed_limit_round = round(speed_limit_final_last * speed_conv) + + if set_speed_round < speed_limit_round: + txt_icon = self.arrow_up + elif set_speed_round > speed_limit_round: + txt_icon = self.arrow_down + + return IconSide.right, txt_icon, icon_alpha, icon_margin_x, icon_margin_y + class SpeedLimitRenderer(Widget, SpeedLimitAlertRenderer): def __init__(self): @@ -192,22 +229,14 @@ class SpeedLimitRenderer(Widget, SpeedLimitAlertRenderer): self._render_mutcd(rect, limit_str, sub_text, txt_color, has_limit, alpha) def _draw_pre_active_arrow(self, sign_rect): - set_speed_rounded = round(self.set_speed) - limit_rounded = round(self.speed_limit_final_last) - - sign_margin = 12 - arrow_spacing = int(sign_margin * 1.4) - arrow_x = sign_rect.x + sign_rect.width + arrow_spacing - - icon_alpha = max(0.0, min(self._pre_active_alpha_filter.x * 255.0, 255.0)) - if icon_alpha > 0: + _, txt_icon, icon_alpha, _, _ = SpeedLimitAlertRenderer.speed_limit_pre_active_icon_helper(self) + if icon_alpha > 0 and txt_icon != self.arrow_blank: + sign_margin = 12 + arrow_spacing = int(sign_margin * 1.4) + arrow_x = sign_rect.x + sign_rect.width + arrow_spacing + arrow_y = sign_rect.y + (sign_rect.height - txt_icon.height) / 2 color = rl.Color(255, 255, 255, int(icon_alpha)) - if set_speed_rounded < limit_rounded: - arrow_y = sign_rect.y + (sign_rect.height - self.arrow_up.height) / 2 - rl.draw_texture(self.arrow_up, int(arrow_x), int(arrow_y), color) - elif set_speed_rounded > limit_rounded: - arrow_y = sign_rect.y + (sign_rect.height - self.arrow_down.height) / 2 - rl.draw_texture(self.arrow_down, int(arrow_x), int(arrow_y), color) + rl.draw_texture(txt_icon, int(arrow_x), int(arrow_y), color) def _render_vienna(self, rect, val, sub, color, has_limit, alpha=1.0): center = rl.Vector2(rect.x + rect.width / 2, rect.y + rect.height / 2) From 60ae57a3edbb0918f9448d321d099ba52c83f6a7 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sat, 28 Feb 2026 20:19:40 -0500 Subject: [PATCH 286/311] [MICI] ui: Speed Limit Assist `preActive` status (#1742) * mici init * obv * hybrid * adapt * less * consolidate * oops Refactor speed limit alert function to use car state directly. * no event border for tizi/tici * abstract it * less * nah --- selfdrive/ui/mici/onroad/alert_renderer.py | 19 +++++++++++++++---- sunnypilot/selfdrive/selfdrived/events.py | 19 ++++++++++++++++--- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/selfdrive/ui/mici/onroad/alert_renderer.py b/selfdrive/ui/mici/onroad/alert_renderer.py index 344c955c3e..68b2aef7a5 100644 --- a/selfdrive/ui/mici/onroad/alert_renderer.py +++ b/selfdrive/ui/mici/onroad/alert_renderer.py @@ -13,6 +13,8 @@ from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.label import UnifiedLabel +from openpilot.selfdrive.ui.sunnypilot.onroad.speed_limit import SpeedLimitAlertRenderer + AlertSize = log.SelfdriveState.AlertSize AlertStatus = log.SelfdriveState.AlertStatus @@ -46,6 +48,7 @@ class IconLayout(NamedTuple): side: IconSide margin_x: int margin_y: int + alpha: float = 255.0 class AlertLayout(NamedTuple): @@ -86,9 +89,10 @@ ALERT_CRITICAL_REBOOT = Alert( ) -class AlertRenderer(Widget): +class AlertRenderer(Widget, SpeedLimitAlertRenderer): def __init__(self): - super().__init__() + Widget.__init__(self) + SpeedLimitAlertRenderer.__init__(self) self._alert_text1_label = UnifiedLabel(text="", font_size=ALERT_FONT_BIG, font_weight=FontWeight.DISPLAY, line_height=0.86, letter_spacing=-0.02) @@ -155,6 +159,7 @@ class AlertRenderer(Widget): def _icon_helper(self, alert: Alert) -> AlertLayout: icon_side = None txt_icon = None + icon_alpha = 255.0 icon_margin_x = 20 icon_margin_y = 18 @@ -191,6 +196,9 @@ class AlertRenderer(Widget): icon_margin_x = 8 icon_margin_y = 0 + elif event_name == 'speedLimitPreActive': + icon_side, txt_icon, icon_alpha, icon_margin_x, icon_margin_y = SpeedLimitAlertRenderer.speed_limit_pre_active_icon_helper(self) + else: self._turn_signal_timer = 0.0 @@ -212,7 +220,7 @@ class AlertRenderer(Widget): text_width, self._rect.height, ) - icon_layout = IconLayout(txt_icon, icon_side, icon_margin_x, icon_margin_y) if txt_icon is not None and icon_side is not None else None + icon_layout = IconLayout(txt_icon, icon_side, icon_margin_x, icon_margin_y, icon_alpha) if txt_icon is not None and icon_side is not None else None return AlertLayout(text_rect, icon_layout) def _render(self, rect: rl.Rectangle) -> bool: @@ -235,6 +243,9 @@ class AlertRenderer(Widget): self._draw_background(alert) + # update speed limit UI states + SpeedLimitAlertRenderer.update(self) + alert_layout = self._icon_helper(alert) self._draw_text(alert, alert_layout) self._draw_icons(alert_layout) @@ -257,7 +268,7 @@ class AlertRenderer(Widget): pos_x = int(self._rect.x + self._rect.width - alert_layout.icon.margin_x - alert_layout.icon.texture.width) if alert_layout.icon.texture not in (self._txt_turn_signal_left, self._txt_turn_signal_right): - icon_alpha = 255 + icon_alpha = alert_layout.icon.alpha else: icon_alpha = int(min(self._turn_signal_alpha_filter.x, 255)) diff --git a/sunnypilot/selfdrive/selfdrived/events.py b/sunnypilot/selfdrive/selfdrived/events.py index d7507ab2e3..b1343b2a08 100644 --- a/sunnypilot/selfdrive/selfdrived/events.py +++ b/sunnypilot/selfdrive/selfdrived/events.py @@ -10,7 +10,7 @@ from openpilot.common.constants import CV from openpilot.sunnypilot.selfdrive.selfdrived.events_base import EventsBase, Priority, ET, Alert, \ NoEntryAlert, ImmediateDisableAlert, EngagementAlert, NormalPermanentAlert, AlertCallbackType, wrong_car_mode_alert from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit import PCM_LONG_REQUIRED_MAX_SET_SPEED, CONFIRM_SPEED_THRESHOLD - +from openpilot.system.hardware import HARDWARE AlertSize = log.SelfdriveState.AlertSize AlertStatus = log.SelfdriveState.AlertStatus @@ -23,6 +23,8 @@ EventNameSP = custom.OnroadEventSP.EventName # get event name from enum EVENT_NAME_SP = {v: k for k, v in EventNameSP.schema.enumerants.items()} +IS_MICI = HARDWARE.get_device_type() == 'mici' + def speed_limit_adjust_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: speedLimit = sm['longitudinalPlanSP'].speedLimit.resolver.speedLimit @@ -37,10 +39,14 @@ def speed_limit_adjust_alert(CP: car.CarParams, CS: car.CarState, sm: messaging. def speed_limit_pre_active_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: speed_conv = CV.MS_TO_KPH if metric else CV.MS_TO_MPH + v_cruise_cluster = CS.vCruiseCluster + set_speed = sm['controlsState'].vCruiseDEPRECATED if v_cruise_cluster == 0.0 else v_cruise_cluster + set_speed_conv = round(set_speed * speed_conv) + speed_limit_final_last = sm['longitudinalPlanSP'].speedLimit.resolver.speedLimitFinalLast speed_limit_final_last_conv = round(speed_limit_final_last * speed_conv) alert_1_str = "" - alert_size = AlertSize.none + alert_size = AlertSize.small if CP.openpilotLongitudinalControl and CP.pcmCruise: # PCM long @@ -50,7 +56,14 @@ def speed_limit_pre_active_alert(CP: car.CarParams, CS: car.CarState, sm: messag speed_unit = "km/h" if metric else "mph" alert_1_str = f"Speed Limit Assist: set to {pcm_long_required_max_set_speed_conv} {speed_unit} to engage" - alert_size = AlertSize.small + else: + if IS_MICI: + if set_speed_conv < speed_limit_final_last_conv: + alert_1_str = "Press + to confirm speed limit" + elif set_speed_conv > speed_limit_final_last_conv: + alert_1_str = "Press - to confirm speed limit" + else: + alert_size = AlertSize.none return Alert( alert_1_str, From a6b562e0c1319e7f0b67c6cb2e7c77ffe785563f Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sat, 28 Feb 2026 20:51:31 -0800 Subject: [PATCH 287/311] jenkins: move panda tests before camera tests (#37498) * jenkins: move panda tests before camera tests * force this time * Revert "force this time" This reverts commit 53508225d39d63b97ff7ecc3a0181a27b5948d1b. --- Jenkinsfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index e58ac817eb..c5ebf6162b 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -210,7 +210,6 @@ node { 'HW + Unit Tests': { deviceStage("tizi-hardware", "tizi-common", ["UNSAFE=1"], [ step("build", "cd system/manager && ./build.py"), - step("test pandad", "pytest selfdrive/pandad/tests/test_pandad.py", [diffPaths: ["panda", "selfdrive/pandad/"]]), step("test power draw", "pytest -s system/hardware/tici/tests/test_power_draw.py"), step("test encoder", "LD_LIBRARY_PATH=/usr/local/lib pytest system/loggerd/tests/test_encoder.py", [diffPaths: ["system/loggerd/"]]), step("test manager", "pytest system/manager/test/test_manager.py"), @@ -219,12 +218,14 @@ node { 'camerad OX03C10': { deviceStage("OX03C10", "tizi-ox03c10", ["UNSAFE=1"], [ step("build", "cd system/manager && ./build.py"), + step("test pandad", "pytest selfdrive/pandad/tests/test_pandad.py", [diffPaths: ["panda", "selfdrive/pandad/"]]), step("test camerad", "pytest system/camerad/test/test_camerad.py", [timeout: 90]), ]) }, 'camerad OS04C10': { deviceStage("OS04C10", "tici-os04c10", ["UNSAFE=1"], [ step("build", "cd system/manager && ./build.py"), + step("test pandad", "pytest selfdrive/pandad/tests/test_pandad.py", [diffPaths: ["panda", "selfdrive/pandad/"]]), step("test camerad", "pytest system/camerad/test/test_camerad.py", [timeout: 90]), ]) }, From de0790f91248acdfbb4c239242514d1692e80967 Mon Sep 17 00:00:00 2001 From: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com> Date: Sat, 28 Feb 2026 21:07:24 -0800 Subject: [PATCH 288/311] sunnypilot modeld: remove thneed modeld (#1731) * sunnypilot modeld: remove unused modeld * more --------- Co-authored-by: Jason Wen --- .github/workflows/sunnypilot-build-model.yaml | 1 - .gitignore | 2 - selfdrive/modeld/modeld.py | 2 +- sunnypilot/modeld/.gitignore | 1 - sunnypilot/modeld/SConscript | 44 --- sunnypilot/modeld/__init__.py | 0 sunnypilot/modeld/fill_model_msg.py | 223 ----------- sunnypilot/modeld/get_model_metadata.py | 28 -- sunnypilot/modeld/modeld | 10 - sunnypilot/modeld/modeld.py | 348 ------------------ sunnypilot/modeld/models/__init__.py | 0 sunnypilot/modeld/models/commonmodel.cc | 50 --- sunnypilot/modeld/models/commonmodel.h | 36 -- sunnypilot/modeld/models/commonmodel.pxd | 18 - sunnypilot/modeld/models/commonmodel_pyx.pxd | 13 - sunnypilot/modeld/models/commonmodel_pyx.pyx | 45 --- sunnypilot/modeld/parse_model_outputs.py | 107 ------ sunnypilot/modeld/runners/__init__.py | 22 -- sunnypilot/modeld/runners/onnxmodel.py | 98 ----- sunnypilot/modeld/runners/run.h | 3 - sunnypilot/modeld/runners/runmodel.h | 49 --- sunnypilot/modeld/runners/runmodel.pxd | 14 - sunnypilot/modeld/runners/runmodel_pyx.pxd | 6 - sunnypilot/modeld/runners/runmodel_pyx.pyx | 37 -- sunnypilot/modeld/runners/thneedmodel.cc | 58 --- sunnypilot/modeld/runners/thneedmodel.h | 17 - sunnypilot/modeld/runners/thneedmodel.pxd | 9 - sunnypilot/modeld/runners/thneedmodel_pyx.pyx | 14 - sunnypilot/modeld/thneed/README | 8 - sunnypilot/modeld/thneed/__init__.py | 0 sunnypilot/modeld/thneed/clutil_legacy.cc | 126 ------- sunnypilot/modeld/thneed/clutil_legacy.h | 13 - sunnypilot/modeld/thneed/serialize.cc | 155 -------- sunnypilot/modeld/thneed/thneed.h | 133 ------- sunnypilot/modeld/thneed/thneed_common.cc | 216 ----------- sunnypilot/modeld/thneed/thneed_pc.cc | 32 -- sunnypilot/modeld/thneed/thneed_qcom2.cc | 258 ------------- sunnypilot/modeld/transforms/loadyuv.cc | 74 ---- sunnypilot/modeld/transforms/loadyuv.cl | 47 --- sunnypilot/modeld/transforms/loadyuv.h | 16 - sunnypilot/modeld/transforms/transform.cc | 97 ----- sunnypilot/modeld/transforms/transform.cl | 54 --- sunnypilot/modeld/transforms/transform.h | 25 -- sunnypilot/modeld_v2/modeld.py | 2 +- .../{modeld => modeld_v2}/modeld_base.py | 0 sunnypilot/{modeld => models}/constants.py | 0 sunnypilot/models/helpers.py | 2 +- .../selfdrive/controls/controlsd_ext.py | 2 +- .../selfdrive/controls/lib/drive_helpers.py | 27 -- 49 files changed, 4 insertions(+), 2538 deletions(-) delete mode 100644 sunnypilot/modeld/.gitignore delete mode 100644 sunnypilot/modeld/SConscript delete mode 100644 sunnypilot/modeld/__init__.py delete mode 100644 sunnypilot/modeld/fill_model_msg.py delete mode 100755 sunnypilot/modeld/get_model_metadata.py delete mode 100755 sunnypilot/modeld/modeld delete mode 100755 sunnypilot/modeld/modeld.py delete mode 100644 sunnypilot/modeld/models/__init__.py delete mode 100644 sunnypilot/modeld/models/commonmodel.cc delete mode 100644 sunnypilot/modeld/models/commonmodel.h delete mode 100644 sunnypilot/modeld/models/commonmodel.pxd delete mode 100644 sunnypilot/modeld/models/commonmodel_pyx.pxd delete mode 100644 sunnypilot/modeld/models/commonmodel_pyx.pyx delete mode 100644 sunnypilot/modeld/parse_model_outputs.py delete mode 100644 sunnypilot/modeld/runners/__init__.py delete mode 100644 sunnypilot/modeld/runners/onnxmodel.py delete mode 100644 sunnypilot/modeld/runners/run.h delete mode 100644 sunnypilot/modeld/runners/runmodel.h delete mode 100644 sunnypilot/modeld/runners/runmodel.pxd delete mode 100644 sunnypilot/modeld/runners/runmodel_pyx.pxd delete mode 100644 sunnypilot/modeld/runners/runmodel_pyx.pyx delete mode 100644 sunnypilot/modeld/runners/thneedmodel.cc delete mode 100644 sunnypilot/modeld/runners/thneedmodel.h delete mode 100644 sunnypilot/modeld/runners/thneedmodel.pxd delete mode 100644 sunnypilot/modeld/runners/thneedmodel_pyx.pyx delete mode 100644 sunnypilot/modeld/thneed/README delete mode 100644 sunnypilot/modeld/thneed/__init__.py delete mode 100644 sunnypilot/modeld/thneed/clutil_legacy.cc delete mode 100644 sunnypilot/modeld/thneed/clutil_legacy.h delete mode 100644 sunnypilot/modeld/thneed/serialize.cc delete mode 100644 sunnypilot/modeld/thneed/thneed.h delete mode 100644 sunnypilot/modeld/thneed/thneed_common.cc delete mode 100644 sunnypilot/modeld/thneed/thneed_pc.cc delete mode 100644 sunnypilot/modeld/thneed/thneed_qcom2.cc delete mode 100644 sunnypilot/modeld/transforms/loadyuv.cc delete mode 100644 sunnypilot/modeld/transforms/loadyuv.cl delete mode 100644 sunnypilot/modeld/transforms/loadyuv.h delete mode 100644 sunnypilot/modeld/transforms/transform.cc delete mode 100644 sunnypilot/modeld/transforms/transform.cl delete mode 100644 sunnypilot/modeld/transforms/transform.h rename sunnypilot/{modeld => modeld_v2}/modeld_base.py (100%) rename sunnypilot/{modeld => models}/constants.py (100%) delete mode 100644 sunnypilot/selfdrive/controls/lib/drive_helpers.py diff --git a/.github/workflows/sunnypilot-build-model.yaml b/.github/workflows/sunnypilot-build-model.yaml index 293c0f2a01..9414a7fd04 100644 --- a/.github/workflows/sunnypilot-build-model.yaml +++ b/.github/workflows/sunnypilot-build-model.yaml @@ -184,7 +184,6 @@ jobs: # Copy the model files rsync -avm \ --include='*.dlc' \ - --include='*.thneed' \ --include='*.pkl' \ --include='*.onnx' \ --exclude='*' \ diff --git a/.gitignore b/.gitignore index b6a1d34606..cd5e64e52b 100644 --- a/.gitignore +++ b/.gitignore @@ -65,8 +65,6 @@ cppcheck_report.txt comma*.sh selfdrive/modeld/models/*.pkl* -sunnypilot/modeld*/thneed/compile -sunnypilot/modeld*/models/*.thneed sunnypilot/modeld*/models/*.pkl # openpilot log files diff --git a/selfdrive/modeld/modeld.py b/selfdrive/modeld/modeld.py index fd55eeb167..494feb99c1 100755 --- a/selfdrive/modeld/modeld.py +++ b/selfdrive/modeld/modeld.py @@ -31,7 +31,7 @@ from openpilot.common.file_chunker import read_file_chunked from openpilot.selfdrive.modeld.constants import ModelConstants, Plan from openpilot.sunnypilot.livedelay.helpers import get_lat_delay -from openpilot.sunnypilot.modeld.modeld_base import ModelStateBase +from openpilot.sunnypilot.modeld_v2.modeld_base import ModelStateBase PROCESS_NAME = "selfdrive.modeld.modeld" diff --git a/sunnypilot/modeld/.gitignore b/sunnypilot/modeld/.gitignore deleted file mode 100644 index 742d3d1205..0000000000 --- a/sunnypilot/modeld/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*_pyx.cpp diff --git a/sunnypilot/modeld/SConscript b/sunnypilot/modeld/SConscript deleted file mode 100644 index 777f842ac0..0000000000 --- a/sunnypilot/modeld/SConscript +++ /dev/null @@ -1,44 +0,0 @@ -Import('env', 'envCython', 'arch', 'cereal', 'messaging', 'common', 'visionipc') -lenv = env.Clone() -lenvCython = envCython.Clone() - -libs = [cereal, messaging, visionipc, common, 'capnp', 'kj', 'pthread'] -frameworks = [] - -common_src = [ - "models/commonmodel.cc", - "transforms/loadyuv.cc", - "transforms/transform.cc", -] - -thneed_src_common = [ - "thneed/clutil_legacy.cc", - "thneed/thneed_common.cc", - "thneed/serialize.cc", -] - -thneed_src_qcom = thneed_src_common + ["thneed/thneed_qcom2.cc"] -thneed_src_pc = thneed_src_common + ["thneed/thneed_pc.cc"] -thneed_src = thneed_src_qcom if arch == "larch64" else thneed_src_pc - -# OpenCL is a framework on Mac -if arch == "Darwin": - frameworks += ['OpenCL'] -else: - libs += ['OpenCL'] - -# Set path definitions -for pathdef, fn in {'TRANSFORM': 'transforms/transform.cl', 'LOADYUV': 'transforms/loadyuv.cl'}.items(): - for xenv in (lenv, lenvCython): - xenv['CXXFLAGS'].append(f'-D{pathdef}_PATH=\\"{File(fn).abspath}\\"') - -cython_libs = envCython["LIBS"] + libs -commonmodel_lib = lenv.Library('commonmodel', common_src) - -lenvCython.Program('runners/runmodel_pyx.so', 'runners/runmodel_pyx.pyx', LIBS=cython_libs, FRAMEWORKS=frameworks) -lenvCython.Program('models/commonmodel_pyx.so', 'models/commonmodel_pyx.pyx', LIBS=[commonmodel_lib, *cython_libs], FRAMEWORKS=frameworks) - -if arch == "larch64": - thneed_lib = env.SharedLibrary('thneed', thneed_src, LIBS=[common, 'OpenCL', 'dl']) - thneedmodel_lib = env.Library('thneedmodel', ['runners/thneedmodel.cc']) - lenvCython.Program('runners/thneedmodel_pyx.so', 'runners/thneedmodel_pyx.pyx', LIBS=envCython["LIBS"]+[thneedmodel_lib, thneed_lib, common, 'dl', 'OpenCL']) diff --git a/sunnypilot/modeld/__init__.py b/sunnypilot/modeld/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/sunnypilot/modeld/fill_model_msg.py b/sunnypilot/modeld/fill_model_msg.py deleted file mode 100644 index a62c451efd..0000000000 --- a/sunnypilot/modeld/fill_model_msg.py +++ /dev/null @@ -1,223 +0,0 @@ -import os -import capnp -import numpy as np -from cereal import log -from openpilot.sunnypilot.modeld.constants import ModelConstants, Plan -from openpilot.sunnypilot.models.helpers import plan_x_idxs_helper -from openpilot.sunnypilot.selfdrive.controls.lib.drive_helpers import CONTROL_N, get_lag_adjusted_curvature, MIN_SPEED - -SEND_RAW_PRED = os.getenv('SEND_RAW_PRED') - -ConfidenceClass = log.ModelDataV2.ConfidenceClass - -class PublishState: - def __init__(self): - self.disengage_buffer = np.zeros(ModelConstants.CONFIDENCE_BUFFER_LEN*ModelConstants.DISENGAGE_WIDTH, dtype=np.float32) - self.prev_brake_5ms2_probs = np.zeros(ModelConstants.FCW_5MS2_PROBS_WIDTH, dtype=np.float32) - self.prev_brake_3ms2_probs = np.zeros(ModelConstants.FCW_3MS2_PROBS_WIDTH, dtype=np.float32) - -def fill_xyzt(builder, t, x, y, z, x_std=None, y_std=None, z_std=None): - builder.t = t - builder.x = x.tolist() - builder.y = y.tolist() - builder.z = z.tolist() - if x_std is not None: - builder.xStd = x_std.tolist() - if y_std is not None: - builder.yStd = y_std.tolist() - if z_std is not None: - builder.zStd = z_std.tolist() - -def fill_xyvat(builder, t, x, y, v, a, x_std=None, y_std=None, v_std=None, a_std=None): - builder.t = t - builder.x = x.tolist() - builder.y = y.tolist() - builder.v = v.tolist() - builder.a = a.tolist() - if x_std is not None: - builder.xStd = x_std.tolist() - if y_std is not None: - builder.yStd = y_std.tolist() - if v_std is not None: - builder.vStd = v_std.tolist() - if a_std is not None: - builder.aStd = a_std.tolist() - -def fill_xyz_poly(builder, degree, x, y, z): - xyz = np.stack([x, y, z], axis=1) - coeffs = np.polynomial.polynomial.polyfit(ModelConstants.T_IDXS, xyz, deg=degree) - builder.xCoefficients = coeffs[:, 0].tolist() - builder.yCoefficients = coeffs[:, 1].tolist() - builder.zCoefficients = coeffs[:, 2].tolist() - -def fill_model_msg(base_msg: capnp._DynamicStructBuilder, extended_msg: capnp._DynamicStructBuilder, - net_output_data: dict[str, np.ndarray], action: log.ModelDataV2.Action, publish_state: PublishState, - vipc_frame_id: int, vipc_frame_id_extra: int, frame_id: int, frame_drop: float, - timestamp_eof: int, model_execution_time: float, valid: bool, - v_ego: float, steer_delay: float, meta_const) -> None: - frame_age = frame_id - vipc_frame_id if frame_id > vipc_frame_id else 0 - frame_drop_perc = frame_drop * 100 - extended_msg.valid = valid - base_msg.valid = valid - - if 'lat_planner_solution' in net_output_data: - x, y, yaw, yawRate = [net_output_data['lat_planner_solution'][0, :, i].tolist() for i in range(4)] - x_sol = np.column_stack([x, y, yaw, yawRate]) - v_ego = max(MIN_SPEED, v_ego) - psis = x_sol[0:CONTROL_N, 2].tolist() - curvatures = (x_sol[0:CONTROL_N, 3] / v_ego).tolist() - desired_curvature = get_lag_adjusted_curvature(steer_delay, v_ego, psis, curvatures) - else: - desired_curvature = float(net_output_data['desired_curvature'][0, 0]) - - driving_model_data = base_msg.drivingModelData - - driving_model_data.frameId = vipc_frame_id - driving_model_data.frameIdExtra = vipc_frame_id_extra - driving_model_data.frameDropPerc = frame_drop_perc - driving_model_data.modelExecutionTime = model_execution_time - - # Populate drivingModelData.action - driving_model_data_action = driving_model_data.action - driving_model_data_action.desiredAcceleration = action.desiredAcceleration - driving_model_data_action.shouldStop = action.shouldStop - driving_model_data_action.desiredCurvature = desired_curvature - - modelV2 = extended_msg.modelV2 - modelV2.frameId = vipc_frame_id - modelV2.frameIdExtra = vipc_frame_id_extra - modelV2.frameAge = frame_age - modelV2.frameDropPerc = frame_drop_perc - modelV2.timestampEof = timestamp_eof - modelV2.modelExecutionTime = model_execution_time - - # plan - position = modelV2.position - fill_xyzt(position, ModelConstants.T_IDXS, *net_output_data['plan'][0,:,Plan.POSITION].T, *net_output_data['plan_stds'][0,:,Plan.POSITION].T) - velocity = modelV2.velocity - fill_xyzt(velocity, ModelConstants.T_IDXS, *net_output_data['plan'][0,:,Plan.VELOCITY].T) - acceleration = modelV2.acceleration - fill_xyzt(acceleration, ModelConstants.T_IDXS, *net_output_data['plan'][0,:,Plan.ACCELERATION].T) - orientation = modelV2.orientation - fill_xyzt(orientation, ModelConstants.T_IDXS, *net_output_data['plan'][0,:,Plan.T_FROM_CURRENT_EULER].T) - orientation_rate = modelV2.orientationRate - fill_xyzt(orientation_rate, ModelConstants.T_IDXS, *net_output_data['plan'][0,:,Plan.ORIENTATION_RATE].T) - - # temporal pose - temporal_pose = modelV2.temporalPoseDEPRECATED - temporal_pose.trans = net_output_data['plan'][0,0,Plan.VELOCITY].tolist() - temporal_pose.transStd = net_output_data['plan_stds'][0,0,Plan.VELOCITY].tolist() - temporal_pose.rot = net_output_data['plan'][0,0,Plan.ORIENTATION_RATE].tolist() - temporal_pose.rotStd = net_output_data['plan_stds'][0,0,Plan.ORIENTATION_RATE].tolist() - - # poly path - poly_path = driving_model_data.path - fill_xyz_poly(poly_path, ModelConstants.POLY_PATH_DEGREE, *net_output_data['plan'][0,:,Plan.POSITION].T) - - # lateral planning - modelV2_action = modelV2.action - modelV2_action.desiredAcceleration = action.desiredAcceleration - modelV2_action.shouldStop = action.shouldStop - modelV2_action.desiredCurvature = desired_curvature - - # times at X_IDXS according to model plan - PLAN_T_IDXS: list[float] = plan_x_idxs_helper(ModelConstants, Plan, net_output_data) - - # lane lines - modelV2.init('laneLines', 4) - for i in range(4): - lane_line = modelV2.laneLines[i] - fill_xyzt(lane_line, PLAN_T_IDXS, np.array(ModelConstants.X_IDXS), net_output_data['lane_lines'][0,i,:,0], net_output_data['lane_lines'][0,i,:,1]) - modelV2.laneLineStds = net_output_data['lane_lines_stds'][0,:,0,0].tolist() - modelV2.laneLineProbs = net_output_data['lane_lines_prob'][0,1::2].tolist() - - lane_line_meta = driving_model_data.laneLineMeta - lane_line_meta.leftY = modelV2.laneLines[1].y[0] - lane_line_meta.leftProb = modelV2.laneLineProbs[1] - lane_line_meta.rightY = modelV2.laneLines[2].y[0] - lane_line_meta.rightProb = modelV2.laneLineProbs[2] - - # road edges - modelV2.init('roadEdges', 2) - for i in range(2): - road_edge = modelV2.roadEdges[i] - fill_xyzt(road_edge, PLAN_T_IDXS, np.array(ModelConstants.X_IDXS), net_output_data['road_edges'][0,i,:,0], net_output_data['road_edges'][0,i,:,1]) - modelV2.roadEdgeStds = net_output_data['road_edges_stds'][0,:,0,0].tolist() - - # leads - modelV2.init('leadsV3', 3) - for i in range(3): - lead = modelV2.leadsV3[i] - fill_xyvat(lead, ModelConstants.LEAD_T_IDXS, *net_output_data['lead'][0,i].T, *net_output_data['lead_stds'][0,i].T) - lead.prob = net_output_data['lead_prob'][0,i].tolist() - lead.probTime = ModelConstants.LEAD_T_OFFSETS[i] - - # meta - meta = modelV2.meta - meta.desireState = net_output_data['desire_state'][0].reshape(-1).tolist() - meta.desirePrediction = net_output_data['desire_pred'][0].reshape(-1).tolist() - meta.engagedProb = net_output_data['meta'][0,meta_const.ENGAGED].item() - meta.init('disengagePredictions') - disengage_predictions = meta.disengagePredictions - disengage_predictions.t = ModelConstants.META_T_IDXS - disengage_predictions.brakeDisengageProbs = net_output_data['meta'][0,meta_const.BRAKE_DISENGAGE].tolist() - disengage_predictions.gasDisengageProbs = net_output_data['meta'][0,meta_const.GAS_DISENGAGE].tolist() - disengage_predictions.steerOverrideProbs = net_output_data['meta'][0,meta_const.STEER_OVERRIDE].tolist() - disengage_predictions.brake3MetersPerSecondSquaredProbs = net_output_data['meta'][0,meta_const.HARD_BRAKE_3].tolist() - disengage_predictions.brake4MetersPerSecondSquaredProbs = net_output_data['meta'][0,meta_const.HARD_BRAKE_4].tolist() - disengage_predictions.brake5MetersPerSecondSquaredProbs = net_output_data['meta'][0,meta_const.HARD_BRAKE_5].tolist() - if 'sim_pose' not in net_output_data: - disengage_predictions.gasPressProbs = net_output_data['meta'][0,meta_const.GAS_PRESS].tolist() - disengage_predictions.brakePressProbs = net_output_data['meta'][0,meta_const.BRAKE_PRESS].tolist() - - publish_state.prev_brake_5ms2_probs[:-1] = publish_state.prev_brake_5ms2_probs[1:] - publish_state.prev_brake_5ms2_probs[-1] = net_output_data['meta'][0,meta_const.HARD_BRAKE_5][0] - publish_state.prev_brake_3ms2_probs[:-1] = publish_state.prev_brake_3ms2_probs[1:] - publish_state.prev_brake_3ms2_probs[-1] = net_output_data['meta'][0,meta_const.HARD_BRAKE_3][0] - hard_brake_predicted = (publish_state.prev_brake_5ms2_probs > ModelConstants.FCW_THRESHOLDS_5MS2).all() and \ - (publish_state.prev_brake_3ms2_probs > ModelConstants.FCW_THRESHOLDS_3MS2).all() - meta.hardBrakePredicted = hard_brake_predicted.item() - - # confidence - if vipc_frame_id % (2*ModelConstants.MODEL_FREQ) == 0: - # any disengage prob - brake_disengage_probs = net_output_data['meta'][0,meta_const.BRAKE_DISENGAGE] - gas_disengage_probs = net_output_data['meta'][0,meta_const.GAS_DISENGAGE] - steer_override_probs = net_output_data['meta'][0,meta_const.STEER_OVERRIDE] - any_disengage_probs = 1-((1-brake_disengage_probs)*(1-gas_disengage_probs)*(1-steer_override_probs)) - # independent disengage prob for each 2s slice - ind_disengage_probs = np.r_[any_disengage_probs[0], np.diff(any_disengage_probs) / (1 - any_disengage_probs[:-1])] - # rolling buf for 2, 4, 6, 8, 10s - publish_state.disengage_buffer[:-ModelConstants.DISENGAGE_WIDTH] = publish_state.disengage_buffer[ModelConstants.DISENGAGE_WIDTH:] - publish_state.disengage_buffer[-ModelConstants.DISENGAGE_WIDTH:] = ind_disengage_probs - - score = 0. - for i in range(ModelConstants.DISENGAGE_WIDTH): - score += publish_state.disengage_buffer[i*ModelConstants.DISENGAGE_WIDTH+ModelConstants.DISENGAGE_WIDTH-1-i].item() / ModelConstants.DISENGAGE_WIDTH - if score < ModelConstants.RYG_GREEN: - modelV2.confidence = ConfidenceClass.green - elif score < ModelConstants.RYG_YELLOW: - modelV2.confidence = ConfidenceClass.yellow - else: - modelV2.confidence = ConfidenceClass.red - - # raw prediction if enabled - if SEND_RAW_PRED: - modelV2.rawPredictions = net_output_data['raw_pred'].tobytes() - -def fill_pose_msg(msg: capnp._DynamicStructBuilder, net_output_data: dict[str, np.ndarray], - vipc_frame_id: int, vipc_dropped_frames: int, timestamp_eof: int, live_calib_seen: bool) -> None: - msg.valid = live_calib_seen & (vipc_dropped_frames < 1) - cameraOdometry = msg.cameraOdometry - - cameraOdometry.frameId = vipc_frame_id - cameraOdometry.timestampEof = timestamp_eof - - cameraOdometry.trans = net_output_data['pose'][0,:3].tolist() - cameraOdometry.rot = net_output_data['pose'][0,3:].tolist() - cameraOdometry.wideFromDeviceEuler = net_output_data['wide_from_device_euler'][0,:].tolist() - cameraOdometry.roadTransformTrans = net_output_data['road_transform'][0,:3].tolist() - cameraOdometry.transStd = net_output_data['pose_stds'][0,:3].tolist() - cameraOdometry.rotStd = net_output_data['pose_stds'][0,3:].tolist() - cameraOdometry.wideFromDeviceEulerStd = net_output_data['wide_from_device_euler_stds'][0,:].tolist() - cameraOdometry.roadTransformTransStd = net_output_data['road_transform_stds'][0,:3].tolist() diff --git a/sunnypilot/modeld/get_model_metadata.py b/sunnypilot/modeld/get_model_metadata.py deleted file mode 100755 index 144860204f..0000000000 --- a/sunnypilot/modeld/get_model_metadata.py +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env python3 -import sys -import pathlib -import onnx -import codecs -import pickle - -def get_name_and_shape(value_info:onnx.ValueInfoProto) -> tuple[str, tuple[int,...]]: - shape = tuple([int(dim.dim_value) for dim in value_info.type.tensor_type.shape.dim]) - name = value_info.name - return name, shape - -if __name__ == "__main__": - model_path = pathlib.Path(sys.argv[1]) - model = onnx.load(str(model_path)) - i = [x.key for x in model.metadata_props].index('output_slices') - output_slices = model.metadata_props[i].value - - metadata = {} - metadata['output_slices'] = pickle.loads(codecs.decode(output_slices.encode(), "base64")) - metadata['input_shapes'] = dict([get_name_and_shape(x) for x in model.graph.input]) - metadata['output_shapes'] = dict([get_name_and_shape(x) for x in model.graph.output]) - - metadata_path = model_path.parent / (model_path.stem + '_metadata.pkl') - with open(metadata_path, 'wb') as f: - pickle.dump(metadata, f) - - print(f'saved metadata to {metadata_path}') diff --git a/sunnypilot/modeld/modeld b/sunnypilot/modeld/modeld deleted file mode 100755 index e1cef4dcc3..0000000000 --- a/sunnypilot/modeld/modeld +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env bash - -DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" -cd "$DIR/../../" - -if [ -f "$DIR/libthneed.so" ]; then - export LD_PRELOAD="$DIR/libthneed.so" -fi - -exec "$DIR/modeld.py" "$@" diff --git a/sunnypilot/modeld/modeld.py b/sunnypilot/modeld/modeld.py deleted file mode 100755 index b36aea405a..0000000000 --- a/sunnypilot/modeld/modeld.py +++ /dev/null @@ -1,348 +0,0 @@ -#!/usr/bin/env python3 -import os -import time -import numpy as np -import cereal.messaging as messaging -from cereal import car, log -from pathlib import Path -from setproctitle import setproctitle -from cereal.messaging import PubMaster, SubMaster -from msgq.visionipc import VisionIpcClient, VisionStreamType, VisionBuf -from opendbc.car.car_helpers import get_demo_car_params -from openpilot.common.swaglog import cloudlog -from openpilot.common.params import Params -from openpilot.common.filter_simple import FirstOrderFilter -from openpilot.common.realtime import DT_MDL, config_realtime_process -from numpy import interp -from openpilot.common.transformations.camera import DEVICE_CAMERAS -from openpilot.common.transformations.model import get_warp_matrix -from openpilot.system import sentry -from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper -from openpilot.selfdrive.controls.lib.drive_helpers import get_accel_from_plan, smooth_value - -from openpilot.sunnypilot.livedelay.helpers import get_lat_delay -from openpilot.sunnypilot.modeld.runners import ModelRunner, Runtime -from openpilot.sunnypilot.modeld.parse_model_outputs import Parser -from openpilot.sunnypilot.modeld.fill_model_msg import fill_model_msg, fill_pose_msg, PublishState -from openpilot.sunnypilot.modeld_v2.camera_offset_helper import CameraOffsetHelper -from openpilot.sunnypilot.modeld.constants import ModelConstants, Plan -from openpilot.sunnypilot.models.helpers import get_active_bundle, get_model_path, load_metadata, prepare_inputs, load_meta_constants -from openpilot.sunnypilot.modeld.models.commonmodel_pyx import ModelFrame, CLContext -from openpilot.sunnypilot.modeld.modeld_base import ModelStateBase - - -PROCESS_NAME = "selfdrive.modeld.modeld_snpe" -SEND_RAW_PRED = os.getenv('SEND_RAW_PRED') - -MODEL_PATHS = { - ModelRunner.THNEED: Path(__file__).parent / 'models/supercombo.thneed', - ModelRunner.ONNX: Path(__file__).parent / 'models/supercombo.onnx'} - -METADATA_PATH = Path(__file__).parent / 'models/supercombo_metadata.pkl' - - -class FrameMeta: - frame_id: int = 0 - timestamp_sof: int = 0 - timestamp_eof: int = 0 - - def __init__(self, vipc=None): - if vipc is not None: - self.frame_id, self.timestamp_sof, self.timestamp_eof = vipc.frame_id, vipc.timestamp_sof, vipc.timestamp_eof - -class ModelState(ModelStateBase): - frame: ModelFrame - wide_frame: ModelFrame - inputs: dict[str, np.ndarray] - output: np.ndarray - prev_desire: np.ndarray # for tracking the rising edge of the pulse - model: ModelRunner - - def __init__(self, context: CLContext): - ModelStateBase.__init__(self) - self.frame = ModelFrame(context) - self.wide_frame = ModelFrame(context) - self.prev_desire = np.zeros(ModelConstants.DESIRE_LEN, dtype=np.float32) - bundle = get_active_bundle() - overrides = {override.key: override.value for override in bundle.overrides} - self.LAT_SMOOTH_SECONDS = float(overrides.get('lat', ".0")) - self.LONG_SMOOTH_SECONDS = float(overrides.get('long', ".0")) - - model_paths = get_model_path() - self.model_metadata = load_metadata() - self.inputs = prepare_inputs(self.model_metadata) - self.meta = load_meta_constants(self.model_metadata) - - self.output_slices = self.model_metadata['output_slices'] - net_output_size = self.model_metadata['output_shapes']['outputs'][1] - self.output = np.zeros(net_output_size, dtype=np.float32) - self.parser = Parser() - - self.model = ModelRunner(model_paths, self.output, Runtime.GPU, False, context) - self.model.addInput("input_imgs", None) - self.model.addInput("big_input_imgs", None) - for k,v in self.inputs.items(): - self.model.addInput(k, v) - - def slice_outputs(self, model_outputs: np.ndarray) -> dict[str, np.ndarray]: - parsed_model_outputs = {k: model_outputs[np.newaxis, v] for k,v in self.output_slices.items()} - if SEND_RAW_PRED: - parsed_model_outputs['raw_pred'] = model_outputs.copy() - return parsed_model_outputs - - def run(self, buf: VisionBuf, wbuf: VisionBuf, transform: np.ndarray, transform_wide: np.ndarray, - inputs: dict[str, np.ndarray], prepare_only: bool) -> dict[str, np.ndarray] | None: - # Model decides when action is completed, so desire input is just a pulse triggered on rising edge - inputs['desire'][0] = 0 - self.inputs['desire'][:-ModelConstants.DESIRE_LEN] = self.inputs['desire'][ModelConstants.DESIRE_LEN:] - self.inputs['desire'][-ModelConstants.DESIRE_LEN:] = np.where(inputs['desire'] - self.prev_desire > .99, inputs['desire'], 0) - self.prev_desire[:] = inputs['desire'] - - for k in self.inputs: - if k in inputs and k != 'desire': - self.inputs[k][:] = inputs[k] - - # if getCLBuffer is not None, frame will be None - self.model.setInputBuffer("input_imgs", self.frame.prepare(buf, transform.flatten(), self.model.getCLBuffer("input_imgs"))) - if wbuf is not None: - self.model.setInputBuffer("big_input_imgs", self.wide_frame.prepare(wbuf, transform_wide.flatten(), self.model.getCLBuffer("big_input_imgs"))) - - if prepare_only: - return None - - self.model.execute() - outputs = self.parser.parse_outputs(self.slice_outputs(self.output)) - - self.inputs['features_buffer'][:-ModelConstants.FEATURE_LEN] = self.inputs['features_buffer'][ModelConstants.FEATURE_LEN:] - self.inputs['features_buffer'][-ModelConstants.FEATURE_LEN:] = outputs['hidden_state'][0, :] - - if "lat_planner_solution" in outputs and "lat_planner_state" in self.inputs.keys(): - self.inputs['lat_planner_state'][2] = interp(DT_MDL, ModelConstants.T_IDXS, outputs['lat_planner_solution'][0, :, 2]) - self.inputs['lat_planner_state'][3] = interp(DT_MDL, ModelConstants.T_IDXS, outputs['lat_planner_solution'][0, :, 3]) - - if "desired_curvature" in outputs: - if "prev_desired_curvs" in self.inputs.keys(): - self.inputs['prev_desired_curvs'][:-1] = self.inputs['prev_desired_curvs'][1:] - self.inputs['prev_desired_curvs'][-1] = outputs['desired_curvature'][0, 0] - - if "prev_desired_curv" in self.inputs.keys(): - self.inputs['prev_desired_curv'][:-1] = self.inputs['prev_desired_curv'][1:] - self.inputs['prev_desired_curv'][-1:] = outputs['desired_curvature'][0, :] - return outputs - - def get_action_from_model(self, model_output: dict[str, np.ndarray], prev_action: log.ModelDataV2.Action, - long_action_t: float) -> log.ModelDataV2.Action: - plan = model_output['plan'][0] - desired_accel, should_stop = get_accel_from_plan(plan[:, Plan.VELOCITY][:, 0], plan[:, Plan.ACCELERATION][:, 0], ModelConstants.T_IDXS, - action_t=long_action_t) - desired_accel = smooth_value(desired_accel, prev_action.desiredAcceleration, self.LONG_SMOOTH_SECONDS) - - return log.ModelDataV2.Action(desiredAcceleration=float(desired_accel), shouldStop=bool(should_stop)) - - -def main(demo=False): - cloudlog.warning("modeld init") - - sentry.set_tag("daemon", PROCESS_NAME) - cloudlog.bind(daemon=PROCESS_NAME) - setproctitle(PROCESS_NAME) - config_realtime_process(7, 54) - - cloudlog.warning("setting up CL context") - cl_context = CLContext() - cloudlog.warning("CL context ready; loading model") - model = ModelState(cl_context) - cloudlog.warning("models loaded, modeld starting") - - # visionipc clients - while True: - available_streams = VisionIpcClient.available_streams("camerad", block=False) - if available_streams: - use_extra_client = VisionStreamType.VISION_STREAM_WIDE_ROAD in available_streams and VisionStreamType.VISION_STREAM_ROAD in available_streams - main_wide_camera = VisionStreamType.VISION_STREAM_ROAD not in available_streams - break - time.sleep(.1) - - vipc_client_main_stream = VisionStreamType.VISION_STREAM_WIDE_ROAD if main_wide_camera else VisionStreamType.VISION_STREAM_ROAD - vipc_client_main = VisionIpcClient("camerad", vipc_client_main_stream, True, cl_context) - vipc_client_extra = VisionIpcClient("camerad", VisionStreamType.VISION_STREAM_WIDE_ROAD, False, cl_context) - cloudlog.warning(f"vision stream set up, main_wide_camera: {main_wide_camera}, use_extra_client: {use_extra_client}") - - while not vipc_client_main.connect(False): - time.sleep(0.1) - while use_extra_client and not vipc_client_extra.connect(False): - time.sleep(0.1) - - cloudlog.warning(f"connected main cam with buffer size: {vipc_client_main.buffer_len} ({vipc_client_main.width} x {vipc_client_main.height})") - if use_extra_client: - cloudlog.warning(f"connected extra cam with buffer size: {vipc_client_extra.buffer_len} ({vipc_client_extra.width} x {vipc_client_extra.height})") - - # messaging - pm = PubMaster(["modelV2", "drivingModelData", "cameraOdometry", "modelDataV2SP"]) - sm = SubMaster(["deviceState", "carState", "roadCameraState", "liveCalibration", "driverMonitoringState", "carControl", "liveDelay"]) - - publish_state = PublishState() - params = Params() - - # setup filter to track dropped frames - frame_dropped_filter = FirstOrderFilter(0., 10., 1. / ModelConstants.MODEL_FREQ) - frame_id = 0 - last_vipc_frame_id = 0 - run_count = 0 - - model_transform_main = np.zeros((3, 3), dtype=np.float32) - model_transform_extra = np.zeros((3, 3), dtype=np.float32) - live_calib_seen = False - buf_main, buf_extra = None, None - meta_main = FrameMeta() - meta_extra = FrameMeta() - camera_offset_helper = CameraOffsetHelper() - - - if demo: - CP = get_demo_car_params() - else: - CP = messaging.log_from_bytes(params.get("CarParams", block=True), car.CarParams) - - cloudlog.info("modeld got CarParams: %s", CP.brand) - - # Enable lagd support for sunnypilot modeld - long_delay = CP.longitudinalActuatorDelay + model.LONG_SMOOTH_SECONDS - prev_action = log.ModelDataV2.Action() - - DH = DesireHelper() - - while True: - # Keep receiving frames until we are at least 1 frame ahead of previous extra frame - while meta_main.timestamp_sof < meta_extra.timestamp_sof + 25000000: - buf_main = vipc_client_main.recv() - meta_main = FrameMeta(vipc_client_main) - if buf_main is None: - break - - if buf_main is None: - cloudlog.debug("vipc_client_main no frame") - continue - - if use_extra_client: - # Keep receiving extra frames until frame id matches main camera - while True: - buf_extra = vipc_client_extra.recv() - meta_extra = FrameMeta(vipc_client_extra) - if buf_extra is None or meta_main.timestamp_sof < meta_extra.timestamp_sof + 25000000: - break - - if buf_extra is None: - cloudlog.debug("vipc_client_extra no frame") - continue - - if abs(meta_main.timestamp_sof - meta_extra.timestamp_sof) > 10000000: - cloudlog.error(f"frames out of sync! main: {meta_main.frame_id} ({meta_main.timestamp_sof / 1e9:.5f}),\ - extra: {meta_extra.frame_id} ({meta_extra.timestamp_sof / 1e9:.5f})") - - else: - # Use single camera - buf_extra = buf_main - meta_extra = meta_main - - sm.update(0) - desire = DH.desire - v_ego = sm["carState"].vEgo - is_rhd = sm["driverMonitoringState"].isRHD - frame_id = sm["roadCameraState"].frameId - if sm.frame % 60 == 0: - model.lat_delay = get_lat_delay(params, sm["liveDelay"].lateralDelay) - camera_offset_helper.set_offset(params.get("CameraOffset", return_default=True)) - lat_delay = model.lat_delay + model.LAT_SMOOTH_SECONDS - if sm.updated["liveCalibration"] and sm.seen['roadCameraState'] and sm.seen['deviceState']: - device_from_calib_euler = np.array(sm["liveCalibration"].rpyCalib, dtype=np.float32) - dc = DEVICE_CAMERAS[(str(sm['deviceState'].deviceType), str(sm['roadCameraState'].sensor))] - model_transform_main = get_warp_matrix(device_from_calib_euler, dc.ecam.intrinsics if main_wide_camera else dc.fcam.intrinsics, False).astype(np.float32) - model_transform_extra = get_warp_matrix(device_from_calib_euler, dc.ecam.intrinsics, True).astype(np.float32) - model_transform_main, model_transform_extra = camera_offset_helper.update(model_transform_main, model_transform_extra, sm, main_wide_camera) - live_calib_seen = True - - traffic_convention = np.zeros(2) - traffic_convention[int(is_rhd)] = 1 - - vec_desire = np.zeros(ModelConstants.DESIRE_LEN, dtype=np.float32) - if desire >= 0 and desire < ModelConstants.DESIRE_LEN: - vec_desire[desire] = 1 - - # tracked dropped frames - vipc_dropped_frames = max(0, meta_main.frame_id - last_vipc_frame_id - 1) - frames_dropped = frame_dropped_filter.update(min(vipc_dropped_frames, 10)) - if run_count < 10: # let frame drops warm up - frame_dropped_filter.x = 0. - frames_dropped = 0. - run_count = run_count + 1 - - frame_drop_ratio = frames_dropped / (1 + frames_dropped) - prepare_only = vipc_dropped_frames > 0 - if prepare_only: - cloudlog.error(f"skipping model eval. Dropped {vipc_dropped_frames} frames") - - inputs:dict[str, np.ndarray] = { - 'desire': vec_desire, - 'traffic_convention': traffic_convention, - } - - if "lateral_control_params" in model.inputs.keys(): - inputs['lateral_control_params'] = np.array([max(v_ego, 0.), lat_delay], dtype=np.float32) - - if "driving_style" in model.inputs.keys(): - inputs['driving_style'] = np.array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0], dtype=np.float32) - - if "nav_features" in model.inputs.keys(): - inputs['nav_features'] = np.zeros(ModelConstants.NAV_FEATURE_LEN, dtype=np.float32) - - if "nav_instructions" in model.inputs.keys(): - inputs['nav_instructions'] = np.zeros(ModelConstants.NAV_INSTRUCTION_LEN, dtype=np.float32) - - mt1 = time.perf_counter() - model_output = model.run(buf_main, buf_extra, model_transform_main, model_transform_extra, inputs, prepare_only) - mt2 = time.perf_counter() - model_execution_time = mt2 - mt1 - - if model_output is not None: - modelv2_send = messaging.new_message('modelV2') - drivingdata_send = messaging.new_message('drivingModelData') - posenet_send = messaging.new_message('cameraOdometry') - mdv2sp_send = messaging.new_message('modelDataV2SP') - action = model.get_action_from_model(model_output, prev_action, long_delay + DT_MDL) - fill_model_msg(drivingdata_send, modelv2_send, model_output, action, publish_state, meta_main.frame_id, meta_extra.frame_id, frame_id, - frame_drop_ratio, meta_main.timestamp_eof, model_execution_time, live_calib_seen, - v_ego, lat_delay, model.meta) - - desire_state = modelv2_send.modelV2.meta.desireState - l_lane_change_prob = desire_state[log.Desire.laneChangeLeft] - r_lane_change_prob = desire_state[log.Desire.laneChangeRight] - lane_change_prob = l_lane_change_prob + r_lane_change_prob - DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob) - modelv2_send.modelV2.meta.laneChangeState = DH.lane_change_state - modelv2_send.modelV2.meta.laneChangeDirection = DH.lane_change_direction - mdv2sp_send.modelDataV2SP.laneTurnDirection = DH.lane_turn_direction - drivingdata_send.drivingModelData.meta.laneChangeState = DH.lane_change_state - drivingdata_send.drivingModelData.meta.laneChangeDirection = DH.lane_change_direction - - fill_pose_msg(posenet_send, model_output, meta_main.frame_id, vipc_dropped_frames, meta_main.timestamp_eof, live_calib_seen) - pm.send('modelV2', modelv2_send) - pm.send('drivingModelData', drivingdata_send) - pm.send('cameraOdometry', posenet_send) - pm.send('modelDataV2SP', mdv2sp_send) - - last_vipc_frame_id = meta_main.frame_id - - -if __name__ == "__main__": - try: - import argparse - parser = argparse.ArgumentParser() - parser.add_argument('--demo', action='store_true', help='A boolean for demo mode.') - args = parser.parse_args() - main(demo=args.demo) - except KeyboardInterrupt: - cloudlog.warning(f"child {PROCESS_NAME} got SIGINT") - except Exception: - sentry.capture_exception() - raise diff --git a/sunnypilot/modeld/models/__init__.py b/sunnypilot/modeld/models/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/sunnypilot/modeld/models/commonmodel.cc b/sunnypilot/modeld/models/commonmodel.cc deleted file mode 100644 index f1e15e5a4e..0000000000 --- a/sunnypilot/modeld/models/commonmodel.cc +++ /dev/null @@ -1,50 +0,0 @@ -#include "sunnypilot/modeld/models/commonmodel.h" - -#include -#include -#include - -#include "common/clutil.h" - -ModelFrame::ModelFrame(cl_device_id device_id, cl_context context) { - input_frames = std::make_unique(buf_size); - - q = CL_CHECK_ERR(clCreateCommandQueue(context, device_id, 0, &err)); - y_cl = CL_CHECK_ERR(clCreateBuffer(context, CL_MEM_READ_WRITE, MODEL_WIDTH * MODEL_HEIGHT, NULL, &err)); - u_cl = CL_CHECK_ERR(clCreateBuffer(context, CL_MEM_READ_WRITE, (MODEL_WIDTH / 2) * (MODEL_HEIGHT / 2), NULL, &err)); - v_cl = CL_CHECK_ERR(clCreateBuffer(context, CL_MEM_READ_WRITE, (MODEL_WIDTH / 2) * (MODEL_HEIGHT / 2), NULL, &err)); - net_input_cl = CL_CHECK_ERR(clCreateBuffer(context, CL_MEM_READ_WRITE, MODEL_FRAME_SIZE * sizeof(float), NULL, &err)); - - transform_init(&transform, context, device_id); - loadyuv_init(&loadyuv, context, device_id, MODEL_WIDTH, MODEL_HEIGHT); -} - -float* ModelFrame::prepare(cl_mem yuv_cl, int frame_width, int frame_height, int frame_stride, int frame_uv_offset, const mat3 &projection, cl_mem *output) { - transform_queue(&this->transform, q, - yuv_cl, frame_width, frame_height, frame_stride, frame_uv_offset, - y_cl, u_cl, v_cl, MODEL_WIDTH, MODEL_HEIGHT, projection); - - if (output == NULL) { - loadyuv_queue(&loadyuv, q, y_cl, u_cl, v_cl, net_input_cl); - - std::memmove(&input_frames[0], &input_frames[MODEL_FRAME_SIZE], sizeof(float) * MODEL_FRAME_SIZE); - CL_CHECK(clEnqueueReadBuffer(q, net_input_cl, CL_TRUE, 0, MODEL_FRAME_SIZE * sizeof(float), &input_frames[MODEL_FRAME_SIZE], 0, nullptr, nullptr)); - clFinish(q); - return &input_frames[0]; - } else { - loadyuv_queue(&loadyuv, q, y_cl, u_cl, v_cl, *output, true); - // NOTE: Since thneed is using a different command queue, this clFinish is needed to ensure the image is ready. - clFinish(q); - return NULL; - } -} - -ModelFrame::~ModelFrame() { - transform_destroy(&transform); - loadyuv_destroy(&loadyuv); - CL_CHECK(clReleaseMemObject(net_input_cl)); - CL_CHECK(clReleaseMemObject(v_cl)); - CL_CHECK(clReleaseMemObject(u_cl)); - CL_CHECK(clReleaseMemObject(y_cl)); - CL_CHECK(clReleaseCommandQueue(q)); -} diff --git a/sunnypilot/modeld/models/commonmodel.h b/sunnypilot/modeld/models/commonmodel.h deleted file mode 100644 index 9d34ebb6d3..0000000000 --- a/sunnypilot/modeld/models/commonmodel.h +++ /dev/null @@ -1,36 +0,0 @@ -#pragma once - -#include -#include - -#include - -#define CL_USE_DEPRECATED_OPENCL_1_2_APIS -#ifdef __APPLE__ -#include -#else -#include -#endif - -#include "common/mat.h" -#include "sunnypilot/modeld/transforms/loadyuv.h" -#include "sunnypilot/modeld/transforms/transform.h" - -class ModelFrame { -public: - ModelFrame(cl_device_id device_id, cl_context context); - ~ModelFrame(); - float* prepare(cl_mem yuv_cl, int width, int height, int frame_stride, int frame_uv_offset, const mat3& transform, cl_mem *output); - - const int MODEL_WIDTH = 512; - const int MODEL_HEIGHT = 256; - const int MODEL_FRAME_SIZE = MODEL_WIDTH * MODEL_HEIGHT * 3 / 2; - const int buf_size = MODEL_FRAME_SIZE * 2; - -private: - Transform transform; - LoadYUVState loadyuv; - cl_command_queue q; - cl_mem y_cl, u_cl, v_cl, net_input_cl; - std::unique_ptr input_frames; -}; diff --git a/sunnypilot/modeld/models/commonmodel.pxd b/sunnypilot/modeld/models/commonmodel.pxd deleted file mode 100644 index c7fd85411d..0000000000 --- a/sunnypilot/modeld/models/commonmodel.pxd +++ /dev/null @@ -1,18 +0,0 @@ -# distutils: language = c++ - -from msgq.visionipc.visionipc cimport cl_device_id, cl_context, cl_mem - -cdef extern from "common/mat.h": - cdef struct mat3: - float v[9] - -cdef extern from "common/clutil.h": - cdef unsigned long CL_DEVICE_TYPE_DEFAULT - cl_device_id cl_get_device_id(unsigned long) - cl_context cl_create_context(cl_device_id) - -cdef extern from "sunnypilot/modeld/models/commonmodel.h": - cppclass ModelFrame: - int buf_size - ModelFrame(cl_device_id, cl_context) - float * prepare(cl_mem, int, int, int, int, mat3, cl_mem*) diff --git a/sunnypilot/modeld/models/commonmodel_pyx.pxd b/sunnypilot/modeld/models/commonmodel_pyx.pxd deleted file mode 100644 index 0bb798625b..0000000000 --- a/sunnypilot/modeld/models/commonmodel_pyx.pxd +++ /dev/null @@ -1,13 +0,0 @@ -# distutils: language = c++ - -from msgq.visionipc.visionipc cimport cl_mem -from msgq.visionipc.visionipc_pyx cimport CLContext as BaseCLContext - -cdef class CLContext(BaseCLContext): - pass - -cdef class CLMem: - cdef cl_mem * mem - - @staticmethod - cdef create(void*) diff --git a/sunnypilot/modeld/models/commonmodel_pyx.pyx b/sunnypilot/modeld/models/commonmodel_pyx.pyx deleted file mode 100644 index d66fddea54..0000000000 --- a/sunnypilot/modeld/models/commonmodel_pyx.pyx +++ /dev/null @@ -1,45 +0,0 @@ -# distutils: language = c++ -# cython: c_string_encoding=ascii, language_level=3 - -import numpy as np -cimport numpy as cnp -from libc.string cimport memcpy - -from msgq.visionipc.visionipc cimport cl_mem -from msgq.visionipc.visionipc_pyx cimport VisionBuf, CLContext as BaseCLContext -from openpilot.sunnypilot.modeld.models.commonmodel cimport CL_DEVICE_TYPE_DEFAULT, cl_get_device_id, cl_create_context -from openpilot.sunnypilot.modeld.models.commonmodel cimport mat3, ModelFrame as cppModelFrame - - -cdef class CLContext(BaseCLContext): - def __cinit__(self): - self.device_id = cl_get_device_id(CL_DEVICE_TYPE_DEFAULT) - self.context = cl_create_context(self.device_id) - -cdef class CLMem: - @staticmethod - cdef create(void * cmem): - mem = CLMem() - mem.mem = cmem - return mem - -cdef class ModelFrame: - cdef cppModelFrame * frame - - def __cinit__(self, CLContext context): - self.frame = new cppModelFrame(context.device_id, context.context) - - def __dealloc__(self): - del self.frame - - def prepare(self, VisionBuf buf, float[:] projection, CLMem output): - cdef mat3 cprojection - memcpy(cprojection.v, &projection[0], 9*sizeof(float)) - cdef float * data - if output is None: - data = self.frame.prepare(buf.buf.buf_cl, buf.width, buf.height, buf.stride, buf.uv_offset, cprojection, NULL) - else: - data = self.frame.prepare(buf.buf.buf_cl, buf.width, buf.height, buf.stride, buf.uv_offset, cprojection, output.mem) - if not data: - return None - return np.asarray( data) diff --git a/sunnypilot/modeld/parse_model_outputs.py b/sunnypilot/modeld/parse_model_outputs.py deleted file mode 100644 index 9cd5f1465c..0000000000 --- a/sunnypilot/modeld/parse_model_outputs.py +++ /dev/null @@ -1,107 +0,0 @@ -import numpy as np -from openpilot.sunnypilot.modeld.constants import ModelConstants - -def safe_exp(x, out=None): - # -11 is around 10**14, more causes float16 overflow - return np.exp(np.clip(x, -np.inf, 11), out=out) - -def sigmoid(x): - return 1. / (1. + safe_exp(-x)) - -def softmax(x, axis=-1): - x -= np.max(x, axis=axis, keepdims=True) - if x.dtype == np.float32 or x.dtype == np.float64: - safe_exp(x, out=x) - else: - x = safe_exp(x) - x /= np.sum(x, axis=axis, keepdims=True) - return x - -class Parser: - def __init__(self, ignore_missing=False): - self.ignore_missing = ignore_missing - - def check_missing(self, outs, name): - if name not in outs and not self.ignore_missing: - raise ValueError(f"Missing output {name}") - return name not in outs - - def parse_categorical_crossentropy(self, name, outs, out_shape=None): - if self.check_missing(outs, name): - return - raw = outs[name] - if out_shape is not None: - raw = raw.reshape((raw.shape[0],) + out_shape) - outs[name] = softmax(raw, axis=-1) - - def parse_binary_crossentropy(self, name, outs): - if self.check_missing(outs, name): - return - raw = outs[name] - outs[name] = sigmoid(raw) - - def parse_mdn(self, name, outs, in_N=0, out_N=1, out_shape=None): - if self.check_missing(outs, name): - return - raw = outs[name] - raw = raw.reshape((raw.shape[0], max(in_N, 1), -1)) - - n_values = (raw.shape[2] - out_N)//2 - pred_mu = raw[:,:,:n_values] - pred_std = safe_exp(raw[:,:,n_values: 2*n_values]) - - if in_N > 1: - weights = np.zeros((raw.shape[0], in_N, out_N), dtype=raw.dtype) - for i in range(out_N): - weights[:,:,i - out_N] = softmax(raw[:,:,i - out_N], axis=-1) - - if out_N == 1: - for fidx in range(weights.shape[0]): - idxs = np.argsort(weights[fidx][:,0])[::-1] - weights[fidx] = weights[fidx][idxs] - pred_mu[fidx] = pred_mu[fidx][idxs] - pred_std[fidx] = pred_std[fidx][idxs] - full_shape = tuple([raw.shape[0], in_N] + list(out_shape)) - outs[name + '_weights'] = weights - outs[name + '_hypotheses'] = pred_mu.reshape(full_shape) - outs[name + '_stds_hypotheses'] = pred_std.reshape(full_shape) - - pred_mu_final = np.zeros((raw.shape[0], out_N, n_values), dtype=raw.dtype) - pred_std_final = np.zeros((raw.shape[0], out_N, n_values), dtype=raw.dtype) - for fidx in range(weights.shape[0]): - for hidx in range(out_N): - idxs = np.argsort(weights[fidx,:,hidx])[::-1] - pred_mu_final[fidx, hidx] = pred_mu[fidx, idxs[0]] - pred_std_final[fidx, hidx] = pred_std[fidx, idxs[0]] - else: - pred_mu_final = pred_mu - pred_std_final = pred_std - - if out_N > 1: - final_shape = tuple([raw.shape[0], out_N] + list(out_shape)) - else: - final_shape = tuple([raw.shape[0],] + list(out_shape)) - outs[name] = pred_mu_final.reshape(final_shape) - outs[name + '_stds'] = pred_std_final.reshape(final_shape) - - def parse_outputs(self, outs: dict[str, np.ndarray]) -> dict[str, np.ndarray]: - self.parse_mdn('plan', outs, in_N=ModelConstants.PLAN_MHP_N, out_N=ModelConstants.PLAN_MHP_SELECTION, - out_shape=(ModelConstants.IDX_N,ModelConstants.PLAN_WIDTH)) - self.parse_mdn('lane_lines', outs, in_N=0, out_N=0, out_shape=(ModelConstants.NUM_LANE_LINES,ModelConstants.IDX_N,ModelConstants.LANE_LINES_WIDTH)) - self.parse_mdn('road_edges', outs, in_N=0, out_N=0, out_shape=(ModelConstants.NUM_ROAD_EDGES,ModelConstants.IDX_N,ModelConstants.LANE_LINES_WIDTH)) - self.parse_mdn('pose', outs, in_N=0, out_N=0, out_shape=(ModelConstants.POSE_WIDTH,)) - self.parse_mdn('road_transform', outs, in_N=0, out_N=0, out_shape=(ModelConstants.POSE_WIDTH,)) - if 'sim_pose' in outs: - self.parse_mdn('sim_pose', outs, in_N=0, out_N=0, out_shape=(ModelConstants.POSE_WIDTH,)) - self.parse_mdn('wide_from_device_euler', outs, in_N=0, out_N=0, out_shape=(ModelConstants.WIDE_FROM_DEVICE_WIDTH,)) - self.parse_mdn('lead', outs, in_N=ModelConstants.LEAD_MHP_N, out_N=ModelConstants.LEAD_MHP_SELECTION, - out_shape=(ModelConstants.LEAD_TRAJ_LEN,ModelConstants.LEAD_WIDTH)) - if 'lat_planner_solution' in outs: - self.parse_mdn('lat_planner_solution', outs, in_N=0, out_N=0, out_shape=(ModelConstants.IDX_N,ModelConstants.LAT_PLANNER_SOLUTION_WIDTH)) - if 'desired_curvature' in outs: - self.parse_mdn('desired_curvature', outs, in_N=0, out_N=0, out_shape=(ModelConstants.DESIRED_CURV_WIDTH,)) - for k in ['lead_prob', 'lane_lines_prob', 'meta']: - self.parse_binary_crossentropy(k, outs) - self.parse_categorical_crossentropy('desire_state', outs, out_shape=(ModelConstants.DESIRE_PRED_WIDTH,)) - self.parse_categorical_crossentropy('desire_pred', outs, out_shape=(ModelConstants.DESIRE_PRED_LEN,ModelConstants.DESIRE_PRED_WIDTH)) - return outs diff --git a/sunnypilot/modeld/runners/__init__.py b/sunnypilot/modeld/runners/__init__.py deleted file mode 100644 index d105685bd8..0000000000 --- a/sunnypilot/modeld/runners/__init__.py +++ /dev/null @@ -1,22 +0,0 @@ -import os -from openpilot.system.hardware import TICI -from openpilot.sunnypilot.modeld.runners.runmodel_pyx import RunModel, Runtime -assert Runtime - -USE_THNEED = int(os.getenv('USE_THNEED', str(int(TICI)))) - -class ModelRunner(RunModel): - THNEED = 'THNEED' - ONNX = 'ONNX' - - def __new__(cls, paths, *args, **kwargs): - if ModelRunner.THNEED in paths and USE_THNEED: - from openpilot.sunnypilot.modeld.runners.thneedmodel_pyx import ThneedModel as Runner - runner_type = ModelRunner.THNEED - elif ModelRunner.ONNX in paths: - from openpilot.sunnypilot.modeld.runners.onnxmodel import ONNXModel as Runner - runner_type = ModelRunner.ONNX - else: - raise Exception("Couldn't select a model runner, make sure to pass at least one valid model path") - - return Runner(str(paths[runner_type]), *args, **kwargs) diff --git a/sunnypilot/modeld/runners/onnxmodel.py b/sunnypilot/modeld/runners/onnxmodel.py deleted file mode 100644 index 047f13dc9d..0000000000 --- a/sunnypilot/modeld/runners/onnxmodel.py +++ /dev/null @@ -1,98 +0,0 @@ -import onnx -import itertools -import os -import sys -import numpy as np -from typing import Any - -from openpilot.sunnypilot.modeld.runners.runmodel_pyx import RunModel - -ORT_TYPES_TO_NP_TYPES = {'tensor(float16)': np.float16, 'tensor(float)': np.float32, 'tensor(uint8)': np.uint8} - -def attributeproto_fp16_to_fp32(attr): - float32_list = np.frombuffer(attr.raw_data, dtype=np.float16) - attr.data_type = 1 - attr.raw_data = float32_list.astype(np.float32).tobytes() - -def convert_fp16_to_fp32(onnx_path_or_bytes): - if isinstance(onnx_path_or_bytes, bytes): - model = onnx.load_from_string(onnx_path_or_bytes) - elif isinstance(onnx_path_or_bytes, str): - model = onnx.load(onnx_path_or_bytes) - - for i in model.graph.initializer: - if i.data_type == 10: - attributeproto_fp16_to_fp32(i) - for i in itertools.chain(model.graph.input, model.graph.output): - if i.type.tensor_type.elem_type == 10: - i.type.tensor_type.elem_type = 1 - for i in model.graph.node: - if i.op_type == 'Cast' and i.attribute[0].i == 10: - i.attribute[0].i = 1 - for a in i.attribute: - if hasattr(a, 't'): - if a.t.data_type == 10: - attributeproto_fp16_to_fp32(a.t) - return model.SerializeToString() - -def create_ort_session(path, fp16_to_fp32): - os.environ["OMP_NUM_THREADS"] = "4" - os.environ["OMP_WAIT_POLICY"] = "PASSIVE" - - import onnxruntime as ort - print("Onnx available providers: ", ort.get_available_providers(), file=sys.stderr) - options = ort.SessionOptions() - options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_DISABLE_ALL - - provider: str | tuple[str, dict[Any, Any]] - if 'OpenVINOExecutionProvider' in ort.get_available_providers() and 'ONNXCPU' not in os.environ: - provider = 'OpenVINOExecutionProvider' - elif 'CUDAExecutionProvider' in ort.get_available_providers() and 'ONNXCPU' not in os.environ: - options.intra_op_num_threads = 2 - provider = ('CUDAExecutionProvider', {'cudnn_conv_algo_search': 'DEFAULT'}) - else: - options.intra_op_num_threads = 2 - options.execution_mode = ort.ExecutionMode.ORT_SEQUENTIAL - options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL - provider = 'CPUExecutionProvider' - - model_data = convert_fp16_to_fp32(path) if fp16_to_fp32 else path - print("Onnx selected provider: ", [provider], file=sys.stderr) - ort_session = ort.InferenceSession(model_data, options, providers=[provider]) - print("Onnx using ", ort_session.get_providers(), file=sys.stderr) - return ort_session - - -class ONNXModel(RunModel): - def __init__(self, path, output, runtime, use_tf8, cl_context): - self.inputs = {} - self.output = output - - self.session = create_ort_session(path, fp16_to_fp32=True) - self.input_names = [x.name for x in self.session.get_inputs()] - self.input_shapes = {x.name: [1, *x.shape[1:]] for x in self.session.get_inputs()} - self.input_dtypes = {x.name: ORT_TYPES_TO_NP_TYPES[x.type] for x in self.session.get_inputs()} - - # run once to initialize CUDA provider - if "CUDAExecutionProvider" in self.session.get_providers(): - self.session.run(None, {k: np.zeros(self.input_shapes[k], dtype=self.input_dtypes[k]) for k in self.input_names}) - print("ready to run onnx model", self.input_shapes, file=sys.stderr) - - def addInput(self, name, buffer): - assert name in self.input_names - self.inputs[name] = buffer - - def setInputBuffer(self, name, buffer): - assert name in self.inputs - self.inputs[name] = buffer - - def getCLBuffer(self, name): - return None - - def execute(self): - inputs = {k: v.view(self.input_dtypes[k]) for k,v in self.inputs.items()} - inputs = {k: v.reshape(self.input_shapes[k]).astype(self.input_dtypes[k]) for k,v in inputs.items()} - outputs = self.session.run(None, inputs) - assert len(outputs) == 1, "Only single model outputs are supported" - self.output[:] = outputs[0] - return self.output diff --git a/sunnypilot/modeld/runners/run.h b/sunnypilot/modeld/runners/run.h deleted file mode 100644 index 55e703ef97..0000000000 --- a/sunnypilot/modeld/runners/run.h +++ /dev/null @@ -1,3 +0,0 @@ -#pragma once - -#include "sunnypilot/modeld/runners/runmodel.h" diff --git a/sunnypilot/modeld/runners/runmodel.h b/sunnypilot/modeld/runners/runmodel.h deleted file mode 100644 index 18cc180cb7..0000000000 --- a/sunnypilot/modeld/runners/runmodel.h +++ /dev/null @@ -1,49 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include "common/clutil.h" -#include "common/swaglog.h" - -#define USE_CPU_RUNTIME 0 -#define USE_GPU_RUNTIME 1 -#define USE_DSP_RUNTIME 2 - -struct ModelInput { - const std::string name; - float *buffer; - int size; - - ModelInput(const std::string _name, float *_buffer, int _size) : name(_name), buffer(_buffer), size(_size) {} - virtual void setBuffer(float *_buffer, int _size) { - assert(size == _size || size == 0); - buffer = _buffer; - size = _size; - } -}; - -class RunModel { -public: - std::vector> inputs; - - virtual ~RunModel() {} - virtual void execute() {} - virtual void* getCLBuffer(const std::string name) { return nullptr; } - - virtual void addInput(const std::string name, float *buffer, int size) { - inputs.push_back(std::unique_ptr(new ModelInput(name, buffer, size))); - } - virtual void setInputBuffer(const std::string name, float *buffer, int size) { - for (auto &input : inputs) { - if (name == input->name) { - input->setBuffer(buffer, size); - return; - } - } - LOGE("Tried to update input `%s` but no input with this name exists", name.c_str()); - assert(false); - } -}; diff --git a/sunnypilot/modeld/runners/runmodel.pxd b/sunnypilot/modeld/runners/runmodel.pxd deleted file mode 100644 index b83434473d..0000000000 --- a/sunnypilot/modeld/runners/runmodel.pxd +++ /dev/null @@ -1,14 +0,0 @@ -# distutils: language = c++ - -from libcpp.string cimport string - -cdef extern from "sunnypilot/modeld/runners/runmodel.h": - cdef int USE_CPU_RUNTIME - cdef int USE_GPU_RUNTIME - cdef int USE_DSP_RUNTIME - - cdef cppclass RunModel: - void addInput(string, float*, int) - void setInputBuffer(string, float*, int) - void * getCLBuffer(string) - void execute() diff --git a/sunnypilot/modeld/runners/runmodel_pyx.pxd b/sunnypilot/modeld/runners/runmodel_pyx.pxd deleted file mode 100644 index b6ede7cf37..0000000000 --- a/sunnypilot/modeld/runners/runmodel_pyx.pxd +++ /dev/null @@ -1,6 +0,0 @@ -# distutils: language = c++ - -from .runmodel cimport RunModel as cppRunModel - -cdef class RunModel: - cdef cppRunModel * model diff --git a/sunnypilot/modeld/runners/runmodel_pyx.pyx b/sunnypilot/modeld/runners/runmodel_pyx.pyx deleted file mode 100644 index d2b4485944..0000000000 --- a/sunnypilot/modeld/runners/runmodel_pyx.pyx +++ /dev/null @@ -1,37 +0,0 @@ -# distutils: language = c++ -# cython: c_string_encoding=ascii, language_level=3 - -from libcpp.string cimport string - -from .runmodel cimport USE_CPU_RUNTIME, USE_GPU_RUNTIME, USE_DSP_RUNTIME -from openpilot.sunnypilot.modeld.models.commonmodel_pyx cimport CLMem - -class Runtime: - CPU = USE_CPU_RUNTIME - GPU = USE_GPU_RUNTIME - DSP = USE_DSP_RUNTIME - -cdef class RunModel: - def __dealloc__(self): - del self.model - - def addInput(self, string name, float[:] buffer): - if buffer is not None: - self.model.addInput(name, &buffer[0], len(buffer)) - else: - self.model.addInput(name, NULL, 0) - - def setInputBuffer(self, string name, float[:] buffer): - if buffer is not None: - self.model.setInputBuffer(name, &buffer[0], len(buffer)) - else: - self.model.setInputBuffer(name, NULL, 0) - - def getCLBuffer(self, string name): - cdef void * cl_buf = self.model.getCLBuffer(name) - if not cl_buf: - return None - return CLMem.create(cl_buf) - - def execute(self): - self.model.execute() diff --git a/sunnypilot/modeld/runners/thneedmodel.cc b/sunnypilot/modeld/runners/thneedmodel.cc deleted file mode 100644 index d83fc0a5f9..0000000000 --- a/sunnypilot/modeld/runners/thneedmodel.cc +++ /dev/null @@ -1,58 +0,0 @@ -#include "sunnypilot/modeld/runners/thneedmodel.h" - -#include - -#include "common/swaglog.h" - -ThneedModel::ThneedModel(const std::string path, float *_output, size_t _output_size, int runtime, bool luse_tf8, cl_context context) { - thneed = new Thneed(true, context); - thneed->load(path.c_str()); - thneed->clexec(); - - recorded = false; - output = _output; -} - -void* ThneedModel::getCLBuffer(const std::string name) { - int index = -1; - for (int i = 0; i < inputs.size(); i++) { - if (name == inputs[i]->name) { - index = i; - break; - } - } - - if (index == -1) { - LOGE("Tried to get CL buffer for input `%s` but no input with this name exists", name.c_str()); - assert(false); - } - - if (thneed->input_clmem.size() >= inputs.size()) { - return &thneed->input_clmem[inputs.size() - index - 1]; - } else { - return nullptr; - } -} - -void ThneedModel::execute() { - if (!recorded) { - thneed->record = true; - float *input_buffers[inputs.size()]; - for (int i = 0; i < inputs.size(); i++) { - input_buffers[inputs.size() - i - 1] = inputs[i]->buffer; - } - - thneed->copy_inputs(input_buffers); - thneed->clexec(); - thneed->copy_output(output); - thneed->stop(); - - recorded = true; - } else { - float *input_buffers[inputs.size()]; - for (int i = 0; i < inputs.size(); i++) { - input_buffers[inputs.size() - i - 1] = inputs[i]->buffer; - } - thneed->execute(input_buffers, output); - } -} diff --git a/sunnypilot/modeld/runners/thneedmodel.h b/sunnypilot/modeld/runners/thneedmodel.h deleted file mode 100644 index ecc7207c1f..0000000000 --- a/sunnypilot/modeld/runners/thneedmodel.h +++ /dev/null @@ -1,17 +0,0 @@ -#pragma once - -#include - -#include "sunnypilot/modeld/runners/runmodel.h" -#include "sunnypilot/modeld/thneed/thneed.h" - -class ThneedModel : public RunModel { -public: - ThneedModel(const std::string path, float *_output, size_t _output_size, int runtime, bool use_tf8 = false, cl_context context = NULL); - void *getCLBuffer(const std::string name); - void execute(); -private: - Thneed *thneed = NULL; - bool recorded; - float *output; -}; diff --git a/sunnypilot/modeld/runners/thneedmodel.pxd b/sunnypilot/modeld/runners/thneedmodel.pxd deleted file mode 100644 index ace7443b50..0000000000 --- a/sunnypilot/modeld/runners/thneedmodel.pxd +++ /dev/null @@ -1,9 +0,0 @@ -# distutils: language = c++ - -from libcpp.string cimport string - -from msgq.visionipc.visionipc cimport cl_context - -cdef extern from "sunnypilot/modeld/runners/thneedmodel.h": - cdef cppclass ThneedModel: - ThneedModel(string, float*, size_t, int, bool, cl_context) diff --git a/sunnypilot/modeld/runners/thneedmodel_pyx.pyx b/sunnypilot/modeld/runners/thneedmodel_pyx.pyx deleted file mode 100644 index eaabc4a776..0000000000 --- a/sunnypilot/modeld/runners/thneedmodel_pyx.pyx +++ /dev/null @@ -1,14 +0,0 @@ -# distutils: language = c++ -# cython: c_string_encoding=ascii, language_level=3 - -from libcpp cimport bool -from libcpp.string cimport string - -from .thneedmodel cimport ThneedModel as cppThneedModel -from openpilot.sunnypilot.modeld.models.commonmodel_pyx cimport CLContext -from openpilot.sunnypilot.modeld.runners.runmodel_pyx cimport RunModel -from openpilot.sunnypilot.modeld.runners.runmodel cimport RunModel as cppRunModel - -cdef class ThneedModel(RunModel): - def __cinit__(self, string path, float[:] output, int runtime, bool use_tf8, CLContext context): - self.model = new cppThneedModel(path, &output[0], len(output), runtime, use_tf8, context.context) diff --git a/sunnypilot/modeld/thneed/README b/sunnypilot/modeld/thneed/README deleted file mode 100644 index f3bc66d8fc..0000000000 --- a/sunnypilot/modeld/thneed/README +++ /dev/null @@ -1,8 +0,0 @@ -thneed is an SNPE accelerator. I know SNPE is already an accelerator, but sometimes things need to go even faster.. - -It runs on the local device, and caches a single model run. Then it replays it, but fast. - -thneed slices through abstraction layers like a fish. - -You need a thneed. - diff --git a/sunnypilot/modeld/thneed/__init__.py b/sunnypilot/modeld/thneed/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/sunnypilot/modeld/thneed/clutil_legacy.cc b/sunnypilot/modeld/thneed/clutil_legacy.cc deleted file mode 100644 index 3bf9316432..0000000000 --- a/sunnypilot/modeld/thneed/clutil_legacy.cc +++ /dev/null @@ -1,126 +0,0 @@ -#include "common/clutil.h" - -#include -#include -#include - -#include "common/util.h" -#include "common/swaglog.h" -#include "sunnypilot/modeld/thneed/clutil_legacy.h" - -void cl_print_build_errors(cl_program program, cl_device_id device) { - cl_build_status status; - clGetProgramBuildInfo(program, device, CL_PROGRAM_BUILD_STATUS, sizeof(status), &status, NULL); - size_t log_size; - clGetProgramBuildInfo(program, device, CL_PROGRAM_BUILD_LOG, 0, NULL, &log_size); - std::string log(log_size, '\0'); - clGetProgramBuildInfo(program, device, CL_PROGRAM_BUILD_LOG, log_size, &log[0], NULL); - - LOGE("build failed; status=%d, log: %s", status, log.c_str()); -} - -cl_program cl_program_from_binary(cl_context ctx, cl_device_id device_id, const uint8_t* binary, size_t length, const char* args) { - cl_program prg = CL_CHECK_ERR(clCreateProgramWithBinary(ctx, 1, &device_id, &length, &binary, NULL, &err)); - if (int err = clBuildProgram(prg, 1, &device_id, args, NULL, NULL); err != 0) { - cl_print_build_errors(prg, device_id); - assert(0); - } - return prg; -} - -// Given a cl code and return a string representation -#define CL_ERR_TO_STR(err) case err: return #err -const char* cl_get_error_string(int err) { - switch (err) { - CL_ERR_TO_STR(CL_SUCCESS); - CL_ERR_TO_STR(CL_DEVICE_NOT_FOUND); - CL_ERR_TO_STR(CL_DEVICE_NOT_AVAILABLE); - CL_ERR_TO_STR(CL_COMPILER_NOT_AVAILABLE); - CL_ERR_TO_STR(CL_MEM_OBJECT_ALLOCATION_FAILURE); - CL_ERR_TO_STR(CL_OUT_OF_RESOURCES); - CL_ERR_TO_STR(CL_OUT_OF_HOST_MEMORY); - CL_ERR_TO_STR(CL_PROFILING_INFO_NOT_AVAILABLE); - CL_ERR_TO_STR(CL_MEM_COPY_OVERLAP); - CL_ERR_TO_STR(CL_IMAGE_FORMAT_MISMATCH); - CL_ERR_TO_STR(CL_IMAGE_FORMAT_NOT_SUPPORTED); - CL_ERR_TO_STR(CL_MAP_FAILURE); - CL_ERR_TO_STR(CL_MISALIGNED_SUB_BUFFER_OFFSET); - CL_ERR_TO_STR(CL_EXEC_STATUS_ERROR_FOR_EVENTS_IN_WAIT_LIST); - CL_ERR_TO_STR(CL_COMPILE_PROGRAM_FAILURE); - CL_ERR_TO_STR(CL_LINKER_NOT_AVAILABLE); - CL_ERR_TO_STR(CL_LINK_PROGRAM_FAILURE); - CL_ERR_TO_STR(CL_DEVICE_PARTITION_FAILED); - CL_ERR_TO_STR(CL_KERNEL_ARG_INFO_NOT_AVAILABLE); - CL_ERR_TO_STR(CL_INVALID_VALUE); - CL_ERR_TO_STR(CL_INVALID_DEVICE_TYPE); - CL_ERR_TO_STR(CL_INVALID_PLATFORM); - CL_ERR_TO_STR(CL_INVALID_DEVICE); - CL_ERR_TO_STR(CL_INVALID_CONTEXT); - CL_ERR_TO_STR(CL_INVALID_QUEUE_PROPERTIES); - CL_ERR_TO_STR(CL_INVALID_COMMAND_QUEUE); - CL_ERR_TO_STR(CL_INVALID_HOST_PTR); - CL_ERR_TO_STR(CL_INVALID_MEM_OBJECT); - CL_ERR_TO_STR(CL_INVALID_IMAGE_FORMAT_DESCRIPTOR); - CL_ERR_TO_STR(CL_INVALID_IMAGE_SIZE); - CL_ERR_TO_STR(CL_INVALID_SAMPLER); - CL_ERR_TO_STR(CL_INVALID_BINARY); - CL_ERR_TO_STR(CL_INVALID_BUILD_OPTIONS); - CL_ERR_TO_STR(CL_INVALID_PROGRAM); - CL_ERR_TO_STR(CL_INVALID_PROGRAM_EXECUTABLE); - CL_ERR_TO_STR(CL_INVALID_KERNEL_NAME); - CL_ERR_TO_STR(CL_INVALID_KERNEL_DEFINITION); - CL_ERR_TO_STR(CL_INVALID_KERNEL); - CL_ERR_TO_STR(CL_INVALID_ARG_INDEX); - CL_ERR_TO_STR(CL_INVALID_ARG_VALUE); - CL_ERR_TO_STR(CL_INVALID_ARG_SIZE); - CL_ERR_TO_STR(CL_INVALID_KERNEL_ARGS); - CL_ERR_TO_STR(CL_INVALID_WORK_DIMENSION); - CL_ERR_TO_STR(CL_INVALID_WORK_GROUP_SIZE); - CL_ERR_TO_STR(CL_INVALID_WORK_ITEM_SIZE); - CL_ERR_TO_STR(CL_INVALID_GLOBAL_OFFSET); - CL_ERR_TO_STR(CL_INVALID_EVENT_WAIT_LIST); - CL_ERR_TO_STR(CL_INVALID_EVENT); - CL_ERR_TO_STR(CL_INVALID_OPERATION); - CL_ERR_TO_STR(CL_INVALID_GL_OBJECT); - CL_ERR_TO_STR(CL_INVALID_BUFFER_SIZE); - CL_ERR_TO_STR(CL_INVALID_MIP_LEVEL); - CL_ERR_TO_STR(CL_INVALID_GLOBAL_WORK_SIZE); - CL_ERR_TO_STR(CL_INVALID_PROPERTY); - CL_ERR_TO_STR(CL_INVALID_IMAGE_DESCRIPTOR); - CL_ERR_TO_STR(CL_INVALID_COMPILER_OPTIONS); - CL_ERR_TO_STR(CL_INVALID_LINKER_OPTIONS); - CL_ERR_TO_STR(CL_INVALID_DEVICE_PARTITION_COUNT); - case -69: return "CL_INVALID_PIPE_SIZE"; - case -70: return "CL_INVALID_DEVICE_QUEUE"; - case -71: return "CL_INVALID_SPEC_ID"; - case -72: return "CL_MAX_SIZE_RESTRICTION_EXCEEDED"; - case -1002: return "CL_INVALID_D3D10_DEVICE_KHR"; - case -1003: return "CL_INVALID_D3D10_RESOURCE_KHR"; - case -1004: return "CL_D3D10_RESOURCE_ALREADY_ACQUIRED_KHR"; - case -1005: return "CL_D3D10_RESOURCE_NOT_ACQUIRED_KHR"; - case -1006: return "CL_INVALID_D3D11_DEVICE_KHR"; - case -1007: return "CL_INVALID_D3D11_RESOURCE_KHR"; - case -1008: return "CL_D3D11_RESOURCE_ALREADY_ACQUIRED_KHR"; - case -1009: return "CL_D3D11_RESOURCE_NOT_ACQUIRED_KHR"; - case -1010: return "CL_INVALID_DX9_MEDIA_ADAPTER_KHR"; - case -1011: return "CL_INVALID_DX9_MEDIA_SURFACE_KHR"; - case -1012: return "CL_DX9_MEDIA_SURFACE_ALREADY_ACQUIRED_KHR"; - case -1013: return "CL_DX9_MEDIA_SURFACE_NOT_ACQUIRED_KHR"; - case -1093: return "CL_INVALID_EGL_OBJECT_KHR"; - case -1092: return "CL_EGL_RESOURCE_NOT_ACQUIRED_KHR"; - case -1001: return "CL_PLATFORM_NOT_FOUND_KHR"; - case -1057: return "CL_DEVICE_PARTITION_FAILED_EXT"; - case -1058: return "CL_INVALID_PARTITION_COUNT_EXT"; - case -1059: return "CL_INVALID_PARTITION_NAME_EXT"; - case -1094: return "CL_INVALID_ACCELERATOR_INTEL"; - case -1095: return "CL_INVALID_ACCELERATOR_TYPE_INTEL"; - case -1096: return "CL_INVALID_ACCELERATOR_DESCRIPTOR_INTEL"; - case -1097: return "CL_ACCELERATOR_TYPE_NOT_SUPPORTED_INTEL"; - case -1000: return "CL_INVALID_GL_SHAREGROUP_REFERENCE_KHR"; - case -1098: return "CL_INVALID_VA_API_MEDIA_ADAPTER_INTEL"; - case -1099: return "CL_INVALID_VA_API_MEDIA_SURFACE_INTEL"; - case -1100: return "CL_VA_API_MEDIA_SURFACE_ALREADY_ACQUIRED_INTEL"; - case -1101: return "CL_VA_API_MEDIA_SURFACE_NOT_ACQUIRED_INTEL"; - default: return "CL_UNKNOWN_ERROR"; - } -} \ No newline at end of file diff --git a/sunnypilot/modeld/thneed/clutil_legacy.h b/sunnypilot/modeld/thneed/clutil_legacy.h deleted file mode 100644 index 8aebdcf1a9..0000000000 --- a/sunnypilot/modeld/thneed/clutil_legacy.h +++ /dev/null @@ -1,13 +0,0 @@ -#pragma once - -#include "common/clutil.h" -#ifdef __APPLE__ -#include -#else -#include -#endif - -#include - -cl_program cl_program_from_binary(cl_context ctx, cl_device_id device_id, const uint8_t* binary, size_t length, const char* args = nullptr); -const char* cl_get_error_string(int err); \ No newline at end of file diff --git a/sunnypilot/modeld/thneed/serialize.cc b/sunnypilot/modeld/thneed/serialize.cc deleted file mode 100644 index 9279a90e0f..0000000000 --- a/sunnypilot/modeld/thneed/serialize.cc +++ /dev/null @@ -1,155 +0,0 @@ -#include -#include - -#include "third_party/json11/json11.hpp" -#include "common/util.h" -#include "common/clutil.h" -#include "common/swaglog.h" -#include "sunnypilot/modeld/thneed/thneed.h" -#include "sunnypilot/modeld/thneed/clutil_legacy.h" -using namespace json11; - -extern map g_program_source; - -void Thneed::load(const char *filename) { - LOGD("Thneed::load: loading from %s\n", filename); - - string buf = util::read_file(filename); - int jsz = *(int *)buf.data(); - string jsonerr; - string jj(buf.data() + sizeof(int), jsz); - Json jdat = Json::parse(jj, jsonerr); - - map real_mem; - real_mem[NULL] = NULL; - - int ptr = sizeof(int)+jsz; - for (auto &obj : jdat["objects"].array_items()) { - auto mobj = obj.object_items(); - int sz = mobj["size"].int_value(); - cl_mem clbuf = NULL; - - if (mobj["buffer_id"].string_value().size() > 0) { - // image buffer must already be allocated - clbuf = real_mem[*(cl_mem*)(mobj["buffer_id"].string_value().data())]; - assert(mobj["needs_load"].bool_value() == false); - } else { - if (mobj["needs_load"].bool_value()) { - clbuf = clCreateBuffer(context, CL_MEM_COPY_HOST_PTR | CL_MEM_READ_WRITE, sz, &buf[ptr], NULL); - if (debug >= 1) printf("loading %p %d @ 0x%X\n", clbuf, sz, ptr); - ptr += sz; - } else { - // TODO: is there a faster way to init zeroed out buffers? - void *host_zeros = calloc(sz, 1); - clbuf = clCreateBuffer(context, CL_MEM_COPY_HOST_PTR | CL_MEM_READ_WRITE, sz, host_zeros, NULL); - free(host_zeros); - } - } - assert(clbuf != NULL); - - if (mobj["arg_type"] == "image2d_t" || mobj["arg_type"] == "image1d_t") { - cl_image_desc desc = {0}; - desc.image_type = (mobj["arg_type"] == "image2d_t") ? CL_MEM_OBJECT_IMAGE2D : CL_MEM_OBJECT_IMAGE1D_BUFFER; - desc.image_width = mobj["width"].int_value(); - desc.image_height = mobj["height"].int_value(); - desc.image_row_pitch = mobj["row_pitch"].int_value(); - assert(sz == desc.image_height*desc.image_row_pitch); -#ifdef QCOM2 - desc.buffer = clbuf; -#else - // TODO: we are creating unused buffers on PC - clReleaseMemObject(clbuf); -#endif - cl_image_format format = {0}; - format.image_channel_order = CL_RGBA; - format.image_channel_data_type = mobj["float32"].bool_value() ? CL_FLOAT : CL_HALF_FLOAT; - - cl_int errcode; - -#ifndef QCOM2 - if (mobj["needs_load"].bool_value()) { - clbuf = clCreateImage(context, CL_MEM_COPY_HOST_PTR | CL_MEM_READ_WRITE, &format, &desc, &buf[ptr-sz], &errcode); - } else { - clbuf = clCreateImage(context, CL_MEM_READ_WRITE, &format, &desc, NULL, &errcode); - } -#else - clbuf = clCreateImage(context, CL_MEM_READ_WRITE, &format, &desc, NULL, &errcode); -#endif - if (clbuf == NULL) { - LOGE("clError: %s create image %zux%zu rp %zu with buffer %p\n", cl_get_error_string(errcode), - desc.image_width, desc.image_height, desc.image_row_pitch, desc.buffer); - } - assert(clbuf != NULL); - } - - real_mem[*(cl_mem*)(mobj["id"].string_value().data())] = clbuf; - } - - map g_programs; - for (const auto &[name, source] : jdat["programs"].object_items()) { - if (debug >= 1) printf("building %s with size %zu\n", name.c_str(), source.string_value().size()); - g_programs[name] = cl_program_from_source(context, device_id, source.string_value()); - } - - for (auto &obj : jdat["inputs"].array_items()) { - auto mobj = obj.object_items(); - int sz = mobj["size"].int_value(); - cl_mem aa = real_mem[*(cl_mem*)(mobj["buffer_id"].string_value().data())]; - input_clmem.push_back(aa); - input_sizes.push_back(sz); - LOGD("Thneed::load: adding input %s with size %d\n", mobj["name"].string_value().data(), sz); - - cl_int cl_err; - void *ret = clEnqueueMapBuffer(command_queue, aa, CL_TRUE, CL_MAP_WRITE, 0, sz, 0, NULL, NULL, &cl_err); - if (cl_err != CL_SUCCESS) LOGE("clError: %s map %p %d\n", cl_get_error_string(cl_err), aa, sz); - assert(cl_err == CL_SUCCESS); - inputs.push_back(ret); - } - - for (auto &obj : jdat["outputs"].array_items()) { - auto mobj = obj.object_items(); - int sz = mobj["size"].int_value(); - LOGD("Thneed::save: adding output with size %d\n", sz); - // TODO: support multiple outputs - output = real_mem[*(cl_mem*)(mobj["buffer_id"].string_value().data())]; - assert(output != NULL); - } - - for (auto &obj : jdat["binaries"].array_items()) { - string name = obj["name"].string_value(); - size_t length = obj["length"].int_value(); - if (debug >= 1) printf("binary %s with size %zu\n", name.c_str(), length); - g_programs[name] = cl_program_from_binary(context, device_id, (const uint8_t*)&buf[ptr], length); - ptr += length; - } - - for (auto &obj : jdat["kernels"].array_items()) { - auto gws = obj["global_work_size"]; - auto lws = obj["local_work_size"]; - auto kk = shared_ptr(new CLQueuedKernel(this)); - - kk->name = obj["name"].string_value(); - kk->program = g_programs[kk->name]; - kk->work_dim = obj["work_dim"].int_value(); - for (int i = 0; i < kk->work_dim; i++) { - kk->global_work_size[i] = gws[i].int_value(); - kk->local_work_size[i] = lws[i].int_value(); - } - kk->num_args = obj["num_args"].int_value(); - for (int i = 0; i < kk->num_args; i++) { - string arg = obj["args"].array_items()[i].string_value(); - int arg_size = obj["args_size"].array_items()[i].int_value(); - kk->args_size.push_back(arg_size); - if (arg_size == 8) { - cl_mem val = *(cl_mem*)(arg.data()); - val = real_mem[val]; - kk->args.push_back(string((char*)&val, sizeof(val))); - } else { - kk->args.push_back(arg); - } - } - kq.push_back(kk); - } - - clFinish(command_queue); -} diff --git a/sunnypilot/modeld/thneed/thneed.h b/sunnypilot/modeld/thneed/thneed.h deleted file mode 100644 index 47e18e0be3..0000000000 --- a/sunnypilot/modeld/thneed/thneed.h +++ /dev/null @@ -1,133 +0,0 @@ -#pragma once - -#ifndef __user -#define __user __attribute__(()) -#endif - -#include -#include -#include -#include -#include - -#include - -#include "third_party/linux/include/msm_kgsl.h" - -using namespace std; - -cl_int thneed_clSetKernelArg(cl_kernel kernel, cl_uint arg_index, size_t arg_size, const void *arg_value); - -namespace json11 { - class Json; -} -class Thneed; - -class GPUMalloc { - public: - GPUMalloc(int size, int fd); - ~GPUMalloc(); - void *alloc(int size); - private: - uint64_t base; - int remaining; -}; - -class CLQueuedKernel { - public: - CLQueuedKernel(Thneed *lthneed) { thneed = lthneed; } - CLQueuedKernel(Thneed *lthneed, - cl_kernel _kernel, - cl_uint _work_dim, - const size_t *_global_work_size, - const size_t *_local_work_size); - cl_int exec(); - void debug_print(bool verbose); - int get_arg_num(const char *search_arg_name); - cl_program program; - string name; - cl_uint num_args; - vector arg_names; - vector arg_types; - vector args; - vector args_size; - cl_kernel kernel = NULL; - json11::Json to_json() const; - - cl_uint work_dim; - size_t global_work_size[3] = {0}; - size_t local_work_size[3] = {0}; - private: - Thneed *thneed; -}; - -class CachedIoctl { - public: - virtual void exec() {} -}; - -class CachedSync: public CachedIoctl { - public: - CachedSync(Thneed *lthneed, string ldata) { thneed = lthneed; data = ldata; } - void exec(); - private: - Thneed *thneed; - string data; -}; - -class CachedCommand: public CachedIoctl { - public: - CachedCommand(Thneed *lthneed, struct kgsl_gpu_command *cmd); - void exec(); - private: - void disassemble(int cmd_index); - struct kgsl_gpu_command cache; - unique_ptr cmds; - unique_ptr objs; - Thneed *thneed; - vector > kq; -}; - -class Thneed { - public: - Thneed(bool do_clinit=false, cl_context _context = NULL); - void stop(); - void execute(float **finputs, float *foutput, bool slow=false); - void wait(); - - vector input_clmem; - vector inputs; - vector input_sizes; - cl_mem output = NULL; - - cl_context context = NULL; - cl_command_queue command_queue; - cl_device_id device_id; - int context_id; - - // protected? - bool record = false; - int debug; - int timestamp; - -#ifdef QCOM2 - unique_ptr ram; - vector > cmds; - int fd; -#endif - - // all CL kernels - void copy_inputs(float **finputs, bool internal=false); - void copy_output(float *foutput); - cl_int clexec(); - vector > kq; - - // pending CL kernels - vector > ckq; - - // loading - void load(const char *filename); - private: - void clinit(); -}; - diff --git a/sunnypilot/modeld/thneed/thneed_common.cc b/sunnypilot/modeld/thneed/thneed_common.cc deleted file mode 100644 index 56fbe70d8f..0000000000 --- a/sunnypilot/modeld/thneed/thneed_common.cc +++ /dev/null @@ -1,216 +0,0 @@ -#include "sunnypilot/modeld/thneed/thneed.h" - -#include -#include -#include - -#include "common/clutil.h" -#include "common/timing.h" - -map, string> g_args; -map, int> g_args_size; -map g_program_source; - -void Thneed::stop() { - //printf("Thneed::stop: recorded %lu commands\n", cmds.size()); - record = false; -} - -void Thneed::clinit() { - device_id = cl_get_device_id(CL_DEVICE_TYPE_DEFAULT); - if (context == NULL) context = CL_CHECK_ERR(clCreateContext(NULL, 1, &device_id, NULL, NULL, &err)); - //cl_command_queue_properties props[3] = {CL_QUEUE_PROPERTIES, CL_QUEUE_PROFILING_ENABLE, 0}; - cl_command_queue_properties props[3] = {CL_QUEUE_PROPERTIES, 0, 0}; - command_queue = CL_CHECK_ERR(clCreateCommandQueueWithProperties(context, device_id, props, &err)); - printf("Thneed::clinit done\n"); -} - -cl_int Thneed::clexec() { - if (debug >= 1) printf("Thneed::clexec: running %lu queued kernels\n", kq.size()); - for (auto &k : kq) { - if (record) ckq.push_back(k); - cl_int ret = k->exec(); - assert(ret == CL_SUCCESS); - } - return clFinish(command_queue); -} - -void Thneed::copy_inputs(float **finputs, bool internal) { - for (int idx = 0; idx < inputs.size(); ++idx) { - if (debug >= 1) printf("copying %lu -- %p -> %p (cl %p)\n", input_sizes[idx], finputs[idx], inputs[idx], input_clmem[idx]); - - if (internal) { - // if it's internal, using memcpy is fine since the buffer sync is cached in the ioctl layer - if (finputs[idx] != NULL) memcpy(inputs[idx], finputs[idx], input_sizes[idx]); - } else { - if (finputs[idx] != NULL) CL_CHECK(clEnqueueWriteBuffer(command_queue, input_clmem[idx], CL_TRUE, 0, input_sizes[idx], finputs[idx], 0, NULL, NULL)); - } - } -} - -void Thneed::copy_output(float *foutput) { - if (output != NULL) { - size_t sz; - clGetMemObjectInfo(output, CL_MEM_SIZE, sizeof(sz), &sz, NULL); - if (debug >= 1) printf("copying %lu for output %p -> %p\n", sz, output, foutput); - CL_CHECK(clEnqueueReadBuffer(command_queue, output, CL_TRUE, 0, sz, foutput, 0, NULL, NULL)); - } else { - printf("CAUTION: model output is NULL, does it have no outputs?\n"); - } -} - -// *********** CLQueuedKernel *********** - -CLQueuedKernel::CLQueuedKernel(Thneed *lthneed, - cl_kernel _kernel, - cl_uint _work_dim, - const size_t *_global_work_size, - const size_t *_local_work_size) { - thneed = lthneed; - kernel = _kernel; - work_dim = _work_dim; - assert(work_dim <= 3); - for (int i = 0; i < work_dim; i++) { - global_work_size[i] = _global_work_size[i]; - local_work_size[i] = _local_work_size[i]; - } - - char _name[0x100]; - clGetKernelInfo(kernel, CL_KERNEL_FUNCTION_NAME, sizeof(_name), _name, NULL); - name = string(_name); - clGetKernelInfo(kernel, CL_KERNEL_NUM_ARGS, sizeof(num_args), &num_args, NULL); - - // get args - for (int i = 0; i < num_args; i++) { - char arg_name[0x100] = {0}; - clGetKernelArgInfo(kernel, i, CL_KERNEL_ARG_NAME, sizeof(arg_name), arg_name, NULL); - arg_names.push_back(string(arg_name)); - clGetKernelArgInfo(kernel, i, CL_KERNEL_ARG_TYPE_NAME, sizeof(arg_name), arg_name, NULL); - arg_types.push_back(string(arg_name)); - - args.push_back(g_args[make_pair(kernel, i)]); - args_size.push_back(g_args_size[make_pair(kernel, i)]); - } - - // get program - clGetKernelInfo(kernel, CL_KERNEL_PROGRAM, sizeof(program), &program, NULL); -} - -int CLQueuedKernel::get_arg_num(const char *search_arg_name) { - for (int i = 0; i < num_args; i++) { - if (arg_names[i] == search_arg_name) return i; - } - printf("failed to find %s in %s\n", search_arg_name, name.c_str()); - assert(false); -} - -cl_int CLQueuedKernel::exec() { - if (kernel == NULL) { - kernel = clCreateKernel(program, name.c_str(), NULL); - arg_names.clear(); - arg_types.clear(); - - for (int j = 0; j < num_args; j++) { - char arg_name[0x100] = {0}; - clGetKernelArgInfo(kernel, j, CL_KERNEL_ARG_NAME, sizeof(arg_name), arg_name, NULL); - arg_names.push_back(string(arg_name)); - clGetKernelArgInfo(kernel, j, CL_KERNEL_ARG_TYPE_NAME, sizeof(arg_name), arg_name, NULL); - arg_types.push_back(string(arg_name)); - - cl_int ret; - if (args[j].size() != 0) { - assert(args[j].size() == args_size[j]); - ret = thneed_clSetKernelArg(kernel, j, args[j].size(), args[j].data()); - } else { - ret = thneed_clSetKernelArg(kernel, j, args_size[j], NULL); - } - assert(ret == CL_SUCCESS); - } - } - - if (thneed->debug >= 1) { - debug_print(thneed->debug >= 2); - } - - return clEnqueueNDRangeKernel(thneed->command_queue, - kernel, work_dim, NULL, global_work_size, local_work_size, 0, NULL, NULL); -} - -void CLQueuedKernel::debug_print(bool verbose) { - printf("%p %56s -- ", kernel, name.c_str()); - for (int i = 0; i < work_dim; i++) { - printf("%4zu ", global_work_size[i]); - } - printf(" -- "); - for (int i = 0; i < work_dim; i++) { - printf("%4zu ", local_work_size[i]); - } - printf("\n"); - - if (verbose) { - for (int i = 0; i < num_args; i++) { - string arg = args[i]; - printf(" %s %s", arg_types[i].c_str(), arg_names[i].c_str()); - void *arg_value = (void*)arg.data(); - int arg_size = arg.size(); - if (arg_size == 0) { - printf(" (size) %d", args_size[i]); - } else if (arg_size == 1) { - printf(" = %d", *((char*)arg_value)); - } else if (arg_size == 2) { - printf(" = %d", *((short*)arg_value)); - } else if (arg_size == 4) { - if (arg_types[i] == "float") { - printf(" = %f", *((float*)arg_value)); - } else { - printf(" = %d", *((int*)arg_value)); - } - } else if (arg_size == 8) { - cl_mem val = (cl_mem)(*((uintptr_t*)arg_value)); - printf(" = %p", val); - if (val != NULL) { - cl_mem_object_type obj_type; - clGetMemObjectInfo(val, CL_MEM_TYPE, sizeof(obj_type), &obj_type, NULL); - if (arg_types[i] == "image2d_t" || arg_types[i] == "image1d_t" || obj_type == CL_MEM_OBJECT_IMAGE2D) { - cl_image_format format; - size_t width, height, depth, array_size, row_pitch, slice_pitch; - cl_mem buf; - clGetImageInfo(val, CL_IMAGE_FORMAT, sizeof(format), &format, NULL); - assert(format.image_channel_order == CL_RGBA); - assert(format.image_channel_data_type == CL_HALF_FLOAT || format.image_channel_data_type == CL_FLOAT); - clGetImageInfo(val, CL_IMAGE_WIDTH, sizeof(width), &width, NULL); - clGetImageInfo(val, CL_IMAGE_HEIGHT, sizeof(height), &height, NULL); - clGetImageInfo(val, CL_IMAGE_ROW_PITCH, sizeof(row_pitch), &row_pitch, NULL); - clGetImageInfo(val, CL_IMAGE_DEPTH, sizeof(depth), &depth, NULL); - clGetImageInfo(val, CL_IMAGE_ARRAY_SIZE, sizeof(array_size), &array_size, NULL); - clGetImageInfo(val, CL_IMAGE_SLICE_PITCH, sizeof(slice_pitch), &slice_pitch, NULL); - assert(depth == 0); - assert(array_size == 0); - assert(slice_pitch == 0); - - clGetImageInfo(val, CL_IMAGE_BUFFER, sizeof(buf), &buf, NULL); - size_t sz = 0; - if (buf != NULL) clGetMemObjectInfo(buf, CL_MEM_SIZE, sizeof(sz), &sz, NULL); - printf(" image %zu x %zu rp %zu @ %p buffer %zu", width, height, row_pitch, buf, sz); - } else { - size_t sz; - clGetMemObjectInfo(val, CL_MEM_SIZE, sizeof(sz), &sz, NULL); - printf(" buffer %zu", sz); - } - } - } - printf("\n"); - } - } -} - -cl_int thneed_clSetKernelArg(cl_kernel kernel, cl_uint arg_index, size_t arg_size, const void *arg_value) { - g_args_size[make_pair(kernel, arg_index)] = arg_size; - if (arg_value != NULL) { - g_args[make_pair(kernel, arg_index)] = string((char*)arg_value, arg_size); - } else { - g_args[make_pair(kernel, arg_index)] = string(""); - } - cl_int ret = clSetKernelArg(kernel, arg_index, arg_size, arg_value); - return ret; -} diff --git a/sunnypilot/modeld/thneed/thneed_pc.cc b/sunnypilot/modeld/thneed/thneed_pc.cc deleted file mode 100644 index 629d0ee359..0000000000 --- a/sunnypilot/modeld/thneed/thneed_pc.cc +++ /dev/null @@ -1,32 +0,0 @@ -#include "sunnypilot/modeld/thneed/thneed.h" - -#include - -#include "common/clutil.h" -#include "common/timing.h" - -Thneed::Thneed(bool do_clinit, cl_context _context) { - context = _context; - if (do_clinit) clinit(); - char *thneed_debug_env = getenv("THNEED_DEBUG"); - debug = (thneed_debug_env != NULL) ? atoi(thneed_debug_env) : 0; -} - -void Thneed::execute(float **finputs, float *foutput, bool slow) { - uint64_t tb, te; - if (debug >= 1) tb = nanos_since_boot(); - - // ****** copy inputs - copy_inputs(finputs); - - // ****** run commands - clexec(); - - // ****** copy outputs - copy_output(foutput); - - if (debug >= 1) { - te = nanos_since_boot(); - printf("model exec in %lu us\n", (te-tb)/1000); - } -} diff --git a/sunnypilot/modeld/thneed/thneed_qcom2.cc b/sunnypilot/modeld/thneed/thneed_qcom2.cc deleted file mode 100644 index b940da1ce1..0000000000 --- a/sunnypilot/modeld/thneed/thneed_qcom2.cc +++ /dev/null @@ -1,258 +0,0 @@ -#include "sunnypilot/modeld/thneed/thneed.h" - -#include -#include - -#include -#include -#include -#include -#include - -#include "common/clutil.h" -#include "common/timing.h" - -Thneed *g_thneed = NULL; -int g_fd = -1; - -void hexdump(uint8_t *d, int len) { - assert((len%4) == 0); - printf(" dumping %p len 0x%x\n", d, len); - for (int i = 0; i < len/4; i++) { - if (i != 0 && (i%0x10) == 0) printf("\n"); - printf("%8x ", d[i]); - } - printf("\n"); -} - -// *********** ioctl interceptor *********** - -extern "C" { - -int (*my_ioctl)(int filedes, unsigned long request, void *argp) = NULL; -#undef ioctl -int ioctl(int filedes, unsigned long request, void *argp) { - request &= 0xFFFFFFFF; // needed on QCOM2 - if (my_ioctl == NULL) my_ioctl = reinterpret_cast(dlsym(RTLD_NEXT, "ioctl")); - Thneed *thneed = g_thneed; - - // save the fd - if (request == IOCTL_KGSL_GPUOBJ_ALLOC) g_fd = filedes; - - // note that this runs always, even without a thneed object - if (request == IOCTL_KGSL_DRAWCTXT_CREATE) { - struct kgsl_drawctxt_create *create = (struct kgsl_drawctxt_create *)argp; - create->flags &= ~KGSL_CONTEXT_PRIORITY_MASK; - create->flags |= 6 << KGSL_CONTEXT_PRIORITY_SHIFT; // priority from 1-15, 1 is max priority - printf("IOCTL_KGSL_DRAWCTXT_CREATE: creating context with flags 0x%x\n", create->flags); - } - - if (thneed != NULL) { - if (request == IOCTL_KGSL_GPU_COMMAND) { - struct kgsl_gpu_command *cmd = (struct kgsl_gpu_command *)argp; - if (thneed->record) { - thneed->timestamp = cmd->timestamp; - thneed->context_id = cmd->context_id; - thneed->cmds.push_back(unique_ptr(new CachedCommand(thneed, cmd))); - } - if (thneed->debug >= 1) { - printf("IOCTL_KGSL_GPU_COMMAND(%2zu): flags: 0x%lx context_id: %u timestamp: %u numcmds: %d numobjs: %d\n", - thneed->cmds.size(), - cmd->flags, - cmd->context_id, cmd->timestamp, cmd->numcmds, cmd->numobjs); - } - } else if (request == IOCTL_KGSL_GPUOBJ_SYNC) { - struct kgsl_gpuobj_sync *cmd = (struct kgsl_gpuobj_sync *)argp; - struct kgsl_gpuobj_sync_obj *objs = (struct kgsl_gpuobj_sync_obj *)(cmd->objs); - - if (thneed->debug >= 2) { - printf("IOCTL_KGSL_GPUOBJ_SYNC count:%d ", cmd->count); - for (int i = 0; i < cmd->count; i++) { - printf(" -- offset:0x%lx len:0x%lx id:%d op:%d ", objs[i].offset, objs[i].length, objs[i].id, objs[i].op); - } - printf("\n"); - } - - if (thneed->record) { - thneed->cmds.push_back(unique_ptr(new - CachedSync(thneed, string((char *)objs, sizeof(struct kgsl_gpuobj_sync_obj)*cmd->count)))); - } - } else if (request == IOCTL_KGSL_DEVICE_WAITTIMESTAMP_CTXTID) { - struct kgsl_device_waittimestamp_ctxtid *cmd = (struct kgsl_device_waittimestamp_ctxtid *)argp; - if (thneed->debug >= 1) { - printf("IOCTL_KGSL_DEVICE_WAITTIMESTAMP_CTXTID: context_id: %d timestamp: %d timeout: %d\n", - cmd->context_id, cmd->timestamp, cmd->timeout); - } - } else if (request == IOCTL_KGSL_SETPROPERTY) { - if (thneed->debug >= 1) { - struct kgsl_device_getproperty *prop = (struct kgsl_device_getproperty *)argp; - printf("IOCTL_KGSL_SETPROPERTY: 0x%x sizebytes:%zu\n", prop->type, prop->sizebytes); - if (thneed->debug >= 2) { - hexdump((uint8_t *)prop->value, prop->sizebytes); - if (prop->type == KGSL_PROP_PWR_CONSTRAINT) { - struct kgsl_device_constraint *constraint = (struct kgsl_device_constraint *)prop->value; - hexdump((uint8_t *)constraint->data, constraint->size); - } - } - } - } else if (request == IOCTL_KGSL_DRAWCTXT_CREATE || request == IOCTL_KGSL_DRAWCTXT_DESTROY) { - // this happens - } else if (request == IOCTL_KGSL_GPUOBJ_ALLOC || request == IOCTL_KGSL_GPUOBJ_FREE) { - // this happens - } else { - if (thneed->debug >= 1) { - printf("other ioctl %lx\n", request); - } - } - } - - int ret = my_ioctl(filedes, request, argp); - // NOTE: This error message goes into stdout and messes up pyenv - // if (ret != 0) printf("ioctl returned %d with errno %d\n", ret, errno); - return ret; -} - -} - -// *********** GPUMalloc *********** - -GPUMalloc::GPUMalloc(int size, int fd) { - struct kgsl_gpuobj_alloc alloc; - memset(&alloc, 0, sizeof(alloc)); - alloc.size = size; - alloc.flags = 0x10000a00; - ioctl(fd, IOCTL_KGSL_GPUOBJ_ALLOC, &alloc); - void *addr = mmap64(NULL, alloc.mmapsize, 0x3, 0x1, fd, alloc.id*0x1000); - assert(addr != MAP_FAILED); - - base = (uint64_t)addr; - remaining = size; -} - -GPUMalloc::~GPUMalloc() { - // TODO: free the GPU malloced area -} - -void *GPUMalloc::alloc(int size) { - void *ret = (void*)base; - size = (size+0xff) & (~0xFF); - assert(size <= remaining); - remaining -= size; - base += size; - return ret; -} - -// *********** CachedSync, at the ioctl layer *********** - -void CachedSync::exec() { - struct kgsl_gpuobj_sync cmd; - - cmd.objs = (uint64_t)data.data(); - cmd.obj_len = data.length(); - cmd.count = data.length() / sizeof(struct kgsl_gpuobj_sync_obj); - - int ret = ioctl(thneed->fd, IOCTL_KGSL_GPUOBJ_SYNC, &cmd); - assert(ret == 0); -} - -// *********** CachedCommand, at the ioctl layer *********** - -CachedCommand::CachedCommand(Thneed *lthneed, struct kgsl_gpu_command *cmd) { - thneed = lthneed; - assert(cmd->numsyncs == 0); - - memcpy(&cache, cmd, sizeof(cache)); - - if (cmd->numcmds > 0) { - cmds = make_unique(cmd->numcmds); - memcpy(cmds.get(), (void *)cmd->cmdlist, sizeof(struct kgsl_command_object)*cmd->numcmds); - cache.cmdlist = (uint64_t)cmds.get(); - for (int i = 0; i < cmd->numcmds; i++) { - void *nn = thneed->ram->alloc(cmds[i].size); - memcpy(nn, (void*)cmds[i].gpuaddr, cmds[i].size); - cmds[i].gpuaddr = (uint64_t)nn; - } - } - - if (cmd->numobjs > 0) { - objs = make_unique(cmd->numobjs); - memcpy(objs.get(), (void *)cmd->objlist, sizeof(struct kgsl_command_object)*cmd->numobjs); - cache.objlist = (uint64_t)objs.get(); - for (int i = 0; i < cmd->numobjs; i++) { - void *nn = thneed->ram->alloc(objs[i].size); - memset(nn, 0, objs[i].size); - objs[i].gpuaddr = (uint64_t)nn; - } - } - - kq = thneed->ckq; - thneed->ckq.clear(); -} - -void CachedCommand::exec() { - cache.timestamp = ++thneed->timestamp; - int ret = ioctl(thneed->fd, IOCTL_KGSL_GPU_COMMAND, &cache); - - if (thneed->debug >= 1) printf("CachedCommand::exec got %d\n", ret); - - if (thneed->debug >= 2) { - for (auto &it : kq) { - it->debug_print(false); - } - } - - assert(ret == 0); -} - -// *********** Thneed *********** - -Thneed::Thneed(bool do_clinit, cl_context _context) { - // TODO: QCOM2 actually requires a different context - //context = _context; - if (do_clinit) clinit(); - assert(g_fd != -1); - fd = g_fd; - ram = make_unique(0x80000, fd); - timestamp = -1; - g_thneed = this; - char *thneed_debug_env = getenv("THNEED_DEBUG"); - debug = (thneed_debug_env != NULL) ? atoi(thneed_debug_env) : 0; -} - -void Thneed::wait() { - struct kgsl_device_waittimestamp_ctxtid wait; - wait.context_id = context_id; - wait.timestamp = timestamp; - wait.timeout = -1; - - uint64_t tb = nanos_since_boot(); - int wret = ioctl(fd, IOCTL_KGSL_DEVICE_WAITTIMESTAMP_CTXTID, &wait); - uint64_t te = nanos_since_boot(); - - if (debug >= 1) printf("wait %d after %lu us\n", wret, (te-tb)/1000); -} - -void Thneed::execute(float **finputs, float *foutput, bool slow) { - uint64_t tb, te; - if (debug >= 1) tb = nanos_since_boot(); - - // ****** copy inputs - copy_inputs(finputs, true); - - // ****** run commands - int i = 0; - for (auto &it : cmds) { - ++i; - if (debug >= 1) printf("run %2d @ %7lu us: ", i, (nanos_since_boot()-tb)/1000); - it->exec(); - if ((i == cmds.size()) || slow) wait(); - } - - // ****** copy outputs - copy_output(foutput); - - if (debug >= 1) { - te = nanos_since_boot(); - printf("model exec in %lu us\n", (te-tb)/1000); - } -} diff --git a/sunnypilot/modeld/transforms/loadyuv.cc b/sunnypilot/modeld/transforms/loadyuv.cc deleted file mode 100644 index f7d571416c..0000000000 --- a/sunnypilot/modeld/transforms/loadyuv.cc +++ /dev/null @@ -1,74 +0,0 @@ -#include "sunnypilot/modeld/transforms/loadyuv.h" - -#include -#include -#include - -void loadyuv_init(LoadYUVState* s, cl_context ctx, cl_device_id device_id, int width, int height) { - memset(s, 0, sizeof(*s)); - - s->width = width; - s->height = height; - - char args[1024]; - snprintf(args, sizeof(args), - "-cl-fast-relaxed-math -cl-denorms-are-zero " - "-DTRANSFORMED_WIDTH=%d -DTRANSFORMED_HEIGHT=%d", - width, height); - cl_program prg = cl_program_from_file(ctx, device_id, LOADYUV_PATH, args); - - s->loadys_krnl = CL_CHECK_ERR(clCreateKernel(prg, "loadys", &err)); - s->loaduv_krnl = CL_CHECK_ERR(clCreateKernel(prg, "loaduv", &err)); - s->copy_krnl = CL_CHECK_ERR(clCreateKernel(prg, "copy", &err)); - - // done with this - CL_CHECK(clReleaseProgram(prg)); -} - -void loadyuv_destroy(LoadYUVState* s) { - CL_CHECK(clReleaseKernel(s->loadys_krnl)); - CL_CHECK(clReleaseKernel(s->loaduv_krnl)); - CL_CHECK(clReleaseKernel(s->copy_krnl)); -} - -void loadyuv_queue(LoadYUVState* s, cl_command_queue q, - cl_mem y_cl, cl_mem u_cl, cl_mem v_cl, - cl_mem out_cl, bool do_shift) { - cl_int global_out_off = 0; - if (do_shift) { - // shift the image in slot 1 to slot 0, then place the new image in slot 1 - global_out_off += (s->width*s->height) + (s->width/2)*(s->height/2)*2; - CL_CHECK(clSetKernelArg(s->copy_krnl, 0, sizeof(cl_mem), &out_cl)); - CL_CHECK(clSetKernelArg(s->copy_krnl, 1, sizeof(cl_int), &global_out_off)); - const size_t copy_work_size = global_out_off/8; - CL_CHECK(clEnqueueNDRangeKernel(q, s->copy_krnl, 1, NULL, - ©_work_size, NULL, 0, 0, NULL)); - } - - CL_CHECK(clSetKernelArg(s->loadys_krnl, 0, sizeof(cl_mem), &y_cl)); - CL_CHECK(clSetKernelArg(s->loadys_krnl, 1, sizeof(cl_mem), &out_cl)); - CL_CHECK(clSetKernelArg(s->loadys_krnl, 2, sizeof(cl_int), &global_out_off)); - - const size_t loadys_work_size = (s->width*s->height)/8; - CL_CHECK(clEnqueueNDRangeKernel(q, s->loadys_krnl, 1, NULL, - &loadys_work_size, NULL, 0, 0, NULL)); - - const size_t loaduv_work_size = ((s->width/2)*(s->height/2))/8; - global_out_off += (s->width*s->height); - - CL_CHECK(clSetKernelArg(s->loaduv_krnl, 0, sizeof(cl_mem), &u_cl)); - CL_CHECK(clSetKernelArg(s->loaduv_krnl, 1, sizeof(cl_mem), &out_cl)); - CL_CHECK(clSetKernelArg(s->loaduv_krnl, 2, sizeof(cl_int), &global_out_off)); - - CL_CHECK(clEnqueueNDRangeKernel(q, s->loaduv_krnl, 1, NULL, - &loaduv_work_size, NULL, 0, 0, NULL)); - - global_out_off += (s->width/2)*(s->height/2); - - CL_CHECK(clSetKernelArg(s->loaduv_krnl, 0, sizeof(cl_mem), &v_cl)); - CL_CHECK(clSetKernelArg(s->loaduv_krnl, 1, sizeof(cl_mem), &out_cl)); - CL_CHECK(clSetKernelArg(s->loaduv_krnl, 2, sizeof(cl_int), &global_out_off)); - - CL_CHECK(clEnqueueNDRangeKernel(q, s->loaduv_krnl, 1, NULL, - &loaduv_work_size, NULL, 0, 0, NULL)); -} diff --git a/sunnypilot/modeld/transforms/loadyuv.cl b/sunnypilot/modeld/transforms/loadyuv.cl deleted file mode 100644 index 7dd3d973a3..0000000000 --- a/sunnypilot/modeld/transforms/loadyuv.cl +++ /dev/null @@ -1,47 +0,0 @@ -#define UV_SIZE ((TRANSFORMED_WIDTH/2)*(TRANSFORMED_HEIGHT/2)) - -__kernel void loadys(__global uchar8 const * const Y, - __global float * out, - int out_offset) -{ - const int gid = get_global_id(0); - const int ois = gid * 8; - const int oy = ois / TRANSFORMED_WIDTH; - const int ox = ois % TRANSFORMED_WIDTH; - - const uchar8 ys = Y[gid]; - const float8 ysf = convert_float8(ys); - - // 02 - // 13 - - __global float* outy0; - __global float* outy1; - if ((oy & 1) == 0) { - outy0 = out + out_offset; //y0 - outy1 = out + out_offset + UV_SIZE*2; //y2 - } else { - outy0 = out + out_offset + UV_SIZE; //y1 - outy1 = out + out_offset + UV_SIZE*3; //y3 - } - - vstore4(ysf.s0246, 0, outy0 + (oy/2) * (TRANSFORMED_WIDTH/2) + ox/2); - vstore4(ysf.s1357, 0, outy1 + (oy/2) * (TRANSFORMED_WIDTH/2) + ox/2); -} - -__kernel void loaduv(__global uchar8 const * const in, - __global float8 * out, - int out_offset) -{ - const int gid = get_global_id(0); - const uchar8 inv = in[gid]; - const float8 outv = convert_float8(inv); - out[gid + out_offset / 8] = outv; -} - -__kernel void copy(__global float8 * inout, - int in_offset) -{ - const int gid = get_global_id(0); - inout[gid] = inout[gid + in_offset / 8]; -} diff --git a/sunnypilot/modeld/transforms/loadyuv.h b/sunnypilot/modeld/transforms/loadyuv.h deleted file mode 100644 index 7d27ef5d46..0000000000 --- a/sunnypilot/modeld/transforms/loadyuv.h +++ /dev/null @@ -1,16 +0,0 @@ -#pragma once - -#include "common/clutil.h" - -typedef struct { - int width, height; - cl_kernel loadys_krnl, loaduv_krnl, copy_krnl; -} LoadYUVState; - -void loadyuv_init(LoadYUVState* s, cl_context ctx, cl_device_id device_id, int width, int height); - -void loadyuv_destroy(LoadYUVState* s); - -void loadyuv_queue(LoadYUVState* s, cl_command_queue q, - cl_mem y_cl, cl_mem u_cl, cl_mem v_cl, - cl_mem out_cl, bool do_shift = false); diff --git a/sunnypilot/modeld/transforms/transform.cc b/sunnypilot/modeld/transforms/transform.cc deleted file mode 100644 index ebab670b53..0000000000 --- a/sunnypilot/modeld/transforms/transform.cc +++ /dev/null @@ -1,97 +0,0 @@ -#include "sunnypilot/modeld/transforms/transform.h" - -#include -#include - -#include "common/clutil.h" - -void transform_init(Transform* s, cl_context ctx, cl_device_id device_id) { - memset(s, 0, sizeof(*s)); - - cl_program prg = cl_program_from_file(ctx, device_id, TRANSFORM_PATH, ""); - s->krnl = CL_CHECK_ERR(clCreateKernel(prg, "warpPerspective", &err)); - // done with this - CL_CHECK(clReleaseProgram(prg)); - - s->m_y_cl = CL_CHECK_ERR(clCreateBuffer(ctx, CL_MEM_READ_WRITE, 3*3*sizeof(float), NULL, &err)); - s->m_uv_cl = CL_CHECK_ERR(clCreateBuffer(ctx, CL_MEM_READ_WRITE, 3*3*sizeof(float), NULL, &err)); -} - -void transform_destroy(Transform* s) { - CL_CHECK(clReleaseMemObject(s->m_y_cl)); - CL_CHECK(clReleaseMemObject(s->m_uv_cl)); - CL_CHECK(clReleaseKernel(s->krnl)); -} - -void transform_queue(Transform* s, - cl_command_queue q, - cl_mem in_yuv, int in_width, int in_height, int in_stride, int in_uv_offset, - cl_mem out_y, cl_mem out_u, cl_mem out_v, - int out_width, int out_height, - const mat3& projection) { - const int zero = 0; - - // sampled using pixel center origin - // (because that's how fastcv and opencv does it) - - mat3 projection_y = projection; - - // in and out uv is half the size of y. - mat3 projection_uv = transform_scale_buffer(projection, 0.5); - - CL_CHECK(clEnqueueWriteBuffer(q, s->m_y_cl, CL_TRUE, 0, 3*3*sizeof(float), (void*)projection_y.v, 0, NULL, NULL)); - CL_CHECK(clEnqueueWriteBuffer(q, s->m_uv_cl, CL_TRUE, 0, 3*3*sizeof(float), (void*)projection_uv.v, 0, NULL, NULL)); - - const int in_y_width = in_width; - const int in_y_height = in_height; - const int in_y_px_stride = 1; - const int in_uv_width = in_width/2; - const int in_uv_height = in_height/2; - const int in_uv_px_stride = 2; - const int in_u_offset = in_uv_offset; - const int in_v_offset = in_uv_offset + 1; - - const int out_y_width = out_width; - const int out_y_height = out_height; - const int out_uv_width = out_width/2; - const int out_uv_height = out_height/2; - - CL_CHECK(clSetKernelArg(s->krnl, 0, sizeof(cl_mem), &in_yuv)); // src - CL_CHECK(clSetKernelArg(s->krnl, 1, sizeof(cl_int), &in_stride)); // src_row_stride - CL_CHECK(clSetKernelArg(s->krnl, 2, sizeof(cl_int), &in_y_px_stride)); // src_px_stride - CL_CHECK(clSetKernelArg(s->krnl, 3, sizeof(cl_int), &zero)); // src_offset - CL_CHECK(clSetKernelArg(s->krnl, 4, sizeof(cl_int), &in_y_height)); // src_rows - CL_CHECK(clSetKernelArg(s->krnl, 5, sizeof(cl_int), &in_y_width)); // src_cols - CL_CHECK(clSetKernelArg(s->krnl, 6, sizeof(cl_mem), &out_y)); // dst - CL_CHECK(clSetKernelArg(s->krnl, 7, sizeof(cl_int), &out_y_width)); // dst_row_stride - CL_CHECK(clSetKernelArg(s->krnl, 8, sizeof(cl_int), &zero)); // dst_offset - CL_CHECK(clSetKernelArg(s->krnl, 9, sizeof(cl_int), &out_y_height)); // dst_rows - CL_CHECK(clSetKernelArg(s->krnl, 10, sizeof(cl_int), &out_y_width)); // dst_cols - CL_CHECK(clSetKernelArg(s->krnl, 11, sizeof(cl_mem), &s->m_y_cl)); // M - - const size_t work_size_y[2] = {(size_t)out_y_width, (size_t)out_y_height}; - - CL_CHECK(clEnqueueNDRangeKernel(q, s->krnl, 2, NULL, - (const size_t*)&work_size_y, NULL, 0, 0, NULL)); - - const size_t work_size_uv[2] = {(size_t)out_uv_width, (size_t)out_uv_height}; - - CL_CHECK(clSetKernelArg(s->krnl, 2, sizeof(cl_int), &in_uv_px_stride)); // src_px_stride - CL_CHECK(clSetKernelArg(s->krnl, 3, sizeof(cl_int), &in_u_offset)); // src_offset - CL_CHECK(clSetKernelArg(s->krnl, 4, sizeof(cl_int), &in_uv_height)); // src_rows - CL_CHECK(clSetKernelArg(s->krnl, 5, sizeof(cl_int), &in_uv_width)); // src_cols - CL_CHECK(clSetKernelArg(s->krnl, 6, sizeof(cl_mem), &out_u)); // dst - CL_CHECK(clSetKernelArg(s->krnl, 7, sizeof(cl_int), &out_uv_width)); // dst_row_stride - CL_CHECK(clSetKernelArg(s->krnl, 8, sizeof(cl_int), &zero)); // dst_offset - CL_CHECK(clSetKernelArg(s->krnl, 9, sizeof(cl_int), &out_uv_height)); // dst_rows - CL_CHECK(clSetKernelArg(s->krnl, 10, sizeof(cl_int), &out_uv_width)); // dst_cols - CL_CHECK(clSetKernelArg(s->krnl, 11, sizeof(cl_mem), &s->m_uv_cl)); // M - - CL_CHECK(clEnqueueNDRangeKernel(q, s->krnl, 2, NULL, - (const size_t*)&work_size_uv, NULL, 0, 0, NULL)); - CL_CHECK(clSetKernelArg(s->krnl, 3, sizeof(cl_int), &in_v_offset)); // src_ofset - CL_CHECK(clSetKernelArg(s->krnl, 6, sizeof(cl_mem), &out_v)); // dst - - CL_CHECK(clEnqueueNDRangeKernel(q, s->krnl, 2, NULL, - (const size_t*)&work_size_uv, NULL, 0, 0, NULL)); -} diff --git a/sunnypilot/modeld/transforms/transform.cl b/sunnypilot/modeld/transforms/transform.cl deleted file mode 100644 index 2ca25920cd..0000000000 --- a/sunnypilot/modeld/transforms/transform.cl +++ /dev/null @@ -1,54 +0,0 @@ -#define INTER_BITS 5 -#define INTER_TAB_SIZE (1 << INTER_BITS) -#define INTER_SCALE 1.f / INTER_TAB_SIZE - -#define INTER_REMAP_COEF_BITS 15 -#define INTER_REMAP_COEF_SCALE (1 << INTER_REMAP_COEF_BITS) - -__kernel void warpPerspective(__global const uchar * src, - int src_row_stride, int src_px_stride, int src_offset, int src_rows, int src_cols, - __global uchar * dst, - int dst_row_stride, int dst_offset, int dst_rows, int dst_cols, - __constant float * M) -{ - int dx = get_global_id(0); - int dy = get_global_id(1); - - if (dx < dst_cols && dy < dst_rows) - { - float X0 = M[0] * dx + M[1] * dy + M[2]; - float Y0 = M[3] * dx + M[4] * dy + M[5]; - float W = M[6] * dx + M[7] * dy + M[8]; - W = W != 0.0f ? INTER_TAB_SIZE / W : 0.0f; - int X = rint(X0 * W), Y = rint(Y0 * W); - - int sx = convert_short_sat(X >> INTER_BITS); - int sy = convert_short_sat(Y >> INTER_BITS); - - short sx_clamp = clamp(sx, 0, src_cols - 1); - short sx_p1_clamp = clamp(sx + 1, 0, src_cols - 1); - short sy_clamp = clamp(sy, 0, src_rows - 1); - short sy_p1_clamp = clamp(sy + 1, 0, src_rows - 1); - int v0 = convert_int(src[mad24(sy_clamp, src_row_stride, src_offset + sx_clamp*src_px_stride)]); - int v1 = convert_int(src[mad24(sy_clamp, src_row_stride, src_offset + sx_p1_clamp*src_px_stride)]); - int v2 = convert_int(src[mad24(sy_p1_clamp, src_row_stride, src_offset + sx_clamp*src_px_stride)]); - int v3 = convert_int(src[mad24(sy_p1_clamp, src_row_stride, src_offset + sx_p1_clamp*src_px_stride)]); - - short ay = (short)(Y & (INTER_TAB_SIZE - 1)); - short ax = (short)(X & (INTER_TAB_SIZE - 1)); - float taby = 1.f/INTER_TAB_SIZE*ay; - float tabx = 1.f/INTER_TAB_SIZE*ax; - - int dst_index = mad24(dy, dst_row_stride, dst_offset + dx); - - int itab0 = convert_short_sat_rte( (1.0f-taby)*(1.0f-tabx) * INTER_REMAP_COEF_SCALE ); - int itab1 = convert_short_sat_rte( (1.0f-taby)*tabx * INTER_REMAP_COEF_SCALE ); - int itab2 = convert_short_sat_rte( taby*(1.0f-tabx) * INTER_REMAP_COEF_SCALE ); - int itab3 = convert_short_sat_rte( taby*tabx * INTER_REMAP_COEF_SCALE ); - - int val = v0 * itab0 + v1 * itab1 + v2 * itab2 + v3 * itab3; - - uchar pix = convert_uchar_sat((val + (1 << (INTER_REMAP_COEF_BITS-1))) >> INTER_REMAP_COEF_BITS); - dst[dst_index] = pix; - } -} diff --git a/sunnypilot/modeld/transforms/transform.h b/sunnypilot/modeld/transforms/transform.h deleted file mode 100644 index 771a7054b3..0000000000 --- a/sunnypilot/modeld/transforms/transform.h +++ /dev/null @@ -1,25 +0,0 @@ -#pragma once - -#define CL_USE_DEPRECATED_OPENCL_1_2_APIS -#ifdef __APPLE__ -#include -#else -#include -#endif - -#include "common/mat.h" - -typedef struct { - cl_kernel krnl; - cl_mem m_y_cl, m_uv_cl; -} Transform; - -void transform_init(Transform* s, cl_context ctx, cl_device_id device_id); - -void transform_destroy(Transform* transform); - -void transform_queue(Transform* s, cl_command_queue q, - cl_mem yuv, int in_width, int in_height, int in_stride, int in_uv_offset, - cl_mem out_y, cl_mem out_u, cl_mem out_v, - int out_width, int out_height, - const mat3& projection); diff --git a/sunnypilot/modeld_v2/modeld.py b/sunnypilot/modeld_v2/modeld.py index c38ba40d4f..231d9d66d3 100755 --- a/sunnypilot/modeld_v2/modeld.py +++ b/sunnypilot/modeld_v2/modeld.py @@ -31,7 +31,7 @@ from openpilot.sunnypilot.modeld_v2.meta_helper import load_meta_constants from openpilot.sunnypilot.modeld_v2.camera_offset_helper import CameraOffsetHelper from openpilot.sunnypilot.livedelay.helpers import get_lat_delay -from openpilot.sunnypilot.modeld.modeld_base import ModelStateBase +from openpilot.sunnypilot.modeld_v2.modeld_base import ModelStateBase from openpilot.sunnypilot.models.helpers import get_active_bundle from openpilot.sunnypilot.models.runners.helpers import get_model_runner diff --git a/sunnypilot/modeld/modeld_base.py b/sunnypilot/modeld_v2/modeld_base.py similarity index 100% rename from sunnypilot/modeld/modeld_base.py rename to sunnypilot/modeld_v2/modeld_base.py diff --git a/sunnypilot/modeld/constants.py b/sunnypilot/models/constants.py similarity index 100% rename from sunnypilot/modeld/constants.py rename to sunnypilot/models/constants.py diff --git a/sunnypilot/models/helpers.py b/sunnypilot/models/helpers.py index 98f7d9e385..5627080319 100644 --- a/sunnypilot/models/helpers.py +++ b/sunnypilot/models/helpers.py @@ -12,7 +12,7 @@ import numpy as np from openpilot.common.params import Params from cereal import custom -from openpilot.sunnypilot.modeld.constants import Meta, MetaTombRaider, MetaSimPose +from openpilot.sunnypilot.models.constants import Meta, MetaTombRaider, MetaSimPose from openpilot.system.hardware.hw import Paths from pathlib import Path diff --git a/sunnypilot/selfdrive/controls/controlsd_ext.py b/sunnypilot/selfdrive/controls/controlsd_ext.py index 3f6ef2b439..4b6c92ae2c 100644 --- a/sunnypilot/selfdrive/controls/controlsd_ext.py +++ b/sunnypilot/selfdrive/controls/controlsd_ext.py @@ -14,7 +14,7 @@ from openpilot.common.params import Params from openpilot.common.swaglog import cloudlog from openpilot.sunnypilot import PARAMS_UPDATE_PERIOD from openpilot.sunnypilot.livedelay.helpers import get_lat_delay -from openpilot.sunnypilot.modeld.modeld_base import ModelStateBase +from openpilot.sunnypilot.modeld_v2.modeld_base import ModelStateBase from openpilot.sunnypilot.selfdrive.controls.lib.blinker_pause_lateral import BlinkerPauseLateral from openpilot.sunnypilot.selfdrive.controls.lib.latcontrol_torque_v0 import LatControlTorque as LatControlTorqueV0 diff --git a/sunnypilot/selfdrive/controls/lib/drive_helpers.py b/sunnypilot/selfdrive/controls/lib/drive_helpers.py deleted file mode 100644 index b0fda7bd8c..0000000000 --- a/sunnypilot/selfdrive/controls/lib/drive_helpers.py +++ /dev/null @@ -1,27 +0,0 @@ -from numpy import clip, interp -from openpilot.common.realtime import DT_MDL -from openpilot.selfdrive.controls.lib.drive_helpers import CONTROL_N, MIN_SPEED, MAX_LATERAL_JERK -from openpilot.sunnypilot.modeld.constants import ModelConstants - - -def get_lag_adjusted_curvature(steer_delay, v_ego, psis, curvatures): - if len(psis) != CONTROL_N: - psis = [0.0]*CONTROL_N - curvatures = [0.0]*CONTROL_N - v_ego = max(MIN_SPEED, v_ego) - - # MPC can plan to turn the wheel and turn back before t_delay. This means - # in high delay cases some corrections never even get commanded. So just use - # psi to calculate a simple linearization of desired curvature - current_curvature_desired = curvatures[0] - psi = interp(steer_delay, ModelConstants.T_IDXS[:CONTROL_N], psis) - average_curvature_desired = psi / (v_ego * steer_delay) - desired_curvature = 2 * average_curvature_desired - current_curvature_desired - - # This is the "desired rate of the setpoint" not an actual desired rate - max_curvature_rate = MAX_LATERAL_JERK / (v_ego**2) # inexact calculation, check https://github.com/commaai/openpilot/pull/24755 - safe_desired_curvature = clip(desired_curvature, - current_curvature_desired - max_curvature_rate * DT_MDL, - current_curvature_desired + max_curvature_rate * DT_MDL) - - return float(safe_desired_curvature) From e7cc70f3fa8345662be574e4b0aae4c0a5662b05 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sat, 28 Feb 2026 21:09:02 -0800 Subject: [PATCH 289/311] consolidate file downloading from C++ to Python (#37497) --- system/loggerd/SConscript | 2 +- tools/cabana/SConscript | 4 +- tools/cabana/mainwin.cc | 2 + tools/cabana/streams/routes.cc | 91 +++++++----- tools/cabana/streams/routes.h | 9 +- tools/cabana/utils/api.cc | 171 ----------------------- tools/cabana/utils/api.h | 47 ------- tools/lib/file_downloader.py | 148 ++++++++++++++++++++ tools/replay/SConscript | 4 +- tools/replay/api.cc | 162 ---------------------- tools/replay/api.h | 15 -- tools/replay/consoleui.cc | 1 + tools/replay/filereader.cc | 47 +------ tools/replay/filereader.h | 8 +- tools/replay/framereader.cc | 15 +- tools/replay/framereader.h | 4 +- tools/replay/logreader.cc | 4 +- tools/replay/logreader.h | 2 +- tools/replay/main.cc | 5 + tools/replay/py_downloader.cc | 218 +++++++++++++++++++++++++++++ tools/replay/py_downloader.h | 24 ++++ tools/replay/route.cc | 75 +++++----- tools/replay/route.h | 5 +- tools/replay/tests/test_replay.cc | 1 + tools/replay/timeline.cc | 2 +- tools/replay/util.cc | 221 ------------------------------ tools/replay/util.h | 6 - 27 files changed, 522 insertions(+), 771 deletions(-) delete mode 100644 tools/cabana/utils/api.cc delete mode 100644 tools/cabana/utils/api.h create mode 100755 tools/lib/file_downloader.py delete mode 100644 tools/replay/api.cc delete mode 100644 tools/replay/api.h create mode 100644 tools/replay/py_downloader.cc create mode 100644 tools/replay/py_downloader.h diff --git a/system/loggerd/SConscript b/system/loggerd/SConscript index fecf448855..827c1ce5d0 100644 --- a/system/loggerd/SConscript +++ b/system/loggerd/SConscript @@ -20,4 +20,4 @@ env.Program('encoderd', ['encoderd.cc'], LIBS=libs + ["jpeg"]) env.Program('bootlog.cc', LIBS=libs) if GetOption('extras'): - env.Program('tests/test_logger', ['tests/test_runner.cc', 'tests/test_logger.cc', 'tests/test_zstd_writer.cc'], LIBS=libs + ['curl', 'crypto']) + env.Program('tests/test_logger', ['tests/test_runner.cc', 'tests/test_logger.cc', 'tests/test_zstd_writer.cc'], LIBS=libs) diff --git a/tools/cabana/SConscript b/tools/cabana/SConscript index e172278d91..cecb5ed8d9 100644 --- a/tools/cabana/SConscript +++ b/tools/cabana/SConscript @@ -81,7 +81,7 @@ if arch == "Darwin": cabana_env['CPPPATH'] += [f"{brew_prefix}/include"] cabana_env['LIBPATH'] += [f"{brew_prefix}/lib"] -cabana_libs = [cereal, messaging, visionipc, replay_lib, 'avformat', 'avcodec', 'swresample', 'avutil', 'x264', 'z', 'bz2', 'zstd', 'curl', 'yuv', 'usb-1.0'] + qt_libs +cabana_libs = [cereal, messaging, visionipc, replay_lib, 'avformat', 'avcodec', 'swresample', 'avutil', 'x264', 'z', 'bz2', 'zstd', 'yuv', 'usb-1.0'] + qt_libs opendbc_path = '-DOPENDBC_FILE_PATH=\'"%s"\'' % (cabana_env.Dir("../../opendbc/dbc").abspath) cabana_env['CXXFLAGS'] += [opendbc_path] @@ -93,7 +93,7 @@ cabana_env.Depends(assets, Glob('/assets/*', exclude=[assets, assets_src, "asset cabana_lib = cabana_env.Library("cabana_lib", ['mainwin.cc', 'streams/socketcanstream.cc', 'streams/pandastream.cc', 'streams/devicestream.cc', 'streams/livestream.cc', 'streams/abstractstream.cc', 'streams/replaystream.cc', 'binaryview.cc', 'historylog.cc', 'videowidget.cc', 'signalview.cc', 'streams/routes.cc', 'dbc/dbc.cc', 'dbc/dbcfile.cc', 'dbc/dbcmanager.cc', - 'utils/export.cc', 'utils/util.cc', 'utils/elidedlabel.cc', 'utils/api.cc', + 'utils/export.cc', 'utils/util.cc', 'utils/elidedlabel.cc', 'chart/chartswidget.cc', 'chart/chart.cc', 'chart/signalselector.cc', 'chart/tiplabel.cc', 'chart/sparkline.cc', 'commands.cc', 'messageswidget.cc', 'streamselector.cc', 'settings.cc', 'panda.cc', 'cameraview.cc', 'detailwidget.cc', 'tools/findsimilarbits.cc', 'tools/findsignal.cc', 'tools/routeinfo.cc'], LIBS=cabana_libs, FRAMEWORKS=base_frameworks) diff --git a/tools/cabana/mainwin.cc b/tools/cabana/mainwin.cc index 1ea3733ed0..a7040f891e 100644 --- a/tools/cabana/mainwin.cc +++ b/tools/cabana/mainwin.cc @@ -24,6 +24,8 @@ #include "tools/cabana/streamselector.h" #include "tools/cabana/tools/findsignal.h" #include "tools/cabana/utils/export.h" +#include "tools/replay/py_downloader.h" +#include "tools/replay/util.h" MainWindow::MainWindow(AbstractStream *stream, const QString &dbc_file) : QMainWindow() { loadFingerprints(); diff --git a/tools/cabana/streams/routes.cc b/tools/cabana/streams/routes.cc index 5915c98065..8539a00b5b 100644 --- a/tools/cabana/streams/routes.cc +++ b/tools/cabana/streams/routes.cc @@ -1,27 +1,33 @@ #include "tools/cabana/streams/routes.h" +#include #include #include #include #include #include +#include #include #include #include +#include +#include -class OneShotHttpRequest : public HttpRequest { -public: - OneShotHttpRequest(QObject *parent) : HttpRequest(parent, false) {} - void send(const QString &url) { - if (reply) { - reply->disconnect(); - reply->abort(); - reply->deleteLater(); - reply = nullptr; - } - sendRequest(url); +#include "tools/replay/py_downloader.h" + +namespace { + +// Parse a PyDownloader JSON response into (success, error_code). +std::pair checkApiResponse(const std::string &result) { + if (result.empty()) return {false, 500}; + auto doc = QJsonDocument::fromJson(QByteArray::fromStdString(result)); + if (doc.isObject() && doc.object().contains("error")) { + return {false, doc.object()["error"].toString() == "unauthorized" ? 401 : 500}; } -}; + return {true, 0}; +} + +} // namespace // The RouteListWidget class extends QListWidget to display a custom message when empty class RouteListWidget : public QListWidget { @@ -41,7 +47,7 @@ public: QString empty_text_ = tr("No items"); }; -RoutesDialog::RoutesDialog(QWidget *parent) : QDialog(parent), route_requester_(new OneShotHttpRequest(this)) { +RoutesDialog::RoutesDialog(QWidget *parent) : QDialog(parent) { setWindowTitle(tr("Remote routes")); QFormLayout *layout = new QFormLayout(this); @@ -52,41 +58,40 @@ RoutesDialog::RoutesDialog(QWidget *parent) : QDialog(parent), route_requester_( layout->addRow(button_box); device_list_->addItem(tr("Loading...")); - // Populate period selector with predefined durations period_selector_->addItem(tr("Last week"), 7); period_selector_->addItem(tr("Last 2 weeks"), 14); period_selector_->addItem(tr("Last month"), 30); period_selector_->addItem(tr("Last 6 months"), 180); period_selector_->addItem(tr("Preserved"), -1); - // Connect signals and slots - QObject::connect(route_requester_, &HttpRequest::requestDone, this, &RoutesDialog::parseRouteList); connect(device_list_, QOverload::of(&QComboBox::currentIndexChanged), this, &RoutesDialog::fetchRoutes); connect(period_selector_, QOverload::of(&QComboBox::currentIndexChanged), this, &RoutesDialog::fetchRoutes); connect(route_list_, &QListWidget::itemDoubleClicked, this, &QDialog::accept); - QObject::connect(button_box, &QDialogButtonBox::accepted, this, &QDialog::accept); - QObject::connect(button_box, &QDialogButtonBox::rejected, this, &QDialog::reject); + connect(button_box, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(button_box, &QDialogButtonBox::rejected, this, &QDialog::reject); - // Send request to fetch devices - HttpRequest *http = new HttpRequest(this, false); - QObject::connect(http, &HttpRequest::requestDone, this, &RoutesDialog::parseDeviceList); - http->sendRequest(CommaApi::BASE_URL + "/v1/me/devices/"); + // Fetch devices + QPointer self = this; + QtConcurrent::run([self]() { + std::string result = PyDownloader::getDevices(); + auto [success, error_code] = checkApiResponse(result); + QMetaObject::invokeMethod(qApp, [self, r = QString::fromStdString(result), success, error_code]() { + if (self) self->parseDeviceList(r, success, error_code); + }, Qt::QueuedConnection); + }); } -void RoutesDialog::parseDeviceList(const QString &json, bool success, QNetworkReply::NetworkError err) { +void RoutesDialog::parseDeviceList(const QString &json, bool success, int error_code) { if (success) { device_list_->clear(); - auto devices = QJsonDocument::fromJson(json.toUtf8()).array(); - for (const QJsonValue &device : devices) { + for (const QJsonValue &device : QJsonDocument::fromJson(json.toUtf8()).array()) { QString dongle_id = device["dongle_id"].toString(); device_list_->addItem(dongle_id, dongle_id); } } else { - bool unauthorized = (err == QNetworkReply::ContentAccessDenied || err == QNetworkReply::AuthenticationRequiredError); - QMessageBox::warning(this, tr("Error"), unauthorized ? tr("Unauthorized, Authenticate with tools/lib/auth.py") : tr("Network error")); + QMessageBox::warning(this, tr("Error"), error_code == 401 ? tr("Unauthorized. Authenticate with tools/lib/auth.py") : tr("Network error")); reject(); } - sender()->deleteLater(); } void RoutesDialog::fetchRoutes() { @@ -95,21 +100,31 @@ void RoutesDialog::fetchRoutes() { route_list_->clear(); route_list_->setEmptyText(tr("Loading...")); - // Construct URL with selected device and date range - QString url = QString("%1/v1/devices/%2").arg(CommaApi::BASE_URL, device_list_->currentText()); + + std::string did = device_list_->currentText().toStdString(); int period = period_selector_->currentData().toInt(); - if (period == -1) { - url += "/routes/preserved"; - } else { + + bool preserved = (period == -1); + int64_t start_ms = 0, end_ms = 0; + if (!preserved) { QDateTime now = QDateTime::currentDateTime(); - url += QString("/routes_segments?start=%1&end=%2") - .arg(now.addDays(-period).toMSecsSinceEpoch()) - .arg(now.toMSecsSinceEpoch()); + start_ms = now.addDays(-period).toMSecsSinceEpoch(); + end_ms = now.toMSecsSinceEpoch(); } - route_requester_->send(url); + + int request_id = ++fetch_id_; + QPointer self = this; + QtConcurrent::run([self, did, start_ms, end_ms, preserved, request_id]() { + std::string result = PyDownloader::getDeviceRoutes(did, start_ms, end_ms, preserved); + if (!self || self->fetch_id_ != request_id) return; + auto [success, error_code] = checkApiResponse(result); + QMetaObject::invokeMethod(qApp, [self, r = QString::fromStdString(result), success, error_code, request_id]() { + if (self && self->fetch_id_ == request_id) self->parseRouteList(r, success, error_code); + }, Qt::QueuedConnection); + }); } -void RoutesDialog::parseRouteList(const QString &json, bool success, QNetworkReply::NetworkError err) { +void RoutesDialog::parseRouteList(const QString &json, bool success, int error_code) { if (success) { for (const QJsonValue &route : QJsonDocument::fromJson(json.toUtf8()).array()) { QDateTime from, to; diff --git a/tools/cabana/streams/routes.h b/tools/cabana/streams/routes.h index 045dc67220..99fa67ef8c 100644 --- a/tools/cabana/streams/routes.h +++ b/tools/cabana/streams/routes.h @@ -1,11 +1,10 @@ #pragma once +#include #include #include -#include "tools/cabana/utils/api.h" class RouteListWidget; -class OneShotHttpRequest; class RoutesDialog : public QDialog { Q_OBJECT @@ -14,12 +13,12 @@ public: QString route(); protected: - void parseDeviceList(const QString &json, bool success, QNetworkReply::NetworkError err); - void parseRouteList(const QString &json, bool success, QNetworkReply::NetworkError err); + void parseDeviceList(const QString &json, bool success, int error_code); + void parseRouteList(const QString &json, bool success, int error_code); void fetchRoutes(); QComboBox *device_list_; QComboBox *period_selector_; RouteListWidget *route_list_; - OneShotHttpRequest *route_requester_; + std::atomic fetch_id_{0}; }; diff --git a/tools/cabana/utils/api.cc b/tools/cabana/utils/api.cc deleted file mode 100644 index 89379a8434..0000000000 --- a/tools/cabana/utils/api.cc +++ /dev/null @@ -1,171 +0,0 @@ -#include "tools/cabana/utils/api.h" - -#include -#include - -#include -#include -#include -#include -#include -#include - -#include -#include - -#include "common/params.h" -#include "common/util.h" -#include "system/hardware/hw.h" -#include "tools/cabana/utils/util.h" - -QString getVersion() { - static QString version = QString::fromStdString(Params().get("Version")); - return version; -} - -QString getUserAgent() { - return "openpilot-" + getVersion(); -} - -std::optional getDongleId() { - std::string id = Params().get("DongleId"); - - if (!id.empty() && (id != "UnregisteredDevice")) { - return QString::fromStdString(id); - } else { - return {}; - } -} - -namespace CommaApi { - -EVP_PKEY *get_private_key() { - static std::unique_ptr pkey(nullptr, EVP_PKEY_free); - if (!pkey) { - FILE *fp = fopen(Path::rsa_file().c_str(), "rb"); - if (!fp) { - qDebug() << "No private key found, please run manager.py or registration.py"; - return nullptr; - } - pkey.reset(PEM_read_PrivateKey(fp, nullptr, nullptr, nullptr)); - fclose(fp); - } - return pkey.get(); -} - -QByteArray rsa_sign(const QByteArray &data) { - EVP_PKEY *pkey = get_private_key(); - if (!pkey) return {}; - - EVP_MD_CTX *mdctx = EVP_MD_CTX_new(); - if (!mdctx) return {}; - - QByteArray sig(EVP_PKEY_size(pkey), Qt::Uninitialized); - size_t sig_len = sig.size(); - - int ret = EVP_DigestSignInit(mdctx, nullptr, EVP_sha256(), nullptr, pkey); - ret &= EVP_DigestSignUpdate(mdctx, data.data(), data.size()); - ret &= EVP_DigestSignFinal(mdctx, (unsigned char*)sig.data(), &sig_len); - - EVP_MD_CTX_free(mdctx); - - if (ret != 1) return {}; - sig.resize(sig_len); - return sig; -} - -QString create_jwt(const QJsonObject &payloads, int expiry) { - QJsonObject header = {{"alg", "RS256"}}; - - auto t = QDateTime::currentSecsSinceEpoch(); - QJsonObject payload = {{"identity", getDongleId().value_or("")}, {"nbf", t}, {"iat", t}, {"exp", t + expiry}}; - for (auto it = payloads.begin(); it != payloads.end(); ++it) { - payload.insert(it.key(), it.value()); - } - - auto b64_opts = QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals; - QString jwt = QJsonDocument(header).toJson(QJsonDocument::Compact).toBase64(b64_opts) + '.' + - QJsonDocument(payload).toJson(QJsonDocument::Compact).toBase64(b64_opts); - - auto hash = QCryptographicHash::hash(jwt.toUtf8(), QCryptographicHash::Sha256); - return jwt + "." + rsa_sign(hash).toBase64(b64_opts); -} - -} // namespace CommaApi - -HttpRequest::HttpRequest(QObject *parent, bool create_jwt, int timeout) : create_jwt(create_jwt), QObject(parent) { - networkTimer = new QTimer(this); - networkTimer->setSingleShot(true); - networkTimer->setInterval(timeout); - connect(networkTimer, &QTimer::timeout, this, &HttpRequest::requestTimeout); -} - -bool HttpRequest::active() const { - return reply != nullptr; -} - -bool HttpRequest::timeout() const { - return reply && reply->error() == QNetworkReply::OperationCanceledError; -} - -void HttpRequest::sendRequest(const QString &requestURL, const HttpRequest::Method method) { - if (active()) { - qDebug() << "HttpRequest is active"; - return; - } - QString token; - if (create_jwt) { - token = CommaApi::create_jwt(); - } else { - QString token_json = QString::fromStdString(util::read_file(util::getenv("HOME") + "/.comma/auth.json")); - QJsonDocument json_d = QJsonDocument::fromJson(token_json.toUtf8()); - token = json_d["access_token"].toString(); - } - - QNetworkRequest request; - request.setUrl(QUrl(requestURL)); - request.setRawHeader("User-Agent", getUserAgent().toUtf8()); - - if (!token.isEmpty()) { - request.setRawHeader(QByteArray("Authorization"), ("JWT " + token).toUtf8()); - } - - if (method == HttpRequest::Method::GET) { - reply = nam()->get(request); - } else if (method == HttpRequest::Method::DELETE) { - reply = nam()->deleteResource(request); - } - - networkTimer->start(); - connect(reply, &QNetworkReply::finished, this, &HttpRequest::requestFinished); -} - -void HttpRequest::requestTimeout() { - reply->abort(); -} - -void HttpRequest::requestFinished() { - networkTimer->stop(); - - if (reply->error() == QNetworkReply::NoError) { - emit requestDone(reply->readAll(), true, reply->error()); - } else { - QString error; - if (reply->error() == QNetworkReply::OperationCanceledError) { - nam()->clearAccessCache(); - nam()->clearConnectionCache(); - error = "Request timed out"; - } else { - error = reply->errorString(); - } - emit requestDone(error, false, reply->error()); - } - - reply->deleteLater(); - reply = nullptr; -} - -QNetworkAccessManager *HttpRequest::nam() { - static QNetworkAccessManager *networkAccessManager = new QNetworkAccessManager(qApp); - return networkAccessManager; -} diff --git a/tools/cabana/utils/api.h b/tools/cabana/utils/api.h deleted file mode 100644 index ad64d7e722..0000000000 --- a/tools/cabana/utils/api.h +++ /dev/null @@ -1,47 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include "common/util.h" - -namespace CommaApi { - -const QString BASE_URL = util::getenv("API_HOST", "https://api.commadotai.com").c_str(); -QByteArray rsa_sign(const QByteArray &data); -QString create_jwt(const QJsonObject &payloads = {}, int expiry = 3600); - -} // namespace CommaApi - -/** - * Makes a request to the request endpoint. - */ - -class HttpRequest : public QObject { - Q_OBJECT - -public: - enum class Method {GET, DELETE}; - - explicit HttpRequest(QObject* parent, bool create_jwt = true, int timeout = 20000); - void sendRequest(const QString &requestURL, const Method method = Method::GET); - bool active() const; - bool timeout() const; - -signals: - void requestDone(const QString &response, bool success, QNetworkReply::NetworkError error); - -protected: - QNetworkReply *reply = nullptr; - -private: - static QNetworkAccessManager *nam(); - QTimer *networkTimer = nullptr; - bool create_jwt; - -private slots: - void requestTimeout(); - void requestFinished(); -}; diff --git a/tools/lib/file_downloader.py b/tools/lib/file_downloader.py new file mode 100755 index 0000000000..c9c26bb307 --- /dev/null +++ b/tools/lib/file_downloader.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python3 +""" +CLI tool for downloading files and querying the comma API. +Called by C++ replay/cabana via subprocess. + +Subcommands: + route-files - Get route file URLs as JSON + download - Download URL to local cache, print local path + devices - List user's devices as JSON + device-routes - List routes for a device as JSON +""" +import argparse +import hashlib +import json +import os +import sys +import tempfile +import shutil + +from openpilot.system.hardware.hw import Paths +from openpilot.tools.lib.api import CommaApi, UnauthorizedError, APIError +from openpilot.tools.lib.auth_config import get_token +from openpilot.tools.lib.url_file import URLFile + + +def api_call(func): + """Run an API call, outputting JSON result or error to stdout.""" + try: + result = func(CommaApi(get_token())) + json.dump(result, sys.stdout) + except UnauthorizedError: + json.dump({"error": "unauthorized"}, sys.stdout) + except APIError as e: + error = "not_found" if getattr(e, 'status_code', 0) == 404 else str(e) + json.dump({"error": error}, sys.stdout) + except Exception as e: + json.dump({"error": str(e)}, sys.stdout) + sys.stdout.write("\n") + sys.stdout.flush() + + +def cache_file_path(url): + url_without_query = url.split("?")[0] + return os.path.join(Paths.download_cache_root(), hashlib.sha256(url_without_query.encode()).hexdigest()) + + +def cmd_route_files(args): + api_call(lambda api: api.get(f"v1/route/{args.route}/files")) + + +def cmd_download(args): + url = args.url + use_cache = not args.no_cache + + if use_cache: + local_path = cache_file_path(url) + if os.path.exists(local_path): + sys.stdout.write(local_path + "\n") + sys.stdout.flush() + return + + try: + uf = URLFile(url, cache=False) + total = uf.get_length() + if total <= 0: + sys.stderr.write("ERROR:File not found or empty\n") + sys.stderr.flush() + sys.exit(1) + + os.makedirs(Paths.download_cache_root(), exist_ok=True) + tmp_fd, tmp_path = tempfile.mkstemp(dir=Paths.download_cache_root()) + try: + downloaded = 0 + chunk_size = 1024 * 1024 + with os.fdopen(tmp_fd, 'wb') as f: + while downloaded < total: + data = uf.read(min(chunk_size, total - downloaded)) + f.write(data) + downloaded += len(data) + sys.stderr.write(f"PROGRESS:{downloaded}:{total}\n") + sys.stderr.flush() + + if use_cache: + shutil.move(tmp_path, local_path) + sys.stdout.write(local_path + "\n") + else: + sys.stdout.write(tmp_path + "\n") + except Exception: + try: + os.unlink(tmp_path) + except OSError: + pass + raise + + except Exception as e: + sys.stderr.write(f"ERROR:{e}\n") + sys.stderr.flush() + sys.exit(1) + + sys.stdout.flush() + + +def cmd_devices(args): + api_call(lambda api: api.get("v1/me/devices/")) + + +def cmd_device_routes(args): + def fetch(api): + if args.preserved: + return api.get(f"v1/devices/{args.dongle_id}/routes/preserved") + params = {} + if args.start is not None: + params['start'] = args.start + if args.end is not None: + params['end'] = args.end + return api.get(f"v1/devices/{args.dongle_id}/routes_segments", params=params) + api_call(fetch) + + +def main(): + parser = argparse.ArgumentParser(description="File downloader CLI for openpilot tools") + subparsers = parser.add_subparsers(dest="command", required=True) + + p_rf = subparsers.add_parser("route-files") + p_rf.add_argument("route") + p_rf.set_defaults(func=cmd_route_files) + + p_dl = subparsers.add_parser("download") + p_dl.add_argument("url") + p_dl.add_argument("--no-cache", action="store_true") + p_dl.set_defaults(func=cmd_download) + + p_dev = subparsers.add_parser("devices") + p_dev.set_defaults(func=cmd_devices) + + p_dr = subparsers.add_parser("device-routes") + p_dr.add_argument("dongle_id") + p_dr.add_argument("--start", type=int, default=None) + p_dr.add_argument("--end", type=int, default=None) + p_dr.add_argument("--preserved", action="store_true") + p_dr.set_defaults(func=cmd_device_routes) + + args = parser.parse_args() + args.func(args) + + +if __name__ == "__main__": + main() diff --git a/tools/replay/SConscript b/tools/replay/SConscript index b39cf6dab1..3efa970b37 100644 --- a/tools/replay/SConscript +++ b/tools/replay/SConscript @@ -7,12 +7,12 @@ base_frameworks = [] base_libs = [common, messaging, cereal, visionipc, 'm', 'ssl', 'crypto', 'pthread'] replay_lib_src = ["replay.cc", "consoleui.cc", "camera.cc", "filereader.cc", "logreader.cc", "framereader.cc", - "route.cc", "util.cc", "seg_mgr.cc", "timeline.cc", "api.cc"] + "route.cc", "util.cc", "seg_mgr.cc", "timeline.cc", "py_downloader.cc"] if arch != "Darwin": replay_lib_src.append("qcom_decoder.cc") replay_lib = replay_env.Library("replay", replay_lib_src, LIBS=base_libs, FRAMEWORKS=base_frameworks) Export('replay_lib') -replay_libs = [replay_lib, 'avformat', 'avcodec', 'swresample', 'avutil', 'x264', 'z', 'bz2', 'zstd', 'curl', 'yuv', 'ncurses'] + base_libs +replay_libs = [replay_lib, 'avformat', 'avcodec', 'swresample', 'avutil', 'x264', 'z', 'bz2', 'zstd', 'yuv', 'ncurses'] + base_libs replay_env.Program("replay", ["main.cc"], LIBS=replay_libs, FRAMEWORKS=base_frameworks) if GetOption('extras'): diff --git a/tools/replay/api.cc b/tools/replay/api.cc deleted file mode 100644 index 85e4e52b28..0000000000 --- a/tools/replay/api.cc +++ /dev/null @@ -1,162 +0,0 @@ - -#include "tools/replay/api.h" - -#include -#include -#include -#include - -#include -#include -#include - -#include "common/params.h" -#include "common/version.h" -#include "system/hardware/hw.h" - -namespace CommaApi2 { - -// Base64 URL-safe character set (uses '-' and '_' instead of '+' and '/') -static const std::string base64url_chars = - "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - "abcdefghijklmnopqrstuvwxyz" - "0123456789-_"; - -std::string base64url_encode(const std::string &in) { - std::string out; - int val = 0, valb = -6; - for (unsigned char c : in) { - val = (val << 8) + c; - valb += 8; - while (valb >= 0) { - out.push_back(base64url_chars[(val >> valb) & 0x3F]); - valb -= 6; - } - } - if (valb > -6) { - out.push_back(base64url_chars[((val << 8) >> (valb + 8)) & 0x3F]); - } - - return out; -} - -EVP_PKEY *get_rsa_private_key() { - static std::unique_ptr rsa_private(nullptr, EVP_PKEY_free); - if (!rsa_private) { - FILE *fp = fopen(Path::rsa_file().c_str(), "rb"); - if (!fp) { - std::cerr << "No RSA private key found, please run manager.py or registration.py" << std::endl; - return nullptr; - } - rsa_private.reset(PEM_read_PrivateKey(fp, NULL, NULL, NULL)); - fclose(fp); - } - return rsa_private.get(); -} - -std::string rsa_sign(const std::string &data) { - EVP_PKEY *private_key = get_rsa_private_key(); - if (!private_key) return {}; - - EVP_MD_CTX *mdctx = EVP_MD_CTX_new(); - assert(mdctx != nullptr); - - std::vector sig(EVP_PKEY_size(private_key)); - uint32_t sig_len; - - EVP_SignInit(mdctx, EVP_sha256()); - EVP_SignUpdate(mdctx, data.data(), data.size()); - int ret = EVP_SignFinal(mdctx, sig.data(), &sig_len, private_key); - - EVP_MD_CTX_free(mdctx); - - assert(ret == 1); - assert(sig.size() == sig_len); - return std::string(sig.begin(), sig.begin() + sig_len); -} - -std::string create_jwt(const json11::Json &extra, int exp_time) { - int now = std::chrono::seconds(std::time(nullptr)).count(); - std::string dongle_id = Params().get("DongleId"); - - // Create header and payload - json11::Json header = json11::Json::object{{"alg", "RS256"}}; - auto payload = json11::Json::object{ - {"identity", dongle_id}, - {"iat", now}, - {"nbf", now}, - {"exp", now + exp_time}, - }; - // Merge extra payload - for (const auto &item : extra.object_items()) { - payload[item.first] = item.second; - } - - // JWT construction - std::string jwt = base64url_encode(header.dump()) + '.' + - base64url_encode(json11::Json(payload).dump()); - - // Hash and sign - std::string hash(SHA256_DIGEST_LENGTH, '\0'); - SHA256((uint8_t *)jwt.data(), jwt.size(), (uint8_t *)hash.data()); - std::string signature = rsa_sign(hash); - - return jwt + "." + base64url_encode(signature); -} - -std::string create_token(bool use_jwt, const json11::Json &payloads, int expiry) { - if (use_jwt) { - return create_jwt(payloads, expiry); - } - - std::string token_json = util::read_file(util::getenv("HOME") + "/.comma/auth.json"); - std::string err; - auto json = json11::Json::parse(token_json, err); - if (!err.empty()) { - std::cerr << "Error parsing auth.json " << err << std::endl; - return ""; - } - return json["access_token"].string_value(); -} - -std::string httpGet(const std::string &url, long *response_code) { - CURL *curl = curl_easy_init(); - assert(curl); - - std::string readBuffer; - const std::string token = CommaApi2::create_token(!Hardware::PC()); - - // Set up the lambda for the write callback - // The '+' makes the lambda non-capturing, allowing it to be used as a C function pointer - auto writeCallback = +[](char *contents, size_t size, size_t nmemb, std::string *userp) ->size_t{ - size_t totalSize = size * nmemb; - userp->append((char *)contents, totalSize); - return totalSize; - }; - - curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCallback); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer); - curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); - - // Handle headers - struct curl_slist *headers = nullptr; - headers = curl_slist_append(headers, "User-Agent: openpilot-" COMMA_VERSION); - if (!token.empty()) { - headers = curl_slist_append(headers, ("Authorization: JWT " + token).c_str()); - } - curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); - - CURLcode res = curl_easy_perform(curl); - - if (response_code) { - curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, response_code); - } - - curl_slist_free_all(headers); - curl_easy_cleanup(curl); - - return res == CURLE_OK ? readBuffer : std::string{}; -} - -} // namespace CommaApi diff --git a/tools/replay/api.h b/tools/replay/api.h deleted file mode 100644 index dff59c0659..0000000000 --- a/tools/replay/api.h +++ /dev/null @@ -1,15 +0,0 @@ -#pragma once - -#include -#include - -#include "common/util.h" -#include "third_party/json11/json11.hpp" - -namespace CommaApi2 { - -const std::string BASE_URL = util::getenv("API_HOST", "https://api.commadotai.com").c_str(); -std::string create_token(bool use_jwt, const json11::Json& payloads = {}, int expiry = 3600); -std::string httpGet(const std::string &url, long *response_code = nullptr); - -} // namespace CommaApi2 diff --git a/tools/replay/consoleui.cc b/tools/replay/consoleui.cc index 4d43df2da8..2d21b4efc0 100644 --- a/tools/replay/consoleui.cc +++ b/tools/replay/consoleui.cc @@ -9,6 +9,7 @@ #include "common/ratekeeper.h" #include "common/util.h" #include "common/version.h" +#include "tools/replay/py_downloader.h" namespace { diff --git a/tools/replay/filereader.cc b/tools/replay/filereader.cc index cb13775a34..93a6a1193f 100644 --- a/tools/replay/filereader.cc +++ b/tools/replay/filereader.cc @@ -1,49 +1,14 @@ #include "tools/replay/filereader.h" -#include - #include "common/util.h" -#include "system/hardware/hw.h" -#include "tools/replay/util.h" - -std::string cacheFilePath(const std::string &url) { - static std::string cache_path = [] { - const std::string comma_cache = Path::download_cache_root(); - util::create_directories(comma_cache, 0755); - return comma_cache.back() == '/' ? comma_cache : comma_cache + "/"; - }(); - - return cache_path + sha256(getUrlWithoutQuery(url)); -} +#include "tools/replay/py_downloader.h" std::string FileReader::read(const std::string &file, std::atomic *abort) { const bool is_remote = (file.find("https://") == 0) || (file.find("http://") == 0); - const std::string local_file = is_remote ? cacheFilePath(file) : file; - std::string result; - - if ((!is_remote || cache_to_local_) && util::file_exists(local_file)) { - result = util::read_file(local_file); - } else if (is_remote) { - result = download(file, abort); - if (cache_to_local_ && !result.empty()) { - std::ofstream fs(local_file, std::ios::binary | std::ios::out); - fs.write(result.data(), result.size()); - } + if (is_remote) { + std::string local_path = PyDownloader::download(file, cache_to_local_, abort); + if (local_path.empty()) return {}; + return util::read_file(local_path); } - return result; -} - -std::string FileReader::download(const std::string &url, std::atomic *abort) { - for (int i = 0; i <= max_retries_ && !(abort && *abort); ++i) { - if (i > 0) { - rWarning("download failed, retrying %d", i); - util::sleep_for(3000); - } - - std::string result = httpGet(url, chunk_size_, abort); - if (!result.empty()) { - return result; - } - } - return {}; + return util::read_file(file); } diff --git a/tools/replay/filereader.h b/tools/replay/filereader.h index 34aa91e858..30740cd0ac 100644 --- a/tools/replay/filereader.h +++ b/tools/replay/filereader.h @@ -5,16 +5,10 @@ class FileReader { public: - FileReader(bool cache_to_local, size_t chunk_size = 0, int retries = 3) - : cache_to_local_(cache_to_local), chunk_size_(chunk_size), max_retries_(retries) {} + FileReader(bool cache_to_local) : cache_to_local_(cache_to_local) {} virtual ~FileReader() {} std::string read(const std::string &file, std::atomic *abort = nullptr); private: - std::string download(const std::string &url, std::atomic *abort); - size_t chunk_size_; - int max_retries_; bool cache_to_local_; }; - -std::string cacheFilePath(const std::string &url); diff --git a/tools/replay/framereader.cc b/tools/replay/framereader.cc index 96ec5915b4..13d4497a58 100644 --- a/tools/replay/framereader.cc +++ b/tools/replay/framereader.cc @@ -7,6 +7,7 @@ #include "common/util.h" #include "third_party/libyuv/include/libyuv.h" +#include "tools/replay/py_downloader.h" #include "tools/replay/util.h" #include "system/hardware/hw.h" @@ -71,13 +72,13 @@ FrameReader::~FrameReader() { if (input_ctx) avformat_close_input(&input_ctx); } -bool FrameReader::load(CameraType type, const std::string &url, bool no_hw_decoder, std::atomic *abort, bool local_cache, int chunk_size, int retries) { - auto local_file_path = (url.find("https://") == 0 || url.find("http://") == 0) ? cacheFilePath(url) : url; - if (!util::file_exists(local_file_path)) { - FileReader f(local_cache, chunk_size, retries); - if (f.read(url, abort).empty()) { - return false; - } +bool FrameReader::load(CameraType type, const std::string &url, bool no_hw_decoder, std::atomic *abort, bool local_cache) { + std::string local_file_path; + if (url.find("https://") == 0 || url.find("http://") == 0) { + local_file_path = PyDownloader::download(url, local_cache, abort); + if (local_file_path.empty()) return false; + } else { + local_file_path = url; } return loadFromFile(type, local_file_path, no_hw_decoder, abort); } diff --git a/tools/replay/framereader.h b/tools/replay/framereader.h index d8e86fce0f..3609d64f8b 100644 --- a/tools/replay/framereader.h +++ b/tools/replay/framereader.h @@ -4,7 +4,6 @@ #include #include "msgq/visionipc/visionbuf.h" -#include "tools/replay/filereader.h" #include "tools/replay/util.h" #ifndef __APPLE__ @@ -22,8 +21,7 @@ class FrameReader { public: FrameReader(); ~FrameReader(); - bool load(CameraType type, const std::string &url, bool no_hw_decoder = false, std::atomic *abort = nullptr, bool local_cache = false, - int chunk_size = -1, int retries = 0); + bool load(CameraType type, const std::string &url, bool no_hw_decoder = false, std::atomic *abort = nullptr, bool local_cache = false); bool loadFromFile(CameraType type, const std::string &file, bool no_hw_decoder = false, std::atomic *abort = nullptr); bool get(int idx, VisionBuf *buf); size_t getFrameCount() const { return packets_info.size(); } diff --git a/tools/replay/logreader.cc b/tools/replay/logreader.cc index 75abb8417b..997a4bc00f 100644 --- a/tools/replay/logreader.cc +++ b/tools/replay/logreader.cc @@ -6,8 +6,8 @@ #include "tools/replay/util.h" #include "common/util.h" -bool LogReader::load(const std::string &url, std::atomic *abort, bool local_cache, int chunk_size, int retries) { - std::string data = FileReader(local_cache, chunk_size, retries).read(url, abort); +bool LogReader::load(const std::string &url, std::atomic *abort, bool local_cache) { + std::string data = FileReader(local_cache).read(url, abort); if (!data.empty()) { if (url.find(".bz2") != std::string::npos || util::starts_with(data, "BZh9")) { data = decompressBZ2(data, abort); diff --git a/tools/replay/logreader.h b/tools/replay/logreader.h index f8d60ffadd..9219878ace 100644 --- a/tools/replay/logreader.h +++ b/tools/replay/logreader.h @@ -28,7 +28,7 @@ class LogReader { public: LogReader(const std::vector &filters = {}) { filters_ = filters; } bool load(const std::string &url, std::atomic *abort = nullptr, - bool local_cache = false, int chunk_size = -1, int retries = 0); + bool local_cache = false); bool load(const char *data, size_t size, std::atomic *abort = nullptr); std::vector events; diff --git a/tools/replay/main.cc b/tools/replay/main.cc index 30f85b1700..bdb9cf4f35 100644 --- a/tools/replay/main.cc +++ b/tools/replay/main.cc @@ -1,5 +1,6 @@ #include +#include #include #include #include @@ -132,6 +133,10 @@ int main(int argc, char *argv[]) { util::set_file_descriptor_limit(1024); #endif + // The vendored ncurses static library has a wrong compiled-in terminfo path. + // Point it at the system terminfo database if not already set. + setenv("TERMINFO_DIRS", "/usr/share/terminfo:/lib/terminfo:/usr/lib/terminfo", 0); + ReplayConfig config; if (!parseArgs(argc, argv, config)) { diff --git a/tools/replay/py_downloader.cc b/tools/replay/py_downloader.cc new file mode 100644 index 0000000000..efaf3c93a2 --- /dev/null +++ b/tools/replay/py_downloader.cc @@ -0,0 +1,218 @@ +#include "tools/replay/py_downloader.h" + +#include +#include +#include +#include +#include + +#include "tools/replay/util.h" + +namespace { + +static std::mutex handler_mutex; +static DownloadProgressHandler progress_handler = nullptr; + +// Run a Python command and capture stdout. Optionally parse stderr for PROGRESS lines. +// Returns stdout content. If abort is signaled, kills the child process. +std::string runPython(const std::vector &args, std::atomic *abort = nullptr, bool parse_progress = false) { + // Build argv for execvp + std::vector argv; + argv.push_back("python3"); + argv.push_back("-m"); + argv.push_back("openpilot.tools.lib.file_downloader"); + for (const auto &a : args) { + argv.push_back(a.c_str()); + } + argv.push_back(nullptr); + + int stdout_pipe[2]; + int stderr_pipe[2]; + if (pipe(stdout_pipe) != 0) { + rWarning("py_downloader: pipe() failed"); + return {}; + } + if (pipe(stderr_pipe) != 0) { + rWarning("py_downloader: pipe() failed"); + close(stdout_pipe[0]); close(stdout_pipe[1]); + return {}; + } + + pid_t pid = fork(); + if (pid < 0) { + rWarning("py_downloader: fork() failed"); + close(stdout_pipe[0]); close(stdout_pipe[1]); + close(stderr_pipe[0]); close(stderr_pipe[1]); + return {}; + } + + if (pid == 0) { + // Child process — detach from controlling terminal so Python + // cannot corrupt terminal settings needed by ncurses in the parent. + setsid(); + int devnull = open("/dev/null", O_RDONLY); + if (devnull >= 0) { + dup2(devnull, STDIN_FILENO); + if (devnull > STDERR_FILENO) close(devnull); + } + + // Clear OPENPILOT_PREFIX so the Python process uses default paths + // (e.g. ~/.comma/auth.json). The prefix is only for IPC in the parent. + unsetenv("OPENPILOT_PREFIX"); + + close(stdout_pipe[0]); + close(stderr_pipe[0]); + dup2(stdout_pipe[1], STDOUT_FILENO); + dup2(stderr_pipe[1], STDERR_FILENO); + close(stdout_pipe[1]); + close(stderr_pipe[1]); + + execvp("python3", const_cast(argv.data())); + _exit(127); + } + + // Parent process + close(stdout_pipe[1]); + close(stderr_pipe[1]); + + std::string stdout_data; + std::string stderr_buf; + char buf[4096]; + + // Use select() to read from both pipes + fd_set rfds; + int max_fd = std::max(stdout_pipe[0], stderr_pipe[0]); + bool stdout_open = true, stderr_open = true; + + while (stdout_open || stderr_open) { + if (abort && *abort) { + kill(pid, SIGTERM); + break; + } + + FD_ZERO(&rfds); + if (stdout_open) FD_SET(stdout_pipe[0], &rfds); + if (stderr_open) FD_SET(stderr_pipe[0], &rfds); + + struct timeval tv = {0, 100000}; // 100ms timeout + int ret = select(max_fd + 1, &rfds, nullptr, nullptr, &tv); + if (ret < 0) break; + + if (stdout_open && FD_ISSET(stdout_pipe[0], &rfds)) { + ssize_t n = read(stdout_pipe[0], buf, sizeof(buf)); + if (n <= 0) { + stdout_open = false; + } else { + stdout_data.append(buf, n); + } + } + + if (stderr_open && FD_ISSET(stderr_pipe[0], &rfds)) { + ssize_t n = read(stderr_pipe[0], buf, sizeof(buf)); + if (n <= 0) { + stderr_open = false; + } else { + stderr_buf.append(buf, n); + // Parse complete lines from stderr + size_t pos; + while ((pos = stderr_buf.find('\n')) != std::string::npos) { + std::string line = stderr_buf.substr(0, pos); + stderr_buf.erase(0, pos + 1); + + if (parse_progress && line.rfind("PROGRESS:", 0) == 0) { + // Parse "PROGRESS::" + auto colon1 = line.find(':', 9); + if (colon1 != std::string::npos) { + try { + uint64_t cur = std::stoull(line.c_str() + 9); + uint64_t total = std::stoull(line.c_str() + colon1 + 1); + std::lock_guard lk(handler_mutex); + if (progress_handler) { + progress_handler(cur, total, true); + } + } catch (...) {} + } + } else if (line.rfind("ERROR:", 0) == 0) { + rWarning("py_downloader: %s", line.c_str() + 6); + } + } + } + } + } + + // Drain remaining pipe data to prevent child from blocking on write + for (int fd : {stdout_pipe[0], stderr_pipe[0]}) { + while (read(fd, buf, sizeof(buf)) > 0) {} + close(fd); + } + + int status; + waitpid(pid, &status, 0); + + bool failed = (abort && *abort) || + (WIFEXITED(status) && WEXITSTATUS(status) != 0) || + WIFSIGNALED(status); + if (failed) { + if (WIFEXITED(status) && WEXITSTATUS(status) != 0) { + rWarning("py_downloader: process exited with code %d", WEXITSTATUS(status)); + } else if (WIFSIGNALED(status)) { + rWarning("py_downloader: process killed by signal %d", WTERMSIG(status)); + } + std::lock_guard lk(handler_mutex); + if (progress_handler) { + progress_handler(0, 0, false); + } + return {}; + } + + // Trim trailing newline + while (!stdout_data.empty() && (stdout_data.back() == '\n' || stdout_data.back() == '\r')) { + stdout_data.pop_back(); + } + + return stdout_data; +} + +} // namespace + +void installDownloadProgressHandler(DownloadProgressHandler handler) { + std::lock_guard lk(handler_mutex); + progress_handler = handler; +} + +namespace PyDownloader { + +std::string download(const std::string &url, bool use_cache, std::atomic *abort) { + std::vector args = {"download", url}; + if (!use_cache) { + args.push_back("--no-cache"); + } + return runPython(args, abort, true); +} + +std::string getRouteFiles(const std::string &route) { + return runPython({"route-files", route}); +} + +std::string getDevices() { + return runPython({"devices"}); +} + +std::string getDeviceRoutes(const std::string &dongle_id, int64_t start_ms, int64_t end_ms, bool preserved) { + std::vector args = {"device-routes", dongle_id}; + if (preserved) { + args.push_back("--preserved"); + } else { + if (start_ms > 0) { + args.push_back("--start"); + args.push_back(std::to_string(start_ms)); + } + if (end_ms > 0) { + args.push_back("--end"); + args.push_back(std::to_string(end_ms)); + } + } + return runPython(args); +} + +} // namespace PyDownloader diff --git a/tools/replay/py_downloader.h b/tools/replay/py_downloader.h new file mode 100644 index 0000000000..535189784c --- /dev/null +++ b/tools/replay/py_downloader.h @@ -0,0 +1,24 @@ +#pragma once + +#include +#include +#include + +typedef std::function DownloadProgressHandler; +void installDownloadProgressHandler(DownloadProgressHandler handler); + +namespace PyDownloader { + +// Downloads url to local cache, returns local file path. Reports progress via installDownloadProgressHandler. +std::string download(const std::string &url, bool use_cache = true, std::atomic *abort = nullptr); + +// Returns JSON string of route files (same format as /v1/route/.../files API) +std::string getRouteFiles(const std::string &route); + +// Returns JSON string of user's devices +std::string getDevices(); + +// Returns JSON string of device routes +std::string getDeviceRoutes(const std::string &dongle_id, int64_t start_ms = 0, int64_t end_ms = 0, bool preserved = false); + +} // namespace PyDownloader diff --git a/tools/replay/route.cc b/tools/replay/route.cc index ba00828267..e7b8ed6bba 100644 --- a/tools/replay/route.cc +++ b/tools/replay/route.cc @@ -6,7 +6,7 @@ #include "third_party/json11/json11.hpp" #include "system/hardware/hw.h" -#include "tools/replay/api.h" +#include "tools/replay/py_downloader.h" #include "tools/replay/replay.h" #include "tools/replay/util.h" @@ -103,43 +103,44 @@ bool Route::loadFromAutoSource() { return !segments_.empty(); } -bool Route::loadFromServer(int retries) { - const std::string url = CommaApi2::BASE_URL + "/v1/route/" + route_.str + "/files"; - for (int i = 1; i <= retries; ++i) { - long response_code = 0; - std::string result = CommaApi2::httpGet(url, &response_code); - if (response_code == 200) { - return loadFromJson(result); - } - - if (response_code == 401 || response_code == 403) { - rWarning(">> Unauthorized. Authenticate with tools/lib/auth.py <<"); - err_ = RouteLoadError::Unauthorized; - break; - } - if (response_code == 404) { - rWarning("The specified route could not be found on the server."); - err_ = RouteLoadError::FileNotFound; - break; - } - +bool Route::loadFromServer() { + std::string result = PyDownloader::getRouteFiles(route_.str); + if (result.empty()) { err_ = RouteLoadError::NetworkError; - rWarning("Retrying %d/%d", i, retries); - util::sleep_for(3000); - } - - return false; -} - -bool Route::loadFromJson(const std::string &json) { - const static std::regex rx(R"(\/(\d+)\/)"); - std::string err; - auto jsonData = json11::Json::parse(json, err); - if (!err.empty()) { - rWarning("JSON parsing error: %s", err.c_str()); + rWarning("Failed to fetch route files from server"); return false; } - for (const auto &value : jsonData.object_items()) { + + // Check for error field in JSON response + std::string parse_err; + auto json = json11::Json::parse(result, parse_err); + if (!parse_err.empty()) { + err_ = RouteLoadError::NetworkError; + rWarning("Failed to parse route files response"); + return false; + } + + if (json.is_object() && json["error"].is_string()) { + const std::string &error = json["error"].string_value(); + if (error == "unauthorized") { + rWarning(">> Unauthorized. Authenticate with tools/lib/auth.py <<"); + err_ = RouteLoadError::Unauthorized; + } else if (error == "not_found") { + rWarning("The specified route could not be found on the server."); + err_ = RouteLoadError::FileNotFound; + } else { + rWarning("API error: %s", error.c_str()); + err_ = RouteLoadError::NetworkError; + } + return false; + } + + return loadFromJson(json); +} + +bool Route::loadFromJson(const json11::Json &json) { + const static std::regex rx(R"(\/(\d+)\/)"); + for (const auto &value : json.object_items()) { const auto &urlArray = value.second.array_items(); for (const auto &url : urlArray) { std::string url_str = url.string_value(); @@ -225,10 +226,10 @@ void Segment::loadFile(int id, const std::string file) { bool success = false; if (id < MAX_CAMERAS) { frames[id] = std::make_unique(); - success = frames[id]->load((CameraType)id, file, flags & REPLAY_FLAG_NO_HW_DECODER, &abort_, local_cache, 20 * 1024 * 1024, 3); + success = frames[id]->load((CameraType)id, file, flags & REPLAY_FLAG_NO_HW_DECODER, &abort_, local_cache); } else { log = std::make_unique(filters_); - success = log->load(file, &abort_, local_cache, 0, 3); + success = log->load(file, &abort_, local_cache); } if (!success) { diff --git a/tools/replay/route.h b/tools/replay/route.h index 0375252a19..119a81152e 100644 --- a/tools/replay/route.h +++ b/tools/replay/route.h @@ -8,6 +8,7 @@ #include #include +#include "third_party/json11/json11.hpp" #include "tools/replay/framereader.h" #include "tools/replay/logreader.h" #include "tools/replay/util.h" @@ -55,8 +56,8 @@ protected: bool loadSegments(); bool loadFromAutoSource(); bool loadFromLocal(); - bool loadFromServer(int retries = 3); - bool loadFromJson(const std::string &json); + bool loadFromServer(); + bool loadFromJson(const json11::Json &json); void addFileToSegment(int seg_num, const std::string &file); RouteIdentifier route_ = {}; std::string data_dir_; diff --git a/tools/replay/tests/test_replay.cc b/tools/replay/tests/test_replay.cc index aed3de59a8..45fcc98191 100644 --- a/tools/replay/tests/test_replay.cc +++ b/tools/replay/tests/test_replay.cc @@ -1,5 +1,6 @@ #define CATCH_CONFIG_MAIN #include "catch2/catch.hpp" +#include "tools/replay/filereader.h" #include "tools/replay/replay.h" const std::string TEST_RLOG_URL = "https://commadataci.blob.core.windows.net/openpilotci/0c94aa1e1296d7c6/2021-05-05--19-48-37/0/rlog.bz2"; diff --git a/tools/replay/timeline.cc b/tools/replay/timeline.cc index 39ea8b1bed..08dca5c89e 100644 --- a/tools/replay/timeline.cc +++ b/tools/replay/timeline.cc @@ -55,7 +55,7 @@ void Timeline::buildTimeline(const Route &route, uint64_t route_start_ts, bool l if (should_exit_) break; auto log = std::make_shared(); - if (!log->load(segment.second.qlog, &should_exit_, local_cache, 0, 3) || log->events.empty()) { + if (!log->load(segment.second.qlog, &should_exit_, local_cache) || log->events.empty()) { continue; // Skip if log loading fails or no events } diff --git a/tools/replay/util.cc b/tools/replay/util.cc index 481564322e..7294de8282 100644 --- a/tools/replay/util.cc +++ b/tools/replay/util.cc @@ -1,20 +1,13 @@ #include "tools/replay/util.h" #include -#include #include #include -#include -#include #include #include -#include #include -#include #include -#include -#include #include #include "common/timing.h" @@ -51,91 +44,6 @@ void logMessage(ReplyMsgType type, const char *fmt, ...) { free(msg_buf); } -namespace { - -struct CURLGlobalInitializer { - CURLGlobalInitializer() { curl_global_init(CURL_GLOBAL_DEFAULT); } - ~CURLGlobalInitializer() { curl_global_cleanup(); } -}; - -static CURLGlobalInitializer curl_initializer; - -template -struct MultiPartWriter { - T *buf; - size_t *total_written; - size_t offset; - size_t end; - - size_t write(char *data, size_t size, size_t count) { - size_t bytes = size * count; - if ((offset + bytes) > end) return 0; - - if constexpr (std::is_same::value) { - memcpy(buf->data() + offset, data, bytes); - } else if constexpr (std::is_same::value) { - buf->seekp(offset); - buf->write(data, bytes); - } - - offset += bytes; - *total_written += bytes; - return bytes; - } -}; - -template -size_t write_cb(char *data, size_t size, size_t count, void *userp) { - auto w = (MultiPartWriter *)userp; - return w->write(data, size, count); -} - -size_t dumy_write_cb(char *data, size_t size, size_t count, void *userp) { return size * count; } - -struct DownloadStats { - void installDownloadProgressHandler(DownloadProgressHandler handler) { - std::lock_guard lk(lock); - download_progress_handler = handler; - } - - void add(const std::string &url, uint64_t total_bytes) { - std::lock_guard lk(lock); - items[url] = {0, total_bytes}; - } - - void remove(const std::string &url) { - std::lock_guard lk(lock); - items.erase(url); - } - - void update(const std::string &url, uint64_t downloaded, bool success = true) { - std::lock_guard lk(lock); - items[url].first = downloaded; - - auto stat = std::accumulate(items.begin(), items.end(), std::pair{}, [=](auto &a, auto &b){ - return std::pair{a.first + b.second.first, a.second + b.second.second}; - }); - double tm = millis_since_boot(); - if (download_progress_handler && ((tm - prev_tm) > 500 || !success || stat.first >= stat.second)) { - download_progress_handler(stat.first, stat.second, success); - prev_tm = tm; - } - } - - std::mutex lock; - std::map> items; - double prev_tm = 0; - DownloadProgressHandler download_progress_handler = nullptr; -}; - -static DownloadStats download_stats; - -} // namespace - -void installDownloadProgressHandler(DownloadProgressHandler handler) { - download_stats.installDownloadProgressHandler(handler); -} - std::string formattedDataSize(size_t size) { if (size < 1024) { return std::to_string(size) + " B"; @@ -146,140 +54,11 @@ std::string formattedDataSize(size_t size) { } } -size_t getRemoteFileSize(const std::string &url, std::atomic *abort) { - CURL *curl = curl_easy_init(); - if (!curl) return -1; - - curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, dumy_write_cb); - curl_easy_setopt(curl, CURLOPT_HEADER, 1); - curl_easy_setopt(curl, CURLOPT_NOBODY, 1); - curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1); - curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); - - CURLM *cm = curl_multi_init(); - curl_multi_add_handle(cm, curl); - int still_running = 1; - while (still_running > 0 && !(abort && *abort)) { - CURLMcode mc = curl_multi_perform(cm, &still_running); - if (mc != CURLM_OK) break; - if (still_running > 0) { - curl_multi_wait(cm, nullptr, 0, 1000, nullptr); - } - } - - double content_length = -1; - curl_easy_getinfo(curl, CURLINFO_CONTENT_LENGTH_DOWNLOAD, &content_length); - curl_multi_remove_handle(cm, curl); - curl_easy_cleanup(curl); - curl_multi_cleanup(cm); - return content_length > 0 ? (size_t)content_length : 0; -} - std::string getUrlWithoutQuery(const std::string &url) { size_t idx = url.find("?"); return (idx == std::string::npos ? url : url.substr(0, idx)); } -template -bool httpDownload(const std::string &url, T &buf, size_t chunk_size, size_t content_length, std::atomic *abort) { - download_stats.add(url, content_length); - - int parts = 1; - if (chunk_size > 0 && content_length > 10 * 1024 * 1024) { - parts = std::nearbyint(content_length / (float)chunk_size); - parts = std::clamp(parts, 1, 5); - } - - CURLM *cm = curl_multi_init(); - size_t written = 0; - std::map> writers; - const int part_size = content_length / parts; - for (int i = 0; i < parts; ++i) { - CURL *eh = curl_easy_init(); - writers[eh] = { - .buf = &buf, - .total_written = &written, - .offset = (size_t)(i * part_size), - .end = i == parts - 1 ? content_length : (i + 1) * part_size, - }; - curl_easy_setopt(eh, CURLOPT_WRITEFUNCTION, write_cb); - curl_easy_setopt(eh, CURLOPT_WRITEDATA, (void *)(&writers[eh])); - curl_easy_setopt(eh, CURLOPT_URL, url.c_str()); - curl_easy_setopt(eh, CURLOPT_RANGE, util::string_format("%d-%d", writers[eh].offset, writers[eh].end - 1).c_str()); - curl_easy_setopt(eh, CURLOPT_HTTPGET, 1); - curl_easy_setopt(eh, CURLOPT_NOSIGNAL, 1); - curl_easy_setopt(eh, CURLOPT_FOLLOWLOCATION, 1); - - curl_multi_add_handle(cm, eh); - } - - int still_running = 1; - size_t prev_written = 0; - while (still_running > 0 && !(abort && *abort)) { - CURLMcode mc = curl_multi_perform(cm, &still_running); - if (mc != CURLM_OK) { - break; - } - if (still_running > 0) { - curl_multi_wait(cm, nullptr, 0, 1000, nullptr); - } - - if (((written - prev_written) / (double)content_length) >= 0.01) { - download_stats.update(url, written); - prev_written = written; - } - } - - CURLMsg *msg; - int msgs_left = -1; - int complete = 0; - while ((msg = curl_multi_info_read(cm, &msgs_left)) && !(abort && *abort)) { - if (msg->msg == CURLMSG_DONE) { - if (msg->data.result == CURLE_OK) { - long res_status = 0; - curl_easy_getinfo(msg->easy_handle, CURLINFO_RESPONSE_CODE, &res_status); - if (res_status == 206) { - complete++; - } else { - rWarning("Download failed: http error code: %d", res_status); - } - } else { - rWarning("Download failed: connection failure: %d", msg->data.result); - } - } - } - - bool success = complete == parts; - download_stats.update(url, written, success); - download_stats.remove(url); - - for (const auto &[e, w] : writers) { - curl_multi_remove_handle(cm, e); - curl_easy_cleanup(e); - } - curl_multi_cleanup(cm); - - return success; -} - -std::string httpGet(const std::string &url, size_t chunk_size, std::atomic *abort) { - size_t size = getRemoteFileSize(url, abort); - if (size == 0) return {}; - - std::string result(size, '\0'); - return httpDownload(url, result, chunk_size, size, abort) ? result : ""; -} - -bool httpDownload(const std::string &url, const std::string &file, size_t chunk_size, std::atomic *abort) { - size_t size = getRemoteFileSize(url, abort); - if (size == 0) return false; - - std::ofstream of(file, std::ios::binary | std::ios::out); - of.seekp(size - 1).write("\0", 1); - return httpDownload(url, of, chunk_size, size, abort); -} - std::string decompressBZ2(const std::string &in, std::atomic *abort) { return decompressBZ2((std::byte *)in.data(), in.size(), abort); } diff --git a/tools/replay/util.h b/tools/replay/util.h index 1f61951d21..ee92190337 100644 --- a/tools/replay/util.h +++ b/tools/replay/util.h @@ -53,12 +53,6 @@ std::string decompressBZ2(const std::byte *in, size_t in_size, std::atomic std::string decompressZST(const std::string &in, std::atomic *abort = nullptr); std::string decompressZST(const std::byte *in, size_t in_size, std::atomic *abort = nullptr); std::string getUrlWithoutQuery(const std::string &url); -size_t getRemoteFileSize(const std::string &url, std::atomic *abort = nullptr); -std::string httpGet(const std::string &url, size_t chunk_size = 0, std::atomic *abort = nullptr); - -typedef std::function DownloadProgressHandler; -void installDownloadProgressHandler(DownloadProgressHandler); -bool httpDownload(const std::string &url, const std::string &file, size_t chunk_size = 0, std::atomic *abort = nullptr); std::string formattedDataSize(size_t size); std::string extractFileName(const std::string& file); std::vector split(std::string_view source, char delimiter); From 8856585129e374ccb2a149dadb091478606d7c42 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sat, 28 Feb 2026 21:14:51 -0800 Subject: [PATCH 290/311] new demo route (#37457) --- selfdrive/debug/mem_usage.py | 2 +- tools/cabana/README.md | 6 +++--- tools/clip/run.py | 2 +- tools/jotpluggler/README.md | 10 +++++----- tools/jotpluggler/pluggle.py | 2 +- tools/plotjuggler/README.md | 8 ++++---- tools/plotjuggler/juggle.py | 2 +- tools/replay/README.md | 8 ++++---- tools/replay/replay.h | 2 +- 9 files changed, 21 insertions(+), 21 deletions(-) diff --git a/selfdrive/debug/mem_usage.py b/selfdrive/debug/mem_usage.py index 3451bfc3d6..bc0e97e7ca 100755 --- a/selfdrive/debug/mem_usage.py +++ b/selfdrive/debug/mem_usage.py @@ -8,7 +8,7 @@ import numpy as np from openpilot.common.utils import tabulate from openpilot.tools.lib.logreader import LogReader -DEMO_ROUTE = "a2a0ccea32023010|2023-07-27--13-01-19" +DEMO_ROUTE = "5beb9b58bd12b691/0000010a--a51155e496" MB = 1024 * 1024 TABULATE_OPTS = dict(tablefmt="simple_grid", stralign="center", numalign="center") diff --git a/tools/cabana/README.md b/tools/cabana/README.md index 7933098e34..a721b1aa13 100644 --- a/tools/cabana/README.md +++ b/tools/cabana/README.md @@ -45,17 +45,17 @@ cabana --demo To load a specific route for replay, provide the route as an argument: ```shell -cabana "a2a0ccea32023010|2023-07-27--13-01-19" +cabana "5beb9b58bd12b691/0000010a--a51155e496" ``` -Replace "0ccea32023010|2023-07-27--13-01-19" with your desired route identifier. +Replace "5beb9b58bd12b691/0000010a--a51155e496" with your desired route identifier. ### Running Cabana with multiple cameras To run Cabana with multiple cameras, use the following command: ```shell -cabana "a2a0ccea32023010|2023-07-27--13-01-19" --dcam --ecam +cabana "5beb9b58bd12b691/0000010a--a51155e496" --dcam --ecam ``` ### Streaming CAN Messages from a comma Device diff --git a/tools/clip/run.py b/tools/clip/run.py index 5711cafa59..a5587f239b 100755 --- a/tools/clip/run.py +++ b/tools/clip/run.py @@ -24,7 +24,7 @@ from openpilot.common.utils import Timer from msgq.visionipc import VisionIpcServer, VisionStreamType FRAMERATE = 20 -DEMO_ROUTE, DEMO_START, DEMO_END = 'a2a0ccea32023010/2023-07-27--13-01-19', 90, 105 +DEMO_ROUTE, DEMO_START, DEMO_END = '5beb9b58bd12b691/0000010a--a51155e496', 90, 105 logger = logging.getLogger('clip') diff --git a/tools/jotpluggler/README.md b/tools/jotpluggler/README.md index c5b43dbd3a..d5e4b8ab0f 100644 --- a/tools/jotpluggler/README.md +++ b/tools/jotpluggler/README.md @@ -21,17 +21,17 @@ options: Example using route name: -`./pluggle.py "a2a0ccea32023010/2023-07-27--13-01-19"` +`./pluggle.py "5beb9b58bd12b691/0000010a--a51155e496"` Examples using segment: -`./pluggle.py "a2a0ccea32023010/2023-07-27--13-01-19/1"` +`./pluggle.py "5beb9b58bd12b691/0000010a--a51155e496/1"` -`./pluggle.py "a2a0ccea32023010/2023-07-27--13-01-19/1/q" # use qlogs` +`./pluggle.py "5beb9b58bd12b691/0000010a--a51155e496/1/q" # use qlogs` Example using segment range: -`./pluggle.py "a2a0ccea32023010/2023-07-27--13-01-19/0:1"` +`./pluggle.py "5beb9b58bd12b691/0000010a--a51155e496/0:1"` ## Demo @@ -41,7 +41,7 @@ For a quick demo, run this command: ## Basic Usage/Features: -- The text box to load a route is a the top left of the page, accepts standard openpilot format routes (e.g. `a2a0ccea32023010/2023-07-27--13-01-19/0:1`, `https://connect.comma.ai/a2a0ccea32023010/2023-07-27--13-01-19/`) +- The text box to load a route is a the top left of the page, accepts standard openpilot format routes (e.g. `5beb9b58bd12b691/0000010a--a51155e496/0:1`, `https://connect.comma.ai/5beb9b58bd12b691/0000010a--a51155e496/`) - The Play/Pause button is at the bottom of the screen, you can drag the bottom slider to seek. The timeline in timeseries plots are synced with the slider. - The Timeseries List sidebar has several dropdowns, the fields each show the field name and value, synced with the timeline (will show N/A until the time of the first message in that field is reached). - There is a search bar for the timeseries list, you can search for structs or fields, or both by separating with a "/" diff --git a/tools/jotpluggler/pluggle.py b/tools/jotpluggler/pluggle.py index 92664ae5b3..2fb6e3e2f4 100755 --- a/tools/jotpluggler/pluggle.py +++ b/tools/jotpluggler/pluggle.py @@ -12,7 +12,7 @@ from openpilot.tools.jotpluggler.data import DataManager from openpilot.tools.jotpluggler.datatree import DataTree from openpilot.tools.jotpluggler.layout import LayoutManager -DEMO_ROUTE = "a2a0ccea32023010|2023-07-27--13-01-19" +DEMO_ROUTE = "5beb9b58bd12b691/0000010a--a51155e496" class WorkerManager: diff --git a/tools/plotjuggler/README.md b/tools/plotjuggler/README.md index 62905a252d..3249971bfc 100644 --- a/tools/plotjuggler/README.md +++ b/tools/plotjuggler/README.md @@ -35,17 +35,17 @@ optional arguments: Example using route name: -`./juggle.py "a2a0ccea32023010/2023-07-27--13-01-19"` +`./juggle.py "5beb9b58bd12b691/0000010a--a51155e496"` Examples using segment: -`./juggle.py "a2a0ccea32023010/2023-07-27--13-01-19/1"` +`./juggle.py "5beb9b58bd12b691/0000010a--a51155e496/1"` -`./juggle.py "a2a0ccea32023010/2023-07-27--13-01-19/1/q" # use qlogs` +`./juggle.py "5beb9b58bd12b691/0000010a--a51155e496/1/q" # use qlogs` Example using segment range: -`./juggle.py "a2a0ccea32023010/2023-07-27--13-01-19/0:1"` +`./juggle.py "5beb9b58bd12b691/0000010a--a51155e496/0:1"` ## Streaming diff --git a/tools/plotjuggler/juggle.py b/tools/plotjuggler/juggle.py index c04efd50b4..c86945ef6b 100755 --- a/tools/plotjuggler/juggle.py +++ b/tools/plotjuggler/juggle.py @@ -21,7 +21,7 @@ juggle_dir = os.path.dirname(os.path.realpath(__file__)) os.environ['LD_LIBRARY_PATH'] = os.environ.get('LD_LIBRARY_PATH', '') + f":{juggle_dir}/bin/" -DEMO_ROUTE = "a2a0ccea32023010|2023-07-27--13-01-19" +DEMO_ROUTE = "5beb9b58bd12b691/0000010a--a51155e496" RELEASES_URL = "https://github.com/commaai/PlotJuggler/releases/download/latest" INSTALL_DIR = os.path.join(juggle_dir, "bin") PLOTJUGGLER_BIN = os.path.join(juggle_dir, "bin/plotjuggler") diff --git a/tools/replay/README.md b/tools/replay/README.md index 794c08f6a3..d2beda9940 100644 --- a/tools/replay/README.md +++ b/tools/replay/README.md @@ -19,7 +19,7 @@ You can replay a route from your comma account by specifying the route name. tools/replay/replay # Example: -tools/replay/replay 'a2a0ccea32023010|2023-07-27--13-01-19' +tools/replay/replay '5beb9b58bd12b691/0000010a--a51155e496' # Replay the default demo route: tools/replay/replay --demo @@ -34,10 +34,10 @@ tools/replay/replay --data_dir="/path_to/route" # Example: # If you have a local route stored at /path_to_routes with segments like: -# a2a0ccea32023010|2023-07-27--13-01-19--0 -# a2a0ccea32023010|2023-07-27--13-01-19--1 +# 5beb9b58bd12b691/0000010a--a51155e496--0 +# 5beb9b58bd12b691/0000010a--a51155e496--1 # You can replay it like this: -tools/replay/replay "a2a0ccea32023010|2023-07-27--13-01-19" --data_dir="/path_to_routes" +tools/replay/replay "5beb9b58bd12b691/0000010a--a51155e496" --data_dir="/path_to_routes" ``` ## Send Messages via ZMQ diff --git a/tools/replay/replay.h b/tools/replay/replay.h index 58c1b71b8a..3e2bc7c00e 100644 --- a/tools/replay/replay.h +++ b/tools/replay/replay.h @@ -12,7 +12,7 @@ #include "tools/replay/seg_mgr.h" #include "tools/replay/timeline.h" -#define DEMO_ROUTE "a2a0ccea32023010|2023-07-27--13-01-19" +#define DEMO_ROUTE "5beb9b58bd12b691/0000010a--a51155e496" enum REPLAY_FLAGS { REPLAY_FLAG_NONE = 0x0000, From d634894300b901fe84b7e575522c834a48f929ba Mon Sep 17 00:00:00 2001 From: Robbe Derks Date: Sat, 28 Feb 2026 21:16:48 -0800 Subject: [PATCH 291/311] Fix thermal sensor readouts on four (#37310) --- cereal/log.capnp | 3 ++- system/hardware/base.py | 3 ++- system/hardware/tici/hardware.py | 8 +++++--- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/cereal/log.capnp b/cereal/log.capnp index 80a604d860..d1f85d325c 100644 --- a/cereal/log.capnp +++ b/cereal/log.capnp @@ -499,7 +499,8 @@ struct DeviceState @0xa4d8b5af2aa492eb { pmicTempC @39 :List(Float32); intakeTempC @46 :Float32; exhaustTempC @47 :Float32; - caseTempC @48 :Float32; + gnssTempC @48 :Float32; + bottomSocTempC @50 :Float32; maxTempC @44 :Float32; # max of other temps, used to control fan thermalZones @38 :List(ThermalZone); thermalStatus @14 :ThermalStatus; diff --git a/system/hardware/base.py b/system/hardware/base.py index c12c0758f5..1a19f908c6 100644 --- a/system/hardware/base.py +++ b/system/hardware/base.py @@ -52,7 +52,8 @@ class ThermalConfig: memory: ThermalZone | None = None intake: ThermalZone | None = None exhaust: ThermalZone | None = None - case: ThermalZone | None = None + gnss: ThermalZone | None = None + bottomSoc: ThermalZone | None = None def get_msg(self): ret = {} diff --git a/system/hardware/tici/hardware.py b/system/hardware/tici/hardware.py index 8219f0a587..15c6e41695 100644 --- a/system/hardware/tici/hardware.py +++ b/system/hardware/tici/hardware.py @@ -321,11 +321,12 @@ class Tici(HardwareBase): os.system("sudo poweroff") def get_thermal_config(self): - intake, exhaust, case = None, None, None + intake, exhaust, gnss, bottomSoc = None, None, None, None if self.get_device_type() == "mici": - case = ThermalZone("case") + gnss = ThermalZone("gnss") intake = ThermalZone("intake") exhaust = ThermalZone("exhaust") + bottomSoc = ThermalZone("bottom_soc") return ThermalConfig(cpu=[ThermalZone(f"cpu{i}-silver-usr") for i in range(4)] + [ThermalZone(f"cpu{i}-gold-usr") for i in range(4)], gpu=[ThermalZone("gpu0-usr"), ThermalZone("gpu1-usr")], @@ -334,7 +335,8 @@ class Tici(HardwareBase): pmic=[ThermalZone("pm8998_tz"), ThermalZone("pm8005_tz")], intake=intake, exhaust=exhaust, - case=case) + gnss=gnss, + bottomSoc=bottomSoc) def set_display_power(self, on): try: From d44fde71176764e93112dd06ddc018bb509d0a3b Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Sat, 28 Feb 2026 21:50:59 -0800 Subject: [PATCH 292/311] multilang: return original string if missing (#37487) should return og if not there --- system/ui/lib/multilang.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/ui/lib/multilang.py b/system/ui/lib/multilang.py index 343c06a1e8..7f191bc75e 100644 --- a/system/ui/lib/multilang.py +++ b/system/ui/lib/multilang.py @@ -181,7 +181,7 @@ class Multilang: self.setup() def tr(self, text: str) -> str: - return self._translations.get(text, text) + return self._translations.get(text, text) or text def trn(self, singular: str, plural: str, n: int) -> str: if singular in self._plurals: From b10c2ada79029de2c8dc166ed40d5b3ecce5821f Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Sat, 28 Feb 2026 21:52:37 -0800 Subject: [PATCH 293/311] ui: match updater/setup/installer figma text styles (#37500) * from figma * match setup figma now * lint --- selfdrive/ui/installer/installer.cc | 10 +++---- system/ui/mici_setup.py | 41 ++++++++++++++--------------- system/ui/mici_updater.py | 38 +++++++++++++++----------- 3 files changed, 47 insertions(+), 42 deletions(-) diff --git a/selfdrive/ui/installer/installer.cc b/selfdrive/ui/installer/installer.cc index 072fa4e24b..7599454194 100644 --- a/selfdrive/ui/installer/installer.cc +++ b/selfdrive/ui/installer/installer.cc @@ -30,7 +30,7 @@ const std::string VALID_CACHE_PATH = "/data/.openpilot_cache"; #define TMP_INSTALL_PATH "/data/tmppilot" -const int FONT_SIZE = 120; +const int FONT_SIZE = 160; extern const uint8_t str_continue[] asm("_binary_selfdrive_ui_installer_continue_openpilot_sh_start"); extern const uint8_t str_continue_end[] asm("_binary_selfdrive_ui_installer_continue_openpilot_sh_end"); @@ -88,7 +88,7 @@ void finishInstall() { int text_width = MeasureText(m, FONT_SIZE); DrawTextEx(font_display, m, (Vector2){(float)(GetScreenWidth() - text_width)/2 + FONT_SIZE, (float)(GetScreenHeight() - FONT_SIZE)/2}, FONT_SIZE, 0, WHITE); } else { - DrawTextEx(font_display, "finishing setup", (Vector2){8, 10}, 82, 0, WHITE); + DrawTextEx(font_display, "finishing setup", (Vector2){12, 0}, 77, 0, (Color){255, 255, 255, (unsigned char)(255 * 0.9)}); } EndDrawing(); util::sleep_for(60 * 1000); @@ -106,10 +106,10 @@ void renderProgress(int progress) { DrawRectangleRec(bar, (Color){70, 91, 234, 255}); DrawTextEx(font_inter, (std::to_string(progress) + "%").c_str(), (Vector2){150, 670}, 85, 0, WHITE); } else { - DrawTextEx(font_display, "installing", (Vector2){8, 10}, 82, 0, WHITE); + DrawTextEx(font_display, "installing...", (Vector2){12, 0}, 77, 0, (Color){255, 255, 255, (unsigned char)(255 * 0.9)}); const std::string percent_str = std::to_string(progress) + "%"; - DrawTextEx(font_roman, percent_str.c_str(), (Vector2){6, (float)(GetScreenHeight() - 128 + 18)}, 128, 0, - (Color){255, 255, 255, (unsigned char)(255 * 0.9 * 0.35)}); + DrawTextEx(font_inter, percent_str.c_str(), (Vector2){12, (float)(GetScreenHeight() - 154 + 20)}, 154, 0, + (Color){255, 255, 255, (unsigned char)(255 * 0.9 * 0.65)}); } EndDrawing(); diff --git a/system/ui/mici_setup.py b/system/ui/mici_setup.py index ca63544439..79965f8c6c 100755 --- a/system/ui/mici_setup.py +++ b/system/ui/mici_setup.py @@ -23,8 +23,7 @@ from openpilot.system.ui.lib.wifi_manager import WifiManager from openpilot.system.ui.lib.scroll_panel2 import GuiScrollPanel2 from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.button import (IconButton, SmallButton, WideRoundedButton, SmallerRoundedButton, - SmallCircleIconButton, WidishRoundedButton, SmallRedPillButton, - FullRoundedButton) + SmallCircleIconButton, WidishRoundedButton, FullRoundedButton) from openpilot.system.ui.widgets.label import UnifiedLabel from openpilot.system.ui.widgets.slider import LargerSlider, SmallSlider from openpilot.selfdrive.ui.mici.layouts.settings.network import WifiUIMici @@ -355,9 +354,9 @@ class DownloadingPage(Widget): def __init__(self): super().__init__() - self._title_label = UnifiedLabel("downloading", 64, text_color=rl.Color(255, 255, 255, int(255 * 0.9)), + self._title_label = UnifiedLabel("downloading...", 64, text_color=rl.Color(255, 255, 255, int(255 * 0.9)), font_weight=FontWeight.DISPLAY) - self._progress_label = UnifiedLabel("", 128, text_color=rl.Color(255, 255, 255, int(255 * 0.9 * 0.35)), + self._progress_label = UnifiedLabel("", 132, text_color=rl.Color(255, 255, 255, int(255 * 0.9 * 0.65)), font_weight=FontWeight.ROMAN, alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM) self._progress = 0 @@ -367,15 +366,15 @@ class DownloadingPage(Widget): def _render(self, rect: rl.Rectangle): self._title_label.render(rl.Rectangle( - rect.x + 20, - rect.y + 10, + rect.x + 12, + rect.y + 2, rect.width, 64, )) self._progress_label.render(rl.Rectangle( - rect.x + 20, - rect.y + 20, + rect.x + 12, + rect.y + 18, rect.width, rect.height, )) @@ -390,11 +389,10 @@ class FailedPage(Widget): self._reason_label = UnifiedLabel("", 36, text_color=rl.Color(255, 255, 255, int(255 * 0.9 * 0.65)), font_weight=FontWeight.ROMAN) - self._reboot_button = SmallRedPillButton("reboot") - self._reboot_button.set_click_callback(reboot_callback) - self._reboot_button.set_enabled(lambda: self.enabled) # for nav stack + self._reboot_slider = SmallSlider("reboot", reboot_callback) + self._reboot_slider.set_enabled(lambda: self.enabled) # for nav stack - self._retry_button = WideRoundedButton("retry") + self._retry_button = SmallButton("retry") self._retry_button.set_click_callback(retry_callback) self._retry_button.set_enabled(lambda: self.enabled) # for nav stack @@ -416,20 +414,21 @@ class FailedPage(Widget): 36, )) - self._reboot_button.render(rl.Rectangle( - rect.x + 8, - rect.y + rect.height - self._reboot_button.rect.height, - self._reboot_button.rect.width, - self._reboot_button.rect.height, - )) - + self._retry_button.set_opacity(1 - self._reboot_slider.slider_percentage) self._retry_button.render(rl.Rectangle( - rect.x + 8 + self._reboot_button.rect.width + 8, - rect.y + rect.height - self._retry_button.rect.height, + self._rect.x + 8, + self._rect.y + self._rect.height - self._retry_button.rect.height, self._retry_button.rect.width, self._retry_button.rect.height, )) + self._reboot_slider.render(rl.Rectangle( + self._rect.x + self._rect.width - self._reboot_slider.rect.width, + self._rect.y + self._rect.height - self._reboot_slider.rect.height, + self._reboot_slider.rect.width, + self._reboot_slider.rect.height, + )) + class NetworkSetupPage(Widget): def __init__(self, wifi_manager, continue_callback: Callable, back_callback: Callable): diff --git a/system/ui/mici_updater.py b/system/ui/mici_updater.py index 5de72ac8c4..c98b310709 100755 --- a/system/ui/mici_updater.py +++ b/system/ui/mici_updater.py @@ -7,10 +7,9 @@ from enum import IntEnum from openpilot.system.hardware import HARDWARE from openpilot.system.ui.lib.application import gui_app, FontWeight -from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.lib.wifi_manager import WifiManager from openpilot.system.ui.widgets import Widget -from openpilot.system.ui.widgets.label import gui_text_box, gui_label, UnifiedLabel +from openpilot.system.ui.widgets.label import UnifiedLabel from openpilot.system.ui.widgets.button import FullRoundedButton from openpilot.system.ui.mici_setup import NetworkSetupPage, FailedPage, NetworkConnectivityMonitor @@ -47,7 +46,7 @@ class Updater(Widget): self._continue_button = FullRoundedButton("continue") self._continue_button.set_click_callback(lambda: self.set_current_screen(Screen.WIFI)) - self._title_label = UnifiedLabel("update required", 48, text_color=rl.Color(255, 115, 0, 255), + self._title_label = UnifiedLabel("update required", 48, text_color=rl.Color(255, 255, 255, int(255 * 0.9)), font_weight=FontWeight.DISPLAY) self._subtitle_label = UnifiedLabel("The download size is approximately 1GB.", 36, text_color=rl.Color(255, 255, 255, int(255 * 0.9)), @@ -56,6 +55,12 @@ class Updater(Widget): self._update_failed_page = FailedPage(HARDWARE.reboot, self._update_failed_retry_callback, title="update failed") + self._progress_title_label = UnifiedLabel("", 64, text_color=rl.Color(255, 255, 255, int(255 * 0.9)), + font_weight=FontWeight.DISPLAY, line_height=0.8) + self._progress_percent_label = UnifiedLabel("", 132, text_color=rl.Color(255, 255, 255, int(255 * 0.9 * 0.65)), + font_weight=FontWeight.ROMAN, + alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM) + def _network_setup_back_callback(self): self.set_current_screen(Screen.PROMPT) @@ -139,20 +144,21 @@ class Updater(Widget): )) def render_progress_screen(self, rect: rl.Rectangle): - title_rect = rl.Rectangle(self._rect.x + 6, self._rect.y - 5, self._rect.width - 12, self._rect.height - 8) - if ' ' in self.progress_text: - font_size = 62 - else: - font_size = 82 - gui_text_box(title_rect, self.progress_text, font_size, font_weight=FontWeight.DISPLAY, - color=rl.Color(255, 255, 255, int(255 * 0.9))) + self._progress_title_label.set_text(self.progress_text.replace("_", "_\n") + "...") + self._progress_title_label.render(rl.Rectangle( + rect.x + 12, + rect.y + 2, + rect.width, + self._progress_title_label.get_content_height(int(rect.width - 20)), + )) - progress_value = f"{self.progress_value}%" - text_height = measure_text_cached(gui_app.font(FontWeight.ROMAN), progress_value, 128).y - progress_rect = rl.Rectangle(self._rect.x + 6, self._rect.y + self._rect.height - text_height + 18, - self._rect.width - 12, text_height) - gui_label(progress_rect, progress_value, 128, font_weight=FontWeight.ROMAN, - color=rl.Color(255, 255, 255, int(255 * 0.9 * 0.35))) + self._progress_percent_label.set_text(f"{self.progress_value}%") + self._progress_percent_label.render(rl.Rectangle( + rect.x + 12, + rect.y + 18, + rect.width, + rect.height, + )) def _update_state(self): self._wifi_manager.process_callbacks() From 24d3f07a2f5556ae1a495f1a2ac4de5148742e5b Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Sat, 28 Feb 2026 22:02:42 -0800 Subject: [PATCH 294/311] Add review terms & conditions to device settings w only accept --- selfdrive/ui/mici/layouts/settings/device.py | 9 +++++++-- system/ui/mici_setup.py | 1 + 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/selfdrive/ui/mici/layouts/settings/device.py b/selfdrive/ui/mici/layouts/settings/device.py index b6f6f71d69..a810a5d1a0 100644 --- a/selfdrive/ui/mici/layouts/settings/device.py +++ b/selfdrive/ui/mici/layouts/settings/device.py @@ -12,7 +12,7 @@ from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigCircleButto from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigConfirmationDialogV2 from openpilot.selfdrive.ui.mici.widgets.pairing_dialog import PairingDialog from openpilot.selfdrive.ui.mici.onroad.driver_camera_dialog import DriverCameraDialog -from openpilot.selfdrive.ui.mici.layouts.onboarding import TrainingGuide +from openpilot.selfdrive.ui.mici.layouts.onboarding import TrainingGuide, TermsPage from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.widgets import Widget @@ -308,15 +308,20 @@ class DeviceLayoutMici(NavScroller): review_training_guide_btn.set_click_callback(lambda: gui_app.push_widget(TrainingGuide(completed_callback=gui_app.pop_widget))) review_training_guide_btn.set_enabled(lambda: ui_state.is_offroad()) + terms_btn = BigButton("terms &\nconditions", "", "icons_mici/settings/device/info.png") + terms_btn.set_click_callback(lambda: gui_app.push_widget(TermsPage(on_accept=gui_app.pop_widget))) + terms_btn.set_enabled(lambda: ui_state.is_offroad()) + self._scroller.add_widgets([ DeviceInfoLayoutMici(), UpdateOpenpilotBigButton(), PairBigButton(), review_training_guide_btn, driver_cam_btn, + terms_btn, + regulatory_btn, reset_calibration_btn, uninstall_openpilot_btn, - regulatory_btn, reboot_btn, self._power_off_btn, ]) diff --git a/system/ui/mici_setup.py b/system/ui/mici_setup.py index 79965f8c6c..16a781aa91 100755 --- a/system/ui/mici_setup.py +++ b/system/ui/mici_setup.py @@ -249,6 +249,7 @@ class TermsPage(Widget): pass def _render(self, _): + rl.draw_rectangle_rec(self._rect, rl.BLACK) scroll_offset = round(self._scroll_panel.update(self._rect, self._content_height + self._continue_button.rect.height + 16)) if scroll_offset <= self._scrolled_down_offset: From d1005f3b695226f8a1b9589699cccd5d6a93c0aa Mon Sep 17 00:00:00 2001 From: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com> Date: Sat, 28 Feb 2026 22:03:43 -0800 Subject: [PATCH 295/311] modeld_v2: decouple planplus scaling from accel (#1730) Co-authored-by: Jason Wen --- sunnypilot/modeld_v2/modeld.py | 5 +-- .../modeld_v2/tests/test_recovery_power.py | 37 ++++++++++++------- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/sunnypilot/modeld_v2/modeld.py b/sunnypilot/modeld_v2/modeld.py index 231d9d66d3..f862286181 100755 --- a/sunnypilot/modeld_v2/modeld.py +++ b/sunnypilot/modeld_v2/modeld.py @@ -163,13 +163,12 @@ class ModelState(ModelStateBase): def get_action_from_model(self, model_output: dict[str, np.ndarray], prev_action: log.ModelDataV2.Action, lat_action_t: float, long_action_t: float, v_ego: float) -> log.ModelDataV2.Action: plan = model_output['plan'][0] - if 'planplus' in model_output and self.PLANPLUS_CONTROL != 1.0: - plan = plan + (self.PLANPLUS_CONTROL - 1.0) * model_output['planplus'][0] desired_accel, should_stop = get_accel_from_plan(plan[:, Plan.VELOCITY][:, 0], plan[:, Plan.ACCELERATION][:, 0], self.constants.T_IDXS, action_t=long_action_t) desired_accel = smooth_value(desired_accel, prev_action.desiredAcceleration, self.LONG_SMOOTH_SECONDS) - desired_curvature = get_curvature_from_output(model_output, plan, v_ego, lat_action_t, self.mlsim) + curvature_plan = plan + (self.PLANPLUS_CONTROL - 1.0) * model_output['planplus'][0] if 'planplus' in model_output and self.PLANPLUS_CONTROL != 1.0 else plan + desired_curvature = get_curvature_from_output(model_output, curvature_plan, v_ego, lat_action_t, self.mlsim) if self.generation is not None and self.generation >= 10: # smooth curvature for post FOF models if v_ego > self.MIN_LAT_CONTROL_SPEED: desired_curvature = smooth_value(desired_curvature, prev_action.desiredCurvature, self.LAT_SMOOTH_SECONDS) diff --git a/sunnypilot/modeld_v2/tests/test_recovery_power.py b/sunnypilot/modeld_v2/tests/test_recovery_power.py index e38b2c86dc..cfa2272386 100644 --- a/sunnypilot/modeld_v2/tests/test_recovery_power.py +++ b/sunnypilot/modeld_v2/tests/test_recovery_power.py @@ -15,7 +15,7 @@ class MockStruct: def test_recovery_power_scaling(): state = MockStruct( - PLANPLUS_CONTROL=1.0, + PLANPLUS_CONTROL=0.75, LONG_SMOOTH_SECONDS=0.3, LAT_SMOOTH_SECONDS=0.1, MIN_LAT_CONTROL_SPEED=0.3, @@ -25,37 +25,46 @@ def test_recovery_power_scaling(): ) prev_action = log.ModelDataV2.Action() recorded_vel: list = [] + recorded_curv_plans: list = [] def mock_accel(plan_vel, plan_accel, t_idxs, action_t=0.0): recorded_vel.append(plan_vel.copy()) return 0.0, False + def mock_curvature(output, plan, vego, lat_action_t, mlsim): + recorded_curv_plans.append(plan.copy()) + return 0.0 + modeld.get_accel_from_plan = mock_accel - modeld.get_curvature_from_output = lambda *args: 0.0 + modeld.get_curvature_from_output = mock_curvature plan = np.random.rand(1, 100, 15).astype(np.float32) planplus = np.random.rand(1, 100, 15).astype(np.float32) + merged_plan = plan + planplus model_output: dict = { - 'plan': plan.copy(), + 'plan': merged_plan.copy(), 'planplus': planplus.copy() } test_cases: list = [ - # (control, v_ego, expected_factor) - (0.55, 20.0, 1.0), - (1.0, 25.0, .75), - (1.5, 25.1, 0.75), - (2.0, 20.0, 1.0), - (0.75, 19.0, 1.0), - (0.8, 25.1, 0.75), + # (control, v_ego) + (0.55, 20.0), + (1.0, 25.0), + (1.5, 25.1), + (2.0, 20.0), + (0.75, 19.0), + (0.8, 25.1), ] - for control, v_ego, factor in test_cases: + for control, v_ego in test_cases: state.PLANPLUS_CONTROL = control recorded_vel.clear() + recorded_curv_plans.clear() ModelState.get_action_from_model(state, model_output, prev_action, 0.0, 0.0, v_ego) - expected_recovery_power = control * factor - expected_plan_vel = plan[0, :, Plan.VELOCITY][:, 0] + expected_recovery_power * planplus[0, :, Plan.VELOCITY][:, 0] + expected_accel_plan_vel = plan[0, :, Plan.VELOCITY][:, 0] + planplus[0, :, Plan.VELOCITY][:, 0] + np.testing.assert_allclose(recorded_vel[0], expected_accel_plan_vel, rtol=1e-5, atol=1e-6) - np.testing.assert_allclose(recorded_vel[0], expected_plan_vel, rtol=1e-5, atol=1e-6) + # For the below, yes, I know this isn't the same slicing as fillmodlmsg. This is to show that the values are only scaled on curv + expected_curv_plan_vel = plan[0, :, Plan.VELOCITY][:, 0] + control * planplus[0, :, Plan.VELOCITY][:, 0] + np.testing.assert_allclose(recorded_curv_plans[0][:, Plan.VELOCITY][:, 0], expected_curv_plan_vel, rtol=1e-5, atol=1e-6) From b2201c2a1dc051d974204290d95cca2b14143eca Mon Sep 17 00:00:00 2001 From: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com> Date: Sat, 28 Feb 2026 22:12:16 -0800 Subject: [PATCH 296/311] CI: validate model after build (#1744) Co-authored-by: Jason Wen --- .github/workflows/sunnypilot-build-model.yaml | 9 ++++ release/ci/model_generator.py | 46 +++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/.github/workflows/sunnypilot-build-model.yaml b/.github/workflows/sunnypilot-build-model.yaml index 9414a7fd04..ff09489b92 100644 --- a/.github/workflows/sunnypilot-build-model.yaml +++ b/.github/workflows/sunnypilot-build-model.yaml @@ -176,6 +176,15 @@ jobs: DEV=QCOM FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 python3 "${{ env.MODELS_DIR }}/../get_model_metadata.py" "$onnx_file" || true done + - name: Validate Model Outputs + run: | + source /etc/profile + export UV_PROJECT_ENVIRONMENT=${HOME}/venv + export VIRTUAL_ENV=$UV_PROJECT_ENVIRONMENT + python3 "${{ github.workspace }}/release/ci/model_generator.py" \ + --validate-only \ + --model-dir "${{ env.MODELS_DIR }}" + - name: Prepare Output run: | sudo rm -rf ${{ env.OUTPUT_DIR }} diff --git a/release/ci/model_generator.py b/release/ci/model_generator.py index 96352254b6..da6b933030 100755 --- a/release/ci/model_generator.py +++ b/release/ci/model_generator.py @@ -1,4 +1,5 @@ import os +import pickle import sys import hashlib import json @@ -6,6 +7,41 @@ import re from pathlib import Path from datetime import datetime, UTC +REQUIRED_OUTPUT_KEYS = frozenset({ + "plan", + "lane_lines", + "road_edges", + "lead", + "desire_state", + "desire_pred", + "meta", + "lead_prob", + "lane_lines_prob", + "pose", + "wide_from_device_euler", + "road_transform", + "hidden_state", +}) +OPTIONAL_OUTPUT_KEYS = frozenset({ + "planplus", + "sim_pose", + "desired_curvature", +}) + + +def validate_model_outputs(metadata_paths: list[Path]) -> None: + combined_keys: set[str] = set() + for path in metadata_paths: + with open(path, "rb") as f: + metadata = pickle.load(f) + combined_keys.update(metadata.get("output_slices", {}).keys()) + missing = REQUIRED_OUTPUT_KEYS - combined_keys + if missing: + raise ValueError(f"Combined model metadata is missing required output keys: {sorted(missing)}") + detected_optional = sorted(OPTIONAL_OUTPUT_KEYS & combined_keys) + if detected_optional: + print(f"Optional output keys detected: {detected_optional}") + def create_short_name(full_name): # Remove parentheses and extract alphanumeric words @@ -124,9 +160,19 @@ if __name__ == "__main__": parser.add_argument("--output-dir", default="./output", help="Output directory for metadata") parser.add_argument("--custom-name", help="Custom display name for the model") parser.add_argument("--is-20hz", action="store_true", help="Whether this is a 20Hz model") + parser.add_argument("--validate-only", action="store_true") parser.add_argument("--upstream-branch", default="unknown", help="Upstream branch name") args = parser.parse_args() + if args.validate_only: + metadata_paths = glob.glob(os.path.join(args.model_dir, "*_metadata.pkl")) + if not metadata_paths: + print(f"No metadata files found in {args.model_dir}", file=sys.stderr) + sys.exit(1) + validate_model_outputs([Path(p) for p in metadata_paths]) + print(f"Validated {len(metadata_paths)} metadata files successfully.") + sys.exit(0) + # Find all ONNX files in the given directory model_paths = glob.glob(os.path.join(args.model_dir, "*.onnx")) if not model_paths: From 42f43c32318159e83229cadd363f68d669f98ba6 Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Sun, 1 Mar 2026 07:50:09 +0100 Subject: [PATCH 297/311] sunnylink: Handle exceptions in `getParamsAllKeysV1` to log crashes (#1722) Handle exceptions in `getParamsAllKeysV1` to prevent crashes - Added `try-except` block to improve error handling during key parsing. - Ensures robustness by logging exceptions and re-raising them. Co-authored-by: Jason Wen --- sunnypilot/sunnylink/athena/sunnylinkd.py | 35 ++++++++++++----------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/sunnypilot/sunnylink/athena/sunnylinkd.py b/sunnypilot/sunnylink/athena/sunnylinkd.py index fe0a248100..e8066dca49 100755 --- a/sunnypilot/sunnylink/athena/sunnylinkd.py +++ b/sunnypilot/sunnylink/athena/sunnylinkd.py @@ -202,28 +202,31 @@ def getParamsAllKeysV1() -> dict[str, str]: with open(METADATA_PATH) as f: metadata = json.load(f) except Exception: - cloudlog.exception("sunnylinkd.getParamsAllKeysV1.exception") + cloudlog.exception("sunnylinkd.getParamsAllKeysV1.metadata.exception") metadata = {} - available_keys: list[str] = [k.decode('utf-8') for k in Params().all_keys()] + try: + available_keys: list[str] = [k.decode('utf-8') for k in Params().all_keys()] - params_dict: dict[str, list[dict[str, str | bool | int | object | dict | None]]] = {"params": []} - for key in available_keys: - value = get_param_as_byte(key, get_default=True) + params_dict: dict[str, list[dict[str, str | bool | int | object | dict | None]]] = {"params": []} + for key in available_keys: + value = get_param_as_byte(key, get_default=True) - param_entry = { - "key": key, - "type": int(params.get_type(key).value), - "default_value": base64.b64encode(value).decode('utf-8') if value else None, - } + param_entry = { + "key": key, + "type": int(params.get_type(key).value), + "default_value": base64.b64encode(value).decode('utf-8') if value else None, + } - if key in metadata: - meta_copy = metadata[key].copy() - param_entry["_extra"] = meta_copy + if key in metadata: + meta_copy = metadata[key].copy() + param_entry["_extra"] = meta_copy - params_dict["params"].append(param_entry) - - return {"keys": json.dumps(params_dict.get("params", []))} + params_dict["params"].append(param_entry) + return {"keys": json.dumps(params_dict.get("params", []))} + except Exception: + cloudlog.exception("sunnylinkd.getParamsAllKeysV1.exception") + raise @dispatcher.add_method From 7b104c682b13a0bd3ea3985877e87efa413ac591 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sun, 1 Mar 2026 01:53:50 -0500 Subject: [PATCH 298/311] ci: no more sunnypilot modeld builds --- .github/workflows/sunnypilot-build-prebuilt.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/sunnypilot-build-prebuilt.yaml b/.github/workflows/sunnypilot-build-prebuilt.yaml index dacbacbe26..3966d1a6c9 100644 --- a/.github/workflows/sunnypilot-build-prebuilt.yaml +++ b/.github/workflows/sunnypilot-build-prebuilt.yaml @@ -180,8 +180,6 @@ jobs: ./release/release_files.py | sort | uniq | rsync -rRl${RUNNER_DEBUG:+v} --files-from=- . $BUILD_DIR/ cd $BUILD_DIR sed -i '/from .board.jungle import PandaJungle, PandaJungleDFU/s/^/#/' panda/__init__.py - echo "Building sunnypilot's modeld..." - scons -j$(nproc) cache_dir=${{env.SCONS_CACHE_DIR}} --minimal sunnypilot/modeld echo "Building sunnypilot's modeld_v2..." scons -j$(nproc) cache_dir=${{env.SCONS_CACHE_DIR}} --minimal sunnypilot/modeld_v2 echo "Building sunnypilot's locationd..." @@ -219,7 +217,6 @@ jobs: --exclude='**/.venv/' \ --exclude='selfdrive/modeld/models/driving_vision.onnx' \ --exclude='selfdrive/modeld/models/driving_policy.onnx' \ - --exclude='sunnypilot/modeld*/models/supercombo.onnx' \ --exclude='third_party/*x86*' \ --exclude='third_party/*Darwin*' \ --delete-excluded \ From cfc28176f2cb5654db7ed3d7c99bd8f1dcc1ecf4 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sun, 1 Mar 2026 02:07:23 -0500 Subject: [PATCH 299/311] [TIZI/TICI] ui: Developer UI cleanup (#1746) * [TIZI/TICI] ui: Developer UI cleanup * why 61 --- .../ui/sunnypilot/onroad/circular_alerts.py | 4 +- .../onroad/developer_ui/__init__.py | 37 ++++++++++--------- .../ui/sunnypilot/onroad/driver_state.py | 5 +-- .../ui/sunnypilot/onroad/hud_renderer.py | 6 +-- 4 files changed, 27 insertions(+), 25 deletions(-) diff --git a/selfdrive/ui/sunnypilot/onroad/circular_alerts.py b/selfdrive/ui/sunnypilot/onroad/circular_alerts.py index 8aa4c71d0d..9c9ab7ac84 100644 --- a/selfdrive/ui/sunnypilot/onroad/circular_alerts.py +++ b/selfdrive/ui/sunnypilot/onroad/circular_alerts.py @@ -9,7 +9,7 @@ import pyray as rl from cereal import log from openpilot.selfdrive.ui import UI_BORDER_SIZE from openpilot.selfdrive.ui.ui_state import ui_state -from openpilot.selfdrive.ui.sunnypilot.onroad.developer_ui import DeveloperUiRenderer +from openpilot.selfdrive.ui.sunnypilot.onroad.developer_ui import DeveloperUiState from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE from openpilot.system.ui.lib.text_measure import measure_text_cached @@ -77,7 +77,7 @@ class CircularAlertsRenderer: return e2e_alert_size = 250 - dev_ui_width_adjustment = 180 if ui_state.developer_ui in (DeveloperUiRenderer.DEV_UI_RIGHT, DeveloperUiRenderer.DEV_UI_BOTH) else 100 + dev_ui_width_adjustment = 180 if ui_state.developer_ui in (DeveloperUiState.RIGHT, DeveloperUiState.BOTH) else 100 x = rect.x + rect.width - e2e_alert_size - dev_ui_width_adjustment - (UI_BORDER_SIZE * 3) y = rect.y + rect.height / 2 + 20 diff --git a/selfdrive/ui/sunnypilot/onroad/developer_ui/__init__.py b/selfdrive/ui/sunnypilot/onroad/developer_ui/__init__.py index 8e19f876a2..8204253d32 100644 --- a/selfdrive/ui/sunnypilot/onroad/developer_ui/__init__.py +++ b/selfdrive/ui/sunnypilot/onroad/developer_ui/__init__.py @@ -4,6 +4,8 @@ Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. This file is part of sunnypilot and is licensed under the MIT License. See the LICENSE.md file in the root directory for more details. """ +from enum import IntEnum + import pyray as rl from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.selfdrive.ui.sunnypilot.onroad.developer_ui.elements import ( @@ -17,18 +19,25 @@ from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.widgets import Widget -class DeveloperUiRenderer(Widget): - DEV_UI_OFF = 0 - DEV_UI_BOTTOM = 1 - DEV_UI_RIGHT = 2 - DEV_UI_BOTH = 3 - BOTTOM_BAR_HEIGHT = 61 +def get_bottom_dev_ui_offset(): + if ui_state.developer_ui in (DeveloperUiState.BOTTOM, DeveloperUiState.BOTH): + return 60 + return 0 + +class DeveloperUiState(IntEnum): + OFF = 0 + BOTTOM = 1 + RIGHT = 2 + BOTH = 3 + + +class DeveloperUiRenderer(Widget): def __init__(self): super().__init__() self._font_bold: rl.Font = gui_app.font(FontWeight.BOLD) self._font_semi_bold: rl.Font = gui_app.font(FontWeight.SEMI_BOLD) - self.dev_ui_mode = self.DEV_UI_OFF + self.dev_ui_mode = DeveloperUiState.OFF self.rel_dist_elem = RelDistElement() self.rel_speed_elem = RelSpeedElement() @@ -45,28 +54,22 @@ class DeveloperUiRenderer(Widget): self.bearing_elem = BearingDegElement() self.altitude_elem = AltitudeElement() - @staticmethod - def get_bottom_dev_ui_offset(): - if ui_state.developer_ui in (DeveloperUiRenderer.DEV_UI_BOTTOM, DeveloperUiRenderer.DEV_UI_BOTH): - return DeveloperUiRenderer.BOTTOM_BAR_HEIGHT - return 0 - def _update_state(self) -> None: self.dev_ui_mode = ui_state.developer_ui def _render(self, rect: rl.Rectangle) -> None: - if self.dev_ui_mode == self.DEV_UI_OFF: + if self.dev_ui_mode == DeveloperUiState.OFF: return sm = ui_state.sm if sm.recv_frame["carState"] < ui_state.started_frame: return - if self.dev_ui_mode == self.DEV_UI_BOTTOM: + if self.dev_ui_mode == DeveloperUiState.BOTTOM: self._draw_bottom_dev_ui(rect) - elif self.dev_ui_mode == self.DEV_UI_RIGHT: + elif self.dev_ui_mode == DeveloperUiState.RIGHT: self._draw_right_dev_ui(rect) - elif self.dev_ui_mode == self.DEV_UI_BOTH: + elif self.dev_ui_mode == DeveloperUiState.BOTH: self._draw_right_dev_ui(rect) self._draw_bottom_dev_ui(rect) diff --git a/selfdrive/ui/sunnypilot/onroad/driver_state.py b/selfdrive/ui/sunnypilot/onroad/driver_state.py index 4f2d264ee2..d3239b9e3d 100644 --- a/selfdrive/ui/sunnypilot/onroad/driver_state.py +++ b/selfdrive/ui/sunnypilot/onroad/driver_state.py @@ -8,13 +8,12 @@ import numpy as np from openpilot.selfdrive.ui import UI_BORDER_SIZE from openpilot.selfdrive.ui.onroad.driver_state import DriverStateRenderer, BTN_SIZE, ARC_LENGTH -from openpilot.selfdrive.ui.sunnypilot.onroad.developer_ui import DeveloperUiRenderer +from openpilot.selfdrive.ui.sunnypilot.onroad.developer_ui import get_bottom_dev_ui_offset class DriverStateRendererSP(DriverStateRenderer): def __init__(self): super().__init__() - self.dev_ui_offset = DeveloperUiRenderer.get_bottom_dev_ui_offset() def _pre_calculate_drawing_elements(self): """Pre-calculate all drawing elements based on the current rectangle""" @@ -22,7 +21,7 @@ class DriverStateRendererSP(DriverStateRenderer): width, height = self._rect.width, self._rect.height offset = UI_BORDER_SIZE + BTN_SIZE // 2 self.position_x = self._rect.x + (width - offset if self.is_rhd else offset) - self.position_y = self._rect.y + height - offset - self.dev_ui_offset + self.position_y = self._rect.y + height - offset - get_bottom_dev_ui_offset() # Pre-calculate the face lines positions positioned_keypoints = self.face_keypoints_transformed + np.array([self.position_x, self.position_y]) diff --git a/selfdrive/ui/sunnypilot/onroad/hud_renderer.py b/selfdrive/ui/sunnypilot/onroad/hud_renderer.py index b96a921a8b..2a66b3664b 100644 --- a/selfdrive/ui/sunnypilot/onroad/hud_renderer.py +++ b/selfdrive/ui/sunnypilot/onroad/hud_renderer.py @@ -8,7 +8,7 @@ import pyray as rl from openpilot.common.constants import CV from openpilot.selfdrive.ui.mici.onroad.torque_bar import TorqueBar -from openpilot.selfdrive.ui.sunnypilot.onroad.developer_ui import DeveloperUiRenderer +from openpilot.selfdrive.ui.sunnypilot.onroad.developer_ui import DeveloperUiRenderer, DeveloperUiState, get_bottom_dev_ui_offset from openpilot.selfdrive.ui.sunnypilot.onroad.road_name import RoadNameRenderer from openpilot.selfdrive.ui.sunnypilot.onroad.rocket_fuel import RocketFuel from openpilot.selfdrive.ui.sunnypilot.onroad.speed_limit import SpeedLimitRenderer @@ -133,8 +133,8 @@ class HudRendererSP(HudRenderer): if ui_state.torque_bar and ui_state.sm['controlsState'].lateralControlState.which() != 'angleState': torque_rect = rect - if ui_state.developer_ui in (DeveloperUiRenderer.DEV_UI_BOTTOM, DeveloperUiRenderer.DEV_UI_BOTH): - torque_rect = rl.Rectangle(rect.x, rect.y, rect.width, rect.height - DeveloperUiRenderer.BOTTOM_BAR_HEIGHT) + if ui_state.developer_ui in (DeveloperUiState.BOTTOM, DeveloperUiState.BOTH): + torque_rect = rl.Rectangle(rect.x, rect.y, rect.width, rect.height - get_bottom_dev_ui_offset()) self._torque_bar.render(torque_rect) self.developer_ui.render(rect) From 0376600decdcb8e0a91c5bd0f9fdca22a1b90c65 Mon Sep 17 00:00:00 2001 From: Nayan Date: Sun, 1 Mar 2026 02:22:47 -0500 Subject: [PATCH 300/311] [TIZI/TICI] ui: dynamic alert size (#1634) * dynamic alert sizing * lint * uhhh.. yeah * more --------- Co-authored-by: Jason Wen --- selfdrive/ui/onroad/augmented_road_view.py | 1 + .../ui/sunnypilot/onroad/alert_renderer.py | 103 ++++++++++++++++++ 2 files changed, 104 insertions(+) create mode 100644 selfdrive/ui/sunnypilot/onroad/alert_renderer.py diff --git a/selfdrive/ui/onroad/augmented_road_view.py b/selfdrive/ui/onroad/augmented_road_view.py index bad9da6fab..8abc8fbb52 100644 --- a/selfdrive/ui/onroad/augmented_road_view.py +++ b/selfdrive/ui/onroad/augmented_road_view.py @@ -15,6 +15,7 @@ from openpilot.common.transformations.camera import DEVICE_CAMERAS, DeviceCamera from openpilot.common.transformations.orientation import rot_from_euler if gui_app.sunnypilot_ui(): + from openpilot.selfdrive.ui.sunnypilot.onroad.alert_renderer import AlertRendererSP as AlertRenderer from openpilot.selfdrive.ui.sunnypilot.onroad.augmented_road_view import BORDER_COLORS_SP, AugmentedRoadViewSP from openpilot.selfdrive.ui.sunnypilot.onroad.driver_state import DriverStateRendererSP as DriverStateRenderer from openpilot.selfdrive.ui.sunnypilot.onroad.hud_renderer import HudRendererSP as HudRenderer diff --git a/selfdrive/ui/sunnypilot/onroad/alert_renderer.py b/selfdrive/ui/sunnypilot/onroad/alert_renderer.py new file mode 100644 index 0000000000..9f48287a70 --- /dev/null +++ b/selfdrive/ui/sunnypilot/onroad/alert_renderer.py @@ -0,0 +1,103 @@ +""" +Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + +This file is part of sunnypilot and is licensed under the MIT License. +See the LICENSE.md file in the root directory for more details. +""" +import pyray as rl +from openpilot.selfdrive.ui.onroad.alert_renderer import AlertRenderer, AlertSize, ALERT_FONT_MEDIUM, ALERT_FONT_BIG, \ + ALERT_FONT_SMALL, ALERT_MARGIN, ALERT_HEIGHTS, ALERT_PADDING, Alert +from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.system.ui.lib.text_measure import measure_text_cached +from openpilot.system.ui.lib.wrap_text import wrap_text + +ALERT_LINE_SPACING = 15 + + +class AlertRendererSP(AlertRenderer): + def __init__(self): + super().__init__() + + def _draw_text(self, rect: rl.Rectangle, alert: Alert) -> None: + if alert.size == AlertSize.small: + self._draw_multiline_centered(alert.text1, rect, self.font_bold, ALERT_FONT_MEDIUM) + + elif alert.size == AlertSize.mid: + wrap_width = int(rect.width) + lines1 = wrap_text(self.font_bold, alert.text1, ALERT_FONT_BIG, wrap_width) + lines2 = wrap_text(self.font_regular, alert.text2, ALERT_FONT_SMALL, wrap_width) if alert.text2 else [] + + total_text_height = len(lines1) * measure_text_cached(self.font_bold, "A", ALERT_FONT_BIG).y + if lines2: + total_text_height += ALERT_LINE_SPACING + len(lines2) * measure_text_cached(self.font_regular, "A", ALERT_FONT_SMALL).y + + curr_y = rect.y + (rect.height - total_text_height) / 2 + + for line in lines1: + line_height = measure_text_cached(self.font_bold, alert.text1, ALERT_FONT_BIG).y + self._draw_line_centered(line, rl.Rectangle(rect.x, curr_y, rect.width, line_height), self.font_bold, ALERT_FONT_BIG) + curr_y += line_height + + if lines2: + curr_y += ALERT_LINE_SPACING + for line in lines2: + line_height = measure_text_cached(self.font_regular, alert.text2, ALERT_FONT_SMALL).y + self._draw_line_centered(line, rl.Rectangle(rect.x, curr_y, rect.width, line_height), self.font_regular, ALERT_FONT_SMALL) + curr_y += line_height + + else: + super()._draw_text(rect, alert) + + def _draw_multiline_centered(self, text, rect, font, font_size, color=rl.WHITE) -> None: + lines = wrap_text(font, text, font_size, rect.width) + line_height = measure_text_cached(font, text, font_size).y + total_height = len(lines) * line_height + curr_y = rect.y + (rect.height - total_height) / 2 + for line in lines: + self._draw_line_centered(line, rl.Rectangle(rect.x, curr_y, rect.width, line_height), font, font_size, color) + curr_y += line_height + + def _draw_line_centered(self, text, rect, font, font_size, color=rl.WHITE) -> None: + text_size = measure_text_cached(font, text, font_size) + x = rect.x + (rect.width - text_size.x) / 2 + y = rect.y + rl.draw_text_ex(font, text, rl.Vector2(x, y), font_size, 0, color) + + def _get_alert_rect(self, rect: rl.Rectangle, size: int) -> rl.Rectangle: + if size == AlertSize.full: + return rect + + dev_ui_info = ui_state.developer_ui + v_adjustment = 40 if dev_ui_info in {2, 3} and size != AlertSize.full else 0 + h_adjustment = 230 if dev_ui_info in {1, 3} and size != AlertSize.full else 0 + + w = int(rect.width - ALERT_MARGIN * 2 - h_adjustment) + h = self._calculate_dynamic_height(size, w) + return rl.Rectangle(rect.x + ALERT_MARGIN, rect.y + rect.height - h + ALERT_MARGIN - v_adjustment, w, + h - ALERT_MARGIN * 2) + + def _calculate_dynamic_height(self, size: int, width: int) -> int: + alert = self.get_alert(ui_state.sm) + if not alert: + return ALERT_HEIGHTS.get(size, 271) + + height = 2 * ALERT_PADDING + wrap_width = width - 2 * ALERT_PADDING + + if size == AlertSize.small: + lines = wrap_text(self.font_bold, alert.text1, ALERT_FONT_MEDIUM, wrap_width) + line_height = measure_text_cached(self.font_bold, alert.text1, ALERT_FONT_MEDIUM).y + height += int(len(lines) * line_height) + elif size == AlertSize.mid: + lines1 = wrap_text(self.font_bold, alert.text1, ALERT_FONT_BIG, wrap_width) + line_height1 = measure_text_cached(self.font_bold, alert.text1, ALERT_FONT_BIG).y + height += int(len(lines1) * line_height1) + + if alert.text2: + lines2 = wrap_text(self.font_regular, alert.text2, ALERT_FONT_SMALL, wrap_width) + line_height2 = measure_text_cached(self.font_regular, alert.text2, ALERT_FONT_SMALL).y + height += int(ALERT_LINE_SPACING + len(lines2) * line_height2) + else: + height = ALERT_HEIGHTS.get(size, 271) + + return int(height) From c188c96956ab55267a5a3fbacdd6f0e0f8c4302f Mon Sep 17 00:00:00 2001 From: Chris Date: Sun, 1 Mar 2026 08:37:13 +0100 Subject: [PATCH 301/311] i18n(fr): Add French translations (#1624) i18n(fr): Add French translations for sunnypilot UI Update 36 existing French translations with corrections and add 369 new sunnypilot-specific translation entries. Co-authored-by: Jason Wen --- selfdrive/ui/translations/app_fr.po | 1803 +++++++++++++++++++++++++-- 1 file changed, 1672 insertions(+), 131 deletions(-) diff --git a/selfdrive/ui/translations/app_fr.po b/selfdrive/ui/translations/app_fr.po index 409761588e..ed9900a6ba 100644 --- a/selfdrive/ui/translations/app_fr.po +++ b/selfdrive/ui/translations/app_fr.po @@ -21,12 +21,12 @@ msgstr "" #: selfdrive/ui/layouts/settings/device.py:160 #, python-format msgid " Steering torque response calibration is complete." -msgstr " L'étalonnage de la réponse du couple de direction est terminé." +msgstr " Calibration de la réponse du couple de direction terminée." #: selfdrive/ui/layouts/settings/device.py:158 #, python-format msgid " Steering torque response calibration is {}% complete." -msgstr " L'étalonnage de la réponse du couple de direction est terminé à {}%." +msgstr " Calibration du couple de direction : {}% effectué." #: selfdrive/ui/layouts/settings/device.py:133 #, python-format @@ -61,32 +61,31 @@ msgstr "5G" #: selfdrive/ui/layouts/settings/developer.py:23 msgid "" -"WARNING: openpilot longitudinal control is in alpha for this car and will " -"disable Automatic Emergency Braking (AEB).

On this car, openpilot " -"defaults to the car's built-in ACC instead of openpilot's longitudinal " -"control. Enable this to switch to openpilot longitudinal control. Enabling " -"Experimental mode is recommended when enabling openpilot longitudinal " -"control alpha. Changing this setting will restart openpilot if the car is " -"powered on." +"WARNING: openpilot longitudinal control is in alpha for this car and will" +" disable Automatic Emergency Braking (AEB).

On this car, " +"openpilot defaults to the car's built-in ACC instead of openpilot's " +"longitudinal control. Enable this to switch to openpilot longitudinal " +"control. Enabling Experimental mode is recommended when enabling openpilot " +"longitudinal control alpha. Changing this setting will restart openpilot if " +"the car is powered on." msgstr "" "ATTENTION : le contrôle longitudinal openpilot est en alpha pour cette " -"voiture et désactivera le freinage d'urgence automatique (AEB).

Sur cette voiture, openpilot utilise par défaut le régulateur de " -"vitesse adaptatif intégré au véhicule plutôt que le contrôle longitudinal " -"d'openpilot. Activez ceci pour passer au contrôle longitudinal openpilot. Il " -"est recommandé d'activer le mode expérimental lors de l'activation du " -"contrôle longitudinal openpilot alpha." +"voiture et désactivera le freinage d'urgence automatique " +"(AEB).


Sur cette voiture, openpilot utilise par défaut le " +"régulateur de vitesse adaptatif intégré au véhicule plutôt que le contrôle " +"longitudinal d'openpilot. Activez ceci pour passer au contrôle longitudinal " +"openpilot. Il est recommandé d'activer le mode expérimental lors de " +"l'activation du contrôle longitudinal openpilot alpha." #: selfdrive/ui/layouts/settings/device.py:148 #, python-format msgid "

Steering lag calibration is complete." -msgstr "

L'étalonnage du délai de réponse de la direction est terminé." +msgstr "

La calibration du délai de direction est terminée." #: selfdrive/ui/layouts/settings/device.py:146 #, python-format msgid "

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

L'étalonnage du délai de réponse de la direction est terminé à {}%." +msgstr "

Calibration du délai de direction : {}% effectué." #: selfdrive/ui/layouts/settings/firehose.py:138 #, python-format @@ -95,11 +94,12 @@ msgstr "ACTIF" #: selfdrive/ui/layouts/settings/developer.py:15 msgid "" -"ADB (Android Debug Bridge) allows connecting to your device over USB or over " -"the network. See https://docs.comma.ai/how-to/connect-to-comma for more info." +"ADB (Android Debug Bridge) allows connecting to your device over USB or over" +" the network. See https://docs.comma.ai/how-to/connect-to-comma for more " +"info." msgstr "" -"ADB (Android Debug Bridge) permet de connecter votre appareil via USB ou via " -"le réseau. Voir https://docs.comma.ai/how-to/connect-to-comma pour plus " +"ADB (Android Debug Bridge) permet de connecter votre appareil via USB ou via" +" le réseau. Voir https://docs.comma.ai/how-to/connect-to-comma pour plus " "d'informations." #: selfdrive/ui/widgets/ssh_key.py:30 @@ -109,7 +109,7 @@ msgstr "AJOUTER" #: system/ui/widgets/network.py:139 #, python-format msgid "APN Setting" -msgstr "Paramètres APN" +msgstr "Paramètre APN" #: selfdrive/ui/widgets/offroad_alerts.py:109 #, python-format @@ -139,8 +139,8 @@ msgstr "Surveillance continue du conducteur" #: selfdrive/ui/layouts/settings/toggles.py:186 #, python-format msgid "" -"An alpha version of openpilot longitudinal control can be tested, along with " -"Experimental mode, on non-release branches." +"An alpha version of openpilot longitudinal control can be tested, along with" +" Experimental mode, on non-release branches." msgstr "" "Une version alpha du contrôle longitudinal openpilot peut être testée, avec " "le mode expérimental, sur des branches non publiées." @@ -210,10 +210,11 @@ msgstr "CONNECTER" #: system/ui/widgets/network.py:369 #, python-format msgid "CONNECTING..." -msgstr "CONNECTER..." +msgstr "CONNEXION..." -#: system/ui/widgets/confirm_dialog.py:23 system/ui/widgets/option_dialog.py:35 -#: system/ui/widgets/keyboard.py:81 system/ui/widgets/network.py:318 +#: system/ui/widgets/confirm_dialog.py:23 +#: system/ui/widgets/option_dialog.py:35 system/ui/widgets/keyboard.py:81 +#: system/ui/widgets/network.py:318 #, python-format msgid "Cancel" msgstr "Annuler" @@ -221,7 +222,7 @@ msgstr "Annuler" #: system/ui/widgets/network.py:134 #, python-format msgid "Cellular Metered" -msgstr "Données cellulaire limitées" +msgstr "Données cellulaires limitées" #: selfdrive/ui/layouts/settings/device.py:68 #, python-format @@ -320,7 +321,7 @@ msgstr "Personnalité de conduite" #: system/ui/widgets/network.py:123 system/ui/widgets/network.py:139 #, python-format msgid "EDIT" -msgstr "EDITER" +msgstr "MODIFIER" #: selfdrive/ui/layouts/sidebar.py:138 msgid "ERROR" @@ -354,7 +355,7 @@ msgstr "Activer les alertes de sortie de voie" #: system/ui/widgets/network.py:129 #, python-format msgid "Enable Roaming" -msgstr "Activer openpilot" +msgstr "Activer l'itinérance" #: selfdrive/ui/layouts/settings/developer.py:48 #, python-format @@ -364,7 +365,7 @@ msgstr "Activer SSH" #: system/ui/widgets/network.py:120 #, python-format msgid "Enable Tethering" -msgstr "Activer les alertes de sortie de voie" +msgstr "Activer le partage de connexion" #: selfdrive/ui/layouts/settings/toggles.py:30 msgid "Enable driver monitoring even when openpilot is not engaged." @@ -389,22 +390,22 @@ msgstr "" #: system/ui/widgets/network.py:204 #, python-format msgid "Enter APN" -msgstr "Saisir l'APN" +msgstr "Entrez l'APN" #: system/ui/widgets/network.py:241 #, python-format msgid "Enter SSID" -msgstr "Entrer le SSID" +msgstr "Entrez le SSID" #: system/ui/widgets/network.py:254 #, python-format msgid "Enter new tethering password" -msgstr "Saisir le mot de passe du partage de connexion" +msgstr "Entrez un nouveau mot de passe de partage" #: system/ui/widgets/network.py:237 system/ui/widgets/network.py:314 #, python-format msgid "Enter password" -msgstr "Saisir le mot de passe" +msgstr "Entrez le mot de passe" #: selfdrive/ui/widgets/ssh_key.py:89 #, python-format @@ -424,8 +425,8 @@ msgstr "Mode expérimental" #: selfdrive/ui/layouts/settings/toggles.py:181 #, python-format msgid "" -"Experimental mode is currently unavailable on this car since the car's stock " -"ACC is used for longitudinal control." +"Experimental mode is currently unavailable on this car since the car's stock" +" ACC is used for longitudinal control." msgstr "" "Le mode expérimental est actuellement indisponible sur cette voiture car " "l'ACC d'origine est utilisé pour le contrôle longitudinal." @@ -433,12 +434,12 @@ msgstr "" #: system/ui/widgets/network.py:373 #, python-format msgid "FORGETTING..." -msgstr "OUBLIER..." +msgstr "SUPPRESSION..." #: selfdrive/ui/widgets/setup.py:44 #, python-format msgid "Finish Setup" -msgstr "Terminer la configuration" +msgstr "Finir la config." #: selfdrive/ui/layouts/settings/settings.py:66 msgid "Firehose" @@ -450,47 +451,35 @@ msgstr "Mode Firehose" #: selfdrive/ui/layouts/settings/firehose.py:25 msgid "" -"For maximum effectiveness, bring your device inside and connect to a good " -"USB-C adapter and Wi-Fi weekly.\n" +"For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.\n" "\n" -"Firehose Mode can also work while you're driving if connected to a hotspot " -"or unlimited SIM card.\n" +"Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.\n" "\n" "\n" "Frequently Asked Questions\n" "\n" -"Does it matter how or where I drive? Nope, just drive as you normally " -"would.\n" +"Does it matter how or where I drive? Nope, just drive as you normally would.\n" "\n" -"Do all of my segments get pulled in Firehose Mode? No, we selectively pull a " -"subset of your segments.\n" +"Do all of my segments get pulled in Firehose Mode? No, we selectively pull a subset of your segments.\n" "\n" -"What's a good USB-C adapter? Any fast phone or laptop charger should be " -"fine.\n" +"What's a good USB-C adapter? Any fast phone or laptop charger should be fine.\n" "\n" -"Does it matter which software I run? Yes, only upstream openpilot (and " -"particular forks) are able to be used for training." +"Does it matter which software I run? Yes, only upstream openpilot (and particular forks) are able to be used for training." msgstr "" -"Pour une efficacité maximale, rentrez votre appareil et connectez-le chaque " -"semaine à un bon adaptateur USB-C et au Wi‑Fi.\n" +"Pour une efficacité maximale, rentrez votre appareil et connectez-le chaque semaine à un bon adaptateur USB-C et au Wi‑Fi.\n" "\n" -"Le Mode Firehose peut aussi fonctionner pendant que vous conduisez si vous " -"êtes connecté à un hotspot ou à une carte SIM illimitée.\n" +"Le Mode Firehose peut aussi fonctionner pendant que vous conduisez si vous êtes connecté à un hotspot ou à une carte SIM illimitée.\n" "\n" "\n" "Foire aux questions\n" "\n" -"Est-ce que la manière ou l'endroit où je conduis compte ? Non, conduisez " -"normalement.\n" +"Est-ce que la manière ou l'endroit où je conduis compte ? Non, conduisez normalement.\n" "\n" -"Tous mes segments sont-ils récupérés en Mode Firehose ? Non, nous récupérons " -"de façon sélective un sous-ensemble de vos segments.\n" +"Tous mes segments sont-ils récupérés en Mode Firehose ? Non, nous récupérons de façon sélective un sous-ensemble de vos segments.\n" "\n" -"Quel est un bon adaptateur USB-C ? Tout chargeur rapide de téléphone ou " -"d'ordinateur portable convient.\n" +"Quel est un bon adaptateur USB-C ? Tout chargeur rapide de téléphone ou d'ordinateur portable convient.\n" "\n" -"Le logiciel utilisé importe-t-il ? Oui, seul openpilot amont (et certains " -"forks) peut être utilisé pour l'entraînement." +"Le logiciel utilisé importe-t-il ? Oui, seul openpilot amont (et certains forks) peut être utilisé pour l'entraînement." #: system/ui/widgets/network.py:318 system/ui/widgets/network.py:451 #, python-format @@ -500,11 +489,11 @@ msgstr "Oublier" #: system/ui/widgets/network.py:319 #, python-format msgid "Forget Wi-Fi Network \"{}\"?" -msgstr "Oublier le réseau Wi-Fi \"{}\" ?" +msgstr "Oublier le réseau Wi‑Fi \"{}\" ?" #: selfdrive/ui/layouts/sidebar.py:71 selfdrive/ui/layouts/sidebar.py:125 msgid "GOOD" -msgstr "BON" +msgstr "BONNE" #: selfdrive/ui/widgets/pairing_dialog.py:128 #, python-format @@ -518,7 +507,7 @@ msgstr "ÉLEVÉ" #: system/ui/widgets/network.py:155 #, python-format msgid "Hidden Network" -msgstr "Réseau" +msgstr "Réseau masqué" #: selfdrive/ui/layouts/settings/firehose.py:140 #, python-format @@ -569,14 +558,14 @@ msgstr "MAX" msgid "" "Maximize your training data uploads to improve openpilot's driving models." msgstr "" -"Maximisez vos envois de données d'entraînement pour améliorer les modèles de " -"conduite d'openpilot." +"Maximisez vos envois de données d'entraînement pour améliorer les modèles de" +" conduite d'openpilot." #: selfdrive/ui/layouts/settings/device.py:59 #: selfdrive/ui/layouts/settings/device.py:60 #, python-format msgid "N/A" -msgstr "NC" +msgstr "N/A" #: selfdrive/ui/layouts/sidebar.py:142 msgid "NO" @@ -661,8 +650,8 @@ msgid "" "Pair your device with comma connect (connect.comma.ai) and claim your comma " "prime offer." msgstr "" -"Associez votre appareil à comma connect (connect.comma.ai) et réclamez votre " -"offre comma prime." +"Associez votre appareil à comma connect (connect.comma.ai) et réclamez votre" +" offre comma prime." #: selfdrive/ui/widgets/setup.py:91 #, python-format @@ -679,15 +668,13 @@ msgstr "Éteindre" #, python-format msgid "Prevent large data uploads when on a metered Wi-Fi connection" msgstr "" -"Eviter les transferts de données volumineux lorsque vous êtes connecté à un " -"réseau Wi-Fi limité" +"Empêcher les téléversements volumineux sur une connexion Wi‑Fi limitée" #: system/ui/widgets/network.py:135 #, python-format msgid "Prevent large data uploads when on a metered cellular connection" msgstr "" -"Eviter les transferts de données volumineux lors d'une connexion à un réseau " -"cellulaire limité" +"Empêcher les téléversements volumineux sur une connexion cellulaire limitée" #: selfdrive/ui/layouts/settings/device.py:25 msgid "" @@ -736,11 +723,11 @@ msgstr "Redémarrer et mettre à jour" #: selfdrive/ui/layouts/settings/toggles.py:27 msgid "" "Receive alerts to steer back into the lane when your vehicle drifts over a " -"detected lane line without a turn signal activated while driving over 31 mph " -"(50 km/h)." +"detected lane line without a turn signal activated while driving over 31 mph" +" (50 km/h)." msgstr "" -"Recevez des alertes pour revenir dans la voie lorsque votre véhicule dépasse " -"une ligne de voie détectée sans clignotant activé en roulant au-delà de 31 " +"Recevez des alertes pour revenir dans la voie lorsque votre véhicule dépasse" +" une ligne de voie détectée sans clignotant activé en roulant au-delà de 31 " "mph (50 km/h)." #: selfdrive/ui/layouts/settings/toggles.py:76 @@ -808,17 +795,17 @@ msgstr "Consultez les règles, fonctionnalités et limitations d'openpilot" #: selfdrive/ui/layouts/settings/software.py:61 #, python-format msgid "SELECT" -msgstr "SELECTIONNER" +msgstr "CHOISIR" #: selfdrive/ui/layouts/settings/developer.py:53 #, python-format msgid "SSH Keys" -msgstr "Clefs SSH" +msgstr "Clés SSH" #: system/ui/widgets/network.py:310 #, python-format msgid "Scanning Wi-Fi networks..." -msgstr "Analyse des réseaux Wi-Fi..." +msgstr "Recherche des réseaux Wi‑Fi..." #: system/ui/widgets/option_dialog.py:36 #, python-format @@ -833,7 +820,7 @@ msgstr "Sélectionner une branche" #: selfdrive/ui/layouts/settings/device.py:91 #, python-format msgid "Select a language" -msgstr "Sélectionner un langage" +msgstr "Sélectionner une langue" #: selfdrive/ui/layouts/settings/device.py:60 #, python-format @@ -856,8 +843,8 @@ msgstr "Standard" #: selfdrive/ui/layouts/settings/toggles.py:22 msgid "" -"Standard is recommended. In aggressive mode, openpilot will follow lead cars " -"closer and be more aggressive with the gas and brake. In relaxed mode " +"Standard is recommended. In aggressive mode, openpilot will follow lead cars" +" closer and be more aggressive with the gas and brake. In relaxed mode " "openpilot will stay further away from lead cars. On supported cars, you can " "cycle through these personalities with your steering wheel distance button." msgstr "" @@ -881,7 +868,7 @@ msgstr "REPRENEZ IMMÉDIATEMENT LE CONTRÔLE" #: selfdrive/ui/layouts/sidebar.py:71 selfdrive/ui/layouts/sidebar.py:125 #: selfdrive/ui/layouts/sidebar.py:127 selfdrive/ui/layouts/sidebar.py:129 msgid "TEMP" -msgstr "TEMPÉRATURE" +msgstr "TEMP." #: selfdrive/ui/layouts/settings/software.py:61 #, python-format @@ -891,7 +878,7 @@ msgstr "Branche cible" #: system/ui/widgets/network.py:124 #, python-format msgid "Tethering Password" -msgstr "Mot de passe du partage de connexion" +msgstr "Mot de passe de partage" #: selfdrive/ui/layouts/settings/settings.py:64 msgid "Toggles" @@ -900,7 +887,7 @@ msgstr "Options" #: selfdrive/ui/layouts/settings/software.py:72 #, python-format msgid "UNINSTALL" -msgstr "DÉSINSTALLER" +msgstr "SUPPRIMER" #: selfdrive/ui/layouts/home.py:155 #, python-format @@ -926,15 +913,15 @@ msgstr "" #: selfdrive/ui/widgets/prime.py:33 #, python-format msgid "Upgrade Now" -msgstr "Mettre à niveau maintenant" +msgstr "Mettre à jour" #: selfdrive/ui/layouts/settings/toggles.py:31 msgid "" "Upload data from the driver facing camera and help improve the driver " "monitoring algorithm." msgstr "" -"Téléverser les données de la caméra orientée conducteur et aider à améliorer " -"l'algorithme de surveillance du conducteur." +"Téléverser les données de la caméra orientée conducteur et aider à améliorer" +" l'algorithme de surveillance du conducteur." #: selfdrive/ui/layouts/settings/toggles.py:88 #, python-format @@ -971,8 +958,8 @@ msgid "" "NEVER ask you to add their GitHub username." msgstr "" "Avertissement : Ceci accorde un accès SSH à toutes les clés publiques dans " -"vos paramètres GitHub. N'entrez jamais un nom d'utilisateur GitHub autre que " -"le vôtre. Un employé comma ne vous demandera JAMAIS d'ajouter son nom " +"vos paramètres GitHub. N'entrez jamais un nom d'utilisateur GitHub autre que" +" le vôtre. Un employé comma ne vous demandera JAMAIS d'ajouter son nom " "d'utilisateur GitHub." #: selfdrive/ui/layouts/onboarding.py:111 @@ -992,12 +979,12 @@ msgstr "Wi‑Fi" #: system/ui/widgets/network.py:144 #, python-format msgid "Wi-Fi Network Metered" -msgstr "Réseau Wi-Fi limité" +msgstr "Réseau Wi‑Fi limité" #: system/ui/widgets/network.py:314 #, python-format msgid "Wrong password" -msgstr "Mauvais mot de passe" +msgstr "Mot de passe incorrect" #: selfdrive/ui/layouts/onboarding.py:145 #, python-format @@ -1026,7 +1013,7 @@ msgstr "comma prime" #: system/ui/widgets/network.py:142 #, python-format msgid "default" -msgstr "défaut" +msgstr "par défaut" #: selfdrive/ui/layouts/settings/device.py:133 #, python-format @@ -1051,7 +1038,7 @@ msgstr "km/h" #: system/ui/widgets/network.py:204 #, python-format msgid "leave blank for automatic configuration" -msgstr "ne pas remplir pour une configuration automatique" +msgstr "laisser vide pour configuration automatique" #: selfdrive/ui/layouts/settings/device.py:134 #, python-format @@ -1091,30 +1078,30 @@ msgstr "openpilot indisponible" #: selfdrive/ui/layouts/settings/toggles.py:158 #, python-format msgid "" -"openpilot defaults to driving in chill mode. Experimental mode enables alpha-" -"level features that aren't ready for chill mode. Experimental features are " -"listed below:

End-to-End Longitudinal Control


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

New Driving Visualization


The driving visualization will " -"transition to the road-facing wide-angle camera at low speeds to better show " -"some turns. The Experimental mode logo will also be shown in the top right " -"corner." +"openpilot defaults to driving in chill mode. Experimental mode enables " +"alpha-level features that aren't ready for chill mode. Experimental features" +" are listed below:

End-to-End Longitudinal Control


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

New Driving Visualization


The driving visualization" +" will transition to the road-facing wide-angle camera at low speeds to " +"better show some turns. The Experimental mode logo will also be shown in the" +" top right corner." msgstr "" "openpilot roule par défaut en mode chill. Le mode expérimental active des " "fonctionnalités de niveau alpha qui ne sont pas prêtes pour le mode chill. " "Les fonctionnalités expérimentales sont listées ci‑dessous:

Contrôle " -"longitudinal de bout en bout


Laissez le modèle de conduite contrôler " -"l'accélérateur et les freins. openpilot conduira comme il pense qu'un humain " -"le ferait, y compris s'arrêter aux feux rouges et aux panneaux stop. Comme " -"le modèle décide de la vitesse à adopter, la vitesse réglée n'agira que " -"comme une limite supérieure. C'est une fonctionnalité de qualité alpha ; des " -"erreurs sont à prévoir.

Nouvelle visualisation de conduite


La " -"visualisation passera à la caméra grand angle orientée route à basse vitesse " -"pour mieux montrer certains virages. Le logo du mode expérimental sera " -"également affiché en haut à droite." +"longitudinal de bout en bout
Laissez le modèle de conduite contrôler" +" l'accélérateur et les freins. openpilot conduira comme il pense qu'un " +"humain le ferait, y compris s'arrêter aux feux rouges et aux panneaux stop. " +"Comme le modèle décide de la vitesse à adopter, la vitesse réglée n'agira " +"que comme une limite supérieure. C'est une fonctionnalité de qualité alpha ;" +" des erreurs sont à prévoir.

Nouvelle visualisation de " +"conduite


La visualisation passera à la caméra grand angle orientée " +"route à basse vitesse pour mieux montrer certains virages. Le logo du mode " +"expérimental sera également affiché en haut à droite." #: selfdrive/ui/layouts/settings/device.py:165 #, python-format @@ -1122,24 +1109,18 @@ msgid "" "openpilot is continuously calibrating, resetting is rarely required. " "Resetting calibration will restart openpilot if the car is powered on." msgstr "" -"La modification de ce réglage redémarrera openpilot si la voiture est sous " +" La modification de ce réglage redémarrera openpilot si la voiture est sous " "tension." #: selfdrive/ui/layouts/settings/firehose.py:20 msgid "" "openpilot learns to drive by watching humans, like you, drive.\n" "\n" -"Firehose Mode allows you to maximize your training data uploads to improve " -"openpilot's driving models. More data means bigger models, which means " -"better Experimental Mode." +"Firehose Mode allows you to maximize your training data uploads to improve openpilot's driving models. More data means bigger models, which means better Experimental Mode." msgstr "" -"openpilot apprend à conduire en regardant des humains, comme vous, " -"conduire.\n" +"openpilot apprend à conduire en regardant des humains, comme vous, conduire.\n" "\n" -"Le Mode Firehose vous permet de maximiser vos envois de données " -"d'entraînement pour améliorer les modèles de conduite d'openpilot. Plus de " -"données signifie des modèles plus grands, ce qui signifie un meilleur Mode " -"expérimental." +"Le Mode Firehose vous permet de maximiser vos envois de données d'entraînement pour améliorer les modèles de conduite d'openpilot. Plus de données signifie des modèles plus grands, ce qui signifie un meilleur Mode expérimental." #: selfdrive/ui/layouts/settings/toggles.py:183 #, python-format @@ -1153,8 +1134,8 @@ msgid "" "openpilot requires the device to be mounted within 4° left or right and " "within 5° up or 9° down." msgstr "" -"openpilot exige que l'appareil soit monté à moins de 4° à gauche ou à droite " -"et à moins de 5° vers le haut ou 9° vers le bas." +"openpilot exige que l'appareil soit monté à moins de 4° à gauche ou à droite" +" et à moins de 5° vers le haut ou 9° vers le bas." #: selfdrive/ui/layouts/settings/device.py:134 #, python-format @@ -1164,7 +1145,7 @@ msgstr "droite" #: system/ui/widgets/network.py:142 #, python-format msgid "unmetered" -msgstr "non limité" +msgstr "illimité" #: selfdrive/ui/layouts/settings/device.py:133 #, python-format @@ -1234,3 +1215,1563 @@ msgstr "✓ ABONNÉ" #, python-format msgid "🔥 Firehose Mode 🔥" msgstr "🔥 Mode Firehose 🔥" + +msgid " (Default)" +msgstr " (Par défaut)" + +msgid "%" +msgstr "%" + +msgid "" +"(Only for highest tiers, and does NOT bring ANY benefit to you yet. We are " +"just testing data volume.)" +msgstr "" +"(Réservé aux niveaux les plus élevés, et ne vous apporte AUCUN avantage pour" +" l'instant. Nous testons juste le volume de données.)" + +msgid ", but we'll be here when you're ready to come back." +msgstr ", mais nous serons là quand vous serez prêt à revenir." + +msgid "" +"WARNING: sunnypilot longitudinal control is in alpha for this car and " +"will disable Automatic Emergency Braking (AEB).

On this car, " +"sunnypilot defaults to the car's built-in ACC instead of sunnypilot's " +"longitudinal control. Enable this to switch to sunnypilot longitudinal " +"control. Enabling Experimental mode is recommended when enabling sunnypilot " +"longitudinal control alpha. Changing this setting will restart sunnypilot if" +" the car is powered on." +msgstr "" +"ATTENTION : le contrôle longitudinal sunnypilot est en alpha pour cette " +"voiture et désactivera le freinage d'urgence automatique " +"(AEB).

Sur cette voiture, sunnypilot utilise par défaut le " +"régulateur de vitesse adaptatif intégré au véhicule plutôt que le contrôle " +"longitudinal de sunnypilot. Activez ceci pour passer au contrôle " +"longitudinal sunnypilot. Il est recommandé d'activer le mode expérimental " +"lors de l'activation du contrôle longitudinal sunnypilot alpha. La " +"modification de ce réglage redémarrera sunnypilot si la voiture est sous " +"tension." + +msgid "" +"A beautiful rainbow effect on the path the model wants to take. It does not " +"affect driving in any way." +msgstr "" +"Un magnifique effet arc-en-ciel sur le trajet que le modèle souhaite " +"emprunter. Cela n'affecte en rien la conduite." + +msgid "" +"A chime and on-screen alert will play when the traffic light you are waiting" +" for turns green and you have no vehicle in front of you.
Note: This " +"chime is only designed as a notification. It is the driver's responsibility " +"to observe their environment and make decisions accordingly." +msgstr "" +"Un carillon et une alerte à l'écran se déclencheront lorsque le feu de " +"circulation que vous attendez passera au vert et qu'il n'y a pas de véhicule" +" devant vous.
Note : Ce carillon est uniquement conçu comme une " +"notification. Il incombe au conducteur d'observer son environnement et de " +"prendre les décisions en conséquence." + +msgid "" +"A chime and on-screen alert will play when you are stopped, and the vehicle " +"in front of you start moving.
Note: This chime is only designed as a " +"notification. It is the driver's responsibility to observe their environment" +" and make decisions accordingly." +msgstr "" +"Un carillon et une alerte à l'écran se déclencheront lorsque vous êtes à " +"l'arrêt et que le véhicule devant vous commence à avancer.
Note : Ce " +"carillon est uniquement conçu comme une notification. Il incombe au " +"conducteur d'observer son environnement et de prendre les décisions en " +"conséquence." + +msgid "ALL TIME" +msgstr "TOUT TEMPS" + +msgid "Actuator Delay:" +msgstr "Délai actionneur :" + +msgid "Adjust Lane Turn Speed" +msgstr "Ajuster la vitesse de virage de voie" + +msgid "Adjust Software Delay" +msgstr "Ajuster le délai logiciel" + +msgid "" +"Adjust the software delay when Live Learning Steer Delay is toggled off. The" +" default software delay value is 0.2" +msgstr "" +"Ajuster le délai logiciel lorsque l'apprentissage en direct du délai de " +"direction est désactivé. La valeur par défaut du délai logiciel est 0.2" + +msgid "All" +msgstr "Tout" + +msgid "All states (~6.0 GB)" +msgstr "Toutes les régions (~6.0 Go)" + +msgid "" +"Allows the driver to provide limited steering input while openpilot is " +"engaged." +msgstr "" +"Permet au conducteur de fournir une entrée de direction limitée pendant " +"qu'openpilot est engagé." + +msgid "Always On" +msgstr "Toujours allumé" + +msgid "" +"An alpha version of sunnypilot longitudinal control can be tested, along " +"with Experimental mode, on non-release branches." +msgstr "" +"Une version alpha du contrôle longitudinal sunnypilot peut être testée, avec" +" le mode expérimental, sur des branches non publiées." + +msgid "" +"Apply a custom timeout for settings UI.
This is the time after which " +"settings UI closes automatically if user is not interacting with the screen." +msgstr "" +"Appliquer un délai d'inactivité personnalisé pour l'interface des " +"paramètres.
C'est le temps après lequel l'interface des paramètres se " +"ferme automatiquement si l'utilisateur n'interagit pas avec l'écran." + +msgid "Are you sure you want to backup your current sunnypilot settings?" +msgstr "" +"Êtes-vous sûr de vouloir sauvegarder vos paramètres sunnypilot actuels ?" + +msgid "Are you sure you want to enter Always Offroad mode?" +msgstr "Êtes-vous sûr de vouloir entrer en mode toujours hors route ?" + +msgid "Are you sure you want to exit Always Offroad mode?" +msgstr "Êtes-vous sûr de vouloir quitter le mode toujours hors route ?" + +msgid "" +"Are you sure you want to reset all sunnypilot settings to default? Once the " +"settings are reset, there is no going back." +msgstr "" +"Êtes-vous sûr de vouloir réinitialiser tous les paramètres sunnypilot par " +"défaut ? Une fois les paramètres réinitialisés, il n'y a pas de retour en " +"arrière." + +msgid "" +"Are you sure you want to restore the last backed up sunnypilot settings?" +msgstr "" +"Êtes-vous sûr de vouloir restaurer la dernière sauvegarde des paramètres " +"sunnypilot ?" + +msgid "Assist" +msgstr "Assistance" + +msgid "" +"Assist: Adjusts the vehicle's cruise speed based on the current road's speed" +" limit when operating the +/- buttons." +msgstr "" +"Assistance : Ajuste la vitesse de croisière du véhicule en fonction de la " +"limite de vitesse de la route actuelle lors de l'utilisation des boutons " +"+/-." + +msgid "Auto (Dark)" +msgstr "Auto (Sombre)" + +msgid "Auto (Default)" +msgstr "Auto (Par défaut)" + +msgid "Auto Lane Change by Blinker" +msgstr "Changement de voie auto par clignotant" + +msgid "Auto Lane Change: Delay with Blind Spot" +msgstr "Changement de voie auto : Délai avec angle mort" + +msgid "Backup Failed" +msgstr "Échec de la sauvegarde" + +msgid "Backup Settings" +msgstr "Sauvegarder les paramètres" + +msgid "" +"Become a sponsor of sunnypilot to get early access to sunnylink features " +"when they become available." +msgstr "" +"Devenez parrain de sunnypilot pour obtenir un accès anticipé aux " +"fonctionnalités sunnylink dès leur disponibilité." + +msgid "Bottom" +msgstr "Bas" + +msgid "CLEAR" +msgstr "VIDER" + +msgid "Cancel Download" +msgstr "Annuler le téléchargement" + +msgid "Car First" +msgstr "Voiture d'abord" + +msgid "" +"Car First: Use Speed Limit data from Car if available, else use from " +"OpenStreetMaps" +msgstr "" +"Voiture d'abord : Utiliser les données de limite de vitesse de la voiture si" +" disponibles, sinon utiliser OpenStreetMaps" + +msgid "Car Only" +msgstr "Voiture uniquement" + +msgid "Car Only: Use Speed Limit data only from Car" +msgstr "" +"Voiture uniquement : Utiliser les données de limite de vitesse uniquement de" +" la voiture" + +msgid "" +"Changing this setting will restart sunnypilot if the car is powered on." +msgstr "" +"La modification de ce réglage redémarrera sunnypilot si la voiture est sous " +"tension." + +msgid "" +"Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is " +"manually pressed in sunnypilot." +msgstr "" +"Choisissez comment le centrage automatique de voie (ALC) se comporte après " +"un appui manuel sur la pédale de frein dans sunnypilot." + +msgid "Choose your sponsorship tier and confirm your support" +msgstr "Choisissez votre niveau de parrainage et confirmez votre soutien" + +msgid "Clear Cache" +msgstr "Vider le cache" + +msgid "Clear Model Cache" +msgstr "Vider le cache des modèles" + +msgid "Click the Sponsor button for more details" +msgstr "Cliquez sur le bouton Sponsor pour plus de détails" + +msgid "Colors represent vehicle fingerprint status:" +msgstr "Les couleurs représentent le statut d'empreinte du véhicule :" + +msgid "Combined" +msgstr "Combiné" + +msgid "Combined: Use combined Speed Limit data from Car & OpenStreetMaps" +msgstr "" +"Combiné : Utiliser les données combinées de limite de vitesse de la voiture " +"et d'OpenStreetMaps" + +msgid "Confirm" +msgstr "Confirmer" + +msgid "Controls state of the device after boot/sleep." +msgstr "Contrôle l'état de l'appareil après démarrage/veille." + +msgid "Cooperative Steering (Beta)" +msgstr "Direction coopérative (Bêta)" + +msgid "Country" +msgstr "Pays" + +msgid "Cruise" +msgstr "Régulateur" + +msgid "Current Model" +msgstr "Modèle actuel" + +msgid "Custom ACC Speed Increments" +msgstr "Incréments de vitesse ACC personnalisés" + +msgid "Custom Longitudinal Tuning" +msgstr "Réglage longitudinal personnalisé" + +msgid "Customize Lane Change" +msgstr "Personnaliser le changement de voie" + +msgid "Customize MADS" +msgstr "Personnaliser MADS" + +msgid "Customize Source" +msgstr "Personnaliser la source" + +msgid "Customize Torque Params" +msgstr "Personnaliser les paramètres de couple" + +msgid "DELETE" +msgstr "SUPPRIMER" + +msgid "DISABLED" +msgstr "DÉSACTIVÉ" + +msgid "Database Update" +msgstr "Mise à jour de la base de données" + +msgid "Decline, uninstall sunnypilot" +msgstr "Refuser, désinstaller sunnypilot" + +msgid "Default" +msgstr "Par défaut" + +msgid "Default Model" +msgstr "Modèle par défaut" + +msgid "Default: Device will boot/wake-up normally & will be ready to engage." +msgstr "Par défaut : L'appareil démarrera normalement et sera prêt à engager." + +msgid "Delay before lateral control resumes after the turn signal ends." +msgstr "" +"Délai avant la reprise du contrôle latéral après la fin du clignotant." + +msgid "Developer UI" +msgstr "Interface développeur" + +msgid "" +"Device will automatically shutdown after set time once the engine is turned off.\n" +"(30h is the default)" +msgstr "" +"L'appareil s'éteindra automatiquement après le temps défini une fois le moteur coupé.\n" +"(30h par défaut)" + +msgid "Disable" +msgstr "Désactiver" + +msgid "Disable Updates" +msgstr "Désactiver les mises à jour" + +msgid "" +"Disable the sunnypilot Longitudinal Control (alpha) toggle to allow " +"Intelligent Cruise Button Management." +msgstr "" +"Désactivez l'option contrôle longitudinal sunnypilot (alpha) pour permettre " +"la gestion intelligente des boutons de régulateur." + +msgid "Disengage" +msgstr "Désengager" + +msgid "Disengage to Enter Always Offroad Mode" +msgstr "Désengager pour entrer en mode toujours hors route" + +msgid "Disengage: ALC will disengage when the brake pedal is pressed." +msgstr "" +"Désengager : ALC se désengagera lorsque la pédale de frein est enfoncée." + +msgid "Display" +msgstr "Affichage" + +msgid "Display Metrics Below Chevron" +msgstr "Afficher les métriques sous le chevron" + +msgid "Display Road Name" +msgstr "Afficher le nom de la route" + +msgid "Display Turn Signals" +msgstr "Afficher les clignotants" + +msgid "Display real-time parameters and metrics from various sources." +msgstr "" +"Afficher les paramètres et métriques en temps réel provenant de diverses " +"sources." + +msgid "" +"Display steering arc on the driving screen when lateral control is enabled." +msgstr "" +"Afficher l'arc de direction sur l'écran de conduite lorsque le contrôle " +"latéral est activé." + +msgid "" +"Display useful metrics below the chevron that tracks the lead car only " +"applicable to cars with sunnypilot longitudinal control." +msgstr "" +"Afficher des métriques utiles sous le chevron qui suit le véhicule en tête, " +"uniquement applicable aux voitures avec le contrôle longitudinal sunnypilot." + +msgid "" +"Displays the name of the road the car is traveling on.
The OpenStreetMap " +"database of the location must be downloaded from the OSM panel to fetch the " +"road name." +msgstr "" +"Affiche le nom de la route sur laquelle la voiture circule.
La base de " +"données OpenStreetMap de la localisation doit être téléchargée depuis le " +"panneau OSM pour récupérer le nom de la route." + +msgid "Distance" +msgstr "Distance" + +msgid "Downloaded Maps" +msgstr "Cartes téléchargées" + +msgid "Downloading Map" +msgstr "Téléchargement de la carte" + +msgid "Downloading Maps..." +msgstr "Téléchargement des cartes..." + +msgid "Driver Camera Preview" +msgstr "Aperçu caméra conducteur" + +msgid "Drives" +msgstr "Trajets" + +msgid "Driving Model" +msgstr "Modèle de conduite" + +msgid "Dynamic" +msgstr "Dynamique" + +msgid "Early Access: Become a sunnypilot Sponsor" +msgstr "Accès anticipé : Devenez parrain sunnypilot" + +msgid "Enable \"Always Offroad\" in Device panel, or turn vehicle off to toggle." +msgstr "" +"Activez \"Toujours hors route\" dans le panneau Appareil, ou éteignez le " +"véhicule pour basculer." + +msgid "Enable Always Offroad" +msgstr "Activer le mode toujours hors route" + +msgid "Enable Custom Tuning" +msgstr "Activer le réglage personnalisé" + +msgid "Enable Dynamic Experimental Control" +msgstr "Activer le contrôle expérimental dynamique" + +msgid "Enable Standstill Timer" +msgstr "Activer le chronomètre à l'arrêt" + +msgid "Enable Tesla Rainbow Mode" +msgstr "Activer le mode arc-en-ciel Tesla" + +msgid "" +"Enable custom Short & Long press increments for cruise speed " +"increase/decrease." +msgstr "" +"Activer les incréments personnalisés d'appui court et long pour " +"augmenter/diminuer la vitesse de croisière." + +msgid "Enable driver monitoring even when sunnypilot is not engaged." +msgstr "" +"Activer la surveillance du conducteur même lorsque sunnypilot n'est pas " +"engagé." + +msgid "Enable sunnylink" +msgstr "Activer sunnylink" + +msgid "Enable sunnylink uploader (infrastructure test)" +msgstr "Activer le téléversement sunnylink" + +msgid "" +"Enable sunnylink uploader to allow sunnypilot to upload your driving data to" +" sunnypilot servers. " +msgstr "" +"Activer le téléversement sunnylink pour permettre à sunnypilot de téléverser" +" vos données de conduite vers les serveurs sunnypilot. " + +msgid "Enable sunnypilot" +msgstr "Activer sunnypilot" + +msgid "" +"Enable the beloved MADS feature. Disable toggle to revert back to stock " +"sunnypilot engagement/disengagement." +msgstr "" +"Activer la fonctionnalité MADS. Désactivez l'option pour revenir au " +"comportement d'engagement/désengagement sunnypilot d'origine." + +msgid "" +"Enable the sunnypilot longitudinal control (alpha) toggle to allow " +"Experimental mode." +msgstr "" +"Activez l'option de contrôle longitudinal sunnypilot (alpha) pour autoriser " +"le mode expérimental." + +msgid "" +"Enable this for the car to learn and adapt its steering response time. " +"Disable to use a fixed steering response time. Keeping this on provides the " +"stock openpilot experience." +msgstr "" +"Activez pour que la voiture apprenne et adapte son temps de réponse de " +"direction. Désactivez pour utiliser un temps de réponse fixe. Garder activé " +"offre l'expérience openpilot d'origine." + +msgid "" +"Enable this to enforce sunnypilot to steer with Torque lateral control." +msgstr "" +"Activez ceci pour forcer sunnypilot à diriger avec le contrôle latéral par " +"couple." + +msgid "" +"Enable toggle to allow the model to determine when to use sunnypilot ACC or " +"sunnypilot End to End Longitudinal." +msgstr "" +"Activez l'option pour permettre au modèle de déterminer quand utiliser le " +"ACC sunnypilot ou le contrôle longitudinal de bout en bout sunnypilot." + +msgid "" +"Enables custom tuning for Torque lateral control. Modifying Lateral " +"Acceleration Factor and Friction below will override the offline values " +"indicated in the YAML files within \"opendbc/car/torque_data\". The values " +"will also be used live when \"Manual Real-Time Tuning\" toggle is enabled." +msgstr "" +"Active le réglage personnalisé pour le contrôle latéral par couple. La " +"modification du facteur d'accélération latérale et de la friction ci-dessous" +" remplacera les valeurs hors ligne indiquées dans les fichiers YAML du " +"dossier \"opendbc/car/torque_data\". Les valeurs seront également utilisées " +"en direct lorsque l'option \"Réglage manuel en temps réel\" est activée." + +msgid "Enables or disables the GitHub runner service." +msgstr "Active ou désactive le service GitHub runner." + +msgid "" +"Enables self-tune for Torque lateral control for platforms that do not use " +"Torque lateral control by default." +msgstr "" +"Active l'auto-réglage pour le contrôle latéral par couple pour les " +"plateformes qui n'utilisent pas le contrôle latéral par couple par défaut." + +msgid "" +"Enabling this will display warnings when a vehicle is detected in your blind" +" spot as long as your car has BSM supported." +msgstr "" +"L'activation affichera des avertissements lorsqu'un véhicule est détecté " +"dans votre angle mort, à condition que votre voiture supporte le BSM." + +msgid "Enforce Factory Longitudinal Control" +msgstr "Forcer le contrôle longitudinal d'usine" + +msgid "Enforce Torque Lateral Control" +msgstr "Forcer le contrôle latéral par couple" + +msgid "" +"Enforces the torque lateral controller to use the fixed values instead of " +"the learned values from Self-Tune. Enabling this toggle overrides Self-Tune " +"values." +msgstr "" +"Force le contrôleur latéral par couple à utiliser les valeurs fixes au lieu " +"des valeurs apprises par l'auto-réglage. L'activation de cette option " +"remplace les valeurs de l'auto-réglage." + +msgid "" +"Engage lateral and longitudinal control with cruise control engagement." +msgstr "" +"Engager le contrôle latéral et longitudinal avec l'activation du régulateur " +"de vitesse." + +msgid "Enter model year (e.g., 2021) and model (Toyota Corolla):" +msgstr "Entrez l'année (ex. 2021) et le modèle (Toyota Corolla) :" + +msgid "Enter search query" +msgstr "Entrez votre recherche" + +msgid "Error Log" +msgstr "Journal d'erreurs" + +msgid "Error: Invalid download. Retry." +msgstr "Erreur : Téléchargement invalide. Réessayez." + +msgid "Exit Always Offroad" +msgstr "Quitter le mode toujours hors route" + +msgid "" +"Experimental feature to enable auto-resume during stop-and-go for certain " +"supported Subaru platforms." +msgstr "" +"Fonctionnalité expérimentale pour activer la reprise automatique en stop-" +"and-go pour certaines plateformes Subaru supportées." + +msgid "" +"Experimental feature to enable stop and go for Subaru Global models with " +"manual handbrake. Models with electric parking brake should keep this " +"disabled. Thanks to martinl for this implementation!" +msgstr "" +"Fonctionnalité expérimentale pour activer le stop and go pour les modèles " +"Subaru Global avec frein à main manuel. Les modèles avec frein de " +"stationnement électrique doivent garder ceci désactivé. Merci à martinl pour" +" cette implémentation !" + +msgid "FAULT" +msgstr "DÉFAUT" + +msgid "FETCHING..." +msgstr "RÉCUPÉRATION..." + +msgid "Fetching Latest Models" +msgstr "Récupération des derniers modèles" + +msgid "Fingerprinted automatically" +msgstr "Empreinte automatique" + +msgid "Fixed" +msgstr "Fixe" + +msgid "Fixed: Adds a fixed offset [Speed Limit + Offset]" +msgstr "Fixe : Ajoute un décalage fixe [Limite de vitesse + Décalage]" + +msgid "Follow the prompts to complete the pairing process" +msgstr "Suivez les instructions pour terminer le processus d'association" + +msgid "" +"For applicable vehicles, always display the true vehicle current speed from " +"wheel speed sensors." +msgstr "" +"Pour les véhicules compatibles, toujours afficher la vitesse réelle actuelle" +" du véhicule à partir des capteurs de vitesse de roue." + +msgid "For secure backup, restore, and remote configuration" +msgstr "" +"Pour la sauvegarde sécurisée, la restauration et la configuration à distance" + +msgid "Friction" +msgstr "Friction" + +msgid "GitHub Runner Service" +msgstr "Service GitHub Runner" + +msgid "Green Traffic Light Alert (Beta)" +msgstr "Alerte feu vert (Bêta)" + +msgid "Hours" +msgstr "Heures" + +msgid "" +"If sponsorship status was not updated, please contact a moderator on the " +"community forum at https://community.sunnypilot.ai" +msgstr "" +"Si le statut de parrainage n'a pas été mis à jour, veuillez contacter un " +"modérateur sur le forum communautaire à https://community.sunnypilot.ai" + +msgid "" +"If you're driving at 20 mph (32 km/h) or below and have your blinker on, the" +" car will plan a turn in that direction at the nearest drivable path. This " +"prevents situations (like at red lights) where the car might plan the wrong " +"turn direction." +msgstr "" +"Si vous roulez à 32 km/h ou moins avec le clignotant activé, la voiture " +"planifiera un virage dans cette direction vers le chemin praticable le plus " +"proche. Cela évite les situations (comme aux feux rouges) où la voiture " +"pourrait planifier la mauvaise direction de virage." + +msgid "Info" +msgstr "Info" + +msgid "Information: Displays the current road's speed limit." +msgstr "Information : Affiche la limite de vitesse de la route actuelle." + +msgid "Intelligent Cruise Button Management (ICBM) (Alpha)" +msgstr "Gestion intelligente des boutons de régulateur (ICBM) (Alpha)" + +msgid "" +"Intelligent Cruise Button Management is currently unavailable on this " +"platform." +msgstr "" +"La gestion intelligente des boutons de régulateur est actuellement " +"indisponible sur cette plateforme." + +msgid "Interactivity Timeout" +msgstr "Délai d'inactivité" + +msgid "" +"Join our Community Forum at https://community.sunnypilot.ai and reach out to" +" a moderator if you have issues" +msgstr "" +"Rejoignez notre forum communautaire à https://community.sunnypilot.ai et " +"contactez un modérateur si vous avez des problèmes" + +msgid "KM" +msgstr "KM" + +msgid "Last checked {}" +msgstr "Dernière vérification {}" + +msgid "Lateral Acceleration Factor" +msgstr "Facteur d'accélération latérale" + +msgid "Lead Departure Alert (Beta)" +msgstr "Alerte de départ du véhicule en tête (Bêta)" + +msgid "Less Restrict Settings for Self-Tune (Beta)" +msgstr "Paramètres moins restrictifs pour l'auto-réglage (Bêta)" + +msgid "" +"Less strict settings when using Self-Tune. This allows torqued to be more " +"forgiving when learning values." +msgstr "" +"Paramètres moins stricts lors de l'utilisation de l'auto-réglage. Cela " +"permet à torqued d'être plus tolérant lors de l'apprentissage des valeurs." + +msgid "Live Learning Steer Delay" +msgstr "Apprentissage en direct du délai de direction" + +msgid "Live Steer Delay:" +msgstr "Délai direction en direct :" + +msgid "Long Press Increment" +msgstr "Incrément appui long" + +msgid "Manual Real-Time Tuning" +msgstr "Réglage manuel en temps réel" + +msgid "Manually selected fingerprint" +msgstr "Empreinte sélectionnée manuellement" + +msgid "Map First" +msgstr "Carte d'abord" + +msgid "" +"Map First: Use Speed Limit data from OpenStreetMaps if available, else use " +"from Car" +msgstr "" +"Carte d'abord : Utiliser les données de limite de vitesse d'OpenStreetMaps " +"si disponibles, sinon utiliser celles de la voiture" + +msgid "Map Only" +msgstr "Carte uniquement" + +msgid "Map Only: Use Speed Limit data only from OpenStreetMaps" +msgstr "" +"Carte uniquement : Utiliser les données de limite de vitesse uniquement " +"d'OpenStreetMaps" + +msgid "Mapd Version" +msgstr "Version Mapd" + +msgid "Max Time Offroad" +msgstr "Durée max hors route" + +msgid "Miles" +msgstr "Km" + +msgid "Minimum Speed to Pause Lateral Control" +msgstr "Vitesse minimum pour suspendre le contrôle latéral" + +msgid "" +"Model download has started in the background. We suggest resetting " +"calibration. Would you like to do that now?" +msgstr "" +"Le téléchargement du modèle a démarré en arrière-plan. Nous suggérons de " +"réinitialiser la calibration. Voulez-vous le faire maintenant ?" + +msgid "Models" +msgstr "Modèles" + +msgid "Modular Assistive Driving System (MADS)" +msgstr "Système d'aide à la conduite modulaire (MADS)" + +msgid "Near" +msgstr "Proche" + +msgid "Neural Network Lateral Control (NNLC)" +msgstr "Contrôle latéral par réseau neuronal (NNLC)" + +msgid "No" +msgstr "Non" + +msgid "No vehicle selected" +msgstr "Aucun véhicule sélectionné" + +msgid "None" +msgstr "Aucun" + +msgid "None: No Offset" +msgstr "Aucun : Pas de décalage" + +msgid "Not Paired" +msgstr "Non associé" + +msgid "Not Sponsor" +msgstr "Non parrain" + +msgid "Not fingerprinted or manually selected" +msgstr "Pas d'empreinte ou sélection manuelle" + +msgid "Not going to lie, it's sad to see you disabled sunnylink" +msgstr "Honnêtement, c'est triste de voir que vous avez désactivé sunnylink" + +msgid "" +"Note: For vehicles without LFA/LKAS button, disabling this will prevent " +"lateral control engagement." +msgstr "" +"Note : Pour les véhicules sans bouton LFA/LKAS, désactiver ceci empêchera " +"l'engagement du contrôle latéral." + +msgid "" +"Note: Once lateral control is engaged via UEM, it will remain engaged until " +"it is manually disabled via the MADS button or car shut off." +msgstr "" +"Note : Une fois le contrôle latéral engagé via UEM, il restera engagé " +"jusqu'à ce qu'il soit désactivé manuellement via le bouton MADS ou " +"l'extinction de la voiture." + +msgid "Nudge" +msgstr "Impulsion" + +msgid "Nudgeless" +msgstr "Sans impulsion" + +msgid "OSM" +msgstr "OSM" + +msgid "Off" +msgstr "Désactivé" + +msgid "Off: Disables the Speed Limit functions." +msgstr "Désactivé : Désactive les fonctions de limite de vitesse." + +msgid "Offline Only" +msgstr "Hors ligne uniquement" + +msgid "Offroad" +msgstr "Hors route" + +msgid "Offroad: Device will be in Always Offroad mode after boot/wake-up." +msgstr "" +"Hors route : L'appareil sera en mode toujours hors route après démarrage." + +msgid "Only available when vehicle is off, or always offroad mode is on" +msgstr "" +"Disponible uniquement lorsque le véhicule est éteint ou en mode toujours " +"hors route" + +msgid "Onroad Brightness" +msgstr "Luminosité en conduite" + +msgid "Onroad Brightness Delay" +msgstr "Délai de luminosité en conduite" + +msgid "Onroad Uploads" +msgstr "Téléversements en conduite" + +msgid "PAST WEEK" +msgstr "SEMAINE PASSÉE" + +msgid "Pair GitHub Account" +msgstr "Associer un compte GitHub" + +msgid "Pair your GitHub account" +msgstr "Associer votre compte GitHub" + +msgid "" +"Pair your GitHub account to grant your device sponsor benefits, including " +"API access on sunnylink." +msgstr "" +"Associez votre compte GitHub pour accorder à votre appareil les avantages de" +" parrain, y compris l'accès API sur sunnylink." + +msgid "Paired" +msgstr "Associé" + +msgid "Pause" +msgstr "Pause" + +msgid "Pause Lateral Control with Blinker" +msgstr "Suspendre le contrôle latéral avec le clignotant" + +msgid "" +"Pause lateral control with blinker when traveling below the desired speed " +"selected." +msgstr "" +"Suspendre le contrôle latéral avec le clignotant lorsque la vitesse est " +"inférieure à la vitesse souhaitée sélectionnée." + +msgid "Pause: ALC will pause when the brake pedal is pressed." +msgstr "" +"Pause : ALC se mettra en pause lorsque la pédale de frein est enfoncée." + +msgid "Percent: Adds a percent offset [Speed Limit + (Offset % Speed Limit)]" +msgstr "" +"Pourcentage : Ajoute un décalage en pourcentage [Limite de vitesse + " +"(Décalage % Limite de vitesse)]" + +msgid "" +"Please enable \"Always Offroad\" mode or turn off the vehicle to adjust " +"these toggles." +msgstr "" +"Veuillez activer le mode \"Toujours hors route\" ou éteindre le véhicule " +"pour ajuster ces options." + +msgid "Please reboot and try again." +msgstr "Veuillez redémarrer et réessayer." + +msgid "Policy Model" +msgstr "Modèle de politique" + +msgid "Post-Blinker Delay" +msgstr "Délai post-clignotant" + +msgid "Predictive" +msgstr "Prédictif" + +msgid "Quickboot Mode" +msgstr "Mode démarrage rapide" + +msgid "" +"Quickboot mode requires updates to be disabled.
Enable 'Disable Updates' " +"in the Software panel first." +msgstr "" +"Le mode démarrage rapide nécessite que les mises à jour soient " +"désactivées.
Activez d'abord 'Désactiver les mises à jour' dans le " +"panneau Logiciel." + +msgid "Quiet Mode" +msgstr "Mode silencieux" + +msgid "REFRESH" +msgstr "ACTUALISER" + +msgid "REGIST..." +msgstr "ENREG..." + +msgid "Re-enter the \"sunnylink\" panel to verify sponsorship status" +msgstr "" +"Retournez dans le panneau \"sunnylink\" pour vérifier le statut de " +"parrainage" + +msgid "Real-Time & Offline" +msgstr "Temps réel et hors ligne" + +msgid "Real-time Acceleration Bar" +msgstr "Barre d'accélération en temps réel" + +msgid "Refresh Model List" +msgstr "Actualiser la liste des modèles" + +msgid "Remain Active" +msgstr "Rester actif" + +msgid "Remain Active: ALC will remain active when the brake pedal is pressed." +msgstr "" +"Rester actif : ALC restera actif lorsque la pédale de frein est enfoncée." + +msgid "Reset Settings" +msgstr "Réinitialiser les paramètres" + +msgid "Restore Failed" +msgstr "Échec de la restauration" + +msgid "Restore Settings" +msgstr "Restaurer les paramètres" + +msgid "Review the rules, features, and limitations of sunnypilot" +msgstr "Consultez les règles, fonctionnalités et limitations de sunnypilot" + +msgid "Right" +msgstr "Droite" + +msgid "Right & Bottom" +msgstr "Droite et bas" + +msgid "SPONSOR" +msgstr "PARRAINER" + +msgid "SUNNYLINK" +msgstr "SUNNYLINK" + +msgid "Scan" +msgstr "Analyser" + +msgid "Scan the QR code to login to your GitHub account" +msgstr "Scannez le code QR pour vous connecter à votre compte GitHub" + +msgid "Scan the QR code to visit sunnyhaibin's GitHub Sponsors page" +msgstr "" +"Scannez le code QR pour visiter la page GitHub Sponsors de sunnyhaibin" + +msgid "Scanning..." +msgstr "Analyse en cours..." + +msgid "Search" +msgstr "Rechercher" + +msgid "Search your vehicle" +msgstr "Rechercher votre véhicule" + +msgid "Select a Model" +msgstr "Sélectionner un modèle" + +msgid "Select a vehicle" +msgstr "Sélectionner un véhicule" + +msgid "Select vehicle to force fingerprint manually." +msgstr "Sélectionnez le véhicule pour forcer l'empreinte manuellement." + +msgid "Self-Tune" +msgstr "Auto-réglage" + +msgid "" +"Set a timer to delay the auto lane change operation when the blinker is " +"used. No nudge on the steering wheel is required to auto lane change if a " +"timer is set. Default is Nudge.
Please use caution when using this " +"feature. Only use the blinker when traffic and road conditions permit." +msgstr "" +"Définir un minuteur pour retarder le changement de voie automatique lorsque " +"le clignotant est utilisé. Aucune impulsion sur le volant n'est nécessaire " +"pour le changement de voie automatique si un minuteur est défini. Par défaut" +" : Impulsion.
Veuillez utiliser cette fonctionnalité avec prudence. " +"N'utilisez le clignotant que lorsque la circulation et les conditions " +"routières le permettent." + +msgid "Set the maximum speed for lane turn desires. Default is 19 mph." +msgstr "" +"Définir la vitesse max. pour les changements de voie assistés. Par défaut : " +"30 km/h." + +msgid "Settings backup completed." +msgstr "Sauvegarde des paramètres terminée." + +msgid "Settings restored. Confirm to restart the interface." +msgstr "Paramètres restaurés. Confirmez pour redémarrer l'interface." + +msgid "Short Press Increment" +msgstr "Incrément appui court" + +msgid "Show Advanced Controls" +msgstr "Afficher les contrôles avancés" + +msgid "Show Blind Spot Warnings" +msgstr "Afficher les alertes d'angle mort" + +msgid "Show a timer on the HUD when the car is at a standstill." +msgstr "Afficher un chronomètre sur le HUD lorsque la voiture est à l'arrêt." + +msgid "" +"Show an indicator on the left side of the screen to display real-time " +"vehicle acceleration and deceleration. This displays what the car is " +"currently doing, not what the planner is requesting." +msgstr "" +"Afficher un indicateur à gauche de l'écran pour visualiser l'accélération et" +" la décélération du véhicule en temps réel. Cela affiche ce que la voiture " +"fait actuellement, pas ce que le planificateur demande." + +msgid "Smart Cruise Control - Map" +msgstr "Régulateur intelligent - Carte" + +msgid "Smart Cruise Control - Vision" +msgstr "Régulateur intelligent - Vision" + +msgid "Software Delay:" +msgstr "Délai logiciel :" + +msgid "Speed" +msgstr "Vitesse" + +msgid "Speed Limit" +msgstr "Limite de vitesse" + +msgid "Speed Limit Offset" +msgstr "Décalage de limite de vitesse" + +msgid "Speed Limit Source" +msgstr "Source de limite de vitesse" + +msgid "Speedometer: Always Display True Speed" +msgstr "Compteur : Toujours afficher la vitesse réelle" + +msgid "Speedometer: Hide from Onroad Screen" +msgstr "Compteur : Masquer de l'écran de conduite" + +msgid "Sponsor Status" +msgstr "Statut de parrainage" + +msgid "Sponsorship isn't required for basic backup/restore" +msgstr "" +"Le parrainage n'est pas requis pour la sauvegarde/restauration de base" + +msgid "" +"Standard is recommended. In aggressive mode, sunnypilot will follow lead " +"cars closer and be more aggressive with the gas and brake. In relaxed mode " +"sunnypilot will stay further away from lead cars. On supported cars, you can" +" cycle through these personalities with your steering wheel distance button." +msgstr "" +"Le mode standard est recommandé. En mode agressif, sunnypilot suivra les " +"véhicules en tête de plus près et sera plus agressif avec l'accélérateur et " +"le frein. En mode détendu, sunnypilot restera plus éloigné des véhicules en " +"tête. Sur les voitures compatibles, vous pouvez parcourir ces personnalités " +"avec le bouton de distance sur le volant." + +msgid "Start Download" +msgstr "Démarrer le téléchargement" + +msgid "Start the vehicle to check vehicle compatibility." +msgstr "Démarrez le véhicule pour vérifier la compatibilité du véhicule." + +msgid "State" +msgstr "Région" + +msgid "Steering" +msgstr "Direction" + +msgid "Steering Arc" +msgstr "Arc de direction" + +msgid "Steering Mode on Brake Pedal" +msgstr "Mode direction au freinage" + +msgid "Stop and Go (Beta)" +msgstr "Stop and Go (Bêta)" + +msgid "Stop and Go for Manual Parking Brake (Beta)" +msgstr "Stop and Go pour frein à main manuel (Bêta)" + +msgid "System reboot required for changes to take effect. Reboot now?" +msgstr "" +"Redémarrage système nécessaire pour appliquer les changements. Redémarrer " +"maintenant ?" + +msgid "THANKS ♥" +msgstr "MERCI ♥" + +msgid "The reset cannot be undone. You have been warned." +msgstr "La réinitialisation ne peut pas être annulée. Vous êtes prévenu." + +msgid "" +"This feature can only be used with sunnypilot longitudinal control enabled." +msgstr "" +"Cette fonctionnalité ne peut être utilisée qu'avec le contrôle longitudinal " +"sunnypilot activé." + +msgid "" +"This feature defaults to OFF, and does not allow selection due to vehicle " +"limitations." +msgstr "" +"Cette fonctionnalité est désactivée par défaut et ne permet pas la sélection" +" en raison des limitations du véhicule." + +msgid "" +"This feature defaults to ON, and does not allow selection due to vehicle " +"limitations." +msgstr "" +"Cette fonctionnalité est activée par défaut et ne permet pas la sélection en" +" raison des limitations du véhicule." + +msgid "This feature is currently not available on this platform." +msgstr "" +"Cette fonctionnalité n'est actuellement pas disponible sur cette plateforme." + +msgid "" +"This feature is not supported on this platform due to vehicle limitations." +msgstr "" +"Cette fonctionnalité n'est pas prise en charge sur cette plateforme en " +"raison des limitations du véhicule." + +msgid "" +"This feature is unavailable because sunnypilot Longitudinal Control (Alpha) " +"is not enabled." +msgstr "" +"Cette fonctionnalité est indisponible car le contrôle longitudinal " +"sunnypilot (Alpha) n'est pas activé." + +msgid "This feature is unavailable while the car is onroad." +msgstr "" +"Cette fonctionnalité est indisponible lorsque la voiture est sur la route." + +msgid "This feature requires sunnypilot longitudinal control to be available." +msgstr "" +"Cette fonctionnalité nécessite que le contrôle longitudinal sunnypilot soit " +"disponible." + +msgid "" +"This is the master switch, it will allow you to cutoff any sunnylink " +"requests should you want to do that." +msgstr "" +"C'est l'interrupteur principal, il vous permettra de couper toutes les " +"requêtes sunnylink si vous le souhaitez." + +msgid "" +"This may be due to weak internet connection or sunnylink registration issue." +" " +msgstr "" +"Cela peut être dû à une connexion internet faible ou un problème " +"d'enregistrement sunnylink. " + +msgid "This platform only supports Disengage mode due to vehicle limitations." +msgstr "" +"Cette plateforme ne supporte que le mode Désengager en raison des " +"limitations du véhicule." + +msgid "This platform supports all MADS settings." +msgstr "Cette plateforme supporte tous les paramètres MADS." + +msgid "This platform supports limited MADS settings." +msgstr "Cette plateforme supporte des paramètres MADS limités." + +msgid "This setting will take effect immediately." +msgstr "Ce paramètre prendra effet immédiatement." + +msgid "This setting will take effect once the device enters offroad state." +msgstr "Ce paramètre prendra effet une fois l'appareil en mode hors route." + +msgid "" +"This will delete ALL downloaded maps\n" +"\n" +"Are you sure you want to delete all maps?" +msgstr "" +"Cela supprimera TOUTES les cartes téléchargées\n" +"\n" +"Êtes-vous sûr de vouloir supprimer toutes les cartes ?" + +msgid "" +"This will delete ALL downloaded models from the cache except the currently " +"active model. Are you sure?" +msgstr "" +"Cela supprimera TOUS les modèles téléchargés du cache sauf le modèle " +"actuellement actif. Êtes-vous sûr ?" + +msgid "" +"This will start the download process and it might take a while to complete." +msgstr "Cela démarrera le téléchargement et peut prendre un certain temps." + +msgid "Time" +msgstr "Temps" + +msgid "" +"Toggle to enable a delay timer for seamless lane changes when blind spot " +"monitoring (BSM) detects a obstructing vehicle, ensuring safe maneuvering." +msgstr "" +"Activer un minuteur de délai pour des changements de voie fluides lorsque la" +" surveillance d'angle mort (BSM) détecte un véhicule gênant, assurant une " +"manœuvre en toute sécurité." + +msgid "" +"Toggle visibility of advanced sunnypilot controls.
This only changes the " +"visibility of the toggles; it does not change the actual enabled/disabled " +"state." +msgstr "" +"Basculer la visibilité des contrôles avancés sunnypilot.
Cela ne change " +"que la visibilité des options, pas leur état activé/désactivé réel." + +msgid "Toggle with Main Cruise" +msgstr "Basculer avec le régulateur principal" + +msgid "Total Delay:" +msgstr "Délai total :" + +msgid "Training Guide" +msgstr "Guide d'entraînement" + +msgid "Trips" +msgstr "Trajets" + +msgid "UI Debug Mode" +msgstr "Mode débogage UI" + +msgid "Unable to restore the settings, try again later." +msgstr "Impossible de restaurer les paramètres, réessayez plus tard." + +msgid "Unified Engagement Mode (UEM)" +msgstr "Mode d'engagement unifié (UEM)" + +msgid "Unrecognized Vehicle" +msgstr "Véhicule non reconnu" + +msgid "Use Lane Turn Desires" +msgstr "Changements de voie assistés" + +msgid "" +"Use map data to estimate the appropriate speed to drive through turns ahead." +msgstr "" +"Utiliser les données cartographiques pour estimer la vitesse appropriée pour" +" aborder les virages à venir." + +msgid "" +"Use the sunnypilot system for adaptive cruise control and lane keep driver " +"assistance. Your attention is required at all times to use this feature." +msgstr "" +"Utiliser le système sunnypilot pour le régulateur de vitesse adaptatif et " +"l'aide au maintien de voie. Votre attention est requise en permanence pour " +"utiliser cette fonctionnalité." + +msgid "" +"Use vision path predictions to estimate the appropriate speed to drive " +"through turns ahead." +msgstr "" +"Utiliser les prédictions de trajectoire visuelle pour estimer la vitesse " +"appropriée pour aborder les virages à venir." + +msgid "Vehicle" +msgstr "Véhicule" + +msgid "View the error log for sunnypilot crashes." +msgstr "Voir le journal d'erreurs pour les plantages sunnypilot." + +msgid "Vision Model" +msgstr "Modèle de vision" + +msgid "Visuals" +msgstr "Visuels" + +msgid "Wake Up Behavior" +msgstr "Comportement au réveil" + +msgid "Warning" +msgstr "Avertissement" + +msgid "" +"Warning: Provides a warning when exceeding the current road's speed limit." +msgstr "" +"Avertissement : Fournit un avertissement lorsque la limite de vitesse de la " +"route actuelle est dépassée." + +msgid "Welcome back!! We're excited to see you've enabled sunnylink again!" +msgstr "" +"Bon retour !! Nous sommes ravis de voir que vous avez réactivé sunnylink !" + +msgid "Welcome to sunnypilot" +msgstr "Bienvenue sur sunnypilot" + +msgid "" +"When enabled, automatic software updates will be off.
This requires a " +"reboot to take effect." +msgstr "" +"Lorsqu'activé, les mises à jour automatiques seront désactivées.
Cela " +"nécessite un redémarrage pour prendre effet." + +msgid "" +"When enabled, pressing the accelerator pedal will disengage sunnypilot." +msgstr "" +"Lorsqu'activé, appuyer sur la pédale d'accélérateur désengagera sunnypilot." + +msgid "" +"When enabled, sunnypilot will attempt to manage the built-in cruise control " +"buttons by emulating button presses for limited longitudinal control." +msgstr "" +"Lorsqu'activé, sunnypilot tentera de gérer les boutons de régulateur de " +"vitesse intégrés en émulant des appuis de bouton pour un contrôle " +"longitudinal limité." + +msgid "When enabled, the speedometer on the onroad screen is not displayed." +msgstr "" +"Lorsqu'activé, le compteur de vitesse sur l'écran de conduite n'est pas " +"affiché." + +msgid "When enabled, visual turn indicators are drawn on the HUD." +msgstr "" +"Lorsqu'activé, les indicateurs visuels de virage sont affichés sur le HUD." + +msgid "" +"When toggled on, this creates a prebuilt file to allow accelerated boot " +"times. When toggled off, it removes the prebuilt file so compilation of " +"locally edited cpp files can be made." +msgstr "" +"Lorsqu'activé, cela crée un fichier précompilé pour accélérer le temps de " +"démarrage. Lorsque désactivé, le fichier précompilé est supprimé pour " +"permettre la compilation des fichiers cpp modifiés localement." + +msgid "Would you like to delete this log?" +msgstr "Voulez-vous supprimer ce journal ?" + +msgid "Yes" +msgstr "Oui" + +msgid "Yes, delete all maps" +msgstr "Oui, supprimer toutes les cartes" + +msgid "You must accept the Terms of Service in order to use sunnypilot." +msgstr "" +"Vous devez accepter les conditions d'utilisation pour utiliser sunnypilot." + +msgid "" +"You must accept the Terms of Service to use sunnypilot. Read the latest " +"terms at https://sunnypilot.ai/terms before continuing." +msgstr "" +"Vous devez accepter les conditions d'utilisation pour utiliser sunnypilot. " +"Consultez les dernières conditions sur https://sunnypilot.ai/terms avant de " +"continuer." + +msgid "Your vehicle will use the Default longitudinal tuning." +msgstr "Votre véhicule utilisera le réglage longitudinal par défaut." + +msgid "Your vehicle will use the Dynamic longitudinal tuning." +msgstr "Votre véhicule utilisera le réglage longitudinal dynamique." + +msgid "Your vehicle will use the Predictive longitudinal tuning." +msgstr "Votre véhicule utilisera le réglage longitudinal prédictif." + +msgid "backing up" +msgstr "sauvegarde en cours" + +msgid "backup" +msgstr "sauvegarde" + +msgid "backup settings" +msgstr "sauvegarder les paramètres" + +msgid "become a sunnypilot sponsor" +msgstr "devenir sponsor sunnypilot" + +msgid "cancel download" +msgstr "annuler le téléchargement" + +msgid "checking..." +msgstr "vérification..." + +msgid "copyparty Service" +msgstr "Service copyparty" + +msgid "" +"copyparty is a very capable file server, you can use it to download your " +"routes, view your logs and even make some edits on some files from your " +"browser. Requires you to connect to your comma locally via its IP address." +msgstr "" +"copyparty est un serveur de fichiers très performant, vous pouvez l'utiliser" +" pour télécharger vos trajets, consulter vos logs et même modifier certains " +"fichiers depuis votre navigateur. Nécessite de vous connecter à votre comma " +"localement via son adresse IP." + +msgid "current model" +msgstr "modèle actuel" + +msgid "default model" +msgstr "modèle par défaut" + +msgid "downloading..." +msgstr "téléchargement..." + +msgid "enable sunnylink" +msgstr "activer sunnylink" + +msgid "failed" +msgstr "échoué" + +msgid "finalizing update..." +msgstr "finalisation de la mise à jour..." + +msgid "from cache" +msgstr "depuis le cache" + +msgid "h" +msgstr "h" + +msgid "m" +msgstr "min" + +msgid "pair" +msgstr "associer" + +msgid "pair with sunnylink" +msgstr "associer avec sunnylink" + +msgid "paired" +msgstr "associé" + +msgid "ready" +msgstr "prêt" + +msgid "recommend disabling this feature if you experience these." +msgstr "" +"nous recommandons de désactiver cette fonctionnalité si vous rencontrez ces " +"problèmes." + +msgid "restore" +msgstr "restaurer" + +msgid "restore settings" +msgstr "restaurer les paramètres" + +msgid "restoring" +msgstr "restauration en cours" + +msgid "s" +msgstr "s" + +msgid "settings backed up" +msgstr "paramètres sauvegardés" + +msgid "slide to backup" +msgstr "glisser pour sauvegarder" + +msgid "slide to restore" +msgstr "glisser pour restaurer" + +msgid "sponsor" +msgstr "sponsor" + +msgid "sunnylink" +msgstr "sunnylink" + +msgid "sunnylink Dongle ID not found. " +msgstr "ID Dongle sunnylink introuvable. " + +msgid "sunnylink Dongle ID not found. Please reboot & try again." +msgstr "ID Dongle sunnylink introuvable. Veuillez redémarrer et réessayer." + +msgid "" +"sunnylink enables secured remote access to your comma device from anywhere, " +"including settings management, remote monitoring, real-time dashboard, etc." +msgstr "" +"sunnylink permet un accès à distance sécurisé à votre appareil comma depuis " +"n'importe où, y compris la gestion des paramètres, la surveillance à " +"distance, le tableau de bord en temps réel, etc." + +msgid "" +"sunnylink is designed to be enabled as part of sunnypilot's core " +"functionality. If sunnylink is disabled, features such as settings " +"management, remote monitoring, real-time dashboards will be unavailable." +msgstr "" +"sunnylink est conçu pour être activé dans le cadre des fonctionnalités de " +"base de sunnypilot. Si sunnylink est désactivé, des fonctionnalités telles " +"que la gestion des paramètres, la surveillance à distance et les tableaux de" +" bord en temps réel seront indisponibles." + +msgid "sunnylink uploader" +msgstr "téléverseur sunnylink" + +msgid "sunnypilot Longitudinal Control (Alpha)" +msgstr "Contrôle longitudinal sunnypilot (Alpha)" + +msgid "" +"sunnypilot Longitudinal Control is the default longitudinal control for this" +" platform." +msgstr "" +"Le contrôle longitudinal sunnypilot est le contrôle longitudinal par défaut " +"pour cette plateforme." + +msgid "sunnypilot Unavailable" +msgstr "sunnypilot indisponible" + +msgid "" +"sunnypilot defaults to driving in chill mode. Experimental mode enables " +"alpha-level features that aren't ready for chill mode. Experimental features" +" are listed below:

End-to-End Longitudinal Control


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

New Driving Visualization


The driving visualization" +" will transition to the road-facing wide-angle camera at low speeds to " +"better show some turns. The Experimental mode logo will also be shown in the" +" top right corner." +msgstr "" +"sunnypilot conduit par défaut en mode chill. Le mode expérimental active des" +" fonctionnalités de niveau alpha qui ne sont pas prêtes pour le mode chill. " +"Les fonctionnalités expérimentales sont listées ci-dessous :

Contrôle" +" longitudinal de bout en bout


Laissez le modèle de conduite " +"contrôler l'accélérateur et les freins. sunnypilot conduira comme il pense " +"qu'un humain le ferait, y compris s'arrêter aux feux rouges et aux panneaux " +"stop. Puisque le modèle de conduite décide de la vitesse, la vitesse définie" +" ne servira que de limite supérieure. Il s'agit d'une fonctionnalité de " +"qualité alpha ; des erreurs sont à prévoir.

Nouvelle visualisation de" +" conduite


La visualisation de conduite passera à la caméra grand " +"angle orientée route à basse vitesse pour mieux montrer certains virages. Le" +" logo du mode expérimental sera également affiché dans le coin supérieur " +"droit." + +msgid "" +"sunnypilot is continuously calibrating, resetting is rarely required. " +"Resetting calibration will restart sunnypilot if the car is powered on." +msgstr "" +"sunnypilot se calibre en permanence, la réinitialisation est rarement " +"nécessaire. La réinitialisation de la calibration redémarrera sunnypilot si " +"la voiture est sous tension." + +msgid "" +"sunnypilot learns to drive by watching humans, like you, drive.\n" +"\n" +"Firehose Mode allows you to maximize your training data uploads to improve openpilot's driving models. More data means bigger models, which means better Experimental Mode." +msgstr "" +"sunnypilot apprend à conduire en regardant des humains, comme vous, conduire.\n" +"\n" +"Le mode Firehose vous permet de maximiser vos envois de données d'entraînement pour améliorer les modèles de conduite d'openpilot. Plus de données signifie de plus grands modèles, ce qui signifie un meilleur mode expérimental." + +msgid "sunnypilot longitudinal control may come in a future update." +msgstr "" +"Le contrôle longitudinal sunnypilot pourrait arriver dans une future mise à " +"jour." + +msgid "" +"sunnypilot will not take over control of gas and brakes. Factory Toyota " +"longitudinal control will be used." +msgstr "" +"sunnypilot ne prendra pas le contrôle de l'accélérateur et des freins. Le " +"contrôle longitudinal Toyota d'origine sera utilisé." From 5ef0040ac6d08c81c8f89281ac4d1e0189956364 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Sat, 28 Feb 2026 23:51:56 -0800 Subject: [PATCH 302/311] ui: delay click callback (#37502) * delay click callback * actually may be better * clean up * clean up --- selfdrive/ui/mici/layouts/settings/device.py | 6 ++++++ selfdrive/ui/mici/layouts/settings/network/wifi_ui.py | 2 ++ selfdrive/ui/mici/widgets/button.py | 2 ++ system/ui/widgets/__init__.py | 10 +++++++++- 4 files changed, 19 insertions(+), 1 deletion(-) diff --git a/selfdrive/ui/mici/layouts/settings/device.py b/selfdrive/ui/mici/layouts/settings/device.py index a810a5d1a0..b7ee5b6f45 100644 --- a/selfdrive/ui/mici/layouts/settings/device.py +++ b/selfdrive/ui/mici/layouts/settings/device.py @@ -117,6 +117,8 @@ class PairBigButton(BigButton): return 64 def _update_state(self): + super()._update_state() + if ui_state.prime_state.is_paired(): self.set_text("paired") if ui_state.prime_state.is_prime(): @@ -164,6 +166,8 @@ class UpdateOpenpilotBigButton(BigButton): self.set_enabled(True) def _handle_mouse_release(self, mouse_pos: MousePos): + super()._handle_mouse_release(mouse_pos) + if not system_time_valid(): dlg = BigDialog(tr("Please connect to Wi-Fi to update"), "") gui_app.push_widget(dlg) @@ -191,6 +195,8 @@ class UpdateOpenpilotBigButton(BigButton): self.set_text("update openpilot") def _update_state(self): + super()._update_state() + if ui_state.started: self.set_enabled(False) return diff --git a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py index cc45daf24b..22d3d1d0da 100644 --- a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py +++ b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py @@ -215,6 +215,8 @@ class WifiButton(BigButton): return self._wifi_manager.connected_ssid == self._network.ssid def _update_state(self): + super()._update_state() + if any((self._network_missing, self._is_connecting, self._is_connected, self._network_forgetting, self._network.security_type == SecurityType.UNSUPPORTED)): self.set_enabled(False) diff --git a/selfdrive/ui/mici/widgets/button.py b/selfdrive/ui/mici/widgets/button.py index 231dafa8eb..b5bd65e2de 100644 --- a/selfdrive/ui/mici/widgets/button.py +++ b/selfdrive/ui/mici/widgets/button.py @@ -36,6 +36,7 @@ class BigCircleButton(Widget): # State self.set_rect(rl.Rectangle(0, 0, 180, 180)) self._scale_filter = BounceFilter(1.0, 0.1, 1 / gui_app.target_fps) + self._click_delay = 0.075 # Icons self._txt_icon = gui_app.texture(icon, *icon_size) @@ -117,6 +118,7 @@ class BigButton(Widget): self.set_icon(icon) self._scale_filter = BounceFilter(1.0, 0.1, 1 / gui_app.target_fps) + self._click_delay = 0.075 self._shake_start: float | None = None self._rotate_icon_t: float | None = None diff --git a/system/ui/widgets/__init__.py b/system/ui/widgets/__init__.py index 1e2f783811..568f58b985 100644 --- a/system/ui/widgets/__init__.py +++ b/system/ui/widgets/__init__.py @@ -30,6 +30,8 @@ class Widget(abc.ABC): self._enabled: bool | Callable[[], bool] = True self._is_visible: bool | Callable[[], bool] = True self._touch_valid_callback: Callable[[], bool] | None = None + self._click_delay: float | None = None # seconds to hold is_pressed after release + self._click_release_time: float | None = None self._click_callback: Callable[[], None] | None = None self._multi_touch = False self.__was_awake = True @@ -51,7 +53,8 @@ class Widget(abc.ABC): @property def is_pressed(self) -> bool: - return any(self.__is_pressed) + # if actually pressed or holding after release + return any(self.__is_pressed) or self._click_release_time is not None @property def enabled(self) -> bool: @@ -98,6 +101,9 @@ class Widget(abc.ABC): self._update_state() + if self._click_release_time is not None and rl.get_time() >= self._click_release_time: + self._click_release_time = None + if not self.is_visible: return None @@ -182,6 +188,8 @@ class Widget(abc.ABC): def _handle_mouse_release(self, mouse_pos: MousePos) -> None: """Optionally handle mouse release events.""" + if self._click_delay is not None: + self._click_release_time = rl.get_time() + self._click_delay if self._click_callback: self._click_callback() From 61658fbfe3bf308eaf8ff0d2593d704d8c946e85 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Sun, 1 Mar 2026 00:56:52 -0800 Subject: [PATCH 303/311] mici setup: new start button (#37501) * pressable * slow * fast and looks great * 0.075 * clean up * fix missing * clean up --- .../assets/icons_mici/setup/green_button.png | 3 --- .../icons_mici/setup/green_button_pressed.png | 3 --- .../assets/icons_mici/setup/start_button.png | 3 +++ .../icons_mici/setup/start_button_pressed.png | 3 +++ system/ui/mici_setup.py | 18 ++++++++++++------ 5 files changed, 18 insertions(+), 12 deletions(-) delete mode 100644 selfdrive/assets/icons_mici/setup/green_button.png delete mode 100644 selfdrive/assets/icons_mici/setup/green_button_pressed.png create mode 100644 selfdrive/assets/icons_mici/setup/start_button.png create mode 100644 selfdrive/assets/icons_mici/setup/start_button_pressed.png diff --git a/selfdrive/assets/icons_mici/setup/green_button.png b/selfdrive/assets/icons_mici/setup/green_button.png deleted file mode 100644 index 9708cfe284..0000000000 --- a/selfdrive/assets/icons_mici/setup/green_button.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:163ac31cb990bdddfe552efef9a68870404caadb1c40fa8a5042b5ae956e6b4c -size 24687 diff --git a/selfdrive/assets/icons_mici/setup/green_button_pressed.png b/selfdrive/assets/icons_mici/setup/green_button_pressed.png deleted file mode 100644 index 030ce61d5b..0000000000 --- a/selfdrive/assets/icons_mici/setup/green_button_pressed.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6e4614adb2d3d0e44c64a855c221ec462a7aee22fff26132ad551035141c1a53 -size 62056 diff --git a/selfdrive/assets/icons_mici/setup/start_button.png b/selfdrive/assets/icons_mici/setup/start_button.png new file mode 100644 index 0000000000..58d8a8b748 --- /dev/null +++ b/selfdrive/assets/icons_mici/setup/start_button.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5e993247160edcbc9c3cba3efa93169028568d484bcfd0bf64f3e3a7ec7556c0 +size 18608 diff --git a/selfdrive/assets/icons_mici/setup/start_button_pressed.png b/selfdrive/assets/icons_mici/setup/start_button_pressed.png new file mode 100644 index 0000000000..564de0bef7 --- /dev/null +++ b/selfdrive/assets/icons_mici/setup/start_button_pressed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1c4f1002ecde9a2b33779c2e784a39b492b4c8d76abc063e935ce0aa971925dd +size 65513 diff --git a/system/ui/mici_setup.py b/system/ui/mici_setup.py index 16a781aa91..33e0b8cf68 100755 --- a/system/ui/mici_setup.py +++ b/system/ui/mici_setup.py @@ -14,6 +14,7 @@ from collections.abc import Callable import pyray as rl from cereal import log +from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.realtime import config_realtime_process, set_core_affinity from openpilot.common.swaglog import cloudlog from openpilot.common.utils import run_cmd @@ -109,16 +110,21 @@ class StartPage(Widget): font_weight=FontWeight.DISPLAY, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER, alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE) - self._start_bg_txt = gui_app.texture("icons_mici/setup/green_button.png", 520, 224) - self._start_bg_pressed_txt = gui_app.texture("icons_mici/setup/green_button_pressed.png", 520, 224) + self._start_bg_txt = gui_app.texture("icons_mici/setup/start_button.png", 500, 224, keep_aspect_ratio=False) + self._start_bg_pressed_txt = gui_app.texture("icons_mici/setup/start_button_pressed.png", 500, 224, keep_aspect_ratio=False) + self._scale_filter = FirstOrderFilter(1.0, 0.1, 1 / gui_app.target_fps) + self._click_delay = 0.075 def _render(self, rect: rl.Rectangle): - draw_x = rect.x + (rect.width - self._start_bg_txt.width) / 2 - draw_y = rect.y + (rect.height - self._start_bg_txt.height) / 2 + scale = self._scale_filter.update(1.07 if self.is_pressed else 1.0) + base_draw_x = rect.x + (rect.width - self._start_bg_txt.width) / 2 + base_draw_y = rect.y + (rect.height - self._start_bg_txt.height) / 2 + draw_x = base_draw_x + (self._start_bg_txt.width * (1 - scale)) / 2 + draw_y = base_draw_y + (self._start_bg_txt.height * (1 - scale)) / 2 texture = self._start_bg_pressed_txt if self.is_pressed else self._start_bg_txt - rl.draw_texture(texture, int(draw_x), int(draw_y), rl.WHITE) + rl.draw_texture_ex(texture, (draw_x, draw_y), 0, scale, rl.WHITE) - self._title.render(rect) + self._title.render(rl.Rectangle(rect.x, rect.y + (draw_y - base_draw_y), rect.width, rect.height)) class SoftwareSelectionPage(Widget): From a7de971334b79fbc0de0386f8be8b70b99e69158 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Sun, 1 Mar 2026 02:41:51 -0800 Subject: [PATCH 304/311] mici setup: use nav stack (#37507) * pressable * slow * fast and looks great * 0.075 * clean up * fix missing * clean up * mici setup use nav stack! * remove flat state! * todo * clean up * clean up ordering * clean up * reset progress on show, dont mutate nav stack from thread * reset text on show too * rename * clean up --- system/ui/mici_setup.py | 243 +++++++++++++++----------------- system/ui/widgets/nav_widget.py | 10 +- 2 files changed, 123 insertions(+), 130 deletions(-) diff --git a/system/ui/mici_setup.py b/system/ui/mici_setup.py index 33e0b8cf68..9656702c18 100755 --- a/system/ui/mici_setup.py +++ b/system/ui/mici_setup.py @@ -7,7 +7,6 @@ import time import urllib.request import urllib.error from urllib.parse import urlparse -from enum import IntEnum import shutil from collections.abc import Callable @@ -23,6 +22,7 @@ from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.wifi_manager import WifiManager from openpilot.system.ui.lib.scroll_panel2 import GuiScrollPanel2 from openpilot.system.ui.widgets import Widget +from openpilot.system.ui.widgets.nav_widget import NavWidget from openpilot.system.ui.widgets.button import (IconButton, SmallButton, WideRoundedButton, SmallerRoundedButton, SmallCircleIconButton, WidishRoundedButton, FullRoundedButton) from openpilot.system.ui.widgets.label import UnifiedLabel @@ -92,16 +92,6 @@ class NetworkConnectivityMonitor: break -class SetupState(IntEnum): - GETTING_STARTED = 0 - NETWORK_SETUP = 1 - NETWORK_SETUP_CUSTOM_SOFTWARE = 2 - SOFTWARE_SELECTION = 3 - DOWNLOADING = 4 - DOWNLOAD_FAILED = 5 - CUSTOM_SOFTWARE_WARNING = 6 - - class StartPage(Widget): def __init__(self): super().__init__() @@ -127,15 +117,24 @@ class StartPage(Widget): self._title.render(rl.Rectangle(rect.x, rect.y + (draw_y - base_draw_y), rect.width, rect.height)) -class SoftwareSelectionPage(Widget): +class SoftwareSelectionPage(NavWidget): def __init__(self, use_openpilot_callback: Callable, use_custom_software_callback: Callable): super().__init__() self._openpilot_slider = LargerSlider("slide to use\nopenpilot", use_openpilot_callback) - self._openpilot_slider.set_enabled(lambda: self.enabled) + self._openpilot_slider.set_enabled(lambda: self.enabled and not self.is_dismissing) self._custom_software_slider = LargerSlider("slide to use\ncustom software", use_custom_software_callback, green=False) - self._custom_software_slider.set_enabled(lambda: self.enabled) + self._custom_software_slider.set_enabled(lambda: self.enabled and not self.is_dismissing) + + def show_event(self): + super().show_event() + self._nav_bar._alpha = 0.0 + + def _update_state(self): + super()._update_state() + if self.is_dismissing: + self.reset() def reset(self): self._openpilot_slider.reset() @@ -367,11 +366,16 @@ class DownloadingPage(Widget): font_weight=FontWeight.ROMAN, alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM) self._progress = 0 + def show_event(self): + super().show_event() + self.set_progress(0) + def set_progress(self, progress: int): self._progress = progress self._progress_label.set_text(f"{progress}%") def _render(self, rect: rl.Rectangle): + rl.draw_rectangle_rec(rect, rl.BLACK) self._title_label.render(rl.Rectangle( rect.x + 12, rect.y + 2, @@ -387,9 +391,10 @@ class DownloadingPage(Widget): )) -class FailedPage(Widget): +class FailedPage(NavWidget): def __init__(self, reboot_callback: Callable, retry_callback: Callable, title: str = "download failed"): super().__init__() + self.set_back_callback(retry_callback) self._title_label = UnifiedLabel(title, 64, text_color=rl.Color(255, 255, 255, int(255 * 0.9)), font_weight=FontWeight.DISPLAY) @@ -406,6 +411,10 @@ class FailedPage(Widget): def set_reason(self, reason: str): self._reason_label.set_text(reason) + def show_event(self): + super().show_event() + self._reboot_slider.reset() + def _render(self, rect: rl.Rectangle): self._title_label.render(rl.Rectangle( rect.x + 8, @@ -437,10 +446,18 @@ class FailedPage(Widget): )) -class NetworkSetupPage(Widget): - def __init__(self, wifi_manager, continue_callback: Callable, back_callback: Callable): +class NetworkSetupPage(NavWidget): + def __init__(self, network_monitor: NetworkConnectivityMonitor, continue_callback: Callable[[bool], None], + back_callback: Callable[[], None] | None): super().__init__() - self._wifi_ui = WifiUIMici(wifi_manager) + self.set_back_callback(back_callback) + + self._wifi_manager = WifiManager() + self._wifi_manager.set_active(True) + self._network_monitor = network_monitor + self._custom_software = False + self._prev_has_internet = False + self._wifi_ui = WifiUIMici(self._wifi_manager) self._no_wifi_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_slash.png", 58, 50) self._wifi_full_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_full.png", 58, 50) @@ -458,9 +475,27 @@ class NetworkSetupPage(Widget): self._continue_button = WidishRoundedButton("continue") self._continue_button.set_enabled(False) - self._continue_button.set_click_callback(continue_callback) + self._continue_button.set_click_callback(lambda: continue_callback(self._custom_software)) - def set_has_internet(self, has_internet: bool): + gui_app.add_nav_stack_tick(self._nav_stack_tick) + + def show_event(self): + super().show_event() + self._prev_has_internet = False + self._network_monitor.reset() + self._set_has_internet(False) + + def _nav_stack_tick(self): + self._wifi_manager.process_callbacks() + + has_internet = self._network_monitor.network_connected.is_set() + if has_internet != self._prev_has_internet: + self._set_has_internet(has_internet) + if has_internet: + gui_app.pop_widgets_to(self) + self._prev_has_internet = has_internet + + def _set_has_internet(self, has_internet: bool): if has_internet: self._network_header.set_title("connected to internet") self._network_header.set_icon(self._wifi_full_txt) @@ -470,6 +505,9 @@ class NetworkSetupPage(Widget): self._network_header.set_icon(self._no_wifi_txt) self._continue_button.set_enabled(False) + def set_custom_software(self, custom_software: bool): + self._custom_software = custom_software + def _render(self, _): self._network_header.render(rl.Rectangle( self._rect.x + 16, @@ -503,122 +541,55 @@ class NetworkSetupPage(Widget): class Setup(Widget): def __init__(self): super().__init__() - self.state = SetupState.GETTING_STARTED - self.failed_url = "" - self.failed_reason = "" self.download_url = "" self.download_progress = 0 self.download_thread = None - self._wifi_manager = WifiManager() - self._wifi_manager.set_active(True) + self._download_failed_reason: str | None = None + self._network_monitor = NetworkConnectivityMonitor() self._network_monitor.start() - self._prev_has_internet = False - gui_app.add_nav_stack_tick(self._nav_stack_tick) + + def getting_started_button_callback(): + self._software_selection_page.reset() + gui_app.push_widget(self._software_selection_page) self._start_page = StartPage() - self._start_page.set_click_callback(self._getting_started_button_callback) + self._start_page.set_click_callback(getting_started_button_callback) + self._start_page.set_enabled(lambda: self.enabled) # for nav stack - self._network_setup_page = NetworkSetupPage(self._wifi_manager, self._network_setup_continue_button_callback, - self._network_setup_back_button_callback) - # TODO: change these to touch_valid - self._network_setup_page.set_enabled(lambda: self.enabled) # for nav stack + self._network_setup_page = NetworkSetupPage(self._network_monitor, self._network_setup_continue_button_callback, + self._pop_to_software_selection) + self._software_selection_page = SoftwareSelectionPage(self._use_openpilot, lambda: gui_app.push_widget(self._custom_software_warning_page)) - self._software_selection_page = SoftwareSelectionPage(self._software_selection_continue_button_callback, - self._software_selection_custom_software_button_callback) - self._software_selection_page.set_enabled(lambda: self.enabled) # for nav stack + self._download_failed_page = FailedPage(HARDWARE.reboot, self._pop_to_software_selection) - self._download_failed_page = FailedPage(HARDWARE.reboot, self._download_failed_startover_button_callback) - self._download_failed_page.set_enabled(lambda: self.enabled) # for nav stack - - self._custom_software_warning_page = CustomSoftwareWarningPage(self._software_selection_custom_software_continue, - self._custom_software_warning_back_button_callback) - self._custom_software_warning_page.set_enabled(lambda: self.enabled) # for nav stack + self._custom_software_warning_page = CustomSoftwareWarningPage(self._software_selection_custom_software_continue, self._pop_to_software_selection) self._downloading_page = DownloadingPage() + gui_app.add_nav_stack_tick(self._nav_stack_tick) + def _nav_stack_tick(self): - has_internet = self._network_monitor.network_connected.is_set() - if has_internet and not self._prev_has_internet: - gui_app.pop_widgets_to(self) - self._prev_has_internet = has_internet + self._downloading_page.set_progress(self.download_progress) - def _update_state(self): - self._wifi_manager.process_callbacks() - - def _set_state(self, state: SetupState): - self.state = state - if self.state == SetupState.SOFTWARE_SELECTION: - self._software_selection_page.reset() - elif self.state == SetupState.CUSTOM_SOFTWARE_WARNING: - self._custom_software_warning_page.reset() - - if self.state in (SetupState.NETWORK_SETUP, SetupState.NETWORK_SETUP_CUSTOM_SOFTWARE): - self._network_setup_page.show_event() - self._network_monitor.reset() - else: - self._network_setup_page.hide_event() + if self._download_failed_reason is not None: + reason = self._download_failed_reason + self._download_failed_reason = None + self._download_failed_page.set_reason(reason) + gui_app.pop_widgets_to(self._software_selection_page, instant=True) # don't reset sliders + gui_app.push_widget(self._download_failed_page) def _render(self, rect: rl.Rectangle): - if self.state == SetupState.GETTING_STARTED: - self._start_page.render(rect) - elif self.state in (SetupState.NETWORK_SETUP, SetupState.NETWORK_SETUP_CUSTOM_SOFTWARE): - self.render_network_setup(rect) - elif self.state == SetupState.SOFTWARE_SELECTION: - self._software_selection_page.render(rect) - elif self.state == SetupState.CUSTOM_SOFTWARE_WARNING: - self._custom_software_warning_page.render(rect) - elif self.state == SetupState.DOWNLOADING: - self.render_downloading(rect) - elif self.state == SetupState.DOWNLOAD_FAILED: - self._download_failed_page.render(rect) - - def _custom_software_warning_back_button_callback(self): - self._set_state(SetupState.SOFTWARE_SELECTION) - - def _getting_started_button_callback(self): - self._set_state(SetupState.SOFTWARE_SELECTION) - - def _software_selection_continue_button_callback(self): - self.use_openpilot() - - def _software_selection_custom_software_button_callback(self): - self._set_state(SetupState.CUSTOM_SOFTWARE_WARNING) - - def _software_selection_custom_software_continue(self): - self._set_state(SetupState.NETWORK_SETUP_CUSTOM_SOFTWARE) - - def _download_failed_startover_button_callback(self): - self._set_state(SetupState.GETTING_STARTED) - - def _network_setup_back_button_callback(self): - self._set_state(SetupState.SOFTWARE_SELECTION) - - def _network_setup_continue_button_callback(self): - if self.state == SetupState.NETWORK_SETUP: - self.download(OPENPILOT_URL) - elif self.state == SetupState.NETWORK_SETUP_CUSTOM_SOFTWARE: - def handle_keyboard_result(text): - url = text.strip() - if url: - self.download(url) - - keyboard = BigInputDialog("custom software URL", confirm_callback=handle_keyboard_result) - gui_app.push_widget(keyboard) + self._start_page.render(rect) def close(self): self._network_monitor.stop() - def render_network_setup(self, rect: rl.Rectangle): - has_internet = self._network_monitor.network_connected.is_set() - self._network_setup_page.set_has_internet(has_internet) - self._network_setup_page.render(rect) + def _pop_to_software_selection(self): + # reset sliders after dismiss completes + gui_app.pop_widgets_to(self._software_selection_page, self._software_selection_page.reset) - def render_downloading(self, rect: rl.Rectangle): - self._downloading_page.set_progress(self.download_progress) - self._downloading_page.render(rect) - - def use_openpilot(self): + def _use_openpilot(self): if os.path.isdir(INSTALL_PATH) and os.path.isfile(VALID_CACHE_PATH): os.remove(VALID_CACHE_PATH) with open(TMP_CONTINUE_PATH, "w") as f: @@ -631,17 +602,40 @@ class Setup(Widget): time.sleep(0.1) gui_app.request_close() else: - self._set_state(SetupState.NETWORK_SETUP) + self._push_network_setup(custom_software=False) - def download(self, url: str): + def _push_network_setup(self, custom_software: bool): + self._network_setup_page.set_custom_software(custom_software) + gui_app.push_widget(self._network_setup_page) + + def _software_selection_custom_software_continue(self): + gui_app.pop_widgets_to(self._software_selection_page, instant=True) # don't reset sliders + self._push_network_setup(custom_software=True) + + def _network_setup_continue_button_callback(self, custom_software): + if not custom_software: + gui_app.pop_widgets_to(self._software_selection_page, instant=True) # don't reset sliders + self._download(OPENPILOT_URL) + else: + def handle_keyboard_result(text): + url = text.strip() + if url: + gui_app.pop_widgets_to(self._software_selection_page, instant=True) # don't reset sliders + self._download(url) + + keyboard = BigInputDialog("custom software URL", "openpilot.comma.ai", confirm_callback=handle_keyboard_result) + gui_app.push_widget(keyboard) + + def _download(self, url: str): # autocomplete incomplete URLs if re.match("^([^/.]+)/([^/]+)$", url): url = f"https://installer.comma.ai/{url}" parsed = urlparse(url, scheme='https') self.download_url = (urlparse(f"https://{url}") if not parsed.netloc else parsed).geturl() + self.download_progress = 0 - self._set_state(SetupState.DOWNLOADING) + gui_app.push_widget(self._downloading_page) self.download_thread = threading.Thread(target=self._download_thread, daemon=True) self.download_thread.start() @@ -672,7 +666,6 @@ class Setup(Widget): if total_size: self.download_progress = int(downloaded * 100 / total_size) - self._downloading_page.set_progress(self.download_progress) is_elf = False with open(tmpfile, 'rb') as f: @@ -680,7 +673,7 @@ class Setup(Widget): is_elf = header == b'\x7fELF' if not is_elf: - self.download_failed(self.download_url, "No custom software found at this URL.") + self._download_failed_reason = "No custom software found at this URL." return # AGNOS might try to execute the installer before this process exits. @@ -697,17 +690,9 @@ class Setup(Widget): except urllib.error.HTTPError as e: if e.code == 409: - error_msg = "Incompatible openpilot version" - self.download_failed(self.download_url, error_msg) + self._download_failed_reason = "Incompatible openpilot version" except Exception: - error_msg = "Invalid URL" - self.download_failed(self.download_url, error_msg) - - def download_failed(self, url: str, reason: str): - self.failed_url = url - self.failed_reason = reason - self._download_failed_page.set_reason(reason) - self._set_state(SetupState.DOWNLOAD_FAILED) + self._download_failed_reason = "Invalid URL" def main(): diff --git a/system/ui/widgets/nav_widget.py b/system/ui/widgets/nav_widget.py index fe17f12a8f..67203d53f4 100644 --- a/system/ui/widgets/nav_widget.py +++ b/system/ui/widgets/nav_widget.py @@ -63,7 +63,8 @@ class NavWidget(Widget, abc.ABC): self._playing_dismiss_animation = False # released and animating away self._y_pos_filter = BounceFilter(0.0, 0.1, 1 / gui_app.target_fps, bounce=1) - self._dismiss_callback: Callable[[], None] | None = None + self._back_callback: Callable[[], None] | None = None # persistent callback for any back navigation + self._dismiss_callback: Callable[[], None] | None = None # transient callback for programmatic dismiss # TODO: move this state into NavBar self._nav_bar = NavBar() @@ -75,6 +76,9 @@ class NavWidget(Widget, abc.ABC): # the top of a vertical scroll panel to prevent erroneous swipes return True + def set_back_callback(self, callback: Callable[[], None]) -> None: + self._back_callback = callback + def _handle_mouse_event(self, mouse_event: MouseEvent) -> None: super()._handle_mouse_event(mouse_event) @@ -145,6 +149,10 @@ class NavWidget(Widget, abc.ABC): if new_y > self._rect.height + DISMISS_PUSH_OFFSET - 10: gui_app.pop_widget() + + if self._back_callback is not None: + self._back_callback() + if self._dismiss_callback is not None: self._dismiss_callback() self._dismiss_callback = None From 308475fcc933664c545a18ef31f73578a65b0288 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Sun, 1 Mar 2026 03:27:41 -0800 Subject: [PATCH 305/311] Fix continue being enabled under WifiUi --- system/ui/mici_setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/ui/mici_setup.py b/system/ui/mici_setup.py index 9656702c18..28da2f1a4b 100755 --- a/system/ui/mici_setup.py +++ b/system/ui/mici_setup.py @@ -499,7 +499,7 @@ class NetworkSetupPage(NavWidget): if has_internet: self._network_header.set_title("connected to internet") self._network_header.set_icon(self._wifi_full_txt) - self._continue_button.set_enabled(self.enabled) + self._continue_button.set_enabled(lambda: self.enabled) else: self._network_header.set_title(self._waiting_text) self._network_header.set_icon(self._no_wifi_txt) From c244a5d4856554e64f206f507d551fad43d8f923 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Sun, 1 Mar 2026 03:41:20 -0800 Subject: [PATCH 306/311] Update BigInputDialog to remove default URL Remove default URL from custom software input dialog. --- system/ui/mici_setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/ui/mici_setup.py b/system/ui/mici_setup.py index 28da2f1a4b..75ea316ece 100755 --- a/system/ui/mici_setup.py +++ b/system/ui/mici_setup.py @@ -623,7 +623,7 @@ class Setup(Widget): gui_app.pop_widgets_to(self._software_selection_page, instant=True) # don't reset sliders self._download(url) - keyboard = BigInputDialog("custom software URL", "openpilot.comma.ai", confirm_callback=handle_keyboard_result) + keyboard = BigInputDialog("custom software URL", confirm_callback=handle_keyboard_result) gui_app.push_widget(keyboard) def _download(self, url: str): From 56ef3751d822d1b99f6a5f901e62146b3b8c1e36 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 1 Mar 2026 10:49:01 -0500 Subject: [PATCH 307/311] [bot] Update Python packages (#1747) Update Python packages Co-authored-by: github-actions[bot] --- docs/CARS.md | 2 +- opendbc_repo | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/CARS.md b/docs/CARS.md index dc8f1be4f1..31f6f2d32b 100644 --- a/docs/CARS.md +++ b/docs/CARS.md @@ -346,7 +346,7 @@ A supported vehicle is one that just works when you install a comma device. All |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 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`.
+1openpilot Longitudinal Control (Alpha) is available behind a toggle; the toggle is only available in non-release branches such as `nightly-dev`.
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.
diff --git a/opendbc_repo b/opendbc_repo index 8b160905e0..628b14cece 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit 8b160905e046ccfc46b1817238e077cf8e55b6d7 +Subproject commit 628b14ceceec2a8e2ee74d805b0c89c2e63df51e From daaec59464e90dca6afd92b473a659e607628ac3 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sun, 1 Mar 2026 11:14:42 -0500 Subject: [PATCH 308/311] Toyota: Stop and Go Hack (Alpha) (#1733) * init * sl * some * more * alpha * bump * onroad cycle it --- common/params_keys.h | 1 + opendbc_repo | 2 +- .../layouts/settings/vehicle/brands/toyota.py | 66 ++++++++++++++++++- sunnypilot/selfdrive/car/interfaces.py | 1 + sunnypilot/sunnylink/params_metadata.json | 4 ++ 5 files changed, 71 insertions(+), 3 deletions(-) diff --git a/common/params_keys.h b/common/params_keys.h index 65402b5b35..3164fe5365 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -219,6 +219,7 @@ inline static std::unordered_map keys = { {"SubaruStopAndGoManualParkingBrake", {PERSISTENT | BACKUP, BOOL, "0"}}, {"TeslaCoopSteering", {PERSISTENT | BACKUP, BOOL, "0"}}, {"ToyotaEnforceStockLongitudinal", {PERSISTENT | BACKUP, BOOL, "0"}}, + {"ToyotaStopAndGoHack", {PERSISTENT | BACKUP, BOOL, "0"}}, {"DynamicExperimentalControl", {PERSISTENT | BACKUP, BOOL, "0"}}, {"BlindSpot", {PERSISTENT | BACKUP, BOOL, "0"}}, diff --git a/opendbc_repo b/opendbc_repo index 628b14cece..9918ec656f 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit 628b14ceceec2a8e2ee74d805b0c89c2e63df51e +Subproject commit 9918ec656ffa0d1a576f8ae159390408adcaf4cd diff --git a/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/toyota.py b/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/toyota.py index fb375e444e..5a696466b1 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/toyota.py +++ b/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/toyota.py @@ -13,10 +13,17 @@ from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp +ONROAD_ONLY_DESCRIPTION = tr_noop("Start the vehicle to check vehicle compatibility.") +SNG_HACK_UNAVAILABLE = tr_noop("sunnypilot Longitudinal Control must be available and enabled for your vehicle to use this feature.") + DESCRIPTIONS = { 'enforce_stock_longitudinal': tr_noop( 'sunnypilot will not take over control of gas and brakes. Factory Toyota longitudinal control will be used.' ), + 'stop_and_go_hack': tr_noop( + 'sunnypilot will allow some Toyota/Lexus cars to auto resume during stop and go traffic. ' + + 'This feature is only applicable to certain models that are able to use longitudinal control. This is an alpha feature. Use at your own risk.' + ) } @@ -32,7 +39,18 @@ class ToyotaSettings(BrandSettings): enabled=lambda: not ui_state.engaged, ) - self.items = [self.enforce_stock_longitudinal, ] + self.stop_and_go_hack = toggle_item_sp( + lambda: tr("Stop and Go Hack (Alpha)"), + description=lambda: tr(DESCRIPTIONS["stop_and_go_hack"]), + initial_state=ui_state.params.get_bool("ToyotaStopAndGoHack"), + callback=self._on_enable_stop_and_go_hack, + enabled=lambda: not ui_state.engaged, + ) + + self.items = [ + self.enforce_stock_longitudinal, + self.stop_and_go_hack, + ] def _on_enable_enforce_stock_longitudinal(self, state: bool): if state: @@ -41,6 +59,8 @@ class ToyotaSettings(BrandSettings): ui_state.params.put_bool("ToyotaEnforceStockLongitudinal", True) if ui_state.params.get_bool("AlphaLongitudinalEnabled"): ui_state.params.put_bool("AlphaLongitudinalEnabled", False) + ui_state.params.put_bool("ToyotaStopAndGoHack", False) + self.stop_and_go_hack.action_item.set_state(False) ui_state.params.put_bool("OnroadCycleRequested", True) else: self.enforce_stock_longitudinal.action_item.set_state(False) @@ -55,5 +75,47 @@ class ToyotaSettings(BrandSettings): ui_state.params.put_bool("ToyotaEnforceStockLongitudinal", False) ui_state.params.put_bool("OnroadCycleRequested", True) + def _on_enable_stop_and_go_hack(self, state: bool): + if state: + def confirm_callback(result: int): + if result == DialogResult.CONFIRM: + ui_state.params.put_bool("ToyotaStopAndGoHack", True) + ui_state.params.put_bool("OnroadCycleRequested", True) + else: + self.stop_and_go_hack.action_item.set_state(False) + + content = (f"

{self.stop_and_go_hack.title}


" + + f"

{self.stop_and_go_hack.description}

") + + dlg = ConfirmDialog(content, tr("Enable"), rich=True, callback=confirm_callback) + gui_app.push_widget(dlg) + + else: + ui_state.params.put_bool("ToyotaStopAndGoHack", False) + ui_state.params.put_bool("OnroadCycleRequested", True) + def update_settings(self): - pass + if ui_state.CP is not None: + longitudinal = ui_state.CP.openpilotLongitudinalControl + enforce_stock = self.enforce_stock_longitudinal.action_item.get_state() + + if longitudinal and not enforce_stock: + self.stop_and_go_hack.action_item.set_enabled(not ui_state.engaged) + new_desc = tr(DESCRIPTIONS["stop_and_go_hack"]) + show_desc = False + else: + self.stop_and_go_hack.action_item.set_enabled(False) + self.stop_and_go_hack.action_item.set_state(False) + new_desc = "" + tr(SNG_HACK_UNAVAILABLE) + "\n\n" + tr(DESCRIPTIONS["stop_and_go_hack"]) + show_desc = True + + if self.stop_and_go_hack.description != new_desc: + self.stop_and_go_hack.set_description(new_desc) + if show_desc: + self.stop_and_go_hack.show_description(True) + else: + self.stop_and_go_hack.action_item.set_enabled(False) + new_desc = "" + tr(ONROAD_ONLY_DESCRIPTION) + "\n\n" + tr(DESCRIPTIONS["stop_and_go_hack"]) + if self.stop_and_go_hack.description != new_desc: + self.stop_and_go_hack.set_description(new_desc) + self.stop_and_go_hack.show_description(True) diff --git a/sunnypilot/selfdrive/car/interfaces.py b/sunnypilot/selfdrive/car/interfaces.py index a93f5724b5..e4db6c8e97 100644 --- a/sunnypilot/selfdrive/car/interfaces.py +++ b/sunnypilot/selfdrive/car/interfaces.py @@ -131,6 +131,7 @@ def initialize_params(params) -> list[dict[str, Any]]: # toyota keys.extend([ "ToyotaEnforceStockLongitudinal", + "ToyotaStopAndGoHack", ]) return [{k: params.get(k, return_default=True)} for k in keys] diff --git a/sunnypilot/sunnylink/params_metadata.json b/sunnypilot/sunnylink/params_metadata.json index 59c473b973..79c2b5caf9 100644 --- a/sunnypilot/sunnylink/params_metadata.json +++ b/sunnypilot/sunnylink/params_metadata.json @@ -1296,6 +1296,10 @@ "title": "Toyota: Enforce Factory Longitudinal Control", "description": "When enabled, sunnypilot will not take over control of gas and brakes. Factory Toyota longitudinal control will be used." }, + "ToyotaStopAndGoHack": { + "title": "Toyota: Stop and Go Hack (Alpha)", + "description": "sunnypilot will allow some Toyota/Lexus cars to auto resume during stop and go traffic. This feature is only applicable to certain models that are able to use longitudinal control. This is an alpha feature. Use at your own risk." + }, "TrainingVersion": { "title": "Training Version", "description": "" From 041606de4ce60d4ce0705ba20e59357612764dc2 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sun, 1 Mar 2026 10:01:41 -0800 Subject: [PATCH 309/311] fix font output targets (#37511) --- selfdrive/ui/SConscript | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selfdrive/ui/SConscript b/selfdrive/ui/SConscript index 5556883ef8..4d7448c62f 100644 --- a/selfdrive/ui/SConscript +++ b/selfdrive/ui/SConscript @@ -6,7 +6,7 @@ Import('env', 'arch', 'common') generator = File("#selfdrive/assets/fonts/process.py") source_files = Glob("#selfdrive/assets/fonts/*.ttf") + Glob("#selfdrive/assets/fonts/*.otf") output_files = [ - (f.abspath.split('.')[0] + ".fnt", f.abspath.split('.')[0] + ".png") + (f"#{Path(f.path).with_suffix('.fnt')}", f"#{Path(f.path).with_suffix('.png')}") for f in source_files if "NotoColor" not in f.name ] From f9b5d1e9e5e0c3e4371f7338c3373a81d0513fe3 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sun, 1 Mar 2026 10:46:26 -0800 Subject: [PATCH 310/311] use vendored libyuv from dependencies (#37512) * vendor libyuv from dependencies * relock libyuv to latest vendor branch * install cmake in macOS setup when missing * lock * unused? * rm that * no yuv for the larch --- SConstruct | 5 +- pyproject.toml | 1 + system/loggerd/SConscript | 3 +- system/loggerd/encoder/ffmpeg_encoder.cc | 2 +- system/loggerd/encoder/v4l_encoder.cc | 1 - third_party/libyuv/.gitignore | 2 - third_party/libyuv/Darwin/lib/libyuv.a | 3 - third_party/libyuv/LICENSE | 29 - third_party/libyuv/aarch64 | 1 - third_party/libyuv/build.sh | 37 - third_party/libyuv/include/libyuv.h | 32 - .../libyuv/include/libyuv/basic_types.h | 118 - third_party/libyuv/include/libyuv/compare.h | 78 - .../libyuv/include/libyuv/compare_row.h | 84 - third_party/libyuv/include/libyuv/convert.h | 259 --- .../libyuv/include/libyuv/convert_argb.h | 319 --- .../libyuv/include/libyuv/convert_from.h | 179 -- .../libyuv/include/libyuv/convert_from_argb.h | 190 -- third_party/libyuv/include/libyuv/cpu_id.h | 81 - .../libyuv/include/libyuv/macros_msa.h | 76 - .../libyuv/include/libyuv/mjpeg_decoder.h | 192 -- .../libyuv/include/libyuv/planar_functions.h | 529 ----- third_party/libyuv/include/libyuv/rotate.h | 117 - .../libyuv/include/libyuv/rotate_argb.h | 33 - .../libyuv/include/libyuv/rotate_row.h | 121 - third_party/libyuv/include/libyuv/row.h | 1963 ----------------- third_party/libyuv/include/libyuv/scale.h | 103 - .../libyuv/include/libyuv/scale_argb.h | 56 - third_party/libyuv/include/libyuv/scale_row.h | 503 ----- third_party/libyuv/include/libyuv/version.h | 16 - .../libyuv/include/libyuv/video_common.h | 184 -- third_party/libyuv/larch64/lib/libyuv.a | 3 - third_party/libyuv/x86_64/lib/libyuv.a | 3 - tools/replay/framereader.cc | 2 +- tools/setup_dependencies.sh | 12 + uv.lock | 31 +- 36 files changed, 38 insertions(+), 5330 deletions(-) delete mode 100644 third_party/libyuv/.gitignore delete mode 100644 third_party/libyuv/Darwin/lib/libyuv.a delete mode 100644 third_party/libyuv/LICENSE delete mode 120000 third_party/libyuv/aarch64 delete mode 100755 third_party/libyuv/build.sh delete mode 100644 third_party/libyuv/include/libyuv.h delete mode 100644 third_party/libyuv/include/libyuv/basic_types.h delete mode 100644 third_party/libyuv/include/libyuv/compare.h delete mode 100644 third_party/libyuv/include/libyuv/compare_row.h delete mode 100644 third_party/libyuv/include/libyuv/convert.h delete mode 100644 third_party/libyuv/include/libyuv/convert_argb.h delete mode 100644 third_party/libyuv/include/libyuv/convert_from.h delete mode 100644 third_party/libyuv/include/libyuv/convert_from_argb.h delete mode 100644 third_party/libyuv/include/libyuv/cpu_id.h delete mode 100644 third_party/libyuv/include/libyuv/macros_msa.h delete mode 100644 third_party/libyuv/include/libyuv/mjpeg_decoder.h delete mode 100644 third_party/libyuv/include/libyuv/planar_functions.h delete mode 100644 third_party/libyuv/include/libyuv/rotate.h delete mode 100644 third_party/libyuv/include/libyuv/rotate_argb.h delete mode 100644 third_party/libyuv/include/libyuv/rotate_row.h delete mode 100644 third_party/libyuv/include/libyuv/row.h delete mode 100644 third_party/libyuv/include/libyuv/scale.h delete mode 100644 third_party/libyuv/include/libyuv/scale_argb.h delete mode 100644 third_party/libyuv/include/libyuv/scale_row.h delete mode 100644 third_party/libyuv/include/libyuv/version.h delete mode 100644 third_party/libyuv/include/libyuv/video_common.h delete mode 100644 third_party/libyuv/larch64/lib/libyuv.a delete mode 100644 third_party/libyuv/x86_64/lib/libyuv.a diff --git a/SConstruct b/SConstruct index 59ffaf4c76..da70bc3924 100644 --- a/SConstruct +++ b/SConstruct @@ -44,12 +44,13 @@ if arch != "larch64": import eigen import ffmpeg as ffmpeg_pkg import libjpeg + import libyuv import ncurses import openssl3 import python3_dev import zeromq import zstd - pkgs = [bzip2, capnproto, eigen, ffmpeg_pkg, libjpeg, ncurses, openssl3, zeromq, zstd] + pkgs = [bzip2, capnproto, eigen, ffmpeg_pkg, libjpeg, libyuv, ncurses, openssl3, zeromq, zstd] py_include = python3_dev.INCLUDE_DIR else: # TODO: remove when AGNOS has our new vendor pkgs @@ -89,7 +90,6 @@ env = Environment( "#third_party/acados/include/blasfeo/include", "#third_party/acados/include/hpipm/include", "#third_party/catch2/include", - "#third_party/libyuv/include", [x.INCLUDE_DIR for x in pkgs], ], LIBPATH=[ @@ -98,7 +98,6 @@ env = Environment( "#third_party", "#selfdrive/pandad", "#rednose/helpers", - f"#third_party/libyuv/{arch}/lib", f"#third_party/acados/{arch}/lib", [x.LIB_DIR for x in pkgs], ], diff --git a/pyproject.toml b/pyproject.toml index c3467342b6..6516c8cd5b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,7 @@ dependencies = [ "eigen @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=eigen", "ffmpeg @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=ffmpeg", "libjpeg @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=libjpeg", + "libyuv @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=libyuv", "openssl3 @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=openssl3", "python3-dev @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=python3-dev", "zstd @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=zstd", diff --git a/system/loggerd/SConscript b/system/loggerd/SConscript index 827c1ce5d0..b02c409240 100644 --- a/system/loggerd/SConscript +++ b/system/loggerd/SConscript @@ -2,11 +2,12 @@ Import('env', 'arch', 'messaging', 'common', 'visionipc') libs = [common, messaging, visionipc, 'avformat', 'avcodec', 'swresample', 'avutil', 'x264', - 'yuv', 'pthread', 'z', 'm', 'zstd'] + 'pthread', 'z', 'm', 'zstd'] src = ['logger.cc', 'zstd_writer.cc', 'video_writer.cc', 'encoder/encoder.cc', 'encoder/v4l_encoder.cc', 'encoder/jpeg_encoder.cc'] if arch != "larch64": src += ['encoder/ffmpeg_encoder.cc'] + libs += ['yuv'] if arch == "Darwin": # exclude v4l diff --git a/system/loggerd/encoder/ffmpeg_encoder.cc b/system/loggerd/encoder/ffmpeg_encoder.cc index 4d6be47182..275a2e481e 100644 --- a/system/loggerd/encoder/ffmpeg_encoder.cc +++ b/system/loggerd/encoder/ffmpeg_encoder.cc @@ -9,7 +9,7 @@ #define __STDC_CONSTANT_MACROS -#include "third_party/libyuv/include/libyuv.h" +#include "libyuv.h" extern "C" { #include diff --git a/system/loggerd/encoder/v4l_encoder.cc b/system/loggerd/encoder/v4l_encoder.cc index 383fa2f0f5..cabd9fd997 100644 --- a/system/loggerd/encoder/v4l_encoder.cc +++ b/system/loggerd/encoder/v4l_encoder.cc @@ -7,7 +7,6 @@ #include "common/util.h" #include "common/timing.h" -#include "third_party/libyuv/include/libyuv.h" #include "third_party/linux/include/msm_media_info.h" // has to be in this order diff --git a/third_party/libyuv/.gitignore b/third_party/libyuv/.gitignore deleted file mode 100644 index 1e943ae6c6..0000000000 --- a/third_party/libyuv/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/libyuv/ -!*.a diff --git a/third_party/libyuv/Darwin/lib/libyuv.a b/third_party/libyuv/Darwin/lib/libyuv.a deleted file mode 100644 index b72979ef19..0000000000 --- a/third_party/libyuv/Darwin/lib/libyuv.a +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:497e01c39e1629a89afa730341fe066c2e926966c5f050003e7fde2ce46d9da3 -size 863648 diff --git a/third_party/libyuv/LICENSE b/third_party/libyuv/LICENSE deleted file mode 100644 index c911747a6b..0000000000 --- a/third_party/libyuv/LICENSE +++ /dev/null @@ -1,29 +0,0 @@ -Copyright 2011 The LibYuv Project Authors. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in - the documentation and/or other materials provided with the - distribution. - - * Neither the name of Google nor the names of its contributors may - be used to endorse or promote products derived from this software - without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/third_party/libyuv/aarch64 b/third_party/libyuv/aarch64 deleted file mode 120000 index 062c65e8d9..0000000000 --- a/third_party/libyuv/aarch64 +++ /dev/null @@ -1 +0,0 @@ -larch64/ \ No newline at end of file diff --git a/third_party/libyuv/build.sh b/third_party/libyuv/build.sh deleted file mode 100755 index 11f88ab46c..0000000000 --- a/third_party/libyuv/build.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env bash -set -e - -export SOURCE_DATE_EPOCH=0 -export ZERO_AR_DATE=1 - -DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" - -ARCHNAME=$(uname -m) -if [ -f /TICI ]; then - ARCHNAME="larch64" -fi - -if [[ "$OSTYPE" == "darwin"* ]]; then - ARCHNAME="Darwin" -fi - -cd $DIR -if [ ! -d libyuv ]; then - git clone --single-branch https://chromium.googlesource.com/libyuv/libyuv -fi - -cd libyuv -git checkout 4a14cb2e81235ecd656e799aecaaf139db8ce4a2 - -# build -cmake . -make -j$(nproc) - -INSTALL_DIR="$DIR/$ARCHNAME" -rm -rf $INSTALL_DIR -mkdir -p $INSTALL_DIR - -rm -rf $DIR/include -mkdir -p $INSTALL_DIR/lib -cp $DIR/libyuv/libyuv.a $INSTALL_DIR/lib -cp -r $DIR/libyuv/include $DIR diff --git a/third_party/libyuv/include/libyuv.h b/third_party/libyuv/include/libyuv.h deleted file mode 100644 index aeffd5ef7a..0000000000 --- a/third_party/libyuv/include/libyuv.h +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2011 The LibYuv Project Authors. All rights reserved. - * - * Use of this source code is governed by a BSD-style license - * that can be found in the LICENSE file in the root of the source - * tree. An additional intellectual property rights grant can be found - * in the file PATENTS. All contributing project authors may - * be found in the AUTHORS file in the root of the source tree. - */ - -#ifndef INCLUDE_LIBYUV_H_ -#define INCLUDE_LIBYUV_H_ - -#include "libyuv/basic_types.h" -#include "libyuv/compare.h" -#include "libyuv/convert.h" -#include "libyuv/convert_argb.h" -#include "libyuv/convert_from.h" -#include "libyuv/convert_from_argb.h" -#include "libyuv/cpu_id.h" -#include "libyuv/mjpeg_decoder.h" -#include "libyuv/planar_functions.h" -#include "libyuv/rotate.h" -#include "libyuv/rotate_argb.h" -#include "libyuv/row.h" -#include "libyuv/scale.h" -#include "libyuv/scale_argb.h" -#include "libyuv/scale_row.h" -#include "libyuv/version.h" -#include "libyuv/video_common.h" - -#endif // INCLUDE_LIBYUV_H_ diff --git a/third_party/libyuv/include/libyuv/basic_types.h b/third_party/libyuv/include/libyuv/basic_types.h deleted file mode 100644 index 5b760ee0d4..0000000000 --- a/third_party/libyuv/include/libyuv/basic_types.h +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright 2011 The LibYuv Project Authors. All rights reserved. - * - * Use of this source code is governed by a BSD-style license - * that can be found in the LICENSE file in the root of the source - * tree. An additional intellectual property rights grant can be found - * in the file PATENTS. All contributing project authors may - * be found in the AUTHORS file in the root of the source tree. - */ - -#ifndef INCLUDE_LIBYUV_BASIC_TYPES_H_ -#define INCLUDE_LIBYUV_BASIC_TYPES_H_ - -#include // for NULL, size_t - -#if defined(_MSC_VER) && (_MSC_VER < 1600) -#include // for uintptr_t on x86 -#else -#include // for uintptr_t -#endif - -#ifndef GG_LONGLONG -#ifndef INT_TYPES_DEFINED -#define INT_TYPES_DEFINED -#ifdef COMPILER_MSVC -typedef unsigned __int64 uint64; -typedef __int64 int64; -#ifndef INT64_C -#define INT64_C(x) x ## I64 -#endif -#ifndef UINT64_C -#define UINT64_C(x) x ## UI64 -#endif -#define INT64_F "I64" -#else // COMPILER_MSVC -#if defined(__LP64__) && !defined(__OpenBSD__) && !defined(__APPLE__) -typedef unsigned long uint64; // NOLINT -typedef long int64; // NOLINT -#ifndef INT64_C -#define INT64_C(x) x ## L -#endif -#ifndef UINT64_C -#define UINT64_C(x) x ## UL -#endif -#define INT64_F "l" -#else // defined(__LP64__) && !defined(__OpenBSD__) && !defined(__APPLE__) -typedef unsigned long long uint64; // NOLINT -typedef long long int64; // NOLINT -#ifndef INT64_C -#define INT64_C(x) x ## LL -#endif -#ifndef UINT64_C -#define UINT64_C(x) x ## ULL -#endif -#define INT64_F "ll" -#endif // __LP64__ -#endif // COMPILER_MSVC -typedef unsigned int uint32; -typedef int int32; -typedef unsigned short uint16; // NOLINT -typedef short int16; // NOLINT -typedef unsigned char uint8; -typedef signed char int8; -#endif // INT_TYPES_DEFINED -#endif // GG_LONGLONG - -// Detect compiler is for x86 or x64. -#if defined(__x86_64__) || defined(_M_X64) || \ - defined(__i386__) || defined(_M_IX86) -#define CPU_X86 1 -#endif -// Detect compiler is for ARM. -#if defined(__arm__) || defined(_M_ARM) -#define CPU_ARM 1 -#endif - -#ifndef ALIGNP -#ifdef __cplusplus -#define ALIGNP(p, t) \ - (reinterpret_cast(((reinterpret_cast(p) + \ - ((t) - 1)) & ~((t) - 1)))) -#else -#define ALIGNP(p, t) \ - ((uint8*)((((uintptr_t)(p) + ((t) - 1)) & ~((t) - 1)))) /* NOLINT */ -#endif -#endif - -#if !defined(LIBYUV_API) -#if defined(_WIN32) || defined(__CYGWIN__) -#if defined(LIBYUV_BUILDING_SHARED_LIBRARY) -#define LIBYUV_API __declspec(dllexport) -#elif defined(LIBYUV_USING_SHARED_LIBRARY) -#define LIBYUV_API __declspec(dllimport) -#else -#define LIBYUV_API -#endif // LIBYUV_BUILDING_SHARED_LIBRARY -#elif defined(__GNUC__) && (__GNUC__ >= 4) && !defined(__APPLE__) && \ - (defined(LIBYUV_BUILDING_SHARED_LIBRARY) || \ - defined(LIBYUV_USING_SHARED_LIBRARY)) -#define LIBYUV_API __attribute__ ((visibility ("default"))) -#else -#define LIBYUV_API -#endif // __GNUC__ -#endif // LIBYUV_API - -#define LIBYUV_BOOL int -#define LIBYUV_FALSE 0 -#define LIBYUV_TRUE 1 - -// Visual C x86 or GCC little endian. -#if defined(__x86_64__) || defined(_M_X64) || \ - defined(__i386__) || defined(_M_IX86) || \ - defined(__arm__) || defined(_M_ARM) || \ - (defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__) -#define LIBYUV_LITTLE_ENDIAN -#endif - -#endif // INCLUDE_LIBYUV_BASIC_TYPES_H_ diff --git a/third_party/libyuv/include/libyuv/compare.h b/third_party/libyuv/include/libyuv/compare.h deleted file mode 100644 index 550712de6e..0000000000 --- a/third_party/libyuv/include/libyuv/compare.h +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright 2011 The LibYuv Project Authors. All rights reserved. - * - * Use of this source code is governed by a BSD-style license - * that can be found in the LICENSE file in the root of the source - * tree. An additional intellectual property rights grant can be found - * in the file PATENTS. All contributing project authors may - * be found in the AUTHORS file in the root of the source tree. - */ - -#ifndef INCLUDE_LIBYUV_COMPARE_H_ -#define INCLUDE_LIBYUV_COMPARE_H_ - -#include "libyuv/basic_types.h" - -#ifdef __cplusplus -namespace libyuv { -extern "C" { -#endif - -// Compute a hash for specified memory. Seed of 5381 recommended. -LIBYUV_API -uint32 HashDjb2(const uint8* src, uint64 count, uint32 seed); - -// Scan an opaque argb image and return fourcc based on alpha offset. -// Returns FOURCC_ARGB, FOURCC_BGRA, or 0 if unknown. -LIBYUV_API -uint32 ARGBDetect(const uint8* argb, int stride_argb, int width, int height); - -// Sum Square Error - used to compute Mean Square Error or PSNR. -LIBYUV_API -uint64 ComputeSumSquareError(const uint8* src_a, - const uint8* src_b, int count); - -LIBYUV_API -uint64 ComputeSumSquareErrorPlane(const uint8* src_a, int stride_a, - const uint8* src_b, int stride_b, - int width, int height); - -static const int kMaxPsnr = 128; - -LIBYUV_API -double SumSquareErrorToPsnr(uint64 sse, uint64 count); - -LIBYUV_API -double CalcFramePsnr(const uint8* src_a, int stride_a, - const uint8* src_b, int stride_b, - int width, int height); - -LIBYUV_API -double I420Psnr(const uint8* src_y_a, int stride_y_a, - const uint8* src_u_a, int stride_u_a, - const uint8* src_v_a, int stride_v_a, - const uint8* src_y_b, int stride_y_b, - const uint8* src_u_b, int stride_u_b, - const uint8* src_v_b, int stride_v_b, - int width, int height); - -LIBYUV_API -double CalcFrameSsim(const uint8* src_a, int stride_a, - const uint8* src_b, int stride_b, - int width, int height); - -LIBYUV_API -double I420Ssim(const uint8* src_y_a, int stride_y_a, - const uint8* src_u_a, int stride_u_a, - const uint8* src_v_a, int stride_v_a, - const uint8* src_y_b, int stride_y_b, - const uint8* src_u_b, int stride_u_b, - const uint8* src_v_b, int stride_v_b, - int width, int height); - -#ifdef __cplusplus -} // extern "C" -} // namespace libyuv -#endif - -#endif // INCLUDE_LIBYUV_COMPARE_H_ diff --git a/third_party/libyuv/include/libyuv/compare_row.h b/third_party/libyuv/include/libyuv/compare_row.h deleted file mode 100644 index 781cad3e65..0000000000 --- a/third_party/libyuv/include/libyuv/compare_row.h +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright 2013 The LibYuv Project Authors. All rights reserved. - * - * Use of this source code is governed by a BSD-style license - * that can be found in the LICENSE file in the root of the source - * tree. An additional intellectual property rights grant can be found - * in the file PATENTS. All contributing project authors may - * be found in the AUTHORS file in the root of the source tree. - */ - -#ifndef INCLUDE_LIBYUV_COMPARE_ROW_H_ -#define INCLUDE_LIBYUV_COMPARE_ROW_H_ - -#include "libyuv/basic_types.h" - -#ifdef __cplusplus -namespace libyuv { -extern "C" { -#endif - -#if defined(__pnacl__) || defined(__CLR_VER) || \ - (defined(__i386__) && !defined(__SSE2__)) -#define LIBYUV_DISABLE_X86 -#endif -// MemorySanitizer does not support assembly code yet. http://crbug.com/344505 -#if defined(__has_feature) -#if __has_feature(memory_sanitizer) -#define LIBYUV_DISABLE_X86 -#endif -#endif - -// Visual C 2012 required for AVX2. -#if defined(_M_IX86) && !defined(__clang__) && \ - defined(_MSC_VER) && _MSC_VER >= 1700 -#define VISUALC_HAS_AVX2 1 -#endif // VisualStudio >= 2012 - -// clang >= 3.4.0 required for AVX2. -#if defined(__clang__) && (defined(__x86_64__) || defined(__i386__)) -#if (__clang_major__ > 3) || (__clang_major__ == 3 && (__clang_minor__ >= 4)) -#define CLANG_HAS_AVX2 1 -#endif // clang >= 3.4 -#endif // __clang__ - -#if !defined(LIBYUV_DISABLE_X86) && \ - defined(_M_IX86) && (defined(VISUALC_HAS_AVX2) || defined(CLANG_HAS_AVX2)) -#define HAS_HASHDJB2_AVX2 -#endif - -// The following are available for Visual C and GCC: -#if !defined(LIBYUV_DISABLE_X86) && \ - (defined(__x86_64__) || (defined(__i386__) || defined(_M_IX86))) -#define HAS_HASHDJB2_SSE41 -#define HAS_SUMSQUAREERROR_SSE2 -#endif - -// The following are available for Visual C and clangcl 32 bit: -#if !defined(LIBYUV_DISABLE_X86) && defined(_M_IX86) && \ - (defined(VISUALC_HAS_AVX2) || defined(CLANG_HAS_AVX2)) -#define HAS_HASHDJB2_AVX2 -#define HAS_SUMSQUAREERROR_AVX2 -#endif - -// The following are available for Neon: -#if !defined(LIBYUV_DISABLE_NEON) && \ - (defined(__ARM_NEON__) || defined(LIBYUV_NEON) || defined(__aarch64__)) -#define HAS_SUMSQUAREERROR_NEON -#endif - -uint32 SumSquareError_C(const uint8* src_a, const uint8* src_b, int count); -uint32 SumSquareError_SSE2(const uint8* src_a, const uint8* src_b, int count); -uint32 SumSquareError_AVX2(const uint8* src_a, const uint8* src_b, int count); -uint32 SumSquareError_NEON(const uint8* src_a, const uint8* src_b, int count); - -uint32 HashDjb2_C(const uint8* src, int count, uint32 seed); -uint32 HashDjb2_SSE41(const uint8* src, int count, uint32 seed); -uint32 HashDjb2_AVX2(const uint8* src, int count, uint32 seed); - -#ifdef __cplusplus -} // extern "C" -} // namespace libyuv -#endif - -#endif // INCLUDE_LIBYUV_COMPARE_ROW_H_ diff --git a/third_party/libyuv/include/libyuv/convert.h b/third_party/libyuv/include/libyuv/convert.h deleted file mode 100644 index d44485847b..0000000000 --- a/third_party/libyuv/include/libyuv/convert.h +++ /dev/null @@ -1,259 +0,0 @@ -/* - * Copyright 2011 The LibYuv Project Authors. All rights reserved. - * - * Use of this source code is governed by a BSD-style license - * that can be found in the LICENSE file in the root of the source - * tree. An additional intellectual property rights grant can be found - * in the file PATENTS. All contributing project authors may - * be found in the AUTHORS file in the root of the source tree. - */ - -#ifndef INCLUDE_LIBYUV_CONVERT_H_ -#define INCLUDE_LIBYUV_CONVERT_H_ - -#include "libyuv/basic_types.h" - -#include "libyuv/rotate.h" // For enum RotationMode. - -// TODO(fbarchard): fix WebRTC source to include following libyuv headers: -#include "libyuv/convert_argb.h" // For WebRTC I420ToARGB. b/620 -#include "libyuv/convert_from.h" // For WebRTC ConvertFromI420. b/620 -#include "libyuv/planar_functions.h" // For WebRTC I420Rect, CopyPlane. b/618 - -#ifdef __cplusplus -namespace libyuv { -extern "C" { -#endif - -// Convert I444 to I420. -LIBYUV_API -int I444ToI420(const uint8* src_y, int src_stride_y, - const uint8* src_u, int src_stride_u, - const uint8* src_v, int src_stride_v, - uint8* dst_y, int dst_stride_y, - uint8* dst_u, int dst_stride_u, - uint8* dst_v, int dst_stride_v, - int width, int height); - -// Convert I422 to I420. -LIBYUV_API -int I422ToI420(const uint8* src_y, int src_stride_y, - const uint8* src_u, int src_stride_u, - const uint8* src_v, int src_stride_v, - uint8* dst_y, int dst_stride_y, - uint8* dst_u, int dst_stride_u, - uint8* dst_v, int dst_stride_v, - int width, int height); - -// Convert I411 to I420. -LIBYUV_API -int I411ToI420(const uint8* src_y, int src_stride_y, - const uint8* src_u, int src_stride_u, - const uint8* src_v, int src_stride_v, - uint8* dst_y, int dst_stride_y, - uint8* dst_u, int dst_stride_u, - uint8* dst_v, int dst_stride_v, - int width, int height); - -// Copy I420 to I420. -#define I420ToI420 I420Copy -LIBYUV_API -int I420Copy(const uint8* src_y, int src_stride_y, - const uint8* src_u, int src_stride_u, - const uint8* src_v, int src_stride_v, - uint8* dst_y, int dst_stride_y, - uint8* dst_u, int dst_stride_u, - uint8* dst_v, int dst_stride_v, - int width, int height); - -// Convert I400 (grey) to I420. -LIBYUV_API -int I400ToI420(const uint8* src_y, int src_stride_y, - uint8* dst_y, int dst_stride_y, - uint8* dst_u, int dst_stride_u, - uint8* dst_v, int dst_stride_v, - int width, int height); - -#define J400ToJ420 I400ToI420 - -// Convert NV12 to I420. -LIBYUV_API -int NV12ToI420(const uint8* src_y, int src_stride_y, - const uint8* src_uv, int src_stride_uv, - uint8* dst_y, int dst_stride_y, - uint8* dst_u, int dst_stride_u, - uint8* dst_v, int dst_stride_v, - int width, int height); - -// Convert NV21 to I420. -LIBYUV_API -int NV21ToI420(const uint8* src_y, int src_stride_y, - const uint8* src_vu, int src_stride_vu, - uint8* dst_y, int dst_stride_y, - uint8* dst_u, int dst_stride_u, - uint8* dst_v, int dst_stride_v, - int width, int height); - -// Convert YUY2 to I420. -LIBYUV_API -int YUY2ToI420(const uint8* src_yuy2, int src_stride_yuy2, - uint8* dst_y, int dst_stride_y, - uint8* dst_u, int dst_stride_u, - uint8* dst_v, int dst_stride_v, - int width, int height); - -// Convert UYVY to I420. -LIBYUV_API -int UYVYToI420(const uint8* src_uyvy, int src_stride_uyvy, - uint8* dst_y, int dst_stride_y, - uint8* dst_u, int dst_stride_u, - uint8* dst_v, int dst_stride_v, - int width, int height); - -// Convert M420 to I420. -LIBYUV_API -int M420ToI420(const uint8* src_m420, int src_stride_m420, - uint8* dst_y, int dst_stride_y, - uint8* dst_u, int dst_stride_u, - uint8* dst_v, int dst_stride_v, - int width, int height); - -// Convert Android420 to I420. -LIBYUV_API -int Android420ToI420(const uint8* src_y, int src_stride_y, - const uint8* src_u, int src_stride_u, - const uint8* src_v, int src_stride_v, - int pixel_stride_uv, - uint8* dst_y, int dst_stride_y, - uint8* dst_u, int dst_stride_u, - uint8* dst_v, int dst_stride_v, - int width, int height); - -// ARGB little endian (bgra in memory) to I420. -LIBYUV_API -int ARGBToI420(const uint8* src_frame, int src_stride_frame, - uint8* dst_y, int dst_stride_y, - uint8* dst_u, int dst_stride_u, - uint8* dst_v, int dst_stride_v, - int width, int height); - -// BGRA little endian (argb in memory) to I420. -LIBYUV_API -int BGRAToI420(const uint8* src_frame, int src_stride_frame, - uint8* dst_y, int dst_stride_y, - uint8* dst_u, int dst_stride_u, - uint8* dst_v, int dst_stride_v, - int width, int height); - -// ABGR little endian (rgba in memory) to I420. -LIBYUV_API -int ABGRToI420(const uint8* src_frame, int src_stride_frame, - uint8* dst_y, int dst_stride_y, - uint8* dst_u, int dst_stride_u, - uint8* dst_v, int dst_stride_v, - int width, int height); - -// RGBA little endian (abgr in memory) to I420. -LIBYUV_API -int RGBAToI420(const uint8* src_frame, int src_stride_frame, - uint8* dst_y, int dst_stride_y, - uint8* dst_u, int dst_stride_u, - uint8* dst_v, int dst_stride_v, - int width, int height); - -// RGB little endian (bgr in memory) to I420. -LIBYUV_API -int RGB24ToI420(const uint8* src_frame, int src_stride_frame, - uint8* dst_y, int dst_stride_y, - uint8* dst_u, int dst_stride_u, - uint8* dst_v, int dst_stride_v, - int width, int height); - -// RGB big endian (rgb in memory) to I420. -LIBYUV_API -int RAWToI420(const uint8* src_frame, int src_stride_frame, - uint8* dst_y, int dst_stride_y, - uint8* dst_u, int dst_stride_u, - uint8* dst_v, int dst_stride_v, - int width, int height); - -// RGB16 (RGBP fourcc) little endian to I420. -LIBYUV_API -int RGB565ToI420(const uint8* src_frame, int src_stride_frame, - uint8* dst_y, int dst_stride_y, - uint8* dst_u, int dst_stride_u, - uint8* dst_v, int dst_stride_v, - int width, int height); - -// RGB15 (RGBO fourcc) little endian to I420. -LIBYUV_API -int ARGB1555ToI420(const uint8* src_frame, int src_stride_frame, - uint8* dst_y, int dst_stride_y, - uint8* dst_u, int dst_stride_u, - uint8* dst_v, int dst_stride_v, - int width, int height); - -// RGB12 (R444 fourcc) little endian to I420. -LIBYUV_API -int ARGB4444ToI420(const uint8* src_frame, int src_stride_frame, - uint8* dst_y, int dst_stride_y, - uint8* dst_u, int dst_stride_u, - uint8* dst_v, int dst_stride_v, - int width, int height); - -#ifdef HAVE_JPEG -// src_width/height provided by capture. -// dst_width/height for clipping determine final size. -LIBYUV_API -int MJPGToI420(const uint8* sample, size_t sample_size, - uint8* dst_y, int dst_stride_y, - uint8* dst_u, int dst_stride_u, - uint8* dst_v, int dst_stride_v, - int src_width, int src_height, - int dst_width, int dst_height); - -// Query size of MJPG in pixels. -LIBYUV_API -int MJPGSize(const uint8* sample, size_t sample_size, - int* width, int* height); -#endif - -// Convert camera sample to I420 with cropping, rotation and vertical flip. -// "src_size" is needed to parse MJPG. -// "dst_stride_y" number of bytes in a row of the dst_y plane. -// Normally this would be the same as dst_width, with recommended alignment -// to 16 bytes for better efficiency. -// If rotation of 90 or 270 is used, stride is affected. The caller should -// allocate the I420 buffer according to rotation. -// "dst_stride_u" number of bytes in a row of the dst_u plane. -// Normally this would be the same as (dst_width + 1) / 2, with -// recommended alignment to 16 bytes for better efficiency. -// If rotation of 90 or 270 is used, stride is affected. -// "crop_x" and "crop_y" are starting position for cropping. -// To center, crop_x = (src_width - dst_width) / 2 -// crop_y = (src_height - dst_height) / 2 -// "src_width" / "src_height" is size of src_frame in pixels. -// "src_height" can be negative indicating a vertically flipped image source. -// "crop_width" / "crop_height" is the size to crop the src to. -// Must be less than or equal to src_width/src_height -// Cropping parameters are pre-rotation. -// "rotation" can be 0, 90, 180 or 270. -// "format" is a fourcc. ie 'I420', 'YUY2' -// Returns 0 for successful; -1 for invalid parameter. Non-zero for failure. -LIBYUV_API -int ConvertToI420(const uint8* src_frame, size_t src_size, - uint8* dst_y, int dst_stride_y, - uint8* dst_u, int dst_stride_u, - uint8* dst_v, int dst_stride_v, - int crop_x, int crop_y, - int src_width, int src_height, - int crop_width, int crop_height, - enum RotationMode rotation, - uint32 format); - -#ifdef __cplusplus -} // extern "C" -} // namespace libyuv -#endif - -#endif // INCLUDE_LIBYUV_CONVERT_H_ diff --git a/third_party/libyuv/include/libyuv/convert_argb.h b/third_party/libyuv/include/libyuv/convert_argb.h deleted file mode 100644 index dc03ac8d5d..0000000000 --- a/third_party/libyuv/include/libyuv/convert_argb.h +++ /dev/null @@ -1,319 +0,0 @@ -/* - * Copyright 2012 The LibYuv Project Authors. All rights reserved. - * - * Use of this source code is governed by a BSD-style license - * that can be found in the LICENSE file in the root of the source - * tree. An additional intellectual property rights grant can be found - * in the file PATENTS. All contributing project authors may - * be found in the AUTHORS file in the root of the source tree. - */ - -#ifndef INCLUDE_LIBYUV_CONVERT_ARGB_H_ -#define INCLUDE_LIBYUV_CONVERT_ARGB_H_ - -#include "libyuv/basic_types.h" - -#include "libyuv/rotate.h" // For enum RotationMode. - -// TODO(fbarchard): This set of functions should exactly match convert.h -// TODO(fbarchard): Add tests. Create random content of right size and convert -// with C vs Opt and or to I420 and compare. -// TODO(fbarchard): Some of these functions lack parameter setting. - -#ifdef __cplusplus -namespace libyuv { -extern "C" { -#endif - -// Alias. -#define ARGBToARGB ARGBCopy - -// Copy ARGB to ARGB. -LIBYUV_API -int ARGBCopy(const uint8* src_argb, int src_stride_argb, - uint8* dst_argb, int dst_stride_argb, - int width, int height); - -// Convert I420 to ARGB. -LIBYUV_API -int I420ToARGB(const uint8* src_y, int src_stride_y, - const uint8* src_u, int src_stride_u, - const uint8* src_v, int src_stride_v, - uint8* dst_argb, int dst_stride_argb, - int width, int height); - -// Duplicate prototype for function in convert_from.h for remoting. -LIBYUV_API -int I420ToABGR(const uint8* src_y, int src_stride_y, - const uint8* src_u, int src_stride_u, - const uint8* src_v, int src_stride_v, - uint8* dst_argb, int dst_stride_argb, - int width, int height); - -// Convert I422 to ARGB. -LIBYUV_API -int I422ToARGB(const uint8* src_y, int src_stride_y, - const uint8* src_u, int src_stride_u, - const uint8* src_v, int src_stride_v, - uint8* dst_argb, int dst_stride_argb, - int width, int height); - -// Convert I444 to ARGB. -LIBYUV_API -int I444ToARGB(const uint8* src_y, int src_stride_y, - const uint8* src_u, int src_stride_u, - const uint8* src_v, int src_stride_v, - uint8* dst_argb, int dst_stride_argb, - int width, int height); - -// Convert J444 to ARGB. -LIBYUV_API -int J444ToARGB(const uint8* src_y, int src_stride_y, - const uint8* src_u, int src_stride_u, - const uint8* src_v, int src_stride_v, - uint8* dst_argb, int dst_stride_argb, - int width, int height); - -// Convert I444 to ABGR. -LIBYUV_API -int I444ToABGR(const uint8* src_y, int src_stride_y, - const uint8* src_u, int src_stride_u, - const uint8* src_v, int src_stride_v, - uint8* dst_abgr, int dst_stride_abgr, - int width, int height); - -// Convert I411 to ARGB. -LIBYUV_API -int I411ToARGB(const uint8* src_y, int src_stride_y, - const uint8* src_u, int src_stride_u, - const uint8* src_v, int src_stride_v, - uint8* dst_argb, int dst_stride_argb, - int width, int height); - -// Convert I420 with Alpha to preattenuated ARGB. -LIBYUV_API -int I420AlphaToARGB(const uint8* src_y, int src_stride_y, - const uint8* src_u, int src_stride_u, - const uint8* src_v, int src_stride_v, - const uint8* src_a, int src_stride_a, - uint8* dst_argb, int dst_stride_argb, - int width, int height, int attenuate); - -// Convert I420 with Alpha to preattenuated ABGR. -LIBYUV_API -int I420AlphaToABGR(const uint8* src_y, int src_stride_y, - const uint8* src_u, int src_stride_u, - const uint8* src_v, int src_stride_v, - const uint8* src_a, int src_stride_a, - uint8* dst_abgr, int dst_stride_abgr, - int width, int height, int attenuate); - -// Convert I400 (grey) to ARGB. Reverse of ARGBToI400. -LIBYUV_API -int I400ToARGB(const uint8* src_y, int src_stride_y, - uint8* dst_argb, int dst_stride_argb, - int width, int height); - -// Convert J400 (jpeg grey) to ARGB. -LIBYUV_API -int J400ToARGB(const uint8* src_y, int src_stride_y, - uint8* dst_argb, int dst_stride_argb, - int width, int height); - -// Alias. -#define YToARGB I400ToARGB - -// Convert NV12 to ARGB. -LIBYUV_API -int NV12ToARGB(const uint8* src_y, int src_stride_y, - const uint8* src_uv, int src_stride_uv, - uint8* dst_argb, int dst_stride_argb, - int width, int height); - -// Convert NV21 to ARGB. -LIBYUV_API -int NV21ToARGB(const uint8* src_y, int src_stride_y, - const uint8* src_vu, int src_stride_vu, - uint8* dst_argb, int dst_stride_argb, - int width, int height); - -// Convert M420 to ARGB. -LIBYUV_API -int M420ToARGB(const uint8* src_m420, int src_stride_m420, - uint8* dst_argb, int dst_stride_argb, - int width, int height); - -// Convert YUY2 to ARGB. -LIBYUV_API -int YUY2ToARGB(const uint8* src_yuy2, int src_stride_yuy2, - uint8* dst_argb, int dst_stride_argb, - int width, int height); - -// Convert UYVY to ARGB. -LIBYUV_API -int UYVYToARGB(const uint8* src_uyvy, int src_stride_uyvy, - uint8* dst_argb, int dst_stride_argb, - int width, int height); - -// Convert J420 to ARGB. -LIBYUV_API -int J420ToARGB(const uint8* src_y, int src_stride_y, - const uint8* src_u, int src_stride_u, - const uint8* src_v, int src_stride_v, - uint8* dst_argb, int dst_stride_argb, - int width, int height); - -// Convert J422 to ARGB. -LIBYUV_API -int J422ToARGB(const uint8* src_y, int src_stride_y, - const uint8* src_u, int src_stride_u, - const uint8* src_v, int src_stride_v, - uint8* dst_argb, int dst_stride_argb, - int width, int height); - -// Convert J420 to ABGR. -LIBYUV_API -int J420ToABGR(const uint8* src_y, int src_stride_y, - const uint8* src_u, int src_stride_u, - const uint8* src_v, int src_stride_v, - uint8* dst_abgr, int dst_stride_abgr, - int width, int height); - -// Convert J422 to ABGR. -LIBYUV_API -int J422ToABGR(const uint8* src_y, int src_stride_y, - const uint8* src_u, int src_stride_u, - const uint8* src_v, int src_stride_v, - uint8* dst_abgr, int dst_stride_abgr, - int width, int height); - -// Convert H420 to ARGB. -LIBYUV_API -int H420ToARGB(const uint8* src_y, int src_stride_y, - const uint8* src_u, int src_stride_u, - const uint8* src_v, int src_stride_v, - uint8* dst_argb, int dst_stride_argb, - int width, int height); - -// Convert H422 to ARGB. -LIBYUV_API -int H422ToARGB(const uint8* src_y, int src_stride_y, - const uint8* src_u, int src_stride_u, - const uint8* src_v, int src_stride_v, - uint8* dst_argb, int dst_stride_argb, - int width, int height); - -// Convert H420 to ABGR. -LIBYUV_API -int H420ToABGR(const uint8* src_y, int src_stride_y, - const uint8* src_u, int src_stride_u, - const uint8* src_v, int src_stride_v, - uint8* dst_abgr, int dst_stride_abgr, - int width, int height); - -// Convert H422 to ABGR. -LIBYUV_API -int H422ToABGR(const uint8* src_y, int src_stride_y, - const uint8* src_u, int src_stride_u, - const uint8* src_v, int src_stride_v, - uint8* dst_abgr, int dst_stride_abgr, - int width, int height); - -// BGRA little endian (argb in memory) to ARGB. -LIBYUV_API -int BGRAToARGB(const uint8* src_frame, int src_stride_frame, - uint8* dst_argb, int dst_stride_argb, - int width, int height); - -// ABGR little endian (rgba in memory) to ARGB. -LIBYUV_API -int ABGRToARGB(const uint8* src_frame, int src_stride_frame, - uint8* dst_argb, int dst_stride_argb, - int width, int height); - -// RGBA little endian (abgr in memory) to ARGB. -LIBYUV_API -int RGBAToARGB(const uint8* src_frame, int src_stride_frame, - uint8* dst_argb, int dst_stride_argb, - int width, int height); - -// Deprecated function name. -#define BG24ToARGB RGB24ToARGB - -// RGB little endian (bgr in memory) to ARGB. -LIBYUV_API -int RGB24ToARGB(const uint8* src_frame, int src_stride_frame, - uint8* dst_argb, int dst_stride_argb, - int width, int height); - -// RGB big endian (rgb in memory) to ARGB. -LIBYUV_API -int RAWToARGB(const uint8* src_frame, int src_stride_frame, - uint8* dst_argb, int dst_stride_argb, - int width, int height); - -// RGB16 (RGBP fourcc) little endian to ARGB. -LIBYUV_API -int RGB565ToARGB(const uint8* src_frame, int src_stride_frame, - uint8* dst_argb, int dst_stride_argb, - int width, int height); - -// RGB15 (RGBO fourcc) little endian to ARGB. -LIBYUV_API -int ARGB1555ToARGB(const uint8* src_frame, int src_stride_frame, - uint8* dst_argb, int dst_stride_argb, - int width, int height); - -// RGB12 (R444 fourcc) little endian to ARGB. -LIBYUV_API -int ARGB4444ToARGB(const uint8* src_frame, int src_stride_frame, - uint8* dst_argb, int dst_stride_argb, - int width, int height); - -#ifdef HAVE_JPEG -// src_width/height provided by capture -// dst_width/height for clipping determine final size. -LIBYUV_API -int MJPGToARGB(const uint8* sample, size_t sample_size, - uint8* dst_argb, int dst_stride_argb, - int src_width, int src_height, - int dst_width, int dst_height); -#endif - -// Convert camera sample to ARGB with cropping, rotation and vertical flip. -// "src_size" is needed to parse MJPG. -// "dst_stride_argb" number of bytes in a row of the dst_argb plane. -// Normally this would be the same as dst_width, with recommended alignment -// to 16 bytes for better efficiency. -// If rotation of 90 or 270 is used, stride is affected. The caller should -// allocate the I420 buffer according to rotation. -// "dst_stride_u" number of bytes in a row of the dst_u plane. -// Normally this would be the same as (dst_width + 1) / 2, with -// recommended alignment to 16 bytes for better efficiency. -// If rotation of 90 or 270 is used, stride is affected. -// "crop_x" and "crop_y" are starting position for cropping. -// To center, crop_x = (src_width - dst_width) / 2 -// crop_y = (src_height - dst_height) / 2 -// "src_width" / "src_height" is size of src_frame in pixels. -// "src_height" can be negative indicating a vertically flipped image source. -// "crop_width" / "crop_height" is the size to crop the src to. -// Must be less than or equal to src_width/src_height -// Cropping parameters are pre-rotation. -// "rotation" can be 0, 90, 180 or 270. -// "format" is a fourcc. ie 'I420', 'YUY2' -// Returns 0 for successful; -1 for invalid parameter. Non-zero for failure. -LIBYUV_API -int ConvertToARGB(const uint8* src_frame, size_t src_size, - uint8* dst_argb, int dst_stride_argb, - int crop_x, int crop_y, - int src_width, int src_height, - int crop_width, int crop_height, - enum RotationMode rotation, - uint32 format); - -#ifdef __cplusplus -} // extern "C" -} // namespace libyuv -#endif - -#endif // INCLUDE_LIBYUV_CONVERT_ARGB_H_ diff --git a/third_party/libyuv/include/libyuv/convert_from.h b/third_party/libyuv/include/libyuv/convert_from.h deleted file mode 100644 index 59c40474f1..0000000000 --- a/third_party/libyuv/include/libyuv/convert_from.h +++ /dev/null @@ -1,179 +0,0 @@ -/* - * Copyright 2011 The LibYuv Project Authors. All rights reserved. - * - * Use of this source code is governed by a BSD-style license - * that can be found in the LICENSE file in the root of the source - * tree. An additional intellectual property rights grant can be found - * in the file PATENTS. All contributing project authors may - * be found in the AUTHORS file in the root of the source tree. - */ - -#ifndef INCLUDE_LIBYUV_CONVERT_FROM_H_ -#define INCLUDE_LIBYUV_CONVERT_FROM_H_ - -#include "libyuv/basic_types.h" -#include "libyuv/rotate.h" - -#ifdef __cplusplus -namespace libyuv { -extern "C" { -#endif - -// See Also convert.h for conversions from formats to I420. - -// I420Copy in convert to I420ToI420. - -LIBYUV_API -int I420ToI422(const uint8* src_y, int src_stride_y, - const uint8* src_u, int src_stride_u, - const uint8* src_v, int src_stride_v, - uint8* dst_y, int dst_stride_y, - uint8* dst_u, int dst_stride_u, - uint8* dst_v, int dst_stride_v, - int width, int height); - -LIBYUV_API -int I420ToI444(const uint8* src_y, int src_stride_y, - const uint8* src_u, int src_stride_u, - const uint8* src_v, int src_stride_v, - uint8* dst_y, int dst_stride_y, - uint8* dst_u, int dst_stride_u, - uint8* dst_v, int dst_stride_v, - int width, int height); - -LIBYUV_API -int I420ToI411(const uint8* src_y, int src_stride_y, - const uint8* src_u, int src_stride_u, - const uint8* src_v, int src_stride_v, - uint8* dst_y, int dst_stride_y, - uint8* dst_u, int dst_stride_u, - uint8* dst_v, int dst_stride_v, - int width, int height); - -// Copy to I400. Source can be I420, I422, I444, I400, NV12 or NV21. -LIBYUV_API -int I400Copy(const uint8* src_y, int src_stride_y, - uint8* dst_y, int dst_stride_y, - int width, int height); - -LIBYUV_API -int I420ToNV12(const uint8* src_y, int src_stride_y, - const uint8* src_u, int src_stride_u, - const uint8* src_v, int src_stride_v, - uint8* dst_y, int dst_stride_y, - uint8* dst_uv, int dst_stride_uv, - int width, int height); - -LIBYUV_API -int I420ToNV21(const uint8* src_y, int src_stride_y, - const uint8* src_u, int src_stride_u, - const uint8* src_v, int src_stride_v, - uint8* dst_y, int dst_stride_y, - uint8* dst_vu, int dst_stride_vu, - int width, int height); - -LIBYUV_API -int I420ToYUY2(const uint8* src_y, int src_stride_y, - const uint8* src_u, int src_stride_u, - const uint8* src_v, int src_stride_v, - uint8* dst_frame, int dst_stride_frame, - int width, int height); - -LIBYUV_API -int I420ToUYVY(const uint8* src_y, int src_stride_y, - const uint8* src_u, int src_stride_u, - const uint8* src_v, int src_stride_v, - uint8* dst_frame, int dst_stride_frame, - int width, int height); - -LIBYUV_API -int I420ToARGB(const uint8* src_y, int src_stride_y, - const uint8* src_u, int src_stride_u, - const uint8* src_v, int src_stride_v, - uint8* dst_argb, int dst_stride_argb, - int width, int height); - -LIBYUV_API -int I420ToBGRA(const uint8* src_y, int src_stride_y, - const uint8* src_u, int src_stride_u, - const uint8* src_v, int src_stride_v, - uint8* dst_argb, int dst_stride_argb, - int width, int height); - -LIBYUV_API -int I420ToABGR(const uint8* src_y, int src_stride_y, - const uint8* src_u, int src_stride_u, - const uint8* src_v, int src_stride_v, - uint8* dst_argb, int dst_stride_argb, - int width, int height); - -LIBYUV_API -int I420ToRGBA(const uint8* src_y, int src_stride_y, - const uint8* src_u, int src_stride_u, - const uint8* src_v, int src_stride_v, - uint8* dst_rgba, int dst_stride_rgba, - int width, int height); - -LIBYUV_API -int I420ToRGB24(const uint8* src_y, int src_stride_y, - const uint8* src_u, int src_stride_u, - const uint8* src_v, int src_stride_v, - uint8* dst_frame, int dst_stride_frame, - int width, int height); - -LIBYUV_API -int I420ToRAW(const uint8* src_y, int src_stride_y, - const uint8* src_u, int src_stride_u, - const uint8* src_v, int src_stride_v, - uint8* dst_frame, int dst_stride_frame, - int width, int height); - -LIBYUV_API -int I420ToRGB565(const uint8* src_y, int src_stride_y, - const uint8* src_u, int src_stride_u, - const uint8* src_v, int src_stride_v, - uint8* dst_frame, int dst_stride_frame, - int width, int height); - -// Convert I420 To RGB565 with 4x4 dither matrix (16 bytes). -// Values in dither matrix from 0 to 7 recommended. -// The order of the dither matrix is first byte is upper left. - -LIBYUV_API -int I420ToRGB565Dither(const uint8* src_y, int src_stride_y, - const uint8* src_u, int src_stride_u, - const uint8* src_v, int src_stride_v, - uint8* dst_frame, int dst_stride_frame, - const uint8* dither4x4, int width, int height); - -LIBYUV_API -int I420ToARGB1555(const uint8* src_y, int src_stride_y, - const uint8* src_u, int src_stride_u, - const uint8* src_v, int src_stride_v, - uint8* dst_frame, int dst_stride_frame, - int width, int height); - -LIBYUV_API -int I420ToARGB4444(const uint8* src_y, int src_stride_y, - const uint8* src_u, int src_stride_u, - const uint8* src_v, int src_stride_v, - uint8* dst_frame, int dst_stride_frame, - int width, int height); - -// Convert I420 to specified format. -// "dst_sample_stride" is bytes in a row for the destination. Pass 0 if the -// buffer has contiguous rows. Can be negative. A multiple of 16 is optimal. -LIBYUV_API -int ConvertFromI420(const uint8* y, int y_stride, - const uint8* u, int u_stride, - const uint8* v, int v_stride, - uint8* dst_sample, int dst_sample_stride, - int width, int height, - uint32 format); - -#ifdef __cplusplus -} // extern "C" -} // namespace libyuv -#endif - -#endif // INCLUDE_LIBYUV_CONVERT_FROM_H_ diff --git a/third_party/libyuv/include/libyuv/convert_from_argb.h b/third_party/libyuv/include/libyuv/convert_from_argb.h deleted file mode 100644 index 8d7f02f8c4..0000000000 --- a/third_party/libyuv/include/libyuv/convert_from_argb.h +++ /dev/null @@ -1,190 +0,0 @@ -/* - * Copyright 2012 The LibYuv Project Authors. All rights reserved. - * - * Use of this source code is governed by a BSD-style license - * that can be found in the LICENSE file in the root of the source - * tree. An additional intellectual property rights grant can be found - * in the file PATENTS. All contributing project authors may - * be found in the AUTHORS file in the root of the source tree. - */ - -#ifndef INCLUDE_LIBYUV_CONVERT_FROM_ARGB_H_ -#define INCLUDE_LIBYUV_CONVERT_FROM_ARGB_H_ - -#include "libyuv/basic_types.h" - -#ifdef __cplusplus -namespace libyuv { -extern "C" { -#endif - -// Copy ARGB to ARGB. -#define ARGBToARGB ARGBCopy -LIBYUV_API -int ARGBCopy(const uint8* src_argb, int src_stride_argb, - uint8* dst_argb, int dst_stride_argb, - int width, int height); - -// Convert ARGB To BGRA. -LIBYUV_API -int ARGBToBGRA(const uint8* src_argb, int src_stride_argb, - uint8* dst_bgra, int dst_stride_bgra, - int width, int height); - -// Convert ARGB To ABGR. -LIBYUV_API -int ARGBToABGR(const uint8* src_argb, int src_stride_argb, - uint8* dst_abgr, int dst_stride_abgr, - int width, int height); - -// Convert ARGB To RGBA. -LIBYUV_API -int ARGBToRGBA(const uint8* src_argb, int src_stride_argb, - uint8* dst_rgba, int dst_stride_rgba, - int width, int height); - -// Convert ARGB To RGB24. -LIBYUV_API -int ARGBToRGB24(const uint8* src_argb, int src_stride_argb, - uint8* dst_rgb24, int dst_stride_rgb24, - int width, int height); - -// Convert ARGB To RAW. -LIBYUV_API -int ARGBToRAW(const uint8* src_argb, int src_stride_argb, - uint8* dst_rgb, int dst_stride_rgb, - int width, int height); - -// Convert ARGB To RGB565. -LIBYUV_API -int ARGBToRGB565(const uint8* src_argb, int src_stride_argb, - uint8* dst_rgb565, int dst_stride_rgb565, - int width, int height); - -// Convert ARGB To RGB565 with 4x4 dither matrix (16 bytes). -// Values in dither matrix from 0 to 7 recommended. -// The order of the dither matrix is first byte is upper left. -// TODO(fbarchard): Consider pointer to 2d array for dither4x4. -// const uint8(*dither)[4][4]; -LIBYUV_API -int ARGBToRGB565Dither(const uint8* src_argb, int src_stride_argb, - uint8* dst_rgb565, int dst_stride_rgb565, - const uint8* dither4x4, int width, int height); - -// Convert ARGB To ARGB1555. -LIBYUV_API -int ARGBToARGB1555(const uint8* src_argb, int src_stride_argb, - uint8* dst_argb1555, int dst_stride_argb1555, - int width, int height); - -// Convert ARGB To ARGB4444. -LIBYUV_API -int ARGBToARGB4444(const uint8* src_argb, int src_stride_argb, - uint8* dst_argb4444, int dst_stride_argb4444, - int width, int height); - -// Convert ARGB To I444. -LIBYUV_API -int ARGBToI444(const uint8* src_argb, int src_stride_argb, - uint8* dst_y, int dst_stride_y, - uint8* dst_u, int dst_stride_u, - uint8* dst_v, int dst_stride_v, - int width, int height); - -// Convert ARGB To I422. -LIBYUV_API -int ARGBToI422(const uint8* src_argb, int src_stride_argb, - uint8* dst_y, int dst_stride_y, - uint8* dst_u, int dst_stride_u, - uint8* dst_v, int dst_stride_v, - int width, int height); - -// Convert ARGB To I420. (also in convert.h) -LIBYUV_API -int ARGBToI420(const uint8* src_argb, int src_stride_argb, - uint8* dst_y, int dst_stride_y, - uint8* dst_u, int dst_stride_u, - uint8* dst_v, int dst_stride_v, - int width, int height); - -// Convert ARGB to J420. (JPeg full range I420). -LIBYUV_API -int ARGBToJ420(const uint8* src_argb, int src_stride_argb, - uint8* dst_yj, int dst_stride_yj, - uint8* dst_u, int dst_stride_u, - uint8* dst_v, int dst_stride_v, - int width, int height); - -// Convert ARGB to J422. -LIBYUV_API -int ARGBToJ422(const uint8* src_argb, int src_stride_argb, - uint8* dst_yj, int dst_stride_yj, - uint8* dst_u, int dst_stride_u, - uint8* dst_v, int dst_stride_v, - int width, int height); - -// Convert ARGB To I411. -LIBYUV_API -int ARGBToI411(const uint8* src_argb, int src_stride_argb, - uint8* dst_y, int dst_stride_y, - uint8* dst_u, int dst_stride_u, - uint8* dst_v, int dst_stride_v, - int width, int height); - -// Convert ARGB to J400. (JPeg full range). -LIBYUV_API -int ARGBToJ400(const uint8* src_argb, int src_stride_argb, - uint8* dst_yj, int dst_stride_yj, - int width, int height); - -// Convert ARGB to I400. -LIBYUV_API -int ARGBToI400(const uint8* src_argb, int src_stride_argb, - uint8* dst_y, int dst_stride_y, - int width, int height); - -// Convert ARGB to G. (Reverse of J400toARGB, which replicates G back to ARGB) -LIBYUV_API -int ARGBToG(const uint8* src_argb, int src_stride_argb, - uint8* dst_g, int dst_stride_g, - int width, int height); - -// Convert ARGB To NV12. -LIBYUV_API -int ARGBToNV12(const uint8* src_argb, int src_stride_argb, - uint8* dst_y, int dst_stride_y, - uint8* dst_uv, int dst_stride_uv, - int width, int height); - -// Convert ARGB To NV21. -LIBYUV_API -int ARGBToNV21(const uint8* src_argb, int src_stride_argb, - uint8* dst_y, int dst_stride_y, - uint8* dst_vu, int dst_stride_vu, - int width, int height); - -// Convert ARGB To NV21. -LIBYUV_API -int ARGBToNV21(const uint8* src_argb, int src_stride_argb, - uint8* dst_y, int dst_stride_y, - uint8* dst_vu, int dst_stride_vu, - int width, int height); - -// Convert ARGB To YUY2. -LIBYUV_API -int ARGBToYUY2(const uint8* src_argb, int src_stride_argb, - uint8* dst_yuy2, int dst_stride_yuy2, - int width, int height); - -// Convert ARGB To UYVY. -LIBYUV_API -int ARGBToUYVY(const uint8* src_argb, int src_stride_argb, - uint8* dst_uyvy, int dst_stride_uyvy, - int width, int height); - -#ifdef __cplusplus -} // extern "C" -} // namespace libyuv -#endif - -#endif // INCLUDE_LIBYUV_CONVERT_FROM_ARGB_H_ diff --git a/third_party/libyuv/include/libyuv/cpu_id.h b/third_party/libyuv/include/libyuv/cpu_id.h deleted file mode 100644 index 7c6c9aeb00..0000000000 --- a/third_party/libyuv/include/libyuv/cpu_id.h +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright 2011 The LibYuv Project Authors. All rights reserved. - * - * Use of this source code is governed by a BSD-style license - * that can be found in the LICENSE file in the root of the source - * tree. An additional intellectual property rights grant can be found - * in the file PATENTS. All contributing project authors may - * be found in the AUTHORS file in the root of the source tree. - */ - -#ifndef INCLUDE_LIBYUV_CPU_ID_H_ -#define INCLUDE_LIBYUV_CPU_ID_H_ - -#include "libyuv/basic_types.h" - -#ifdef __cplusplus -namespace libyuv { -extern "C" { -#endif - -// Internal flag to indicate cpuid requires initialization. -static const int kCpuInitialized = 0x1; - -// These flags are only valid on ARM processors. -static const int kCpuHasARM = 0x2; -static const int kCpuHasNEON = 0x4; -// 0x8 reserved for future ARM flag. - -// These flags are only valid on x86 processors. -static const int kCpuHasX86 = 0x10; -static const int kCpuHasSSE2 = 0x20; -static const int kCpuHasSSSE3 = 0x40; -static const int kCpuHasSSE41 = 0x80; -static const int kCpuHasSSE42 = 0x100; -static const int kCpuHasAVX = 0x200; -static const int kCpuHasAVX2 = 0x400; -static const int kCpuHasERMS = 0x800; -static const int kCpuHasFMA3 = 0x1000; -static const int kCpuHasAVX3 = 0x2000; -// 0x2000, 0x4000, 0x8000 reserved for future X86 flags. - -// These flags are only valid on MIPS processors. -static const int kCpuHasMIPS = 0x10000; -static const int kCpuHasDSPR2 = 0x20000; -static const int kCpuHasMSA = 0x40000; - -// Internal function used to auto-init. -LIBYUV_API -int InitCpuFlags(void); - -// Internal function for parsing /proc/cpuinfo. -LIBYUV_API -int ArmCpuCaps(const char* cpuinfo_name); - -// Detect CPU has SSE2 etc. -// Test_flag parameter should be one of kCpuHas constants above. -// returns non-zero if instruction set is detected -static __inline int TestCpuFlag(int test_flag) { - LIBYUV_API extern int cpu_info_; - return (!cpu_info_ ? InitCpuFlags() : cpu_info_) & test_flag; -} - -// For testing, allow CPU flags to be disabled. -// ie MaskCpuFlags(~kCpuHasSSSE3) to disable SSSE3. -// MaskCpuFlags(-1) to enable all cpu specific optimizations. -// MaskCpuFlags(1) to disable all cpu specific optimizations. -LIBYUV_API -void MaskCpuFlags(int enable_flags); - -// Low level cpuid for X86. Returns zeros on other CPUs. -// eax is the info type that you want. -// ecx is typically the cpu number, and should normally be zero. -LIBYUV_API -void CpuId(uint32 eax, uint32 ecx, uint32* cpu_info); - -#ifdef __cplusplus -} // extern "C" -} // namespace libyuv -#endif - -#endif // INCLUDE_LIBYUV_CPU_ID_H_ diff --git a/third_party/libyuv/include/libyuv/macros_msa.h b/third_party/libyuv/include/libyuv/macros_msa.h deleted file mode 100644 index 92ed21c385..0000000000 --- a/third_party/libyuv/include/libyuv/macros_msa.h +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright 2016 The LibYuv Project Authors. All rights reserved. - * - * Use of this source code is governed by a BSD-style license - * that can be found in the LICENSE file in the root of the source - * tree. An additional intellectual property rights grant can be found - * in the file PATENTS. All contributing project authors may - * be found in the AUTHORS file in the root of the source tree. - */ - -#ifndef INCLUDE_LIBYUV_MACROS_MSA_H_ -#define INCLUDE_LIBYUV_MACROS_MSA_H_ - -#if !defined(LIBYUV_DISABLE_MSA) && defined(__mips_msa) -#include -#include - -#define LD_B(RTYPE, psrc) *((RTYPE*)(psrc)) /* NOLINT */ -#define LD_UB(...) LD_B(v16u8, __VA_ARGS__) - -#define ST_B(RTYPE, in, pdst) *((RTYPE*)(pdst)) = (in) /* NOLINT */ -#define ST_UB(...) ST_B(v16u8, __VA_ARGS__) - -/* Description : Load two vectors with 16 'byte' sized elements - Arguments : Inputs - psrc, stride - Outputs - out0, out1 - Return Type - as per RTYPE - Details : Load 16 byte elements in 'out0' from (psrc) - Load 16 byte elements in 'out1' from (psrc + stride) -*/ -#define LD_B2(RTYPE, psrc, stride, out0, out1) { \ - out0 = LD_B(RTYPE, (psrc)); \ - out1 = LD_B(RTYPE, (psrc) + stride); \ -} -#define LD_UB2(...) LD_B2(v16u8, __VA_ARGS__) - -#define LD_B4(RTYPE, psrc, stride, out0, out1, out2, out3) { \ - LD_B2(RTYPE, (psrc), stride, out0, out1); \ - LD_B2(RTYPE, (psrc) + 2 * stride , stride, out2, out3); \ -} -#define LD_UB4(...) LD_B4(v16u8, __VA_ARGS__) - -/* Description : Store two vectors with stride each having 16 'byte' sized - elements - Arguments : Inputs - in0, in1, pdst, stride - Details : Store 16 byte elements from 'in0' to (pdst) - Store 16 byte elements from 'in1' to (pdst + stride) -*/ -#define ST_B2(RTYPE, in0, in1, pdst, stride) { \ - ST_B(RTYPE, in0, (pdst)); \ - ST_B(RTYPE, in1, (pdst) + stride); \ -} -#define ST_UB2(...) ST_B2(v16u8, __VA_ARGS__) -# -#define ST_B4(RTYPE, in0, in1, in2, in3, pdst, stride) { \ - ST_B2(RTYPE, in0, in1, (pdst), stride); \ - ST_B2(RTYPE, in2, in3, (pdst) + 2 * stride, stride); \ -} -#define ST_UB4(...) ST_B4(v16u8, __VA_ARGS__) -# -/* Description : Shuffle byte vector elements as per mask vector - Arguments : Inputs - in0, in1, in2, in3, mask0, mask1 - Outputs - out0, out1 - Return Type - as per RTYPE - Details : Byte elements from 'in0' & 'in1' are copied selectively to - 'out0' as per control vector 'mask0' -*/ -#define VSHF_B2(RTYPE, in0, in1, in2, in3, mask0, mask1, out0, out1) { \ - out0 = (RTYPE) __msa_vshf_b((v16i8) mask0, (v16i8) in1, (v16i8) in0); \ - out1 = (RTYPE) __msa_vshf_b((v16i8) mask1, (v16i8) in3, (v16i8) in2); \ -} -#define VSHF_B2_UB(...) VSHF_B2(v16u8, __VA_ARGS__) - -#endif /* !defined(LIBYUV_DISABLE_MSA) && defined(__mips_msa) */ - -#endif // INCLUDE_LIBYUV_MACROS_MSA_H_ diff --git a/third_party/libyuv/include/libyuv/mjpeg_decoder.h b/third_party/libyuv/include/libyuv/mjpeg_decoder.h deleted file mode 100644 index 4975bae5b7..0000000000 --- a/third_party/libyuv/include/libyuv/mjpeg_decoder.h +++ /dev/null @@ -1,192 +0,0 @@ -/* - * Copyright 2012 The LibYuv Project Authors. All rights reserved. - * - * Use of this source code is governed by a BSD-style license - * that can be found in the LICENSE file in the root of the source - * tree. An additional intellectual property rights grant can be found - * in the file PATENTS. All contributing project authors may - * be found in the AUTHORS file in the root of the source tree. - */ - -#ifndef INCLUDE_LIBYUV_MJPEG_DECODER_H_ -#define INCLUDE_LIBYUV_MJPEG_DECODER_H_ - -#include "libyuv/basic_types.h" - -#ifdef __cplusplus -// NOTE: For a simplified public API use convert.h MJPGToI420(). - -struct jpeg_common_struct; -struct jpeg_decompress_struct; -struct jpeg_source_mgr; - -namespace libyuv { - -#ifdef __cplusplus -extern "C" { -#endif - -LIBYUV_BOOL ValidateJpeg(const uint8* sample, size_t sample_size); - -#ifdef __cplusplus -} // extern "C" -#endif - -static const uint32 kUnknownDataSize = 0xFFFFFFFF; - -enum JpegSubsamplingType { - kJpegYuv420, - kJpegYuv422, - kJpegYuv411, - kJpegYuv444, - kJpegYuv400, - kJpegUnknown -}; - -struct Buffer { - const uint8* data; - int len; -}; - -struct BufferVector { - Buffer* buffers; - int len; - int pos; -}; - -struct SetJmpErrorMgr; - -// MJPEG ("Motion JPEG") is a pseudo-standard video codec where the frames are -// simply independent JPEG images with a fixed huffman table (which is omitted). -// It is rarely used in video transmission, but is common as a camera capture -// format, especially in Logitech devices. This class implements a decoder for -// MJPEG frames. -// -// See http://tools.ietf.org/html/rfc2435 -class LIBYUV_API MJpegDecoder { - public: - typedef void (*CallbackFunction)(void* opaque, - const uint8* const* data, - const int* strides, - int rows); - - static const int kColorSpaceUnknown; - static const int kColorSpaceGrayscale; - static const int kColorSpaceRgb; - static const int kColorSpaceYCbCr; - static const int kColorSpaceCMYK; - static const int kColorSpaceYCCK; - - MJpegDecoder(); - ~MJpegDecoder(); - - // Loads a new frame, reads its headers, and determines the uncompressed - // image format. - // Returns LIBYUV_TRUE if image looks valid and format is supported. - // If return value is LIBYUV_TRUE, then the values for all the following - // getters are populated. - // src_len is the size of the compressed mjpeg frame in bytes. - LIBYUV_BOOL LoadFrame(const uint8* src, size_t src_len); - - // Returns width of the last loaded frame in pixels. - int GetWidth(); - - // Returns height of the last loaded frame in pixels. - int GetHeight(); - - // Returns format of the last loaded frame. The return value is one of the - // kColorSpace* constants. - int GetColorSpace(); - - // Number of color components in the color space. - int GetNumComponents(); - - // Sample factors of the n-th component. - int GetHorizSampFactor(int component); - - int GetVertSampFactor(int component); - - int GetHorizSubSampFactor(int component); - - int GetVertSubSampFactor(int component); - - // Public for testability. - int GetImageScanlinesPerImcuRow(); - - // Public for testability. - int GetComponentScanlinesPerImcuRow(int component); - - // Width of a component in bytes. - int GetComponentWidth(int component); - - // Height of a component. - int GetComponentHeight(int component); - - // Width of a component in bytes with padding for DCTSIZE. Public for testing. - int GetComponentStride(int component); - - // Size of a component in bytes. - int GetComponentSize(int component); - - // Call this after LoadFrame() if you decide you don't want to decode it - // after all. - LIBYUV_BOOL UnloadFrame(); - - // Decodes the entire image into a one-buffer-per-color-component format. - // dst_width must match exactly. dst_height must be <= to image height; if - // less, the image is cropped. "planes" must have size equal to at least - // GetNumComponents() and they must point to non-overlapping buffers of size - // at least GetComponentSize(i). The pointers in planes are incremented - // to point to after the end of the written data. - // TODO(fbarchard): Add dst_x, dst_y to allow specific rect to be decoded. - LIBYUV_BOOL DecodeToBuffers(uint8** planes, int dst_width, int dst_height); - - // Decodes the entire image and passes the data via repeated calls to a - // callback function. Each call will get the data for a whole number of - // image scanlines. - // TODO(fbarchard): Add dst_x, dst_y to allow specific rect to be decoded. - LIBYUV_BOOL DecodeToCallback(CallbackFunction fn, void* opaque, - int dst_width, int dst_height); - - // The helper function which recognizes the jpeg sub-sampling type. - static JpegSubsamplingType JpegSubsamplingTypeHelper( - int* subsample_x, int* subsample_y, int number_of_components); - - private: - void AllocOutputBuffers(int num_outbufs); - void DestroyOutputBuffers(); - - LIBYUV_BOOL StartDecode(); - LIBYUV_BOOL FinishDecode(); - - void SetScanlinePointers(uint8** data); - LIBYUV_BOOL DecodeImcuRow(); - - int GetComponentScanlinePadding(int component); - - // A buffer holding the input data for a frame. - Buffer buf_; - BufferVector buf_vec_; - - jpeg_decompress_struct* decompress_struct_; - jpeg_source_mgr* source_mgr_; - SetJmpErrorMgr* error_mgr_; - - // LIBYUV_TRUE iff at least one component has scanline padding. (i.e., - // GetComponentScanlinePadding() != 0.) - LIBYUV_BOOL has_scanline_padding_; - - // Temporaries used to point to scanline outputs. - int num_outbufs_; // Outermost size of all arrays below. - uint8*** scanlines_; - int* scanlines_sizes_; - // Temporary buffer used for decoding when we can't decode directly to the - // output buffers. Large enough for just one iMCU row. - uint8** databuf_; - int* databuf_strides_; -}; - -} // namespace libyuv - -#endif // __cplusplus -#endif // INCLUDE_LIBYUV_MJPEG_DECODER_H_ diff --git a/third_party/libyuv/include/libyuv/planar_functions.h b/third_party/libyuv/include/libyuv/planar_functions.h deleted file mode 100644 index 1b57b29261..0000000000 --- a/third_party/libyuv/include/libyuv/planar_functions.h +++ /dev/null @@ -1,529 +0,0 @@ -/* - * Copyright 2011 The LibYuv Project Authors. All rights reserved. - * - * Use of this source code is governed by a BSD-style license - * that can be found in the LICENSE file in the root of the source - * tree. An additional intellectual property rights grant can be found - * in the file PATENTS. All contributing project authors may - * be found in the AUTHORS file in the root of the source tree. - */ - -#ifndef INCLUDE_LIBYUV_PLANAR_FUNCTIONS_H_ -#define INCLUDE_LIBYUV_PLANAR_FUNCTIONS_H_ - -#include "libyuv/basic_types.h" - -// TODO(fbarchard): Remove the following headers includes. -#include "libyuv/convert.h" -#include "libyuv/convert_argb.h" - -#ifdef __cplusplus -namespace libyuv { -extern "C" { -#endif - -// Copy a plane of data. -LIBYUV_API -void CopyPlane(const uint8* src_y, int src_stride_y, - uint8* dst_y, int dst_stride_y, - int width, int height); - -LIBYUV_API -void CopyPlane_16(const uint16* src_y, int src_stride_y, - uint16* dst_y, int dst_stride_y, - int width, int height); - -// Set a plane of data to a 32 bit value. -LIBYUV_API -void SetPlane(uint8* dst_y, int dst_stride_y, - int width, int height, - uint32 value); - -// Split interleaved UV plane into separate U and V planes. -LIBYUV_API -void SplitUVPlane(const uint8* src_uv, int src_stride_uv, - uint8* dst_u, int dst_stride_u, - uint8* dst_v, int dst_stride_v, - int width, int height); - -// Merge separate U and V planes into one interleaved UV plane. -LIBYUV_API -void MergeUVPlane(const uint8* src_u, int src_stride_u, - const uint8* src_v, int src_stride_v, - uint8* dst_uv, int dst_stride_uv, - int width, int height); - -// Copy I400. Supports inverting. -LIBYUV_API -int I400ToI400(const uint8* src_y, int src_stride_y, - uint8* dst_y, int dst_stride_y, - int width, int height); - -#define J400ToJ400 I400ToI400 - -// Copy I422 to I422. -#define I422ToI422 I422Copy -LIBYUV_API -int I422Copy(const uint8* src_y, int src_stride_y, - const uint8* src_u, int src_stride_u, - const uint8* src_v, int src_stride_v, - uint8* dst_y, int dst_stride_y, - uint8* dst_u, int dst_stride_u, - uint8* dst_v, int dst_stride_v, - int width, int height); - -// Copy I444 to I444. -#define I444ToI444 I444Copy -LIBYUV_API -int I444Copy(const uint8* src_y, int src_stride_y, - const uint8* src_u, int src_stride_u, - const uint8* src_v, int src_stride_v, - uint8* dst_y, int dst_stride_y, - uint8* dst_u, int dst_stride_u, - uint8* dst_v, int dst_stride_v, - int width, int height); - -// Convert YUY2 to I422. -LIBYUV_API -int YUY2ToI422(const uint8* src_yuy2, int src_stride_yuy2, - uint8* dst_y, int dst_stride_y, - uint8* dst_u, int dst_stride_u, - uint8* dst_v, int dst_stride_v, - int width, int height); - -// Convert UYVY to I422. -LIBYUV_API -int UYVYToI422(const uint8* src_uyvy, int src_stride_uyvy, - uint8* dst_y, int dst_stride_y, - uint8* dst_u, int dst_stride_u, - uint8* dst_v, int dst_stride_v, - int width, int height); - -LIBYUV_API -int YUY2ToNV12(const uint8* src_yuy2, int src_stride_yuy2, - uint8* dst_y, int dst_stride_y, - uint8* dst_uv, int dst_stride_uv, - int width, int height); - -LIBYUV_API -int UYVYToNV12(const uint8* src_uyvy, int src_stride_uyvy, - uint8* dst_y, int dst_stride_y, - uint8* dst_uv, int dst_stride_uv, - int width, int height); - -// Convert I420 to I400. (calls CopyPlane ignoring u/v). -LIBYUV_API -int I420ToI400(const uint8* src_y, int src_stride_y, - const uint8* src_u, int src_stride_u, - const uint8* src_v, int src_stride_v, - uint8* dst_y, int dst_stride_y, - int width, int height); - -// Alias -#define J420ToJ400 I420ToI400 -#define I420ToI420Mirror I420Mirror - -// I420 mirror. -LIBYUV_API -int I420Mirror(const uint8* src_y, int src_stride_y, - const uint8* src_u, int src_stride_u, - const uint8* src_v, int src_stride_v, - uint8* dst_y, int dst_stride_y, - uint8* dst_u, int dst_stride_u, - uint8* dst_v, int dst_stride_v, - int width, int height); - -// Alias -#define I400ToI400Mirror I400Mirror - -// I400 mirror. A single plane is mirrored horizontally. -// Pass negative height to achieve 180 degree rotation. -LIBYUV_API -int I400Mirror(const uint8* src_y, int src_stride_y, - uint8* dst_y, int dst_stride_y, - int width, int height); - -// Alias -#define ARGBToARGBMirror ARGBMirror - -// ARGB mirror. -LIBYUV_API -int ARGBMirror(const uint8* src_argb, int src_stride_argb, - uint8* dst_argb, int dst_stride_argb, - int width, int height); - -// Convert NV12 to RGB565. -LIBYUV_API -int NV12ToRGB565(const uint8* src_y, int src_stride_y, - const uint8* src_uv, int src_stride_uv, - uint8* dst_rgb565, int dst_stride_rgb565, - int width, int height); - -// I422ToARGB is in convert_argb.h -// Convert I422 to BGRA. -LIBYUV_API -int I422ToBGRA(const uint8* src_y, int src_stride_y, - const uint8* src_u, int src_stride_u, - const uint8* src_v, int src_stride_v, - uint8* dst_bgra, int dst_stride_bgra, - int width, int height); - -// Convert I422 to ABGR. -LIBYUV_API -int I422ToABGR(const uint8* src_y, int src_stride_y, - const uint8* src_u, int src_stride_u, - const uint8* src_v, int src_stride_v, - uint8* dst_abgr, int dst_stride_abgr, - int width, int height); - -// Convert I422 to RGBA. -LIBYUV_API -int I422ToRGBA(const uint8* src_y, int src_stride_y, - const uint8* src_u, int src_stride_u, - const uint8* src_v, int src_stride_v, - uint8* dst_rgba, int dst_stride_rgba, - int width, int height); - -// Alias -#define RGB24ToRAW RAWToRGB24 - -LIBYUV_API -int RAWToRGB24(const uint8* src_raw, int src_stride_raw, - uint8* dst_rgb24, int dst_stride_rgb24, - int width, int height); - -// Draw a rectangle into I420. -LIBYUV_API -int I420Rect(uint8* dst_y, int dst_stride_y, - uint8* dst_u, int dst_stride_u, - uint8* dst_v, int dst_stride_v, - int x, int y, int width, int height, - int value_y, int value_u, int value_v); - -// Draw a rectangle into ARGB. -LIBYUV_API -int ARGBRect(uint8* dst_argb, int dst_stride_argb, - int x, int y, int width, int height, uint32 value); - -// Convert ARGB to gray scale ARGB. -LIBYUV_API -int ARGBGrayTo(const uint8* src_argb, int src_stride_argb, - uint8* dst_argb, int dst_stride_argb, - int width, int height); - -// Make a rectangle of ARGB gray scale. -LIBYUV_API -int ARGBGray(uint8* dst_argb, int dst_stride_argb, - int x, int y, int width, int height); - -// Make a rectangle of ARGB Sepia tone. -LIBYUV_API -int ARGBSepia(uint8* dst_argb, int dst_stride_argb, - int x, int y, int width, int height); - -// Apply a matrix rotation to each ARGB pixel. -// matrix_argb is 4 signed ARGB values. -128 to 127 representing -2 to 2. -// The first 4 coefficients apply to B, G, R, A and produce B of the output. -// The next 4 coefficients apply to B, G, R, A and produce G of the output. -// The next 4 coefficients apply to B, G, R, A and produce R of the output. -// The last 4 coefficients apply to B, G, R, A and produce A of the output. -LIBYUV_API -int ARGBColorMatrix(const uint8* src_argb, int src_stride_argb, - uint8* dst_argb, int dst_stride_argb, - const int8* matrix_argb, - int width, int height); - -// Deprecated. Use ARGBColorMatrix instead. -// Apply a matrix rotation to each ARGB pixel. -// matrix_argb is 3 signed ARGB values. -128 to 127 representing -1 to 1. -// The first 4 coefficients apply to B, G, R, A and produce B of the output. -// The next 4 coefficients apply to B, G, R, A and produce G of the output. -// The last 4 coefficients apply to B, G, R, A and produce R of the output. -LIBYUV_API -int RGBColorMatrix(uint8* dst_argb, int dst_stride_argb, - const int8* matrix_rgb, - int x, int y, int width, int height); - -// Apply a color table each ARGB pixel. -// Table contains 256 ARGB values. -LIBYUV_API -int ARGBColorTable(uint8* dst_argb, int dst_stride_argb, - const uint8* table_argb, - int x, int y, int width, int height); - -// Apply a color table each ARGB pixel but preserve destination alpha. -// Table contains 256 ARGB values. -LIBYUV_API -int RGBColorTable(uint8* dst_argb, int dst_stride_argb, - const uint8* table_argb, - int x, int y, int width, int height); - -// Apply a luma/color table each ARGB pixel but preserve destination alpha. -// Table contains 32768 values indexed by [Y][C] where 7 it 7 bit luma from -// RGB (YJ style) and C is an 8 bit color component (R, G or B). -LIBYUV_API -int ARGBLumaColorTable(const uint8* src_argb, int src_stride_argb, - uint8* dst_argb, int dst_stride_argb, - const uint8* luma_rgb_table, - int width, int height); - -// Apply a 3 term polynomial to ARGB values. -// poly points to a 4x4 matrix. The first row is constants. The 2nd row is -// coefficients for b, g, r and a. The 3rd row is coefficients for b squared, -// g squared, r squared and a squared. The 4rd row is coefficients for b to -// the 3, g to the 3, r to the 3 and a to the 3. The values are summed and -// result clamped to 0 to 255. -// A polynomial approximation can be dirived using software such as 'R'. - -LIBYUV_API -int ARGBPolynomial(const uint8* src_argb, int src_stride_argb, - uint8* dst_argb, int dst_stride_argb, - const float* poly, - int width, int height); - -// Convert plane of 16 bit shorts to half floats. -// Source values are multiplied by scale before storing as half float. -LIBYUV_API -int HalfFloatPlane(const uint16* src_y, int src_stride_y, - uint16* dst_y, int dst_stride_y, - float scale, - int width, int height); - -// Quantize a rectangle of ARGB. Alpha unaffected. -// scale is a 16 bit fractional fixed point scaler between 0 and 65535. -// interval_size should be a value between 1 and 255. -// interval_offset should be a value between 0 and 255. -LIBYUV_API -int ARGBQuantize(uint8* dst_argb, int dst_stride_argb, - int scale, int interval_size, int interval_offset, - int x, int y, int width, int height); - -// Copy ARGB to ARGB. -LIBYUV_API -int ARGBCopy(const uint8* src_argb, int src_stride_argb, - uint8* dst_argb, int dst_stride_argb, - int width, int height); - -// Copy Alpha channel of ARGB to alpha of ARGB. -LIBYUV_API -int ARGBCopyAlpha(const uint8* src_argb, int src_stride_argb, - uint8* dst_argb, int dst_stride_argb, - int width, int height); - -// Extract the alpha channel from ARGB. -LIBYUV_API -int ARGBExtractAlpha(const uint8* src_argb, int src_stride_argb, - uint8* dst_a, int dst_stride_a, - int width, int height); - -// Copy Y channel to Alpha of ARGB. -LIBYUV_API -int ARGBCopyYToAlpha(const uint8* src_y, int src_stride_y, - uint8* dst_argb, int dst_stride_argb, - int width, int height); - -typedef void (*ARGBBlendRow)(const uint8* src_argb0, const uint8* src_argb1, - uint8* dst_argb, int width); - -// Get function to Alpha Blend ARGB pixels and store to destination. -LIBYUV_API -ARGBBlendRow GetARGBBlend(); - -// Alpha Blend ARGB images and store to destination. -// Source is pre-multiplied by alpha using ARGBAttenuate. -// Alpha of destination is set to 255. -LIBYUV_API -int ARGBBlend(const uint8* src_argb0, int src_stride_argb0, - const uint8* src_argb1, int src_stride_argb1, - uint8* dst_argb, int dst_stride_argb, - int width, int height); - -// Alpha Blend plane and store to destination. -// Source is not pre-multiplied by alpha. -LIBYUV_API -int BlendPlane(const uint8* src_y0, int src_stride_y0, - const uint8* src_y1, int src_stride_y1, - const uint8* alpha, int alpha_stride, - uint8* dst_y, int dst_stride_y, - int width, int height); - -// Alpha Blend YUV images and store to destination. -// Source is not pre-multiplied by alpha. -// Alpha is full width x height and subsampled to half size to apply to UV. -LIBYUV_API -int I420Blend(const uint8* src_y0, int src_stride_y0, - const uint8* src_u0, int src_stride_u0, - const uint8* src_v0, int src_stride_v0, - const uint8* src_y1, int src_stride_y1, - const uint8* src_u1, int src_stride_u1, - const uint8* src_v1, int src_stride_v1, - const uint8* alpha, int alpha_stride, - uint8* dst_y, int dst_stride_y, - uint8* dst_u, int dst_stride_u, - uint8* dst_v, int dst_stride_v, - int width, int height); - -// Multiply ARGB image by ARGB image. Shifted down by 8. Saturates to 255. -LIBYUV_API -int ARGBMultiply(const uint8* src_argb0, int src_stride_argb0, - const uint8* src_argb1, int src_stride_argb1, - uint8* dst_argb, int dst_stride_argb, - int width, int height); - -// Add ARGB image with ARGB image. Saturates to 255. -LIBYUV_API -int ARGBAdd(const uint8* src_argb0, int src_stride_argb0, - const uint8* src_argb1, int src_stride_argb1, - uint8* dst_argb, int dst_stride_argb, - int width, int height); - -// Subtract ARGB image (argb1) from ARGB image (argb0). Saturates to 0. -LIBYUV_API -int ARGBSubtract(const uint8* src_argb0, int src_stride_argb0, - const uint8* src_argb1, int src_stride_argb1, - uint8* dst_argb, int dst_stride_argb, - int width, int height); - -// Convert I422 to YUY2. -LIBYUV_API -int I422ToYUY2(const uint8* src_y, int src_stride_y, - const uint8* src_u, int src_stride_u, - const uint8* src_v, int src_stride_v, - uint8* dst_frame, int dst_stride_frame, - int width, int height); - -// Convert I422 to UYVY. -LIBYUV_API -int I422ToUYVY(const uint8* src_y, int src_stride_y, - const uint8* src_u, int src_stride_u, - const uint8* src_v, int src_stride_v, - uint8* dst_frame, int dst_stride_frame, - int width, int height); - -// Convert unattentuated ARGB to preattenuated ARGB. -LIBYUV_API -int ARGBAttenuate(const uint8* src_argb, int src_stride_argb, - uint8* dst_argb, int dst_stride_argb, - int width, int height); - -// Convert preattentuated ARGB to unattenuated ARGB. -LIBYUV_API -int ARGBUnattenuate(const uint8* src_argb, int src_stride_argb, - uint8* dst_argb, int dst_stride_argb, - int width, int height); - -// Internal function - do not call directly. -// Computes table of cumulative sum for image where the value is the sum -// of all values above and to the left of the entry. Used by ARGBBlur. -LIBYUV_API -int ARGBComputeCumulativeSum(const uint8* src_argb, int src_stride_argb, - int32* dst_cumsum, int dst_stride32_cumsum, - int width, int height); - -// Blur ARGB image. -// dst_cumsum table of width * (height + 1) * 16 bytes aligned to -// 16 byte boundary. -// dst_stride32_cumsum is number of ints in a row (width * 4). -// radius is number of pixels around the center. e.g. 1 = 3x3. 2=5x5. -// Blur is optimized for radius of 5 (11x11) or less. -LIBYUV_API -int ARGBBlur(const uint8* src_argb, int src_stride_argb, - uint8* dst_argb, int dst_stride_argb, - int32* dst_cumsum, int dst_stride32_cumsum, - int width, int height, int radius); - -// Multiply ARGB image by ARGB value. -LIBYUV_API -int ARGBShade(const uint8* src_argb, int src_stride_argb, - uint8* dst_argb, int dst_stride_argb, - int width, int height, uint32 value); - -// Interpolate between two images using specified amount of interpolation -// (0 to 255) and store to destination. -// 'interpolation' is specified as 8 bit fraction where 0 means 100% src0 -// and 255 means 1% src0 and 99% src1. -LIBYUV_API -int InterpolatePlane(const uint8* src0, int src_stride0, - const uint8* src1, int src_stride1, - uint8* dst, int dst_stride, - int width, int height, int interpolation); - -// Interpolate between two ARGB images using specified amount of interpolation -// Internally calls InterpolatePlane with width * 4 (bpp). -LIBYUV_API -int ARGBInterpolate(const uint8* src_argb0, int src_stride_argb0, - const uint8* src_argb1, int src_stride_argb1, - uint8* dst_argb, int dst_stride_argb, - int width, int height, int interpolation); - -// Interpolate between two YUV images using specified amount of interpolation -// Internally calls InterpolatePlane on each plane where the U and V planes -// are half width and half height. -LIBYUV_API -int I420Interpolate(const uint8* src0_y, int src0_stride_y, - const uint8* src0_u, int src0_stride_u, - const uint8* src0_v, int src0_stride_v, - const uint8* src1_y, int src1_stride_y, - const uint8* src1_u, int src1_stride_u, - const uint8* src1_v, int src1_stride_v, - uint8* dst_y, int dst_stride_y, - uint8* dst_u, int dst_stride_u, - uint8* dst_v, int dst_stride_v, - int width, int height, int interpolation); - -#if defined(__pnacl__) || defined(__CLR_VER) || \ - (defined(__i386__) && !defined(__SSE2__)) -#define LIBYUV_DISABLE_X86 -#endif -// MemorySanitizer does not support assembly code yet. http://crbug.com/344505 -#if defined(__has_feature) -#if __has_feature(memory_sanitizer) -#define LIBYUV_DISABLE_X86 -#endif -#endif -// The following are available on all x86 platforms: -#if !defined(LIBYUV_DISABLE_X86) && \ - (defined(_M_IX86) || defined(__x86_64__) || defined(__i386__)) -#define HAS_ARGBAFFINEROW_SSE2 -#endif - -// Row function for copying pixels from a source with a slope to a row -// of destination. Useful for scaling, rotation, mirror, texture mapping. -LIBYUV_API -void ARGBAffineRow_C(const uint8* src_argb, int src_argb_stride, - uint8* dst_argb, const float* uv_dudv, int width); -LIBYUV_API -void ARGBAffineRow_SSE2(const uint8* src_argb, int src_argb_stride, - uint8* dst_argb, const float* uv_dudv, int width); - -// Shuffle ARGB channel order. e.g. BGRA to ARGB. -// shuffler is 16 bytes and must be aligned. -LIBYUV_API -int ARGBShuffle(const uint8* src_bgra, int src_stride_bgra, - uint8* dst_argb, int dst_stride_argb, - const uint8* shuffler, int width, int height); - -// Sobel ARGB effect with planar output. -LIBYUV_API -int ARGBSobelToPlane(const uint8* src_argb, int src_stride_argb, - uint8* dst_y, int dst_stride_y, - int width, int height); - -// Sobel ARGB effect. -LIBYUV_API -int ARGBSobel(const uint8* src_argb, int src_stride_argb, - uint8* dst_argb, int dst_stride_argb, - int width, int height); - -// Sobel ARGB effect w/ Sobel X, Sobel, Sobel Y in ARGB. -LIBYUV_API -int ARGBSobelXY(const uint8* src_argb, int src_stride_argb, - uint8* dst_argb, int dst_stride_argb, - int width, int height); - -#ifdef __cplusplus -} // extern "C" -} // namespace libyuv -#endif - -#endif // INCLUDE_LIBYUV_PLANAR_FUNCTIONS_H_ diff --git a/third_party/libyuv/include/libyuv/rotate.h b/third_party/libyuv/include/libyuv/rotate.h deleted file mode 100644 index 8a2da9a5aa..0000000000 --- a/third_party/libyuv/include/libyuv/rotate.h +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright 2011 The LibYuv Project Authors. All rights reserved. - * - * Use of this source code is governed by a BSD-style license - * that can be found in the LICENSE file in the root of the source - * tree. An additional intellectual property rights grant can be found - * in the file PATENTS. All contributing project authors may - * be found in the AUTHORS file in the root of the source tree. - */ - -#ifndef INCLUDE_LIBYUV_ROTATE_H_ -#define INCLUDE_LIBYUV_ROTATE_H_ - -#include "libyuv/basic_types.h" - -#ifdef __cplusplus -namespace libyuv { -extern "C" { -#endif - -// Supported rotation. -typedef enum RotationMode { - kRotate0 = 0, // No rotation. - kRotate90 = 90, // Rotate 90 degrees clockwise. - kRotate180 = 180, // Rotate 180 degrees. - kRotate270 = 270, // Rotate 270 degrees clockwise. - - // Deprecated. - kRotateNone = 0, - kRotateClockwise = 90, - kRotateCounterClockwise = 270, -} RotationModeEnum; - -// Rotate I420 frame. -LIBYUV_API -int I420Rotate(const uint8* src_y, int src_stride_y, - const uint8* src_u, int src_stride_u, - const uint8* src_v, int src_stride_v, - uint8* dst_y, int dst_stride_y, - uint8* dst_u, int dst_stride_u, - uint8* dst_v, int dst_stride_v, - int src_width, int src_height, enum RotationMode mode); - -// Rotate NV12 input and store in I420. -LIBYUV_API -int NV12ToI420Rotate(const uint8* src_y, int src_stride_y, - const uint8* src_uv, int src_stride_uv, - uint8* dst_y, int dst_stride_y, - uint8* dst_u, int dst_stride_u, - uint8* dst_v, int dst_stride_v, - int src_width, int src_height, enum RotationMode mode); - -// Rotate a plane by 0, 90, 180, or 270. -LIBYUV_API -int RotatePlane(const uint8* src, int src_stride, - uint8* dst, int dst_stride, - int src_width, int src_height, enum RotationMode mode); - -// Rotate planes by 90, 180, 270. Deprecated. -LIBYUV_API -void RotatePlane90(const uint8* src, int src_stride, - uint8* dst, int dst_stride, - int width, int height); - -LIBYUV_API -void RotatePlane180(const uint8* src, int src_stride, - uint8* dst, int dst_stride, - int width, int height); - -LIBYUV_API -void RotatePlane270(const uint8* src, int src_stride, - uint8* dst, int dst_stride, - int width, int height); - -LIBYUV_API -void RotateUV90(const uint8* src, int src_stride, - uint8* dst_a, int dst_stride_a, - uint8* dst_b, int dst_stride_b, - int width, int height); - -// Rotations for when U and V are interleaved. -// These functions take one input pointer and -// split the data into two buffers while -// rotating them. Deprecated. -LIBYUV_API -void RotateUV180(const uint8* src, int src_stride, - uint8* dst_a, int dst_stride_a, - uint8* dst_b, int dst_stride_b, - int width, int height); - -LIBYUV_API -void RotateUV270(const uint8* src, int src_stride, - uint8* dst_a, int dst_stride_a, - uint8* dst_b, int dst_stride_b, - int width, int height); - -// The 90 and 270 functions are based on transposes. -// Doing a transpose with reversing the read/write -// order will result in a rotation by +- 90 degrees. -// Deprecated. -LIBYUV_API -void TransposePlane(const uint8* src, int src_stride, - uint8* dst, int dst_stride, - int width, int height); - -LIBYUV_API -void TransposeUV(const uint8* src, int src_stride, - uint8* dst_a, int dst_stride_a, - uint8* dst_b, int dst_stride_b, - int width, int height); - -#ifdef __cplusplus -} // extern "C" -} // namespace libyuv -#endif - -#endif // INCLUDE_LIBYUV_ROTATE_H_ diff --git a/third_party/libyuv/include/libyuv/rotate_argb.h b/third_party/libyuv/include/libyuv/rotate_argb.h deleted file mode 100644 index 21fe7e1807..0000000000 --- a/third_party/libyuv/include/libyuv/rotate_argb.h +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2012 The LibYuv Project Authors. All rights reserved. - * - * Use of this source code is governed by a BSD-style license - * that can be found in the LICENSE file in the root of the source - * tree. An additional intellectual property rights grant can be found - * in the file PATENTS. All contributing project authors may - * be found in the AUTHORS file in the root of the source tree. - */ - -#ifndef INCLUDE_LIBYUV_ROTATE_ARGB_H_ -#define INCLUDE_LIBYUV_ROTATE_ARGB_H_ - -#include "libyuv/basic_types.h" -#include "libyuv/rotate.h" // For RotationMode. - -#ifdef __cplusplus -namespace libyuv { -extern "C" { -#endif - -// Rotate ARGB frame -LIBYUV_API -int ARGBRotate(const uint8* src_argb, int src_stride_argb, - uint8* dst_argb, int dst_stride_argb, - int src_width, int src_height, enum RotationMode mode); - -#ifdef __cplusplus -} // extern "C" -} // namespace libyuv -#endif - -#endif // INCLUDE_LIBYUV_ROTATE_ARGB_H_ diff --git a/third_party/libyuv/include/libyuv/rotate_row.h b/third_party/libyuv/include/libyuv/rotate_row.h deleted file mode 100644 index 6abd201677..0000000000 --- a/third_party/libyuv/include/libyuv/rotate_row.h +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright 2013 The LibYuv Project Authors. All rights reserved. - * - * Use of this source code is governed by a BSD-style license - * that can be found in the LICENSE file in the root of the source - * tree. An additional intellectual property rights grant can be found - * in the file PATENTS. All contributing project authors may - * be found in the AUTHORS file in the root of the source tree. - */ - -#ifndef INCLUDE_LIBYUV_ROTATE_ROW_H_ -#define INCLUDE_LIBYUV_ROTATE_ROW_H_ - -#include "libyuv/basic_types.h" - -#ifdef __cplusplus -namespace libyuv { -extern "C" { -#endif - -#if defined(__pnacl__) || defined(__CLR_VER) || \ - (defined(__i386__) && !defined(__SSE2__)) -#define LIBYUV_DISABLE_X86 -#endif -// MemorySanitizer does not support assembly code yet. http://crbug.com/344505 -#if defined(__has_feature) -#if __has_feature(memory_sanitizer) -#define LIBYUV_DISABLE_X86 -#endif -#endif -// The following are available for Visual C and clangcl 32 bit: -#if !defined(LIBYUV_DISABLE_X86) && defined(_M_IX86) -#define HAS_TRANSPOSEWX8_SSSE3 -#define HAS_TRANSPOSEUVWX8_SSE2 -#endif - -// The following are available for GCC 32 or 64 bit but not NaCL for 64 bit: -#if !defined(LIBYUV_DISABLE_X86) && \ - (defined(__i386__) || (defined(__x86_64__) && !defined(__native_client__))) -#define HAS_TRANSPOSEWX8_SSSE3 -#endif - -// The following are available for 64 bit GCC but not NaCL: -#if !defined(LIBYUV_DISABLE_X86) && !defined(__native_client__) && \ - defined(__x86_64__) -#define HAS_TRANSPOSEWX8_FAST_SSSE3 -#define HAS_TRANSPOSEUVWX8_SSE2 -#endif - -#if !defined(LIBYUV_DISABLE_NEON) && !defined(__native_client__) && \ - (defined(__ARM_NEON__) || defined(LIBYUV_NEON) || defined(__aarch64__)) -#define HAS_TRANSPOSEWX8_NEON -#define HAS_TRANSPOSEUVWX8_NEON -#endif - -#if !defined(LIBYUV_DISABLE_MIPS) && !defined(__native_client__) && \ - defined(__mips__) && \ - defined(__mips_dsp) && (__mips_dsp_rev >= 2) -#define HAS_TRANSPOSEWX8_DSPR2 -#define HAS_TRANSPOSEUVWX8_DSPR2 -#endif // defined(__mips__) - -void TransposeWxH_C(const uint8* src, int src_stride, - uint8* dst, int dst_stride, int width, int height); - -void TransposeWx8_C(const uint8* src, int src_stride, - uint8* dst, int dst_stride, int width); -void TransposeWx8_NEON(const uint8* src, int src_stride, - uint8* dst, int dst_stride, int width); -void TransposeWx8_SSSE3(const uint8* src, int src_stride, - uint8* dst, int dst_stride, int width); -void TransposeWx8_Fast_SSSE3(const uint8* src, int src_stride, - uint8* dst, int dst_stride, int width); -void TransposeWx8_DSPR2(const uint8* src, int src_stride, - uint8* dst, int dst_stride, int width); -void TransposeWx8_Fast_DSPR2(const uint8* src, int src_stride, - uint8* dst, int dst_stride, int width); - -void TransposeWx8_Any_NEON(const uint8* src, int src_stride, - uint8* dst, int dst_stride, int width); -void TransposeWx8_Any_SSSE3(const uint8* src, int src_stride, - uint8* dst, int dst_stride, int width); -void TransposeWx8_Fast_Any_SSSE3(const uint8* src, int src_stride, - uint8* dst, int dst_stride, int width); -void TransposeWx8_Any_DSPR2(const uint8* src, int src_stride, - uint8* dst, int dst_stride, int width); - -void TransposeUVWxH_C(const uint8* src, int src_stride, - uint8* dst_a, int dst_stride_a, - uint8* dst_b, int dst_stride_b, - int width, int height); - -void TransposeUVWx8_C(const uint8* src, int src_stride, - uint8* dst_a, int dst_stride_a, - uint8* dst_b, int dst_stride_b, int width); -void TransposeUVWx8_SSE2(const uint8* src, int src_stride, - uint8* dst_a, int dst_stride_a, - uint8* dst_b, int dst_stride_b, int width); -void TransposeUVWx8_NEON(const uint8* src, int src_stride, - uint8* dst_a, int dst_stride_a, - uint8* dst_b, int dst_stride_b, int width); -void TransposeUVWx8_DSPR2(const uint8* src, int src_stride, - uint8* dst_a, int dst_stride_a, - uint8* dst_b, int dst_stride_b, int width); - -void TransposeUVWx8_Any_SSE2(const uint8* src, int src_stride, - uint8* dst_a, int dst_stride_a, - uint8* dst_b, int dst_stride_b, int width); -void TransposeUVWx8_Any_NEON(const uint8* src, int src_stride, - uint8* dst_a, int dst_stride_a, - uint8* dst_b, int dst_stride_b, int width); -void TransposeUVWx8_Any_DSPR2(const uint8* src, int src_stride, - uint8* dst_a, int dst_stride_a, - uint8* dst_b, int dst_stride_b, int width); - -#ifdef __cplusplus -} // extern "C" -} // namespace libyuv -#endif - -#endif // INCLUDE_LIBYUV_ROTATE_ROW_H_ diff --git a/third_party/libyuv/include/libyuv/row.h b/third_party/libyuv/include/libyuv/row.h deleted file mode 100644 index b810221ec7..0000000000 --- a/third_party/libyuv/include/libyuv/row.h +++ /dev/null @@ -1,1963 +0,0 @@ -/* - * Copyright 2011 The LibYuv Project Authors. All rights reserved. - * - * Use of this source code is governed by a BSD-style license - * that can be found in the LICENSE file in the root of the source - * tree. An additional intellectual property rights grant can be found - * in the file PATENTS. All contributing project authors may - * be found in the AUTHORS file in the root of the source tree. - */ - -#ifndef INCLUDE_LIBYUV_ROW_H_ -#define INCLUDE_LIBYUV_ROW_H_ - -#include // For malloc. - -#include "libyuv/basic_types.h" - -#ifdef __cplusplus -namespace libyuv { -extern "C" { -#endif - -#define IS_ALIGNED(p, a) (!((uintptr_t)(p) & ((a) - 1))) - -#define align_buffer_64(var, size) \ - uint8* var##_mem = (uint8*)(malloc((size) + 63)); /* NOLINT */ \ - uint8* var = (uint8*)(((intptr_t)(var##_mem) + 63) & ~63) /* NOLINT */ - -#define free_aligned_buffer_64(var) \ - free(var##_mem); \ - var = 0 - -#if defined(__pnacl__) || defined(__CLR_VER) || \ - (defined(__i386__) && !defined(__SSE2__)) -#define LIBYUV_DISABLE_X86 -#endif -// MemorySanitizer does not support assembly code yet. http://crbug.com/344505 -#if defined(__has_feature) -#if __has_feature(memory_sanitizer) -#define LIBYUV_DISABLE_X86 -#endif -#endif -// True if compiling for SSSE3 as a requirement. -#if defined(__SSSE3__) || (defined(_M_IX86_FP) && (_M_IX86_FP >= 3)) -#define LIBYUV_SSSE3_ONLY -#endif - -#if defined(__native_client__) -#define LIBYUV_DISABLE_NEON -#endif -// clang >= 3.5.0 required for Arm64. -#if defined(__clang__) && defined(__aarch64__) && !defined(LIBYUV_DISABLE_NEON) -#if (__clang_major__ < 3) || (__clang_major__ == 3 && (__clang_minor__ < 5)) -#define LIBYUV_DISABLE_NEON -#endif // clang >= 3.5 -#endif // __clang__ - -// GCC >= 4.7.0 required for AVX2. -#if defined(__GNUC__) && (defined(__x86_64__) || defined(__i386__)) -#if (__GNUC__ > 4) || (__GNUC__ == 4 && (__GNUC_MINOR__ >= 7)) -#define GCC_HAS_AVX2 1 -#endif // GNUC >= 4.7 -#endif // __GNUC__ - -// clang >= 3.4.0 required for AVX2. -#if defined(__clang__) && (defined(__x86_64__) || defined(__i386__)) -#if (__clang_major__ > 3) || (__clang_major__ == 3 && (__clang_minor__ >= 4)) -#define CLANG_HAS_AVX2 1 -#endif // clang >= 3.4 -#endif // __clang__ - -// Visual C 2012 required for AVX2. -#if defined(_M_IX86) && !defined(__clang__) && \ - defined(_MSC_VER) && _MSC_VER >= 1700 -#define VISUALC_HAS_AVX2 1 -#endif // VisualStudio >= 2012 - -// The following are available on all x86 platforms: -#if !defined(LIBYUV_DISABLE_X86) && \ - (defined(_M_IX86) || defined(__x86_64__) || defined(__i386__)) -// Conversions: -#define HAS_ABGRTOUVROW_SSSE3 -#define HAS_ABGRTOYROW_SSSE3 -#define HAS_ARGB1555TOARGBROW_SSE2 -#define HAS_ARGB4444TOARGBROW_SSE2 -#define HAS_ARGBSETROW_X86 -#define HAS_ARGBSHUFFLEROW_SSE2 -#define HAS_ARGBSHUFFLEROW_SSSE3 -#define HAS_ARGBTOARGB1555ROW_SSE2 -#define HAS_ARGBTOARGB4444ROW_SSE2 -#define HAS_ARGBTORAWROW_SSSE3 -#define HAS_ARGBTORGB24ROW_SSSE3 -#define HAS_ARGBTORGB565DITHERROW_SSE2 -#define HAS_ARGBTORGB565ROW_SSE2 -#define HAS_ARGBTOUV444ROW_SSSE3 -#define HAS_ARGBTOUVJROW_SSSE3 -#define HAS_ARGBTOUVROW_SSSE3 -#define HAS_ARGBTOYJROW_SSSE3 -#define HAS_ARGBTOYROW_SSSE3 -#define HAS_ARGBEXTRACTALPHAROW_SSE2 -#define HAS_BGRATOUVROW_SSSE3 -#define HAS_BGRATOYROW_SSSE3 -#define HAS_COPYROW_ERMS -#define HAS_COPYROW_SSE2 -#define HAS_H422TOARGBROW_SSSE3 -#define HAS_I400TOARGBROW_SSE2 -#define HAS_I422TOARGB1555ROW_SSSE3 -#define HAS_I422TOARGB4444ROW_SSSE3 -#define HAS_I422TOARGBROW_SSSE3 -#define HAS_I422TORGB24ROW_SSSE3 -#define HAS_I422TORGB565ROW_SSSE3 -#define HAS_I422TORGBAROW_SSSE3 -#define HAS_I422TOUYVYROW_SSE2 -#define HAS_I422TOYUY2ROW_SSE2 -#define HAS_I444TOARGBROW_SSSE3 -#define HAS_J400TOARGBROW_SSE2 -#define HAS_J422TOARGBROW_SSSE3 -#define HAS_MERGEUVROW_SSE2 -#define HAS_MIRRORROW_SSSE3 -#define HAS_MIRRORUVROW_SSSE3 -#define HAS_NV12TOARGBROW_SSSE3 -#define HAS_NV12TORGB565ROW_SSSE3 -#define HAS_NV21TOARGBROW_SSSE3 -#define HAS_RAWTOARGBROW_SSSE3 -#define HAS_RAWTORGB24ROW_SSSE3 -#define HAS_RAWTOYROW_SSSE3 -#define HAS_RGB24TOARGBROW_SSSE3 -#define HAS_RGB24TOYROW_SSSE3 -#define HAS_RGB565TOARGBROW_SSE2 -#define HAS_RGBATOUVROW_SSSE3 -#define HAS_RGBATOYROW_SSSE3 -#define HAS_SETROW_ERMS -#define HAS_SETROW_X86 -#define HAS_SPLITUVROW_SSE2 -#define HAS_UYVYTOARGBROW_SSSE3 -#define HAS_UYVYTOUV422ROW_SSE2 -#define HAS_UYVYTOUVROW_SSE2 -#define HAS_UYVYTOYROW_SSE2 -#define HAS_YUY2TOARGBROW_SSSE3 -#define HAS_YUY2TOUV422ROW_SSE2 -#define HAS_YUY2TOUVROW_SSE2 -#define HAS_YUY2TOYROW_SSE2 - -// Effects: -#define HAS_ARGBADDROW_SSE2 -#define HAS_ARGBAFFINEROW_SSE2 -#define HAS_ARGBATTENUATEROW_SSSE3 -#define HAS_ARGBBLENDROW_SSSE3 -#define HAS_ARGBCOLORMATRIXROW_SSSE3 -#define HAS_ARGBCOLORTABLEROW_X86 -#define HAS_ARGBCOPYALPHAROW_SSE2 -#define HAS_ARGBCOPYYTOALPHAROW_SSE2 -#define HAS_ARGBGRAYROW_SSSE3 -#define HAS_ARGBLUMACOLORTABLEROW_SSSE3 -#define HAS_ARGBMIRRORROW_SSE2 -#define HAS_ARGBMULTIPLYROW_SSE2 -#define HAS_ARGBPOLYNOMIALROW_SSE2 -#define HAS_ARGBQUANTIZEROW_SSE2 -#define HAS_ARGBSEPIAROW_SSSE3 -#define HAS_ARGBSHADEROW_SSE2 -#define HAS_ARGBSUBTRACTROW_SSE2 -#define HAS_ARGBUNATTENUATEROW_SSE2 -#define HAS_BLENDPLANEROW_SSSE3 -#define HAS_COMPUTECUMULATIVESUMROW_SSE2 -#define HAS_CUMULATIVESUMTOAVERAGEROW_SSE2 -#define HAS_INTERPOLATEROW_SSSE3 -#define HAS_RGBCOLORTABLEROW_X86 -#define HAS_SOBELROW_SSE2 -#define HAS_SOBELTOPLANEROW_SSE2 -#define HAS_SOBELXROW_SSE2 -#define HAS_SOBELXYROW_SSE2 -#define HAS_SOBELYROW_SSE2 - -// The following functions fail on gcc/clang 32 bit with fpic and framepointer. -// caveat: clangcl uses row_win.cc which works. -#if defined(NDEBUG) || !(defined(_DEBUG) && defined(__i386__)) || \ - !defined(__i386__) || defined(_MSC_VER) -// TODO(fbarchard): fix build error on x86 debug -// https://code.google.com/p/libyuv/issues/detail?id=524 -#define HAS_I411TOARGBROW_SSSE3 -// TODO(fbarchard): fix build error on android_full_debug=1 -// https://code.google.com/p/libyuv/issues/detail?id=517 -#define HAS_I422ALPHATOARGBROW_SSSE3 -#endif -#endif - -// The following are available on all x86 platforms, but -// require VS2012, clang 3.4 or gcc 4.7. -// The code supports NaCL but requires a new compiler and validator. -#if !defined(LIBYUV_DISABLE_X86) && (defined(VISUALC_HAS_AVX2) || \ - defined(CLANG_HAS_AVX2) || defined(GCC_HAS_AVX2)) -#define HAS_ARGBCOPYALPHAROW_AVX2 -#define HAS_ARGBCOPYYTOALPHAROW_AVX2 -#define HAS_ARGBMIRRORROW_AVX2 -#define HAS_ARGBPOLYNOMIALROW_AVX2 -#define HAS_ARGBSHUFFLEROW_AVX2 -#define HAS_ARGBTORGB565DITHERROW_AVX2 -#define HAS_ARGBTOUVJROW_AVX2 -#define HAS_ARGBTOUVROW_AVX2 -#define HAS_ARGBTOYJROW_AVX2 -#define HAS_ARGBTOYROW_AVX2 -#define HAS_COPYROW_AVX -#define HAS_H422TOARGBROW_AVX2 -#define HAS_I400TOARGBROW_AVX2 -#if !(defined(_DEBUG) && defined(__i386__)) -// TODO(fbarchard): fix build error on android_full_debug=1 -// https://code.google.com/p/libyuv/issues/detail?id=517 -#define HAS_I422ALPHATOARGBROW_AVX2 -#endif -#define HAS_I411TOARGBROW_AVX2 -#define HAS_I422TOARGB1555ROW_AVX2 -#define HAS_I422TOARGB4444ROW_AVX2 -#define HAS_I422TOARGBROW_AVX2 -#define HAS_I422TORGB24ROW_AVX2 -#define HAS_I422TORGB565ROW_AVX2 -#define HAS_I422TORGBAROW_AVX2 -#define HAS_I444TOARGBROW_AVX2 -#define HAS_INTERPOLATEROW_AVX2 -#define HAS_J422TOARGBROW_AVX2 -#define HAS_MERGEUVROW_AVX2 -#define HAS_MIRRORROW_AVX2 -#define HAS_NV12TOARGBROW_AVX2 -#define HAS_NV12TORGB565ROW_AVX2 -#define HAS_NV21TOARGBROW_AVX2 -#define HAS_SPLITUVROW_AVX2 -#define HAS_UYVYTOARGBROW_AVX2 -#define HAS_UYVYTOUV422ROW_AVX2 -#define HAS_UYVYTOUVROW_AVX2 -#define HAS_UYVYTOYROW_AVX2 -#define HAS_YUY2TOARGBROW_AVX2 -#define HAS_YUY2TOUV422ROW_AVX2 -#define HAS_YUY2TOUVROW_AVX2 -#define HAS_YUY2TOYROW_AVX2 -#define HAS_HALFFLOATROW_AVX2 - -// Effects: -#define HAS_ARGBADDROW_AVX2 -#define HAS_ARGBATTENUATEROW_AVX2 -#define HAS_ARGBMULTIPLYROW_AVX2 -#define HAS_ARGBSUBTRACTROW_AVX2 -#define HAS_ARGBUNATTENUATEROW_AVX2 -#define HAS_BLENDPLANEROW_AVX2 -#endif - -// The following are available for AVX2 Visual C and clangcl 32 bit: -// TODO(fbarchard): Port to gcc. -#if !defined(LIBYUV_DISABLE_X86) && defined(_M_IX86) && \ - (defined(VISUALC_HAS_AVX2) || defined(CLANG_HAS_AVX2)) -#define HAS_ARGB1555TOARGBROW_AVX2 -#define HAS_ARGB4444TOARGBROW_AVX2 -#define HAS_ARGBTOARGB1555ROW_AVX2 -#define HAS_ARGBTOARGB4444ROW_AVX2 -#define HAS_ARGBTORGB565ROW_AVX2 -#define HAS_J400TOARGBROW_AVX2 -#define HAS_RGB565TOARGBROW_AVX2 -#endif - -// The following are also available on x64 Visual C. -#if !defined(LIBYUV_DISABLE_X86) && defined(_MSC_VER) && defined(_M_X64) && \ - (!defined(__clang__) || defined(__SSSE3__)) -#define HAS_I422ALPHATOARGBROW_SSSE3 -#define HAS_I422TOARGBROW_SSSE3 -#endif - -// The following are available on gcc x86 platforms: -// TODO(fbarchard): Port to Visual C. -#if !defined(LIBYUV_DISABLE_X86) && \ - (defined(__x86_64__) || (defined(__i386__) && !defined(_MSC_VER))) -#define HAS_HALFFLOATROW_SSE2 -#endif - -// The following are available on Neon platforms: -#if !defined(LIBYUV_DISABLE_NEON) && \ - (defined(__aarch64__) || defined(__ARM_NEON__) || defined(LIBYUV_NEON)) -#define HAS_ABGRTOUVROW_NEON -#define HAS_ABGRTOYROW_NEON -#define HAS_ARGB1555TOARGBROW_NEON -#define HAS_ARGB1555TOUVROW_NEON -#define HAS_ARGB1555TOYROW_NEON -#define HAS_ARGB4444TOARGBROW_NEON -#define HAS_ARGB4444TOUVROW_NEON -#define HAS_ARGB4444TOYROW_NEON -#define HAS_ARGBSETROW_NEON -#define HAS_ARGBTOARGB1555ROW_NEON -#define HAS_ARGBTOARGB4444ROW_NEON -#define HAS_ARGBTORAWROW_NEON -#define HAS_ARGBTORGB24ROW_NEON -#define HAS_ARGBTORGB565DITHERROW_NEON -#define HAS_ARGBTORGB565ROW_NEON -#define HAS_ARGBTOUV411ROW_NEON -#define HAS_ARGBTOUV444ROW_NEON -#define HAS_ARGBTOUVJROW_NEON -#define HAS_ARGBTOUVROW_NEON -#define HAS_ARGBTOYJROW_NEON -#define HAS_ARGBTOYROW_NEON -#define HAS_ARGBEXTRACTALPHAROW_NEON -#define HAS_BGRATOUVROW_NEON -#define HAS_BGRATOYROW_NEON -#define HAS_COPYROW_NEON -#define HAS_I400TOARGBROW_NEON -#define HAS_I411TOARGBROW_NEON -#define HAS_I422ALPHATOARGBROW_NEON -#define HAS_I422TOARGB1555ROW_NEON -#define HAS_I422TOARGB4444ROW_NEON -#define HAS_I422TOARGBROW_NEON -#define HAS_I422TORGB24ROW_NEON -#define HAS_I422TORGB565ROW_NEON -#define HAS_I422TORGBAROW_NEON -#define HAS_I422TOUYVYROW_NEON -#define HAS_I422TOYUY2ROW_NEON -#define HAS_I444TOARGBROW_NEON -#define HAS_J400TOARGBROW_NEON -#define HAS_MERGEUVROW_NEON -#define HAS_MIRRORROW_NEON -#define HAS_MIRRORUVROW_NEON -#define HAS_NV12TOARGBROW_NEON -#define HAS_NV12TORGB565ROW_NEON -#define HAS_NV21TOARGBROW_NEON -#define HAS_RAWTOARGBROW_NEON -#define HAS_RAWTORGB24ROW_NEON -#define HAS_RAWTOUVROW_NEON -#define HAS_RAWTOYROW_NEON -#define HAS_RGB24TOARGBROW_NEON -#define HAS_RGB24TOUVROW_NEON -#define HAS_RGB24TOYROW_NEON -#define HAS_RGB565TOARGBROW_NEON -#define HAS_RGB565TOUVROW_NEON -#define HAS_RGB565TOYROW_NEON -#define HAS_RGBATOUVROW_NEON -#define HAS_RGBATOYROW_NEON -#define HAS_SETROW_NEON -#define HAS_SPLITUVROW_NEON -#define HAS_UYVYTOARGBROW_NEON -#define HAS_UYVYTOUV422ROW_NEON -#define HAS_UYVYTOUVROW_NEON -#define HAS_UYVYTOYROW_NEON -#define HAS_YUY2TOARGBROW_NEON -#define HAS_YUY2TOUV422ROW_NEON -#define HAS_YUY2TOUVROW_NEON -#define HAS_YUY2TOYROW_NEON - -// Effects: -#define HAS_ARGBADDROW_NEON -#define HAS_ARGBATTENUATEROW_NEON -#define HAS_ARGBBLENDROW_NEON -#define HAS_ARGBCOLORMATRIXROW_NEON -#define HAS_ARGBGRAYROW_NEON -#define HAS_ARGBMIRRORROW_NEON -#define HAS_ARGBMULTIPLYROW_NEON -#define HAS_ARGBQUANTIZEROW_NEON -#define HAS_ARGBSEPIAROW_NEON -#define HAS_ARGBSHADEROW_NEON -#define HAS_ARGBSHUFFLEROW_NEON -#define HAS_ARGBSUBTRACTROW_NEON -#define HAS_INTERPOLATEROW_NEON -#define HAS_SOBELROW_NEON -#define HAS_SOBELTOPLANEROW_NEON -#define HAS_SOBELXROW_NEON -#define HAS_SOBELXYROW_NEON -#define HAS_SOBELYROW_NEON -#endif - -// The following are available on Mips platforms: -#if !defined(LIBYUV_DISABLE_MIPS) && defined(__mips__) && \ - (_MIPS_SIM == _MIPS_SIM_ABI32) && (__mips_isa_rev < 6) -#define HAS_COPYROW_MIPS -#if defined(__mips_dsp) && (__mips_dsp_rev >= 2) -#define HAS_I422TOARGBROW_DSPR2 -#define HAS_INTERPOLATEROW_DSPR2 -#define HAS_MIRRORROW_DSPR2 -#define HAS_MIRRORUVROW_DSPR2 -#define HAS_SPLITUVROW_DSPR2 -#endif -#endif - -#if !defined(LIBYUV_DISABLE_MSA) && defined(__mips_msa) -#define HAS_MIRRORROW_MSA -#define HAS_ARGBMIRRORROW_MSA -#endif - -#if defined(_MSC_VER) && !defined(__CLR_VER) && !defined(__clang__) -#if defined(VISUALC_HAS_AVX2) -#define SIMD_ALIGNED(var) __declspec(align(32)) var -#else -#define SIMD_ALIGNED(var) __declspec(align(16)) var -#endif -typedef __declspec(align(16)) int16 vec16[8]; -typedef __declspec(align(16)) int32 vec32[4]; -typedef __declspec(align(16)) int8 vec8[16]; -typedef __declspec(align(16)) uint16 uvec16[8]; -typedef __declspec(align(16)) uint32 uvec32[4]; -typedef __declspec(align(16)) uint8 uvec8[16]; -typedef __declspec(align(32)) int16 lvec16[16]; -typedef __declspec(align(32)) int32 lvec32[8]; -typedef __declspec(align(32)) int8 lvec8[32]; -typedef __declspec(align(32)) uint16 ulvec16[16]; -typedef __declspec(align(32)) uint32 ulvec32[8]; -typedef __declspec(align(32)) uint8 ulvec8[32]; -#elif !defined(__pnacl__) && (defined(__GNUC__) || defined(__clang__)) -// Caveat GCC 4.2 to 4.7 have a known issue using vectors with const. -#if defined(CLANG_HAS_AVX2) || defined(GCC_HAS_AVX2) -#define SIMD_ALIGNED(var) var __attribute__((aligned(32))) -#else -#define SIMD_ALIGNED(var) var __attribute__((aligned(16))) -#endif -typedef int16 __attribute__((vector_size(16))) vec16; -typedef int32 __attribute__((vector_size(16))) vec32; -typedef int8 __attribute__((vector_size(16))) vec8; -typedef uint16 __attribute__((vector_size(16))) uvec16; -typedef uint32 __attribute__((vector_size(16))) uvec32; -typedef uint8 __attribute__((vector_size(16))) uvec8; -typedef int16 __attribute__((vector_size(32))) lvec16; -typedef int32 __attribute__((vector_size(32))) lvec32; -typedef int8 __attribute__((vector_size(32))) lvec8; -typedef uint16 __attribute__((vector_size(32))) ulvec16; -typedef uint32 __attribute__((vector_size(32))) ulvec32; -typedef uint8 __attribute__((vector_size(32))) ulvec8; -#else -#define SIMD_ALIGNED(var) var -typedef int16 vec16[8]; -typedef int32 vec32[4]; -typedef int8 vec8[16]; -typedef uint16 uvec16[8]; -typedef uint32 uvec32[4]; -typedef uint8 uvec8[16]; -typedef int16 lvec16[16]; -typedef int32 lvec32[8]; -typedef int8 lvec8[32]; -typedef uint16 ulvec16[16]; -typedef uint32 ulvec32[8]; -typedef uint8 ulvec8[32]; -#endif - -#if defined(__aarch64__) -// This struct is for Arm64 color conversion. -struct YuvConstants { - uvec16 kUVToRB; - uvec16 kUVToRB2; - uvec16 kUVToG; - uvec16 kUVToG2; - vec16 kUVBiasBGR; - vec32 kYToRgb; -}; -#elif defined(__arm__) -// This struct is for ArmV7 color conversion. -struct YuvConstants { - uvec8 kUVToRB; - uvec8 kUVToG; - vec16 kUVBiasBGR; - vec32 kYToRgb; -}; -#else -// This struct is for Intel color conversion. -struct YuvConstants { - int8 kUVToB[32]; - int8 kUVToG[32]; - int8 kUVToR[32]; - int16 kUVBiasB[16]; - int16 kUVBiasG[16]; - int16 kUVBiasR[16]; - int16 kYToRgb[16]; -}; - -// Offsets into YuvConstants structure -#define KUVTOB 0 -#define KUVTOG 32 -#define KUVTOR 64 -#define KUVBIASB 96 -#define KUVBIASG 128 -#define KUVBIASR 160 -#define KYTORGB 192 -#endif - -// Conversion matrix for YUV to RGB -extern const struct YuvConstants SIMD_ALIGNED(kYuvI601Constants); // BT.601 -extern const struct YuvConstants SIMD_ALIGNED(kYuvJPEGConstants); // JPeg -extern const struct YuvConstants SIMD_ALIGNED(kYuvH709Constants); // BT.709 - -// Conversion matrix for YVU to BGR -extern const struct YuvConstants SIMD_ALIGNED(kYvuI601Constants); // BT.601 -extern const struct YuvConstants SIMD_ALIGNED(kYvuJPEGConstants); // JPeg -extern const struct YuvConstants SIMD_ALIGNED(kYvuH709Constants); // BT.709 - -#if defined(__APPLE__) || defined(__x86_64__) || defined(__llvm__) -#define OMITFP -#else -#define OMITFP __attribute__((optimize("omit-frame-pointer"))) -#endif - -// NaCL macros for GCC x86 and x64. -#if defined(__native_client__) -#define LABELALIGN ".p2align 5\n" -#else -#define LABELALIGN -#endif -#if defined(__native_client__) && defined(__x86_64__) -// r14 is used for MEMOP macros. -#define NACL_R14 "r14", -#define BUNDLELOCK ".bundle_lock\n" -#define BUNDLEUNLOCK ".bundle_unlock\n" -#define MEMACCESS(base) "%%nacl:(%%r15,%q" #base ")" -#define MEMACCESS2(offset, base) "%%nacl:" #offset "(%%r15,%q" #base ")" -#define MEMLEA(offset, base) #offset "(%q" #base ")" -#define MEMLEA3(offset, index, scale) \ - #offset "(,%q" #index "," #scale ")" -#define MEMLEA4(offset, base, index, scale) \ - #offset "(%q" #base ",%q" #index "," #scale ")" -#define MEMMOVESTRING(s, d) "%%nacl:(%q" #s "),%%nacl:(%q" #d "), %%r15" -#define MEMSTORESTRING(reg, d) "%%" #reg ",%%nacl:(%q" #d "), %%r15" -#define MEMOPREG(opcode, offset, base, index, scale, reg) \ - BUNDLELOCK \ - "lea " #offset "(%q" #base ",%q" #index "," #scale "),%%r14d\n" \ - #opcode " (%%r15,%%r14),%%" #reg "\n" \ - BUNDLEUNLOCK -#define MEMOPMEM(opcode, reg, offset, base, index, scale) \ - BUNDLELOCK \ - "lea " #offset "(%q" #base ",%q" #index "," #scale "),%%r14d\n" \ - #opcode " %%" #reg ",(%%r15,%%r14)\n" \ - BUNDLEUNLOCK -#define MEMOPARG(opcode, offset, base, index, scale, arg) \ - BUNDLELOCK \ - "lea " #offset "(%q" #base ",%q" #index "," #scale "),%%r14d\n" \ - #opcode " (%%r15,%%r14),%" #arg "\n" \ - BUNDLEUNLOCK -#define VMEMOPREG(opcode, offset, base, index, scale, reg1, reg2) \ - BUNDLELOCK \ - "lea " #offset "(%q" #base ",%q" #index "," #scale "),%%r14d\n" \ - #opcode " (%%r15,%%r14),%%" #reg1 ",%%" #reg2 "\n" \ - BUNDLEUNLOCK -#define VEXTOPMEM(op, sel, reg, offset, base, index, scale) \ - BUNDLELOCK \ - "lea " #offset "(%q" #base ",%q" #index "," #scale "),%%r14d\n" \ - #op " $" #sel ",%%" #reg ",(%%r15,%%r14)\n" \ - BUNDLEUNLOCK -#else // defined(__native_client__) && defined(__x86_64__) -#define NACL_R14 -#define BUNDLEALIGN -#define MEMACCESS(base) "(%" #base ")" -#define MEMACCESS2(offset, base) #offset "(%" #base ")" -#define MEMLEA(offset, base) #offset "(%" #base ")" -#define MEMLEA3(offset, index, scale) \ - #offset "(,%" #index "," #scale ")" -#define MEMLEA4(offset, base, index, scale) \ - #offset "(%" #base ",%" #index "," #scale ")" -#define MEMMOVESTRING(s, d) -#define MEMSTORESTRING(reg, d) -#define MEMOPREG(opcode, offset, base, index, scale, reg) \ - #opcode " " #offset "(%" #base ",%" #index "," #scale "),%%" #reg "\n" -#define MEMOPMEM(opcode, reg, offset, base, index, scale) \ - #opcode " %%" #reg ","#offset "(%" #base ",%" #index "," #scale ")\n" -#define MEMOPARG(opcode, offset, base, index, scale, arg) \ - #opcode " " #offset "(%" #base ",%" #index "," #scale "),%" #arg "\n" -#define VMEMOPREG(opcode, offset, base, index, scale, reg1, reg2) \ - #opcode " " #offset "(%" #base ",%" #index "," #scale "),%%" #reg1 ",%%" \ - #reg2 "\n" -#define VEXTOPMEM(op, sel, reg, offset, base, index, scale) \ - #op " $" #sel ",%%" #reg ","#offset "(%" #base ",%" #index "," #scale ")\n" -#endif // defined(__native_client__) && defined(__x86_64__) - -#if defined(__arm__) || defined(__aarch64__) -#undef MEMACCESS -#if defined(__native_client__) -#define MEMACCESS(base) ".p2align 3\nbic %" #base ", #0xc0000000\n" -#else -#define MEMACCESS(base) -#endif -#endif - -void I444ToARGBRow_NEON(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void I422ToARGBRow_NEON(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void I422AlphaToARGBRow_NEON(const uint8* y_buf, - const uint8* u_buf, - const uint8* v_buf, - const uint8* a_buf, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void I422ToARGBRow_NEON(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void I411ToARGBRow_NEON(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void I422ToRGBARow_NEON(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_rgba, - const struct YuvConstants* yuvconstants, - int width); -void I422ToRGB24Row_NEON(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_rgb24, - const struct YuvConstants* yuvconstants, - int width); -void I422ToRGB565Row_NEON(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_rgb565, - const struct YuvConstants* yuvconstants, - int width); -void I422ToARGB1555Row_NEON(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_argb1555, - const struct YuvConstants* yuvconstants, - int width); -void I422ToARGB4444Row_NEON(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_argb4444, - const struct YuvConstants* yuvconstants, - int width); -void NV12ToARGBRow_NEON(const uint8* src_y, - const uint8* src_uv, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void NV12ToRGB565Row_NEON(const uint8* src_y, - const uint8* src_uv, - uint8* dst_rgb565, - const struct YuvConstants* yuvconstants, - int width); -void NV21ToARGBRow_NEON(const uint8* src_y, - const uint8* src_vu, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void YUY2ToARGBRow_NEON(const uint8* src_yuy2, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void UYVYToARGBRow_NEON(const uint8* src_uyvy, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); - -void ARGBToYRow_AVX2(const uint8* src_argb, uint8* dst_y, int width); -void ARGBToYRow_Any_AVX2(const uint8* src_argb, uint8* dst_y, int width); -void ARGBToYRow_SSSE3(const uint8* src_argb, uint8* dst_y, int width); -void ARGBToYJRow_AVX2(const uint8* src_argb, uint8* dst_y, int width); -void ARGBToYJRow_Any_AVX2(const uint8* src_argb, uint8* dst_y, int width); -void ARGBToYJRow_SSSE3(const uint8* src_argb, uint8* dst_y, int width); -void BGRAToYRow_SSSE3(const uint8* src_bgra, uint8* dst_y, int width); -void ABGRToYRow_SSSE3(const uint8* src_abgr, uint8* dst_y, int width); -void RGBAToYRow_SSSE3(const uint8* src_rgba, uint8* dst_y, int width); -void RGB24ToYRow_SSSE3(const uint8* src_rgb24, uint8* dst_y, int width); -void RAWToYRow_SSSE3(const uint8* src_raw, uint8* dst_y, int width); -void ARGBToYRow_NEON(const uint8* src_argb, uint8* dst_y, int width); -void ARGBToYJRow_NEON(const uint8* src_argb, uint8* dst_y, int width); -void ARGBToUV444Row_NEON(const uint8* src_argb, uint8* dst_u, uint8* dst_v, - int width); -void ARGBToUV411Row_NEON(const uint8* src_argb, uint8* dst_u, uint8* dst_v, - int width); -void ARGBToUVRow_NEON(const uint8* src_argb, int src_stride_argb, - uint8* dst_u, uint8* dst_v, int width); -void ARGBToUVJRow_NEON(const uint8* src_argb, int src_stride_argb, - uint8* dst_u, uint8* dst_v, int width); -void BGRAToUVRow_NEON(const uint8* src_bgra, int src_stride_bgra, - uint8* dst_u, uint8* dst_v, int width); -void ABGRToUVRow_NEON(const uint8* src_abgr, int src_stride_abgr, - uint8* dst_u, uint8* dst_v, int width); -void RGBAToUVRow_NEON(const uint8* src_rgba, int src_stride_rgba, - uint8* dst_u, uint8* dst_v, int width); -void RGB24ToUVRow_NEON(const uint8* src_rgb24, int src_stride_rgb24, - uint8* dst_u, uint8* dst_v, int width); -void RAWToUVRow_NEON(const uint8* src_raw, int src_stride_raw, - uint8* dst_u, uint8* dst_v, int width); -void RGB565ToUVRow_NEON(const uint8* src_rgb565, int src_stride_rgb565, - uint8* dst_u, uint8* dst_v, int width); -void ARGB1555ToUVRow_NEON(const uint8* src_argb1555, int src_stride_argb1555, - uint8* dst_u, uint8* dst_v, int width); -void ARGB4444ToUVRow_NEON(const uint8* src_argb4444, int src_stride_argb4444, - uint8* dst_u, uint8* dst_v, int width); -void BGRAToYRow_NEON(const uint8* src_bgra, uint8* dst_y, int width); -void ABGRToYRow_NEON(const uint8* src_abgr, uint8* dst_y, int width); -void RGBAToYRow_NEON(const uint8* src_rgba, uint8* dst_y, int width); -void RGB24ToYRow_NEON(const uint8* src_rgb24, uint8* dst_y, int width); -void RAWToYRow_NEON(const uint8* src_raw, uint8* dst_y, int width); -void RGB565ToYRow_NEON(const uint8* src_rgb565, uint8* dst_y, int width); -void ARGB1555ToYRow_NEON(const uint8* src_argb1555, uint8* dst_y, int width); -void ARGB4444ToYRow_NEON(const uint8* src_argb4444, uint8* dst_y, int width); -void ARGBToYRow_C(const uint8* src_argb, uint8* dst_y, int width); -void ARGBToYJRow_C(const uint8* src_argb, uint8* dst_y, int width); -void BGRAToYRow_C(const uint8* src_bgra, uint8* dst_y, int width); -void ABGRToYRow_C(const uint8* src_abgr, uint8* dst_y, int width); -void RGBAToYRow_C(const uint8* src_rgba, uint8* dst_y, int width); -void RGB24ToYRow_C(const uint8* src_rgb24, uint8* dst_y, int width); -void RAWToYRow_C(const uint8* src_raw, uint8* dst_y, int width); -void RGB565ToYRow_C(const uint8* src_rgb565, uint8* dst_y, int width); -void ARGB1555ToYRow_C(const uint8* src_argb1555, uint8* dst_y, int width); -void ARGB4444ToYRow_C(const uint8* src_argb4444, uint8* dst_y, int width); -void ARGBToYRow_Any_SSSE3(const uint8* src_argb, uint8* dst_y, int width); -void ARGBToYJRow_Any_SSSE3(const uint8* src_argb, uint8* dst_y, int width); -void BGRAToYRow_Any_SSSE3(const uint8* src_bgra, uint8* dst_y, int width); -void ABGRToYRow_Any_SSSE3(const uint8* src_abgr, uint8* dst_y, int width); -void RGBAToYRow_Any_SSSE3(const uint8* src_rgba, uint8* dst_y, int width); -void RGB24ToYRow_Any_SSSE3(const uint8* src_rgb24, uint8* dst_y, int width); -void RAWToYRow_Any_SSSE3(const uint8* src_raw, uint8* dst_y, int width); -void ARGBToYRow_Any_NEON(const uint8* src_argb, uint8* dst_y, int width); -void ARGBToYJRow_Any_NEON(const uint8* src_argb, uint8* dst_y, int width); -void BGRAToYRow_Any_NEON(const uint8* src_bgra, uint8* dst_y, int width); -void ABGRToYRow_Any_NEON(const uint8* src_abgr, uint8* dst_y, int width); -void RGBAToYRow_Any_NEON(const uint8* src_rgba, uint8* dst_y, int width); -void RGB24ToYRow_Any_NEON(const uint8* src_rgb24, uint8* dst_y, int width); -void RAWToYRow_Any_NEON(const uint8* src_raw, uint8* dst_y, int width); -void RGB565ToYRow_Any_NEON(const uint8* src_rgb565, uint8* dst_y, int width); -void ARGB1555ToYRow_Any_NEON(const uint8* src_argb1555, uint8* dst_y, - int width); -void ARGB4444ToYRow_Any_NEON(const uint8* src_argb4444, uint8* dst_y, - int width); - -void ARGBToUVRow_AVX2(const uint8* src_argb, int src_stride_argb, - uint8* dst_u, uint8* dst_v, int width); -void ARGBToUVJRow_AVX2(const uint8* src_argb, int src_stride_argb, - uint8* dst_u, uint8* dst_v, int width); -void ARGBToUVRow_SSSE3(const uint8* src_argb, int src_stride_argb, - uint8* dst_u, uint8* dst_v, int width); -void ARGBToUVJRow_SSSE3(const uint8* src_argb, int src_stride_argb, - uint8* dst_u, uint8* dst_v, int width); -void BGRAToUVRow_SSSE3(const uint8* src_bgra, int src_stride_bgra, - uint8* dst_u, uint8* dst_v, int width); -void ABGRToUVRow_SSSE3(const uint8* src_abgr, int src_stride_abgr, - uint8* dst_u, uint8* dst_v, int width); -void RGBAToUVRow_SSSE3(const uint8* src_rgba, int src_stride_rgba, - uint8* dst_u, uint8* dst_v, int width); -void ARGBToUVRow_Any_AVX2(const uint8* src_argb, int src_stride_argb, - uint8* dst_u, uint8* dst_v, int width); -void ARGBToUVJRow_Any_AVX2(const uint8* src_argb, int src_stride_argb, - uint8* dst_u, uint8* dst_v, int width); -void ARGBToUVRow_Any_SSSE3(const uint8* src_argb, int src_stride_argb, - uint8* dst_u, uint8* dst_v, int width); -void ARGBToUVJRow_Any_SSSE3(const uint8* src_argb, int src_stride_argb, - uint8* dst_u, uint8* dst_v, int width); -void BGRAToUVRow_Any_SSSE3(const uint8* src_bgra, int src_stride_bgra, - uint8* dst_u, uint8* dst_v, int width); -void ABGRToUVRow_Any_SSSE3(const uint8* src_abgr, int src_stride_abgr, - uint8* dst_u, uint8* dst_v, int width); -void RGBAToUVRow_Any_SSSE3(const uint8* src_rgba, int src_stride_rgba, - uint8* dst_u, uint8* dst_v, int width); -void ARGBToUV444Row_Any_NEON(const uint8* src_argb, uint8* dst_u, uint8* dst_v, - int width); -void ARGBToUV411Row_Any_NEON(const uint8* src_argb, uint8* dst_u, uint8* dst_v, - int width); -void ARGBToUVRow_Any_NEON(const uint8* src_argb, int src_stride_argb, - uint8* dst_u, uint8* dst_v, int width); -void ARGBToUVJRow_Any_NEON(const uint8* src_argb, int src_stride_argb, - uint8* dst_u, uint8* dst_v, int width); -void BGRAToUVRow_Any_NEON(const uint8* src_bgra, int src_stride_bgra, - uint8* dst_u, uint8* dst_v, int width); -void ABGRToUVRow_Any_NEON(const uint8* src_abgr, int src_stride_abgr, - uint8* dst_u, uint8* dst_v, int width); -void RGBAToUVRow_Any_NEON(const uint8* src_rgba, int src_stride_rgba, - uint8* dst_u, uint8* dst_v, int width); -void RGB24ToUVRow_Any_NEON(const uint8* src_rgb24, int src_stride_rgb24, - uint8* dst_u, uint8* dst_v, int width); -void RAWToUVRow_Any_NEON(const uint8* src_raw, int src_stride_raw, - uint8* dst_u, uint8* dst_v, int width); -void RGB565ToUVRow_Any_NEON(const uint8* src_rgb565, int src_stride_rgb565, - uint8* dst_u, uint8* dst_v, int width); -void ARGB1555ToUVRow_Any_NEON(const uint8* src_argb1555, - int src_stride_argb1555, - uint8* dst_u, uint8* dst_v, int width); -void ARGB4444ToUVRow_Any_NEON(const uint8* src_argb4444, - int src_stride_argb4444, - uint8* dst_u, uint8* dst_v, int width); -void ARGBToUVRow_C(const uint8* src_argb, int src_stride_argb, - uint8* dst_u, uint8* dst_v, int width); -void ARGBToUVJRow_C(const uint8* src_argb, int src_stride_argb, - uint8* dst_u, uint8* dst_v, int width); -void BGRAToUVRow_C(const uint8* src_bgra, int src_stride_bgra, - uint8* dst_u, uint8* dst_v, int width); -void ABGRToUVRow_C(const uint8* src_abgr, int src_stride_abgr, - uint8* dst_u, uint8* dst_v, int width); -void RGBAToUVRow_C(const uint8* src_rgba, int src_stride_rgba, - uint8* dst_u, uint8* dst_v, int width); -void RGB24ToUVRow_C(const uint8* src_rgb24, int src_stride_rgb24, - uint8* dst_u, uint8* dst_v, int width); -void RAWToUVRow_C(const uint8* src_raw, int src_stride_raw, - uint8* dst_u, uint8* dst_v, int width); -void RGB565ToUVRow_C(const uint8* src_rgb565, int src_stride_rgb565, - uint8* dst_u, uint8* dst_v, int width); -void ARGB1555ToUVRow_C(const uint8* src_argb1555, int src_stride_argb1555, - uint8* dst_u, uint8* dst_v, int width); -void ARGB4444ToUVRow_C(const uint8* src_argb4444, int src_stride_argb4444, - uint8* dst_u, uint8* dst_v, int width); - -void ARGBToUV444Row_SSSE3(const uint8* src_argb, - uint8* dst_u, uint8* dst_v, int width); -void ARGBToUV444Row_Any_SSSE3(const uint8* src_argb, - uint8* dst_u, uint8* dst_v, int width); - -void ARGBToUV444Row_C(const uint8* src_argb, - uint8* dst_u, uint8* dst_v, int width); -void ARGBToUV411Row_C(const uint8* src_argb, - uint8* dst_u, uint8* dst_v, int width); - -void MirrorRow_AVX2(const uint8* src, uint8* dst, int width); -void MirrorRow_SSSE3(const uint8* src, uint8* dst, int width); -void MirrorRow_NEON(const uint8* src, uint8* dst, int width); -void MirrorRow_DSPR2(const uint8* src, uint8* dst, int width); -void MirrorRow_MSA(const uint8* src, uint8* dst, int width); -void MirrorRow_C(const uint8* src, uint8* dst, int width); -void MirrorRow_Any_AVX2(const uint8* src, uint8* dst, int width); -void MirrorRow_Any_SSSE3(const uint8* src, uint8* dst, int width); -void MirrorRow_Any_SSE2(const uint8* src, uint8* dst, int width); -void MirrorRow_Any_NEON(const uint8* src, uint8* dst, int width); -void MirrorRow_Any_MSA(const uint8* src, uint8* dst, int width); - -void MirrorUVRow_SSSE3(const uint8* src_uv, uint8* dst_u, uint8* dst_v, - int width); -void MirrorUVRow_NEON(const uint8* src_uv, uint8* dst_u, uint8* dst_v, - int width); -void MirrorUVRow_DSPR2(const uint8* src_uv, uint8* dst_u, uint8* dst_v, - int width); -void MirrorUVRow_C(const uint8* src_uv, uint8* dst_u, uint8* dst_v, int width); - -void ARGBMirrorRow_AVX2(const uint8* src, uint8* dst, int width); -void ARGBMirrorRow_SSE2(const uint8* src, uint8* dst, int width); -void ARGBMirrorRow_NEON(const uint8* src, uint8* dst, int width); -void ARGBMirrorRow_MSA(const uint8* src, uint8* dst, int width); -void ARGBMirrorRow_C(const uint8* src, uint8* dst, int width); -void ARGBMirrorRow_Any_AVX2(const uint8* src, uint8* dst, int width); -void ARGBMirrorRow_Any_SSE2(const uint8* src, uint8* dst, int width); -void ARGBMirrorRow_Any_NEON(const uint8* src, uint8* dst, int width); -void ARGBMirrorRow_Any_MSA(const uint8* src, uint8* dst, int width); - -void SplitUVRow_C(const uint8* src_uv, uint8* dst_u, uint8* dst_v, int width); -void SplitUVRow_SSE2(const uint8* src_uv, uint8* dst_u, uint8* dst_v, - int width); -void SplitUVRow_AVX2(const uint8* src_uv, uint8* dst_u, uint8* dst_v, - int width); -void SplitUVRow_NEON(const uint8* src_uv, uint8* dst_u, uint8* dst_v, - int width); -void SplitUVRow_DSPR2(const uint8* src_uv, uint8* dst_u, uint8* dst_v, - int width); -void SplitUVRow_Any_SSE2(const uint8* src_uv, uint8* dst_u, uint8* dst_v, - int width); -void SplitUVRow_Any_AVX2(const uint8* src_uv, uint8* dst_u, uint8* dst_v, - int width); -void SplitUVRow_Any_NEON(const uint8* src_uv, uint8* dst_u, uint8* dst_v, - int width); -void SplitUVRow_Any_DSPR2(const uint8* src_uv, uint8* dst_u, uint8* dst_v, - int width); - -void MergeUVRow_C(const uint8* src_u, const uint8* src_v, uint8* dst_uv, - int width); -void MergeUVRow_SSE2(const uint8* src_u, const uint8* src_v, uint8* dst_uv, - int width); -void MergeUVRow_AVX2(const uint8* src_u, const uint8* src_v, uint8* dst_uv, - int width); -void MergeUVRow_NEON(const uint8* src_u, const uint8* src_v, uint8* dst_uv, - int width); -void MergeUVRow_Any_SSE2(const uint8* src_u, const uint8* src_v, uint8* dst_uv, - int width); -void MergeUVRow_Any_AVX2(const uint8* src_u, const uint8* src_v, uint8* dst_uv, - int width); -void MergeUVRow_Any_NEON(const uint8* src_u, const uint8* src_v, uint8* dst_uv, - int width); - -void CopyRow_SSE2(const uint8* src, uint8* dst, int count); -void CopyRow_AVX(const uint8* src, uint8* dst, int count); -void CopyRow_ERMS(const uint8* src, uint8* dst, int count); -void CopyRow_NEON(const uint8* src, uint8* dst, int count); -void CopyRow_MIPS(const uint8* src, uint8* dst, int count); -void CopyRow_C(const uint8* src, uint8* dst, int count); -void CopyRow_Any_SSE2(const uint8* src, uint8* dst, int count); -void CopyRow_Any_AVX(const uint8* src, uint8* dst, int count); -void CopyRow_Any_NEON(const uint8* src, uint8* dst, int count); - -void CopyRow_16_C(const uint16* src, uint16* dst, int count); - -void ARGBCopyAlphaRow_C(const uint8* src_argb, uint8* dst_argb, int width); -void ARGBCopyAlphaRow_SSE2(const uint8* src_argb, uint8* dst_argb, int width); -void ARGBCopyAlphaRow_AVX2(const uint8* src_argb, uint8* dst_argb, int width); -void ARGBCopyAlphaRow_Any_SSE2(const uint8* src_argb, uint8* dst_argb, - int width); -void ARGBCopyAlphaRow_Any_AVX2(const uint8* src_argb, uint8* dst_argb, - int width); - -void ARGBExtractAlphaRow_C(const uint8* src_argb, uint8* dst_a, int width); -void ARGBExtractAlphaRow_SSE2(const uint8* src_argb, uint8* dst_a, int width); -void ARGBExtractAlphaRow_NEON(const uint8* src_argb, uint8* dst_a, int width); -void ARGBExtractAlphaRow_Any_SSE2(const uint8* src_argb, uint8* dst_a, - int width); -void ARGBExtractAlphaRow_Any_NEON(const uint8* src_argb, uint8* dst_a, - int width); - -void ARGBCopyYToAlphaRow_C(const uint8* src_y, uint8* dst_argb, int width); -void ARGBCopyYToAlphaRow_SSE2(const uint8* src_y, uint8* dst_argb, int width); -void ARGBCopyYToAlphaRow_AVX2(const uint8* src_y, uint8* dst_argb, int width); -void ARGBCopyYToAlphaRow_Any_SSE2(const uint8* src_y, uint8* dst_argb, - int width); -void ARGBCopyYToAlphaRow_Any_AVX2(const uint8* src_y, uint8* dst_argb, - int width); - -void SetRow_C(uint8* dst, uint8 v8, int count); -void SetRow_X86(uint8* dst, uint8 v8, int count); -void SetRow_ERMS(uint8* dst, uint8 v8, int count); -void SetRow_NEON(uint8* dst, uint8 v8, int count); -void SetRow_Any_X86(uint8* dst, uint8 v8, int count); -void SetRow_Any_NEON(uint8* dst, uint8 v8, int count); - -void ARGBSetRow_C(uint8* dst_argb, uint32 v32, int count); -void ARGBSetRow_X86(uint8* dst_argb, uint32 v32, int count); -void ARGBSetRow_NEON(uint8* dst_argb, uint32 v32, int count); -void ARGBSetRow_Any_NEON(uint8* dst_argb, uint32 v32, int count); - -// ARGBShufflers for BGRAToARGB etc. -void ARGBShuffleRow_C(const uint8* src_argb, uint8* dst_argb, - const uint8* shuffler, int width); -void ARGBShuffleRow_SSE2(const uint8* src_argb, uint8* dst_argb, - const uint8* shuffler, int width); -void ARGBShuffleRow_SSSE3(const uint8* src_argb, uint8* dst_argb, - const uint8* shuffler, int width); -void ARGBShuffleRow_AVX2(const uint8* src_argb, uint8* dst_argb, - const uint8* shuffler, int width); -void ARGBShuffleRow_NEON(const uint8* src_argb, uint8* dst_argb, - const uint8* shuffler, int width); -void ARGBShuffleRow_Any_SSE2(const uint8* src_argb, uint8* dst_argb, - const uint8* shuffler, int width); -void ARGBShuffleRow_Any_SSSE3(const uint8* src_argb, uint8* dst_argb, - const uint8* shuffler, int width); -void ARGBShuffleRow_Any_AVX2(const uint8* src_argb, uint8* dst_argb, - const uint8* shuffler, int width); -void ARGBShuffleRow_Any_NEON(const uint8* src_argb, uint8* dst_argb, - const uint8* shuffler, int width); - -void RGB24ToARGBRow_SSSE3(const uint8* src_rgb24, uint8* dst_argb, int width); -void RAWToARGBRow_SSSE3(const uint8* src_raw, uint8* dst_argb, int width); -void RAWToRGB24Row_SSSE3(const uint8* src_raw, uint8* dst_rgb24, int width); -void RGB565ToARGBRow_SSE2(const uint8* src_rgb565, uint8* dst_argb, int width); -void ARGB1555ToARGBRow_SSE2(const uint8* src_argb1555, uint8* dst_argb, - int width); -void ARGB4444ToARGBRow_SSE2(const uint8* src_argb4444, uint8* dst_argb, - int width); -void RGB565ToARGBRow_AVX2(const uint8* src_rgb565, uint8* dst_argb, int width); -void ARGB1555ToARGBRow_AVX2(const uint8* src_argb1555, uint8* dst_argb, - int width); -void ARGB4444ToARGBRow_AVX2(const uint8* src_argb4444, uint8* dst_argb, - int width); - -void RGB24ToARGBRow_NEON(const uint8* src_rgb24, uint8* dst_argb, int width); -void RAWToARGBRow_NEON(const uint8* src_raw, uint8* dst_argb, int width); -void RAWToRGB24Row_NEON(const uint8* src_raw, uint8* dst_rgb24, int width); -void RGB565ToARGBRow_NEON(const uint8* src_rgb565, uint8* dst_argb, int width); -void ARGB1555ToARGBRow_NEON(const uint8* src_argb1555, uint8* dst_argb, - int width); -void ARGB4444ToARGBRow_NEON(const uint8* src_argb4444, uint8* dst_argb, - int width); -void RGB24ToARGBRow_C(const uint8* src_rgb24, uint8* dst_argb, int width); -void RAWToARGBRow_C(const uint8* src_raw, uint8* dst_argb, int width); -void RAWToRGB24Row_C(const uint8* src_raw, uint8* dst_rgb24, int width); -void RGB565ToARGBRow_C(const uint8* src_rgb, uint8* dst_argb, int width); -void ARGB1555ToARGBRow_C(const uint8* src_argb, uint8* dst_argb, int width); -void ARGB4444ToARGBRow_C(const uint8* src_argb, uint8* dst_argb, int width); -void RGB24ToARGBRow_Any_SSSE3(const uint8* src_rgb24, uint8* dst_argb, - int width); -void RAWToARGBRow_Any_SSSE3(const uint8* src_raw, uint8* dst_argb, int width); -void RAWToRGB24Row_Any_SSSE3(const uint8* src_raw, uint8* dst_rgb24, int width); - -void RGB565ToARGBRow_Any_SSE2(const uint8* src_rgb565, uint8* dst_argb, - int width); -void ARGB1555ToARGBRow_Any_SSE2(const uint8* src_argb1555, uint8* dst_argb, - int width); -void ARGB4444ToARGBRow_Any_SSE2(const uint8* src_argb4444, uint8* dst_argb, - int width); -void RGB565ToARGBRow_Any_AVX2(const uint8* src_rgb565, uint8* dst_argb, - int width); -void ARGB1555ToARGBRow_Any_AVX2(const uint8* src_argb1555, uint8* dst_argb, - int width); -void ARGB4444ToARGBRow_Any_AVX2(const uint8* src_argb4444, uint8* dst_argb, - int width); - -void RGB24ToARGBRow_Any_NEON(const uint8* src_rgb24, uint8* dst_argb, - int width); -void RAWToARGBRow_Any_NEON(const uint8* src_raw, uint8* dst_argb, int width); -void RAWToRGB24Row_Any_NEON(const uint8* src_raw, uint8* dst_rgb24, int width); -void RGB565ToARGBRow_Any_NEON(const uint8* src_rgb565, uint8* dst_argb, - int width); -void ARGB1555ToARGBRow_Any_NEON(const uint8* src_argb1555, uint8* dst_argb, - int width); -void ARGB4444ToARGBRow_Any_NEON(const uint8* src_argb4444, uint8* dst_argb, - int width); - -void ARGBToRGB24Row_SSSE3(const uint8* src_argb, uint8* dst_rgb, int width); -void ARGBToRAWRow_SSSE3(const uint8* src_argb, uint8* dst_rgb, int width); -void ARGBToRGB565Row_SSE2(const uint8* src_argb, uint8* dst_rgb, int width); -void ARGBToARGB1555Row_SSE2(const uint8* src_argb, uint8* dst_rgb, int width); -void ARGBToARGB4444Row_SSE2(const uint8* src_argb, uint8* dst_rgb, int width); - -void ARGBToRGB565DitherRow_C(const uint8* src_argb, uint8* dst_rgb, - const uint32 dither4, int width); -void ARGBToRGB565DitherRow_SSE2(const uint8* src_argb, uint8* dst_rgb, - const uint32 dither4, int width); -void ARGBToRGB565DitherRow_AVX2(const uint8* src_argb, uint8* dst_rgb, - const uint32 dither4, int width); - -void ARGBToRGB565Row_AVX2(const uint8* src_argb, uint8* dst_rgb, int width); -void ARGBToARGB1555Row_AVX2(const uint8* src_argb, uint8* dst_rgb, int width); -void ARGBToARGB4444Row_AVX2(const uint8* src_argb, uint8* dst_rgb, int width); - -void ARGBToRGB24Row_NEON(const uint8* src_argb, uint8* dst_rgb, int width); -void ARGBToRAWRow_NEON(const uint8* src_argb, uint8* dst_rgb, int width); -void ARGBToRGB565Row_NEON(const uint8* src_argb, uint8* dst_rgb, int width); -void ARGBToARGB1555Row_NEON(const uint8* src_argb, uint8* dst_rgb, int width); -void ARGBToARGB4444Row_NEON(const uint8* src_argb, uint8* dst_rgb, int width); -void ARGBToRGB565DitherRow_NEON(const uint8* src_argb, uint8* dst_rgb, - const uint32 dither4, int width); - -void ARGBToRGBARow_C(const uint8* src_argb, uint8* dst_rgb, int width); -void ARGBToRGB24Row_C(const uint8* src_argb, uint8* dst_rgb, int width); -void ARGBToRAWRow_C(const uint8* src_argb, uint8* dst_rgb, int width); -void ARGBToRGB565Row_C(const uint8* src_argb, uint8* dst_rgb, int width); -void ARGBToARGB1555Row_C(const uint8* src_argb, uint8* dst_rgb, int width); -void ARGBToARGB4444Row_C(const uint8* src_argb, uint8* dst_rgb, int width); - -void J400ToARGBRow_SSE2(const uint8* src_y, uint8* dst_argb, int width); -void J400ToARGBRow_AVX2(const uint8* src_y, uint8* dst_argb, int width); -void J400ToARGBRow_NEON(const uint8* src_y, uint8* dst_argb, int width); -void J400ToARGBRow_C(const uint8* src_y, uint8* dst_argb, int width); -void J400ToARGBRow_Any_SSE2(const uint8* src_y, uint8* dst_argb, int width); -void J400ToARGBRow_Any_AVX2(const uint8* src_y, uint8* dst_argb, int width); -void J400ToARGBRow_Any_NEON(const uint8* src_y, uint8* dst_argb, int width); - -void I444ToARGBRow_C(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void I422ToARGBRow_C(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void I422ToARGBRow_C(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void I422AlphaToARGBRow_C(const uint8* y_buf, - const uint8* u_buf, - const uint8* v_buf, - const uint8* a_buf, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void I411ToARGBRow_C(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void NV12ToARGBRow_C(const uint8* src_y, - const uint8* src_uv, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void NV12ToRGB565Row_C(const uint8* src_y, - const uint8* src_uv, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void NV21ToARGBRow_C(const uint8* src_y, - const uint8* src_uv, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void YUY2ToARGBRow_C(const uint8* src_yuy2, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void UYVYToARGBRow_C(const uint8* src_uyvy, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void I422ToRGBARow_C(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_rgba, - const struct YuvConstants* yuvconstants, - int width); -void I422ToRGB24Row_C(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_rgb24, - const struct YuvConstants* yuvconstants, - int width); -void I422ToARGB4444Row_C(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_argb4444, - const struct YuvConstants* yuvconstants, - int width); -void I422ToARGB1555Row_C(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_argb4444, - const struct YuvConstants* yuvconstants, - int width); -void I422ToRGB565Row_C(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_rgb565, - const struct YuvConstants* yuvconstants, - int width); -void I422ToARGBRow_AVX2(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void I422ToARGBRow_AVX2(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void I422ToRGBARow_AVX2(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void I444ToARGBRow_SSSE3(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void I444ToARGBRow_AVX2(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void I444ToARGBRow_SSSE3(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void I444ToARGBRow_AVX2(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void I422ToARGBRow_SSSE3(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void I422AlphaToARGBRow_SSSE3(const uint8* y_buf, - const uint8* u_buf, - const uint8* v_buf, - const uint8* a_buf, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void I422AlphaToARGBRow_AVX2(const uint8* y_buf, - const uint8* u_buf, - const uint8* v_buf, - const uint8* a_buf, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void I422ToARGBRow_SSSE3(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void I411ToARGBRow_SSSE3(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void I411ToARGBRow_AVX2(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void NV12ToARGBRow_SSSE3(const uint8* src_y, - const uint8* src_uv, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void NV12ToARGBRow_AVX2(const uint8* src_y, - const uint8* src_uv, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void NV12ToRGB565Row_SSSE3(const uint8* src_y, - const uint8* src_uv, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void NV12ToRGB565Row_AVX2(const uint8* src_y, - const uint8* src_uv, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void NV21ToARGBRow_SSSE3(const uint8* src_y, - const uint8* src_uv, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void NV21ToARGBRow_AVX2(const uint8* src_y, - const uint8* src_uv, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void YUY2ToARGBRow_SSSE3(const uint8* src_yuy2, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void UYVYToARGBRow_SSSE3(const uint8* src_uyvy, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void YUY2ToARGBRow_AVX2(const uint8* src_yuy2, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void UYVYToARGBRow_AVX2(const uint8* src_uyvy, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void I422ToRGBARow_SSSE3(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_rgba, - const struct YuvConstants* yuvconstants, - int width); -void I422ToARGB4444Row_SSSE3(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void I422ToARGB4444Row_AVX2(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void I422ToARGB1555Row_SSSE3(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void I422ToARGB1555Row_AVX2(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void I422ToRGB565Row_SSSE3(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void I422ToRGB565Row_AVX2(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void I422ToRGB24Row_SSSE3(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_rgb24, - const struct YuvConstants* yuvconstants, - int width); -void I422ToRGB24Row_AVX2(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_rgb24, - const struct YuvConstants* yuvconstants, - int width); -void I422ToARGBRow_Any_AVX2(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void I422ToRGBARow_Any_AVX2(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void I444ToARGBRow_Any_SSSE3(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void I444ToARGBRow_Any_AVX2(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void I422ToARGBRow_Any_SSSE3(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void I422AlphaToARGBRow_Any_SSSE3(const uint8* y_buf, - const uint8* u_buf, - const uint8* v_buf, - const uint8* a_buf, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void I422AlphaToARGBRow_Any_AVX2(const uint8* y_buf, - const uint8* u_buf, - const uint8* v_buf, - const uint8* a_buf, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void I411ToARGBRow_Any_SSSE3(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void I411ToARGBRow_Any_AVX2(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void NV12ToARGBRow_Any_SSSE3(const uint8* src_y, - const uint8* src_uv, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void NV12ToARGBRow_Any_AVX2(const uint8* src_y, - const uint8* src_uv, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void NV21ToARGBRow_Any_SSSE3(const uint8* src_y, - const uint8* src_vu, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void NV21ToARGBRow_Any_AVX2(const uint8* src_y, - const uint8* src_vu, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void NV12ToRGB565Row_Any_SSSE3(const uint8* src_y, - const uint8* src_uv, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void NV12ToRGB565Row_Any_AVX2(const uint8* src_y, - const uint8* src_uv, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void YUY2ToARGBRow_Any_SSSE3(const uint8* src_yuy2, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void UYVYToARGBRow_Any_SSSE3(const uint8* src_uyvy, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void YUY2ToARGBRow_Any_AVX2(const uint8* src_yuy2, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void UYVYToARGBRow_Any_AVX2(const uint8* src_uyvy, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void I422ToRGBARow_Any_SSSE3(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_rgba, - const struct YuvConstants* yuvconstants, - int width); -void I422ToARGB4444Row_Any_SSSE3(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_rgba, - const struct YuvConstants* yuvconstants, - int width); -void I422ToARGB4444Row_Any_AVX2(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_rgba, - const struct YuvConstants* yuvconstants, - int width); -void I422ToARGB1555Row_Any_SSSE3(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_rgba, - const struct YuvConstants* yuvconstants, - int width); -void I422ToARGB1555Row_Any_AVX2(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_rgba, - const struct YuvConstants* yuvconstants, - int width); -void I422ToRGB565Row_Any_SSSE3(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_rgba, - const struct YuvConstants* yuvconstants, - int width); -void I422ToRGB565Row_Any_AVX2(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_rgba, - const struct YuvConstants* yuvconstants, - int width); -void I422ToRGB24Row_Any_SSSE3(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void I422ToRGB24Row_Any_AVX2(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); - -void I400ToARGBRow_C(const uint8* src_y, uint8* dst_argb, int width); -void I400ToARGBRow_SSE2(const uint8* src_y, uint8* dst_argb, int width); -void I400ToARGBRow_AVX2(const uint8* src_y, uint8* dst_argb, int width); -void I400ToARGBRow_NEON(const uint8* src_y, uint8* dst_argb, int width); -void I400ToARGBRow_Any_SSE2(const uint8* src_y, uint8* dst_argb, int width); -void I400ToARGBRow_Any_AVX2(const uint8* src_y, uint8* dst_argb, int width); -void I400ToARGBRow_Any_NEON(const uint8* src_y, uint8* dst_argb, int width); - -// ARGB preattenuated alpha blend. -void ARGBBlendRow_SSSE3(const uint8* src_argb, const uint8* src_argb1, - uint8* dst_argb, int width); -void ARGBBlendRow_NEON(const uint8* src_argb, const uint8* src_argb1, - uint8* dst_argb, int width); -void ARGBBlendRow_C(const uint8* src_argb, const uint8* src_argb1, - uint8* dst_argb, int width); - -// Unattenuated planar alpha blend. -void BlendPlaneRow_SSSE3(const uint8* src0, const uint8* src1, - const uint8* alpha, uint8* dst, int width); -void BlendPlaneRow_Any_SSSE3(const uint8* src0, const uint8* src1, - const uint8* alpha, uint8* dst, int width); -void BlendPlaneRow_AVX2(const uint8* src0, const uint8* src1, - const uint8* alpha, uint8* dst, int width); -void BlendPlaneRow_Any_AVX2(const uint8* src0, const uint8* src1, - const uint8* alpha, uint8* dst, int width); -void BlendPlaneRow_C(const uint8* src0, const uint8* src1, - const uint8* alpha, uint8* dst, int width); - -// ARGB multiply images. Same API as Blend, but these require -// pointer and width alignment for SSE2. -void ARGBMultiplyRow_C(const uint8* src_argb, const uint8* src_argb1, - uint8* dst_argb, int width); -void ARGBMultiplyRow_SSE2(const uint8* src_argb, const uint8* src_argb1, - uint8* dst_argb, int width); -void ARGBMultiplyRow_Any_SSE2(const uint8* src_argb, const uint8* src_argb1, - uint8* dst_argb, int width); -void ARGBMultiplyRow_AVX2(const uint8* src_argb, const uint8* src_argb1, - uint8* dst_argb, int width); -void ARGBMultiplyRow_Any_AVX2(const uint8* src_argb, const uint8* src_argb1, - uint8* dst_argb, int width); -void ARGBMultiplyRow_NEON(const uint8* src_argb, const uint8* src_argb1, - uint8* dst_argb, int width); -void ARGBMultiplyRow_Any_NEON(const uint8* src_argb, const uint8* src_argb1, - uint8* dst_argb, int width); - -// ARGB add images. -void ARGBAddRow_C(const uint8* src_argb, const uint8* src_argb1, - uint8* dst_argb, int width); -void ARGBAddRow_SSE2(const uint8* src_argb, const uint8* src_argb1, - uint8* dst_argb, int width); -void ARGBAddRow_Any_SSE2(const uint8* src_argb, const uint8* src_argb1, - uint8* dst_argb, int width); -void ARGBAddRow_AVX2(const uint8* src_argb, const uint8* src_argb1, - uint8* dst_argb, int width); -void ARGBAddRow_Any_AVX2(const uint8* src_argb, const uint8* src_argb1, - uint8* dst_argb, int width); -void ARGBAddRow_NEON(const uint8* src_argb, const uint8* src_argb1, - uint8* dst_argb, int width); -void ARGBAddRow_Any_NEON(const uint8* src_argb, const uint8* src_argb1, - uint8* dst_argb, int width); - -// ARGB subtract images. Same API as Blend, but these require -// pointer and width alignment for SSE2. -void ARGBSubtractRow_C(const uint8* src_argb, const uint8* src_argb1, - uint8* dst_argb, int width); -void ARGBSubtractRow_SSE2(const uint8* src_argb, const uint8* src_argb1, - uint8* dst_argb, int width); -void ARGBSubtractRow_Any_SSE2(const uint8* src_argb, const uint8* src_argb1, - uint8* dst_argb, int width); -void ARGBSubtractRow_AVX2(const uint8* src_argb, const uint8* src_argb1, - uint8* dst_argb, int width); -void ARGBSubtractRow_Any_AVX2(const uint8* src_argb, const uint8* src_argb1, - uint8* dst_argb, int width); -void ARGBSubtractRow_NEON(const uint8* src_argb, const uint8* src_argb1, - uint8* dst_argb, int width); -void ARGBSubtractRow_Any_NEON(const uint8* src_argb, const uint8* src_argb1, - uint8* dst_argb, int width); - -void ARGBToRGB24Row_Any_SSSE3(const uint8* src_argb, uint8* dst_rgb, int width); -void ARGBToRAWRow_Any_SSSE3(const uint8* src_argb, uint8* dst_rgb, int width); -void ARGBToRGB565Row_Any_SSE2(const uint8* src_argb, uint8* dst_rgb, int width); -void ARGBToARGB1555Row_Any_SSE2(const uint8* src_argb, uint8* dst_rgb, - int width); -void ARGBToARGB4444Row_Any_SSE2(const uint8* src_argb, uint8* dst_rgb, - int width); - -void ARGBToRGB565DitherRow_Any_SSE2(const uint8* src_argb, uint8* dst_rgb, - const uint32 dither4, int width); -void ARGBToRGB565DitherRow_Any_AVX2(const uint8* src_argb, uint8* dst_rgb, - const uint32 dither4, int width); - -void ARGBToRGB565Row_Any_AVX2(const uint8* src_argb, uint8* dst_rgb, int width); -void ARGBToARGB1555Row_Any_AVX2(const uint8* src_argb, uint8* dst_rgb, - int width); -void ARGBToARGB4444Row_Any_AVX2(const uint8* src_argb, uint8* dst_rgb, - int width); - -void ARGBToRGB24Row_Any_NEON(const uint8* src_argb, uint8* dst_rgb, int width); -void ARGBToRAWRow_Any_NEON(const uint8* src_argb, uint8* dst_rgb, int width); -void ARGBToRGB565Row_Any_NEON(const uint8* src_argb, uint8* dst_rgb, int width); -void ARGBToARGB1555Row_Any_NEON(const uint8* src_argb, uint8* dst_rgb, - int width); -void ARGBToARGB4444Row_Any_NEON(const uint8* src_argb, uint8* dst_rgb, - int width); -void ARGBToRGB565DitherRow_Any_NEON(const uint8* src_argb, uint8* dst_rgb, - const uint32 dither4, int width); - -void I444ToARGBRow_Any_NEON(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void I422ToARGBRow_Any_NEON(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void I422AlphaToARGBRow_Any_NEON(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - const uint8* src_a, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void I411ToARGBRow_Any_NEON(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void I422ToRGBARow_Any_NEON(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void I422ToRGB24Row_Any_NEON(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void I422ToARGB4444Row_Any_NEON(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void I422ToARGB1555Row_Any_NEON(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void I422ToRGB565Row_Any_NEON(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void NV12ToARGBRow_Any_NEON(const uint8* src_y, - const uint8* src_uv, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void NV21ToARGBRow_Any_NEON(const uint8* src_y, - const uint8* src_vu, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void NV12ToRGB565Row_Any_NEON(const uint8* src_y, - const uint8* src_uv, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void YUY2ToARGBRow_Any_NEON(const uint8* src_yuy2, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void UYVYToARGBRow_Any_NEON(const uint8* src_uyvy, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void I422ToARGBRow_DSPR2(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void I422ToARGBRow_DSPR2(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); - -void YUY2ToYRow_AVX2(const uint8* src_yuy2, uint8* dst_y, int width); -void YUY2ToUVRow_AVX2(const uint8* src_yuy2, int stride_yuy2, - uint8* dst_u, uint8* dst_v, int width); -void YUY2ToUV422Row_AVX2(const uint8* src_yuy2, - uint8* dst_u, uint8* dst_v, int width); -void YUY2ToYRow_SSE2(const uint8* src_yuy2, uint8* dst_y, int width); -void YUY2ToUVRow_SSE2(const uint8* src_yuy2, int stride_yuy2, - uint8* dst_u, uint8* dst_v, int width); -void YUY2ToUV422Row_SSE2(const uint8* src_yuy2, - uint8* dst_u, uint8* dst_v, int width); -void YUY2ToYRow_NEON(const uint8* src_yuy2, uint8* dst_y, int width); -void YUY2ToUVRow_NEON(const uint8* src_yuy2, int stride_yuy2, - uint8* dst_u, uint8* dst_v, int width); -void YUY2ToUV422Row_NEON(const uint8* src_yuy2, - uint8* dst_u, uint8* dst_v, int width); -void YUY2ToYRow_C(const uint8* src_yuy2, uint8* dst_y, int width); -void YUY2ToUVRow_C(const uint8* src_yuy2, int stride_yuy2, - uint8* dst_u, uint8* dst_v, int width); -void YUY2ToUV422Row_C(const uint8* src_yuy2, - uint8* dst_u, uint8* dst_v, int width); -void YUY2ToYRow_Any_AVX2(const uint8* src_yuy2, uint8* dst_y, int width); -void YUY2ToUVRow_Any_AVX2(const uint8* src_yuy2, int stride_yuy2, - uint8* dst_u, uint8* dst_v, int width); -void YUY2ToUV422Row_Any_AVX2(const uint8* src_yuy2, - uint8* dst_u, uint8* dst_v, int width); -void YUY2ToYRow_Any_SSE2(const uint8* src_yuy2, uint8* dst_y, int width); -void YUY2ToUVRow_Any_SSE2(const uint8* src_yuy2, int stride_yuy2, - uint8* dst_u, uint8* dst_v, int width); -void YUY2ToUV422Row_Any_SSE2(const uint8* src_yuy2, - uint8* dst_u, uint8* dst_v, int width); -void YUY2ToYRow_Any_NEON(const uint8* src_yuy2, uint8* dst_y, int width); -void YUY2ToUVRow_Any_NEON(const uint8* src_yuy2, int stride_yuy2, - uint8* dst_u, uint8* dst_v, int width); -void YUY2ToUV422Row_Any_NEON(const uint8* src_yuy2, - uint8* dst_u, uint8* dst_v, int width); -void UYVYToYRow_AVX2(const uint8* src_uyvy, uint8* dst_y, int width); -void UYVYToUVRow_AVX2(const uint8* src_uyvy, int stride_uyvy, - uint8* dst_u, uint8* dst_v, int width); -void UYVYToUV422Row_AVX2(const uint8* src_uyvy, - uint8* dst_u, uint8* dst_v, int width); -void UYVYToYRow_SSE2(const uint8* src_uyvy, uint8* dst_y, int width); -void UYVYToUVRow_SSE2(const uint8* src_uyvy, int stride_uyvy, - uint8* dst_u, uint8* dst_v, int width); -void UYVYToUV422Row_SSE2(const uint8* src_uyvy, - uint8* dst_u, uint8* dst_v, int width); -void UYVYToYRow_AVX2(const uint8* src_uyvy, uint8* dst_y, int width); -void UYVYToUVRow_AVX2(const uint8* src_uyvy, int stride_uyvy, - uint8* dst_u, uint8* dst_v, int width); -void UYVYToUV422Row_AVX2(const uint8* src_uyvy, - uint8* dst_u, uint8* dst_v, int width); -void UYVYToYRow_NEON(const uint8* src_uyvy, uint8* dst_y, int width); -void UYVYToUVRow_NEON(const uint8* src_uyvy, int stride_uyvy, - uint8* dst_u, uint8* dst_v, int width); -void UYVYToUV422Row_NEON(const uint8* src_uyvy, - uint8* dst_u, uint8* dst_v, int width); - -void UYVYToYRow_C(const uint8* src_uyvy, uint8* dst_y, int width); -void UYVYToUVRow_C(const uint8* src_uyvy, int stride_uyvy, - uint8* dst_u, uint8* dst_v, int width); -void UYVYToUV422Row_C(const uint8* src_uyvy, - uint8* dst_u, uint8* dst_v, int width); -void UYVYToYRow_Any_AVX2(const uint8* src_uyvy, uint8* dst_y, int width); -void UYVYToUVRow_Any_AVX2(const uint8* src_uyvy, int stride_uyvy, - uint8* dst_u, uint8* dst_v, int width); -void UYVYToUV422Row_Any_AVX2(const uint8* src_uyvy, - uint8* dst_u, uint8* dst_v, int width); -void UYVYToYRow_Any_SSE2(const uint8* src_uyvy, uint8* dst_y, int width); -void UYVYToUVRow_Any_SSE2(const uint8* src_uyvy, int stride_uyvy, - uint8* dst_u, uint8* dst_v, int width); -void UYVYToUV422Row_Any_SSE2(const uint8* src_uyvy, - uint8* dst_u, uint8* dst_v, int width); -void UYVYToYRow_Any_NEON(const uint8* src_uyvy, uint8* dst_y, int width); -void UYVYToUVRow_Any_NEON(const uint8* src_uyvy, int stride_uyvy, - uint8* dst_u, uint8* dst_v, int width); -void UYVYToUV422Row_Any_NEON(const uint8* src_uyvy, - uint8* dst_u, uint8* dst_v, int width); - -void I422ToYUY2Row_C(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_yuy2, int width); -void I422ToUYVYRow_C(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_uyvy, int width); -void I422ToYUY2Row_SSE2(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_yuy2, int width); -void I422ToUYVYRow_SSE2(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_uyvy, int width); -void I422ToYUY2Row_Any_SSE2(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_yuy2, int width); -void I422ToUYVYRow_Any_SSE2(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_uyvy, int width); -void I422ToYUY2Row_NEON(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_yuy2, int width); -void I422ToUYVYRow_NEON(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_uyvy, int width); -void I422ToYUY2Row_Any_NEON(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_yuy2, int width); -void I422ToUYVYRow_Any_NEON(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_uyvy, int width); - -// Effects related row functions. -void ARGBAttenuateRow_C(const uint8* src_argb, uint8* dst_argb, int width); -void ARGBAttenuateRow_SSSE3(const uint8* src_argb, uint8* dst_argb, int width); -void ARGBAttenuateRow_AVX2(const uint8* src_argb, uint8* dst_argb, int width); -void ARGBAttenuateRow_NEON(const uint8* src_argb, uint8* dst_argb, int width); -void ARGBAttenuateRow_Any_SSE2(const uint8* src_argb, uint8* dst_argb, - int width); -void ARGBAttenuateRow_Any_SSSE3(const uint8* src_argb, uint8* dst_argb, - int width); -void ARGBAttenuateRow_Any_AVX2(const uint8* src_argb, uint8* dst_argb, - int width); -void ARGBAttenuateRow_Any_NEON(const uint8* src_argb, uint8* dst_argb, - int width); - -// Inverse table for unattenuate, shared by C and SSE2. -extern const uint32 fixed_invtbl8[256]; -void ARGBUnattenuateRow_C(const uint8* src_argb, uint8* dst_argb, int width); -void ARGBUnattenuateRow_SSE2(const uint8* src_argb, uint8* dst_argb, int width); -void ARGBUnattenuateRow_AVX2(const uint8* src_argb, uint8* dst_argb, int width); -void ARGBUnattenuateRow_Any_SSE2(const uint8* src_argb, uint8* dst_argb, - int width); -void ARGBUnattenuateRow_Any_AVX2(const uint8* src_argb, uint8* dst_argb, - int width); - -void ARGBGrayRow_C(const uint8* src_argb, uint8* dst_argb, int width); -void ARGBGrayRow_SSSE3(const uint8* src_argb, uint8* dst_argb, int width); -void ARGBGrayRow_NEON(const uint8* src_argb, uint8* dst_argb, int width); - -void ARGBSepiaRow_C(uint8* dst_argb, int width); -void ARGBSepiaRow_SSSE3(uint8* dst_argb, int width); -void ARGBSepiaRow_NEON(uint8* dst_argb, int width); - -void ARGBColorMatrixRow_C(const uint8* src_argb, uint8* dst_argb, - const int8* matrix_argb, int width); -void ARGBColorMatrixRow_SSSE3(const uint8* src_argb, uint8* dst_argb, - const int8* matrix_argb, int width); -void ARGBColorMatrixRow_NEON(const uint8* src_argb, uint8* dst_argb, - const int8* matrix_argb, int width); - -void ARGBColorTableRow_C(uint8* dst_argb, const uint8* table_argb, int width); -void ARGBColorTableRow_X86(uint8* dst_argb, const uint8* table_argb, int width); - -void RGBColorTableRow_C(uint8* dst_argb, const uint8* table_argb, int width); -void RGBColorTableRow_X86(uint8* dst_argb, const uint8* table_argb, int width); - -void ARGBQuantizeRow_C(uint8* dst_argb, int scale, int interval_size, - int interval_offset, int width); -void ARGBQuantizeRow_SSE2(uint8* dst_argb, int scale, int interval_size, - int interval_offset, int width); -void ARGBQuantizeRow_NEON(uint8* dst_argb, int scale, int interval_size, - int interval_offset, int width); - -void ARGBShadeRow_C(const uint8* src_argb, uint8* dst_argb, int width, - uint32 value); -void ARGBShadeRow_SSE2(const uint8* src_argb, uint8* dst_argb, int width, - uint32 value); -void ARGBShadeRow_NEON(const uint8* src_argb, uint8* dst_argb, int width, - uint32 value); - -// Used for blur. -void CumulativeSumToAverageRow_SSE2(const int32* topleft, const int32* botleft, - int width, int area, uint8* dst, int count); -void ComputeCumulativeSumRow_SSE2(const uint8* row, int32* cumsum, - const int32* previous_cumsum, int width); - -void CumulativeSumToAverageRow_C(const int32* topleft, const int32* botleft, - int width, int area, uint8* dst, int count); -void ComputeCumulativeSumRow_C(const uint8* row, int32* cumsum, - const int32* previous_cumsum, int width); - -LIBYUV_API -void ARGBAffineRow_C(const uint8* src_argb, int src_argb_stride, - uint8* dst_argb, const float* uv_dudv, int width); -LIBYUV_API -void ARGBAffineRow_SSE2(const uint8* src_argb, int src_argb_stride, - uint8* dst_argb, const float* uv_dudv, int width); - -// Used for I420Scale, ARGBScale, and ARGBInterpolate. -void InterpolateRow_C(uint8* dst_ptr, const uint8* src_ptr, - ptrdiff_t src_stride_ptr, - int width, int source_y_fraction); -void InterpolateRow_SSSE3(uint8* dst_ptr, const uint8* src_ptr, - ptrdiff_t src_stride_ptr, int width, - int source_y_fraction); -void InterpolateRow_AVX2(uint8* dst_ptr, const uint8* src_ptr, - ptrdiff_t src_stride_ptr, int width, - int source_y_fraction); -void InterpolateRow_NEON(uint8* dst_ptr, const uint8* src_ptr, - ptrdiff_t src_stride_ptr, int width, - int source_y_fraction); -void InterpolateRow_DSPR2(uint8* dst_ptr, const uint8* src_ptr, - ptrdiff_t src_stride_ptr, int width, - int source_y_fraction); -void InterpolateRow_Any_NEON(uint8* dst_ptr, const uint8* src_ptr, - ptrdiff_t src_stride_ptr, int width, - int source_y_fraction); -void InterpolateRow_Any_SSSE3(uint8* dst_ptr, const uint8* src_ptr, - ptrdiff_t src_stride_ptr, int width, - int source_y_fraction); -void InterpolateRow_Any_AVX2(uint8* dst_ptr, const uint8* src_ptr, - ptrdiff_t src_stride_ptr, int width, - int source_y_fraction); -void InterpolateRow_Any_DSPR2(uint8* dst_ptr, const uint8* src_ptr, - ptrdiff_t src_stride_ptr, int width, - int source_y_fraction); - -void InterpolateRow_16_C(uint16* dst_ptr, const uint16* src_ptr, - ptrdiff_t src_stride_ptr, - int width, int source_y_fraction); - -// Sobel images. -void SobelXRow_C(const uint8* src_y0, const uint8* src_y1, const uint8* src_y2, - uint8* dst_sobelx, int width); -void SobelXRow_SSE2(const uint8* src_y0, const uint8* src_y1, - const uint8* src_y2, uint8* dst_sobelx, int width); -void SobelXRow_NEON(const uint8* src_y0, const uint8* src_y1, - const uint8* src_y2, uint8* dst_sobelx, int width); -void SobelYRow_C(const uint8* src_y0, const uint8* src_y1, - uint8* dst_sobely, int width); -void SobelYRow_SSE2(const uint8* src_y0, const uint8* src_y1, - uint8* dst_sobely, int width); -void SobelYRow_NEON(const uint8* src_y0, const uint8* src_y1, - uint8* dst_sobely, int width); -void SobelRow_C(const uint8* src_sobelx, const uint8* src_sobely, - uint8* dst_argb, int width); -void SobelRow_SSE2(const uint8* src_sobelx, const uint8* src_sobely, - uint8* dst_argb, int width); -void SobelRow_NEON(const uint8* src_sobelx, const uint8* src_sobely, - uint8* dst_argb, int width); -void SobelToPlaneRow_C(const uint8* src_sobelx, const uint8* src_sobely, - uint8* dst_y, int width); -void SobelToPlaneRow_SSE2(const uint8* src_sobelx, const uint8* src_sobely, - uint8* dst_y, int width); -void SobelToPlaneRow_NEON(const uint8* src_sobelx, const uint8* src_sobely, - uint8* dst_y, int width); -void SobelXYRow_C(const uint8* src_sobelx, const uint8* src_sobely, - uint8* dst_argb, int width); -void SobelXYRow_SSE2(const uint8* src_sobelx, const uint8* src_sobely, - uint8* dst_argb, int width); -void SobelXYRow_NEON(const uint8* src_sobelx, const uint8* src_sobely, - uint8* dst_argb, int width); -void SobelRow_Any_SSE2(const uint8* src_sobelx, const uint8* src_sobely, - uint8* dst_argb, int width); -void SobelRow_Any_NEON(const uint8* src_sobelx, const uint8* src_sobely, - uint8* dst_argb, int width); -void SobelToPlaneRow_Any_SSE2(const uint8* src_sobelx, const uint8* src_sobely, - uint8* dst_y, int width); -void SobelToPlaneRow_Any_NEON(const uint8* src_sobelx, const uint8* src_sobely, - uint8* dst_y, int width); -void SobelXYRow_Any_SSE2(const uint8* src_sobelx, const uint8* src_sobely, - uint8* dst_argb, int width); -void SobelXYRow_Any_NEON(const uint8* src_sobelx, const uint8* src_sobely, - uint8* dst_argb, int width); - -void ARGBPolynomialRow_C(const uint8* src_argb, - uint8* dst_argb, const float* poly, - int width); -void ARGBPolynomialRow_SSE2(const uint8* src_argb, - uint8* dst_argb, const float* poly, - int width); -void ARGBPolynomialRow_AVX2(const uint8* src_argb, - uint8* dst_argb, const float* poly, - int width); - -// Scale and convert to half float. -void HalfFloatRow_C(const uint16* src, uint16* dst, float scale, int width); -void HalfFloatRow_AVX2(const uint16* src, uint16* dst, float scale, int width); -void HalfFloatRow_Any_AVX2(const uint16* src, uint16* dst, float scale, - int width); -void HalfFloatRow_SSE2(const uint16* src, uint16* dst, float scale, int width); -void HalfFloatRow_Any_SSE2(const uint16* src, uint16* dst, float scale, - int width); - -void ARGBLumaColorTableRow_C(const uint8* src_argb, uint8* dst_argb, int width, - const uint8* luma, uint32 lumacoeff); -void ARGBLumaColorTableRow_SSSE3(const uint8* src_argb, uint8* dst_argb, - int width, - const uint8* luma, uint32 lumacoeff); - -#ifdef __cplusplus -} // extern "C" -} // namespace libyuv -#endif - -#endif // INCLUDE_LIBYUV_ROW_H_ diff --git a/third_party/libyuv/include/libyuv/scale.h b/third_party/libyuv/include/libyuv/scale.h deleted file mode 100644 index ae14694598..0000000000 --- a/third_party/libyuv/include/libyuv/scale.h +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright 2011 The LibYuv Project Authors. All rights reserved. - * - * Use of this source code is governed by a BSD-style license - * that can be found in the LICENSE file in the root of the source - * tree. An additional intellectual property rights grant can be found - * in the file PATENTS. All contributing project authors may - * be found in the AUTHORS file in the root of the source tree. - */ - -#ifndef INCLUDE_LIBYUV_SCALE_H_ -#define INCLUDE_LIBYUV_SCALE_H_ - -#include "libyuv/basic_types.h" - -#ifdef __cplusplus -namespace libyuv { -extern "C" { -#endif - -// Supported filtering. -typedef enum FilterMode { - kFilterNone = 0, // Point sample; Fastest. - kFilterLinear = 1, // Filter horizontally only. - kFilterBilinear = 2, // Faster than box, but lower quality scaling down. - kFilterBox = 3 // Highest quality. -} FilterModeEnum; - -// Scale a YUV plane. -LIBYUV_API -void ScalePlane(const uint8* src, int src_stride, - int src_width, int src_height, - uint8* dst, int dst_stride, - int dst_width, int dst_height, - enum FilterMode filtering); - -LIBYUV_API -void ScalePlane_16(const uint16* src, int src_stride, - int src_width, int src_height, - uint16* dst, int dst_stride, - int dst_width, int dst_height, - enum FilterMode filtering); - -// Scales a YUV 4:2:0 image from the src width and height to the -// dst width and height. -// If filtering is kFilterNone, a simple nearest-neighbor algorithm is -// used. This produces basic (blocky) quality at the fastest speed. -// If filtering is kFilterBilinear, interpolation is used to produce a better -// quality image, at the expense of speed. -// If filtering is kFilterBox, averaging is used to produce ever better -// quality image, at further expense of speed. -// Returns 0 if successful. - -LIBYUV_API -int I420Scale(const uint8* src_y, int src_stride_y, - const uint8* src_u, int src_stride_u, - const uint8* src_v, int src_stride_v, - int src_width, int src_height, - uint8* dst_y, int dst_stride_y, - uint8* dst_u, int dst_stride_u, - uint8* dst_v, int dst_stride_v, - int dst_width, int dst_height, - enum FilterMode filtering); - -LIBYUV_API -int I420Scale_16(const uint16* src_y, int src_stride_y, - const uint16* src_u, int src_stride_u, - const uint16* src_v, int src_stride_v, - int src_width, int src_height, - uint16* dst_y, int dst_stride_y, - uint16* dst_u, int dst_stride_u, - uint16* dst_v, int dst_stride_v, - int dst_width, int dst_height, - enum FilterMode filtering); - -#ifdef __cplusplus -// Legacy API. Deprecated. -LIBYUV_API -int Scale(const uint8* src_y, const uint8* src_u, const uint8* src_v, - int src_stride_y, int src_stride_u, int src_stride_v, - int src_width, int src_height, - uint8* dst_y, uint8* dst_u, uint8* dst_v, - int dst_stride_y, int dst_stride_u, int dst_stride_v, - int dst_width, int dst_height, - LIBYUV_BOOL interpolate); - -// Legacy API. Deprecated. -LIBYUV_API -int ScaleOffset(const uint8* src_i420, int src_width, int src_height, - uint8* dst_i420, int dst_width, int dst_height, int dst_yoffset, - LIBYUV_BOOL interpolate); - -// For testing, allow disabling of specialized scalers. -LIBYUV_API -void SetUseReferenceImpl(LIBYUV_BOOL use); -#endif // __cplusplus - -#ifdef __cplusplus -} // extern "C" -} // namespace libyuv -#endif - -#endif // INCLUDE_LIBYUV_SCALE_H_ diff --git a/third_party/libyuv/include/libyuv/scale_argb.h b/third_party/libyuv/include/libyuv/scale_argb.h deleted file mode 100644 index 35cd191c0f..0000000000 --- a/third_party/libyuv/include/libyuv/scale_argb.h +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2012 The LibYuv Project Authors. All rights reserved. - * - * Use of this source code is governed by a BSD-style license - * that can be found in the LICENSE file in the root of the source - * tree. An additional intellectual property rights grant can be found - * in the file PATENTS. All contributing project authors may - * be found in the AUTHORS file in the root of the source tree. - */ - -#ifndef INCLUDE_LIBYUV_SCALE_ARGB_H_ -#define INCLUDE_LIBYUV_SCALE_ARGB_H_ - -#include "libyuv/basic_types.h" -#include "libyuv/scale.h" // For FilterMode - -#ifdef __cplusplus -namespace libyuv { -extern "C" { -#endif - -LIBYUV_API -int ARGBScale(const uint8* src_argb, int src_stride_argb, - int src_width, int src_height, - uint8* dst_argb, int dst_stride_argb, - int dst_width, int dst_height, - enum FilterMode filtering); - -// Clipped scale takes destination rectangle coordinates for clip values. -LIBYUV_API -int ARGBScaleClip(const uint8* src_argb, int src_stride_argb, - int src_width, int src_height, - uint8* dst_argb, int dst_stride_argb, - int dst_width, int dst_height, - int clip_x, int clip_y, int clip_width, int clip_height, - enum FilterMode filtering); - -// Scale with YUV conversion to ARGB and clipping. -LIBYUV_API -int YUVToARGBScaleClip(const uint8* src_y, int src_stride_y, - const uint8* src_u, int src_stride_u, - const uint8* src_v, int src_stride_v, - uint32 src_fourcc, - int src_width, int src_height, - uint8* dst_argb, int dst_stride_argb, - uint32 dst_fourcc, - int dst_width, int dst_height, - int clip_x, int clip_y, int clip_width, int clip_height, - enum FilterMode filtering); - -#ifdef __cplusplus -} // extern "C" -} // namespace libyuv -#endif - -#endif // INCLUDE_LIBYUV_SCALE_ARGB_H_ diff --git a/third_party/libyuv/include/libyuv/scale_row.h b/third_party/libyuv/include/libyuv/scale_row.h deleted file mode 100644 index 791fbf7d05..0000000000 --- a/third_party/libyuv/include/libyuv/scale_row.h +++ /dev/null @@ -1,503 +0,0 @@ -/* - * Copyright 2013 The LibYuv Project Authors. All rights reserved. - * - * Use of this source code is governed by a BSD-style license - * that can be found in the LICENSE file in the root of the source - * tree. An additional intellectual property rights grant can be found - * in the file PATENTS. All contributing project authors may - * be found in the AUTHORS file in the root of the source tree. - */ - -#ifndef INCLUDE_LIBYUV_SCALE_ROW_H_ -#define INCLUDE_LIBYUV_SCALE_ROW_H_ - -#include "libyuv/basic_types.h" -#include "libyuv/scale.h" - -#ifdef __cplusplus -namespace libyuv { -extern "C" { -#endif - -#if defined(__pnacl__) || defined(__CLR_VER) || \ - (defined(__i386__) && !defined(__SSE2__)) -#define LIBYUV_DISABLE_X86 -#endif -// MemorySanitizer does not support assembly code yet. http://crbug.com/344505 -#if defined(__has_feature) -#if __has_feature(memory_sanitizer) -#define LIBYUV_DISABLE_X86 -#endif -#endif - -// GCC >= 4.7.0 required for AVX2. -#if defined(__GNUC__) && (defined(__x86_64__) || defined(__i386__)) -#if (__GNUC__ > 4) || (__GNUC__ == 4 && (__GNUC_MINOR__ >= 7)) -#define GCC_HAS_AVX2 1 -#endif // GNUC >= 4.7 -#endif // __GNUC__ - -// clang >= 3.4.0 required for AVX2. -#if defined(__clang__) && (defined(__x86_64__) || defined(__i386__)) -#if (__clang_major__ > 3) || (__clang_major__ == 3 && (__clang_minor__ >= 4)) -#define CLANG_HAS_AVX2 1 -#endif // clang >= 3.4 -#endif // __clang__ - -// Visual C 2012 required for AVX2. -#if defined(_M_IX86) && !defined(__clang__) && \ - defined(_MSC_VER) && _MSC_VER >= 1700 -#define VISUALC_HAS_AVX2 1 -#endif // VisualStudio >= 2012 - -// The following are available on all x86 platforms: -#if !defined(LIBYUV_DISABLE_X86) && \ - (defined(_M_IX86) || defined(__x86_64__) || defined(__i386__)) -#define HAS_FIXEDDIV1_X86 -#define HAS_FIXEDDIV_X86 -#define HAS_SCALEARGBCOLS_SSE2 -#define HAS_SCALEARGBCOLSUP2_SSE2 -#define HAS_SCALEARGBFILTERCOLS_SSSE3 -#define HAS_SCALEARGBROWDOWN2_SSE2 -#define HAS_SCALEARGBROWDOWNEVEN_SSE2 -#define HAS_SCALECOLSUP2_SSE2 -#define HAS_SCALEFILTERCOLS_SSSE3 -#define HAS_SCALEROWDOWN2_SSSE3 -#define HAS_SCALEROWDOWN34_SSSE3 -#define HAS_SCALEROWDOWN38_SSSE3 -#define HAS_SCALEROWDOWN4_SSSE3 -#define HAS_SCALEADDROW_SSE2 -#endif - -// The following are available on all x86 platforms, but -// require VS2012, clang 3.4 or gcc 4.7. -// The code supports NaCL but requires a new compiler and validator. -#if !defined(LIBYUV_DISABLE_X86) && (defined(VISUALC_HAS_AVX2) || \ - defined(CLANG_HAS_AVX2) || defined(GCC_HAS_AVX2)) -#define HAS_SCALEADDROW_AVX2 -#define HAS_SCALEROWDOWN2_AVX2 -#define HAS_SCALEROWDOWN4_AVX2 -#endif - -// The following are available on Neon platforms: -#if !defined(LIBYUV_DISABLE_NEON) && !defined(__native_client__) && \ - (defined(__ARM_NEON__) || defined(LIBYUV_NEON) || defined(__aarch64__)) -#define HAS_SCALEARGBCOLS_NEON -#define HAS_SCALEARGBROWDOWN2_NEON -#define HAS_SCALEARGBROWDOWNEVEN_NEON -#define HAS_SCALEFILTERCOLS_NEON -#define HAS_SCALEROWDOWN2_NEON -#define HAS_SCALEROWDOWN34_NEON -#define HAS_SCALEROWDOWN38_NEON -#define HAS_SCALEROWDOWN4_NEON -#define HAS_SCALEARGBFILTERCOLS_NEON -#endif - -// The following are available on Mips platforms: -#if !defined(LIBYUV_DISABLE_MIPS) && !defined(__native_client__) && \ - defined(__mips__) && defined(__mips_dsp) && (__mips_dsp_rev >= 2) -#define HAS_SCALEROWDOWN2_DSPR2 -#define HAS_SCALEROWDOWN4_DSPR2 -#define HAS_SCALEROWDOWN34_DSPR2 -#define HAS_SCALEROWDOWN38_DSPR2 -#endif - -// Scale ARGB vertically with bilinear interpolation. -void ScalePlaneVertical(int src_height, - int dst_width, int dst_height, - int src_stride, int dst_stride, - const uint8* src_argb, uint8* dst_argb, - int x, int y, int dy, - int bpp, enum FilterMode filtering); - -void ScalePlaneVertical_16(int src_height, - int dst_width, int dst_height, - int src_stride, int dst_stride, - const uint16* src_argb, uint16* dst_argb, - int x, int y, int dy, - int wpp, enum FilterMode filtering); - -// Simplify the filtering based on scale factors. -enum FilterMode ScaleFilterReduce(int src_width, int src_height, - int dst_width, int dst_height, - enum FilterMode filtering); - -// Divide num by div and return as 16.16 fixed point result. -int FixedDiv_C(int num, int div); -int FixedDiv_X86(int num, int div); -// Divide num - 1 by div - 1 and return as 16.16 fixed point result. -int FixedDiv1_C(int num, int div); -int FixedDiv1_X86(int num, int div); -#ifdef HAS_FIXEDDIV_X86 -#define FixedDiv FixedDiv_X86 -#define FixedDiv1 FixedDiv1_X86 -#else -#define FixedDiv FixedDiv_C -#define FixedDiv1 FixedDiv1_C -#endif - -// Compute slope values for stepping. -void ScaleSlope(int src_width, int src_height, - int dst_width, int dst_height, - enum FilterMode filtering, - int* x, int* y, int* dx, int* dy); - -void ScaleRowDown2_C(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst, int dst_width); -void ScaleRowDown2_16_C(const uint16* src_ptr, ptrdiff_t src_stride, - uint16* dst, int dst_width); -void ScaleRowDown2Linear_C(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst, int dst_width); -void ScaleRowDown2Linear_16_C(const uint16* src_ptr, ptrdiff_t src_stride, - uint16* dst, int dst_width); -void ScaleRowDown2Box_C(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst, int dst_width); -void ScaleRowDown2Box_Odd_C(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst, int dst_width); -void ScaleRowDown2Box_16_C(const uint16* src_ptr, ptrdiff_t src_stride, - uint16* dst, int dst_width); -void ScaleRowDown4_C(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst, int dst_width); -void ScaleRowDown4_16_C(const uint16* src_ptr, ptrdiff_t src_stride, - uint16* dst, int dst_width); -void ScaleRowDown4Box_C(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst, int dst_width); -void ScaleRowDown4Box_16_C(const uint16* src_ptr, ptrdiff_t src_stride, - uint16* dst, int dst_width); -void ScaleRowDown34_C(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst, int dst_width); -void ScaleRowDown34_16_C(const uint16* src_ptr, ptrdiff_t src_stride, - uint16* dst, int dst_width); -void ScaleRowDown34_0_Box_C(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* d, int dst_width); -void ScaleRowDown34_0_Box_16_C(const uint16* src_ptr, ptrdiff_t src_stride, - uint16* d, int dst_width); -void ScaleRowDown34_1_Box_C(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* d, int dst_width); -void ScaleRowDown34_1_Box_16_C(const uint16* src_ptr, ptrdiff_t src_stride, - uint16* d, int dst_width); -void ScaleCols_C(uint8* dst_ptr, const uint8* src_ptr, - int dst_width, int x, int dx); -void ScaleCols_16_C(uint16* dst_ptr, const uint16* src_ptr, - int dst_width, int x, int dx); -void ScaleColsUp2_C(uint8* dst_ptr, const uint8* src_ptr, - int dst_width, int, int); -void ScaleColsUp2_16_C(uint16* dst_ptr, const uint16* src_ptr, - int dst_width, int, int); -void ScaleFilterCols_C(uint8* dst_ptr, const uint8* src_ptr, - int dst_width, int x, int dx); -void ScaleFilterCols_16_C(uint16* dst_ptr, const uint16* src_ptr, - int dst_width, int x, int dx); -void ScaleFilterCols64_C(uint8* dst_ptr, const uint8* src_ptr, - int dst_width, int x, int dx); -void ScaleFilterCols64_16_C(uint16* dst_ptr, const uint16* src_ptr, - int dst_width, int x, int dx); -void ScaleRowDown38_C(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst, int dst_width); -void ScaleRowDown38_16_C(const uint16* src_ptr, ptrdiff_t src_stride, - uint16* dst, int dst_width); -void ScaleRowDown38_3_Box_C(const uint8* src_ptr, - ptrdiff_t src_stride, - uint8* dst_ptr, int dst_width); -void ScaleRowDown38_3_Box_16_C(const uint16* src_ptr, - ptrdiff_t src_stride, - uint16* dst_ptr, int dst_width); -void ScaleRowDown38_2_Box_C(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst_ptr, int dst_width); -void ScaleRowDown38_2_Box_16_C(const uint16* src_ptr, ptrdiff_t src_stride, - uint16* dst_ptr, int dst_width); -void ScaleAddRow_C(const uint8* src_ptr, uint16* dst_ptr, int src_width); -void ScaleAddRow_16_C(const uint16* src_ptr, uint32* dst_ptr, int src_width); -void ScaleARGBRowDown2_C(const uint8* src_argb, - ptrdiff_t src_stride, - uint8* dst_argb, int dst_width); -void ScaleARGBRowDown2Linear_C(const uint8* src_argb, - ptrdiff_t src_stride, - uint8* dst_argb, int dst_width); -void ScaleARGBRowDown2Box_C(const uint8* src_argb, ptrdiff_t src_stride, - uint8* dst_argb, int dst_width); -void ScaleARGBRowDownEven_C(const uint8* src_argb, ptrdiff_t src_stride, - int src_stepx, - uint8* dst_argb, int dst_width); -void ScaleARGBRowDownEvenBox_C(const uint8* src_argb, - ptrdiff_t src_stride, - int src_stepx, - uint8* dst_argb, int dst_width); -void ScaleARGBCols_C(uint8* dst_argb, const uint8* src_argb, - int dst_width, int x, int dx); -void ScaleARGBCols64_C(uint8* dst_argb, const uint8* src_argb, - int dst_width, int x, int dx); -void ScaleARGBColsUp2_C(uint8* dst_argb, const uint8* src_argb, - int dst_width, int, int); -void ScaleARGBFilterCols_C(uint8* dst_argb, const uint8* src_argb, - int dst_width, int x, int dx); -void ScaleARGBFilterCols64_C(uint8* dst_argb, const uint8* src_argb, - int dst_width, int x, int dx); - -// Specialized scalers for x86. -void ScaleRowDown2_SSSE3(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst_ptr, int dst_width); -void ScaleRowDown2Linear_SSSE3(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst_ptr, int dst_width); -void ScaleRowDown2Box_SSSE3(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst_ptr, int dst_width); -void ScaleRowDown2_AVX2(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst_ptr, int dst_width); -void ScaleRowDown2Linear_AVX2(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst_ptr, int dst_width); -void ScaleRowDown2Box_AVX2(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst_ptr, int dst_width); -void ScaleRowDown4_SSSE3(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst_ptr, int dst_width); -void ScaleRowDown4Box_SSSE3(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst_ptr, int dst_width); -void ScaleRowDown4_AVX2(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst_ptr, int dst_width); -void ScaleRowDown4Box_AVX2(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst_ptr, int dst_width); - -void ScaleRowDown34_SSSE3(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst_ptr, int dst_width); -void ScaleRowDown34_1_Box_SSSE3(const uint8* src_ptr, - ptrdiff_t src_stride, - uint8* dst_ptr, int dst_width); -void ScaleRowDown34_0_Box_SSSE3(const uint8* src_ptr, - ptrdiff_t src_stride, - uint8* dst_ptr, int dst_width); -void ScaleRowDown38_SSSE3(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst_ptr, int dst_width); -void ScaleRowDown38_3_Box_SSSE3(const uint8* src_ptr, - ptrdiff_t src_stride, - uint8* dst_ptr, int dst_width); -void ScaleRowDown38_2_Box_SSSE3(const uint8* src_ptr, - ptrdiff_t src_stride, - uint8* dst_ptr, int dst_width); -void ScaleRowDown2_Any_SSSE3(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst_ptr, int dst_width); -void ScaleRowDown2Linear_Any_SSSE3(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst_ptr, int dst_width); -void ScaleRowDown2Box_Any_SSSE3(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst_ptr, int dst_width); -void ScaleRowDown2Box_Odd_SSSE3(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst_ptr, int dst_width); -void ScaleRowDown2_Any_AVX2(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst_ptr, int dst_width); -void ScaleRowDown2Linear_Any_AVX2(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst_ptr, int dst_width); -void ScaleRowDown2Box_Any_AVX2(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst_ptr, int dst_width); -void ScaleRowDown2Box_Odd_AVX2(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst_ptr, int dst_width); -void ScaleRowDown4_Any_SSSE3(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst_ptr, int dst_width); -void ScaleRowDown4Box_Any_SSSE3(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst_ptr, int dst_width); -void ScaleRowDown4_Any_AVX2(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst_ptr, int dst_width); -void ScaleRowDown4Box_Any_AVX2(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst_ptr, int dst_width); - -void ScaleRowDown34_Any_SSSE3(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst_ptr, int dst_width); -void ScaleRowDown34_1_Box_Any_SSSE3(const uint8* src_ptr, - ptrdiff_t src_stride, - uint8* dst_ptr, int dst_width); -void ScaleRowDown34_0_Box_Any_SSSE3(const uint8* src_ptr, - ptrdiff_t src_stride, - uint8* dst_ptr, int dst_width); -void ScaleRowDown38_Any_SSSE3(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst_ptr, int dst_width); -void ScaleRowDown38_3_Box_Any_SSSE3(const uint8* src_ptr, - ptrdiff_t src_stride, - uint8* dst_ptr, int dst_width); -void ScaleRowDown38_2_Box_Any_SSSE3(const uint8* src_ptr, - ptrdiff_t src_stride, - uint8* dst_ptr, int dst_width); - -void ScaleAddRow_SSE2(const uint8* src_ptr, uint16* dst_ptr, int src_width); -void ScaleAddRow_AVX2(const uint8* src_ptr, uint16* dst_ptr, int src_width); -void ScaleAddRow_Any_SSE2(const uint8* src_ptr, uint16* dst_ptr, int src_width); -void ScaleAddRow_Any_AVX2(const uint8* src_ptr, uint16* dst_ptr, int src_width); - -void ScaleFilterCols_SSSE3(uint8* dst_ptr, const uint8* src_ptr, - int dst_width, int x, int dx); -void ScaleColsUp2_SSE2(uint8* dst_ptr, const uint8* src_ptr, - int dst_width, int x, int dx); - - -// ARGB Column functions -void ScaleARGBCols_SSE2(uint8* dst_argb, const uint8* src_argb, - int dst_width, int x, int dx); -void ScaleARGBFilterCols_SSSE3(uint8* dst_argb, const uint8* src_argb, - int dst_width, int x, int dx); -void ScaleARGBColsUp2_SSE2(uint8* dst_argb, const uint8* src_argb, - int dst_width, int x, int dx); -void ScaleARGBFilterCols_NEON(uint8* dst_argb, const uint8* src_argb, - int dst_width, int x, int dx); -void ScaleARGBCols_NEON(uint8* dst_argb, const uint8* src_argb, - int dst_width, int x, int dx); -void ScaleARGBFilterCols_Any_NEON(uint8* dst_argb, const uint8* src_argb, - int dst_width, int x, int dx); -void ScaleARGBCols_Any_NEON(uint8* dst_argb, const uint8* src_argb, - int dst_width, int x, int dx); - -// ARGB Row functions -void ScaleARGBRowDown2_SSE2(const uint8* src_argb, ptrdiff_t src_stride, - uint8* dst_argb, int dst_width); -void ScaleARGBRowDown2Linear_SSE2(const uint8* src_argb, ptrdiff_t src_stride, - uint8* dst_argb, int dst_width); -void ScaleARGBRowDown2Box_SSE2(const uint8* src_argb, ptrdiff_t src_stride, - uint8* dst_argb, int dst_width); -void ScaleARGBRowDown2_NEON(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst, int dst_width); -void ScaleARGBRowDown2Linear_NEON(const uint8* src_argb, ptrdiff_t src_stride, - uint8* dst_argb, int dst_width); -void ScaleARGBRowDown2Box_NEON(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst, int dst_width); -void ScaleARGBRowDown2_Any_SSE2(const uint8* src_argb, ptrdiff_t src_stride, - uint8* dst_argb, int dst_width); -void ScaleARGBRowDown2Linear_Any_SSE2(const uint8* src_argb, - ptrdiff_t src_stride, - uint8* dst_argb, int dst_width); -void ScaleARGBRowDown2Box_Any_SSE2(const uint8* src_argb, ptrdiff_t src_stride, - uint8* dst_argb, int dst_width); -void ScaleARGBRowDown2_Any_NEON(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst, int dst_width); -void ScaleARGBRowDown2Linear_Any_NEON(const uint8* src_argb, - ptrdiff_t src_stride, - uint8* dst_argb, int dst_width); -void ScaleARGBRowDown2Box_Any_NEON(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst, int dst_width); - -void ScaleARGBRowDownEven_SSE2(const uint8* src_argb, ptrdiff_t src_stride, - int src_stepx, uint8* dst_argb, int dst_width); -void ScaleARGBRowDownEvenBox_SSE2(const uint8* src_argb, ptrdiff_t src_stride, - int src_stepx, - uint8* dst_argb, int dst_width); -void ScaleARGBRowDownEven_NEON(const uint8* src_argb, ptrdiff_t src_stride, - int src_stepx, - uint8* dst_argb, int dst_width); -void ScaleARGBRowDownEvenBox_NEON(const uint8* src_argb, ptrdiff_t src_stride, - int src_stepx, - uint8* dst_argb, int dst_width); -void ScaleARGBRowDownEven_Any_SSE2(const uint8* src_argb, ptrdiff_t src_stride, - int src_stepx, - uint8* dst_argb, int dst_width); -void ScaleARGBRowDownEvenBox_Any_SSE2(const uint8* src_argb, - ptrdiff_t src_stride, - int src_stepx, - uint8* dst_argb, int dst_width); -void ScaleARGBRowDownEven_Any_NEON(const uint8* src_argb, ptrdiff_t src_stride, - int src_stepx, - uint8* dst_argb, int dst_width); -void ScaleARGBRowDownEvenBox_Any_NEON(const uint8* src_argb, - ptrdiff_t src_stride, - int src_stepx, - uint8* dst_argb, int dst_width); - -// ScaleRowDown2Box also used by planar functions -// NEON downscalers with interpolation. - -// Note - not static due to reuse in convert for 444 to 420. -void ScaleRowDown2_NEON(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst, int dst_width); -void ScaleRowDown2Linear_NEON(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst, int dst_width); -void ScaleRowDown2Box_NEON(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst, int dst_width); - -void ScaleRowDown4_NEON(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst_ptr, int dst_width); -void ScaleRowDown4Box_NEON(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst_ptr, int dst_width); - -// Down scale from 4 to 3 pixels. Use the neon multilane read/write -// to load up the every 4th pixel into a 4 different registers. -// Point samples 32 pixels to 24 pixels. -void ScaleRowDown34_NEON(const uint8* src_ptr, - ptrdiff_t src_stride, - uint8* dst_ptr, int dst_width); -void ScaleRowDown34_0_Box_NEON(const uint8* src_ptr, - ptrdiff_t src_stride, - uint8* dst_ptr, int dst_width); -void ScaleRowDown34_1_Box_NEON(const uint8* src_ptr, - ptrdiff_t src_stride, - uint8* dst_ptr, int dst_width); - -// 32 -> 12 -void ScaleRowDown38_NEON(const uint8* src_ptr, - ptrdiff_t src_stride, - uint8* dst_ptr, int dst_width); -// 32x3 -> 12x1 -void ScaleRowDown38_3_Box_NEON(const uint8* src_ptr, - ptrdiff_t src_stride, - uint8* dst_ptr, int dst_width); -// 32x2 -> 12x1 -void ScaleRowDown38_2_Box_NEON(const uint8* src_ptr, - ptrdiff_t src_stride, - uint8* dst_ptr, int dst_width); - -void ScaleRowDown2_Any_NEON(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst, int dst_width); -void ScaleRowDown2Linear_Any_NEON(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst, int dst_width); -void ScaleRowDown2Box_Any_NEON(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst, int dst_width); -void ScaleRowDown2Box_Odd_NEON(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst, int dst_width); -void ScaleRowDown4_Any_NEON(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst_ptr, int dst_width); -void ScaleRowDown4Box_Any_NEON(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst_ptr, int dst_width); -void ScaleRowDown34_Any_NEON(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst_ptr, int dst_width); -void ScaleRowDown34_0_Box_Any_NEON(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst_ptr, int dst_width); -void ScaleRowDown34_1_Box_Any_NEON(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst_ptr, int dst_width); -// 32 -> 12 -void ScaleRowDown38_Any_NEON(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst_ptr, int dst_width); -// 32x3 -> 12x1 -void ScaleRowDown38_3_Box_Any_NEON(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst_ptr, int dst_width); -// 32x2 -> 12x1 -void ScaleRowDown38_2_Box_Any_NEON(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst_ptr, int dst_width); - -void ScaleAddRow_NEON(const uint8* src_ptr, uint16* dst_ptr, int src_width); -void ScaleAddRow_Any_NEON(const uint8* src_ptr, uint16* dst_ptr, int src_width); - -void ScaleFilterCols_NEON(uint8* dst_ptr, const uint8* src_ptr, - int dst_width, int x, int dx); - -void ScaleFilterCols_Any_NEON(uint8* dst_ptr, const uint8* src_ptr, - int dst_width, int x, int dx); - -void ScaleRowDown2_DSPR2(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst, int dst_width); -void ScaleRowDown2Box_DSPR2(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst, int dst_width); -void ScaleRowDown4_DSPR2(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst, int dst_width); -void ScaleRowDown4Box_DSPR2(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst, int dst_width); -void ScaleRowDown34_DSPR2(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst, int dst_width); -void ScaleRowDown34_0_Box_DSPR2(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* d, int dst_width); -void ScaleRowDown34_1_Box_DSPR2(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* d, int dst_width); -void ScaleRowDown38_DSPR2(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst, int dst_width); -void ScaleRowDown38_2_Box_DSPR2(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst_ptr, int dst_width); -void ScaleRowDown38_3_Box_DSPR2(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst_ptr, int dst_width); - -#ifdef __cplusplus -} // extern "C" -} // namespace libyuv -#endif - -#endif // INCLUDE_LIBYUV_SCALE_ROW_H_ diff --git a/third_party/libyuv/include/libyuv/version.h b/third_party/libyuv/include/libyuv/version.h deleted file mode 100644 index 3a8f6337ca..0000000000 --- a/third_party/libyuv/include/libyuv/version.h +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright 2012 The LibYuv Project Authors. All rights reserved. - * - * Use of this source code is governed by a BSD-style license - * that can be found in the LICENSE file in the root of the source - * tree. An additional intellectual property rights grant can be found - * in the file PATENTS. All contributing project authors may - * be found in the AUTHORS file in the root of the source tree. - */ - -#ifndef INCLUDE_LIBYUV_VERSION_H_ -#define INCLUDE_LIBYUV_VERSION_H_ - -#define LIBYUV_VERSION 1622 - -#endif // INCLUDE_LIBYUV_VERSION_H_ diff --git a/third_party/libyuv/include/libyuv/video_common.h b/third_party/libyuv/include/libyuv/video_common.h deleted file mode 100644 index cb425426a2..0000000000 --- a/third_party/libyuv/include/libyuv/video_common.h +++ /dev/null @@ -1,184 +0,0 @@ -/* - * Copyright 2011 The LibYuv Project Authors. All rights reserved. - * - * Use of this source code is governed by a BSD-style license - * that can be found in the LICENSE file in the root of the source - * tree. An additional intellectual property rights grant can be found - * in the file PATENTS. All contributing project authors may - * be found in the AUTHORS file in the root of the source tree. - */ - -// Common definitions for video, including fourcc and VideoFormat. - -#ifndef INCLUDE_LIBYUV_VIDEO_COMMON_H_ -#define INCLUDE_LIBYUV_VIDEO_COMMON_H_ - -#include "libyuv/basic_types.h" - -#ifdef __cplusplus -namespace libyuv { -extern "C" { -#endif - -////////////////////////////////////////////////////////////////////////////// -// Definition of FourCC codes -////////////////////////////////////////////////////////////////////////////// - -// Convert four characters to a FourCC code. -// Needs to be a macro otherwise the OS X compiler complains when the kFormat* -// constants are used in a switch. -#ifdef __cplusplus -#define FOURCC(a, b, c, d) ( \ - (static_cast(a)) | (static_cast(b) << 8) | \ - (static_cast(c) << 16) | (static_cast(d) << 24)) -#else -#define FOURCC(a, b, c, d) ( \ - ((uint32)(a)) | ((uint32)(b) << 8) | /* NOLINT */ \ - ((uint32)(c) << 16) | ((uint32)(d) << 24)) /* NOLINT */ -#endif - -// Some pages discussing FourCC codes: -// http://www.fourcc.org/yuv.php -// http://v4l2spec.bytesex.org/spec/book1.htm -// http://developer.apple.com/quicktime/icefloe/dispatch020.html -// http://msdn.microsoft.com/library/windows/desktop/dd206750.aspx#nv12 -// http://people.xiph.org/~xiphmont/containers/nut/nut4cc.txt - -// FourCC codes grouped according to implementation efficiency. -// Primary formats should convert in 1 efficient step. -// Secondary formats are converted in 2 steps. -// Auxilliary formats call primary converters. -enum FourCC { - // 9 Primary YUV formats: 5 planar, 2 biplanar, 2 packed. - FOURCC_I420 = FOURCC('I', '4', '2', '0'), - FOURCC_I422 = FOURCC('I', '4', '2', '2'), - FOURCC_I444 = FOURCC('I', '4', '4', '4'), - FOURCC_I411 = FOURCC('I', '4', '1', '1'), - FOURCC_I400 = FOURCC('I', '4', '0', '0'), - FOURCC_NV21 = FOURCC('N', 'V', '2', '1'), - FOURCC_NV12 = FOURCC('N', 'V', '1', '2'), - FOURCC_YUY2 = FOURCC('Y', 'U', 'Y', '2'), - FOURCC_UYVY = FOURCC('U', 'Y', 'V', 'Y'), - - // 2 Secondary YUV formats: row biplanar. - FOURCC_M420 = FOURCC('M', '4', '2', '0'), - FOURCC_Q420 = FOURCC('Q', '4', '2', '0'), // deprecated. - - // 9 Primary RGB formats: 4 32 bpp, 2 24 bpp, 3 16 bpp. - FOURCC_ARGB = FOURCC('A', 'R', 'G', 'B'), - FOURCC_BGRA = FOURCC('B', 'G', 'R', 'A'), - FOURCC_ABGR = FOURCC('A', 'B', 'G', 'R'), - FOURCC_24BG = FOURCC('2', '4', 'B', 'G'), - FOURCC_RAW = FOURCC('r', 'a', 'w', ' '), - FOURCC_RGBA = FOURCC('R', 'G', 'B', 'A'), - FOURCC_RGBP = FOURCC('R', 'G', 'B', 'P'), // rgb565 LE. - FOURCC_RGBO = FOURCC('R', 'G', 'B', 'O'), // argb1555 LE. - FOURCC_R444 = FOURCC('R', '4', '4', '4'), // argb4444 LE. - - // 4 Secondary RGB formats: 4 Bayer Patterns. deprecated. - FOURCC_RGGB = FOURCC('R', 'G', 'G', 'B'), - FOURCC_BGGR = FOURCC('B', 'G', 'G', 'R'), - FOURCC_GRBG = FOURCC('G', 'R', 'B', 'G'), - FOURCC_GBRG = FOURCC('G', 'B', 'R', 'G'), - - // 1 Primary Compressed YUV format. - FOURCC_MJPG = FOURCC('M', 'J', 'P', 'G'), - - // 5 Auxiliary YUV variations: 3 with U and V planes are swapped, 1 Alias. - FOURCC_YV12 = FOURCC('Y', 'V', '1', '2'), - FOURCC_YV16 = FOURCC('Y', 'V', '1', '6'), - FOURCC_YV24 = FOURCC('Y', 'V', '2', '4'), - FOURCC_YU12 = FOURCC('Y', 'U', '1', '2'), // Linux version of I420. - FOURCC_J420 = FOURCC('J', '4', '2', '0'), - FOURCC_J400 = FOURCC('J', '4', '0', '0'), // unofficial fourcc - FOURCC_H420 = FOURCC('H', '4', '2', '0'), // unofficial fourcc - - // 14 Auxiliary aliases. CanonicalFourCC() maps these to canonical fourcc. - FOURCC_IYUV = FOURCC('I', 'Y', 'U', 'V'), // Alias for I420. - FOURCC_YU16 = FOURCC('Y', 'U', '1', '6'), // Alias for I422. - FOURCC_YU24 = FOURCC('Y', 'U', '2', '4'), // Alias for I444. - FOURCC_YUYV = FOURCC('Y', 'U', 'Y', 'V'), // Alias for YUY2. - FOURCC_YUVS = FOURCC('y', 'u', 'v', 's'), // Alias for YUY2 on Mac. - FOURCC_HDYC = FOURCC('H', 'D', 'Y', 'C'), // Alias for UYVY. - FOURCC_2VUY = FOURCC('2', 'v', 'u', 'y'), // Alias for UYVY on Mac. - FOURCC_JPEG = FOURCC('J', 'P', 'E', 'G'), // Alias for MJPG. - FOURCC_DMB1 = FOURCC('d', 'm', 'b', '1'), // Alias for MJPG on Mac. - FOURCC_BA81 = FOURCC('B', 'A', '8', '1'), // Alias for BGGR. - FOURCC_RGB3 = FOURCC('R', 'G', 'B', '3'), // Alias for RAW. - FOURCC_BGR3 = FOURCC('B', 'G', 'R', '3'), // Alias for 24BG. - FOURCC_CM32 = FOURCC(0, 0, 0, 32), // Alias for BGRA kCMPixelFormat_32ARGB - FOURCC_CM24 = FOURCC(0, 0, 0, 24), // Alias for RAW kCMPixelFormat_24RGB - FOURCC_L555 = FOURCC('L', '5', '5', '5'), // Alias for RGBO. - FOURCC_L565 = FOURCC('L', '5', '6', '5'), // Alias for RGBP. - FOURCC_5551 = FOURCC('5', '5', '5', '1'), // Alias for RGBO. - - // 1 Auxiliary compressed YUV format set aside for capturer. - FOURCC_H264 = FOURCC('H', '2', '6', '4'), - - // Match any fourcc. - FOURCC_ANY = -1, -}; - -enum FourCCBpp { - // Canonical fourcc codes used in our code. - FOURCC_BPP_I420 = 12, - FOURCC_BPP_I422 = 16, - FOURCC_BPP_I444 = 24, - FOURCC_BPP_I411 = 12, - FOURCC_BPP_I400 = 8, - FOURCC_BPP_NV21 = 12, - FOURCC_BPP_NV12 = 12, - FOURCC_BPP_YUY2 = 16, - FOURCC_BPP_UYVY = 16, - FOURCC_BPP_M420 = 12, - FOURCC_BPP_Q420 = 12, - FOURCC_BPP_ARGB = 32, - FOURCC_BPP_BGRA = 32, - FOURCC_BPP_ABGR = 32, - FOURCC_BPP_RGBA = 32, - FOURCC_BPP_24BG = 24, - FOURCC_BPP_RAW = 24, - FOURCC_BPP_RGBP = 16, - FOURCC_BPP_RGBO = 16, - FOURCC_BPP_R444 = 16, - FOURCC_BPP_RGGB = 8, - FOURCC_BPP_BGGR = 8, - FOURCC_BPP_GRBG = 8, - FOURCC_BPP_GBRG = 8, - FOURCC_BPP_YV12 = 12, - FOURCC_BPP_YV16 = 16, - FOURCC_BPP_YV24 = 24, - FOURCC_BPP_YU12 = 12, - FOURCC_BPP_J420 = 12, - FOURCC_BPP_J400 = 8, - FOURCC_BPP_H420 = 12, - FOURCC_BPP_MJPG = 0, // 0 means unknown. - FOURCC_BPP_H264 = 0, - FOURCC_BPP_IYUV = 12, - FOURCC_BPP_YU16 = 16, - FOURCC_BPP_YU24 = 24, - FOURCC_BPP_YUYV = 16, - FOURCC_BPP_YUVS = 16, - FOURCC_BPP_HDYC = 16, - FOURCC_BPP_2VUY = 16, - FOURCC_BPP_JPEG = 1, - FOURCC_BPP_DMB1 = 1, - FOURCC_BPP_BA81 = 8, - FOURCC_BPP_RGB3 = 24, - FOURCC_BPP_BGR3 = 24, - FOURCC_BPP_CM32 = 32, - FOURCC_BPP_CM24 = 24, - - // Match any fourcc. - FOURCC_BPP_ANY = 0, // 0 means unknown. -}; - -// Converts fourcc aliases into canonical ones. -LIBYUV_API uint32 CanonicalFourCC(uint32 fourcc); - -#ifdef __cplusplus -} // extern "C" -} // namespace libyuv -#endif - -#endif // INCLUDE_LIBYUV_VIDEO_COMMON_H_ diff --git a/third_party/libyuv/larch64/lib/libyuv.a b/third_party/libyuv/larch64/lib/libyuv.a deleted file mode 100644 index 9c4a32bcdb..0000000000 --- a/third_party/libyuv/larch64/lib/libyuv.a +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:adafce26582e425164df7af36253ce58e3ed1dba9965650745c93bd96e42e976 -size 462482 diff --git a/third_party/libyuv/x86_64/lib/libyuv.a b/third_party/libyuv/x86_64/lib/libyuv.a deleted file mode 100644 index 391b1c8769..0000000000 --- a/third_party/libyuv/x86_64/lib/libyuv.a +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:00f9759c67c6fa21657fabde9e096478ea5809716989599f673f638f039431e5 -size 504790 diff --git a/tools/replay/framereader.cc b/tools/replay/framereader.cc index 13d4497a58..a643f0d3a7 100644 --- a/tools/replay/framereader.cc +++ b/tools/replay/framereader.cc @@ -6,7 +6,7 @@ #include #include "common/util.h" -#include "third_party/libyuv/include/libyuv.h" +#include "libyuv.h" #include "tools/replay/py_downloader.h" #include "tools/replay/util.h" #include "system/hardware/hw.h" diff --git a/tools/setup_dependencies.sh b/tools/setup_dependencies.sh index 8132cd16dc..0b785bf4a2 100755 --- a/tools/setup_dependencies.sh +++ b/tools/setup_dependencies.sh @@ -113,12 +113,24 @@ function install_python_deps() { source .venv/bin/activate } +function install_macos_deps() { + if ! command -v brew > /dev/null 2>&1; then + echo "homebrew not found, skipping macOS system dependency install" + return 0 + fi + + if ! command -v cmake > /dev/null 2>&1; then + brew install cmake + fi +} + # --- Main --- if [[ "$OSTYPE" == "linux-gnu"* ]]; then install_ubuntu_deps echo "[ ] installed system dependencies t=$SECONDS" elif [[ "$OSTYPE" == "darwin"* ]]; then + install_macos_deps if [[ $SHELL == "/bin/zsh" ]]; then RC_FILE="$HOME/.zshrc" elif [[ $SHELL == "/bin/bash" ]]; then diff --git a/uv.lock b/uv.lock index 63c8c4cffa..f5c128fcb7 100644 --- a/uv.lock +++ b/uv.lock @@ -116,12 +116,12 @@ wheels = [ [[package]] name = "bzip2" version = "1.0.8" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=bzip2&rev=releases#11895d3f6bc62a229e84c87505948f15f9018ce0" } +source = { git = "https://github.com/commaai/dependencies.git?subdirectory=bzip2&rev=releases#12581f30b45b570dd0bbc36055fe1532f5a8ef60" } [[package]] name = "capnproto" version = "1.0.1" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=capnproto&rev=releases#11895d3f6bc62a229e84c87505948f15f9018ce0" } +source = { git = "https://github.com/commaai/dependencies.git?subdirectory=capnproto&rev=releases#12581f30b45b570dd0bbc36055fe1532f5a8ef60" } [[package]] name = "casadi" @@ -381,7 +381,7 @@ wheels = [ [[package]] name = "eigen" version = "3.4.0" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=eigen&rev=releases#11895d3f6bc62a229e84c87505948f15f9018ce0" } +source = { git = "https://github.com/commaai/dependencies.git?subdirectory=eigen&rev=releases#12581f30b45b570dd0bbc36055fe1532f5a8ef60" } [[package]] name = "execnet" @@ -395,7 +395,7 @@ wheels = [ [[package]] name = "ffmpeg" version = "7.1.0" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=ffmpeg&rev=releases#11895d3f6bc62a229e84c87505948f15f9018ce0" } +source = { git = "https://github.com/commaai/dependencies.git?subdirectory=ffmpeg&rev=releases#12581f30b45b570dd0bbc36055fe1532f5a8ef60" } [[package]] name = "fonttools" @@ -442,7 +442,7 @@ wheels = [ [[package]] name = "gcc-arm-none-eabi" version = "13.2.1" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=gcc-arm-none-eabi&rev=releases#11895d3f6bc62a229e84c87505948f15f9018ce0" } +source = { git = "https://github.com/commaai/dependencies.git?subdirectory=gcc-arm-none-eabi&rev=releases#12581f30b45b570dd0bbc36055fe1532f5a8ef60" } [[package]] name = "ghp-import" @@ -459,7 +459,7 @@ wheels = [ [[package]] name = "git-lfs" version = "3.6.1" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=git-lfs&rev=releases#11895d3f6bc62a229e84c87505948f15f9018ce0" } +source = { git = "https://github.com/commaai/dependencies.git?subdirectory=git-lfs&rev=releases#12581f30b45b570dd0bbc36055fe1532f5a8ef60" } [[package]] name = "google-crc32c" @@ -577,7 +577,7 @@ wheels = [ [[package]] name = "libjpeg" version = "3.1.0" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=libjpeg&rev=releases#11895d3f6bc62a229e84c87505948f15f9018ce0" } +source = { git = "https://github.com/commaai/dependencies.git?subdirectory=libjpeg&rev=releases#12581f30b45b570dd0bbc36055fe1532f5a8ef60" } [[package]] name = "libusb1" @@ -590,6 +590,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/94/6d/344a164d32d65d503ffe9201cd74cf13a020099dc446554d1e50b07f167b/libusb1-3.3.1-py3-none-win_amd64.whl", hash = "sha256:6e21b772d80d6487fbb55d3d2141218536db302da82f1983754e96c72781c102", size = 141080, upload-time = "2025-03-24T05:36:46.594Z" }, ] +[[package]] +name = "libyuv" +version = "1922.0" +source = { git = "https://github.com/commaai/dependencies.git?subdirectory=libyuv&rev=releases#12581f30b45b570dd0bbc36055fe1532f5a8ef60" } + [[package]] name = "markdown" version = "3.10.2" @@ -740,7 +745,7 @@ wheels = [ [[package]] name = "ncurses" version = "6.5" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=ncurses&rev=releases#11895d3f6bc62a229e84c87505948f15f9018ce0" } +source = { git = "https://github.com/commaai/dependencies.git?subdirectory=ncurses&rev=releases#12581f30b45b570dd0bbc36055fe1532f5a8ef60" } [[package]] name = "numpy" @@ -801,6 +806,7 @@ dependencies = [ { name = "json-rpc" }, { name = "libjpeg" }, { name = "libusb1" }, + { name = "libyuv" }, { name = "ncurses" }, { name = "numpy" }, { name = "openssl3" }, @@ -883,6 +889,7 @@ requires-dist = [ { name = "json-rpc" }, { name = "libjpeg", git = "https://github.com/commaai/dependencies.git?subdirectory=libjpeg&rev=releases" }, { name = "libusb1" }, + { name = "libyuv", git = "https://github.com/commaai/dependencies.git?subdirectory=libyuv&rev=releases" }, { name = "matplotlib", marker = "extra == 'dev'" }, { name = "metadrive-simulator", marker = "platform_machine != 'aarch64' and extra == 'tools'", git = "https://github.com/commaai/metadrive.git?rev=minimal" }, { name = "mkdocs", marker = "extra == 'docs'" }, @@ -928,7 +935,7 @@ provides-extras = ["docs", "testing", "dev", "tools"] [[package]] name = "openssl3" version = "3.4.1" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=openssl3&rev=releases#11895d3f6bc62a229e84c87505948f15f9018ce0" } +source = { git = "https://github.com/commaai/dependencies.git?subdirectory=openssl3&rev=releases#12581f30b45b570dd0bbc36055fe1532f5a8ef60" } [[package]] name = "packaging" @@ -1299,7 +1306,7 @@ wheels = [ [[package]] name = "python3-dev" version = "3.12.8" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=python3-dev&rev=releases#11895d3f6bc62a229e84c87505948f15f9018ce0" } +source = { git = "https://github.com/commaai/dependencies.git?subdirectory=python3-dev&rev=releases#12581f30b45b570dd0bbc36055fe1532f5a8ef60" } [[package]] name = "pyyaml" @@ -1667,7 +1674,7 @@ wheels = [ [[package]] name = "zeromq" version = "4.3.5" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=zeromq&rev=releases#11895d3f6bc62a229e84c87505948f15f9018ce0" } +source = { git = "https://github.com/commaai/dependencies.git?subdirectory=zeromq&rev=releases#12581f30b45b570dd0bbc36055fe1532f5a8ef60" } [[package]] name = "zstandard" @@ -1697,4 +1704,4 @@ wheels = [ [[package]] name = "zstd" version = "1.5.6" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=zstd&rev=releases#11895d3f6bc62a229e84c87505948f15f9018ce0" } +source = { git = "https://github.com/commaai/dependencies.git?subdirectory=zstd&rev=releases#12581f30b45b570dd0bbc36055fe1532f5a8ef60" } From 9c5bf2ce0af3860749ebac3a5c039a6bd874eae1 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sun, 1 Mar 2026 13:50:44 -0500 Subject: [PATCH 311/311] ui: `AlertFadeAnimator` for longitudinal-related statuses (#1748) --- .../sunnypilot/onroad/smart_cruise_control.py | 53 +++++++++---------- selfdrive/ui/sunnypilot/onroad/speed_limit.py | 19 ++----- system/ui/sunnypilot/lib/utils.py | 24 +++++++++ 3 files changed, 53 insertions(+), 43 deletions(-) diff --git a/selfdrive/ui/sunnypilot/onroad/smart_cruise_control.py b/selfdrive/ui/sunnypilot/onroad/smart_cruise_control.py index b4848141e9..c89bd914be 100644 --- a/selfdrive/ui/sunnypilot/onroad/smart_cruise_control.py +++ b/selfdrive/ui/sunnypilot/onroad/smart_cruise_control.py @@ -10,6 +10,7 @@ from openpilot.selfdrive.ui.onroad.hud_renderer import COLORS from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.text_measure import measure_text_cached +from openpilot.system.ui.sunnypilot.lib.utils import AlertFadeAnimator from openpilot.system.ui.widgets import Widget @@ -18,12 +19,13 @@ class SmartCruiseControlRenderer(Widget): super().__init__() self.vision_enabled = False self.vision_active = False - self.vision_frame = 0 self.map_enabled = False self.map_active = False - self.map_frame = 0 self.long_override = False + self._vision_fade = AlertFadeAnimator(gui_app.target_fps) + self._map_fade = AlertFadeAnimator(gui_app.target_fps) + self.font = gui_app.font(FontWeight.BOLD) def update(self): @@ -41,21 +43,10 @@ class SmartCruiseControlRenderer(Widget): if sm.updated["carControl"]: self.long_override = sm["carControl"].cruiseControl.override - if self.vision_active: - self.vision_frame += 1 - else: - self.vision_frame = 0 + self._vision_fade.update(self.vision_active) + self._map_fade.update(self.map_active) - if self.map_active: - self.map_frame += 1 - else: - self.map_frame = 0 - - @staticmethod - def _pulse_element(frame): - return not (frame % gui_app.target_fps < (gui_app.target_fps / 2.5)) - - def _draw_icon(self, rect_center_x, rect_height, x_offset, y_offset, name): + def _draw_icon(self, rect_center_x, rect_height, x_offset, y_offset, name, alpha=1.0): text = name font_size = 36 padding_v = 5 @@ -65,9 +56,12 @@ class SmartCruiseControlRenderer(Widget): box_height = int(sz.y + padding_v * 2) if self.long_override: - box_color = COLORS.OVERRIDE + color = COLORS.OVERRIDE + box_color = rl.Color(color.r, color.g, color.b, int(alpha * 255)) else: - box_color = rl.Color(0, 255, 0, 255) + box_color = rl.Color(0, 255, 0, int(alpha * 255)) + + text_color = rl.Color(0, 0, 0, int(alpha * 255)) screen_y = rect_height / 4 + y_offset @@ -75,13 +69,14 @@ class SmartCruiseControlRenderer(Widget): box_y = screen_y - box_height / 2 # Draw rounded background box - rl.draw_rectangle_rounded(rl.Rectangle(box_x, box_y, box_width, box_height), 0.2, 10, box_color) + if alpha > 0.01: + rl.draw_rectangle_rounded(rl.Rectangle(box_x, box_y, box_width, box_height), 0.2, 10, box_color) - # Draw text centered in the box (black color for contrast against bright green/grey) - text_pos_x = box_x + (box_width - sz.x) / 2 - text_pos_y = box_y + (box_height - sz.y) / 2 + # Draw text centered in the box (black color for contrast against bright green/grey) + text_pos_x = box_x + (box_width - sz.x) / 2 + text_pos_y = box_y + (box_height - sz.y) / 2 - rl.draw_text_ex(self.font, text, rl.Vector2(text_pos_x, text_pos_y), font_size, 0, rl.BLACK) + rl.draw_text_ex(self.font, text, rl.Vector2(text_pos_x, text_pos_y), font_size, 0, text_color) def _render(self, rect: rl.Rectangle): x_offset = -260 @@ -101,10 +96,10 @@ class SmartCruiseControlRenderer(Widget): y_scc_m = orders[idx] idx += 1 - scc_vision_pulse = self._pulse_element(self.vision_frame) - if (self.vision_enabled and not self.vision_active) or (self.vision_active and scc_vision_pulse): - self._draw_icon(rect.x + rect.width / 2, rect.height, x_offset, y_scc_v, "SCC-V") + if self.vision_enabled: + alpha = self._vision_fade.alpha if self.vision_active else 1.0 + self._draw_icon(rect.x + rect.width / 2, rect.height, x_offset, y_scc_v, "SCC-V", alpha) - scc_map_pulse = self._pulse_element(self.map_frame) - if (self.map_enabled and not self.map_active) or (self.map_active and scc_map_pulse): - self._draw_icon(rect.x + rect.width / 2, rect.height, x_offset, y_scc_m, "SCC-M") + if self.map_enabled: + alpha = self._map_fade.alpha if self.map_active else 1.0 + self._draw_icon(rect.x + rect.width / 2, rect.height, x_offset, y_scc_m, "SCC-M", alpha) diff --git a/selfdrive/ui/sunnypilot/onroad/speed_limit.py b/selfdrive/ui/sunnypilot/onroad/speed_limit.py index 02df292cd6..1b03e9a204 100644 --- a/selfdrive/ui/sunnypilot/onroad/speed_limit.py +++ b/selfdrive/ui/sunnypilot/onroad/speed_limit.py @@ -11,7 +11,6 @@ import pyray as rl from cereal import custom from openpilot.common.constants import CV -from openpilot.common.filter_simple import FirstOrderFilter from openpilot.selfdrive.ui.onroad.hud_renderer import UI_CONFIG from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.common import Mode as SpeedLimitMode @@ -19,6 +18,7 @@ from openpilot.system.hardware import HARDWARE from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.lib.text_measure import measure_text_cached +from openpilot.system.ui.sunnypilot.lib.utils import AlertFadeAnimator from openpilot.system.ui.widgets import Widget METER_TO_FOOT = 3.28084 @@ -58,23 +58,14 @@ class SpeedLimitAlertRenderer: self.arrow_blank = rl.load_texture_from_image(blank_image) rl.unload_image(blank_image) - self._pre_active_alpha_filter = FirstOrderFilter(1.0, 0.05, 1 / gui_app.target_fps) - self._pre_active_alert_frame = 0 + self._pre_active_fade = AlertFadeAnimator(gui_app.target_fps, duration_on=0.75, rc=0.05) def update(self): assist_state = ui_state.sm['longitudinalPlanSP'].speedLimit.assist.state - if assist_state == AssistState.preActive: - self._pre_active_alert_frame += 1 - if (self._pre_active_alert_frame % gui_app.target_fps) < (gui_app.target_fps * 0.75): - self._pre_active_alpha_filter.x = 1.0 - else: - self._pre_active_alpha_filter.update(0.0) - else: - self._pre_active_alert_frame = 0 - self._pre_active_alpha_filter.update(1.0) + self._pre_active_fade.update(assist_state == AssistState.preActive) def speed_limit_pre_active_icon_helper(self): - icon_alpha = max(0.0, min(self._pre_active_alpha_filter.x * 255.0, 255.0)) + icon_alpha = max(0.0, min(self._pre_active_fade.alpha * 255.0, 255.0)) txt_icon = self.arrow_blank icon_margin_x = 10 icon_margin_y = 18 @@ -197,7 +188,7 @@ class SpeedLimitRenderer(Widget, SpeedLimitAlertRenderer): sign_rect = rl.Rectangle(x, y, width, UI_CONFIG.set_speed_height + 6 * 2) - alpha = self._pre_active_alpha_filter.x + alpha = self._pre_active_fade.alpha if ui_state.speed_limit_mode != SpeedLimitMode.off: self._draw_sign_main(sign_rect, alpha) diff --git a/system/ui/sunnypilot/lib/utils.py b/system/ui/sunnypilot/lib/utils.py index 09230da14b..ecaba86f4b 100644 --- a/system/ui/sunnypilot/lib/utils.py +++ b/system/ui/sunnypilot/lib/utils.py @@ -10,3 +10,27 @@ from openpilot.system.ui.sunnypilot.widgets.list_view import ButtonActionSP class NoElideButtonAction(ButtonActionSP): def get_width_hint(self): return super().get_width_hint() + 1 + + +class AlertFadeAnimator: + def __init__(self, target_fps: int, duration_on: float = 0.75, rc: float = 0.05): + from openpilot.common.filter_simple import FirstOrderFilter + self._filter = FirstOrderFilter(1.0, rc, 1 / target_fps) + self._frame = 0 + self._target_fps = target_fps + self._duration_on = duration_on + + def update(self, active: bool): + if active: + self._frame += 1 + if (self._frame % self._target_fps) < (self._target_fps * self._duration_on): + self._filter.x = 1.0 + else: + self._filter.update(0.0) + else: + self._frame = 0 + self._filter.update(1.0) + + @property + def alpha(self) -> float: + return self._filter.x