Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/config/schema/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ mod computer_use;
mod hid;
mod otg_network;
mod stream;
mod switch;
mod uac;
mod watchdog;
mod web;
Expand All @@ -20,6 +21,7 @@ pub use computer_use::*;
pub use hid::*;
pub use otg_network::*;
pub use stream::*;
pub use switch::*;
pub use uac::*;
pub use watchdog::*;
pub use web::*;
Expand All @@ -36,6 +38,7 @@ pub struct AppConfig {
pub otg_network: OtgNetworkConfig,
pub msd: MsdConfig,
pub atx: AtxConfig,
pub switch: SwitchConfig,
pub audio: AudioConfig,
pub stream: StreamConfig,
pub web: WebConfig,
Expand All @@ -60,6 +63,7 @@ impl AppConfig {
self.hid.mouse_absolute = false;
}
self.atx.normalize();
self.switch.normalize();
}

pub fn apply_platform_defaults(&mut self) {
Expand Down
116 changes: 116 additions & 0 deletions src/config/schema/switch.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
use serde::{Deserialize, Serialize};
use typeshare::typeshare;

use crate::switch::{
SwitchControllerConfig, SWITCH_DEFAULT_BAUD_RATE, SWITCH_DEFAULT_CHANNELS, SWITCH_MAX_CHANNELS,
};


/// BliSwitch / XH-HK4401 KVM 输入切换器的持久化配置。
#[typeshare]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(default)]
pub struct SwitchConfig {
pub enabled: bool,
/// 切换器所在串口设备(如 `/dev/ttyUSB1`)。
pub device: String,
pub baud_rate: u32,
/// 可切换的通道数(1..=8)。
pub channel_count: u8,
/// 各通道的显示名(超出 `channel_count` 的部分忽略)。
pub channel_names: Vec<String>,
}

impl Default for SwitchConfig {
fn default() -> Self {
Self {
enabled: false,
device: String::new(),
baud_rate: SWITCH_DEFAULT_BAUD_RATE,
channel_count: SWITCH_DEFAULT_CHANNELS,
channel_names: Vec::new(),
}
}
}

impl SwitchConfig {
/// 将配置规范化到合法范围。
pub fn normalize(&mut self) {
if self.device.trim().is_empty() || self.channel_count == 0 {
self.enabled = false;
}
if self.baud_rate == 0 {
self.baud_rate = SWITCH_DEFAULT_BAUD_RATE;
}
if self.channel_count > SWITCH_MAX_CHANNELS {
self.channel_count = SWITCH_MAX_CHANNELS;
}
self.device = self.device.trim().to_string();
self.channel_names = self
.channel_names
.iter()
.take(self.channel_count as usize)
.cloned()
.collect();
}

/// 第 N 路(1 基)通道的显示名。
pub fn channel_label(&self, channel: u8) -> String {
let idx = (channel as usize).saturating_sub(1);
self.channel_names
.get(idx)
.filter(|name| !name.trim().is_empty())
.cloned()
.unwrap_or_else(|| format!("Input {}", channel))
}

pub fn to_controller_config(&self) -> SwitchControllerConfig {
SwitchControllerConfig {
enabled: self.enabled,
device: self.device.clone(),
baud_rate: self.baud_rate,
channel_count: self.channel_count,
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_default_config() {
let config = SwitchConfig::default();
assert!(!config.enabled);
assert_eq!(config.baud_rate, 19200);
assert_eq!(config.channel_count, 8);
}

#[test]
fn test_normalize_disables_without_device() {
let mut config = SwitchConfig::default();
config.enabled = true;
config.normalize();
assert!(!config.enabled);
}

#[test]
fn test_normalize_clamps_channels() {
let mut config = SwitchConfig {
enabled: true,
device: "/dev/ttyUSB0".to_string(),
channel_count: 16,
..Default::default()
};
config.normalize();
assert_eq!(config.channel_count, SWITCH_MAX_CHANNELS);
}

#[test]
fn test_channel_label_uses_name() {
let mut config = SwitchConfig::default();
config.channel_names = vec!["M2Pro".to_string()];
assert_eq!(config.channel_label(1), "M2Pro");
assert_eq!(config.channel_label(2), "Input 2");
}
}
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ pub mod stream;
#[cfg(feature = "desktop")]
pub mod stream_encoder;
#[cfg(feature = "desktop")]
pub mod switch;
#[cfg(feature = "desktop")]
pub mod update;
#[cfg(feature = "desktop")]
pub mod utils;
Expand Down
17 changes: 17 additions & 0 deletions src/runtime/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use std::sync::Arc;
use tokio::sync::broadcast;

use crate::atx::AtxController;
use crate::switch::SwitchController;
use crate::audio::{AudioController, AudioControllerConfig, AudioQuality};
use crate::auth::{SessionStore, TwoFactorService, UserStore};
use crate::computer_use::ComputerUseManager;
Expand Down Expand Up @@ -116,6 +117,7 @@ impl RuntimeBuilder {
#[cfg(unix)]
let msd = build_msd(&config, &data_dir, &otg_service, &events).await;
let atx = build_atx(&config).await;
let switch = build_switch(&config).await;
let audio = build_audio(&config, &events).await;
let extensions = Arc::new(ExtensionManager::new());
tracing::info!("Extension manager initialized");
Expand Down Expand Up @@ -168,6 +170,7 @@ impl RuntimeBuilder {
#[cfg(unix)]
msd,
atx,
switch,
audio,
extensions.clone(),
events.clone(),
Expand Down Expand Up @@ -460,6 +463,20 @@ async fn build_atx(config: &AppConfig) -> Option<AtxController> {
Some(controller)
}

async fn build_switch(config: &AppConfig) -> Option<SwitchController> {
if !config.switch.enabled {
tracing::info!("KVM switch disabled in configuration");
return None;
}

let controller = SwitchController::new(config.switch.to_controller_config());
if let Err(error) = controller.init().await {
tracing::warn!("Failed to initialize KVM switch controller: {}", error);
return None;
}
Some(controller)
}

async fn build_audio(config: &AppConfig, events: &Arc<EventBus>) -> Arc<AudioController> {
let quality = config
.audio
Expand Down
6 changes: 6 additions & 0 deletions src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::{collections::VecDeque, path::PathBuf, sync::Arc};
use tokio::sync::{broadcast, watch, Mutex, RwLock};

use crate::atx::AtxController;
use crate::switch::SwitchController;
use crate::audio::AudioController;
use crate::auth::{SessionStore, TwoFactorService, UserStore};
use crate::computer_use::ComputerUseManager;
Expand Down Expand Up @@ -32,6 +33,7 @@ pub struct ConfigApplyLocks {
pub otg: Arc<Mutex<()>>,
pub audio: Arc<Mutex<()>>,
pub atx: Arc<Mutex<()>>,
pub switch: Arc<Mutex<()>>,
pub rustdesk: Arc<Mutex<()>>,
pub vnc: Arc<Mutex<()>>,
pub rtsp: Arc<Mutex<()>>,
Expand All @@ -52,6 +54,7 @@ impl ConfigApplyLocks {
otg: Arc::new(Mutex::new(())),
audio: Arc::new(Mutex::new(())),
atx: Arc::new(Mutex::new(())),
switch: Arc::new(Mutex::new(())),
rustdesk: Arc::new(Mutex::new(())),
vnc: Arc::new(Mutex::new(())),
rtsp: Arc::new(Mutex::new(())),
Expand All @@ -76,6 +79,7 @@ pub struct AppState {
#[cfg(unix)]
pub msd: Arc<RwLock<Option<MsdController>>>,
pub atx: Arc<RwLock<Option<AtxController>>>,
pub switch: Arc<RwLock<Option<SwitchController>>>,
pub audio: Arc<AudioController>,
#[cfg(unix)]
pub uac_playback: Arc<RwLock<Option<crate::audio::uac::UacPlayback>>>,
Expand Down Expand Up @@ -107,6 +111,7 @@ impl AppState {
computer_use: Arc<ComputerUseManager>,
#[cfg(unix)] msd: Option<MsdController>,
atx: Option<AtxController>,
switch: Option<SwitchController>,
audio: Arc<AudioController>,
extensions: Arc<ExtensionManager>,
events: Arc<EventBus>,
Expand Down Expand Up @@ -153,6 +158,7 @@ impl AppState {
#[cfg(unix)]
msd,
atx: Arc::new(RwLock::new(atx)),
switch: Arc::new(RwLock::new(switch)),
audio,
usb,
remote_access,
Expand Down
Loading