Ensure interface is unblocked, free of monitor children, in managed mode, and admin-up. Design contract: wardriving owns every wlan adapter that isn't the WiFi-management interface. Anything else (leftover airmon mon0, bettercap monitor child, manual `iw set type mon
(self, interface)
| 2721 | return 'companion' |
| 2722 | # Ambiguous USB-UART bridge (common on ESP/GPS dev boards): probe NMEA. |
| 2723 | if vid in ('10c4', '1a86', '0403', '067b') or not vid: |
| 2724 | try: |
| 2725 | from gps_manager import _probe_nmea |
| 2726 | if _probe_nmea(node, timeout_per_baud=1.0): |
| 2727 | return 'gps' |
| 2728 | except Exception: |
| 2729 | pass |
| 2730 | # A bridge that isn't a GPS is almost always an ESP companion board. |
| 2731 | return 'companion' |
| 2732 | return 'unknown' |
| 2733 | |
| 2734 | def _attach_device(self, role, node): |
| 2735 | """Attach the right handler for a newly-classified device.""" |
| 2736 | if role == 'gps': |
| 2737 | if self._gps is None: |
| 2738 | return False |
| 2739 | if self._gps.attach(node): |
| 2740 | logger.info(f"[usb-monitor] GPS attached on {node}") |
| 2741 | return True |
| 2742 | logger.warning(f"[usb-monitor] GPS attach failed on {node}") |
| 2743 | return False |
| 2744 | if role == 'companion': |
| 2745 | if self._start_companion_thread(node) is not None: |
| 2746 | logger.info(f"[usb-monitor] companion attached on {node}") |
| 2747 | return True |
| 2748 | return False |
| 2749 | return False |
| 2750 | |
| 2751 | def _detach_device(self, entry): |
| 2752 | """Tear down the handler for a device that was unplugged.""" |
| 2753 | role, node = entry.get('role'), entry.get('node') |
| 2754 | if role == 'gps' and self._gps is not None: |
| 2755 | logger.info(f"[usb-monitor] GPS unplugged ({node}) — detaching") |
| 2756 | self._gps.detach() |
| 2757 | elif role == 'companion': |
| 2758 | logger.info(f"[usb-monitor] companion unplugged ({node}) — stopping listener") |
| 2759 | self.stop_serial(node) |
| 2760 | |
| 2761 | def _device_monitor_loop(self): |
| 2762 | """Poll the USB-serial device set ~every 1.5 s; attach/detach GPS and |
| 2763 | companion handlers as devices appear, disappear, or re-enumerate.""" |
| 2764 | logger.info("[usb-monitor] started") |
| 2765 | while self._running: |
| 2766 | try: |
| 2767 | present = self._enumerate_serial_devices() |
| 2768 | managed = self._managed_devices |
| 2769 | |
| 2770 | # Removals: a managed device id is no longer present. |
| 2771 | for dev_id in list(managed.keys()): |
| 2772 | if dev_id not in present: |
| 2773 | self._detach_device(managed.pop(dev_id)) |
| 2774 | |
| 2775 | # Additions / re-enumerations. |
| 2776 | for dev_id, node in present.items(): |
| 2777 | cur = managed.get(dev_id) |
| 2778 | if cur is None: |
| 2779 | role = self._classify_serial_port(node) |
| 2780 | if role == 'unknown': |
no test coverage detected