diff --git a/core/openwrt/src/wifi_scan.rs b/core/openwrt/src/wifi_scan.rs index 48f75f45..ac21918e 100644 --- a/core/openwrt/src/wifi_scan.rs +++ b/core/openwrt/src/wifi_scan.rs @@ -12,13 +12,57 @@ pub struct ScannedNetwork { pub fn scan_networks(router: &Router) -> Result> { let (iface, temp) = find_wireless_iface(router)?; let output = router.run_ok(&format!("iwinfo {} scan 2>&1", iface))?; + let result = if output.contains("Scanning not possible") { + // Vendor MediaTek `mt_wifi` driver (see find_wireless_iface) doesn't + // support scanning through iwinfo/nl80211 at all. Fall back to its own + // private ioctl site-survey, which works on the same interface. + scan_via_mtk_site_survey(router, &iface) + } else if output.contains("No scan results") || output.trim().is_empty() { + Ok(vec![]) + } else { + parse_iwinfo_scan(&output) + }; if temp { let _ = router.run(&format!("iw dev {} del 2>/dev/null", iface)); } - if output.contains("No scan results") || output.trim().is_empty() { - return Ok(vec![]); + result +} + +fn scan_via_mtk_site_survey(router: &Router, iface: &str) -> Result> { + let _ = router.run(&format!("iwpriv {} set SiteSurvey=1 2>/dev/null", iface)); + std::thread::sleep(std::time::Duration::from_secs(4)); + let output = router.run_ok(&format!("iwpriv {} get_site_survey 2>&1", iface))?; + parse_mtk_site_survey(&output) +} + +/// Parses MediaTek's `iwpriv get_site_survey` fixed-width table. +/// Column offsets come from the header row layout, which is part of the +/// vendor SDK's ioctl response format shared across OEMs (GL.iNet, etc.), +/// not something set per-device. +fn parse_mtk_site_survey(output: &str) -> Result> { + let mut networks = Vec::new(); + for line in output.lines() { + if !line.trim_start().as_bytes().first().is_some_and(u8::is_ascii_digit) { + continue; // skip header/summary lines; data rows start with an index + } + let ssid = line.get(8..41).unwrap_or("").trim().to_string(); + if ssid.is_empty() { + continue; + } + let bssid = line.get(41..61).unwrap_or("").trim().to_string(); + let security = line.get(61..84).unwrap_or(""); + let channel: u8 = line.get(4..8).and_then(|s| s.trim().parse().ok()).unwrap_or(0); + let signal: i32 = line.get(84..92).and_then(|s| s.trim().parse().ok()).unwrap_or(-100); + networks.push(ScannedNetwork { + ssid, + bssid, + signal, + channel, + encryption: normalize_encryption(security), + }); } - parse_iwinfo_scan(&output) + networks.sort_by(|a, b| b.signal.cmp(&a.signal)); + Ok(networks) } /// Returns `(interface_name, is_temporary)`. @@ -31,6 +75,17 @@ fn find_wireless_iface(router: &Router) -> Result<(String, bool)> { return Ok((out.trim().to_string(), false)); } + // Some vendor wifi drivers (e.g. MediaTek's out-of-tree `mt_wifi`/`mtk` SDK + // driver used by GL.iNet and others) never register with cfg80211/mac80211, + // so they have no `iw dev` entry and no /sys/class/ieee80211 phy even though + // the radio is real and already up. `iwinfo` abstracts over those vendor + // backends too, so fall back to its device listing before concluding there's + // no radio at all. + let (iwinfo_out, _) = router.run("iwinfo 2>/dev/null | awk '/^[A-Za-z]/{print $1; exit}'")?; + if !iwinfo_out.trim().is_empty() { + return Ok((iwinfo_out.trim().to_string(), false)); + } + // Find the phy — if this is empty the device has no WiFi hardware at all let (phy_out, _) = router.run("ls /sys/class/ieee80211/ 2>/dev/null | head -1")?; let phy = phy_out.trim().to_string();