Skip to content

Commit 3a10473

Browse files
committed
wifi: replace NetworkManager with wpa_supplicant
1 parent 503531a commit 3a10473

13 files changed

Lines changed: 2026 additions & 798 deletions

File tree

openpilot/selfdrive/ui/layouts/settings/settings.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ def __init__(self):
5353

5454
# Panel configuration
5555
wifi_manager = WifiManager()
56-
wifi_manager.set_active(False)
5756

5857
self._panels = {
5958
PanelType.DEVICE: PanelInfo(tr_noop("Device"), DeviceLayout()),

openpilot/selfdrive/ui/mici/layouts/settings/network/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
from openpilot.selfdrive.ui.mici.layouts.settings.network.wifi_ui import WifiIcon
44
from openpilot.selfdrive.ui.mici.widgets.button import BigButton
55
from openpilot.system.ui.lib.application import gui_app
6-
from openpilot.system.ui.lib.wifi_manager import WifiManager, ConnectStatus, SecurityType, normalize_ssid
6+
from openpilot.system.ui.lib.wifi_manager import WifiManager, ConnectStatus, SecurityType
7+
from openpilot.system.ui.lib.wpa_ctrl import normalize_ssid
78

89

910
class WifiNetworkButton(BigButton):

openpilot/selfdrive/ui/mici/layouts/settings/network/network_layout.py

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,12 @@ def __init__(self):
1414
super().__init__()
1515

1616
self._wifi_manager = WifiManager()
17-
self._wifi_manager.set_active(False)
1817
self._wifi_ui = WifiUIMici(self._wifi_manager)
1918

2019
self._wifi_manager.add_callbacks(
2120
networks_updated=self._on_network_updated,
21+
activated=lambda: self._on_tethering_finished(),
22+
disconnected=lambda: self._on_tethering_finished(),
2223
)
2324

2425
# ******** Tethering ********
@@ -32,9 +33,13 @@ def tethering_toggle_callback(checked: bool):
3233

3334
def tethering_password_callback(password: str):
3435
if password:
35-
self._tethering_toggle_btn.set_enabled(False)
36-
self._tethering_password_btn.set_enabled(False)
3736
self._wifi_manager.set_tethering_password(password)
37+
if self._wifi_manager.is_tethering_active():
38+
self._tethering_toggle_btn.set_enabled(False)
39+
self._tethering_password_btn.set_enabled(False)
40+
else:
41+
self._tethering_toggle_btn.set_enabled(True)
42+
self._tethering_password_btn.set_enabled(True)
3843

3944
def tethering_password_clicked():
4045
tethering_password = self._wifi_manager.tethering_password
@@ -100,14 +105,12 @@ def _update_state(self):
100105

101106
def show_event(self):
102107
super().show_event()
103-
self._wifi_manager.set_active(True)
104108

105109
# Process wifi callbacks while at any point in the nav stack
106110
gui_app.add_nav_stack_tick(self._wifi_manager.process_callbacks)
107111

108112
def hide_event(self):
109113
super().hide_event()
110-
self._wifi_manager.set_active(False)
111114

112115
gui_app.remove_nav_stack_tick(self._wifi_manager.process_callbacks)
113116

@@ -123,14 +126,16 @@ def update_apn(apn: str):
123126
dlg = BigInputDialog("enter APN...", current_apn, minimum_length=0, confirm_callback=update_apn)
124127
gui_app.push_widget(dlg)
125128

126-
def _on_network_updated(self, networks: list[Network]):
127-
# Update tethering state
129+
def _on_tethering_finished(self):
128130
tethering_active = self._wifi_manager.is_tethering_active()
129-
# TODO: use real signals (like activated/settings changed, etc.) to speed up re-enabling buttons
130131
self._tethering_toggle_btn.set_enabled(True)
131132
self._tethering_password_btn.set_enabled(True)
132-
self._network_metered_btn.set_enabled(lambda: not tethering_active and bool(self._wifi_manager.ipv4_address))
133133
self._tethering_toggle_btn.set_checked(tethering_active)
134+
self._on_network_updated(self._wifi_manager.networks)
135+
136+
def _on_network_updated(self, networks: list[Network]):
137+
tethering_active = self._wifi_manager.is_tethering_active()
138+
self._network_metered_btn.set_enabled(lambda: not tethering_active and bool(self._wifi_manager.ipv4_address))
134139

135140
# Update network metered
136141
self._network_metered_btn.set_value(

openpilot/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
from openpilot.system.ui.lib.application import gui_app, MousePos, FontWeight
1010
from openpilot.system.ui.widgets import Widget
1111
from openpilot.system.ui.widgets.scroller import NavScroller
12-
from openpilot.system.ui.lib.wifi_manager import WifiManager, Network, SecurityType, normalize_ssid
12+
from openpilot.system.ui.lib.wifi_manager import WifiManager, Network, SecurityType
13+
from openpilot.system.ui.lib.wpa_ctrl import normalize_ssid
1314

1415

1516
class LoadingAnimation(Widget):
@@ -220,7 +221,7 @@ def _update_state(self):
220221
elif self._is_connected:
221222
self.set_value("tethering" if self._network.is_tethering else "connected")
222223
elif self._network_missing:
223-
# after connecting/connected since NM will still attempt to connect/stay connected for a while
224+
# after connecting/connected since wpa_supplicant will still attempt to connect/stay connected for a while
224225
self.set_value("not in range")
225226
else:
226227
self.set_value("unsupported")
@@ -297,7 +298,6 @@ def any_network_forgetting(self) -> bool:
297298
def show_event(self):
298299
# Re-sort scroller items and update from latest scan results
299300
super().show_event()
300-
self._wifi_manager.set_active(True)
301301
self._networks = {n.ssid: n for n in self._wifi_manager.networks}
302302
self._update_buttons(re_sort=True)
303303

@@ -339,6 +339,9 @@ def _connect_with_password(self, ssid: str, password: str):
339339
self._move_network_to_front(ssid, scroll=True)
340340

341341
def _connect_to_network(self, ssid: str):
342+
if self._wifi_manager.is_tethering_active():
343+
return
344+
342345
network = self._networks.get(ssid)
343346
if network is None:
344347
cloudlog.warning(f"Trying to connect to unknown network: {ssid}")
@@ -356,11 +359,14 @@ def _connect_to_network(self, ssid: str):
356359

357360
def _on_need_auth(self, ssid, incorrect_password=True):
358361
if incorrect_password:
362+
found = False
359363
for btn in self._scroller.items:
360364
if isinstance(btn, WifiButton) and btn.network.ssid == ssid:
361365
btn.set_wrong_password()
366+
found = True
362367
break
363-
return
368+
if found:
369+
return
364370

365371
dlg = BigInputDialog("enter password...", "", minimum_length=8,
366372
confirm_callback=lambda _password: self._connect_with_password(ssid, _password))
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
"""udhcpc lifecycle for a single interface, with a one-shot default-route metric fixup."""
2+
import subprocess
3+
import threading
4+
5+
from openpilot.common.swaglog import cloudlog
6+
7+
8+
class DhcpClient:
9+
"""Manage udhcpc for DHCP on wlan0."""
10+
11+
# Above NM's eth0 default (metric 100) so ETH keeps priority; matches NM's wifi metric.
12+
DEFAULT_ROUTE_METRIC = 600
13+
14+
def __init__(self, iface: str = "wlan0"):
15+
self._iface = iface
16+
self._proc: subprocess.Popen | None = None
17+
self._metric_thread: threading.Thread | None = None
18+
self._metric_stop = threading.Event()
19+
20+
def start(self):
21+
self.stop()
22+
# If a previous UI/manager process crashed, its sudo udhcpc -f survives as
23+
# an orphan and self.stop() above can't reach it (we only track our own Popen).
24+
# Without this, two udhcpc instances would race lease renewals on the same
25+
# interface and the orphan can re-add the address after a managed flush.
26+
subprocess.run(["sudo", "pkill", "-f", f"udhcpc.*-i {self._iface}"], check=False)
27+
self._metric_stop.clear()
28+
try:
29+
self._proc = subprocess.Popen(
30+
["sudo", "udhcpc", "-i", self._iface, "-f", "-t", "5", "-T", "3"],
31+
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
32+
start_new_session=True,
33+
)
34+
except Exception:
35+
cloudlog.exception("Failed to start udhcpc")
36+
return
37+
self._metric_thread = threading.Thread(target=self._fix_default_route_metric, daemon=True)
38+
self._metric_thread.start()
39+
40+
def _fix_default_route_metric(self):
41+
"""Replace udhcpc's metric-0 default route with metric 600.
42+
busybox udhcpc can't set a bind-time metric; without this, wlan0 silently beats
43+
eth0 on every DHCP bind. Same-router renewals skip the re-install, so a one-shot
44+
bump survives the lease. Polls until the route is bumped or stop() is called;
45+
no deadline because udhcpc's worst-case retry window (5×3s + 20s -A backoff)
46+
can outlive any short timeout, leaving a metric-0 route to hijack eth0."""
47+
while not self._metric_stop.is_set():
48+
try:
49+
out = subprocess.check_output(
50+
["ip", "-4", "route", "show", "default", "dev", self._iface],
51+
text=True, timeout=2,
52+
).strip()
53+
except Exception:
54+
# Transient: udhcpc may not have installed the route yet, or the iface is briefly down.
55+
# Fall through to the throttled wait and retry; stop() unblocks the loop.
56+
cloudlog.exception("Failed to query wlan0 default route")
57+
out = ""
58+
59+
for line in out.splitlines():
60+
parts = line.split()
61+
if "via" not in parts:
62+
continue
63+
try:
64+
gw = parts[parts.index("via") + 1]
65+
except IndexError:
66+
continue
67+
if "metric" in parts:
68+
try:
69+
if int(parts[parts.index("metric") + 1]) == self.DEFAULT_ROUTE_METRIC:
70+
return
71+
except (IndexError, ValueError):
72+
pass
73+
subprocess.run(
74+
["sudo", "ip", "-4", "route", "flush", "exact", "0.0.0.0/0", "dev", self._iface],
75+
check=False,
76+
)
77+
subprocess.run(
78+
["sudo", "ip", "-4", "route", "add", "default", "via", gw,
79+
"dev", self._iface, "metric", str(self.DEFAULT_ROUTE_METRIC)],
80+
check=False,
81+
)
82+
return
83+
84+
self._metric_stop.wait(0.2)
85+
86+
def stop(self):
87+
self._metric_stop.set()
88+
if self._metric_thread is not None:
89+
self._metric_thread.join(timeout=2)
90+
self._metric_thread = None
91+
if self._proc is not None:
92+
try:
93+
self._proc.terminate()
94+
self._proc.wait(timeout=3)
95+
except Exception:
96+
try:
97+
self._proc.kill()
98+
self._proc.wait()
99+
except Exception:
100+
pass
101+
self._proc = None
102+
# Same orphan risk as start(): a previous sudo udhcpc -f can survive our
103+
# tracked Popen (e.g. terminate timed out, sudo wrapper died but child
104+
# didn't). Without this, the orphan re-adds the address right after the
105+
# flush below and resurrects the route after we've moved on.
106+
subprocess.run(["sudo", "pkill", "-f", f"udhcpc.*-i {self._iface}"], check=False)
107+
subprocess.run(["sudo", "ip", "addr", "flush", "dev", self._iface], capture_output=True, check=False)

openpilot/system/ui/lib/networkmanager.py

Lines changed: 0 additions & 64 deletions
This file was deleted.

0 commit comments

Comments
 (0)