fix(otg): 优化运行时状态监测与未枚举提示

This commit is contained in:
mofeng-git
2026-03-28 22:06:53 +08:00
parent a2a8b3802d
commit 7d52b2e2ea
4 changed files with 151 additions and 125 deletions

View File

@@ -141,7 +141,7 @@ pub struct OtgBackend {
/// Screen resolution for absolute mouse (using parking_lot::RwLock for sync access)
screen_resolution: parking_lot::RwLock<Option<(u32, u32)>>,
/// UDC name for state checking (e.g., "fcc00000.usb")
udc_name: parking_lot::RwLock<Option<String>>,
udc_name: Arc<parking_lot::RwLock<Option<String>>>,
/// 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<AtomicBool>,
/// Keyboard LED listener thread.
led_worker: Mutex<Option<thread::JoinHandle<()>>>,
/// Runtime monitor stop flag.
runtime_worker_stop: Arc<AtomicBool>,
/// Runtime monitor thread.
runtime_worker: Mutex<Option<thread::JoinHandle<()>>>,
}
/// 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<Option<String>>) -> 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<String> {
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<File>,
path: &PathBuf,
led_state: &Arc<parking_lot::RwLock<LedState>>,
) -> 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<File> = 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

View File

@@ -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',

View File

@@ -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 服务',

View File

@@ -241,6 +241,7 @@ const videoDetails = computed<StatusDetail[]>(() => {
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<StatusDetail[]>(() => {
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<StatusDetail[]>(() => {
]
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