mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-07-20 08:42:11 +08:00
5359f6d354
* 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 7de04b11c2285c298bb1ec907782026c795ab207. 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 e237d9a720915fb6bd67c539123d3e2d9d582ce1. * organize? * Revert "organize?" This reverts commit 3aca3e5d629c947711ade88799febeb3f61eda87. * 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 085dd185b083f5905a4e71ba3e8c0565175e04aa. * 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
417 lines
15 KiB
Python
417 lines
15 KiB
Python
import atexit
|
|
import threading
|
|
import time
|
|
import uuid
|
|
from collections.abc import Callable
|
|
from dataclasses import dataclass
|
|
from enum import IntEnum
|
|
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.threading import DBusRouter, open_dbus_connection as open_dbus_connection_threading
|
|
from jeepney.low_level import MessageType
|
|
from jeepney.wrappers import Properties
|
|
|
|
from openpilot.common.swaglog import cloudlog
|
|
from openpilot.system.ui.lib.networkmanager import (NM, NM_WIRELESS_IFACE, NM_802_11_AP_SEC_PAIR_WEP40,
|
|
NM_802_11_AP_SEC_PAIR_WEP104, NM_802_11_AP_SEC_GROUP_WEP40,
|
|
NM_802_11_AP_SEC_GROUP_WEP104, NM_802_11_AP_SEC_KEY_MGMT_PSK,
|
|
NM_802_11_AP_SEC_KEY_MGMT_802_1X, NM_802_11_AP_FLAGS_NONE,
|
|
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_STATE_REASON_SUPPLICANT_DISCONNECT,
|
|
NM_DEVICE_STATE_REASON_NEW_ACTIVATION,
|
|
NMDeviceState)
|
|
|
|
TETHERING_IP_ADDRESS = "192.168.43.1"
|
|
DEFAULT_TETHERING_PASSWORD = "swagswagcomma"
|
|
SIGNAL_QUEUE_SIZE = 10
|
|
SCAN_PERIOD_SECONDS = 10
|
|
|
|
|
|
class SecurityType(IntEnum):
|
|
OPEN = 0
|
|
WPA = 1
|
|
WPA2 = 2
|
|
WPA3 = 3
|
|
UNSUPPORTED = 4
|
|
|
|
|
|
def get_security_type(flags: int, wpa_flags: int, rsn_flags: int) -> SecurityType:
|
|
wpa_props = wpa_flags | rsn_flags
|
|
|
|
# obtained by looking at flags of networks in the office as reported by an Android phone
|
|
supports_wpa = (NM_802_11_AP_SEC_PAIR_WEP40 | NM_802_11_AP_SEC_PAIR_WEP104 | NM_802_11_AP_SEC_GROUP_WEP40 |
|
|
NM_802_11_AP_SEC_GROUP_WEP104 | NM_802_11_AP_SEC_KEY_MGMT_PSK)
|
|
|
|
if (flags == NM_802_11_AP_FLAGS_NONE) or ((flags & NM_802_11_AP_FLAGS_WPS) and not (wpa_props & supports_wpa)):
|
|
return SecurityType.OPEN
|
|
elif (flags & NM_802_11_AP_FLAGS_PRIVACY) and (wpa_props & supports_wpa) and not (wpa_props & NM_802_11_AP_SEC_KEY_MGMT_802_1X):
|
|
return SecurityType.WPA
|
|
else:
|
|
cloudlog.warning(f"Unsupported network! flags: {flags}, wpa_flags: {wpa_flags}, rsn_flags: {rsn_flags}")
|
|
return SecurityType.UNSUPPORTED
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Network:
|
|
ssid: str
|
|
strength: int
|
|
is_connected: bool
|
|
security_type: SecurityType
|
|
is_saved: bool
|
|
|
|
@classmethod
|
|
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)
|
|
is_connected = any(ap.is_connected for ap in aps)
|
|
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,
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
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":
|
|
ssid = bytes(ap_props['Ssid'][1]).decode("utf-8", "replace")
|
|
bssid = str(ap_props['HwAddress'][1])
|
|
strength = int(ap_props['Strength'][1])
|
|
flags = int(ap_props['Flags'][1])
|
|
wpa_flags = int(ap_props['WpaFlags'][1])
|
|
rsn_flags = int(ap_props['RsnFlags'][1])
|
|
|
|
return cls(
|
|
ssid=ssid,
|
|
bssid=bssid,
|
|
strength=strength,
|
|
is_connected=ap_path == active_ap_path,
|
|
flags=flags,
|
|
wpa_flags=wpa_flags,
|
|
rsn_flags=rsn_flags,
|
|
ap_path=ap_path,
|
|
)
|
|
|
|
|
|
class WifiManager:
|
|
def __init__(self):
|
|
self._networks = [] # a network can be comprised of multiple APs
|
|
self._active = True # used to not run when not in settings
|
|
self._exit = False
|
|
|
|
# DBus connections
|
|
try:
|
|
self._router_main = DBusRouter(open_dbus_connection_threading(bus="SYSTEM")) # used by scanner / general method calls
|
|
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:
|
|
cloudlog.exception("Failed to connect to system D-Bus")
|
|
self._exit = True
|
|
|
|
# Store wifi device path
|
|
self._wifi_device: str | None = None
|
|
|
|
# State
|
|
self._connecting_to_ssid: str = ""
|
|
self._last_network_update: float = 0.0
|
|
|
|
# Callbacks
|
|
# TODO: implement a callback queue to avoid blocking UI thread
|
|
self._need_auth: Callable[[str], None] | None = None
|
|
self._activated: Callable[[], None] | None = None
|
|
self._forgotten: Callable[[str], None] | None = None
|
|
self._networks_updated: Callable[[list[Network]], None] | None = None
|
|
self._disconnected: Callable[[], None] | None = None
|
|
|
|
self._lock = threading.Lock()
|
|
|
|
self._scan_thread = threading.Thread(target=self._network_scanner, daemon=True)
|
|
self._scan_thread.start()
|
|
|
|
self._state_thread = threading.Thread(target=self._monitor_state, daemon=True)
|
|
self._state_thread.start()
|
|
|
|
atexit.register(self.stop)
|
|
|
|
def set_callbacks(self, need_auth: Callable[[str], None],
|
|
activated: Callable[[], None] | None,
|
|
forgotten: Callable[[str], None],
|
|
networks_updated: Callable[[list[Network]], None],
|
|
disconnected: Callable[[], None]):
|
|
self._need_auth = need_auth
|
|
self._activated = activated
|
|
self._forgotten = forgotten
|
|
self._networks_updated = networks_updated
|
|
self._disconnected = disconnected
|
|
|
|
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):
|
|
device_path = self._wait_for_wifi_device()
|
|
if device_path is None:
|
|
return
|
|
|
|
rule = MatchRule(
|
|
type="signal",
|
|
interface=NM_DEVICE_IFACE,
|
|
member="StateChanged",
|
|
path=device_path,
|
|
)
|
|
|
|
# Filter for StateChanged signal
|
|
self._conn_monitor.send_and_get_reply(message_bus.AddMatch(rule))
|
|
|
|
with self._conn_monitor.filter(rule, bufsize=SIGNAL_QUEUE_SIZE) as q:
|
|
while not self._exit:
|
|
# TODO: always run, and ensure callbacks don't block UI thread
|
|
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)
|
|
except TimeoutError:
|
|
continue
|
|
|
|
new_state, previous_state, change_reason = msg.body
|
|
|
|
# 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)
|
|
if self._need_auth is not None:
|
|
self._need_auth(self._connecting_to_ssid)
|
|
self._connecting_to_ssid = ""
|
|
|
|
elif new_state == NMDeviceState.ACTIVATED:
|
|
if self._activated is not None:
|
|
self._update_networks()
|
|
self._activated()
|
|
self._connecting_to_ssid = ""
|
|
|
|
elif new_state == NMDeviceState.DISCONNECTED and change_reason != NM_DEVICE_STATE_REASON_NEW_ACTIVATION:
|
|
self._connecting_to_ssid = ""
|
|
if self._disconnected is not None:
|
|
self._disconnected()
|
|
|
|
def _network_scanner(self):
|
|
self._wait_for_wifi_device()
|
|
|
|
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()
|
|
self._request_scan()
|
|
self._last_network_update = time.monotonic()
|
|
time.sleep(1 / 2.)
|
|
|
|
def _wait_for_wifi_device(self) -> str | None:
|
|
with self._lock:
|
|
device_path: str | None = None
|
|
while not self._exit:
|
|
device_path = self._get_wifi_device()
|
|
if device_path is not None:
|
|
break
|
|
time.sleep(1)
|
|
return device_path
|
|
|
|
def _get_wifi_device(self) -> str | None:
|
|
if self._wifi_device is not None:
|
|
return self._wifi_device
|
|
|
|
device_paths = self._router_main.send_and_get_reply(new_method_call(self._nm, 'GetDevices')).body[0]
|
|
for device_path in device_paths:
|
|
dev_addr = DBusAddress(device_path, bus_name=NM, interface=NM_DEVICE_IFACE)
|
|
dev_type = self._router_main.send_and_get_reply(Properties(dev_addr).get('DeviceType')).body[0][1]
|
|
|
|
if dev_type == NM_DEVICE_TYPE_WIFI:
|
|
self._wifi_device = device_path
|
|
break
|
|
|
|
return self._wifi_device
|
|
|
|
def _get_connections(self) -> list[str]:
|
|
settings_addr = DBusAddress(NM_SETTINGS_PATH, bus_name=NM, interface=NM_SETTINGS_IFACE)
|
|
return list(self._router_main.send_and_get_reply(new_method_call(settings_addr, 'ListConnections')).body[0])
|
|
|
|
def _connection_by_ssid(self, ssid: str, known_connections: list[str] | None = None) -> str | None:
|
|
for conn_path in known_connections or self._get_connections():
|
|
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"))
|
|
|
|
# ignore connections removed during iteration (need auth, etc.)
|
|
if reply.header.message_type == MessageType.error:
|
|
cloudlog.warning(f"Failed to get connection properties for {conn_path}")
|
|
continue
|
|
|
|
settings = reply.body[0]
|
|
if "802-11-wireless" in settings and settings['802-11-wireless']['ssid'][1].decode("utf-8", "replace") == ssid:
|
|
return conn_path
|
|
return None
|
|
|
|
def connect_to_network(self, ssid: str, password: str):
|
|
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)
|
|
|
|
is_hidden = False
|
|
|
|
connection = {
|
|
'connection': {
|
|
'type': ('s', '802-11-wireless'),
|
|
'uuid': ('s', str(uuid.uuid4())),
|
|
'id': ('s', f'openpilot connection {ssid}'),
|
|
'autoconnect-retries': ('i', 0),
|
|
},
|
|
'802-11-wireless': {
|
|
'ssid': ('ay', ssid.encode("utf-8")),
|
|
'hidden': ('b', is_hidden),
|
|
'mode': ('s', 'infrastructure'),
|
|
},
|
|
'ipv4': {
|
|
'method': ('s', 'auto'),
|
|
'dns-priority': ('i', 600),
|
|
},
|
|
'ipv6': {'method': ('s', 'ignore')},
|
|
}
|
|
|
|
if password:
|
|
connection['802-11-wireless-security'] = {
|
|
'key-mgmt': ('s', 'wpa-psk'),
|
|
'auth-alg': ('s', 'open'),
|
|
'psk': ('s', password),
|
|
}
|
|
|
|
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()
|
|
|
|
def forget_connection(self, ssid: str, block: bool = False):
|
|
def worker():
|
|
conn_path = self._connection_by_ssid(ssid)
|
|
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 self._forgotten is not None:
|
|
self._update_networks()
|
|
self._forgotten(ssid)
|
|
|
|
if block:
|
|
worker()
|
|
else:
|
|
threading.Thread(target=worker, daemon=True).start()
|
|
|
|
def activate_connection(self, ssid: str, block: bool = False):
|
|
def worker():
|
|
conn_path = self._connection_by_ssid(ssid)
|
|
if conn_path is not None:
|
|
if self._wifi_device is None:
|
|
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, "/")))
|
|
|
|
if block:
|
|
worker()
|
|
else:
|
|
threading.Thread(target=worker, daemon=True).start()
|
|
|
|
def _request_scan(self):
|
|
if self._wifi_device is None:
|
|
cloudlog.warning("No WiFi device found")
|
|
return
|
|
|
|
wifi_addr = DBusAddress(self._wifi_device, bus_name=NM, interface=NM_WIRELESS_IFACE)
|
|
reply = self._router_main.send_and_get_reply(new_method_call(wifi_addr, 'RequestScan', 'a{sv}', ({},)))
|
|
|
|
if reply.header.message_type == MessageType.error:
|
|
cloudlog.warning(f"Failed to request scan: {reply}")
|
|
|
|
def _update_networks(self):
|
|
if self._wifi_device is None:
|
|
cloudlog.warning("No WiFi device found")
|
|
return
|
|
|
|
# returns '/' if no active AP
|
|
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]
|
|
|
|
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())
|
|
|
|
# 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 == "":
|
|
continue
|
|
|
|
if ap.ssid not in aps:
|
|
aps[ap.ssid] = []
|
|
|
|
aps[ap.ssid].append(ap)
|
|
except Exception:
|
|
# 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, self._connection_by_ssid(ssid, known_connections) is not None)
|
|
for ssid, ap_list in aps.items()]
|
|
networks.sort(key=lambda n: (-n.is_connected, -n.strength, n.ssid.lower()))
|
|
self._networks = networks
|
|
|
|
if self._networks_updated is not None:
|
|
self._networks_updated(self._networks)
|
|
|
|
def __del__(self):
|
|
self.stop()
|
|
|
|
def stop(self):
|
|
if not self._exit:
|
|
self._exit = True
|
|
self._scan_thread.join()
|
|
self._state_thread.join()
|
|
|
|
self._router_main.close()
|
|
self._router_main.conn.close()
|
|
self._conn_monitor.close()
|