Scan a single WiFi interface for networks using iw. Uses an active full-channel sweep (scan trigger with all frequencies) so that every nearby network is discovered in a single pass, even when the interface is associated and stationary.
(self, interface)
| 2802 | # every cycle — only the genuine start-up detection is announced. |
| 2803 | (logger.debug if quiet else logger.info)( |
| 2804 | f"Wardriving detected {len(interfaces)} WiFi interface(s): " |
| 2805 | f"{', '.join(interfaces) or '(none)'}") |
| 2806 | return interfaces |
| 2807 | |
| 2808 | @staticmethod |
| 2809 | def _iface_rfkill_blocked(iface): |
| 2810 | """True when this radio is soft/hard rfkill-blocked. A freshly plugged |
| 2811 | USB dongle commonly comes up blocked, so it enumerates but never scans.""" |
| 2812 | try: |
| 2813 | phy = os.path.realpath(f'/sys/class/net/{iface}/phy80211') |
| 2814 | for entry in os.listdir(f'{phy}/rfkill'): |
| 2815 | if not entry.startswith('rfkill'): |
| 2816 | continue |
| 2817 | for kind in ('soft', 'hard'): |
| 2818 | try: |
| 2819 | with open(f'{phy}/rfkill/{entry}/{kind}') as f: |
| 2820 | if f.read().strip() == '1': |
| 2821 | return True |
| 2822 | except OSError: |
| 2823 | pass |
| 2824 | except OSError: |
| 2825 | pass |
| 2826 | return False |
| 2827 | |
| 2828 | def _scan_loop(self): |
| 2829 | """Main scanning loop — runs fast continuous scans.""" |
| 2830 | logger.info(f"Scan loop started with {len(self.interfaces)} interface(s)") |
| 2831 | |
| 2832 | while self._running: |
| 2833 | scan_start = time.time() |
| 2834 | |
| 2835 | # Pick up a hot-plugged (or removed) WiFi adapter without a restart. |
| 2836 | if scan_start - self._last_iface_rescan >= self._iface_rescan_interval: |
| 2837 | self._last_iface_rescan = scan_start |
| 2838 | self._refresh_interfaces() |
| 2839 | |
| 2840 | try: |
| 2841 | # Get GPS position |
| 2842 | pos = self._gps.get_position() if self._gps else None |
| 2843 | lat = pos['lat'] if pos else None |
| 2844 | lon = pos['lon'] if pos else None |
| 2845 | alt = pos['alt'] if pos else None |
| 2846 | speed = pos['speed_kmh'] if pos else None |
| 2847 | hdop = pos['hdop'] if pos else None |
| 2848 | |
| 2849 | # Log GPS track periodically |
| 2850 | if pos and (time.time() - self._last_gps_track) >= self._gps_track_interval: |
| 2851 | self.session.log_gps_track( |
| 2852 | lat, lon, alt, speed, |
| 2853 | pos.get('satellites', 0), hdop |
| 2854 | ) |
| 2855 | self._last_gps_track = time.time() |
| 2856 | |
| 2857 | # Scan each interface in parallel — distinct radios can sweep |
| 2858 | # concurrently. With one adapter this is a no-op; with two it |
| 2859 | # halves wall-clock cost and gives each adapter the same RF |
| 2860 | # observation window so antenna comparisons are fair. |
| 2861 | total_found = 0 |
no test coverage detected