fix BLE wifi status delivery and websocket compile issues

This commit is contained in:
showen
2026-03-12 08:07:21 +08:00
parent 7548064401
commit 8ed9c93c8e
10 changed files with 886 additions and 49 deletions

View File

@@ -1,7 +1,7 @@
use crate::core::config::AppConfig;
use crate::core::message::{Envelope, Message};
use anyhow::Result;
use std::sync::{mpsc, Arc};
use crate::core::config::AppConfig;
/// 所有功能都通过实现此 trait 接入系统
pub trait Plugin: Send {
@@ -11,6 +11,11 @@ pub trait Plugin: Send {
/// 插件信息
fn info(&self) -> PluginInfo;
/// 声明启动所需的插件依赖
fn dependencies(&self) -> Vec<&'static str> {
vec![]
}
/// 初始化:获取发送通道,声明订阅的消息类型
fn init(&mut self, ctx: PluginContext) -> Result<()>;

View File

@@ -1,7 +1,8 @@
use crate::core::config::AppConfig;
use crate::core::message::{Destination, Envelope, Message};
use crate::core::plugin::{Plugin, PluginContext};
use anyhow::Result;
use anyhow::{anyhow, Result};
use std::collections::{HashMap, HashSet};
use std::sync::{mpsc, Arc};
/// 中央调度器:插件注册、生命周期管理、消息路由
@@ -33,6 +34,8 @@ impl ServiceManager {
/// 按注册顺序 init() + start() 所有插件
pub fn start_all(&mut self) -> Result<()> {
self.validate_and_sort_plugins()?;
// init
for plugin in &mut self.plugins {
let ctx = PluginContext {
@@ -105,18 +108,120 @@ impl ServiceManager {
println!("[ServiceManager] 收到 Shutdown 指令");
self.broadcast_message(Message::Shutdown);
}
Message::WifiResult(payload) => {
self.broadcast_message(Message::WifiResult(payload));
}
Message::PlayerStatus(status) => {
self.broadcast_message(Message::PlayerStatus(status));
}
Message::StateChanged {
old_state,
new_state,
} => {
self.broadcast_message(Message::StateChanged {
old_state,
new_state,
});
}
Message::WifiProvisioned { ssid, ip } => {
self.broadcast_message(Message::WifiProvisioned { ssid, ip });
}
Message::ConfigReloadRequest => {
println!("[ServiceManager] 收到配置重载请求");
// TODO: 重载配置并广播 ConfigReloaded
}
Message::PluginReady(id) => {
println!("[ServiceManager] 插件 '{}' 就绪", id);
self.broadcast_message(Message::PluginReady(id));
}
_ => {}
}
Ok(())
}
fn validate_and_sort_plugins(&mut self) -> Result<()> {
let mut plugin_ids = Vec::with_capacity(self.plugins.len());
let mut plugin_set = HashSet::with_capacity(self.plugins.len());
let mut dependency_map = HashMap::with_capacity(self.plugins.len());
for plugin in &self.plugins {
let id = plugin.id();
if !plugin_set.insert(id) {
return Err(anyhow!("duplicate plugin id registered: '{id}'"));
}
plugin_ids.push(id);
dependency_map.insert(id, plugin.dependencies());
}
for (plugin_id, dependencies) in &dependency_map {
for dependency in dependencies {
if dependency == plugin_id {
return Err(anyhow!("plugin '{plugin_id}' cannot depend on itself"));
}
if !plugin_set.contains(dependency) {
return Err(anyhow!(
"plugin '{plugin_id}' depends on missing plugin '{dependency}'"
));
}
}
}
let mut resolved = HashSet::with_capacity(plugin_ids.len());
let mut sorted_ids = Vec::with_capacity(plugin_ids.len());
while sorted_ids.len() < plugin_ids.len() {
let mut progressed = false;
for plugin_id in &plugin_ids {
if resolved.contains(plugin_id) {
continue;
}
let dependencies = dependency_map
.get(plugin_id)
.expect("plugin dependency map must contain all registered ids");
if dependencies
.iter()
.all(|dependency| resolved.contains(dependency))
{
resolved.insert(*plugin_id);
sorted_ids.push(*plugin_id);
progressed = true;
}
}
if !progressed {
let unresolved = plugin_ids
.iter()
.copied()
.filter(|plugin_id| !resolved.contains(plugin_id))
.collect::<Vec<_>>()
.join(", ");
return Err(anyhow!(
"plugin dependency cycle detected among: {unresolved}"
));
}
}
let mut remaining_plugins = std::mem::take(&mut self.plugins);
let mut ordered_plugins = Vec::with_capacity(remaining_plugins.len());
for plugin_id in sorted_ids {
let index = remaining_plugins
.iter()
.position(|plugin| plugin.id() == plugin_id)
.ok_or_else(|| anyhow!("plugin '{plugin_id}' disappeared during sorting"))?;
ordered_plugins.push(remaining_plugins.remove(index));
}
self.plugins = ordered_plugins;
Ok(())
}
fn broadcast_message(&mut self, msg: Message) {
let should_shutdown = matches!(&msg, Message::Shutdown);

View File

@@ -4,6 +4,7 @@ use dbus::arg::{PropMap, Variant};
use dbus::blocking::stdintf::org_freedesktop_dbus::{ObjectManager, Properties};
use dbus::blocking::Connection;
use dbus::channel::MatchingReceiver;
use dbus::channel::Sender;
use dbus::message::MatchRule;
use dbus::Path;
use dbus_crossroads::{Crossroads, IfaceBuilder, IfaceToken, MethodErr};
@@ -46,6 +47,8 @@ struct SharedState {
ssid: Arc<Mutex<String>>,
password: Arc<Mutex<String>>,
status: Arc<Mutex<String>>,
notifying: Arc<AtomicBool>,
pending_notify: Arc<AtomicBool>,
}
impl SharedState {
@@ -55,6 +58,8 @@ impl SharedState {
ssid: Arc::new(Mutex::new(String::new())),
password: Arc::new(Mutex::new(String::new())),
status: Arc::new(Mutex::new(r#"{"ok":true,"action":"idle"}"#.to_string())),
notifying: Arc::new(AtomicBool::new(false)),
pending_notify: Arc::new(AtomicBool::new(false)),
}
}
@@ -62,6 +67,15 @@ impl SharedState {
self.status.lock().unwrap().as_bytes().to_vec()
}
fn read_value(&self, kind: &CharacteristicKind) -> Vec<u8> {
match kind {
CharacteristicKind::Status => self.read_status(),
CharacteristicKind::Ssid
| CharacteristicKind::Password
| CharacteristicKind::Command => Vec::new(),
}
}
fn set_ssid(&self, value: &[u8]) {
*self.ssid.lock().unwrap() = bytes_to_string(value);
}
@@ -72,6 +86,22 @@ impl SharedState {
fn set_status(&self, status: impl Into<String>) {
*self.status.lock().unwrap() = status.into();
self.pending_notify.store(true, Ordering::SeqCst);
}
fn set_notifying(&self, notifying: bool) {
self.notifying.store(notifying, Ordering::SeqCst);
if notifying {
self.pending_notify.store(true, Ordering::SeqCst);
}
}
fn is_notifying(&self) -> bool {
self.notifying.load(Ordering::SeqCst)
}
fn take_pending_notification(&self) -> bool {
self.pending_notify.swap(false, Ordering::SeqCst)
}
fn dispatch_command(&self, raw: &[u8]) -> Result<()> {
@@ -294,7 +324,7 @@ fn run_server_connection(
service: Path::from(SERVICE_PATH.to_string()),
flags: vec!["read".to_string(), "notify".to_string()],
kind: CharacteristicKind::Status,
shared,
shared: shared.clone(),
},
);
cr.insert(
@@ -330,6 +360,9 @@ fn run_server_connection(
.map_err(|_| anyhow!("failed to notify BLE server readiness"))?;
while !stop.load(Ordering::SeqCst) {
if shared.is_notifying() && shared.take_pending_notification() {
emit_status_notification(&conn_server, &shared)?;
}
conn_server
.process(SERVER_TIMEOUT)
.context("BLE server connection process loop failed")?;
@@ -377,18 +410,18 @@ fn register_characteristic_iface(
.get(|_, data| Ok(data.flags.clone()));
b.property::<Vec<Path<'static>>, _>("Descriptors")
.get(|_, _| Ok(Vec::new()));
b.property::<Vec<u8>, _>("Value")
.get(|_, data| Ok(data.shared.read_value(&data.kind)));
b.property::<bool, _>("Notifying").get(|_, data| {
Ok(matches!(data.kind, CharacteristicKind::Status) && data.shared.is_notifying())
});
b.method(
"ReadValue",
("options",),
("value",),
|_, data, (_options,): (PropMap,)| {
let value = match data.kind {
CharacteristicKind::Ssid => Vec::new(),
CharacteristicKind::Password => Vec::new(),
CharacteristicKind::Command => Vec::new(),
CharacteristicKind::Status => data.shared.read_status(),
};
let value = data.shared.read_value(&data.kind);
Ok((value,))
},
);
@@ -413,8 +446,24 @@ fn register_characteristic_iface(
},
);
b.method("StartNotify", (), (), |_, _, ()| Ok(()));
b.method("StopNotify", (), (), |_, _, ()| Ok(()));
b.method("StartNotify", (), (), |_, data, ()| match data.kind {
CharacteristicKind::Status => {
data.shared.set_notifying(true);
Ok(())
}
_ => Err(MethodErr::failed(
"notify is only supported on status characteristic",
)),
});
b.method("StopNotify", (), (), |_, data, ()| match data.kind {
CharacteristicKind::Status => {
data.shared.set_notifying(false);
Ok(())
}
_ => Err(MethodErr::failed(
"notify is only supported on status characteristic",
)),
});
},
)
}
@@ -482,7 +531,14 @@ fn build_managed_objects() -> ManagedObjects {
Variant(Box::new(Path::from(SERVICE_PATH))),
);
props.insert("UUID".into(), Variant(Box::new(uuid.to_string())));
let value = if path == CHAR_STATUS_PATH {
r#"{"ok":true,"action":"idle"}"#.as_bytes().to_vec()
} else {
Vec::new()
};
props.insert("Flags".into(), Variant(Box::new(flags)));
props.insert("Value".into(), Variant(Box::new(value)));
props.insert("Notifying".into(), Variant(Box::new(false)));
props.insert(
"Descriptors".into(),
Variant(Box::new(Vec::<Path<'static>>::new())),
@@ -588,3 +644,25 @@ fn drain_control_messages(shared: &SharedState, control_rx: &Receiver<BleControl
}
}
}
fn emit_status_notification(conn: &Connection, shared: &SharedState) -> Result<()> {
let mut changed = PropMap::new();
changed.insert("Value".into(), Variant(Box::new(shared.read_status())));
changed.insert("Notifying".into(), Variant(Box::new(shared.is_notifying())));
let signal = dbus::Message::signal(
&Path::from(CHAR_STATUS_PATH),
&"org.freedesktop.DBus.Properties".into(),
&"PropertiesChanged".into(),
)
.append3(
"org.bluez.GattCharacteristic1".to_string(),
changed,
Vec::<String>::new(),
);
conn.send(signal)
.map_err(|_| anyhow!("failed to emit BLE status notification"))?;
Ok(())
}

View File

@@ -51,6 +51,10 @@ impl Plugin for BlePlugin {
}
}
fn dependencies(&self) -> Vec<&'static str> {
vec![]
}
fn init(&mut self, ctx: PluginContext) -> Result<()> {
self.stop.store(false, Ordering::SeqCst);
self.ctx = Some(ctx);

View File

@@ -6,10 +6,29 @@ mod routes;
use crate::core::config::AppConfig;
use crate::core::message::{Envelope, Message};
use crate::core::plugin::{Plugin, PluginContext, PluginInfo, Platform};
use crate::core::plugin::{Platform, Plugin, PluginContext, PluginInfo};
use anyhow::{Context, Result};
use serde::Serialize;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Condvar, Mutex};
use tokio::sync::broadcast;
#[derive(Serialize)]
struct WsEvent<'a, T> {
#[serde(rename = "type")]
event_type: &'a str,
data: T,
}
fn encode_ws_event<T: Serialize>(event_type: &str, data: T) -> Option<String> {
match serde_json::to_string(&WsEvent { event_type, data }) {
Ok(payload) => Some(payload),
Err(error) => {
eprintln!("[HttpPlugin] failed to serialize websocket event '{event_type}': {error}");
None
}
}
}
struct PendingWifiResponse {
version: u64,
@@ -22,10 +41,12 @@ pub(crate) struct HttpState {
config: Mutex<Arc<AppConfig>>,
player_status: Mutex<crate::core::message::PlayerStatusData>,
ble_ready: AtomicBool,
ws_events: broadcast::Sender<String>,
}
impl HttpState {
fn new(config: Arc<AppConfig>) -> Self {
let (ws_events, _) = broadcast::channel(32);
let player_status = crate::core::message::PlayerStatusData {
running: false,
paused: !config.playback.auto_start,
@@ -44,6 +65,7 @@ impl HttpState {
config: Mutex::new(config),
player_status: Mutex::new(player_status),
ble_ready: AtomicBool::new(false),
ws_events,
}
}
@@ -92,8 +114,42 @@ impl HttpState {
self.ble_ready.load(Ordering::SeqCst)
}
fn publish_ws(&self, payload: String) {
let _ = self.ws_events.send(payload);
}
pub(crate) fn ws_snapshots(&self) -> Vec<String> {
let mut snapshots = Vec::new();
if let Some(payload) = encode_ws_event("status_update", self.player_status()) {
snapshots.push(payload);
}
let config = self.config();
if let Some(payload) = encode_ws_event("config_update", config.as_ref()) {
snapshots.push(payload);
}
if let Some(payload) = encode_ws_event(
"ble_update",
serde_json::json!({ "ready": self.ble_ready() }),
) {
snapshots.push(payload);
}
snapshots
}
pub(crate) fn ws_subscribe(&self) -> broadcast::Receiver<String> {
self.ws_events.subscribe()
}
fn set_ble_ready(&self, ready: bool) {
self.ble_ready.store(ready, Ordering::SeqCst);
if let Some(payload) = encode_ws_event("ble_update", serde_json::json!({ "ready": ready }))
{
self.publish_ws(payload);
}
}
}
@@ -118,7 +174,9 @@ impl Default for HttpPlugin {
}
impl Plugin for HttpPlugin {
fn id(&self) -> &'static str { "http" }
fn id(&self) -> &'static str {
"http"
}
fn info(&self) -> PluginInfo {
PluginInfo {
@@ -129,6 +187,10 @@ impl Plugin for HttpPlugin {
}
}
fn dependencies(&self) -> Vec<&'static str> {
vec!["video"]
}
fn init(&mut self, ctx: PluginContext) -> Result<()> {
self.state = Some(Arc::new(HttpState::new(Arc::clone(&ctx.config))));
self.ctx = Some(ctx);
@@ -201,8 +263,29 @@ impl Plugin for HttpPlugin {
match msg {
Message::WifiResult(payload) => state.publish_wifi_result(payload),
Message::PlayerStatus(status) => state.update_player_status(status),
Message::ConfigReloaded(config) => state.replace_config(config),
Message::PlayerStatus(status) => {
state.update_player_status(status.clone());
if let Some(payload) = encode_ws_event("status_update", &status) {
state.publish_ws(payload);
}
}
Message::ConfigReloaded(config) => {
state.replace_config(Arc::clone(&config));
if let Some(payload) = encode_ws_event("config_update", config.as_ref()) {
state.publish_ws(payload);
}
}
Message::StateChanged {
old_state,
new_state,
} => {
if let Some(payload) = encode_ws_event(
"state_update",
serde_json::json!({ "old_state": old_state, "new_state": new_state }),
) {
state.publish_ws(payload);
}
}
Message::PluginReady("ble") => state.set_ble_ready(true),
Message::Shutdown => state.set_ble_ready(false),
_ => {}
@@ -211,5 +294,7 @@ impl Plugin for HttpPlugin {
Ok(())
}
fn stop(&mut self) -> Result<()> { Ok(()) }
fn stop(&mut self) -> Result<()> {
Ok(())
}
}

View File

@@ -2,7 +2,8 @@ use super::HttpState;
use crate::core::config::{self, AppConfig};
use crate::core::message::{Destination, Envelope, Message, PlayerCommand, WifiCommand};
use bytes::Buf;
use futures_util::TryStreamExt;
use futures_util::{SinkExt, StreamExt, TryStreamExt};
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::convert::Infallible;
@@ -19,13 +20,13 @@ struct WifiConnectRequest {
password: String,
}
#[derive(Deserialize)]
#[derive(Default, Deserialize)]
struct WifiApStartRequest {
ssid: Option<String>,
password: Option<String>,
}
#[derive(Deserialize)]
#[derive(Default, Deserialize)]
struct BleStartRequest {
device_name: Option<String>,
}
@@ -82,9 +83,9 @@ pub(crate) fn build_routes(
.or(wifi_ap_stop_route(tx.clone(), Arc::clone(&state)))
.or(ble_start_route(Arc::clone(&state)))
.or(ble_stop_route())
.or(ble_status_route(state));
.or(ble_status_route(Arc::clone(&state)));
root_route().or(api).with(
root_route().or(ws_route(Arc::clone(&state))).or(api).with(
warp::cors()
.allow_any_origin()
.allow_headers(["content-type"])
@@ -98,6 +99,16 @@ fn root_route() -> impl Filter<Extract = impl Reply, Error = warp::Rejection> +
.map(|| warp::reply::html(WEB_UI_HTML))
}
fn ws_route(
state: Arc<HttpState>,
) -> impl Filter<Extract = impl Reply, Error = warp::Rejection> + Clone {
warp::path("ws").and(warp::ws()).and(with_state(state)).map(
|ws: warp::ws::Ws, state: Arc<HttpState>| {
ws.on_upgrade(move |socket| websocket_session(socket, state))
},
)
}
fn status_route(
state: Arc<HttpState>,
) -> impl Filter<Extract = impl Reply, Error = warp::Rejection> + Clone {
@@ -216,9 +227,9 @@ fn scene_route(
fn trigger_route(
tx: mpsc::Sender<Envelope>,
) -> impl Filter<Extract = impl Reply, Error = warp::Rejection> + Clone {
warp::path!("api" / "trigger" / String / String)
let with_value = warp::path!("api" / "trigger" / String / String)
.and(warp::post())
.and(with_tx(tx))
.and(with_tx(tx.clone()))
.and_then(|name: String, value: String, tx| async move {
send_video_command(
tx,
@@ -229,7 +240,24 @@ fn trigger_route(
format!("触发器 '{name}' 已发送,值: {value}"),
)
.await
})
});
let without_value = warp::path!("api" / "trigger" / String)
.and(warp::post())
.and(with_tx(tx))
.and_then(|name: String, tx| async move {
send_video_command(
tx,
Message::Trigger {
name: name.clone(),
value: String::new(),
},
format!("触发器 '{name}' 已发送"),
)
.await
});
with_value.or(without_value)
}
fn config_get_route(
@@ -357,10 +385,14 @@ fn wifi_ap_start_route(
) -> impl Filter<Extract = impl Reply, Error = warp::Rejection> + Clone {
warp::path!("api" / "wifi" / "ap" / "start")
.and(warp::post())
.and(warp::body::json())
.and(warp::body::bytes())
.and(with_tx(tx))
.and(with_state(state))
.and_then(|req: WifiApStartRequest, tx, state| async move {
.and_then(|body: bytes::Bytes, tx, state| async move {
let req: WifiApStartRequest = match parse_optional_json(&body) {
Ok(req) => req,
Err(reply) => return Ok::<_, Infallible>(*reply),
};
let ssid = req.ssid.unwrap_or_else(|| "showen".to_string());
let password = req.password.unwrap_or_else(|| "12345678".to_string());
let success_ssid = ssid.clone();
@@ -383,8 +415,10 @@ fn wifi_ap_stop_route(
.and(with_tx(tx))
.and(with_state(state))
.and_then(|tx, state| async move {
wifi_action_reply(tx, state, WifiCommand::ApStop, |_| "AP 热点已关闭".to_string())
.await
wifi_action_reply(tx, state, WifiCommand::ApStop, |_| {
"AP 热点已关闭".to_string()
})
.await
})
}
@@ -393,11 +427,17 @@ fn ble_start_route(
) -> impl Filter<Extract = impl Reply, Error = warp::Rejection> + Clone {
warp::path!("api" / "ble" / "start")
.and(warp::post())
.and(warp::body::json())
.and(warp::body::bytes())
.and(with_state(state))
.and_then(|req: BleStartRequest, state: Arc<HttpState>| async move {
.and_then(|body: bytes::Bytes, state: Arc<HttpState>| async move {
let req: BleStartRequest = match parse_optional_json(&body) {
Ok(req) => req,
Err(reply) => return Ok::<_, Infallible>(*reply),
};
let config = state.config();
let device_name = req.device_name.unwrap_or_else(|| config.ble.device_name.clone());
let device_name = req
.device_name
.unwrap_or_else(|| config.ble.device_name.clone());
Ok::<_, Infallible>(success_json(format!(
"BLE 配网服务已内嵌运行中,设备名: {device_name}"
)))
@@ -436,7 +476,12 @@ async fn handle_config_update(
) -> Result<warp::reply::Response, Infallible> {
let raw = match std::str::from_utf8(&body) {
Ok(raw) => raw,
Err(_) => return Ok(error_json(StatusCode::BAD_REQUEST, "请求体不是有效的 UTF-8")),
Err(_) => {
return Ok(error_json(
StatusCode::BAD_REQUEST,
"请求体不是有效的 UTF-8",
))
}
};
let current = state.config();
@@ -687,7 +732,10 @@ async fn wifi_request(
while guard.version == version {
let now = Instant::now();
if now >= deadline {
return Err(error_json(StatusCode::GATEWAY_TIMEOUT, "等待 WiFi 响应超时"));
return Err(error_json(
StatusCode::GATEWAY_TIMEOUT,
"等待 WiFi 响应超时",
));
}
let result = state
@@ -705,7 +753,10 @@ async fn wifi_request(
guard = next_guard;
if wait_result.timed_out() && guard.version == version {
return Err(error_json(StatusCode::GATEWAY_TIMEOUT, "等待 WiFi 响应超时"));
return Err(error_json(
StatusCode::GATEWAY_TIMEOUT,
"等待 WiFi 响应超时",
));
}
}
@@ -731,6 +782,64 @@ async fn wifi_request(
Ok(payload)
}
async fn websocket_session(ws: warp::ws::WebSocket, state: Arc<HttpState>) {
let (mut sender, mut receiver) = ws.split();
for payload in state.ws_snapshots() {
if sender.send(warp::ws::Message::text(payload)).await.is_err() {
return;
}
}
let mut events = state.ws_subscribe();
loop {
tokio::select! {
event = events.recv() => {
match event {
Ok(payload) => {
if sender.send(warp::ws::Message::text(payload)).await.is_err() {
break;
}
}
Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {}
Err(tokio::sync::broadcast::error::RecvError::Closed) => break,
}
}
incoming = receiver.next() => {
match incoming {
Some(Ok(message)) => {
if message.is_ping() {
if sender.send(warp::ws::Message::pong(message.as_bytes())).await.is_err() {
break;
}
} else if message.is_close() {
break;
}
}
Some(Err(_)) | None => break,
}
}
}
}
}
fn parse_optional_json<T>(body: &bytes::Bytes) -> Result<T, Box<warp::reply::Response>>
where
T: DeserializeOwned + Default,
{
if body.is_empty() {
return Ok(T::default());
}
serde_json::from_slice(body).map_err(|error| {
Box::new(error_json(
StatusCode::BAD_REQUEST,
&format!("JSON 格式错误: {error}"),
))
})
}
fn list_video_files(dir: &Path) -> Vec<VideoFileInfo> {
let mut files = Vec::new();

View File

@@ -6,7 +6,7 @@ use crate::core::{message::*, plugin::*};
use anyhow::{anyhow, Context, Result};
use serde::Serialize;
use serde_json::json;
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use std::process::Command;
use std::thread;
use std::time::Duration;
@@ -60,7 +60,7 @@ impl WifiPlugin {
ctx.tx.send(Envelope {
from: "wifi",
to: Destination::Manager,
to: Destination::Broadcast,
message: Message::WifiResult(payload),
})?;
@@ -95,9 +95,12 @@ impl WifiPlugin {
let networks = output
.lines()
.filter(|line| !line.trim().is_empty())
.map(|line| {
.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()
@@ -106,13 +109,22 @@ impl WifiPlugin {
.unwrap_or_default();
let security = parts.next().unwrap_or_default().trim().to_string();
WifiNetwork {
Some(WifiNetwork {
ssid,
signal,
security,
}
})
})
.collect::<Vec<_>>();
.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,
@@ -122,7 +134,11 @@ impl WifiPlugin {
}
fn connect_network(&self, ssid: &str, password: &str) -> Result<serde_json::Value> {
let output = Self::run_nmcli(&["device", "wifi", "connect", ssid, "password", password])?;
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,
@@ -234,6 +250,10 @@ impl Plugin for WifiPlugin {
}
}
fn dependencies(&self) -> Vec<&'static str> {
vec![]
}
fn init(&mut self, ctx: PluginContext) -> Result<()> {
self.ctx = Some(ctx);
Ok(())