Skip to content

Commit 77263e7

Browse files
jackislandingadrianmerazadamshiervani
authored
Feature/usb config - Rebasing USB Config Changes on Dev Branch (#185)
* rebasing on dev branch * fixed formatting * fixed formatting * removed query params * moved usb settings to hardware setting * swapped from error to log * added fix for any change to product name now resulting in show the spinner as custom on page reload * formatting --------- Co-authored-by: JackTheRooster <[email protected]> Co-authored-by: Adam Shiervani <[email protected]>
1 parent 92aec30 commit 77263e7

File tree

6 files changed

+490
-7
lines changed

6 files changed

+490
-7
lines changed

config.go

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

15+
type UsbConfig struct {
16+
VendorId string `json:"vendor_id"`
17+
ProductId string `json:"product_id"`
18+
SerialNumber string `json:"serial_number"`
19+
Manufacturer string `json:"manufacturer"`
20+
Product string `json:"product"`
21+
}
22+
1523
type Config struct {
1624
CloudURL string `json:"cloud_url"`
1725
CloudToken string `json:"cloud_token"`
@@ -28,6 +36,7 @@ type Config struct {
2836
DisplayMaxBrightness int `json:"display_max_brightness"`
2937
DisplayDimAfterSec int `json:"display_dim_after_sec"`
3038
DisplayOffAfterSec int `json:"display_off_after_sec"`
39+
UsbConfig UsbConfig `json:"usb_config"`
3140
}
3241

3342
const configPath = "/userdata/kvm_config.json"
@@ -39,6 +48,13 @@ var defaultConfig = &Config{
3948
DisplayMaxBrightness: 64,
4049
DisplayDimAfterSec: 120, // 2 minutes
4150
DisplayOffAfterSec: 1800, // 30 minutes
51+
UsbConfig: UsbConfig{
52+
VendorId: "0x1d6b", //The Linux Foundation
53+
ProductId: "0x0104", //Multifunction Composite Gadget
54+
SerialNumber: "",
55+
Manufacturer: "JetKVM",
56+
Product: "JetKVM USB Emulation Device",
57+
},
4258
}
4359

4460
var (

jsonrpc.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,29 @@ func rpcSetUsbEmulationState(enabled bool) error {
538538
}
539539
}
540540

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

0 commit comments

Comments
 (0)