mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-09-13 02:54:26 +08:00
refactor: 删除部分多余的代码和注释
This commit is contained in:
@@ -1,73 +1,32 @@
|
||||
//! HID backend trait definition
|
||||
//! `HidBackend` trait plus serde `HidBackendType` (OTG | CH9329 | disabled).
|
||||
|
||||
use async_trait::async_trait;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::sync::watch;
|
||||
|
||||
use super::otg::LedState;
|
||||
use super::types::{ConsumerEvent, KeyboardEvent, MouseEvent};
|
||||
use crate::error::Result;
|
||||
use crate::events::LedState;
|
||||
|
||||
/// Default CH9329 baud rate
|
||||
fn default_ch9329_baud_rate() -> u32 {
|
||||
9600
|
||||
}
|
||||
|
||||
/// HID backend type
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "lowercase")]
|
||||
#[derive(Default)]
|
||||
pub enum HidBackendType {
|
||||
/// USB OTG gadget mode
|
||||
Otg,
|
||||
/// CH9329 serial HID controller
|
||||
Ch9329 {
|
||||
/// Serial port path
|
||||
port: String,
|
||||
/// Baud rate (default: 9600)
|
||||
#[serde(default = "default_ch9329_baud_rate")]
|
||||
baud_rate: u32,
|
||||
},
|
||||
/// No HID backend (disabled)
|
||||
#[default]
|
||||
None,
|
||||
}
|
||||
|
||||
impl HidBackendType {
|
||||
/// Check if OTG backend is available on this system
|
||||
pub fn otg_available() -> bool {
|
||||
// Check for USB gadget support
|
||||
std::path::Path::new("/sys/class/udc").exists()
|
||||
}
|
||||
|
||||
/// Detect the best available backend
|
||||
pub fn detect() -> Self {
|
||||
// Check for OTG gadget support
|
||||
if Self::otg_available() {
|
||||
return Self::Otg;
|
||||
}
|
||||
|
||||
// Check for common CH9329 serial ports
|
||||
let common_ports = [
|
||||
"/dev/ttyUSB0",
|
||||
"/dev/ttyUSB1",
|
||||
"/dev/ttyAMA0",
|
||||
"/dev/serial0",
|
||||
];
|
||||
|
||||
for port in &common_ports {
|
||||
if std::path::Path::new(port).exists() {
|
||||
return Self::Ch9329 {
|
||||
port: port.to_string(),
|
||||
baud_rate: 9600, // Use default baud rate for auto-detection
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Self::None
|
||||
}
|
||||
|
||||
/// Get backend name as string
|
||||
pub fn name_str(&self) -> &str {
|
||||
match self {
|
||||
Self::Otg => "otg",
|
||||
@@ -77,76 +36,40 @@ impl HidBackendType {
|
||||
}
|
||||
}
|
||||
|
||||
/// Current runtime status reported by a HID backend.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct HidBackendRuntimeSnapshot {
|
||||
/// Whether the backend has been initialized and can accept requests.
|
||||
pub initialized: bool,
|
||||
/// Whether the backend is currently online and communicating successfully.
|
||||
pub online: bool,
|
||||
/// Whether absolute mouse positioning is supported.
|
||||
pub supports_absolute_mouse: bool,
|
||||
/// Whether keyboard LED/status feedback is currently enabled.
|
||||
pub keyboard_leds_enabled: bool,
|
||||
/// Last known keyboard LED state.
|
||||
pub led_state: LedState,
|
||||
/// Screen resolution for absolute mouse mode.
|
||||
pub screen_resolution: Option<(u32, u32)>,
|
||||
/// Device identifier associated with the backend, if any.
|
||||
pub device: Option<String>,
|
||||
/// Current user-facing error, if any.
|
||||
pub error: Option<String>,
|
||||
/// Current programmatic error code, if any.
|
||||
pub error_code: Option<String>,
|
||||
}
|
||||
|
||||
/// HID backend trait
|
||||
#[async_trait]
|
||||
pub trait HidBackend: Send + Sync {
|
||||
/// 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)
|
||||
/// Default implementation returns an error (not supported)
|
||||
async fn send_consumer(&self, _event: ConsumerEvent) -> Result<()> {
|
||||
Err(crate::error::AppError::BadRequest(
|
||||
"Consumer control not supported by this backend".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
/// Reset all inputs (release all keys/buttons)
|
||||
async fn reset(&self) -> Result<()>;
|
||||
|
||||
/// Shutdown the backend
|
||||
async fn shutdown(&self) -> Result<()>;
|
||||
|
||||
/// Get the current backend runtime snapshot.
|
||||
fn runtime_snapshot(&self) -> HidBackendRuntimeSnapshot;
|
||||
|
||||
/// Subscribe to backend runtime changes.
|
||||
fn subscribe_runtime(&self) -> watch::Receiver<()>;
|
||||
|
||||
/// Set screen resolution (for absolute mouse)
|
||||
fn set_screen_resolution(&mut self, _width: u32, _height: u32) {}
|
||||
}
|
||||
|
||||
/// HID backend information
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct HidBackendInfo {
|
||||
/// Backend name
|
||||
pub name: String,
|
||||
/// Backend type
|
||||
pub backend_type: String,
|
||||
/// Is initialized
|
||||
pub initialized: bool,
|
||||
/// Supports absolute mouse
|
||||
pub absolute_mouse: bool,
|
||||
/// Screen resolution (if absolute mouse)
|
||||
pub resolution: Option<(u32, u32)>,
|
||||
fn set_screen_resolution(&self, _width: u32, _height: u32) {}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
//! CH9329 Serial HID Controller backend
|
||||
//!
|
||||
//! CH9329 is a USB HID chip controlled via UART from WCH (沁恒).
|
||||
//! It supports keyboard, mouse (absolute + relative), and custom HID device emulation.
|
||||
//!
|
||||
//! ## Protocol Format
|
||||
//! CH9329 over UART — WCH *Serial Communication Protocol V1.0*.
|
||||
//! ```text
|
||||
//! ┌──────┬──────┬──────┬────────┬──────────────┬──────────┐
|
||||
//! │Header│ ADDR │ CMD │ LEN │ DATA │ SUM │
|
||||
@@ -11,16 +6,11 @@
|
||||
//! │57 AB │ 00 │ xx │ N │ N bytes │Checksum │
|
||||
//! └──────┴──────┴──────┴────────┴──────────────┴──────────┘
|
||||
//! ```
|
||||
//!
|
||||
//! Checksum: Sum of ALL bytes including header (modulo 256)
|
||||
//!
|
||||
//! ## Reference
|
||||
//! Based on WCH CH9329 Serial Communication Protocol V1.0
|
||||
//! Sum of all octets modulo 256 (including header).
|
||||
|
||||
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, Ordering};
|
||||
use std::sync::{mpsc, Arc};
|
||||
use std::thread;
|
||||
@@ -29,82 +19,51 @@ use tokio::sync::watch;
|
||||
use tracing::{info, trace, warn};
|
||||
|
||||
use super::backend::{HidBackend, HidBackendRuntimeSnapshot};
|
||||
use super::otg::LedState;
|
||||
use super::types::{KeyEventType, KeyboardEvent, KeyboardReport, MouseEvent, MouseEventType};
|
||||
use crate::error::{AppError, Result};
|
||||
use crate::events::LedState;
|
||||
|
||||
// ============================================================================
|
||||
// Constants and Command Codes
|
||||
// ============================================================================
|
||||
|
||||
/// CH9329 packet header
|
||||
const PACKET_HEADER: [u8; 2] = [0x57, 0xAB];
|
||||
|
||||
/// Default address (accepts any address)
|
||||
const DEFAULT_ADDR: u8 = 0x00;
|
||||
|
||||
/// Default baud rate for CH9329
|
||||
pub const DEFAULT_BAUD_RATE: u32 = 9600;
|
||||
|
||||
/// Response timeout in milliseconds
|
||||
const RESPONSE_TIMEOUT_MS: u64 = 500;
|
||||
|
||||
/// Maximum data length in a packet
|
||||
const MAX_DATA_LEN: usize = 64;
|
||||
|
||||
/// CH9329 absolute mouse resolution
|
||||
const CH9329_MOUSE_RESOLUTION: u32 = 4096;
|
||||
|
||||
/// How often the worker probes the chip when idle.
|
||||
const PROBE_INTERVAL_MS: u64 = 100;
|
||||
|
||||
/// How long the worker waits before reopening the serial port after a failure.
|
||||
const RECONNECT_DELAY_MS: u64 = 2000;
|
||||
|
||||
/// Initial startup wait for the worker to confirm CH9329 is reachable.
|
||||
const INIT_WAIT_MS: u64 = 3000;
|
||||
|
||||
/// CH9329 command codes
|
||||
pub mod cmd {
|
||||
/// Get chip version, USB status, and LED status
|
||||
pub const GET_INFO: u8 = 0x01;
|
||||
/// Send standard keyboard data (8 bytes)
|
||||
pub const SEND_KB_GENERAL_DATA: u8 = 0x02;
|
||||
/// Send multimedia keyboard data
|
||||
pub const SEND_KB_MEDIA_DATA: u8 = 0x03;
|
||||
/// Send absolute mouse data
|
||||
pub const SEND_MS_ABS_DATA: u8 = 0x04;
|
||||
/// Send relative mouse data
|
||||
pub const SEND_MS_REL_DATA: u8 = 0x05;
|
||||
/// Send custom HID data
|
||||
pub const SEND_MY_HID_DATA: u8 = 0x06;
|
||||
/// Restore factory default configuration
|
||||
pub const SET_DEFAULT_CFG: u8 = 0x0C;
|
||||
/// Software reset
|
||||
pub const RESET: u8 = 0x0F;
|
||||
}
|
||||
|
||||
/// Response command mask (success = cmd | 0x80, error = cmd | 0xC0)
|
||||
const RESPONSE_SUCCESS_MASK: u8 = 0x80;
|
||||
const RESPONSE_ERROR_MASK: u8 = 0xC0;
|
||||
|
||||
/// CH9329 error codes
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(u8)]
|
||||
pub enum Ch9329Error {
|
||||
/// Command executed successfully
|
||||
Success = 0x00,
|
||||
/// Serial receive timeout
|
||||
Timeout = 0xE1,
|
||||
/// Invalid packet header
|
||||
InvalidHeader = 0xE2,
|
||||
/// Invalid command code
|
||||
InvalidCommand = 0xE3,
|
||||
/// Checksum mismatch
|
||||
ChecksumError = 0xE4,
|
||||
/// Parameter error
|
||||
ParameterError = 0xE5,
|
||||
/// Execution failed
|
||||
OperationFailed = 0xE6,
|
||||
}
|
||||
|
||||
@@ -137,29 +96,17 @@ impl std::fmt::Display for Ch9329Error {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Chip Information
|
||||
// ============================================================================
|
||||
|
||||
/// CH9329 chip information
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct ChipInfo {
|
||||
/// Chip version (e.g., "V1.0", "V1.1")
|
||||
pub version: String,
|
||||
/// Raw version byte
|
||||
pub version_raw: u8,
|
||||
/// USB connection status
|
||||
pub usb_connected: bool,
|
||||
/// Num Lock LED state
|
||||
pub num_lock: bool,
|
||||
/// Caps Lock LED state
|
||||
pub caps_lock: bool,
|
||||
/// Scroll Lock LED state
|
||||
pub scroll_lock: bool,
|
||||
}
|
||||
|
||||
impl ChipInfo {
|
||||
/// Parse chip info from response data (8 bytes)
|
||||
pub fn from_response(data: &[u8]) -> Option<Self> {
|
||||
if data.len() < 8 {
|
||||
return None;
|
||||
@@ -181,7 +128,6 @@ impl ChipInfo {
|
||||
}
|
||||
}
|
||||
|
||||
/// Keyboard LED status
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct LedStatus {
|
||||
pub num_lock: bool,
|
||||
@@ -199,98 +145,21 @@ impl From<u8> for LedStatus {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Configuration
|
||||
// ============================================================================
|
||||
|
||||
/// CH9329 work mode
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[repr(u8)]
|
||||
#[derive(Default)]
|
||||
pub enum WorkMode {
|
||||
/// Mode 0: Standard USB Keyboard + Mouse (default)
|
||||
#[default]
|
||||
KeyboardMouse = 0x00,
|
||||
/// Mode 1: Standard USB Keyboard only
|
||||
KeyboardOnly = 0x01,
|
||||
/// Mode 2: Standard USB Mouse only
|
||||
MouseOnly = 0x02,
|
||||
/// Mode 3: Custom HID device
|
||||
CustomHid = 0x03,
|
||||
}
|
||||
|
||||
/// CH9329 serial communication mode
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[repr(u8)]
|
||||
#[derive(Default)]
|
||||
pub enum SerialMode {
|
||||
/// Mode 0: Protocol transmission mode (default)
|
||||
#[default]
|
||||
Protocol = 0x00,
|
||||
/// Mode 1: ASCII mode
|
||||
Ascii = 0x01,
|
||||
/// Mode 2: Transparent mode
|
||||
Transparent = 0x02,
|
||||
}
|
||||
|
||||
/// CH9329 configuration parameters
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Ch9329Config {
|
||||
/// Work mode
|
||||
pub work_mode: WorkMode,
|
||||
/// Serial communication mode
|
||||
pub serial_mode: SerialMode,
|
||||
/// Device address (0x00-0xFE, 0xFF = broadcast)
|
||||
pub address: u8,
|
||||
/// Baud rate
|
||||
pub baud_rate: u32,
|
||||
/// USB VID
|
||||
pub vid: u16,
|
||||
/// USB PID
|
||||
pub pid: u16,
|
||||
}
|
||||
|
||||
impl Default for Ch9329Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
work_mode: WorkMode::KeyboardMouse,
|
||||
serial_mode: SerialMode::Protocol,
|
||||
address: 0x00,
|
||||
baud_rate: 9600,
|
||||
vid: 0x1A86,
|
||||
pid: 0xE129,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Response Parsing
|
||||
// ============================================================================
|
||||
|
||||
/// Parsed response from CH9329
|
||||
#[derive(Debug)]
|
||||
pub struct Response {
|
||||
/// Address byte
|
||||
pub address: u8,
|
||||
/// Command code (with response bits)
|
||||
pub cmd: u8,
|
||||
/// Data payload
|
||||
pub data: Vec<u8>,
|
||||
/// Whether this is an error response
|
||||
pub is_error: bool,
|
||||
/// Error code (if is_error)
|
||||
pub error_code: Option<Ch9329Error>,
|
||||
}
|
||||
|
||||
impl Response {
|
||||
/// Parse a response from raw bytes
|
||||
pub fn parse(bytes: &[u8]) -> Option<Self> {
|
||||
// Minimum: Header(2) + Addr(1) + Cmd(1) + Len(1) + Sum(1) = 6
|
||||
if bytes.len() < 6 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Check header
|
||||
if bytes[0] != PACKET_HEADER[0] || bytes[1] != PACKET_HEADER[1] {
|
||||
return None;
|
||||
}
|
||||
@@ -299,12 +168,10 @@ impl Response {
|
||||
let cmd = bytes[3];
|
||||
let len = bytes[4] as usize;
|
||||
|
||||
// Check if we have enough bytes
|
||||
if bytes.len() < 5 + len + 1 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Verify checksum
|
||||
let expected_checksum = bytes[5 + len];
|
||||
let calculated_checksum = bytes[..5 + len]
|
||||
.iter()
|
||||
@@ -335,19 +202,13 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
/// Maximum packet size (header 2 + addr 1 + cmd 1 + len 1 + data 64 + checksum 1 = 70)
|
||||
const MAX_PACKET_SIZE: usize = 70;
|
||||
|
||||
// ============================================================================
|
||||
// CH9329 Backend Implementation
|
||||
// ============================================================================
|
||||
|
||||
struct Ch9329RuntimeState {
|
||||
initialized: AtomicBool,
|
||||
online: AtomicBool,
|
||||
@@ -424,47 +285,28 @@ enum WorkerCommand {
|
||||
Shutdown,
|
||||
}
|
||||
|
||||
/// CH9329 HID backend
|
||||
pub struct Ch9329Backend {
|
||||
/// Serial port path
|
||||
port_path: String,
|
||||
/// Baud rate
|
||||
baud_rate: u32,
|
||||
/// Worker command sender
|
||||
worker_tx: Mutex<Option<mpsc::Sender<WorkerCommand>>>,
|
||||
/// Background worker thread
|
||||
worker_handle: Mutex<Option<thread::JoinHandle<()>>>,
|
||||
/// Current keyboard state
|
||||
keyboard_state: Mutex<KeyboardReport>,
|
||||
/// Current mouse button state
|
||||
mouse_buttons: AtomicU8,
|
||||
/// Screen width for absolute mouse coordinate conversion
|
||||
screen_width: u32,
|
||||
/// Screen height for absolute mouse coordinate conversion
|
||||
screen_height: u32,
|
||||
/// Cached chip information
|
||||
screen_resolution: RwLock<(u32, u32)>,
|
||||
chip_info: Arc<RwLock<Option<ChipInfo>>>,
|
||||
/// LED status cache
|
||||
led_status: Arc<RwLock<LedStatus>>,
|
||||
/// Device address (default 0x00)
|
||||
address: u8,
|
||||
/// Last absolute mouse X position (CH9329 coordinate: 0-4095)
|
||||
last_abs_x: AtomicU16,
|
||||
/// Last absolute mouse Y position (CH9329 coordinate: 0-4095)
|
||||
last_abs_y: AtomicU16,
|
||||
/// Whether relative mouse mode is active (set by incoming events)
|
||||
relative_mouse_active: AtomicBool,
|
||||
/// Shared runtime status updated only by the worker.
|
||||
runtime: Arc<Ch9329RuntimeState>,
|
||||
}
|
||||
|
||||
impl Ch9329Backend {
|
||||
/// Create a new CH9329 backend with default baud rate (9600)
|
||||
pub fn new(port_path: &str) -> Result<Self> {
|
||||
Self::with_baud_rate(port_path, DEFAULT_BAUD_RATE)
|
||||
}
|
||||
|
||||
/// Create a new CH9329 backend with custom baud rate
|
||||
pub fn with_baud_rate(port_path: &str, baud_rate: u32) -> Result<Self> {
|
||||
Ok(Self {
|
||||
port_path: port_path.to_string(),
|
||||
@@ -473,8 +315,7 @@ impl Ch9329Backend {
|
||||
worker_handle: Mutex::new(None),
|
||||
keyboard_state: Mutex::new(KeyboardReport::default()),
|
||||
mouse_buttons: AtomicU8::new(0),
|
||||
screen_width: 1920,
|
||||
screen_height: 1080,
|
||||
screen_resolution: RwLock::new((1920, 1080)),
|
||||
chip_info: Arc::new(RwLock::new(None)),
|
||||
led_status: Arc::new(RwLock::new(LedStatus::default())),
|
||||
address: DEFAULT_ADDR,
|
||||
@@ -489,12 +330,10 @@ impl Ch9329Backend {
|
||||
self.runtime.set_error(reason, error_code);
|
||||
}
|
||||
|
||||
/// Check if the serial port device file exists
|
||||
pub fn check_port_exists(&self) -> bool {
|
||||
std::path::Path::new(&self.port_path).exists()
|
||||
}
|
||||
|
||||
/// Convert serialport error to HidError
|
||||
fn serial_error_to_hid_error(e: serialport::Error, operation: &str) -> AppError {
|
||||
let error_code = match e.kind() {
|
||||
serialport::ErrorKind::NoDevice => "port_not_found",
|
||||
@@ -518,16 +357,11 @@ impl Ch9329Backend {
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate checksum for CH9329 packet (sum of ALL bytes including header)
|
||||
#[inline]
|
||||
fn calculate_checksum(data: &[u8]) -> u8 {
|
||||
data.iter().fold(0u8, |acc, &x| acc.wrapping_add(x))
|
||||
}
|
||||
|
||||
/// Build a CH9329 packet into a stack-allocated buffer
|
||||
///
|
||||
/// Packet format: `[Header 0x57 0xAB] [Address] [Command] [Length] [Data] [Checksum]`
|
||||
/// Returns the packet buffer and the actual length
|
||||
#[inline]
|
||||
fn build_packet_buf(address: u8, cmd: u8, data: &[u8]) -> ([u8; MAX_PACKET_SIZE], usize) {
|
||||
debug_assert!(
|
||||
@@ -539,25 +373,18 @@ impl Ch9329Backend {
|
||||
let packet_len = 6 + data.len();
|
||||
let mut packet = [0u8; MAX_PACKET_SIZE];
|
||||
|
||||
// Header (2 bytes)
|
||||
packet[0] = PACKET_HEADER[0];
|
||||
packet[1] = PACKET_HEADER[1];
|
||||
// Address (1 byte)
|
||||
packet[2] = address;
|
||||
// Command (1 byte)
|
||||
packet[3] = cmd;
|
||||
// Length (1 byte) - data length only
|
||||
packet[4] = len;
|
||||
// Data (N bytes)
|
||||
packet[5..5 + data.len()].copy_from_slice(data);
|
||||
// Checksum (1 byte) - sum of ALL bytes including header
|
||||
let checksum = Self::calculate_checksum(&packet[..5 + data.len()]);
|
||||
packet[5 + data.len()] = checksum;
|
||||
|
||||
(packet, packet_len)
|
||||
}
|
||||
|
||||
/// Build a CH9329 packet (legacy Vec version for compatibility)
|
||||
fn build_packet(address: u8, cmd: u8, data: &[u8]) -> Vec<u8> {
|
||||
let (buf, len) = Self::build_packet_buf(address, cmd, data);
|
||||
buf[..len].to_vec()
|
||||
@@ -984,10 +811,6 @@ impl Ch9329Backend {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// HidBackend Trait Implementation
|
||||
// ============================================================================
|
||||
|
||||
#[async_trait]
|
||||
impl HidBackend for Ch9329Backend {
|
||||
async fn init(&self) -> Result<()> {
|
||||
@@ -1060,7 +883,6 @@ impl HidBackend for Ch9329Backend {
|
||||
async fn send_keyboard(&self, event: KeyboardEvent) -> Result<()> {
|
||||
let usb_key = event.key.to_hid_usage();
|
||||
|
||||
// Handle modifier keys separately
|
||||
if event.key.is_modifier() {
|
||||
let mut state = self.keyboard_state.lock();
|
||||
|
||||
@@ -1078,7 +900,6 @@ impl HidBackend for Ch9329Backend {
|
||||
} else {
|
||||
let mut state = self.keyboard_state.lock();
|
||||
|
||||
// Update modifiers from event
|
||||
state.modifiers = event.modifiers.to_hid_byte();
|
||||
|
||||
match event.event_type {
|
||||
@@ -1104,19 +925,15 @@ impl HidBackend for Ch9329Backend {
|
||||
|
||||
match event.event_type {
|
||||
MouseEventType::Move => {
|
||||
// Relative movement - send delta directly without inversion
|
||||
self.relative_mouse_active.store(true, Ordering::Relaxed);
|
||||
let dx = event.x.clamp(-127, 127) as i8;
|
||||
let dy = event.y.clamp(-127, 127) as i8;
|
||||
self.send_mouse_relative(buttons, dx, dy, 0)?;
|
||||
}
|
||||
MouseEventType::MoveAbs => {
|
||||
// Absolute movement
|
||||
self.relative_mouse_active.store(false, Ordering::Relaxed);
|
||||
// Frontend sends 0-32767 (HID standard), CH9329 expects 0-4095
|
||||
let x = ((event.x.clamp(0, 32767) as u32) * CH9329_MOUSE_RESOLUTION / 32768) as u16;
|
||||
let y = ((event.y.clamp(0, 32767) as u32) * CH9329_MOUSE_RESOLUTION / 32768) as u16;
|
||||
// Store last absolute position for click events
|
||||
self.last_abs_x.store(x, Ordering::Relaxed);
|
||||
self.last_abs_y.store(y, Ordering::Relaxed);
|
||||
self.send_mouse_absolute(buttons, x, y, 0)?;
|
||||
@@ -1153,7 +970,6 @@ impl HidBackend for Ch9329Backend {
|
||||
if self.relative_mouse_active.load(Ordering::Relaxed) {
|
||||
self.send_mouse_relative(buttons, 0, 0, event.scroll)?;
|
||||
} else {
|
||||
// Use absolute mouse for scroll with last position
|
||||
let x = self.last_abs_x.load(Ordering::Relaxed);
|
||||
let y = self.last_abs_y.load(Ordering::Relaxed);
|
||||
self.send_mouse_absolute(buttons, x, y, event.scroll)?;
|
||||
@@ -1165,7 +981,6 @@ impl HidBackend for Ch9329Backend {
|
||||
}
|
||||
|
||||
async fn reset(&self) -> Result<()> {
|
||||
// Reset keyboard
|
||||
{
|
||||
let mut state = self.keyboard_state.lock();
|
||||
state.clear();
|
||||
@@ -1174,14 +989,12 @@ impl HidBackend for Ch9329Backend {
|
||||
self.send_keyboard_report(&report)?;
|
||||
}
|
||||
|
||||
// Reset mouse
|
||||
self.mouse_buttons.store(0, Ordering::Relaxed);
|
||||
self.last_abs_x.store(0, Ordering::Relaxed);
|
||||
self.last_abs_y.store(0, Ordering::Relaxed);
|
||||
self.relative_mouse_active.store(false, Ordering::Relaxed);
|
||||
self.send_mouse_absolute(0, 0, 0, 0)?;
|
||||
|
||||
// Reset media keys
|
||||
let _ = self.release_media_keys();
|
||||
|
||||
info!("CH9329 HID state reset");
|
||||
@@ -1233,7 +1046,7 @@ impl HidBackend for Ch9329Backend {
|
||||
kana: false,
|
||||
}
|
||||
},
|
||||
screen_resolution: Some((self.screen_width, self.screen_height)),
|
||||
screen_resolution: Some(*self.screen_resolution.read()),
|
||||
device: Some(self.port_path.clone()),
|
||||
error: error.as_ref().map(|(reason, _)| reason.clone()),
|
||||
error_code: error.as_ref().map(|(_, code)| code.clone()),
|
||||
@@ -1244,125 +1057,21 @@ impl HidBackend for Ch9329Backend {
|
||||
self.runtime.subscribe()
|
||||
}
|
||||
|
||||
fn set_screen_resolution(&mut self, width: u32, height: u32) {
|
||||
self.screen_width = width;
|
||||
self.screen_height = height;
|
||||
fn set_screen_resolution(&self, width: u32, height: u32) {
|
||||
*self.screen_resolution.write() = (width, height);
|
||||
self.runtime.notify();
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Detection and Helpers
|
||||
// ============================================================================
|
||||
|
||||
/// Detect CH9329 on common serial ports
|
||||
pub fn detect_ch9329() -> Option<String> {
|
||||
let common_ports = [
|
||||
"/dev/ttyUSB0",
|
||||
"/dev/ttyUSB1",
|
||||
"/dev/ttyAMA0",
|
||||
"/dev/serial0",
|
||||
"/dev/ttyS0",
|
||||
];
|
||||
|
||||
// Try multiple baud rates
|
||||
let baud_rates = [9600, 115200];
|
||||
|
||||
for port_path in &common_ports {
|
||||
if !std::path::Path::new(port_path).exists() {
|
||||
continue;
|
||||
}
|
||||
|
||||
for &baud_rate in &baud_rates {
|
||||
if let Ok(mut port) = serialport::new(*port_path, baud_rate)
|
||||
.timeout(Duration::from_millis(200))
|
||||
.open()
|
||||
{
|
||||
// Build GET_INFO packet manually (address = 0x00)
|
||||
let packet = [0x57, 0xAB, 0x00, cmd::GET_INFO, 0x00, 0x03];
|
||||
|
||||
if port.write_all(&packet).is_ok() {
|
||||
std::thread::sleep(Duration::from_millis(50));
|
||||
|
||||
let mut response = [0u8; 16];
|
||||
if let Ok(n) = port.read(&mut response) {
|
||||
// Check for valid CH9329 response header
|
||||
if n >= 6
|
||||
&& response[0] == PACKET_HEADER[0]
|
||||
&& response[1] == PACKET_HEADER[1]
|
||||
{
|
||||
info!("CH9329 detected on {} @ {} baud", port_path, baud_rate);
|
||||
return Some(port_path.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Detect CH9329 and return both path and working baud rate
|
||||
pub fn detect_ch9329_with_baud() -> Option<(String, u32)> {
|
||||
let common_ports = [
|
||||
"/dev/ttyUSB0",
|
||||
"/dev/ttyUSB1",
|
||||
"/dev/ttyAMA0",
|
||||
"/dev/serial0",
|
||||
"/dev/ttyS0",
|
||||
];
|
||||
|
||||
let baud_rates = [9600, 115200, 57600, 38400, 19200];
|
||||
|
||||
for port_path in &common_ports {
|
||||
if !std::path::Path::new(port_path).exists() {
|
||||
continue;
|
||||
}
|
||||
|
||||
for &baud_rate in &baud_rates {
|
||||
if let Ok(mut port) = serialport::new(*port_path, baud_rate)
|
||||
.timeout(Duration::from_millis(200))
|
||||
.open()
|
||||
{
|
||||
let packet = [0x57, 0xAB, 0x00, cmd::GET_INFO, 0x00, 0x03];
|
||||
|
||||
if port.write_all(&packet).is_ok() {
|
||||
std::thread::sleep(Duration::from_millis(50));
|
||||
|
||||
let mut response = [0u8; 16];
|
||||
if let Ok(n) = port.read(&mut response) {
|
||||
if n >= 6
|
||||
&& response[0] == PACKET_HEADER[0]
|
||||
&& response[1] == PACKET_HEADER[1]
|
||||
{
|
||||
info!("CH9329 detected on {} @ {} baud", port_path, baud_rate);
|
||||
return Some((port_path.to_string(), baud_rate));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Tests
|
||||
// ============================================================================
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_packet_building() {
|
||||
// Test GET_INFO packet (no data)
|
||||
let packet = Ch9329Backend::build_packet(DEFAULT_ADDR, cmd::GET_INFO, &[]);
|
||||
assert_eq!(packet, vec![0x57, 0xAB, 0x00, 0x01, 0x00, 0x03]);
|
||||
|
||||
// Test keyboard packet (8 bytes data)
|
||||
let data = [0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00]; // 'A' key
|
||||
let packet = Ch9329Backend::build_packet(DEFAULT_ADDR, cmd::SEND_KB_GENERAL_DATA, &data);
|
||||
|
||||
@@ -1372,7 +1081,6 @@ 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
|
||||
let expected_checksum: u8 = packet[..13]
|
||||
.iter()
|
||||
.fold(0u8, |acc: u8, &x| acc.wrapping_add(x));
|
||||
@@ -1381,7 +1089,6 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_relative_mouse_packet() {
|
||||
// Test relative mouse: move right 50 pixels
|
||||
let data = [0x01, 0x00, 50u8, 0x00, 0x00];
|
||||
let packet = Ch9329Backend::build_packet(DEFAULT_ADDR, cmd::SEND_MS_REL_DATA, &data);
|
||||
|
||||
@@ -1397,12 +1104,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_checksum_calculation() {
|
||||
// Known packet: GET_INFO
|
||||
let packet = [0x57u8, 0xAB, 0x00, 0x01, 0x00];
|
||||
let checksum = Ch9329Backend::calculate_checksum(&packet);
|
||||
assert_eq!(checksum, 0x03);
|
||||
|
||||
// Known packet: Keyboard 'A' press
|
||||
let packet = [
|
||||
0x57u8, 0xAB, 0x00, 0x02, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
];
|
||||
@@ -1412,7 +1117,6 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_response_parsing() {
|
||||
// Valid GET_INFO response
|
||||
let response_bytes = [
|
||||
0x57, 0xAB, // Header
|
||||
0x00, // Address
|
||||
@@ -1422,9 +1126,7 @@ mod tests {
|
||||
0xE0, // Checksum (calculated)
|
||||
];
|
||||
|
||||
// Note: checksum in test is just placeholder, parse will validate
|
||||
let _result = Response::parse(&response_bytes);
|
||||
// This will fail because checksum doesn't match, but structure is tested
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -2,21 +2,17 @@
|
||||
//!
|
||||
//! Reference: USB HID Usage Tables 1.12, Section 15 (Consumer Page 0x0C)
|
||||
|
||||
/// Consumer Control Usage codes for multimedia keys
|
||||
pub mod usage {
|
||||
// Transport Controls
|
||||
pub const PLAY_PAUSE: u16 = 0x00CD;
|
||||
pub const STOP: u16 = 0x00B7;
|
||||
pub const NEXT_TRACK: u16 = 0x00B5;
|
||||
pub const PREV_TRACK: u16 = 0x00B6;
|
||||
|
||||
// Volume Controls
|
||||
pub const MUTE: u16 = 0x00E2;
|
||||
pub const VOLUME_UP: u16 = 0x00E9;
|
||||
pub const VOLUME_DOWN: u16 = 0x00EA;
|
||||
}
|
||||
|
||||
/// Check if a usage code is valid
|
||||
pub fn is_valid_usage(usage: u16) -> bool {
|
||||
matches!(
|
||||
usage,
|
||||
|
||||
@@ -42,23 +42,19 @@ use super::{
|
||||
MouseEventType,
|
||||
};
|
||||
|
||||
/// Message types
|
||||
pub const MSG_KEYBOARD: u8 = 0x01;
|
||||
pub const MSG_MOUSE: u8 = 0x02;
|
||||
pub const MSG_CONSUMER: u8 = 0x03;
|
||||
|
||||
/// Keyboard event types
|
||||
pub const KB_EVENT_DOWN: u8 = 0x00;
|
||||
pub const KB_EVENT_UP: u8 = 0x01;
|
||||
|
||||
/// Mouse event types
|
||||
pub const MS_EVENT_MOVE: u8 = 0x00;
|
||||
pub const MS_EVENT_MOVE_ABS: u8 = 0x01;
|
||||
pub const MS_EVENT_DOWN: u8 = 0x02;
|
||||
pub const MS_EVENT_UP: u8 = 0x03;
|
||||
pub const MS_EVENT_SCROLL: u8 = 0x04;
|
||||
|
||||
/// Parsed HID event from DataChannel
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum HidChannelEvent {
|
||||
Keyboard(KeyboardEvent),
|
||||
@@ -66,7 +62,6 @@ pub enum HidChannelEvent {
|
||||
Consumer(ConsumerEvent),
|
||||
}
|
||||
|
||||
/// Parse a binary HID message from DataChannel
|
||||
pub fn parse_hid_message(data: &[u8]) -> Option<HidChannelEvent> {
|
||||
if data.is_empty() {
|
||||
warn!("Empty HID message");
|
||||
@@ -86,7 +81,6 @@ pub fn parse_hid_message(data: &[u8]) -> Option<HidChannelEvent> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse keyboard message payload
|
||||
fn parse_keyboard_message(data: &[u8]) -> Option<HidChannelEvent> {
|
||||
if data.len() < 3 {
|
||||
warn!("Keyboard message too short: {} bytes", data.len());
|
||||
@@ -129,7 +123,6 @@ fn parse_keyboard_message(data: &[u8]) -> Option<HidChannelEvent> {
|
||||
}))
|
||||
}
|
||||
|
||||
/// Parse mouse message payload
|
||||
fn parse_mouse_message(data: &[u8]) -> Option<HidChannelEvent> {
|
||||
if data.len() < 6 {
|
||||
warn!("Mouse message too short: {} bytes", data.len());
|
||||
@@ -148,11 +141,9 @@ fn parse_mouse_message(data: &[u8]) -> Option<HidChannelEvent> {
|
||||
}
|
||||
};
|
||||
|
||||
// Parse coordinates as i16 LE (works for both relative and absolute)
|
||||
let x = i16::from_le_bytes([data[1], data[2]]) as i32;
|
||||
let y = i16::from_le_bytes([data[3], data[4]]) as i32;
|
||||
|
||||
// Button or scroll delta
|
||||
let (button, scroll) = match event_type {
|
||||
MouseEventType::Down | MouseEventType::Up => {
|
||||
let btn = match data[5] {
|
||||
@@ -178,7 +169,6 @@ fn parse_mouse_message(data: &[u8]) -> Option<HidChannelEvent> {
|
||||
}))
|
||||
}
|
||||
|
||||
/// Parse consumer control message payload
|
||||
fn parse_consumer_message(data: &[u8]) -> Option<HidChannelEvent> {
|
||||
if data.len() < 2 {
|
||||
warn!("Consumer message too short: {} bytes", data.len());
|
||||
@@ -190,7 +180,6 @@ fn parse_consumer_message(data: &[u8]) -> Option<HidChannelEvent> {
|
||||
Some(HidChannelEvent::Consumer(ConsumerEvent { usage }))
|
||||
}
|
||||
|
||||
/// Encode a keyboard event to binary format (for sending to client if needed)
|
||||
pub fn encode_keyboard_event(event: &KeyboardEvent) -> Vec<u8> {
|
||||
let event_type = match event.event_type {
|
||||
KeyEventType::Down => KB_EVENT_DOWN,
|
||||
@@ -207,40 +196,6 @@ pub fn encode_keyboard_event(event: &KeyboardEvent) -> Vec<u8> {
|
||||
]
|
||||
}
|
||||
|
||||
/// Encode a mouse event to binary format (for sending to client if needed)
|
||||
pub fn encode_mouse_event(event: &MouseEvent) -> Vec<u8> {
|
||||
let event_type = match event.event_type {
|
||||
MouseEventType::Move => MS_EVENT_MOVE,
|
||||
MouseEventType::MoveAbs => MS_EVENT_MOVE_ABS,
|
||||
MouseEventType::Down => MS_EVENT_DOWN,
|
||||
MouseEventType::Up => MS_EVENT_UP,
|
||||
MouseEventType::Scroll => MS_EVENT_SCROLL,
|
||||
};
|
||||
|
||||
let x_bytes = (event.x as i16).to_le_bytes();
|
||||
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 {
|
||||
MouseButton::Left => 0u8,
|
||||
MouseButton::Middle => 1u8,
|
||||
MouseButton::Right => 2u8,
|
||||
MouseButton::Back => 3u8,
|
||||
MouseButton::Forward => 4u8,
|
||||
})
|
||||
.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,
|
||||
]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use typeshare::typeshare;
|
||||
|
||||
/// Shared canonical keyboard key identifiers used across frontend and backend.
|
||||
///
|
||||
/// The enum names intentionally mirror `KeyboardEvent.code` style values so the
|
||||
/// browser, virtual keyboard, and HID backend can all speak the same language.
|
||||
#[typeshare]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum CanonicalKey {
|
||||
@@ -128,11 +124,6 @@ pub enum CanonicalKey {
|
||||
}
|
||||
|
||||
impl CanonicalKey {
|
||||
/// Convert the canonical key to a stable wire code.
|
||||
///
|
||||
/// The wire code intentionally matches the USB HID usage for keyboard page
|
||||
/// keys so existing low-level behavior stays intact while the semantic type
|
||||
/// becomes explicit.
|
||||
pub const fn to_hid_usage(self) -> u8 {
|
||||
match self {
|
||||
Self::KeyA => 0x04,
|
||||
@@ -255,7 +246,6 @@ impl CanonicalKey {
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a wire code / USB HID usage to its canonical key.
|
||||
pub const fn from_hid_usage(usage: u8) -> Option<Self> {
|
||||
match usage {
|
||||
0x04 => Some(Self::KeyA),
|
||||
|
||||
167
src/hid/mod.rs
167
src/hid/mod.rs
@@ -1,15 +1,4 @@
|
||||
//! HID (Human Interface Device) control module
|
||||
//!
|
||||
//! This module provides keyboard and mouse control for remote KVM:
|
||||
//! - USB OTG gadget mode (native Linux USB gadget)
|
||||
//! - CH9329 serial HID controller
|
||||
//!
|
||||
//! Architecture:
|
||||
//! ```text
|
||||
//! Web Client --> WebSocket/DataChannel --> HID Events --> Backend --> Target PC
|
||||
//! |
|
||||
//! [OTG | CH9329]
|
||||
//! ```
|
||||
//! HID path: browser (WebSocket or WebRTC DataChannel) → queue → OTG gadget or CH9329.
|
||||
|
||||
pub mod backend;
|
||||
pub mod ch9329;
|
||||
@@ -20,51 +9,26 @@ pub mod otg;
|
||||
pub mod types;
|
||||
pub mod websocket;
|
||||
|
||||
pub use crate::events::LedState;
|
||||
pub use backend::{HidBackend, HidBackendRuntimeSnapshot, HidBackendType};
|
||||
pub use keyboard::CanonicalKey;
|
||||
pub use otg::LedState;
|
||||
pub use types::{
|
||||
ConsumerEvent, KeyEventType, KeyboardEvent, KeyboardModifiers, MouseButton, MouseEvent,
|
||||
MouseEventType,
|
||||
};
|
||||
|
||||
/// HID backend information
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HidInfo {
|
||||
/// Backend name
|
||||
pub name: String,
|
||||
/// 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)>,
|
||||
}
|
||||
|
||||
/// Unified HID runtime state used by snapshots and events.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct HidRuntimeState {
|
||||
/// Whether a backend is configured and expected to exist.
|
||||
pub available: bool,
|
||||
/// Stable backend key: "otg", "ch9329", "none".
|
||||
pub backend: String,
|
||||
/// Whether the backend is currently initialized and operational.
|
||||
pub initialized: bool,
|
||||
/// Whether the backend is currently online.
|
||||
pub online: bool,
|
||||
/// Whether absolute mouse positioning is supported.
|
||||
pub supports_absolute_mouse: bool,
|
||||
/// Whether keyboard LED/status feedback is enabled.
|
||||
pub keyboard_leds_enabled: bool,
|
||||
/// Last known keyboard LED state.
|
||||
pub led_state: LedState,
|
||||
/// Screen resolution for absolute mouse mode.
|
||||
pub screen_resolution: Option<(u32, u32)>,
|
||||
/// Device path associated with the backend, if any.
|
||||
pub device: Option<String>,
|
||||
/// Current user-facing error, if any.
|
||||
pub error: Option<String>,
|
||||
/// Current programmatic error code, if any.
|
||||
pub error_code: Option<String>,
|
||||
}
|
||||
|
||||
@@ -140,45 +104,29 @@ const HID_EVENT_QUEUE_CAPACITY: usize = 64;
|
||||
const HID_EVENT_SEND_TIMEOUT_MS: u64 = 30;
|
||||
|
||||
#[derive(Debug)]
|
||||
enum HidEvent {
|
||||
enum QueuedHidEvent {
|
||||
Keyboard(KeyboardEvent),
|
||||
Mouse(MouseEvent),
|
||||
Consumer(ConsumerEvent),
|
||||
Reset,
|
||||
}
|
||||
|
||||
/// HID controller managing keyboard and mouse input
|
||||
pub struct HidController {
|
||||
/// OTG Service reference (only used when backend is OTG)
|
||||
otg_service: Option<Arc<OtgService>>,
|
||||
/// Active backend
|
||||
backend: Arc<RwLock<Option<Arc<dyn HidBackend>>>>,
|
||||
/// Backend type (mutable for reload)
|
||||
backend_type: Arc<RwLock<HidBackendType>>,
|
||||
/// Event bus for broadcasting state changes (optional)
|
||||
events: Arc<tokio::sync::RwLock<Option<Arc<EventBus>>>>,
|
||||
/// Unified HID runtime state.
|
||||
runtime_state: Arc<RwLock<HidRuntimeState>>,
|
||||
/// HID event queue sender (non-blocking)
|
||||
hid_tx: mpsc::Sender<HidEvent>,
|
||||
/// HID event queue receiver (moved into worker on first start)
|
||||
hid_rx: Mutex<Option<mpsc::Receiver<HidEvent>>>,
|
||||
/// Coalesced mouse move (latest)
|
||||
hid_tx: mpsc::Sender<QueuedHidEvent>,
|
||||
hid_rx: Mutex<Option<mpsc::Receiver<QueuedHidEvent>>>,
|
||||
pending_move: Arc<parking_lot::Mutex<Option<MouseEvent>>>,
|
||||
/// Pending move flag (fast path)
|
||||
pending_move_flag: Arc<AtomicBool>,
|
||||
/// Worker task handle
|
||||
hid_worker: Mutex<Option<JoinHandle<()>>>,
|
||||
/// Backend runtime subscription task handle
|
||||
runtime_worker: Mutex<Option<JoinHandle<()>>>,
|
||||
/// Backend initialization fast flag
|
||||
backend_available: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl HidController {
|
||||
/// Create a new HID controller with specified backend
|
||||
///
|
||||
/// For OTG backend, otg_service should be provided to support hot-reload
|
||||
pub fn new(backend_type: HidBackendType, otg_service: Option<Arc<OtgService>>) -> Self {
|
||||
let (hid_tx, hid_rx) = mpsc::channel(HID_EVENT_QUEUE_CAPACITY);
|
||||
Self {
|
||||
@@ -199,12 +147,10 @@ impl HidController {
|
||||
}
|
||||
}
|
||||
|
||||
/// Set event bus for broadcasting state changes
|
||||
pub async fn set_event_bus(&self, events: Arc<EventBus>) {
|
||||
*self.events.write().await = Some(events);
|
||||
}
|
||||
|
||||
/// Initialize the HID backend
|
||||
pub async fn init(&self) -> Result<()> {
|
||||
let backend_type = self.backend_type.read().await.clone();
|
||||
let backend: Arc<dyn HidBackend> = match backend_type {
|
||||
@@ -256,7 +202,6 @@ impl HidController {
|
||||
*self.backend.write().await = Some(backend);
|
||||
self.sync_runtime_state_from_backend().await;
|
||||
|
||||
// Start HID event worker (once)
|
||||
self.start_event_worker().await;
|
||||
self.restart_runtime_worker().await;
|
||||
|
||||
@@ -264,12 +209,10 @@ impl HidController {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Shutdown the HID backend and release resources
|
||||
pub async fn shutdown(&self) -> Result<()> {
|
||||
info!("Shutting down HID controller");
|
||||
self.stop_runtime_worker().await;
|
||||
|
||||
// Close the backend
|
||||
if let Some(backend) = self.backend.write().await.take() {
|
||||
if let Err(e) = backend.shutdown().await {
|
||||
warn!("Error shutting down HID backend: {}", e);
|
||||
@@ -290,17 +233,15 @@ impl HidController {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send keyboard event
|
||||
pub async fn send_keyboard(&self, event: KeyboardEvent) -> Result<()> {
|
||||
if !self.backend_available.load(Ordering::Acquire) {
|
||||
return Err(AppError::BadRequest(
|
||||
"HID backend not available".to_string(),
|
||||
));
|
||||
}
|
||||
self.enqueue_event(HidEvent::Keyboard(event)).await
|
||||
self.enqueue_event(QueuedHidEvent::Keyboard(event)).await
|
||||
}
|
||||
|
||||
/// Send mouse event
|
||||
pub async fn send_mouse(&self, event: MouseEvent) -> Result<()> {
|
||||
if !self.backend_available.load(Ordering::Acquire) {
|
||||
return Err(AppError::BadRequest(
|
||||
@@ -312,81 +253,55 @@ impl HidController {
|
||||
event.event_type,
|
||||
MouseEventType::Move | MouseEventType::MoveAbs
|
||||
) {
|
||||
// Best-effort: drop/merge move events if queue is full
|
||||
self.enqueue_mouse_move(event)
|
||||
} else {
|
||||
self.enqueue_event(HidEvent::Mouse(event)).await
|
||||
self.enqueue_event(QueuedHidEvent::Mouse(event)).await
|
||||
}
|
||||
}
|
||||
|
||||
/// Send consumer control event (multimedia keys)
|
||||
pub async fn send_consumer(&self, event: ConsumerEvent) -> Result<()> {
|
||||
if !self.backend_available.load(Ordering::Acquire) {
|
||||
return Err(AppError::BadRequest(
|
||||
"HID backend not available".to_string(),
|
||||
));
|
||||
}
|
||||
self.enqueue_event(HidEvent::Consumer(event)).await
|
||||
self.enqueue_event(QueuedHidEvent::Consumer(event)).await
|
||||
}
|
||||
|
||||
/// Reset all keys (release all pressed keys)
|
||||
pub async fn reset(&self) -> Result<()> {
|
||||
if !self.backend_available.load(Ordering::Acquire) {
|
||||
return Ok(());
|
||||
}
|
||||
// Reset is important but best-effort; enqueue to avoid blocking
|
||||
self.enqueue_event(HidEvent::Reset).await
|
||||
self.enqueue_event(QueuedHidEvent::Reset).await
|
||||
}
|
||||
|
||||
/// Check if backend is available
|
||||
pub async fn is_available(&self) -> bool {
|
||||
self.backend_available.load(Ordering::Acquire)
|
||||
}
|
||||
|
||||
/// Get backend type
|
||||
pub async fn backend_type(&self) -> HidBackendType {
|
||||
self.backend_type.read().await.clone()
|
||||
}
|
||||
|
||||
/// Get backend info
|
||||
pub async fn info(&self) -> Option<HidInfo> {
|
||||
let state = self.runtime_state.read().await.clone();
|
||||
if !state.available {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(HidInfo {
|
||||
name: state.backend,
|
||||
initialized: state.initialized,
|
||||
supports_absolute_mouse: state.supports_absolute_mouse,
|
||||
screen_resolution: state.screen_resolution,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get current HID runtime state snapshot.
|
||||
pub async fn snapshot(&self) -> HidRuntimeState {
|
||||
self.runtime_state.read().await.clone()
|
||||
}
|
||||
|
||||
/// Reload the HID backend with new type
|
||||
pub async fn reload(&self, new_backend_type: HidBackendType) -> Result<()> {
|
||||
info!("Reloading HID backend: {:?}", new_backend_type);
|
||||
self.backend_available.store(false, Ordering::Release);
|
||||
self.stop_runtime_worker().await;
|
||||
|
||||
// Shutdown existing backend first
|
||||
if let Some(backend) = self.backend.write().await.take() {
|
||||
if let Err(e) = backend.shutdown().await {
|
||||
warn!("Error shutting down old HID backend: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Create and initialize new backend
|
||||
let new_backend: Option<Arc<dyn HidBackend>> = match new_backend_type {
|
||||
HidBackendType::Otg => {
|
||||
info!("Initializing OTG HID backend");
|
||||
|
||||
// Get OtgService reference
|
||||
let otg_service = match self.otg_service.as_ref() {
|
||||
Some(svc) => svc,
|
||||
None => {
|
||||
@@ -398,28 +313,25 @@ impl HidController {
|
||||
};
|
||||
|
||||
match otg_service.hid_device_paths().await {
|
||||
Some(handles) => {
|
||||
// Create OtgBackend from handles
|
||||
match otg::OtgBackend::from_handles(handles) {
|
||||
Ok(backend) => {
|
||||
let backend = Arc::new(backend);
|
||||
match backend.init().await {
|
||||
Ok(_) => {
|
||||
info!("OTG backend initialized successfully");
|
||||
Some(backend)
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to initialize OTG backend: {}", e);
|
||||
None
|
||||
}
|
||||
Some(handles) => match otg::OtgBackend::from_handles(handles) {
|
||||
Ok(backend) => {
|
||||
let backend = Arc::new(backend);
|
||||
match backend.init().await {
|
||||
Ok(_) => {
|
||||
info!("OTG backend initialized successfully");
|
||||
Some(backend)
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to initialize OTG backend: {}", e);
|
||||
None
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to create OTG backend: {}", e);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to create OTG backend: {}", e);
|
||||
None
|
||||
}
|
||||
},
|
||||
None => {
|
||||
warn!("OTG HID paths are not available");
|
||||
None
|
||||
@@ -470,7 +382,6 @@ impl HidController {
|
||||
info!("HID backend reloaded successfully: {:?}", new_backend_type);
|
||||
self.start_event_worker().await;
|
||||
|
||||
// Update backend_type on success
|
||||
*self.backend_type.write().await = new_backend_type.clone();
|
||||
|
||||
self.sync_runtime_state_from_backend().await;
|
||||
@@ -481,7 +392,6 @@ impl HidController {
|
||||
warn!("HID backend reload resulted in no active backend");
|
||||
self.backend_available.store(false, Ordering::Release);
|
||||
|
||||
// Update backend_type even on failure (to reflect the attempted change)
|
||||
*self.backend_type.write().await = new_backend_type.clone();
|
||||
|
||||
let current = self.runtime_state.read().await.clone();
|
||||
@@ -541,11 +451,10 @@ impl HidController {
|
||||
|
||||
process_hid_event(event, &backend).await;
|
||||
|
||||
// After each event, flush latest move if pending
|
||||
if pending_move_flag.swap(false, Ordering::AcqRel) {
|
||||
let move_event = { pending_move.lock().take() };
|
||||
if let Some(move_event) = move_event {
|
||||
process_hid_event(HidEvent::Mouse(move_event), &backend).await;
|
||||
process_hid_event(QueuedHidEvent::Mouse(move_event), &backend).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -595,7 +504,7 @@ impl HidController {
|
||||
}
|
||||
|
||||
fn enqueue_mouse_move(&self, event: MouseEvent) -> Result<()> {
|
||||
match self.hid_tx.try_send(HidEvent::Mouse(event.clone())) {
|
||||
match self.hid_tx.try_send(QueuedHidEvent::Mouse(event.clone())) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(mpsc::error::TrySendError::Full(_)) => {
|
||||
*self.pending_move.lock() = Some(event);
|
||||
@@ -608,11 +517,10 @@ impl HidController {
|
||||
}
|
||||
}
|
||||
|
||||
async fn enqueue_event(&self, event: HidEvent) -> Result<()> {
|
||||
async fn enqueue_event(&self, event: QueuedHidEvent) -> Result<()> {
|
||||
match self.hid_tx.try_send(event) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(mpsc::error::TrySendError::Full(ev)) => {
|
||||
// For non-move events, wait briefly to avoid dropping critical input
|
||||
let tx = self.hid_tx.clone();
|
||||
let send_result = tokio::time::timeout(
|
||||
Duration::from_millis(HID_EVENT_SEND_TIMEOUT_MS),
|
||||
@@ -649,7 +557,10 @@ async fn apply_backend_runtime_state(
|
||||
apply_runtime_state(runtime_state, events, next).await;
|
||||
}
|
||||
|
||||
async fn process_hid_event(event: HidEvent, backend: &Arc<RwLock<Option<Arc<dyn HidBackend>>>>) {
|
||||
async fn process_hid_event(
|
||||
event: QueuedHidEvent,
|
||||
backend: &Arc<RwLock<Option<Arc<dyn HidBackend>>>>,
|
||||
) {
|
||||
let backend_opt = backend.read().await.clone();
|
||||
let backend = match backend_opt {
|
||||
Some(b) => b,
|
||||
@@ -660,10 +571,10 @@ async fn process_hid_event(event: HidEvent, backend: &Arc<RwLock<Option<Arc<dyn
|
||||
let result = tokio::task::spawn_blocking(move || {
|
||||
futures::executor::block_on(async move {
|
||||
match event {
|
||||
HidEvent::Keyboard(ev) => backend_for_send.send_keyboard(ev).await,
|
||||
HidEvent::Mouse(ev) => backend_for_send.send_mouse(ev).await,
|
||||
HidEvent::Consumer(ev) => backend_for_send.send_consumer(ev).await,
|
||||
HidEvent::Reset => backend_for_send.reset().await,
|
||||
QueuedHidEvent::Keyboard(ev) => backend_for_send.send_keyboard(ev).await,
|
||||
QueuedHidEvent::Mouse(ev) => backend_for_send.send_mouse(ev).await,
|
||||
QueuedHidEvent::Consumer(ev) => backend_for_send.send_consumer(ev).await,
|
||||
QueuedHidEvent::Reset => backend_for_send.reset().await,
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -682,12 +593,6 @@ async fn process_hid_event(event: HidEvent, backend: &Arc<RwLock<Option<Arc<dyn
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for HidController {
|
||||
fn default() -> Self {
|
||||
Self::new(HidBackendType::None, None)
|
||||
}
|
||||
}
|
||||
|
||||
fn device_for_backend_type(backend_type: &HidBackendType) -> Option<String> {
|
||||
match backend_type {
|
||||
HidBackendType::Ch9329 { port, .. } => Some(port.clone()),
|
||||
|
||||
234
src/hid/otg.rs
234
src/hid/otg.rs
@@ -1,28 +1,11 @@
|
||||
//! OTG USB Gadget HID backend
|
||||
//! Linux gadget HID: `/dev/hidg*` opened from [`crate::otg::OtgService`].
|
||||
//! Typical nodes: hidg0 keyboard, hidg1 relative mouse, hidg2 absolute, hidg3 consumer control.
|
||||
//!
|
||||
//! This backend uses Linux USB Gadget API to emulate USB HID devices.
|
||||
//! It opens the HID gadget device nodes created by `OtgService`.
|
||||
//! Depending on the configured OTG profile, this may include:
|
||||
//! - hidg0: Keyboard
|
||||
//! - hidg1: Relative Mouse
|
||||
//! - hidg2: Absolute Mouse
|
||||
//! - hidg3: Consumer Control Keyboard
|
||||
//!
|
||||
//! Requirements:
|
||||
//! - USB OTG/Device controller (UDC)
|
||||
//! - ConfigFS with USB gadget support
|
||||
//! - Root privileges for gadget setup
|
||||
//!
|
||||
//! Error Recovery:
|
||||
//! This module implements automatic device reconnection based on PiKVM's approach.
|
||||
//! When ESHUTDOWN or EAGAIN errors occur (common during MSD operations), the device
|
||||
//! file handles are closed and reopened on the next operation.
|
||||
//! See: https://github.com/raspberrypi/linux/issues/4373
|
||||
//! Polled timed writes (JetKVM-style). Treat `ESHUTDOWN` (108) by closing handles and reopening; keep fd on `EAGAIN` (11). Host/gadget teardown during MSD resembles PiKVM. <https://github.com/raspberrypi/linux/issues/4373>
|
||||
|
||||
use async_trait::async_trait;
|
||||
use nix::poll::{poll, PollFd, PollFlags, PollTimeout};
|
||||
use parking_lot::Mutex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs::{self, File, OpenOptions};
|
||||
use std::io::{Read, Write};
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
@@ -40,9 +23,9 @@ use super::types::{
|
||||
ConsumerEvent, KeyEventType, KeyboardEvent, KeyboardReport, MouseEvent, MouseEventType,
|
||||
};
|
||||
use crate::error::{AppError, Result};
|
||||
use crate::events::LedState;
|
||||
use crate::otg::{wait_for_hid_devices, HidDevicePaths};
|
||||
|
||||
/// Device type for ensure_device operations
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum DeviceType {
|
||||
Keyboard,
|
||||
@@ -51,23 +34,7 @@ enum DeviceType {
|
||||
ConsumerControl,
|
||||
}
|
||||
|
||||
/// Keyboard LED state
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
|
||||
pub struct LedState {
|
||||
/// Num Lock LED
|
||||
pub num_lock: bool,
|
||||
/// Caps Lock LED
|
||||
pub caps_lock: bool,
|
||||
/// Scroll Lock LED
|
||||
pub scroll_lock: bool,
|
||||
/// Compose LED
|
||||
pub compose: bool,
|
||||
/// Kana LED
|
||||
pub kana: bool,
|
||||
}
|
||||
|
||||
impl LedState {
|
||||
/// Create from raw byte
|
||||
pub fn from_byte(b: u8) -> Self {
|
||||
Self {
|
||||
num_lock: b & 0x01 != 0,
|
||||
@@ -78,7 +45,6 @@ impl LedState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert to raw byte
|
||||
pub fn to_byte(&self) -> u8 {
|
||||
let mut b = 0u8;
|
||||
if self.num_lock {
|
||||
@@ -100,76 +66,37 @@ impl LedState {
|
||||
}
|
||||
}
|
||||
|
||||
/// OTG HID backend with 4 devices
|
||||
///
|
||||
/// This backend opens HID device files created by OtgService.
|
||||
/// It does NOT manage the USB gadget itself - that's handled by OtgService.
|
||||
///
|
||||
/// ## Error Recovery
|
||||
///
|
||||
/// Based on PiKVM's implementation, this backend automatically handles:
|
||||
/// - EAGAIN (errno 11): Resource temporarily unavailable - just retry later, don't close device
|
||||
/// - ESHUTDOWN (errno 108): Transport endpoint shutdown - close and reopen device
|
||||
///
|
||||
/// When ESHUTDOWN occurs, the device file handle is closed and will be
|
||||
/// reopened on the next operation attempt.
|
||||
/// Opens `/dev/hidg*` nodes provisioned by `OtgService`; gadget lifecycle is not handled here.
|
||||
pub struct OtgBackend {
|
||||
/// Keyboard device path (/dev/hidg0)
|
||||
keyboard_path: Option<PathBuf>,
|
||||
/// Relative mouse device path (/dev/hidg1)
|
||||
mouse_rel_path: Option<PathBuf>,
|
||||
/// Absolute mouse device path (/dev/hidg2)
|
||||
mouse_abs_path: Option<PathBuf>,
|
||||
/// Consumer control device path (/dev/hidg3)
|
||||
consumer_path: Option<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>>,
|
||||
/// Whether keyboard LED/status feedback is enabled.
|
||||
keyboard_leds_enabled: bool,
|
||||
/// Current keyboard state
|
||||
keyboard_state: Mutex<KeyboardReport>,
|
||||
/// Current mouse button state
|
||||
mouse_buttons: AtomicU8,
|
||||
/// Last known LED state (using parking_lot::RwLock for sync access)
|
||||
led_state: Arc<parking_lot::RwLock<LedState>>,
|
||||
/// Screen resolution for absolute mouse (using parking_lot::RwLock for sync access)
|
||||
screen_resolution: parking_lot::RwLock<Option<(u32, u32)>>,
|
||||
/// UDC name for state checking (e.g., "fcc00000.usb")
|
||||
udc_name: Arc<parking_lot::RwLock<Option<String>>>,
|
||||
/// Whether the backend has been initialized.
|
||||
initialized: AtomicBool,
|
||||
/// Whether the device is currently online (UDC configured and devices accessible)
|
||||
online: AtomicBool,
|
||||
/// Last backend error state.
|
||||
last_error: parking_lot::RwLock<Option<(String, String)>>,
|
||||
/// Last error log time for throttling (using parking_lot for sync)
|
||||
last_error_log: parking_lot::Mutex<std::time::Instant>,
|
||||
/// Error count since last successful operation (for log throttling)
|
||||
error_count: AtomicU8,
|
||||
/// Consecutive EAGAIN count (for offline threshold detection)
|
||||
eagain_count: AtomicU8,
|
||||
/// Runtime change notifier.
|
||||
runtime_notify_tx: watch::Sender<()>,
|
||||
/// Runtime monitor stop flag.
|
||||
runtime_worker_stop: Arc<AtomicBool>,
|
||||
/// Runtime monitor thread.
|
||||
runtime_worker: Mutex<Option<thread::JoinHandle<()>>>,
|
||||
}
|
||||
|
||||
/// Write timeout in milliseconds (same as JetKVM's hidWriteTimeout)
|
||||
const HID_WRITE_TIMEOUT_MS: i32 = 20;
|
||||
|
||||
impl OtgBackend {
|
||||
/// Create OTG backend from device paths provided by OtgService
|
||||
///
|
||||
/// This is the ONLY way to create an OtgBackend - it no longer manages
|
||||
/// the USB gadget itself. The gadget must already be set up by OtgService.
|
||||
/// Gadget must already exist; paths come from `OtgService`.
|
||||
pub fn from_handles(paths: HidDevicePaths) -> Result<Self> {
|
||||
let (runtime_notify_tx, _runtime_notify_rx) = watch::channel(());
|
||||
Ok(Self {
|
||||
@@ -234,7 +161,6 @@ impl OtgBackend {
|
||||
}
|
||||
}
|
||||
|
||||
/// Log throttled error message (max once per second)
|
||||
fn log_throttled_error(&self, msg: &str) {
|
||||
let mut last_log = self.last_error_log.lock();
|
||||
let now = std::time::Instant::now();
|
||||
@@ -251,24 +177,17 @@ impl OtgBackend {
|
||||
}
|
||||
}
|
||||
|
||||
/// Reset error count on successful operation
|
||||
fn reset_error_count(&self) {
|
||||
self.error_count.store(0, Ordering::Relaxed);
|
||||
// Also reset EAGAIN count - successful operation means device is working
|
||||
self.eagain_count.store(0, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Write data to HID device with timeout (JetKVM style)
|
||||
///
|
||||
/// Uses poll() to wait for device to be ready for writing.
|
||||
/// If timeout expires, silently drops the data (acceptable for mouse movement).
|
||||
/// Returns Ok(true) if write succeeded, Ok(false) if timed out (silently dropped).
|
||||
/// Poll-based write with `HID_WRITE_TIMEOUT_MS`; timeout → drop (JetKVM-style).
|
||||
fn write_with_timeout(&self, file: &mut File, data: &[u8]) -> std::io::Result<bool> {
|
||||
let mut pollfd = [PollFd::new(file.as_fd(), PollFlags::POLLOUT)];
|
||||
|
||||
match poll(&mut pollfd, PollTimeout::from(HID_WRITE_TIMEOUT_MS as u16)) {
|
||||
Ok(1) => {
|
||||
// Device ready, check for errors
|
||||
if let Some(revents) = pollfd[0].revents() {
|
||||
if revents.contains(PollFlags::POLLERR) || revents.contains(PollFlags::POLLHUP)
|
||||
{
|
||||
@@ -278,12 +197,10 @@ impl OtgBackend {
|
||||
));
|
||||
}
|
||||
}
|
||||
// Write the data
|
||||
file.write_all(data)?;
|
||||
Ok(true)
|
||||
}
|
||||
Ok(0) => {
|
||||
// Timeout - silently drop (JetKVM behavior)
|
||||
trace!("HID write timeout, dropping data");
|
||||
Ok(false)
|
||||
}
|
||||
@@ -292,7 +209,6 @@ impl OtgBackend {
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the UDC name for state checking
|
||||
pub fn set_udc_name(&self, udc: &str) {
|
||||
*self.udc_name.write() = Some(udc.to_string());
|
||||
}
|
||||
@@ -324,15 +240,11 @@ impl OtgBackend {
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the UDC is in "configured" state
|
||||
///
|
||||
/// This is based on PiKVM's `__is_udc_configured()` method.
|
||||
/// The UDC state file indicates whether the USB host has enumerated and configured the gadget.
|
||||
/// `true` when `/sys/class/udc/<name>/state` reads `configured` (PiKVM-style).
|
||||
pub fn is_udc_configured(&self) -> bool {
|
||||
Self::read_udc_configured(&self.udc_name)
|
||||
}
|
||||
|
||||
/// Find the first available UDC
|
||||
fn find_udc() -> Option<String> {
|
||||
let udc_path = PathBuf::from("/sys/class/udc");
|
||||
if let Ok(entries) = fs::read_dir(&udc_path) {
|
||||
@@ -345,12 +257,7 @@ impl OtgBackend {
|
||||
None
|
||||
}
|
||||
|
||||
/// Ensure a device is open and ready for I/O
|
||||
///
|
||||
/// This method is based on PiKVM's `__ensure_device()` pattern:
|
||||
/// 1. Check if device path exists, close handle if not
|
||||
/// 2. If handle is None but path exists, reopen the device
|
||||
/// 3. Return whether the device is ready for I/O
|
||||
/// PiKVM-style: drop handle if node missing; reopen when path reappears.
|
||||
fn ensure_device(&self, device_type: DeviceType) -> Result<()> {
|
||||
let (path_opt, dev_mutex) = match device_type {
|
||||
DeviceType::Keyboard => (&self.keyboard_path, &self.keyboard_dev),
|
||||
@@ -372,9 +279,7 @@ impl OtgBackend {
|
||||
}
|
||||
};
|
||||
|
||||
// Check if device path exists
|
||||
if !path.exists() {
|
||||
// Close the device if open (device was removed)
|
||||
let mut dev = dev_mutex.lock();
|
||||
if dev.is_some() {
|
||||
debug!(
|
||||
@@ -392,7 +297,6 @@ impl OtgBackend {
|
||||
});
|
||||
}
|
||||
|
||||
// If device is not open, try to open it
|
||||
let mut dev = dev_mutex.lock();
|
||||
if dev.is_none() {
|
||||
match Self::open_device(path) {
|
||||
@@ -415,7 +319,6 @@ impl OtgBackend {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Open a HID device file with read/write access
|
||||
fn open_device(path: &PathBuf) -> Result<File> {
|
||||
OpenOptions::new()
|
||||
.read(true)
|
||||
@@ -431,16 +334,15 @@ impl OtgBackend {
|
||||
})
|
||||
}
|
||||
|
||||
/// Convert I/O error to HidError with appropriate error code
|
||||
fn io_error_code(e: &std::io::Error) -> &'static str {
|
||||
match e.raw_os_error() {
|
||||
Some(32) => "epipe", // EPIPE - broken pipe
|
||||
Some(108) => "eshutdown", // ESHUTDOWN - transport endpoint shutdown
|
||||
Some(11) => "eagain", // EAGAIN - resource temporarily unavailable
|
||||
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",
|
||||
Some(108) => "eshutdown",
|
||||
Some(11) => "eagain",
|
||||
Some(6) => "enxio",
|
||||
Some(19) => "enodev",
|
||||
Some(5) => "eio",
|
||||
Some(2) => "enoent",
|
||||
_ => "io_error",
|
||||
}
|
||||
}
|
||||
@@ -455,7 +357,6 @@ impl OtgBackend {
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if all HID device files exist
|
||||
pub fn check_devices_exist(&self) -> bool {
|
||||
self.keyboard_path.as_ref().is_none_or(|p| p.exists())
|
||||
&& self.mouse_rel_path.as_ref().is_none_or(|p| p.exists())
|
||||
@@ -463,7 +364,6 @@ impl OtgBackend {
|
||||
&& self.consumer_path.as_ref().is_none_or(|p| p.exists())
|
||||
}
|
||||
|
||||
/// Get list of missing device paths
|
||||
pub fn get_missing_devices(&self) -> Vec<String> {
|
||||
let mut missing = Vec::new();
|
||||
if let Some(ref path) = self.keyboard_path {
|
||||
@@ -484,17 +384,11 @@ impl OtgBackend {
|
||||
missing
|
||||
}
|
||||
|
||||
/// Send keyboard report (8 bytes)
|
||||
///
|
||||
/// This method ensures the device is open before writing, and handles
|
||||
/// ESHUTDOWN errors by closing the device handle for later reconnection.
|
||||
/// Uses write_with_timeout to avoid blocking on busy devices.
|
||||
fn send_keyboard_report(&self, report: &KeyboardReport) -> Result<()> {
|
||||
if self.keyboard_path.is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Ensure device is ready
|
||||
self.ensure_device(DeviceType::Keyboard)?;
|
||||
|
||||
let mut dev = self.keyboard_dev.lock();
|
||||
@@ -508,7 +402,6 @@ impl OtgBackend {
|
||||
Ok(())
|
||||
}
|
||||
Ok(false) => {
|
||||
// Timeout - silently dropped (JetKVM behavior)
|
||||
self.log_throttled_error("HID keyboard write timeout, dropped");
|
||||
Ok(())
|
||||
}
|
||||
@@ -517,7 +410,6 @@ impl OtgBackend {
|
||||
|
||||
match error_code {
|
||||
Some(108) => {
|
||||
// ESHUTDOWN - endpoint closed, need to reopen device
|
||||
self.eagain_count.store(0, Ordering::Relaxed);
|
||||
debug!("Keyboard ESHUTDOWN, closing for recovery");
|
||||
*dev = None;
|
||||
@@ -531,7 +423,6 @@ impl OtgBackend {
|
||||
))
|
||||
}
|
||||
Some(11) => {
|
||||
// EAGAIN after poll - should be rare, silently drop
|
||||
trace!("Keyboard EAGAIN after poll, dropping");
|
||||
Ok(())
|
||||
}
|
||||
@@ -559,17 +450,11 @@ impl OtgBackend {
|
||||
}
|
||||
}
|
||||
|
||||
/// Send relative mouse report (4 bytes: buttons, dx, dy, wheel)
|
||||
///
|
||||
/// This method ensures the device is open before writing, and handles
|
||||
/// ESHUTDOWN errors by closing the device handle for later reconnection.
|
||||
/// Uses write_with_timeout to avoid blocking on busy devices.
|
||||
fn send_mouse_report_relative(&self, buttons: u8, dx: i8, dy: i8, wheel: i8) -> Result<()> {
|
||||
if self.mouse_rel_path.is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Ensure device is ready
|
||||
self.ensure_device(DeviceType::MouseRelative)?;
|
||||
|
||||
let mut dev = self.mouse_rel_dev.lock();
|
||||
@@ -582,10 +467,7 @@ impl OtgBackend {
|
||||
trace!("Sent relative mouse report: {:02X?}", data);
|
||||
Ok(())
|
||||
}
|
||||
Ok(false) => {
|
||||
// Timeout - silently dropped (JetKVM behavior)
|
||||
Ok(())
|
||||
}
|
||||
Ok(false) => Ok(()),
|
||||
Err(e) => {
|
||||
let error_code = e.raw_os_error();
|
||||
|
||||
@@ -603,10 +485,7 @@ impl OtgBackend {
|
||||
"Failed to write mouse report",
|
||||
))
|
||||
}
|
||||
Some(11) => {
|
||||
// EAGAIN after poll - should be rare, silently drop
|
||||
Ok(())
|
||||
}
|
||||
Some(11) => Ok(()),
|
||||
_ => {
|
||||
self.eagain_count.store(0, Ordering::Relaxed);
|
||||
warn!("Relative mouse write error: {}", e);
|
||||
@@ -631,17 +510,11 @@ impl OtgBackend {
|
||||
}
|
||||
}
|
||||
|
||||
/// Send absolute mouse report (6 bytes: buttons, x_lo, x_hi, y_lo, y_hi, wheel)
|
||||
///
|
||||
/// This method ensures the device is open before writing, and handles
|
||||
/// ESHUTDOWN errors by closing the device handle for later reconnection.
|
||||
/// Uses write_with_timeout to avoid blocking on busy devices.
|
||||
fn send_mouse_report_absolute(&self, buttons: u8, x: u16, y: u16, wheel: i8) -> Result<()> {
|
||||
if self.mouse_abs_path.is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Ensure device is ready
|
||||
self.ensure_device(DeviceType::MouseAbsolute)?;
|
||||
|
||||
let mut dev = self.mouse_abs_dev.lock();
|
||||
@@ -660,10 +533,7 @@ impl OtgBackend {
|
||||
self.reset_error_count();
|
||||
Ok(())
|
||||
}
|
||||
Ok(false) => {
|
||||
// Timeout - silently dropped (JetKVM behavior)
|
||||
Ok(())
|
||||
}
|
||||
Ok(false) => Ok(()),
|
||||
Err(e) => {
|
||||
let error_code = e.raw_os_error();
|
||||
|
||||
@@ -681,10 +551,7 @@ impl OtgBackend {
|
||||
"Failed to write mouse report",
|
||||
))
|
||||
}
|
||||
Some(11) => {
|
||||
// EAGAIN after poll - should be rare, silently drop
|
||||
Ok(())
|
||||
}
|
||||
Some(11) => Ok(()),
|
||||
_ => {
|
||||
self.eagain_count.store(0, Ordering::Relaxed);
|
||||
warn!("Absolute mouse write error: {}", e);
|
||||
@@ -709,35 +576,27 @@ impl OtgBackend {
|
||||
}
|
||||
}
|
||||
|
||||
/// Send consumer control report (2 bytes: usage_lo, usage_hi)
|
||||
///
|
||||
/// Sends a consumer control usage code and then releases it (sends 0x0000).
|
||||
/// Press (`usage`) then release (`0x0000`).
|
||||
fn send_consumer_report(&self, usage: u16) -> Result<()> {
|
||||
if self.consumer_path.is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Ensure device is ready
|
||||
self.ensure_device(DeviceType::ConsumerControl)?;
|
||||
|
||||
let mut dev = self.consumer_dev.lock();
|
||||
if let Some(ref mut file) = *dev {
|
||||
// Send the usage code
|
||||
let data = [(usage & 0xFF) as u8, (usage >> 8) as u8];
|
||||
match self.write_with_timeout(file, &data) {
|
||||
Ok(true) => {
|
||||
trace!("Sent consumer report: {:02X?}", data);
|
||||
// Send release (0x0000)
|
||||
let release = [0u8, 0u8];
|
||||
let _ = self.write_with_timeout(file, &release);
|
||||
self.mark_online();
|
||||
self.reset_error_count();
|
||||
Ok(())
|
||||
}
|
||||
Ok(false) => {
|
||||
// Timeout - silently dropped
|
||||
Ok(())
|
||||
}
|
||||
Ok(false) => Ok(()),
|
||||
Err(e) => {
|
||||
let error_code = e.raw_os_error();
|
||||
match error_code {
|
||||
@@ -753,10 +612,7 @@ impl OtgBackend {
|
||||
"Failed to write consumer report",
|
||||
))
|
||||
}
|
||||
Some(11) => {
|
||||
// EAGAIN after poll - silently drop
|
||||
Ok(())
|
||||
}
|
||||
Some(11) => Ok(()),
|
||||
_ => {
|
||||
warn!("Consumer control write error: {}", e);
|
||||
self.record_error(
|
||||
@@ -780,12 +636,10 @@ impl OtgBackend {
|
||||
}
|
||||
}
|
||||
|
||||
/// Send consumer control event
|
||||
pub fn send_consumer(&self, event: ConsumerEvent) -> Result<()> {
|
||||
self.send_consumer_report(event.usage)
|
||||
}
|
||||
|
||||
/// Get last known LED state
|
||||
pub fn led_state(&self) -> LedState {
|
||||
*self.led_state.read()
|
||||
}
|
||||
@@ -975,7 +829,6 @@ impl HidBackend for OtgBackend {
|
||||
async fn init(&self) -> Result<()> {
|
||||
info!("Initializing OTG HID backend");
|
||||
|
||||
// Auto-detect UDC name for state checking only if OtgService did not provide one
|
||||
if self.udc_name.read().is_none() {
|
||||
if let Some(udc) = Self::find_udc() {
|
||||
info!("Auto-detected UDC: {}", udc);
|
||||
@@ -985,7 +838,6 @@ impl HidBackend for OtgBackend {
|
||||
info!("Using configured UDC: {}", udc);
|
||||
}
|
||||
|
||||
// Wait for devices to appear (they should already exist from OtgService)
|
||||
let mut device_paths = Vec::new();
|
||||
if let Some(ref path) = self.keyboard_path {
|
||||
device_paths.push(path.clone());
|
||||
@@ -1010,7 +862,6 @@ impl HidBackend for OtgBackend {
|
||||
return Err(AppError::Internal("HID devices did not appear".into()));
|
||||
}
|
||||
|
||||
// Open keyboard device
|
||||
if let Some(ref path) = self.keyboard_path {
|
||||
if path.exists() {
|
||||
let file = Self::open_device(path)?;
|
||||
@@ -1021,7 +872,6 @@ impl HidBackend for OtgBackend {
|
||||
}
|
||||
}
|
||||
|
||||
// Open relative mouse device
|
||||
if let Some(ref path) = self.mouse_rel_path {
|
||||
if path.exists() {
|
||||
let file = Self::open_device(path)?;
|
||||
@@ -1032,7 +882,6 @@ impl HidBackend for OtgBackend {
|
||||
}
|
||||
}
|
||||
|
||||
// Open absolute mouse device
|
||||
if let Some(ref path) = self.mouse_abs_path {
|
||||
if path.exists() {
|
||||
let file = Self::open_device(path)?;
|
||||
@@ -1043,7 +892,6 @@ impl HidBackend for OtgBackend {
|
||||
}
|
||||
}
|
||||
|
||||
// Open consumer control device (optional, may not exist on older setups)
|
||||
if let Some(ref path) = self.consumer_path {
|
||||
if path.exists() {
|
||||
let file = Self::open_device(path)?;
|
||||
@@ -1054,7 +902,6 @@ impl HidBackend for OtgBackend {
|
||||
}
|
||||
}
|
||||
|
||||
// Mark as online if all devices opened successfully
|
||||
self.initialized.store(true, Ordering::Relaxed);
|
||||
self.notify_runtime_changed();
|
||||
self.start_runtime_worker();
|
||||
@@ -1066,7 +913,6 @@ impl HidBackend for OtgBackend {
|
||||
async fn send_keyboard(&self, event: KeyboardEvent) -> Result<()> {
|
||||
let usb_key = event.key.to_hid_usage();
|
||||
|
||||
// Handle modifier keys separately
|
||||
if event.key.is_modifier() {
|
||||
let mut state = self.keyboard_state.lock();
|
||||
|
||||
@@ -1084,7 +930,6 @@ impl HidBackend for OtgBackend {
|
||||
} else {
|
||||
let mut state = self.keyboard_state.lock();
|
||||
|
||||
// Update modifiers from event
|
||||
state.modifiers = event.modifiers.to_hid_byte();
|
||||
|
||||
match event.event_type {
|
||||
@@ -1110,15 +955,12 @@ impl HidBackend for OtgBackend {
|
||||
|
||||
match event.event_type {
|
||||
MouseEventType::Move => {
|
||||
// Relative movement - use hidg1
|
||||
let dx = event.x.clamp(-127, 127) as i8;
|
||||
let dy = event.y.clamp(-127, 127) as i8;
|
||||
self.send_mouse_report_relative(buttons, dx, dy, 0)?;
|
||||
}
|
||||
MouseEventType::MoveAbs => {
|
||||
// Absolute movement - use hidg2
|
||||
// Frontend sends 0-32767 range directly (standard HID absolute mouse range)
|
||||
// Don't send button state with move - buttons are handled separately on relative device
|
||||
// Coordinates 0–32767; buttons are sent only on the relative endpoint.
|
||||
let x = event.x.clamp(0, 32767) as u16;
|
||||
let y = event.y.clamp(0, 32767) as u16;
|
||||
self.send_mouse_report_absolute(0, x, y, 0)?;
|
||||
@@ -1127,7 +969,6 @@ impl HidBackend for OtgBackend {
|
||||
if let Some(button) = event.button {
|
||||
let bit = button.to_hid_bit();
|
||||
let new_buttons = self.mouse_buttons.fetch_or(bit, Ordering::Relaxed) | bit;
|
||||
// Send on relative device for button clicks
|
||||
self.send_mouse_report_relative(new_buttons, 0, 0, 0)?;
|
||||
}
|
||||
}
|
||||
@@ -1147,7 +988,6 @@ impl HidBackend for OtgBackend {
|
||||
}
|
||||
|
||||
async fn reset(&self) -> Result<()> {
|
||||
// Reset keyboard
|
||||
{
|
||||
let mut state = self.keyboard_state.lock();
|
||||
state.clear();
|
||||
@@ -1156,7 +996,6 @@ impl HidBackend for OtgBackend {
|
||||
self.send_keyboard_report(&report)?;
|
||||
}
|
||||
|
||||
// Reset mouse
|
||||
self.mouse_buttons.store(0, Ordering::Relaxed);
|
||||
self.send_mouse_report_relative(0, 0, 0, 0)?;
|
||||
self.send_mouse_report_absolute(0, 0, 0, 0)?;
|
||||
@@ -1168,16 +1007,13 @@ impl HidBackend for OtgBackend {
|
||||
async fn shutdown(&self) -> Result<()> {
|
||||
self.stop_runtime_worker();
|
||||
|
||||
// Reset before closing
|
||||
self.reset().await?;
|
||||
|
||||
// Close devices
|
||||
*self.keyboard_dev.lock() = None;
|
||||
*self.mouse_rel_dev.lock() = None;
|
||||
*self.mouse_abs_dev.lock() = None;
|
||||
*self.consumer_dev.lock() = None;
|
||||
|
||||
// Gadget cleanup is handled by OtgService, not here
|
||||
self.initialized.store(false, Ordering::Relaxed);
|
||||
self.online.store(false, Ordering::Relaxed);
|
||||
self.clear_error();
|
||||
@@ -1199,31 +1035,18 @@ impl HidBackend for OtgBackend {
|
||||
self.send_consumer_report(event.usage)
|
||||
}
|
||||
|
||||
fn set_screen_resolution(&mut self, width: u32, height: u32) {
|
||||
fn set_screen_resolution(&self, width: u32, height: u32) {
|
||||
*self.screen_resolution.write() = Some((width, height));
|
||||
self.notify_runtime_changed();
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if OTG HID gadget is available
|
||||
pub fn is_otg_available() -> bool {
|
||||
// Check for existing HID devices (they should be created by OtgService)
|
||||
let kb = PathBuf::from("/dev/hidg0");
|
||||
let mouse_rel = PathBuf::from("/dev/hidg1");
|
||||
let mouse_abs = PathBuf::from("/dev/hidg2");
|
||||
|
||||
kb.exists() || mouse_rel.exists() || mouse_abs.exists()
|
||||
}
|
||||
|
||||
/// Implement Drop for OtgBackend to close device files
|
||||
impl Drop for OtgBackend {
|
||||
fn drop(&mut self) {
|
||||
self.runtime_worker_stop.store(true, Ordering::Relaxed);
|
||||
if let Some(handle) = self.runtime_worker.get_mut().take() {
|
||||
let _ = handle.join();
|
||||
}
|
||||
// Close device files
|
||||
// Note: Gadget cleanup is handled by OtgService, not here
|
||||
*self.keyboard_dev.lock() = None;
|
||||
*self.mouse_rel_dev.lock() = None;
|
||||
*self.mouse_abs_dev.lock() = None;
|
||||
@@ -1236,12 +1059,6 @@ impl Drop for OtgBackend {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_otg_availability_check() {
|
||||
// This just tests the function runs without panicking
|
||||
let _available = is_otg_available();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_led_state() {
|
||||
let state = LedState::from_byte(0b00000011);
|
||||
@@ -1254,7 +1071,6 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_report_sizes() {
|
||||
// Keyboard report is 8 bytes
|
||||
let kb_report = KeyboardReport::default();
|
||||
assert_eq!(kb_report.to_bytes().len(), 8);
|
||||
}
|
||||
|
||||
@@ -1,50 +1,37 @@
|
||||
//! HID event types for keyboard and mouse
|
||||
//! Keyboard/mouse/consumer structs (`KeyboardEvent`, `MouseEvent`, …).
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::keyboard::CanonicalKey;
|
||||
|
||||
/// Keyboard event type
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum KeyEventType {
|
||||
/// Key pressed down
|
||||
Down,
|
||||
/// Key released
|
||||
Up,
|
||||
}
|
||||
|
||||
/// Keyboard modifier flags
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct KeyboardModifiers {
|
||||
/// Left Control
|
||||
#[serde(default)]
|
||||
pub left_ctrl: bool,
|
||||
/// Left Shift
|
||||
#[serde(default)]
|
||||
pub left_shift: bool,
|
||||
/// Left Alt
|
||||
#[serde(default)]
|
||||
pub left_alt: bool,
|
||||
/// Left Meta (Windows/Super key)
|
||||
#[serde(default)]
|
||||
pub left_meta: bool,
|
||||
/// Right Control
|
||||
#[serde(default)]
|
||||
pub right_ctrl: bool,
|
||||
/// Right Shift
|
||||
#[serde(default)]
|
||||
pub right_shift: bool,
|
||||
/// Right Alt (AltGr)
|
||||
#[serde(default)]
|
||||
pub right_alt: bool,
|
||||
/// Right Meta
|
||||
#[serde(default)]
|
||||
pub right_meta: bool,
|
||||
}
|
||||
|
||||
impl KeyboardModifiers {
|
||||
/// Convert to USB HID modifier byte
|
||||
pub fn to_hid_byte(&self) -> u8 {
|
||||
let mut byte = 0u8;
|
||||
if self.left_ctrl {
|
||||
@@ -74,7 +61,6 @@ impl KeyboardModifiers {
|
||||
byte
|
||||
}
|
||||
|
||||
/// Create from USB HID modifier byte
|
||||
pub fn from_hid_byte(byte: u8) -> Self {
|
||||
Self {
|
||||
left_ctrl: byte & 0x01 != 0,
|
||||
@@ -88,7 +74,6 @@ impl KeyboardModifiers {
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if any modifier is active
|
||||
pub fn any(&self) -> bool {
|
||||
self.left_ctrl
|
||||
|| self.left_shift
|
||||
@@ -101,21 +86,16 @@ impl KeyboardModifiers {
|
||||
}
|
||||
}
|
||||
|
||||
/// Keyboard event
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct KeyboardEvent {
|
||||
/// Event type (down/up)
|
||||
#[serde(rename = "type")]
|
||||
pub event_type: KeyEventType,
|
||||
/// Canonical keyboard key identifier shared across frontend and backend
|
||||
pub key: CanonicalKey,
|
||||
/// Modifier keys state
|
||||
#[serde(default)]
|
||||
pub modifiers: KeyboardModifiers,
|
||||
}
|
||||
|
||||
impl KeyboardEvent {
|
||||
/// Create a key down event
|
||||
pub fn key_down(key: CanonicalKey, modifiers: KeyboardModifiers) -> Self {
|
||||
Self {
|
||||
event_type: KeyEventType::Down,
|
||||
@@ -124,7 +104,6 @@ impl KeyboardEvent {
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a key up event
|
||||
pub fn key_up(key: CanonicalKey, modifiers: KeyboardModifiers) -> Self {
|
||||
Self {
|
||||
event_type: KeyEventType::Up,
|
||||
@@ -134,7 +113,6 @@ impl KeyboardEvent {
|
||||
}
|
||||
}
|
||||
|
||||
/// Mouse button
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum MouseButton {
|
||||
@@ -146,7 +124,6 @@ pub enum MouseButton {
|
||||
}
|
||||
|
||||
impl MouseButton {
|
||||
/// Convert to USB HID button bit
|
||||
pub fn to_hid_bit(&self) -> u8 {
|
||||
match self {
|
||||
MouseButton::Left => 0x01,
|
||||
@@ -158,44 +135,31 @@ impl MouseButton {
|
||||
}
|
||||
}
|
||||
|
||||
/// Mouse event type
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum MouseEventType {
|
||||
/// Mouse moved (relative movement)
|
||||
Move,
|
||||
/// Mouse moved (absolute position)
|
||||
MoveAbs,
|
||||
/// Button pressed
|
||||
Down,
|
||||
/// Button released
|
||||
Up,
|
||||
/// Mouse wheel scroll
|
||||
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,
|
||||
}
|
||||
|
||||
impl MouseEvent {
|
||||
/// Create a relative move event
|
||||
pub fn move_rel(dx: i32, dy: i32) -> Self {
|
||||
Self {
|
||||
event_type: MouseEventType::Move,
|
||||
@@ -206,7 +170,6 @@ impl MouseEvent {
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an absolute move event
|
||||
pub fn move_abs(x: i32, y: i32) -> Self {
|
||||
Self {
|
||||
event_type: MouseEventType::MoveAbs,
|
||||
@@ -217,7 +180,6 @@ impl MouseEvent {
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a button down event
|
||||
pub fn button_down(button: MouseButton) -> Self {
|
||||
Self {
|
||||
event_type: MouseEventType::Down,
|
||||
@@ -228,7 +190,6 @@ impl MouseEvent {
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a button up event
|
||||
pub fn button_up(button: MouseButton) -> Self {
|
||||
Self {
|
||||
event_type: MouseEventType::Up,
|
||||
@@ -239,7 +200,6 @@ impl MouseEvent {
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a scroll event
|
||||
pub fn scroll(delta: i8) -> Self {
|
||||
Self {
|
||||
event_type: MouseEventType::Scroll,
|
||||
@@ -251,35 +211,19 @@ impl MouseEvent {
|
||||
}
|
||||
}
|
||||
|
||||
/// Combined HID event (keyboard or mouse)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "device", rename_all = "lowercase")]
|
||||
pub enum HidEvent {
|
||||
Keyboard(KeyboardEvent),
|
||||
Mouse(MouseEvent),
|
||||
Consumer(ConsumerEvent),
|
||||
}
|
||||
|
||||
/// Consumer control event (multimedia keys)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ConsumerEvent {
|
||||
/// Consumer control usage code (e.g., 0x00CD for Play/Pause)
|
||||
pub usage: u16,
|
||||
}
|
||||
|
||||
/// USB HID keyboard report (8 bytes)
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct KeyboardReport {
|
||||
/// Modifier byte
|
||||
pub modifiers: u8,
|
||||
/// Reserved byte
|
||||
pub reserved: u8,
|
||||
/// Key codes (up to 6 simultaneous keys)
|
||||
pub keys: [u8; 6],
|
||||
}
|
||||
|
||||
impl KeyboardReport {
|
||||
/// Convert to bytes for USB HID
|
||||
pub fn to_bytes(&self) -> [u8; 8] {
|
||||
[
|
||||
self.modifiers,
|
||||
@@ -293,7 +237,6 @@ impl KeyboardReport {
|
||||
]
|
||||
}
|
||||
|
||||
/// Add a key to the report
|
||||
pub fn add_key(&mut self, key: u8) -> bool {
|
||||
for slot in &mut self.keys {
|
||||
if *slot == 0 {
|
||||
@@ -304,56 +247,21 @@ impl KeyboardReport {
|
||||
false // All slots full
|
||||
}
|
||||
|
||||
/// Remove a key from the report
|
||||
pub fn remove_key(&mut self, key: u8) {
|
||||
for slot in &mut self.keys {
|
||||
if *slot == key {
|
||||
*slot = 0;
|
||||
}
|
||||
}
|
||||
// Compact the array
|
||||
self.keys.sort_by(|a, b| b.cmp(a));
|
||||
}
|
||||
|
||||
/// Clear all keys
|
||||
pub fn clear(&mut self) {
|
||||
self.modifiers = 0;
|
||||
self.keys = [0; 6];
|
||||
}
|
||||
}
|
||||
|
||||
/// USB HID mouse report
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct MouseReport {
|
||||
/// Button state
|
||||
pub buttons: u8,
|
||||
/// X movement (-127 to 127)
|
||||
pub x: i8,
|
||||
/// Y movement (-127 to 127)
|
||||
pub y: i8,
|
||||
/// Wheel movement (-127 to 127)
|
||||
pub wheel: i8,
|
||||
}
|
||||
|
||||
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]
|
||||
}
|
||||
|
||||
/// Convert to bytes for USB HID (absolute mouse)
|
||||
pub fn to_bytes_absolute(&self, x: u16, y: u16) -> [u8; 6] {
|
||||
[
|
||||
self.buttons,
|
||||
(x & 0xFF) as u8,
|
||||
(x >> 8) as u8,
|
||||
(y & 0xFF) as u8,
|
||||
(y >> 8) as u8,
|
||||
self.wheel as u8,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -1,13 +1,4 @@
|
||||
//! WebSocket HID channel for HTTP/MJPEG mode
|
||||
//!
|
||||
//! This provides an alternative to WebRTC DataChannel for HID input
|
||||
//! when using MJPEG streaming mode.
|
||||
//!
|
||||
//! Uses binary protocol only (same format as DataChannel):
|
||||
//! - Keyboard: [0x01, event_type, key, modifiers] (4 bytes)
|
||||
//! - Mouse: [0x02, event_type, x_lo, x_hi, y_lo, y_hi, button/scroll] (7 bytes)
|
||||
//!
|
||||
//! See datachannel.rs for detailed protocol specification.
|
||||
//! MJPEG mode: HID over WebSocket — same binary framing as [`super::datachannel`] (`0x01`/`0x02`/`0x03`; layout detailed there).
|
||||
|
||||
use axum::{
|
||||
extract::{
|
||||
@@ -24,25 +15,20 @@ use super::datachannel::{parse_hid_message, HidChannelEvent};
|
||||
use crate::state::AppState;
|
||||
use crate::utils::LogThrottler;
|
||||
|
||||
/// 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 {
|
||||
ws.on_upgrade(move |socket| handle_hid_socket(socket, state))
|
||||
}
|
||||
|
||||
/// Handle HID WebSocket connection
|
||||
async fn handle_hid_socket(socket: WebSocket, state: Arc<AppState>) {
|
||||
let (mut sender, mut receiver) = socket.split();
|
||||
// Log throttler for error messages (5 second interval)
|
||||
let log_throttler = LogThrottler::with_secs(5);
|
||||
|
||||
info!("WebSocket HID connection established (binary protocol)");
|
||||
|
||||
// Check if HID controller is available and send initial status
|
||||
let hid_available = state.hid.is_available().await;
|
||||
let initial_response = if hid_available {
|
||||
vec![RESP_OK]
|
||||
@@ -59,17 +45,14 @@ async fn handle_hid_socket(socket: WebSocket, state: Arc<AppState>) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Process incoming messages (binary only)
|
||||
while let Some(msg) = receiver.next().await {
|
||||
match msg {
|
||||
Ok(Message::Binary(data)) => {
|
||||
// Check HID availability before processing each message
|
||||
let hid_available = state.hid.is_available().await;
|
||||
if !hid_available {
|
||||
if log_throttler.should_log("hid_unavailable") {
|
||||
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;
|
||||
@@ -77,15 +60,12 @@ async fn handle_hid_socket(socket: WebSocket, state: Arc<AppState>) {
|
||||
}
|
||||
|
||||
if let Err(e) = handle_binary_message(&data, &state).await {
|
||||
// Log with throttling to avoid spam
|
||||
if log_throttler.should_log("binary_hid_error") {
|
||||
warn!("Binary HID message error: {}", e);
|
||||
}
|
||||
// Don't send error response for every failed message to reduce overhead
|
||||
}
|
||||
}
|
||||
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",
|
||||
@@ -111,7 +91,6 @@ async fn handle_hid_socket(socket: WebSocket, state: Arc<AppState>) {
|
||||
}
|
||||
}
|
||||
|
||||
// Reset HID state to release any held keys/buttons
|
||||
if let Err(e) = state.hid.reset().await {
|
||||
warn!("Failed to reset HID on WebSocket disconnect: {}", e);
|
||||
}
|
||||
@@ -119,7 +98,6 @@ async fn handle_hid_socket(socket: WebSocket, state: Arc<AppState>) {
|
||||
info!("WebSocket HID connection ended");
|
||||
}
|
||||
|
||||
/// Handle binary HID message (same format as DataChannel)
|
||||
async fn handle_binary_message(data: &[u8], state: &AppState) -> Result<(), String> {
|
||||
let event = parse_hid_message(data).ok_or("Invalid binary HID message")?;
|
||||
|
||||
@@ -160,12 +138,10 @@ mod tests {
|
||||
assert_eq!(RESP_OK, 0x00);
|
||||
assert_eq!(RESP_ERR_HID_UNAVAILABLE, 0x01);
|
||||
assert_eq!(RESP_ERR_INVALID_MESSAGE, 0x02);
|
||||
// assert_eq!(RESP_ERR_SEND_FAILED, 0x03); // TODO: fix test
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_keyboard_message_format() {
|
||||
// Keyboard message: [0x01, event_type, key, modifiers]
|
||||
let data = [MSG_KEYBOARD, KB_EVENT_DOWN, 0x04, 0x01]; // 'A' key with left ctrl
|
||||
let event = parse_hid_message(&data);
|
||||
assert!(event.is_some());
|
||||
@@ -173,7 +149,6 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_mouse_message_format() {
|
||||
// Mouse message: [0x02, event_type, x_lo, x_hi, y_lo, y_hi, extra]
|
||||
let data = [MSG_MOUSE, MS_EVENT_MOVE, 0x0A, 0x00, 0xF6, 0xFF, 0x00]; // x=10, y=-10
|
||||
let event = parse_hid_message(&data);
|
||||
assert!(event.is_some());
|
||||
|
||||
Reference in New Issue
Block a user