mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-03-29 14:46:33 +08:00
fix(otg): 优化运行时状态监测与未枚举提示
This commit is contained in:
263
src/hid/otg.rs
263
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<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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 服务',
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user