mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-03-29 22:56:45 +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 for absolute mouse (using parking_lot::RwLock for sync access)
|
||||||
screen_resolution: parking_lot::RwLock<Option<(u32, u32)>>,
|
screen_resolution: parking_lot::RwLock<Option<(u32, u32)>>,
|
||||||
/// UDC name for state checking (e.g., "fcc00000.usb")
|
/// 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.
|
/// Whether the backend has been initialized.
|
||||||
initialized: AtomicBool,
|
initialized: AtomicBool,
|
||||||
/// Whether the device is currently online (UDC configured and devices accessible)
|
/// Whether the device is currently online (UDC configured and devices accessible)
|
||||||
@@ -156,10 +156,10 @@ pub struct OtgBackend {
|
|||||||
eagain_count: AtomicU8,
|
eagain_count: AtomicU8,
|
||||||
/// Runtime change notifier.
|
/// Runtime change notifier.
|
||||||
runtime_notify_tx: watch::Sender<()>,
|
runtime_notify_tx: watch::Sender<()>,
|
||||||
/// LED listener stop flag.
|
/// Runtime monitor stop flag.
|
||||||
led_worker_stop: Arc<AtomicBool>,
|
runtime_worker_stop: Arc<AtomicBool>,
|
||||||
/// Keyboard LED listener thread.
|
/// Runtime monitor thread.
|
||||||
led_worker: Mutex<Option<thread::JoinHandle<()>>>,
|
runtime_worker: Mutex<Option<thread::JoinHandle<()>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Write timeout in milliseconds (same as JetKVM's hidWriteTimeout)
|
/// Write timeout in milliseconds (same as JetKVM's hidWriteTimeout)
|
||||||
@@ -186,7 +186,7 @@ impl OtgBackend {
|
|||||||
mouse_buttons: AtomicU8::new(0),
|
mouse_buttons: AtomicU8::new(0),
|
||||||
led_state: Arc::new(parking_lot::RwLock::new(LedState::default())),
|
led_state: Arc::new(parking_lot::RwLock::new(LedState::default())),
|
||||||
screen_resolution: parking_lot::RwLock::new(Some((1920, 1080))),
|
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),
|
initialized: AtomicBool::new(false),
|
||||||
online: AtomicBool::new(false),
|
online: AtomicBool::new(false),
|
||||||
last_error: parking_lot::RwLock::new(None),
|
last_error: parking_lot::RwLock::new(None),
|
||||||
@@ -194,8 +194,8 @@ impl OtgBackend {
|
|||||||
error_count: AtomicU8::new(0),
|
error_count: AtomicU8::new(0),
|
||||||
eagain_count: AtomicU8::new(0),
|
eagain_count: AtomicU8::new(0),
|
||||||
runtime_notify_tx,
|
runtime_notify_tx,
|
||||||
led_worker_stop: Arc::new(AtomicBool::new(false)),
|
runtime_worker_stop: Arc::new(AtomicBool::new(false)),
|
||||||
led_worker: Mutex::new(None),
|
runtime_worker: Mutex::new(None),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -297,13 +297,16 @@ impl OtgBackend {
|
|||||||
*self.udc_name.write() = Some(udc.to_string());
|
*self.udc_name.write() = Some(udc.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if the UDC is in "configured" state
|
fn read_udc_configured(udc_name: &parking_lot::RwLock<Option<String>>) -> bool {
|
||||||
///
|
let current_udc = udc_name.read().clone().or_else(Self::find_udc);
|
||||||
/// This is based on PiKVM's `__is_udc_configured()` method.
|
if let Some(udc) = current_udc {
|
||||||
/// The UDC state file indicates whether the USB host has enumerated and configured the gadget.
|
{
|
||||||
pub fn is_udc_configured(&self) -> bool {
|
let mut guard = udc_name.write();
|
||||||
let udc_name = self.udc_name.read();
|
if guard.as_ref() != Some(&udc) {
|
||||||
if let Some(ref udc) = *udc_name {
|
*guard = Some(udc.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let state_path = format!("/sys/class/udc/{}/state", udc);
|
let state_path = format!("/sys/class/udc/{}/state", udc);
|
||||||
match fs::read_to_string(&state_path) {
|
match fs::read_to_string(&state_path) {
|
||||||
Ok(content) => {
|
Ok(content) => {
|
||||||
@@ -313,26 +316,22 @@ impl OtgBackend {
|
|||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
debug!("Failed to read UDC state from {}: {}", state_path, 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
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// No UDC name set, try to auto-detect
|
true
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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
|
/// Find the first available UDC
|
||||||
fn find_udc() -> Option<String> {
|
fn find_udc() -> Option<String> {
|
||||||
let udc_path = PathBuf::from("/sys/class/udc");
|
let udc_path = PathBuf::from("/sys/class/udc");
|
||||||
@@ -824,107 +823,131 @@ impl OtgBackend {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn start_led_worker(&self) {
|
fn poll_keyboard_led_once(
|
||||||
if !self.keyboard_leds_enabled {
|
file: &mut Option<File>,
|
||||||
return;
|
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 {
|
let Some(file_ref) = file.as_mut() else {
|
||||||
return;
|
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() {
|
if worker.is_some() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.led_worker_stop.store(false, Ordering::Relaxed);
|
self.runtime_worker_stop.store(false, Ordering::Relaxed);
|
||||||
let stop = self.led_worker_stop.clone();
|
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 led_state = self.led_state.clone();
|
||||||
|
let udc_name = self.udc_name.clone();
|
||||||
let runtime_notify_tx = self.runtime_notify_tx.clone();
|
let runtime_notify_tx = self.runtime_notify_tx.clone();
|
||||||
|
|
||||||
let handle = thread::Builder::new()
|
let handle = thread::Builder::new()
|
||||||
.name("otg-led-listener".to_string())
|
.name("otg-runtime-monitor".to_string())
|
||||||
.spawn(move || {
|
.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) {
|
while !stop.load(Ordering::Relaxed) {
|
||||||
let mut file = match OpenOptions::new()
|
let mut changed = false;
|
||||||
.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;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
while !stop.load(Ordering::Relaxed) {
|
let current_udc_configured = Self::read_udc_configured(&udc_name);
|
||||||
let mut pollfd = [PollFd::new(
|
if last_udc_configured != Some(current_udc_configured) {
|
||||||
file.as_fd(),
|
last_udc_configured = Some(current_udc_configured);
|
||||||
PollFlags::POLLIN | PollFlags::POLLERR | PollFlags::POLLHUP,
|
changed = true;
|
||||||
)];
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !stop.load(Ordering::Relaxed) {
|
if keyboard_leds_enabled {
|
||||||
thread::sleep(Duration::from_millis(100));
|
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);
|
*worker = Some(handle);
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
warn!("Failed to spawn OTG keyboard LED listener: {}", err);
|
warn!("Failed to spawn OTG runtime monitor: {}", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn stop_led_worker(&self) {
|
fn stop_runtime_worker(&self) {
|
||||||
self.led_worker_stop.store(true, Ordering::Relaxed);
|
self.runtime_worker_stop.store(true, Ordering::Relaxed);
|
||||||
if let Some(handle) = self.led_worker.lock().take() {
|
if let Some(handle) = self.runtime_worker.lock().take() {
|
||||||
let _ = handle.join();
|
let _ = handle.join();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1034,7 +1057,7 @@ impl HidBackend for OtgBackend {
|
|||||||
// Mark as online if all devices opened successfully
|
// Mark as online if all devices opened successfully
|
||||||
self.initialized.store(true, Ordering::Relaxed);
|
self.initialized.store(true, Ordering::Relaxed);
|
||||||
self.notify_runtime_changed();
|
self.notify_runtime_changed();
|
||||||
self.start_led_worker();
|
self.start_runtime_worker();
|
||||||
self.mark_online();
|
self.mark_online();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -1143,7 +1166,7 @@ impl HidBackend for OtgBackend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn shutdown(&self) -> Result<()> {
|
async fn shutdown(&self) -> Result<()> {
|
||||||
self.stop_led_worker();
|
self.stop_runtime_worker();
|
||||||
|
|
||||||
// Reset before closing
|
// Reset before closing
|
||||||
self.reset().await?;
|
self.reset().await?;
|
||||||
@@ -1195,8 +1218,8 @@ pub fn is_otg_available() -> bool {
|
|||||||
/// Implement Drop for OtgBackend to close device files
|
/// Implement Drop for OtgBackend to close device files
|
||||||
impl Drop for OtgBackend {
|
impl Drop for OtgBackend {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
self.led_worker_stop.store(true, Ordering::Relaxed);
|
self.runtime_worker_stop.store(true, Ordering::Relaxed);
|
||||||
if let Some(handle) = self.led_worker.get_mut().take() {
|
if let Some(handle) = self.runtime_worker.get_mut().take() {
|
||||||
let _ = handle.join();
|
let _ = handle.join();
|
||||||
}
|
}
|
||||||
// Close device files
|
// Close device files
|
||||||
|
|||||||
@@ -363,7 +363,7 @@ export default {
|
|||||||
recovered: 'HID Recovered',
|
recovered: 'HID Recovered',
|
||||||
recoveredDesc: '{backend} HID device reconnected successfully',
|
recoveredDesc: '{backend} HID device reconnected successfully',
|
||||||
errorHints: {
|
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',
|
disabled: 'HID backend is disabled',
|
||||||
hidDeviceMissing: 'HID gadget device node is missing, try restarting HID service',
|
hidDeviceMissing: 'HID gadget device node is missing, try restarting HID service',
|
||||||
notOpened: 'HID device is not open, try restarting HID service',
|
notOpened: 'HID device is not open, try restarting HID service',
|
||||||
|
|||||||
@@ -363,7 +363,7 @@ export default {
|
|||||||
recovered: 'HID 已恢复',
|
recovered: 'HID 已恢复',
|
||||||
recoveredDesc: '{backend} HID 设备已成功重连',
|
recoveredDesc: '{backend} HID 设备已成功重连',
|
||||||
errorHints: {
|
errorHints: {
|
||||||
udcNotConfigured: '被控机尚未完成 USB 枚举',
|
udcNotConfigured: 'OTG 已就绪,等待被控机连接并完成 USB 枚举',
|
||||||
disabled: 'HID 后端已禁用',
|
disabled: 'HID 后端已禁用',
|
||||||
hidDeviceMissing: '未找到 HID 设备节点,可尝试重启 HID 服务',
|
hidDeviceMissing: '未找到 HID 设备节点,可尝试重启 HID 服务',
|
||||||
notOpened: 'HID 设备尚未打开,可尝试重启 HID 服务',
|
notOpened: 'HID 设备尚未打开,可尝试重启 HID 服务',
|
||||||
|
|||||||
@@ -241,6 +241,7 @@ const videoDetails = computed<StatusDetail[]>(() => {
|
|||||||
|
|
||||||
const hidStatus = computed<'connected' | 'connecting' | 'disconnected' | 'error'>(() => {
|
const hidStatus = computed<'connected' | 'connecting' | 'disconnected' | 'error'>(() => {
|
||||||
const hid = systemStore.hid
|
const hid = systemStore.hid
|
||||||
|
if (hid?.errorCode === 'udc_not_configured') return 'disconnected'
|
||||||
if (hid?.error) return 'error'
|
if (hid?.error) return 'error'
|
||||||
|
|
||||||
// In WebRTC mode, check DataChannel status first
|
// In WebRTC mode, check DataChannel status first
|
||||||
@@ -348,11 +349,13 @@ const hidDetails = computed<StatusDetail[]>(() => {
|
|||||||
const hid = systemStore.hid
|
const hid = systemStore.hid
|
||||||
if (!hid) return []
|
if (!hid) return []
|
||||||
const errorMessage = buildHidErrorMessage(hid.error, hid.errorCode, hid.backend)
|
const errorMessage = buildHidErrorMessage(hid.error, hid.errorCode, hid.backend)
|
||||||
|
const hidErrorStatus: StatusDetail['status'] =
|
||||||
|
hid.errorCode === 'udc_not_configured' ? 'warning' : 'error'
|
||||||
|
|
||||||
const details: StatusDetail[] = [
|
const details: StatusDetail[] = [
|
||||||
{ label: t('statusCard.device'), value: hid.device || '-' },
|
{ label: t('statusCard.device'), value: hid.device || '-' },
|
||||||
{ label: t('statusCard.backend'), value: hid.backend || t('common.unknown') },
|
{ 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.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' },
|
{ 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) {
|
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) {
|
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
|
// Add HID channel info based on video mode
|
||||||
|
|||||||
Reference in New Issue
Block a user