fix: 修复 CH9329 健康检测错误和切换错误 #251 #255 #265

This commit is contained in:
mofeng-git
2026-06-13 16:47:21 +08:00
parent 5de7ecd4c5
commit da61644dbc
9 changed files with 199 additions and 91 deletions

View File

@@ -37,7 +37,14 @@ pub struct AppConfig {
} }
impl AppConfig { impl AppConfig {
pub fn enforce_invariants(&mut self) {
if self.hid.backend != HidBackend::Otg {
self.msd.enabled = false;
}
}
pub fn apply_platform_defaults(&mut self) { pub fn apply_platform_defaults(&mut self) {
crate::platform::defaults::apply(self); crate::platform::defaults::apply(self);
self.enforce_invariants();
} }
} }

View File

@@ -27,7 +27,8 @@ impl ConfigStore {
} }
pub async fn load(&self) -> Result<()> { pub async fn load(&self) -> Result<()> {
let config = Self::load_config(&self.pool).await?; let mut config = Self::load_config(&self.pool).await?;
config.enforce_invariants();
self.cache.store(Arc::new(config)); self.cache.store(Arc::new(config));
Ok(()) Ok(())
} }
@@ -73,6 +74,8 @@ impl ConfigStore {
pub async fn set(&self, config: AppConfig) -> Result<()> { pub async fn set(&self, config: AppConfig) -> Result<()> {
let _guard = self.write_lock.lock().await; let _guard = self.write_lock.lock().await;
let mut config = config;
config.enforce_invariants();
Self::save_config_to_db(&self.pool, &config).await?; Self::save_config_to_db(&self.pool, &config).await?;
self.cache.store(Arc::new(config)); self.cache.store(Arc::new(config));
@@ -91,6 +94,7 @@ impl ConfigStore {
let current = self.cache.load(); let current = self.cache.load();
let mut config = (**current).clone(); let mut config = (**current).clone();
f(&mut config); f(&mut config);
config.enforce_invariants();
Self::save_config_to_db(&self.pool, &config).await?; Self::save_config_to_db(&self.pool, &config).await?;

View File

@@ -15,7 +15,7 @@ use std::sync::{mpsc, Arc};
use std::thread; use std::thread;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use tokio::sync::watch; use tokio::sync::watch;
use tracing::{info, trace}; use tracing::{info, trace, warn};
use super::backend::{HidBackend, HidBackendRuntimeSnapshot}; use super::backend::{HidBackend, HidBackendRuntimeSnapshot};
use super::ch9329_proto::{ use super::ch9329_proto::{
@@ -36,6 +36,8 @@ const RECONNECT_DELAY_MS: u64 = 2000;
const INIT_WAIT_MS: u64 = 3000; const INIT_WAIT_MS: u64 = 3000;
const RECONNECT_COMMAND_POLL_MS: u64 = 100;
struct Ch9329RuntimeState { struct Ch9329RuntimeState {
initialized: AtomicBool, initialized: AtomicBool,
online: AtomicBool, online: AtomicBool,
@@ -117,15 +119,15 @@ pub struct Ch9329Backend {
baud_rate: u32, baud_rate: u32,
worker_tx: Mutex<Option<mpsc::Sender<WorkerCommand>>>, worker_tx: Mutex<Option<mpsc::Sender<WorkerCommand>>>,
worker_handle: Mutex<Option<thread::JoinHandle<()>>>, worker_handle: Mutex<Option<thread::JoinHandle<()>>>,
keyboard_state: Mutex<KeyboardReport>, keyboard_state: Arc<Mutex<KeyboardReport>>,
mouse_buttons: AtomicU8, mouse_buttons: Arc<AtomicU8>,
screen_resolution: RwLock<(u32, u32)>, screen_resolution: RwLock<(u32, u32)>,
chip_info: Arc<RwLock<Option<ChipInfo>>>, chip_info: Arc<RwLock<Option<ChipInfo>>>,
led_status: Arc<RwLock<LedStatus>>, led_status: Arc<RwLock<LedStatus>>,
address: u8, address: u8,
last_abs_x: AtomicU16, last_abs_x: Arc<AtomicU16>,
last_abs_y: AtomicU16, last_abs_y: Arc<AtomicU16>,
relative_mouse_active: AtomicBool, relative_mouse_active: Arc<AtomicBool>,
runtime: Arc<Ch9329RuntimeState>, runtime: Arc<Ch9329RuntimeState>,
} }
@@ -140,15 +142,15 @@ impl Ch9329Backend {
baud_rate, baud_rate,
worker_tx: Mutex::new(None), worker_tx: Mutex::new(None),
worker_handle: Mutex::new(None), worker_handle: Mutex::new(None),
keyboard_state: Mutex::new(KeyboardReport::default()), keyboard_state: Arc::new(Mutex::new(KeyboardReport::default())),
mouse_buttons: AtomicU8::new(0), mouse_buttons: Arc::new(AtomicU8::new(0)),
screen_resolution: RwLock::new((1920, 1080)), screen_resolution: RwLock::new((1920, 1080)),
chip_info: Arc::new(RwLock::new(None)), chip_info: Arc::new(RwLock::new(None)),
led_status: Arc::new(RwLock::new(LedStatus::default())), led_status: Arc::new(RwLock::new(LedStatus::default())),
address: DEFAULT_ADDR, address: DEFAULT_ADDR,
last_abs_x: AtomicU16::new(0), last_abs_x: Arc::new(AtomicU16::new(0)),
last_abs_y: AtomicU16::new(0), last_abs_y: Arc::new(AtomicU16::new(0)),
relative_mouse_active: AtomicBool::new(false), relative_mouse_active: Arc::new(AtomicBool::new(false)),
runtime: Arc::new(Ch9329RuntimeState::new()), runtime: Arc::new(Ch9329RuntimeState::new()),
}) })
} }
@@ -168,9 +170,27 @@ impl Ch9329Backend {
std::path::Path::new(&self.port_path).exists() std::path::Path::new(&self.port_path).exists()
} }
fn serial_error_to_hid_error(e: serialport::Error, operation: &str) -> AppError { 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() { let error_code = match e.kind() {
serialport::ErrorKind::NoDevice => "port_not_found", serialport::ErrorKind::NoDevice if !port_present => "port_not_found",
serialport::ErrorKind::NoDevice => "device_unavailable",
serialport::ErrorKind::InvalidInput => "invalid_config", serialport::ErrorKind::InvalidInput => "invalid_config",
serialport::ErrorKind::Io(_) => "io_error", serialport::ErrorKind::Io(_) => "io_error",
_ => "serial_error", _ => "serial_error",
@@ -204,7 +224,7 @@ impl Ch9329Backend {
serialport::new(port_path, baud_rate) serialport::new(port_path, baud_rate)
.timeout(Duration::from_millis(RESPONSE_TIMEOUT_MS)) .timeout(Duration::from_millis(RESPONSE_TIMEOUT_MS))
.open() .open()
.map_err(|e| Self::serial_error_to_hid_error(e, "Failed to open serial port")) .map_err(|e| Self::serial_error_to_hid_error(port_path, e, "Failed to open serial port"))
} }
fn write_packet( fn write_packet(
@@ -302,6 +322,28 @@ impl Ch9329Backend {
.ok_or_else(|| Self::backend_error("Failed to parse chip info", "invalid_response")) .ok_or_else(|| Self::backend_error("Failed to parse chip info", "invalid_response"))
} }
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 update_chip_info_cache( fn update_chip_info_cache(
chip_info: &Arc<RwLock<Option<ChipInfo>>>, chip_info: &Arc<RwLock<Option<ChipInfo>>>,
led_status: &Arc<RwLock<LedStatus>>, led_status: &Arc<RwLock<LedStatus>>,
@@ -342,6 +384,49 @@ impl Ch9329Backend {
}) })
} }
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(_) | Err(mpsc::RecvTimeoutError::Timeout) => {}
}
}
}
fn release_state_on_port(port: &mut dyn serialport::SerialPort, address: u8) -> Result<()> {
let reset_sequence = [(cmd::SEND_KB_GENERAL_DATA, vec![0; 8])];
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( fn worker_reconnect_loop(
rx: &mpsc::Receiver<WorkerCommand>, rx: &mpsc::Receiver<WorkerCommand>,
port_path: &str, port_path: &str,
@@ -351,18 +436,9 @@ impl Ch9329Backend {
led_status: &Arc<RwLock<LedStatus>>, led_status: &Arc<RwLock<LedStatus>>,
runtime: &Arc<Ch9329RuntimeState>, runtime: &Arc<Ch9329RuntimeState>,
) -> Option<Box<dyn serialport::SerialPort>> { ) -> Option<Box<dyn serialport::SerialPort>> {
runtime.set_offline();
loop { loop {
match rx.recv_timeout(Duration::from_millis(RECONNECT_DELAY_MS)) { match Self::open_ready_port(port_path, baud_rate, address) {
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)) => { Ok((port, info)) => {
info!( info!(
"CH9329 reconnected: {}, USB: {}", "CH9329 reconnected: {}, USB: {}",
@@ -380,11 +456,9 @@ impl Ch9329Backend {
return Some(port); return Some(port);
} }
Err(err) => { Err(err) => {
if let AppError::HidError { Self::record_runtime_error(runtime, &err);
reason, error_code, .. if !Self::wait_reconnect_delay(rx) {
} = err return None;
{
runtime.set_error(reason, error_code);
} }
} }
} }
@@ -437,36 +511,43 @@ impl Ch9329Backend {
chip_info: Arc<RwLock<Option<ChipInfo>>>, chip_info: Arc<RwLock<Option<ChipInfo>>>,
led_status: Arc<RwLock<LedStatus>>, led_status: Arc<RwLock<LedStatus>>,
runtime: Arc<Ch9329RuntimeState>, 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>>, init_tx: mpsc::Sender<Result<ChipInfo>>,
) { ) {
runtime.set_initialized(true); runtime.set_initialized(true);
let mut port = match Self::open_port(&port_path, baud_rate).and_then(|mut port| { let mut init_tx = Some(init_tx);
let info = Self::query_chip_info_on_port(port.as_mut(), address)?; let mut port = loop {
Ok((port, info)) match Self::open_ready_port(&port_path, baud_rate, address) {
}) { Ok((port, info)) => {
Ok((port, info)) => { info!(
info!( "CH9329 serial port opened: {} @ {} baud",
"CH9329 serial port opened: {} @ {} baud", port_path, baud_rate
port_path, baud_rate );
); if Self::update_chip_info_cache(&chip_info, &led_status, info.clone()) {
if Self::update_chip_info_cache(&chip_info, &led_status, info.clone()) { runtime.notify();
runtime.notify(); }
runtime.set_online();
if let Some(init_tx) = init_tx.take() {
let _ = init_tx.send(Ok(info));
}
break port;
} }
runtime.set_online(); Err(err) => {
let _ = init_tx.send(Ok(info)); Self::record_runtime_error(&runtime, &err);
port if let Some(init_tx) = init_tx.take() {
} let _ = init_tx.send(Err(err));
Err(err) => { }
if let AppError::HidError { if !Self::wait_reconnect_delay(&rx) {
reason, error_code, .. runtime.set_offline();
} = &err runtime.set_initialized(false);
{ return;
runtime.set_error(reason.clone(), error_code.clone()); }
} }
let _ = init_tx.send(Err(err));
runtime.set_initialized(false);
return;
} }
}; };
@@ -482,6 +563,7 @@ impl Ch9329Backend {
} }
Self::try_best_effort_reset(port.as_mut(), address); Self::try_best_effort_reset(port.as_mut(), address);
drop(port);
let Some(new_port) = Self::worker_reconnect_loop( let Some(new_port) = Self::worker_reconnect_loop(
&rx, &rx,
@@ -500,28 +582,17 @@ impl Ch9329Backend {
} }
} }
Ok(WorkerCommand::ResetState) => { Ok(WorkerCommand::ResetState) => {
let reset_sequence = [ Self::clear_local_state(
(cmd::SEND_KB_GENERAL_DATA, vec![0; 8]), &keyboard_state,
(cmd::SEND_MS_ABS_DATA, vec![0x02, 0, 0, 0, 0, 0, 0]), &mouse_buttons,
(cmd::SEND_KB_MEDIA_DATA, vec![0x02, 0x00, 0x00, 0x00]), &last_abs_x,
]; &last_abs_y,
&relative_mouse_active,
let mut reset_failed = false; );
for (cmd, data) in reset_sequence { if let Err(err) = Self::release_state_on_port(port.as_mut(), address) {
if let Err(err) = Self::xfer_packet(port.as_mut(), address, cmd, &data) { Self::record_runtime_error(&runtime, &err);
if let AppError::HidError { Self::try_best_effort_reset(port.as_mut(), address);
reason, error_code, .. drop(port);
} = 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( let Some(new_port) = Self::worker_reconnect_loop(
&rx, &rx,
&port_path, &port_path,
@@ -556,6 +627,7 @@ impl Ch9329Backend {
} }
Self::try_best_effort_reset(port.as_mut(), address); Self::try_best_effort_reset(port.as_mut(), address);
drop(port);
let Some(new_port) = Self::worker_reconnect_loop( let Some(new_port) = Self::worker_reconnect_loop(
&rx, &rx,
@@ -596,12 +668,29 @@ impl HidBackend for Ch9329Backend {
let chip_info = self.chip_info.clone(); let chip_info = self.chip_info.clone();
let led_status = self.led_status.clone(); let led_status = self.led_status.clone();
let runtime = self.runtime.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() let handle = thread::Builder::new()
.name("ch9329-worker".to_string()) .name("ch9329-worker".to_string())
.spawn(move || { .spawn(move || {
Self::worker_loop( Self::worker_loop(
port_path, baud_rate, address, rx, chip_info, led_status, runtime, init_tx, 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)))?; .map_err(|e| AppError::Internal(format!("Failed to spawn CH9329 worker: {}", e)))?;
@@ -626,7 +715,6 @@ impl HidBackend for Ch9329Backend {
Ok(()) Ok(())
} }
Ok(Err(err)) => { Ok(Err(err)) => {
let _ = handle.join();
self.record_error( self.record_error(
format!( format!(
"CH9329 not responding on {} @ {} baud: {}", "CH9329 not responding on {} @ {} baud: {}",
@@ -634,10 +722,13 @@ impl HidBackend for Ch9329Backend {
), ),
"init_failed", "init_failed",
); );
Err(AppError::Internal(format!( warn!(
"CH9329 not responding on {} @ {} baud: {}", "CH9329 not responding on {} @ {} baud, retrying in background: {}",
self.port_path, self.baud_rate, err self.port_path, self.baud_rate, err
))) );
*self.worker_tx.lock() = Some(tx);
*self.worker_handle.lock() = Some(handle);
Ok(())
} }
Err(_) => { Err(_) => {
let _ = tx.send(WorkerCommand::Shutdown); let _ = tx.send(WorkerCommand::Shutdown);

View File

@@ -167,7 +167,8 @@ pub async fn apply_hid_config(
new_config: &HidConfig, new_config: &HidConfig,
options: ConfigApplyOptions, options: ConfigApplyOptions,
) -> Result<()> { ) -> Result<()> {
let current_msd_enabled = state.config.get().msd.enabled; let current_config = state.config.get();
let current_msd_enabled = current_config.msd.enabled && new_config.backend == HidBackend::Otg;
new_config.validate_otg_endpoint_budget(current_msd_enabled)?; new_config.validate_otg_endpoint_budget(current_msd_enabled)?;
let descriptor_changed = old_config.otg_descriptor != new_config.otg_descriptor; let descriptor_changed = old_config.otg_descriptor != new_config.otg_descriptor;
@@ -235,18 +236,19 @@ pub async fn apply_msd_config(
new_config: &MsdConfig, new_config: &MsdConfig,
options: ConfigApplyOptions, options: ConfigApplyOptions,
) -> Result<()> { ) -> Result<()> {
state let current_config = state.config.get();
.config let hid_backend_is_otg = current_config.hid.backend == HidBackend::Otg;
.get() let effective_new_msd_enabled = new_config.enabled && hid_backend_is_otg;
current_config
.hid .hid
.validate_otg_endpoint_budget(new_config.enabled)?; .validate_otg_endpoint_budget(effective_new_msd_enabled)?;
tracing::info!("MSD config sent, checking if reload needed..."); tracing::info!("MSD config sent, checking if reload needed...");
tracing::debug!("Old MSD config: {:?}", old_config); tracing::debug!("Old MSD config: {:?}", old_config);
tracing::debug!("New MSD config: {:?}", new_config); tracing::debug!("New MSD config: {:?}", new_config);
let old_msd_enabled = old_config.enabled; let old_msd_enabled = old_config.enabled;
let new_msd_enabled = new_config.enabled; let new_msd_enabled = effective_new_msd_enabled;
let msd_dir_changed = old_config.msd_dir != new_config.msd_dir; let msd_dir_changed = old_config.msd_dir != new_config.msd_dir;
tracing::info!( tracing::info!(

View File

@@ -25,6 +25,7 @@ pub async fn update_hid_config(
.config .config
.update(|config| { .update(|config| {
req.apply_to(&mut config.hid); req.apply_to(&mut config.hid);
config.enforce_invariants();
}) })
.await?; .await?;

View File

@@ -25,6 +25,7 @@ pub async fn update_msd_config(
.config .config
.update(|config| { .update(|config| {
req.apply_to(&mut config.msd); req.apply_to(&mut config.msd);
config.enforce_invariants();
}) })
.await?; .await?;

View File

@@ -132,6 +132,7 @@ pub async fn setup_init(
if let Some(enabled) = req.msd_enabled { if let Some(enabled) = req.msd_enabled {
config.msd.enabled = enabled; config.msd.enabled = enabled;
} }
config.enforce_invariants();
// Extension settings // Extension settings
if let Some(enabled) = req.ttyd_enabled { if let Some(enabled) = req.ttyd_enabled {

View File

@@ -319,6 +319,7 @@ function hidErrorHint(errorCode?: string | null, backend?: string | null, reason
case 'io_error': case 'io_error':
case 'write_failed': case 'write_failed':
case 'read_failed': case 'read_failed':
case 'device_unavailable':
if (backend === 'otg') return t('hid.errorHints.otgIoError') if (backend === 'otg') return t('hid.errorHints.otgIoError')
if (backend === 'ch9329') return t('hid.errorHints.ch9329IoError') if (backend === 'ch9329') return t('hid.errorHints.ch9329IoError')
return t('hid.errorHints.ioError') return t('hid.errorHints.ioError')

View File

@@ -1286,10 +1286,10 @@ async function saveConfig() {
hidUpdate.otg_functions = { ...config.value.hid_otg_functions } hidUpdate.otg_functions = { ...config.value.hid_otg_functions }
hidUpdate.otg_keyboard_leds = config.value.hid_otg_keyboard_leds hidUpdate.otg_keyboard_leds = config.value.hid_otg_keyboard_leds
} }
await configStore.updateMsd({
enabled: config.value.msd_enabled,
})
await configStore.updateHid(hidUpdate) await configStore.updateHid(hidUpdate)
await configStore.updateMsd({
enabled: config.value.hid_backend === 'otg' && config.value.msd_enabled,
})
} }
if (activeSection.value === 'msd') { if (activeSection.value === 'msd') {