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:
mofeng-git
2026-01-11 10:41:57 +08:00
parent 9feb74b72c
commit 206594e292
110 changed files with 3955 additions and 2251 deletions

View File

@@ -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

View File

@@ -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,
]
}

View File

@@ -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
};

View File

@@ -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);

View File

@@ -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;

View File

@@ -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

View File

@@ -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)

View File

@@ -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() {