From 0365dc57b825579fb28cb17fe8c4e08e89ce3c08 Mon Sep 17 00:00:00 2001 From: JackTheRooster Date: Fri, 21 Feb 2025 22:17:02 -0600 Subject: [PATCH] rebasing on dev branch --- config.go | 16 + jsonrpc.go | 25 + ui/src/components/USBConfigDialog.tsx | 216 ++++ ui/src/components/sidebar/settings.tsx | 1396 +++++++++++++----------- ui/src/hooks/stores.ts | 120 +- usb.go | 48 +- 6 files changed, 1152 insertions(+), 669 deletions(-) create mode 100644 ui/src/components/USBConfigDialog.tsx diff --git a/config.go b/config.go index ccbce8e..4119a0c 100644 --- a/config.go +++ b/config.go @@ -12,6 +12,14 @@ type WakeOnLanDevice struct { MacAddress string `json:"macAddress"` } +type UsbConfig struct { + VendorId string `json:"vendor_id"` + ProductId string `json:"product_id"` + SerialNumber string `json:"serial_number"` + Manufacturer string `json:"manufacturer"` + Product string `json:"product"` +} + type Config struct { CloudURL string `json:"cloud_url"` CloudToken string `json:"cloud_token"` @@ -28,6 +36,7 @@ type Config struct { DisplayMaxBrightness int `json:"display_max_brightness"` DisplayDimAfterSec int `json:"display_dim_after_sec"` DisplayOffAfterSec int `json:"display_off_after_sec"` + UsbConfig UsbConfig `json:"usb_config"` } const configPath = "/userdata/kvm_config.json" @@ -39,6 +48,13 @@ var defaultConfig = &Config{ DisplayMaxBrightness: 64, DisplayDimAfterSec: 120, // 2 minutes DisplayOffAfterSec: 1800, // 30 minutes + UsbConfig: UsbConfig{ + VendorId: "0x1d6b", //The Linux Foundation + ProductId: "0x0104", //Multifunction Composite Gadget + SerialNumber: "", + Manufacturer: "JetKVM", + Product: "JetKVM USB Emulation Device", + }, } var ( diff --git a/jsonrpc.go b/jsonrpc.go index 619e561..de2f1e7 100644 --- a/jsonrpc.go +++ b/jsonrpc.go @@ -538,6 +538,29 @@ func rpcSetUsbEmulationState(enabled bool) error { } } +func rpcGetUsbConfig() (UsbConfig, error) { + LoadConfig() + return config.UsbConfig, nil +} + +func rpcSetUsbConfig(usbConfig UsbConfig) error { + LoadConfig() + config.UsbConfig = usbConfig + + err := UpdateGadgetConfig() + if err != nil { + return fmt.Errorf("failed to write gadget config: %w", err) + } + + err = SaveConfig() + if err != nil { + return fmt.Errorf("failed to save usb config: %w", err) + } + + log.Printf("[jsonrpc.go:rpcSetUsbConfig] usb config set to %s", usbConfig) + return nil +} + func rpcGetWakeOnLanDevices() ([]WakeOnLanDevice, error) { if config.WakeOnLanDevices == nil { return []WakeOnLanDevice{}, nil @@ -764,6 +787,8 @@ var rpcHandlers = map[string]RPCHandler{ "isUpdatePending": {Func: rpcIsUpdatePending}, "getUsbEmulationState": {Func: rpcGetUsbEmulationState}, "setUsbEmulationState": {Func: rpcSetUsbEmulationState, Params: []string{"enabled"}}, + "getUsbConfig": {Func: rpcGetUsbConfig}, + "setUsbConfig": {Func: rpcSetUsbConfig, Params: []string{"usbConfig"}}, "checkMountUrl": {Func: rpcCheckMountUrl, Params: []string{"url"}}, "getVirtualMediaState": {Func: rpcGetVirtualMediaState}, "getStorageSpace": {Func: rpcGetStorageSpace}, diff --git a/ui/src/components/USBConfigDialog.tsx b/ui/src/components/USBConfigDialog.tsx new file mode 100644 index 0000000..f286670 --- /dev/null +++ b/ui/src/components/USBConfigDialog.tsx @@ -0,0 +1,216 @@ +import { GridCard } from "@/components/Card"; +import {useCallback, useEffect, useState} from "react"; +import { Button } from "@components/Button"; +import LogoBlueIcon from "@/assets/logo-blue.svg"; +import LogoWhiteIcon from "@/assets/logo-white.svg"; +import Modal from "@components/Modal"; +import { InputFieldWithLabel } from "./InputField"; +import { useJsonRpc } from "@/hooks/useJsonRpc"; +import { useUsbConfigModalStore } from "@/hooks/stores"; +import ExtLink from "@components/ExtLink"; +import { UsbConfigState } from "@/hooks/stores" + +export default function USBConfigDialog({ + open, + setOpen, +}: { + open: boolean; + setOpen: (open: boolean) => void; +}) { + return ( + setOpen(false)}> + + + ); +} + +export function Dialog({ setOpen }: { setOpen: (open: boolean) => void }) { + const { modalView, setModalView } = useUsbConfigModalStore(); + const [error, setError] = useState(null); + + const [send] = useJsonRpc(); + + const handleUsbConfigChange = useCallback((usbConfig: object) => { + send("setUsbConfig", { usbConfig }, resp => { + if ("error" in resp) { + setError(`Failed to update USB Config: ${resp.error.data || "Unknown error"}`); + return; + } + setModalView("updateUsbConfigSuccess"); + }); + }, [send, setModalView]); + + return ( + +
+ {modalView === "updateUsbConfig" && ( + setOpen(false)} + error={error} + /> + )} + {modalView === "updateUsbConfigSuccess" && ( + setOpen(false)} + /> + )} +
+
+ ); +} + +function UpdateUsbConfigModal({ + onSetUsbConfig, + onCancel, + error, +}: { + onSetUsbConfig: (usb_config: object) => void; + onCancel: () => void; + error: string | null; +}) { + const [usbConfigState, setUsbConfigState] = useState({ + vendor_id: '', + product_id: '', + serial_number: '', + manufacturer: '', + product: '' + }); + const [send] = useJsonRpc(); + + const syncUsbConfig = useCallback(() => { + send("getUsbConfig", {}, resp => { + if ("error" in resp) { + console.error("Failed to load USB Config:", resp.error); + } else { + setUsbConfigState(resp.result as UsbConfigState); + } + }); + }, [send, setUsbConfigState]); + + // Load stored usb config from the backend + useEffect(() => { + syncUsbConfig(); + }, [syncUsbConfig]); + + const handleUsbVendorIdChange = (value: string) => { + setUsbConfigState({... usbConfigState, vendor_id: value}) + }; + + const handleUsbProductIdChange = (value: string) => { + setUsbConfigState({... usbConfigState, product_id: value}) + }; + + const handleUsbSerialChange = (value: string) => { + setUsbConfigState({... usbConfigState, serial_number: value}) + }; + + const handleUsbManufacturer = (value: string) => { + setUsbConfigState({... usbConfigState, manufacturer: value}) + }; + + const handleUsbProduct = (value: string) => { + setUsbConfigState({... usbConfigState, product: value}) + }; + + return ( +
+
+ + +
+
+
+

USB Emulation Configuration

+

+ Set custom USB parameters to control how the USB device is emulated. + The device will rebind once the parameters are updated. +

+
+ + Look up USB Device IDs here + +
+
+ handleUsbVendorIdChange(e.target.value)} + /> + handleUsbProductIdChange(e.target.value)} + /> + handleUsbSerialChange(e.target.value)} + /> + handleUsbManufacturer(e.target.value)} + /> + handleUsbProduct(e.target.value)} + /> +
+
+ {error &&

{error}

} +
+
+ ); +} + +function SuccessModal({ + headline, + description, + onClose, +}: { + headline: string; + description: string; + onClose: () => void; +}) { + return ( +
+
+ + +
+
+
+

{headline}

+

{description}

+
+
+
+ ); +} \ No newline at end of file diff --git a/ui/src/components/sidebar/settings.tsx b/ui/src/components/sidebar/settings.tsx index add9ab3..d7b4218 100644 --- a/ui/src/components/sidebar/settings.tsx +++ b/ui/src/components/sidebar/settings.tsx @@ -5,6 +5,7 @@ import { useSettingsStore, useUiStore, useUpdateStore, + useUsbConfigModalStore } from "@/hooks/stores"; import { Checkbox } from "@components/Checkbox"; import { Button, LinkButton } from "@components/Button"; @@ -27,13 +28,15 @@ import { LocalDevice } from "@routes/devices.$id"; import { useRevalidator } from "react-router-dom"; import { ShieldCheckIcon } from "@heroicons/react/20/solid"; import { CLOUD_APP, SIGNAL_API } from "@/ui.config"; +import USBConfigDialog from "@components/USBConfigDialog"; +import { UsbConfigState } from "@/hooks/stores" export function SettingsItem({ - title, - description, - children, - className, -}: { + title, + description, + children, + className, + }: { title: string; description: string | React.ReactNode; children?: React.ReactNode; @@ -41,18 +44,31 @@ export function SettingsItem({ name?: string; }) { return ( - + ); } + +const generatedSerialNumber = [generateNumber(1,9), generateHex(7,7), 0, 1].join("&"); + +function generateNumber(min: number, max: number) { + return Math.floor(Math.random() * (max - min + 1) + min); +} + +function generateHex(min: number, max: number) { + const len = generateNumber(min, max); + const n = (Math.random() * 0xfffff * 1000000).toString(16); + return n.slice(0, len); +} + const defaultEdid = - "00ffffffffffff0052620188008888881c150103800000780a0dc9a05747982712484c00000001010101010101010101010101010101023a801871382d40582c4500c48e2100001e011d007251d01e206e285500c48e2100001e000000fc00543734392d6648443732300a20000000fd00147801ff1d000a202020202020017b"; + "00ffffffffffff0052620188008888881c150103800000780a0dc9a05747982712484c00000001010101010101010101010101010101023a801871382d40582c4500c48e2100001e011d007251d01e206e285500c48e2100001e000000fc00543734392d6648443732300a20000000fd00147801ff1d000a202020202020017b"; const edids = [ { value: defaultEdid, @@ -60,17 +76,17 @@ const edids = [ }, { value: - "00FFFFFFFFFFFF00047265058A3F6101101E0104A53420783FC125A8554EA0260D5054BFEF80714F8140818081C081008B009500B300283C80A070B023403020360006442100001A000000FD00304C575716010A202020202020000000FC0042323436574C0A202020202020000000FF0054384E4545303033383532320A01F802031CF14F90020304050607011112131415161F2309070783010000011D8018711C1620582C250006442100009E011D007251D01E206E28550006442100001E8C0AD08A20E02D10103E9600064421000018C344806E70B028401720A80406442100001E00000000000000000000000000000000000000000000000000000096", + "00FFFFFFFFFFFF00047265058A3F6101101E0104A53420783FC125A8554EA0260D5054BFEF80714F8140818081C081008B009500B300283C80A070B023403020360006442100001A000000FD00304C575716010A202020202020000000FC0042323436574C0A202020202020000000FF0054384E4545303033383532320A01F802031CF14F90020304050607011112131415161F2309070783010000011D8018711C1620582C250006442100009E011D007251D01E206E28550006442100001E8C0AD08A20E02D10103E9600064421000018C344806E70B028401720A80406442100001E00000000000000000000000000000000000000000000000000000096", label: "Acer B246WL, 1920x1200", }, { value: - "00FFFFFFFFFFFF0006B3872401010101021F010380342078EA6DB5A7564EA0250D5054BF6F00714F8180814081C0A9409500B300D1C0283C80A070B023403020360006442100001A000000FD00314B1E5F19000A202020202020000000FC00504132343851560A2020202020000000FF004D314C4D51533035323135370A014D02032AF14B900504030201111213141F230907078301000065030C001000681A00000101314BE6E2006A023A801871382D40582C450006442100001ECD5F80B072B0374088D0360006442100001C011D007251D01E206E28550006442100001E8C0AD08A20E02D10103E960006442100001800000000000000000000000000DC", + "00FFFFFFFFFFFF0006B3872401010101021F010380342078EA6DB5A7564EA0250D5054BF6F00714F8180814081C0A9409500B300D1C0283C80A070B023403020360006442100001A000000FD00314B1E5F19000A202020202020000000FC00504132343851560A2020202020000000FF004D314C4D51533035323135370A014D02032AF14B900504030201111213141F230907078301000065030C001000681A00000101314BE6E2006A023A801871382D40582C450006442100001ECD5F80B072B0374088D0360006442100001C011D007251D01E206E28550006442100001E8C0AD08A20E02D10103E960006442100001800000000000000000000000000DC", label: "ASUS PA248QV, 1920x1200", }, { value: - "00FFFFFFFFFFFF0010AC132045393639201E0103803C22782ACD25A3574B9F270D5054A54B00714F8180A9C0D1C00101010101010101023A801871382D40582C450056502100001E000000FF00335335475132330A2020202020000000FC0044454C4C204432373231480A20000000FD00384C1E5311000A202020202020018102031AB14F90050403020716010611121513141F65030C001000023A801871382D40582C450056502100001E011D8018711C1620582C250056502100009E011D007251D01E206E28550056502100001E8C0AD08A20E02D10103E960056502100001800000000000000000000000000000000000000000000000000000000004F", + "00FFFFFFFFFFFF0010AC132045393639201E0103803C22782ACD25A3574B9F270D5054A54B00714F8180A9C0D1C00101010101010101023A801871382D40582C450056502100001E000000FF00335335475132330A2020202020000000FC0044454C4C204432373231480A20000000FD00384C1E5311000A202020202020018102031AB14F90050403020716010611121513141F65030C001000023A801871382D40582C450056502100001E011D8018711C1620582C250056502100009E011D007251D01E206E28550056502100001E8C0AD08A20E02D10103E960056502100001800000000000000000000000000000000000000000000000000000000004F", label: "DELL D2721H, 1920x1080", }, ]; @@ -85,6 +101,7 @@ export default function SettingsSidebar() { const [jiggler, setJiggler] = useState(false); const [edid, setEdid] = useState(null); const [customEdidValue, setCustomEdidValue] = useState(null); + const [usbConfigProduct, setUsbConfigProduct] = useState(""); const [isAdopted, setAdopted] = useState(false); const [deviceId, setDeviceId] = useState(null); @@ -112,20 +129,98 @@ export default function SettingsSidebar() { }); }, [send]); - const handleUsbEmulationToggle = useCallback( - (enabled: boolean) => { - send("setUsbEmulationState", { enabled: enabled }, resp => { - if ("error" in resp) { - notifications.error( - `Failed to ${enabled ? "enable" : "disable"} USB emulation: ${resp.error.data || "Unknown error"}`, - ); - return; - } - setUsbEmulationEnabled(enabled); - getUsbEmulationState(); - }); + const syncUsbConfigProduct = useCallback(() => { + send("getUsbConfig", {}, resp => { + if ("error" in resp) { + console.error("Failed to load USB Config:", resp.error); + } else { + console.error("syncUsbConfigProduct#getUsbConfig result:", resp.result); + const usbConfigState = resp.result as UsbConfigState + setUsbConfigProduct(usbConfigState.product); + } + }); + }, [send, setUsbConfigProduct]); + + // Load stored usb config product from the backend + useEffect(() => { + syncUsbConfigProduct(); + }, [syncUsbConfigProduct]); + + const usbConfigs = [ + { + label: "JetKVM Default", + value: "JetKVM USB Emulation Device" }, - [getUsbEmulationState, send], + { + label: "Logitech Universal Adapter", + value: "Logitech USB Input Device" + }, + { + label: "Microsoft Wireless MultiMedia Keyboard", + value: "Wireless MultiMedia Keyboard" + }, + { + label: "Dell Multimedia Pro Keyboard", + value: "Multimedia Pro Keyboard" + } + ]; + + interface USBConfig { + vendor_id: string; + product_id: string; + serial_number: string | null; + manufacturer: string; + product: string; + } + + type UsbConfigMap = Record; + + + const usbConfigData: UsbConfigMap = { + "JetKVM USB Emulation Device": { + vendor_id: "0x1d6b", + product_id: "0x0104", + serial_number: deviceId, + manufacturer: "JetKVM", + product: "JetKVM USB Emulation Device", + }, + "Logitech USB Input Device": { + vendor_id: "0x046d", + product_id: "0xc52b", + serial_number: generatedSerialNumber, + manufacturer: "Logitech (x64)", + product: "Logitech USB Input Device", + }, + "Wireless MultiMedia Keyboard": { + vendor_id: "0x045e", + product_id: "0x005f", + serial_number: generatedSerialNumber, + manufacturer: "Microsoft", + product: "Wireless MultiMedia Keyboard", + }, + "Multimedia Pro Keyboard": { + vendor_id: "0x413c", + product_id: "0x2011", + serial_number: generatedSerialNumber, + manufacturer: "Dell Inc.", + product: "Multimedia Pro Keyboard", + } + } + + const handleUsbEmulationToggle = useCallback( + (enabled: boolean) => { + send("setUsbEmulationState", { enabled: enabled }, resp => { + if ("error" in resp) { + notifications.error( + `Failed to ${enabled ? "enable" : "disable"} USB emulation: ${resp.error.data || "Unknown error"}`, + ); + return; + } + setUsbEmulationEnabled(enabled); + getUsbEmulationState(); + }); + }, + [getUsbEmulationState, send], ); const getCloudState = useCallback(() => { @@ -140,7 +235,7 @@ export default function SettingsSidebar() { send("deregisterDevice", {}, resp => { if ("error" in resp) { notifications.error( - `Failed to de-register device: ${resp.error.data || "Unknown error"}`, + `Failed to de-register device: ${resp.error.data || "Unknown error"}`, ); return; } @@ -153,7 +248,7 @@ export default function SettingsSidebar() { send("setStreamQualityFactor", { factor: Number(factor) }, resp => { if ("error" in resp) { notifications.error( - `Failed to set stream quality: ${resp.error.data || "Unknown error"}`, + `Failed to set stream quality: ${resp.error.data || "Unknown error"}`, ); return; } @@ -165,7 +260,7 @@ export default function SettingsSidebar() { send("setAutoUpdateState", { enabled }, resp => { if ("error" in resp) { notifications.error( - `Failed to set auto-update: ${resp.error.data || "Unknown error"}`, + `Failed to set auto-update: ${resp.error.data || "Unknown error"}`, ); return; } @@ -177,7 +272,7 @@ export default function SettingsSidebar() { send("setDevChannelState", { enabled }, resp => { if ("error" in resp) { notifications.error( - `Failed to set dev channel state: ${resp.error.data || "Unknown error"}`, + `Failed to set dev channel state: ${resp.error.data || "Unknown error"}`, ); return; } @@ -185,11 +280,26 @@ export default function SettingsSidebar() { }); }; + const handleUsbConfigChange = (product: string) => { + const usbConfig = usbConfigData[product]; + console.info(`USB config: ${JSON.stringify(usbConfig)}`) + send("setUsbConfig", { usbConfig }, resp => { + if ("error" in resp) { + notifications.error( + `Failed to set usb config: ${resp.error.data || "Unknown error"}`, + ); + return; + } + setUsbConfigProduct(usbConfig.product); + notifications.success(`USB Config set to ${usbConfig.manufacturer} ${usbConfig.product}`); + }); + }; + const handleJigglerChange = (enabled: boolean) => { send("setJigglerState", { enabled }, resp => { if ("error" in resp) { notifications.error( - `Failed to set jiggler state: ${resp.error.data || "Unknown error"}`, + `Failed to set jiggler state: ${resp.error.data || "Unknown error"}`, ); return; } @@ -214,21 +324,21 @@ export default function SettingsSidebar() { }; const handleDevModeChange = useCallback( - (developerMode: boolean) => { - send("setDevModeState", { enabled: developerMode }, resp => { - if ("error" in resp) { - notifications.error( - `Failed to set dev mode: ${resp.error.data || "Unknown error"}`, - ); - return; - } - setDeveloperMode(developerMode); - setTimeout(() => { - sidebarRef.current?.scrollTo({ top: 5000, behavior: "smooth" }); - }, 0); - }); - }, - [send, setDeveloperMode], + (developerMode: boolean) => { + send("setDevModeState", { enabled: developerMode }, resp => { + if ("error" in resp) { + notifications.error( + `Failed to set dev mode: ${resp.error.data || "Unknown error"}`, + ); + return; + } + setDeveloperMode(developerMode); + setTimeout(() => { + sidebarRef.current?.scrollTo({ top: 5000, behavior: "smooth" }); + }, 0); + }); + }, + [send, setDeveloperMode], ); const handleBacklightSettingsChange = (settings: BacklightSettings) => { @@ -246,7 +356,7 @@ export default function SettingsSidebar() { send("setBacklightSettings", { params: settings.backlightSettings }, resp => { if ("error" in resp) { notifications.error( - `Failed to set backlight settings: ${resp.error.data || "Unknown error"}`, + `Failed to set backlight settings: ${resp.error.data || "Unknown error"}`, ); return; } @@ -258,7 +368,7 @@ export default function SettingsSidebar() { send("setSSHKeyState", { sshKey }, resp => { if ("error" in resp) { notifications.error( - `Failed to update SSH key: ${resp.error.data || "Unknown error"}`, + `Failed to update SSH key: ${resp.error.data || "Unknown error"}`, ); return; } @@ -314,7 +424,7 @@ export default function SettingsSidebar() { const receivedEdid = resp.result as string; const matchingEdid = edids.find( - x => x.value.toLowerCase() === receivedEdid.toLowerCase(), + x => x.value.toLowerCase() === receivedEdid.toLowerCase(), ); if (matchingEdid) { @@ -331,7 +441,7 @@ export default function SettingsSidebar() { send("getBacklightSettings", {}, resp => { if ("error" in resp) { notifications.error( - `Failed to get backlight settings: ${resp.error.data || "Unknown error"}`, + `Failed to get backlight settings: ${resp.error.data || "Unknown error"}`, ); return; } @@ -368,8 +478,8 @@ export default function SettingsSidebar() { const getDevice = useCallback(async () => { try { const status = await api - .GET(`${SIGNAL_API}/device`) - .then(res => res.json() as Promise); + .GET(`${SIGNAL_API}/device`) + .then(res => res.json() as Promise); setLocalDevice(status); } catch (error) { notifications.error("Failed to get authentication status"); @@ -377,7 +487,9 @@ export default function SettingsSidebar() { }, []); const { setModalView: setLocalAuthModalView } = useLocalAuthModalStore(); + const { setModalView: setUsbConfigModalView } = useUsbConfigModalStore(); const [isLocalAuthDialogOpen, setIsLocalAuthDialogOpen] = useState(false); + const [isUsbConfigDialogOpen, setIsUsbConfigDialogOpen] = useState(false); useEffect(() => { if (isOnDevice) getDevice(); @@ -391,6 +503,14 @@ export default function SettingsSidebar() { } }, [getDevice, isLocalAuthDialogOpen]); + useEffect(() => { + if (!isOnDevice) return; + // Refresh device status when the local usb config dialog is closed + if (!isUsbConfigDialogOpen) { + getDevice(); + } + }, [getDevice, isUsbConfigDialogOpen]); + const revalidator = useRevalidator(); const [currentTheme, setCurrentTheme] = useState(() => { @@ -404,8 +524,8 @@ export default function SettingsSidebar() { localStorage.removeItem("theme"); // Check system preference const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches - ? "dark" - : "light"; + ? "dark" + : "light"; root.classList.remove("light", "dark"); root.classList.add(systemTheme); } else { @@ -419,7 +539,7 @@ export default function SettingsSidebar() { send("resetConfig", {}, resp => { if ("error" in resp) { notifications.error( - `Failed to reset configuration: ${resp.error.data || "Unknown error"}`, + `Failed to reset configuration: ${resp.error.data || "Unknown error"}`, ); return; } @@ -428,577 +548,621 @@ export default function SettingsSidebar() { }, [send]); return ( -
e.stopPropagation()} - onKeyUp={e => e.stopPropagation()} - > -
e.stopPropagation()} + onKeyUp={e => e.stopPropagation()} > -
-
- - App: {currentVersions.appVersion} -
- System: {currentVersions.systemVersion} - - ) : ( - "Loading current versions..." - ) - } - /> -
-
+
+
+ + +
+ + { + setHideCursor(e.target.checked); + }} + /> + + + { + handleJigglerChange(e.target.checked); + }} + /> + +
+ +
+ + +
+
- - -
- - { - setHideCursor(e.target.checked); - }} - /> - - - { - handleJigglerChange(e.target.checked); - }} - /> - +
+
- -
- - -
-
-
-
-
-
- -
- - handleStreamQualityChange(e.target.value)} - /> - - - { - if (e.target.value === "custom") { - setEdid("custom"); - setCustomEdidValue(""); - } else { - handleEDIDChange(e.target.value as string); - } - }} - options={[...edids, { value: "custom", label: "Custom" }]} - /> - - {customEdidValue !== null && ( - <> - - setCustomEdidValue(e.target.value)} - /> -
-
- - )} -
-
- {isOnDevice && ( - <> -
-
- - - -
- -
-
-

- Cloud Security -

-
-
    -
  • • End-to-end encryption using WebRTC (DTLS and SRTP)
  • -
  • • Zero Trust security model
  • -
  • • OIDC (OpenID Connect) authentication
  • -
  • • All streams encrypted in transit
  • -
-
- -
- All cloud components are open-source and available on{" "} - - GitHub - - . -
-
-
- -
- -
-
-
-
- - {!isAdopted ? ( -
- -
- ) : ( -
-
-

- Your device is adopted to JetKVM Cloud -

-
-
-
-
- )} -
- - )} -
- {isOnDevice ? ( - <> -
- - -
- - {localDevice?.authMode === "password" ? ( -
-
-
- - ) : null} -
- - -
- - { - handleAutoUpdateChange(e.target.checked); - }} - /> - - - { - handleDevChannelChange(e.target.checked); - }} - /> - -
-
-
- - - - { - setCurrentTheme(e.target.value); - handleThemeChange(e.target.value); - }} - /> - -
-
- -
- - { - settings.backlightSettings.max_brightness = parseInt(e.target.value) - handleBacklightSettingsChange(settings.backlightSettings); - }} - /> - - {settings.backlightSettings.max_brightness != 0 && ( - <> - - { - settings.backlightSettings.dim_after = parseInt(e.target.value) - handleBacklightSettingsChange(settings.backlightSettings); - }} - /> - - - { - settings.backlightSettings.off_after = parseInt(e.target.value) - handleBacklightSettingsChange(settings.backlightSettings); - }} - /> - - - )} -

- The display will wake up when the connection state changes, or when touched. -

-
-
- - -
- - handleDevModeChange(e.target.checked)} - /> - - - {settings.developerMode && ( -
- handleSSHKeyChange(e.target.value)} - placeholder="Enter your SSH public key" - /> -

- The default SSH user is root. -

-
-
-
- )} - - { - settings.setDebugMode(e.target.checked); - }} - /> - - - {settings.debugMode && ( - <> - -
+ + )} +
+
+ {isOnDevice && ( + <> +
+
+ + + +
+ +
+
+

+ Cloud Security +

+
+
    +
  • • End-to-end encryption using WebRTC (DTLS and SRTP)
  • +
  • • Zero Trust security model
  • +
  • • OIDC (OpenID Connect) authentication
  • +
  • • All streams encrypted in transit
  • +
+
+ +
+ All cloud components are open-source and available on{" "} + + GitHub + + . +
+
+
+ +
+ +
+
+
+
+ + {!isAdopted ? ( +
+ +
+ ) : ( +
+
+

+ Your device is adopted to JetKVM Cloud +

+
+
+
+
+ )} +
+ + )} +
+ {isOnDevice ? ( + <> +
+ + +
+ + {localDevice?.authMode === "password" ? ( +
+
+
+ + ) : null} +
+ + +
+ + { + handleAutoUpdateChange(e.target.checked); + }} + /> + + + { + handleDevChannelChange(e.target.checked); + }} + /> + +
+
+
+ + + + { + setCurrentTheme(e.target.value); + handleThemeChange(e.target.value); + }} + /> + +
+
+ +
+ + { + settings.backlightSettings.max_brightness = parseInt(e.target.value) + handleBacklightSettingsChange(settings.backlightSettings); + }} + /> + + {settings.backlightSettings.max_brightness != 0 && ( + <> + + { + settings.backlightSettings.dim_after = parseInt(e.target.value) + handleBacklightSettingsChange(settings.backlightSettings); + }} + /> + + + { + settings.backlightSettings.off_after = parseInt(e.target.value) + handleBacklightSettingsChange(settings.backlightSettings); + }} + /> + + + )} +

+ The display will wake up when the connection state changes, or when touched. +

+
+
+ + +
+ + handleDevModeChange(e.target.checked)} + /> + + + {settings.developerMode && ( +
+ handleSSHKeyChange(e.target.value)} + placeholder="Enter your SSH public key" + /> +

+ The default SSH user is root. +

+
+
+
+ )} + + { + settings.setDebugMode(e.target.checked); + }} + /> + + {settings.developerMode && ( + + { + if (e.target.value === "custom") { + setUsbConfigProduct(e.target.value); + } else { + handleUsbConfigChange(e.target.value as string); + } + }} + options={[...usbConfigs, { value: "custom", label: "Custom" }]} + /> + + )} + {usbConfigProduct === "custom" && ( + +
+ { + // Revalidate the current route to refresh the local device status and dependent UI components + revalidator.revalidate(); + setIsUsbConfigDialogOpen(x); + }} + /> + { + // Revalidate the current route to refresh the local device status and dependent UI components + revalidator.revalidate(); + setIsLocalAuthDialogOpen(x); + }} + />
- { - // Revalidate the current route to refresh the local device status and dependent UI components - revalidator.revalidate(); - setIsLocalAuthDialogOpen(x); - }} - /> -
); } diff --git a/ui/src/hooks/stores.ts b/ui/src/hooks/stores.ts index 5b1366c..4f4f07a 100644 --- a/ui/src/hooks/stores.ts +++ b/ui/src/hooks/stores.ts @@ -3,9 +3,9 @@ import { createJSONStorage, persist } from "zustand/middleware"; // Utility function to append stats to a Map const appendStatToMap = ( - stat: T, - prevMap: Map, - maxEntries = 130, + stat: T, + prevMap: Map, + maxEntries = 130, ): Map => { if (prevMap.size > maxEntries) { const firstKey = prevMap.keys().next().value; @@ -71,20 +71,20 @@ export const useUiStore = create(set => ({ setWakeOnLanModalVisibility: enabled => set({ isWakeOnLanModalVisible: enabled }), toggleSidebarView: view => - set(state => { - if (state.sidebarView === view) { - return { sidebarView: null }; - } else { - return { sidebarView: view }; - } - }), + set(state => { + if (state.sidebarView === view) { + return { sidebarView: null }; + } else { + return { sidebarView: view }; + } + }), modalView: null, setModalView: view => set({ modalView: view }), isAttachedVirtualKeyboardVisible: true, setAttachedVirtualKeyboardVisibility: enabled => - set({ isAttachedVirtualKeyboardVisible: enabled }), + set({ isAttachedVirtualKeyboardVisible: enabled }), })); interface RTCState { @@ -283,33 +283,33 @@ interface SettingsState { } export const useSettingsStore = create( - persist( - set => ({ - isCursorHidden: false, - setCursorVisibility: enabled => set({ isCursorHidden: enabled }), + persist( + set => ({ + isCursorHidden: false, + setCursorVisibility: enabled => set({ isCursorHidden: enabled }), - mouseMode: "absolute", - setMouseMode: mode => set({ mouseMode: mode }), + mouseMode: "absolute", + setMouseMode: mode => set({ mouseMode: mode }), - debugMode: import.meta.env.DEV, - setDebugMode: enabled => set({ debugMode: enabled }), + debugMode: import.meta.env.DEV, + setDebugMode: enabled => set({ debugMode: enabled }), - // Add developer mode with default value - developerMode: false, - setDeveloperMode: enabled => set({ developerMode: enabled }), + // Add developer mode with default value + developerMode: false, + setDeveloperMode: enabled => set({ developerMode: enabled }), - backlightSettings: { - max_brightness: 100, - dim_after: 10000, - off_after: 50000, - }, - setBacklightSettings: (settings: BacklightSettings) => set({ backlightSettings: settings }), - }), - { - name: "settings", - storage: createJSONStorage(() => localStorage), - }, - ), + backlightSettings: { + max_brightness: 100, + dim_after: 10000, + off_after: 50000, + }, + setBacklightSettings: (settings: BacklightSettings) => set({ backlightSettings: settings }), + }), + { + name: "settings", + storage: createJSONStorage(() => localStorage), + }, + ), ); export interface RemoteVirtualMediaState { @@ -356,7 +356,7 @@ export const useMountMediaStore = create(set => ({ uploadedFiles: [], addUploadedFile: file => - set(state => ({ uploadedFiles: [...state.uploadedFiles, file] })), + set(state => ({ uploadedFiles: [...state.uploadedFiles, file] })), errorMessage: null, setErrorMessage: message => set({ errorMessage: message }), @@ -477,12 +477,12 @@ export interface UpdateState { setOtaState: (state: UpdateState["otaState"]) => void; setUpdateDialogHasBeenMinimized: (hasBeenMinimized: boolean) => void; modalView: - | "loading" - | "updating" - | "upToDate" - | "updateAvailable" - | "updateCompleted" - | "error"; + | "loading" + | "updating" + | "upToDate" + | "updateAvailable" + | "updateCompleted" + | "error"; setModalView: (view: UpdateState["modalView"]) => void; isUpdateDialogOpen: boolean; setIsUpdateDialogOpen: (isOpen: boolean) => void; @@ -517,7 +517,7 @@ export const useUpdateStore = create(set => ({ updateDialogHasBeenMinimized: false, setUpdateDialogHasBeenMinimized: hasBeenMinimized => - set({ updateDialogHasBeenMinimized: hasBeenMinimized }), + set({ updateDialogHasBeenMinimized: hasBeenMinimized }), modalView: "loading", setModalView: view => set({ modalView: view }), isUpdateDialogOpen: false, @@ -526,14 +526,38 @@ export const useUpdateStore = create(set => ({ setUpdateErrorMessage: errorMessage => set({ updateErrorMessage: errorMessage }), })); +interface UsbConfigModalState { + modalView: + | "updateUsbConfig" + | "updateUsbConfigSuccess"; + errorMessage: string | null; + setModalView: (view: UsbConfigModalState["modalView"]) => void; + setErrorMessage: (message: string | null) => void; +} + +export interface UsbConfigState { + vendor_id: string; + product_id: string; + serial_number: string; + manufacturer: string; + product: string; +} + +export const useUsbConfigModalStore = create(set => ({ + modalView: "updateUsbConfig", + errorMessage: null, + setModalView: view => set({ modalView: view }), + setErrorMessage: message => set({ errorMessage: message }), +})); + interface LocalAuthModalState { modalView: - | "createPassword" - | "deletePassword" - | "updatePassword" - | "creationSuccess" - | "deleteSuccess" - | "updateSuccess"; + | "createPassword" + | "deletePassword" + | "updatePassword" + | "creationSuccess" + | "deleteSuccess" + | "updateSuccess"; errorMessage: string | null; setModalView: (view: LocalAuthModalState["modalView"]) => void; setErrorMessage: (message: string | null) => void; diff --git a/usb.go b/usb.go index e302815..0ff27aa 100644 --- a/usb.go +++ b/usb.go @@ -58,6 +58,44 @@ func init() { //TODO: read hid reports(capslock, numlock, etc) from keyboardHidFile } +func UpdateGadgetConfig() error { + LoadConfig() + gadgetAttrs := [][]string{ + {"idVendor", config.UsbConfig.VendorId}, + {"idProduct", config.UsbConfig.ProductId}, + } + err := writeGadgetAttrs(kvmGadgetPath, gadgetAttrs) + if err != nil { + return err + } + + log.Printf("Successfully updated usb gadget attributes: %v", gadgetAttrs) + + strAttrs := [][]string{ + {"serialnumber", config.UsbConfig.SerialNumber}, + {"manufacturer", config.UsbConfig.Manufacturer}, + {"product", config.UsbConfig.Product}, + } + gadgetStringsPath := filepath.Join(kvmGadgetPath, "strings", "0x409") + err = os.MkdirAll(gadgetStringsPath, 0755) + if err != nil { + return err + } + err = writeGadgetAttrs(gadgetStringsPath, strAttrs) + if err != nil { + return err + } + + log.Printf("Successfully updated usb string attributes: %s", strAttrs) + + err = rebindUsb() + if err != nil { + return err + } + + return nil +} + func writeGadgetAttrs(basePath string, attrs [][]string) error { for _, item := range attrs { filePath := filepath.Join(basePath, item[0]) @@ -80,9 +118,9 @@ func writeGadgetConfig() error { } err = writeGadgetAttrs(kvmGadgetPath, [][]string{ - {"bcdUSB", "0x0200"}, //USB 2.0 - {"idVendor", "0x1d6b"}, //The Linux Foundation - {"idProduct", "0104"}, //Multifunction Composite Gadget¬ + {"bcdUSB", "0x0200"}, //USB 2.0 + {"idVendor", config.UsbConfig.VendorId}, //The Linux Foundation + {"idProduct", config.UsbConfig.ProductId}, //Multifunction Composite Gadget¬ {"bcdDevice", "0100"}, }) if err != nil { @@ -97,8 +135,8 @@ func writeGadgetConfig() error { err = writeGadgetAttrs(gadgetStringsPath, [][]string{ {"serialnumber", GetDeviceID()}, - {"manufacturer", "JetKVM"}, - {"product", "JetKVM USB Emulation Device"}, + {"manufacturer", config.UsbConfig.Manufacturer}, + {"product", config.UsbConfig.Product}, }) if err != nil { return err