Compare commits

..

10 Commits

13 changed files with 77 additions and 110 deletions

View File

@@ -4,8 +4,6 @@ use std::collections::{HashMap, HashSet};
use std::fs;
use std::path::{Path, PathBuf};
pub type Config = AppConfig;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AppConfig {
@@ -72,6 +70,7 @@ pub struct PerspectiveCorrectionConfig {
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ChromaKeyConfig {
#[serde(default)]
pub enabled: bool,
@@ -106,6 +105,7 @@ fn default_hsv_max() -> [i32; 3] {
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct BrightnessAdjustConfig {
#[serde(default)]
pub enabled: bool,
@@ -272,8 +272,9 @@ pub struct RemoteControlConfig {
pub port: u16,
}
/// BLE 配网配置(新增)
/// BLE 配网配置
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct BleConfig {
#[serde(default = "default_ble_enabled")]
pub enabled: bool,
@@ -443,14 +444,16 @@ impl PerspectiveCorrectionConfig {
impl ChromaKeyConfig {
pub fn validate(&self) -> Result<()> {
validate_hsv_component("display.chroma_key.hsv_min[0]", self.hsv_min[0], 0, 180)?;
validate_hsv_component("display.chroma_key.hsv_min[1]", self.hsv_min[1], 0, 255)?;
validate_hsv_component("display.chroma_key.hsv_min[2]", self.hsv_min[2], 0, 255)?;
validate_hsv_component("display.chroma_key.hsv_max[0]", self.hsv_max[0], 0, 180)?;
validate_hsv_component("display.chroma_key.hsv_max[1]", self.hsv_max[1], 0, 255)?;
validate_hsv_component("display.chroma_key.hsv_max[2]", self.hsv_max[2], 0, 255)?;
for index in 0..3 {
let hsv_ranges = [180, 255, 255]; // H, S, V max values
for (index, &max_val) in hsv_ranges.iter().enumerate() {
validate_hsv_component(
&format!("display.chroma_key.hsv_min[{index}]"),
self.hsv_min[index], 0, max_val,
)?;
validate_hsv_component(
&format!("display.chroma_key.hsv_max[{index}]"),
self.hsv_max[index], 0, max_val,
)?;
if self.hsv_min[index] > self.hsv_max[index] {
bail!("display.chroma_key hsv_min[{index}] 不能大于 hsv_max[{index}]");
}

View File

@@ -1,4 +1,5 @@
use crate::core::message::{Destination, Envelope, Message, PlayerCommand, WifiCommand};
use crate::core::plugin_ids;
/// 命令解析结果
pub struct DispatchResult {
@@ -137,7 +138,7 @@ fn ok_video(from: &str, message: Message) -> Result<DispatchResult, String> {
Ok(DispatchResult {
envelope: Envelope {
from: from.to_string(),
to: Destination::Plugin("video".to_string()),
to: Destination::Plugin(plugin_ids::VIDEO.to_string()),
message,
},
})
@@ -147,7 +148,7 @@ fn ok_wifi(from: &str, message: Message) -> Result<DispatchResult, String> {
Ok(DispatchResult {
envelope: Envelope {
from: from.to_string(),
to: Destination::Plugin("wifi".to_string()),
to: Destination::Plugin(plugin_ids::WIFI.to_string()),
message,
},
})

View File

@@ -10,7 +10,7 @@ pub struct Envelope {
}
/// 消息目的地
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum Destination {
/// 点对点发送给指定插件
Plugin(String),
@@ -68,7 +68,7 @@ pub enum Message {
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum PlayerCommand {
Play,
Pause,
@@ -78,7 +78,7 @@ pub enum PlayerCommand {
ChangeScene(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PlayerStatusData {
pub running: bool,
pub paused: bool,
@@ -88,7 +88,7 @@ pub struct PlayerStatusData {
pub current_video: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum WifiCommand {
Scan,
Connect { ssid: String, password: String },
@@ -100,7 +100,7 @@ pub enum WifiCommand {
// ── 设备管理 ──
/// 像素格式
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum PixelFormat {
/// RGBA 8888 格式(每像素 4 字节)
RGBA8888,
@@ -111,7 +111,7 @@ pub enum PixelFormat {
}
/// 传感器类型
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum SensorType {
/// 温度传感器
Temperature,
@@ -124,7 +124,7 @@ pub enum SensorType {
}
/// 触摸动作
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum TouchAction {
/// 按下
Down,
@@ -135,7 +135,7 @@ pub enum TouchAction {
}
/// 设备能力
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum DeviceCapability {
/// 显示屏
Display,

View File

@@ -9,5 +9,15 @@ pub mod plugin_repo;
pub mod service_manager;
pub mod version_manager;
/// 内置插件 ID 常量
pub mod plugin_ids {
pub const VIDEO: &str = "video";
pub const HTTP: &str = "http";
pub const WIFI: &str = "wifi";
pub const BLE: &str = "ble";
pub const DEVICE: &str = "device";
pub const SCREEN: &str = "screen";
}
#[cfg(test)]
mod tests;

View File

@@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize};
use std::sync::{mpsc, Arc};
/// 单项能力测试结果
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CapabilityTestResult {
pub capability: String,
pub passed: bool,
@@ -56,7 +56,7 @@ pub trait Plugin: Send {
fn stop(&mut self) -> Result<()>;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PluginInfo {
pub name: String,
pub version: String,

View File

@@ -52,7 +52,7 @@ fn default_test_timeout() -> u64 {
}
/// 插件错误处理策略
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ErrorPolicy {
/// 自动回退到上一个稳定版本

View File

@@ -1137,7 +1137,7 @@ impl ServiceManager {
}
/// 插件状态信息(用于 API 查询)
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct PluginStateInfo {
pub id: String,
pub info: crate::core::plugin::PluginInfo,

View File

@@ -244,7 +244,7 @@ impl Plugin for HttpPlugin {
}
fn dependencies(&self) -> Vec<String> {
vec!["video".to_string()]
vec![crate::core::plugin_ids::VIDEO.to_string()]
}
fn init(&mut self, ctx: PluginContext) -> Result<()> {

View File

@@ -1712,11 +1712,17 @@ fn read_attachment_response(path: &Path) -> warp::reply::Response {
match std::fs::read(path) {
Ok(data) => {
let filename = path.file_name().unwrap_or_default().to_string_lossy();
// Sanitize filename for Content-Disposition header to prevent header injection
let safe_filename = filename
.replace('\\', "_")
.replace('"', "_")
.replace('\r', "")
.replace('\n', "");
match warp::http::Response::builder()
.header("Content-Type", "application/octet-stream")
.header(
"Content-Disposition",
format!("attachment; filename=\"{filename}\""),
format!("attachment; filename=\"{safe_filename}\""),
)
.header("Content-Length", data.len().to_string())
.body(data)

View File

@@ -9,6 +9,7 @@
use crate::core::{
message::{Destination, DeviceCommand, Envelope, Message},
plugin::*,
plugin_ids,
};
use anyhow::Result;
@@ -25,7 +26,7 @@ impl ScreenPlugin {
if let Some(ctx) = &self.ctx {
let envelope = Envelope {
from: self.id().to_string(),
to: Destination::Plugin("device".to_string()),
to: Destination::Plugin(plugin_ids::DEVICE.to_string()),
message: Message::DeviceCommand(DeviceCommand::SetSleepInhibit(true)),
};
let _ = ctx.tx.send(envelope);
@@ -36,7 +37,7 @@ impl ScreenPlugin {
if let Some(ctx) = &self.ctx {
let envelope = Envelope {
from: self.id().to_string(),
to: Destination::Plugin("device".to_string()),
to: Destination::Plugin(plugin_ids::DEVICE.to_string()),
message: Message::DeviceCommand(DeviceCommand::SetSleepInhibit(false)),
};
let _ = ctx.tx.send(envelope);
@@ -47,7 +48,7 @@ impl ScreenPlugin {
if let Some(ctx) = &self.ctx {
let envelope = Envelope {
from: self.id().to_string(),
to: Destination::Plugin("device".to_string()),
to: Destination::Plugin(plugin_ids::DEVICE.to_string()),
message: Message::DeviceCommand(DeviceCommand::SetCursorVisible(!hidden)),
};
let _ = ctx.tx.send(envelope);
@@ -76,7 +77,7 @@ impl Plugin for ScreenPlugin {
}
fn dependencies(&self) -> Vec<String> {
vec!["device".to_string()]
vec![plugin_ids::DEVICE.to_string()]
}
fn init(&mut self, ctx: PluginContext) -> Result<()> {

View File

@@ -7,6 +7,7 @@ pub mod state_machine;
use crate::core::message::{Destination, Envelope, Message, PlayerCommand, PlayerStatusData};
use crate::core::plugin::{Platform, Plugin, PluginContext, PluginInfo};
use crate::core::plugin_ids;
use anyhow::{anyhow, Context, Result};
use opencv::highgui;
use processor::VideoProcessor;
@@ -151,7 +152,7 @@ impl Plugin for VideoPlugin {
if let Some(ctx) = &self.ctx {
let _ = ctx.tx.send(Envelope {
from: self.id().to_string(),
to: Destination::Plugin("screen".to_string()),
to: Destination::Plugin(plugin_ids::SCREEN.to_string()),
message: Message::ScreenLockRequest(true),
});
}
@@ -162,7 +163,7 @@ impl Plugin for VideoPlugin {
if let Some(ctx) = &self.ctx {
let _ = ctx.tx.send(Envelope {
from: self.id().to_string(),
to: Destination::Plugin("screen".to_string()),
to: Destination::Plugin(plugin_ids::SCREEN.to_string()),
message: Message::ScreenLockRequest(false),
});
}
@@ -356,12 +357,7 @@ fn publish_state_changed(
}
fn status_changed(old: &PlayerStatusData, new: &PlayerStatusData) -> bool {
old.running != new.running
|| old.paused != new.paused
|| old.in_transition != new.in_transition
|| old.current_index != new.current_index
|| old.playlist_length != new.playlist_length
|| old.current_video != new.current_video
old != new
}
fn lock_processor(

View File

@@ -13,28 +13,26 @@ use opencv::{
};
use rand::Rng;
use std::collections::HashSet;
use std::sync::{Arc, Mutex, MutexGuard};
use std::sync::{Arc, Mutex, MutexGuard, OnceLock};
use std::time::Instant;
type WindowRectKey = (String, i32, i32, i32, i32);
type LoggedSizes = ((i32, i32), (i32, i32), (i32, i32));
fn log_mat_size(label: &str, frame: &Mat) {
use std::sync::Mutex;
static SEEN: Mutex<Option<HashSet<(String, i32, i32)>>> = Mutex::new(None);
static SEEN: OnceLock<Mutex<HashSet<(String, i32, i32)>>> = OnceLock::new();
let key = (label.to_string(), frame.cols(), frame.rows());
let mut guard = SEEN.lock().unwrap();
let set = guard.get_or_insert_with(HashSet::new);
if set.insert(key) {
let mut guard = SEEN
.get_or_init(|| Mutex::new(HashSet::new()))
.lock()
.unwrap();
if guard.insert(key) {
println!("{}: cols={} rows={}", label, frame.cols(), frame.rows());
}
}
fn log_window_image_rect(window_name: &str) {
use std::sync::Mutex;
static SEEN: Mutex<Option<HashSet<WindowRectKey>>> = Mutex::new(None);
static SEEN: OnceLock<Mutex<HashSet<WindowRectKey>>> = OnceLock::new();
match highgui::get_window_image_rect(window_name) {
Ok(rect) => {
let key = (
@@ -44,9 +42,11 @@ fn log_window_image_rect(window_name: &str) {
rect.width,
rect.height,
);
let mut guard = SEEN.lock().unwrap();
let set = guard.get_or_insert_with(HashSet::new);
if set.insert(key) {
let mut guard = SEEN
.get_or_init(|| Mutex::new(HashSet::new()))
.lock()
.unwrap();
if guard.insert(key) {
println!(
"window.image_rect {}: x={} y={} width={} height={}",
window_name, rect.x, rect.y, rect.width, rect.height,

View File

@@ -36,25 +36,6 @@ impl WifiPlugin {
Self { ctx: None }
}
fn build_connect_args(ssid: &str, password: &str) -> Vec<String> {
let mut args: Vec<String> = ["device", "wifi", "connect", ssid]
.into_iter()
.map(String::from)
.collect();
if !password.trim().is_empty() {
args.push("password".to_string());
args.push(password.to_string());
}
args
}
fn build_hotspot_args(ssid: &str, password: &str) -> Vec<String> {
["device", "wifi", "hotspot", "ssid", ssid, "password", password]
.into_iter()
.map(String::from)
.collect()
}
fn run_nmcli(args: &[impl AsRef<std::ffi::OsStr> + std::fmt::Debug]) -> Result<String> {
let output = Command::new("nmcli")
.args(args)
@@ -189,7 +170,12 @@ impl WifiPlugin {
}
fn connect_network(&self, ssid: &str, password: &str) -> Result<serde_json::Value> {
let output = Self::run_nmcli(&Self::build_connect_args(ssid, password))?;
let mut args = vec!["device", "wifi", "connect", ssid];
if !password.trim().is_empty() {
args.push("password");
args.push(password);
}
let output = Self::run_nmcli(&args)?;
Ok(json!({
"ok": true,
@@ -260,7 +246,7 @@ impl WifiPlugin {
}
fn ap_start(&self, ssid: &str, password: &str) -> Result<serde_json::Value> {
let output = Self::run_nmcli(&Self::build_hotspot_args(ssid, password))?;
let output = Self::run_nmcli(&["device", "wifi", "hotspot", "ssid", ssid, "password", password])?;
Ok(json!({
"ok": true,
@@ -280,7 +266,7 @@ impl WifiPlugin {
"connection",
"show",
"--active",
])?;;
])?;
let hotspot_name = active
.lines()
.map(str::trim)
@@ -362,40 +348,4 @@ mod tests {
assert_eq!(fields, vec!["wlan0", "wifi", "connected", "Office:LAN"]);
}
#[test]
fn connect_args_preserve_special_characters() {
let args =
WifiPlugin::build_connect_args(r#"ssid \"qa\" demo"#, r#"p@ss\\word with spaces"#);
assert_eq!(
args,
vec![
"device",
"wifi",
"connect",
r#"ssid \"qa\" demo"#,
"password",
r#"p@ss\\word with spaces"#,
]
);
}
#[test]
fn hotspot_args_preserve_special_characters() {
let args = WifiPlugin::build_hotspot_args("Showen AP", r#"\\quoted pass\\"#);
assert_eq!(
args,
vec![
"device",
"wifi",
"hotspot",
"ssid",
"Showen AP",
"password",
r#"\\quoted pass\\"#,
]
);
}
}