refactor: 重构部分事件检查逻辑,修复 ch9329 hid 状态显示异常

This commit is contained in:
mofeng-git
2026-03-26 12:33:24 +08:00
parent ae26e3c863
commit 779aa180ad
20 changed files with 1025 additions and 1709 deletions

View File

@@ -75,6 +75,19 @@ impl HidBackendType {
}
}
/// Current runtime status reported by a HID backend.
#[derive(Debug, Clone, Default)]
pub struct HidBackendStatus {
/// 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,
/// Current user-facing error, if any.
pub error: Option<String>,
/// Current programmatic error code, if any.
pub error_code: Option<String>,
}
/// HID backend trait
#[async_trait]
pub trait HidBackend: Send + Sync {
@@ -104,12 +117,8 @@ pub trait HidBackend: Send + Sync {
/// Shutdown the backend
async fn shutdown(&self) -> Result<()>;
/// Perform backend health check.
///
/// Default implementation assumes backend is healthy.
fn health_check(&self) -> Result<()> {
Ok(())
}
/// Get the current backend runtime status.
fn status(&self) -> HidBackendStatus;
/// Check if backend supports absolute mouse positioning
fn supports_absolute_mouse(&self) -> bool {

File diff suppressed because it is too large Load Diff

View File

@@ -16,13 +16,11 @@ pub mod ch9329;
pub mod consumer;
pub mod datachannel;
pub mod keymap;
pub mod monitor;
pub mod otg;
pub mod types;
pub mod websocket;
pub use backend::{HidBackend, HidBackendType};
pub use monitor::{HidHealthMonitor, HidHealthStatus, HidMonitorConfig};
pub use backend::{HidBackend, HidBackendStatus, HidBackendType};
pub use otg::LedState;
pub use types::{
ConsumerEvent, KeyEventType, KeyboardEvent, KeyboardModifiers, MouseButton, MouseEvent,
@@ -33,7 +31,7 @@ pub use types::{
#[derive(Debug, Clone)]
pub struct HidInfo {
/// Backend name
pub name: &'static str,
pub name: String,
/// Whether backend is initialized
pub initialized: bool,
/// Whether absolute mouse positioning is supported
@@ -42,12 +40,84 @@ pub struct HidInfo {
pub screen_resolution: Option<(u32, u32)>,
}
/// Unified HID runtime state used by snapshots and events.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HidRuntimeState {
/// Whether a backend is configured and expected to exist.
pub available: bool,
/// Stable backend key: "otg", "ch9329", "none".
pub backend: String,
/// Whether the backend is currently initialized and operational.
pub initialized: bool,
/// Whether the backend is currently online.
pub online: bool,
/// Whether absolute mouse positioning is supported.
pub supports_absolute_mouse: bool,
/// Screen resolution for absolute mouse mode.
pub screen_resolution: Option<(u32, u32)>,
/// Device path 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.
pub error_code: Option<String>,
}
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,
screen_resolution: None,
device: device_for_backend_type(backend_type),
error: None,
error_code: None,
}
}
fn from_backend(backend_type: &HidBackendType, backend: &dyn HidBackend) -> Self {
let status = backend.status();
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,
}
}
fn with_error(
backend_type: &HidBackendType,
current: &Self,
reason: impl Into<String>,
error_code: impl Into<String>,
) -> 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.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 tokio::sync::RwLock;
use tracing::{info, warn};
use tokio::sync::RwLock;
use crate::error::{AppError, Result};
use crate::events::{EventBus, SystemEvent};
use crate::otg::OtgService;
use std::time::Duration;
use tokio::sync::mpsc;
@@ -56,7 +126,6 @@ use tokio::task::JoinHandle;
const HID_EVENT_QUEUE_CAPACITY: usize = 64;
const HID_EVENT_SEND_TIMEOUT_MS: u64 = 30;
const HID_HEALTH_CHECK_INTERVAL_MS: u64 = 1000;
#[derive(Debug)]
enum HidEvent {
@@ -75,9 +144,9 @@ pub struct HidController {
/// Backend type (mutable for reload)
backend_type: Arc<RwLock<HidBackendType>>,
/// Event bus for broadcasting state changes (optional)
events: tokio::sync::RwLock<Option<Arc<crate::events::EventBus>>>,
/// Health monitor for error tracking and recovery
monitor: Arc<HidHealthMonitor>,
events: Arc<tokio::sync::RwLock<Option<Arc<EventBus>>>>,
/// Unified HID runtime state.
runtime_state: Arc<RwLock<HidRuntimeState>>,
/// HID event queue sender (non-blocking)
hid_tx: mpsc::Sender<HidEvent>,
/// HID event queue receiver (moved into worker on first start)
@@ -88,10 +157,8 @@ pub struct HidController {
pending_move_flag: Arc<AtomicBool>,
/// Worker task handle
hid_worker: Mutex<Option<JoinHandle<()>>>,
/// Health check task handle
hid_health_checker: Mutex<Option<JoinHandle<()>>>,
/// Backend availability fast flag
backend_available: AtomicBool,
/// Backend initialization fast flag
backend_available: Arc<AtomicBool>,
}
impl HidController {
@@ -103,24 +170,23 @@ impl HidController {
Self {
otg_service,
backend: Arc::new(RwLock::new(None)),
backend_type: Arc::new(RwLock::new(backend_type)),
events: tokio::sync::RwLock::new(None),
monitor: Arc::new(HidHealthMonitor::with_defaults()),
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),
hid_health_checker: Mutex::new(None),
backend_available: AtomicBool::new(false),
backend_available: Arc::new(AtomicBool::new(false)),
}
}
/// Set event bus for broadcasting state changes
pub async fn set_event_bus(&self, events: Arc<crate::events::EventBus>) {
*self.events.write().await = Some(events.clone());
// Also set event bus on the monitor for health notifications
self.monitor.set_event_bus(events).await;
pub async fn set_event_bus(&self, events: Arc<EventBus>) {
*self.events.write().await = Some(events);
}
/// Initialize the HID backend
@@ -157,13 +223,27 @@ impl HidController {
}
};
backend.init().await?;
if let Err(e) = backend.init().await {
self.backend_available.store(false, Ordering::Release);
let error_state = {
let backend_type = self.backend_type.read().await.clone();
let current = self.runtime_state.read().await.clone();
HidRuntimeState::with_error(
&backend_type,
&current,
format!("Failed to initialize HID backend: {}", e),
"init_failed",
)
};
self.apply_runtime_state(error_state).await;
return Err(e);
}
*self.backend.write().await = Some(backend);
self.backend_available.store(true, Ordering::Release);
self.sync_runtime_state_from_backend().await;
// Start HID event worker (once)
self.start_event_worker().await;
self.start_health_checker().await;
info!("HID backend initialized: {:?}", backend_type);
Ok(())
@@ -172,14 +252,25 @@ impl HidController {
/// Shutdown the HID backend and release resources
pub async fn shutdown(&self) -> Result<()> {
info!("Shutting down HID controller");
self.stop_health_checker().await;
// Close the backend
*self.backend.write().await = None;
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;
// If OTG backend, notify OtgService to disable HID
let backend_type = self.backend_type.read().await.clone();
if matches!(backend_type, HidBackendType::Otg) {
if let Some(ref otg_service) = self.otg_service {
info!("Disabling HID functions in OtgService");
@@ -241,7 +332,7 @@ impl HidController {
/// Check if backend is available
pub async fn is_available(&self) -> bool {
self.backend.read().await.is_some()
self.backend_available.load(Ordering::Acquire)
}
/// Get backend type
@@ -251,60 +342,40 @@ impl HidController {
/// Get backend info
pub async fn info(&self) -> Option<HidInfo> {
let backend = self.backend.read().await;
backend.as_ref().map(|b| HidInfo {
name: b.name(),
initialized: true,
supports_absolute_mouse: b.supports_absolute_mouse(),
screen_resolution: b.screen_resolution(),
let state = self.runtime_state.read().await.clone();
if !state.available {
return None;
}
Some(HidInfo {
name: state.backend,
initialized: state.initialized,
supports_absolute_mouse: state.supports_absolute_mouse,
screen_resolution: state.screen_resolution,
})
}
/// Get current HID runtime state snapshot.
pub async fn snapshot(&self) -> HidRuntimeState {
self.runtime_state.read().await.clone()
}
/// Get current state as SystemEvent
pub async fn current_state_event(&self) -> crate::events::SystemEvent {
let backend = self.backend.read().await;
let backend_type = self.backend_type().await;
let (backend_name, initialized) = match backend.as_ref() {
Some(b) => (b.name(), true),
None => (backend_type.name_str(), false),
};
// Include error information from monitor
let (error, error_code) = match self.monitor.status().await {
HidHealthStatus::Error {
reason, error_code, ..
} => (Some(reason), Some(error_code)),
_ => (None, None),
};
crate::events::SystemEvent::HidStateChanged {
backend: backend_name.to_string(),
initialized,
error,
error_code,
let state = self.snapshot().await;
SystemEvent::HidStateChanged {
backend: state.backend,
initialized: state.initialized,
online: state.online,
error: state.error,
error_code: state.error_code,
}
}
/// Get the health monitor reference
pub fn monitor(&self) -> &Arc<HidHealthMonitor> {
&self.monitor
}
/// Get current health status
pub async fn health_status(&self) -> HidHealthStatus {
self.monitor.status().await
}
/// Check if the HID backend is healthy
pub async fn is_healthy(&self) -> bool {
self.monitor.is_healthy().await
}
/// Reload the HID backend with new type
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_health_checker().await;
// Shutdown existing backend first
if let Some(backend) = self.backend.write().await.take() {
@@ -403,27 +474,21 @@ impl HidController {
*self.backend.write().await = 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.backend_available.store(true, Ordering::Release);
self.start_event_worker().await;
self.start_health_checker().await;
// Update backend_type on success
*self.backend_type.write().await = new_backend_type.clone();
// Reset monitor state on successful reload
self.monitor.reset().await;
// Publish HID state changed event
let backend_name = new_backend_type.name_str().to_string();
self.publish_event(crate::events::SystemEvent::HidStateChanged {
backend: backend_name,
initialized: true,
error: None,
error_code: None,
})
.await;
self.sync_runtime_state_from_backend().await;
Ok(())
} else {
@@ -433,14 +498,14 @@ impl HidController {
// Update backend_type even on failure (to reflect the attempted change)
*self.backend_type.write().await = new_backend_type.clone();
// Publish event with initialized=false
self.publish_event(crate::events::SystemEvent::HidStateChanged {
backend: new_backend_type.name_str().to_string(),
initialized: false,
error: Some("Failed to initialize HID backend".to_string()),
error_code: Some("init_failed".to_string()),
})
.await;
let current = self.runtime_state.read().await.clone();
let error_state = HidRuntimeState::with_error(
&new_backend_type,
&current,
"Failed to initialize HID backend",
"init_failed",
);
self.apply_runtime_state(error_state).await;
Err(AppError::Internal(
"Failed to reload HID backend".to_string(),
@@ -448,11 +513,22 @@ impl HidController {
}
}
/// Publish event to event bus if available
async fn publish_event(&self, event: crate::events::SystemEvent) {
if let Some(events) = self.events.read().await.as_ref() {
events.publish(event);
}
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();
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;
}
async fn start_event_worker(&self) {
@@ -468,8 +544,10 @@ impl HidController {
};
let backend = self.backend.clone();
let monitor = self.monitor.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();
@@ -481,7 +559,15 @@ impl HidController {
None => break,
};
process_hid_event(event, &backend, &monitor, &backend_type).await;
process_hid_event(
event,
&backend,
&backend_type,
&runtime_state,
&events,
backend_available.as_ref(),
)
.await;
// After each event, flush latest move if pending
if pending_move_flag.swap(false, Ordering::AcqRel) {
@@ -490,8 +576,10 @@ impl HidController {
process_hid_event(
HidEvent::Mouse(move_event),
&backend,
&monitor,
&backend_type,
&runtime_state,
&events,
backend_available.as_ref(),
)
.await;
}
@@ -502,87 +590,6 @@ impl HidController {
*worker_guard = Some(handle);
}
async fn start_health_checker(&self) {
let mut checker_guard = self.hid_health_checker.lock().await;
if checker_guard.is_some() {
return;
}
let backend = self.backend.clone();
let backend_type = self.backend_type.clone();
let monitor = self.monitor.clone();
let handle = tokio::spawn(async move {
let mut ticker =
tokio::time::interval(Duration::from_millis(HID_HEALTH_CHECK_INTERVAL_MS));
ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
loop {
ticker.tick().await;
let backend_opt = backend.read().await.clone();
let Some(active_backend) = backend_opt else {
continue;
};
let backend_name = backend_type.read().await.name_str().to_string();
let result =
tokio::task::spawn_blocking(move || active_backend.health_check()).await;
match result {
Ok(Ok(())) => {
if monitor.is_error().await {
monitor.report_recovered(&backend_name).await;
}
}
Ok(Err(AppError::HidError {
backend,
reason,
error_code,
})) => {
monitor
.report_error(&backend, None, &reason, &error_code)
.await;
}
Ok(Err(e)) => {
monitor
.report_error(
&backend_name,
None,
&format!("HID health check failed: {}", e),
"health_check_failed",
)
.await;
}
Err(e) => {
monitor
.report_error(
&backend_name,
None,
&format!("HID health check task failed: {}", e),
"health_check_join_failed",
)
.await;
}
}
}
});
*checker_guard = Some(handle);
}
async fn stop_health_checker(&self) {
let handle_opt = {
let mut checker_guard = self.hid_health_checker.lock().await;
checker_guard.take()
};
if let Some(handle) = handle_opt {
handle.abort();
let _ = handle.await;
}
}
fn enqueue_mouse_move(&self, event: MouseEvent) -> Result<()> {
match self.hid_tx.try_send(HidEvent::Mouse(event.clone())) {
Ok(_) => Ok(()),
@@ -625,8 +632,10 @@ impl HidController {
async fn process_hid_event(
event: HidEvent,
backend: &Arc<RwLock<Option<Arc<dyn HidBackend>>>>,
monitor: &Arc<HidHealthMonitor>,
backend_type: &Arc<RwLock<HidBackendType>>,
runtime_state: &Arc<RwLock<HidRuntimeState>>,
events: &Arc<tokio::sync::RwLock<Option<Arc<EventBus>>>>,
backend_available: &AtomicBool,
) {
let backend_opt = backend.read().await.clone();
let backend = match backend_opt {
@@ -634,13 +643,14 @@ async fn process_hid_event(
None => return,
};
let backend_for_send = backend.clone();
let result = tokio::task::spawn_blocking(move || {
futures::executor::block_on(async move {
match event {
HidEvent::Keyboard(ev) => backend.send_keyboard(ev).await,
HidEvent::Mouse(ev) => backend.send_mouse(ev).await,
HidEvent::Consumer(ev) => backend.send_consumer(ev).await,
HidEvent::Reset => backend.reset().await,
HidEvent::Keyboard(ev) => backend_for_send.send_keyboard(ev).await,
HidEvent::Mouse(ev) => backend_for_send.send_mouse(ev).await,
HidEvent::Consumer(ev) => backend_for_send.send_consumer(ev).await,
HidEvent::Reset => backend_for_send.reset().await,
}
})
})
@@ -652,27 +662,16 @@ async fn process_hid_event(
};
match result {
Ok(_) => {
if monitor.is_error().await {
let backend_type = backend_type.read().await;
monitor.report_recovered(backend_type.name_str()).await;
}
}
Ok(_) => {}
Err(e) => {
if let AppError::HidError {
ref backend,
ref reason,
ref error_code,
} = e
{
if error_code != "eagain_retry" {
monitor
.report_error(backend, None, reason, error_code)
.await;
}
}
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 {
@@ -680,3 +679,40 @@ impl Default for HidController {
Self::new(HidBackendType::None, None)
}
}
fn device_for_backend_type(backend_type: &HidBackendType) -> Option<String> {
match backend_type {
HidBackendType::Ch9329 { port, .. } => Some(port.clone()),
_ => None,
}
}
async fn apply_runtime_state(
runtime_state: &Arc<RwLock<HidRuntimeState>>,
events: &Arc<tokio::sync::RwLock<Option<Arc<EventBus>>>>,
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.publish(SystemEvent::HidStateChanged {
backend: next.backend,
initialized: next.initialized,
online: next.online,
error: next.error,
error_code: next.error_code,
});
}
}

View File

@@ -1,416 +0,0 @@
//! HID device health monitoring
//!
//! This module provides health monitoring for HID devices, including:
//! - Device connectivity checks
//! - Automatic reconnection on failure
//! - Error tracking and notification
//! - Log throttling to prevent log flooding
use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::RwLock;
use tracing::{debug, warn};
use crate::events::{EventBus, SystemEvent};
use crate::utils::LogThrottler;
/// HID health status
#[derive(Debug, Clone, PartialEq, Default)]
pub enum HidHealthStatus {
/// Device is healthy and operational
#[default]
Healthy,
/// Device has an error, attempting recovery
Error {
/// Human-readable error reason
reason: String,
/// Error code for programmatic handling
error_code: String,
/// Number of recovery attempts made
retry_count: u32,
},
/// Device is disconnected
Disconnected,
}
/// HID health monitor configuration
#[derive(Debug, Clone)]
pub struct HidMonitorConfig {
/// Health check interval in milliseconds
pub check_interval_ms: u64,
/// Retry interval when device is lost (milliseconds)
pub retry_interval_ms: u64,
/// Maximum retry attempts before giving up (0 = infinite)
pub max_retries: u32,
/// Log throttle interval in seconds
pub log_throttle_secs: u64,
/// Recovery cooldown in milliseconds (suppress logs after recovery)
pub recovery_cooldown_ms: u64,
}
impl Default for HidMonitorConfig {
fn default() -> Self {
Self {
check_interval_ms: 1000,
retry_interval_ms: 1000,
max_retries: 0, // infinite retry
log_throttle_secs: 5,
recovery_cooldown_ms: 1000, // 1 second cooldown after recovery
}
}
}
/// HID health monitor
///
/// Monitors HID device health and manages error recovery.
/// Publishes WebSocket events when device status changes.
pub struct HidHealthMonitor {
/// Current health status
status: RwLock<HidHealthStatus>,
/// Event bus for notifications
events: RwLock<Option<Arc<EventBus>>>,
/// Log throttler to prevent log flooding
throttler: LogThrottler,
/// Configuration
config: HidMonitorConfig,
/// Whether monitoring is active (reserved for future use)
#[allow(dead_code)]
running: AtomicBool,
/// Current retry count
retry_count: AtomicU32,
/// Last error code (for change detection)
last_error_code: RwLock<Option<String>>,
/// Last recovery timestamp (milliseconds since start, for cooldown)
last_recovery_ms: AtomicU64,
/// Start instant for timing
start_instant: Instant,
}
impl HidHealthMonitor {
/// Create a new HID health monitor with the specified configuration
pub fn new(config: HidMonitorConfig) -> Self {
let throttle_secs = config.log_throttle_secs;
Self {
status: RwLock::new(HidHealthStatus::Healthy),
events: RwLock::new(None),
throttler: LogThrottler::with_secs(throttle_secs),
config,
running: AtomicBool::new(false),
retry_count: AtomicU32::new(0),
last_error_code: RwLock::new(None),
last_recovery_ms: AtomicU64::new(0),
start_instant: Instant::now(),
}
}
/// Create a new HID health monitor with default configuration
pub fn with_defaults() -> Self {
Self::new(HidMonitorConfig::default())
}
/// Set the event bus for broadcasting state changes
pub async fn set_event_bus(&self, events: Arc<EventBus>) {
*self.events.write().await = Some(events);
}
/// Report an error from HID operations
///
/// This method is called when an HID operation fails. It:
/// 1. Updates the health status
/// 2. Logs the error (with throttling and cooldown respect)
/// 3. Publishes a WebSocket event if the error is new or changed
///
/// # Arguments
///
/// * `backend` - The HID backend type ("otg" or "ch9329")
/// * `device` - The device path (if known)
/// * `reason` - Human-readable error description
/// * `error_code` - Error code for programmatic handling
pub async fn report_error(
&self,
backend: &str,
device: Option<&str>,
reason: &str,
error_code: &str,
) {
let count = self.retry_count.fetch_add(1, Ordering::Relaxed) + 1;
// Check if we're in cooldown period after recent recovery
let current_ms = self.start_instant.elapsed().as_millis() as u64;
let last_recovery = self.last_recovery_ms.load(Ordering::Relaxed);
let in_cooldown =
last_recovery > 0 && current_ms < last_recovery + self.config.recovery_cooldown_ms;
// Check if error code changed
let error_changed = {
let last = self.last_error_code.read().await;
last.as_ref().map(|s| s.as_str()) != Some(error_code)
};
// Log with throttling (skip if in cooldown period unless error type changed)
let throttle_key = format!("hid_{}_{}", backend, error_code);
if !in_cooldown && (error_changed || self.throttler.should_log(&throttle_key)) {
warn!(
"HID {} error: {} (code: {}, attempt: {})",
backend, reason, error_code, count
);
}
// Update last error code
*self.last_error_code.write().await = Some(error_code.to_string());
// Update status
*self.status.write().await = HidHealthStatus::Error {
reason: reason.to_string(),
error_code: error_code.to_string(),
retry_count: count,
};
// Publish event (only if error changed or first occurrence, and not in cooldown)
if !in_cooldown && (error_changed || count == 1) {
if let Some(ref events) = *self.events.read().await {
events.publish(SystemEvent::HidDeviceLost {
backend: backend.to_string(),
device: device.map(|s| s.to_string()),
reason: reason.to_string(),
error_code: error_code.to_string(),
});
}
}
}
/// Report that a reconnection attempt is starting
///
/// Publishes a reconnecting event to notify clients.
///
/// # Arguments
///
/// * `backend` - The HID backend type
pub async fn report_reconnecting(&self, backend: &str) {
let attempt = self.retry_count.load(Ordering::Relaxed);
// Only publish every 5 attempts to avoid event spam
if attempt == 1 || attempt.is_multiple_of(5) {
debug!("HID {} reconnecting, attempt {}", backend, attempt);
if let Some(ref events) = *self.events.read().await {
events.publish(SystemEvent::HidReconnecting {
backend: backend.to_string(),
attempt,
});
}
}
}
/// Report that the device has recovered
///
/// This method is called when the HID device successfully reconnects.
/// It resets the error state and publishes a recovery event.
///
/// # Arguments
///
/// * `backend` - The HID backend type
pub async fn report_recovered(&self, backend: &str) {
let prev_status = self.status.read().await.clone();
// Only report recovery if we were in an error state
if prev_status != HidHealthStatus::Healthy {
let retry_count = self.retry_count.load(Ordering::Relaxed);
// Set cooldown timestamp
let current_ms = self.start_instant.elapsed().as_millis() as u64;
self.last_recovery_ms.store(current_ms, Ordering::Relaxed);
// Only log and publish events if there were multiple retries
// (avoid log spam for transient single-retry recoveries)
if retry_count > 1 {
debug!("HID {} recovered after {} retries", backend, retry_count);
// Publish recovery event
if let Some(ref events) = *self.events.read().await {
events.publish(SystemEvent::HidRecovered {
backend: backend.to_string(),
});
// Also publish state changed to indicate healthy state
events.publish(SystemEvent::HidStateChanged {
backend: backend.to_string(),
initialized: true,
error: None,
error_code: None,
});
}
}
// Reset state (always reset, even for single-retry recoveries)
self.retry_count.store(0, Ordering::Relaxed);
*self.last_error_code.write().await = None;
*self.status.write().await = HidHealthStatus::Healthy;
}
}
/// Get the current health status
pub async fn status(&self) -> HidHealthStatus {
self.status.read().await.clone()
}
/// Get the current retry count
pub fn retry_count(&self) -> u32 {
self.retry_count.load(Ordering::Relaxed)
}
/// Check if the monitor is in an error state
pub async fn is_error(&self) -> bool {
matches!(*self.status.read().await, HidHealthStatus::Error { .. })
}
/// Check if the monitor is healthy
pub async fn is_healthy(&self) -> bool {
matches!(*self.status.read().await, HidHealthStatus::Healthy)
}
/// Reset the monitor to healthy state without publishing events
///
/// This is useful during initialization.
pub async fn reset(&self) {
self.retry_count.store(0, Ordering::Relaxed);
*self.last_error_code.write().await = None;
*self.status.write().await = HidHealthStatus::Healthy;
self.throttler.clear_all();
}
/// Get the configuration
pub fn config(&self) -> &HidMonitorConfig {
&self.config
}
/// Check if we should continue retrying
///
/// Returns `false` if max_retries is set and we've exceeded it.
pub fn should_retry(&self) -> bool {
if self.config.max_retries == 0 {
return true; // Infinite retry
}
self.retry_count.load(Ordering::Relaxed) < self.config.max_retries
}
/// Get the retry interval
pub fn retry_interval(&self) -> Duration {
Duration::from_millis(self.config.retry_interval_ms)
}
}
impl Default for HidHealthMonitor {
fn default() -> Self {
Self::with_defaults()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_initial_status() {
let monitor = HidHealthMonitor::with_defaults();
assert!(monitor.is_healthy().await);
assert!(!monitor.is_error().await);
assert_eq!(monitor.retry_count(), 0);
}
#[tokio::test]
async fn test_report_error() {
let monitor = HidHealthMonitor::with_defaults();
monitor
.report_error("otg", Some("/dev/hidg0"), "Device not found", "enoent")
.await;
assert!(monitor.is_error().await);
assert_eq!(monitor.retry_count(), 1);
if let HidHealthStatus::Error {
reason,
error_code,
retry_count,
} = monitor.status().await
{
assert_eq!(reason, "Device not found");
assert_eq!(error_code, "enoent");
assert_eq!(retry_count, 1);
} else {
panic!("Expected Error status");
}
}
#[tokio::test]
async fn test_report_recovered() {
let monitor = HidHealthMonitor::with_defaults();
// First report an error
monitor
.report_error("ch9329", None, "Port not found", "port_not_found")
.await;
assert!(monitor.is_error().await);
// Then report recovery
monitor.report_recovered("ch9329").await;
assert!(monitor.is_healthy().await);
assert_eq!(monitor.retry_count(), 0);
}
#[tokio::test]
async fn test_retry_count_increments() {
let monitor = HidHealthMonitor::with_defaults();
for i in 1..=5 {
monitor.report_error("otg", None, "Error", "io_error").await;
assert_eq!(monitor.retry_count(), i);
}
}
#[tokio::test]
async fn test_should_retry_infinite() {
let monitor = HidHealthMonitor::new(HidMonitorConfig {
max_retries: 0, // infinite
..Default::default()
});
for _ in 0..100 {
monitor.report_error("otg", None, "Error", "io_error").await;
assert!(monitor.should_retry());
}
}
#[tokio::test]
async fn test_should_retry_limited() {
let monitor = HidHealthMonitor::new(HidMonitorConfig {
max_retries: 3,
..Default::default()
});
assert!(monitor.should_retry());
monitor.report_error("otg", None, "Error", "io_error").await;
assert!(monitor.should_retry()); // 1 < 3
monitor.report_error("otg", None, "Error", "io_error").await;
assert!(monitor.should_retry()); // 2 < 3
monitor.report_error("otg", None, "Error", "io_error").await;
assert!(!monitor.should_retry()); // 3 >= 3
}
#[tokio::test]
async fn test_reset() {
let monitor = HidHealthMonitor::with_defaults();
monitor.report_error("otg", None, "Error", "io_error").await;
assert!(monitor.is_error().await);
monitor.reset().await;
assert!(monitor.is_healthy().await);
assert_eq!(monitor.retry_count(), 0);
}
}

View File

@@ -28,7 +28,7 @@ use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
use tracing::{debug, info, trace, warn};
use super::backend::HidBackend;
use super::backend::{HidBackend, HidBackendStatus};
use super::keymap;
use super::types::{
ConsumerEvent, KeyEventType, KeyboardEvent, KeyboardReport, MouseEvent, MouseEventType,
@@ -134,8 +134,12 @@ pub struct OtgBackend {
screen_resolution: parking_lot::RwLock<Option<(u32, u32)>>,
/// UDC name for state checking (e.g., "fcc00000.usb")
udc_name: parking_lot::RwLock<Option<String>>,
/// Whether the backend has been initialized.
initialized: AtomicBool,
/// Whether the device is currently online (UDC configured and devices accessible)
online: AtomicBool,
/// Last backend error state.
last_error: parking_lot::RwLock<Option<(String, String)>>,
/// Last error log time for throttling (using parking_lot for sync)
last_error_log: parking_lot::Mutex<std::time::Instant>,
/// Error count since last successful operation (for log throttling)
@@ -167,13 +171,29 @@ impl OtgBackend {
led_state: parking_lot::RwLock::new(LedState::default()),
screen_resolution: parking_lot::RwLock::new(Some((1920, 1080))),
udc_name: parking_lot::RwLock::new(None),
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),
})
}
fn clear_error(&self) {
*self.last_error.write() = None;
}
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()));
}
fn mark_online(&self) {
self.online.store(true, Ordering::Relaxed);
self.clear_error();
}
/// Log throttled error message (max once per second)
fn log_throttled_error(&self, msg: &str) {
let mut last_log = self.last_error_log.lock();
@@ -308,12 +328,13 @@ impl OtgBackend {
let path = match path_opt {
Some(p) => p,
None => {
self.online.store(false, Ordering::Relaxed);
return Err(AppError::HidError {
let err = AppError::HidError {
backend: "otg".to_string(),
reason: "Device disabled".to_string(),
error_code: "disabled".to_string(),
});
};
self.record_error("Device disabled", "disabled");
return Err(err);
}
};
@@ -328,10 +349,11 @@ impl OtgBackend {
);
*dev = None;
}
self.online.store(false, Ordering::Relaxed);
let reason = format!("Device not found: {}", path.display());
self.record_error(reason.clone(), "enoent");
return Err(AppError::HidError {
backend: "otg".to_string(),
reason: format!("Device not found: {}", path.display()),
reason,
error_code: "enoent".to_string(),
});
}
@@ -346,12 +368,16 @@ impl OtgBackend {
}
Err(e) => {
warn!("Failed to reopen HID device {}: {}", path.display(), e);
self.record_error(
format!("Failed to reopen HID device {}: {}", path.display(), e),
"not_opened",
);
return Err(e);
}
}
}
self.online.store(true, Ordering::Relaxed);
self.mark_online();
Ok(())
}
@@ -372,8 +398,8 @@ impl OtgBackend {
}
/// Convert I/O error to HidError with appropriate error code
fn io_error_to_hid_error(e: std::io::Error, operation: &str) -> AppError {
let error_code = match e.raw_os_error() {
fn io_error_code(e: &std::io::Error) -> &'static str {
match e.raw_os_error() {
Some(32) => "epipe", // EPIPE - broken pipe
Some(108) => "eshutdown", // ESHUTDOWN - transport endpoint shutdown
Some(11) => "eagain", // EAGAIN - resource temporarily unavailable
@@ -382,7 +408,11 @@ impl OtgBackend {
Some(5) => "eio", // EIO - I/O error
Some(2) => "enoent", // ENOENT - no such file or directory
_ => "io_error",
};
}
}
fn io_error_to_hid_error(e: std::io::Error, operation: &str) -> AppError {
let error_code = Self::io_error_code(&e);
AppError::HidError {
backend: "otg".to_string(),
@@ -438,7 +468,7 @@ impl OtgBackend {
let data = report.to_bytes();
match self.write_with_timeout(file, &data) {
Ok(true) => {
self.online.store(true, Ordering::Relaxed);
self.mark_online();
self.reset_error_count();
debug!("Sent keyboard report: {:02X?}", data);
Ok(())
@@ -454,10 +484,13 @@ impl OtgBackend {
match error_code {
Some(108) => {
// ESHUTDOWN - endpoint closed, need to reopen device
self.online.store(false, Ordering::Relaxed);
self.eagain_count.store(0, Ordering::Relaxed);
debug!("Keyboard ESHUTDOWN, closing for recovery");
*dev = None;
self.record_error(
format!("Failed to write keyboard report: {}", e),
"eshutdown",
);
Err(Self::io_error_to_hid_error(
e,
"Failed to write keyboard report",
@@ -469,9 +502,12 @@ impl OtgBackend {
Ok(())
}
_ => {
self.online.store(false, Ordering::Relaxed);
self.eagain_count.store(0, Ordering::Relaxed);
warn!("Keyboard write error: {}", e);
self.record_error(
format!("Failed to write keyboard report: {}", e),
Self::io_error_code(&e),
);
Err(Self::io_error_to_hid_error(
e,
"Failed to write keyboard report",
@@ -507,7 +543,7 @@ impl OtgBackend {
let data = [buttons, dx as u8, dy as u8, wheel as u8];
match self.write_with_timeout(file, &data) {
Ok(true) => {
self.online.store(true, Ordering::Relaxed);
self.mark_online();
self.reset_error_count();
trace!("Sent relative mouse report: {:02X?}", data);
Ok(())
@@ -521,10 +557,13 @@ impl OtgBackend {
match error_code {
Some(108) => {
self.online.store(false, Ordering::Relaxed);
self.eagain_count.store(0, Ordering::Relaxed);
debug!("Relative mouse ESHUTDOWN, closing for recovery");
*dev = None;
self.record_error(
format!("Failed to write mouse report: {}", e),
"eshutdown",
);
Err(Self::io_error_to_hid_error(
e,
"Failed to write mouse report",
@@ -535,9 +574,12 @@ impl OtgBackend {
Ok(())
}
_ => {
self.online.store(false, Ordering::Relaxed);
self.eagain_count.store(0, Ordering::Relaxed);
warn!("Relative mouse write error: {}", e);
self.record_error(
format!("Failed to write mouse report: {}", e),
Self::io_error_code(&e),
);
Err(Self::io_error_to_hid_error(
e,
"Failed to write mouse report",
@@ -580,7 +622,7 @@ impl OtgBackend {
];
match self.write_with_timeout(file, &data) {
Ok(true) => {
self.online.store(true, Ordering::Relaxed);
self.mark_online();
self.reset_error_count();
Ok(())
}
@@ -593,10 +635,13 @@ impl OtgBackend {
match error_code {
Some(108) => {
self.online.store(false, Ordering::Relaxed);
self.eagain_count.store(0, Ordering::Relaxed);
debug!("Absolute mouse ESHUTDOWN, closing for recovery");
*dev = None;
self.record_error(
format!("Failed to write mouse report: {}", e),
"eshutdown",
);
Err(Self::io_error_to_hid_error(
e,
"Failed to write mouse report",
@@ -607,9 +652,12 @@ impl OtgBackend {
Ok(())
}
_ => {
self.online.store(false, Ordering::Relaxed);
self.eagain_count.store(0, Ordering::Relaxed);
warn!("Absolute mouse write error: {}", e);
self.record_error(
format!("Failed to write mouse report: {}", e),
Self::io_error_code(&e),
);
Err(Self::io_error_to_hid_error(
e,
"Failed to write mouse report",
@@ -648,7 +696,7 @@ impl OtgBackend {
// Send release (0x0000)
let release = [0u8, 0u8];
let _ = self.write_with_timeout(file, &release);
self.online.store(true, Ordering::Relaxed);
self.mark_online();
self.reset_error_count();
Ok(())
}
@@ -660,9 +708,12 @@ impl OtgBackend {
let error_code = e.raw_os_error();
match error_code {
Some(108) => {
self.online.store(false, Ordering::Relaxed);
debug!("Consumer control ESHUTDOWN, closing for recovery");
*dev = None;
self.record_error(
format!("Failed to write consumer report: {}", e),
"eshutdown",
);
Err(Self::io_error_to_hid_error(
e,
"Failed to write consumer report",
@@ -673,8 +724,11 @@ impl OtgBackend {
Ok(())
}
_ => {
self.online.store(false, Ordering::Relaxed);
warn!("Consumer control write error: {}", e);
self.record_error(
format!("Failed to write consumer report: {}", e),
Self::io_error_code(&e),
);
Err(Self::io_error_to_hid_error(
e,
"Failed to write consumer report",
@@ -812,7 +866,8 @@ impl HidBackend for OtgBackend {
}
// Mark as online if all devices opened successfully
self.online.store(true, Ordering::Relaxed);
self.initialized.store(true, Ordering::Relaxed);
self.mark_online();
Ok(())
}
@@ -935,33 +990,40 @@ impl HidBackend for OtgBackend {
*self.consumer_dev.lock() = None;
// Gadget cleanup is handled by OtgService, not here
self.initialized.store(false, Ordering::Relaxed);
self.online.store(false, Ordering::Relaxed);
self.clear_error();
info!("OTG backend shutdown");
Ok(())
}
fn health_check(&self) -> Result<()> {
if !self.check_devices_exist() {
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();
self.online.store(false, Ordering::Relaxed);
return Err(AppError::HidError {
backend: "otg".to_string(),
reason: format!("HID device node missing: {}", missing.join(", ")),
error_code: "enoent".to_string(),
});
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(),
));
}
if !self.is_udc_configured() {
self.online.store(false, Ordering::Relaxed);
return Err(AppError::HidError {
backend: "otg".to_string(),
reason: "UDC is not in configured state".to_string(),
error_code: "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()),
}
self.online.store(true, Ordering::Relaxed);
Ok(())
}
fn supports_absolute_mouse(&self) -> bool {