From 5de5ee42c232cb570dcef83db2fbb2b0d4046f00 Mon Sep 17 00:00:00 2001 From: mofeng-git Date: Sun, 6 Sep 2026 11:19:55 +0800 Subject: [PATCH] =?UTF-8?q?feat(web):=20=E6=96=B0=E5=A2=9E=20HID=20?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E5=BC=95=E5=AF=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/App.vue | 11 +- web/src/api/config.ts | 3 +- web/src/components/BluetoothHidSettings.vue | 28 +++ web/src/components/ConsoleLayoutPreview.vue | 82 +++++++ web/src/components/HidConfigPopover.vue | 249 ++------------------ web/src/components/HidDeviceOverview.vue | 33 +++ web/src/components/HidDeviceSettings.vue | 28 +++ web/src/components/HidDriverDialog.vue | 190 +++++++++++++++ web/src/components/HidDriverForm.vue | 84 +++++++ web/src/components/HidWireframeDevice.vue | 33 +++ web/src/components/HidWiringDiagram.vue | 87 +++++++ web/src/composables/useHidConnection.ts | 49 ++++ web/src/i18n/en-US.ts | 82 +++++++ web/src/i18n/zh-CN.ts | 82 +++++++ web/src/lib/hidGuide.ts | 73 ++++++ web/src/lib/hidStatus.ts | 41 ++++ web/src/stores/config.ts | 4 +- web/src/types/bluetooth.ts | 7 + web/src/types/generated.ts | 18 ++ web/src/views/ConsoleView.vue | 35 +-- web/src/views/SettingsView.vue | 162 ++++--------- web/src/views/SetupView.vue | 185 +++------------ 22 files changed, 1044 insertions(+), 522 deletions(-) create mode 100644 web/src/components/BluetoothHidSettings.vue create mode 100644 web/src/components/ConsoleLayoutPreview.vue create mode 100644 web/src/components/HidDeviceOverview.vue create mode 100644 web/src/components/HidDeviceSettings.vue create mode 100644 web/src/components/HidDriverDialog.vue create mode 100644 web/src/components/HidDriverForm.vue create mode 100644 web/src/components/HidWireframeDevice.vue create mode 100644 web/src/components/HidWiringDiagram.vue create mode 100644 web/src/composables/useHidConnection.ts create mode 100644 web/src/lib/hidGuide.ts create mode 100644 web/src/lib/hidStatus.ts create mode 100644 web/src/types/bluetooth.ts diff --git a/web/src/App.vue b/web/src/App.vue index a4da8b42..81dbeaa3 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -1,7 +1,9 @@ diff --git a/web/src/components/HidDeviceOverview.vue b/web/src/components/HidDeviceOverview.vue new file mode 100644 index 00000000..e3fa7700 --- /dev/null +++ b/web/src/components/HidDeviceOverview.vue @@ -0,0 +1,33 @@ + + diff --git a/web/src/components/HidDeviceSettings.vue b/web/src/components/HidDeviceSettings.vue new file mode 100644 index 00000000..c3fb9750 --- /dev/null +++ b/web/src/components/HidDeviceSettings.vue @@ -0,0 +1,28 @@ + + diff --git a/web/src/components/HidDriverDialog.vue b/web/src/components/HidDriverDialog.vue new file mode 100644 index 00000000..c41f4d4a --- /dev/null +++ b/web/src/components/HidDriverDialog.vue @@ -0,0 +1,190 @@ + + diff --git a/web/src/components/HidDriverForm.vue b/web/src/components/HidDriverForm.vue new file mode 100644 index 00000000..48e84285 --- /dev/null +++ b/web/src/components/HidDriverForm.vue @@ -0,0 +1,84 @@ + + diff --git a/web/src/components/HidWireframeDevice.vue b/web/src/components/HidWireframeDevice.vue new file mode 100644 index 00000000..2178178f --- /dev/null +++ b/web/src/components/HidWireframeDevice.vue @@ -0,0 +1,33 @@ + + + diff --git a/web/src/components/HidWiringDiagram.vue b/web/src/components/HidWiringDiagram.vue new file mode 100644 index 00000000..07d90e5a --- /dev/null +++ b/web/src/components/HidWiringDiagram.vue @@ -0,0 +1,87 @@ + + + diff --git a/web/src/composables/useHidConnection.ts b/web/src/composables/useHidConnection.ts new file mode 100644 index 00000000..0acfeaab --- /dev/null +++ b/web/src/composables/useHidConnection.ts @@ -0,0 +1,49 @@ +import { ref, watch, onMounted, onUnmounted, onDeactivated, onActivated, type Ref } from 'vue' +import type { hidApi } from '@/api' +import { request } from '@/api/request' +import type { BluetoothStatus } from '@/types/bluetooth' +export function useHidConnection(active: Ref, backend: Ref) { + const status = ref> | null>(null) + const bluetooth = ref(null) + const error = ref('') + let timer: ReturnType | undefined + let controller: AbortController | undefined + let generation = 0, inFlight = false, mounted = false, deactivated = false + function stop() { generation++; clearTimeout(timer); controller?.abort(); status.value = null; bluetooth.value = null } + async function refresh() { + if (!mounted || deactivated || !active.value || document.hidden || inFlight) return + inFlight = true + const own = generation + controller = new AbortController() + const signal = controller.signal + const timeout = setTimeout(() => controller?.abort(), 5000) + try { + const [hidResult, btResult] = await Promise.allSettled([ + request>>('/hid/status', { signal }, { toastOnError: false }), + backend.value === 'bluetooth' ? request('/hid/bluetooth', { signal }, { toastOnError: false }) : Promise.resolve(null), + ]) + if (own !== generation) return + if (hidResult.status === 'rejected') throw hidResult.reason + if (btResult.status === 'rejected') throw btResult.reason + const hid = hidResult.value, bt = btResult.value + status.value = hid.backend === backend.value ? hid : null + bluetooth.value = hid.backend === backend.value ? bt : null + error.value = '' + } catch (e) { + if (own !== generation) return + status.value = null; bluetooth.value = null + error.value = e instanceof Error ? e.message : String(e) + } finally { + clearTimeout(timeout) + inFlight = false + if (mounted && !deactivated && active.value && !document.hidden) timer = setTimeout(refresh, 2000) + } + } + function restart() { stop(); error.value = ''; void refresh() } + watch([active, backend], restart) + onMounted(() => { mounted = true; document.addEventListener('visibilitychange', restart); restart() }) + onActivated(() => { deactivated = false; restart() }) + onDeactivated(() => { deactivated = true; stop() }) + onUnmounted(() => { mounted = false; stop(); document.removeEventListener('visibilitychange', restart) }) + return { status, bluetooth, error, restart } +} diff --git a/web/src/i18n/en-US.ts b/web/src/i18n/en-US.ts index ae9e517b..62a68a92 100644 --- a/web/src/i18n/en-US.ts +++ b/web/src/i18n/en-US.ts @@ -1,4 +1,86 @@ export default { + hidGuide: { + missingDevice: 'The previous device {device} is unavailable. Check the current selection.', + connectionTimeout: 'Connection timed out. Check the data cable and the controlled computer’s USB port. You can connect later or reconfigure.', + "deviceTitle": "Driver and device", + "features": "Feature configuration", + "saveFeatures": "Save features", + "configure": "Configure driver", + "reconfigure": "Reconfigure", + "driver": "HID driver", + "driver_otg": "USB OTG", + "driver_ch9329": "CH9329", + "driver_bluetooth": "Bluetooth HID", + "driver_none": "Disabled", + "device_otg": "UDC", + "device_ch9329": "Serial port", + "device_bluetooth": "Local Bluetooth adapter", + "host": "Controlled computer", + "hostUsb": "Host USB", + "otgPort": "One-KVM OTG USB", + "serialLink": "CH340 → Serial connection → CH9329", + "integratedCable": "CH340 + CH9329 integrated cable", + "dataCable": "USB data cable", + "wireless": "Bluetooth connection", + "wiring_otg": "One-KVM OTG USB port → USB data cable → controlled computer USB port.", + "wiring_ch9329": "Connect One-KVM to CH9329 over serial, then connect CH9329 USB to the controlled computer.", + "wiring_bluetooth": "On the controlled computer, open System Settings → Add Bluetooth device → select the advertised HID name.", + "disabledHelp": "One-KVM cannot send keyboard or mouse input while HID is disabled.", + "selectDevice": "Select a device", + "noDevices": "No devices found. Check the connection and refresh.", + "nameInvalid": "Use 1–64 UTF-8 bytes without control characters.", + "unconfigured": "HID is not configured", + "disabled": "HID is disabled", + "legacyAuto": "Automatic (legacy configuration)", + "unknown": "Unknown or unable to read status", + "ready": "HID ready", + "preparing": "Connected, preparing HID", + "paired": "Paired, waiting for connection", + "pairing": "Waiting for pairing", + "waiting": "Applied, waiting for connection", + "initializing": "Initializing", + "appliedHelp": "Configuration applied. Closing keeps the configuration and completed pairing.", + "draftHelp": "Choose a driver and device. The running configuration changes only when you apply.", + "dirty": "There are unsaved feature changes. Return to save them, or discard them to continue.", + "returnSave": "Return to save", + "discard": "Discard changes and continue", + "pairInstructions": "Add a Bluetooth device in the controlled computer’s system settings and select “{name}”. The pairing window lasts 120 seconds.", + "pairTimeout": "The pairing window ended. You can open it again.", + "reopenPairing": "Reopen pairing", + "resetWarning": "Previous One-KVM HID bonds will be cleared. Remove the old device on the controlled computer before adding it again.", + "disableUsb": "Leaving USB OTG will disable: {functions}.", + "msd": "Virtual disk", + "network": "USB network", + "audio": "USB audio", + "done": "Done", + "later": "Connect later", + "configureLater": "Configure later", + "useConfiguration": "Use this configuration", + "repair": "Pair again", + "uncertain": "The previous request has an unknown result. Check the connection first; applying again will clear bonds again.", + "checkConnection": "Check applied configuration", + "resumeRetry": "The current configuration differs from the pending selection. Review it and retry." +}, + bluetoothHid: { + paired: 'Paired; waiting for the computer to connect keyboard and mouse', + "description": "Classic Bluetooth keyboard and relative mouse.", + "adapter": "Bluetooth adapter", + "name": "Device name", + "peer": "Computer address (optional)", + "peerHelp": "Leave blank to reuse the only bonded device, or select a computer during pairing.", + "ready": "Keyboard and mouse ready", + "pairing": "Pairing open: {seconds}s", + "connected": "Connected; waiting for HID channels", + "waiting": "Waiting for the paired computer", + "unavailable": "Bluetooth unavailable or starting", + "openPairing": "Pair for 2 minutes", + "closePairing": "Close pairing", + "disconnect": "Disconnect", + "forget": "Forget computer", + "windows": "Windows 11: Settings → Bluetooth & devices → Add device → Bluetooth. After forgetting, remove the device in Windows before pairing again.", + "applyFirst": "Apply the HID settings first, then open pairing." +}, + videoInput: { format: 'Input Format', resolution: 'Resolution', diff --git a/web/src/i18n/zh-CN.ts b/web/src/i18n/zh-CN.ts index e136c888..1fd43e7b 100644 --- a/web/src/i18n/zh-CN.ts +++ b/web/src/i18n/zh-CN.ts @@ -1,4 +1,86 @@ export default { + hidGuide: { + missingDevice: '原设备 {device} 已不可用,请检查当前选择。', + connectionTimeout: '等待连接超时,请检查数据线及被控机 USB 端口。可稍后连接或重新配置。', + "deviceTitle": "驱动与设备", + "features": "功能配置", + "saveFeatures": "保存功能配置", + "configure": "配置驱动", + "reconfigure": "重新配置", + "driver": "HID 驱动", + "driver_otg": "USB OTG", + "driver_ch9329": "CH9329", + "driver_bluetooth": "蓝牙 HID", + "driver_none": "禁用", + "device_otg": "UDC", + "device_ch9329": "串口", + "device_bluetooth": "本机蓝牙适配器", + "host": "被控机", + "hostUsb": "被控机 USB", + "otgPort": "One-KVM OTG USB", + "serialLink": "CH340 → 串口连接 → CH9329", + "integratedCable": "CH340 + CH9329 一体线", + "dataCable": "USB 数据线", + "wireless": "蓝牙无线连接", + "wiring_otg": "One-KVM 的 OTG USB 端口 → USB 数据线 → 被控机 USB 端口。", + "wiring_ch9329": "One-KVM 通过串口连接 CH9329,CH9329 的 USB 连接被控机。", + "wiring_bluetooth": "在被控机打开系统设置 → 添加蓝牙设备 → 选择广播的 HID 设备名称。", + "disabledHelp": "禁用后 One-KVM 无法发送键盘和鼠标输入。", + "selectDevice": "请选择具体设备", + "noDevices": "未找到可用设备,请检查连接后刷新。", + "nameInvalid": "名称须为 1–64 个 UTF-8 字节,且不能含控制字符。", + "unconfigured": "尚未配置 HID", + "disabled": "HID 已禁用", + "legacyAuto": "自动选择(旧配置)", + "unknown": "状态未知或读取失败", + "ready": "HID 可用", + "preparing": "已连接,HID 准备中", + "paired": "已配对,等待连接", + "pairing": "等待配对", + "waiting": "已应用,等待连接", + "initializing": "正在初始化", + "appliedHelp": "配置已应用。关闭引导会保留配置和已完成的配对。", + "draftHelp": "选择驱动和设备,点击应用后才会更改当前配置。", + "dirty": "功能配置有未保存的修改。请返回保存,或放弃修改后继续。", + "returnSave": "返回保存", + "discard": "放弃修改并继续", + "pairInstructions": "请在被控机的系统设置中添加蓝牙设备,选择“{name}”。配对窗口为 120 秒。", + "pairTimeout": "配对窗口已结束,可重新开启。", + "reopenPairing": "重新开启配对", + "resetWarning": "将清除原 One-KVM HID 绑定;请在被控机中删除旧设备后重新添加。", + "disableUsb": "切出 USB OTG 将关闭:{functions}。", + "msd": "虚拟磁盘", + "network": "USB 网络", + "audio": "USB 音频", + "done": "完成", + "later": "稍后连接", + "configureLater": "稍后配置", + "useConfiguration": "使用此配置", + "repair": "重新配对", + "uncertain": "上次请求结果不明。可先检查连接;再次应用将重新清除绑定。", + "checkConnection": "检查已应用配置", + "resumeRetry": "已读取当前配置,与待应用选择不一致。请检查选择后重试。" +}, + bluetoothHid: { + paired: '已配对,等待电脑建立键鼠连接', + "description": "经典蓝牙键盘和相对鼠标。", + "adapter": "蓝牙适配器", + "name": "设备名称", + "peer": "被控电脑地址(可选)", + "peerHelp": "留空时复用唯一的已绑定设备,或在配对时选择电脑。", + "ready": "键盘和鼠标已就绪", + "pairing": "配对窗口剩余 {seconds} 秒", + "connected": "已连接,等待 HID 通道建立", + "waiting": "等待已配对电脑连接", + "unavailable": "蓝牙不可用或正在启动", + "openPairing": "开启配对(2 分钟)", + "closePairing": "关闭配对", + "disconnect": "断开连接", + "forget": "忘记电脑", + "windows": "Windows 11:设置 → 蓝牙和其他设备 → 添加设备 → 蓝牙。忘记设备后,请同时在 Windows 删除设备再重新配对。", + "applyFirst": "先应用 HID 设置,再开启配对。" +}, + videoInput: { format: '输入格式', resolution: '分辨率', diff --git a/web/src/lib/hidGuide.ts b/web/src/lib/hidGuide.ts new file mode 100644 index 00000000..cfdcbc69 --- /dev/null +++ b/web/src/lib/hidGuide.ts @@ -0,0 +1,73 @@ +import type { HidConfig, HidConfigUpdate } from '../types/generated' +import type { BluetoothStatus } from '../types/bluetooth' +export type Driver = 'otg' | 'ch9329' | 'bluetooth' | 'none' +export interface HidSelection { + backend: Driver; otg_udc: string; ch9329_port: string; ch9329_baudrate: number + bluetooth: { adapter: string; name: string } +} +export function selectionFrom(hid?: HidConfig | null): HidSelection { + return { backend: hid?.backend ?? 'ch9329', otg_udc: hid?.otg_udc ?? '', + ch9329_port: hid?.ch9329_port ?? '', ch9329_baudrate: hid?.ch9329_baudrate ?? 9600, + bluetooth: { adapter: hid?.bluetooth.adapter ?? '', name: hid?.bluetooth.name ?? 'One-KVM HID' } } +} +export function selectDevice(saved: string, candidates: string[]): string { + return candidates.includes(saved) ? saved : candidates.length === 1 ? candidates[0]! : '' +} +export function validName(name: string): boolean { + const length = new TextEncoder().encode(name).length + return length > 0 && length <= 64 && !/[\u0000-\u001f\u007f-\u009f]/u.test(name) +} +export function deviceRequest(draft: HidSelection): HidConfigUpdate { + const backend = draft.backend as HidConfigUpdate['backend'] + switch (draft.backend) { + case 'otg': return { backend, otg_udc: draft.otg_udc } + case 'ch9329': return { backend, ch9329_port: draft.ch9329_port, ch9329_baudrate: draft.ch9329_baudrate } + case 'bluetooth': return { backend, bluetooth: { ...draft.bluetooth }, bluetooth_reset_pairing: true } + case 'none': return { backend } + } +} +export function matchesSelection(hid: HidConfig, draft: HidSelection): boolean { + if (hid.backend !== draft.backend) return false + switch (draft.backend) { + case 'otg': return hid.otg_udc === draft.otg_udc + case 'ch9329': return hid.ch9329_port === draft.ch9329_port && hid.ch9329_baudrate === draft.ch9329_baudrate + case 'bluetooth': return hid.bluetooth.adapter === draft.bluetooth.adapter && hid.bluetooth.name === draft.bluetooth.name + case 'none': return true + } +} +export function bluetoothStage(status: BluetoothStatus | null): string { + if (!status || status.error) return 'unknown' + if (status.ready) return 'ready' + if (status.connected) return 'preparing' + if (status.devices.some(d => d.address === status.peer && d.paired)) return 'paired' + if (status.pairing_seconds > 0) return 'pairing' + return status.initialized ? 'waiting' : 'initializing' +} +export interface HidDeviceStatus { + backend: string; online: boolean; error?: string | null; error_code?: string | null +} +export function hidDeviceError(status?: HidDeviceStatus | null): string | null { + // An unplugged OTG cable is an expected connection state, not a hardware fault. + if (status?.backend === 'otg' && status.error_code === 'udc_not_configured') return null + return status?.error ?? null +} +export function hidDeviceStage(status: HidDeviceStatus | null | undefined, bluetooth: BluetoothStatus | null, readError = ''): string { + if (!status || readError || hidDeviceError(status)) return 'unknown' + if (status.backend === 'bluetooth') return bluetoothStage(bluetooth) + if (status.backend === 'otg' && status.error_code === 'udc_not_configured') return 'waiting' + return status.online ? 'ready' : 'waiting' +} +export const pendingHidKey = 'one-kvm.pending-hid.v1' +export interface PendingHid { selection: HidSelection; phase: 'selected' | 'applying' | 'applied' } +export function readPendingHid(): PendingHid | null { + try { + const value = JSON.parse(sessionStorage.getItem(pendingHidKey) ?? 'null') + if (value && ['selected', 'applying', 'applied'].includes(value.phase) + && ['otg', 'ch9329', 'bluetooth', 'none'].includes(value.selection?.backend) + && typeof value.selection.otg_udc === 'string' && typeof value.selection.ch9329_port === 'string' + && typeof value.selection.ch9329_baudrate === 'number' + && typeof value.selection.bluetooth?.adapter === 'string' && typeof value.selection.bluetooth?.name === 'string') return value + } catch { /* Invalid or unavailable session storage is not an applied configuration. */ } + return null +} +export function writePendingHid(value: PendingHid) { sessionStorage.setItem(pendingHidKey, JSON.stringify(value)) } diff --git a/web/src/lib/hidStatus.ts b/web/src/lib/hidStatus.ts new file mode 100644 index 00000000..dc8ec432 --- /dev/null +++ b/web/src/lib/hidStatus.ts @@ -0,0 +1,41 @@ +type Status = 'connected' | 'connecting' | 'disconnected' | 'error' + +interface HidState { + available: boolean + initialized: boolean + online: boolean + backend: string + error?: string | null + errorCode?: string | null +} + +interface InputTransport { + useWebRtc: boolean + dataChannelReady: boolean + rtcConnecting: boolean + rtcConnected: boolean + wsConnected: boolean + wsNetworkError: boolean + wsHidUnavailable: boolean +} + +export function getHidStatus(hid: HidState | null, transport: InputTransport): Status { + if (hid?.errorCode === 'udc_not_configured') return 'disconnected' + if (hid?.error) return 'error' + if (!hid?.available) return 'disconnected' + + // A browser DataChannel can stay open after the controlled computer disconnects. + // Both the HID backend and the browser input transport must be ready. + if (!hid.online) { + return hid.initialized && hid.backend !== 'bluetooth' ? 'connecting' : 'disconnected' + } + + if (transport.useWebRtc) { + if (transport.dataChannelReady) return 'connected' + if (transport.rtcConnecting || transport.rtcConnected) return 'connecting' + } + + if (transport.wsNetworkError) return 'connecting' + if (!transport.wsConnected || transport.wsHidUnavailable) return 'disconnected' + return 'connected' +} diff --git a/web/src/stores/config.ts b/web/src/stores/config.ts index 0cd16ee9..81a0d6c7 100644 --- a/web/src/stores/config.ts +++ b/web/src/stores/config.ts @@ -516,8 +516,8 @@ export const useConfigStore = defineStore('config', () => { return response } - async function updateHid(update: HidConfigUpdate) { - const response = await hidConfigApi.update(update) + async function updateHid(update: HidConfigUpdate, signal?: AbortSignal) { + const response = await hidConfigApi.update(update, signal) hid.value = response return response } diff --git a/web/src/types/bluetooth.ts b/web/src/types/bluetooth.ts new file mode 100644 index 00000000..f0bc9f3a --- /dev/null +++ b/web/src/types/bluetooth.ts @@ -0,0 +1,7 @@ +export interface BluetoothAdapter { name: string; address: string; powered: boolean } +export interface BluetoothStatus { + initialized: boolean; connected: boolean; ready: boolean; adapter: string; adapter_address: string + peer?: string | null; pairing_seconds: number; control_connected: boolean; interrupt_connected: boolean + error?: string | null + devices: Array<{ address: string; name: string; paired: boolean; connected: boolean }> +} diff --git a/web/src/types/generated.ts b/web/src/types/generated.ts index 4712034e..5b47af16 100644 --- a/web/src/types/generated.ts +++ b/web/src/types/generated.ts @@ -16,9 +16,16 @@ export interface VideoConfig { quality: number; } +export interface BluetoothHidConfig { + adapter: string; + name: string; + peer?: string; +} + export enum HidBackend { Otg = "otg", Ch9329 = "ch9329", + Bluetooth = "bluetooth", None = "none", } @@ -54,6 +61,7 @@ export interface Ch9329DescriptorConfig { } export interface HidConfig { + bluetooth: BluetoothHidConfig; backend: HidBackend; otg_udc?: string; otg_descriptor?: OtgDescriptorConfig; @@ -304,6 +312,13 @@ export interface WatchdogConfig { enabled: boolean; } +/** Configuration for the USB Audio Class microphone gadget. */ +export interface UacConfig { + enabled: boolean; + sample_rate: number; + channels: number; +} + export interface AppConfig { initialized: boolean; auth: AuthConfig; @@ -322,6 +337,7 @@ export interface AppConfig { rtsp: RtspConfig; redfish: RedfishConfig; watchdog: WatchdogConfig; + uac: UacConfig; } /** Update for a single ATX output binding */ @@ -548,6 +564,8 @@ export interface OtgHidFunctionsUpdate { } export interface HidConfigUpdate { + bluetooth_reset_pairing?: boolean; + bluetooth?: BluetoothHidConfig; backend?: HidBackend; ch9329_port?: string; ch9329_baudrate?: number; diff --git a/web/src/views/ConsoleView.vue b/web/src/views/ConsoleView.vue index e774fbfb..93444222 100644 --- a/web/src/views/ConsoleView.vue +++ b/web/src/views/ConsoleView.vue @@ -25,6 +25,7 @@ import { keyboardEventToCanonicalKey, updateModifierMaskForKey } from '@/lib/key import { toast } from 'vue-sonner' import { cn, generateUUID } from '@/lib/utils' import { formatFpsValue } from '@/lib/fps' +import { getHidStatus } from '@/lib/hidStatus' import { videoDebugLog } from '@/lib/debugLog' import { formatVideoDeviceLabel } from '@/lib/video-device-label' import { isAudioDeviceLostStateReason, isAudioStreamDeviceLostPayload } from '@/lib/streamSignal' @@ -212,7 +213,7 @@ const isConsoleActive = ref(false) function syncMouseModeFromConfig() { const mouseAbsolute = configStore.hid?.mouse_absolute if (typeof mouseAbsolute !== 'boolean') return - const nextMode: 'absolute' | 'relative' = mouseAbsolute ? 'absolute' : 'relative' + const nextMode: 'absolute' | 'relative' = mouseAbsolute && configStore.hid?.backend !== 'bluetooth' ? 'absolute' : 'relative' if (mouseMode.value !== nextMode) { resetTouchInput() mouseMode.value = nextMode @@ -353,27 +354,15 @@ const videoDetails = computed(() => { return details }) -const hidStatus = computed<'connected' | 'connecting' | 'disconnected' | 'error'>(() => { - const hid = systemStore.hid - if (hid?.errorCode === 'udc_not_configured') return 'disconnected' - if (hid?.error) return 'error' - - if (videoMode.value !== 'mjpeg') { - if (webrtc.dataChannelReady.value) return 'connected' - if (webrtc.isConnecting.value) return 'connecting' - if (webrtc.isConnected.value) return 'connecting' - } - - if (hidWs.networkError.value) return 'connecting' - - if (!hidWs.connected.value) return 'disconnected' - - if (hidWs.hidUnavailable.value) return 'disconnected' - - if (hid?.available && hid.online) return 'connected' - if (hid?.available && hid.initialized) return 'connecting' - return 'disconnected' -}) +const hidStatus = computed(() => getHidStatus(systemStore.hid, { + useWebRtc: videoMode.value !== 'mjpeg', + dataChannelReady: webrtc.dataChannelReady.value, + rtcConnecting: webrtc.isConnecting.value, + rtcConnected: webrtc.isConnected.value, + wsConnected: hidWs.connected.value, + wsNetworkError: hidWs.networkError.value, + wsHidUnavailable: hidWs.hidUnavailable.value, +})) const hidQuickInfo = computed(() => { const hid = systemStore.hid @@ -3072,7 +3061,7 @@ function handleToggleMouseMode() { exitPointerLock() } - mouseMode.value = mouseMode.value === 'absolute' ? 'relative' : 'absolute' + mouseMode.value = configStore.hid?.backend === 'bluetooth' ? 'relative' : (mouseMode.value === 'absolute' ? 'relative' : 'absolute') pendingMouseMove = null accumulatedDelta = { x: 0, y: 0 } lastMousePosition.value = { x: 0, y: 0 } diff --git a/web/src/views/SettingsView.vue b/web/src/views/SettingsView.vue index 22b188f5..f076783d 100644 --- a/web/src/views/SettingsView.vue +++ b/web/src/views/SettingsView.vue @@ -1,5 +1,7 @@