fix: 修复适配全志平台 OTG 低端点情况

This commit is contained in:
mofeng
2026-01-29 20:00:40 +08:00
parent 2938af32a9
commit 9cb0dd146e
14 changed files with 916 additions and 13 deletions

View File

@@ -84,6 +84,7 @@ export const systemApi = {
hid_ch9329_port?: string
hid_ch9329_baudrate?: number
hid_otg_udc?: string
hid_otg_profile?: string
encoder_backend?: string
audio_device?: string
ttyd_enabled?: boolean

View File

@@ -265,6 +265,10 @@ export default {
// Help tooltips
ch9329Help: 'CH9329 is a serial-to-HID chip connected via serial port. Works with most hardware configurations.',
otgHelp: 'USB OTG mode emulates HID devices directly through USB Device Controller. Requires hardware OTG support.',
otgAdvanced: 'Advanced: OTG Preset',
otgProfile: 'Initial HID Preset',
otgProfileDesc: 'Choose the initial OTG HID preset. You can change this later in Settings.',
otgLowEndpointHint: 'Detected low-endpoint UDC; multimedia keys will be disabled automatically.',
videoDeviceHelp: 'Select the video capture device for capturing the remote host display. Usually an HDMI capture card.',
videoFormatHelp: 'MJPEG has best compatibility. H.264/H.265 uses less bandwidth but requires encoding support.',
// Extensions
@@ -584,10 +588,12 @@ export default {
otgHidProfile: 'OTG HID Profile',
otgHidProfileDesc: 'Select which HID functions are exposed to the host',
profile: 'Profile',
otgProfileFull: 'Full (keyboard + relative mouse + absolute mouse + consumer + MSD)',
otgProfileFullNoMsd: 'Full (keyboard + relative mouse + absolute mouse + consumer, no MSD)',
otgProfileLegacyKeyboard: 'Legacy: keyboard only',
otgProfileLegacyMouseRelative: 'Legacy: relative mouse only',
otgProfileFull: 'Keyboard + relative mouse + absolute mouse + multimedia + MSD',
otgProfileFullNoMsd: 'Keyboard + relative mouse + absolute mouse + multimedia (no MSD)',
otgProfileFullNoConsumer: 'Keyboard + relative mouse + absolute mouse + MSD (no multimedia)',
otgProfileFullNoConsumerNoMsd: 'Keyboard + relative mouse + absolute mouse (no multimedia, no MSD)',
otgProfileLegacyKeyboard: 'Keyboard only',
otgProfileLegacyMouseRelative: 'Relative mouse only',
otgProfileCustom: 'Custom',
otgFunctionKeyboard: 'Keyboard',
otgFunctionKeyboardDesc: 'Standard HID keyboard device',
@@ -600,6 +606,7 @@ export default {
otgFunctionMsd: 'Mass Storage (MSD)',
otgFunctionMsdDesc: 'Expose USB storage to the host',
otgProfileWarning: 'Changing HID functions will reconnect the USB device',
otgLowEndpointHint: 'Low-endpoint UDC detected; multimedia keys will be disabled automatically.',
otgFunctionMinWarning: 'Enable at least one HID function before saving',
// OTG Descriptor
otgDescriptor: 'USB Device Descriptor',

View File

@@ -265,6 +265,10 @@ export default {
// Help tooltips
ch9329Help: 'CH9329 是一款串口转 HID 芯片,通过串口连接到主机。适用于大多数硬件配置。',
otgHelp: 'USB OTG 模式通过 USB 设备控制器直接模拟 HID 设备。需要硬件支持 USB OTG 功能。',
otgAdvanced: '高级OTG 预设',
otgProfile: '初始 HID 预设',
otgProfileDesc: '选择 OTG HID 的初始预设,后续可在设置中修改。',
otgLowEndpointHint: '检测到低端点 UDC将自动禁用多媒体键。',
videoDeviceHelp: '选择用于捕获远程主机画面的视频采集设备。通常是 HDMI 采集卡。',
videoFormatHelp: 'MJPEG 格式兼容性最好H.264/H.265 带宽占用更低但需要编码支持。',
// Extensions
@@ -584,10 +588,12 @@ export default {
otgHidProfile: 'OTG HID 组合',
otgHidProfileDesc: '选择对目标主机暴露的 HID 功能',
profile: '组合',
otgProfileFull: '完整(键盘 + 相对鼠标 + 绝对鼠标 + 多媒体 + 虚拟媒体',
otgProfileFullNoMsd: '完整(键盘 + 相对鼠标 + 绝对鼠标 + 多媒体不含虚拟媒体)',
otgProfileLegacyKeyboard: '兼容:仅键盘',
otgProfileLegacyMouseRelative: '兼容:仅相对鼠标',
otgProfileFull: '键盘 + 相对鼠标 + 绝对鼠标 + 多媒体 + 虚拟媒体',
otgProfileFullNoMsd: '键盘 + 相对鼠标 + 绝对鼠标 + 多媒体不含虚拟媒体)',
otgProfileFullNoConsumer: '键盘 + 相对鼠标 + 绝对鼠标 + 虚拟媒体(不含多媒体)',
otgProfileFullNoConsumerNoMsd: '键盘 + 相对鼠标 + 绝对鼠标(不含多媒体与虚拟媒体)',
otgProfileLegacyKeyboard: '仅键盘',
otgProfileLegacyMouseRelative: '仅相对鼠标',
otgProfileCustom: '自定义',
otgFunctionKeyboard: '键盘',
otgFunctionKeyboardDesc: '标准 HID 键盘设备',
@@ -600,6 +606,7 @@ export default {
otgFunctionMsd: '虚拟媒体MSD',
otgFunctionMsdDesc: '向目标主机暴露 USB 存储',
otgProfileWarning: '修改 HID 功能将导致 USB 设备重新连接',
otgLowEndpointHint: '检测到低端点 UDC将自动禁用多媒体键。',
otgFunctionMinWarning: '请至少启用一个 HID 功能后再保存',
// OTG Descriptor
otgDescriptor: 'USB 设备描述符',

View File

@@ -95,6 +95,7 @@ export const useAuthStore = defineStore('auth', () => {
hid_ch9329_port?: string
hid_ch9329_baudrate?: number
hid_otg_udc?: string
hid_otg_profile?: string
encoder_backend?: string
audio_device?: string
ttyd_enabled?: boolean

View File

@@ -60,6 +60,10 @@ export enum OtgHidProfile {
Full = "full",
/** Full HID device set without MSD */
FullNoMsd = "full_no_msd",
/** Full HID device set without consumer control */
FullNoConsumer = "full_no_consumer",
/** Full HID device set without consumer control and MSD */
FullNoConsumerNoMsd = "full_no_consumer_no_msd",
/** Legacy profile: only keyboard */
LegacyKeyboard = "legacy_keyboard",
/** Legacy profile: only relative mouse */

View File

@@ -220,12 +220,14 @@ interface DeviceConfig {
}>
serial: Array<{ path: string; name: string }>
audio: Array<{ name: string; description: string }>
udc: Array<{ name: string }>
}
const devices = ref<DeviceConfig>({
video: [],
serial: [],
audio: [],
udc: [],
})
const config = ref({
@@ -237,6 +239,7 @@ const config = ref({
hid_backend: 'ch9329',
hid_serial_device: '',
hid_serial_baudrate: 9600,
hid_otg_udc: '',
hid_otg_profile: 'full' as OtgHidProfile,
hid_otg_functions: {
keyboard: true,
@@ -257,6 +260,39 @@ const config = ref({
// 跟踪服务器是否已配置 TURN 密码
const hasTurnPassword = ref(false)
const configLoaded = ref(false)
const devicesLoaded = ref(false)
const hidProfileAligned = ref(false)
const isLowEndpointUdc = computed(() => {
if (config.value.hid_otg_udc) {
return /musb/i.test(config.value.hid_otg_udc)
}
return devices.value.udc.some((udc) => /musb/i.test(udc.name))
})
const showLowEndpointHint = computed(() =>
config.value.hid_backend === 'otg' && isLowEndpointUdc.value
)
function alignHidProfileForLowEndpoint() {
if (hidProfileAligned.value) return
if (!configLoaded.value || !devicesLoaded.value) return
if (config.value.hid_backend !== 'otg') {
hidProfileAligned.value = true
return
}
if (!isLowEndpointUdc.value) {
hidProfileAligned.value = true
return
}
if (config.value.hid_otg_profile === 'full') {
config.value.hid_otg_profile = 'full_no_consumer' as OtgHidProfile
} else if (config.value.hid_otg_profile === 'full_no_msd') {
config.value.hid_otg_profile = 'full_no_consumer_no_msd' as OtgHidProfile
}
hidProfileAligned.value = true
}
const isHidFunctionSelectionValid = computed(() => {
if (config.value.hid_backend !== 'otg') return true
@@ -550,6 +586,10 @@ async function saveConfig() {
desiredMsdEnabled = true
} else if (config.value.hid_otg_profile === 'full_no_msd') {
desiredMsdEnabled = false
} else if (config.value.hid_otg_profile === 'full_no_consumer') {
desiredMsdEnabled = true
} else if (config.value.hid_otg_profile === 'full_no_consumer_no_msd') {
desiredMsdEnabled = false
} else if (
config.value.hid_otg_profile === 'legacy_keyboard'
|| config.value.hid_otg_profile === 'legacy_mouse_relative'
@@ -624,6 +664,7 @@ async function loadConfig() {
hid_backend: hid.backend || 'none',
hid_serial_device: hid.ch9329_port || '',
hid_serial_baudrate: hid.ch9329_baudrate || 9600,
hid_otg_udc: hid.otg_udc || '',
hid_otg_profile: (hid.otg_profile || 'full') as OtgHidProfile,
hid_otg_functions: {
keyboard: hid.otg_functions?.keyboard ?? true,
@@ -664,6 +705,9 @@ async function loadConfig() {
}
} catch (e) {
console.error('Failed to load config:', e)
} finally {
configLoaded.value = true
alignHidProfileForLowEndpoint()
}
}
@@ -672,6 +716,9 @@ async function loadDevices() {
devices.value = await configApi.listDevices()
} catch (e) {
console.error('Failed to load devices:', e)
} finally {
devicesLoaded.value = true
alignHidProfileForLowEndpoint()
}
}
@@ -1492,6 +1539,8 @@ onMounted(async () => {
<select id="otg-profile" v-model="config.hid_otg_profile" class="w-full h-9 px-3 rounded-md border border-input bg-background text-sm">
<option value="full">{{ t('settings.otgProfileFull') }}</option>
<option value="full_no_msd">{{ t('settings.otgProfileFullNoMsd') }}</option>
<option value="full_no_consumer">{{ t('settings.otgProfileFullNoConsumer') }}</option>
<option value="full_no_consumer_no_msd">{{ t('settings.otgProfileFullNoConsumerNoMsd') }}</option>
<option value="legacy_keyboard">{{ t('settings.otgProfileLegacyKeyboard') }}</option>
<option value="legacy_mouse_relative">{{ t('settings.otgProfileLegacyMouseRelative') }}</option>
<option value="custom">{{ t('settings.otgProfileCustom') }}</option>
@@ -1541,6 +1590,9 @@ onMounted(async () => {
<p class="text-xs text-amber-600 dark:text-amber-400">
{{ t('settings.otgProfileWarning') }}
</p>
<p v-if="showLowEndpointHint" class="text-xs text-amber-600 dark:text-amber-400">
{{ t('settings.otgLowEndpointHint') }}
</p>
</div>
<Separator class="my-4" />
<div class="space-y-4">

View File

@@ -96,6 +96,9 @@ const hidBackend = ref('ch9329')
const ch9329Port = ref('')
const ch9329Baudrate = ref(9600)
const otgUdc = ref('')
const hidOtgProfile = ref('full')
const otgProfileTouched = ref(false)
const showAdvancedOtg = ref(false)
// Extension settings
const ttydEnabled = ref(false)
@@ -200,6 +203,26 @@ const availableFps = computed(() => {
return resolution?.fps || []
})
const isLowEndpointUdc = computed(() => {
if (otgUdc.value) {
return /musb/i.test(otgUdc.value)
}
return devices.value.udc.some((udc) => /musb/i.test(udc.name))
})
function applyOtgProfileDefault() {
if (otgProfileTouched.value) return
if (hidBackend.value !== 'otg') return
const preferred = isLowEndpointUdc.value ? 'full_no_consumer' : 'full'
if (hidOtgProfile.value === preferred) return
hidOtgProfile.value = preferred
}
function onOtgProfileChange(value: unknown) {
hidOtgProfile.value = typeof value === 'string' ? value : 'full'
otgProfileTouched.value = true
}
// Common baud rates for CH9329
const baudRates = [9600, 19200, 38400, 57600, 115200]
@@ -315,6 +338,17 @@ watch(hidBackend, (newBackend) => {
if (newBackend === 'otg' && !otgUdc.value && devices.value.udc.length > 0) {
otgUdc.value = devices.value.udc[0]?.name || ''
}
applyOtgProfileDefault()
})
watch(otgUdc, () => {
applyOtgProfileDefault()
})
watch(showAdvancedOtg, (open) => {
if (open) {
applyOtgProfileDefault()
}
})
onMounted(async () => {
@@ -336,6 +370,7 @@ onMounted(async () => {
if (result.udc.length > 0 && result.udc[0]) {
otgUdc.value = result.udc[0].name
}
applyOtgProfileDefault()
// Auto-select audio device if available (and no video device to trigger watch)
if (result.audio.length > 0 && !audioDevice.value) {
@@ -487,6 +522,7 @@ async function handleSetup() {
}
if (hidBackend.value === 'otg' && otgUdc.value) {
setupData.hid_otg_udc = otgUdc.value
setupData.hid_otg_profile = hidOtgProfile.value
}
// Encoder backend setting
@@ -924,6 +960,46 @@ const stepIcons = [User, Video, Keyboard, Puzzle]
{{ t('setup.noUdcDevices') }}
</p>
</div>
<div class="mt-2 border rounded-lg">
<button
type="button"
class="w-full flex items-center justify-between p-3 text-left hover:bg-muted/50 rounded-lg transition-colors"
@click="showAdvancedOtg = !showAdvancedOtg"
>
<span class="text-sm font-medium">
{{ t('setup.otgAdvanced') }} ({{ t('common.optional') }})
</span>
<ChevronRight
class="h-4 w-4 transition-transform duration-200"
:class="{ 'rotate-90': showAdvancedOtg }"
/>
</button>
<div v-if="showAdvancedOtg" class="px-3 pb-3 space-y-3">
<p class="text-xs text-muted-foreground">
{{ t('setup.otgProfileDesc') }}
</p>
<div class="space-y-2">
<Label for="otgProfile">{{ t('setup.otgProfile') }}</Label>
<Select :model-value="hidOtgProfile" @update:modelValue="onOtgProfileChange">
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="full">{{ t('settings.otgProfileFull') }}</SelectItem>
<SelectItem value="full_no_msd">{{ t('settings.otgProfileFullNoMsd') }}</SelectItem>
<SelectItem value="full_no_consumer">{{ t('settings.otgProfileFullNoConsumer') }}</SelectItem>
<SelectItem value="full_no_consumer_no_msd">{{ t('settings.otgProfileFullNoConsumerNoMsd') }}</SelectItem>
<SelectItem value="legacy_keyboard">{{ t('settings.otgProfileLegacyKeyboard') }}</SelectItem>
<SelectItem value="legacy_mouse_relative">{{ t('settings.otgProfileLegacyMouseRelative') }}</SelectItem>
</SelectContent>
</Select>
</div>
<p v-if="isLowEndpointUdc" class="text-xs text-amber-600 dark:text-amber-400">
{{ t('setup.otgLowEndpointHint') }}
</p>
</div>
</div>
</div>
</div>