mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-03-15 15:36:44 +08:00
feat(hid): 增加 HID 后端健康检查与错误码上报,完善前端掉线恢复状态同步及错误提示展示
This commit is contained in:
@@ -29,21 +29,84 @@ export function useConsoleEvents(handlers: ConsoleEventHandlers) {
|
||||
const { on, off, connect } = useWebSocket()
|
||||
const unifiedAudio = getUnifiedAudio()
|
||||
const noop = () => {}
|
||||
const HID_TOAST_DEDUPE_MS = 30_000
|
||||
const hidLastToastAt = new Map<string, number>()
|
||||
|
||||
function hidErrorHint(errorCode?: string, backend?: string): string {
|
||||
switch (errorCode) {
|
||||
case 'udc_not_configured':
|
||||
return t('hid.errorHints.udcNotConfigured')
|
||||
case 'enoent':
|
||||
return t('hid.errorHints.hidDeviceMissing')
|
||||
case 'port_not_found':
|
||||
case 'port_not_opened':
|
||||
return t('hid.errorHints.portNotFound')
|
||||
case 'no_response':
|
||||
return t('hid.errorHints.noResponse')
|
||||
case 'protocol_error':
|
||||
case 'invalid_response':
|
||||
return t('hid.errorHints.protocolError')
|
||||
case 'health_check_failed':
|
||||
case 'health_check_join_failed':
|
||||
return t('hid.errorHints.healthCheckFailed')
|
||||
case 'eio':
|
||||
case 'epipe':
|
||||
case 'eshutdown':
|
||||
if (backend === 'otg') {
|
||||
return t('hid.errorHints.otgIoError')
|
||||
}
|
||||
if (backend === 'ch9329') {
|
||||
return t('hid.errorHints.ch9329IoError')
|
||||
}
|
||||
return t('hid.errorHints.ioError')
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
function formatHidReason(reason: string, errorCode?: string, backend?: string): string {
|
||||
const hint = hidErrorHint(errorCode, backend)
|
||||
if (!hint) return reason
|
||||
return `${reason} (${hint})`
|
||||
}
|
||||
|
||||
// HID event handlers
|
||||
function handleHidStateChanged(_data: unknown) {
|
||||
// Empty handler to prevent warning - HID state handled via device_info
|
||||
function handleHidStateChanged(data: {
|
||||
backend: string
|
||||
initialized: boolean
|
||||
error?: string | null
|
||||
error_code?: string | null
|
||||
}) {
|
||||
systemStore.updateHidStateFromEvent({
|
||||
backend: data.backend,
|
||||
initialized: data.initialized,
|
||||
error: data.error ?? null,
|
||||
error_code: data.error_code ?? null,
|
||||
})
|
||||
}
|
||||
|
||||
function handleHidDeviceLost(data: { backend: string; device?: string; reason: string; error_code: string }) {
|
||||
const temporaryErrors = ['eagain', 'eagain_retry']
|
||||
if (temporaryErrors.includes(data.error_code)) return
|
||||
|
||||
if (systemStore.hid) {
|
||||
systemStore.hid.initialized = false
|
||||
systemStore.updateHidStateFromEvent({
|
||||
backend: data.backend,
|
||||
initialized: false,
|
||||
error: data.reason,
|
||||
error_code: data.error_code,
|
||||
})
|
||||
|
||||
const dedupeKey = `${data.backend}:${data.error_code}`
|
||||
const now = Date.now()
|
||||
const last = hidLastToastAt.get(dedupeKey) ?? 0
|
||||
if (now - last < HID_TOAST_DEDUPE_MS) {
|
||||
return
|
||||
}
|
||||
hidLastToastAt.set(dedupeKey, now)
|
||||
|
||||
const reason = formatHidReason(data.reason, data.error_code, data.backend)
|
||||
toast.error(t('hid.deviceLost'), {
|
||||
description: t('hid.deviceLostDesc', { backend: data.backend, reason: data.reason }),
|
||||
description: t('hid.deviceLostDesc', { backend: data.backend, reason }),
|
||||
duration: 5000,
|
||||
})
|
||||
}
|
||||
@@ -58,9 +121,12 @@ export function useConsoleEvents(handlers: ConsoleEventHandlers) {
|
||||
}
|
||||
|
||||
function handleHidRecovered(data: { backend: string }) {
|
||||
if (systemStore.hid) {
|
||||
systemStore.hid.initialized = true
|
||||
}
|
||||
systemStore.updateHidStateFromEvent({
|
||||
backend: data.backend,
|
||||
initialized: true,
|
||||
error: null,
|
||||
error_code: null,
|
||||
})
|
||||
toast.success(t('hid.recovered'), {
|
||||
description: t('hid.recoveredDesc', { backend: data.backend }),
|
||||
duration: 3000,
|
||||
|
||||
@@ -361,6 +361,17 @@ export default {
|
||||
reconnectingDesc: 'Attempting to reconnect (attempt {attempt})',
|
||||
recovered: 'HID Recovered',
|
||||
recoveredDesc: '{backend} HID device reconnected successfully',
|
||||
errorHints: {
|
||||
udcNotConfigured: 'Target host has not finished USB enumeration yet',
|
||||
hidDeviceMissing: 'HID gadget device node is missing, try restarting HID service',
|
||||
portNotFound: 'Serial port not found, check CH9329 wiring and device path',
|
||||
noResponse: 'No response from CH9329, check baud rate and power',
|
||||
protocolError: 'CH9329 replied with invalid protocol data',
|
||||
healthCheckFailed: 'Background health check failed',
|
||||
ioError: 'I/O communication error detected',
|
||||
otgIoError: 'OTG link is unstable, check USB cable and host port',
|
||||
ch9329IoError: 'CH9329 serial link is unstable, check wiring and power',
|
||||
},
|
||||
},
|
||||
audio: {
|
||||
// Device monitoring
|
||||
@@ -812,6 +823,7 @@ export default {
|
||||
networkError: 'Network Error',
|
||||
disconnected: 'Disconnected',
|
||||
availability: 'Availability',
|
||||
errorCode: 'Error Code',
|
||||
hidUnavailable: 'HID Unavailable',
|
||||
sampleRate: 'Sample Rate',
|
||||
channels: 'Channels',
|
||||
|
||||
@@ -361,6 +361,17 @@ export default {
|
||||
reconnectingDesc: '正在尝试重连(第 {attempt} 次)',
|
||||
recovered: 'HID 已恢复',
|
||||
recoveredDesc: '{backend} HID 设备已成功重连',
|
||||
errorHints: {
|
||||
udcNotConfigured: '被控机尚未完成 USB 枚举',
|
||||
hidDeviceMissing: '未找到 HID 设备节点,可尝试重启 HID 服务',
|
||||
portNotFound: '找不到串口设备,请检查 CH9329 接线与设备路径',
|
||||
noResponse: 'CH9329 无响应,请检查波特率与供电',
|
||||
protocolError: 'CH9329 返回了无效协议数据',
|
||||
healthCheckFailed: '后台健康检查失败',
|
||||
ioError: '检测到 I/O 通信异常',
|
||||
otgIoError: 'OTG 链路不稳定,请检查 USB 线和被控机接口',
|
||||
ch9329IoError: 'CH9329 串口链路不稳定,请检查接线与供电',
|
||||
},
|
||||
},
|
||||
audio: {
|
||||
// 设备监控
|
||||
@@ -812,6 +823,7 @@ export default {
|
||||
networkError: '网络错误',
|
||||
disconnected: '已断开',
|
||||
availability: '可用性',
|
||||
errorCode: '错误码',
|
||||
hidUnavailable: 'HID不可用',
|
||||
sampleRate: '采样率',
|
||||
channels: '声道',
|
||||
|
||||
@@ -35,6 +35,7 @@ interface HidState {
|
||||
supportsAbsoluteMouse: boolean
|
||||
device: string | null
|
||||
error: string | null
|
||||
errorCode: string | null
|
||||
}
|
||||
|
||||
interface AtxState {
|
||||
@@ -185,6 +186,7 @@ export const useSystemStore = defineStore('system', () => {
|
||||
supportsAbsoluteMouse: state.supports_absolute_mouse,
|
||||
device: null,
|
||||
error: null,
|
||||
errorCode: null,
|
||||
}
|
||||
return state
|
||||
} catch (e) {
|
||||
@@ -287,6 +289,8 @@ export const useSystemStore = defineStore('system', () => {
|
||||
supportsAbsoluteMouse: data.hid.supports_absolute_mouse,
|
||||
device: data.hid.device,
|
||||
error: data.hid.error,
|
||||
// system.device_info does not include HID error_code, keep latest one when error still exists.
|
||||
errorCode: data.hid.error ? (hid.value?.errorCode ?? null) : null,
|
||||
}
|
||||
|
||||
// Update MSD state (optional)
|
||||
@@ -356,6 +360,28 @@ export const useSystemStore = defineStore('system', () => {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update HID state from hid.state_changed / hid.device_lost events.
|
||||
*/
|
||||
function updateHidStateFromEvent(data: {
|
||||
backend: string
|
||||
initialized: boolean
|
||||
error?: string | null
|
||||
error_code?: string | null
|
||||
}) {
|
||||
const current = hid.value
|
||||
const nextBackend = data.backend || current?.backend || 'unknown'
|
||||
hid.value = {
|
||||
available: nextBackend !== 'none',
|
||||
backend: nextBackend,
|
||||
initialized: data.initialized,
|
||||
supportsAbsoluteMouse: current?.supportsAbsoluteMouse ?? false,
|
||||
device: current?.device ?? null,
|
||||
error: data.error ?? null,
|
||||
errorCode: data.error_code ?? null,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
version,
|
||||
buildDate,
|
||||
@@ -380,6 +406,7 @@ export const useSystemStore = defineStore('system', () => {
|
||||
updateWsConnection,
|
||||
updateHidWsConnection,
|
||||
updateFromDeviceInfo,
|
||||
updateHidStateFromEvent,
|
||||
updateStreamClients,
|
||||
setStreamOnline,
|
||||
}
|
||||
|
||||
@@ -226,6 +226,9 @@ const videoDetails = computed<StatusDetail[]>(() => {
|
||||
})
|
||||
|
||||
const hidStatus = computed<'connected' | 'connecting' | 'disconnected' | 'error'>(() => {
|
||||
const hid = systemStore.hid
|
||||
if (hid?.error) return 'error'
|
||||
|
||||
// In WebRTC mode, check DataChannel status first
|
||||
if (videoMode.value !== 'mjpeg') {
|
||||
// DataChannel is ready - HID is connected via WebRTC
|
||||
@@ -248,8 +251,8 @@ const hidStatus = computed<'connected' | 'connecting' | 'disconnected' | 'error'
|
||||
if (hidWs.hidUnavailable.value) return 'disconnected'
|
||||
|
||||
// Normal status based on system state
|
||||
if (systemStore.hid?.available && systemStore.hid?.initialized) return 'connected'
|
||||
if (systemStore.hid?.available && !systemStore.hid?.initialized) return 'connecting'
|
||||
if (hid?.available && hid.initialized) return 'connected'
|
||||
if (hid?.available && !hid.initialized) return 'connecting'
|
||||
return 'disconnected'
|
||||
})
|
||||
|
||||
@@ -261,17 +264,66 @@ const hidQuickInfo = computed(() => {
|
||||
return mouseMode.value === 'absolute' ? t('statusCard.absolute') : t('statusCard.relative')
|
||||
})
|
||||
|
||||
function hidErrorHint(errorCode?: string | null, backend?: string | null): string {
|
||||
switch (errorCode) {
|
||||
case 'udc_not_configured':
|
||||
return t('hid.errorHints.udcNotConfigured')
|
||||
case 'enoent':
|
||||
return t('hid.errorHints.hidDeviceMissing')
|
||||
case 'port_not_found':
|
||||
case 'port_not_opened':
|
||||
return t('hid.errorHints.portNotFound')
|
||||
case 'no_response':
|
||||
return t('hid.errorHints.noResponse')
|
||||
case 'protocol_error':
|
||||
case 'invalid_response':
|
||||
return t('hid.errorHints.protocolError')
|
||||
case 'health_check_failed':
|
||||
case 'health_check_join_failed':
|
||||
return t('hid.errorHints.healthCheckFailed')
|
||||
case 'eio':
|
||||
case 'epipe':
|
||||
case 'eshutdown':
|
||||
if (backend === 'otg') return t('hid.errorHints.otgIoError')
|
||||
if (backend === 'ch9329') return t('hid.errorHints.ch9329IoError')
|
||||
return t('hid.errorHints.ioError')
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
function buildHidErrorMessage(reason?: string | null, errorCode?: string | null, backend?: string | null): string {
|
||||
if (!reason && !errorCode) return ''
|
||||
const hint = hidErrorHint(errorCode, backend)
|
||||
if (reason && hint) return `${reason} (${hint})`
|
||||
if (reason) return reason
|
||||
return hint || t('common.error')
|
||||
}
|
||||
|
||||
const hidErrorMessage = computed(() => {
|
||||
const hid = systemStore.hid
|
||||
return buildHidErrorMessage(hid?.error, hid?.errorCode, hid?.backend)
|
||||
})
|
||||
|
||||
const hidDetails = computed<StatusDetail[]>(() => {
|
||||
const hid = systemStore.hid
|
||||
if (!hid) return []
|
||||
const errorMessage = buildHidErrorMessage(hid.error, hid.errorCode, hid.backend)
|
||||
|
||||
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.initialized ? 'ok' : 'warning' },
|
||||
{ label: t('statusCard.initialized'), value: hid.initialized ? t('statusCard.yes') : t('statusCard.no'), status: hid.error ? 'error' : hid.initialized ? 'ok' : 'warning' },
|
||||
{ label: t('statusCard.currentMode'), value: mouseMode.value === 'absolute' ? t('statusCard.absolute') : t('statusCard.relative'), status: 'ok' },
|
||||
]
|
||||
|
||||
if (hid.errorCode) {
|
||||
details.push({ label: t('statusCard.errorCode'), value: hid.errorCode, status: 'error' })
|
||||
}
|
||||
if (errorMessage) {
|
||||
details.push({ label: t('common.error'), value: errorMessage, status: 'error' })
|
||||
}
|
||||
|
||||
// Add HID channel info based on video mode
|
||||
if (videoMode.value !== 'mjpeg') {
|
||||
// WebRTC mode - show DataChannel status
|
||||
@@ -2058,6 +2110,7 @@ onUnmounted(() => {
|
||||
type="hid"
|
||||
:status="hidStatus"
|
||||
:quick-info="hidQuickInfo"
|
||||
:error-message="hidErrorMessage"
|
||||
:details="hidDetails"
|
||||
:hover-align="hidHoverAlign"
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user