raylib: clean up networking (#36039)

* stasj

* remove one of many classes

* clean up and fix

* clean up

* stash/draft: oh this is sick

* so epic

* some clean up

* what the fuck, it doesn't even use these

* more epic initializers + make it kind of work

* so simple, wonder if we should further 2x reduce line count

* i've never ever seen this pattern b4, rm

* remove bs add niceness

* minor organization

* set security type and support listing and rming conns

* forget and connect

* jeepney is actually pretty good, it's 2x faster to get wifi device (0.005s to 0.002s)

* temp

* do blocking add in worker thread

* add jeepney

* lets finish with python-dbus first then evaluate - revert jeepney

This reverts commit 7de04b11c2.

and

* safe wrap

* missing

* saved connections

* set rest of callbacks

* skip hidden APs, simplify _running

* add state management

* either wrong password or disconnected for now

* i can't believe we didn't check this...

* disable button if unsupported!!!

* hide/show event no lag hopefully yayay

* fix hide event

* remove old wifi manager

* cache wifi device path + some clean up

* more clean up

* more clean up

* temp disable blocking prime thread

* hackily get device path once

* ok

* debug

* fix open networks

* debug

* clean up

* all threads wait for device path, and user functions dont ever attempt to get, just skip

* same place

* helper

* Revert "helper"

This reverts commit e237d9a720.

* organize?

* Revert "organize?"

This reverts commit 3aca3e5d62.

* c word is a bad word

* rk monitor debug for now

* nothing crazy

* improve checkmark responsiveness

* when forgetting: this is correct, but feels unresponsive

* this feels good

* need these two to keep forgetting and activating responsive

* sort by connected, strength, then name

* handle non-critical race condition

* log more

* unused

* oh jubilee is sick you can block on signals!!

* proof of concept to see if works on device

whoiops

* so sucking fick

* ah this is not generic, it's a filter on the return vals

* flip around to not drop

* oh thank god

* fix

* stash

* atomic replace

* clean up

* add old to keep track of what's moved over

* these are already moved

* so much junk

* so much junk

* more

* tethering wasn't used so we can ignore that for now

* no params now

* rm duplicate imports

* not used anymore

* move get wifi device over to jeepney! ~no additional lines

* request scan w/ keepney

* get_conns

* _connection_by_ssid_jeepney is 2x faster (0.01 vs 0.02s)

* do forget and activate

* _update_networks matches!

* rm old update_networks

* replace connect_to_network, about same time (yes i removed thread call)

* no more python-dbus!k

* doesn't hurt

* AP.from_dbus: actually handle incorrect paths w/ jeep + more efficient single call

* properly handle errors

* it's jeepney now

* less state

* using the thread safe router passes a race condition test that conn failed!

* bad to copy from old wifimanager

* fix conn usage

* clean up

* curious if locks are lagging

* not for now

* Revert "curious if locks are lagging"

This reverts commit 085dd185b0.

* clean up _monitor_state

* remove tests

* clean up dataclasses

* sort

* lint: okay fine it can be non by virtue of exiting right at the perfect time

* some network clean up

* some wifi manager clean up

* this is handled

* stop can be called manually, from deleting wifimanager, or exiting python. some protection

* its not mutable anymore

* scan on enter

* clean up

* back

* lint

* catch dbus fail to connect

catch dbus fail to connect
pull/36065/head
Shane Smiskol 2 days ago committed by GitHub
parent a70e4c3074
commit 5359f6d354
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      pyproject.toml
  2. 8
      selfdrive/ui/layouts/settings/settings.py
  3. 970
      system/ui/lib/wifi_manager.py
  4. 5
      system/ui/setup.py
  5. 5
      system/ui/updater.py
  6. 52
      system/ui/widgets/network.py
  7. 22
      uv.lock

@ -101,8 +101,8 @@ dev = [
"av", "av",
"azure-identity", "azure-identity",
"azure-storage-blob", "azure-storage-blob",
"dbus-next",
"dictdiffer", "dictdiffer",
"jeepney",
"matplotlib", "matplotlib",
"opencv-python-headless", "opencv-python-headless",
"parameterized >=0.8, <0.9", "parameterized >=0.8, <0.9",

@ -9,7 +9,7 @@ from openpilot.selfdrive.ui.layouts.settings.software import SoftwareLayout
from openpilot.selfdrive.ui.layouts.settings.toggles import TogglesLayout from openpilot.selfdrive.ui.layouts.settings.toggles import TogglesLayout
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.lib.wifi_manager import WifiManagerWrapper from openpilot.system.ui.lib.wifi_manager import WifiManager
from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.network import WifiManagerUI from openpilot.system.ui.widgets.network import WifiManagerUI
@ -54,12 +54,12 @@ class SettingsLayout(Widget):
self._current_panel = PanelType.DEVICE self._current_panel = PanelType.DEVICE
# Panel configuration # Panel configuration
self.wifi_manager = WifiManagerWrapper() wifi_manager = WifiManager()
self.wifi_ui = WifiManagerUI(self.wifi_manager) wifi_manager.set_active(False)
self._panels = { self._panels = {
PanelType.DEVICE: PanelInfo("Device", DeviceLayout()), PanelType.DEVICE: PanelInfo("Device", DeviceLayout()),
PanelType.NETWORK: PanelInfo("Network", self.wifi_ui), PanelType.NETWORK: PanelInfo("Network", WifiManagerUI(wifi_manager)),
PanelType.TOGGLES: PanelInfo("Toggles", TogglesLayout()), PanelType.TOGGLES: PanelInfo("Toggles", TogglesLayout()),
PanelType.SOFTWARE: PanelInfo("Software", SoftwareLayout()), PanelType.SOFTWARE: PanelInfo("Software", SoftwareLayout()),
PanelType.FIREHOSE: PanelInfo("Firehose", FirehoseLayout()), PanelType.FIREHOSE: PanelInfo("Firehose", FirehoseLayout()),

File diff suppressed because it is too large Load Diff

@ -19,7 +19,7 @@ from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.button import Button, ButtonStyle, ButtonRadio from openpilot.system.ui.widgets.button import Button, ButtonStyle, ButtonRadio
from openpilot.system.ui.widgets.keyboard import Keyboard from openpilot.system.ui.widgets.keyboard import Keyboard
from openpilot.system.ui.widgets.label import Label, TextAlignment from openpilot.system.ui.widgets.label import Label, TextAlignment
from openpilot.system.ui.widgets.network import WifiManagerUI, WifiManagerWrapper from openpilot.system.ui.widgets.network import WifiManagerUI, WifiManager
NetworkType = log.DeviceState.NetworkType NetworkType = log.DeviceState.NetworkType
@ -72,8 +72,7 @@ class Setup(Widget):
self.download_url = "" self.download_url = ""
self.download_progress = 0 self.download_progress = 0
self.download_thread = None self.download_thread = None
self.wifi_manager = WifiManagerWrapper() self.wifi_ui = WifiManagerUI(WifiManager())
self.wifi_ui = WifiManagerUI(self.wifi_manager)
self.keyboard = Keyboard() self.keyboard = Keyboard()
self.selected_radio = None self.selected_radio = None
self.warning = gui_app.texture("icons/warning.png", 150, 150) self.warning = gui_app.texture("icons/warning.png", 150, 150)

@ -7,7 +7,7 @@ from enum import IntEnum
from openpilot.system.hardware import HARDWARE from openpilot.system.hardware import HARDWARE
from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.wifi_manager import WifiManagerWrapper from openpilot.system.ui.lib.wifi_manager import WifiManager
from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.button import gui_button, ButtonStyle from openpilot.system.ui.widgets.button import gui_button, ButtonStyle
from openpilot.system.ui.widgets.label import gui_text_box, gui_label from openpilot.system.ui.widgets.label import gui_text_box, gui_label
@ -43,8 +43,7 @@ class Updater(Widget):
self.show_reboot_button = False self.show_reboot_button = False
self.process = None self.process = None
self.update_thread = None self.update_thread = None
self.wifi_manager = WifiManagerWrapper() self.wifi_manager_ui = WifiManagerUI(WifiManager())
self.wifi_manager_ui = WifiManagerUI(self.wifi_manager)
def install_update(self): def install_update(self):
self.current_screen = Screen.PROGRESS self.current_screen = Screen.PROGRESS

@ -6,7 +6,7 @@ from typing import cast
import pyray as rl import pyray as rl
from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel
from openpilot.system.ui.lib.wifi_manager import NetworkInfo, WifiManagerCallbacks, WifiManagerWrapper, SecurityType from openpilot.system.ui.lib.wifi_manager import WifiManager, SecurityType, Network
from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.button import ButtonStyle, Button from openpilot.system.ui.widgets.button import ButtonStyle, Button
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog
@ -36,34 +36,35 @@ class UIState(IntEnum):
class WifiManagerUI(Widget): class WifiManagerUI(Widget):
def __init__(self, wifi_manager: WifiManagerWrapper): def __init__(self, wifi_manager: WifiManager):
super().__init__() super().__init__()
self.wifi_manager = wifi_manager
self.state: UIState = UIState.IDLE self.state: UIState = UIState.IDLE
self._state_network: NetworkInfo | None = None # for CONNECTING / NEEDS_AUTH / SHOW_FORGET_CONFIRM / FORGETTING self._state_network: Network | None = None # for CONNECTING / NEEDS_AUTH / SHOW_FORGET_CONFIRM / FORGETTING
self._password_retry: bool = False # for NEEDS_AUTH self._password_retry: bool = False # for NEEDS_AUTH
self.btn_width: int = 200 self.btn_width: int = 200
self.scroll_panel = GuiScrollPanel() self.scroll_panel = GuiScrollPanel()
self.keyboard = Keyboard(max_text_size=MAX_PASSWORD_LENGTH, min_text_size=MIN_PASSWORD_LENGTH, show_password_toggle=True) self.keyboard = Keyboard(max_text_size=MAX_PASSWORD_LENGTH, min_text_size=MIN_PASSWORD_LENGTH, show_password_toggle=True)
self._load_icons() self._load_icons()
self._networks: list[NetworkInfo] = [] self._networks: list[Network] = []
self._networks_buttons: dict[str, Button] = {} self._networks_buttons: dict[str, Button] = {}
self._forget_networks_buttons: dict[str, Button] = {} self._forget_networks_buttons: dict[str, Button] = {}
self._lock = Lock() self._lock = Lock()
self.wifi_manager = wifi_manager
self._confirm_dialog = ConfirmDialog("", "Forget", "Cancel") self._confirm_dialog = ConfirmDialog("", "Forget", "Cancel")
self.wifi_manager.set_callbacks( self.wifi_manager.set_callbacks(need_auth=self._on_need_auth,
WifiManagerCallbacks(
need_auth=self._on_need_auth,
activated=self._on_activated, activated=self._on_activated,
forgotten=self._on_forgotten, forgotten=self._on_forgotten,
networks_updated=self._on_network_updated, networks_updated=self._on_network_updated,
connection_failed=self._on_connection_failed disconnected=self._on_disconnected)
)
) def show_event(self):
self.wifi_manager.start() # start/stop scanning when widget is visible
self.wifi_manager.connect() self.wifi_manager.set_active(True)
def hide_event(self):
self.wifi_manager.set_active(False)
def _load_icons(self): def _load_icons(self):
for icon in STRENGTH_ICONS + ["icons/checkmark.png", "icons/circled_slash.png", "icons/lock_closed.png"]: for icon in STRENGTH_ICONS + ["icons/checkmark.png", "icons/circled_slash.png", "icons/lock_closed.png"]:
@ -78,7 +79,7 @@ class WifiManagerUI(Widget):
if self.state == UIState.NEEDS_AUTH and self._state_network: if self.state == UIState.NEEDS_AUTH and self._state_network:
self.keyboard.set_title("Wrong password" if self._password_retry else "Enter password", f"for {self._state_network.ssid}") self.keyboard.set_title("Wrong password" if self._password_retry else "Enter password", f"for {self._state_network.ssid}")
self.keyboard.reset() self.keyboard.reset()
gui_app.set_modal_overlay(self.keyboard, lambda result: self._on_password_entered(cast(NetworkInfo, self._state_network), result)) 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: elif self.state == UIState.SHOW_FORGET_CONFIRM and self._state_network:
self._confirm_dialog.set_text(f'Forget Wi-Fi Network "{self._state_network.ssid}"?') self._confirm_dialog.set_text(f'Forget Wi-Fi Network "{self._state_network.ssid}"?')
self._confirm_dialog.reset() self._confirm_dialog.reset()
@ -86,7 +87,7 @@ class WifiManagerUI(Widget):
else: else:
self._draw_network_list(rect) self._draw_network_list(rect)
def _on_password_entered(self, network: NetworkInfo, result: int): def _on_password_entered(self, network: Network, result: int):
if result == 1: if result == 1:
password = self.keyboard.text password = self.keyboard.text
self.keyboard.clear() self.keyboard.clear()
@ -121,7 +122,7 @@ class WifiManagerUI(Widget):
rl.end_scissor_mode() rl.end_scissor_mode()
def _draw_network_item(self, rect, network: NetworkInfo, clicked: bool): def _draw_network_item(self, rect, network: Network, clicked: bool):
spacing = 50 spacing = 50
ssid_rect = rl.Rectangle(rect.x, rect.y, rect.width - self.btn_width * 2, ITEM_HEIGHT) ssid_rect = rl.Rectangle(rect.x, rect.y, rect.width - self.btn_width * 2, ITEM_HEIGHT)
signal_icon_rect = rl.Rectangle(rect.x + rect.width - ICON_SIZE, rect.y + (ITEM_HEIGHT - ICON_SIZE) / 2, ICON_SIZE, ICON_SIZE) signal_icon_rect = rl.Rectangle(rect.x + rect.width - ICON_SIZE, rect.y + (ITEM_HEIGHT - ICON_SIZE) / 2, ICON_SIZE, ICON_SIZE)
@ -174,10 +175,10 @@ class WifiManagerUI(Widget):
self.state = UIState.SHOW_FORGET_CONFIRM self.state = UIState.SHOW_FORGET_CONFIRM
self._state_network = network self._state_network = network
def _draw_status_icon(self, rect, network: NetworkInfo): def _draw_status_icon(self, rect, network: Network):
"""Draw the status icon based on network's connection state""" """Draw the status icon based on network's connection state"""
icon_file = None icon_file = None
if network.is_connected: if network.is_connected and self.state != UIState.CONNECTING:
icon_file = "icons/checkmark.png" icon_file = "icons/checkmark.png"
elif network.security_type == SecurityType.UNSUPPORTED: elif network.security_type == SecurityType.UNSUPPORTED:
icon_file = "icons/circled_slash.png" icon_file = "icons/circled_slash.png"
@ -191,12 +192,12 @@ class WifiManagerUI(Widget):
icon_rect = rl.Vector2(rect.x, rect.y + (ICON_SIZE - texture.height) / 2) icon_rect = rl.Vector2(rect.x, rect.y + (ICON_SIZE - texture.height) / 2)
rl.draw_texture_v(texture, icon_rect, rl.WHITE) rl.draw_texture_v(texture, icon_rect, rl.WHITE)
def _draw_signal_strength_icon(self, rect: rl.Rectangle, network: NetworkInfo): def _draw_signal_strength_icon(self, rect: rl.Rectangle, network: Network):
"""Draw the Wi-Fi signal strength icon based on network's signal strength""" """Draw the Wi-Fi signal strength icon based on network's signal strength"""
strength_level = max(0, min(3, round(network.strength / 33.0))) strength_level = max(0, min(3, round(network.strength / 33.0)))
rl.draw_texture_v(gui_app.texture(STRENGTH_ICONS[strength_level], ICON_SIZE, ICON_SIZE), rl.Vector2(rect.x, rect.y), rl.WHITE) rl.draw_texture_v(gui_app.texture(STRENGTH_ICONS[strength_level], ICON_SIZE, ICON_SIZE), rl.Vector2(rect.x, rect.y), rl.WHITE)
def connect_to_network(self, network: NetworkInfo, password=''): def connect_to_network(self, network: Network, password=''):
self.state = UIState.CONNECTING self.state = UIState.CONNECTING
self._state_network = network self._state_network = network
if network.is_saved and not password: if network.is_saved and not password:
@ -204,13 +205,12 @@ class WifiManagerUI(Widget):
else: else:
self.wifi_manager.connect_to_network(network.ssid, password) self.wifi_manager.connect_to_network(network.ssid, password)
def forget_network(self, network: NetworkInfo): def forget_network(self, network: Network):
self.state = UIState.FORGETTING self.state = UIState.FORGETTING
self._state_network = network self._state_network = network
network.is_saved = False
self.wifi_manager.forget_connection(network.ssid) self.wifi_manager.forget_connection(network.ssid)
def _on_network_updated(self, networks: list[NetworkInfo]): def _on_network_updated(self, networks: list[Network]):
with self._lock: with self._lock:
self._networks = networks self._networks = networks
for n in self._networks: for n in self._networks:
@ -237,7 +237,7 @@ class WifiManagerUI(Widget):
if self.state == UIState.FORGETTING: if self.state == UIState.FORGETTING:
self.state = UIState.IDLE self.state = UIState.IDLE
def _on_connection_failed(self, ssid: str, error: str): def _on_disconnected(self):
with self._lock: with self._lock:
if self.state == UIState.CONNECTING: if self.state == UIState.CONNECTING:
self.state = UIState.IDLE self.state = UIState.IDLE
@ -245,13 +245,11 @@ class WifiManagerUI(Widget):
def main(): def main():
gui_app.init_window("Wi-Fi Manager") gui_app.init_window("Wi-Fi Manager")
wifi_manager = WifiManagerWrapper() wifi_ui = WifiManagerUI(WifiManager())
wifi_ui = WifiManagerUI(wifi_manager)
for _ in gui_app.render(): for _ in gui_app.render():
wifi_ui.render(rl.Rectangle(50, 50, gui_app.width - 100, gui_app.height - 100)) wifi_ui.render(rl.Rectangle(50, 50, gui_app.width - 100, gui_app.height - 100))
wifi_manager.shutdown()
gui_app.close() gui_app.close()

@ -442,15 +442,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/56/c8/46ac27096684f33e27dab749ef43c6b0119c6a0d852971eaefb73256dc4c/cython-3.1.3-py3-none-any.whl", hash = "sha256:d13025b34f72f77bf7f65c1cd628914763e6c285f4deb934314c922b91e6be5a", size = 1225725, upload-time = "2025-08-13T06:19:09.593Z" }, { url = "https://files.pythonhosted.org/packages/56/c8/46ac27096684f33e27dab749ef43c6b0119c6a0d852971eaefb73256dc4c/cython-3.1.3-py3-none-any.whl", hash = "sha256:d13025b34f72f77bf7f65c1cd628914763e6c285f4deb934314c922b91e6be5a", size = 1225725, upload-time = "2025-08-13T06:19:09.593Z" },
] ]
[[package]]
name = "dbus-next"
version = "0.2.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ce/45/6a40fbe886d60a8c26f480e7d12535502b5ba123814b3b9a0b002ebca198/dbus_next-0.2.3.tar.gz", hash = "sha256:f4eae26909332ada528c0a3549dda8d4f088f9b365153952a408e28023a626a5", size = 71112, upload-time = "2021-07-25T22:11:28.398Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d2/fc/c0a3f4c4eaa5a22fbef91713474666e13d0ea2a69c84532579490a9f2cc8/dbus_next-0.2.3-py3-none-any.whl", hash = "sha256:58948f9aff9db08316734c0be2a120f6dc502124d9642f55e90ac82ffb16a18b", size = 57885, upload-time = "2021-07-25T22:11:25.466Z" },
]
[[package]] [[package]]
name = "dictdiffer" name = "dictdiffer"
version = "0.9.0" version = "0.9.0"
@ -690,6 +681,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320, upload-time = "2024-10-08T23:04:09.501Z" }, { url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320, upload-time = "2024-10-08T23:04:09.501Z" },
] ]
[[package]]
name = "jeepney"
version = "0.9.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758, upload-time = "2025-02-27T18:51:01.684Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" },
]
[[package]] [[package]]
name = "jinja2" name = "jinja2"
version = "3.1.6" version = "3.1.6"
@ -1241,8 +1241,8 @@ dev = [
{ name = "av" }, { name = "av" },
{ name = "azure-identity" }, { name = "azure-identity" },
{ name = "azure-storage-blob" }, { name = "azure-storage-blob" },
{ name = "dbus-next" },
{ name = "dictdiffer" }, { name = "dictdiffer" },
{ name = "jeepney" },
{ name = "matplotlib" }, { name = "matplotlib" },
{ name = "opencv-python-headless" }, { name = "opencv-python-headless" },
{ name = "parameterized" }, { name = "parameterized" },
@ -1294,11 +1294,11 @@ requires-dist = [
{ name = "codespell", marker = "extra == 'testing'" }, { name = "codespell", marker = "extra == 'testing'" },
{ name = "crcmod" }, { name = "crcmod" },
{ name = "cython" }, { name = "cython" },
{ name = "dbus-next", marker = "extra == 'dev'" },
{ name = "dictdiffer", marker = "extra == 'dev'" }, { name = "dictdiffer", marker = "extra == 'dev'" },
{ name = "future-fstrings" }, { name = "future-fstrings" },
{ name = "hypothesis", marker = "extra == 'testing'", specifier = "==6.47.*" }, { name = "hypothesis", marker = "extra == 'testing'", specifier = "==6.47.*" },
{ name = "inputs" }, { name = "inputs" },
{ name = "jeepney", marker = "extra == 'dev'" },
{ name = "jinja2", marker = "extra == 'docs'" }, { name = "jinja2", marker = "extra == 'docs'" },
{ name = "json-rpc" }, { name = "json-rpc" },
{ name = "libusb1" }, { name = "libusb1" },

Loading…
Cancel
Save