mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-09-13 02:54:26 +08:00
feat: 初步增加 Windows 支持
This commit is contained in:
@@ -10,29 +10,24 @@
|
||||
|
||||
use async_trait::async_trait;
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::atomic::{AtomicBool, AtomicU16, AtomicU8, Ordering};
|
||||
use std::sync::{mpsc, Arc};
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant};
|
||||
use tokio::sync::watch;
|
||||
use tracing::{info, trace, warn};
|
||||
use tracing::{info, trace};
|
||||
|
||||
use super::backend::{HidBackend, HidBackendRuntimeSnapshot};
|
||||
use super::ch9329_proto::{
|
||||
build_packet, cmd, expected_response_cmd, try_extract_response, ChipInfo, LedStatus, Response,
|
||||
DEFAULT_ADDR, DEFAULT_BAUD_RATE, MAX_PACKET_SIZE,
|
||||
};
|
||||
use super::types::{KeyEventType, KeyboardEvent, KeyboardReport, MouseEvent, MouseEventType};
|
||||
use crate::error::{AppError, Result};
|
||||
use crate::events::LedState;
|
||||
|
||||
const PACKET_HEADER: [u8; 2] = [0x57, 0xAB];
|
||||
|
||||
const DEFAULT_ADDR: u8 = 0x00;
|
||||
|
||||
pub const DEFAULT_BAUD_RATE: u32 = 9600;
|
||||
|
||||
const RESPONSE_TIMEOUT_MS: u64 = 500;
|
||||
|
||||
const MAX_DATA_LEN: usize = 64;
|
||||
|
||||
const CH9329_MOUSE_RESOLUTION: u32 = 4096;
|
||||
|
||||
const PROBE_INTERVAL_MS: u64 = 100;
|
||||
@@ -41,173 +36,6 @@ const RECONNECT_DELAY_MS: u64 = 2000;
|
||||
|
||||
const INIT_WAIT_MS: u64 = 3000;
|
||||
|
||||
pub mod cmd {
|
||||
pub const GET_INFO: u8 = 0x01;
|
||||
pub const SEND_KB_GENERAL_DATA: u8 = 0x02;
|
||||
pub const SEND_KB_MEDIA_DATA: u8 = 0x03;
|
||||
pub const SEND_MS_ABS_DATA: u8 = 0x04;
|
||||
pub const SEND_MS_REL_DATA: u8 = 0x05;
|
||||
pub const SEND_MY_HID_DATA: u8 = 0x06;
|
||||
pub const SET_DEFAULT_CFG: u8 = 0x0C;
|
||||
pub const RESET: u8 = 0x0F;
|
||||
}
|
||||
|
||||
const RESPONSE_SUCCESS_MASK: u8 = 0x80;
|
||||
const RESPONSE_ERROR_MASK: u8 = 0xC0;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(u8)]
|
||||
pub enum Ch9329Error {
|
||||
Success = 0x00,
|
||||
Timeout = 0xE1,
|
||||
InvalidHeader = 0xE2,
|
||||
InvalidCommand = 0xE3,
|
||||
ChecksumError = 0xE4,
|
||||
ParameterError = 0xE5,
|
||||
OperationFailed = 0xE6,
|
||||
}
|
||||
|
||||
impl From<u8> for Ch9329Error {
|
||||
fn from(code: u8) -> Self {
|
||||
match code {
|
||||
0x00 => Ch9329Error::Success,
|
||||
0xE1 => Ch9329Error::Timeout,
|
||||
0xE2 => Ch9329Error::InvalidHeader,
|
||||
0xE3 => Ch9329Error::InvalidCommand,
|
||||
0xE4 => Ch9329Error::ChecksumError,
|
||||
0xE5 => Ch9329Error::ParameterError,
|
||||
0xE6 => Ch9329Error::OperationFailed,
|
||||
_ => Ch9329Error::OperationFailed,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Ch9329Error {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Ch9329Error::Success => write!(f, "Success"),
|
||||
Ch9329Error::Timeout => write!(f, "Serial receive timeout"),
|
||||
Ch9329Error::InvalidHeader => write!(f, "Invalid packet header"),
|
||||
Ch9329Error::InvalidCommand => write!(f, "Invalid command code"),
|
||||
Ch9329Error::ChecksumError => write!(f, "Checksum mismatch"),
|
||||
Ch9329Error::ParameterError => write!(f, "Parameter error"),
|
||||
Ch9329Error::OperationFailed => write!(f, "Operation failed"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct ChipInfo {
|
||||
pub version: String,
|
||||
pub version_raw: u8,
|
||||
pub usb_connected: bool,
|
||||
pub num_lock: bool,
|
||||
pub caps_lock: bool,
|
||||
pub scroll_lock: bool,
|
||||
}
|
||||
|
||||
impl ChipInfo {
|
||||
pub fn from_response(data: &[u8]) -> Option<Self> {
|
||||
if data.len() < 8 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let version_raw = data[0];
|
||||
let version = format!("V{}.{}", version_raw >> 4, version_raw & 0x0F);
|
||||
let usb_connected = data[1] == 0x01;
|
||||
let led_status = data[2];
|
||||
|
||||
Some(Self {
|
||||
version,
|
||||
version_raw,
|
||||
usb_connected,
|
||||
num_lock: (led_status & 0x01) != 0,
|
||||
caps_lock: (led_status & 0x02) != 0,
|
||||
scroll_lock: (led_status & 0x04) != 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct LedStatus {
|
||||
pub num_lock: bool,
|
||||
pub caps_lock: bool,
|
||||
pub scroll_lock: bool,
|
||||
}
|
||||
|
||||
impl From<u8> for LedStatus {
|
||||
fn from(byte: u8) -> Self {
|
||||
Self {
|
||||
num_lock: (byte & 0x01) != 0,
|
||||
caps_lock: (byte & 0x02) != 0,
|
||||
scroll_lock: (byte & 0x04) != 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Response {
|
||||
pub address: u8,
|
||||
pub cmd: u8,
|
||||
pub data: Vec<u8>,
|
||||
pub is_error: bool,
|
||||
pub error_code: Option<Ch9329Error>,
|
||||
}
|
||||
|
||||
impl Response {
|
||||
pub fn parse(bytes: &[u8]) -> Option<Self> {
|
||||
if bytes.len() < 6 {
|
||||
return None;
|
||||
}
|
||||
|
||||
if bytes[0] != PACKET_HEADER[0] || bytes[1] != PACKET_HEADER[1] {
|
||||
return None;
|
||||
}
|
||||
|
||||
let address = bytes[2];
|
||||
let cmd = bytes[3];
|
||||
let len = bytes[4] as usize;
|
||||
|
||||
if bytes.len() < 5 + len + 1 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let expected_checksum = bytes[5 + len];
|
||||
let calculated_checksum = bytes[..5 + len]
|
||||
.iter()
|
||||
.fold(0u8, |acc, &x| acc.wrapping_add(x));
|
||||
|
||||
if expected_checksum != calculated_checksum {
|
||||
warn!(
|
||||
"CH9329 checksum mismatch: expected {:02X}, got {:02X}",
|
||||
expected_checksum, calculated_checksum
|
||||
);
|
||||
return None;
|
||||
}
|
||||
|
||||
let data = bytes[5..5 + len].to_vec();
|
||||
let is_error = (cmd & RESPONSE_ERROR_MASK) == RESPONSE_ERROR_MASK;
|
||||
let error_code = if is_error && !data.is_empty() {
|
||||
Some(Ch9329Error::from(data[0]))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Some(Self {
|
||||
address,
|
||||
cmd,
|
||||
data,
|
||||
is_error,
|
||||
error_code,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_success(&self) -> bool {
|
||||
!self.is_error && (self.data.is_empty() || self.data[0] == Ch9329Error::Success as u8)
|
||||
}
|
||||
}
|
||||
|
||||
const MAX_PACKET_SIZE: usize = 70;
|
||||
|
||||
struct Ch9329RuntimeState {
|
||||
initialized: AtomicBool,
|
||||
@@ -331,6 +159,13 @@ impl Ch9329Backend {
|
||||
}
|
||||
|
||||
pub fn check_port_exists(&self) -> bool {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
return crate::utils::list_serial_ports()
|
||||
.iter()
|
||||
.any(|port| port.eq_ignore_ascii_case(&self.port_path));
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
std::path::Path::new(&self.port_path).exists()
|
||||
}
|
||||
|
||||
@@ -358,39 +193,8 @@ impl Ch9329Backend {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn calculate_checksum(data: &[u8]) -> u8 {
|
||||
data.iter().fold(0u8, |acc, &x| acc.wrapping_add(x))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn build_packet_buf(address: u8, cmd: u8, data: &[u8]) -> ([u8; MAX_PACKET_SIZE], usize) {
|
||||
debug_assert!(
|
||||
data.len() <= MAX_DATA_LEN,
|
||||
"Data too long for CH9329 packet"
|
||||
);
|
||||
|
||||
let len = data.len() as u8;
|
||||
let packet_len = 6 + data.len();
|
||||
let mut packet = [0u8; MAX_PACKET_SIZE];
|
||||
|
||||
packet[0] = PACKET_HEADER[0];
|
||||
packet[1] = PACKET_HEADER[1];
|
||||
packet[2] = address;
|
||||
packet[3] = cmd;
|
||||
packet[4] = len;
|
||||
packet[5..5 + data.len()].copy_from_slice(data);
|
||||
let checksum = Self::calculate_checksum(&packet[..5 + data.len()]);
|
||||
packet[5 + data.len()] = checksum;
|
||||
|
||||
(packet, packet_len)
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
fn open_port(port_path: &str, baud_rate: u32) -> Result<Box<dyn serialport::SerialPort>> {
|
||||
#[cfg(not(windows))]
|
||||
if !std::path::Path::new(port_path).exists() {
|
||||
return Err(Self::backend_error(
|
||||
format!("Serial port {} not found", port_path),
|
||||
@@ -410,46 +214,13 @@ impl Ch9329Backend {
|
||||
cmd: u8,
|
||||
data: &[u8],
|
||||
) -> Result<()> {
|
||||
let packet = Self::build_packet(address, cmd, data);
|
||||
let packet = build_packet(address, cmd, data);
|
||||
port.write_all(&packet).map_err(|e| {
|
||||
Self::backend_error(format!("Failed to write to CH9329: {}", e), "write_failed")
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn try_extract_response(buffer: &[u8]) -> Option<(Response, usize)> {
|
||||
let mut offset = 0;
|
||||
while offset + 6 <= buffer.len() {
|
||||
if buffer[offset] != PACKET_HEADER[0] || buffer[offset + 1] != PACKET_HEADER[1] {
|
||||
offset += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
let len = buffer[offset + 4] as usize;
|
||||
let frame_len = 6 + len;
|
||||
if offset + frame_len > buffer.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let frame = &buffer[offset..offset + frame_len];
|
||||
if let Some(response) = Response::parse(frame) {
|
||||
return Some((response, offset + frame_len));
|
||||
}
|
||||
|
||||
offset += 1;
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn expected_response_cmd(cmd: u8, is_error: bool) -> u8 {
|
||||
cmd | if is_error {
|
||||
RESPONSE_ERROR_MASK
|
||||
} else {
|
||||
RESPONSE_SUCCESS_MASK
|
||||
}
|
||||
}
|
||||
|
||||
fn xfer_packet(
|
||||
port: &mut dyn serialport::SerialPort,
|
||||
address: u8,
|
||||
@@ -460,8 +231,8 @@ impl Ch9329Backend {
|
||||
|
||||
let mut pending = Vec::with_capacity(128);
|
||||
let deadline = Instant::now() + Duration::from_millis(RESPONSE_TIMEOUT_MS);
|
||||
let expected_ok = Self::expected_response_cmd(cmd, false);
|
||||
let expected_err = Self::expected_response_cmd(cmd, true);
|
||||
let expected_ok = expected_response_cmd(cmd, false);
|
||||
let expected_err = expected_response_cmd(cmd, true);
|
||||
|
||||
loop {
|
||||
let mut chunk = [0u8; 128];
|
||||
@@ -469,7 +240,7 @@ impl Ch9329Backend {
|
||||
Ok(n) if n > 0 => {
|
||||
pending.extend_from_slice(&chunk[..n]);
|
||||
|
||||
while let Some((response, consumed)) = Self::try_extract_response(&pending) {
|
||||
while let Some((response, consumed)) = try_extract_response(&pending) {
|
||||
pending.drain(..consumed);
|
||||
if response.cmd == expected_ok || response.cmd == expected_err {
|
||||
return Ok(response);
|
||||
@@ -1023,7 +794,14 @@ impl HidBackend for Ch9329Backend {
|
||||
let mut online = initialized && self.runtime.online.load(Ordering::Relaxed);
|
||||
let mut error = self.runtime.last_error.read().clone();
|
||||
|
||||
if initialized && !self.check_port_exists() {
|
||||
#[cfg(windows)]
|
||||
let port_still_present = crate::utils::list_serial_ports()
|
||||
.iter()
|
||||
.any(|port| port.eq_ignore_ascii_case(&self.port_path));
|
||||
#[cfg(not(windows))]
|
||||
let port_still_present = self.check_port_exists();
|
||||
|
||||
if initialized && !port_still_present {
|
||||
online = false;
|
||||
error = Some((
|
||||
format!("Serial port {} not found", self.port_path),
|
||||
@@ -1066,14 +844,15 @@ impl HidBackend for Ch9329Backend {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use super::ch9329_proto::{build_packet, calculate_checksum};
|
||||
|
||||
#[test]
|
||||
fn test_packet_building() {
|
||||
let packet = Ch9329Backend::build_packet(DEFAULT_ADDR, cmd::GET_INFO, &[]);
|
||||
let packet = build_packet(DEFAULT_ADDR, cmd::GET_INFO, &[]);
|
||||
assert_eq!(packet, vec![0x57, 0xAB, 0x00, 0x01, 0x00, 0x03]);
|
||||
|
||||
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);
|
||||
let packet = build_packet(DEFAULT_ADDR, cmd::SEND_KB_GENERAL_DATA, &data);
|
||||
|
||||
assert_eq!(packet[0], 0x57); // Header
|
||||
assert_eq!(packet[1], 0xAB); // Header
|
||||
@@ -1090,7 +869,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_relative_mouse_packet() {
|
||||
let data = [0x01, 0x00, 50u8, 0x00, 0x00];
|
||||
let packet = Ch9329Backend::build_packet(DEFAULT_ADDR, cmd::SEND_MS_REL_DATA, &data);
|
||||
let packet = build_packet(DEFAULT_ADDR, cmd::SEND_MS_REL_DATA, &data);
|
||||
|
||||
assert_eq!(packet[0], 0x57);
|
||||
assert_eq!(packet[1], 0xAB);
|
||||
@@ -1105,13 +884,13 @@ mod tests {
|
||||
#[test]
|
||||
fn test_checksum_calculation() {
|
||||
let packet = [0x57u8, 0xAB, 0x00, 0x01, 0x00];
|
||||
let checksum = Ch9329Backend::calculate_checksum(&packet);
|
||||
let checksum = calculate_checksum(&packet);
|
||||
assert_eq!(checksum, 0x03);
|
||||
|
||||
let packet = [
|
||||
0x57u8, 0xAB, 0x00, 0x02, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
];
|
||||
let checksum = Ch9329Backend::calculate_checksum(&packet);
|
||||
let checksum = calculate_checksum(&packet);
|
||||
assert_eq!(checksum, 0x10);
|
||||
}
|
||||
|
||||
|
||||
225
src/hid/ch9329_proto.rs
Normal file
225
src/hid/ch9329_proto.rs
Normal file
@@ -0,0 +1,225 @@
|
||||
//! Shared CH9329 protocol types and packet helpers.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
const PACKET_HEADER: [u8; 2] = [0x57, 0xAB];
|
||||
pub const RESPONSE_SUCCESS_MASK: u8 = 0x80;
|
||||
pub const RESPONSE_ERROR_MASK: u8 = 0xC0;
|
||||
|
||||
pub const DEFAULT_ADDR: u8 = 0x00;
|
||||
pub const DEFAULT_BAUD_RATE: u32 = 9600;
|
||||
pub const MAX_DATA_LEN: usize = 64;
|
||||
pub const MAX_PACKET_SIZE: usize = 70;
|
||||
|
||||
pub mod cmd {
|
||||
pub const GET_INFO: u8 = 0x01;
|
||||
pub const SEND_KB_GENERAL_DATA: u8 = 0x02;
|
||||
pub const SEND_KB_MEDIA_DATA: u8 = 0x03;
|
||||
pub const SEND_MS_ABS_DATA: u8 = 0x04;
|
||||
pub const SEND_MS_REL_DATA: u8 = 0x05;
|
||||
pub const RESET: u8 = 0x0F;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(u8)]
|
||||
pub enum Ch9329Error {
|
||||
Success = 0x00,
|
||||
Timeout = 0xE1,
|
||||
InvalidHeader = 0xE2,
|
||||
InvalidCommand = 0xE3,
|
||||
ChecksumError = 0xE4,
|
||||
ParameterError = 0xE5,
|
||||
OperationFailed = 0xE6,
|
||||
}
|
||||
|
||||
impl From<u8> for Ch9329Error {
|
||||
fn from(code: u8) -> Self {
|
||||
match code {
|
||||
0x00 => Ch9329Error::Success,
|
||||
0xE1 => Ch9329Error::Timeout,
|
||||
0xE2 => Ch9329Error::InvalidHeader,
|
||||
0xE3 => Ch9329Error::InvalidCommand,
|
||||
0xE4 => Ch9329Error::ChecksumError,
|
||||
0xE5 => Ch9329Error::ParameterError,
|
||||
0xE6 => Ch9329Error::OperationFailed,
|
||||
_ => Ch9329Error::OperationFailed,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Ch9329Error {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Ch9329Error::Success => write!(f, "Success"),
|
||||
Ch9329Error::Timeout => write!(f, "Serial receive timeout"),
|
||||
Ch9329Error::InvalidHeader => write!(f, "Invalid packet header"),
|
||||
Ch9329Error::InvalidCommand => write!(f, "Invalid command code"),
|
||||
Ch9329Error::ChecksumError => write!(f, "Checksum mismatch"),
|
||||
Ch9329Error::ParameterError => write!(f, "Parameter error"),
|
||||
Ch9329Error::OperationFailed => write!(f, "Operation failed"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct ChipInfo {
|
||||
pub version: String,
|
||||
pub version_raw: u8,
|
||||
pub usb_connected: bool,
|
||||
pub num_lock: bool,
|
||||
pub caps_lock: bool,
|
||||
pub scroll_lock: bool,
|
||||
}
|
||||
|
||||
impl ChipInfo {
|
||||
pub fn from_response(data: &[u8]) -> Option<Self> {
|
||||
if data.len() < 8 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let version_raw = data[0];
|
||||
let version = format!("V{}.{}", version_raw >> 4, version_raw & 0x0F);
|
||||
let usb_connected = data[1] == 0x01;
|
||||
let led_status = data[2];
|
||||
|
||||
Some(Self {
|
||||
version,
|
||||
version_raw,
|
||||
usb_connected,
|
||||
num_lock: (led_status & 0x01) != 0,
|
||||
caps_lock: (led_status & 0x02) != 0,
|
||||
scroll_lock: (led_status & 0x04) != 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct LedStatus {
|
||||
pub num_lock: bool,
|
||||
pub caps_lock: bool,
|
||||
pub scroll_lock: bool,
|
||||
}
|
||||
|
||||
impl From<u8> for LedStatus {
|
||||
fn from(byte: u8) -> Self {
|
||||
Self {
|
||||
num_lock: (byte & 0x01) != 0,
|
||||
caps_lock: (byte & 0x02) != 0,
|
||||
scroll_lock: (byte & 0x04) != 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Response {
|
||||
pub cmd: u8,
|
||||
pub data: Vec<u8>,
|
||||
pub is_error: bool,
|
||||
pub error_code: Option<Ch9329Error>,
|
||||
}
|
||||
|
||||
impl Response {
|
||||
pub fn parse(bytes: &[u8]) -> Option<Self> {
|
||||
if bytes.len() < 6 || bytes[0] != PACKET_HEADER[0] || bytes[1] != PACKET_HEADER[1] {
|
||||
return None;
|
||||
}
|
||||
|
||||
let cmd = bytes[3];
|
||||
let len = bytes[4] as usize;
|
||||
if bytes.len() < 5 + len + 1 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let expected_checksum = bytes[5 + len];
|
||||
let calculated_checksum = bytes[..5 + len]
|
||||
.iter()
|
||||
.fold(0u8, |acc, &x| acc.wrapping_add(x));
|
||||
if expected_checksum != calculated_checksum {
|
||||
tracing::warn!(
|
||||
"CH9329 checksum mismatch: expected {:02X}, got {:02X}",
|
||||
expected_checksum,
|
||||
calculated_checksum
|
||||
);
|
||||
return None;
|
||||
}
|
||||
|
||||
let data = bytes[5..5 + len].to_vec();
|
||||
let is_error = (cmd & RESPONSE_ERROR_MASK) == RESPONSE_ERROR_MASK;
|
||||
let error_code = if is_error && !data.is_empty() {
|
||||
Some(Ch9329Error::from(data[0]))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Some(Self {
|
||||
cmd,
|
||||
data,
|
||||
is_error,
|
||||
error_code,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn calculate_checksum(data: &[u8]) -> u8 {
|
||||
data.iter().fold(0u8, |acc, &x| acc.wrapping_add(x))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn build_packet_buf(address: u8, cmd: u8, data: &[u8]) -> ([u8; MAX_PACKET_SIZE], usize) {
|
||||
debug_assert!(data.len() <= MAX_DATA_LEN, "Data too long for CH9329 packet");
|
||||
|
||||
let len = data.len() as u8;
|
||||
let packet_len = 6 + data.len();
|
||||
let mut packet = [0u8; MAX_PACKET_SIZE];
|
||||
|
||||
packet[0] = PACKET_HEADER[0];
|
||||
packet[1] = PACKET_HEADER[1];
|
||||
packet[2] = address;
|
||||
packet[3] = cmd;
|
||||
packet[4] = len;
|
||||
packet[5..5 + data.len()].copy_from_slice(data);
|
||||
packet[5 + data.len()] = calculate_checksum(&packet[..5 + data.len()]);
|
||||
|
||||
(packet, packet_len)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn build_packet(address: u8, cmd: u8, data: &[u8]) -> Vec<u8> {
|
||||
let (buf, len) = build_packet_buf(address, cmd, data);
|
||||
buf[..len].to_vec()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn expected_response_cmd(cmd: u8, is_error: bool) -> u8 {
|
||||
cmd | if is_error {
|
||||
RESPONSE_ERROR_MASK
|
||||
} else {
|
||||
RESPONSE_SUCCESS_MASK
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_extract_response(buffer: &[u8]) -> Option<(Response, usize)> {
|
||||
let mut offset = 0;
|
||||
while offset + 6 <= buffer.len() {
|
||||
if buffer[offset] != PACKET_HEADER[0] || buffer[offset + 1] != PACKET_HEADER[1] {
|
||||
offset += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
let len = buffer[offset + 4] as usize;
|
||||
let frame_len = 6 + len;
|
||||
if offset + frame_len > buffer.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let frame = &buffer[offset..offset + frame_len];
|
||||
if let Some(response) = Response::parse(frame) {
|
||||
return Some((response, offset + frame_len));
|
||||
}
|
||||
|
||||
offset += 1;
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
80
src/hid/factory.rs
Normal file
80
src/hid/factory.rs
Normal file
@@ -0,0 +1,80 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use tracing::{info, warn};
|
||||
|
||||
use super::{ch9329, HidBackend, HidBackendType};
|
||||
use crate::error::{AppError, Result};
|
||||
#[cfg(unix)]
|
||||
use crate::otg::OtgService;
|
||||
|
||||
pub struct HidBackendFactory {
|
||||
#[cfg(unix)]
|
||||
otg_service: Option<Arc<OtgService>>,
|
||||
}
|
||||
|
||||
impl HidBackendFactory {
|
||||
#[cfg(unix)]
|
||||
pub fn new(otg_service: Option<Arc<OtgService>>) -> Self {
|
||||
Self { otg_service }
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
|
||||
pub async fn create_initialized(
|
||||
&self,
|
||||
backend_type: &HidBackendType,
|
||||
) -> Result<Option<Arc<dyn HidBackend>>> {
|
||||
let backend = match self.create(backend_type).await? {
|
||||
Some(backend) => backend,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
backend.init().await?;
|
||||
Ok(Some(backend))
|
||||
}
|
||||
|
||||
async fn create(&self, backend_type: &HidBackendType) -> Result<Option<Arc<dyn HidBackend>>> {
|
||||
match backend_type {
|
||||
HidBackendType::Otg => self.create_otg_backend().await.map(Some),
|
||||
HidBackendType::Ch9329 { port, baud_rate } => {
|
||||
info!(
|
||||
"Initializing CH9329 HID backend on {} @ {} baud",
|
||||
port, baud_rate
|
||||
);
|
||||
Ok(Some(Arc::new(ch9329::Ch9329Backend::with_baud_rate(
|
||||
port, *baud_rate,
|
||||
)?)))
|
||||
}
|
||||
HidBackendType::None => {
|
||||
warn!("HID backend disabled");
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
async fn create_otg_backend(&self) -> Result<Arc<dyn HidBackend>> {
|
||||
let otg_service = self
|
||||
.otg_service
|
||||
.as_ref()
|
||||
.ok_or_else(|| AppError::Config("OTG backend not available".to_string()))?;
|
||||
|
||||
let handles = otg_service
|
||||
.hid_device_paths()
|
||||
.await
|
||||
.ok_or_else(|| AppError::Config("OTG HID paths are not available".to_string()))?;
|
||||
|
||||
info!("Creating OTG HID backend from device paths");
|
||||
Ok(Arc::new(super::otg::OtgBackend::from_handles(handles)?))
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
async fn create_otg_backend(&self) -> Result<Arc<dyn HidBackend>> {
|
||||
Err(AppError::Config(
|
||||
"OTG HID is only available on Linux".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
162
src/hid/mod.rs
162
src/hid/mod.rs
@@ -1,11 +1,16 @@
|
||||
//! HID path: browser (WebSocket or WebRTC DataChannel) → queue → OTG gadget or CH9329.
|
||||
|
||||
pub mod backend;
|
||||
mod ch9329_proto;
|
||||
pub mod ch9329;
|
||||
pub mod consumer;
|
||||
pub mod datachannel;
|
||||
mod factory;
|
||||
pub mod keyboard;
|
||||
#[cfg(unix)]
|
||||
pub mod otg;
|
||||
#[cfg(unix)]
|
||||
mod otg_device;
|
||||
pub mod types;
|
||||
pub mod websocket;
|
||||
|
||||
@@ -95,7 +100,9 @@ use tracing::{info, warn};
|
||||
|
||||
use crate::error::{AppError, Result};
|
||||
use crate::events::EventBus;
|
||||
#[cfg(unix)]
|
||||
use crate::otg::OtgService;
|
||||
use factory::HidBackendFactory;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::task::JoinHandle;
|
||||
@@ -112,7 +119,7 @@ enum QueuedHidEvent {
|
||||
}
|
||||
|
||||
pub struct HidController {
|
||||
otg_service: Option<Arc<OtgService>>,
|
||||
backend_factory: HidBackendFactory,
|
||||
backend: Arc<RwLock<Option<Arc<dyn HidBackend>>>>,
|
||||
backend_type: Arc<RwLock<HidBackendType>>,
|
||||
events: Arc<tokio::sync::RwLock<Option<Arc<EventBus>>>>,
|
||||
@@ -127,11 +134,33 @@ pub struct HidController {
|
||||
}
|
||||
|
||||
impl HidController {
|
||||
#[cfg(unix)]
|
||||
pub fn new(backend_type: HidBackendType, otg_service: Option<Arc<OtgService>>) -> Self {
|
||||
let (hid_tx, hid_rx) = mpsc::channel(HID_EVENT_QUEUE_CAPACITY);
|
||||
Self {
|
||||
otg_service,
|
||||
backend: Arc::new(RwLock::new(None)),
|
||||
backend_factory: HidBackendFactory::new(otg_service),
|
||||
backend_type: Arc::new(RwLock::new(backend_type.clone())),
|
||||
events: Arc::new(tokio::sync::RwLock::new(None)),
|
||||
runtime_state: Arc::new(RwLock::new(HidRuntimeState::from_backend_type(
|
||||
&backend_type,
|
||||
))),
|
||||
hid_tx,
|
||||
hid_rx: Mutex::new(Some(hid_rx)),
|
||||
pending_move: Arc::new(parking_lot::Mutex::new(None)),
|
||||
pending_move_flag: Arc::new(AtomicBool::new(false)),
|
||||
hid_worker: Mutex::new(None),
|
||||
runtime_worker: Mutex::new(None),
|
||||
backend_available: Arc::new(AtomicBool::new(false)),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
pub fn new(backend_type: HidBackendType) -> Self {
|
||||
let (hid_tx, hid_rx) = mpsc::channel(HID_EVENT_QUEUE_CAPACITY);
|
||||
Self {
|
||||
backend: Arc::new(RwLock::new(None)),
|
||||
backend_factory: HidBackendFactory::new(),
|
||||
backend_type: Arc::new(RwLock::new(backend_type.clone())),
|
||||
events: Arc::new(tokio::sync::RwLock::new(None)),
|
||||
runtime_state: Arc::new(RwLock::new(HidRuntimeState::from_backend_type(
|
||||
@@ -153,51 +182,22 @@ impl HidController {
|
||||
|
||||
pub async fn init(&self) -> Result<()> {
|
||||
let backend_type = self.backend_type.read().await.clone();
|
||||
let backend: Arc<dyn HidBackend> = match backend_type {
|
||||
HidBackendType::Otg => {
|
||||
let otg_service = self
|
||||
.otg_service
|
||||
.as_ref()
|
||||
.ok_or_else(|| AppError::Internal("OtgService not available".into()))?;
|
||||
|
||||
let handles = otg_service.hid_device_paths().await.ok_or_else(|| {
|
||||
AppError::Config("OTG HID paths are not available".to_string())
|
||||
})?;
|
||||
|
||||
info!("Creating OTG HID backend from device paths");
|
||||
Arc::new(otg::OtgBackend::from_handles(handles)?)
|
||||
}
|
||||
HidBackendType::Ch9329 {
|
||||
ref port,
|
||||
baud_rate,
|
||||
} => {
|
||||
info!(
|
||||
"Initializing CH9329 HID backend on {} @ {} baud",
|
||||
port, baud_rate
|
||||
);
|
||||
Arc::new(ch9329::Ch9329Backend::with_baud_rate(port, baud_rate)?)
|
||||
}
|
||||
HidBackendType::None => {
|
||||
warn!("HID backend disabled");
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = backend.init().await {
|
||||
self.backend_available.store(false, Ordering::Release);
|
||||
let error_state = {
|
||||
let backend_type = self.backend_type.read().await.clone();
|
||||
let backend = match self.backend_factory.create_initialized(&backend_type).await {
|
||||
Ok(Some(backend)) => backend,
|
||||
Ok(None) => return Ok(()),
|
||||
Err(error) => {
|
||||
self.backend_available.store(false, Ordering::Release);
|
||||
let current = self.runtime_state.read().await.clone();
|
||||
HidRuntimeState::with_error(
|
||||
let error_state = HidRuntimeState::with_error(
|
||||
&backend_type,
|
||||
¤t,
|
||||
format!("Failed to initialize HID backend: {}", e),
|
||||
format!("Failed to initialize HID backend: {}", error),
|
||||
"init_failed",
|
||||
)
|
||||
};
|
||||
self.apply_runtime_state(error_state).await;
|
||||
return Err(e);
|
||||
}
|
||||
);
|
||||
self.apply_runtime_state(error_state).await;
|
||||
return Err(error);
|
||||
}
|
||||
};
|
||||
|
||||
*self.backend.write().await = Some(backend);
|
||||
self.sync_runtime_state_from_backend().await;
|
||||
@@ -298,73 +298,15 @@ impl HidController {
|
||||
}
|
||||
}
|
||||
|
||||
let new_backend: Option<Arc<dyn HidBackend>> = match new_backend_type {
|
||||
HidBackendType::Otg => {
|
||||
info!("Initializing OTG HID backend");
|
||||
|
||||
let otg_service = match self.otg_service.as_ref() {
|
||||
Some(svc) => svc,
|
||||
None => {
|
||||
warn!("OTG backend requires OtgService, but it's not available");
|
||||
return Err(AppError::Config(
|
||||
"OTG backend not available (OtgService missing)".to_string(),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
match otg_service.hid_device_paths().await {
|
||||
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
|
||||
}
|
||||
},
|
||||
None => {
|
||||
warn!("OTG HID paths are not available");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
HidBackendType::Ch9329 {
|
||||
ref port,
|
||||
baud_rate,
|
||||
} => {
|
||||
info!(
|
||||
"Initializing CH9329 HID backend on {} @ {} baud",
|
||||
port, baud_rate
|
||||
);
|
||||
match ch9329::Ch9329Backend::with_baud_rate(port, baud_rate) {
|
||||
Ok(b) => {
|
||||
let backend = Arc::new(b);
|
||||
match backend.init().await {
|
||||
Ok(_) => Some(backend),
|
||||
Err(e) => {
|
||||
warn!("Failed to initialize CH9329 backend: {}", e);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to create CH9329 backend: {}", e);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
HidBackendType::None => {
|
||||
warn!("HID backend disabled");
|
||||
let new_backend = match self
|
||||
.backend_factory
|
||||
.create_initialized(&new_backend_type)
|
||||
.await
|
||||
{
|
||||
Ok(backend) => backend,
|
||||
Err(error) if matches!(&new_backend_type, HidBackendType::None) => return Err(error),
|
||||
Err(error) => {
|
||||
warn!("Failed to initialize HID backend: {}", error);
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
208
src/hid/otg.rs
208
src/hid/otg.rs
@@ -4,12 +4,10 @@
|
||||
//! 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 std::fs::{self, File, OpenOptions};
|
||||
use std::io::{Read, Write};
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
use std::os::unix::io::AsFd;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
|
||||
use std::sync::Arc;
|
||||
@@ -19,6 +17,7 @@ use tokio::sync::watch;
|
||||
use tracing::{debug, info, trace, warn};
|
||||
|
||||
use super::backend::{HidBackend, HidBackendRuntimeSnapshot};
|
||||
use super::otg_device::OtgDeviceIo;
|
||||
use super::types::{
|
||||
ConsumerEvent, KeyEventType, KeyboardEvent, KeyboardReport, MouseEvent, MouseEventType,
|
||||
};
|
||||
@@ -87,7 +86,6 @@ pub struct OtgBackend {
|
||||
last_error: parking_lot::RwLock<Option<(String, String)>>,
|
||||
last_error_log: parking_lot::Mutex<std::time::Instant>,
|
||||
error_count: AtomicU8,
|
||||
eagain_count: AtomicU8,
|
||||
runtime_notify_tx: watch::Sender<()>,
|
||||
runtime_worker_stop: Arc<AtomicBool>,
|
||||
runtime_worker: Mutex<Option<thread::JoinHandle<()>>>,
|
||||
@@ -119,7 +117,6 @@ impl OtgBackend {
|
||||
last_error: parking_lot::RwLock::new(None),
|
||||
last_error_log: parking_lot::Mutex::new(std::time::Instant::now()),
|
||||
error_count: AtomicU8::new(0),
|
||||
eagain_count: AtomicU8::new(0),
|
||||
runtime_notify_tx,
|
||||
runtime_worker_stop: Arc::new(AtomicBool::new(false)),
|
||||
runtime_worker: Mutex::new(None),
|
||||
@@ -179,34 +176,11 @@ impl OtgBackend {
|
||||
|
||||
fn reset_error_count(&self) {
|
||||
self.error_count.store(0, Ordering::Relaxed);
|
||||
self.eagain_count.store(0, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// 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) => {
|
||||
if let Some(revents) = pollfd[0].revents() {
|
||||
if revents.contains(PollFlags::POLLERR) || revents.contains(PollFlags::POLLHUP)
|
||||
{
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::BrokenPipe,
|
||||
"Device error or hangup",
|
||||
));
|
||||
}
|
||||
}
|
||||
file.write_all(data)?;
|
||||
Ok(true)
|
||||
}
|
||||
Ok(0) => {
|
||||
trace!("HID write timeout, dropping data");
|
||||
Ok(false)
|
||||
}
|
||||
Ok(_) => Ok(false),
|
||||
Err(e) => Err(std::io::Error::other(e)),
|
||||
}
|
||||
OtgDeviceIo::write_with_timeout(file, data, HID_WRITE_TIMEOUT_MS)
|
||||
}
|
||||
|
||||
pub fn set_udc_name(&self, udc: &str) {
|
||||
@@ -357,6 +331,32 @@ impl OtgBackend {
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_write_error(
|
||||
&self,
|
||||
dev: &mut Option<File>,
|
||||
err: std::io::Error,
|
||||
operation: &str,
|
||||
device_label: &str,
|
||||
) -> Result<()> {
|
||||
match err.raw_os_error() {
|
||||
Some(108) => {
|
||||
debug!("{} ESHUTDOWN, closing for recovery", device_label);
|
||||
*dev = None;
|
||||
self.record_error(format!("{}: {}", operation, err), "eshutdown");
|
||||
Err(Self::io_error_to_hid_error(err, operation))
|
||||
}
|
||||
Some(11) => {
|
||||
trace!("{} EAGAIN after poll, dropping", device_label);
|
||||
Ok(())
|
||||
}
|
||||
_ => {
|
||||
warn!("{} write error: {}", device_label, err);
|
||||
self.record_error(format!("{}: {}", operation, err), Self::io_error_code(&err));
|
||||
Err(Self::io_error_to_hid_error(err, operation))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
@@ -405,41 +405,12 @@ impl OtgBackend {
|
||||
self.log_throttled_error("HID keyboard write timeout, dropped");
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
let error_code = e.raw_os_error();
|
||||
|
||||
match error_code {
|
||||
Some(108) => {
|
||||
self.eagain_count.store(0, Ordering::Relaxed);
|
||||
debug!("Keyboard ESHUTDOWN, closing for recovery");
|
||||
*dev = None;
|
||||
self.record_error(
|
||||
format!("Failed to write keyboard report: {}", e),
|
||||
"eshutdown",
|
||||
);
|
||||
Err(Self::io_error_to_hid_error(
|
||||
e,
|
||||
"Failed to write keyboard report",
|
||||
))
|
||||
}
|
||||
Some(11) => {
|
||||
trace!("Keyboard EAGAIN after poll, dropping");
|
||||
Ok(())
|
||||
}
|
||||
_ => {
|
||||
self.eagain_count.store(0, Ordering::Relaxed);
|
||||
warn!("Keyboard write error: {}", e);
|
||||
self.record_error(
|
||||
format!("Failed to write keyboard report: {}", e),
|
||||
Self::io_error_code(&e),
|
||||
);
|
||||
Err(Self::io_error_to_hid_error(
|
||||
e,
|
||||
"Failed to write keyboard report",
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => self.handle_write_error(
|
||||
&mut dev,
|
||||
e,
|
||||
"Failed to write keyboard report",
|
||||
"Keyboard",
|
||||
),
|
||||
}
|
||||
} else {
|
||||
Err(AppError::HidError {
|
||||
@@ -468,38 +439,12 @@ impl OtgBackend {
|
||||
Ok(())
|
||||
}
|
||||
Ok(false) => Ok(()),
|
||||
Err(e) => {
|
||||
let error_code = e.raw_os_error();
|
||||
|
||||
match error_code {
|
||||
Some(108) => {
|
||||
self.eagain_count.store(0, Ordering::Relaxed);
|
||||
debug!("Relative mouse ESHUTDOWN, closing for recovery");
|
||||
*dev = None;
|
||||
self.record_error(
|
||||
format!("Failed to write mouse report: {}", e),
|
||||
"eshutdown",
|
||||
);
|
||||
Err(Self::io_error_to_hid_error(
|
||||
e,
|
||||
"Failed to write mouse report",
|
||||
))
|
||||
}
|
||||
Some(11) => Ok(()),
|
||||
_ => {
|
||||
self.eagain_count.store(0, Ordering::Relaxed);
|
||||
warn!("Relative mouse write error: {}", e);
|
||||
self.record_error(
|
||||
format!("Failed to write mouse report: {}", e),
|
||||
Self::io_error_code(&e),
|
||||
);
|
||||
Err(Self::io_error_to_hid_error(
|
||||
e,
|
||||
"Failed to write mouse report",
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => self.handle_write_error(
|
||||
&mut dev,
|
||||
e,
|
||||
"Failed to write mouse report",
|
||||
"Relative mouse",
|
||||
),
|
||||
}
|
||||
} else {
|
||||
Err(AppError::HidError {
|
||||
@@ -534,38 +479,12 @@ impl OtgBackend {
|
||||
Ok(())
|
||||
}
|
||||
Ok(false) => Ok(()),
|
||||
Err(e) => {
|
||||
let error_code = e.raw_os_error();
|
||||
|
||||
match error_code {
|
||||
Some(108) => {
|
||||
self.eagain_count.store(0, Ordering::Relaxed);
|
||||
debug!("Absolute mouse ESHUTDOWN, closing for recovery");
|
||||
*dev = None;
|
||||
self.record_error(
|
||||
format!("Failed to write mouse report: {}", e),
|
||||
"eshutdown",
|
||||
);
|
||||
Err(Self::io_error_to_hid_error(
|
||||
e,
|
||||
"Failed to write mouse report",
|
||||
))
|
||||
}
|
||||
Some(11) => Ok(()),
|
||||
_ => {
|
||||
self.eagain_count.store(0, Ordering::Relaxed);
|
||||
warn!("Absolute mouse write error: {}", e);
|
||||
self.record_error(
|
||||
format!("Failed to write mouse report: {}", e),
|
||||
Self::io_error_code(&e),
|
||||
);
|
||||
Err(Self::io_error_to_hid_error(
|
||||
e,
|
||||
"Failed to write mouse report",
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => self.handle_write_error(
|
||||
&mut dev,
|
||||
e,
|
||||
"Failed to write mouse report",
|
||||
"Absolute mouse",
|
||||
),
|
||||
}
|
||||
} else {
|
||||
Err(AppError::HidError {
|
||||
@@ -597,35 +516,12 @@ impl OtgBackend {
|
||||
Ok(())
|
||||
}
|
||||
Ok(false) => Ok(()),
|
||||
Err(e) => {
|
||||
let error_code = e.raw_os_error();
|
||||
match error_code {
|
||||
Some(108) => {
|
||||
debug!("Consumer control ESHUTDOWN, closing for recovery");
|
||||
*dev = None;
|
||||
self.record_error(
|
||||
format!("Failed to write consumer report: {}", e),
|
||||
"eshutdown",
|
||||
);
|
||||
Err(Self::io_error_to_hid_error(
|
||||
e,
|
||||
"Failed to write consumer report",
|
||||
))
|
||||
}
|
||||
Some(11) => Ok(()),
|
||||
_ => {
|
||||
warn!("Consumer control write error: {}", e);
|
||||
self.record_error(
|
||||
format!("Failed to write consumer report: {}", e),
|
||||
Self::io_error_code(&e),
|
||||
);
|
||||
Err(Self::io_error_to_hid_error(
|
||||
e,
|
||||
"Failed to write consumer report",
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => self.handle_write_error(
|
||||
&mut dev,
|
||||
e,
|
||||
"Failed to write consumer report",
|
||||
"Consumer control",
|
||||
),
|
||||
}
|
||||
} else {
|
||||
Err(AppError::HidError {
|
||||
|
||||
50
src/hid/otg_device.rs
Normal file
50
src/hid/otg_device.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
#[cfg(unix)]
|
||||
use std::fs::{File, OpenOptions};
|
||||
#[cfg(unix)]
|
||||
use std::io::{Read, Write};
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::io::AsFd;
|
||||
#[cfg(unix)]
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[cfg(unix)]
|
||||
use nix::poll::{poll, PollFd, PollFlags, PollTimeout};
|
||||
#[cfg(unix)]
|
||||
use tracing::trace;
|
||||
|
||||
#[cfg(unix)]
|
||||
pub struct OtgDeviceIo;
|
||||
|
||||
#[cfg(unix)]
|
||||
impl OtgDeviceIo {
|
||||
pub fn write_with_timeout(
|
||||
file: &mut File,
|
||||
data: &[u8],
|
||||
timeout_ms: i32,
|
||||
) -> std::io::Result<bool> {
|
||||
let mut pollfd = [PollFd::new(file.as_fd(), PollFlags::POLLOUT)];
|
||||
match poll(&mut pollfd, PollTimeout::from(timeout_ms as u16)) {
|
||||
Ok(1) => {
|
||||
if let Some(revents) = pollfd[0].revents() {
|
||||
if revents.contains(PollFlags::POLLERR) || revents.contains(PollFlags::POLLHUP)
|
||||
{
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::BrokenPipe,
|
||||
"Device error or hangup",
|
||||
));
|
||||
}
|
||||
}
|
||||
file.write_all(data)?;
|
||||
Ok(true)
|
||||
}
|
||||
Ok(0) => {
|
||||
trace!("HID write timeout, dropping data");
|
||||
Ok(false)
|
||||
}
|
||||
Ok(_) => Ok(false),
|
||||
Err(e) => Err(std::io::Error::other(e)),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user