mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-03-26 04:46:35 +08:00
feat(hid): 增加 HID 后端健康检查与错误码上报,完善前端掉线恢复状态同步及错误提示展示
This commit is contained in:
@@ -295,7 +295,10 @@ impl AtxKeyExecutor {
|
||||
|
||||
/// Pulse Serial relay
|
||||
async fn pulse_serial(&self, duration: Duration) -> Result<()> {
|
||||
info!("Pulse serial relay on {} pin {}", self.config.device, self.config.pin);
|
||||
info!(
|
||||
"Pulse serial relay on {} pin {}",
|
||||
self.config.device, self.config.pin
|
||||
);
|
||||
// Turn relay on
|
||||
self.send_serial_relay_command(true)?;
|
||||
|
||||
@@ -328,7 +331,7 @@ impl AtxKeyExecutor {
|
||||
// Checksum = A0 + channel + state
|
||||
let state = if on { 1 } else { 0 };
|
||||
let checksum = 0xA0u8.wrapping_add(channel).wrapping_add(state);
|
||||
|
||||
|
||||
// Example for Channel 1:
|
||||
// ON: A0 01 01 A2
|
||||
// OFF: A0 01 00 A1
|
||||
|
||||
@@ -104,6 +104,13 @@ 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(())
|
||||
}
|
||||
|
||||
/// Check if backend supports absolute mouse positioning
|
||||
fn supports_absolute_mouse(&self) -> bool {
|
||||
false
|
||||
|
||||
@@ -741,6 +741,20 @@ impl Ch9329Backend {
|
||||
}
|
||||
}
|
||||
|
||||
fn update_chip_info_cache(&self, response: &Response) -> Result<ChipInfo> {
|
||||
if let Some(info) = ChipInfo::from_response(&response.data) {
|
||||
*self.chip_info.write() = Some(info.clone());
|
||||
*self.led_status.write() = LedStatus {
|
||||
num_lock: info.num_lock,
|
||||
caps_lock: info.caps_lock,
|
||||
scroll_lock: info.scroll_lock,
|
||||
};
|
||||
Ok(info)
|
||||
} else {
|
||||
Err(AppError::Internal("Failed to parse chip info".to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Public API
|
||||
// ========================================================================
|
||||
@@ -759,18 +773,15 @@ impl Ch9329Backend {
|
||||
response.cmd, response.data, response.is_error
|
||||
);
|
||||
|
||||
if let Some(info) = ChipInfo::from_response(&response.data) {
|
||||
// Update cache
|
||||
*self.chip_info.write() = Some(info.clone());
|
||||
*self.led_status.write() = LedStatus {
|
||||
num_lock: info.num_lock,
|
||||
caps_lock: info.caps_lock,
|
||||
scroll_lock: info.scroll_lock,
|
||||
};
|
||||
Ok(info)
|
||||
} else {
|
||||
Err(AppError::Internal("Failed to parse chip info".to_string()))
|
||||
if response.is_error {
|
||||
let reason = response
|
||||
.error_code
|
||||
.map(|e| format!("CH9329 error response: {}", e))
|
||||
.unwrap_or_else(|| "CH9329 returned error response".to_string());
|
||||
return Err(AppError::Internal(reason));
|
||||
}
|
||||
|
||||
self.update_chip_info_cache(&response)
|
||||
}
|
||||
|
||||
/// Get cached LED status
|
||||
@@ -1103,6 +1114,56 @@ impl HidBackend for Ch9329Backend {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn health_check(&self) -> Result<()> {
|
||||
if !self.check_port_exists() {
|
||||
return Err(AppError::HidError {
|
||||
backend: "ch9329".to_string(),
|
||||
reason: format!("Serial port {} not found", self.port_path),
|
||||
error_code: "port_not_found".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
if !self.is_port_open() {
|
||||
return Err(AppError::HidError {
|
||||
backend: "ch9329".to_string(),
|
||||
reason: "CH9329 serial port is not open".to_string(),
|
||||
error_code: "port_not_opened".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let response =
|
||||
self.send_and_receive(cmd::GET_INFO, &[])
|
||||
.map_err(|e| AppError::HidError {
|
||||
backend: "ch9329".to_string(),
|
||||
reason: format!("CH9329 health check failed: {}", e),
|
||||
error_code: "no_response".to_string(),
|
||||
})?;
|
||||
|
||||
if response.is_error {
|
||||
let reason = response
|
||||
.error_code
|
||||
.map(|e| format!("CH9329 error response: {}", e))
|
||||
.unwrap_or_else(|| "CH9329 returned error response".to_string());
|
||||
return Err(AppError::HidError {
|
||||
backend: "ch9329".to_string(),
|
||||
reason,
|
||||
error_code: "protocol_error".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
self.update_chip_info_cache(&response)
|
||||
.map_err(|e| AppError::HidError {
|
||||
backend: "ch9329".to_string(),
|
||||
reason: format!("CH9329 invalid response: {}", e),
|
||||
error_code: "invalid_response".to_string(),
|
||||
})?;
|
||||
|
||||
self.error_count.store(0, Ordering::Relaxed);
|
||||
*self.last_success.lock() = Some(Instant::now());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn supports_absolute_mouse(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
@@ -56,6 +56,7 @@ 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 {
|
||||
@@ -87,6 +88,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,
|
||||
}
|
||||
@@ -108,6 +111,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),
|
||||
hid_health_checker: Mutex::new(None),
|
||||
backend_available: AtomicBool::new(false),
|
||||
}
|
||||
}
|
||||
@@ -159,6 +163,7 @@ impl HidController {
|
||||
|
||||
// Start HID event worker (once)
|
||||
self.start_event_worker().await;
|
||||
self.start_health_checker().await;
|
||||
|
||||
info!("HID backend initialized: {:?}", backend_type);
|
||||
Ok(())
|
||||
@@ -167,6 +172,7 @@ 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;
|
||||
@@ -298,6 +304,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_health_checker().await;
|
||||
|
||||
// Shutdown existing backend first
|
||||
if let Some(backend) = self.backend.write().await.take() {
|
||||
@@ -400,6 +407,7 @@ impl HidController {
|
||||
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();
|
||||
@@ -494,6 +502,87 @@ 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(()),
|
||||
|
||||
@@ -940,6 +940,30 @@ impl HidBackend for OtgBackend {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn health_check(&self) -> Result<()> {
|
||||
if !self.check_devices_exist() {
|
||||
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(),
|
||||
});
|
||||
}
|
||||
|
||||
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(),
|
||||
});
|
||||
}
|
||||
|
||||
self.online.store(true, Ordering::Relaxed);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn supports_absolute_mouse(&self) -> bool {
|
||||
self.mouse_abs_path.as_ref().is_some_and(|p| p.exists())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user