mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-09-12 18:44:25 +08:00
refactor(otg): 简化运行时与设置逻辑
This commit is contained in:
@@ -2,7 +2,9 @@
|
||||
|
||||
use async_trait::async_trait;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::sync::watch;
|
||||
|
||||
use super::otg::LedState;
|
||||
use super::types::{ConsumerEvent, KeyboardEvent, MouseEvent};
|
||||
use crate::error::Result;
|
||||
|
||||
@@ -76,12 +78,22 @@ impl HidBackendType {
|
||||
}
|
||||
|
||||
/// Current runtime status reported by a HID backend.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct HidBackendStatus {
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct HidBackendRuntimeSnapshot {
|
||||
/// Whether the backend has been initialized and can accept requests.
|
||||
pub initialized: bool,
|
||||
/// Whether the backend is currently online and communicating successfully.
|
||||
pub online: bool,
|
||||
/// Whether absolute mouse positioning is supported.
|
||||
pub supports_absolute_mouse: bool,
|
||||
/// Whether keyboard LED/status feedback is currently enabled.
|
||||
pub keyboard_leds_enabled: bool,
|
||||
/// Last known keyboard LED state.
|
||||
pub led_state: LedState,
|
||||
/// Screen resolution for absolute mouse mode.
|
||||
pub screen_resolution: Option<(u32, u32)>,
|
||||
/// Device identifier associated with the backend, if any.
|
||||
pub device: Option<String>,
|
||||
/// Current user-facing error, if any.
|
||||
pub error: Option<String>,
|
||||
/// Current programmatic error code, if any.
|
||||
@@ -91,9 +103,6 @@ pub struct HidBackendStatus {
|
||||
/// HID backend trait
|
||||
#[async_trait]
|
||||
pub trait HidBackend: Send + Sync {
|
||||
/// Get backend name
|
||||
fn name(&self) -> &'static str;
|
||||
|
||||
/// Initialize the backend
|
||||
async fn init(&self) -> Result<()>;
|
||||
|
||||
@@ -117,18 +126,11 @@ pub trait HidBackend: Send + Sync {
|
||||
/// Shutdown the backend
|
||||
async fn shutdown(&self) -> Result<()>;
|
||||
|
||||
/// Get the current backend runtime status.
|
||||
fn status(&self) -> HidBackendStatus;
|
||||
/// Get the current backend runtime snapshot.
|
||||
fn runtime_snapshot(&self) -> HidBackendRuntimeSnapshot;
|
||||
|
||||
/// Check if backend supports absolute mouse positioning
|
||||
fn supports_absolute_mouse(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Get screen resolution (for absolute mouse)
|
||||
fn screen_resolution(&self) -> Option<(u32, u32)> {
|
||||
None
|
||||
}
|
||||
/// Subscribe to backend runtime changes.
|
||||
fn subscribe_runtime(&self) -> watch::Receiver<()>;
|
||||
|
||||
/// Set screen resolution (for absolute mouse)
|
||||
fn set_screen_resolution(&mut self, _width: u32, _height: u32) {}
|
||||
|
||||
@@ -25,9 +25,11 @@ use std::sync::atomic::{AtomicBool, AtomicU16, AtomicU8, Ordering};
|
||||
use std::sync::{mpsc, Arc};
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant};
|
||||
use tokio::sync::watch;
|
||||
use tracing::{info, trace, warn};
|
||||
|
||||
use super::backend::{HidBackend, HidBackendStatus};
|
||||
use super::backend::{HidBackend, HidBackendRuntimeSnapshot};
|
||||
use super::otg::LedState;
|
||||
use super::types::{KeyEventType, KeyboardEvent, KeyboardReport, MouseEvent, MouseEventType};
|
||||
use crate::error::{AppError, Result};
|
||||
|
||||
@@ -180,7 +182,7 @@ impl ChipInfo {
|
||||
}
|
||||
|
||||
/// Keyboard LED status
|
||||
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct LedStatus {
|
||||
pub num_lock: bool,
|
||||
pub caps_lock: bool,
|
||||
@@ -346,28 +348,73 @@ const MAX_PACKET_SIZE: usize = 70;
|
||||
// CH9329 Backend Implementation
|
||||
// ============================================================================
|
||||
|
||||
#[derive(Default)]
|
||||
struct Ch9329RuntimeState {
|
||||
initialized: AtomicBool,
|
||||
online: AtomicBool,
|
||||
last_error: RwLock<Option<(String, String)>>,
|
||||
last_success: Mutex<Option<Instant>>,
|
||||
notify_tx: watch::Sender<()>,
|
||||
}
|
||||
|
||||
impl Ch9329RuntimeState {
|
||||
fn new() -> Self {
|
||||
let (notify_tx, _notify_rx) = watch::channel(());
|
||||
Self {
|
||||
initialized: AtomicBool::new(false),
|
||||
online: AtomicBool::new(false),
|
||||
last_error: RwLock::new(None),
|
||||
notify_tx,
|
||||
}
|
||||
}
|
||||
|
||||
fn subscribe(&self) -> watch::Receiver<()> {
|
||||
self.notify_tx.subscribe()
|
||||
}
|
||||
|
||||
fn notify(&self) {
|
||||
let _ = self.notify_tx.send(());
|
||||
}
|
||||
|
||||
fn clear_error(&self) {
|
||||
*self.last_error.write() = None;
|
||||
let mut guard = self.last_error.write();
|
||||
if guard.is_some() {
|
||||
*guard = None;
|
||||
self.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn set_online(&self) {
|
||||
self.online.store(true, Ordering::Relaxed);
|
||||
*self.last_success.lock() = Some(Instant::now());
|
||||
self.clear_error();
|
||||
let was_online = self.online.swap(true, Ordering::Relaxed);
|
||||
let mut error = self.last_error.write();
|
||||
let cleared_error = error.take().is_some();
|
||||
drop(error);
|
||||
if !was_online || cleared_error {
|
||||
self.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn set_error(&self, reason: impl Into<String>, error_code: impl Into<String>) {
|
||||
self.online.store(false, Ordering::Relaxed);
|
||||
*self.last_error.write() = Some((reason.into(), error_code.into()));
|
||||
let reason = reason.into();
|
||||
let error_code = error_code.into();
|
||||
let was_online = self.online.swap(false, Ordering::Relaxed);
|
||||
let mut error = self.last_error.write();
|
||||
let changed = error.as_ref() != Some(&(reason.clone(), error_code.clone()));
|
||||
*error = Some((reason, error_code));
|
||||
drop(error);
|
||||
if was_online || changed {
|
||||
self.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn set_initialized(&self, initialized: bool) {
|
||||
if self.initialized.swap(initialized, Ordering::Relaxed) != initialized {
|
||||
self.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn set_offline(&self) {
|
||||
if self.online.swap(false, Ordering::Relaxed) {
|
||||
self.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -434,7 +481,7 @@ impl Ch9329Backend {
|
||||
last_abs_x: AtomicU16::new(0),
|
||||
last_abs_y: AtomicU16::new(0),
|
||||
relative_mouse_active: AtomicBool::new(false),
|
||||
runtime: Arc::new(Ch9329RuntimeState::default()),
|
||||
runtime: Arc::new(Ch9329RuntimeState::new()),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -442,24 +489,11 @@ impl Ch9329Backend {
|
||||
self.runtime.set_error(reason, error_code);
|
||||
}
|
||||
|
||||
fn mark_online(&self) {
|
||||
self.runtime.set_online();
|
||||
}
|
||||
|
||||
fn clear_error(&self) {
|
||||
self.runtime.clear_error();
|
||||
}
|
||||
|
||||
/// Check if the serial port device file exists
|
||||
pub fn check_port_exists(&self) -> bool {
|
||||
std::path::Path::new(&self.port_path).exists()
|
||||
}
|
||||
|
||||
/// Get the serial port path
|
||||
pub fn port_path(&self) -> &str {
|
||||
&self.port_path
|
||||
}
|
||||
|
||||
/// Convert serialport error to HidError
|
||||
fn serial_error_to_hid_error(e: serialport::Error, operation: &str) -> AppError {
|
||||
let error_code = match e.kind() {
|
||||
@@ -675,23 +709,33 @@ impl Ch9329Backend {
|
||||
chip_info: &Arc<RwLock<Option<ChipInfo>>>,
|
||||
led_status: &Arc<RwLock<LedStatus>>,
|
||||
info: ChipInfo,
|
||||
) {
|
||||
*chip_info.write() = Some(info.clone());
|
||||
*led_status.write() = LedStatus {
|
||||
) -> bool {
|
||||
let next_led_status = LedStatus {
|
||||
num_lock: info.num_lock,
|
||||
caps_lock: info.caps_lock,
|
||||
scroll_lock: info.scroll_lock,
|
||||
};
|
||||
*chip_info.write() = Some(info);
|
||||
let mut led_guard = led_status.write();
|
||||
let changed = *led_guard != next_led_status;
|
||||
*led_guard = next_led_status;
|
||||
changed
|
||||
}
|
||||
|
||||
fn enqueue_command(&self, command: WorkerCommand) -> Result<()> {
|
||||
let guard = self.worker_tx.lock();
|
||||
let sender = guard
|
||||
.as_ref()
|
||||
.ok_or_else(|| Self::backend_error("CH9329 worker is not running", "worker_stopped"))?;
|
||||
sender
|
||||
.send(command)
|
||||
.map_err(|_| Self::backend_error("CH9329 worker stopped", "worker_stopped"))
|
||||
let Some(sender) = guard.as_ref() else {
|
||||
self.record_error("CH9329 worker is not running", "worker_stopped");
|
||||
return Err(Self::backend_error(
|
||||
"CH9329 worker is not running",
|
||||
"worker_stopped",
|
||||
));
|
||||
};
|
||||
|
||||
sender.send(command).map_err(|_| {
|
||||
self.record_error("CH9329 worker stopped", "worker_stopped");
|
||||
Self::backend_error("CH9329 worker stopped", "worker_stopped")
|
||||
})
|
||||
}
|
||||
|
||||
fn send_packet(&self, cmd: u8, data: &[u8]) -> Result<()> {
|
||||
@@ -701,19 +745,6 @@ impl Ch9329Backend {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn error_count(&self) -> u32 {
|
||||
0
|
||||
}
|
||||
|
||||
/// Check if device communication is healthy (recent successful operation)
|
||||
pub fn is_healthy(&self) -> bool {
|
||||
if let Some(last) = *self.runtime.last_success.lock() {
|
||||
last.elapsed() < Duration::from_secs(30)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn worker_reconnect_loop(
|
||||
rx: &mpsc::Receiver<WorkerCommand>,
|
||||
port_path: &str,
|
||||
@@ -745,7 +776,9 @@ impl Ch9329Backend {
|
||||
"disconnected"
|
||||
}
|
||||
);
|
||||
Self::update_chip_info_cache(chip_info, led_status, info);
|
||||
if Self::update_chip_info_cache(chip_info, led_status, info) {
|
||||
runtime.notify();
|
||||
}
|
||||
runtime.set_online();
|
||||
return Some(port);
|
||||
}
|
||||
@@ -761,36 +794,6 @@ impl Ch9329Backend {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get cached chip information
|
||||
pub fn get_chip_info(&self) -> Option<ChipInfo> {
|
||||
self.chip_info.read().clone()
|
||||
}
|
||||
|
||||
pub fn query_chip_info(&self) -> Result<ChipInfo> {
|
||||
if let Some(info) = self.get_chip_info() {
|
||||
return Ok(info);
|
||||
}
|
||||
|
||||
let error = self.runtime.last_error.read().clone();
|
||||
Err(match error {
|
||||
Some((reason, error_code)) => Self::backend_error(reason, error_code),
|
||||
None => Self::backend_error("CH9329 info unavailable", "not_ready"),
|
||||
})
|
||||
}
|
||||
|
||||
/// Get cached LED status
|
||||
pub fn get_led_status(&self) -> LedStatus {
|
||||
*self.led_status.read()
|
||||
}
|
||||
|
||||
pub fn software_reset(&self) -> Result<()> {
|
||||
self.send_packet(cmd::RESET, &[])
|
||||
}
|
||||
|
||||
pub fn restore_factory_defaults(&self) -> Result<()> {
|
||||
self.send_packet(cmd::SET_DEFAULT_CFG, &[])
|
||||
}
|
||||
|
||||
fn send_keyboard_report(&self, report: &KeyboardReport) -> Result<()> {
|
||||
let data = report.to_bytes();
|
||||
self.send_packet(cmd::SEND_KB_GENERAL_DATA, &data)
|
||||
@@ -805,20 +808,6 @@ impl Ch9329Backend {
|
||||
self.send_packet(cmd::SEND_KB_MEDIA_DATA, data)
|
||||
}
|
||||
|
||||
pub fn send_acpi_key(&self, power: bool, sleep: bool, wake: bool) -> Result<()> {
|
||||
let mut byte = 0u8;
|
||||
if power {
|
||||
byte |= 0x01;
|
||||
}
|
||||
if sleep {
|
||||
byte |= 0x02;
|
||||
}
|
||||
if wake {
|
||||
byte |= 0x04;
|
||||
}
|
||||
self.send_media_key(&[0x01, byte])
|
||||
}
|
||||
|
||||
pub fn release_media_keys(&self) -> Result<()> {
|
||||
self.send_media_key(&[0x02, 0x00, 0x00, 0x00])
|
||||
}
|
||||
@@ -843,13 +832,6 @@ impl Ch9329Backend {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn send_custom_hid(&self, data: &[u8]) -> Result<()> {
|
||||
if data.len() > MAX_DATA_LEN {
|
||||
return Err(AppError::Internal("Custom HID data too long".to_string()));
|
||||
}
|
||||
self.send_packet(cmd::SEND_MY_HID_DATA, data)
|
||||
}
|
||||
|
||||
fn worker_loop(
|
||||
port_path: String,
|
||||
baud_rate: u32,
|
||||
@@ -860,7 +842,7 @@ impl Ch9329Backend {
|
||||
runtime: Arc<Ch9329RuntimeState>,
|
||||
init_tx: mpsc::Sender<Result<ChipInfo>>,
|
||||
) {
|
||||
runtime.initialized.store(true, Ordering::Relaxed);
|
||||
runtime.set_initialized(true);
|
||||
|
||||
let mut port = match Self::open_port(&port_path, baud_rate).and_then(|mut port| {
|
||||
let info = Self::query_chip_info_on_port(port.as_mut(), address)?;
|
||||
@@ -871,7 +853,9 @@ impl Ch9329Backend {
|
||||
"CH9329 serial port opened: {} @ {} baud",
|
||||
port_path, baud_rate
|
||||
);
|
||||
Self::update_chip_info_cache(&chip_info, &led_status, info.clone());
|
||||
if Self::update_chip_info_cache(&chip_info, &led_status, info.clone()) {
|
||||
runtime.notify();
|
||||
}
|
||||
runtime.set_online();
|
||||
let _ = init_tx.send(Ok(info));
|
||||
port
|
||||
@@ -884,7 +868,7 @@ impl Ch9329Backend {
|
||||
runtime.set_error(reason.clone(), error_code.clone());
|
||||
}
|
||||
let _ = init_tx.send(Err(err));
|
||||
runtime.initialized.store(false, Ordering::Relaxed);
|
||||
runtime.set_initialized(false);
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -961,7 +945,9 @@ impl Ch9329Backend {
|
||||
Err(mpsc::RecvTimeoutError::Timeout) => {
|
||||
match Self::query_chip_info_on_port(port.as_mut(), address) {
|
||||
Ok(info) => {
|
||||
Self::update_chip_info_cache(&chip_info, &led_status, info);
|
||||
if Self::update_chip_info_cache(&chip_info, &led_status, info) {
|
||||
runtime.notify();
|
||||
}
|
||||
runtime.set_online();
|
||||
}
|
||||
Err(err) => {
|
||||
@@ -993,8 +979,8 @@ impl Ch9329Backend {
|
||||
}
|
||||
}
|
||||
|
||||
runtime.online.store(false, Ordering::Relaxed);
|
||||
runtime.initialized.store(false, Ordering::Relaxed);
|
||||
runtime.set_offline();
|
||||
runtime.set_initialized(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1004,10 +990,6 @@ impl Ch9329Backend {
|
||||
|
||||
#[async_trait]
|
||||
impl HidBackend for Ch9329Backend {
|
||||
fn name(&self) -> &'static str {
|
||||
"CH9329 Serial"
|
||||
}
|
||||
|
||||
async fn init(&self) -> Result<()> {
|
||||
if self.worker_handle.lock().is_some() {
|
||||
return Ok(());
|
||||
@@ -1047,7 +1029,7 @@ impl HidBackend for Ch9329Backend {
|
||||
);
|
||||
*self.worker_tx.lock() = Some(tx);
|
||||
*self.worker_handle.lock() = Some(handle);
|
||||
self.mark_online();
|
||||
self.runtime.set_online();
|
||||
Ok(())
|
||||
}
|
||||
Ok(Err(err)) => {
|
||||
@@ -1215,15 +1197,15 @@ impl HidBackend for Ch9329Backend {
|
||||
if let Some(handle) = self.worker_handle.lock().take() {
|
||||
let _ = handle.join();
|
||||
}
|
||||
self.runtime.initialized.store(false, Ordering::Relaxed);
|
||||
self.runtime.online.store(false, Ordering::Relaxed);
|
||||
self.clear_error();
|
||||
self.runtime.set_offline();
|
||||
self.runtime.set_initialized(false);
|
||||
self.runtime.clear_error();
|
||||
|
||||
info!("CH9329 backend shutdown");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn status(&self) -> HidBackendStatus {
|
||||
fn runtime_snapshot(&self) -> HidBackendRuntimeSnapshot {
|
||||
let initialized = self.runtime.initialized.load(Ordering::Relaxed);
|
||||
let mut online = initialized && self.runtime.online.load(Ordering::Relaxed);
|
||||
let mut error = self.runtime.last_error.read().clone();
|
||||
@@ -1236,25 +1218,36 @@ impl HidBackend for Ch9329Backend {
|
||||
));
|
||||
}
|
||||
|
||||
HidBackendStatus {
|
||||
HidBackendRuntimeSnapshot {
|
||||
initialized,
|
||||
online,
|
||||
supports_absolute_mouse: true,
|
||||
keyboard_leds_enabled: true,
|
||||
led_state: {
|
||||
let led = *self.led_status.read();
|
||||
LedState {
|
||||
num_lock: led.num_lock,
|
||||
caps_lock: led.caps_lock,
|
||||
scroll_lock: led.scroll_lock,
|
||||
compose: false,
|
||||
kana: false,
|
||||
}
|
||||
},
|
||||
screen_resolution: Some((self.screen_width, self.screen_height)),
|
||||
device: Some(self.port_path.clone()),
|
||||
error: error.as_ref().map(|(reason, _)| reason.clone()),
|
||||
error_code: error.as_ref().map(|(_, code)| code.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
fn supports_absolute_mouse(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn screen_resolution(&self) -> Option<(u32, u32)> {
|
||||
Some((self.screen_width, self.screen_height))
|
||||
fn subscribe_runtime(&self) -> watch::Receiver<()> {
|
||||
self.runtime.subscribe()
|
||||
}
|
||||
|
||||
fn set_screen_resolution(&mut self, width: u32, height: u32) {
|
||||
self.screen_width = width;
|
||||
self.screen_height = height;
|
||||
self.runtime.notify();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
177
src/hid/mod.rs
177
src/hid/mod.rs
@@ -20,7 +20,7 @@ pub mod otg;
|
||||
pub mod types;
|
||||
pub mod websocket;
|
||||
|
||||
pub use backend::{HidBackend, HidBackendStatus, HidBackendType};
|
||||
pub use backend::{HidBackend, HidBackendRuntimeSnapshot, HidBackendType};
|
||||
pub use keyboard::CanonicalKey;
|
||||
pub use otg::LedState;
|
||||
pub use types::{
|
||||
@@ -54,6 +54,10 @@ pub struct HidRuntimeState {
|
||||
pub online: bool,
|
||||
/// Whether absolute mouse positioning is supported.
|
||||
pub supports_absolute_mouse: bool,
|
||||
/// Whether keyboard LED/status feedback is enabled.
|
||||
pub keyboard_leds_enabled: bool,
|
||||
/// Last known keyboard LED state.
|
||||
pub led_state: LedState,
|
||||
/// Screen resolution for absolute mouse mode.
|
||||
pub screen_resolution: Option<(u32, u32)>,
|
||||
/// Device path associated with the backend, if any.
|
||||
@@ -72,6 +76,8 @@ impl HidRuntimeState {
|
||||
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,
|
||||
@@ -79,18 +85,21 @@ impl HidRuntimeState {
|
||||
}
|
||||
}
|
||||
|
||||
fn from_backend(backend_type: &HidBackendType, backend: &dyn HidBackend) -> Self {
|
||||
let status = backend.status();
|
||||
fn from_backend(backend_type: &HidBackendType, snapshot: HidBackendRuntimeSnapshot) -> Self {
|
||||
Self {
|
||||
available: !matches!(backend_type, HidBackendType::None),
|
||||
backend: backend_type.name_str().to_string(),
|
||||
initialized: status.initialized,
|
||||
online: status.online,
|
||||
supports_absolute_mouse: backend.supports_absolute_mouse(),
|
||||
screen_resolution: backend.screen_resolution(),
|
||||
device: device_for_backend_type(backend_type),
|
||||
error: status.error,
|
||||
error_code: status.error_code,
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,6 +114,8 @@ impl HidRuntimeState {
|
||||
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());
|
||||
@@ -114,13 +125,13 @@ impl HidRuntimeState {
|
||||
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::RwLock;
|
||||
use tracing::{info, warn};
|
||||
|
||||
use crate::error::{AppError, Result};
|
||||
use crate::events::EventBus;
|
||||
use crate::otg::OtgService;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::task::JoinHandle;
|
||||
@@ -158,6 +169,8 @@ pub struct HidController {
|
||||
pending_move_flag: Arc<AtomicBool>,
|
||||
/// Worker task handle
|
||||
hid_worker: Mutex<Option<JoinHandle<()>>>,
|
||||
/// Backend runtime subscription task handle
|
||||
runtime_worker: Mutex<Option<JoinHandle<()>>>,
|
||||
/// Backend initialization fast flag
|
||||
backend_available: Arc<AtomicBool>,
|
||||
}
|
||||
@@ -181,6 +194,7 @@ impl HidController {
|
||||
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)),
|
||||
}
|
||||
}
|
||||
@@ -195,16 +209,15 @@ impl HidController {
|
||||
let backend_type = self.backend_type.read().await.clone();
|
||||
let backend: Arc<dyn HidBackend> = match backend_type {
|
||||
HidBackendType::Otg => {
|
||||
// Request HID functions from OtgService
|
||||
let otg_service = self
|
||||
.otg_service
|
||||
.as_ref()
|
||||
.ok_or_else(|| AppError::Internal("OtgService not available".into()))?;
|
||||
|
||||
info!("Requesting HID functions from OtgService");
|
||||
let handles = otg_service.enable_hid().await?;
|
||||
let handles = otg_service.hid_device_paths().await.ok_or_else(|| {
|
||||
AppError::Config("OTG HID paths are not available".to_string())
|
||||
})?;
|
||||
|
||||
// Create OtgBackend from handles (no longer manages gadget itself)
|
||||
info!("Creating OTG HID backend from device paths");
|
||||
Arc::new(otg::OtgBackend::from_handles(handles)?)
|
||||
}
|
||||
@@ -245,6 +258,7 @@ impl HidController {
|
||||
|
||||
// Start HID event worker (once)
|
||||
self.start_event_worker().await;
|
||||
self.restart_runtime_worker().await;
|
||||
|
||||
info!("HID backend initialized: {:?}", backend_type);
|
||||
Ok(())
|
||||
@@ -253,6 +267,7 @@ impl HidController {
|
||||
/// Shutdown the HID backend and release resources
|
||||
pub async fn shutdown(&self) -> Result<()> {
|
||||
info!("Shutting down HID controller");
|
||||
self.stop_runtime_worker().await;
|
||||
|
||||
// Close the backend
|
||||
if let Some(backend) = self.backend.write().await.take() {
|
||||
@@ -271,14 +286,6 @@ impl HidController {
|
||||
}
|
||||
self.apply_runtime_state(shutdown_state).await;
|
||||
|
||||
// If OTG backend, notify OtgService to disable HID
|
||||
if matches!(backend_type, HidBackendType::Otg) {
|
||||
if let Some(ref otg_service) = self.otg_service {
|
||||
info!("Disabling HID functions in OtgService");
|
||||
otg_service.disable_hid().await?;
|
||||
}
|
||||
}
|
||||
|
||||
info!("HID controller shutdown complete");
|
||||
Ok(())
|
||||
}
|
||||
@@ -365,6 +372,7 @@ impl HidController {
|
||||
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;
|
||||
|
||||
// Shutdown existing backend first
|
||||
if let Some(backend) = self.backend.write().await.take() {
|
||||
@@ -389,9 +397,8 @@ impl HidController {
|
||||
}
|
||||
};
|
||||
|
||||
// Request HID functions from OtgService
|
||||
match otg_service.enable_hid().await {
|
||||
Ok(handles) => {
|
||||
match otg_service.hid_device_paths().await {
|
||||
Some(handles) => {
|
||||
// Create OtgBackend from handles
|
||||
match otg::OtgBackend::from_handles(handles) {
|
||||
Ok(backend) => {
|
||||
@@ -403,29 +410,18 @@ impl HidController {
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to initialize OTG backend: {}", e);
|
||||
// Cleanup: disable HID in OtgService
|
||||
if let Err(e2) = otg_service.disable_hid().await {
|
||||
warn!(
|
||||
"Failed to cleanup HID after init failure: {}",
|
||||
e2
|
||||
);
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to create OTG backend: {}", e);
|
||||
// Cleanup: disable HID in OtgService
|
||||
if let Err(e2) = otg_service.disable_hid().await {
|
||||
warn!("Failed to cleanup HID after creation failure: {}", e2);
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to enable HID in OtgService: {}", e);
|
||||
None => {
|
||||
warn!("OTG HID paths are not available");
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -478,6 +474,7 @@ impl HidController {
|
||||
*self.backend_type.write().await = new_backend_type.clone();
|
||||
|
||||
self.sync_runtime_state_from_backend().await;
|
||||
self.restart_runtime_worker().await;
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
@@ -508,16 +505,14 @@ impl HidController {
|
||||
|
||||
async fn sync_runtime_state_from_backend(&self) {
|
||||
let backend_opt = self.backend.read().await.clone();
|
||||
let backend_type = self.backend_type.read().await.clone();
|
||||
|
||||
let next = match backend_opt.as_ref() {
|
||||
Some(backend) => HidRuntimeState::from_backend(&backend_type, backend.as_ref()),
|
||||
None => HidRuntimeState::from_backend_type(&backend_type),
|
||||
};
|
||||
|
||||
self.backend_available
|
||||
.store(next.initialized, Ordering::Release);
|
||||
self.apply_runtime_state(next).await;
|
||||
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) {
|
||||
@@ -533,10 +528,6 @@ impl HidController {
|
||||
};
|
||||
|
||||
let backend = self.backend.clone();
|
||||
let backend_type = self.backend_type.clone();
|
||||
let runtime_state = self.runtime_state.clone();
|
||||
let events = self.events.clone();
|
||||
let backend_available = self.backend_available.clone();
|
||||
let pending_move = self.pending_move.clone();
|
||||
let pending_move_flag = self.pending_move_flag.clone();
|
||||
|
||||
@@ -548,29 +539,13 @@ impl HidController {
|
||||
None => break,
|
||||
};
|
||||
|
||||
process_hid_event(
|
||||
event,
|
||||
&backend,
|
||||
&backend_type,
|
||||
&runtime_state,
|
||||
&events,
|
||||
backend_available.as_ref(),
|
||||
)
|
||||
.await;
|
||||
process_hid_event(event, &backend).await;
|
||||
|
||||
// After each event, flush latest move if pending
|
||||
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(
|
||||
HidEvent::Mouse(move_event),
|
||||
&backend,
|
||||
&backend_type,
|
||||
&runtime_state,
|
||||
&events,
|
||||
backend_available.as_ref(),
|
||||
)
|
||||
.await;
|
||||
process_hid_event(HidEvent::Mouse(move_event), &backend).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -579,6 +554,46 @@ impl HidController {
|
||||
*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(HidEvent::Mouse(event.clone())) {
|
||||
Ok(_) => Ok(()),
|
||||
@@ -618,14 +633,23 @@ impl HidController {
|
||||
}
|
||||
}
|
||||
|
||||
async fn process_hid_event(
|
||||
event: HidEvent,
|
||||
backend: &Arc<RwLock<Option<Arc<dyn HidBackend>>>>,
|
||||
async fn apply_backend_runtime_state(
|
||||
backend_type: &Arc<RwLock<HidBackendType>>,
|
||||
runtime_state: &Arc<RwLock<HidRuntimeState>>,
|
||||
events: &Arc<tokio::sync::RwLock<Option<Arc<EventBus>>>>,
|
||||
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: HidEvent, backend: &Arc<RwLock<Option<Arc<dyn HidBackend>>>>) {
|
||||
let backend_opt = backend.read().await.clone();
|
||||
let backend = match backend_opt {
|
||||
Some(b) => b,
|
||||
@@ -656,11 +680,6 @@ async fn process_hid_event(
|
||||
warn!("HID event processing failed: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
let backend_kind = backend_type.read().await.clone();
|
||||
let next = HidRuntimeState::from_backend(&backend_kind, backend.as_ref());
|
||||
backend_available.store(next.initialized, Ordering::Release);
|
||||
apply_runtime_state(runtime_state, events, next).await;
|
||||
}
|
||||
|
||||
impl Default for HidController {
|
||||
|
||||
315
src/hid/otg.rs
315
src/hid/otg.rs
@@ -1,10 +1,12 @@
|
||||
//! OTG USB Gadget HID backend
|
||||
//!
|
||||
//! This backend uses Linux USB Gadget API to emulate USB HID devices.
|
||||
//! It creates and manages three HID devices:
|
||||
//! - hidg0: Keyboard (8-byte reports, with LED feedback)
|
||||
//! - hidg1: Relative Mouse (4-byte reports)
|
||||
//! - hidg2: Absolute Mouse (6-byte reports)
|
||||
//! It opens the HID gadget device nodes created by `OtgService`.
|
||||
//! Depending on the configured OTG profile, this may include:
|
||||
//! - hidg0: Keyboard
|
||||
//! - hidg1: Relative Mouse
|
||||
//! - hidg2: Absolute Mouse
|
||||
//! - hidg3: Consumer Control Keyboard
|
||||
//!
|
||||
//! Requirements:
|
||||
//! - USB OTG/Device controller (UDC)
|
||||
@@ -20,15 +22,20 @@
|
||||
use async_trait::async_trait;
|
||||
use nix::poll::{poll, PollFd, PollFlags, PollTimeout};
|
||||
use parking_lot::Mutex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs::{self, File, OpenOptions};
|
||||
use std::io::{Read, Write};
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
use std::os::unix::io::AsFd;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::watch;
|
||||
use tracing::{debug, info, trace, warn};
|
||||
|
||||
use super::backend::{HidBackend, HidBackendStatus};
|
||||
use super::backend::{HidBackend, HidBackendRuntimeSnapshot};
|
||||
use super::types::{
|
||||
ConsumerEvent, KeyEventType, KeyboardEvent, KeyboardReport, MouseEvent, MouseEventType,
|
||||
};
|
||||
@@ -45,7 +52,7 @@ enum DeviceType {
|
||||
}
|
||||
|
||||
/// Keyboard LED state
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
|
||||
pub struct LedState {
|
||||
/// Num Lock LED
|
||||
pub num_lock: bool,
|
||||
@@ -123,12 +130,14 @@ pub struct OtgBackend {
|
||||
mouse_abs_dev: Mutex<Option<File>>,
|
||||
/// Consumer control device file
|
||||
consumer_dev: Mutex<Option<File>>,
|
||||
/// Whether keyboard LED/status feedback is enabled.
|
||||
keyboard_leds_enabled: bool,
|
||||
/// Current keyboard state
|
||||
keyboard_state: Mutex<KeyboardReport>,
|
||||
/// Current mouse button state
|
||||
mouse_buttons: AtomicU8,
|
||||
/// Last known LED state (using parking_lot::RwLock for sync access)
|
||||
led_state: parking_lot::RwLock<LedState>,
|
||||
led_state: Arc<parking_lot::RwLock<LedState>>,
|
||||
/// Screen resolution for absolute mouse (using parking_lot::RwLock for sync access)
|
||||
screen_resolution: parking_lot::RwLock<Option<(u32, u32)>>,
|
||||
/// UDC name for state checking (e.g., "fcc00000.usb")
|
||||
@@ -145,6 +154,12 @@ pub struct OtgBackend {
|
||||
error_count: AtomicU8,
|
||||
/// Consecutive EAGAIN count (for offline threshold detection)
|
||||
eagain_count: AtomicU8,
|
||||
/// Runtime change notifier.
|
||||
runtime_notify_tx: watch::Sender<()>,
|
||||
/// LED listener stop flag.
|
||||
led_worker_stop: Arc<AtomicBool>,
|
||||
/// Keyboard LED listener thread.
|
||||
led_worker: Mutex<Option<thread::JoinHandle<()>>>,
|
||||
}
|
||||
|
||||
/// Write timeout in milliseconds (same as JetKVM's hidWriteTimeout)
|
||||
@@ -156,6 +171,7 @@ impl OtgBackend {
|
||||
/// This is the ONLY way to create an OtgBackend - it no longer manages
|
||||
/// the USB gadget itself. The gadget must already be set up by OtgService.
|
||||
pub fn from_handles(paths: HidDevicePaths) -> Result<Self> {
|
||||
let (runtime_notify_tx, _runtime_notify_rx) = watch::channel(());
|
||||
Ok(Self {
|
||||
keyboard_path: paths.keyboard,
|
||||
mouse_rel_path: paths.mouse_relative,
|
||||
@@ -165,32 +181,57 @@ impl OtgBackend {
|
||||
mouse_rel_dev: Mutex::new(None),
|
||||
mouse_abs_dev: Mutex::new(None),
|
||||
consumer_dev: Mutex::new(None),
|
||||
keyboard_leds_enabled: paths.keyboard_leds_enabled,
|
||||
keyboard_state: Mutex::new(KeyboardReport::default()),
|
||||
mouse_buttons: AtomicU8::new(0),
|
||||
led_state: parking_lot::RwLock::new(LedState::default()),
|
||||
led_state: Arc::new(parking_lot::RwLock::new(LedState::default())),
|
||||
screen_resolution: parking_lot::RwLock::new(Some((1920, 1080))),
|
||||
udc_name: parking_lot::RwLock::new(None),
|
||||
udc_name: parking_lot::RwLock::new(paths.udc),
|
||||
initialized: AtomicBool::new(false),
|
||||
online: AtomicBool::new(false),
|
||||
last_error: parking_lot::RwLock::new(None),
|
||||
last_error_log: parking_lot::Mutex::new(std::time::Instant::now()),
|
||||
error_count: AtomicU8::new(0),
|
||||
eagain_count: AtomicU8::new(0),
|
||||
runtime_notify_tx,
|
||||
led_worker_stop: Arc::new(AtomicBool::new(false)),
|
||||
led_worker: Mutex::new(None),
|
||||
})
|
||||
}
|
||||
|
||||
fn notify_runtime_changed(&self) {
|
||||
let _ = self.runtime_notify_tx.send(());
|
||||
}
|
||||
|
||||
fn clear_error(&self) {
|
||||
*self.last_error.write() = None;
|
||||
let mut error = self.last_error.write();
|
||||
if error.is_some() {
|
||||
*error = None;
|
||||
self.notify_runtime_changed();
|
||||
}
|
||||
}
|
||||
|
||||
fn record_error(&self, reason: impl Into<String>, error_code: impl Into<String>) {
|
||||
self.online.store(false, Ordering::Relaxed);
|
||||
*self.last_error.write() = Some((reason.into(), error_code.into()));
|
||||
let reason = reason.into();
|
||||
let error_code = error_code.into();
|
||||
let was_online = self.online.swap(false, Ordering::Relaxed);
|
||||
let mut error = self.last_error.write();
|
||||
let changed = error.as_ref() != Some(&(reason.clone(), error_code.clone()));
|
||||
*error = Some((reason, error_code));
|
||||
drop(error);
|
||||
if was_online || changed {
|
||||
self.notify_runtime_changed();
|
||||
}
|
||||
}
|
||||
|
||||
fn mark_online(&self) {
|
||||
self.online.store(true, Ordering::Relaxed);
|
||||
self.clear_error();
|
||||
let was_online = self.online.swap(true, Ordering::Relaxed);
|
||||
let mut error = self.last_error.write();
|
||||
let cleared_error = error.take().is_some();
|
||||
drop(error);
|
||||
if !was_online || cleared_error {
|
||||
self.notify_runtime_changed();
|
||||
}
|
||||
}
|
||||
|
||||
/// Log throttled error message (max once per second)
|
||||
@@ -305,11 +346,6 @@ impl OtgBackend {
|
||||
None
|
||||
}
|
||||
|
||||
/// Check if device is online
|
||||
pub fn is_online(&self) -> bool {
|
||||
self.online.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// Ensure a device is open and ready for I/O
|
||||
///
|
||||
/// This method is based on PiKVM's `__ensure_device()` pattern:
|
||||
@@ -750,49 +786,180 @@ impl OtgBackend {
|
||||
self.send_consumer_report(event.usage)
|
||||
}
|
||||
|
||||
/// Read keyboard LED state (non-blocking)
|
||||
pub fn read_led_state(&self) -> Result<Option<LedState>> {
|
||||
let mut dev = self.keyboard_dev.lock();
|
||||
if let Some(ref mut file) = *dev {
|
||||
let mut buf = [0u8; 1];
|
||||
match file.read(&mut buf) {
|
||||
Ok(1) => {
|
||||
let state = LedState::from_byte(buf[0]);
|
||||
// Update LED state (using parking_lot RwLock)
|
||||
*self.led_state.write() = state;
|
||||
Ok(Some(state))
|
||||
}
|
||||
Ok(_) => Ok(None), // No data available
|
||||
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => Ok(None),
|
||||
Err(e) => Err(AppError::Internal(format!(
|
||||
"Failed to read LED state: {}",
|
||||
e
|
||||
))),
|
||||
}
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Get last known LED state
|
||||
pub fn led_state(&self) -> LedState {
|
||||
*self.led_state.read()
|
||||
}
|
||||
|
||||
fn build_runtime_snapshot(&self) -> HidBackendRuntimeSnapshot {
|
||||
let initialized = self.initialized.load(Ordering::Relaxed);
|
||||
let mut online = initialized && self.online.load(Ordering::Relaxed);
|
||||
let mut error = self.last_error.read().clone();
|
||||
|
||||
if initialized && !self.check_devices_exist() {
|
||||
online = false;
|
||||
let missing = self.get_missing_devices();
|
||||
error = Some((
|
||||
format!("HID device node missing: {}", missing.join(", ")),
|
||||
"enoent".to_string(),
|
||||
));
|
||||
} else if initialized && !self.is_udc_configured() {
|
||||
online = false;
|
||||
error = Some((
|
||||
"UDC is not in configured state".to_string(),
|
||||
"udc_not_configured".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
HidBackendRuntimeSnapshot {
|
||||
initialized,
|
||||
online,
|
||||
supports_absolute_mouse: self.mouse_abs_path.as_ref().is_some_and(|p| p.exists()),
|
||||
keyboard_leds_enabled: self.keyboard_leds_enabled,
|
||||
led_state: self.led_state(),
|
||||
screen_resolution: *self.screen_resolution.read(),
|
||||
device: self.udc_name.read().clone(),
|
||||
error: error.as_ref().map(|(reason, _)| reason.clone()),
|
||||
error_code: error.as_ref().map(|(_, code)| code.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
fn start_led_worker(&self) {
|
||||
if !self.keyboard_leds_enabled {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(path) = self.keyboard_path.clone() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut worker = self.led_worker.lock();
|
||||
if worker.is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.led_worker_stop.store(false, Ordering::Relaxed);
|
||||
let stop = self.led_worker_stop.clone();
|
||||
let led_state = self.led_state.clone();
|
||||
let runtime_notify_tx = self.runtime_notify_tx.clone();
|
||||
|
||||
let handle = thread::Builder::new()
|
||||
.name("otg-led-listener".to_string())
|
||||
.spawn(move || {
|
||||
while !stop.load(Ordering::Relaxed) {
|
||||
let mut file = match OpenOptions::new()
|
||||
.read(true)
|
||||
.custom_flags(libc::O_NONBLOCK)
|
||||
.open(&path)
|
||||
{
|
||||
Ok(file) => file,
|
||||
Err(err) => {
|
||||
warn!(
|
||||
"Failed to open OTG keyboard LED listener {}: {}",
|
||||
path.display(),
|
||||
err
|
||||
);
|
||||
let _ = runtime_notify_tx.send(());
|
||||
thread::sleep(Duration::from_millis(500));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
while !stop.load(Ordering::Relaxed) {
|
||||
let mut pollfd = [PollFd::new(
|
||||
file.as_fd(),
|
||||
PollFlags::POLLIN | PollFlags::POLLERR | PollFlags::POLLHUP,
|
||||
)];
|
||||
|
||||
match poll(&mut pollfd, PollTimeout::from(500u16)) {
|
||||
Ok(0) => continue,
|
||||
Ok(_) => {
|
||||
let Some(revents) = pollfd[0].revents() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if revents.contains(PollFlags::POLLERR)
|
||||
|| revents.contains(PollFlags::POLLHUP)
|
||||
{
|
||||
let _ = runtime_notify_tx.send(());
|
||||
break;
|
||||
}
|
||||
|
||||
if !revents.contains(PollFlags::POLLIN) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut buf = [0u8; 1];
|
||||
match file.read(&mut buf) {
|
||||
Ok(1) => {
|
||||
let next = LedState::from_byte(buf[0]);
|
||||
let changed = {
|
||||
let mut guard = led_state.write();
|
||||
if *guard == next {
|
||||
false
|
||||
} else {
|
||||
*guard = next;
|
||||
true
|
||||
}
|
||||
};
|
||||
if changed {
|
||||
let _ = runtime_notify_tx.send(());
|
||||
}
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => {}
|
||||
Err(err) => {
|
||||
warn!("OTG keyboard LED listener read failed: {}", err);
|
||||
let _ = runtime_notify_tx.send(());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("OTG keyboard LED listener poll failed: {}", err);
|
||||
let _ = runtime_notify_tx.send(());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !stop.load(Ordering::Relaxed) {
|
||||
thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
match handle {
|
||||
Ok(handle) => {
|
||||
*worker = Some(handle);
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("Failed to spawn OTG keyboard LED listener: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn stop_led_worker(&self) {
|
||||
self.led_worker_stop.store(true, Ordering::Relaxed);
|
||||
if let Some(handle) = self.led_worker.lock().take() {
|
||||
let _ = handle.join();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl HidBackend for OtgBackend {
|
||||
fn name(&self) -> &'static str {
|
||||
"OTG USB Gadget"
|
||||
}
|
||||
|
||||
async fn init(&self) -> Result<()> {
|
||||
info!("Initializing OTG HID backend");
|
||||
|
||||
// Auto-detect UDC name for state checking
|
||||
if let Some(udc) = Self::find_udc() {
|
||||
info!("Auto-detected UDC: {}", udc);
|
||||
self.set_udc_name(&udc);
|
||||
// Auto-detect UDC name for state checking only if OtgService did not provide one
|
||||
if self.udc_name.read().is_none() {
|
||||
if let Some(udc) = Self::find_udc() {
|
||||
info!("Auto-detected UDC: {}", udc);
|
||||
self.set_udc_name(&udc);
|
||||
}
|
||||
} else if let Some(udc) = self.udc_name.read().clone() {
|
||||
info!("Using configured UDC: {}", udc);
|
||||
}
|
||||
|
||||
// Wait for devices to appear (they should already exist from OtgService)
|
||||
@@ -866,6 +1033,8 @@ impl HidBackend for OtgBackend {
|
||||
|
||||
// Mark as online if all devices opened successfully
|
||||
self.initialized.store(true, Ordering::Relaxed);
|
||||
self.notify_runtime_changed();
|
||||
self.start_led_worker();
|
||||
self.mark_online();
|
||||
|
||||
Ok(())
|
||||
@@ -974,6 +1143,8 @@ impl HidBackend for OtgBackend {
|
||||
}
|
||||
|
||||
async fn shutdown(&self) -> Result<()> {
|
||||
self.stop_led_worker();
|
||||
|
||||
// Reset before closing
|
||||
self.reset().await?;
|
||||
|
||||
@@ -987,53 +1158,27 @@ impl HidBackend for OtgBackend {
|
||||
self.initialized.store(false, Ordering::Relaxed);
|
||||
self.online.store(false, Ordering::Relaxed);
|
||||
self.clear_error();
|
||||
self.notify_runtime_changed();
|
||||
|
||||
info!("OTG backend shutdown");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn status(&self) -> HidBackendStatus {
|
||||
let initialized = self.initialized.load(Ordering::Relaxed);
|
||||
let mut online = initialized && self.online.load(Ordering::Relaxed);
|
||||
let mut error = self.last_error.read().clone();
|
||||
|
||||
if initialized && !self.check_devices_exist() {
|
||||
online = false;
|
||||
let missing = self.get_missing_devices();
|
||||
error = Some((
|
||||
format!("HID device node missing: {}", missing.join(", ")),
|
||||
"enoent".to_string(),
|
||||
));
|
||||
} else if initialized && !self.is_udc_configured() {
|
||||
online = false;
|
||||
error = Some((
|
||||
"UDC is not in configured state".to_string(),
|
||||
"udc_not_configured".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
HidBackendStatus {
|
||||
initialized,
|
||||
online,
|
||||
error: error.as_ref().map(|(reason, _)| reason.clone()),
|
||||
error_code: error.as_ref().map(|(_, code)| code.clone()),
|
||||
}
|
||||
fn runtime_snapshot(&self) -> HidBackendRuntimeSnapshot {
|
||||
self.build_runtime_snapshot()
|
||||
}
|
||||
|
||||
fn supports_absolute_mouse(&self) -> bool {
|
||||
self.mouse_abs_path.as_ref().is_some_and(|p| p.exists())
|
||||
fn subscribe_runtime(&self) -> watch::Receiver<()> {
|
||||
self.runtime_notify_tx.subscribe()
|
||||
}
|
||||
|
||||
async fn send_consumer(&self, event: ConsumerEvent) -> Result<()> {
|
||||
self.send_consumer_report(event.usage)
|
||||
}
|
||||
|
||||
fn screen_resolution(&self) -> Option<(u32, u32)> {
|
||||
*self.screen_resolution.read()
|
||||
}
|
||||
|
||||
fn set_screen_resolution(&mut self, width: u32, height: u32) {
|
||||
*self.screen_resolution.write() = Some((width, height));
|
||||
self.notify_runtime_changed();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1050,6 +1195,10 @@ pub fn is_otg_available() -> bool {
|
||||
/// Implement Drop for OtgBackend to close device files
|
||||
impl Drop for OtgBackend {
|
||||
fn drop(&mut self) {
|
||||
self.led_worker_stop.store(true, Ordering::Relaxed);
|
||||
if let Some(handle) = self.led_worker.get_mut().take() {
|
||||
let _ = handle.join();
|
||||
}
|
||||
// Close device files
|
||||
// Note: Gadget cleanup is handled by OtgService, not here
|
||||
*self.keyboard_dev.lock() = None;
|
||||
|
||||
Reference in New Issue
Block a user