Parse a single line from one ESP32 serial companion.
(self, line: str, companion: '_CompanionState')
| 3589 | subprocess.run(['sudo', 'rfkill', 'unblock', 'wifi'], |
| 3590 | capture_output=True, timeout=3) |
| 3591 | except Exception as e: |
| 3592 | logger.debug(f"rfkill unblock failed: {e}") |
| 3593 | |
| 3594 | # Never tear the uplink out of NetworkManager or reset its type. Doing |
| 3595 | # so is unrecoverable (NM will not reconnect an unmanaged device), so |
| 3596 | # the box would silently lose connectivity — see _management_ifaces. |
| 3597 | # It still gets rfkill-unblocked and brought up above/below, which is |
| 3598 | # harmless, and it remains scannable. |
| 3599 | if interface in self._management_ifaces(): |
| 3600 | if interface not in self._iface_prepared: |
| 3601 | logger.info( |
| 3602 | f"Wardriving: {interface} is the management/uplink radio — " |
| 3603 | f"scanning it but leaving NetworkManager and its mode alone") |
| 3604 | self._iface_prepared.add(interface) |
| 3605 | return |
| 3606 | |
| 3607 | # Claim non-associated interfaces from NetworkManager so its autoscan |
| 3608 | # doesn't race our scan trigger (kernel returns -EBUSY when NM has a |
| 3609 | # scan in flight). Skip the management interface (the one currently |
| 3610 | # connected to an AP) — we must not touch that or we kill WiFi. |
| 3611 | if not self._is_associated(interface): |
| 3612 | try: |
| 3613 | r = subprocess.run( |
| 3614 | ['sudo', 'nmcli', 'dev', 'set', interface, 'managed', 'no'], |
| 3615 | capture_output=True, text=True, timeout=3 |
| 3616 | ) |
| 3617 | if r.returncode == 0 and interface not in self._iface_prepared: |
| 3618 | logger.info( |
| 3619 | f"Wardriving: {interface} marked unmanaged in NetworkManager " |
| 3620 | f"(prevents autoscan races)" |
| 3621 | ) |
| 3622 | except Exception as e: |
| 3623 | logger.debug(f"nmcli unmanage on {interface} failed: {e}") |
| 3624 | |
| 3625 | # Kill any monitor/AP child interfaces on this phy first. |
| 3626 | self._cleanup_phy_children(interface) |
| 3627 | |
| 3628 | current_type = self._iface_type(interface) |
| 3629 | |
| 3630 | # Force managed if (a) we know it's not managed, or (b) we couldn't |
| 3631 | # read the type at all — the latter usually means the interface is in |
| 3632 | # a weird state and we want to reset it anyway. The cost of a |
| 3633 | # redundant down/up on an already-managed iface is ~100 ms. |
| 3634 | needs_reset = (not current_type) or (current_type != 'managed') |
| 3635 | if needs_reset: |
| 3636 | reason = current_type or 'unknown-state' |
| 3637 | logger.warning( |
| 3638 | f"Wardriving: {interface} state='{reason}' — forcing managed for scanning" |
| 3639 | ) |
| 3640 | try: |
| 3641 | subprocess.run(['sudo', 'ip', 'link', 'set', interface, 'down'], |
| 3642 | capture_output=True, timeout=3) |
| 3643 | r = subprocess.run(['sudo', 'iw', 'dev', interface, 'set', 'type', 'managed'], |
| 3644 | capture_output=True, text=True, timeout=3) |
| 3645 | if r.returncode != 0: |
| 3646 | logger.warning(f"iw set type managed on {interface} failed: {r.stderr.strip()}") |
| 3647 | except Exception as e: |
| 3648 | logger.warning(f"forcing managed on {interface} exception: {e}") |
no test coverage detected