archy/core/openwrt/src/wifi_scan.rs

102 lines
3.5 KiB
Rust
Raw Normal View History

use anyhow::Result;
use crate::Router;
pub struct ScannedNetwork {
pub ssid: String,
pub bssid: String,
pub signal: i32,
pub channel: u8,
pub encryption: String,
}
pub fn scan_networks(router: &Router) -> Result<Vec<ScannedNetwork>> {
let iface = find_wireless_iface(router)?;
let output = router.run_ok(&format!("iwinfo {} scan 2>&1", iface))?;
if output.contains("No scan results") || output.trim().is_empty() {
return Ok(vec![]);
}
parse_iwinfo_scan(&output)
}
fn find_wireless_iface(router: &Router) -> Result<String> {
// `iw dev` works on any mac80211 system and handles new-style interface names
// (phy0-ap0, phy0-sta0, etc.) used by OpenWrt 25.x mt76 drivers.
let (out, _) = router.run("iw dev 2>/dev/null | awk '/Interface/{print $2}' | head -1")?;
let iface = out.trim().to_string();
if !iface.is_empty() {
return Ok(iface);
}
// Fallback: any entry in /sys/class/net with a wireless subdir
let (out2, _) = router.run(
"for i in /sys/class/net/*/wireless; do [ -d \"$i\" ] && basename $(dirname $i) && break; done 2>/dev/null",
)?;
let iface2 = out2.trim().to_string();
if !iface2.is_empty() {
return Ok(iface2);
}
anyhow::bail!("No wireless interface found on this router")
}
fn parse_iwinfo_scan(output: &str) -> Result<Vec<ScannedNetwork>> {
let mut networks: Vec<ScannedNetwork> = Vec::new();
let mut current: Option<ScannedNetwork> = None;
for line in output.lines() {
let line = line.trim();
if line.starts_with("Cell ") {
if let Some(n) = current.take() {
if !n.ssid.is_empty() {
networks.push(n);
}
}
let bssid = line.split("Address:").nth(1).unwrap_or("").trim().to_string();
current = Some(ScannedNetwork {
ssid: String::new(),
bssid,
signal: -100,
channel: 0,
encryption: "none".to_string(),
});
} else if let Some(ref mut n) = current {
if let Some(rest) = line.strip_prefix("ESSID:") {
n.ssid = rest.trim().trim_matches('"').to_string();
} else if line.contains("Channel:") && !line.starts_with("Encryption") {
if let Some(ch_part) = line.split("Channel:").nth(1) {
n.channel = ch_part.trim().split_whitespace().next()
.and_then(|s| s.parse().ok())
.unwrap_or(0);
}
} else if line.starts_with("Signal:") {
if let Some(dbm_str) = line.split_whitespace().nth(1) {
n.signal = dbm_str.parse().unwrap_or(-100);
}
} else if let Some(rest) = line.strip_prefix("Encryption:") {
n.encryption = normalize_encryption(rest.trim());
}
}
}
if let Some(n) = current {
if !n.ssid.is_empty() {
networks.push(n);
}
}
networks.sort_by(|a, b| b.signal.cmp(&a.signal));
Ok(networks)
}
fn normalize_encryption(raw: &str) -> String {
let lower = raw.to_lowercase();
if lower.contains("wpa3") || lower.contains("sae") {
"sae".to_string()
} else if lower.contains("wpa2") || lower.contains("psk2") {
"psk2".to_string()
} else if lower.contains("wpa") {
"psk".to_string()
} else if lower.contains("none") || lower.contains("open") || lower.is_empty() {
"none".to_string()
} else {
lower
}
}