Files
ShowenV2/src/core/dispatch.rs

318 lines
9.9 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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) => (&params[..pos], &params[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)
));
}
}