Scan for available Wi-Fi networks and mark those with system profiles
(self, interface=None)
| 1468 | # actually carrying the connection. With a dongle that is wlan1, |
| 1469 | # while the built-in is busy hosting the AP — checking the built-in |
| 1470 | # would report "not connected" for a perfectly good uplink. |
| 1471 | client_iface = self._client_wifi_interface() |
| 1472 | |
| 1473 | # Method 2: Check using iwconfig (if available) |
| 1474 | try: |
| 1475 | result = subprocess.run(['iwconfig', client_iface], |
| 1476 | capture_output=True, text=True, timeout=5) |
| 1477 | if result.returncode == 0 and 'ESSID:' in result.stdout and 'Not-Associated' not in result.stdout: |
| 1478 | # Verify we have an IP address |
| 1479 | ip_result = subprocess.run(['ip', 'addr', 'show', client_iface], |
| 1480 | capture_output=True, text=True, timeout=5) |
| 1481 | if ip_result.returncode == 0 and 'inet ' in ip_result.stdout: |
| 1482 | return True |
| 1483 | except FileNotFoundError: |
| 1484 | pass # iwconfig not available |
| 1485 | |
| 1486 | # Method 3: Check if we can reach the internet (but be quick about it) |
| 1487 | try: |
| 1488 | result = subprocess.run(['ping', '-c', '1', '-W', '2', '1.1.1.1'], |
| 1489 | capture_output=True, text=True, timeout=5) |
| 1490 | if result.returncode == 0: |
| 1491 | # Ensure it's going through the client radio |
| 1492 | route_result = subprocess.run(['ip', 'route', 'get', '1.1.1.1'], |
| 1493 | capture_output=True, text=True, timeout=3) |
| 1494 | if route_result.returncode == 0 and f'dev {client_iface}' in route_result.stdout: |
| 1495 | return True |
| 1496 | except Exception: |
| 1497 | pass # Network unreachable, that's fine |
| 1498 | |
| 1499 | return False |
| 1500 | |
| 1501 | except Exception as e: |
| 1502 | self.logger.error(f"Error checking Wi-Fi connection: {e}") |
| 1503 | return False |
| 1504 | |
| 1505 | def _ensure_radios_unblocked(self): |
| 1506 | """Unblock soft-blocked WiFi radios so client and AP mode can start. |
| 1507 | |
| 1508 | A fresh OS image (no WiFi country set) or a newly plugged USB dongle |
| 1509 | leaves the radio soft-blocked by rfkill. In that state hostapd cannot |
| 1510 | bring the AP up, so start_ap_mode() fails silently and the endless |
| 1511 | loop never presents the fallback AP. The installer/updater install a |
| 1512 | persistent udev rule, but a device that has never run them (or cannot |
| 1513 | update because it has no network) needs this runtime self-heal. |
| 1514 | """ |
| 1515 | try: |
| 1516 | result = subprocess.run(['rfkill', 'list', 'wifi'], |
| 1517 | capture_output=True, text=True, timeout=5) |
| 1518 | if result.returncode != 0: |
| 1519 | return |
| 1520 | if 'Hard blocked: yes' in result.stdout: |
| 1521 | self.logger.error( |
| 1522 | "WiFi radio is HARD blocked (hardware switch) - " |
| 1523 | "cannot unblock via software") |
| 1524 | if 'Soft blocked: yes' not in result.stdout: |
| 1525 | return |
| 1526 | self.logger.warning( |
| 1527 | "WiFi radio soft-blocked by rfkill - unblocking " |
no test coverage detected