feat: config验证 + StateMachine + WifiPlugin + ScreenPlugin
团队交付 Phase 1 第一轮: - 张明远: config.rs 完整验证逻辑 (Display/VideoItem/Transition/Scenes/StateMachine) - 李思琪: state_machine.rs 完整实现 (defer/ignore triggers, 加权随机, loop range) - 王浩然: wifi/mod.rs WiFi管理插件 (scan/connect/status/ap via nmcli) - 赵雨薇: screen/mod.rs 屏幕管理插件 (systemd-inhibit唤醒锁 + unclutter光标) cargo check 零 warning 通过。 Co-Authored-By: GPT-5.4 <noreply@openai.com> Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,23 +1,223 @@
|
||||
//! WifiPlugin — WiFi 管理
|
||||
//!
|
||||
//! 通过 nmcli 实现 WiFi 扫描、连接、AP 热点。
|
||||
//! 通过 nmcli 实现 WiFi 扫描、连接、状态查询、AP 热点启停。
|
||||
|
||||
use crate::core::message::Message;
|
||||
use crate::core::plugin::{Plugin, PluginContext, PluginInfo, Platform};
|
||||
use anyhow::Result;
|
||||
use crate::core::{message::*, plugin::*};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use serde::Serialize;
|
||||
use serde_json::json;
|
||||
use std::collections::HashMap;
|
||||
use std::process::Command;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
pub struct WifiPlugin {
|
||||
ctx: Option<PluginContext>,
|
||||
}
|
||||
|
||||
#[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<String>,
|
||||
}
|
||||
|
||||
impl WifiPlugin {
|
||||
pub fn new() -> Self {
|
||||
Self { ctx: None }
|
||||
}
|
||||
|
||||
fn run_nmcli(args: &[&str]) -> Result<String> {
|
||||
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::Manager,
|
||||
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<serde_json::Value> {
|
||||
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())
|
||||
.map(|line| {
|
||||
let mut parts = line.splitn(3, ':');
|
||||
let ssid = parts.next().unwrap_or_default().trim().to_string();
|
||||
let signal = parts
|
||||
.next()
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.parse::<i32>()
|
||||
.unwrap_or_default();
|
||||
let security = parts.next().unwrap_or_default().trim().to_string();
|
||||
|
||||
WifiNetwork {
|
||||
ssid,
|
||||
signal,
|
||||
security,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(json!({
|
||||
"ok": true,
|
||||
"action": "scan",
|
||||
"networks": networks,
|
||||
}))
|
||||
}
|
||||
|
||||
fn connect_network(&self, ssid: &str, password: &str) -> Result<serde_json::Value> {
|
||||
let output = Self::run_nmcli(&["device", "wifi", "connect", ssid, "password", password])?;
|
||||
|
||||
Ok(json!({
|
||||
"ok": true,
|
||||
"action": "connect",
|
||||
"ssid": ssid,
|
||||
"output": output,
|
||||
}))
|
||||
}
|
||||
|
||||
fn status(&self) -> Result<serde_json::Value> {
|
||||
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<String, Vec<String>> = 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::<Vec<_>>();
|
||||
|
||||
Ok(json!({
|
||||
"ok": true,
|
||||
"action": "status",
|
||||
"devices": devices,
|
||||
}))
|
||||
}
|
||||
|
||||
fn ap_start(&self, ssid: &str, password: &str) -> Result<serde_json::Value> {
|
||||
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<serde_json::Value> {
|
||||
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 Plugin for WifiPlugin {
|
||||
fn id(&self) -> &'static str { "wifi" }
|
||||
fn id(&self) -> &'static str {
|
||||
"wifi"
|
||||
}
|
||||
|
||||
fn info(&self) -> PluginInfo {
|
||||
PluginInfo {
|
||||
@@ -33,7 +233,20 @@ impl Plugin for WifiPlugin {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn start(&mut self) -> Result<()> { Ok(()) }
|
||||
fn handle_message(&mut self, _msg: Message) -> Result<()> { Ok(()) }
|
||||
fn stop(&mut self) -> Result<()> { 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(())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user