refactor(otg): 简化运行时与设置逻辑

This commit is contained in:
mofeng-git
2026-03-28 21:09:10 +08:00
parent 4784cb75e4
commit f4283f45a4
27 changed files with 1427 additions and 1249 deletions

View File

@@ -119,9 +119,12 @@ const myClientId = generateUUID()
// HID state
const mouseMode = ref<'absolute' | 'relative'>('absolute')
const pressedKeys = ref<CanonicalKey[]>([])
const keyboardLed = ref({
capsLock: false,
})
const keyboardLed = computed(() => ({
capsLock: systemStore.hid?.ledState.capsLock ?? false,
numLock: systemStore.hid?.ledState.numLock ?? false,
scrollLock: systemStore.hid?.ledState.scrollLock ?? false,
}))
const keyboardLedEnabled = computed(() => systemStore.hid?.keyboardLedsEnabled ?? false)
const activeModifierMask = ref(0)
const mousePosition = ref({ x: 0, y: 0 })
const lastMousePosition = ref({ x: 0, y: 0 }) // Track last position for relative mode
@@ -346,6 +349,13 @@ const hidDetails = computed<StatusDetail[]>(() => {
{ 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' },
{
label: t('settings.otgKeyboardLeds'),
value: hid.keyboardLedsEnabled
? `Caps:${hid.ledState.capsLock ? t('common.on') : t('common.off')} Num:${hid.ledState.numLock ? t('common.on') : t('common.off')} Scroll:${hid.ledState.scrollLock ? t('common.on') : t('common.off')}`
: t('infobar.keyboardLedUnavailable'),
status: hid.keyboardLedsEnabled ? 'ok' : undefined,
},
]
if (hid.errorCode) {
@@ -1618,8 +1628,6 @@ function handleKeyDown(e: KeyboardEvent) {
})
}
keyboardLed.value.capsLock = e.getModifierState('CapsLock')
const canonicalKey = keyboardEventToCanonicalKey(e.code, e.key)
if (canonicalKey === undefined) {
console.warn(`[HID] Unmapped key down: code=${e.code}, key=${e.key}`)
@@ -2088,10 +2096,6 @@ function handleVirtualKeyDown(key: CanonicalKey) {
if (!pressedKeys.value.includes(key)) {
pressedKeys.value = [...pressedKeys.value, key]
}
// Toggle CapsLock state when virtual keyboard presses CapsLock
if (key === CanonicalKey.CapsLock) {
keyboardLed.value.capsLock = !keyboardLed.value.capsLock
}
}
function handleVirtualKeyUp(key: CanonicalKey) {
@@ -2536,6 +2540,9 @@ onUnmounted(() => {
<InfoBar
:pressed-keys="pressedKeys"
:caps-lock="keyboardLed.capsLock"
:num-lock="keyboardLed.numLock"
:scroll-lock="keyboardLed.scrollLock"
:keyboard-led-enabled="keyboardLedEnabled"
:mouse-position="mousePosition"
:debug-mode="false"
/>

View File

@@ -33,6 +33,7 @@ import type {
AtxDriverType,
ActiveLevel,
AtxDevices,
OtgEndpointBudget,
OtgHidProfile,
OtgHidFunctions,
} from '@/types/generated'
@@ -326,13 +327,15 @@ const config = ref({
hid_serial_device: '',
hid_serial_baudrate: 9600,
hid_otg_udc: '',
hid_otg_profile: 'full' as OtgHidProfile,
hid_otg_profile: 'custom' as OtgHidProfile,
hid_otg_endpoint_budget: 'six' as OtgEndpointBudget,
hid_otg_functions: {
keyboard: true,
mouse_relative: true,
mouse_absolute: true,
consumer: true,
} as OtgHidFunctions,
hid_otg_keyboard_leds: false,
msd_enabled: false,
msd_dir: '',
encoder_backend: 'auto',
@@ -345,20 +348,6 @@ const config = ref({
// Tracks whether TURN password is configured on the server
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
)
type OtgSelfCheckLevel = 'info' | 'warn' | 'error'
type OtgCheckGroupStatus = 'ok' | 'warn' | 'error' | 'skipped'
@@ -619,28 +608,80 @@ async function onRunVideoEncoderSelfCheckClick() {
await runVideoEncoderSelfCheck()
}
function alignHidProfileForLowEndpoint() {
if (hidProfileAligned.value) return
if (!configLoaded.value || !devicesLoaded.value) return
if (config.value.hid_backend !== 'otg') {
hidProfileAligned.value = true
return
function defaultOtgEndpointBudgetForUdc(udc?: string): OtgEndpointBudget {
return /musb/i.test(udc || '') ? 'five' as OtgEndpointBudget : 'six' as OtgEndpointBudget
}
function normalizeOtgEndpointBudget(budget: OtgEndpointBudget | undefined, udc?: string): OtgEndpointBudget {
if (!budget || budget === 'auto') {
return defaultOtgEndpointBudgetForUdc(udc)
}
if (!isLowEndpointUdc.value) {
hidProfileAligned.value = true
return
return budget
}
function endpointLimitForBudget(budget: OtgEndpointBudget): number | null {
if (budget === 'unlimited') return null
return budget === 'five' ? 5 : 6
}
const effectiveOtgFunctions = computed(() => ({ ...config.value.hid_otg_functions }))
const otgEndpointLimit = computed(() =>
endpointLimitForBudget(config.value.hid_otg_endpoint_budget)
)
const otgRequiredEndpoints = computed(() => {
if (config.value.hid_backend !== 'otg') return 0
const functions = effectiveOtgFunctions.value
let endpoints = 0
if (functions.keyboard) {
endpoints += 1
if (config.value.hid_otg_keyboard_leds) endpoints += 1
}
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
if (functions.mouse_relative) endpoints += 1
if (functions.mouse_absolute) endpoints += 1
if (functions.consumer) endpoints += 1
if (config.value.msd_enabled) endpoints += 2
return endpoints
})
const isOtgEndpointBudgetValid = computed(() => {
if (config.value.hid_backend !== 'otg') return true
const limit = otgEndpointLimit.value
return limit === null || otgRequiredEndpoints.value <= limit
})
const otgEndpointUsageText = computed(() => {
const limit = otgEndpointLimit.value
if (limit === null) {
return t('settings.otgEndpointUsageUnlimited', { used: otgRequiredEndpoints.value })
}
return t('settings.otgEndpointUsage', { used: otgRequiredEndpoints.value, limit })
})
const showOtgEndpointBudgetHint = computed(() =>
config.value.hid_backend === 'otg'
)
const isKeyboardLedToggleDisabled = computed(() =>
config.value.hid_backend !== 'otg' || !effectiveOtgFunctions.value.keyboard
)
function describeEndpointBudget(budget: OtgEndpointBudget): string {
switch (budget) {
case 'five':
return '5'
case 'six':
return '6'
case 'unlimited':
return t('settings.otgEndpointBudgetUnlimited')
default:
return '6'
}
hidProfileAligned.value = true
}
const isHidFunctionSelectionValid = computed(() => {
if (config.value.hid_backend !== 'otg') return true
if (config.value.hid_otg_profile !== 'custom') return true
const f = config.value.hid_otg_functions
return !!(f.keyboard || f.mouse_relative || f.mouse_absolute || f.consumer)
})
@@ -946,26 +987,9 @@ async function saveConfig() {
// HID config
if (activeSection.value === 'hid') {
if (!isHidFunctionSelectionValid.value) {
if (!isHidFunctionSelectionValid.value || !isOtgEndpointBudgetValid.value) {
return
}
let desiredMsdEnabled = config.value.msd_enabled
if (config.value.hid_backend === 'otg') {
if (config.value.hid_otg_profile === 'full') {
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'
) {
desiredMsdEnabled = false
}
}
const hidUpdate: any = {
backend: config.value.hid_backend as any,
ch9329_port: config.value.hid_serial_device || undefined,
@@ -980,16 +1004,15 @@ async function saveConfig() {
product: otgProduct.value || 'One-KVM USB Device',
serial_number: otgSerialNumber.value || undefined,
}
hidUpdate.otg_profile = config.value.hid_otg_profile
hidUpdate.otg_profile = 'custom'
hidUpdate.otg_endpoint_budget = config.value.hid_otg_endpoint_budget
hidUpdate.otg_functions = { ...config.value.hid_otg_functions }
hidUpdate.otg_keyboard_leds = config.value.hid_otg_keyboard_leds
}
savePromises.push(configStore.updateHid(hidUpdate))
if (config.value.msd_enabled !== desiredMsdEnabled) {
config.value.msd_enabled = desiredMsdEnabled
}
savePromises.push(
configStore.updateMsd({
enabled: desiredMsdEnabled,
enabled: config.value.msd_enabled,
})
)
}
@@ -1034,13 +1057,15 @@ async function loadConfig() {
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_profile: 'custom' as OtgHidProfile,
hid_otg_endpoint_budget: normalizeOtgEndpointBudget(hid.otg_endpoint_budget, hid.otg_udc || ''),
hid_otg_functions: {
keyboard: hid.otg_functions?.keyboard ?? true,
mouse_relative: hid.otg_functions?.mouse_relative ?? true,
mouse_absolute: hid.otg_functions?.mouse_absolute ?? true,
consumer: hid.otg_functions?.consumer ?? true,
} as OtgHidFunctions,
hid_otg_keyboard_leds: hid.otg_keyboard_leds ?? false,
msd_enabled: msd.enabled || false,
msd_dir: msd.msd_dir || '',
encoder_backend: stream.encoder || 'auto',
@@ -1065,9 +1090,6 @@ async function loadConfig() {
} catch (e) {
console.error('Failed to load config:', e)
} finally {
configLoaded.value = true
alignHidProfileForLowEndpoint()
}
}
@@ -1076,9 +1098,6 @@ async function loadDevices() {
devices.value = await configApi.listDevices()
} catch (e) {
console.error('Failed to load devices:', e)
} finally {
devicesLoaded.value = true
alignHidProfileForLowEndpoint()
}
}
@@ -2230,63 +2249,75 @@ watch(() => route.query.tab, (tab) => {
<p class="text-sm text-muted-foreground">{{ t('settings.otgHidProfileDesc') }}</p>
</div>
<div class="space-y-2">
<Label for="otg-profile">{{ t('settings.profile') }}</Label>
<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>
<Label for="otg-endpoint-budget">{{ t('settings.otgEndpointBudget') }}</Label>
<select id="otg-endpoint-budget" v-model="config.hid_otg_endpoint_budget" class="w-full h-9 px-3 rounded-md border border-input bg-background text-sm">
<option value="five">5</option>
<option value="six">6</option>
<option value="unlimited">{{ t('settings.otgEndpointBudgetUnlimited') }}</option>
</select>
<p class="text-xs text-muted-foreground">{{ otgEndpointUsageText }}</p>
</div>
<div v-if="config.hid_otg_profile === 'custom'" class="space-y-3 rounded-md border border-border/60 p-3">
<div class="flex items-center justify-between">
<div>
<Label>{{ t('settings.otgFunctionKeyboard') }}</Label>
<p class="text-xs text-muted-foreground">{{ t('settings.otgFunctionKeyboardDesc') }}</p>
<div class="space-y-3">
<div class="space-y-3 rounded-md border border-border/60 p-3">
<div class="flex items-center justify-between">
<div>
<Label>{{ t('settings.otgFunctionMouseRelative') }}</Label>
<p class="text-xs text-muted-foreground">{{ t('settings.otgFunctionMouseRelativeDesc') }}</p>
</div>
<Switch v-model="config.hid_otg_functions.mouse_relative" />
</div>
<Separator />
<div class="flex items-center justify-between">
<div>
<Label>{{ t('settings.otgFunctionMouseAbsolute') }}</Label>
<p class="text-xs text-muted-foreground">{{ t('settings.otgFunctionMouseAbsoluteDesc') }}</p>
</div>
<Switch v-model="config.hid_otg_functions.mouse_absolute" />
</div>
<Switch v-model="config.hid_otg_functions.keyboard" />
</div>
<Separator />
<div class="flex items-center justify-between">
<div>
<Label>{{ t('settings.otgFunctionMouseRelative') }}</Label>
<p class="text-xs text-muted-foreground">{{ t('settings.otgFunctionMouseRelativeDesc') }}</p>
<div class="space-y-3 rounded-md border border-border/60 p-3">
<div class="flex items-center justify-between">
<div>
<Label>{{ t('settings.otgFunctionKeyboard') }}</Label>
<p class="text-xs text-muted-foreground">{{ t('settings.otgFunctionKeyboardDesc') }}</p>
</div>
<Switch v-model="config.hid_otg_functions.keyboard" />
</div>
<Separator />
<div class="flex items-center justify-between">
<div>
<Label>{{ t('settings.otgFunctionConsumer') }}</Label>
<p class="text-xs text-muted-foreground">{{ t('settings.otgFunctionConsumerDesc') }}</p>
</div>
<Switch v-model="config.hid_otg_functions.consumer" />
</div>
<Separator />
<div class="flex items-center justify-between">
<div>
<Label>{{ t('settings.otgKeyboardLeds') }}</Label>
<p class="text-xs text-muted-foreground">{{ t('settings.otgKeyboardLedsDesc') }}</p>
</div>
<Switch v-model="config.hid_otg_keyboard_leds" :disabled="isKeyboardLedToggleDisabled" />
</div>
<Switch v-model="config.hid_otg_functions.mouse_relative" />
</div>
<Separator />
<div class="flex items-center justify-between">
<div>
<Label>{{ t('settings.otgFunctionMouseAbsolute') }}</Label>
<p class="text-xs text-muted-foreground">{{ t('settings.otgFunctionMouseAbsoluteDesc') }}</p>
<div class="space-y-3 rounded-md border border-border/60 p-3">
<div class="flex items-center justify-between">
<div>
<Label>{{ t('settings.otgFunctionMsd') }}</Label>
<p class="text-xs text-muted-foreground">{{ t('settings.otgFunctionMsdDesc') }}</p>
</div>
<Switch v-model="config.msd_enabled" />
</div>
<Switch v-model="config.hid_otg_functions.mouse_absolute" />
</div>
<Separator />
<div class="flex items-center justify-between">
<div>
<Label>{{ t('settings.otgFunctionConsumer') }}</Label>
<p class="text-xs text-muted-foreground">{{ t('settings.otgFunctionConsumerDesc') }}</p>
</div>
<Switch v-model="config.hid_otg_functions.consumer" />
</div>
<Separator />
<div class="flex items-center justify-between">
<div>
<Label>{{ t('settings.otgFunctionMsd') }}</Label>
<p class="text-xs text-muted-foreground">{{ t('settings.otgFunctionMsdDesc') }}</p>
</div>
<Switch v-model="config.msd_enabled" />
</div>
</div>
<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 v-if="showOtgEndpointBudgetHint" class="text-xs text-muted-foreground">
{{ t('settings.otgEndpointBudgetHint') }}
</p>
<p v-if="!isOtgEndpointBudgetValid" class="text-xs text-amber-600 dark:text-amber-400">
{{ t('settings.otgEndpointExceeded', { used: otgRequiredEndpoints, limit: describeEndpointBudget(config.hid_otg_endpoint_budget) }) }}
</p>
</div>
<Separator class="my-4" />

View File

@@ -97,7 +97,12 @@ const ch9329Port = ref('')
const ch9329Baudrate = ref(9600)
const otgUdc = ref('')
const hidOtgProfile = ref('full')
const otgMsdEnabled = ref(true)
const otgEndpointBudget = ref<'five' | 'six' | 'unlimited'>('six')
const otgKeyboardLeds = ref(true)
const otgProfileTouched = ref(false)
const otgEndpointBudgetTouched = ref(false)
const otgKeyboardLedsTouched = ref(false)
const showAdvancedOtg = ref(false)
// Extension settings
@@ -203,19 +208,67 @@ const availableFps = computed(() => {
return resolution?.fps || []
})
const isLowEndpointUdc = computed(() => {
if (otgUdc.value) {
return /musb/i.test(otgUdc.value)
function defaultOtgEndpointBudgetForUdc(udc?: string): 'five' | 'six' {
return /musb/i.test(udc || '') ? 'five' : 'six'
}
function endpointLimitForBudget(budget: 'five' | 'six' | 'unlimited'): number | null {
if (budget === 'unlimited') return null
return budget === 'five' ? 5 : 6
}
const otgRequiredEndpoints = computed(() => {
if (hidBackend.value !== 'otg') return 0
const functions = {
keyboard: hidOtgProfile.value === 'full' || hidOtgProfile.value === 'full_no_consumer' || hidOtgProfile.value === 'legacy_keyboard',
mouseRelative: hidOtgProfile.value === 'full' || hidOtgProfile.value === 'full_no_consumer' || hidOtgProfile.value === 'legacy_mouse_relative',
mouseAbsolute: hidOtgProfile.value === 'full' || hidOtgProfile.value === 'full_no_consumer',
consumer: hidOtgProfile.value === 'full',
}
return devices.value.udc.some((udc) => /musb/i.test(udc.name))
let endpoints = 0
if (functions.keyboard) {
endpoints += 1
if (otgKeyboardLeds.value) endpoints += 1
}
if (functions.mouseRelative) endpoints += 1
if (functions.mouseAbsolute) endpoints += 1
if (functions.consumer) endpoints += 1
if (otgMsdEnabled.value) endpoints += 2
return endpoints
})
function applyOtgProfileDefault() {
if (otgProfileTouched.value) return
const otgProfileHasKeyboard = computed(() =>
hidOtgProfile.value === 'full'
|| hidOtgProfile.value === 'full_no_consumer'
|| hidOtgProfile.value === 'legacy_keyboard'
)
const isOtgEndpointBudgetValid = computed(() => {
const limit = endpointLimitForBudget(otgEndpointBudget.value)
return limit === null || otgRequiredEndpoints.value <= limit
})
const otgEndpointUsageText = computed(() => {
const limit = endpointLimitForBudget(otgEndpointBudget.value)
if (limit === null) {
return t('settings.otgEndpointUsageUnlimited', { used: otgRequiredEndpoints.value })
}
return t('settings.otgEndpointUsage', { used: otgRequiredEndpoints.value, limit })
})
function applyOtgDefaults() {
if (hidBackend.value !== 'otg') return
const preferred = isLowEndpointUdc.value ? 'full_no_consumer' : 'full'
if (hidOtgProfile.value === preferred) return
hidOtgProfile.value = preferred
const recommendedBudget = defaultOtgEndpointBudgetForUdc(otgUdc.value)
if (!otgEndpointBudgetTouched.value) {
otgEndpointBudget.value = recommendedBudget
}
if (!otgProfileTouched.value) {
hidOtgProfile.value = 'full_no_consumer'
}
if (!otgKeyboardLedsTouched.value) {
otgKeyboardLeds.value = otgEndpointBudget.value !== 'five'
}
}
function onOtgProfileChange(value: unknown) {
@@ -223,6 +276,20 @@ function onOtgProfileChange(value: unknown) {
otgProfileTouched.value = true
}
function onOtgEndpointBudgetChange(value: unknown) {
otgEndpointBudget.value =
value === 'five' || value === 'six' || value === 'unlimited' ? value : 'six'
otgEndpointBudgetTouched.value = true
if (!otgKeyboardLedsTouched.value) {
otgKeyboardLeds.value = otgEndpointBudget.value !== 'five'
}
}
function onOtgKeyboardLedsChange(value: boolean) {
otgKeyboardLeds.value = value
otgKeyboardLedsTouched.value = true
}
// Common baud rates for CH9329
const baudRates = [9600, 19200, 38400, 57600, 115200]
@@ -338,16 +405,16 @@ watch(hidBackend, (newBackend) => {
if (newBackend === 'otg' && !otgUdc.value && devices.value.udc.length > 0) {
otgUdc.value = devices.value.udc[0]?.name || ''
}
applyOtgProfileDefault()
applyOtgDefaults()
})
watch(otgUdc, () => {
applyOtgProfileDefault()
applyOtgDefaults()
})
watch(showAdvancedOtg, (open) => {
if (open) {
applyOtgProfileDefault()
applyOtgDefaults()
}
})
@@ -370,7 +437,7 @@ onMounted(async () => {
if (result.udc.length > 0 && result.udc[0]) {
otgUdc.value = result.udc[0].name
}
applyOtgProfileDefault()
applyOtgDefaults()
// Auto-select audio device if available (and no video device to trigger watch)
if (result.audio.length > 0 && !audioDevice.value) {
@@ -461,6 +528,13 @@ function validateStep3(): boolean {
error.value = t('setup.selectUdc')
return false
}
if (hidBackend.value === 'otg' && !isOtgEndpointBudgetValid.value) {
error.value = t('settings.otgEndpointExceeded', {
used: otgRequiredEndpoints.value,
limit: otgEndpointBudget.value === 'unlimited' ? t('settings.otgEndpointBudgetUnlimited') : otgEndpointBudget.value === 'five' ? '5' : '6',
})
return false
}
return true
}
@@ -523,6 +597,9 @@ async function handleSetup() {
if (hidBackend.value === 'otg' && otgUdc.value) {
setupData.hid_otg_udc = otgUdc.value
setupData.hid_otg_profile = hidOtgProfile.value
setupData.hid_otg_endpoint_budget = otgEndpointBudget.value
setupData.hid_otg_keyboard_leds = otgKeyboardLeds.value
setupData.msd_enabled = otgMsdEnabled.value
}
// Encoder backend setting
@@ -990,16 +1067,47 @@ const stepIcons = [User, Video, Keyboard, Puzzle]
</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') }}
<div class="space-y-2">
<Label for="otgEndpointBudget">{{ t('settings.otgEndpointBudget') }}</Label>
<Select :model-value="otgEndpointBudget" @update:modelValue="onOtgEndpointBudgetChange">
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="five">5</SelectItem>
<SelectItem value="six">6</SelectItem>
<SelectItem value="unlimited">{{ t('settings.otgEndpointBudgetUnlimited') }}</SelectItem>
</SelectContent>
</Select>
<p class="text-xs text-muted-foreground">
{{ otgEndpointUsageText }}
</p>
</div>
<div class="flex items-center justify-between rounded-md border border-border/60 p-3">
<div>
<Label>{{ t('settings.otgKeyboardLeds') }}</Label>
<p class="text-xs text-muted-foreground">{{ t('settings.otgKeyboardLedsDesc') }}</p>
</div>
<Switch :model-value="otgKeyboardLeds" :disabled="!otgProfileHasKeyboard" @update:model-value="onOtgKeyboardLedsChange" />
</div>
<div class="flex items-center justify-between rounded-md border border-border/60 p-3">
<div>
<Label>{{ t('settings.otgFunctionMsd') }}</Label>
<p class="text-xs text-muted-foreground">{{ t('settings.otgFunctionMsdDesc') }}</p>
</div>
<Switch v-model="otgMsdEnabled" />
</div>
<p class="text-xs text-muted-foreground">
{{ t('settings.otgEndpointBudgetHint') }}
</p>
<p v-if="!isOtgEndpointBudgetValid" class="text-xs text-amber-600 dark:text-amber-400">
{{ t('settings.otgEndpointExceeded', { used: otgRequiredEndpoints, limit: otgEndpointBudget === 'unlimited' ? t('settings.otgEndpointBudgetUnlimited') : otgEndpointBudget === 'five' ? '5' : '6' }) }}
</p>
</div>
</div>