refactor: 重构部分事件检查逻辑,修复 ch9329 hid 状态显示异常

This commit is contained in:
mofeng-git
2026-03-26 12:33:24 +08:00
parent ae26e3c863
commit 779aa180ad
20 changed files with 1025 additions and 1709 deletions

View File

@@ -327,8 +327,12 @@ export const hidApi = {
available: boolean
backend: string
initialized: boolean
online: boolean
supports_absolute_mouse: boolean
screen_resolution: [number, number] | null
device: string | null
error: string | null
error_code: string | null
}>('/hid/status'),
otgSelfCheck: () =>

View File

@@ -123,8 +123,7 @@ async function applyConfig() {
}
await audioApi.start()
// Note: handleAudioStateChanged in ConsoleView will handle the connection
// when it receives the audio.state_changed event with streaming=true
// ConsoleView will react when system.device_info reflects streaming=true.
} catch (startError) {
// Audio start failed - config was saved but streaming not started
console.info('[AudioConfig] Audio start failed:', startError)

View File

@@ -5,7 +5,6 @@ import { useI18n } from 'vue-i18n'
import { toast } from 'vue-sonner'
import { useSystemStore } from '@/stores/system'
import { useWebSocket } from '@/composables/useWebSocket'
import { getUnifiedAudio } from '@/composables/useUnifiedAudio'
export interface ConsoleEventHandlers {
onStreamConfigChanging?: (data: { reason?: string }) => void
@@ -20,119 +19,13 @@ export interface ConsoleEventHandlers {
onStreamReconnecting?: (data: { device: string; attempt: number }) => void
onStreamRecovered?: (data: { device: string }) => void
onDeviceInfo?: (data: any) => void
onAudioStateChanged?: (data: { streaming: boolean; device: string | null }) => void
}
export function useConsoleEvents(handlers: ConsoleEventHandlers) {
const { t } = useI18n()
const systemStore = useSystemStore()
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: {
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
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 }),
duration: 5000,
})
}
function handleHidReconnecting(data: { backend: string; attempt: number }) {
if (data.attempt === 1 || data.attempt % 5 === 0) {
toast.info(t('hid.reconnecting'), {
description: t('hid.reconnectingDesc', { attempt: data.attempt }),
duration: 3000,
})
}
}
function handleHidRecovered(data: { backend: string }) {
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,
})
}
// Stream device monitoring handlers
function handleStreamDeviceLost(data: { device: string; reason: string }) {
if (systemStore.stream) {
@@ -177,93 +70,8 @@ export function useConsoleEvents(handlers: ConsoleEventHandlers) {
handlers.onStreamStateChanged?.(data)
}
// Audio device monitoring handlers
function handleAudioDeviceLost(data: { device?: string; reason: string; error_code: string }) {
if (systemStore.audio) {
systemStore.audio.streaming = false
systemStore.audio.error = data.reason
}
toast.error(t('audio.deviceLost'), {
description: t('audio.deviceLostDesc', { device: data.device || 'default', reason: data.reason }),
duration: 5000,
})
}
function handleAudioReconnecting(data: { attempt: number }) {
if (data.attempt === 1 || data.attempt % 5 === 0) {
toast.info(t('audio.reconnecting'), {
description: t('audio.reconnectingDesc', { attempt: data.attempt }),
duration: 3000,
})
}
}
function handleAudioRecovered(data: { device?: string }) {
if (systemStore.audio) {
systemStore.audio.error = null
}
toast.success(t('audio.recovered'), {
description: t('audio.recoveredDesc', { device: data.device || 'default' }),
duration: 3000,
})
}
async function handleAudioStateChanged(data: { streaming: boolean; device: string | null }) {
if (!data.streaming) {
unifiedAudio.disconnect()
return
}
handlers.onAudioStateChanged?.(data)
}
// MSD event handlers
function handleMsdStateChanged(_data: { mode: string; connected: boolean }) {
systemStore.fetchMsdState().catch(() => null)
}
function handleMsdImageMounted(data: { image_id: string; image_name: string; size: number; cdrom: boolean }) {
toast.success(t('msd.imageMounted', { name: data.image_name }), {
description: `${(data.size / 1024 / 1024).toFixed(2)} MB - ${data.cdrom ? 'CD-ROM' : 'Disk'}`,
duration: 3000,
})
systemStore.fetchMsdState().catch(() => null)
}
function handleMsdImageUnmounted() {
toast.info(t('msd.imageUnmounted'), {
duration: 2000,
})
systemStore.fetchMsdState().catch(() => null)
}
function handleMsdError(data: { reason: string; error_code: string }) {
if (systemStore.msd) {
systemStore.msd.error = data.reason
}
toast.error(t('msd.error'), {
description: t('msd.errorDesc', { reason: data.reason }),
duration: 5000,
})
}
function handleMsdRecovered() {
if (systemStore.msd) {
systemStore.msd.error = null
}
toast.success(t('msd.recovered'), {
description: t('msd.recoveredDesc'),
duration: 3000,
})
}
// Subscribe to all events
function subscribe() {
// HID events
on('hid.state_changed', handleHidStateChanged)
on('hid.device_lost', handleHidDeviceLost)
on('hid.reconnecting', handleHidReconnecting)
on('hid.recovered', handleHidRecovered)
// Stream events
on('stream.config_changing', handlers.onStreamConfigChanging ?? noop)
on('stream.config_applied', handlers.onStreamConfigApplied ?? noop)
@@ -277,19 +85,6 @@ export function useConsoleEvents(handlers: ConsoleEventHandlers) {
on('stream.reconnecting', handleStreamReconnecting)
on('stream.recovered', handleStreamRecovered)
// Audio events
on('audio.state_changed', handleAudioStateChanged)
on('audio.device_lost', handleAudioDeviceLost)
on('audio.reconnecting', handleAudioReconnecting)
on('audio.recovered', handleAudioRecovered)
// MSD events
on('msd.state_changed', handleMsdStateChanged)
on('msd.image_mounted', handleMsdImageMounted)
on('msd.image_unmounted', handleMsdImageUnmounted)
on('msd.error', handleMsdError)
on('msd.recovered', handleMsdRecovered)
// System events
on('system.device_info', handlers.onDeviceInfo ?? noop)
@@ -299,11 +94,6 @@ export function useConsoleEvents(handlers: ConsoleEventHandlers) {
// Unsubscribe from all events
function unsubscribe() {
off('hid.state_changed', handleHidStateChanged)
off('hid.device_lost', handleHidDeviceLost)
off('hid.reconnecting', handleHidReconnecting)
off('hid.recovered', handleHidRecovered)
off('stream.config_changing', handlers.onStreamConfigChanging ?? noop)
off('stream.config_applied', handlers.onStreamConfigApplied ?? noop)
off('stream.stats_update', handlers.onStreamStatsUpdate ?? noop)
@@ -316,17 +106,6 @@ export function useConsoleEvents(handlers: ConsoleEventHandlers) {
off('stream.reconnecting', handleStreamReconnecting)
off('stream.recovered', handleStreamRecovered)
off('audio.state_changed', handleAudioStateChanged)
off('audio.device_lost', handleAudioDeviceLost)
off('audio.reconnecting', handleAudioReconnecting)
off('audio.recovered', handleAudioRecovered)
off('msd.state_changed', handleMsdStateChanged)
off('msd.image_mounted', handleMsdImageMounted)
off('msd.image_unmounted', handleMsdImageUnmounted)
off('msd.error', handleMsdError)
off('msd.recovered', handleMsdRecovered)
off('system.device_info', handlers.onDeviceInfo ?? noop)
}

View File

@@ -363,14 +363,21 @@ export default {
recoveredDesc: '{backend} HID device reconnected successfully',
errorHints: {
udcNotConfigured: 'Target host has not finished USB enumeration yet',
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',
portNotFound: 'Serial port not found, check CH9329 wiring and device path',
noResponse: 'No response from CH9329, check baud rate and power',
noResponseWithCmd: 'No response from CH9329, check baud rate and power (cmd {cmd})',
invalidConfig: 'Serial port parameters are invalid, check device path and baud rate',
protocolError: 'CH9329 replied with invalid protocol data',
healthCheckFailed: 'Background health check failed',
deviceDisconnected: 'HID device disconnected, check cable and host port',
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',
serialError: 'Serial communication error, check CH9329 wiring and config',
initFailed: 'CH9329 initialization failed, check serial settings and power',
shutdown: 'HID backend has stopped',
},
},
audio: {

View File

@@ -363,14 +363,21 @@ export default {
recoveredDesc: '{backend} HID 设备已成功重连',
errorHints: {
udcNotConfigured: '被控机尚未完成 USB 枚举',
disabled: 'HID 后端已禁用',
hidDeviceMissing: '未找到 HID 设备节点,可尝试重启 HID 服务',
notOpened: 'HID 设备尚未打开,可尝试重启 HID 服务',
portNotFound: '找不到串口设备,请检查 CH9329 接线与设备路径',
noResponse: 'CH9329 无响应,请检查波特率与供电',
noResponseWithCmd: 'CH9329 无响应,请检查波特率与供电(命令 {cmd}',
invalidConfig: '串口参数无效,请检查设备路径与波特率配置',
protocolError: 'CH9329 返回了无效协议数据',
healthCheckFailed: '后台健康检查失败',
deviceDisconnected: 'HID 设备已断开,请检查线缆与接口',
ioError: '检测到 I/O 通信异常',
otgIoError: 'OTG 链路不稳定,请检查 USB 线和被控机接口',
ch9329IoError: 'CH9329 串口链路不稳定,请检查接线与供电',
serialError: '串口通信异常,请检查 CH9329 接线与配置',
initFailed: 'CH9329 初始化失败,请检查串口参数与供电',
shutdown: 'HID 后端已停止',
},
},
audio: {

View File

@@ -32,6 +32,7 @@ interface HidState {
available: boolean
backend: string
initialized: boolean
online: boolean
supportsAbsoluteMouse: boolean
device: string | null
error: string | null
@@ -86,9 +87,11 @@ export interface HidDeviceInfo {
available: boolean
backend: string
initialized: boolean
online: boolean
supports_absolute_mouse: boolean
device: string | null
error: string | null
error_code?: string | null
}
export interface MsdDeviceInfo {
@@ -183,10 +186,11 @@ export const useSystemStore = defineStore('system', () => {
available: state.available,
backend: state.backend,
initialized: state.initialized,
online: state.online,
supportsAbsoluteMouse: state.supports_absolute_mouse,
device: null,
error: null,
errorCode: null,
device: state.device ?? null,
error: state.error ?? null,
errorCode: state.error_code ?? null,
}
return state
} catch (e) {
@@ -286,11 +290,11 @@ export const useSystemStore = defineStore('system', () => {
available: data.hid.available,
backend: data.hid.backend,
initialized: data.hid.initialized,
online: data.hid.online,
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,
errorCode: data.hid.error_code ?? null,
}
// Update MSD state (optional)
@@ -360,28 +364,6 @@ 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,
@@ -406,7 +388,6 @@ export const useSystemStore = defineStore('system', () => {
updateWsConnection,
updateHidWsConnection,
updateFromDeviceInfo,
updateHidStateFromEvent,
updateStreamClients,
setStreamOnline,
}

View File

@@ -81,7 +81,6 @@ const consoleEvents = useConsoleEvents({
onStreamDeviceLost: handleStreamDeviceLost,
onStreamRecovered: handleStreamRecovered,
onDeviceInfo: handleDeviceInfo,
onAudioStateChanged: handleAudioStateChanged,
})
// Video mode state
@@ -251,8 +250,8 @@ const hidStatus = computed<'connected' | 'connecting' | 'disconnected' | 'error'
if (hidWs.hidUnavailable.value) return 'disconnected'
// Normal status based on system state
if (hid?.available && hid.initialized) return 'connected'
if (hid?.available && !hid.initialized) return 'connecting'
if (hid?.available && hid.online) return 'connected'
if (hid?.available && hid.initialized) return 'connecting'
return 'disconnected'
})
@@ -264,29 +263,54 @@ const hidQuickInfo = computed(() => {
return mouseMode.value === 'absolute' ? t('statusCard.absolute') : t('statusCard.relative')
})
function hidErrorHint(errorCode?: string | null, backend?: string | null): string {
function extractCh9329Command(reason?: string | null): string | null {
if (!reason) return null
const match = reason.match(/cmd 0x([0-9a-f]{2})/i)
const cmd = match?.[1]
return cmd ? `0x${cmd.toUpperCase()}` : null
}
function hidErrorHint(errorCode?: string | null, backend?: string | null, reason?: string | null): string {
const ch9329Command = extractCh9329Command(reason)
switch (errorCode) {
case 'udc_not_configured':
return t('hid.errorHints.udcNotConfigured')
case 'disabled':
return t('hid.errorHints.disabled')
case 'enoent':
return t('hid.errorHints.hidDeviceMissing')
case 'not_opened':
return t('hid.errorHints.notOpened')
case 'port_not_found':
case 'port_not_opened':
return t('hid.errorHints.portNotFound')
case 'invalid_config':
return t('hid.errorHints.invalidConfig')
case 'no_response':
return t('hid.errorHints.noResponse')
return t(ch9329Command ? 'hid.errorHints.noResponseWithCmd' : 'hid.errorHints.noResponse', {
cmd: ch9329Command ?? '',
})
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 'enxio':
case 'enodev':
return t('hid.errorHints.deviceDisconnected')
case 'eio':
case 'epipe':
case 'eshutdown':
case 'io_error':
case 'write_failed':
case 'read_failed':
if (backend === 'otg') return t('hid.errorHints.otgIoError')
if (backend === 'ch9329') return t('hid.errorHints.ch9329IoError')
return t('hid.errorHints.ioError')
case 'serial_error':
return t('hid.errorHints.serialError')
case 'init_failed':
return t('hid.errorHints.initFailed')
case 'shutdown':
return t('hid.errorHints.shutdown')
default:
return ''
}
@@ -294,8 +318,8 @@ function hidErrorHint(errorCode?: string | null, backend?: string | null): strin
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})`
const hint = hidErrorHint(errorCode, backend, reason)
if (hint) return hint
if (reason) return reason
return hint || t('common.error')
}
@@ -314,6 +338,7 @@ const hidDetails = computed<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.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' },
]
@@ -932,8 +957,22 @@ async function restoreInitialMode(serverMode: VideoMode) {
}
function handleDeviceInfo(data: any) {
const prevAudioStreaming = systemStore.audio?.streaming ?? false
const prevAudioDevice = systemStore.audio?.device ?? null
systemStore.updateFromDeviceInfo(data)
const nextAudioStreaming = systemStore.audio?.streaming ?? false
const nextAudioDevice = systemStore.audio?.device ?? null
if (
prevAudioStreaming !== nextAudioStreaming ||
prevAudioDevice !== nextAudioDevice
) {
void handleAudioStateChanged({
streaming: nextAudioStreaming,
device: nextAudioDevice,
})
}
// Skip mode sync if video config is being changed
// This prevents false-positive mode changes during config switching
if (data.video?.config_changing) {