//! WifiPlugin — WiFi 管理 //! //! 通过 nmcli 实现 WiFi 扫描、连接、状态查询、AP 热点启停。 use crate::core::{message::*, plugin::*}; use anyhow::{anyhow, Context, Result}; use serde::Serialize; use serde_json::json; use std::collections::{HashMap, HashSet}; use std::process::Command; use std::thread; use std::time::Duration; pub struct WifiPlugin { ctx: Option, } #[derive(Serialize)] struct WifiNetwork { ssid: String, signal: i32, security: String, } #[derive(Serialize)] struct DeviceStatus { device: String, device_type: String, state: String, connection: String, ip4_addresses: Vec, } impl WifiPlugin { pub fn new() -> Self { Self { ctx: None } } fn run_nmcli(args: &[&str]) -> Result { let output = Command::new("nmcli") .args(args) .output() .with_context(|| format!("failed to execute nmcli {:?}", args))?; if output.status.success() { Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()) } else { let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string(); let message = if !stderr.is_empty() { stderr } else { stdout }; Err(anyhow!("nmcli {:?} failed: {}", args, message)) } } fn send_result(&self, payload: String) -> Result<()> { let ctx = self .ctx .as_ref() .context("wifi plugin context is not initialized")?; ctx.tx.send(Envelope { from: "wifi", to: Destination::Broadcast, message: Message::WifiResult(payload), })?; Ok(()) } fn handle_wifi_command(&self, cmd: WifiCommand) -> String { let result = match cmd { WifiCommand::Scan => self.scan_networks(), WifiCommand::Connect { ssid, password } => self.connect_network(&ssid, &password), WifiCommand::Status => self.status(), WifiCommand::ApStart { ssid, password } => self.ap_start(&ssid, &password), WifiCommand::ApStop => self.ap_stop(), }; match result { Ok(value) => value.to_string(), Err(err) => json!({ "ok": false, "error": err.to_string(), }) .to_string(), } } fn scan_networks(&self) -> Result { Self::run_nmcli(&["device", "wifi", "rescan"])?; thread::sleep(Duration::from_secs(2)); let output = Self::run_nmcli(&["-t", "-f", "SSID,SIGNAL,SECURITY", "device", "wifi", "list"])?; let networks = output .lines() .filter(|line| !line.trim().is_empty()) .filter_map(|line| { let mut parts = line.splitn(3, ':'); let ssid = parts.next().unwrap_or_default().trim().to_string(); if ssid.is_empty() { return None; } let signal = parts .next() .unwrap_or_default() .trim() .parse::() .unwrap_or_default(); let security = parts.next().unwrap_or_default().trim().to_string(); Some(WifiNetwork { ssid, signal, security, }) }) .fold( (HashSet::new(), Vec::new()), |(mut seen, mut networks), network| { if seen.insert(network.ssid.clone()) { networks.push(network); } (seen, networks) }, ) .1; Ok(json!({ "ok": true, "action": "scan", "networks": networks, })) } fn connect_network(&self, ssid: &str, password: &str) -> Result { let mut args = vec!["device", "wifi", "connect", ssid]; if !password.trim().is_empty() { args.extend(["password", password]); } let output = Self::run_nmcli(&args)?; Ok(json!({ "ok": true, "action": "connect", "ssid": ssid, "output": output, })) } fn status(&self) -> Result { let device_output = Self::run_nmcli(&[ "-t", "-f", "DEVICE,TYPE,STATE,CONNECTION", "device", "status", ])?; let ip_output = Self::run_nmcli(&["-t", "-f", "DEVICE,IP4.ADDRESS", "device", "show"])?; let mut ip_map: HashMap> = HashMap::new(); for line in ip_output.lines().filter(|line| !line.trim().is_empty()) { let mut parts = line.splitn(2, ':'); let device = parts.next().unwrap_or_default().trim(); let address = parts.next().unwrap_or_default().trim(); if device.is_empty() || address.is_empty() { continue; } ip_map .entry(device.to_string()) .or_default() .push(address.to_string()); } let devices = device_output .lines() .filter(|line| !line.trim().is_empty()) .map(|line| { let mut parts = line.splitn(4, ':'); let device = parts.next().unwrap_or_default().trim().to_string(); DeviceStatus { ip4_addresses: ip_map.remove(&device).unwrap_or_default(), device, device_type: parts.next().unwrap_or_default().trim().to_string(), state: parts.next().unwrap_or_default().trim().to_string(), connection: parts.next().unwrap_or_default().trim().to_string(), } }) .collect::>(); Ok(json!({ "ok": true, "action": "status", "devices": devices, })) } fn ap_start(&self, ssid: &str, password: &str) -> Result { let output = Self::run_nmcli(&[ "device", "wifi", "hotspot", "ssid", ssid, "password", password, ])?; Ok(json!({ "ok": true, "action": "ap_start", "ssid": ssid, "output": output, })) } fn ap_stop(&self) -> Result { let active = Self::run_nmcli(&["-t", "-f", "NAME", "connection", "show", "--active"])?; let hotspot_name = active .lines() .map(str::trim) .find(|name| *name == "hotspot") .ok_or_else(|| anyhow!("active hotspot connection 'hotspot' not found"))?; let output = Self::run_nmcli(&["connection", "down", hotspot_name])?; Ok(json!({ "ok": true, "action": "ap_stop", "connection": hotspot_name, "output": output, })) } } impl Default for WifiPlugin { fn default() -> Self { Self::new() } } impl Plugin for WifiPlugin { fn id(&self) -> &'static str { "wifi" } fn info(&self) -> PluginInfo { PluginInfo { name: "WiFi Manager", version: "0.2.0", description: "WiFi 管理 (nmcli)", platform: Platform::Linux, } } fn dependencies(&self) -> Vec<&'static str> { vec![] } fn init(&mut self, ctx: PluginContext) -> Result<()> { self.ctx = Some(ctx); Ok(()) } fn start(&mut self) -> Result<()> { Ok(()) } fn handle_message(&mut self, msg: Message) -> Result<()> { if let Message::WifiCommand(cmd) = msg { let payload = self.handle_wifi_command(cmd); self.send_result(payload)?; } Ok(()) } fn stop(&mut self) -> Result<()> { Ok(()) } }