mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-03-15 07:26:44 +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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -584,10 +584,12 @@ export default {
|
||||
atxDriver: 'Driver Type',
|
||||
atxDriverNone: 'Disabled',
|
||||
atxDriverGpio: 'GPIO',
|
||||
atxDriverUsbRelay: 'USB Relay',
|
||||
atxDriverUsbRelay: 'USB LCUS HID Relay',
|
||||
atxDriverSerial: 'USB LCUS Serial Relay',
|
||||
atxDevice: 'Device',
|
||||
atxPin: 'GPIO Pin',
|
||||
atxChannel: 'Relay Channel',
|
||||
atxSharedSerialBaudHint: 'When Power and Reset share one serial relay device, baud rate is controlled by the first config',
|
||||
atxActiveLevel: 'Active Level',
|
||||
atxLevelHigh: 'Active High',
|
||||
atxLevelLow: 'Active Low',
|
||||
|
||||
@@ -584,10 +584,12 @@ export default {
|
||||
atxDriver: '驱动类型',
|
||||
atxDriverNone: '禁用',
|
||||
atxDriverGpio: 'GPIO',
|
||||
atxDriverUsbRelay: 'USB 继电器',
|
||||
atxDriverUsbRelay: 'USB LCUS HID继电器',
|
||||
atxDriverSerial: 'USB LCUS 串口继电器',
|
||||
atxDevice: '设备',
|
||||
atxPin: 'GPIO 引脚',
|
||||
atxChannel: '继电器通道',
|
||||
atxSharedSerialBaudHint: 'Power 与 Reset 使用同一串口继电器时,波特率由第一个配置统一控制',
|
||||
atxActiveLevel: '有效电平',
|
||||
atxLevelHigh: '高电平有效',
|
||||
atxLevelLow: '低电平有效',
|
||||
|
||||
@@ -321,7 +321,7 @@ const config = ref({
|
||||
turn_password: '',
|
||||
})
|
||||
|
||||
// 跟踪服务器是否已配置 TURN 密码
|
||||
// Tracks whether TURN password is configured on the server
|
||||
const hasTurnPassword = ref(false)
|
||||
const configLoaded = ref(false)
|
||||
const devicesLoaded = ref(false)
|
||||
@@ -623,6 +623,22 @@ const atxDevices = ref<AtxDevices>({
|
||||
serial_ports: [],
|
||||
})
|
||||
|
||||
const ch9329ReservedSerialDevice = computed(() => {
|
||||
if (config.value.hid_backend !== 'ch9329') return ''
|
||||
return config.value.hid_serial_device.trim()
|
||||
})
|
||||
|
||||
const isSharedAtxSerialRelay = computed(() => {
|
||||
const power = atxConfig.value.power
|
||||
const reset = atxConfig.value.reset
|
||||
return (
|
||||
power.driver === 'serial'
|
||||
&& reset.driver === 'serial'
|
||||
&& !!power.device.trim()
|
||||
&& power.device === reset.device
|
||||
)
|
||||
})
|
||||
|
||||
// Encoder backend
|
||||
const availableBackends = ref<EncoderBackendInfo[]>([])
|
||||
|
||||
@@ -816,16 +832,16 @@ async function changePassword() {
|
||||
}
|
||||
}
|
||||
|
||||
// Save config - 使用域分离 API
|
||||
// Save config using domain-separated APIs
|
||||
async function saveConfig() {
|
||||
loading.value = true
|
||||
saved.value = false
|
||||
|
||||
try {
|
||||
// 根据当前激活的 section 只保存相关配置
|
||||
// Save only config related to the active section
|
||||
const savePromises: Promise<unknown>[] = []
|
||||
|
||||
// Video 配置(包括编码器和 WebRTC/STUN/TURN 设置)
|
||||
// Video config (including encoder and WebRTC/STUN/TURN settings)
|
||||
if (activeSection.value === 'video') {
|
||||
savePromises.push(
|
||||
configStore.updateVideo({
|
||||
@@ -836,7 +852,7 @@ async function saveConfig() {
|
||||
fps: config.value.video_fps,
|
||||
})
|
||||
)
|
||||
// 同时保存 Stream/Encoder 和 STUN/TURN 配置
|
||||
// Save Stream/Encoder and STUN/TURN config together
|
||||
savePromises.push(
|
||||
configStore.updateStream({
|
||||
encoder: config.value.encoder_backend as any,
|
||||
@@ -848,7 +864,7 @@ async function saveConfig() {
|
||||
)
|
||||
}
|
||||
|
||||
// HID 配置
|
||||
// HID config
|
||||
if (activeSection.value === 'hid') {
|
||||
if (!isHidFunctionSelectionValid.value) {
|
||||
return
|
||||
@@ -875,7 +891,7 @@ async function saveConfig() {
|
||||
ch9329_port: config.value.hid_serial_device || undefined,
|
||||
ch9329_baudrate: config.value.hid_serial_baudrate,
|
||||
}
|
||||
// 如果是 OTG 后端,添加描述符配置
|
||||
// Add descriptor config for OTG backend
|
||||
if (config.value.hid_backend === 'otg') {
|
||||
hidUpdate.otg_descriptor = {
|
||||
vendor_id: parseInt(otgVendorIdHex.value, 16) || 0x1d6b,
|
||||
@@ -898,7 +914,7 @@ async function saveConfig() {
|
||||
)
|
||||
}
|
||||
|
||||
// MSD 配置
|
||||
// MSD config
|
||||
if (activeSection.value === 'msd') {
|
||||
savePromises.push(
|
||||
configStore.updateMsd({
|
||||
@@ -917,10 +933,10 @@ async function saveConfig() {
|
||||
}
|
||||
}
|
||||
|
||||
// Load config - 使用域分离 API
|
||||
// Load config using domain-separated APIs
|
||||
async function loadConfig() {
|
||||
try {
|
||||
// 并行加载所有域配置
|
||||
// Load all domain configs in parallel
|
||||
const [video, stream, hid, msd] = await Promise.all([
|
||||
configStore.refreshVideo(),
|
||||
configStore.refreshStream(),
|
||||
@@ -952,13 +968,13 @@ async function loadConfig() {
|
||||
stun_server: stream.stun_server || '',
|
||||
turn_server: stream.turn_server || '',
|
||||
turn_username: stream.turn_username || '',
|
||||
turn_password: '', // 密码不从服务器返回,仅用于设置
|
||||
turn_password: '', // Password is never returned from server; set-only field
|
||||
}
|
||||
|
||||
// 设置是否已配置 TURN 密码
|
||||
// Track whether TURN password is configured
|
||||
hasTurnPassword.value = stream.has_turn_password || false
|
||||
|
||||
// 加载 OTG 描述符配置
|
||||
// Load OTG descriptor config
|
||||
if (hid.otg_descriptor) {
|
||||
otgVendorIdHex.value = hid.otg_descriptor.vendor_id?.toString(16).padStart(4, '0') || '1d6b'
|
||||
otgProductIdHex.value = hid.otg_descriptor.product_id?.toString(16).padStart(4, '0') || '0104'
|
||||
@@ -1154,6 +1170,8 @@ async function loadAtxConfig() {
|
||||
led: { ...config.led },
|
||||
wol_interface: config.wol_interface || '',
|
||||
}
|
||||
clearAtxSerialDeviceConflicts()
|
||||
syncSharedAtxSerialBaudRate()
|
||||
} catch (e) {
|
||||
console.error('Failed to load ATX config:', e)
|
||||
}
|
||||
@@ -1171,6 +1189,7 @@ async function saveAtxConfig() {
|
||||
loading.value = true
|
||||
saved.value = false
|
||||
try {
|
||||
syncSharedAtxSerialBaudRate()
|
||||
await configStore.updateAtx({
|
||||
enabled: atxConfig.value.enabled,
|
||||
power: {
|
||||
@@ -1185,7 +1204,9 @@ async function saveAtxConfig() {
|
||||
device: atxConfig.value.reset.device || undefined,
|
||||
pin: atxConfig.value.reset.pin,
|
||||
active_level: atxConfig.value.reset.active_level,
|
||||
baud_rate: atxConfig.value.reset.baud_rate,
|
||||
baud_rate: isSharedAtxSerialRelay.value
|
||||
? atxConfig.value.power.baud_rate
|
||||
: atxConfig.value.reset.baud_rate,
|
||||
},
|
||||
led: {
|
||||
enabled: atxConfig.value.led.enabled,
|
||||
@@ -1215,6 +1236,55 @@ function getAtxDevicesForDriver(driver: string): string[] {
|
||||
return []
|
||||
}
|
||||
|
||||
function isAtxSerialDeviceReserved(device: string): boolean {
|
||||
const reserved = ch9329ReservedSerialDevice.value
|
||||
return !!reserved && device === reserved
|
||||
}
|
||||
|
||||
function formatAtxDeviceLabel(driver: string, device: string): string {
|
||||
if (driver === 'serial' && isAtxSerialDeviceReserved(device)) {
|
||||
return `${device} (CH9329 in use)`
|
||||
}
|
||||
return device
|
||||
}
|
||||
|
||||
function clearAtxSerialDeviceConflicts() {
|
||||
const reserved = ch9329ReservedSerialDevice.value
|
||||
if (!reserved) return
|
||||
|
||||
if (atxConfig.value.power.driver === 'serial' && atxConfig.value.power.device === reserved) {
|
||||
atxConfig.value.power.device = ''
|
||||
}
|
||||
if (atxConfig.value.reset.driver === 'serial' && atxConfig.value.reset.device === reserved) {
|
||||
atxConfig.value.reset.device = ''
|
||||
}
|
||||
}
|
||||
|
||||
function syncSharedAtxSerialBaudRate() {
|
||||
if (!isSharedAtxSerialRelay.value) return
|
||||
atxConfig.value.reset.baud_rate = atxConfig.value.power.baud_rate
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [config.value.hid_backend, config.value.hid_serial_device],
|
||||
() => {
|
||||
clearAtxSerialDeviceConflicts()
|
||||
},
|
||||
)
|
||||
|
||||
watch(
|
||||
() => [
|
||||
atxConfig.value.power.driver,
|
||||
atxConfig.value.power.device,
|
||||
atxConfig.value.power.baud_rate,
|
||||
atxConfig.value.reset.driver,
|
||||
atxConfig.value.reset.device,
|
||||
],
|
||||
() => {
|
||||
syncSharedAtxSerialBaudRate()
|
||||
},
|
||||
)
|
||||
|
||||
// RustDesk management functions
|
||||
async function loadRustdeskConfig() {
|
||||
rustdeskLoading.value = true
|
||||
@@ -2482,21 +2552,34 @@ watch(() => config.value.hid_backend, async () => {
|
||||
<option value="none">{{ t('settings.atxDriverNone') }}</option>
|
||||
<option value="gpio">{{ t('settings.atxDriverGpio') }}</option>
|
||||
<option value="usbrelay">{{ t('settings.atxDriverUsbRelay') }}</option>
|
||||
<option value="serial">Serial (LCUS)</option>
|
||||
<option value="serial">{{ t('settings.atxDriverSerial') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="power-device">{{ t('settings.atxDevice') }}</Label>
|
||||
<select id="power-device" v-model="atxConfig.power.device" class="w-full h-9 px-3 rounded-md border border-input bg-background text-sm" :disabled="atxConfig.power.driver === 'none'">
|
||||
<option value="">{{ t('settings.selectDevice') }}</option>
|
||||
<option v-for="dev in getAtxDevicesForDriver(atxConfig.power.driver)" :key="dev" :value="dev">{{ dev }}</option>
|
||||
<option
|
||||
v-for="dev in getAtxDevicesForDriver(atxConfig.power.driver)"
|
||||
:key="dev"
|
||||
:value="dev"
|
||||
:disabled="atxConfig.power.driver === 'serial' && isAtxSerialDeviceReserved(dev)"
|
||||
>
|
||||
{{ formatAtxDeviceLabel(atxConfig.power.driver, dev) }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<div class="space-y-2">
|
||||
<Label for="power-pin">{{ ['usbrelay', 'serial'].includes(atxConfig.power.driver) ? t('settings.atxChannel') : t('settings.atxPin') }}</Label>
|
||||
<Input id="power-pin" type="number" v-model.number="atxConfig.power.pin" min="0" :disabled="atxConfig.power.driver === 'none'" />
|
||||
<Input
|
||||
id="power-pin"
|
||||
type="number"
|
||||
v-model.number="atxConfig.power.pin"
|
||||
:min="atxConfig.power.driver === 'serial' ? 1 : 0"
|
||||
:disabled="atxConfig.power.driver === 'none'"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="atxConfig.power.driver === 'gpio'" class="space-y-2">
|
||||
<Label for="power-level">{{ t('settings.atxActiveLevel') }}</Label>
|
||||
@@ -2533,21 +2616,34 @@ watch(() => config.value.hid_backend, async () => {
|
||||
<option value="none">{{ t('settings.atxDriverNone') }}</option>
|
||||
<option value="gpio">{{ t('settings.atxDriverGpio') }}</option>
|
||||
<option value="usbrelay">{{ t('settings.atxDriverUsbRelay') }}</option>
|
||||
<option value="serial">Serial (LCUS)</option>
|
||||
<option value="serial">{{ t('settings.atxDriverSerial') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="reset-device">{{ t('settings.atxDevice') }}</Label>
|
||||
<select id="reset-device" v-model="atxConfig.reset.device" class="w-full h-9 px-3 rounded-md border border-input bg-background text-sm" :disabled="atxConfig.reset.driver === 'none'">
|
||||
<option value="">{{ t('settings.selectDevice') }}</option>
|
||||
<option v-for="dev in getAtxDevicesForDriver(atxConfig.reset.driver)" :key="dev" :value="dev">{{ dev }}</option>
|
||||
<option
|
||||
v-for="dev in getAtxDevicesForDriver(atxConfig.reset.driver)"
|
||||
:key="dev"
|
||||
:value="dev"
|
||||
:disabled="atxConfig.reset.driver === 'serial' && isAtxSerialDeviceReserved(dev)"
|
||||
>
|
||||
{{ formatAtxDeviceLabel(atxConfig.reset.driver, dev) }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<div class="space-y-2">
|
||||
<Label for="reset-pin">{{ ['usbrelay', 'serial'].includes(atxConfig.reset.driver) ? t('settings.atxChannel') : t('settings.atxPin') }}</Label>
|
||||
<Input id="reset-pin" type="number" v-model.number="atxConfig.reset.pin" min="0" :disabled="atxConfig.reset.driver === 'none'" />
|
||||
<Input
|
||||
id="reset-pin"
|
||||
type="number"
|
||||
v-model.number="atxConfig.reset.pin"
|
||||
:min="atxConfig.reset.driver === 'serial' ? 1 : 0"
|
||||
:disabled="atxConfig.reset.driver === 'none'"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="atxConfig.reset.driver === 'gpio'" class="space-y-2">
|
||||
<Label for="reset-level">{{ t('settings.atxActiveLevel') }}</Label>
|
||||
@@ -2558,13 +2654,21 @@ watch(() => config.value.hid_backend, async () => {
|
||||
</div>
|
||||
<div v-if="atxConfig.reset.driver === 'serial'" class="space-y-2">
|
||||
<Label for="reset-baudrate">{{ t('settings.baudRate') }}</Label>
|
||||
<select id="reset-baudrate" v-model.number="atxConfig.reset.baud_rate" class="w-full h-9 px-3 rounded-md border border-input bg-background text-sm">
|
||||
<select
|
||||
id="reset-baudrate"
|
||||
v-model.number="atxConfig.reset.baud_rate"
|
||||
class="w-full h-9 px-3 rounded-md border border-input bg-background text-sm"
|
||||
:disabled="isSharedAtxSerialRelay"
|
||||
>
|
||||
<option :value="9600">9600</option>
|
||||
<option :value="19200">19200</option>
|
||||
<option :value="38400">38400</option>
|
||||
<option :value="57600">57600</option>
|
||||
<option :value="115200">115200</option>
|
||||
</select>
|
||||
<p v-if="isSharedAtxSerialRelay" class="text-xs text-muted-foreground">
|
||||
{{ t('settings.atxSharedSerialBaudHint') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -3174,7 +3278,7 @@ watch(() => config.value.hid_backend, async () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Device Password (直接显示) -->
|
||||
<!-- Device Password (shown directly) -->
|
||||
<div class="grid gap-2 sm:grid-cols-4 sm:items-center">
|
||||
<Label class="sm:text-right">{{ t('extensions.rustdesk.devicePassword') }}</Label>
|
||||
<div class="sm:col-span-3 flex items-center gap-2">
|
||||
|
||||
Reference in New Issue
Block a user