Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
7361289
created feature branch
adrianmeraz Jan 24, 2025
391de74
removed debug asterisks
adrianmeraz Jan 24, 2025
8329137
cleanup
adrianmeraz Jan 25, 2025
f8e6683
cleanup
adrianmeraz Jan 25, 2025
789f2be
cleanup
adrianmeraz Jan 25, 2025
fd6e2fa
cleanup
adrianmeraz Jan 25, 2025
8c54eac
cleanup
adrianmeraz Jan 25, 2025
30385f8
cleanup
adrianmeraz Jan 25, 2025
3be51a1
cleanup
adrianmeraz Jan 25, 2025
e9b68f8
cleanup
adrianmeraz Jan 25, 2025
ac377c4
cleanup
adrianmeraz Jan 25, 2025
61477e1
cleanup
adrianmeraz Jan 25, 2025
8108597
cleanup
adrianmeraz Jan 25, 2025
afebc2d
cleanup
adrianmeraz Jan 25, 2025
c6ba93c
cleaned up settings file
adrianmeraz Jan 25, 2025
e9096c3
removed unused dep
adrianmeraz Jan 25, 2025
03fd750
prettifying
adrianmeraz Jan 25, 2025
0cd406f
linted modules
adrianmeraz Jan 25, 2025
2e7493c
updated descriptions
adrianmeraz Jan 25, 2025
ac1403d
renamed module to match convention
adrianmeraz Jan 25, 2025
21b458b
renamed usb name to usb product to match convention
adrianmeraz Jan 25, 2025
a67e445
moved setting
adrianmeraz Jan 25, 2025
318594a
added config default values
adrianmeraz Jan 25, 2025
f67b4c1
changed to dev mode
adrianmeraz Jan 25, 2025
f7eba7c
added rpc function to get usb config
adrianmeraz Jan 25, 2025
382c07b
added logging
adrianmeraz Jan 25, 2025
bffac9a
cleaned up var names
adrianmeraz Jan 25, 2025
6a6ab14
changed to defaultValue
adrianmeraz Jan 25, 2025
313f780
input fields now load previous values
adrianmeraz Jan 25, 2025
621c333
added logging
adrianmeraz Jan 26, 2025
682b591
cleaned up logging
adrianmeraz Jan 26, 2025
58c5875
added regex patterns on inputs
adrianmeraz Jan 26, 2025
abd95ab
added VirtualMediaEnabled config
adrianmeraz Jan 26, 2025
c5961ac
removed trailing characters
adrianmeraz Jan 29, 2025
af92498
reverted formatting
jackislanding Jan 30, 2025
065f1bd
fix for initial values being empty
adrianmeraz Jan 30, 2025
194fad9
Revert "fix for initial values being empty"
adrianmeraz Jan 30, 2025
da1a57e
Revert "Revert "fix for initial values being empty""
adrianmeraz Jan 30, 2025
08defe6
Merge remote-tracking branch 'origin/feature/configure-usb-ids' into …
adrianmeraz Jan 30, 2025
1870144
squash! cleanup
adrianmeraz Jan 31, 2025
b037b8a
Merge branch 'feature/configure-usb-ids' of github.com:jackislanding/…
adrianmeraz Jan 31, 2025
23771e7
added preconfigured options for USB Emulation
adrianmeraz Feb 18, 2025
546bca4
added link to lookup usb device ids
adrianmeraz Feb 18, 2025
97c8fb7
added generated serial numbers
adrianmeraz Feb 19, 2025
aade074
now select inputs change correctly
adrianmeraz Feb 19, 2025
33a01ab
usb configuration now preserved on dropdown selector
adrianmeraz Feb 20, 2025
b7ba6f7
moved UsbConfigState interface to stores
adrianmeraz Feb 20, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 30 additions & 12 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,42 @@ 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"`
GoogleIdentity string `json:"google_identity"`
JigglerEnabled bool `json:"jiggler_enabled"`
AutoUpdateEnabled bool `json:"auto_update_enabled"`
IncludePreRelease bool `json:"include_pre_release"`
HashedPassword string `json:"hashed_password"`
LocalAuthToken string `json:"local_auth_token"`
LocalAuthMode string `json:"localAuthMode"` //TODO: fix it with migration
WakeOnLanDevices []WakeOnLanDevice `json:"wake_on_lan_devices"`
CloudURL string `json:"cloud_url"`
CloudToken string `json:"cloud_token"`
GoogleIdentity string `json:"google_identity"`
JigglerEnabled bool `json:"jiggler_enabled"`
AutoUpdateEnabled bool `json:"auto_update_enabled"`
IncludePreRelease bool `json:"include_pre_release"`
HashedPassword string `json:"hashed_password"`
LocalAuthToken string `json:"local_auth_token"`
LocalAuthMode string `json:"localAuthMode"` //TODO: fix it with migration
WakeOnLanDevices []WakeOnLanDevice `json:"wake_on_lan_devices"`
UsbConfig UsbConfig `json:"usb_config"`
VirtualMediaEnabled bool `json:"virtual_media_enabled"`
}

const configPath = "/userdata/kvm_config.json"

var defaultConfig = &Config{
CloudURL: "https://api.jetkvm.com",
AutoUpdateEnabled: true, // Set a default value
CloudURL: "https://api.jetkvm.com",
AutoUpdateEnabled: true, // Set a default value
VirtualMediaEnabled: true,
UsbConfig: UsbConfig{
VendorId: "0x1d6b", //The Linux Foundation
ProductId: "0x0104", //Multifunction Composite Gadget
SerialNumber: "",
Manufacturer: "JetKVM",
Product: "JetKVM USB Emulation Device",
},
}

var config *Config
Expand Down
25 changes: 25 additions & 0 deletions jsonrpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,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) {
LoadConfig()
if config.WakeOnLanDevices == nil {
Expand Down Expand Up @@ -542,6 +565,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},
Expand Down
217 changes: 217 additions & 0 deletions ui/src/components/USBConfigDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
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 (
<Modal open={open} onClose={() => setOpen(false)}>
<Dialog setOpen={setOpen} />
</Modal>
);
}

export function Dialog({ setOpen }: { setOpen: (open: boolean) => void }) {
const { modalView, setModalView } = useUsbConfigModalStore();
const [error, setError] = useState<string | null>(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 (
<GridCard cardClassName="relative max-w-lg mx-auto text-left pointer-events-auto dark:bg-slate-800">
<div className="p-10">
{modalView === "updateUsbConfig" && (
<UpdateUsbConfigModal
onSetUsbConfig={handleUsbConfigChange}
onCancel={() => setOpen(false)}
error={error}
/>
)}

{modalView === "updateUsbConfigSuccess" && (
<SuccessModal
headline="USB Configuration Updated Successfully"
description="You've successfully updated the USB Configuration"
onClose={() => setOpen(false)}
/>
)}
</div>
</GridCard>
);
}

function UpdateUsbConfigModal({
onSetUsbConfig,
onCancel,
error,
}: {
onSetUsbConfig: (usb_config: object) => void;
onCancel: () => void;
error: string | null;
}) {
const [usbConfigState, setUsbConfigState] = useState<UsbConfigState>({
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 (
<div className="flex flex-col items-start justify-start space-y-4 text-left">
<div>
<img src={LogoWhiteIcon} alt="" className="h-[24px] hidden dark:block" />
<img src={LogoBlueIcon} alt="" className="h-[24px] dark:hidden" />
</div>
<div className="space-y-4">
<div>
<h2 className="text-lg font-semibold dark:text-white">USB Emulation Configuration</h2>
<p className="text-sm text-slate-600 dark:text-slate-400">
Set custom USB parameters to control how the USB device is emulated.
The device will rebind once the parameters are updated.
</p>
<div className="flex justify-start mt-4 text-xs text-slate-500 dark:text-slate-400">
<ExtLink
href={`https://the-sz.com/products/usbid/index.php?v=${usbConfigState.vendor_id}&p=${usbConfigState.product_id}`}
className="hover:underline"
>
Look up USB Device IDs here
</ExtLink>
</div>
</div>
<InputFieldWithLabel
required
label="Vendor ID"
placeholder="Enter Vendor ID"
pattern="^0[xX][\da-fA-F]{4}$"
defaultValue={usbConfigState?.vendor_id}
onChange={e => handleUsbVendorIdChange(e.target.value)}
/>
<InputFieldWithLabel
required
label="Product ID"
placeholder="Enter Product ID"
pattern="^0[xX][\da-fA-F]{4}$"
defaultValue={usbConfigState?.product_id}
onChange={e => handleUsbProductIdChange(e.target.value)}
/>
<InputFieldWithLabel
required
label="Serial Number"
placeholder="Enter Serial Number"
defaultValue={usbConfigState?.serial_number}
onChange={e => handleUsbSerialChange(e.target.value)}
/>
<InputFieldWithLabel
required
label="Manufacturer"
placeholder="Enter Manufacturer"
defaultValue={usbConfigState?.manufacturer}
onChange={e => handleUsbManufacturer(e.target.value)}
/>
<InputFieldWithLabel
required
label="Product Name"
placeholder="Enter Product Name"
defaultValue={usbConfigState?.product}
onChange={e => handleUsbProduct(e.target.value)}
/>
<div className="flex gap-x-2">
<Button
size="SM"
theme="primary"
text="Update USB Config"
onClick={() => onSetUsbConfig(usbConfigState)}
/>
<Button size="SM" theme="light" text="Not Now" onClick={onCancel} />
</div>
{error && <p className="text-sm text-red-500">{error}</p>}
</div>
</div>
);
}

function SuccessModal({
headline,
description,
onClose,
}: {
headline: string;
description: string;
onClose: () => void;
}) {
return (
<div className="flex flex-col items-start justify-start w-full max-w-lg space-y-4 text-left">
<div>
<img src={LogoWhiteIcon} alt="" className="h-[24px] hidden dark:block" />
<img src={LogoBlueIcon} alt="" className="h-[24px] dark:hidden" />
</div>
<div className="space-y-4">
<div>
<h2 className="text-lg font-semibold dark:text-white">{headline}</h2>
<p className="text-sm text-slate-600 dark:text-slate-400">{description}</p>
</div>
<Button size="SM" theme="primary" text="Close" onClick={onClose} />
</div>
</div>
);
}
Loading