AAwireless: Account for Failed DHCP

This commit is contained in:
Danny
2026-11-04 05:38:52 -08:00
parent e96ab43d4c
commit ad47e77a93
5 changed files with 64 additions and 14 deletions
@@ -2,13 +2,14 @@
import re
DEFAULTS = {"connect_delay_s": 0.0, "max_window": 2, "phone_services": False, "phone_frames": False,
"bootstrap_experiments": False, "stay_while_projecting": False}
"bootstrap_experiments": False, "stay_while_projecting": False, "dhcp_patience_s": 0.0}
FINGERPRINT_KEYS = ("head_unit_make", "head_unit_model", "car_make", "car_model", "car_year")
RULES: list[dict] = [
{"id": "aawireless", "label": "AAWireless", "match": {},
"tuning": {"max_window": 30, "phone_services": True, "phone_frames": True, "stay_while_projecting": True}},
"tuning": {"max_window": 30, "phone_services": True, "phone_frames": True, "stay_while_projecting": True,
"dhcp_patience_s": 6.0}},
{"id": "unofficial", "label": "Unofficial dongle", "match": {}, "tuning": {"bootstrap_experiments": True}},
]
+35 -9
View File
@@ -40,7 +40,9 @@ NM_AP_IFACE = "org.freedesktop.NetworkManager.AccessPoint"
DEVICE_TYPE_WIFI = 2
DEVICE_STATE_DISCONNECTED = 30
DEVICE_STATE_PREPARE = 40
DEVICE_STATE_IP_CONFIG = 70
DEVICE_STATE_ACTIVATED = 100
DHCP_RESTARTS = 3
DEVICE_STATES_NOT_MANAGED = (10, 20) # NM_DEVICE_STATE_UNMANAGED, NM_DEVICE_STATE_UNAVAILABLE
DEVICE_READY_TIMEOUT_S = 5.0
# A shared interface can be briefly unmanaged (for example while the Wi-Fi mode is being switched). Bugsink showed 19 joins
@@ -152,6 +154,7 @@ class NetworkLease:
self.local_ip = ""
self.device_ready = False
self.autoconnect_paused = False
self.dhcp_patience = 0.0
self.marker = AUTOCONNECT_MARKER
self._lock = threading.Lock()
@@ -206,13 +209,15 @@ class NetworkLease:
with self._lock:
self._prepare_device(cancelled)
def acquire(self, credentials: WifiCredentials, timeout: float = JOIN_TIMEOUT_S, cancelled: Callable[[], bool] = lambda: False) -> str:
def acquire(self, credentials: WifiCredentials, timeout: float = JOIN_TIMEOUT_S, cancelled: Callable[[], bool] = lambda: False,
dhcp_patience: float = 0.0) -> str:
"""Join the projection network and return the comma's IPv4 address on it.
The BSSID the car reports is tried first (fast, unambiguous); if that
attempt fails, one retry matches the SSID alone in case the reported BSSID
belongs to a different radio of the head unit.
"""
self.dhcp_patience = dhcp_patience
attempts = [credentials]
if credentials.bssid:
attempts.append(replace(credentials, bssid=""))
@@ -246,6 +251,22 @@ class NetworkLease:
self.active_path = self.connection_path = ""
def _acquire_once(self, credentials: WifiCredentials, timeout: float, cancelled: Callable[[], bool]) -> str:
deadline = time.monotonic() + timeout
restarts = 0
while True:
self._activate(credentials, cancelled)
if self._wait_activated(deadline, timeout, cancelled, restarts < DHCP_RESTARTS):
break
restarts += 1
self.log("wifi_dhcp_restart", interface=self.interface, patience_s=self.dhcp_patience, restarts=restarts)
self._drop_attempt()
self.local_ip = self._ipv4()
if self.dedicated:
self._check_overlap()
self.log("wifi_joined", interface=self.interface, local_ip=self.local_ip, **self._radio())
return self.local_ip
def _activate(self, credentials: WifiCredentials, cancelled: Callable[[], bool]) -> None:
with self._lock:
if not self._get(NM_PATH, NM_IFACE, "WirelessEnabled"):
raise NetworkError("Wi-Fi is turned off on the comma")
@@ -271,7 +292,9 @@ class NetworkLease:
{"persist": ("s", "volatile")}), timeout=15.0)
self.connection_path, self.active_path = body[0], body[1]
deadline = time.monotonic() + timeout
def _wait_activated(self, deadline: float, timeout: float, cancelled: Callable[[], bool], may_restart: bool) -> bool:
"""True once active; False when DHCP sat unanswered past dhcp_patience and the join should start over."""
ip_config_since = 0.0
while True:
if cancelled():
raise NetworkError("cancelled")
@@ -280,17 +303,20 @@ class NetworkLease:
except NetworkError as error:
raise NetworkError("The projection network connection was removed (wrong key or out of range?)") from error
if state == ACTIVE_STATE_ACTIVATED:
break
return True
if state == ACTIVE_STATE_DEACTIVATED:
raise NetworkError("NetworkManager could not join the projection network")
if time.monotonic() >= deadline:
now = time.monotonic()
if now >= deadline:
raise JoinTimeout(f"Timed out joining the projection network after {timeout:.0f} s")
if may_restart and self.dhcp_patience > 0:
if self._get(self.device_path, NM_DEVICE_IFACE, "State") == DEVICE_STATE_IP_CONFIG:
ip_config_since = ip_config_since or now
if now - ip_config_since >= self.dhcp_patience:
return False
else:
ip_config_since = 0.0
time.sleep(0.25)
self.local_ip = self._ipv4()
if self.dedicated:
self._check_overlap()
self.log("wifi_joined", interface=self.interface, local_ip=self.local_ip, **self._radio())
return self.local_ip
def _frequency(self, path: str) -> int:
access_point = self._get(path, NM_WIRELESS_IFACE, "ActiveAccessPoint")
@@ -1487,7 +1487,9 @@ class Supervisor:
# a car that once took 70 s to be ready gets more than the standard wait for the Wi-Fi handshake
patience=max(strategy.patience, self._ladder.ready_patience(address, 20.0)))
self._set(state="wifi_start")
result = boot.run(lambda credentials: lease.acquire(credentials, cancelled=boot.join_is_cancelled), cancelled=self._stop.is_set)
patience = car_profiles.tuning(config)["dhcp_patience_s"]
join_options = {"dhcp_patience": patience} if patience else {}
result = boot.run(lambda credentials: lease.acquire(credentials, cancelled=boot.join_is_cancelled, **join_options), cancelled=self._stop.is_set)
except Exception as error:
if _stray_link(error):
raise StrayLinkError(f"Bluetooth link to the car is stuck: {error}") from error # says nothing about the cached channel
@@ -40,7 +40,7 @@ def test_aawireless_tuning_applies_only_when_chosen():
chosen = config(car_profile_override="aawireless", receiver_name="AAWireless-4F2A")
assert car_profiles.resolve(chosen) == ("aawireless", "override")
assert car_profiles.tuning(chosen) == {"connect_delay_s": 0.0, "max_window": 30, "phone_services": True, "phone_frames": True,
"bootstrap_experiments": False, "stay_while_projecting": True}
"bootstrap_experiments": False, "stay_while_projecting": True, "dhcp_patience_s": 6.0}
assert car_profiles.summary(chosen)["car_profile_label"] == "AAWireless"
for other in (config(receiver_name="AAWireless-4F2A"), config(car_profile_override="default")):
assert car_profiles.tuning(other) == car_profiles.DEFAULTS
@@ -1012,8 +1012,9 @@ def test_supervisor_stop_while_waiting_for_car(identity, tmp_path, monkeypatch):
# ------------------------------------------------------------------- network
class FakeNetworkManager:
def __init__(self, fail_with_bssid=False):
def __init__(self, fail_with_bssid=False, stalled_dhcp=0):
self.fail_with_bssid = fail_with_bssid
self.stalled_dhcp = stalled_dhcp
self.active_device_connection = "/active/home"
self.states = {}
self.added = []
@@ -1031,6 +1032,8 @@ class FakeNetworkManager:
active = f"/active/aa{len(self.added)}"
locked = "bssid" in settings["802-11-wireless"]
self.states[active] = network.ACTIVE_STATE_DEACTIVATED if (locked and self.fail_with_bssid) else network.ACTIVE_STATE_ACTIVATED
if len(self.added) <= self.stalled_dhcp:
self.states[active] = 1 # activating: the access point never answers DHCP
self.active_device_connection = active
return [f"/settings/aa{len(self.added)}", active]
if member == "GetSettings":
@@ -1043,6 +1046,8 @@ class FakeNetworkManager:
values = {"WirelessEnabled": True, "DeviceType": 2, "Interface": "wlan0", "ActiveConnection": self.active_device_connection,
"Connection": "/settings/home", "Ip4Config": "/ip4/1", "Autoconnect": self.autoconnect,
"ActiveAccessPoint": "/ap/1", "Frequency": 5745}
if name == "State" and interface.endswith("Device"):
return 70
if name == "State":
return self.states.get(path, 4)
if name == "AddressData":
@@ -1113,6 +1118,22 @@ def test_network_lease_waits_for_a_slow_honda_access_point(monkeypatch):
assert budgets == [("AA:BB:CC:DD:EE:FF", 90.0), ("", 30.0)]
def test_network_lease_restarts_a_join_whose_dhcp_goes_unanswered():
nm = FakeNetworkManager(stalled_dhcp=2)
lease = make_lease(nm)
assert lease.acquire(credentials(), timeout=10.0, dhcp_patience=0.3) == "192.168.50.23"
assert len(nm.added) == 3
def test_network_lease_waits_for_dhcp_without_patience():
from openpilot.starpilot.system.starport import network
nm = FakeNetworkManager(stalled_dhcp=2)
lease = make_lease(nm)
with pytest.raises(network.JoinTimeout):
lease.acquire(credentials(), timeout=1.0)
assert len(nm.added) == 2 # only the SSID-only retry, never a DHCP restart
def test_network_lease_does_not_undo_user_network_change():
nm = FakeNetworkManager()
lease = make_lease(nm)