mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-03-26 04:46:35 +08:00
fix(atx): 完善串口继电器配置校验与前端防冲突
This commit is contained in:
@@ -73,6 +73,8 @@ impl AtxKeyExecutor {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.validate_runtime_config()?;
|
||||
|
||||
match self.config.driver {
|
||||
AtxDriverType::Gpio => self.init_gpio().await?,
|
||||
AtxDriverType::UsbRelay => self.init_usb_relay().await?,
|
||||
@@ -84,6 +86,39 @@ impl AtxKeyExecutor {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_runtime_config(&self) -> Result<()> {
|
||||
match self.config.driver {
|
||||
AtxDriverType::Serial => {
|
||||
if self.config.pin == 0 {
|
||||
return Err(AppError::Config(
|
||||
"Serial ATX channel must be 1-based (>= 1)".to_string(),
|
||||
));
|
||||
}
|
||||
if self.config.pin > u8::MAX as u32 {
|
||||
return Err(AppError::Config(format!(
|
||||
"Serial ATX channel must be <= {}",
|
||||
u8::MAX
|
||||
)));
|
||||
}
|
||||
if self.config.baud_rate == 0 {
|
||||
return Err(AppError::Config(
|
||||
"Serial ATX baud_rate must be greater than 0".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
AtxDriverType::UsbRelay => {
|
||||
if self.config.pin > u8::MAX as u32 {
|
||||
return Err(AppError::Config(format!(
|
||||
"USB relay channel must be <= {}",
|
||||
u8::MAX
|
||||
)));
|
||||
}
|
||||
}
|
||||
AtxDriverType::Gpio | AtxDriverType::None => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Initialize GPIO backend
|
||||
async fn init_gpio(&mut self) -> Result<()> {
|
||||
info!(
|
||||
@@ -146,11 +181,7 @@ impl AtxKeyExecutor {
|
||||
self.config.device, self.config.pin
|
||||
);
|
||||
|
||||
let baud_rate = if self.config.baud_rate > 0 {
|
||||
self.config.baud_rate
|
||||
} else {
|
||||
9600
|
||||
};
|
||||
let baud_rate = self.config.baud_rate;
|
||||
|
||||
let port = serialport::new(&self.config.device, baud_rate)
|
||||
.timeout(Duration::from_millis(100))
|
||||
@@ -235,7 +266,13 @@ impl AtxKeyExecutor {
|
||||
|
||||
/// Send USB relay command using cached handle
|
||||
fn send_usb_relay_command(&self, on: bool) -> Result<()> {
|
||||
let channel = self.config.pin as u8;
|
||||
let channel = u8::try_from(self.config.pin).map_err(|_| {
|
||||
AppError::Config(format!(
|
||||
"USB relay channel {} exceeds max {}",
|
||||
self.config.pin,
|
||||
u8::MAX
|
||||
))
|
||||
})?;
|
||||
|
||||
// Standard HID relay command format
|
||||
let cmd = if on {
|
||||
@@ -273,9 +310,18 @@ impl AtxKeyExecutor {
|
||||
|
||||
/// Send Serial relay command using cached handle
|
||||
fn send_serial_relay_command(&self, on: bool) -> Result<()> {
|
||||
// user config pin should be 1 for most LCUS modules (A0 01 01 A2)
|
||||
// if user set 0, it will send A0 00 01 A1 which might not work
|
||||
let channel = self.config.pin as u8;
|
||||
let channel = u8::try_from(self.config.pin).map_err(|_| {
|
||||
AppError::Config(format!(
|
||||
"Serial relay channel {} exceeds max {}",
|
||||
self.config.pin,
|
||||
u8::MAX
|
||||
))
|
||||
})?;
|
||||
if channel == 0 {
|
||||
return Err(AppError::Config(
|
||||
"Serial relay channel must be 1-based (>= 1)".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// LCUS-Type Protocol
|
||||
// Frame: [StopByte(A0), Channel, State, Checksum]
|
||||
@@ -408,4 +454,46 @@ mod tests {
|
||||
assert_eq!(timing::LONG_PRESS.as_millis(), 5000);
|
||||
assert_eq!(timing::RESET_PRESS.as_millis(), 500);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_executor_init_rejects_serial_channel_zero() {
|
||||
let config = AtxKeyConfig {
|
||||
driver: AtxDriverType::Serial,
|
||||
device: "/dev/ttyUSB0".to_string(),
|
||||
pin: 0,
|
||||
active_level: ActiveLevel::High,
|
||||
baud_rate: 9600,
|
||||
};
|
||||
let mut executor = AtxKeyExecutor::new(config);
|
||||
let err = executor.init().await.unwrap_err();
|
||||
assert!(matches!(err, AppError::Config(_)));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_executor_init_rejects_serial_channel_overflow() {
|
||||
let config = AtxKeyConfig {
|
||||
driver: AtxDriverType::Serial,
|
||||
device: "/dev/ttyUSB0".to_string(),
|
||||
pin: 256,
|
||||
active_level: ActiveLevel::High,
|
||||
baud_rate: 9600,
|
||||
};
|
||||
let mut executor = AtxKeyExecutor::new(config);
|
||||
let err = executor.init().await.unwrap_err();
|
||||
assert!(matches!(err, AppError::Config(_)));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_executor_init_rejects_zero_serial_baud_rate() {
|
||||
let config = AtxKeyConfig {
|
||||
driver: AtxDriverType::Serial,
|
||||
device: "/dev/ttyUSB0".to_string(),
|
||||
pin: 1,
|
||||
active_level: ActiveLevel::High,
|
||||
baud_rate: 0,
|
||||
};
|
||||
let mut executor = AtxKeyExecutor::new(config);
|
||||
let err = executor.init().await.unwrap_err();
|
||||
assert!(matches!(err, AppError::Config(_)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +1,39 @@
|
||||
//! ATX 配置 Handler
|
||||
//! ATX configuration handlers
|
||||
|
||||
use axum::{extract::State, Json};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::config::AtxConfig;
|
||||
use crate::error::Result;
|
||||
use crate::atx::AtxDriverType;
|
||||
use crate::config::{AtxConfig, HidBackend, HidConfig};
|
||||
use crate::error::{AppError, Result};
|
||||
use crate::state::AppState;
|
||||
|
||||
use super::apply::apply_atx_config;
|
||||
use super::types::AtxConfigUpdate;
|
||||
|
||||
/// 获取 ATX 配置
|
||||
/// Get ATX configuration
|
||||
pub async fn get_atx_config(State(state): State<Arc<AppState>>) -> Json<AtxConfig> {
|
||||
Json(state.config.get().atx.clone())
|
||||
}
|
||||
|
||||
/// 更新 ATX 配置
|
||||
/// Update ATX configuration
|
||||
pub async fn update_atx_config(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(req): Json<AtxConfigUpdate>,
|
||||
) -> Result<Json<AtxConfig>> {
|
||||
// 1. 验证请求
|
||||
req.validate()?;
|
||||
// 1. Read current configuration snapshot
|
||||
let current_config = state.config.get();
|
||||
let old_atx_config = current_config.atx.clone();
|
||||
|
||||
// 2. 获取旧配置
|
||||
let old_atx_config = state.config.get().atx.clone();
|
||||
// 2. Validate request, including merged effective serial parameter checks
|
||||
req.validate_with_current(&old_atx_config)?;
|
||||
|
||||
// 3. 应用更新到配置存储
|
||||
// 3. Ensure ATX serial devices do not conflict with HID CH9329 serial device
|
||||
let mut merged_atx_config = old_atx_config.clone();
|
||||
req.apply_to(&mut merged_atx_config);
|
||||
validate_serial_device_conflict(&merged_atx_config, ¤t_config.hid)?;
|
||||
|
||||
// 4. Persist update into config store
|
||||
state
|
||||
.config
|
||||
.update(|config| {
|
||||
@@ -34,13 +41,65 @@ pub async fn update_atx_config(
|
||||
})
|
||||
.await?;
|
||||
|
||||
// 4. 获取新配置
|
||||
// 5. Load new config
|
||||
let new_atx_config = state.config.get().atx.clone();
|
||||
|
||||
// 5. 应用到子系统(热重载)
|
||||
// 6. Apply to subsystem (hot reload)
|
||||
if let Err(e) = apply_atx_config(&state, &old_atx_config, &new_atx_config).await {
|
||||
tracing::error!("Failed to apply ATX config: {}", e);
|
||||
}
|
||||
|
||||
Ok(Json(new_atx_config))
|
||||
}
|
||||
|
||||
fn validate_serial_device_conflict(atx: &AtxConfig, hid: &HidConfig) -> Result<()> {
|
||||
if hid.backend != HidBackend::Ch9329 {
|
||||
return Ok(());
|
||||
}
|
||||
let reserved = hid.ch9329_port.trim();
|
||||
if reserved.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
for (name, key) in [("power", &atx.power), ("reset", &atx.reset)] {
|
||||
if key.driver == AtxDriverType::Serial && key.device.trim() == reserved {
|
||||
return Err(AppError::BadRequest(format!(
|
||||
"ATX {} serial device '{}' conflicts with HID CH9329 serial device",
|
||||
name, reserved
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_validate_serial_device_conflict_rejects_ch9329_overlap() {
|
||||
let mut atx = AtxConfig::default();
|
||||
atx.power.driver = AtxDriverType::Serial;
|
||||
atx.power.device = "/dev/ttyUSB0".to_string();
|
||||
|
||||
let mut hid = HidConfig::default();
|
||||
hid.backend = HidBackend::Ch9329;
|
||||
hid.ch9329_port = "/dev/ttyUSB0".to_string();
|
||||
|
||||
assert!(validate_serial_device_conflict(&atx, &hid).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_serial_device_conflict_allows_non_ch9329_backend() {
|
||||
let mut atx = AtxConfig::default();
|
||||
atx.power.driver = AtxDriverType::Serial;
|
||||
atx.power.device = "/dev/ttyUSB0".to_string();
|
||||
|
||||
let mut hid = HidConfig::default();
|
||||
hid.backend = HidBackend::None;
|
||||
hid.ch9329_port = "/dev/ttyUSB0".to_string();
|
||||
|
||||
assert!(validate_serial_device_conflict(&atx, &hid).is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,21 +93,21 @@ impl VideoConfigUpdate {
|
||||
|
||||
// ===== Stream Config =====
|
||||
|
||||
/// Stream 配置响应(包含 has_turn_password 字段)
|
||||
/// Stream configuration response (includes has_turn_password)
|
||||
#[typeshare]
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
pub struct StreamConfigResponse {
|
||||
pub mode: StreamMode,
|
||||
pub encoder: EncoderType,
|
||||
pub bitrate_preset: BitratePreset,
|
||||
/// 是否有公共 ICE 服务器可用(编译时确定)
|
||||
/// Whether public ICE servers are available (compile-time decision)
|
||||
pub has_public_ice_servers: bool,
|
||||
/// 当前是否正在使用公共 ICE 服务器(STUN/TURN 都为空时)
|
||||
/// Whether public ICE servers are currently in use (when STUN/TURN are unset)
|
||||
pub using_public_ice_servers: bool,
|
||||
pub stun_server: Option<String>,
|
||||
pub turn_server: Option<String>,
|
||||
pub turn_username: Option<String>,
|
||||
/// 指示是否已设置 TURN 密码(实际密码不返回)
|
||||
/// Indicates whether TURN password has been configured (password is not returned)
|
||||
pub has_turn_password: bool,
|
||||
}
|
||||
|
||||
@@ -397,6 +397,7 @@ impl MsdConfigUpdate {
|
||||
pub struct AtxKeyConfigUpdate {
|
||||
pub driver: Option<crate::atx::AtxDriverType>,
|
||||
pub device: Option<String>,
|
||||
pub baud_rate: Option<u32>,
|
||||
pub pin: Option<u32>,
|
||||
pub active_level: Option<crate::atx::ActiveLevel>,
|
||||
}
|
||||
@@ -439,6 +440,37 @@ impl AtxConfigUpdate {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn validate_with_current(&self, current: &AtxConfig) -> crate::error::Result<()> {
|
||||
self.validate()?;
|
||||
|
||||
// Validate with full context after applying the patch payload.
|
||||
let mut merged = current.clone();
|
||||
self.apply_to(&mut merged);
|
||||
|
||||
Self::validate_effective_key_config(&merged.power, "power")?;
|
||||
Self::validate_effective_key_config(&merged.reset, "reset")?;
|
||||
Self::validate_shared_serial_baud_rate(&merged)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_shared_serial_baud_rate(config: &AtxConfig) -> crate::error::Result<()> {
|
||||
let power = &config.power;
|
||||
let reset = &config.reset;
|
||||
let same_serial_device = power.driver == crate::atx::AtxDriverType::Serial
|
||||
&& reset.driver == crate::atx::AtxDriverType::Serial
|
||||
&& !power.device.trim().is_empty()
|
||||
&& power.device.trim() == reset.device.trim();
|
||||
|
||||
if same_serial_device && power.baud_rate != reset.baud_rate {
|
||||
return Err(AppError::BadRequest(
|
||||
"ATX power/reset sharing the same serial relay device must use one baud_rate"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_key_config(key: &AtxKeyConfigUpdate, name: &str) -> crate::error::Result<()> {
|
||||
if let Some(ref device) = key.device {
|
||||
if !device.is_empty() && !std::path::Path::new(device).exists() {
|
||||
@@ -448,6 +480,88 @@ impl AtxConfigUpdate {
|
||||
)));
|
||||
}
|
||||
}
|
||||
if let Some(baud_rate) = key.baud_rate {
|
||||
if baud_rate == 0 {
|
||||
return Err(AppError::BadRequest(format!(
|
||||
"{} baud_rate must be greater than 0",
|
||||
name
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(driver) = key.driver {
|
||||
match driver {
|
||||
crate::atx::AtxDriverType::Serial => {
|
||||
if let Some(pin) = key.pin {
|
||||
if pin == 0 {
|
||||
return Err(AppError::BadRequest(format!(
|
||||
"{} serial channel must be 1-based (>= 1)",
|
||||
name
|
||||
)));
|
||||
}
|
||||
if pin > u8::MAX as u32 {
|
||||
return Err(AppError::BadRequest(format!(
|
||||
"{} serial channel must be <= {}",
|
||||
name,
|
||||
u8::MAX
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
crate::atx::AtxDriverType::UsbRelay => {
|
||||
if let Some(pin) = key.pin {
|
||||
if pin > u8::MAX as u32 {
|
||||
return Err(AppError::BadRequest(format!(
|
||||
"{} USB relay channel must be <= {}",
|
||||
name,
|
||||
u8::MAX
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
crate::atx::AtxDriverType::Gpio | crate::atx::AtxDriverType::None => {}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_effective_key_config(
|
||||
key: &crate::atx::AtxKeyConfig,
|
||||
name: &str,
|
||||
) -> crate::error::Result<()> {
|
||||
match key.driver {
|
||||
crate::atx::AtxDriverType::Serial => {
|
||||
if key.pin == 0 {
|
||||
return Err(AppError::BadRequest(format!(
|
||||
"{} serial channel must be 1-based (>= 1)",
|
||||
name
|
||||
)));
|
||||
}
|
||||
if key.pin > u8::MAX as u32 {
|
||||
return Err(AppError::BadRequest(format!(
|
||||
"{} serial channel must be <= {}",
|
||||
name,
|
||||
u8::MAX
|
||||
)));
|
||||
}
|
||||
if key.baud_rate == 0 {
|
||||
return Err(AppError::BadRequest(format!(
|
||||
"{} baud_rate must be greater than 0",
|
||||
name
|
||||
)));
|
||||
}
|
||||
}
|
||||
crate::atx::AtxDriverType::UsbRelay => {
|
||||
if key.pin > u8::MAX as u32 {
|
||||
return Err(AppError::BadRequest(format!(
|
||||
"{} USB relay channel must be <= {}",
|
||||
name,
|
||||
u8::MAX
|
||||
)));
|
||||
}
|
||||
}
|
||||
crate::atx::AtxDriverType::Gpio | crate::atx::AtxDriverType::None => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -476,6 +590,9 @@ impl AtxConfigUpdate {
|
||||
if let Some(ref device) = update.device {
|
||||
config.device = device.clone();
|
||||
}
|
||||
if let Some(baud_rate) = update.baud_rate {
|
||||
config.baud_rate = baud_rate;
|
||||
}
|
||||
if let Some(pin) = update.pin {
|
||||
config.pin = pin;
|
||||
}
|
||||
@@ -784,3 +901,83 @@ impl WebConfigUpdate {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_atx_apply_key_update_applies_baud_rate() {
|
||||
let mut config = AtxConfig::default();
|
||||
let update = AtxConfigUpdate {
|
||||
enabled: None,
|
||||
power: Some(AtxKeyConfigUpdate {
|
||||
driver: Some(crate::atx::AtxDriverType::Serial),
|
||||
device: Some("/dev/null".to_string()),
|
||||
baud_rate: Some(19200),
|
||||
pin: Some(1),
|
||||
active_level: None,
|
||||
}),
|
||||
reset: None,
|
||||
led: None,
|
||||
wol_interface: None,
|
||||
};
|
||||
|
||||
update.apply_to(&mut config);
|
||||
assert_eq!(config.power.baud_rate, 19200);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_atx_validate_with_current_rejects_serial_pin_zero() {
|
||||
let mut current = AtxConfig::default();
|
||||
current.power.driver = crate::atx::AtxDriverType::Serial;
|
||||
current.power.device = "/dev/null".to_string();
|
||||
current.power.pin = 1;
|
||||
current.power.baud_rate = 9600;
|
||||
|
||||
let update = AtxConfigUpdate {
|
||||
enabled: None,
|
||||
power: Some(AtxKeyConfigUpdate {
|
||||
driver: None,
|
||||
device: None,
|
||||
baud_rate: None,
|
||||
pin: Some(0),
|
||||
active_level: None,
|
||||
}),
|
||||
reset: None,
|
||||
led: None,
|
||||
wol_interface: None,
|
||||
};
|
||||
|
||||
assert!(update.validate_with_current(¤t).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_atx_validate_with_current_rejects_shared_serial_baud_mismatch() {
|
||||
let mut current = AtxConfig::default();
|
||||
current.power.driver = crate::atx::AtxDriverType::Serial;
|
||||
current.power.device = "/dev/ttyUSB0".to_string();
|
||||
current.power.pin = 1;
|
||||
current.power.baud_rate = 9600;
|
||||
current.reset.driver = crate::atx::AtxDriverType::Serial;
|
||||
current.reset.device = "/dev/ttyUSB0".to_string();
|
||||
current.reset.pin = 2;
|
||||
current.reset.baud_rate = 9600;
|
||||
|
||||
let update = AtxConfigUpdate {
|
||||
enabled: None,
|
||||
power: None,
|
||||
reset: Some(AtxKeyConfigUpdate {
|
||||
driver: None,
|
||||
device: None,
|
||||
baud_rate: Some(115200),
|
||||
pin: None,
|
||||
active_level: None,
|
||||
}),
|
||||
led: None,
|
||||
wol_interface: None,
|
||||
};
|
||||
|
||||
assert!(update.validate_with_current(¤t).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user