From 99c2fcc797c406774690e55e4e9e21973072a5fa Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Wed, 11 Feb 2026 15:05:10 -0800 Subject: [PATCH 001/189] 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 e0a9e7ca7..1d1dfc114 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/189] 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 1d1dfc114..9d0658033 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/189] 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 9d0658033..8cc99c620 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/189] 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 8cc99c620..8912d2fd9 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/189] 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 8912d2fd9..aa486a8c0 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/189] 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 aa486a8c0..1c80fa28d 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/189] 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 db68632c0..955dc4bfa 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/189] 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 1c80fa28d..833a204cb 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/189] 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 833a204cb..3d4da444b 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/189] 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 44d8d95e9..8a6041299 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 ca5b7b6cb..4f04be624 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 1c68cf05c..15a0e5eff 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 f8381a7e0..000000000 --- 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 b364e79d4..000000000 --- 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 8e10d6197..000000000 --- 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 4c4e814ed..2c191c1a7 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 91f359744..b13a84f70 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 000000000..75cc65f84 --- /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 717799857..94871c199 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 846bb8d2c..df000979e 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 d3341e76e..000000000 --- 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 176d7eb6d..000000000 --- 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 4ac64d917..000000000 --- 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 0bb798625..000000000 --- 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 5b7d11bc7..000000000 --- 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 776381341..000000000 --- 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 c93f5cd03..000000000 --- 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 970187a6d..000000000 --- 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 659059cd2..000000000 --- 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 305643cf4..000000000 --- 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 2ca25920c..000000000 --- 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 771a7054b..000000000 --- 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 d35474f37..3f671f610 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 8af72e5f4..d168a7e80 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 e288c6d8b..c28330b32 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 88bca7f77..329192b63 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 c26859cbc..7f35e06a8 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 d741e13cf..6a7f599ab 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 5c3e7a9d2..73e0a78da 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 13cb13f98..a02b8a6ca 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 a92e4c8de..c4401c958 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 cf169f4dc..cc8ef7c88 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 0086319f5..000000000 --- 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 d5960a43f..000000000 --- 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 39f907239..000000000 --- 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 2729e8b9e..000000000 --- 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 a765bd526..000000000 --- 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 794158389..000000000 --- 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 6328a1cd9..000000000 --- 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 945daa83d..000000000 --- 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 e3c14c640..000000000 --- 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 4e334a291..000000000 --- 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 9855cd75e..000000000 --- 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 d4cfc6728..b79d046fc 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 5c2131d4b..f428e9972 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 136c4119f..698ab9885 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 67ad2cc8c..6abbc4793 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/189] 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 9da157660..fdba5c350 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/189] 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 3d4da444b..86fcdb79f 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/189] 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 a47231340..134626746 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/189] 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 86fcdb79f..b2f077422 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/189] 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 955dc4bfa..2b5c6be6e 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/189] 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 03c49fca6..43539d128 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/189] 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 230c537d6..4ccf6995b 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 76e75d542..5e891588f 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 a9fd28cc3..000000000 --- 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 779c219fc..2cbf1cedb 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 29844ccda..36b192238 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/189] 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 692b84d65..b185e2b43 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/189] 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 3f671f610..a6ccaa104 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/189] 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 2b5c6be6e..5c9616524 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 b2f077422..72373ecbe 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 8f5168958..9de44c585 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/189] 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 8f66d89bf..ad1ff1a18 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/189] 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 fdba5c350..1efc6dde9 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/189] 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 1fef0a125..af18f0662 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 220da05c1..7bc6732c6 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 b13a84f70..1808cfec2 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 94871c199..956ea8a6a 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 000000000..d60a9632a --- /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 df000979e..a8b633bf0 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/189] 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 921c27465..c0a2baa8e 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 0f34dbe43..159902d51 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/189] 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 fb1d56a1f..7de9b5227 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 5c9616524..d25b28d63 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 78b6b5cec..1322e95ef 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/189] 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 7de9b5227..3a0da36bb 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 72373ecbe..2dfbe63cc 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/189] 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 d25b28d63..1c11c0973 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 ffa2ff4db..19d54e651 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 2dfbe63cc..187199b5c 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/189] 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 187199b5c..e199c74f2 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/189] 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 f5dab7249..d4bbb7491 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 36b192238..101f3bb56 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/189] 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 1c11c0973..b27e03ed1 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 e199c74f2..2a4a9ab71 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 43539d128..fcba1952c 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/189] 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 b6145e042..ad68d6ee9 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 3a0da36bb..bda619fee 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 b88ac2049..6b4cb92c1 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 1322e95ef..b5c0d0528 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 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 032/189] 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 a581f6874..bde6d4423 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 033/189] 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 1efc6dde9..a668cc776 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 034/189] 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 0ae0b3535..fba7699be 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 035/189] 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 fba7699be..26ef4dd9a 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 036/189] 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 c095eda8a..6f81755d4 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 f7226cdb8..cb292405c 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 5e0b782c1..fd59db985 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 b63d884a4..ef30d6037 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 93e139f0e..edc2228c0 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 8a20f397d..000000000 --- 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 9c452faf6..cdfb5019b 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 b08950341..32d129bc2 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 2fd4a4def..d048dbd0c 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 637807e07..aa10d1ae4 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 d5c58ddd6..df2b4f7ee 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 b6ee57801..25682e6b1 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 11f7184ef..9b53392f6 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 eff70d254..fd4a99be6 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 da4b18199..69dfb67e9 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 037/189] 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 4f04be624..05885c0b5 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 000000000..1cbd8aa60 --- /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 2c4d839ae..cca174350 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 35a7d947a..0e4e10133 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 d20f9d33a..b8408cd88 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 4454845fc..bf7accbbd 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 26ef4dd9a..7e126d4f5 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 8c41926e0..afee4cfb7 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 142e64050..c04efd50b 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 038/189] [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 2c191c1a7..fa514e7dd 160000 --- a/msgq_repo +++ b/msgq_repo @@ -1 +1 @@ -Subproject commit 2c191c1a72ae8119b93b49e1bc12d4a99b751855 +Subproject commit fa514e7dd77025952d94e893c622a5006e260321 diff --git a/opendbc_repo b/opendbc_repo index 143e87f3e..245cb1f20 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit 143e87f3e4565abd1d2d70c32adcb3167d9c64ca +Subproject commit 245cb1f2056071a7625e2ad7e4515f57784515bd diff --git a/panda b/panda index b99d79692..24fe11466 160000 --- a/panda +++ b/panda @@ -1 +1 @@ -Subproject commit b99d79692409514d296d85f367897fa2f86daaa5 +Subproject commit 24fe11466de37a1a761c16e35353ab39602e03e0 diff --git a/uv.lock b/uv.lock index 8909934e9..2f095265b 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 039/189] 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 fe7c8ef33..aab0f58b0 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 f959c613c..2a6d619f7 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 a8b633bf0..3fe3e0e6d 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 2f095265b..cd077d3de 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 040/189] 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 cf12360e6..63cb062eb 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 041/189] 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 6f81755d4..e58ac817e 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 81635aa31..5dfc1c3ec 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 1f3f97b08..5f8de8689 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 6f89e0480..000000000 --- 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 042/189] 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 aab0f58b0..fe7c8ef33 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 043/189] 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 98f2a5e8c..90a32ffca 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 a668cc776..1cb0b42ad 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 000000000..9f2104ec4 --- /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 044/189] 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 9044a97f5..000000000 --- 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 fe7c8ef33..2cca34442 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 2a6d619f7..8eb86101a 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 90a32ffca..39e2f8970 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 1e88ce57d..000000000 --- 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 481ac111b..000000000 --- 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 68df5879e..000000000 --- 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 6fb8d5049..92664ae5b 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 cd077d3de..51e71ce64 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 045/189] 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 2cca34442..03e818f42 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 046/189] 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 5025d407c..e5cfd34bd 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 047/189] 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 e5cfd34bd..000000000 --- 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 000000000..3663257da --- /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 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 048/189] 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 03e818f42..d89250731 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 049/189] 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 3663257da..72ced4985 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 050/189] [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 245cb1f20..647441e42 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit 245cb1f2056071a7625e2ad7e4515f57784515bd +Subproject commit 647441e42db5735e249344449ad857eeeeff7224 diff --git a/panda b/panda index 24fe11466..b1191df61 160000 --- a/panda +++ b/panda @@ -1 +1 @@ -Subproject commit 24fe11466de37a1a761c16e35353ab39602e03e0 +Subproject commit b1191df619dbc201b9444634a29e2c016fee6619 diff --git a/uv.lock b/uv.lock index 51e71ce64..bd4632942 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 051/189] 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 9bed566e1..3451bfc3d 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 052/189] 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 bde6d4423..f2b88a3ca 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 053/189] 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 f2b88a3ca..42efa381b 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 054/189] 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 462dd7a36..20eaa70cd 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 3bdf22499..6beb300d7 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 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 055/189] 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 000000000..9fc65fb9e --- /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 7623f669c..ae3fee735 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 056/189] 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 b27e03ed1..679372404 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 2a4a9ab71..e1343137a 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 057/189] 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 101f3bb56..3fb5bce96 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 058/189] 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 05885c0b5..4f04be624 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 1cbd8aa60..000000000 --- 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 cca174350..95b3913c4 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 0e4e10133..11f88ab46 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 b8408cd88..d20f9d33a 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 bf7accbbd..4454845fc 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 7e126d4f5..82748e961 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 afee4cfb7..8c41926e0 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 059/189] 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 e1343137a..42432f4bd 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 060/189] 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 42432f4bd..fc3160e64 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 061/189] 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 19d54e651..f16271a50 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 fc3160e64..a0d684c78 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 062/189] 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 ec361536d..55d1c2052 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 d6238c285a5c7be6bc7620154a2624c49f1ebb4b Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 17 Feb 2026 19:41:31 -0800 Subject: [PATCH 063/189] 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 bda619fee..3854e998e 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 064/189] 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 a0d684c78..6737946f4 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 065/189] 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 6737946f4..d403f91f8 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 066/189] 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 d403f91f8..57a40895c 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 966bb6cc549f1fa55c3f03379428d625e5bc1da9 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 17 Feb 2026 22:41:51 -0800 Subject: [PATCH 067/189] 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 3854e998e..c14d8ad8e 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 57a40895c..f22c6c0d9 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 068/189] 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 c14d8ad8e..659cd5ff3 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 80f4becabfad873a273dcce0fbcfb3c9cabc5971 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Wed, 18 Feb 2026 01:03:39 -0800 Subject: [PATCH 069/189] 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 679372404..5bf811e26 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 070/189] 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 f22c6c0d9..14a32e0a8 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 071/189] 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 5bf811e26..dd8b26e54 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 072/189] 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 14a32e0a8..8f4d8bdaf 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 073/189] 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 8f4d8bdaf..61522ffde 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 074/189] 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 f12c95eaf..b78a1d8ea 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 fcba1952c..5930a2a6e 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 075/189] 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 61522ffde..52246beb7 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 076/189] 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 fa5ac731b..392f365d0 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 077/189] 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 39e2f8970..74ab2675d 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 000000000..e21a8d896 --- /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 42efa381b..974edb42a 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 000000000..3f1de1051 --- /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 078/189] 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 1cb0b42ad..bb047b201 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 079/189] 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 bb047b201..e424d11f6 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 da314a394..755de674d 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 080/189] 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 7ebb4262f..5c8748783 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 081/189] 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 afb710e80..119cf2999 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 9b1c4a183..24234d4c5 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 3377ce6c6..91ddaaa9c 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 612c518dd68b994186bf3d29970c44e673523afb Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Wed, 18 Feb 2026 22:42:05 -0800 Subject: [PATCH 082/189] 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 659cd5ff3..1940b680e 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 dd8b26e54..66b8e352f 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 f16271a50..e04e3eead 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 52246beb7..479bad7ec 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 9de44c585..186164916 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 083/189] 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 479bad7ec..8f1382a52 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 084/189] 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 8f1382a52..05eac7959 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 085/189] 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 66b8e352f..e8fefb82d 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 05eac7959..a5b5833d7 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 186164916..3e5e4a1d5 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 086/189] 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 58ead6593..9b7d4f9ec 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 b489286f5..000000000 --- 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 5a84afce0..2295ca3cb 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 000000000..9bd9d8c7b --- /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 6fab931cc..000000000 --- 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 087/189] add kia k7 to release notes --- RELEASES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASES.md b/RELEASES.md index 6191c6ba3..6ea887efc 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 088/189] bump panda (#37265) --- panda | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/panda b/panda index b1191df61..e1da7dc91 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 089/189] add power reduction to release notes (#37266) --- RELEASES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASES.md b/RELEASES.md index 6ea887efc..895dcbba7 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 090/189] 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 5930a2a6e..69a50ed84 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 091/189] 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 1940b680e..cd0f4ee80 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 e8fefb82d..34e73e823 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 a5b5833d7..25c4548e9 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 3e5e4a1d5..1f146fbdd 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 092/189] 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 4248fef2e..a399d7679 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 000000000..ffa256b71 --- /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 093/189] 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 00ae6a188..0a2f2daca 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 094/189] 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 69a50ed84..9bb788321 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 095/189] 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 ffa256b71..12fa608b3 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 096/189] 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 702854f98..15d44e24d 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 5d61c1c95..25294511d 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 646c81750..17ab60172 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 0a2f2daca..751373dba 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 e0df8f270..c197b4545 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 7fae2dfd2..dbe5e241a 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 1f202141c..f8fb589b6 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 f69ad8c49..a4518a252 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 e424d11f6..62a808209 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 7fe0dfbbc..0e271c72d 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 f960cf723..c07b2463f 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 3c9406688..c9452fc53 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 88389cb05..b31a9eb3b 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 755de674d..f056fddbf 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 3922c27aa..b22b24085 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 bf64361be..bb70b75b2 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 ebf4b3bec..c040e1a40 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 97618660b..354483676 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 7d90d5692..77fca9fe3 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 4ec92f507..531725688 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 1f146fbdd..fcd56607c 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 62578d1cf..206400a74 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 097/189] 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 9bd9d8c7b..4d649fda8 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 098/189] 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 af18f0662..062801d78 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 000000000..f03d04a38 --- /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 1808cfec2..35be2acc0 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 956ea8a6a..6befe210a 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 d60a9632a..000000000 --- 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 3fe3e0e6d..8cfbea02c 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 099/189] 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 8eb86101a..ac10630a9 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 13d51a636..e2d912a83 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 bd4632942..4f1c957f3 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 100/189] 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 ac10630a9..92d82e833 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 a3dfce9c2..e9b5aff6d 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 4f1c957f3..86d324ec4 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 101/189] 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 583eb8b0d..afdab8a51 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 8bfd2ea97..3320723fe 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 000000000..7cd21bb9c --- /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 92d82e833..12134ef1d 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 24d2faa0d..1bc59326a 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 aa70e49f5..05fef93b4 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 94f5b3323..a7f3d68c1 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 ad1ff1a18..1eb88d720 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 354c7f00a..5c3381edc 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 76d0c2842..ab1d2c7b3 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 ab1800b4f..90bc46b18 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 723112163..6989f8957 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 5f26daf78..f4942e486 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 3177814f9..599c99013 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 e4dabd3df..a9de0690a 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 4fa6d8953..e5f6ceaa3 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 0151940c4..123f14238 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 86d324ec4..e14591dee 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 102/189] 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 f03d04a38..139a7dcac 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 103/189] 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 377b1eedc..42c8f8fd2 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 3f9c9c1c5..23f2c135d 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 4015746c0..627b4845a 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 55d1c2052..10b2e8ab2 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 98a391360..923cc3aad 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 818060c3b..56f009645 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 d89250731..e1765eb56 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 e5f6ceaa3..000000000 --- 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 104/189] rm mapbox-earcut (#37284) --- pyproject.toml | 1 - uv.lock | 22 ---------------------- 2 files changed, 23 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 12134ef1d..b17af33de 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 e14591dee..b3aa498e9 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 105/189] 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 10b2e8ab2..d2c2447d7 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 106/189] 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 139a7dcac..f03d04a38 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 107/189] 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 062801d78..af18f0662 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 f03d04a38..000000000 --- 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 35be2acc0..1808cfec2 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 6befe210a..956ea8a6a 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 000000000..d60a9632a --- /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 8cfbea02c..3fe3e0e6d 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 108/189] 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 b17af33de..f8614a47e 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 903fcc891..d894b8af4 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 2001d23d7..838b1e9f4 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 b3aa498e9..844b1db70 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 109/189] 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 f8614a47e..4becd1756 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 844b1db70..8b8bfd639 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 110/189] 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 4f04be624..3b8aadf91 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 362a51f5c..3d64f8320 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 b79d046fc..025797d1e 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 000000000..00709734a --- /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 f428e9972..a20176419 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 82748e961..a5a6eed04 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 111/189] 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 b78a1d8ea..be2c4747f 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 a399d7679..539074453 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 ad68d6ee9..383c7ef9b 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 134626746..e1beae4fe 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 d305906e1..a2288752e 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 cd0f4ee80..9f49521c3 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 34e73e823..191836252 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 a6f59a038..2372104a0 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 c16504fac..d6fb75a4d 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 69bcca401..99e33e864 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 bab3d6e6f..26a5d132c 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 12fa608b3..c7f21bb2f 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 6b4cb92c1..612f31b75 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 88bab2d00..088d2a0b6 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 f8fb589b6..17d89fbd5 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 a4518a252..e66e04b82 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 62a808209..7ed7ce936 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 fde4f25ff..18194d736 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 0e271c72d..e3cac2618 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 c07b2463f..1ff550e4b 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 f056fddbf..7c0bd2bf3 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 925afd7d1..357e67293 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 b5c0d0528..aa6f54c50 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 5c8748783..5de72ac8c 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 b22b24085..23f6b344e 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 bb70b75b2..39f95cc8a 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 c040e1a40..9824638cd 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 b3b1276a5..25c908c72 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 531725688..49c59a431 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 6d2e08e05..59a245138 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 fcd56607c..668565a03 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 112/189] 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 a2c509f94..26bad25c3 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 113/189] 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 ad6569372..4527a0296 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 114/189] 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 a20176419..aa98102d4 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 a5a6eed04..3f13cbe74 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 115/189] 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 7b229620a..c144bd569 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 d338f097d..5711cafa5 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 116/189] 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 e1765eb56..71ff03cba 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 aab149974..000000000 --- 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 117/189] 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 71ff03cba..35f04e61c 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 118/189] 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 35f04e61c..40dfaaa80 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 4becd1756..5aeb2ffab 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 ece999c54803655186244620112402698816af0f Mon Sep 17 00:00:00 2001 From: Andrey Litvitski Date: Sun, 22 Feb 2026 07:10:16 +0300 Subject: [PATCH 119/189] 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 7583095ea..62468c744 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 120/189] 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 539074453..b7fafd894 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 383c7ef9b..9b5db1c52 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 e1beae4fe..4776a6042 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 a2288752e..eb3331c86 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 9f49521c3..e73383c0d 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 191836252..b974b502a 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 2372104a0..0dbe1b220 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 d6fb75a4d..0e9be393e 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 26a5d132c..4fddc88f6 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 612f31b75..f57938c8e 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 088d2a0b6..64b2c9a06 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 25c908c72..37254c8b3 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 000000000..c1b1df129 --- /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 121/189] 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 7c0bd2bf3..5d284bf45 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 37254c8b3..cc8d72959 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 122/189] 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 9bb788321..f5d5738e9 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 123/189] 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 be2c4747f..928ea942d 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 60f64b31b..778d71855 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 f57938c8e..8a1c5df65 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 f5d5738e9..94e00872d 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 124/189] 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 928ea942d..f6dbad89a 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 778d71855..565d27593 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 9b5db1c52..f107df720 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 4776a6042..d919df1c1 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 e73383c0d..ad4e43a1a 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 0dbe1b220..15fd68199 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 0e9be393e..b0f718923 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 8a1c5df65..c695949e4 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 94e00872d..e50a48bbc 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 125/189] 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 d4bbb7491..5e7e9bfea 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 000000000..bf7790b93 --- /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 000000000..6f97fe5ed --- /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 126/189] 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 5e7e9bfea..31884e5f1 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 127/189] 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 b974b502a..1c5bd5887 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 c695949e4..32624bbdd 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 e50a48bbc..8de86fca9 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 128/189] 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 8de86fca9..8630b4703 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 129/189] 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 8630b4703..9393dd07e 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 130/189] 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 25c4548e9..f95ac7d88 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 131/189] 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 eae5af77f..000000000 --- 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 0da6c384d..000000000 --- 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 905170fd1..000000000 --- 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 88eb4ac2a..000000000 --- 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 9fc152d3d..65bd71f65 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 2a3e83713..000000000 --- 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 1c5bd5887..c43a294e4 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 e04e3eead..c47928d8b 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 f95ac7d88..4ca0a382d 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 9393dd07e..0543b1395 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 132/189] 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 4d649fda8..9bd9d8c7b 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 133/189] 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 8a6041299..6ea1450be 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 e1da7dc91..49f72e931 160000 --- a/panda +++ b/panda @@ -1 +1 @@ -Subproject commit e1da7dc918c0bcda6fbbdd9ee6f89c5428ec5039 +Subproject commit 49f72e931f09ceecb00c0c7937808fcaeecd3c17 diff --git a/pyproject.toml b/pyproject.toml index 5aeb2ffab..cc56eb359 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 aa98102d4..06ab8fda3 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 3f13cbe74..dece43975 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 8b8bfd639..aa03f6d5e 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 134/189] 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 df50cfad2..000000000 --- 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 135/189] 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 3b8aadf91..3d611af0b 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 cc56eb359..93d7b380b 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 cc8ef7c88..fecf44885 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 025797d1e..8ea59d076 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 06ab8fda3..e4b8a6455 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 dece43975..e2ede7fbb 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 698ab9885..b39cf6dab 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 aa03f6d5e..9bfbd05dd 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 136/189] 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 e4b8a6455..0c9fada75 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 e2ede7fbb..4a66beeec 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 137/189] 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 93d7b380b..508a6230b 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 b1859518a..000000000 --- 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 e31fda372..f44d217d5 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 c19f1bf9d..d2c90cafb 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 fd8f691d1..f91d6a144 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 0c9fada75..26fb45f05 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 4a66beeec..3fe5cc59b 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 9bfbd05dd..fa1c4f61b 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 138/189] 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 8ea59d076..90de21265 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 d9fcae6f2..4c11bfc8b 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 b39cf6dab..47b25df16 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 75abb8417..0d9e053ab 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 ba0082826..663c4b43c 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 aed3de59a..f4afc2996 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 481564322..cc37c19ec 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 1f61951d2..fc4d2d54f 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 139/189] 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 3d611af0b..148c11906 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 508a6230b..71e40e391 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 26fb45f05..7d24b6c09 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 3fe5cc59b..c4eee0f67 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 fa1c4f61b..3327ba4db 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 140/189] 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 40dfaaa80..79520105a 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 148c11906..5f450e6b5 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 c4eee0f67..1ab9ca02e 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 141/189] 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 71e40e391..3697b004c 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 7d24b6c09..d14a2a535 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 1ab9ca02e..f9f773205 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 3327ba4db..9432df071 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 142/189] 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 5f450e6b5..cb1c97d15 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 3697b004c..03ab5902f 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 d14a2a535..effa68b9c 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 f9f773205..1a8a17bfe 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 9432df071..33c210a90 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 143/189] 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 79520105a..92cdd2376 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 1a8a17bfe..9607f70cf 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 144/189] 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 effa68b9c..4a1cf9a8c 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 145/189] 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 ad4e43a1a..299df5fea 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 146/189] 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 c43a294e4..c9bc54a8b 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 4ca0a382d..4f02b7062 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 147/189] 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 cb0cf66b1..865c8b38c 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 148/189] 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 f6dbad89a..dc01aba56 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 149/189] 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 56f009645..958f60d2e 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 92cdd2376..ff4fc3909 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 150/189] [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 65f79cdba..5487ef1ee 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 647441e42..b10b43105 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 6fb96b6e5..e10f67fa4 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 802243ff3..a87727e27 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 33c210a90..11ff98927 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 151/189] 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 3fb5bce96..f81b7f0df 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 152/189] 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 c7f21bb2f..7441ed5a2 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 32624bbdd..8e978066c 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 153/189] 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 cb1c97d15..446fcc417 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 30ee18f63..de3a94d71 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 7e6381516..a79acd7d5 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 de87fa3e0..d927b98a4 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 26a2bd60b..84b47e187 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 f46db4d9f..e4483ee7a 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 fa514e7dd..ed2777747 160000 --- a/msgq_repo +++ b/msgq_repo @@ -1 +1 @@ -Subproject commit fa514e7dd77025952d94e893c622a5006e260321 +Subproject commit ed2777747d60de5a399b74ef1d4be4c1fb406ae1 diff --git a/opendbc_repo b/opendbc_repo index b10b43105..ffe10c0c8 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit b10b43105bcd891f95920aac29d01177e0be4940 +Subproject commit ffe10c0c809cc48726292c832b109a14fee603be diff --git a/rednose_repo b/rednose_repo index 7fddc8e6d..6ccb8d055 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 25682e6b1..f54c26e50 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 9b53392f6..8499a1aab 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 0de3e13c0..a03a26bea 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 6ee3af13b..383fa2f0f 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 47da32102..375592961 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 8e3a74d2d..6aa0c8be4 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 40a45a68d..61509c256 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 bc2380e55..14491b1ef 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 0a0c07a15..a9ceacd80 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 ca069b358..61e4ee051 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 55018f28f..a05bf24b4 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 4a1cf9a8c..6793cf2d1 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 eb5409daa..44ff16ce4 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 154/189] 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 af18f0662..062801d78 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 000000000..ac9ddbb38 --- /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 1808cfec2..ed5c50beb 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 956ea8a6a..28190db3e 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 d60a9632a..000000000 --- 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 3fe3e0e6d..bff59366d 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 155/189] 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 c0a2baa8e..ecf1e8503 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 159902d51..4d731965d 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 106a06e3a..72d874b02 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 6ea1450be..000000000 --- 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 4d58a1507..8d1fa8224 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 2887fff74..000000000 --- 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 4454845fc..000000000 --- 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 6793cf2d1..000000000 --- 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 8c41926e0..966d4ba43 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 000000000..6dd1f294c --- /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 be4cfb68f..000000000 --- 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 156/189] 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 03ab5902f..da36ea9df 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 9bd9d8c7b..2e7e6a0ba 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 91930c2d0deb0e9d49e874ff8a49c8e04c69896e Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Mon, 23 Feb 2026 19:37:59 -0800 Subject: [PATCH 157/189] 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 865c8b38c..8ed9ec62f 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 158/189] 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 5cae15210..000000000 --- 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 000000000..9db0c2cd8 --- /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 f81b7f0df..5f76535ef 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 159/189] 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 b185e2b43..c1b74a850 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 160/189] 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 4f02b7062..110fd7218 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 161/189] 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 455cdeef7..0cdc513a0 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 162/189] 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 3696334d5..000000000 --- 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 000000000..e61a678c1 --- /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 000000000..470bfc50c --- /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 9ebff76b5..88e6985f1 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 000000000..999cdafcc --- /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 000000000..eea6eded8 --- /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 5f76535ef..20da5a06c 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 0cdc513a0..734c5b5d6 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 163/189] 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 0543b1395..7eff8ae59 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 164/189] 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 c9bc54a8b..2efc45f46 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 20da5a06c..14786245d 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 165/189] 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 b6cd7834e..000000000 --- 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 6bcb68dac..000000000 --- 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 e34277873..000000000 --- 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 64dd04c31..1c1fd8404 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 a6fa1a62b..3b056c3e0 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 5d284bf45..1640b0d07 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 166/189] 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 14786245d..3ea650ece 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 167/189] 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 c13f7fd31..c92365156 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 168/189] 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 6dd1f294c..cae869f3c 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 169/189] 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 cae869f3c..79f56333c 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 a1e9cf9df91b2ce1e4aca6174a6f4eb979f08375 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Mon, 23 Feb 2026 21:42:24 -0800 Subject: [PATCH 170/189] 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 a03a26bea..20ba2e44d 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 000000000..7571cccdd --- /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 bded80b2e..6ff3667d8 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 ad2a5f939..343c06a1e 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 79f56333c..98f1d878f 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 171/189] 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 20ba2e44d..5556883ef 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 172/189] 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 446fcc417..bee043c54 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 da36ea9df..3c4b1897a 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 98f1d878f..368c6f200 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 11ff98927..2a03eb740 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 173/189] 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 bee043c54..16c1d8ee7 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 3c4b1897a..f983aca8a 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 368c6f200..df06d4ab1 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 00:29:20 -0800 Subject: [PATCH 174/189] 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 299df5fea..8f9b18b9a 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 175/189] 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 aa6f54c50..51feb63c0 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 176/189] 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 51feb63c0..6c73f14e3 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 39f95cc8a..8098e9ea2 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 177/189] 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 9c0ea75b4..67125d709 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 734c5b5d6..8f4bbfc01 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 178/189] 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 dc01aba56..3e3948eea 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 565d27593..bc1cd02c5 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 f107df720..b04d69682 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 d919df1c1..cd7172455 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 8f9b18b9a..bdae92456 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 b0f718923..d6a91b40f 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 179/189] 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 7eff8ae59..b01b34be1 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 180/189] 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 f983aca8a..b40bed23d 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 d894b8af4..903fcc891 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 838b1e9f4..2001d23d7 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 1dcc48922..b27732b18 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 181/189] 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 16c1d8ee7..8d58cc012 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 b40bed23d..e7debf78d 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 df06d4ab1..7c0bf7d70 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 b27732b18..658f62a33 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 182/189] 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 2efc45f46..ec6b0c9dc 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 183/189] 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 e7debf78d..bdcbd7780 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 903fcc891..d894b8af4 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 ed5c50beb..95ac06bb1 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 2001d23d7..838b1e9f4 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 658f62a33..909441769 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 184/189] 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 038f51ca9..5c11e8ca1 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 185/189] 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 958f60d2e..f3a1a3950 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 ff4fc3909..fa327ea74 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 90de21265..137e77d85 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 966d4ba43..f5c5b6082 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 186/189] 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 c1b1df129..2944f47a7 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 187/189] 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 b01b34be1..b2aeb5574 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 188/189] 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 ec6b0c9dc..c69dc8b22 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 110fd7218..00d7e4589 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 1792a600535de37e238fe958cd0c8f115343f23a Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 24 Feb 2026 23:24:08 -0800 Subject: [PATCH 189/189] 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 00d7e4589..ed8d65bae 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: