Files
One-KVM/docs/modules/hid.md
mofeng-git 0c82d1a840 feat(rustdesk): 完整实现RustDesk协议和P2P连接
重大变更:
- 从prost切换到protobuf 3.4实现完整的RustDesk协议栈
- 新增P2P打洞模块(punch.rs)支持直连和中继回退
- 重构加密系统:临时Curve25519密钥对+Ed25519签名
- 完善HID适配器:支持CapsLock状态同步和修饰键映射
- 添加音频流支持:Opus编码+音频帧适配器
- 优化视频流:改进帧适配器和编码器协商
- 移除pacer.rs简化视频管道

扩展系统:
- 在设置向导中添加扩展步骤(ttyd/rustdesk切换)
- 扩展可用性检测和自动启动
- 新增WebConfig handler用于Web服务器配置

前端改进:
- SetupView增加第4步扩展配置
- 音频设备列表和配置界面
- 新增多语言支持(en-US/zh-CN)
- TypeScript类型生成更新

文档:
- 更新系统架构文档
- 完善config/hid/rustdesk/video/webrtc模块文档
2026-01-03 19:34:07 +08:00

31 KiB
Raw Permalink Blame History

HID 模块文档

1. 模块概述

HID (Human Interface Device) 模块负责将键盘和鼠标事件转发到目标计算机,是 One-KVM 实现远程控制的核心模块。

1.1 主要功能

  • 键盘事件处理 (按键、修饰键)
  • 鼠标事件处理 (移动、点击、滚轮)
  • 多媒体键支持 (Consumer Control)
  • 支持绝对和相对鼠标模式
  • 多后端支持 (OTG、CH9329)
  • WebSocket 和 DataChannel 输入
  • 自动错误恢复和健康监控

1.2 USB Endpoint 使用

OTG 模式下的 endpoint 分配:

功能 IN 端点 OUT 端点 说明
Keyboard 1 1 带 LED 反馈
MouseRelative 1 0 相对鼠标
MouseAbsolute 1 0 绝对鼠标
ConsumerControl 1 0 多媒体键
HID 总计 4 1
MSD 1 1 大容量存储
全部总计 5 2 兼容 6 endpoint 设备

EP0 (控制端点) 独立于数据端点,不计入上述统计。

1.3 文件结构

src/hid/
├── mod.rs              # HidController 主控制器
├── backend.rs          # HidBackend trait 和 HidBackendType
├── otg.rs              # OTG USB Gadget 后端实现
├── ch9329.rs           # CH9329 串口 HID 控制器后端
├── consumer.rs         # Consumer Control usage codes
├── keymap.rs           # JS keyCode 到 USB HID 的转换表
├── types.rs            # 事件类型定义 (KeyboardEvent, MouseEvent等)
├── monitor.rs          # 健康监视器 (HidHealthMonitor)
├── datachannel.rs      # DataChannel 二进制协议解析
└── websocket.rs        # WebSocket 二进制协议适配

2. 架构设计

2.1 整体架构

┌─────────────────────────────────────────────────────────────────────────────┐
│                          HID Architecture                                    │
└─────────────────────────────────────────────────────────────────────────────┘

           Browser Input Events
                   │
         ┌─────────┴─────────┐
         │                   │
         ▼                   ▼
┌─────────────────┐  ┌─────────────────┐
│   WebSocket     │  │  DataChannel    │
│   Handler       │  │   Handler       │
│ (websocket.rs)  │  │(datachannel.rs) │
└────────┬────────┘  └────────┬────────┘
         │                    │
         └──────────┬─────────┘
                    │
                    ▼
         ┌─────────────────────┐
         │   HidController     │
         │     (mod.rs)        │
         │  - send_keyboard()  │
         │  - send_mouse()     │
         │  - send_consumer()  │
         │  - monitor (health) │
         └──────────┬──────────┘
                    │
         ┌──────────┼──────────┐
         │          │          │
         ▼          ▼          ▼
┌─────────────┐ ┌──────────┐ ┌──────────┐
│  OTG Backend│ │  CH9329  │ │   None   │
│  (otg.rs)   │ │ Backend  │ │ (dummy)  │
└──────┬──────┘ └────┬─────┘ └──────────┘
       │             │
       ▼             ▼
┌─────────────┐ ┌──────────┐
│ /dev/hidg*  │ │ Serial   │
│ USB Gadget  │ │ Port     │
└─────────────┘ └──────────┘
       │             │
       └──────┬──────┘
              │
              ▼
      ┌─────────────┐
      │  Target PC  │
      └─────────────┘

2.2 后端选择

┌─────────────────────────────────────────────────────────────────────────────┐
│                        Backend Selection                                     │
└─────────────────────────────────────────────────────────────────────────────┘

HidBackendType::Otg
    │
    ├── 检查 OtgService 是否可用
    │
    ├── 请求 HID 函数 (4个设备, 共4个IN端点, 1个OUT端点)
    │   ├── /dev/hidg0 (键盘, 1 IN, 1 OUT for LED)
    │   ├── /dev/hidg1 (相对鼠标, 1 IN)
    │   ├── /dev/hidg2 (绝对鼠标, 1 IN)
    │   └── /dev/hidg3 (Consumer Control, 1 IN)
    │
    └── 创建 OtgBackend (从 HidDevicePaths)

HidBackendType::Ch9329 { port, baud_rate }
    │
    ├── 打开串口设备
    │
    ├── 初始化 CH9329 芯片
    │
    └── 创建 Ch9329Backend

HidBackendType::None
    │
    └── 不创建后端 (HID 功能禁用)

3. 核心组件

3.1 HidController (mod.rs)

HID 控制器主类,统一管理所有 HID 操作。

pub struct HidController {
    /// OTG Service reference (only used when backend is OTG)
    otg_service: Option<Arc<OtgService>>,

    /// Active backend
    backend: Arc<RwLock<Option<Box<dyn HidBackend>>>>,

    /// Backend type (mutable for reload)
    backend_type: RwLock<HidBackendType>,

    /// Event bus for broadcasting state changes (optional)
    events: RwLock<Option<Arc<EventBus>>>,

    /// Health monitor for error tracking and recovery
    monitor: Arc<HidHealthMonitor>,
}

impl HidController {
    /// Create a new HID controller with specified backend
    pub fn new(backend_type: HidBackendType, otg_service: Option<Arc<OtgService>>) -> Self;

    /// Set event bus for broadcasting state changes
    pub async fn set_event_bus(&self, events: Arc<EventBus>);

    /// Initialize the HID backend
    pub async fn init(&self) -> Result<()>;

    /// Shutdown the HID backend and release resources
    pub async fn shutdown(&self) -> Result<()>;

    /// Send keyboard event
    pub async fn send_keyboard(&self, event: KeyboardEvent) -> Result<()>;

    /// Send mouse event
    pub async fn send_mouse(&self, event: MouseEvent) -> Result<()>;

    /// Send consumer control event (multimedia keys)
    pub async fn send_consumer(&self, event: ConsumerEvent) -> Result<()>;

    /// Reset all keys (release all pressed keys)
    pub async fn reset(&self) -> Result<()>;

    /// Check if backend is available
    pub async fn is_available(&self) -> bool;

    /// Get backend type
    pub async fn backend_type(&self) -> HidBackendType;

    /// Get backend info
    pub async fn info(&self) -> Option<HidInfo>;

    /// Get current state as SystemEvent
    pub async fn current_state_event(&self) -> SystemEvent;

    /// Get the health monitor reference
    pub fn monitor(&self) -> &Arc<HidHealthMonitor>;

    /// Get current health status
    pub async fn health_status(&self) -> HidHealthStatus;

    /// Check if the HID backend is healthy
    pub async fn is_healthy(&self) -> bool;

    /// Reload the HID backend with new type
    pub async fn reload(&self, new_backend_type: HidBackendType) -> Result<()>;
}

pub struct HidInfo {
    /// Backend name
    pub name: &'static str,
    /// Whether backend is initialized
    pub initialized: bool,
    /// Whether absolute mouse positioning is supported
    pub supports_absolute_mouse: bool,
    /// Screen resolution for absolute mouse
    pub screen_resolution: Option<(u32, u32)>,
}

3.2 HidBackend Trait (backend.rs)

#[async_trait]
pub trait HidBackend: Send + Sync {
    /// Get backend name
    fn name(&self) -> &'static str;

    /// Initialize the backend
    async fn init(&self) -> Result<()>;

    /// Send a keyboard event
    async fn send_keyboard(&self, event: KeyboardEvent) -> Result<()>;

    /// Send a mouse event
    async fn send_mouse(&self, event: MouseEvent) -> Result<()>;

    /// Send a consumer control event (multimedia keys)
    async fn send_consumer(&self, event: ConsumerEvent) -> Result<()>;

    /// Reset all inputs (release all keys/buttons)
    async fn reset(&self) -> Result<()>;

    /// Shutdown the backend
    async fn shutdown(&self) -> Result<()>;

    /// Check if backend supports absolute mouse positioning
    fn supports_absolute_mouse(&self) -> bool;

    /// Get screen resolution (for absolute mouse)
    fn screen_resolution(&self) -> Option<(u32, u32)>;

    /// Set screen resolution (for absolute mouse)
    fn set_screen_resolution(&mut self, width: u32, height: u32);
}

#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum HidBackendType {
    /// USB OTG gadget mode
    Otg,

    /// CH9329 serial HID controller
    Ch9329 {
        port: String,
        baud_rate: u32,
    },

    /// No HID backend (disabled)
    None,
}

impl HidBackendType {
    /// Check if OTG backend is available on this system
    pub fn otg_available() -> bool;

    /// Detect the best available backend
    pub fn detect() -> Self;

    /// Get backend name as string
    pub fn name_str(&self) -> &str;
}

3.3 OTG 后端 (otg.rs)

通过 Linux USB OTG gadget 模拟 HID 设备。

pub struct OtgBackend {
    /// Keyboard device path (/dev/hidg0)
    keyboard_path: PathBuf,
    /// Relative mouse device path (/dev/hidg1)
    mouse_rel_path: PathBuf,
    /// Absolute mouse device path (/dev/hidg2)
    mouse_abs_path: PathBuf,
    /// Consumer control device path (/dev/hidg3)
    consumer_path: PathBuf,

    /// Keyboard device file
    keyboard_dev: Mutex<Option<File>>,
    /// Relative mouse device file
    mouse_rel_dev: Mutex<Option<File>>,
    /// Absolute mouse device file
    mouse_abs_dev: Mutex<Option<File>>,
    /// Consumer control device file
    consumer_dev: Mutex<Option<File>>,

    /// Current keyboard state
    keyboard_state: Mutex<KeyboardReport>,
    /// Current mouse button state
    mouse_buttons: AtomicU8,

    /// Last known LED state
    led_state: RwLock<LedState>,
    /// Screen resolution for absolute mouse
    screen_resolution: RwLock<Option<(u32, u32)>>,

    /// UDC name for state checking
    udc_name: RwLock<Option<String>>,
    /// Whether the device is currently online
    online: AtomicBool,

    /// Error tracking (for log throttling)
    last_error_log: Mutex<Instant>,
    error_count: AtomicU8,
    eagain_count: AtomicU8,
}

impl OtgBackend {
    /// Create OTG backend from device paths provided by OtgService
    pub fn from_handles(paths: HidDevicePaths) -> Result<Self>;

    /// Set the UDC name for state checking
    pub fn set_udc_name(&self, udc: &str);

    /// Check if the UDC is in "configured" state
    pub fn is_udc_configured(&self) -> bool;

    /// Check if device is online
    pub fn is_online(&self) -> bool;

    /// Read keyboard LED state (non-blocking)
    pub fn read_led_state(&self) -> Result<Option<LedState>>;

    /// Get last known LED state
    pub fn led_state(&self) -> LedState;
}

/// Keyboard LED state
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct LedState {
    pub num_lock: bool,
    pub caps_lock: bool,
    pub scroll_lock: bool,
    pub compose: bool,
    pub kana: bool,
}

impl LedState {
    /// Create from raw byte
    pub fn from_byte(b: u8) -> Self;

    /// Convert to raw byte
    pub fn to_byte(&self) -> u8;
}

HID 报告格式

/// 键盘报告 (8 字节)
#[derive(Debug, Clone, Default)]
pub struct KeyboardReport {
    pub modifiers: u8,    // Ctrl, Shift, Alt, Meta
    pub reserved: u8,     // 保留
    pub keys: [u8; 6],    // 最多 6 个按键 USB HID code
}

impl KeyboardReport {
    /// Convert to bytes for USB HID
    pub fn to_bytes(&self) -> [u8; 8];

    /// Add a key to the report
    pub fn add_key(&mut self, key: u8) -> bool;

    /// Remove a key from the report
    pub fn remove_key(&mut self, key: u8);

    /// Clear all keys
    pub fn clear(&mut self);
}

/// 鼠标报告 (相对模式 4 字节, 绝对模式 6 字节)
#[derive(Debug, Clone, Default)]
pub struct MouseReport {
    pub buttons: u8,      // 按钮状态
    pub x: i8,            // X 移动 (-127 ~ 127) for relative
    pub y: i8,            // Y 移动 (-127 ~ 127) for relative
    pub wheel: i8,        // 滚轮 (-127 ~ 127)
}

impl MouseReport {
    /// Convert to bytes for USB HID (relative mouse)
    pub fn to_bytes_relative(&self) -> [u8; 4];

    /// Convert to bytes for USB HID (absolute mouse)
    pub fn to_bytes_absolute(&self, x: u16, y: u16) -> [u8; 6];
}

错误恢复机制

OTG 后端实现了基于 PiKVM 和 JetKVM 的自动错误恢复:

  • EAGAIN (errno 11): 资源暂时不可用 - 使用 poll() 等待设备可写,超时后静默丢弃
  • ESHUTDOWN (errno 108): 传输端点关闭 - 关闭设备句柄,下次操作时自动重新打开
  • Write Timeout: 使用 500ms 超时 (HID_WRITE_TIMEOUT_MS),超时后静默丢弃数据
  • 日志限流: 防止大量错误日志泛滥

3.4 CH9329 后端 (ch9329.rs)

通过 CH9329 芯片(串口转 HID实现 HID 功能。

pub struct Ch9329Backend {
    /// 串口设备
    port: Mutex<Box<dyn SerialPort>>,

    /// 设备路径
    device_path: String,

    /// 波特率
    baud_rate: u32,

    /// 当前键盘状态
    keyboard_state: Mutex<KeyboardState>,

    /// 连接状态
    connected: AtomicBool,
}

impl Ch9329Backend {
    /// 创建 CH9329 后端
    pub fn with_baud_rate(device: &str, baud_rate: u32) -> Result<Self>;

    /// 发送命令
    fn send_command(&self, cmd: &[u8]) -> Result<Vec<u8>>;

    /// 发送键盘数据包
    fn send_keyboard_packet(&self, report: &KeyboardReport) -> Result<()>;

    /// 发送鼠标数据包
    fn send_mouse_packet(&self, report: &[u8], absolute: bool) -> Result<()>;
}

CH9329 协议

帧格式:
┌──────┬──────┬──────┬──────────┬──────────┬──────┐
│ HEAD │ ADDR │ CMD  │ LEN      │ DATA     │ SUM  │
│ 0x57 │ 0xAB │ 0xXX │ data_len │ payload  │ csum │
└──────┴──────┴──────┴──────────┴──────────┴──────┘

命令码:
0x02 - 发送键盘数据
0x04 - 发送绝对鼠标数据
0x05 - 发送相对鼠标数据
0x0E - 获取芯片信息

4. 事件类型

4.1 键盘事件 (types.rs)

/// Keyboard event type
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum KeyEventType {
    Down,  // 按键按下
    Up,    // 按键释放
}

/// Keyboard modifier flags
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct KeyboardModifiers {
    pub left_ctrl: bool,
    pub left_shift: bool,
    pub left_alt: bool,
    pub left_meta: bool,
    pub right_ctrl: bool,
    pub right_shift: bool,
    pub right_alt: bool,
    pub right_meta: bool,
}

impl KeyboardModifiers {
    /// Convert to USB HID modifier byte
    pub fn to_hid_byte(&self) -> u8;

    /// Create from USB HID modifier byte
    pub fn from_hid_byte(byte: u8) -> Self;

    /// Check if any modifier is active
    pub fn any(&self) -> bool;
}

/// Keyboard event
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KeyboardEvent {
    /// Event type (down/up)
    #[serde(rename = "type")]
    pub event_type: KeyEventType,

    /// Key code (USB HID usage code or JavaScript keyCode)
    pub key: u8,

    /// Modifier keys state
    #[serde(default)]
    pub modifiers: KeyboardModifiers,

    /// If true, key is already USB HID code (skip js_to_usb conversion)
    #[serde(default)]
    pub is_usb_hid: bool,
}

4.2 鼠标事件 (types.rs)

/// Mouse button
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum MouseButton {
    Left,
    Right,
    Middle,
    Back,
    Forward,
}

impl MouseButton {
    /// Convert to USB HID button bit
    pub fn to_hid_bit(&self) -> u8;
}

/// Mouse event type
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum MouseEventType {
    Move,     // 相对移动
    MoveAbs,  // 绝对位置
    Down,     // 按钮按下
    Up,       // 按钮释放
    Scroll,   // 滚轮滚动
}

/// Mouse event
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MouseEvent {
    /// Event type
    #[serde(rename = "type")]
    pub event_type: MouseEventType,

    /// X coordinate or delta
    #[serde(default)]
    pub x: i32,

    /// Y coordinate or delta
    #[serde(default)]
    pub y: i32,

    /// Button (for down/up events)
    #[serde(default)]
    pub button: Option<MouseButton>,

    /// Scroll delta (for scroll events)
    #[serde(default)]
    pub scroll: i8,
}

4.3 Consumer Control 事件 (types.rs)

/// Consumer control event (multimedia keys)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConsumerEvent {
    /// USB HID Consumer Control Usage Code
    pub usage: u16,
}

// 常用 Usage Codes (定义在 consumer.rs)
pub mod usage {
    pub const PLAY_PAUSE: u16 = 0x00CD;
    pub const STOP: u16 = 0x00B7;
    pub const NEXT_TRACK: u16 = 0x00B5;
    pub const PREV_TRACK: u16 = 0x00B6;
    pub const MUTE: u16 = 0x00E2;
    pub const VOLUME_UP: u16 = 0x00E9;
    pub const VOLUME_DOWN: u16 = 0x00EA;
}

5. 按键映射

5.1 按键转换 (keymap.rs)

模块使用固定大小的查找表 (256 字节) 实现 JavaScript keyCode 到 USB HID usage code 的 O(1) 转换。

/// Convert JavaScript keyCode to USB HID keyCode
pub fn js_to_usb(js_code: u8) -> Option<u8>;

/// Check if a key code is a modifier key
pub fn is_modifier_key(usb_code: u8) -> bool;

/// Get modifier bit for a modifier key
pub fn modifier_bit(usb_code: u8) -> Option<u8>;

// USB HID key codes 定义在 usb 子模块
pub mod usb {
    pub const KEY_A: u8 = 0x04;
    pub const KEY_ENTER: u8 = 0x28;
    pub const KEY_LEFT_CTRL: u8 = 0xE0;
    // ... 等等
}

// JavaScript key codes 定义在 js 子模块
pub mod js {
    pub const KEY_A: u8 = 65;
    pub const KEY_ENTER: u8 = 13;
    // ... 等等
}

5.2 转换示例

JavaScript    →    USB HID
65 (KEY_A)    →    0x04
13 (ENTER)    →    0x28
37 (LEFT)     →    0x50
17 (CTRL)     →    0xE0

6. 输入处理器

6.1 WebSocket Handler (websocket.rs)

使用二进制协议 (与 DataChannel 格式相同):

/// Binary response codes
const RESP_OK: u8 = 0x00;
const RESP_ERR_HID_UNAVAILABLE: u8 = 0x01;
const RESP_ERR_INVALID_MESSAGE: u8 = 0x02;

/// WebSocket HID upgrade handler
pub async fn ws_hid_handler(
    ws: WebSocketUpgrade,
    State(state): State<Arc<AppState>>
) -> Response;

/// Handle HID WebSocket connection
async fn handle_hid_socket(socket: WebSocket, state: Arc<AppState>);

/// Handle binary HID message (same format as DataChannel)
async fn handle_binary_message(data: &[u8], state: &AppState) -> Result<(), String>;

6.2 DataChannel Handler (datachannel.rs)

用于 WebRTC 模式下的 HID 事件处理,使用二进制协议。

二进制消息格式

消息类型常量:
MSG_KEYBOARD = 0x01  // 键盘事件
MSG_MOUSE    = 0x02  // 鼠标事件
MSG_CONSUMER = 0x03  // Consumer Control 事件

键盘消息 (4 字节):
┌──────────┬──────────┬──────────┬──────────┐
│ MSG_TYPE │ EVENT    │ KEY_CODE │ MODIFIER │
│  0x01    │ 0/1      │ JS code  │ bitmask  │
└──────────┴──────────┴──────────┴──────────┘
EVENT: 0=down, 1=up

鼠标消息 (7 字节):
┌──────────┬──────────┬──────────┬──────────┬──────────┐
│ MSG_TYPE │ EVENT    │ X (i16)  │ Y (i16)  │ BTN/SCRL │
│  0x02    │ 0-4      │ LE       │ LE       │ u8/i8    │
└──────────┴──────────┴──────────┴──────────┴──────────┘
EVENT: 0=move, 1=moveabs, 2=down, 3=up, 4=scroll

Consumer Control 消息 (3 字节):
┌──────────┬──────────────────────┐
│ MSG_TYPE │ USAGE CODE (u16 LE)  │
│  0x03    │ e.g. 0x00CD          │
└──────────┴──────────────────────┘
/// Parsed HID event from DataChannel
#[derive(Debug, Clone)]
pub enum HidChannelEvent {
    Keyboard(KeyboardEvent),
    Mouse(MouseEvent),
    Consumer(ConsumerEvent),
}

/// Parse a binary HID message from DataChannel
pub fn parse_hid_message(data: &[u8]) -> Option<HidChannelEvent>;

/// Encode events to binary format (for sending to client if needed)
pub fn encode_keyboard_event(event: &KeyboardEvent) -> Vec<u8>;
pub fn encode_mouse_event(event: &MouseEvent) -> Vec<u8>;

7. 健康监视

7.1 HidHealthMonitor (monitor.rs)

/// HID health status
#[derive(Debug, Clone, PartialEq)]
pub enum HidHealthStatus {
    /// Device is healthy and operational
    Healthy,

    /// Device has an error, attempting recovery
    Error {
        reason: String,
        error_code: String,
        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
    pub recovery_cooldown_ms: u64,
}

/// HID health monitor
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,
    /// Current retry count
    retry_count: AtomicU32,
    /// Last error code (for change detection)
    last_error_code: RwLock<Option<String>>,
    /// Last recovery timestamp (for cooldown)
    last_recovery_ms: AtomicU64,
}

impl HidHealthMonitor {
    /// Create a new HID health monitor
    pub fn new(config: HidMonitorConfig) -> Self;

    /// Create with default configuration
    pub fn with_defaults() -> Self;

    /// Set the event bus for broadcasting state changes
    pub async fn set_event_bus(&self, events: Arc<EventBus>);

    /// Report an error from HID operations
    pub async fn report_error(
        &self,
        backend: &str,
        device: Option<&str>,
        reason: &str,
        error_code: &str,
    );

    /// Report that a reconnection attempt is starting
    pub async fn report_reconnecting(&self, backend: &str);

    /// Report that the device has recovered
    pub async fn report_recovered(&self, backend: &str);

    /// Get the current health status
    pub async fn status(&self) -> HidHealthStatus;

    /// Get the current retry count
    pub fn retry_count(&self) -> u32;

    /// Check if the monitor is in an error state
    pub async fn is_error(&self) -> bool;

    /// Check if the monitor is healthy
    pub async fn is_healthy(&self) -> bool;

    /// Reset the monitor to healthy state
    pub async fn reset(&self);

    /// Check if we should continue retrying
    pub fn should_retry(&self) -> bool;

    /// Get the retry interval
    pub fn retry_interval(&self) -> Duration;
}

错误处理流程

  1. 报告错误: report_error() - 更新状态、限流日志、发布事件
  2. 重连通知: report_reconnecting() - 每5次尝试发布一次事件
  3. 恢复通知: report_recovered() - 重置状态、发布恢复事件
  4. 日志限流: 5秒内不重复日志相同错误
  5. 恢复冷却: 恢复后1秒内抑制错误日志

8. 系统事件

pub enum SystemEvent {
    /// HID state changed
    HidStateChanged {
        backend: String,
        initialized: bool,
        error: Option<String>,
        error_code: Option<String>,
    },

    /// HID device lost
    HidDeviceLost {
        backend: String,
        device: Option<String>,
        reason: String,
        error_code: String,
    },

    /// HID reconnecting
    HidReconnecting {
        backend: String,
        attempt: u32,
    },

    /// HID recovered
    HidRecovered {
        backend: String,
    },
}

9. 错误处理

pub enum AppError {
    /// HID backend error
    HidError {
        backend: String,
        reason: String,
        error_code: String,
    },

    // ... 其他错误类型
}

常见错误码:

  • enoent - 设备文件不存在 (ENOENT)
  • epipe - 管道断开 (EPIPE)
  • eshutdown - 端点关闭 (ESHUTDOWN)
  • eagain - 资源暂时不可用 (EAGAIN)
  • eagain_retry - EAGAIN 重试中 (内部使用,不报告给监视器)
  • enxio - 设备或地址不存在 (ENXIO)
  • enodev - 设备不存在 (ENODEV)
  • eio - I/O 错误 (EIO)
  • io_error - 其他 I/O 错误
  • not_opened - 设备未打开
  • init_failed - 初始化失败

10. 使用示例

10.1 初始化 HID 控制器

// 创建 HID 控制器 (OTG 模式)
let hid = HidController::new(
    HidBackendType::Otg,
    Some(otg_service.clone())
);

// 设置事件总线
hid.set_event_bus(event_bus.clone()).await;

// 初始化后端
hid.init().await?;

10.2 发送键盘事件

// 按下 Ctrl+C
hid.send_keyboard(KeyboardEvent {
    event_type: KeyEventType::Down,
    key: 67, // JS keyCode for 'C'
    modifiers: KeyboardModifiers {
        left_ctrl: true,
        ..Default::default()
    },
    is_usb_hid: false,
}).await?;

// 释放 Ctrl+C
hid.send_keyboard(KeyboardEvent {
    event_type: KeyEventType::Up,
    key: 67,
    modifiers: KeyboardModifiers::default(),
    is_usb_hid: false,
}).await?;

10.3 发送鼠标事件

// 移动鼠标到绝对位置 (屏幕中心)
hid.send_mouse(MouseEvent {
    event_type: MouseEventType::MoveAbs,
    x: 16384,  // 0-32767 范围 (HID 标准)
    y: 16384,
    button: None,
    scroll: 0,
}).await?;

// 点击左键
hid.send_mouse(MouseEvent::button_down(MouseButton::Left)).await?;
hid.send_mouse(MouseEvent::button_up(MouseButton::Left)).await?;

// 相对移动
hid.send_mouse(MouseEvent::move_rel(10, -10)).await?;

// 滚轮滚动
hid.send_mouse(MouseEvent::scroll(-1)).await?;

10.4 发送多媒体键

use crate::hid::consumer::usage;

// 播放/暂停
hid.send_consumer(ConsumerEvent {
    usage: usage::PLAY_PAUSE,
}).await?;

// 音量增加
hid.send_consumer(ConsumerEvent {
    usage: usage::VOLUME_UP,
}).await?;

10.5 重新加载后端

// 切换到 CH9329 后端
hid.reload(HidBackendType::Ch9329 {
    port: "/dev/ttyUSB0".to_string(),
    baud_rate: 9600,
}).await?;

11. 常见问题

Q: OTG 模式下键盘/鼠标不工作?

  1. 检查 /dev/hidg* 设备是否存在: ls -l /dev/hidg*
  2. 检查 USB gadget 是否正确配置: ls /sys/kernel/config/usb_gadget/
  3. 检查 UDC 是否绑定: cat /sys/kernel/config/usb_gadget/*/UDC
  4. 检查目标 PC 是否识别 USB 设备 (在目标 PC 上运行 dmesg 或查看设备管理器)
  5. 查看 One-KVM 日志: journalctl -u one-kvm -f

Q: CH9329 无法初始化?

  1. 检查串口设备路径: ls -l /dev/ttyUSB*
  2. 检查串口权限: sudo chmod 666 /dev/ttyUSB0
  3. 检查波特率设置 (默认 9600)
  4. 使用 minicomscreen 测试串口连接:
    minicom -D /dev/ttyUSB0 -b 9600
    

Q: 鼠标定位不准确?

  1. 使用绝对鼠标模式 (默认)
  2. 确保前端发送的坐标在 0-32767 范围内
  3. 检查前端是否正确处理屏幕缩放
  4. 检查浏览器缩放级别 (应为 100%)

Q: 按键有延迟?

  1. 检查网络延迟: ping <kvm-ip>
  2. 使用 WebRTC 模式 (DataChannel) 而不是 WebSocket
  3. 减少网络跳数 (避免多层代理)
  4. 检查服务器 CPU 负载

Q: 频繁出现 ESHUTDOWN 错误?

这是正常现象,通常发生在:

  • MSD (大容量存储) 设备挂载/卸载时
  • USB 主机重新枚举设备时
  • 目标 PC 进入休眠/唤醒时

OTG 后端会自动处理这些错误并重新打开设备,无需人工干预。

Q: 如何查看 LED 状态 (Num Lock, Caps Lock)?

let led_state = otg_backend.led_state();
println!("Caps Lock: {}", led_state.caps_lock);
println!("Num Lock: {}", led_state.num_lock);

LED 状态会在键盘设备的 OUT endpoint 接收到数据时自动更新。


12. 性能优化

12.1 零拷贝写入

OTG 后端使用 write_all() 直接写入设备文件,避免额外的内存拷贝。

12.2 非阻塞 I/O

所有设备文件以 O_NONBLOCK 模式打开,配合 poll() 实现超时控制。

12.3 事件批处理

前端可以批量发送多个事件,后端逐个处理。对于鼠标移动,超时的帧会被静默丢弃。

12.4 日志限流

使用 LogThrottler 防止大量重复日志影响性能:

  • HID 错误日志: 5秒限流
  • 恢复后冷却: 1秒内不记录新错误

13. 安全考虑

13.1 设备权限

  • OTG gadget 设备文件 (/dev/hidg*) 需要读写权限
  • 通常需要 root 权限或添加用户到 input
  • 建议使用 udev 规则自动设置权限

13.2 输入验证

  • 所有来自前端的事件都经过验证
  • 无效的按键码会被忽略或映射到默认值
  • 鼠标坐标会被限制在有效范围内

13.3 资源限制

  • 键盘报告最多支持 6 个同时按键 (USB HID 标准)
  • 鼠标移动范围限制在 -127127 (相对) 或 032767 (绝对)
  • 超时的 HID 写入会被丢弃,不会无限等待

14. 调试技巧

14.1 启用详细日志

RUST_LOG=one_kvm::hid=debug ./one-kvm

14.2 监控 HID 设备

# 监控键盘事件
sudo cat /dev/hidg0 | hexdump -C

# 监控鼠标事件
sudo cat /dev/hidg1 | hexdump -C

14.3 检查 USB 枚举

在目标 PC 上:

# Linux
dmesg | grep -i hid
lsusb

# Windows
# 打开设备管理器 -> 人体学输入设备

14.4 测试 CH9329

# 发送测试命令 (获取版本)
echo -ne '\x57\xAB\x0E\x00\x0E' > /dev/ttyUSB0

15. 参考资料