use crate::core::message::{Destination, Envelope, Message, PlayerCommand, WifiCommand}; use crate::core::plugin_ids; /// 命令解析结果 pub struct DispatchResult { pub envelope: Envelope, } /// 解析文本命令为 DispatchResult。 /// /// `ssid_hint` / `password_hint` 仅用于不含参数的 `connect` / `ap_start`(BLE 先写 /// SSID/Password 特征,再写 Command)。带参数版本 `connect:SSID:PASS` 直接解析内联值。 pub fn parse_command( command: &str, from: &str, ssid_hint: &str, password_hint: &str, ) -> Result { let command = command.trim(); if command.is_empty() { return Err("empty command".into()); } // 尝试按冒号拆分以获取命令名和参数 let (cmd, rest) = match command.find(':') { Some(pos) => (&command[..pos], Some(&command[pos + 1..])), None => (command, None), }; match cmd { // ── 播放控制 ── "play" => ok_video(from, Message::PlayerCommand(PlayerCommand::Play)), "pause" => ok_video(from, Message::PlayerCommand(PlayerCommand::Pause)), "next" => ok_video(from, Message::PlayerCommand(PlayerCommand::Next)), "prev" => ok_video(from, Message::PlayerCommand(PlayerCommand::Previous)), "goto" => { let index_str = rest.ok_or("goto requires index (e.g. goto:3)")?; let index: usize = index_str .parse() .map_err(|_| format!("invalid index: {index_str}"))?; ok_video(from, Message::PlayerCommand(PlayerCommand::Goto(index))) } "scene" => { let name = rest.ok_or("scene requires name (e.g. scene:idle)")?; if name.is_empty() { return Err("scene name cannot be empty".into()); } ok_video( from, Message::PlayerCommand(PlayerCommand::ChangeScene(name.to_string())), ) } "trigger" => { let params = rest.ok_or("trigger requires name:value (e.g. trigger:voice:name)")?; let (name, value) = match params.find(':') { Some(pos) => (¶ms[..pos], ¶ms[pos + 1..]), None => (params, ""), }; if name.is_empty() { return Err("trigger name cannot be empty".into()); } ok_video( from, Message::Trigger { name: name.to_string(), value: value.to_string(), }, ) } // ── WiFi 命令 ── "scan" => ok_wifi(from, Message::WifiCommand(WifiCommand::Scan)), "status" => ok_wifi(from, Message::WifiCommand(WifiCommand::Status)), "connect" => { let (ssid, password) = parse_wifi_credentials(rest, ssid_hint, password_hint); if ssid.is_empty() { return Err("ssid required for connect".into()); } ok_wifi( from, Message::WifiCommand(WifiCommand::Connect { ssid, password }), ) } "ap_start" => { let (ssid, password) = parse_wifi_credentials(rest, ssid_hint, password_hint); if ssid.is_empty() { return Err("ssid required for ap_start".into()); } ok_wifi( from, Message::WifiCommand(WifiCommand::ApStart { ssid, password }), ) } "ap_stop" => ok_wifi(from, Message::WifiCommand(WifiCommand::ApStop)), // ── 配置 ── "config_reload" => Ok(DispatchResult { envelope: Envelope { from: from.to_string(), to: Destination::Manager, message: Message::ConfigReloadRequest, }, }), _ => Err(format!("unsupported command: {cmd}")), } } /// 解析 WiFi 凭据:优先使用内联参数 `SSID:PASS`,回退到 hint 值。 /// 密码中可能包含冒号,因此用 `splitn(2, ':')` 拆分。 fn parse_wifi_credentials( rest: Option<&str>, ssid_hint: &str, password_hint: &str, ) -> (String, String) { match rest { Some(params) if !params.is_empty() => { let mut parts = params.splitn(2, ':'); let ssid = parts.next().unwrap_or("").to_string(); let password = parts.next().unwrap_or("").to_string(); if ssid.is_empty() { (ssid_hint.to_string(), password_hint.to_string()) } else { (ssid, password) } } _ => (ssid_hint.to_string(), password_hint.to_string()), } } fn ok_video(from: &str, message: Message) -> Result { Ok(DispatchResult { envelope: Envelope { from: from.to_string(), to: Destination::Plugin(plugin_ids::VIDEO.to_string()), message, }, }) } fn ok_wifi(from: &str, message: Message) -> Result { Ok(DispatchResult { envelope: Envelope { from: from.to_string(), to: Destination::Plugin(plugin_ids::WIFI.to_string()), message, }, }) } #[cfg(test)] mod tests { use super::*; fn dispatch(cmd: &str) -> Result { parse_command(cmd, "test", "", "") } fn dispatch_with_hints(cmd: &str, ssid: &str, pass: &str) -> Result { parse_command(cmd, "test", ssid, pass) } #[test] fn test_player_commands() { assert!(matches!( dispatch("play").unwrap().envelope.message, Message::PlayerCommand(PlayerCommand::Play) )); assert!(matches!( dispatch("pause").unwrap().envelope.message, Message::PlayerCommand(PlayerCommand::Pause) )); assert!(matches!( dispatch("next").unwrap().envelope.message, Message::PlayerCommand(PlayerCommand::Next) )); assert!(matches!( dispatch("prev").unwrap().envelope.message, Message::PlayerCommand(PlayerCommand::Previous) )); } #[test] fn test_goto() { let result = dispatch("goto:5").unwrap(); assert!(matches!( result.envelope.message, Message::PlayerCommand(PlayerCommand::Goto(5)) )); assert!(dispatch("goto").is_err()); assert!(dispatch("goto:abc").is_err()); } #[test] fn test_scene() { let result = dispatch("scene:idle").unwrap(); assert!(matches!( result.envelope.message, Message::PlayerCommand(PlayerCommand::ChangeScene(ref name)) if name == "idle" )); assert!(dispatch("scene").is_err()); assert!(dispatch("scene:").is_err()); } #[test] fn test_trigger() { let result = dispatch("trigger:voice:name").unwrap(); assert!(matches!( result.envelope.message, Message::Trigger { ref name, ref value } if name == "voice" && value == "name" )); // trigger without value let result = dispatch("trigger:button").unwrap(); assert!(matches!( result.envelope.message, Message::Trigger { ref name, ref value } if name == "button" && value == "" )); assert!(dispatch("trigger").is_err()); assert!(dispatch("trigger:").is_err()); } #[test] fn test_wifi_scan_status() { assert!(matches!( dispatch("scan").unwrap().envelope.message, Message::WifiCommand(WifiCommand::Scan) )); assert!(matches!( dispatch("status").unwrap().envelope.message, Message::WifiCommand(WifiCommand::Status) )); } #[test] fn test_connect_with_hint() { let result = dispatch_with_hints("connect", "MySSID", "pass123").unwrap(); assert!(matches!( result.envelope.message, Message::WifiCommand(WifiCommand::Connect { ref ssid, ref password }) if ssid == "MySSID" && password == "pass123" )); } #[test] fn test_connect_inline() { let result = dispatch("connect:MySSID:p@ss:w0rd").unwrap(); assert!(matches!( result.envelope.message, Message::WifiCommand(WifiCommand::Connect { ref ssid, ref password }) if ssid == "MySSID" && password == "p@ss:w0rd" )); } #[test] fn test_connect_no_ssid() { assert!(dispatch("connect").is_err()); } #[test] fn test_ap_start_inline() { let result = dispatch("ap_start:showen:12345678").unwrap(); assert!(matches!( result.envelope.message, Message::WifiCommand(WifiCommand::ApStart { ref ssid, ref password }) if ssid == "showen" && password == "12345678" )); } #[test] fn test_ap_stop() { assert!(matches!( dispatch("ap_stop").unwrap().envelope.message, Message::WifiCommand(WifiCommand::ApStop) )); } #[test] fn test_config_reload() { let result = dispatch("config_reload").unwrap(); assert!(matches!( result.envelope.message, Message::ConfigReloadRequest )); assert!(matches!(result.envelope.to, Destination::Manager)); } #[test] fn test_routing() { let r = dispatch("play").unwrap(); assert!(matches!(r.envelope.to, Destination::Plugin(ref id) if id == "video")); let r = dispatch("scan").unwrap(); assert!(matches!(r.envelope.to, Destination::Plugin(ref id) if id == "wifi")); } #[test] fn test_unsupported() { assert!(dispatch("unknown").is_err()); assert!(dispatch("").is_err()); } #[test] fn test_whitespace_trimmed() { assert!(matches!( dispatch(" play ").unwrap().envelope.message, Message::PlayerCommand(PlayerCommand::Play) )); } }