Compare commits
10 Commits
8952bb14d2
...
5770b7007c
| Author | SHA1 | Date | |
|---|---|---|---|
| 5770b7007c | |||
| 1ded747a0c | |||
| 0cdea53f8d | |||
| c52dd6b336 | |||
| 8d1ebf02e3 | |||
| 98d827f5c3 | |||
| c8dece351c | |||
| fbe064253c | |||
| 2507629b4c | |||
| 96a170f719 |
@@ -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}]");
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
/// 自动回退到上一个稳定版本
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<()> {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<()> {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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\\"#,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user