diff --git a/bootstrap/startup.json.default b/bootstrap/startup.json.default index 6d7fab2c9f..2d3cff5226 100644 --- a/bootstrap/startup.json.default +++ b/bootstrap/startup.json.default @@ -14,8 +14,8 @@ "bind": "/sys/", "mode": "rw" }, - "/var/run/wpa_supplicant/wlan0": { - "bind": "/var/run/wpa_supplicant/wlan0", + "/var/run/wpa_supplicant": { + "bind": "/var/run/wpa_supplicant", "mode": "rw" }, "/tmp/wpa_playground": { diff --git a/core/frontend/src/components/wifi/WifiUpdater.vue b/core/frontend/src/components/wifi/WifiUpdater.vue index 115b8c1c31..cc07c07d87 100644 --- a/core/frontend/src/components/wifi/WifiUpdater.vue +++ b/core/frontend/src/components/wifi/WifiUpdater.vue @@ -20,7 +20,7 @@ export default Vue.extend({ callPeriodically(this.fetchSavedNetworks, 5000) callPeriodically(this.fetchNetworkStatus, 5000) callPeriodically(this.fetchHotspotStatus, 10000) - callPeriodically(this.fetchAvailableNetworks, 20000) + callPeriodically(this.fetchAvailableNetworks, 6000) callPeriodically(this.fetchHotspotCredentials, 10000) }, methods: { @@ -101,7 +101,7 @@ export default Vue.extend({ await back_axios({ method: 'get', url: `${wifi.API_URL}/scan`, - timeout: 20000, + timeout: 5000, }) .then((response) => { const saved_networks_ssids = wifi.saved_networks?.map((network: SavedNetwork) => network.ssid) diff --git a/core/services/wifi/WifiManager.py b/core/services/wifi/WifiManager.py index e590efcdba..def84a8079 100644 --- a/core/services/wifi/WifiManager.py +++ b/core/services/wifi/WifiManager.py @@ -53,6 +53,9 @@ def connect(self, path: Any) -> None: except Exception: logger.exception("Could not load previous hotspot settings.") + asyncio.run(self.wpa.send_command_autoscan("periodic:10")) + asyncio.run(self.wpa.send_command_scan_interval(10)) + @staticmethod def __decode_escaped(data: bytes) -> str: """Decode escaped byte array @@ -128,42 +131,15 @@ def hotspot(self) -> HotspotManager: async def get_wifi_available(self) -> List[ScannedWifiNetwork]: """Get a dict from the wifi signals available""" + try: + await self.wpa.send_command_scan(timeout=30) + data = await self.wpa.send_command_scan_results() + networks_list = WifiManager.__dict_from_table(data) + return [ScannedWifiNetwork(**network) for network in networks_list] - async def perform_new_scan() -> None: - try: - # We store the scan task here so that new scan requests that happen in the interval - # where this one is running can check when it has finished. - # Otherwise, it could happen that a new scan is initiated before the ones waiting know - # the previous one has finished, making them stay on the loop unnecessarily. - self._scan_task = asyncio.create_task(self.wpa.send_command_scan(timeout=30)) - await self._scan_task - data = await self.wpa.send_command_scan_results() - networks_list = WifiManager.__dict_from_table(data) - self._updated_scan_results = [ScannedWifiNetwork(**network) for network in networks_list] - self._time_last_scan = time.time() - except Exception as error: - if self._scan_task is not None: - self._scan_task.cancel() - self._updated_scan_results = None - raise FetchError("Failed to fetch wifi list.") from error - - # Performs a new scan only if more than 30 seconds passed since last scan - if time.time() - self._time_last_scan < 30: - return self._updated_scan_results or [] - # Performs a new scan only if it's the first one or the last one is already done - # In case there's one running already, wait for it to finish and use its result - if self._scan_task is None or self._scan_task.done(): - await perform_new_scan() - else: - awaited_scan_task_name = self._scan_task.get_name() - while self._scan_task.get_name() == awaited_scan_task_name and not self._scan_task.done(): - logger.info(f"Waiting for {awaited_scan_task_name} results.") - await asyncio.sleep(0.5) - - if self._updated_scan_results is None: - raise FetchError("Failed to fetch wifi list.") - - return self._updated_scan_results + except Exception as error: + logger.error(f"Failed to fetch wifi list: {error}") + raise FetchError("Failed to fetch wifi list.") from error async def get_saved_wifi_network(self) -> List[SavedWifiNetwork]: """Get a list of saved wifi networks""" diff --git a/core/services/wifi/main.py b/core/services/wifi/main.py index c6e950b629..52be8f9811 100755 --- a/core/services/wifi/main.py +++ b/core/services/wifi/main.py @@ -4,10 +4,12 @@ import asyncio import logging import os +import stat import sys from pathlib import Path from typing import Any, List, Optional +from aiocache import cached from commonwealth.utils.apis import ( GenericErrorHandlingRoute, PrettyJSONResponse, @@ -55,6 +57,7 @@ async def network_status() -> Any: @app.get("/scan", response_model=List[ScannedWifiNetwork], summary="Retrieve available wifi networks.") @version(1, 0) +@cached(ttl=15, namespace="scan") async def scan() -> Any: logger.info("Trying to perform network scan.") try: @@ -231,10 +234,29 @@ def get_hotspot_credentials() -> Any: socket_name = args.socket_name else: logger.info("Connecting via default socket.") - available_sockets = os.listdir(wpa_socket_folder) + + def is_socket(file_path: str) -> bool: + try: + mode = os.stat(file_path).st_mode + return stat.S_ISSOCK(mode) + except Exception as error: + logger.warning(f"Could not check if '{file_path}' is a socket: {error}") + return False + + # We are going to sort and get the latest file, since this in theory will be an external interface + # added by the user + entries = os.scandir(wpa_socket_folder) + available_sockets = sorted( + [ + entry.path + for entry in entries + if entry.name.startswith(("wlan", "wifi", "wlp")) and is_socket(entry.path) + ] + ) if not available_sockets: raise RuntimeError("No wifi sockets available.") - socket_name = available_sockets[0] + socket_name = available_sockets[-1] + logger.info(f"Going to use {socket_name} file") WLAN_SOCKET = os.path.join(wpa_socket_folder, socket_name) wifi_manager.connect(WLAN_SOCKET) except Exception as socket_connection_error: diff --git a/core/services/wifi/setup.py b/core/services/wifi/setup.py index 2adfc1b746..f99d026994 100644 --- a/core/services/wifi/setup.py +++ b/core/services/wifi/setup.py @@ -9,6 +9,7 @@ license="MIT", py_modules=[], install_requires=[ + "aiocache == 0.12.2", "aiofiles == 0.6.0", "commonwealth == 0.1.0", "fastapi == 0.105.0", diff --git a/core/services/wifi/wpa_supplicant.py b/core/services/wifi/wpa_supplicant.py index 37beb9f67a..652c430d94 100644 --- a/core/services/wifi/wpa_supplicant.py +++ b/core/services/wifi/wpa_supplicant.py @@ -4,6 +4,7 @@ import socket import time from pathlib import Path +from threading import Lock from typing import Optional, Tuple, Union from loguru import logger @@ -17,6 +18,7 @@ class WPASupplicant: def __init__(self) -> None: self.sock: Optional[socket.socket] = None + self.lock = Lock() def __del__(self) -> None: if self.sock: @@ -56,12 +58,15 @@ async def send_command(self, command: str, timeout: float) -> bytes: timeout {float} -- Maximum time (in seconds) allowed for receiving an answer before raising a BusyError """ assert self.sock, "No socket assigned to WPA Supplicant" + # pylint: disable=consider-using-with + self.lock.acquire() timeout_start = time.time() while time.time() - timeout_start < timeout: try: self.sock.send(command.encode("utf-8")) data, _ = self.sock.recvfrom(self.BUFFER_SIZE) + logger.debug(data) except Exception as error: # Oh my, something is wrong! # For now, let us report the error but not without recreating the socket @@ -72,16 +77,25 @@ async def send_command(self, command: str, timeout: float) -> bytes: self.run(self.target) except Exception as inner_error: logger.error(f"Failed to send command and failed to recreate wpa socket: {inner_error}") + self.lock.release() raise SockCommError(error_message) from error if b"FAIL-BUSY" in data: logger.info(f"Busy during {command} operation. Trying again...") - await asyncio.sleep(0.1) + await asyncio.sleep(1) + continue + if b"CTRL-EVENT-SCAN-STARTED" in data: + logger.info(f"Scan started during {command} operation. Waiting for results...") + await asyncio.sleep(1) continue break else: + self.lock.release() raise BusyError(f"{command} operation took more than specified timeout ({timeout}). Cancelling.") + if self.lock.locked(): + self.lock.release() + if data == b"FAIL": raise WPAOperationFail(f"WPA operation {command} failed.") @@ -235,6 +249,33 @@ async def send_command_disconnect(self, timeout: float = 1) -> bytes: """ return await self.send_command("DISCONNECT", timeout) + async def send_command_autoscan(self, parameter: str, timeout: float = 1) -> bytes: + """Send message: AUTOSCAN + + Automatic scan + This is an optional set of parameters for automatic scanning + within an interface in following format: + autoscan=: + autoscan is like bgscan but on disconnected or inactive state. + For instance, on exponential module parameters would be : + autoscan=exponential:3:300 + Which means a delay between scans on a base exponential of 3, + up to the limit of 300 seconds (3, 9, 27 ... 300) + For periodic module, parameters would be + autoscan=periodic:30 + So a delay of 30 seconds will be applied between each scan. + Note: If sched_scan_plans are configured and supported by the driver, + autoscan is ignored. + """ + return await self.send_command(f"AUTOSCAN {parameter}", timeout) + + async def send_command_scan_interval(self, seconds: int, timeout: float = 1) -> bytes: + """Send message: SCAN_INTERVAL + + scan interval in seconds. + """ + return await self.send_command(f"SCAN_INTERVAL {seconds}", timeout) + async def send_command_scan(self, timeout: float = 1) -> bytes: """Send message: SCAN diff --git a/core/start-blueos-core b/core/start-blueos-core index aa6a0190ef..16ad31d141 100755 --- a/core/start-blueos-core +++ b/core/start-blueos-core @@ -109,7 +109,7 @@ PRIORITY_SERVICES=( SERVICES=( # This services are not prioritized because they are not fundamental for the vehicle to work 'kraken',0,"nice -19 $SERVICES_PATH/kraken/main.py" - 'wifi',0,"nice -19 $SERVICES_PATH/wifi/main.py --socket wlan0" + 'wifi',0,"nice -19 $SERVICES_PATH/wifi/main.py" # This services are not as important as the others 'beacon',250,"$SERVICES_PATH/beacon/main.py" 'bridget',0,"nice -19 $RUN_AS_REGULAR_USER $SERVICES_PATH/bridget/main.py" diff --git a/core/tools/blueos_startup_update/blueos_startup_update.py b/core/tools/blueos_startup_update/blueos_startup_update.py index 81751c2c69..56cda25010 100755 --- a/core/tools/blueos_startup_update/blueos_startup_update.py +++ b/core/tools/blueos_startup_update/blueos_startup_update.py @@ -25,16 +25,17 @@ DELTA_JSON = { "core": { "binds": { - "/run/udev": {"bind": "/run/udev", "mode": "ro"}, "/etc/blueos": {"bind": "/etc/blueos", "mode": "rw"}, - "/etc/machine-id": {"bind": "/etc/machine-id", "mode": "ro"}, "/etc/dhcpcd.conf": {"bind": "/etc/dhcpcd.conf", "mode": "rw"}, - "/usr/blueos/userdata": {"bind": "/usr/blueos/userdata", "mode": "rw"}, - "/usr/blueos/extensions": {"bind": "/usr/blueos/extensions", "mode": "rw"}, - "/usr/blueos/bin": {"bind": "/usr/blueos/bin", "mode": "rw"}, + "/etc/machine-id": {"bind": "/etc/machine-id", "mode": "ro"}, "/etc/resolv.conf.host": {"bind": "/etc/resolv.conf.host", "mode": "ro"}, "/home/pi/.ssh": {"bind": "/home/pi/.ssh", "mode": "rw"}, + "/run/udev": {"bind": "/run/udev", "mode": "ro"}, "/sys/": {"bind": "/sys/", "mode": "rw"}, + "/usr/blueos/bin": {"bind": "/usr/blueos/bin", "mode": "rw"}, + "/usr/blueos/extensions": {"bind": "/usr/blueos/extensions", "mode": "rw"}, + "/usr/blueos/userdata": {"bind": "/usr/blueos/userdata", "mode": "rw"}, + "/var/run/wpa_supplicant": {"bind": "/var/run/wpa_supplicant", "mode": "rw"}, } } }