feat: 新增 Linux 绝对鼠标兼容模式 #266;新增 CH9329 描述符设置

This commit is contained in:
mofeng-git
2026-06-14 20:59:23 +08:00
parent da61644dbc
commit 5c98aea7e3
21 changed files with 1403 additions and 105 deletions

View File

@@ -34,6 +34,38 @@ impl Default for OtgDescriptorConfig {
}
}
#[typeshare]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Ch9329DescriptorConfig {
pub vendor_id: u16,
pub product_id: u16,
pub manufacturer: String,
pub product: String,
pub serial_number: Option<String>,
}
impl Default for Ch9329DescriptorConfig {
fn default() -> Self {
Self {
vendor_id: 0x1a86,
product_id: 0xe129,
manufacturer: "WCH.CN".to_string(),
product: "CH9329".to_string(),
serial_number: None,
}
}
}
#[typeshare]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Ch9329DescriptorState {
pub descriptor: Ch9329DescriptorConfig,
pub manufacturer_enabled: bool,
pub product_enabled: bool,
pub serial_enabled: bool,
pub config_mode_available: bool,
}
#[typeshare]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
@@ -191,6 +223,10 @@ pub struct HidConfig {
pub otg_keyboard_leds: bool,
pub ch9329_port: String,
pub ch9329_baudrate: u32,
#[serde(default)]
pub ch9329_hybrid_mouse: bool,
#[serde(default)]
pub ch9329_descriptor: Ch9329DescriptorConfig,
pub mouse_absolute: bool,
}
@@ -206,6 +242,8 @@ impl Default for HidConfig {
otg_keyboard_leds: false,
ch9329_port: "/dev/ttyUSB0".to_string(),
ch9329_baudrate: 9600,
ch9329_hybrid_mouse: false,
ch9329_descriptor: Ch9329DescriptorConfig::default(),
mouse_absolute: true,
}
}

View File

@@ -363,7 +363,7 @@ mod tests {
fn parse_cpu_model_from_model_name_field() {
let input = "processor\t: 0\nmodel name\t: Intel(R) Xeon(R)\n";
assert_eq!(
parse_cpu_model_from_cpuinfo_content(input),
parse_cpu_model_from_cpuinfo_content(Some(input)),
Some("Intel(R) Xeon(R)".to_string())
);
}
@@ -372,7 +372,7 @@ mod tests {
fn parse_cpu_model_from_model_field() {
let input = "processor\t: 0\nModel\t\t: Raspberry Pi 4 Model B Rev 1.4\n";
assert_eq!(
parse_cpu_model_from_cpuinfo_content(input),
parse_cpu_model_from_cpuinfo_content(Some(input)),
Some("Raspberry Pi 4 Model B Rev 1.4".to_string())
);
}

View File

@@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize};
use tokio::sync::watch;
use super::types::{ConsumerEvent, KeyboardEvent, MouseEvent};
use crate::config::{Ch9329DescriptorConfig, Ch9329DescriptorState};
use crate::error::Result;
use crate::events::LedState;
@@ -21,6 +22,8 @@ pub enum HidBackendType {
port: String,
#[serde(default = "default_ch9329_baud_rate")]
baud_rate: u32,
#[serde(default)]
hybrid_mouse: bool,
},
#[default]
None,
@@ -63,6 +66,21 @@ pub trait HidBackend: Send + Sync {
))
}
async fn apply_ch9329_descriptor(
&self,
_descriptor: &Ch9329DescriptorConfig,
) -> Result<Ch9329DescriptorState> {
Err(crate::error::AppError::BadRequest(
"CH9329 descriptor configuration is not supported by this backend".to_string(),
))
}
async fn read_ch9329_descriptor(&self) -> Result<Ch9329DescriptorState> {
Err(crate::error::AppError::BadRequest(
"CH9329 descriptor reading is not supported by this backend".to_string(),
))
}
async fn reset(&self) -> Result<()>;
async fn shutdown(&self) -> Result<()>;

View File

@@ -14,7 +14,7 @@ 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 tokio::sync::{oneshot, watch};
use tracing::{info, trace, warn};
use super::backend::{HidBackend, HidBackendRuntimeSnapshot};
@@ -23,6 +23,7 @@ use super::ch9329_proto::{
DEFAULT_ADDR, DEFAULT_BAUD_RATE, MAX_PACKET_SIZE,
};
use super::types::{KeyEventType, KeyboardEvent, KeyboardReport, MouseEvent, MouseEventType};
use crate::config::{Ch9329DescriptorConfig, Ch9329DescriptorState};
use crate::error::{AppError, Result};
use crate::events::LedState;
@@ -38,6 +39,101 @@ const INIT_WAIT_MS: u64 = 3000;
const RECONNECT_COMMAND_POLL_MS: u64 = 100;
const PARAM_CFG_LEN: usize = 50;
const PARAM_CFG_VID_PID_OFFSET: usize = 11;
const PARAM_CFG_STRING_FLAGS_OFFSET: usize = 36;
const DESCRIPTOR_READ_RETRIES: usize = 3;
const DESCRIPTOR_RETRY_DELAY_MS: u64 = 80;
const DESCRIPTOR_APPLY_RESET_WAIT_MS: u64 = 3000;
const USB_STRING_MAX_LEN: usize = 23;
const USB_STRING_FLAG_ENABLE: u8 = 0x80;
const USB_STRING_FLAG_MANUFACTURER: u8 = 0x04;
const USB_STRING_FLAG_PRODUCT: u8 = 0x02;
const USB_STRING_FLAG_SERIAL: u8 = 0x01;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum UsbStringType {
Manufacturer = 0x00,
Product = 0x01,
Serial = 0x02,
}
impl UsbStringType {
fn as_u8(self) -> u8 {
self as u8
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct ParameterConfig {
bytes: [u8; PARAM_CFG_LEN],
}
impl ParameterConfig {
fn from_response(data: &[u8]) -> Result<Self> {
let bytes: [u8; PARAM_CFG_LEN] = data.try_into().map_err(|_| {
Ch9329Backend::backend_error(
"Invalid CH9329 parameter config length",
"invalid_response",
)
})?;
Self::validate_parameter_layout(&bytes)?;
Ok(Self { bytes })
}
fn validate_parameter_layout(bytes: &[u8; PARAM_CFG_LEN]) -> Result<()> {
const VALID_WORK_MODES: [u8; 8] = [0x00, 0x01, 0x02, 0x03, 0x80, 0x81, 0x82, 0x83];
const VALID_SERIAL_MODES: [u8; 6] = [0x00, 0x01, 0x02, 0x80, 0x81, 0x82];
if !VALID_WORK_MODES.contains(&bytes[0]) || !VALID_SERIAL_MODES.contains(&bytes[1]) {
return Err(Ch9329Backend::backend_error(
format!(
"CH9329 did not return parameter config; enter protocol configuration mode by pulling SET low before reading or writing descriptors; response [{}]: {}",
bytes.len(),
Ch9329Backend::hex_bytes(bytes),
),
"invalid_response",
));
}
Ok(())
}
fn set_vid_pid(&mut self, vendor_id: u16, product_id: u16) {
let offset = PARAM_CFG_VID_PID_OFFSET;
self.bytes[offset..offset + 2].copy_from_slice(&vendor_id.to_le_bytes());
self.bytes[offset + 2..offset + 4].copy_from_slice(&product_id.to_le_bytes());
}
fn set_string_flags(&mut self, descriptor: &Ch9329DescriptorConfig) {
let mut flags = self.bytes[PARAM_CFG_STRING_FLAGS_OFFSET] & 0x78;
flags |= USB_STRING_FLAG_ENABLE | USB_STRING_FLAG_MANUFACTURER | USB_STRING_FLAG_PRODUCT;
if descriptor
.serial_number
.as_ref()
.is_some_and(|s| !s.is_empty())
{
flags |= USB_STRING_FLAG_SERIAL;
}
self.bytes[PARAM_CFG_STRING_FLAGS_OFFSET] = flags;
}
fn descriptor_base(&self) -> Ch9329DescriptorConfig {
let offset = PARAM_CFG_VID_PID_OFFSET;
Ch9329DescriptorConfig {
vendor_id: u16::from_le_bytes([self.bytes[offset], self.bytes[offset + 1]]),
product_id: u16::from_le_bytes([self.bytes[offset + 2], self.bytes[offset + 3]]),
manufacturer: String::new(),
product: String::new(),
serial_number: None,
}
}
fn string_flags(&self) -> u8 {
self.bytes[PARAM_CFG_STRING_FLAGS_OFFSET]
}
}
struct Ch9329RuntimeState {
initialized: AtomicBool,
online: AtomicBool,
@@ -109,7 +205,17 @@ impl Ch9329RuntimeState {
}
enum WorkerCommand {
Packet { cmd: u8, data: Vec<u8> },
Packet {
cmd: u8,
data: Vec<u8>,
},
ApplyDescriptor {
descriptor: Ch9329DescriptorConfig,
result_tx: oneshot::Sender<Result<Ch9329DescriptorState>>,
},
ReadDescriptor {
result_tx: oneshot::Sender<Result<Ch9329DescriptorState>>,
},
ResetState,
Shutdown,
}
@@ -128,6 +234,7 @@ pub struct Ch9329Backend {
last_abs_x: Arc<AtomicU16>,
last_abs_y: Arc<AtomicU16>,
relative_mouse_active: Arc<AtomicBool>,
hybrid_mouse: bool,
runtime: Arc<Ch9329RuntimeState>,
}
@@ -137,6 +244,10 @@ impl Ch9329Backend {
}
pub fn with_baud_rate(port_path: &str, baud_rate: u32) -> Result<Self> {
Self::with_options(port_path, baud_rate, false)
}
pub fn with_options(port_path: &str, baud_rate: u32, hybrid_mouse: bool) -> Result<Self> {
Ok(Self {
port_path: port_path.to_string(),
baud_rate,
@@ -151,6 +262,7 @@ impl Ch9329Backend {
last_abs_x: Arc::new(AtomicU16::new(0)),
last_abs_y: Arc::new(AtomicU16::new(0)),
relative_mouse_active: Arc::new(AtomicBool::new(false)),
hybrid_mouse,
runtime: Arc::new(Ch9329RuntimeState::new()),
})
}
@@ -221,10 +333,16 @@ impl Ch9329Backend {
));
}
serialport::new(port_path, baud_rate)
let port = serialport::new(port_path, baud_rate)
.timeout(Duration::from_millis(RESPONSE_TIMEOUT_MS))
.open()
.map_err(|e| Self::serial_error_to_hid_error(port_path, e, "Failed to open serial port"))
.map_err(|e| {
Self::serial_error_to_hid_error(port_path, e, "Failed to open serial port")
})?;
let _ = port.clear(serialport::ClearBuffer::All);
Ok(port)
}
fn write_packet(
@@ -246,6 +364,8 @@ impl Ch9329Backend {
cmd: u8,
data: &[u8],
) -> Result<Response> {
let _ = port.clear(serialport::ClearBuffer::Input);
Self::write_packet(port, address, cmd, data)?;
let mut pending = Vec::with_capacity(128);
@@ -305,11 +425,7 @@ impl Ch9329Backend {
}
}
fn query_chip_info_on_port(
port: &mut dyn serialport::SerialPort,
address: u8,
) -> Result<ChipInfo> {
let response = Self::xfer_packet(port, address, cmd::GET_INFO, &[])?;
fn ensure_success(response: Response) -> Result<Response> {
if response.is_error {
let reason = response
.error_code
@@ -317,11 +433,272 @@ impl Ch9329Backend {
.unwrap_or_else(|| "CH9329 returned error response".to_string());
return Err(Self::backend_error(reason, "protocol_error"));
}
Ok(response)
}
fn query_chip_info_on_port(
port: &mut dyn serialport::SerialPort,
address: u8,
) -> Result<ChipInfo> {
let response = Self::ensure_success(Self::xfer_packet(port, address, cmd::GET_INFO, &[])?)?;
ChipInfo::from_response(&response.data)
.ok_or_else(|| Self::backend_error("Failed to parse chip info", "invalid_response"))
}
fn read_parameter_config_on_port(
port: &mut dyn serialport::SerialPort,
address: u8,
) -> Result<ParameterConfig> {
let mut last_error = None;
for attempt in 0..DESCRIPTOR_READ_RETRIES {
let result =
Self::ensure_success(Self::xfer_packet(port, address, cmd::GET_PARA_CFG, &[])?)
.and_then(|response| ParameterConfig::from_response(&response.data));
match result {
Ok(config) => return Ok(config),
Err(err)
if attempt + 1 < DESCRIPTOR_READ_RETRIES && Self::is_invalid_response(&err) =>
{
last_error = Some(err);
thread::sleep(Duration::from_millis(DESCRIPTOR_RETRY_DELAY_MS));
}
Err(err) => return Err(err),
}
}
Err(last_error.unwrap_or_else(|| {
Self::backend_error("Failed to read CH9329 parameter config", "invalid_response")
}))
}
fn write_parameter_config_on_port(
port: &mut dyn serialport::SerialPort,
address: u8,
config: &ParameterConfig,
) -> Result<()> {
Self::ensure_success(Self::xfer_packet(
port,
address,
cmd::SET_PARA_CFG,
&config.bytes,
)?)?;
Ok(())
}
fn hex_bytes(data: &[u8]) -> String {
let mut out = String::with_capacity(data.len().saturating_mul(3).saturating_sub(1));
for (index, byte) in data.iter().enumerate() {
if index > 0 {
out.push(' ');
}
use std::fmt::Write as _;
let _ = write!(&mut out, "{:02x}", byte);
}
out
}
fn write_usb_string_on_port(
port: &mut dyn serialport::SerialPort,
address: u8,
string_type: UsbStringType,
value: &str,
) -> Result<()> {
let data = Self::build_usb_string_data(string_type, value)?;
Self::ensure_success(Self::xfer_packet(
port,
address,
cmd::SET_USB_STRING,
&data,
)?)?;
Ok(())
}
fn read_usb_string_on_port(
port: &mut dyn serialport::SerialPort,
address: u8,
string_type: UsbStringType,
) -> Result<String> {
let data = [string_type.as_u8()];
let response = Self::ensure_success(Self::xfer_packet(
port,
address,
cmd::GET_USB_STRING,
&data,
)?)?;
Self::parse_usb_string_response(&response.data)
}
fn read_usb_string_with_retry_on_port(
port: &mut dyn serialport::SerialPort,
address: u8,
string_type: UsbStringType,
) -> Result<String> {
let mut last_error = None;
for attempt in 0..DESCRIPTOR_READ_RETRIES {
match Self::read_usb_string_on_port(port, address, string_type) {
Ok(value) => return Ok(value),
Err(err)
if attempt + 1 < DESCRIPTOR_READ_RETRIES && Self::is_invalid_response(&err) =>
{
last_error = Some(err);
thread::sleep(Duration::from_millis(DESCRIPTOR_RETRY_DELAY_MS));
}
Err(err) => return Err(err),
}
}
Err(last_error.unwrap_or_else(|| {
Self::backend_error("Failed to read CH9329 USB string", "invalid_response")
}))
}
fn build_usb_string_data(string_type: UsbStringType, value: &str) -> Result<Vec<u8>> {
let value = value.as_bytes();
if value.len() > USB_STRING_MAX_LEN {
return Err(Self::backend_error(
"CH9329 USB string is too long",
"invalid_config",
));
}
let mut data = Vec::with_capacity(2 + value.len());
data.push(string_type.as_u8());
data.push(value.len() as u8);
data.extend_from_slice(value);
Ok(data)
}
fn parse_usb_string_response(data: &[u8]) -> Result<String> {
if data.len() < 2 {
return Err(Self::backend_error(
"Invalid CH9329 USB string response length",
"invalid_response",
));
}
let len = data[1] as usize;
if data.len() < 2 + len || len > USB_STRING_MAX_LEN {
return Err(Self::backend_error(
"Invalid CH9329 USB string response payload",
"invalid_response",
));
}
String::from_utf8(data[2..2 + len].to_vec()).map_err(|_| {
Self::backend_error("Invalid CH9329 USB string encoding", "invalid_response")
})
}
fn read_device_descriptor_on_port(
port: &mut dyn serialport::SerialPort,
address: u8,
) -> Result<Ch9329DescriptorState> {
let config = Self::read_parameter_config_on_port(port, address)?;
let flags = config.string_flags();
let strings_enabled = flags & USB_STRING_FLAG_ENABLE != 0;
let manufacturer_enabled = strings_enabled && flags & USB_STRING_FLAG_MANUFACTURER != 0;
let product_enabled = strings_enabled && flags & USB_STRING_FLAG_PRODUCT != 0;
let serial_enabled = strings_enabled && flags & USB_STRING_FLAG_SERIAL != 0;
let mut descriptor = config.descriptor_base();
descriptor.manufacturer = if manufacturer_enabled {
Self::read_usb_string_with_retry_on_port(port, address, UsbStringType::Manufacturer)?
} else {
String::new()
};
descriptor.product = if product_enabled {
Self::read_usb_string_with_retry_on_port(port, address, UsbStringType::Product)?
} else {
String::new()
};
descriptor.serial_number = if serial_enabled {
let value =
Self::read_usb_string_with_retry_on_port(port, address, UsbStringType::Serial)?;
if value.is_empty() {
None
} else {
Some(value)
}
} else {
None
};
Ok(Ch9329DescriptorState {
descriptor,
manufacturer_enabled,
product_enabled,
serial_enabled,
config_mode_available: true,
})
}
fn apply_device_descriptor_on_port(
port: &mut dyn serialport::SerialPort,
address: u8,
descriptor: &Ch9329DescriptorConfig,
) -> Result<Ch9329DescriptorState> {
let mut config = Self::read_parameter_config_on_port(&mut *port, address)?;
config.set_vid_pid(descriptor.vendor_id, descriptor.product_id);
config.set_string_flags(descriptor);
Self::write_parameter_config_on_port(&mut *port, address, &config)?;
Self::write_usb_string_on_port(
&mut *port,
address,
UsbStringType::Manufacturer,
&descriptor.manufacturer,
)?;
Self::write_usb_string_on_port(
&mut *port,
address,
UsbStringType::Product,
&descriptor.product,
)?;
if let Some(serial_number) = descriptor.serial_number.as_deref() {
if !serial_number.is_empty() {
Self::write_usb_string_on_port(
&mut *port,
address,
UsbStringType::Serial,
serial_number,
)?;
}
}
Self::try_best_effort_reset(port, address);
thread::sleep(Duration::from_millis(DESCRIPTOR_APPLY_RESET_WAIT_MS));
Self::query_chip_info_on_port(port, address)?;
Ok(Ch9329DescriptorState {
descriptor: descriptor.clone(),
manufacturer_enabled: true,
product_enabled: true,
serial_enabled: descriptor
.serial_number
.as_ref()
.is_some_and(|value| !value.is_empty()),
config_mode_available: true,
})
}
pub fn apply_device_descriptor(
port_path: &str,
baud_rate: u32,
descriptor: &Ch9329DescriptorConfig,
) -> Result<Ch9329DescriptorState> {
let mut port = Self::open_port(port_path, baud_rate)?;
Self::apply_device_descriptor_on_port(port.as_mut(), DEFAULT_ADDR, descriptor)
}
pub fn read_device_descriptor(
port_path: &str,
baud_rate: u32,
) -> Result<Ch9329DescriptorState> {
let mut port = Self::open_port(port_path, baud_rate)?;
Self::read_device_descriptor_on_port(port.as_mut(), DEFAULT_ADDR)
}
fn open_ready_port(
port_path: &str,
baud_rate: u32,
@@ -344,6 +721,41 @@ impl Ch9329Backend {
}
}
fn is_invalid_response(err: &AppError) -> bool {
matches!(
err,
AppError::HidError {
backend,
error_code,
..
} if backend == "ch9329" && error_code == "invalid_response"
)
}
fn should_recover_descriptor_error(err: &AppError) -> bool {
match err {
AppError::HidError {
backend,
error_code,
..
} if backend == "ch9329" => matches!(
error_code.as_str(),
"no_response"
| "write_failed"
| "read_failed"
| "io_error"
| "device_unavailable"
| "serial_error"
| "enxio"
| "enodev"
| "epipe"
| "eshutdown"
),
AppError::HidError { .. } => false,
_ => true,
}
}
fn update_chip_info_cache(
chip_info: &Arc<RwLock<Option<ChipInfo>>>,
led_status: &Arc<RwLock<LedStatus>>,
@@ -398,13 +810,29 @@ impl Ch9329Backend {
Ok(WorkerCommand::Shutdown) | Err(mpsc::RecvTimeoutError::Disconnected) => {
return false;
}
Ok(_) | Err(mpsc::RecvTimeoutError::Timeout) => {}
Ok(command) => Self::reject_command_while_reconnecting(command),
Err(mpsc::RecvTimeoutError::Timeout) => {}
}
}
}
fn reject_command_while_reconnecting(command: WorkerCommand) {
let err = || Self::backend_error("CH9329 is reconnecting", "reconnecting");
match command {
WorkerCommand::ApplyDescriptor { result_tx, .. }
| WorkerCommand::ReadDescriptor { result_tx } => {
let _ = result_tx.send(Err(err()));
}
WorkerCommand::Packet { .. } | WorkerCommand::ResetState | WorkerCommand::Shutdown => {}
}
}
fn release_state_on_port(port: &mut dyn serialport::SerialPort, address: u8) -> Result<()> {
let reset_sequence = [(cmd::SEND_KB_GENERAL_DATA, vec![0; 8])];
let reset_sequence = [
(cmd::SEND_KB_GENERAL_DATA, vec![0; 8]),
(cmd::SEND_MS_REL_DATA, vec![0x01, 0, 0, 0, 0]),
(cmd::SEND_MS_ABS_DATA, vec![0x02, 0, 0, 0, 0, 0, 0]),
];
for (cmd, data) in reset_sequence {
Self::xfer_packet(port, address, cmd, &data)?;
@@ -465,6 +893,39 @@ impl Ch9329Backend {
}
}
fn recover_worker_port(
mut port: Box<dyn serialport::SerialPort>,
rx: &mpsc::Receiver<WorkerCommand>,
port_path: &str,
baud_rate: u32,
address: u8,
chip_info: &Arc<RwLock<Option<ChipInfo>>>,
led_status: &Arc<RwLock<LedStatus>>,
runtime: &Arc<Ch9329RuntimeState>,
) -> Option<Box<dyn serialport::SerialPort>> {
Self::try_best_effort_reset(port.as_mut(), address);
drop(port);
Self::worker_reconnect_loop(
rx, port_path, baud_rate, address, chip_info, led_status, runtime,
)
}
fn finish_oneshot_command<T>(
runtime: &Arc<Ch9329RuntimeState>,
result: Result<T>,
result_tx: oneshot::Sender<Result<T>>,
) -> bool {
let success = result.is_ok();
if let Err(ref err) = result {
Self::record_runtime_error(runtime, err);
}
let _ = result_tx.send(result);
if success {
runtime.set_online();
}
success
}
fn send_keyboard_report(&self, report: &KeyboardReport) -> Result<()> {
let data = report.to_bytes();
self.send_packet(cmd::SEND_KB_GENERAL_DATA, &data)
@@ -503,6 +964,18 @@ impl Ch9329Backend {
Ok(())
}
fn should_send_button_wheel_relative(&self) -> bool {
self.hybrid_mouse || self.relative_mouse_active.load(Ordering::Relaxed)
}
fn absolute_move_buttons(&self, buttons: u8) -> u8 {
if self.hybrid_mouse {
0
} else {
buttons
}
}
fn worker_loop(
port_path: String,
baud_rate: u32,
@@ -552,28 +1025,24 @@ impl Ch9329Backend {
};
loop {
let recover_port = |port| {
Self::recover_worker_port(
port,
&rx,
&port_path,
baud_rate,
address,
&chip_info,
&led_status,
&runtime,
)
};
match rx.recv_timeout(Duration::from_millis(PROBE_INTERVAL_MS)) {
Ok(WorkerCommand::Packet { cmd, data }) => {
if let Err(err) = Self::xfer_packet(port.as_mut(), address, cmd, &data) {
if let AppError::HidError {
reason, error_code, ..
} = err
{
runtime.set_error(reason, error_code);
}
Self::try_best_effort_reset(port.as_mut(), address);
drop(port);
let Some(new_port) = Self::worker_reconnect_loop(
&rx,
&port_path,
baud_rate,
address,
&chip_info,
&led_status,
&runtime,
) else {
Self::record_runtime_error(&runtime, &err);
let Some(new_port) = recover_port(port) else {
break;
};
port = new_port;
@@ -581,6 +1050,36 @@ impl Ch9329Backend {
runtime.set_online();
}
}
Ok(WorkerCommand::ApplyDescriptor {
descriptor,
result_tx,
}) => {
let result =
Self::apply_device_descriptor_on_port(port.as_mut(), address, &descriptor);
let should_recover = result
.as_ref()
.is_err_and(Self::should_recover_descriptor_error);
if !Self::finish_oneshot_command(&runtime, result, result_tx) && should_recover
{
let Some(new_port) = recover_port(port) else {
break;
};
port = new_port;
}
}
Ok(WorkerCommand::ReadDescriptor { result_tx }) => {
let result = Self::read_device_descriptor_on_port(port.as_mut(), address);
let should_recover = result
.as_ref()
.is_err_and(Self::should_recover_descriptor_error);
if !Self::finish_oneshot_command(&runtime, result, result_tx) && should_recover
{
let Some(new_port) = recover_port(port) else {
break;
};
port = new_port;
}
}
Ok(WorkerCommand::ResetState) => {
Self::clear_local_state(
&keyboard_state,
@@ -591,17 +1090,7 @@ impl Ch9329Backend {
);
if let Err(err) = Self::release_state_on_port(port.as_mut(), address) {
Self::record_runtime_error(&runtime, &err);
Self::try_best_effort_reset(port.as_mut(), address);
drop(port);
let Some(new_port) = Self::worker_reconnect_loop(
&rx,
&port_path,
baud_rate,
address,
&chip_info,
&led_status,
&runtime,
) else {
let Some(new_port) = recover_port(port) else {
break;
};
port = new_port;
@@ -619,25 +1108,8 @@ impl Ch9329Backend {
runtime.set_online();
}
Err(err) => {
if let AppError::HidError {
reason, error_code, ..
} = err
{
runtime.set_error(reason, error_code);
}
Self::try_best_effort_reset(port.as_mut(), address);
drop(port);
let Some(new_port) = Self::worker_reconnect_loop(
&rx,
&port_path,
baud_rate,
address,
&chip_info,
&led_status,
&runtime,
) else {
Self::record_runtime_error(&runtime, &err);
let Some(new_port) = recover_port(port) else {
break;
};
port = new_port;
@@ -797,14 +1269,14 @@ impl HidBackend for Ch9329Backend {
let y = ((event.y.clamp(0, 32767) as u32) * CH9329_MOUSE_RESOLUTION / 32768) as u16;
self.last_abs_x.store(x, Ordering::Relaxed);
self.last_abs_y.store(y, Ordering::Relaxed);
self.send_mouse_absolute(buttons, x, y, 0)?;
self.send_mouse_absolute(self.absolute_move_buttons(buttons), x, y, 0)?;
}
MouseEventType::Down => {
if let Some(button) = event.button {
let bit = button.to_hid_bit();
let new_buttons = self.mouse_buttons.fetch_or(bit, Ordering::Relaxed) | bit;
trace!("Mouse down: {:?} buttons=0x{:02X}", button, new_buttons);
if self.relative_mouse_active.load(Ordering::Relaxed) {
if self.should_send_button_wheel_relative() {
self.send_mouse_relative(new_buttons, 0, 0, 0)?;
} else {
let x = self.last_abs_x.load(Ordering::Relaxed);
@@ -818,7 +1290,7 @@ impl HidBackend for Ch9329Backend {
let bit = button.to_hid_bit();
let new_buttons = self.mouse_buttons.fetch_and(!bit, Ordering::Relaxed) & !bit;
trace!("Mouse up: {:?} buttons=0x{:02X}", button, new_buttons);
if self.relative_mouse_active.load(Ordering::Relaxed) {
if self.should_send_button_wheel_relative() {
self.send_mouse_relative(new_buttons, 0, 0, 0)?;
} else {
let x = self.last_abs_x.load(Ordering::Relaxed);
@@ -828,7 +1300,7 @@ impl HidBackend for Ch9329Backend {
}
}
MouseEventType::Scroll => {
if self.relative_mouse_active.load(Ordering::Relaxed) {
if self.should_send_button_wheel_relative() {
self.send_mouse_relative(buttons, 0, 0, event.scroll)?;
} else {
let x = self.last_abs_x.load(Ordering::Relaxed);
@@ -841,6 +1313,28 @@ impl HidBackend for Ch9329Backend {
Ok(())
}
async fn apply_ch9329_descriptor(
&self,
descriptor: &Ch9329DescriptorConfig,
) -> Result<Ch9329DescriptorState> {
let (result_tx, result_rx) = oneshot::channel();
self.enqueue_command(WorkerCommand::ApplyDescriptor {
descriptor: descriptor.clone(),
result_tx,
})?;
result_rx
.await
.map_err(|_| Self::backend_error("CH9329 worker stopped", "worker_stopped"))?
}
async fn read_ch9329_descriptor(&self) -> Result<Ch9329DescriptorState> {
let (result_tx, result_rx) = oneshot::channel();
self.enqueue_command(WorkerCommand::ReadDescriptor { result_tx })?;
result_rx
.await
.map_err(|_| Self::backend_error("CH9329 worker stopped", "worker_stopped"))?
}
async fn reset(&self) -> Result<()> {
{
let mut state = self.keyboard_state.lock();
@@ -854,6 +1348,7 @@ impl HidBackend for Ch9329Backend {
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_relative(0, 0, 0, 0)?;
self.send_mouse_absolute(0, 0, 0, 0)?;
let _ = self.release_media_keys();
@@ -933,8 +1428,8 @@ impl HidBackend for Ch9329Backend {
#[cfg(test)]
mod tests {
use super::ch9329_proto::{build_packet, calculate_checksum};
use super::*;
use crate::hid::ch9329_proto::{build_packet, calculate_checksum, Ch9329Error};
#[test]
fn test_packet_building() {
@@ -1030,4 +1525,143 @@ mod tests {
assert_eq!(Ch9329Error::from(0xE1), Ch9329Error::Timeout);
assert_eq!(Ch9329Error::from(0xE4), Ch9329Error::ChecksumError);
}
#[test]
fn test_parameter_config_updates_vid_pid_and_string_flags() {
let mut raw = [0u8; PARAM_CFG_LEN];
raw[0] = 0x80;
raw[1] = 0x80;
raw[PARAM_CFG_STRING_FLAGS_OFFSET] = 0x7F;
let mut config = ParameterConfig::from_response(&raw).unwrap();
let descriptor = Ch9329DescriptorConfig {
vendor_id: 0x1209,
product_id: 0x9329,
manufacturer: "One-KVM".to_string(),
product: "One-KVM HID".to_string(),
serial_number: Some("ABC123".to_string()),
};
config.set_vid_pid(descriptor.vendor_id, descriptor.product_id);
config.set_string_flags(&descriptor);
assert_eq!(
&config.bytes[PARAM_CFG_VID_PID_OFFSET..PARAM_CFG_VID_PID_OFFSET + 4],
&[0x09, 0x12, 0x29, 0x93]
);
assert_eq!(
config.bytes[PARAM_CFG_STRING_FLAGS_OFFSET],
0x78 | USB_STRING_FLAG_ENABLE
| USB_STRING_FLAG_MANUFACTURER
| USB_STRING_FLAG_PRODUCT
| USB_STRING_FLAG_SERIAL
);
}
#[test]
fn test_parameter_config_disables_custom_serial_when_empty() {
let mut raw = [0u8; PARAM_CFG_LEN];
raw[0] = 0x80;
raw[1] = 0x80;
let mut config = ParameterConfig::from_response(&raw).unwrap();
let descriptor = Ch9329DescriptorConfig {
vendor_id: 0x1a86,
product_id: 0xe129,
manufacturer: "WCH.CN".to_string(),
product: "CH9329".to_string(),
serial_number: None,
};
config.set_string_flags(&descriptor);
assert_eq!(
config.bytes[PARAM_CFG_STRING_FLAGS_OFFSET],
USB_STRING_FLAG_ENABLE | USB_STRING_FLAG_MANUFACTURER | USB_STRING_FLAG_PRODUCT
);
}
#[test]
fn test_parameter_config_parses_vid_pid_and_string_flags() {
let mut raw = [0u8; PARAM_CFG_LEN];
raw[0] = 0x80;
raw[1] = 0x80;
raw[PARAM_CFG_VID_PID_OFFSET..PARAM_CFG_VID_PID_OFFSET + 4]
.copy_from_slice(&[0x86, 0x1a, 0x29, 0xe1]);
raw[PARAM_CFG_STRING_FLAGS_OFFSET] =
USB_STRING_FLAG_ENABLE | USB_STRING_FLAG_MANUFACTURER | USB_STRING_FLAG_SERIAL;
let config = ParameterConfig::from_response(&raw).unwrap();
let descriptor = config.descriptor_base();
assert_eq!(descriptor.vendor_id, 0x1a86);
assert_eq!(descriptor.product_id, 0xe129);
assert_eq!(
config.string_flags(),
USB_STRING_FLAG_ENABLE | USB_STRING_FLAG_MANUFACTURER | USB_STRING_FLAG_SERIAL
);
}
#[test]
fn test_parameter_config_rejects_usb_string_descriptor_response() {
let mut raw = [0u8; PARAM_CFG_LEN];
raw[..24].copy_from_slice(b"W\0C\0H\0 \0U\0A\0R\0T\0 \0T\0O\0 \0");
let err = ParameterConfig::from_response(&raw).unwrap_err();
assert!(err.to_string().contains("protocol configuration mode"));
}
#[test]
fn test_usb_string_data() {
let data = Ch9329Backend::build_usb_string_data(UsbStringType::Product, "One-KVM").unwrap();
assert_eq!(
data,
vec![0x01, 7, b'O', b'n', b'e', b'-', b'K', b'V', b'M']
);
}
#[test]
fn test_usb_string_data_rejects_overlong_value() {
let err = Ch9329Backend::build_usb_string_data(
UsbStringType::Manufacturer,
"x".repeat(24).as_str(),
)
.unwrap_err();
assert!(err.to_string().contains("too long"));
}
#[test]
fn test_usb_string_response_parsing() {
let value = Ch9329Backend::parse_usb_string_response(&[
UsbStringType::Manufacturer.as_u8(),
7,
b'O',
b'n',
b'e',
b'-',
b'K',
b'V',
b'M',
])
.unwrap();
assert_eq!(value, "One-KVM");
}
#[test]
fn test_hybrid_mouse_routes_buttons_and_wheel_to_relative_reports() {
let backend = Ch9329Backend::with_options("/dev/null", DEFAULT_BAUD_RATE, true).unwrap();
assert!(backend.should_send_button_wheel_relative());
assert_eq!(backend.absolute_move_buttons(0x07), 0);
}
#[test]
fn test_default_mouse_mode_preserves_absolute_report_buttons() {
let backend = Ch9329Backend::with_baud_rate("/dev/null", DEFAULT_BAUD_RATE).unwrap();
assert!(!backend.should_send_button_wheel_relative());
assert_eq!(backend.absolute_move_buttons(0x07), 0x07);
}
}

View File

@@ -17,6 +17,10 @@ pub mod cmd {
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 GET_PARA_CFG: u8 = 0x08;
pub const SET_PARA_CFG: u8 = 0x09;
pub const GET_USB_STRING: u8 = 0x0A;
pub const SET_USB_STRING: u8 = 0x0B;
pub const RESET: u8 = 0x0F;
}

View File

@@ -39,13 +39,19 @@ impl HidBackendFactory {
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 } => {
HidBackendType::Ch9329 {
port,
baud_rate,
hybrid_mouse,
} => {
info!(
"Initializing CH9329 HID backend on {} @ {} baud",
port, baud_rate
"Initializing CH9329 HID backend on {} @ {} baud, hybrid_mouse={}",
port, baud_rate, hybrid_mouse
);
Ok(Some(Arc::new(ch9329::Ch9329Backend::with_baud_rate(
port, *baud_rate,
Ok(Some(Arc::new(ch9329::Ch9329Backend::with_options(
port,
*baud_rate,
*hybrid_mouse,
)?)))
}
HidBackendType::None => {

View File

@@ -98,6 +98,7 @@ use std::time::Duration;
use tokio::sync::RwLock;
use tracing::{info, warn};
use crate::config::{Ch9329DescriptorConfig, Ch9329DescriptorState};
use crate::error::{AppError, Result};
use crate::events::EventBus;
#[cfg(unix)]
@@ -287,6 +288,36 @@ impl HidController {
self.runtime_state.read().await.clone()
}
async fn ch9329_backend(&self) -> Result<Arc<dyn HidBackend>> {
if !matches!(
*self.backend_type.read().await,
HidBackendType::Ch9329 { .. }
) {
return Err(AppError::BadRequest(
"Current HID backend is not CH9329".to_string(),
));
}
self.backend
.read()
.await
.clone()
.ok_or_else(|| AppError::BadRequest("CH9329 backend not available".to_string()))
}
pub async fn apply_ch9329_descriptor(
&self,
descriptor: &Ch9329DescriptorConfig,
) -> Result<Ch9329DescriptorState> {
self.ch9329_backend()
.await?
.apply_ch9329_descriptor(descriptor)
.await
}
pub async fn read_ch9329_descriptor(&self) -> Result<Ch9329DescriptorState> {
self.ch9329_backend().await?.read_ch9329_descriptor().await
}
pub async fn reload(&self, new_backend_type: HidBackendType) -> Result<()> {
info!("Reloading HID backend: {:?}", new_backend_type);
self.backend_available.store(false, Ordering::Release);

View File

@@ -305,6 +305,7 @@ async fn main() -> anyhow::Result<()> {
config::HidBackend::Ch9329 => HidBackendType::Ch9329 {
port: config.hid.ch9329_port.clone(),
baud_rate: config.hid.ch9329_baudrate,
hybrid_mouse: config.hid.ch9329_hybrid_mouse,
},
config::HidBackend::None => HidBackendType::None,
};

View File

@@ -315,6 +315,7 @@ async fn build_app_state(
config::HidBackend::Ch9329 => HidBackendType::Ch9329 {
port: config.hid.ch9329_port.clone(),
baud_rate: config.hid.ch9329_baudrate,
hybrid_mouse: config.hid.ch9329_hybrid_mouse,
},
config::HidBackend::None => HidBackendType::None,
};

View File

@@ -33,6 +33,7 @@ fn hid_backend_type(config: &HidConfig) -> crate::hid::HidBackendType {
HidBackend::Ch9329 => crate::hid::HidBackendType::Ch9329 {
port: config.ch9329_port.clone(),
baud_rate: config.ch9329_baudrate,
hybrid_mouse: config.ch9329_hybrid_mouse,
},
HidBackend::None => crate::hid::HidBackendType::None,
}
@@ -179,10 +180,12 @@ pub async fn apply_hid_config(
old_config.effective_otg_keyboard_leds() != new_config.effective_otg_keyboard_leds();
let endpoint_budget_changed =
old_config.resolved_otg_endpoint_limit() != new_config.resolved_otg_endpoint_limit();
let ch9329_runtime_changed = old_config.ch9329_hybrid_mouse != new_config.ch9329_hybrid_mouse;
if old_config.backend == new_config.backend
&& old_config.ch9329_port == new_config.ch9329_port
&& old_config.ch9329_baudrate == new_config.ch9329_baudrate
&& !ch9329_runtime_changed
&& old_config.otg_udc == new_config.otg_udc
&& !descriptor_changed
&& !hid_functions_changed

View File

@@ -1,7 +1,7 @@
use axum::{extract::State, Json};
use std::sync::Arc;
use crate::config::HidConfig;
use crate::config::{HidBackend, HidConfig};
use crate::error::Result;
use crate::state::AppState;
@@ -21,10 +21,20 @@ pub async fn update_hid_config(
let _apply_guard = try_apply_lock(&state.config_apply_locks.otg, "otg")?;
let old_hid_config = state.config.get().hid.clone();
let mut staged_hid_config = old_hid_config.clone();
req.apply_to(&mut staged_hid_config);
let descriptor_update = req
.ch9329_descriptor
.as_ref()
.map(|_| staged_hid_config.ch9329_descriptor.clone());
if descriptor_update.is_some() {
staged_hid_config.ch9329_descriptor = old_hid_config.ch9329_descriptor.clone();
}
state
.config
.update(|config| {
req.apply_to(&mut config.hid);
config.hid = staged_hid_config.clone();
config.enforce_invariants();
})
.await?;
@@ -39,5 +49,21 @@ pub async fn update_hid_config(
)
.await?;
if let Some(descriptor) = descriptor_update {
if new_hid_config.backend != HidBackend::Ch9329 {
return Ok(Json(new_hid_config));
}
let actual = state.hid.apply_ch9329_descriptor(&descriptor).await?;
state
.config
.update(|config| {
config.hid.ch9329_descriptor = actual.descriptor.clone();
config.enforce_invariants();
})
.await?;
return Ok(Json(state.config.get().hid.clone()));
}
Ok(Json(new_hid_config))
}

View File

@@ -292,12 +292,63 @@ impl OtgHidFunctionsUpdate {
}
}
#[typeshare]
#[derive(Debug, Deserialize)]
pub struct Ch9329DescriptorConfigUpdate {
pub vendor_id: Option<u16>,
pub product_id: Option<u16>,
pub manufacturer: Option<String>,
pub product: Option<String>,
pub serial_number: Option<String>,
}
impl Ch9329DescriptorConfigUpdate {
pub fn validate(&self) -> crate::error::Result<()> {
Self::validate_optional_string("Manufacturer", self.manufacturer.as_deref())?;
Self::validate_optional_string("Product", self.product.as_deref())?;
Self::validate_optional_string("Serial number", self.serial_number.as_deref())?;
Ok(())
}
fn validate_optional_string(label: &str, value: Option<&str>) -> crate::error::Result<()> {
if let Some(value) = value {
if value.as_bytes().len() > 23 {
return Err(AppError::BadRequest(format!(
"{} string too long (max 23 bytes for CH9329)",
label
)));
}
}
Ok(())
}
pub fn apply_to(&self, config: &mut Ch9329DescriptorConfig) {
if let Some(v) = self.vendor_id {
config.vendor_id = v;
}
if let Some(v) = self.product_id {
config.product_id = v;
}
if let Some(ref v) = self.manufacturer {
config.manufacturer = v.clone();
}
if let Some(ref v) = self.product {
config.product = v.clone();
}
if let Some(ref v) = self.serial_number {
config.serial_number = if v.is_empty() { None } else { Some(v.clone()) };
}
}
}
#[typeshare]
#[derive(Debug, Deserialize)]
pub struct HidConfigUpdate {
pub backend: Option<HidBackend>,
pub ch9329_port: Option<String>,
pub ch9329_baudrate: Option<u32>,
pub ch9329_hybrid_mouse: Option<bool>,
pub ch9329_descriptor: Option<Ch9329DescriptorConfigUpdate>,
pub otg_udc: Option<String>,
pub otg_descriptor: Option<OtgDescriptorConfigUpdate>,
pub otg_profile: Option<OtgHidProfile>,
@@ -320,6 +371,9 @@ impl HidConfigUpdate {
if let Some(ref desc) = self.otg_descriptor {
desc.validate()?;
}
if let Some(ref desc) = self.ch9329_descriptor {
desc.validate()?;
}
Ok(())
}
@@ -333,6 +387,12 @@ impl HidConfigUpdate {
if let Some(baudrate) = self.ch9329_baudrate {
config.ch9329_baudrate = baudrate;
}
if let Some(enabled) = self.ch9329_hybrid_mouse {
config.ch9329_hybrid_mouse = enabled;
}
if let Some(ref desc) = self.ch9329_descriptor {
desc.apply_to(&mut config.ch9329_descriptor);
}
if let Some(ref udc) = self.otg_udc {
config.otg_udc = Some(udc.clone());
}

View File

@@ -1,4 +1,11 @@
use super::*;
use crate::error::AppError;
#[derive(Deserialize)]
pub struct Ch9329DescriptorQuery {
pub port: Option<String>,
pub baud_rate: Option<u32>,
}
#[derive(Serialize)]
pub struct HidStatus {
@@ -51,3 +58,57 @@ pub async fn hid_reset(State(state): State<Arc<AppState>>) -> Result<Json<LoginR
message: Some("HID state reset".to_string()),
}))
}
/// Read the CH9329 USB descriptor, falling back to the saved config when SET is not low.
pub async fn hid_ch9329_descriptor(
State(state): State<Arc<AppState>>,
Query(query): Query<Ch9329DescriptorQuery>,
) -> Result<Json<crate::config::Ch9329DescriptorState>> {
let config = state.config.get();
let hid = &config.hid;
let port = query.port.as_deref().filter(|port| !port.trim().is_empty());
let baud_rate = query.baud_rate;
let descriptor_result = match (port, baud_rate) {
(Some(port), Some(baud_rate))
if port != hid.ch9329_port || baud_rate != hid.ch9329_baudrate =>
{
crate::hid::ch9329::Ch9329Backend::read_device_descriptor(port, baud_rate)
}
_ => state.hid.read_ch9329_descriptor().await,
};
let descriptor = match descriptor_result {
Ok(descriptor) => descriptor,
Err(err) if is_ch9329_config_mode_unavailable(&err) => cached_ch9329_descriptor(hid),
Err(err) => return Err(err),
};
Ok(Json(descriptor))
}
fn is_ch9329_config_mode_unavailable(err: &AppError) -> bool {
matches!(
err,
AppError::HidError {
backend,
error_code,
..
} if backend == "ch9329" && error_code == "invalid_response"
)
}
fn cached_ch9329_descriptor(
hid: &crate::config::HidConfig,
) -> crate::config::Ch9329DescriptorState {
let descriptor = hid.ch9329_descriptor.clone();
crate::config::Ch9329DescriptorState {
manufacturer_enabled: !descriptor.manufacturer.is_empty(),
product_enabled: !descriptor.product.is_empty(),
serial_enabled: descriptor
.serial_number
.as_ref()
.is_some_and(|value| !value.is_empty()),
config_mode_available: false,
descriptor,
}
}

View File

@@ -170,6 +170,7 @@ pub async fn setup_init(
crate::config::HidBackend::Ch9329 => crate::hid::HidBackendType::Ch9329 {
port: new_config.hid.ch9329_port.clone(),
baud_rate: new_config.hid.ch9329_baudrate,
hybrid_mouse: new_config.hid.ch9329_hybrid_mouse,
},
crate::config::HidBackend::None => crate::hid::HidBackendType::None,
};

View File

@@ -73,6 +73,10 @@ pub fn create_router(state: Arc<AppState>) -> Router {
.route("/webrtc/close", post(handlers::webrtc_close_session))
// HID endpoints
.route("/hid/status", get(handlers::hid_status))
.route(
"/hid/ch9329/descriptor",
get(handlers::hid_ch9329_descriptor),
)
.route("/hid/reset", post(handlers::hid_reset))
// WebSocket HID endpoint (for MJPEG mode)
.route("/ws/hid", any(ws_hid_handler))