mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-06-18 17:51:49 +08:00
1668 lines
57 KiB
Rust
1668 lines
57 KiB
Rust
//! CH9329 over UART — WCH *Serial Communication Protocol V1.0*.
|
|
//! ```text
|
|
//! ┌──────┬──────┬──────┬────────┬──────────────┬──────────┐
|
|
//! │Header│ ADDR │ CMD │ LEN │ DATA │ SUM │
|
|
//! ├──────┼──────┼──────┼────────┼──────────────┼──────────┤
|
|
//! │57 AB │ 00 │ xx │ N │ N bytes │Checksum │
|
|
//! └──────┴──────┴──────┴────────┴──────────────┴──────────┘
|
|
//! ```
|
|
//! Sum of all octets modulo 256 (including header).
|
|
|
|
use async_trait::async_trait;
|
|
use parking_lot::{Mutex, RwLock};
|
|
use std::sync::atomic::{AtomicBool, AtomicU16, AtomicU8, Ordering};
|
|
use std::sync::{mpsc, Arc};
|
|
use std::thread;
|
|
use std::time::{Duration, Instant};
|
|
use tokio::sync::{oneshot, watch};
|
|
use tracing::{info, trace, warn};
|
|
|
|
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::config::{Ch9329DescriptorConfig, Ch9329DescriptorState};
|
|
use crate::error::{AppError, Result};
|
|
use crate::events::LedState;
|
|
|
|
const RESPONSE_TIMEOUT_MS: u64 = 500;
|
|
|
|
const CH9329_MOUSE_RESOLUTION: u32 = 4096;
|
|
|
|
const PROBE_INTERVAL_MS: u64 = 100;
|
|
|
|
const RECONNECT_DELAY_MS: u64 = 2000;
|
|
|
|
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,
|
|
last_error: RwLock<Option<(String, String)>>,
|
|
notify_tx: watch::Sender<()>,
|
|
}
|
|
|
|
impl Ch9329RuntimeState {
|
|
fn new() -> Self {
|
|
let (notify_tx, _notify_rx) = watch::channel(());
|
|
Self {
|
|
initialized: AtomicBool::new(false),
|
|
online: AtomicBool::new(false),
|
|
last_error: RwLock::new(None),
|
|
notify_tx,
|
|
}
|
|
}
|
|
|
|
fn subscribe(&self) -> watch::Receiver<()> {
|
|
self.notify_tx.subscribe()
|
|
}
|
|
|
|
fn notify(&self) {
|
|
let _ = self.notify_tx.send(());
|
|
}
|
|
|
|
fn clear_error(&self) {
|
|
let mut guard = self.last_error.write();
|
|
if guard.is_some() {
|
|
*guard = None;
|
|
self.notify();
|
|
}
|
|
}
|
|
|
|
fn set_online(&self) {
|
|
let was_online = self.online.swap(true, Ordering::Relaxed);
|
|
let mut error = self.last_error.write();
|
|
let cleared_error = error.take().is_some();
|
|
drop(error);
|
|
if !was_online || cleared_error {
|
|
self.notify();
|
|
}
|
|
}
|
|
|
|
fn set_error(&self, reason: impl Into<String>, error_code: impl Into<String>) {
|
|
let reason = reason.into();
|
|
let error_code = error_code.into();
|
|
let was_online = self.online.swap(false, Ordering::Relaxed);
|
|
let mut error = self.last_error.write();
|
|
let changed = error.as_ref() != Some(&(reason.clone(), error_code.clone()));
|
|
*error = Some((reason, error_code));
|
|
drop(error);
|
|
if was_online || changed {
|
|
self.notify();
|
|
}
|
|
}
|
|
|
|
fn set_initialized(&self, initialized: bool) {
|
|
if self.initialized.swap(initialized, Ordering::Relaxed) != initialized {
|
|
self.notify();
|
|
}
|
|
}
|
|
|
|
fn set_offline(&self) {
|
|
if self.online.swap(false, Ordering::Relaxed) {
|
|
self.notify();
|
|
}
|
|
}
|
|
}
|
|
|
|
enum WorkerCommand {
|
|
Packet {
|
|
cmd: u8,
|
|
data: Vec<u8>,
|
|
},
|
|
ApplyDescriptor {
|
|
descriptor: Ch9329DescriptorConfig,
|
|
result_tx: oneshot::Sender<Result<Ch9329DescriptorState>>,
|
|
},
|
|
ReadDescriptor {
|
|
result_tx: oneshot::Sender<Result<Ch9329DescriptorState>>,
|
|
},
|
|
ResetState,
|
|
Shutdown,
|
|
}
|
|
|
|
pub struct Ch9329Backend {
|
|
port_path: String,
|
|
baud_rate: u32,
|
|
worker_tx: Mutex<Option<mpsc::Sender<WorkerCommand>>>,
|
|
worker_handle: Mutex<Option<thread::JoinHandle<()>>>,
|
|
keyboard_state: Arc<Mutex<KeyboardReport>>,
|
|
mouse_buttons: Arc<AtomicU8>,
|
|
screen_resolution: RwLock<(u32, u32)>,
|
|
chip_info: Arc<RwLock<Option<ChipInfo>>>,
|
|
led_status: Arc<RwLock<LedStatus>>,
|
|
address: u8,
|
|
last_abs_x: Arc<AtomicU16>,
|
|
last_abs_y: Arc<AtomicU16>,
|
|
relative_mouse_active: Arc<AtomicBool>,
|
|
hybrid_mouse: bool,
|
|
runtime: Arc<Ch9329RuntimeState>,
|
|
}
|
|
|
|
impl Ch9329Backend {
|
|
pub fn new(port_path: &str) -> Result<Self> {
|
|
Self::with_baud_rate(port_path, DEFAULT_BAUD_RATE)
|
|
}
|
|
|
|
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,
|
|
worker_tx: Mutex::new(None),
|
|
worker_handle: Mutex::new(None),
|
|
keyboard_state: Arc::new(Mutex::new(KeyboardReport::default())),
|
|
mouse_buttons: Arc::new(AtomicU8::new(0)),
|
|
screen_resolution: RwLock::new((1920, 1080)),
|
|
chip_info: Arc::new(RwLock::new(None)),
|
|
led_status: Arc::new(RwLock::new(LedStatus::default())),
|
|
address: DEFAULT_ADDR,
|
|
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()),
|
|
})
|
|
}
|
|
|
|
fn record_error(&self, reason: impl Into<String>, error_code: impl Into<String>) {
|
|
self.runtime.set_error(reason, error_code);
|
|
}
|
|
|
|
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()
|
|
}
|
|
|
|
fn serial_error_to_hid_error(
|
|
port_path: &str,
|
|
e: serialport::Error,
|
|
operation: &str,
|
|
) -> AppError {
|
|
let port_present = {
|
|
#[cfg(windows)]
|
|
{
|
|
crate::utils::list_serial_ports()
|
|
.iter()
|
|
.any(|port| port.eq_ignore_ascii_case(port_path))
|
|
}
|
|
#[cfg(not(windows))]
|
|
{
|
|
std::path::Path::new(port_path).exists()
|
|
}
|
|
};
|
|
|
|
let error_code = match e.kind() {
|
|
serialport::ErrorKind::NoDevice if !port_present => "port_not_found",
|
|
serialport::ErrorKind::NoDevice => "device_unavailable",
|
|
serialport::ErrorKind::InvalidInput => "invalid_config",
|
|
serialport::ErrorKind::Io(_) => "io_error",
|
|
_ => "serial_error",
|
|
};
|
|
|
|
AppError::HidError {
|
|
backend: "ch9329".to_string(),
|
|
reason: format!("{}: {}", operation, e),
|
|
error_code: error_code.to_string(),
|
|
}
|
|
}
|
|
|
|
fn backend_error(reason: impl Into<String>, error_code: impl Into<String>) -> AppError {
|
|
AppError::HidError {
|
|
backend: "ch9329".to_string(),
|
|
reason: reason.into(),
|
|
error_code: error_code.into(),
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
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),
|
|
"port_not_found",
|
|
));
|
|
}
|
|
|
|
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")
|
|
})?;
|
|
|
|
let _ = port.clear(serialport::ClearBuffer::All);
|
|
|
|
Ok(port)
|
|
}
|
|
|
|
fn write_packet(
|
|
port: &mut dyn serialport::SerialPort,
|
|
address: u8,
|
|
cmd: u8,
|
|
data: &[u8],
|
|
) -> Result<()> {
|
|
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 xfer_packet(
|
|
port: &mut dyn serialport::SerialPort,
|
|
address: u8,
|
|
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);
|
|
let deadline = Instant::now() + Duration::from_millis(RESPONSE_TIMEOUT_MS);
|
|
let expected_ok = expected_response_cmd(cmd, false);
|
|
let expected_err = expected_response_cmd(cmd, true);
|
|
|
|
loop {
|
|
let mut chunk = [0u8; 128];
|
|
match port.read(&mut chunk) {
|
|
Ok(n) if n > 0 => {
|
|
pending.extend_from_slice(&chunk[..n]);
|
|
|
|
while let Some((response, consumed)) = try_extract_response(&pending) {
|
|
pending.drain(..consumed);
|
|
if response.cmd == expected_ok || response.cmd == expected_err {
|
|
return Ok(response);
|
|
}
|
|
|
|
trace!(
|
|
"CH9329 ignored out-of-order response: expected 0x{:02X}/0x{:02X}, got 0x{:02X}",
|
|
expected_ok,
|
|
expected_err,
|
|
response.cmd
|
|
);
|
|
}
|
|
|
|
if pending.len() > MAX_PACKET_SIZE * 4 {
|
|
let keep = MAX_PACKET_SIZE;
|
|
pending.drain(..pending.len().saturating_sub(keep));
|
|
}
|
|
}
|
|
Ok(_) => {}
|
|
Err(e) if e.kind() == std::io::ErrorKind::TimedOut => {}
|
|
Err(e) => {
|
|
return Err(Self::backend_error(
|
|
format!("Failed to read from CH9329: {}", e),
|
|
"read_failed",
|
|
));
|
|
}
|
|
}
|
|
|
|
if Instant::now() >= deadline {
|
|
return Err(Self::backend_error(
|
|
format!("No matching response from CH9329 for cmd 0x{:02X}", cmd),
|
|
"no_response",
|
|
));
|
|
}
|
|
|
|
thread::sleep(Duration::from_millis(1));
|
|
}
|
|
}
|
|
|
|
fn try_best_effort_reset(port: &mut dyn serialport::SerialPort, address: u8) {
|
|
if let Err(err) = Self::write_packet(port, address, cmd::RESET, &[]) {
|
|
trace!("CH9329 best-effort reset failed: {}", err);
|
|
}
|
|
}
|
|
|
|
fn ensure_success(response: Response) -> Result<Response> {
|
|
if response.is_error {
|
|
let reason = response
|
|
.error_code
|
|
.map(|e| format!("CH9329 error response: {}", e))
|
|
.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,
|
|
address: u8,
|
|
) -> Result<(Box<dyn serialport::SerialPort>, ChipInfo)> {
|
|
Self::open_port(port_path, baud_rate).and_then(|mut port| {
|
|
let info = Self::query_chip_info_on_port(port.as_mut(), address)?;
|
|
Ok((port, info))
|
|
})
|
|
}
|
|
|
|
fn record_runtime_error(runtime: &Arc<Ch9329RuntimeState>, err: &AppError) {
|
|
if let AppError::HidError {
|
|
reason, error_code, ..
|
|
} = err
|
|
{
|
|
runtime.set_error(reason.clone(), error_code.clone());
|
|
} else {
|
|
runtime.set_error(err.to_string(), "error");
|
|
}
|
|
}
|
|
|
|
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>>,
|
|
info: ChipInfo,
|
|
) -> bool {
|
|
let next_led_status = LedStatus {
|
|
num_lock: info.num_lock,
|
|
caps_lock: info.caps_lock,
|
|
scroll_lock: info.scroll_lock,
|
|
};
|
|
*chip_info.write() = Some(info);
|
|
let mut led_guard = led_status.write();
|
|
let changed = *led_guard != next_led_status;
|
|
*led_guard = next_led_status;
|
|
changed
|
|
}
|
|
|
|
fn enqueue_command(&self, command: WorkerCommand) -> Result<()> {
|
|
let guard = self.worker_tx.lock();
|
|
let Some(sender) = guard.as_ref() else {
|
|
self.record_error("CH9329 worker is not running", "worker_stopped");
|
|
return Err(Self::backend_error(
|
|
"CH9329 worker is not running",
|
|
"worker_stopped",
|
|
));
|
|
};
|
|
|
|
sender.send(command).map_err(|_| {
|
|
self.record_error("CH9329 worker stopped", "worker_stopped");
|
|
Self::backend_error("CH9329 worker stopped", "worker_stopped")
|
|
})
|
|
}
|
|
|
|
fn send_packet(&self, cmd: u8, data: &[u8]) -> Result<()> {
|
|
self.enqueue_command(WorkerCommand::Packet {
|
|
cmd,
|
|
data: data.to_vec(),
|
|
})
|
|
}
|
|
|
|
fn wait_reconnect_delay(rx: &mpsc::Receiver<WorkerCommand>) -> bool {
|
|
let deadline = Instant::now() + Duration::from_millis(RECONNECT_DELAY_MS);
|
|
loop {
|
|
let now = Instant::now();
|
|
if now >= deadline {
|
|
return true;
|
|
}
|
|
|
|
let remaining = deadline.saturating_duration_since(now);
|
|
let timeout = remaining.min(Duration::from_millis(RECONNECT_COMMAND_POLL_MS));
|
|
match rx.recv_timeout(timeout) {
|
|
Ok(WorkerCommand::Shutdown) | Err(mpsc::RecvTimeoutError::Disconnected) => {
|
|
return false;
|
|
}
|
|
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]),
|
|
(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)?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn clear_local_state(
|
|
keyboard_state: &Arc<Mutex<KeyboardReport>>,
|
|
mouse_buttons: &Arc<AtomicU8>,
|
|
last_abs_x: &Arc<AtomicU16>,
|
|
last_abs_y: &Arc<AtomicU16>,
|
|
relative_mouse_active: &Arc<AtomicBool>,
|
|
) {
|
|
keyboard_state.lock().clear();
|
|
mouse_buttons.store(0, Ordering::Relaxed);
|
|
last_abs_x.store(0, Ordering::Relaxed);
|
|
last_abs_y.store(0, Ordering::Relaxed);
|
|
relative_mouse_active.store(false, Ordering::Relaxed);
|
|
}
|
|
|
|
fn worker_reconnect_loop(
|
|
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>> {
|
|
runtime.set_offline();
|
|
loop {
|
|
match Self::open_ready_port(port_path, baud_rate, address) {
|
|
Ok((port, info)) => {
|
|
info!(
|
|
"CH9329 reconnected: {}, USB: {}",
|
|
info.version,
|
|
if info.usb_connected {
|
|
"connected"
|
|
} else {
|
|
"disconnected"
|
|
}
|
|
);
|
|
if Self::update_chip_info_cache(chip_info, led_status, info) {
|
|
runtime.notify();
|
|
}
|
|
runtime.set_online();
|
|
return Some(port);
|
|
}
|
|
Err(err) => {
|
|
Self::record_runtime_error(runtime, &err);
|
|
if !Self::wait_reconnect_delay(rx) {
|
|
return None;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
pub fn send_media_key(&self, data: &[u8]) -> Result<()> {
|
|
if data.len() < 2 || data.len() > 4 {
|
|
return Err(AppError::Internal(
|
|
"Invalid media key data length".to_string(),
|
|
));
|
|
}
|
|
self.send_packet(cmd::SEND_KB_MEDIA_DATA, data)
|
|
}
|
|
|
|
pub fn release_media_keys(&self) -> Result<()> {
|
|
self.send_media_key(&[0x02, 0x00, 0x00, 0x00])
|
|
}
|
|
|
|
fn send_mouse_relative(&self, buttons: u8, dx: i8, dy: i8, wheel: i8) -> Result<()> {
|
|
let data = [0x01, buttons, dx as u8, dy as u8, wheel as u8];
|
|
self.send_packet(cmd::SEND_MS_REL_DATA, &data)
|
|
}
|
|
|
|
fn send_mouse_absolute(&self, buttons: u8, x: u16, y: u16, wheel: i8) -> Result<()> {
|
|
let data = [
|
|
0x02,
|
|
buttons,
|
|
(x & 0xFF) as u8,
|
|
(x >> 8) as u8,
|
|
(y & 0xFF) as u8,
|
|
(y >> 8) as u8,
|
|
wheel as u8,
|
|
];
|
|
self.send_packet(cmd::SEND_MS_ABS_DATA, &data)?;
|
|
trace!("CH9329 mouse: buttons=0x{:02X} pos=({},{})", buttons, x, y);
|
|
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,
|
|
address: u8,
|
|
rx: mpsc::Receiver<WorkerCommand>,
|
|
chip_info: Arc<RwLock<Option<ChipInfo>>>,
|
|
led_status: Arc<RwLock<LedStatus>>,
|
|
runtime: Arc<Ch9329RuntimeState>,
|
|
keyboard_state: Arc<Mutex<KeyboardReport>>,
|
|
mouse_buttons: Arc<AtomicU8>,
|
|
last_abs_x: Arc<AtomicU16>,
|
|
last_abs_y: Arc<AtomicU16>,
|
|
relative_mouse_active: Arc<AtomicBool>,
|
|
init_tx: mpsc::Sender<Result<ChipInfo>>,
|
|
) {
|
|
runtime.set_initialized(true);
|
|
|
|
let mut init_tx = Some(init_tx);
|
|
let mut port = loop {
|
|
match Self::open_ready_port(&port_path, baud_rate, address) {
|
|
Ok((port, info)) => {
|
|
info!(
|
|
"CH9329 serial port opened: {} @ {} baud",
|
|
port_path, baud_rate
|
|
);
|
|
if Self::update_chip_info_cache(&chip_info, &led_status, info.clone()) {
|
|
runtime.notify();
|
|
}
|
|
runtime.set_online();
|
|
if let Some(init_tx) = init_tx.take() {
|
|
let _ = init_tx.send(Ok(info));
|
|
}
|
|
break port;
|
|
}
|
|
Err(err) => {
|
|
Self::record_runtime_error(&runtime, &err);
|
|
if let Some(init_tx) = init_tx.take() {
|
|
let _ = init_tx.send(Err(err));
|
|
}
|
|
if !Self::wait_reconnect_delay(&rx) {
|
|
runtime.set_offline();
|
|
runtime.set_initialized(false);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
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) {
|
|
Self::record_runtime_error(&runtime, &err);
|
|
let Some(new_port) = recover_port(port) else {
|
|
break;
|
|
};
|
|
port = new_port;
|
|
} else {
|
|
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,
|
|
&mouse_buttons,
|
|
&last_abs_x,
|
|
&last_abs_y,
|
|
&relative_mouse_active,
|
|
);
|
|
if let Err(err) = Self::release_state_on_port(port.as_mut(), address) {
|
|
Self::record_runtime_error(&runtime, &err);
|
|
let Some(new_port) = recover_port(port) else {
|
|
break;
|
|
};
|
|
port = new_port;
|
|
} else {
|
|
runtime.set_online();
|
|
}
|
|
}
|
|
Ok(WorkerCommand::Shutdown) => break,
|
|
Err(mpsc::RecvTimeoutError::Timeout) => {
|
|
match Self::query_chip_info_on_port(port.as_mut(), address) {
|
|
Ok(info) => {
|
|
if Self::update_chip_info_cache(&chip_info, &led_status, info) {
|
|
runtime.notify();
|
|
}
|
|
runtime.set_online();
|
|
}
|
|
Err(err) => {
|
|
Self::record_runtime_error(&runtime, &err);
|
|
let Some(new_port) = recover_port(port) else {
|
|
break;
|
|
};
|
|
port = new_port;
|
|
}
|
|
}
|
|
}
|
|
Err(mpsc::RecvTimeoutError::Disconnected) => break,
|
|
}
|
|
}
|
|
|
|
runtime.set_offline();
|
|
runtime.set_initialized(false);
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl HidBackend for Ch9329Backend {
|
|
async fn init(&self) -> Result<()> {
|
|
if self.worker_handle.lock().is_some() {
|
|
return Ok(());
|
|
}
|
|
|
|
let (tx, rx) = mpsc::channel();
|
|
let (init_tx, init_rx) = mpsc::channel();
|
|
let port_path = self.port_path.clone();
|
|
let baud_rate = self.baud_rate;
|
|
let address = self.address;
|
|
let chip_info = self.chip_info.clone();
|
|
let led_status = self.led_status.clone();
|
|
let runtime = self.runtime.clone();
|
|
let keyboard_state = self.keyboard_state.clone();
|
|
let mouse_buttons = self.mouse_buttons.clone();
|
|
let last_abs_x = self.last_abs_x.clone();
|
|
let last_abs_y = self.last_abs_y.clone();
|
|
let relative_mouse_active = self.relative_mouse_active.clone();
|
|
|
|
let handle = thread::Builder::new()
|
|
.name("ch9329-worker".to_string())
|
|
.spawn(move || {
|
|
Self::worker_loop(
|
|
port_path,
|
|
baud_rate,
|
|
address,
|
|
rx,
|
|
chip_info,
|
|
led_status,
|
|
runtime,
|
|
keyboard_state,
|
|
mouse_buttons,
|
|
last_abs_x,
|
|
last_abs_y,
|
|
relative_mouse_active,
|
|
init_tx,
|
|
);
|
|
})
|
|
.map_err(|e| AppError::Internal(format!("Failed to spawn CH9329 worker: {}", e)))?;
|
|
|
|
match init_rx.recv_timeout(Duration::from_millis(INIT_WAIT_MS)) {
|
|
Ok(Ok(info)) => {
|
|
info!(
|
|
"CH9329 chip detected: {}, USB: {}, LEDs: NumLock={}, CapsLock={}, ScrollLock={}",
|
|
info.version,
|
|
if info.usb_connected {
|
|
"connected"
|
|
} else {
|
|
"disconnected"
|
|
},
|
|
info.num_lock,
|
|
info.caps_lock,
|
|
info.scroll_lock
|
|
);
|
|
*self.worker_tx.lock() = Some(tx);
|
|
*self.worker_handle.lock() = Some(handle);
|
|
self.runtime.set_online();
|
|
Ok(())
|
|
}
|
|
Ok(Err(err)) => {
|
|
self.record_error(
|
|
format!(
|
|
"CH9329 not responding on {} @ {} baud: {}",
|
|
self.port_path, self.baud_rate, err
|
|
),
|
|
"init_failed",
|
|
);
|
|
warn!(
|
|
"CH9329 not responding on {} @ {} baud, retrying in background: {}",
|
|
self.port_path, self.baud_rate, err
|
|
);
|
|
*self.worker_tx.lock() = Some(tx);
|
|
*self.worker_handle.lock() = Some(handle);
|
|
Ok(())
|
|
}
|
|
Err(_) => {
|
|
let _ = tx.send(WorkerCommand::Shutdown);
|
|
let _ = handle.join();
|
|
self.record_error("Timed out waiting for CH9329 worker init", "init_timeout");
|
|
Err(AppError::Internal(
|
|
"Timed out waiting for CH9329 initialization".to_string(),
|
|
))
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn send_keyboard(&self, event: KeyboardEvent) -> Result<()> {
|
|
let usb_key = event.key.to_hid_usage();
|
|
|
|
if event.key.is_modifier() {
|
|
let mut state = self.keyboard_state.lock();
|
|
|
|
if let Some(bit) = event.key.modifier_bit() {
|
|
match event.event_type {
|
|
KeyEventType::Down => state.modifiers |= bit,
|
|
KeyEventType::Up => state.modifiers &= !bit,
|
|
}
|
|
}
|
|
|
|
let report = state.clone();
|
|
drop(state);
|
|
|
|
self.send_keyboard_report(&report)?;
|
|
} else {
|
|
let mut state = self.keyboard_state.lock();
|
|
|
|
state.modifiers = event.modifiers.to_hid_byte();
|
|
|
|
match event.event_type {
|
|
KeyEventType::Down => {
|
|
state.add_key(usb_key);
|
|
}
|
|
KeyEventType::Up => {
|
|
state.remove_key(usb_key);
|
|
}
|
|
}
|
|
|
|
let report = state.clone();
|
|
drop(state);
|
|
|
|
self.send_keyboard_report(&report)?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn send_mouse(&self, event: MouseEvent) -> Result<()> {
|
|
let buttons = self.mouse_buttons.load(Ordering::Relaxed);
|
|
|
|
match event.event_type {
|
|
MouseEventType::Move => {
|
|
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 => {
|
|
self.relative_mouse_active.store(false, Ordering::Relaxed);
|
|
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;
|
|
self.last_abs_x.store(x, Ordering::Relaxed);
|
|
self.last_abs_y.store(y, Ordering::Relaxed);
|
|
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.should_send_button_wheel_relative() {
|
|
self.send_mouse_relative(new_buttons, 0, 0, 0)?;
|
|
} else {
|
|
let x = self.last_abs_x.load(Ordering::Relaxed);
|
|
let y = self.last_abs_y.load(Ordering::Relaxed);
|
|
self.send_mouse_absolute(new_buttons, x, y, 0)?;
|
|
}
|
|
}
|
|
}
|
|
MouseEventType::Up => {
|
|
if let Some(button) = event.button {
|
|
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.should_send_button_wheel_relative() {
|
|
self.send_mouse_relative(new_buttons, 0, 0, 0)?;
|
|
} else {
|
|
let x = self.last_abs_x.load(Ordering::Relaxed);
|
|
let y = self.last_abs_y.load(Ordering::Relaxed);
|
|
self.send_mouse_absolute(new_buttons, x, y, 0)?;
|
|
}
|
|
}
|
|
}
|
|
MouseEventType::Scroll => {
|
|
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);
|
|
let y = self.last_abs_y.load(Ordering::Relaxed);
|
|
self.send_mouse_absolute(buttons, x, y, event.scroll)?;
|
|
}
|
|
}
|
|
}
|
|
|
|
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();
|
|
state.clear();
|
|
let report = state.clone();
|
|
drop(state);
|
|
self.send_keyboard_report(&report)?;
|
|
}
|
|
|
|
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_relative(0, 0, 0, 0)?;
|
|
self.send_mouse_absolute(0, 0, 0, 0)?;
|
|
|
|
let _ = self.release_media_keys();
|
|
|
|
info!("CH9329 HID state reset");
|
|
Ok(())
|
|
}
|
|
|
|
async fn shutdown(&self) -> Result<()> {
|
|
let _ = self.enqueue_command(WorkerCommand::ResetState);
|
|
let sender = self.worker_tx.lock().take();
|
|
if let Some(sender) = sender {
|
|
let _ = sender.send(WorkerCommand::Shutdown);
|
|
}
|
|
if let Some(handle) = self.worker_handle.lock().take() {
|
|
let _ = handle.join();
|
|
}
|
|
self.runtime.set_offline();
|
|
self.runtime.set_initialized(false);
|
|
self.runtime.clear_error();
|
|
|
|
info!("CH9329 backend shutdown");
|
|
Ok(())
|
|
}
|
|
|
|
fn runtime_snapshot(&self) -> HidBackendRuntimeSnapshot {
|
|
let initialized = self.runtime.initialized.load(Ordering::Relaxed);
|
|
let mut online = initialized && self.runtime.online.load(Ordering::Relaxed);
|
|
let mut error = self.runtime.last_error.read().clone();
|
|
|
|
#[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),
|
|
"port_not_found".to_string(),
|
|
));
|
|
}
|
|
|
|
HidBackendRuntimeSnapshot {
|
|
initialized,
|
|
online,
|
|
supports_absolute_mouse: true,
|
|
keyboard_leds_enabled: true,
|
|
led_state: {
|
|
let led = *self.led_status.read();
|
|
LedState {
|
|
num_lock: led.num_lock,
|
|
caps_lock: led.caps_lock,
|
|
scroll_lock: led.scroll_lock,
|
|
compose: false,
|
|
kana: false,
|
|
}
|
|
},
|
|
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()),
|
|
}
|
|
}
|
|
|
|
fn subscribe_runtime(&self) -> watch::Receiver<()> {
|
|
self.runtime.subscribe()
|
|
}
|
|
|
|
fn set_screen_resolution(&self, width: u32, height: u32) {
|
|
*self.screen_resolution.write() = (width, height);
|
|
self.runtime.notify();
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::hid::ch9329_proto::{build_packet, calculate_checksum, Ch9329Error};
|
|
|
|
#[test]
|
|
fn test_packet_building() {
|
|
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 = build_packet(DEFAULT_ADDR, cmd::SEND_KB_GENERAL_DATA, &data);
|
|
|
|
assert_eq!(packet[0], 0x57); // Header
|
|
assert_eq!(packet[1], 0xAB); // Header
|
|
assert_eq!(packet[2], 0x00); // Address
|
|
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
|
|
let expected_checksum: u8 = packet[..13]
|
|
.iter()
|
|
.fold(0u8, |acc: u8, &x| acc.wrapping_add(x));
|
|
assert_eq!(packet[13], expected_checksum);
|
|
}
|
|
|
|
#[test]
|
|
fn test_relative_mouse_packet() {
|
|
let data = [0x01, 0x00, 50u8, 0x00, 0x00];
|
|
let packet = build_packet(DEFAULT_ADDR, cmd::SEND_MS_REL_DATA, &data);
|
|
|
|
assert_eq!(packet[0], 0x57);
|
|
assert_eq!(packet[1], 0xAB);
|
|
assert_eq!(packet[2], 0x00); // Address
|
|
assert_eq!(packet[3], 0x05); // CMD_SEND_MS_REL_DATA
|
|
assert_eq!(packet[4], 5); // Length = 5
|
|
assert_eq!(packet[5], 0x01); // Mode marker
|
|
assert_eq!(packet[6], 0x00); // Buttons
|
|
assert_eq!(packet[7], 50); // X delta
|
|
}
|
|
|
|
#[test]
|
|
fn test_checksum_calculation() {
|
|
let packet = [0x57u8, 0xAB, 0x00, 0x01, 0x00];
|
|
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 = calculate_checksum(&packet);
|
|
assert_eq!(checksum, 0x10);
|
|
}
|
|
|
|
#[test]
|
|
fn test_response_parsing() {
|
|
let response_bytes = [
|
|
0x57, 0xAB, // Header
|
|
0x00, // Address
|
|
0x81, // Command (GET_INFO | 0x80 = success)
|
|
0x08, // Length
|
|
0x31, 0x01, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, // Data
|
|
0xE0, // Checksum (calculated)
|
|
];
|
|
|
|
let _result = Response::parse(&response_bytes);
|
|
}
|
|
|
|
#[test]
|
|
fn test_chip_info_parsing() {
|
|
let data = [0x31, 0x01, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00];
|
|
let info = ChipInfo::from_response(&data).unwrap();
|
|
|
|
assert_eq!(info.version, "V3.1");
|
|
assert_eq!(info.version_raw, 0x31);
|
|
assert!(info.usb_connected);
|
|
assert!(info.num_lock);
|
|
assert!(info.caps_lock);
|
|
assert!(!info.scroll_lock);
|
|
}
|
|
|
|
#[test]
|
|
fn test_led_status() {
|
|
let led = LedStatus::from(0x07);
|
|
assert!(led.num_lock);
|
|
assert!(led.caps_lock);
|
|
assert!(led.scroll_lock);
|
|
|
|
let led = LedStatus::from(0x00);
|
|
assert!(!led.num_lock);
|
|
assert!(!led.caps_lock);
|
|
assert!(!led.scroll_lock);
|
|
}
|
|
|
|
#[test]
|
|
fn test_error_codes() {
|
|
assert_eq!(Ch9329Error::from(0x00), Ch9329Error::Success);
|
|
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);
|
|
}
|
|
}
|