//! HID path: browser (WebSocket or WebRTC DataChannel) → queue → OTG gadget or CH9329. pub mod backend; #[cfg(target_os = "linux")] mod bluetooth; pub mod ch9329; mod ch9329_proto; pub mod consumer; pub mod datachannel; mod factory; pub mod keyboard; mod macos_drag; #[cfg(unix)] pub mod otg; #[cfg(unix)] mod otg_device; pub mod types; pub mod websocket; pub use crate::events::LedState; pub use backend::{HidBackend, HidBackendRuntimeSnapshot, HidBackendType}; pub use keyboard::CanonicalKey; pub use types::{ ConsumerEvent, KeyEventType, KeyboardEvent, KeyboardModifiers, MouseButton, MouseEvent, MouseEventType, }; #[derive(Debug, Clone, PartialEq, Eq)] pub struct HidRuntimeState { pub available: bool, pub backend: String, pub initialized: bool, pub online: bool, pub supports_absolute_mouse: bool, pub keyboard_leds_enabled: bool, pub led_state: LedState, pub screen_resolution: Option<(u32, u32)>, pub device: Option, pub error: Option, pub error_code: Option, } impl HidRuntimeState { fn from_backend_type(backend_type: &HidBackendType) -> Self { Self { available: !matches!(backend_type, HidBackendType::None), backend: backend_type.name_str().to_string(), initialized: false, online: false, supports_absolute_mouse: false, keyboard_leds_enabled: false, led_state: LedState::default(), screen_resolution: None, device: device_for_backend_type(backend_type), error: None, error_code: None, } } fn from_backend(backend_type: &HidBackendType, snapshot: HidBackendRuntimeSnapshot) -> Self { Self { available: !matches!(backend_type, HidBackendType::None), backend: backend_type.name_str().to_string(), initialized: snapshot.initialized, online: snapshot.online, supports_absolute_mouse: snapshot.supports_absolute_mouse, keyboard_leds_enabled: snapshot.keyboard_leds_enabled, led_state: snapshot.led_state, screen_resolution: snapshot.screen_resolution, device: snapshot .device .or_else(|| device_for_backend_type(backend_type)), error: snapshot.error, error_code: snapshot.error_code, } } fn with_error( backend_type: &HidBackendType, current: &Self, reason: impl Into, error_code: impl Into, ) -> Self { let mut next = current.clone(); next.available = !matches!(backend_type, HidBackendType::None); next.backend = backend_type.name_str().to_string(); next.initialized = false; next.online = false; next.keyboard_leds_enabled = false; next.led_state = LedState::default(); next.device = device_for_backend_type(backend_type); next.error = Some(reason.into()); next.error_code = Some(error_code.into()); next } } use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use std::time::Duration; use tokio::sync::RwLock; use tracing::{info, warn}; use crate::config::{Ch9329DescriptorConfig, Ch9329DescriptorState}; use crate::error::{AppError, Result}; use crate::events::EventBus; #[cfg(unix)] use crate::otg::OtgService; use factory::HidBackendFactory; use tokio::sync::mpsc; use tokio::sync::Mutex; use tokio::task::JoinHandle; const HID_EVENT_QUEUE_CAPACITY: usize = 64; const HID_EVENT_SEND_TIMEOUT_MS: u64 = 30; #[derive(Debug)] enum QueuedHidEvent { Keyboard(KeyboardEvent), Mouse(MouseEvent), Consumer(ConsumerEvent), Reset, } pub struct HidController { backend_factory: HidBackendFactory, backend: Arc>>>, backend_type: Arc>, events: Arc>>>, runtime_state: Arc>, hid_tx: mpsc::Sender, hid_rx: Mutex>>, pending_move: Arc>>, pending_move_flag: Arc, hid_worker: Mutex>>, runtime_worker: Mutex>>, backend_available: Arc, reset_requested: Arc, screen_resolution: parking_lot::RwLock<(u32, u32)>, } impl HidController { #[cfg(unix)] pub fn new(backend_type: HidBackendType, otg_service: Option>) -> Self { let (hid_tx, hid_rx) = mpsc::channel(HID_EVENT_QUEUE_CAPACITY); Self { backend: Arc::new(RwLock::new(None)), backend_factory: HidBackendFactory::new(otg_service), backend_type: Arc::new(RwLock::new(backend_type.clone())), events: Arc::new(tokio::sync::RwLock::new(None)), runtime_state: Arc::new(RwLock::new(HidRuntimeState::from_backend_type( &backend_type, ))), hid_tx, hid_rx: Mutex::new(Some(hid_rx)), pending_move: Arc::new(parking_lot::Mutex::new(None)), pending_move_flag: Arc::new(AtomicBool::new(false)), hid_worker: Mutex::new(None), runtime_worker: Mutex::new(None), backend_available: Arc::new(AtomicBool::new(false)), reset_requested: Arc::new(AtomicBool::new(false)), screen_resolution: parking_lot::RwLock::new((1920, 1080)), } } #[cfg(not(unix))] pub fn new(backend_type: HidBackendType) -> Self { let (hid_tx, hid_rx) = mpsc::channel(HID_EVENT_QUEUE_CAPACITY); Self { backend: Arc::new(RwLock::new(None)), backend_factory: HidBackendFactory::new(), backend_type: Arc::new(RwLock::new(backend_type.clone())), events: Arc::new(tokio::sync::RwLock::new(None)), runtime_state: Arc::new(RwLock::new(HidRuntimeState::from_backend_type( &backend_type, ))), hid_tx, hid_rx: Mutex::new(Some(hid_rx)), pending_move: Arc::new(parking_lot::Mutex::new(None)), pending_move_flag: Arc::new(AtomicBool::new(false)), hid_worker: Mutex::new(None), runtime_worker: Mutex::new(None), backend_available: Arc::new(AtomicBool::new(false)), reset_requested: Arc::new(AtomicBool::new(false)), screen_resolution: parking_lot::RwLock::new((1920, 1080)), } } #[cfg(target_os = "linux")] pub fn set_bond_store(&self, store: crate::db::hid_bonds::HidBondStore) { let _ = self.backend_factory.bonds.set(Arc::new(store)); } pub async fn set_event_bus(&self, events: Arc) { *self.events.write().await = Some(events); } pub async fn init(&self) -> Result<()> { let backend_type = self.backend_type.read().await.clone(); let backend = match self.backend_factory.create_initialized(&backend_type).await { Ok(Some(backend)) => backend, Ok(None) => return Ok(()), Err(error) => { self.backend_available.store(false, Ordering::Release); let current = self.runtime_state.read().await.clone(); let error_state = HidRuntimeState::with_error( &backend_type, ¤t, format!("Failed to initialize HID backend: {}", error), "init_failed", ); self.apply_runtime_state(error_state).await; return Err(error); } }; { let mut slot = self.backend.write().await; let (width, height) = *self.screen_resolution.read(); backend.set_screen_resolution(width, height); *slot = Some(backend); } self.sync_runtime_state_from_backend().await; self.start_event_worker().await; self.restart_runtime_worker().await; info!("HID backend initialized: {:?}", backend_type); Ok(()) } pub async fn shutdown(&self) -> Result<()> { info!("Shutting down HID controller"); self.stop_runtime_worker().await; if let Some(backend) = self.backend.write().await.take() { if let Err(e) = backend.shutdown().await { warn!("Error shutting down HID backend: {}", e); } } self.backend_available.store(false, Ordering::Release); let backend_type = self.backend_type.read().await.clone(); let mut shutdown_state = HidRuntimeState::from_backend_type(&backend_type); if matches!(backend_type, HidBackendType::None) { shutdown_state.available = false; } else { shutdown_state.error = Some("HID backend stopped".to_string()); shutdown_state.error_code = Some("shutdown".to_string()); } self.apply_runtime_state(shutdown_state).await; info!("HID controller shutdown complete"); Ok(()) } pub async fn prepare_otg_rebuild(&self) -> Result<()> { let backend_type = self.backend_type.read().await.clone(); if !matches!(backend_type, HidBackendType::Otg { .. }) { return Ok(()); } info!("Preparing OTG HID backend for gadget rebuild"); self.backend_available.store(false, Ordering::Release); self.stop_runtime_worker().await; if let Some(backend) = self.backend.write().await.take() { backend.prepare_rebuild().await?; } let current = self.runtime_state.read().await.clone(); let rebuilding_state = HidRuntimeState::with_error( &backend_type, ¤t, "OTG gadget is rebuilding", "rebuilding", ); self.apply_runtime_state(rebuilding_state).await; Ok(()) } pub async fn send_keyboard(&self, event: KeyboardEvent) -> Result<()> { if !self.backend_available.load(Ordering::Acquire) { return Err(AppError::BadRequest( "HID backend not available".to_string(), )); } self.enqueue_event(QueuedHidEvent::Keyboard(event)).await } pub async fn send_mouse(&self, event: MouseEvent) -> Result<()> { if !self.backend_available.load(Ordering::Acquire) { return Err(AppError::BadRequest( "HID backend not available".to_string(), )); } if matches!( event.event_type, MouseEventType::Move | MouseEventType::MoveAbs ) { self.enqueue_mouse_move(event) } else { self.enqueue_event(QueuedHidEvent::Mouse(event)).await } } pub async fn send_consumer(&self, event: ConsumerEvent) -> Result<()> { if !self.backend_available.load(Ordering::Acquire) { return Err(AppError::BadRequest( "HID backend not available".to_string(), )); } self.enqueue_event(QueuedHidEvent::Consumer(event)).await } pub async fn bluetooth_status(&self) -> Result { let backend = self .backend .read() .await .clone() .ok_or_else(|| AppError::BadRequest("HID unavailable".into()))?; backend.bluetooth_status().await } pub async fn bluetooth_action(&self, action: &str, seconds: u32) -> Result<()> { let backend = self .backend .read() .await .clone() .ok_or_else(|| AppError::BadRequest("HID unavailable".into()))?; backend.bluetooth_action(action, seconds).await } pub async fn reset(&self) -> Result<()> { if !self.backend_available.load(Ordering::Acquire) { return Ok(()); } self.enqueue_event(QueuedHidEvent::Reset).await } pub async fn is_available(&self) -> bool { self.backend_available.load(Ordering::Acquire) } pub async fn backend_type(&self) -> HidBackendType { self.backend_type.read().await.clone() } /// Keep the active capture dimensions across HID reloads and USB rebuilds. pub async fn set_screen_resolution(&self, width: u32, height: u32) { if width == 0 || height == 0 || width > 65535 || height > 65535 { return; } { let mut resolution = self.screen_resolution.write(); if *resolution == (width, height) { return; } *resolution = (width, height); } if let Some(backend) = self.backend.read().await.as_ref() { let (width, height) = *self.screen_resolution.read(); backend.set_screen_resolution(width, height); } } pub async fn snapshot(&self) -> HidRuntimeState { self.runtime_state.read().await.clone() } async fn ch9329_backend(&self) -> Result> { if !matches!( *self.backend_type.read().await, HidBackendType::Ch9329 { .. } ) { return Err(AppError::BadRequest( "Current HID backend is not CH9329".to_string(), )); } self.backend .read() .await .clone() .ok_or_else(|| AppError::BadRequest("CH9329 backend not available".to_string())) } pub async fn apply_ch9329_descriptor( &self, descriptor: &Ch9329DescriptorConfig, ) -> Result { self.ch9329_backend() .await? .apply_ch9329_descriptor(descriptor) .await } pub async fn read_ch9329_descriptor(&self) -> Result { self.ch9329_backend().await?.read_ch9329_descriptor().await } pub async fn reload(&self, new_backend_type: HidBackendType) -> Result<()> { info!("Reloading HID backend: {:?}", new_backend_type); self.backend_available.store(false, Ordering::Release); self.stop_runtime_worker().await; if let Some(backend) = self.backend.write().await.take() { if let Err(e) = backend.shutdown().await { // A Bluetooth shutdown may fail to restore adapter settings. Surface // that failure so the config transaction can roll back. if matches!( *self.backend_type.read().await, HidBackendType::Bluetooth { .. } ) { return Err(e); } warn!("Error shutting down old HID backend: {}", e); } } let new_backend = match self .backend_factory .create_initialized(&new_backend_type) .await { Ok(backend) => backend, Err(error) if matches!(&new_backend_type, HidBackendType::None) => return Err(error), Err(error) => { warn!("Failed to initialize HID backend: {}", error); None } }; { let mut slot = self.backend.write().await; if let Some(backend) = new_backend.as_ref() { let (width, height) = *self.screen_resolution.read(); backend.set_screen_resolution(width, height); } *slot = new_backend; } if matches!(new_backend_type, HidBackendType::None) { *self.backend_type.write().await = HidBackendType::None; self.apply_runtime_state(HidRuntimeState::from_backend_type(&HidBackendType::None)) .await; return Ok(()); } if self.backend.read().await.is_some() { info!("HID backend reloaded successfully: {:?}", new_backend_type); self.start_event_worker().await; *self.backend_type.write().await = new_backend_type.clone(); self.sync_runtime_state_from_backend().await; self.restart_runtime_worker().await; Ok(()) } else { warn!("HID backend reload resulted in no active backend"); self.backend_available.store(false, Ordering::Release); *self.backend_type.write().await = new_backend_type.clone(); let current = self.runtime_state.read().await.clone(); let error_state = HidRuntimeState::with_error( &new_backend_type, ¤t, "Failed to initialize HID backend", "init_failed", ); self.apply_runtime_state(error_state).await; Err(AppError::Internal( "Failed to reload HID backend".to_string(), )) } } async fn apply_runtime_state(&self, next: HidRuntimeState) { apply_runtime_state(&self.runtime_state, &self.events, next).await; } async fn sync_runtime_state_from_backend(&self) { let backend_opt = self.backend.read().await.clone(); apply_backend_runtime_state( &self.backend_type, &self.runtime_state, &self.events, self.backend_available.as_ref(), backend_opt.as_deref(), ) .await; } async fn start_event_worker(&self) { let mut worker_guard = self.hid_worker.lock().await; if worker_guard.is_some() { return; } let mut rx_guard = self.hid_rx.lock().await; let rx = match rx_guard.take() { Some(rx) => rx, None => return, }; let backend = self.backend.clone(); let pending_move = self.pending_move.clone(); let pending_move_flag = self.pending_move_flag.clone(); let reset_requested = self.reset_requested.clone(); let handle = tokio::spawn(async move { let mut rx = rx; loop { let event = match rx.recv().await { Some(ev) => ev, None => break, }; if reset_requested.swap(false, Ordering::AcqRel) { // A full input queue must not lose a key/button release and leave // the host stuck. Discard the obsolete batch and send all-up. while rx.try_recv().is_ok() {} *pending_move.lock() = None; pending_move_flag.store(false, Ordering::Release); process_hid_event(QueuedHidEvent::Reset, &backend).await; continue; } process_hid_event(event, &backend).await; if pending_move_flag.swap(false, Ordering::AcqRel) { let move_event = { pending_move.lock().take() }; if let Some(move_event) = move_event { process_hid_event(QueuedHidEvent::Mouse(move_event), &backend).await; } } } }); *worker_guard = Some(handle); } async fn restart_runtime_worker(&self) { self.stop_runtime_worker().await; let backend_opt = self.backend.read().await.clone(); let Some(backend) = backend_opt else { return; }; let mut runtime_rx = backend.subscribe_runtime(); let runtime_state = self.runtime_state.clone(); let events = self.events.clone(); let backend_available = self.backend_available.clone(); let backend_type = self.backend_type.clone(); let handle = tokio::spawn(async move { loop { if runtime_rx.changed().await.is_err() { break; } apply_backend_runtime_state( &backend_type, &runtime_state, &events, backend_available.as_ref(), Some(backend.as_ref()), ) .await; } }); *self.runtime_worker.lock().await = Some(handle); } async fn stop_runtime_worker(&self) { if let Some(handle) = self.runtime_worker.lock().await.take() { handle.abort(); } } fn enqueue_mouse_move(&self, event: MouseEvent) -> Result<()> { match self.hid_tx.try_send(QueuedHidEvent::Mouse(event.clone())) { Ok(_) => Ok(()), Err(mpsc::error::TrySendError::Full(_)) => { merge_pending_move(&mut self.pending_move.lock(), event); self.pending_move_flag.store(true, Ordering::Release); Ok(()) } Err(mpsc::error::TrySendError::Closed(_)) => { Err(AppError::BadRequest("HID event queue closed".to_string())) } } } async fn enqueue_event(&self, event: QueuedHidEvent) -> Result<()> { match self.hid_tx.try_send(event) { Ok(_) => Ok(()), Err(mpsc::error::TrySendError::Full(ev)) => { let tx = self.hid_tx.clone(); let send_result = tokio::time::timeout( Duration::from_millis(HID_EVENT_SEND_TIMEOUT_MS), tx.send(ev), ) .await; match send_result { Ok(Ok(())) => Ok(()), Ok(Err(_)) => Err(AppError::BadRequest("HID event queue closed".into())), Err(_) => { self.reset_requested.store(true, Ordering::Release); warn!("HID event queue full; scheduling all-input release"); Err(AppError::ServiceUnavailable( "HID input queue full; input state will be reset".into(), )) } } } Err(mpsc::error::TrySendError::Closed(_)) => { Err(AppError::BadRequest("HID event queue closed".to_string())) } } } } async fn apply_backend_runtime_state( backend_type: &Arc>, runtime_state: &Arc>, events: &Arc>>>, backend_available: &AtomicBool, backend: Option<&dyn HidBackend>, ) { let backend_kind = backend_type.read().await.clone(); let next = match backend { Some(backend) => HidRuntimeState::from_backend(&backend_kind, backend.runtime_snapshot()), None => HidRuntimeState::from_backend_type(&backend_kind), }; backend_available.store(next.initialized, Ordering::Release); apply_runtime_state(runtime_state, events, next).await; } async fn process_hid_event( event: QueuedHidEvent, backend: &Arc>>>, ) { let backend_opt = backend.read().await.clone(); let backend = match backend_opt { Some(b) => b, None => return, }; let backend_for_send = backend.clone(); let result = tokio::task::spawn_blocking(move || { futures::executor::block_on(async move { match event { QueuedHidEvent::Keyboard(ev) => backend_for_send.send_keyboard(ev).await, QueuedHidEvent::Mouse(ev) => backend_for_send.send_mouse(ev).await, QueuedHidEvent::Consumer(ev) => backend_for_send.send_consumer(ev).await, QueuedHidEvent::Reset => backend_for_send.reset().await, } }) }) .await; let result = match result { Ok(r) => r, Err(_) => return, }; match result { Ok(_) => {} Err(e) => { warn!("HID event processing failed: {}", e); } } } fn device_for_backend_type(backend_type: &HidBackendType) -> Option { match backend_type { HidBackendType::Ch9329 { port, .. } => Some(port.clone()), _ => None, } } async fn apply_runtime_state( runtime_state: &Arc>, events: &Arc>>>, next: HidRuntimeState, ) { let changed = { let mut guard = runtime_state.write().await; if *guard == next { false } else { *guard = next.clone(); true } }; if !changed { return; } if let Some(events) = events.read().await.as_ref() { events.mark_device_info_dirty(); } } fn merge_pending_move(pending: &mut Option, event: MouseEvent) { if let Some(previous) = pending { if previous.event_type == MouseEventType::Move && event.event_type == MouseEventType::Move { previous.x = previous.x.saturating_add(event.x).clamp(-32767, 32767); previous.y = previous.y.saturating_add(event.y).clamp(-32767, 32767); return; } } *pending = Some(event); } #[cfg(test)] mod queue_tests { use super::*; #[tokio::test] async fn screen_resolution_updates_backend_and_retains_valid_dimensions() { #[cfg(unix)] let controller = HidController::new(HidBackendType::None, None); #[cfg(not(unix))] let controller = HidController::new(HidBackendType::None); let backend = Arc::new(ch9329::Ch9329Backend::new("/dev/null").unwrap()); *controller.backend.write().await = Some(backend.clone()); controller.set_screen_resolution(3840, 2160).await; assert_eq!( backend.runtime_snapshot().screen_resolution, Some((3840, 2160)) ); controller.set_screen_resolution(0, 0).await; assert_eq!(*controller.screen_resolution.read(), (3840, 2160)); controller.set_screen_resolution(1280, 720).await; assert_eq!( backend.runtime_snapshot().screen_resolution, Some((1280, 720)) ); } struct TestBackend { pressed: Arc, reset_done: Arc, runtime: tokio::sync::watch::Sender<()>, } #[async_trait::async_trait] impl HidBackend for TestBackend { async fn init(&self) -> Result<()> { Ok(()) } async fn send_keyboard(&self, _: KeyboardEvent) -> Result<()> { self.pressed.store(true, Ordering::Release); Ok(()) } async fn send_mouse(&self, _: MouseEvent) -> Result<()> { Ok(()) } async fn reset(&self) -> Result<()> { self.pressed.store(false, Ordering::Release); self.reset_done.notify_one(); Ok(()) } async fn shutdown(&self) -> Result<()> { Ok(()) } fn runtime_snapshot(&self) -> HidBackendRuntimeSnapshot { HidBackendRuntimeSnapshot::default() } fn subscribe_runtime(&self) -> tokio::sync::watch::Receiver<()> { self.runtime.subscribe() } } #[tokio::test] async fn congested_queue_releases_host_instead_of_replaying_keydowns() { #[cfg(unix)] let controller = HidController::new(HidBackendType::None, None); #[cfg(not(unix))] let controller = HidController::new(HidBackendType::None); let pressed = Arc::new(AtomicBool::new(true)); let done = Arc::new(tokio::sync::Notify::new()); let (runtime, _) = tokio::sync::watch::channel(()); *controller.backend.write().await = Some(Arc::new(TestBackend { pressed: pressed.clone(), reset_done: done.clone(), runtime, })); for _ in 0..HID_EVENT_QUEUE_CAPACITY { controller .enqueue_event(QueuedHidEvent::Keyboard(KeyboardEvent::key_down( CanonicalKey::KeyA, KeyboardModifiers::default(), ))) .await .unwrap(); } assert!(controller .enqueue_event(QueuedHidEvent::Reset) .await .is_err()); controller.start_event_worker().await; tokio::time::timeout(Duration::from_secs(1), done.notified()) .await .unwrap(); assert!(!pressed.load(Ordering::Acquire)); } #[test] fn relative_motion_is_accumulated_but_absolute_is_replaced() { let mut pending = Some(MouseEvent::move_rel(100, -20)); merge_pending_move(&mut pending, MouseEvent::move_rel(80, 30)); assert_eq!( (pending.as_ref().unwrap().x, pending.as_ref().unwrap().y), (180, 10) ); merge_pending_move(&mut pending, MouseEvent::move_abs(10, 20)); merge_pending_move(&mut pending, MouseEvent::move_abs(30, 40)); assert_eq!( (pending.as_ref().unwrap().x, pending.as_ref().unwrap().y), (30, 40) ); } }