318 lines
9.9 KiB
Rust
318 lines
9.9 KiB
Rust
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<DispatchResult, String> {
|
||
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<DispatchResult, String> {
|
||
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<DispatchResult, String> {
|
||
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<DispatchResult, String> {
|
||
parse_command(cmd, "test", "", "")
|
||
}
|
||
|
||
fn dispatch_with_hints(cmd: &str, ssid: &str, pass: &str) -> Result<DispatchResult, String> {
|
||
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)
|
||
));
|
||
}
|
||
}
|