mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-07-30 05:11:45 +08:00
feat: 初步支持 USB 网卡桥接;删除 OTG 端点预算管理
This commit is contained in:
@@ -10,6 +10,12 @@ import type {
|
||||
HidConfigUpdate,
|
||||
MsdConfig,
|
||||
MsdConfigUpdate,
|
||||
NetworkInterfaceInfo,
|
||||
OtgNetworkConfig,
|
||||
OtgNetworkConfigUpdate,
|
||||
OtgNetworkStatus,
|
||||
OtgConfigUpdate,
|
||||
OtgConfigResponse,
|
||||
AtxConfig,
|
||||
AtxConfigUpdate,
|
||||
AtxDevices,
|
||||
@@ -86,6 +92,28 @@ export const msdConfigApi = {
|
||||
}),
|
||||
}
|
||||
|
||||
export const otgNetworkApi = {
|
||||
get: () => request<OtgNetworkConfig>('/config/otg-network'),
|
||||
|
||||
update: (config: OtgNetworkConfigUpdate) =>
|
||||
request<OtgNetworkConfig>('/config/otg-network', {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(config),
|
||||
}),
|
||||
|
||||
status: () => request<OtgNetworkStatus>('/otg/network/status'),
|
||||
|
||||
interfaces: () => request<NetworkInterfaceInfo[]>('/devices/network'),
|
||||
}
|
||||
|
||||
export const otgConfigApi = {
|
||||
update: (config: OtgConfigUpdate) =>
|
||||
request<OtgConfigResponse>('/config/otg', {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(config),
|
||||
}),
|
||||
}
|
||||
|
||||
export interface WolHistoryEntry {
|
||||
mac_address: string
|
||||
updated_at: number
|
||||
|
||||
@@ -123,7 +123,6 @@ export const systemApi = {
|
||||
hid_ch9329_baudrate?: number
|
||||
hid_otg_udc?: string
|
||||
hid_otg_profile?: string
|
||||
hid_otg_endpoint_budget?: string
|
||||
hid_otg_keyboard_leds?: boolean
|
||||
msd_enabled?: boolean
|
||||
encoder_backend?: string
|
||||
@@ -804,6 +803,8 @@ export {
|
||||
streamConfigApi,
|
||||
hidConfigApi,
|
||||
msdConfigApi,
|
||||
otgConfigApi,
|
||||
otgNetworkApi,
|
||||
atxConfigApi,
|
||||
audioConfigApi,
|
||||
extensionsApi,
|
||||
|
||||
@@ -263,7 +263,6 @@ export default {
|
||||
progress: 'Step {current} of {total}',
|
||||
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.',
|
||||
otgLowEndpointHint: 'Detected low-endpoint UDC; Consumer Control Keyboard 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.',
|
||||
stepExtensions: 'Extensions',
|
||||
@@ -510,7 +509,7 @@ export default {
|
||||
accountSubtitle: 'Manage credentials and session policy',
|
||||
networkSubtitle: 'Configure web server ports, listen addresses and SSL certificate',
|
||||
videoSubtitle: 'Configure capture device, video encoder and WebRTC ICE servers',
|
||||
hidSubtitle: 'Configure keyboard and mouse backend with USB gadget descriptors',
|
||||
hidSubtitle: 'Configure the HID backend, device, descriptors, and USB gadget functions',
|
||||
msdSubtitle: 'Manage Mass Storage Device image directory',
|
||||
atxSubtitle: 'Configure remote power control hardware and Wake-on-LAN',
|
||||
environmentSubtitle: 'System runtime environment and USB device maintenance',
|
||||
@@ -712,10 +711,12 @@ export default {
|
||||
supportedFormats: 'Supported Codecs',
|
||||
encoderHint: 'Hardware encoders deliver lower latency and CPU usage; software encoders offer broader compatibility at a higher resource cost.',
|
||||
hidSettings: 'HID Settings',
|
||||
hidSettingsDesc: 'Configure keyboard and mouse control',
|
||||
hidSettingsDesc: 'Configure backend, device, descriptors, and functions in order',
|
||||
hidBackend: 'HID Backend',
|
||||
serialDevice: 'Serial Device',
|
||||
baudRate: 'Baud Rate',
|
||||
otgUdc: 'OTG Device Controller',
|
||||
otgUdcDesc: 'Select the UDC used by the USB gadget, or leave blank for automatic selection',
|
||||
ch9329Options: 'CH9329 Options',
|
||||
ch9329OptionsDesc: 'Configure runtime compatibility for the CH9329 serial HID chip',
|
||||
ch9329HybridMouse: 'Linux Absolute Mouse Compatibility',
|
||||
@@ -728,14 +729,8 @@ export default {
|
||||
ch9329DescriptorReadRequired: 'Read the CH9329 descriptor successfully before saving',
|
||||
ch9329DescriptorWarning: 'Saving writes CH9329 parameters; changes may not show until the device is power-cycled or reconnected',
|
||||
ch9329StringLengthWarning: 'CH9329 strings are limited to 23 bytes',
|
||||
otgHidProfile: 'OTG HID Functions',
|
||||
otgHidProfileDesc: 'Select which HID functions are exposed to the host',
|
||||
otgEndpointBudget: 'Max Endpoints',
|
||||
otgEndpointBudgetUnlimited: 'Unlimited',
|
||||
otgEndpointBudgetHint: 'This is a hardware limit. If the OTG selection exceeds the real hardware endpoint count, OTG will fail.',
|
||||
otgEndpointUsage: 'Endpoint usage: {used} / {limit}',
|
||||
otgEndpointUsageUnlimited: 'Endpoint usage: {used} / unlimited',
|
||||
otgEndpointExceeded: 'The current OTG selection needs {used} endpoints, exceeding the limit {limit}.',
|
||||
otgHidProfile: 'OTG Functions',
|
||||
otgHidProfileDesc: 'Select which USB functions are exposed to the host',
|
||||
otgFunctionKeyboard: 'Keyboard',
|
||||
otgFunctionKeyboardDesc: 'Standard HID keyboard device',
|
||||
otgKeyboardLeds: 'Keyboard LED Status',
|
||||
@@ -748,9 +743,14 @@ export default {
|
||||
otgFunctionConsumerDesc: 'Consumer Control keys such as volume/play/pause',
|
||||
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; Consumer Control Keyboard will be disabled automatically.',
|
||||
otgProfileWarning: 'Saving reconnects the USB device; unsupported combinations show an error and restore the previous configuration',
|
||||
otgFunctionMinWarning: 'Enable at least one HID function before saving',
|
||||
otgNetwork: 'USB Virtual Ethernet',
|
||||
otgRuntimeDegraded: 'OTG runtime is degraded',
|
||||
otgNetworkDesc: 'Bridge the USB link to an existing uplink',
|
||||
otgNetworkDriver: 'Host Driver Mode',
|
||||
otgNetworkInterface: 'Bridge Interface',
|
||||
otgNetworkNone: 'None',
|
||||
otgDescriptor: 'USB Device Descriptor',
|
||||
otgDescriptorDesc: 'Configure USB device identification',
|
||||
vendorId: 'Vendor ID (VID)',
|
||||
|
||||
@@ -263,7 +263,6 @@ export default {
|
||||
progress: '步骤 {current} / {total}',
|
||||
ch9329Help: 'CH9329 是一款串口转 HID 芯片,通过串口连接到主机。适用于大多数硬件配置。',
|
||||
otgHelp: 'USB OTG 模式通过 USB 设备控制器直接模拟 HID 设备。需要硬件支持 USB OTG 功能。',
|
||||
otgLowEndpointHint: '检测到低端点 UDC,将自动禁用多媒体键盘。',
|
||||
videoDeviceHelp: '选择用于捕获远程主机画面的视频采集设备。通常是 HDMI 采集卡。',
|
||||
videoFormatHelp: 'MJPEG 格式兼容性最好,H.264/H.265 带宽占用更低但需要编码支持。',
|
||||
stepExtensions: '扩展设置',
|
||||
@@ -509,7 +508,7 @@ export default {
|
||||
accountSubtitle: '管理登录凭据与会话策略',
|
||||
networkSubtitle: '配置 Web 服务端口、监听地址与 SSL 证书',
|
||||
videoSubtitle: '配置采集设备、视频编码器与 WebRTC 信令服务器',
|
||||
hidSubtitle: '配置键盘鼠标后端与 USB Gadget 描述符',
|
||||
hidSubtitle: '配置 HID 后端、设备、描述符与 USB Gadget 功能',
|
||||
msdSubtitle: '管理虚拟存储设备 (MSD) 镜像目录',
|
||||
atxSubtitle: '配置远程电源控制硬件与网络唤醒',
|
||||
environmentSubtitle: '系统级运行环境与 USB 设备维护',
|
||||
@@ -711,10 +710,12 @@ export default {
|
||||
supportedFormats: '支持的编码格式',
|
||||
encoderHint: '硬件编码器延迟和 CPU 占用比软件编码低,画质预设更好',
|
||||
hidSettings: 'HID 设置',
|
||||
hidSettingsDesc: '配置键盘和鼠标控制',
|
||||
hidSettingsDesc: '按后端、设备、描述符和功能顺序配置',
|
||||
hidBackend: 'HID 后端',
|
||||
serialDevice: '串口设备',
|
||||
baudRate: '波特率',
|
||||
otgUdc: 'OTG 设备控制器',
|
||||
otgUdcDesc: '选择用于 USB Gadget 的 UDC,留空时自动选择',
|
||||
ch9329Options: 'CH9329 选项',
|
||||
ch9329OptionsDesc: '配置 CH9329 串口 HID 芯片的运行兼容性',
|
||||
ch9329HybridMouse: 'Linux 绝对鼠标兼容模式',
|
||||
@@ -727,14 +728,8 @@ export default {
|
||||
ch9329DescriptorReadRequired: '需要先成功读取 CH9329 描述符才能保存',
|
||||
ch9329DescriptorWarning: '保存会写入 CH9329 参数;需要重新上电或重新插拔后才会变化',
|
||||
ch9329StringLengthWarning: 'CH9329 字符串最长为 23 字节',
|
||||
otgHidProfile: 'OTG HID 功能',
|
||||
otgHidProfileDesc: '选择对目标主机暴露的 HID 功能',
|
||||
otgEndpointBudget: '最大端点数量',
|
||||
otgEndpointBudgetUnlimited: '无限制',
|
||||
otgEndpointBudgetHint: '此为硬件限制。若超出硬件端点数量,OTG 功能将无法使用。',
|
||||
otgEndpointUsage: '当前端点占用:{used} / {limit}',
|
||||
otgEndpointUsageUnlimited: '当前端点占用:{used} / 不限',
|
||||
otgEndpointExceeded: '当前 OTG 组合需要 {used} 个端点,已超出上限 {limit}。',
|
||||
otgHidProfile: 'OTG 功能',
|
||||
otgHidProfileDesc: '选择对目标主机暴露的 USB 功能',
|
||||
otgFunctionKeyboard: '键盘',
|
||||
otgFunctionKeyboardDesc: '标准 HID 键盘设备',
|
||||
otgKeyboardLeds: '键盘状态灯',
|
||||
@@ -747,9 +742,14 @@ export default {
|
||||
otgFunctionConsumerDesc: '音量/播放/暂停等多媒体按键',
|
||||
otgFunctionMsd: '虚拟媒体(MSD)',
|
||||
otgFunctionMsdDesc: '向目标主机暴露 USB 存储',
|
||||
otgProfileWarning: '修改 HID 功能将导致 USB 设备重新连接',
|
||||
otgLowEndpointHint: '检测到低端点 UDC,将自动禁用多媒体键盘。',
|
||||
otgProfileWarning: '保存后 USB 设备会重新连接;若控制器不支持当前组合,将显示失败并恢复原配置',
|
||||
otgFunctionMinWarning: '请至少启用一个 HID 功能后再保存',
|
||||
otgNetwork: 'USB 虚拟网卡',
|
||||
otgRuntimeDegraded: 'OTG 运行时处于降级状态',
|
||||
otgNetworkDesc: '桥接到现有上联网卡',
|
||||
otgNetworkDriver: '目标机驱动模式',
|
||||
otgNetworkInterface: '桥接网卡',
|
||||
otgNetworkNone: '无',
|
||||
otgDescriptor: 'USB 设备描述符',
|
||||
otgDescriptorDesc: '配置 USB 设备标识信息',
|
||||
vendorId: '厂商 ID (VID)',
|
||||
|
||||
@@ -85,7 +85,6 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
hid_ch9329_baudrate?: number
|
||||
hid_otg_udc?: string
|
||||
hid_otg_profile?: string
|
||||
hid_otg_endpoint_budget?: string
|
||||
hid_otg_keyboard_leds?: boolean
|
||||
msd_enabled?: boolean
|
||||
encoder_backend?: string
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
audioConfigApi,
|
||||
hidConfigApi,
|
||||
msdConfigApi,
|
||||
otgConfigApi,
|
||||
rtspConfigApi,
|
||||
rustdeskConfigApi,
|
||||
streamConfigApi,
|
||||
@@ -24,6 +25,8 @@ import type {
|
||||
HidConfigUpdate,
|
||||
MsdConfig,
|
||||
MsdConfigUpdate,
|
||||
OtgConfigResponse,
|
||||
OtgConfigUpdate,
|
||||
StreamConfigResponse,
|
||||
StreamConfigUpdate,
|
||||
VideoConfig,
|
||||
@@ -525,6 +528,13 @@ export const useConfigStore = defineStore('config', () => {
|
||||
return response
|
||||
}
|
||||
|
||||
async function updateOtg(update: OtgConfigUpdate): Promise<OtgConfigResponse> {
|
||||
const response = await otgConfigApi.update(update)
|
||||
hid.value = response.hid
|
||||
msd.value = response.msd
|
||||
return response
|
||||
}
|
||||
|
||||
async function updateStream(update: StreamConfigUpdate) {
|
||||
const response = await streamConfigApi.update(update)
|
||||
stream.value = response
|
||||
@@ -642,6 +652,7 @@ export const useConfigStore = defineStore('config', () => {
|
||||
updateAudio,
|
||||
updateHid,
|
||||
updateMsd,
|
||||
updateOtg,
|
||||
updateStream,
|
||||
updateWeb,
|
||||
updateAtx,
|
||||
|
||||
@@ -40,13 +40,6 @@ export enum OtgHidProfile {
|
||||
Custom = "custom",
|
||||
}
|
||||
|
||||
export enum OtgEndpointBudget {
|
||||
Auto = "auto",
|
||||
Five = "five",
|
||||
Six = "six",
|
||||
Unlimited = "unlimited",
|
||||
}
|
||||
|
||||
export interface OtgHidFunctions {
|
||||
keyboard: boolean;
|
||||
mouse_relative: boolean;
|
||||
@@ -67,7 +60,6 @@ export interface HidConfig {
|
||||
otg_udc?: string;
|
||||
otg_descriptor?: OtgDescriptorConfig;
|
||||
otg_profile?: OtgHidProfile;
|
||||
otg_endpoint_budget?: OtgEndpointBudget;
|
||||
otg_functions?: OtgHidFunctions;
|
||||
otg_keyboard_leds?: boolean;
|
||||
ch9329_port: string;
|
||||
@@ -77,6 +69,22 @@ export interface HidConfig {
|
||||
mouse_absolute: boolean;
|
||||
}
|
||||
|
||||
export enum OtgNetworkDriverMode {
|
||||
Ncm = "ncm",
|
||||
Ecm = "ecm",
|
||||
Rndis = "rndis",
|
||||
}
|
||||
|
||||
export interface OtgNetworkConfig {
|
||||
enabled: boolean;
|
||||
driver_mode: OtgNetworkDriverMode;
|
||||
/** Empty means select the connected NetworkManager Ethernet interface. */
|
||||
bridge_interface: string;
|
||||
/** Empty values are resolved from the machine identity at runtime. */
|
||||
host_mac: string;
|
||||
device_mac: string;
|
||||
}
|
||||
|
||||
export interface MsdConfig {
|
||||
enabled: boolean;
|
||||
msd_dir: string;
|
||||
@@ -286,6 +294,7 @@ export interface AppConfig {
|
||||
auth: AuthConfig;
|
||||
video: VideoConfig;
|
||||
hid: HidConfig;
|
||||
otg_network: OtgNetworkConfig;
|
||||
msd: MsdConfig;
|
||||
atx: AtxConfig;
|
||||
audio: AudioConfig;
|
||||
@@ -537,7 +546,6 @@ export interface HidConfigUpdate {
|
||||
otg_udc?: string;
|
||||
otg_descriptor?: OtgDescriptorConfigUpdate;
|
||||
otg_profile?: OtgHidProfile;
|
||||
otg_endpoint_budget?: OtgEndpointBudget;
|
||||
otg_functions?: OtgHidFunctionsUpdate;
|
||||
otg_keyboard_leds?: boolean;
|
||||
mouse_absolute?: boolean;
|
||||
@@ -548,6 +556,49 @@ export interface MsdConfigUpdate {
|
||||
msd_dir?: string;
|
||||
}
|
||||
|
||||
export interface NetworkInterfaceInfo {
|
||||
name: string;
|
||||
interface_type: string;
|
||||
state: string;
|
||||
connection: string;
|
||||
addresses: string[];
|
||||
has_default_route: boolean;
|
||||
bridge_supported: boolean;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
export enum OtgRuntimeHealth {
|
||||
Healthy = "healthy",
|
||||
Applying = "applying",
|
||||
Degraded = "degraded",
|
||||
}
|
||||
|
||||
export interface OtgNetworkStatus {
|
||||
health: OtgRuntimeHealth;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface OtgConfigResponse {
|
||||
hid: HidConfig;
|
||||
msd: MsdConfig;
|
||||
network: OtgNetworkConfig;
|
||||
status: OtgNetworkStatus;
|
||||
}
|
||||
|
||||
export interface OtgNetworkConfigUpdate {
|
||||
enabled?: boolean;
|
||||
driver_mode?: OtgNetworkDriverMode;
|
||||
bridge_interface?: string;
|
||||
host_mac?: string;
|
||||
device_mac?: string;
|
||||
}
|
||||
|
||||
export interface OtgConfigUpdate {
|
||||
hid?: HidConfigUpdate;
|
||||
msd?: MsdConfigUpdate;
|
||||
network?: OtgNetworkConfigUpdate;
|
||||
}
|
||||
|
||||
export interface RtspConfigResponse {
|
||||
enabled: boolean;
|
||||
bind: string;
|
||||
|
||||
@@ -10,6 +10,7 @@ import { useAuthStore } from '@/stores/auth'
|
||||
import {
|
||||
authApi,
|
||||
configApi,
|
||||
otgNetworkApi,
|
||||
hidApi,
|
||||
streamApi,
|
||||
atxConfigApi,
|
||||
@@ -42,11 +43,12 @@ import type {
|
||||
AtxDriverType,
|
||||
ActiveLevel,
|
||||
AtxDevices,
|
||||
OtgEndpointBudget,
|
||||
OtgHidProfile,
|
||||
OtgHidFunctions,
|
||||
Ch9329DescriptorConfig,
|
||||
Ch9329DescriptorState,
|
||||
NetworkInterfaceInfo,
|
||||
OtgNetworkStatus,
|
||||
} from '@/types/generated'
|
||||
import { FrpProxyType, FrpcConfigMode } from '@/types/generated'
|
||||
import { formatFpsLabel, toConfigFps } from '@/lib/fps'
|
||||
@@ -94,7 +96,6 @@ import {
|
||||
EyeOff,
|
||||
Save,
|
||||
Check,
|
||||
HardDrive,
|
||||
Power,
|
||||
Server,
|
||||
Menu,
|
||||
@@ -125,19 +126,18 @@ const authStore = useAuthStore()
|
||||
const featureVisibility = useFeatureVisibility()
|
||||
|
||||
const isWindows = computed(() => systemStore.platform?.mode === 'windows')
|
||||
const msdAvailable = computed(() => systemStore.platform?.msd.available ?? systemStore.capabilities?.msd.available ?? false)
|
||||
|
||||
const activeSection = ref<SettingsSectionId>('appearance')
|
||||
const mobileMenuOpen = ref(false)
|
||||
const loading = ref(false)
|
||||
const saved = ref(false)
|
||||
const saveError = ref('')
|
||||
const SETTINGS_SECTION_IDS = [
|
||||
'appearance',
|
||||
'account',
|
||||
'network',
|
||||
'video',
|
||||
'hid',
|
||||
'msd',
|
||||
'atx',
|
||||
'environment',
|
||||
'ext-ttyd',
|
||||
@@ -150,19 +150,19 @@ const SETTINGS_SECTION_ID_SET = new Set<string>(SETTINGS_SECTION_IDS)
|
||||
|
||||
const navGroups = computed(() => [
|
||||
{
|
||||
title: t('settings.system'),
|
||||
title: t('settings.software'),
|
||||
items: [
|
||||
{ id: 'appearance', label: t('settings.appearance'), icon: Sun },
|
||||
{ id: 'account', label: t('settings.account'), icon: User },
|
||||
{ id: 'network', label: t('settings.network'), icon: Globe },
|
||||
{ id: 'about', label: t('settings.about'), icon: Info },
|
||||
]
|
||||
},
|
||||
{
|
||||
title: t('settings.hardware'),
|
||||
title: t('settings.system'),
|
||||
items: [
|
||||
{ id: 'video', label: t('settings.video'), icon: Monitor, status: config.value.video_device ? t('settings.configured') : null },
|
||||
{ id: 'hid', label: t('settings.hid'), icon: Keyboard, status: config.value.hid_backend.toUpperCase() },
|
||||
...(msdAvailable.value ? [{ id: 'msd', label: t('settings.msd'), icon: HardDrive }] : []),
|
||||
{ id: 'video', label: t('settings.video'), icon: Monitor },
|
||||
{ id: 'hid', label: t('settings.hid'), icon: Keyboard },
|
||||
{ id: 'atx', label: t('settings.atx'), icon: Power },
|
||||
{ id: 'environment', label: t('settings.environment'), icon: Server },
|
||||
]
|
||||
@@ -174,12 +174,6 @@ const navGroups = computed(() => [
|
||||
{ id: 'third-party-access', label: t('extensions.thirdPartyAccess.title'), icon: ScreenShare },
|
||||
{ id: 'ext-remote-access', label: t('extensions.remoteAccess.title'), icon: ExternalLink },
|
||||
]
|
||||
},
|
||||
{
|
||||
title: t('settings.other'),
|
||||
items: [
|
||||
{ id: 'about', label: t('settings.about'), icon: Info },
|
||||
]
|
||||
}
|
||||
])
|
||||
|
||||
@@ -225,6 +219,7 @@ function normalizeSettingsSection(value: unknown): SettingsSectionId | null {
|
||||
if (value === 'access-control') return 'account'
|
||||
if (value === 'ext-frpc') return 'ext-remote-access'
|
||||
if (value === 'redfish') return 'third-party-access'
|
||||
if (value === 'msd') return 'hid'
|
||||
if (value === 'ext-rustdesk' || value === 'ext-vnc' || value === 'ext-rtsp') return 'third-party-access'
|
||||
return isSettingsSectionId(value) ? value : null
|
||||
}
|
||||
@@ -253,7 +248,6 @@ async function loadSectionData(section: SettingsSectionId) {
|
||||
])
|
||||
return
|
||||
case 'hid':
|
||||
case 'msd':
|
||||
await Promise.all([
|
||||
loadConfig(),
|
||||
loadDevices(),
|
||||
@@ -591,7 +585,6 @@ const config = ref({
|
||||
hid_serial_baudrate: 9600,
|
||||
hid_otg_udc: '',
|
||||
hid_otg_profile: 'custom' as OtgHidProfile,
|
||||
hid_otg_endpoint_budget: 'six' as OtgEndpointBudget,
|
||||
hid_otg_functions: {
|
||||
keyboard: true,
|
||||
mouse_relative: true,
|
||||
@@ -602,6 +595,9 @@ const config = ref({
|
||||
hid_ch9329_hybrid_mouse: false,
|
||||
msd_enabled: false,
|
||||
msd_dir: '',
|
||||
otg_network_enabled: false,
|
||||
otg_network_driver: 'ncm' as 'ncm' | 'ecm' | 'rndis',
|
||||
otg_network_interface: '',
|
||||
encoder_backend: 'auto',
|
||||
stun_server: '',
|
||||
turn_server: '',
|
||||
@@ -609,6 +605,20 @@ const config = ref({
|
||||
turn_password: '',
|
||||
})
|
||||
|
||||
const otgNetworkInterfaces = ref<NetworkInterfaceInfo[]>([])
|
||||
const otgNetworkInterfacesLoaded = ref(false)
|
||||
const otgNetworkStatus = ref<OtgNetworkStatus | null>(null)
|
||||
|
||||
function syncOtgNetworkInterface() {
|
||||
const firstInterface = otgNetworkInterfaces.value[0]?.name || ''
|
||||
const selectedInterfaceExists = otgNetworkInterfaces.value.some(
|
||||
item => item.name === config.value.otg_network_interface,
|
||||
)
|
||||
if (!selectedInterfaceExists) {
|
||||
config.value.otg_network_interface = firstInterface
|
||||
}
|
||||
}
|
||||
|
||||
type OtgSelfCheckLevel = 'info' | 'warn' | 'error'
|
||||
type OtgCheckGroupStatus = 'ok' | 'warn' | 'error' | 'skipped'
|
||||
|
||||
@@ -909,78 +919,12 @@ function usbSpeedLabel(speed?: string): string {
|
||||
return map[speed] || `${speed} Mbps`
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
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 (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'
|
||||
}
|
||||
}
|
||||
|
||||
const isHidFunctionSelectionValid = computed(() => {
|
||||
if (config.value.hid_backend !== 'otg') return true
|
||||
const f = config.value.hid_otg_functions
|
||||
@@ -1117,16 +1061,9 @@ const isCh9329DescriptorDirty = computed(() => {
|
||||
|
||||
const isHidSettingsValid = computed(() =>
|
||||
isHidFunctionSelectionValid.value
|
||||
&& isOtgEndpointBudgetValid.value
|
||||
&& isCh9329DescriptorValid.value
|
||||
)
|
||||
|
||||
watch(() => config.value.msd_enabled, (enabled) => {
|
||||
if (!enabled && activeSection.value === 'msd') {
|
||||
activeSection.value = 'hid'
|
||||
}
|
||||
})
|
||||
|
||||
watch(bindMode, (mode) => {
|
||||
if (mode === 'custom' && bindAddressList.value.length === 0) {
|
||||
bindAddressList.value = ['']
|
||||
@@ -1201,8 +1138,6 @@ const selectedBackendFormats = computed(() => {
|
||||
return backend?.supported_formats || []
|
||||
})
|
||||
|
||||
const isCh9329Backend = computed(() => config.value.hid_backend === 'ch9329')
|
||||
|
||||
const selectedDevice = computed(() => {
|
||||
return devices.value.video.find(d => d.path === config.value.video_device)
|
||||
})
|
||||
@@ -1408,6 +1343,7 @@ async function changePassword() {
|
||||
async function saveConfig() {
|
||||
loading.value = true
|
||||
saved.value = false
|
||||
saveError.value = ''
|
||||
|
||||
try {
|
||||
if (activeSection.value === 'video') {
|
||||
@@ -1436,6 +1372,7 @@ async function saveConfig() {
|
||||
ch9329_port: config.value.hid_serial_device || undefined,
|
||||
ch9329_baudrate: config.value.hid_serial_baudrate,
|
||||
ch9329_hybrid_mouse: config.value.hid_ch9329_hybrid_mouse,
|
||||
otg_udc: config.value.hid_otg_udc,
|
||||
}
|
||||
if (config.value.hid_backend === 'ch9329' && isCh9329DescriptorDirty.value) {
|
||||
hidUpdate.ch9329_descriptor = {
|
||||
@@ -1455,22 +1392,23 @@ async function saveConfig() {
|
||||
serial_number: otgSerialNumber.value || undefined,
|
||||
}
|
||||
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
|
||||
}
|
||||
await configStore.updateHid(hidUpdate)
|
||||
if (config.value.hid_backend === 'otg') {
|
||||
await configStore.updateMsd({ enabled: config.value.msd_enabled })
|
||||
} else {
|
||||
await configStore.updateMsd({ enabled: false })
|
||||
}
|
||||
}
|
||||
|
||||
if (activeSection.value === 'msd') {
|
||||
await configStore.updateMsd({
|
||||
msd_dir: config.value.msd_dir || undefined,
|
||||
const otgEnabled = config.value.hid_backend === 'otg'
|
||||
const response = await configStore.updateOtg({
|
||||
hid: hidUpdate,
|
||||
msd: {
|
||||
enabled: otgEnabled && config.value.msd_enabled,
|
||||
msd_dir: config.value.msd_dir || undefined,
|
||||
},
|
||||
network: {
|
||||
enabled: otgEnabled && config.value.otg_network_enabled,
|
||||
driver_mode: config.value.otg_network_driver as any,
|
||||
bridge_interface: config.value.otg_network_interface,
|
||||
},
|
||||
})
|
||||
otgNetworkStatus.value = response.status
|
||||
}
|
||||
|
||||
if (activeSection.value !== 'hid') {
|
||||
@@ -1478,7 +1416,12 @@ async function saveConfig() {
|
||||
}
|
||||
saved.value = true
|
||||
setTimeout(() => (saved.value = false), 2000)
|
||||
} catch {
|
||||
} catch (error) {
|
||||
saveError.value = error instanceof Error ? error.message : t('api.operationFailedDesc')
|
||||
if (activeSection.value === 'hid') {
|
||||
await loadConfig().catch(() => undefined)
|
||||
otgNetworkStatus.value = await otgNetworkApi.status().catch(() => null)
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -1486,11 +1429,18 @@ async function saveConfig() {
|
||||
|
||||
async function loadConfig() {
|
||||
try {
|
||||
const [video, stream, hid, msd] = await Promise.all([
|
||||
const [video, stream, hid, msd, otgNetwork] = await Promise.all([
|
||||
configStore.refreshVideo(),
|
||||
configStore.refreshStream(),
|
||||
configStore.refreshHid(),
|
||||
configStore.refreshMsd(),
|
||||
otgNetworkApi.get().catch(() => ({
|
||||
enabled: false,
|
||||
driver_mode: 'ncm' as const,
|
||||
bridge_interface: '',
|
||||
host_mac: '',
|
||||
device_mac: '',
|
||||
})),
|
||||
])
|
||||
|
||||
config.value = {
|
||||
@@ -1504,7 +1454,6 @@ async function loadConfig() {
|
||||
hid_serial_baudrate: hid.ch9329_baudrate || 9600,
|
||||
hid_otg_udc: hid.otg_udc || '',
|
||||
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,
|
||||
@@ -1515,12 +1464,18 @@ async function loadConfig() {
|
||||
hid_ch9329_hybrid_mouse: hid.ch9329_hybrid_mouse ?? false,
|
||||
msd_enabled: msd.enabled || false,
|
||||
msd_dir: msd.msd_dir || '',
|
||||
otg_network_enabled: otgNetwork.enabled,
|
||||
otg_network_driver: otgNetwork.driver_mode,
|
||||
otg_network_interface: otgNetwork.bridge_interface,
|
||||
encoder_backend: stream.encoder || 'auto',
|
||||
stun_server: stream.stun_server || '',
|
||||
turn_server: stream.turn_server || '',
|
||||
turn_username: stream.turn_username || '',
|
||||
turn_password: stream.turn_password || '',
|
||||
}
|
||||
if (otgNetworkInterfacesLoaded.value) {
|
||||
syncOtgNetworkInterface()
|
||||
}
|
||||
|
||||
if (hid.otg_descriptor) {
|
||||
otgVendorIdHex.value = hid.otg_descriptor.vendor_id?.toString(16).padStart(4, '0') || '1d6b'
|
||||
@@ -1539,14 +1494,21 @@ async function loadConfig() {
|
||||
} else {
|
||||
clearCh9329DescriptorState()
|
||||
}
|
||||
|
||||
otgNetworkStatus.value = await otgNetworkApi.status().catch(() => null)
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDevices() {
|
||||
try {
|
||||
devices.value = await configApi.listDevices()
|
||||
const [deviceConfig, networkInterfaces] = await Promise.all([
|
||||
configApi.listDevices(),
|
||||
otgNetworkApi.interfaces().catch(() => []),
|
||||
])
|
||||
devices.value = deviceConfig
|
||||
otgNetworkInterfaces.value = networkInterfaces
|
||||
otgNetworkInterfacesLoaded.value = true
|
||||
syncOtgNetworkInterface()
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
@@ -2701,7 +2663,6 @@ watch(isWindows, () => {
|
||||
>
|
||||
<component :is="item.icon" class="h-4 w-4" />
|
||||
<span>{{ item.label }}</span>
|
||||
<Badge v-if="item.status" variant="outline" :class="['ml-auto text-xs', activeSection === item.id ? 'border-primary-foreground/50 text-primary-foreground' : '']">{{ item.status }}</Badge>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
@@ -2738,7 +2699,6 @@ watch(isWindows, () => {
|
||||
>
|
||||
<component :is="item.icon" class="h-4 w-4 shrink-0" />
|
||||
<span class="truncate">{{ item.label }}</span>
|
||||
<Badge v-if="item.status" variant="outline" :class="['ml-auto text-[10px] px-1.5 py-0 h-4', activeSection === item.id ? 'border-primary-foreground/50 text-primary-foreground' : '']">{{ item.status }}</Badge>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
@@ -3080,24 +3040,16 @@ watch(isWindows, () => {
|
||||
<option :value="115200">115200</option>
|
||||
</select>
|
||||
</div>
|
||||
<div v-if="config.hid_backend === 'otg'" class="space-y-2">
|
||||
<Label for="otg-udc">{{ t('settings.otgUdc') }}</Label>
|
||||
<select id="otg-udc" v-model="config.hid_otg_udc" class="w-full h-9 px-3 rounded-md border border-input bg-background text-sm">
|
||||
<option value="">{{ t('settings.autoRecommended') }}</option>
|
||||
<option v-for="udc in devices.udc" :key="udc.name" :value="udc.name">{{ udc.name }}</option>
|
||||
</select>
|
||||
<p class="text-xs text-muted-foreground">{{ t('settings.otgUdcDesc') }}</p>
|
||||
</div>
|
||||
|
||||
<template v-if="config.hid_backend === 'ch9329'">
|
||||
<Separator class="my-4" />
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<h4 class="text-sm font-medium">{{ t('settings.ch9329Options') }}</h4>
|
||||
<p class="text-sm text-muted-foreground">{{ t('settings.ch9329OptionsDesc') }}</p>
|
||||
</div>
|
||||
<div class="space-y-3 rounded-md border border-border/60 p-3">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<Label>{{ t('settings.ch9329HybridMouse') }}</Label>
|
||||
<p class="text-xs text-muted-foreground">{{ t('settings.ch9329HybridMouseDesc') }}</p>
|
||||
</div>
|
||||
<Switch v-model="config.hid_ch9329_hybrid_mouse" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Separator class="my-4" />
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
@@ -3182,88 +3134,26 @@ watch(isWindows, () => {
|
||||
{{ t('settings.ch9329DescriptorWarning') }}
|
||||
</p>
|
||||
</div>
|
||||
<Separator class="my-4" />
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<h4 class="text-sm font-medium">{{ t('settings.ch9329Options') }}</h4>
|
||||
<p class="text-sm text-muted-foreground">{{ t('settings.ch9329OptionsDesc') }}</p>
|
||||
</div>
|
||||
<div class="space-y-3 rounded-md border border-border/60 p-3">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<Label>{{ t('settings.ch9329HybridMouse') }}</Label>
|
||||
<p class="text-xs text-muted-foreground">{{ t('settings.ch9329HybridMouseDesc') }}</p>
|
||||
</div>
|
||||
<Switch v-model="config.hid_ch9329_hybrid_mouse" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- OTG Descriptor Settings -->
|
||||
<template v-if="config.hid_backend === 'otg'">
|
||||
<Separator class="my-4" />
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<h4 class="text-sm font-medium">{{ t('settings.otgHidProfile') }}</h4>
|
||||
<p class="text-sm text-muted-foreground">{{ t('settings.otgHidProfileDesc') }}</p>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<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 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>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs text-amber-600 dark:text-amber-400">
|
||||
{{ t('settings.otgProfileWarning') }}
|
||||
</p>
|
||||
<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" />
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
@@ -3323,7 +3213,118 @@ watch(isWindows, () => {
|
||||
{{ t('settings.descriptorWarning') }}
|
||||
</p>
|
||||
</div>
|
||||
<Separator class="my-4" />
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<h4 class="text-sm font-medium">{{ t('settings.otgHidProfile') }}</h4>
|
||||
<p class="text-sm text-muted-foreground">{{ t('settings.otgHidProfileDesc') }}</p>
|
||||
</div>
|
||||
<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 gap-4">
|
||||
<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 gap-4">
|
||||
<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>
|
||||
</div>
|
||||
<div class="space-y-3 rounded-md border border-border/60 p-3">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<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 gap-4">
|
||||
<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 gap-4">
|
||||
<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>
|
||||
</div>
|
||||
<div class="space-y-3 rounded-md border border-border/60 p-3">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<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>
|
||||
<template v-if="config.msd_enabled">
|
||||
<Separator />
|
||||
<div class="space-y-2">
|
||||
<Label for="msd-dir">{{ t('settings.msdDir') }}</Label>
|
||||
<Input id="msd-dir" v-model="config.msd_dir" placeholder="/etc/one-kvm/msd" />
|
||||
<p class="text-xs text-muted-foreground">{{ t('settings.msdDirDesc') }}</p>
|
||||
<p class="text-xs text-muted-foreground">{{ t('settings.msdDirHint') }}</p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="space-y-3 rounded-md border border-border/60 p-3">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<Label>{{ t('settings.otgNetwork') }}</Label>
|
||||
<p class="text-xs text-muted-foreground">{{ t('settings.otgNetworkDesc') }}</p>
|
||||
</div>
|
||||
<Switch v-model="config.otg_network_enabled" />
|
||||
</div>
|
||||
<template v-if="config.otg_network_enabled">
|
||||
<Separator />
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<div class="space-y-2">
|
||||
<Label for="otg-network-driver">{{ t('settings.otgNetworkDriver') }}</Label>
|
||||
<select id="otg-network-driver" v-model="config.otg_network_driver" class="w-full h-9 px-3 rounded-md border border-input bg-background text-sm">
|
||||
<option value="ncm">NCM</option>
|
||||
<option value="ecm">ECM</option>
|
||||
<option value="rndis">RNDIS / Windows</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="otg-network-interface">{{ t('settings.otgNetworkInterface') }}</Label>
|
||||
<select id="otg-network-interface" v-model="config.otg_network_interface" class="w-full h-9 px-3 rounded-md border border-input bg-background text-sm" :disabled="otgNetworkInterfaces.length === 0">
|
||||
<option v-if="otgNetworkInterfaces.length === 0" value="">{{ t('settings.otgNetworkNone') }}</option>
|
||||
<option
|
||||
v-for="item in otgNetworkInterfaces"
|
||||
:key="item.name"
|
||||
:value="item.name"
|
||||
>
|
||||
{{ item.name }} · {{ item.interface_type }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<p v-if="otgNetworkStatus?.health === 'degraded'" class="text-xs text-destructive">
|
||||
{{ t('settings.otgRuntimeDegraded') }}: {{ otgNetworkStatus.error || t('common.error') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs text-amber-600 dark:text-amber-400">
|
||||
{{ t('settings.otgProfileWarning') }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -3918,42 +3919,6 @@ watch(isWindows, () => {
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<!-- MSD Section -->
|
||||
<div v-show="activeSection === 'msd' && config.msd_enabled" class="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('settings.msdSettings') }}</CardTitle>
|
||||
<CardDescription>{{ t('settings.msdDesc') }}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<div v-if="isCh9329Backend" class="rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-sm text-amber-900">
|
||||
<p class="font-medium">{{ t('settings.msdCh9329Warning') }}</p>
|
||||
<p class="text-xs text-amber-900/80">{{ t('settings.msdCh9329WarningDesc') }}</p>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="msd-dir">{{ t('settings.msdDir') }}</Label>
|
||||
<Input id="msd-dir" v-model="config.msd_dir" placeholder="/etc/one-kvm/msd" :disabled="isCh9329Backend" />
|
||||
<p class="text-xs text-muted-foreground">{{ t('settings.msdDirDesc') }}</p>
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground">{{ t('settings.msdDirHint') }}</p>
|
||||
</div>
|
||||
<Separator />
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm font-medium">{{ t('settings.msdStatus') }}</p>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ config.msd_enabled ? t('settings.willBeEnabledAfterSave') : t('settings.disabled') }}
|
||||
</p>
|
||||
</div>
|
||||
<Badge :variant="config.msd_enabled ? 'default' : 'secondary'">
|
||||
{{ config.msd_enabled ? t('common.enabled') : t('common.disabled') }}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<!-- ATX Section -->
|
||||
<div v-show="activeSection === 'atx'" class="space-y-6">
|
||||
<Card>
|
||||
@@ -5311,7 +5276,7 @@ watch(isWindows, () => {
|
||||
</div>
|
||||
|
||||
<!-- Save Button (sticky) -->
|
||||
<div v-if="['video', 'hid', 'msd'].includes(activeSection)" class="sticky bottom-0 pt-3 sm:pt-4 pb-3 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80 border-t -mx-3 px-3 sm:-mx-6 sm:px-6 lg:-mx-8 lg:px-8">
|
||||
<div v-if="['video', 'hid'].includes(activeSection)" class="sticky bottom-0 pt-3 sm:pt-4 pb-3 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80 border-t -mx-3 px-3 sm:-mx-6 sm:px-6 lg:-mx-8 lg:px-8">
|
||||
<div class="flex items-center justify-between gap-2 sm:gap-3">
|
||||
<p v-if="activeSection === 'hid' && !isHidFunctionSelectionValid" class="text-xs text-amber-600 dark:text-amber-400 flex items-center gap-1.5 min-w-0">
|
||||
<AlertTriangle class="h-3.5 w-3.5 shrink-0" />
|
||||
@@ -5325,6 +5290,7 @@ watch(isWindows, () => {
|
||||
<AlertTriangle class="h-3.5 w-3.5 shrink-0" />
|
||||
<span class="truncate">{{ t('settings.ch9329DescriptorLoading') }}</span>
|
||||
</p>
|
||||
<p v-if="saveError" class="text-xs text-destructive">{{ saveError }}</p>
|
||||
<p v-else class="text-xs text-muted-foreground hidden sm:block">{{ t('settings.unsavedChangesHint') }}</p>
|
||||
<Button class="shrink-0 ml-auto" :disabled="loading || (activeSection === 'hid' && !isHidSettingsValid)" @click="saveConfig">
|
||||
<Loader2 v-if="loading" class="h-4 w-4 mr-2 animate-spin" /><Check v-else-if="saved" class="h-4 w-4 mr-2" /><Save v-else class="h-4 w-4 mr-2" />{{ loading ? t('actionbar.applying') : saved ? t('common.success') : t('common.save') }}
|
||||
|
||||
@@ -78,7 +78,6 @@ const ch9329Baudrate = ref(9600)
|
||||
const otgUdc = ref('')
|
||||
const hidOtgProfile = ref('full_no_consumer')
|
||||
const otgMsdEnabled = ref(true)
|
||||
const otgEndpointBudget = ref<'five' | 'six' | 'unlimited'>('six')
|
||||
const otgKeyboardLeds = ref(true)
|
||||
|
||||
const ttydEnabled = ref(false)
|
||||
@@ -203,46 +202,11 @@ const availableFps = computed(() => {
|
||||
return resolution?.fps || []
|
||||
})
|
||||
|
||||
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',
|
||||
}
|
||||
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
|
||||
})
|
||||
|
||||
const isOtgEndpointBudgetValid = computed(() => {
|
||||
const limit = endpointLimitForBudget(otgEndpointBudget.value)
|
||||
return limit === null || otgRequiredEndpoints.value <= limit
|
||||
})
|
||||
|
||||
function applyOtgDefaults() {
|
||||
if (hidBackend.value !== 'otg') return
|
||||
|
||||
otgEndpointBudget.value = defaultOtgEndpointBudgetForUdc(otgUdc.value)
|
||||
hidOtgProfile.value = 'full_no_consumer'
|
||||
otgKeyboardLeds.value = otgEndpointBudget.value !== 'five'
|
||||
otgKeyboardLeds.value = true
|
||||
}
|
||||
|
||||
const baudRates = [9600, 19200, 38400, 57600, 115200]
|
||||
@@ -477,13 +441,6 @@ 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
|
||||
}
|
||||
|
||||
@@ -543,7 +500,6 @@ 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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user