From 7d52b2e2eae7d1e8ffdf1a8a7e3170f2aec90a04 Mon Sep 17 00:00:00 2001 From: mofeng-git Date: Sat, 28 Mar 2026 22:06:53 +0800 Subject: [PATCH] =?UTF-8?q?fix(otg):=20=E4=BC=98=E5=8C=96=E8=BF=90?= =?UTF-8?q?=E8=A1=8C=E6=97=B6=E7=8A=B6=E6=80=81=E7=9B=91=E6=B5=8B=E4=B8=8E?= =?UTF-8?q?=E6=9C=AA=E6=9E=9A=E4=B8=BE=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hid/otg.rs | 263 ++++++++++++++++++---------------- web/src/i18n/en-US.ts | 2 +- web/src/i18n/zh-CN.ts | 2 +- web/src/views/ConsoleView.vue | 9 +- 4 files changed, 151 insertions(+), 125 deletions(-) diff --git a/src/hid/otg.rs b/src/hid/otg.rs index fb0d52c9..ebbbf3ca 100644 --- a/src/hid/otg.rs +++ b/src/hid/otg.rs @@ -141,7 +141,7 @@ pub struct OtgBackend { /// Screen resolution for absolute mouse (using parking_lot::RwLock for sync access) screen_resolution: parking_lot::RwLock>, /// UDC name for state checking (e.g., "fcc00000.usb") - udc_name: parking_lot::RwLock>, + udc_name: Arc>>, /// Whether the backend has been initialized. initialized: AtomicBool, /// Whether the device is currently online (UDC configured and devices accessible) @@ -156,10 +156,10 @@ pub struct OtgBackend { eagain_count: AtomicU8, /// Runtime change notifier. runtime_notify_tx: watch::Sender<()>, - /// LED listener stop flag. - led_worker_stop: Arc, - /// Keyboard LED listener thread. - led_worker: Mutex>>, + /// Runtime monitor stop flag. + runtime_worker_stop: Arc, + /// Runtime monitor thread. + runtime_worker: Mutex>>, } /// Write timeout in milliseconds (same as JetKVM's hidWriteTimeout) @@ -186,7 +186,7 @@ impl OtgBackend { mouse_buttons: AtomicU8::new(0), led_state: Arc::new(parking_lot::RwLock::new(LedState::default())), screen_resolution: parking_lot::RwLock::new(Some((1920, 1080))), - udc_name: parking_lot::RwLock::new(paths.udc), + udc_name: Arc::new(parking_lot::RwLock::new(paths.udc)), initialized: AtomicBool::new(false), online: AtomicBool::new(false), last_error: parking_lot::RwLock::new(None), @@ -194,8 +194,8 @@ impl OtgBackend { error_count: AtomicU8::new(0), eagain_count: AtomicU8::new(0), runtime_notify_tx, - led_worker_stop: Arc::new(AtomicBool::new(false)), - led_worker: Mutex::new(None), + runtime_worker_stop: Arc::new(AtomicBool::new(false)), + runtime_worker: Mutex::new(None), }) } @@ -297,13 +297,16 @@ impl OtgBackend { *self.udc_name.write() = Some(udc.to_string()); } - /// Check if the UDC is in "configured" state - /// - /// This is based on PiKVM's `__is_udc_configured()` method. - /// The UDC state file indicates whether the USB host has enumerated and configured the gadget. - pub fn is_udc_configured(&self) -> bool { - let udc_name = self.udc_name.read(); - if let Some(ref udc) = *udc_name { + fn read_udc_configured(udc_name: &parking_lot::RwLock>) -> bool { + let current_udc = udc_name.read().clone().or_else(Self::find_udc); + if let Some(udc) = current_udc { + { + let mut guard = udc_name.write(); + if guard.as_ref() != Some(&udc) { + *guard = Some(udc.clone()); + } + } + let state_path = format!("/sys/class/udc/{}/state", udc); match fs::read_to_string(&state_path) { Ok(content) => { @@ -313,26 +316,22 @@ impl OtgBackend { } Err(e) => { debug!("Failed to read UDC state from {}: {}", state_path, e); - // If we can't read the state, assume it might be configured - // to avoid blocking operations unnecessarily true } } } else { - // No UDC name set, try to auto-detect - if let Some(udc) = Self::find_udc() { - drop(udc_name); - *self.udc_name.write() = Some(udc.clone()); - let state_path = format!("/sys/class/udc/{}/state", udc); - fs::read_to_string(&state_path) - .map(|s| s.trim().to_lowercase() == "configured") - .unwrap_or(true) - } else { - true - } + true } } + /// Check if the UDC is in "configured" state + /// + /// This is based on PiKVM's `__is_udc_configured()` method. + /// The UDC state file indicates whether the USB host has enumerated and configured the gadget. + pub fn is_udc_configured(&self) -> bool { + Self::read_udc_configured(&self.udc_name) + } + /// Find the first available UDC fn find_udc() -> Option { let udc_path = PathBuf::from("/sys/class/udc"); @@ -824,107 +823,131 @@ impl OtgBackend { } } - fn start_led_worker(&self) { - if !self.keyboard_leds_enabled { - return; + fn poll_keyboard_led_once( + file: &mut Option, + path: &PathBuf, + led_state: &Arc>, + ) -> bool { + if file.is_none() { + match OpenOptions::new() + .read(true) + .custom_flags(libc::O_NONBLOCK) + .open(path) + { + Ok(opened) => { + *file = Some(opened); + } + Err(err) => { + warn!( + "Failed to open OTG keyboard LED listener {}: {}", + path.display(), + err + ); + thread::sleep(Duration::from_millis(500)); + return false; + } + } } - let Some(path) = self.keyboard_path.clone() else { - return; + let Some(file_ref) = file.as_mut() else { + return false; }; - let mut worker = self.led_worker.lock(); + let mut pollfd = [PollFd::new( + file_ref.as_fd(), + PollFlags::POLLIN | PollFlags::POLLERR | PollFlags::POLLHUP, + )]; + + match poll(&mut pollfd, PollTimeout::from(500u16)) { + Ok(0) => false, + Ok(_) => { + let Some(revents) = pollfd[0].revents() else { + return false; + }; + + if revents.contains(PollFlags::POLLERR) || revents.contains(PollFlags::POLLHUP) { + *file = None; + return true; + } + + if !revents.contains(PollFlags::POLLIN) { + return false; + } + + let mut buf = [0u8; 1]; + match file_ref.read(&mut buf) { + Ok(1) => { + let next = LedState::from_byte(buf[0]); + let mut guard = led_state.write(); + if *guard == next { + false + } else { + *guard = next; + true + } + } + Ok(_) => false, + Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => false, + Err(err) => { + warn!("OTG keyboard LED listener read failed: {}", err); + *file = None; + true + } + } + } + Err(err) => { + warn!("OTG keyboard LED listener poll failed: {}", err); + *file = None; + true + } + } + } + + fn start_runtime_worker(&self) { + let mut worker = self.runtime_worker.lock(); if worker.is_some() { return; } - self.led_worker_stop.store(false, Ordering::Relaxed); - let stop = self.led_worker_stop.clone(); + self.runtime_worker_stop.store(false, Ordering::Relaxed); + let stop = self.runtime_worker_stop.clone(); + let keyboard_leds_enabled = self.keyboard_leds_enabled; + let keyboard_path = self.keyboard_path.clone(); let led_state = self.led_state.clone(); + let udc_name = self.udc_name.clone(); let runtime_notify_tx = self.runtime_notify_tx.clone(); let handle = thread::Builder::new() - .name("otg-led-listener".to_string()) + .name("otg-runtime-monitor".to_string()) .spawn(move || { + let mut last_udc_configured = Some(Self::read_udc_configured(&udc_name)); + let mut keyboard_led_file: Option = None; + while !stop.load(Ordering::Relaxed) { - let mut file = match OpenOptions::new() - .read(true) - .custom_flags(libc::O_NONBLOCK) - .open(&path) - { - Ok(file) => file, - Err(err) => { - warn!( - "Failed to open OTG keyboard LED listener {}: {}", - path.display(), - err - ); - let _ = runtime_notify_tx.send(()); - thread::sleep(Duration::from_millis(500)); - continue; - } - }; + let mut changed = false; - while !stop.load(Ordering::Relaxed) { - let mut pollfd = [PollFd::new( - file.as_fd(), - PollFlags::POLLIN | PollFlags::POLLERR | PollFlags::POLLHUP, - )]; - - match poll(&mut pollfd, PollTimeout::from(500u16)) { - Ok(0) => continue, - Ok(_) => { - let Some(revents) = pollfd[0].revents() else { - continue; - }; - - if revents.contains(PollFlags::POLLERR) - || revents.contains(PollFlags::POLLHUP) - { - let _ = runtime_notify_tx.send(()); - break; - } - - if !revents.contains(PollFlags::POLLIN) { - continue; - } - - let mut buf = [0u8; 1]; - match file.read(&mut buf) { - Ok(1) => { - let next = LedState::from_byte(buf[0]); - let changed = { - let mut guard = led_state.write(); - if *guard == next { - false - } else { - *guard = next; - true - } - }; - if changed { - let _ = runtime_notify_tx.send(()); - } - } - Ok(_) => {} - Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => {} - Err(err) => { - warn!("OTG keyboard LED listener read failed: {}", err); - let _ = runtime_notify_tx.send(()); - break; - } - } - } - Err(err) => { - warn!("OTG keyboard LED listener poll failed: {}", err); - let _ = runtime_notify_tx.send(()); - break; - } - } + let current_udc_configured = Self::read_udc_configured(&udc_name); + if last_udc_configured != Some(current_udc_configured) { + last_udc_configured = Some(current_udc_configured); + changed = true; } - if !stop.load(Ordering::Relaxed) { - thread::sleep(Duration::from_millis(100)); + if keyboard_leds_enabled { + if let Some(path) = keyboard_path.as_ref() { + changed |= Self::poll_keyboard_led_once( + &mut keyboard_led_file, + path, + &led_state, + ); + } else { + thread::sleep(Duration::from_millis(500)); + } + } else { + thread::sleep(Duration::from_millis(500)); + } + + if changed { + let _ = runtime_notify_tx.send(()); } } }); @@ -934,14 +957,14 @@ impl OtgBackend { *worker = Some(handle); } Err(err) => { - warn!("Failed to spawn OTG keyboard LED listener: {}", err); + warn!("Failed to spawn OTG runtime monitor: {}", err); } } } - fn stop_led_worker(&self) { - self.led_worker_stop.store(true, Ordering::Relaxed); - if let Some(handle) = self.led_worker.lock().take() { + fn stop_runtime_worker(&self) { + self.runtime_worker_stop.store(true, Ordering::Relaxed); + if let Some(handle) = self.runtime_worker.lock().take() { let _ = handle.join(); } } @@ -1034,7 +1057,7 @@ impl HidBackend for OtgBackend { // Mark as online if all devices opened successfully self.initialized.store(true, Ordering::Relaxed); self.notify_runtime_changed(); - self.start_led_worker(); + self.start_runtime_worker(); self.mark_online(); Ok(()) @@ -1143,7 +1166,7 @@ impl HidBackend for OtgBackend { } async fn shutdown(&self) -> Result<()> { - self.stop_led_worker(); + self.stop_runtime_worker(); // Reset before closing self.reset().await?; @@ -1195,8 +1218,8 @@ pub fn is_otg_available() -> bool { /// Implement Drop for OtgBackend to close device files impl Drop for OtgBackend { fn drop(&mut self) { - self.led_worker_stop.store(true, Ordering::Relaxed); - if let Some(handle) = self.led_worker.get_mut().take() { + self.runtime_worker_stop.store(true, Ordering::Relaxed); + if let Some(handle) = self.runtime_worker.get_mut().take() { let _ = handle.join(); } // Close device files diff --git a/web/src/i18n/en-US.ts b/web/src/i18n/en-US.ts index 3215207f..e9b469f8 100644 --- a/web/src/i18n/en-US.ts +++ b/web/src/i18n/en-US.ts @@ -363,7 +363,7 @@ export default { recovered: 'HID Recovered', recoveredDesc: '{backend} HID device reconnected successfully', errorHints: { - udcNotConfigured: 'Target host has not finished USB enumeration yet', + udcNotConfigured: 'OTG is ready, waiting for the target host to connect and finish USB enumeration', disabled: 'HID backend is disabled', hidDeviceMissing: 'HID gadget device node is missing, try restarting HID service', notOpened: 'HID device is not open, try restarting HID service', diff --git a/web/src/i18n/zh-CN.ts b/web/src/i18n/zh-CN.ts index 662a93f3..ff1b4bba 100644 --- a/web/src/i18n/zh-CN.ts +++ b/web/src/i18n/zh-CN.ts @@ -363,7 +363,7 @@ export default { recovered: 'HID 已恢复', recoveredDesc: '{backend} HID 设备已成功重连', errorHints: { - udcNotConfigured: '被控机尚未完成 USB 枚举', + udcNotConfigured: 'OTG 已就绪,等待被控机连接并完成 USB 枚举', disabled: 'HID 后端已禁用', hidDeviceMissing: '未找到 HID 设备节点,可尝试重启 HID 服务', notOpened: 'HID 设备尚未打开,可尝试重启 HID 服务', diff --git a/web/src/views/ConsoleView.vue b/web/src/views/ConsoleView.vue index 8d179046..a5dc312b 100644 --- a/web/src/views/ConsoleView.vue +++ b/web/src/views/ConsoleView.vue @@ -241,6 +241,7 @@ const videoDetails = computed(() => { const hidStatus = computed<'connected' | 'connecting' | 'disconnected' | 'error'>(() => { const hid = systemStore.hid + if (hid?.errorCode === 'udc_not_configured') return 'disconnected' if (hid?.error) return 'error' // In WebRTC mode, check DataChannel status first @@ -348,11 +349,13 @@ const hidDetails = computed(() => { const hid = systemStore.hid if (!hid) return [] const errorMessage = buildHidErrorMessage(hid.error, hid.errorCode, hid.backend) + const hidErrorStatus: StatusDetail['status'] = + hid.errorCode === 'udc_not_configured' ? 'warning' : 'error' const details: StatusDetail[] = [ { label: t('statusCard.device'), value: hid.device || '-' }, { label: t('statusCard.backend'), value: hid.backend || t('common.unknown') }, - { label: t('statusCard.initialized'), value: hid.initialized ? t('statusCard.yes') : t('statusCard.no'), status: hid.error ? 'error' : hid.initialized ? 'ok' : 'warning' }, + { label: t('statusCard.initialized'), value: hid.initialized ? t('statusCard.yes') : t('statusCard.no'), status: hid.error && hid.errorCode !== 'udc_not_configured' ? 'error' : hid.initialized ? 'ok' : 'warning' }, { label: t('statusCard.online'), value: hid.online ? t('statusCard.yes') : t('statusCard.no'), status: hid.online ? 'ok' : hid.initialized ? 'warning' : 'error' }, { label: t('statusCard.currentMode'), value: mouseMode.value === 'absolute' ? t('statusCard.absolute') : t('statusCard.relative'), status: 'ok' }, { @@ -365,10 +368,10 @@ const hidDetails = computed(() => { ] if (hid.errorCode) { - details.push({ label: t('statusCard.errorCode'), value: hid.errorCode, status: 'error' }) + details.push({ label: t('statusCard.errorCode'), value: hid.errorCode, status: hidErrorStatus }) } if (errorMessage) { - details.push({ label: t('common.error'), value: errorMessage, status: 'error' }) + details.push({ label: t('common.error'), value: errorMessage, status: hidErrorStatus }) } // Add HID channel info based on video mode