mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-02-03 11:31:53 +08:00
feat(video): 事务化切换与前端统一编排,增强视频输入格式支持
- 后端:切换事务+transition_id,/stream/mode 返回 switching/transition_id 与实际 codec - 事件:新增 mode_switching/mode_ready,config/webrtc_ready/mode_changed 关联事务 - 编码/格式:扩展 NV21/NV16/NV24/RGB/BGR 输入与转换链路,RKMPP direct input 优化 - 前端:useVideoSession 统一切换,失败回退真实切回 MJPEG,菜单格式同步修复 - 清理:useVideoStream 降级为 MJPEG-only
This commit is contained in:
@@ -21,7 +21,7 @@ use async_trait::async_trait;
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::io::{Read, Write};
|
||||
use std::sync::atomic::{AtomicBool, AtomicU16, AtomicU8, AtomicU32, Ordering};
|
||||
use std::sync::atomic::{AtomicBool, AtomicU16, AtomicU32, AtomicU8, Ordering};
|
||||
use std::time::{Duration, Instant};
|
||||
use tracing::{debug, info, trace, warn};
|
||||
|
||||
@@ -358,8 +358,7 @@ impl Response {
|
||||
|
||||
/// Check if the response indicates success
|
||||
pub fn is_success(&self) -> bool {
|
||||
!self.is_error
|
||||
&& (self.data.is_empty() || self.data[0] == Ch9329Error::Success as u8)
|
||||
!self.is_error && (self.data.is_empty() || self.data[0] == Ch9329Error::Success as u8)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -489,7 +488,10 @@ impl Ch9329Backend {
|
||||
.map_err(|e| Self::serial_error_to_hid_error(e, "Failed to open serial port"))?;
|
||||
|
||||
*self.port.lock() = Some(port);
|
||||
info!("CH9329 serial port reopened: {} @ {} baud", self.port_path, self.baud_rate);
|
||||
info!(
|
||||
"CH9329 serial port reopened: {} @ {} baud",
|
||||
self.port_path, self.baud_rate
|
||||
);
|
||||
|
||||
// Verify connection with GET_INFO command
|
||||
self.query_chip_info().map_err(|e| {
|
||||
@@ -518,7 +520,10 @@ impl Ch9329Backend {
|
||||
/// Returns the packet buffer and the actual length
|
||||
#[inline]
|
||||
fn build_packet_buf(&self, cmd: u8, data: &[u8]) -> ([u8; MAX_PACKET_SIZE], usize) {
|
||||
debug_assert!(data.len() <= MAX_DATA_LEN, "Data too long for CH9329 packet");
|
||||
debug_assert!(
|
||||
data.len() <= MAX_DATA_LEN,
|
||||
"Data too long for CH9329 packet"
|
||||
);
|
||||
|
||||
let len = data.len() as u8;
|
||||
let packet_len = 6 + data.len();
|
||||
@@ -554,16 +559,19 @@ impl Ch9329Backend {
|
||||
|
||||
let mut port_guard = self.port.lock();
|
||||
if let Some(ref mut port) = *port_guard {
|
||||
port.write_all(&packet[..packet_len]).map_err(|e| {
|
||||
AppError::HidError {
|
||||
port.write_all(&packet[..packet_len])
|
||||
.map_err(|e| AppError::HidError {
|
||||
backend: "ch9329".to_string(),
|
||||
reason: format!("Failed to write to CH9329: {}", e),
|
||||
error_code: "write_failed".to_string(),
|
||||
}
|
||||
})?;
|
||||
})?;
|
||||
// Only log mouse button events at debug level to avoid flooding
|
||||
if cmd == cmd::SEND_MS_ABS_DATA && data.len() >= 2 && data[1] != 0 {
|
||||
debug!("CH9329 TX [cmd=0x{:02X}]: {:02X?}", cmd, &packet[..packet_len]);
|
||||
debug!(
|
||||
"CH9329 TX [cmd=0x{:02X}]: {:02X?}",
|
||||
cmd,
|
||||
&packet[..packet_len]
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
} else {
|
||||
@@ -655,7 +663,11 @@ impl Ch9329Backend {
|
||||
info!(
|
||||
"CH9329: Recovery successful, chip version: {}, USB: {}",
|
||||
info.version,
|
||||
if info.usb_connected { "connected" } else { "disconnected" }
|
||||
if info.usb_connected {
|
||||
"connected"
|
||||
} else {
|
||||
"disconnected"
|
||||
}
|
||||
);
|
||||
// Reset error count on successful recovery
|
||||
self.error_count.store(0, Ordering::Relaxed);
|
||||
@@ -695,9 +707,8 @@ impl Ch9329Backend {
|
||||
let mut port_guard = self.port.lock();
|
||||
if let Some(ref mut port) = *port_guard {
|
||||
// Send packet
|
||||
port.write_all(&packet).map_err(|e| {
|
||||
AppError::Internal(format!("Failed to write to CH9329: {}", e))
|
||||
})?;
|
||||
port.write_all(&packet)
|
||||
.map_err(|e| AppError::Internal(format!("Failed to write to CH9329: {}", e)))?;
|
||||
trace!("CH9329 TX: {:02X?}", packet);
|
||||
|
||||
// Wait for response - use shorter delay for faster response
|
||||
@@ -725,7 +736,10 @@ impl Ch9329Backend {
|
||||
debug!("CH9329 response timeout (may be normal)");
|
||||
Err(AppError::Internal("CH9329 response timeout".to_string()))
|
||||
}
|
||||
Err(e) => Err(AppError::Internal(format!("Failed to read from CH9329: {}", e))),
|
||||
Err(e) => Err(AppError::Internal(format!(
|
||||
"Failed to read from CH9329: {}",
|
||||
e
|
||||
))),
|
||||
}
|
||||
} else {
|
||||
Err(AppError::Internal("CH9329 port not opened".to_string()))
|
||||
@@ -799,7 +813,9 @@ impl Ch9329Backend {
|
||||
if response.is_success() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(AppError::Internal("Failed to restore factory defaults".to_string()))
|
||||
Err(AppError::Internal(
|
||||
"Failed to restore factory defaults".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -820,7 +836,9 @@ impl Ch9329Backend {
|
||||
/// For other multimedia keys: data = [0x02, byte2, byte3, byte4]
|
||||
pub fn send_media_key(&self, data: &[u8]) -> Result<()> {
|
||||
if data.len() < 2 || data.len() > 4 {
|
||||
return Err(AppError::Internal("Invalid media key data length".to_string()));
|
||||
return Err(AppError::Internal(
|
||||
"Invalid media key data length".to_string(),
|
||||
));
|
||||
}
|
||||
self.send_packet(cmd::SEND_KB_MEDIA_DATA, data)
|
||||
}
|
||||
@@ -871,10 +889,7 @@ impl Ch9329Backend {
|
||||
// Use send_packet which has retry logic built-in
|
||||
self.send_packet(cmd::SEND_MS_ABS_DATA, &data)?;
|
||||
|
||||
trace!(
|
||||
"CH9329 mouse: buttons=0x{:02X} pos=({},{})",
|
||||
buttons, x, y
|
||||
);
|
||||
trace!("CH9329 mouse: buttons=0x{:02X} pos=({},{})", buttons, x, y);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -930,7 +945,11 @@ impl HidBackend for Ch9329Backend {
|
||||
info!(
|
||||
"CH9329 chip detected: {}, USB: {}, LEDs: NumLock={}, CapsLock={}, ScrollLock={}",
|
||||
info.version,
|
||||
if info.usb_connected { "connected" } else { "disconnected" },
|
||||
if info.usb_connected {
|
||||
"connected"
|
||||
} else {
|
||||
"disconnected"
|
||||
},
|
||||
info.num_lock,
|
||||
info.caps_lock,
|
||||
info.scroll_lock
|
||||
@@ -1128,10 +1147,7 @@ pub fn detect_ch9329() -> Option<String> {
|
||||
&& response[0] == PACKET_HEADER[0]
|
||||
&& response[1] == PACKET_HEADER[1]
|
||||
{
|
||||
info!(
|
||||
"CH9329 detected on {} @ {} baud",
|
||||
port_path, baud_rate
|
||||
);
|
||||
info!("CH9329 detected on {} @ {} baud", port_path, baud_rate);
|
||||
return Some(port_path.to_string());
|
||||
}
|
||||
}
|
||||
@@ -1176,10 +1192,7 @@ pub fn detect_ch9329_with_baud() -> Option<(String, u32)> {
|
||||
&& response[0] == PACKET_HEADER[0]
|
||||
&& response[1] == PACKET_HEADER[1]
|
||||
{
|
||||
info!(
|
||||
"CH9329 detected on {} @ {} baud",
|
||||
port_path, baud_rate
|
||||
);
|
||||
info!("CH9329 detected on {} @ {} baud", port_path, baud_rate);
|
||||
return Some((port_path.to_string(), baud_rate));
|
||||
}
|
||||
}
|
||||
@@ -1217,7 +1230,7 @@ mod tests {
|
||||
assert_eq!(packet[3], cmd::SEND_KB_GENERAL_DATA); // Command
|
||||
assert_eq!(packet[4], 8); // Length (8 data bytes)
|
||||
assert_eq!(&packet[5..13], &data); // Data
|
||||
// Checksum = 0x57 + 0xAB + 0x00 + 0x02 + 0x08 + 0x00 + 0x00 + 0x04 + ... = 0x10
|
||||
// Checksum = 0x57 + 0xAB + 0x00 + 0x02 + 0x08 + 0x00 + 0x00 + 0x04 + ... = 0x10
|
||||
let expected_checksum: u8 = packet[..13].iter().fold(0u8, |acc, &x| acc.wrapping_add(x));
|
||||
assert_eq!(packet[13], expected_checksum);
|
||||
}
|
||||
@@ -1234,10 +1247,10 @@ mod tests {
|
||||
assert_eq!(packet[1], 0xAB);
|
||||
assert_eq!(packet[2], 0x00); // Address
|
||||
assert_eq!(packet[3], 0x05); // CMD_SEND_MS_REL_DATA
|
||||
assert_eq!(packet[4], 5); // Length = 5
|
||||
assert_eq!(packet[4], 5); // Length = 5
|
||||
assert_eq!(packet[5], 0x01); // Mode marker
|
||||
assert_eq!(packet[6], 0x00); // Buttons
|
||||
assert_eq!(packet[7], 50); // X delta
|
||||
assert_eq!(packet[7], 50); // X delta
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1248,7 +1261,9 @@ mod tests {
|
||||
assert_eq!(checksum, 0x03);
|
||||
|
||||
// Known packet: Keyboard 'A' press
|
||||
let packet = [0x57u8, 0xAB, 0x00, 0x02, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00];
|
||||
let packet = [
|
||||
0x57u8, 0xAB, 0x00, 0x02, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
];
|
||||
let checksum = Ch9329Backend::calculate_checksum(&packet);
|
||||
assert_eq!(checksum, 0x10);
|
||||
}
|
||||
@@ -1258,11 +1273,11 @@ mod tests {
|
||||
// Valid GET_INFO response
|
||||
let response_bytes = [
|
||||
0x57, 0xAB, // Header
|
||||
0x00, // Address
|
||||
0x81, // Command (GET_INFO | 0x80 = success)
|
||||
0x08, // Length
|
||||
0x00, // Address
|
||||
0x81, // Command (GET_INFO | 0x80 = success)
|
||||
0x08, // Length
|
||||
0x31, 0x01, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, // Data
|
||||
0xE0, // Checksum (calculated)
|
||||
0xE0, // Checksum (calculated)
|
||||
];
|
||||
|
||||
// Note: checksum in test is just placeholder, parse will validate
|
||||
|
||||
@@ -210,27 +210,23 @@ pub fn encode_mouse_event(event: &MouseEvent) -> Vec<u8> {
|
||||
let y_bytes = (event.y as i16).to_le_bytes();
|
||||
|
||||
let extra = match event.event_type {
|
||||
MouseEventType::Down | MouseEventType::Up => {
|
||||
event.button.as_ref().map(|b| match b {
|
||||
MouseEventType::Down | MouseEventType::Up => event
|
||||
.button
|
||||
.as_ref()
|
||||
.map(|b| match b {
|
||||
MouseButton::Left => 0u8,
|
||||
MouseButton::Middle => 1u8,
|
||||
MouseButton::Right => 2u8,
|
||||
MouseButton::Back => 3u8,
|
||||
MouseButton::Forward => 4u8,
|
||||
}).unwrap_or(0)
|
||||
}
|
||||
})
|
||||
.unwrap_or(0),
|
||||
MouseEventType::Scroll => event.scroll as u8,
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
vec![
|
||||
MSG_MOUSE,
|
||||
event_type,
|
||||
x_bytes[0],
|
||||
x_bytes[1],
|
||||
y_bytes[0],
|
||||
y_bytes[1],
|
||||
extra,
|
||||
MSG_MOUSE, event_type, x_bytes[0], x_bytes[1], y_bytes[0], y_bytes[1], extra,
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -278,16 +278,16 @@ static JS_TO_USB_TABLE: [u8; 256] = {
|
||||
}
|
||||
|
||||
// Numbers 1-9, 0 (JS 49-57, 48 -> USB 0x1E-0x27)
|
||||
table[49] = usb::KEY_1; // 1
|
||||
table[50] = usb::KEY_2; // 2
|
||||
table[51] = usb::KEY_3; // 3
|
||||
table[52] = usb::KEY_4; // 4
|
||||
table[53] = usb::KEY_5; // 5
|
||||
table[54] = usb::KEY_6; // 6
|
||||
table[55] = usb::KEY_7; // 7
|
||||
table[56] = usb::KEY_8; // 8
|
||||
table[57] = usb::KEY_9; // 9
|
||||
table[48] = usb::KEY_0; // 0
|
||||
table[49] = usb::KEY_1; // 1
|
||||
table[50] = usb::KEY_2; // 2
|
||||
table[51] = usb::KEY_3; // 3
|
||||
table[52] = usb::KEY_4; // 4
|
||||
table[53] = usb::KEY_5; // 5
|
||||
table[54] = usb::KEY_6; // 6
|
||||
table[55] = usb::KEY_7; // 7
|
||||
table[56] = usb::KEY_8; // 8
|
||||
table[57] = usb::KEY_9; // 9
|
||||
table[48] = usb::KEY_0; // 0
|
||||
|
||||
// Function keys F1-F12 (JS 112-123 -> USB 0x3A-0x45)
|
||||
table[112] = usb::KEY_F1;
|
||||
@@ -304,25 +304,25 @@ static JS_TO_USB_TABLE: [u8; 256] = {
|
||||
table[123] = usb::KEY_F12;
|
||||
|
||||
// Control keys
|
||||
table[13] = usb::KEY_ENTER; // Enter
|
||||
table[27] = usb::KEY_ESCAPE; // Escape
|
||||
table[8] = usb::KEY_BACKSPACE; // Backspace
|
||||
table[9] = usb::KEY_TAB; // Tab
|
||||
table[32] = usb::KEY_SPACE; // Space
|
||||
table[20] = usb::KEY_CAPS_LOCK; // Caps Lock
|
||||
table[13] = usb::KEY_ENTER; // Enter
|
||||
table[27] = usb::KEY_ESCAPE; // Escape
|
||||
table[8] = usb::KEY_BACKSPACE; // Backspace
|
||||
table[9] = usb::KEY_TAB; // Tab
|
||||
table[32] = usb::KEY_SPACE; // Space
|
||||
table[20] = usb::KEY_CAPS_LOCK; // Caps Lock
|
||||
|
||||
// Punctuation (JS codes vary by browser/layout)
|
||||
table[189] = usb::KEY_MINUS; // -
|
||||
table[187] = usb::KEY_EQUAL; // =
|
||||
table[219] = usb::KEY_LEFT_BRACKET; // [
|
||||
table[189] = usb::KEY_MINUS; // -
|
||||
table[187] = usb::KEY_EQUAL; // =
|
||||
table[219] = usb::KEY_LEFT_BRACKET; // [
|
||||
table[221] = usb::KEY_RIGHT_BRACKET; // ]
|
||||
table[220] = usb::KEY_BACKSLASH; // \
|
||||
table[186] = usb::KEY_SEMICOLON; // ;
|
||||
table[222] = usb::KEY_APOSTROPHE; // '
|
||||
table[192] = usb::KEY_GRAVE; // `
|
||||
table[188] = usb::KEY_COMMA; // ,
|
||||
table[190] = usb::KEY_PERIOD; // .
|
||||
table[191] = usb::KEY_SLASH; // /
|
||||
table[220] = usb::KEY_BACKSLASH; // \
|
||||
table[186] = usb::KEY_SEMICOLON; // ;
|
||||
table[222] = usb::KEY_APOSTROPHE; // '
|
||||
table[192] = usb::KEY_GRAVE; // `
|
||||
table[188] = usb::KEY_COMMA; // ,
|
||||
table[190] = usb::KEY_PERIOD; // .
|
||||
table[191] = usb::KEY_SLASH; // /
|
||||
|
||||
// Navigation keys
|
||||
table[45] = usb::KEY_INSERT;
|
||||
@@ -359,14 +359,14 @@ static JS_TO_USB_TABLE: [u8; 256] = {
|
||||
// Special keys
|
||||
table[19] = usb::KEY_PAUSE;
|
||||
table[145] = usb::KEY_SCROLL_LOCK;
|
||||
table[93] = usb::KEY_APPLICATION; // Context menu
|
||||
table[93] = usb::KEY_APPLICATION; // Context menu
|
||||
|
||||
// Modifier keys
|
||||
table[17] = usb::KEY_LEFT_CTRL;
|
||||
table[16] = usb::KEY_LEFT_SHIFT;
|
||||
table[18] = usb::KEY_LEFT_ALT;
|
||||
table[91] = usb::KEY_LEFT_META; // Left Windows/Command
|
||||
table[92] = usb::KEY_RIGHT_META; // Right Windows/Command
|
||||
table[91] = usb::KEY_LEFT_META; // Left Windows/Command
|
||||
table[92] = usb::KEY_RIGHT_META; // Right Windows/Command
|
||||
|
||||
table
|
||||
};
|
||||
|
||||
106
src/hid/mod.rs
106
src/hid/mod.rs
@@ -102,8 +102,14 @@ impl HidController {
|
||||
info!("Creating OTG HID backend from device paths");
|
||||
Box::new(otg::OtgBackend::from_handles(handles)?)
|
||||
}
|
||||
HidBackendType::Ch9329 { ref port, baud_rate } => {
|
||||
info!("Initializing CH9329 HID backend on {} @ {} baud", port, baud_rate);
|
||||
HidBackendType::Ch9329 {
|
||||
ref port,
|
||||
baud_rate,
|
||||
} => {
|
||||
info!(
|
||||
"Initializing CH9329 HID backend on {} @ {} baud",
|
||||
port, baud_rate
|
||||
);
|
||||
Box::new(ch9329::Ch9329Backend::with_baud_rate(port, baud_rate)?)
|
||||
}
|
||||
HidBackendType::None => {
|
||||
@@ -157,16 +163,25 @@ impl HidController {
|
||||
// Report error to monitor, but skip temporary EAGAIN retries
|
||||
// - "eagain_retry": within threshold, just temporary busy
|
||||
// - "eagain": exceeded threshold, report as error
|
||||
if let AppError::HidError { ref backend, ref reason, ref error_code } = e {
|
||||
if let AppError::HidError {
|
||||
ref backend,
|
||||
ref reason,
|
||||
ref error_code,
|
||||
} = e
|
||||
{
|
||||
if error_code != "eagain_retry" {
|
||||
self.monitor.report_error(backend, None, reason, error_code).await;
|
||||
self.monitor
|
||||
.report_error(backend, None, reason, error_code)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
None => Err(AppError::BadRequest("HID backend not available".to_string())),
|
||||
None => Err(AppError::BadRequest(
|
||||
"HID backend not available".to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,16 +203,25 @@ impl HidController {
|
||||
// Report error to monitor, but skip temporary EAGAIN retries
|
||||
// - "eagain_retry": within threshold, just temporary busy
|
||||
// - "eagain": exceeded threshold, report as error
|
||||
if let AppError::HidError { ref backend, ref reason, ref error_code } = e {
|
||||
if let AppError::HidError {
|
||||
ref backend,
|
||||
ref reason,
|
||||
ref error_code,
|
||||
} = e
|
||||
{
|
||||
if error_code != "eagain_retry" {
|
||||
self.monitor.report_error(backend, None, reason, error_code).await;
|
||||
self.monitor
|
||||
.report_error(backend, None, reason, error_code)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
None => Err(AppError::BadRequest("HID backend not available".to_string())),
|
||||
None => Err(AppError::BadRequest(
|
||||
"HID backend not available".to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,26 +229,33 @@ impl HidController {
|
||||
pub async fn send_consumer(&self, event: ConsumerEvent) -> Result<()> {
|
||||
let backend = self.backend.read().await;
|
||||
match backend.as_ref() {
|
||||
Some(b) => {
|
||||
match b.send_consumer(event).await {
|
||||
Ok(_) => {
|
||||
if self.monitor.is_error().await {
|
||||
let backend_type = self.backend_type.read().await;
|
||||
self.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" {
|
||||
self.monitor.report_error(backend, None, reason, error_code).await;
|
||||
}
|
||||
}
|
||||
Err(e)
|
||||
Some(b) => match b.send_consumer(event).await {
|
||||
Ok(_) => {
|
||||
if self.monitor.is_error().await {
|
||||
let backend_type = self.backend_type.read().await;
|
||||
self.monitor.report_recovered(backend_type.name_str()).await;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
None => Err(AppError::BadRequest("HID backend not available".to_string())),
|
||||
Err(e) => {
|
||||
if let AppError::HidError {
|
||||
ref backend,
|
||||
ref reason,
|
||||
ref error_code,
|
||||
} = e
|
||||
{
|
||||
if error_code != "eagain_retry" {
|
||||
self.monitor
|
||||
.report_error(backend, None, reason, error_code)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
Err(e)
|
||||
}
|
||||
},
|
||||
None => Err(AppError::BadRequest(
|
||||
"HID backend not available".to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -269,9 +300,9 @@ impl HidController {
|
||||
|
||||
// Include error information from monitor
|
||||
let (error, error_code) = match self.monitor.status().await {
|
||||
HidHealthStatus::Error { reason, error_code, .. } => {
|
||||
(Some(reason), Some(error_code))
|
||||
}
|
||||
HidHealthStatus::Error {
|
||||
reason, error_code, ..
|
||||
} => (Some(reason), Some(error_code)),
|
||||
_ => (None, None),
|
||||
};
|
||||
|
||||
@@ -320,7 +351,7 @@ impl HidController {
|
||||
None => {
|
||||
warn!("OTG backend requires OtgService, but it's not available");
|
||||
return Err(AppError::Config(
|
||||
"OTG backend not available (OtgService missing)".to_string()
|
||||
"OTG backend not available (OtgService missing)".to_string(),
|
||||
));
|
||||
}
|
||||
};
|
||||
@@ -341,7 +372,10 @@ impl HidController {
|
||||
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);
|
||||
warn!(
|
||||
"Failed to cleanup HID after init failure: {}",
|
||||
e2
|
||||
);
|
||||
}
|
||||
None
|
||||
}
|
||||
@@ -363,8 +397,14 @@ impl HidController {
|
||||
}
|
||||
}
|
||||
}
|
||||
HidBackendType::Ch9329 { ref port, baud_rate } => {
|
||||
info!("Initializing CH9329 HID backend on {} @ {} baud", port, baud_rate);
|
||||
HidBackendType::Ch9329 {
|
||||
ref port,
|
||||
baud_rate,
|
||||
} => {
|
||||
info!(
|
||||
"Initializing CH9329 HID backend on {} @ {} baud",
|
||||
port, baud_rate
|
||||
);
|
||||
match ch9329::Ch9329Backend::with_baud_rate(port, baud_rate) {
|
||||
Ok(b) => {
|
||||
let boxed = Box::new(b);
|
||||
|
||||
@@ -144,7 +144,8 @@ impl HidHealthMonitor {
|
||||
// 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;
|
||||
let in_cooldown =
|
||||
last_recovery > 0 && current_ms < last_recovery + self.config.recovery_cooldown_ms;
|
||||
|
||||
// Check if error code changed
|
||||
let error_changed = {
|
||||
@@ -229,10 +230,7 @@ impl HidHealthMonitor {
|
||||
// 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
|
||||
);
|
||||
debug!("HID {} recovered after {} retries", backend, retry_count);
|
||||
|
||||
// Publish recovery event
|
||||
if let Some(ref events) = *self.events.read().await {
|
||||
@@ -372,9 +370,7 @@ mod tests {
|
||||
let monitor = HidHealthMonitor::with_defaults();
|
||||
|
||||
for i in 1..=5 {
|
||||
monitor
|
||||
.report_error("otg", None, "Error", "io_error")
|
||||
.await;
|
||||
monitor.report_error("otg", None, "Error", "io_error").await;
|
||||
assert_eq!(monitor.retry_count(), i);
|
||||
}
|
||||
}
|
||||
@@ -387,9 +383,7 @@ mod tests {
|
||||
});
|
||||
|
||||
for _ in 0..100 {
|
||||
monitor
|
||||
.report_error("otg", None, "Error", "io_error")
|
||||
.await;
|
||||
monitor.report_error("otg", None, "Error", "io_error").await;
|
||||
assert!(monitor.should_retry());
|
||||
}
|
||||
}
|
||||
@@ -417,9 +411,7 @@ mod tests {
|
||||
async fn test_reset() {
|
||||
let monitor = HidHealthMonitor::with_defaults();
|
||||
|
||||
monitor
|
||||
.report_error("otg", None, "Error", "io_error")
|
||||
.await;
|
||||
monitor.report_error("otg", None, "Error", "io_error").await;
|
||||
assert!(monitor.is_error().await);
|
||||
|
||||
monitor.reset().await;
|
||||
|
||||
142
src/hid/otg.rs
142
src/hid/otg.rs
@@ -30,9 +30,11 @@ use tracing::{debug, info, trace, warn};
|
||||
|
||||
use super::backend::HidBackend;
|
||||
use super::keymap;
|
||||
use super::types::{ConsumerEvent, KeyEventType, KeyboardEvent, KeyboardReport, MouseEvent, MouseEventType};
|
||||
use super::types::{
|
||||
ConsumerEvent, KeyEventType, KeyboardEvent, KeyboardReport, MouseEvent, MouseEventType,
|
||||
};
|
||||
use crate::error::{AppError, Result};
|
||||
use crate::otg::{HidDevicePaths, wait_for_hid_devices};
|
||||
use crate::otg::{wait_for_hid_devices, HidDevicePaths};
|
||||
|
||||
/// Device type for ensure_device operations
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
@@ -73,11 +75,21 @@ impl LedState {
|
||||
/// Convert to raw byte
|
||||
pub fn to_byte(&self) -> u8 {
|
||||
let mut b = 0u8;
|
||||
if self.num_lock { b |= 0x01; }
|
||||
if self.caps_lock { b |= 0x02; }
|
||||
if self.scroll_lock { b |= 0x04; }
|
||||
if self.compose { b |= 0x08; }
|
||||
if self.kana { b |= 0x10; }
|
||||
if self.num_lock {
|
||||
b |= 0x01;
|
||||
}
|
||||
if self.caps_lock {
|
||||
b |= 0x02;
|
||||
}
|
||||
if self.scroll_lock {
|
||||
b |= 0x04;
|
||||
}
|
||||
if self.compose {
|
||||
b |= 0x08;
|
||||
}
|
||||
if self.kana {
|
||||
b |= 0x10;
|
||||
}
|
||||
b
|
||||
}
|
||||
}
|
||||
@@ -145,7 +157,9 @@ impl OtgBackend {
|
||||
keyboard_path: paths.keyboard,
|
||||
mouse_rel_path: paths.mouse_relative,
|
||||
mouse_abs_path: paths.mouse_absolute,
|
||||
consumer_path: paths.consumer.unwrap_or_else(|| PathBuf::from("/dev/hidg3")),
|
||||
consumer_path: paths
|
||||
.consumer
|
||||
.unwrap_or_else(|| PathBuf::from("/dev/hidg3")),
|
||||
keyboard_dev: Mutex::new(None),
|
||||
mouse_rel_dev: Mutex::new(None),
|
||||
mouse_abs_dev: Mutex::new(None),
|
||||
@@ -198,7 +212,8 @@ impl OtgBackend {
|
||||
Ok(1) => {
|
||||
// Device ready, check for errors
|
||||
if let Some(revents) = pollfd[0].revents() {
|
||||
if revents.contains(PollFlags::POLLERR) || revents.contains(PollFlags::POLLHUP) {
|
||||
if revents.contains(PollFlags::POLLERR) || revents.contains(PollFlags::POLLHUP)
|
||||
{
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::BrokenPipe,
|
||||
"Device error or hangup",
|
||||
@@ -297,7 +312,10 @@ impl OtgBackend {
|
||||
// Close the device if open (device was removed)
|
||||
let mut dev = dev_mutex.lock();
|
||||
if dev.is_some() {
|
||||
debug!("Device path {} no longer exists, closing handle", path.display());
|
||||
debug!(
|
||||
"Device path {} no longer exists, closing handle",
|
||||
path.display()
|
||||
);
|
||||
*dev = None;
|
||||
}
|
||||
self.online.store(false, Ordering::Relaxed);
|
||||
@@ -335,20 +353,24 @@ impl OtgBackend {
|
||||
.custom_flags(libc::O_NONBLOCK)
|
||||
.open(path)
|
||||
.map_err(|e| {
|
||||
AppError::Internal(format!("Failed to open HID device {}: {}", path.display(), e))
|
||||
AppError::Internal(format!(
|
||||
"Failed to open HID device {}: {}",
|
||||
path.display(),
|
||||
e
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
/// 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() {
|
||||
Some(32) => "epipe", // EPIPE - broken pipe
|
||||
Some(108) => "eshutdown", // ESHUTDOWN - transport endpoint shutdown
|
||||
Some(11) => "eagain", // EAGAIN - resource temporarily unavailable
|
||||
Some(6) => "enxio", // ENXIO - no such device or address
|
||||
Some(19) => "enodev", // ENODEV - no such device
|
||||
Some(5) => "eio", // EIO - I/O error
|
||||
Some(2) => "enoent", // ENOENT - no such file or directory
|
||||
Some(32) => "epipe", // EPIPE - broken pipe
|
||||
Some(108) => "eshutdown", // ESHUTDOWN - transport endpoint shutdown
|
||||
Some(11) => "eagain", // EAGAIN - resource temporarily unavailable
|
||||
Some(6) => "enxio", // ENXIO - no such device or address
|
||||
Some(19) => "enodev", // ENODEV - no such device
|
||||
Some(5) => "eio", // EIO - I/O error
|
||||
Some(2) => "enoent", // ENOENT - no such file or directory
|
||||
_ => "io_error",
|
||||
};
|
||||
|
||||
@@ -361,9 +383,7 @@ impl OtgBackend {
|
||||
|
||||
/// Check if all HID device files exist
|
||||
pub fn check_devices_exist(&self) -> bool {
|
||||
self.keyboard_path.exists()
|
||||
&& self.mouse_rel_path.exists()
|
||||
&& self.mouse_abs_path.exists()
|
||||
self.keyboard_path.exists() && self.mouse_rel_path.exists() && self.mouse_abs_path.exists()
|
||||
}
|
||||
|
||||
/// Get list of missing device paths
|
||||
@@ -415,7 +435,10 @@ impl OtgBackend {
|
||||
self.eagain_count.store(0, Ordering::Relaxed);
|
||||
debug!("Keyboard ESHUTDOWN, closing for recovery");
|
||||
*dev = None;
|
||||
Err(Self::io_error_to_hid_error(e, "Failed to write keyboard report"))
|
||||
Err(Self::io_error_to_hid_error(
|
||||
e,
|
||||
"Failed to write keyboard report",
|
||||
))
|
||||
}
|
||||
Some(11) => {
|
||||
// EAGAIN after poll - should be rare, silently drop
|
||||
@@ -426,7 +449,10 @@ impl OtgBackend {
|
||||
self.online.store(false, Ordering::Relaxed);
|
||||
self.eagain_count.store(0, Ordering::Relaxed);
|
||||
warn!("Keyboard write error: {}", e);
|
||||
Err(Self::io_error_to_hid_error(e, "Failed to write keyboard report"))
|
||||
Err(Self::io_error_to_hid_error(
|
||||
e,
|
||||
"Failed to write keyboard report",
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -472,7 +498,10 @@ impl OtgBackend {
|
||||
self.eagain_count.store(0, Ordering::Relaxed);
|
||||
debug!("Relative mouse ESHUTDOWN, closing for recovery");
|
||||
*dev = None;
|
||||
Err(Self::io_error_to_hid_error(e, "Failed to write mouse report"))
|
||||
Err(Self::io_error_to_hid_error(
|
||||
e,
|
||||
"Failed to write mouse report",
|
||||
))
|
||||
}
|
||||
Some(11) => {
|
||||
// EAGAIN after poll - should be rare, silently drop
|
||||
@@ -482,7 +511,10 @@ impl OtgBackend {
|
||||
self.online.store(false, Ordering::Relaxed);
|
||||
self.eagain_count.store(0, Ordering::Relaxed);
|
||||
warn!("Relative mouse write error: {}", e);
|
||||
Err(Self::io_error_to_hid_error(e, "Failed to write mouse report"))
|
||||
Err(Self::io_error_to_hid_error(
|
||||
e,
|
||||
"Failed to write mouse report",
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -534,7 +566,10 @@ impl OtgBackend {
|
||||
self.eagain_count.store(0, Ordering::Relaxed);
|
||||
debug!("Absolute mouse ESHUTDOWN, closing for recovery");
|
||||
*dev = None;
|
||||
Err(Self::io_error_to_hid_error(e, "Failed to write mouse report"))
|
||||
Err(Self::io_error_to_hid_error(
|
||||
e,
|
||||
"Failed to write mouse report",
|
||||
))
|
||||
}
|
||||
Some(11) => {
|
||||
// EAGAIN after poll - should be rare, silently drop
|
||||
@@ -544,7 +579,10 @@ impl OtgBackend {
|
||||
self.online.store(false, Ordering::Relaxed);
|
||||
self.eagain_count.store(0, Ordering::Relaxed);
|
||||
warn!("Absolute mouse write error: {}", e);
|
||||
Err(Self::io_error_to_hid_error(e, "Failed to write mouse report"))
|
||||
Err(Self::io_error_to_hid_error(
|
||||
e,
|
||||
"Failed to write mouse report",
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -590,7 +628,10 @@ impl OtgBackend {
|
||||
self.online.store(false, Ordering::Relaxed);
|
||||
debug!("Consumer control ESHUTDOWN, closing for recovery");
|
||||
*dev = None;
|
||||
Err(Self::io_error_to_hid_error(e, "Failed to write consumer report"))
|
||||
Err(Self::io_error_to_hid_error(
|
||||
e,
|
||||
"Failed to write consumer report",
|
||||
))
|
||||
}
|
||||
Some(11) => {
|
||||
// EAGAIN after poll - silently drop
|
||||
@@ -599,7 +640,10 @@ impl OtgBackend {
|
||||
_ => {
|
||||
self.online.store(false, Ordering::Relaxed);
|
||||
warn!("Consumer control write error: {}", e);
|
||||
Err(Self::io_error_to_hid_error(e, "Failed to write consumer report"))
|
||||
Err(Self::io_error_to_hid_error(
|
||||
e,
|
||||
"Failed to write consumer report",
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -632,7 +676,10 @@ impl OtgBackend {
|
||||
}
|
||||
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))),
|
||||
Err(e) => Err(AppError::Internal(format!(
|
||||
"Failed to read LED state: {}",
|
||||
e
|
||||
))),
|
||||
}
|
||||
} else {
|
||||
Ok(None)
|
||||
@@ -677,34 +724,55 @@ impl HidBackend for OtgBackend {
|
||||
*self.keyboard_dev.lock() = Some(file);
|
||||
info!("Keyboard device opened: {}", self.keyboard_path.display());
|
||||
} else {
|
||||
warn!("Keyboard device not found: {}", self.keyboard_path.display());
|
||||
warn!(
|
||||
"Keyboard device not found: {}",
|
||||
self.keyboard_path.display()
|
||||
);
|
||||
}
|
||||
|
||||
// Open relative mouse device
|
||||
if self.mouse_rel_path.exists() {
|
||||
let file = Self::open_device(&self.mouse_rel_path)?;
|
||||
*self.mouse_rel_dev.lock() = Some(file);
|
||||
info!("Relative mouse device opened: {}", self.mouse_rel_path.display());
|
||||
info!(
|
||||
"Relative mouse device opened: {}",
|
||||
self.mouse_rel_path.display()
|
||||
);
|
||||
} else {
|
||||
warn!("Relative mouse device not found: {}", self.mouse_rel_path.display());
|
||||
warn!(
|
||||
"Relative mouse device not found: {}",
|
||||
self.mouse_rel_path.display()
|
||||
);
|
||||
}
|
||||
|
||||
// Open absolute mouse device
|
||||
if self.mouse_abs_path.exists() {
|
||||
let file = Self::open_device(&self.mouse_abs_path)?;
|
||||
*self.mouse_abs_dev.lock() = Some(file);
|
||||
info!("Absolute mouse device opened: {}", self.mouse_abs_path.display());
|
||||
info!(
|
||||
"Absolute mouse device opened: {}",
|
||||
self.mouse_abs_path.display()
|
||||
);
|
||||
} else {
|
||||
warn!("Absolute mouse device not found: {}", self.mouse_abs_path.display());
|
||||
warn!(
|
||||
"Absolute mouse device not found: {}",
|
||||
self.mouse_abs_path.display()
|
||||
);
|
||||
}
|
||||
|
||||
// Open consumer control device (optional, may not exist on older setups)
|
||||
if self.consumer_path.exists() {
|
||||
let file = Self::open_device(&self.consumer_path)?;
|
||||
*self.consumer_dev.lock() = Some(file);
|
||||
info!("Consumer control device opened: {}", self.consumer_path.display());
|
||||
info!(
|
||||
"Consumer control device opened: {}",
|
||||
self.consumer_path.display()
|
||||
);
|
||||
} else {
|
||||
debug!("Consumer control device not found: {}", self.consumer_path.display());
|
||||
debug!(
|
||||
"Consumer control device not found: {}",
|
||||
self.consumer_path.display()
|
||||
);
|
||||
}
|
||||
|
||||
// Mark as online if all devices opened successfully
|
||||
|
||||
@@ -341,12 +341,7 @@ pub struct MouseReport {
|
||||
impl MouseReport {
|
||||
/// Convert to bytes for USB HID (relative mouse)
|
||||
pub fn to_bytes_relative(&self) -> [u8; 4] {
|
||||
[
|
||||
self.buttons,
|
||||
self.x as u8,
|
||||
self.y as u8,
|
||||
self.wheel as u8,
|
||||
]
|
||||
[self.buttons, self.x as u8, self.y as u8, self.wheel as u8]
|
||||
}
|
||||
|
||||
/// Convert to bytes for USB HID (absolute mouse)
|
||||
|
||||
@@ -50,7 +50,11 @@ async fn handle_hid_socket(socket: WebSocket, state: Arc<AppState>) {
|
||||
vec![RESP_ERR_HID_UNAVAILABLE]
|
||||
};
|
||||
|
||||
if sender.send(Message::Binary(initial_response.into())).await.is_err() {
|
||||
if sender
|
||||
.send(Message::Binary(initial_response.into()))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
error!("Failed to send initial HID status");
|
||||
return;
|
||||
}
|
||||
@@ -66,7 +70,9 @@ async fn handle_hid_socket(socket: WebSocket, state: Arc<AppState>) {
|
||||
warn!("HID controller not available, ignoring message");
|
||||
}
|
||||
// Send error response (optional, for client awareness)
|
||||
let _ = sender.send(Message::Binary(vec![RESP_ERR_HID_UNAVAILABLE].into())).await;
|
||||
let _ = sender
|
||||
.send(Message::Binary(vec![RESP_ERR_HID_UNAVAILABLE].into()))
|
||||
.await;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -81,9 +87,14 @@ async fn handle_hid_socket(socket: WebSocket, state: Arc<AppState>) {
|
||||
Ok(Message::Text(text)) => {
|
||||
// Text messages are no longer supported
|
||||
if log_throttler.should_log("text_message_rejected") {
|
||||
debug!("Received text message (not supported): {} bytes", text.len());
|
||||
debug!(
|
||||
"Received text message (not supported): {} bytes",
|
||||
text.len()
|
||||
);
|
||||
}
|
||||
let _ = sender.send(Message::Binary(vec![RESP_ERR_INVALID_MESSAGE].into())).await;
|
||||
let _ = sender
|
||||
.send(Message::Binary(vec![RESP_ERR_INVALID_MESSAGE].into()))
|
||||
.await;
|
||||
}
|
||||
Ok(Message::Ping(data)) => {
|
||||
let _ = sender.send(Message::Pong(data)).await;
|
||||
@@ -142,7 +153,7 @@ async fn handle_binary_message(data: &[u8], state: &AppState) -> Result<(), Stri
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::hid::datachannel::{MSG_KEYBOARD, MSG_MOUSE, KB_EVENT_DOWN, MS_EVENT_MOVE};
|
||||
use crate::hid::datachannel::{KB_EVENT_DOWN, MSG_KEYBOARD, MSG_MOUSE, MS_EVENT_MOVE};
|
||||
|
||||
#[test]
|
||||
fn test_response_codes() {
|
||||
|
||||
Reference in New Issue
Block a user