diff --git a/src/web/handlers/setup.rs b/src/web/handlers/setup.rs index 9e30dfbc..5855ded8 100644 --- a/src/web/handlers/setup.rs +++ b/src/web/handlers/setup.rs @@ -30,7 +30,8 @@ pub struct SetupRequest { // Audio settings pub audio_device: Option, // HID settings - pub hid_backend: Option, + pub hid_backend: Option, + pub hid_bluetooth: Option, pub hid_ch9329_port: Option, pub hid_ch9329_baudrate: Option, pub hid_otg_udc: Option, @@ -42,6 +43,38 @@ pub struct SetupRequest { pub rustdesk_enabled: Option, } +impl SetupRequest { + fn hid_config(&self, current: &crate::config::HidConfig) -> Result { + let mut hid = current.clone(); + if let Some(backend) = &self.hid_backend { + hid.backend = backend.clone(); + } + if let Some(bluetooth) = &self.hid_bluetooth { + hid.bluetooth = bluetooth.clone(); + } + if let Some(port) = &self.hid_ch9329_port { + hid.ch9329_port = port.clone(); + } + if let Some(baudrate) = self.hid_ch9329_baudrate { + hid.ch9329_baudrate = baudrate; + } + if let Some(udc) = &self.hid_otg_udc { + hid.otg_udc = Some(udc.clone()); + } + if let Some(profile) = &self.hid_otg_profile { + if let Some(parsed) = crate::config::OtgHidProfile::from_legacy_str(profile) { + hid.otg_profile = parsed; + } + } + if let Some(enabled) = self.hid_otg_keyboard_leds { + hid.otg_keyboard_leds = enabled; + } + hid.bluetooth.validate()?; + hid.validate_otg_functions()?; + Ok(hid) + } +} + pub async fn setup_init( State(state): State>, Json(mut req): Json, @@ -97,6 +130,8 @@ pub async fn setup_init( } let old_config = state.config.get(); + // Validate the selected HID configuration before creating the one-time account. + let hid_config = req.hid_config(&old_config.hid)?; // Create single system user state @@ -134,30 +169,7 @@ pub async fn setup_init( } // HID settings - if let Some(backend) = req.hid_backend.clone() { - config.hid.backend = match backend.as_str() { - "otg" => crate::config::HidBackend::Otg, - "ch9329" => crate::config::HidBackend::Ch9329, - _ => crate::config::HidBackend::None, - }; - } - if let Some(port) = req.hid_ch9329_port.clone() { - config.hid.ch9329_port = port; - } - if let Some(baudrate) = req.hid_ch9329_baudrate { - config.hid.ch9329_baudrate = baudrate; - } - if let Some(udc) = req.hid_otg_udc.clone() { - config.hid.otg_udc = Some(udc); - } - if let Some(profile) = req.hid_otg_profile.clone() { - if let Some(parsed) = crate::config::OtgHidProfile::from_legacy_str(&profile) { - config.hid.otg_profile = parsed; - } - } - if let Some(enabled) = req.hid_otg_keyboard_leds { - config.hid.otg_keyboard_leds = enabled; - } + config.hid = hid_config.clone(); if let Some(enabled) = req.msd_enabled { config.msd.enabled = enabled; } @@ -265,3 +277,106 @@ pub async fn setup_init( message: Some("Setup completed".to_string()), })) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::config::{AppConfig, HidBackend, HidConfig, OtgHidProfile}; + use serde_json::{json, Value}; + + fn request(mut hid: Value) -> SetupRequest { + hid["username"] = json!("admin"); + hid["password"] = json!("test-password"); + serde_json::from_value(hid).unwrap() + } + + #[test] + fn setup_applies_selected_otg_device_and_legacy_options() { + let req = request(json!({ + "hid_backend": "otg", + "hid_otg_udc": "fe800000.usb", + "hid_otg_profile": "full_no_consumer_no_msd", + "hid_otg_keyboard_leds": true + })); + let hid = req.hid_config(&HidConfig::default()).unwrap(); + assert_eq!(hid.backend, HidBackend::Otg); + assert_eq!(hid.otg_udc.as_deref(), Some("fe800000.usb")); + assert_eq!(hid.otg_profile, OtgHidProfile::FullNoConsumer); + assert!(hid.effective_otg_keyboard_leds()); + } + + #[test] + fn setup_applies_selected_ch9329_port_and_baudrate() { + let req = request(json!({ + "hid_backend": "ch9329", + "hid_ch9329_port": "/dev/ttyUSB2", + "hid_ch9329_baudrate": 115200 + })); + let hid = req.hid_config(&HidConfig::default()).unwrap(); + assert_eq!(hid.backend, HidBackend::Ch9329); + assert_eq!(hid.ch9329_port, "/dev/ttyUSB2"); + assert_eq!(hid.ch9329_baudrate, 115200); + } + + #[test] + fn setup_applies_bluetooth_selection_and_usb_invariants() { + let req = request(json!({ + "hid_backend": "bluetooth", + "hid_bluetooth": { "adapter": "hci1", "name": "My KVM" } + })); + let mut config = AppConfig::default(); + config.hid = req.hid_config(&config.hid).unwrap(); + config.hid.mouse_absolute = true; + config.msd.enabled = true; + config.otg_network.enabled = true; + config.uac.enabled = true; + config.enforce_invariants(); + + assert_eq!(config.hid.backend, HidBackend::Bluetooth); + assert_eq!(config.hid.bluetooth.adapter, "hci1"); + assert_eq!(config.hid.bluetooth.name, "My KVM"); + assert_eq!(config.hid.bluetooth.peer, None); + assert!(!config.hid.mouse_absolute); + assert!(!config.msd.enabled && !config.otg_network.enabled && !config.uac.enabled); + } + + #[test] + fn setup_preserves_omitted_settings_and_allows_explicit_disable() { + let current = HidConfig { + backend: HidBackend::Ch9329, + ch9329_port: "COM7".into(), + ch9329_baudrate: 57600, + ..Default::default() + }; + assert_eq!(request(json!({})).hid_config(¤t).unwrap(), current); + let disabled = request(json!({ "hid_backend": "none" })) + .hid_config(¤t) + .unwrap(); + assert_eq!(disabled.backend, HidBackend::None); + assert_eq!(disabled.ch9329_port, current.ch9329_port); + assert_eq!(disabled.ch9329_baudrate, current.ch9329_baudrate); + } + + #[test] + fn setup_rejects_unknown_backend_instead_of_disabling_hid() { + assert!(serde_json::from_value::(json!({ + "username": "admin", "password": "test-password", + "hid_backend": "unsupported" + })) + .is_err()); + } + + #[test] + fn setup_rejects_invalid_bluetooth_configuration() { + for bluetooth in [ + json!({ "adapter": "/dev/hci0", "name": "My KVM" }), + json!({ "adapter": "hci0", "name": "" }), + json!({ "adapter": "hci0", "name": "蓝".repeat(24) }), + ] { + let req = request(json!({ + "hid_backend": "bluetooth", "hid_bluetooth": bluetooth + })); + assert!(req.hid_config(&HidConfig::default()).is_err()); + } + } +} diff --git a/web/package.json b/web/package.json index fa5b0429..00bf1703 100644 --- a/web/package.json +++ b/web/package.json @@ -27,14 +27,10 @@ "vue-sonner": "^2.0.9" }, "devDependencies": { - "@tailwindcss/forms": "^0.5.11", - "@tailwindcss/typography": "^0.5.20", "@tailwindcss/vite": "^4.3.3", "@types/node": "^24.13.3", "@vitejs/plugin-vue": "^6.0.8", "@vue/tsconfig": "^0.8.1", - "autoprefixer": "^10.5.4", - "postcss": "^8.5.19", "tailwindcss": "^4.3.3", "typescript": "~5.9.3", "vite": "^7.3.6", diff --git a/web/src/api/index.ts b/web/src/api/index.ts index f955a426..762ebc66 100644 --- a/web/src/api/index.ts +++ b/web/src/api/index.ts @@ -169,7 +169,8 @@ export const systemApi = { video_width?: number video_height?: number video_fps?: number - hid_backend?: string + hid_backend?: 'otg' | 'ch9329' | 'bluetooth' | 'none' + hid_bluetooth?: { adapter: string; name: string } hid_ch9329_port?: string hid_ch9329_baudrate?: number hid_otg_udc?: string diff --git a/web/src/stores/auth.ts b/web/src/stores/auth.ts index 867e8bd8..1fe04f59 100644 --- a/web/src/stores/auth.ts +++ b/web/src/stores/auth.ts @@ -107,26 +107,7 @@ export const useAuthStore = defineStore('auth', () => { } } - async function setup(data: { - username: string - password: string - video_device?: string - video_format?: string - video_width?: number - video_height?: number - video_fps?: number - hid_backend?: string - hid_ch9329_port?: string - hid_ch9329_baudrate?: number - hid_otg_udc?: string - hid_otg_profile?: string - hid_otg_keyboard_leds?: boolean - msd_enabled?: boolean - encoder_backend?: string - audio_device?: string - ttyd_enabled?: boolean - rustdesk_enabled?: boolean - }) { + async function setup(data: Parameters[0]) { loading.value = true error.value = null diff --git a/web/src/views/SetupView.vue b/web/src/views/SetupView.vue index e6446094..e5ed58bf 100644 --- a/web/src/views/SetupView.vue +++ b/web/src/views/SetupView.vue @@ -347,7 +347,7 @@ function prevStep() { async function handleSetup() { error.value = '' - if (loading.value) return + if (loading.value || !validateStep3()) return loading.value = true // Reconcile a previous timed-out account request before submitting again. @@ -385,7 +385,16 @@ async function handleSetup() { setupData.video_fps = toConfigFps(videoFps.value) } - setupData.hid_backend = 'none' + const hid = hidSelection.value + setupData.hid_backend = hid.backend + if (hid.backend === 'ch9329') { + setupData.hid_ch9329_port = hid.ch9329_port + setupData.hid_ch9329_baudrate = hid.ch9329_baudrate + } else if (hid.backend === 'otg') { + setupData.hid_otg_udc = hid.otg_udc + } else if (hid.backend === 'bluetooth') { + setupData.hid_bluetooth = { ...hid.bluetooth } + } setupData.msd_enabled = false // Encoder backend setting