Skip to content

Commit 7f74d74

Browse files
adrianmerazNevexo
authored andcommitted
squashed: jackislanding: Configurable USB IDs
Pulled from https://github.com/jackislanding/jack-kvm/tree/feature/configure-usb-ids but squashed all the commits down as it was merging hell and would make the jetkvm-next tree huge. Haven't tested this yet, so not tagging next here on this commit. removed debug asterisks cleanup cleanup cleanup cleanup cleanup cleanup cleanup cleanup cleanup cleanup cleanup cleanup cleaned up settings file removed unused dep prettifying linted modules converted input fields to use a modal to save space in settings updated descriptions updated descriptions renamed module to match convention renamed usb name to usb product to match convention added usb config defaults moved setting added config default values changed to dev mode added rpc function to get usb config now loads usb config values to set input values added logging cleaned up var names changed to defaultValue input fields now load previous values added logging cleaned up logging added regex patterns on inputs added VirtualMediaEnabled config removed trailing characters
1 parent 61ed8aa commit 7f74d74

File tree

6 files changed

+821
-472
lines changed

6 files changed

+821
-472
lines changed

config.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@ type WakeOnLanDevice struct {
1111
MacAddress string `json:"macAddress"`
1212
}
1313

14+
type UsbConfig struct {
15+
VendorId string `json:"vendor_id"`
16+
ProductId string `json:"product_id"`
17+
SerialNumber string `json:"serial_number"`
18+
Manufacturer string `json:"manufacturer"`
19+
Product string `json:"product"`
20+
}
21+
1422
type Config struct {
1523
CloudURL string `json:"cloud_url"`
1624
CloudToken string `json:"cloud_token"`
@@ -26,6 +34,8 @@ type Config struct {
2634
DisplayDimAfterSec int `json:"display_dim_after_sec"`
2735
DisplayOffAfterSec int `json:"display_off_after_sec"`
2836
EdidString string `json:"hdmi_edid_string"`
37+
UsbConfig UsbConfig `json:"usb_config"`
38+
VirtualMediaEnabled bool `json:"virtual_media_enabled"`
2939
}
3040

3141
const configPath = "/userdata/kvm_config.json"
@@ -36,6 +46,14 @@ var defaultConfig = &Config{
3646
DisplayMaxBrightness: 64,
3747
DisplayDimAfterSec: 120, // 2 minutes
3848
DisplayOffAfterSec: 1800, // 30 minutes
49+
VirtualMediaEnabled: true,
50+
UsbConfig: UsbConfig{
51+
VendorId: "0x1d6b", //The Linux Foundation
52+
ProductId: "0x0104", //Multifunction Composite Gadget
53+
SerialNumber: "",
54+
Manufacturer: "JetKVM",
55+
Product: "JetKVM USB Emulation Device",
56+
},
3957
}
4058

4159
var config *Config

jsonrpc.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,29 @@ func rpcSetUsbEmulationState(enabled bool) error {
379379
}
380380
}
381381

382+
func rpcGetUsbConfig() (UsbConfig, error) {
383+
LoadConfig()
384+
return config.UsbConfig, nil
385+
}
386+
387+
func rpcSetUsbConfig(usbConfig UsbConfig) error {
388+
LoadConfig()
389+
config.UsbConfig = usbConfig
390+
391+
err := UpdateGadgetConfig()
392+
if err != nil {
393+
return fmt.Errorf("failed to write gadget config: %w", err)
394+
}
395+
396+
err = SaveConfig()
397+
if err != nil {
398+
return fmt.Errorf("failed to save usb config: %w", err)
399+
}
400+
401+
log.Printf("[jsonrpc.go:rpcSetUsbConfig] usb config set to %s", usbConfig)
402+
return nil
403+
}
404+
382405
func rpcGetWakeOnLanDevices() ([]WakeOnLanDevice, error) {
383406
LoadConfig()
384407
if config.WakeOnLanDevices == nil {
@@ -443,6 +466,8 @@ var rpcHandlers = map[string]*jsonrpc.RPCHandler{
443466
"isUpdatePending": {Func: rpcIsUpdatePending},
444467
"getUsbEmulationState": {Func: rpcGetUsbEmulationState},
445468
"setUsbEmulationState": {Func: rpcSetUsbEmulationState, Params: []string{"enabled"}},
469+
"getUsbConfig": {Func: rpcGetUsbConfig},
470+
"setUsbConfig": {Func: rpcSetUsbConfig, Params: []string{"usbConfig"}},
446471
"checkMountUrl": {Func: rpcCheckMountUrl, Params: []string{"url"}},
447472
"getVirtualMediaState": {Func: rpcGetVirtualMediaState},
448473
"getStorageSpace": {Func: rpcGetStorageSpace},
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
import { GridCard } from "@/components/Card";
2+
import {useCallback, useEffect, useState} from "react";
3+
import { Button } from "@components/Button";
4+
import LogoBlueIcon from "@/assets/logo-blue.svg";
5+
import LogoWhiteIcon from "@/assets/logo-white.svg";
6+
import Modal from "@components/Modal";
7+
import { InputFieldWithLabel } from "./InputField";
8+
import { useJsonRpc } from "@/hooks/useJsonRpc";
9+
import { useUsbConfigModalStore } from "@/hooks/stores";
10+
11+
export interface UsbConfigState {
12+
vendor_id: string;
13+
product_id: string;
14+
serial_number: string;
15+
manufacturer: string;
16+
product: string;
17+
}
18+
19+
export default function USBConfigDialog({
20+
open,
21+
setOpen,
22+
}: {
23+
open: boolean;
24+
setOpen: (open: boolean) => void;
25+
}) {
26+
return (
27+
<Modal open={open} onClose={() => setOpen(false)}>
28+
<Dialog setOpen={setOpen} />
29+
</Modal>
30+
);
31+
}
32+
33+
export function Dialog({ setOpen }: { setOpen: (open: boolean) => void }) {
34+
const { modalView, setModalView } = useUsbConfigModalStore();
35+
const [error, setError] = useState<string | null>(null);
36+
37+
const [send] = useJsonRpc();
38+
39+
const handleUsbConfigChange = useCallback((usbConfig: object) => {
40+
send("setUsbConfig", { usbConfig }, resp => {
41+
if ("error" in resp) {
42+
setError(`Failed to update USB Config: ${resp.error.data || "Unknown error"}`);
43+
return;
44+
}
45+
setModalView("updateUsbConfigSuccess");
46+
});
47+
}, [send, setModalView]);
48+
49+
return (
50+
<GridCard cardClassName="relative max-w-lg mx-auto text-left pointer-events-auto dark:bg-slate-800">
51+
<div className="p-10">
52+
{modalView === "updateUsbConfig" && (
53+
<UpdateUsbConfigModal
54+
onSetUsbConfig={handleUsbConfigChange}
55+
onCancel={() => setOpen(false)}
56+
error={error}
57+
/>
58+
)}
59+
60+
{modalView === "updateUsbConfigSuccess" && (
61+
<SuccessModal
62+
headline="USB Configuration Updated Successfully"
63+
description="You've successfully updated the USB Configuration"
64+
onClose={() => setOpen(false)}
65+
/>
66+
)}
67+
</div>
68+
</GridCard>
69+
);
70+
}
71+
72+
function UpdateUsbConfigModal({
73+
onSetUsbConfig,
74+
onCancel,
75+
error,
76+
}: {
77+
onSetUsbConfig: (usb_config: object) => void;
78+
onCancel: () => void;
79+
error: string | null;
80+
}) {
81+
const [usbConfig, setUsbConfig] = useState({
82+
vendor_id: '',
83+
product_id: '',
84+
serial_number: '',
85+
manufacturer: '',
86+
product: '',
87+
})
88+
89+
const [usbConfigState, setUsbConfigState] = useState<UsbConfigState>();
90+
const [send] = useJsonRpc();
91+
92+
const syncUsbConfig = useCallback(() => {
93+
send("getUsbConfig", {}, resp => {
94+
if ("error" in resp) {
95+
console.error("Failed to load USB Config:", resp.error);
96+
} else {
97+
setUsbConfigState(resp.result as UsbConfigState);
98+
}
99+
});
100+
}, [send, setUsbConfigState]);
101+
102+
// Load stored usb config from the backend
103+
useEffect(() => {
104+
syncUsbConfig();
105+
}, [syncUsbConfig]);
106+
107+
const handleUsbVendorIdChange = (value: string) => {
108+
setUsbConfig({... usbConfig, vendor_id: value})
109+
};
110+
111+
const handleUsbProductIdChange = (value: string) => {
112+
setUsbConfig({... usbConfig, product_id: value})
113+
};
114+
115+
const handleUsbSerialChange = (value: string) => {
116+
setUsbConfig({... usbConfig, serial_number: value})
117+
};
118+
119+
const handleUsbManufacturer = (value: string) => {
120+
setUsbConfig({... usbConfig, manufacturer: value})
121+
};
122+
123+
const handleUsbProduct = (value: string) => {
124+
setUsbConfig({... usbConfig, product: value})
125+
};
126+
127+
return (
128+
<div className="flex flex-col items-start justify-start space-y-4 text-left">
129+
<div>
130+
<img src={LogoWhiteIcon} alt="" className="h-[24px] hidden dark:block" />
131+
<img src={LogoBlueIcon} alt="" className="h-[24px] dark:hidden" />
132+
</div>
133+
<div className="space-y-4">
134+
<div>
135+
<h2 className="text-lg font-semibold dark:text-white">USB Emulation Configuration</h2>
136+
<p className="text-sm text-slate-600 dark:text-slate-400">
137+
Set custom USB parameters to control how the USB device is emulated.
138+
The device will rebind once the parameters are updated.
139+
</p>
140+
</div>
141+
<InputFieldWithLabel
142+
required
143+
label="Vendor ID"
144+
placeholder="Enter Vendor ID"
145+
pattern="^0[xX][\da-fA-F]{4}$"
146+
defaultValue={usbConfigState?.vendor_id}
147+
onChange={e => handleUsbVendorIdChange(e.target.value)}
148+
/>
149+
<InputFieldWithLabel
150+
required
151+
label="Product ID"
152+
placeholder="Enter Product ID"
153+
pattern="^0[xX][\da-fA-F]{4}$"
154+
defaultValue={usbConfigState?.product_id}
155+
onChange={e => handleUsbProductIdChange(e.target.value)}
156+
/>
157+
<InputFieldWithLabel
158+
required
159+
label="Serial Number"
160+
placeholder="Enter Serial Number"
161+
defaultValue={usbConfigState?.serial_number}
162+
onChange={e => handleUsbSerialChange(e.target.value)}
163+
/>
164+
<InputFieldWithLabel
165+
required
166+
label="Manufacturer"
167+
placeholder="Enter Manufacturer"
168+
defaultValue={usbConfigState?.manufacturer}
169+
onChange={e => handleUsbManufacturer(e.target.value)}
170+
/>
171+
<InputFieldWithLabel
172+
required
173+
label="Product Name"
174+
placeholder="Enter Product Name"
175+
defaultValue={usbConfigState?.product}
176+
onChange={e => handleUsbProduct(e.target.value)}
177+
/>
178+
<div className="flex gap-x-2">
179+
<Button
180+
size="SM"
181+
theme="primary"
182+
text="Update USB Config"
183+
onClick={() => onSetUsbConfig(usbConfig)}
184+
/>
185+
<Button size="SM" theme="light" text="Not Now" onClick={onCancel} />
186+
</div>
187+
{error && <p className="text-sm text-red-500">{error}</p>}
188+
</div>
189+
</div>
190+
);
191+
}
192+
193+
function SuccessModal({
194+
headline,
195+
description,
196+
onClose,
197+
}: {
198+
headline: string;
199+
description: string;
200+
onClose: () => void;
201+
}) {
202+
return (
203+
<div className="flex flex-col items-start justify-start w-full max-w-lg space-y-4 text-left">
204+
<div>
205+
<img src={LogoWhiteIcon} alt="" className="h-[24px] hidden dark:block" />
206+
<img src={LogoBlueIcon} alt="" className="h-[24px] dark:hidden" />
207+
</div>
208+
<div className="space-y-4">
209+
<div>
210+
<h2 className="text-lg font-semibold dark:text-white">{headline}</h2>
211+
<p className="text-sm text-slate-600 dark:text-slate-400">{description}</p>
212+
</div>
213+
<Button size="SM" theme="primary" text="Close" onClick={onClose} />
214+
</div>
215+
</div>
216+
);
217+
}

0 commit comments

Comments
 (0)