mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-06-14 03:32:00 +08:00
944 lines
33 KiB
Rust
944 lines
33 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::watch;
|
|
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 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;
|
|
|
|
|
|
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> },
|
|
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: Mutex<KeyboardReport>,
|
|
mouse_buttons: AtomicU8,
|
|
screen_resolution: RwLock<(u32, u32)>,
|
|
chip_info: Arc<RwLock<Option<ChipInfo>>>,
|
|
led_status: Arc<RwLock<LedStatus>>,
|
|
address: u8,
|
|
last_abs_x: AtomicU16,
|
|
last_abs_y: AtomicU16,
|
|
relative_mouse_active: AtomicBool,
|
|
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> {
|
|
Ok(Self {
|
|
port_path: port_path.to_string(),
|
|
baud_rate,
|
|
worker_tx: Mutex::new(None),
|
|
worker_handle: Mutex::new(None),
|
|
keyboard_state: Mutex::new(KeyboardReport::default()),
|
|
mouse_buttons: 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: AtomicU16::new(0),
|
|
last_abs_y: AtomicU16::new(0),
|
|
relative_mouse_active: AtomicBool::new(false),
|
|
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(e: serialport::Error, operation: &str) -> AppError {
|
|
let error_code = match e.kind() {
|
|
serialport::ErrorKind::NoDevice => "port_not_found",
|
|
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",
|
|
));
|
|
}
|
|
|
|
serialport::new(port_path, baud_rate)
|
|
.timeout(Duration::from_millis(RESPONSE_TIMEOUT_MS))
|
|
.open()
|
|
.map_err(|e| Self::serial_error_to_hid_error(e, "Failed to open serial 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> {
|
|
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 query_chip_info_on_port(
|
|
port: &mut dyn serialport::SerialPort,
|
|
address: u8,
|
|
) -> Result<ChipInfo> {
|
|
let response = Self::xfer_packet(port, address, cmd::GET_INFO, &[])?;
|
|
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"));
|
|
}
|
|
|
|
ChipInfo::from_response(&response.data)
|
|
.ok_or_else(|| Self::backend_error("Failed to parse chip info", "invalid_response"))
|
|
}
|
|
|
|
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 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>> {
|
|
loop {
|
|
match rx.recv_timeout(Duration::from_millis(RECONNECT_DELAY_MS)) {
|
|
Ok(WorkerCommand::Shutdown) => return None,
|
|
Ok(_) => continue,
|
|
Err(mpsc::RecvTimeoutError::Disconnected) => return None,
|
|
Err(mpsc::RecvTimeoutError::Timeout) => {}
|
|
}
|
|
|
|
match 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))
|
|
}) {
|
|
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) => {
|
|
if let AppError::HidError {
|
|
reason, error_code, ..
|
|
} = err
|
|
{
|
|
runtime.set_error(reason, error_code);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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 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>,
|
|
init_tx: mpsc::Sender<Result<ChipInfo>>,
|
|
) {
|
|
runtime.set_initialized(true);
|
|
|
|
let mut port = match 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))
|
|
}) {
|
|
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();
|
|
let _ = init_tx.send(Ok(info));
|
|
port
|
|
}
|
|
Err(err) => {
|
|
if let AppError::HidError {
|
|
reason, error_code, ..
|
|
} = &err
|
|
{
|
|
runtime.set_error(reason.clone(), error_code.clone());
|
|
}
|
|
let _ = init_tx.send(Err(err));
|
|
runtime.set_initialized(false);
|
|
return;
|
|
}
|
|
};
|
|
|
|
loop {
|
|
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);
|
|
|
|
let Some(new_port) = Self::worker_reconnect_loop(
|
|
&rx,
|
|
&port_path,
|
|
baud_rate,
|
|
address,
|
|
&chip_info,
|
|
&led_status,
|
|
&runtime,
|
|
) else {
|
|
break;
|
|
};
|
|
port = new_port;
|
|
} else {
|
|
runtime.set_online();
|
|
}
|
|
}
|
|
Ok(WorkerCommand::ResetState) => {
|
|
let reset_sequence = [
|
|
(cmd::SEND_KB_GENERAL_DATA, vec![0; 8]),
|
|
(cmd::SEND_MS_ABS_DATA, vec![0x02, 0, 0, 0, 0, 0, 0]),
|
|
(cmd::SEND_KB_MEDIA_DATA, vec![0x02, 0x00, 0x00, 0x00]),
|
|
];
|
|
|
|
let mut reset_failed = false;
|
|
for (cmd, data) in reset_sequence {
|
|
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);
|
|
}
|
|
reset_failed = true;
|
|
Self::try_best_effort_reset(port.as_mut(), address);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if reset_failed {
|
|
let Some(new_port) = Self::worker_reconnect_loop(
|
|
&rx,
|
|
&port_path,
|
|
baud_rate,
|
|
address,
|
|
&chip_info,
|
|
&led_status,
|
|
&runtime,
|
|
) 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) => {
|
|
if let AppError::HidError {
|
|
reason, error_code, ..
|
|
} = err
|
|
{
|
|
runtime.set_error(reason, error_code);
|
|
}
|
|
|
|
Self::try_best_effort_reset(port.as_mut(), address);
|
|
|
|
let Some(new_port) = Self::worker_reconnect_loop(
|
|
&rx,
|
|
&port_path,
|
|
baud_rate,
|
|
address,
|
|
&chip_info,
|
|
&led_status,
|
|
&runtime,
|
|
) 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 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, 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)) => {
|
|
let _ = handle.join();
|
|
self.record_error(
|
|
format!(
|
|
"CH9329 not responding on {} @ {} baud: {}",
|
|
self.port_path, self.baud_rate, err
|
|
),
|
|
"init_failed",
|
|
);
|
|
Err(AppError::Internal(format!(
|
|
"CH9329 not responding on {} @ {} baud: {}",
|
|
self.port_path, self.baud_rate, err
|
|
)))
|
|
}
|
|
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(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) {
|
|
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.relative_mouse_active.load(Ordering::Relaxed) {
|
|
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.relative_mouse_active.load(Ordering::Relaxed) {
|
|
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 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_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 super::ch9329_proto::{build_packet, calculate_checksum};
|
|
|
|
#[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);
|
|
}
|
|
}
|