mirror of https://github.com/jetkvm/kvm.git
Compare commits
3 Commits
0cee284561
...
c1d771cced
Author | SHA1 | Date |
---|---|---|
|
c1d771cced | |
|
019934d33e | |
|
0c5c69f2d3 |
|
@ -1,8 +1,11 @@
|
||||||
package usbgadget
|
package usbgadget
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
var keyboardConfig = gadgetConfigItem{
|
var keyboardConfig = gadgetConfigItem{
|
||||||
|
@ -36,6 +39,7 @@ var keyboardReportDesc = []byte{
|
||||||
0x81, 0x03, /* INPUT (Cnst,Var,Abs) */
|
0x81, 0x03, /* INPUT (Cnst,Var,Abs) */
|
||||||
0x95, 0x05, /* REPORT_COUNT (5) */
|
0x95, 0x05, /* REPORT_COUNT (5) */
|
||||||
0x75, 0x01, /* REPORT_SIZE (1) */
|
0x75, 0x01, /* REPORT_SIZE (1) */
|
||||||
|
|
||||||
0x05, 0x08, /* USAGE_PAGE (LEDs) */
|
0x05, 0x08, /* USAGE_PAGE (LEDs) */
|
||||||
0x19, 0x01, /* USAGE_MINIMUM (Num Lock) */
|
0x19, 0x01, /* USAGE_MINIMUM (Num Lock) */
|
||||||
0x29, 0x05, /* USAGE_MAXIMUM (Kana) */
|
0x29, 0x05, /* USAGE_MAXIMUM (Kana) */
|
||||||
|
@ -54,13 +58,139 @@ var keyboardReportDesc = []byte{
|
||||||
0xc0, /* END_COLLECTION */
|
0xc0, /* END_COLLECTION */
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *UsbGadget) keyboardWriteHidFile(data []byte) error {
|
const (
|
||||||
if u.keyboardHidFile == nil {
|
hidReadBufferSize = 8
|
||||||
var err error
|
// https://www.usb.org/sites/default/files/documents/hid1_11.pdf
|
||||||
u.keyboardHidFile, err = os.OpenFile("/dev/hidg0", os.O_RDWR, 0666)
|
// https://www.usb.org/sites/default/files/hut1_2.pdf
|
||||||
if err != nil {
|
KeyboardLedMaskNumLock = 1 << 0
|
||||||
return fmt.Errorf("failed to open hidg0: %w", err)
|
KeyboardLedMaskCapsLock = 1 << 1
|
||||||
|
KeyboardLedMaskScrollLock = 1 << 2
|
||||||
|
KeyboardLedMaskCompose = 1 << 3
|
||||||
|
KeyboardLedMaskKana = 1 << 4
|
||||||
|
ValidKeyboardLedMasks = KeyboardLedMaskNumLock | KeyboardLedMaskCapsLock | KeyboardLedMaskScrollLock | KeyboardLedMaskCompose | KeyboardLedMaskKana
|
||||||
|
)
|
||||||
|
|
||||||
|
// Synchronization between LED states and CAPS LOCK, NUM LOCK, SCROLL LOCK,
|
||||||
|
// COMPOSE, and KANA events is maintained by the host and NOT the keyboard. If
|
||||||
|
// using the keyboard descriptor in Appendix B, LED states are set by sending a
|
||||||
|
// 5-bit absolute report to the keyboard via a Set_Report(Output) request.
|
||||||
|
type KeyboardState struct {
|
||||||
|
NumLock bool `json:"num_lock"`
|
||||||
|
CapsLock bool `json:"caps_lock"`
|
||||||
|
ScrollLock bool `json:"scroll_lock"`
|
||||||
|
Compose bool `json:"compose"`
|
||||||
|
Kana bool `json:"kana"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func getKeyboardState(b byte) KeyboardState {
|
||||||
|
// should we check if it's the correct usage page?
|
||||||
|
return KeyboardState{
|
||||||
|
NumLock: b&KeyboardLedMaskNumLock != 0,
|
||||||
|
CapsLock: b&KeyboardLedMaskCapsLock != 0,
|
||||||
|
ScrollLock: b&KeyboardLedMaskScrollLock != 0,
|
||||||
|
Compose: b&KeyboardLedMaskCompose != 0,
|
||||||
|
Kana: b&KeyboardLedMaskKana != 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UsbGadget) updateKeyboardState(b byte) {
|
||||||
|
u.keyboardStateLock.Lock()
|
||||||
|
defer u.keyboardStateLock.Unlock()
|
||||||
|
|
||||||
|
if b&^ValidKeyboardLedMasks != 0 {
|
||||||
|
u.log.Trace().Uint8("b", b).Msg("contains invalid bits, ignoring")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newState := getKeyboardState(b)
|
||||||
|
if reflect.DeepEqual(u.keyboardState, newState) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
u.log.Info().Interface("old", u.keyboardState).Interface("new", newState).Msg("keyboardState updated")
|
||||||
|
u.keyboardState = newState
|
||||||
|
|
||||||
|
if u.onKeyboardStateChange != nil {
|
||||||
|
(*u.onKeyboardStateChange)(newState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UsbGadget) SetOnKeyboardStateChange(f func(state KeyboardState)) {
|
||||||
|
u.onKeyboardStateChange = &f
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UsbGadget) GetKeyboardState() KeyboardState {
|
||||||
|
u.keyboardStateLock.Lock()
|
||||||
|
defer u.keyboardStateLock.Unlock()
|
||||||
|
|
||||||
|
return u.keyboardState
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UsbGadget) listenKeyboardEvents() {
|
||||||
|
var path string
|
||||||
|
if u.keyboardHidFile != nil {
|
||||||
|
path = u.keyboardHidFile.Name()
|
||||||
|
}
|
||||||
|
l := u.log.With().Str("listener", "keyboardEvents").Str("path", path).Logger()
|
||||||
|
l.Trace().Msg("starting")
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
buf := make([]byte, hidReadBufferSize)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-u.keyboardStateCtx.Done():
|
||||||
|
l.Info().Msg("context done")
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
l.Trace().Msg("reading from keyboard")
|
||||||
|
if u.keyboardHidFile == nil {
|
||||||
|
l.Error().Msg("keyboardHidFile is nil")
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
n, err := u.keyboardHidFile.Read(buf)
|
||||||
|
if err != nil {
|
||||||
|
l.Error().Err(err).Msg("failed to read")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
l.Trace().Int("n", n).Bytes("buf", buf).Msg("got data from keyboard")
|
||||||
|
if n != 1 {
|
||||||
|
l.Trace().Int("n", n).Msg("expected 1 byte, got")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
u.updateKeyboardState(buf[0])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UsbGadget) openKeyboardHidFile() error {
|
||||||
|
if u.keyboardHidFile != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
u.keyboardHidFile, err = os.OpenFile("/dev/hidg0", os.O_RDWR, 0666)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open hidg0: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.keyboardStateCancel != nil {
|
||||||
|
u.keyboardStateCancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
u.keyboardStateCtx, u.keyboardStateCancel = context.WithCancel(context.Background())
|
||||||
|
u.listenKeyboardEvents()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UsbGadget) OpenKeyboardHidFile() error {
|
||||||
|
return u.openKeyboardHidFile()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UsbGadget) keyboardWriteHidFile(data []byte) error {
|
||||||
|
if err := u.openKeyboardHidFile(); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := u.keyboardHidFile.Write(data)
|
_, err := u.keyboardHidFile.Write(data)
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
package usbgadget
|
package usbgadget
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"sync"
|
"sync"
|
||||||
|
@ -59,6 +60,11 @@ type UsbGadget struct {
|
||||||
relMouseHidFile *os.File
|
relMouseHidFile *os.File
|
||||||
relMouseLock sync.Mutex
|
relMouseLock sync.Mutex
|
||||||
|
|
||||||
|
keyboardState KeyboardState
|
||||||
|
keyboardStateLock sync.Mutex
|
||||||
|
keyboardStateCtx context.Context
|
||||||
|
keyboardStateCancel context.CancelFunc
|
||||||
|
|
||||||
enabledDevices Devices
|
enabledDevices Devices
|
||||||
|
|
||||||
strictMode bool // only intended for testing for now
|
strictMode bool // only intended for testing for now
|
||||||
|
@ -70,6 +76,8 @@ type UsbGadget struct {
|
||||||
tx *UsbGadgetTransaction
|
tx *UsbGadgetTransaction
|
||||||
txLock sync.Mutex
|
txLock sync.Mutex
|
||||||
|
|
||||||
|
onKeyboardStateChange *func(state KeyboardState)
|
||||||
|
|
||||||
log *zerolog.Logger
|
log *zerolog.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,20 +104,25 @@ func newUsbGadget(name string, configMap map[string]gadgetConfigItem, enabledDev
|
||||||
config = &Config{isEmpty: true}
|
config = &Config{isEmpty: true}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
keyboardCtx, keyboardCancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
g := &UsbGadget{
|
g := &UsbGadget{
|
||||||
name: name,
|
name: name,
|
||||||
kvmGadgetPath: path.Join(gadgetPath, name),
|
kvmGadgetPath: path.Join(gadgetPath, name),
|
||||||
configC1Path: path.Join(gadgetPath, name, "configs/c.1"),
|
configC1Path: path.Join(gadgetPath, name, "configs/c.1"),
|
||||||
configMap: configMap,
|
configMap: configMap,
|
||||||
customConfig: *config,
|
customConfig: *config,
|
||||||
configLock: sync.Mutex{},
|
configLock: sync.Mutex{},
|
||||||
keyboardLock: sync.Mutex{},
|
keyboardLock: sync.Mutex{},
|
||||||
absMouseLock: sync.Mutex{},
|
absMouseLock: sync.Mutex{},
|
||||||
relMouseLock: sync.Mutex{},
|
relMouseLock: sync.Mutex{},
|
||||||
txLock: sync.Mutex{},
|
txLock: sync.Mutex{},
|
||||||
enabledDevices: *enabledDevices,
|
keyboardStateCtx: keyboardCtx,
|
||||||
lastUserInput: time.Now(),
|
keyboardStateCancel: keyboardCancel,
|
||||||
log: logger,
|
keyboardState: KeyboardState{},
|
||||||
|
enabledDevices: *enabledDevices,
|
||||||
|
lastUserInput: time.Now(),
|
||||||
|
log: logger,
|
||||||
|
|
||||||
strictMode: config.strictMode,
|
strictMode: config.strictMode,
|
||||||
|
|
||||||
|
|
|
@ -1017,6 +1017,7 @@ var rpcHandlers = map[string]RPCHandler{
|
||||||
"setNetworkSettings": {Func: rpcSetNetworkSettings, Params: []string{"settings"}},
|
"setNetworkSettings": {Func: rpcSetNetworkSettings, Params: []string{"settings"}},
|
||||||
"renewDHCPLease": {Func: rpcRenewDHCPLease},
|
"renewDHCPLease": {Func: rpcRenewDHCPLease},
|
||||||
"keyboardReport": {Func: rpcKeyboardReport, Params: []string{"modifier", "keys"}},
|
"keyboardReport": {Func: rpcKeyboardReport, Params: []string{"modifier", "keys"}},
|
||||||
|
"getKeyboardLedState": {Func: rpcGetKeyboardLedState},
|
||||||
"absMouseReport": {Func: rpcAbsMouseReport, Params: []string{"x", "y", "buttons"}},
|
"absMouseReport": {Func: rpcAbsMouseReport, Params: []string{"x", "y", "buttons"}},
|
||||||
"relMouseReport": {Func: rpcRelMouseReport, Params: []string{"dx", "dy", "buttons"}},
|
"relMouseReport": {Func: rpcRelMouseReport, Params: []string{"dx", "dy", "buttons"}},
|
||||||
"wheelReport": {Func: rpcWheelReport, Params: []string{"wheelY"}},
|
"wheelReport": {Func: rpcWheelReport, Params: []string{"wheelY"}},
|
||||||
|
|
|
@ -36,9 +36,9 @@ export default function InfoBar() {
|
||||||
console.log(`Error on DataChannel '${rpcDataChannel.label}': ${e}`);
|
console.log(`Error on DataChannel '${rpcDataChannel.label}': ${e}`);
|
||||||
}, [rpcDataChannel]);
|
}, [rpcDataChannel]);
|
||||||
|
|
||||||
const isCapsLockActive = useHidStore(state => state.isCapsLockActive);
|
const keyboardLedState = useHidStore(state => state.keyboardLedState);
|
||||||
const isNumLockActive = useHidStore(state => state.isNumLockActive);
|
const keyboardLedStateSyncAvailable = useHidStore(state => state.keyboardLedStateSyncAvailable);
|
||||||
const isScrollLockActive = useHidStore(state => state.isScrollLockActive);
|
const keyboardLedSync = useSettingsStore(state => state.keyboardLedSync);
|
||||||
|
|
||||||
const isTurnServerInUse = useRTCStore(state => state.isTurnServerInUse);
|
const isTurnServerInUse = useRTCStore(state => state.isTurnServerInUse);
|
||||||
|
|
||||||
|
@ -118,10 +118,24 @@ export default function InfoBar() {
|
||||||
Relayed by Cloudflare
|
Relayed by Cloudflare
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{keyboardLedStateSyncAvailable ? (
|
||||||
|
<div
|
||||||
|
className={cx(
|
||||||
|
"shrink-0 p-1 px-1.5 text-xs",
|
||||||
|
keyboardLedSync !== "browser"
|
||||||
|
? "text-black dark:text-white"
|
||||||
|
: "text-slate-800/20 dark:text-slate-300/20",
|
||||||
|
)}
|
||||||
|
title={"Your keyboard LED state is managed by" + (keyboardLedSync === "browser" ? " the browser" : " the host")}
|
||||||
|
>
|
||||||
|
{keyboardLedSync === "browser" ? "Browser" : "Host"}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
<div
|
<div
|
||||||
className={cx(
|
className={cx(
|
||||||
"shrink-0 p-1 px-1.5 text-xs",
|
"shrink-0 p-1 px-1.5 text-xs",
|
||||||
isCapsLockActive
|
keyboardLedState?.caps_lock
|
||||||
? "text-black dark:text-white"
|
? "text-black dark:text-white"
|
||||||
: "text-slate-800/20 dark:text-slate-300/20",
|
: "text-slate-800/20 dark:text-slate-300/20",
|
||||||
)}
|
)}
|
||||||
|
@ -131,7 +145,7 @@ export default function InfoBar() {
|
||||||
<div
|
<div
|
||||||
className={cx(
|
className={cx(
|
||||||
"shrink-0 p-1 px-1.5 text-xs",
|
"shrink-0 p-1 px-1.5 text-xs",
|
||||||
isNumLockActive
|
keyboardLedState?.num_lock
|
||||||
? "text-black dark:text-white"
|
? "text-black dark:text-white"
|
||||||
: "text-slate-800/20 dark:text-slate-300/20",
|
: "text-slate-800/20 dark:text-slate-300/20",
|
||||||
)}
|
)}
|
||||||
|
@ -141,13 +155,23 @@ export default function InfoBar() {
|
||||||
<div
|
<div
|
||||||
className={cx(
|
className={cx(
|
||||||
"shrink-0 p-1 px-1.5 text-xs",
|
"shrink-0 p-1 px-1.5 text-xs",
|
||||||
isScrollLockActive
|
keyboardLedState?.scroll_lock
|
||||||
? "text-black dark:text-white"
|
? "text-black dark:text-white"
|
||||||
: "text-slate-800/20 dark:text-slate-300/20",
|
: "text-slate-800/20 dark:text-slate-300/20",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
Scroll Lock
|
Scroll Lock
|
||||||
</div>
|
</div>
|
||||||
|
{keyboardLedState?.compose ? (
|
||||||
|
<div className="shrink-0 p-1 px-1.5 text-xs">
|
||||||
|
Compose
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{keyboardLedState?.kana ? (
|
||||||
|
<div className="shrink-0 p-1 px-1.5 text-xs">
|
||||||
|
Kana
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useShallow } from "zustand/react/shallow";
|
||||||
import Keyboard from "react-simple-keyboard";
|
|
||||||
import { ChevronDownIcon } from "@heroicons/react/16/solid";
|
import { ChevronDownIcon } from "@heroicons/react/16/solid";
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
import Keyboard from "react-simple-keyboard";
|
||||||
|
|
||||||
import Card from "@components/Card";
|
import Card from "@components/Card";
|
||||||
// eslint-disable-next-line import/order
|
// eslint-disable-next-line import/order
|
||||||
|
@ -9,12 +10,12 @@ import { Button } from "@components/Button";
|
||||||
|
|
||||||
import "react-simple-keyboard/build/css/index.css";
|
import "react-simple-keyboard/build/css/index.css";
|
||||||
|
|
||||||
import { useHidStore, useUiStore } from "@/hooks/stores";
|
|
||||||
import { cx } from "@/cva.config";
|
|
||||||
import { keys, modifiers, keyDisplayMap } from "@/keyboardMappings";
|
|
||||||
import useKeyboard from "@/hooks/useKeyboard";
|
|
||||||
import DetachIconRaw from "@/assets/detach-icon.svg";
|
|
||||||
import AttachIconRaw from "@/assets/attach-icon.svg";
|
import AttachIconRaw from "@/assets/attach-icon.svg";
|
||||||
|
import DetachIconRaw from "@/assets/detach-icon.svg";
|
||||||
|
import { cx } from "@/cva.config";
|
||||||
|
import { useHidStore, useSettingsStore, useUiStore } from "@/hooks/stores";
|
||||||
|
import useKeyboard from "@/hooks/useKeyboard";
|
||||||
|
import { keyDisplayMap, keys, modifiers } from "@/keyboardMappings";
|
||||||
|
|
||||||
export const DetachIcon = ({ className }: { className?: string }) => {
|
export const DetachIcon = ({ className }: { className?: string }) => {
|
||||||
return <img src={DetachIconRaw} alt="Detach Icon" className={className} />;
|
return <img src={DetachIconRaw} alt="Detach Icon" className={className} />;
|
||||||
|
@ -40,7 +41,17 @@ function KeyboardWrapper() {
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
const [position, setPosition] = useState({ x: 0, y: 0 });
|
const [position, setPosition] = useState({ x: 0, y: 0 });
|
||||||
const [newPosition, setNewPosition] = useState({ x: 0, y: 0 });
|
const [newPosition, setNewPosition] = useState({ x: 0, y: 0 });
|
||||||
const isCapsLockActive = useHidStore(state => state.isCapsLockActive);
|
|
||||||
|
const isCapsLockActive = useHidStore(useShallow(state => state.keyboardLedState?.caps_lock));
|
||||||
|
|
||||||
|
// HID related states
|
||||||
|
const keyboardLedStateSyncAvailable = useHidStore(state => state.keyboardLedStateSyncAvailable);
|
||||||
|
const keyboardLedSync = useSettingsStore(state => state.keyboardLedSync);
|
||||||
|
const isKeyboardLedManagedByHost = useMemo(() =>
|
||||||
|
keyboardLedSync !== "browser" && keyboardLedStateSyncAvailable,
|
||||||
|
[keyboardLedSync, keyboardLedStateSyncAvailable],
|
||||||
|
);
|
||||||
|
|
||||||
const setIsCapsLockActive = useHidStore(state => state.setIsCapsLockActive);
|
const setIsCapsLockActive = useHidStore(state => state.setIsCapsLockActive);
|
||||||
|
|
||||||
const startDrag = useCallback((e: MouseEvent | TouchEvent) => {
|
const startDrag = useCallback((e: MouseEvent | TouchEvent) => {
|
||||||
|
@ -157,14 +168,16 @@ function KeyboardWrapper() {
|
||||||
toggleLayout();
|
toggleLayout();
|
||||||
|
|
||||||
if (isCapsLockActive) {
|
if (isCapsLockActive) {
|
||||||
setIsCapsLockActive(false);
|
if (!isKeyboardLedManagedByHost) {
|
||||||
|
setIsCapsLockActive(false);
|
||||||
|
}
|
||||||
sendKeyboardEvent([keys["CapsLock"]], []);
|
sendKeyboardEvent([keys["CapsLock"]], []);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle caps lock state change
|
// Handle caps lock state change
|
||||||
if (isKeyCaps) {
|
if (isKeyCaps && !isKeyboardLedManagedByHost) {
|
||||||
setIsCapsLockActive(!isCapsLockActive);
|
setIsCapsLockActive(!isCapsLockActive);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -183,7 +196,7 @@ function KeyboardWrapper() {
|
||||||
|
|
||||||
setTimeout(resetKeyboardState, 100);
|
setTimeout(resetKeyboardState, 100);
|
||||||
},
|
},
|
||||||
[isCapsLockActive, sendKeyboardEvent, resetKeyboardState, setIsCapsLockActive],
|
[isCapsLockActive, isKeyboardLedManagedByHost, sendKeyboardEvent, resetKeyboardState, setIsCapsLockActive],
|
||||||
);
|
);
|
||||||
|
|
||||||
const virtualKeyboard = useHidStore(state => state.isVirtualKeyboardEnabled);
|
const virtualKeyboard = useHidStore(state => state.isVirtualKeyboardEnabled);
|
||||||
|
|
|
@ -47,6 +47,18 @@ export default function WebRTCVideo() {
|
||||||
clientHeight: videoClientHeight,
|
clientHeight: videoClientHeight,
|
||||||
} = useVideoStore();
|
} = useVideoStore();
|
||||||
|
|
||||||
|
// HID related states
|
||||||
|
const keyboardLedStateSyncAvailable = useHidStore(state => state.keyboardLedStateSyncAvailable);
|
||||||
|
const keyboardLedSync = useSettingsStore(state => state.keyboardLedSync);
|
||||||
|
const isKeyboardLedManagedByHost = useMemo(() =>
|
||||||
|
keyboardLedSync !== "browser" && keyboardLedStateSyncAvailable,
|
||||||
|
[keyboardLedSync, keyboardLedStateSyncAvailable],
|
||||||
|
);
|
||||||
|
|
||||||
|
const setIsNumLockActive = useHidStore(state => state.setIsNumLockActive);
|
||||||
|
const setIsCapsLockActive = useHidStore(state => state.setIsCapsLockActive);
|
||||||
|
const setIsScrollLockActive = useHidStore(state => state.setIsScrollLockActive);
|
||||||
|
|
||||||
// RTC related states
|
// RTC related states
|
||||||
const peerConnection = useRTCStore(state => state.peerConnection);
|
const peerConnection = useRTCStore(state => state.peerConnection);
|
||||||
|
|
||||||
|
@ -55,10 +67,6 @@ export default function WebRTCVideo() {
|
||||||
const hdmiError = ["no_lock", "no_signal", "out_of_range"].includes(hdmiState);
|
const hdmiError = ["no_lock", "no_signal", "out_of_range"].includes(hdmiState);
|
||||||
const isVideoLoading = !isPlaying;
|
const isVideoLoading = !isPlaying;
|
||||||
|
|
||||||
// Keyboard related states
|
|
||||||
const { setIsNumLockActive, setIsCapsLockActive, setIsScrollLockActive } =
|
|
||||||
useHidStore();
|
|
||||||
|
|
||||||
// Misc states and hooks
|
// Misc states and hooks
|
||||||
const disableVideoFocusTrap = useUiStore(state => state.disableVideoFocusTrap);
|
const disableVideoFocusTrap = useUiStore(state => state.disableVideoFocusTrap);
|
||||||
const [send] = useJsonRpc();
|
const [send] = useJsonRpc();
|
||||||
|
@ -355,9 +363,11 @@ export default function WebRTCVideo() {
|
||||||
|
|
||||||
// console.log(document.activeElement);
|
// console.log(document.activeElement);
|
||||||
|
|
||||||
setIsNumLockActive(e.getModifierState("NumLock"));
|
if (!isKeyboardLedManagedByHost) {
|
||||||
setIsCapsLockActive(e.getModifierState("CapsLock"));
|
setIsNumLockActive(e.getModifierState("NumLock"));
|
||||||
setIsScrollLockActive(e.getModifierState("ScrollLock"));
|
setIsCapsLockActive(e.getModifierState("CapsLock"));
|
||||||
|
setIsScrollLockActive(e.getModifierState("ScrollLock"));
|
||||||
|
}
|
||||||
|
|
||||||
if (code == "IntlBackslash" && ["`", "~"].includes(key)) {
|
if (code == "IntlBackslash" && ["`", "~"].includes(key)) {
|
||||||
code = "Backquote";
|
code = "Backquote";
|
||||||
|
@ -388,11 +398,12 @@ export default function WebRTCVideo() {
|
||||||
sendKeyboardEvent([...new Set(newKeys)], [...new Set(newModifiers)]);
|
sendKeyboardEvent([...new Set(newKeys)], [...new Set(newModifiers)]);
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
|
handleModifierKeys,
|
||||||
|
sendKeyboardEvent,
|
||||||
|
isKeyboardLedManagedByHost,
|
||||||
setIsNumLockActive,
|
setIsNumLockActive,
|
||||||
setIsCapsLockActive,
|
setIsCapsLockActive,
|
||||||
setIsScrollLockActive,
|
setIsScrollLockActive,
|
||||||
handleModifierKeys,
|
|
||||||
sendKeyboardEvent,
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -401,9 +412,11 @@ export default function WebRTCVideo() {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const prev = useHidStore.getState();
|
const prev = useHidStore.getState();
|
||||||
|
|
||||||
setIsNumLockActive(e.getModifierState("NumLock"));
|
if (!isKeyboardLedManagedByHost) {
|
||||||
setIsCapsLockActive(e.getModifierState("CapsLock"));
|
setIsNumLockActive(e.getModifierState("NumLock"));
|
||||||
setIsScrollLockActive(e.getModifierState("ScrollLock"));
|
setIsCapsLockActive(e.getModifierState("CapsLock"));
|
||||||
|
setIsScrollLockActive(e.getModifierState("ScrollLock"));
|
||||||
|
}
|
||||||
|
|
||||||
// Filtering out the key that was just released (keys[e.code])
|
// Filtering out the key that was just released (keys[e.code])
|
||||||
const newKeys = prev.activeKeys.filter(k => k !== keys[e.code]).filter(Boolean);
|
const newKeys = prev.activeKeys.filter(k => k !== keys[e.code]).filter(Boolean);
|
||||||
|
@ -417,11 +430,12 @@ export default function WebRTCVideo() {
|
||||||
sendKeyboardEvent([...new Set(newKeys)], [...new Set(newModifiers)]);
|
sendKeyboardEvent([...new Set(newKeys)], [...new Set(newModifiers)]);
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
|
handleModifierKeys,
|
||||||
|
sendKeyboardEvent,
|
||||||
|
isKeyboardLedManagedByHost,
|
||||||
setIsNumLockActive,
|
setIsNumLockActive,
|
||||||
setIsCapsLockActive,
|
setIsCapsLockActive,
|
||||||
setIsScrollLockActive,
|
setIsScrollLockActive,
|
||||||
handleModifierKeys,
|
|
||||||
sendKeyboardEvent,
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -283,6 +283,8 @@ export const useVideoStore = create<VideoState>(set => ({
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
export type KeyboardLedSync = "auto" | "browser" | "host";
|
||||||
|
|
||||||
interface SettingsState {
|
interface SettingsState {
|
||||||
isCursorHidden: boolean;
|
isCursorHidden: boolean;
|
||||||
setCursorVisibility: (enabled: boolean) => void;
|
setCursorVisibility: (enabled: boolean) => void;
|
||||||
|
@ -305,6 +307,9 @@ interface SettingsState {
|
||||||
|
|
||||||
keyboardLayout: string;
|
keyboardLayout: string;
|
||||||
setKeyboardLayout: (layout: string) => void;
|
setKeyboardLayout: (layout: string) => void;
|
||||||
|
|
||||||
|
keyboardLedSync: KeyboardLedSync;
|
||||||
|
setKeyboardLedSync: (sync: KeyboardLedSync) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useSettingsStore = create(
|
export const useSettingsStore = create(
|
||||||
|
@ -336,6 +341,9 @@ export const useSettingsStore = create(
|
||||||
|
|
||||||
keyboardLayout: "en-US",
|
keyboardLayout: "en-US",
|
||||||
setKeyboardLayout: layout => set({ keyboardLayout: layout }),
|
setKeyboardLayout: layout => set({ keyboardLayout: layout }),
|
||||||
|
|
||||||
|
keyboardLedSync: "auto",
|
||||||
|
setKeyboardLedSync: sync => set({ keyboardLedSync: sync }),
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: "settings",
|
name: "settings",
|
||||||
|
@ -405,6 +413,21 @@ export const useMountMediaStore = create<MountMediaState>(set => ({
|
||||||
setErrorMessage: message => set({ errorMessage: message }),
|
setErrorMessage: message => set({ errorMessage: message }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
export interface KeyboardLedState {
|
||||||
|
num_lock: boolean;
|
||||||
|
caps_lock: boolean;
|
||||||
|
scroll_lock: boolean;
|
||||||
|
compose: boolean;
|
||||||
|
kana: boolean;
|
||||||
|
};
|
||||||
|
const defaultKeyboardLedState: KeyboardLedState = {
|
||||||
|
num_lock: false,
|
||||||
|
caps_lock: false,
|
||||||
|
scroll_lock: false,
|
||||||
|
compose: false,
|
||||||
|
kana: false,
|
||||||
|
};
|
||||||
|
|
||||||
export interface HidState {
|
export interface HidState {
|
||||||
activeKeys: number[];
|
activeKeys: number[];
|
||||||
activeModifiers: number[];
|
activeModifiers: number[];
|
||||||
|
@ -423,18 +446,18 @@ export interface HidState {
|
||||||
altGrCtrlTime: number; // _altGrCtrlTime
|
altGrCtrlTime: number; // _altGrCtrlTime
|
||||||
setAltGrCtrlTime: (time: number) => void;
|
setAltGrCtrlTime: (time: number) => void;
|
||||||
|
|
||||||
isNumLockActive: boolean;
|
keyboardLedState?: KeyboardLedState;
|
||||||
setIsNumLockActive: (enabled: boolean) => void;
|
setKeyboardLedState: (state: KeyboardLedState) => void;
|
||||||
|
setIsNumLockActive: (active: boolean) => void;
|
||||||
|
setIsCapsLockActive: (active: boolean) => void;
|
||||||
|
setIsScrollLockActive: (active: boolean) => void;
|
||||||
|
|
||||||
isScrollLockActive: boolean;
|
keyboardLedStateSyncAvailable: boolean;
|
||||||
setIsScrollLockActive: (enabled: boolean) => void;
|
setKeyboardLedStateSyncAvailable: (available: boolean) => void;
|
||||||
|
|
||||||
isVirtualKeyboardEnabled: boolean;
|
isVirtualKeyboardEnabled: boolean;
|
||||||
setVirtualKeyboardEnabled: (enabled: boolean) => void;
|
setVirtualKeyboardEnabled: (enabled: boolean) => void;
|
||||||
|
|
||||||
isCapsLockActive: boolean;
|
|
||||||
setIsCapsLockActive: (enabled: boolean) => void;
|
|
||||||
|
|
||||||
isPasteModeEnabled: boolean;
|
isPasteModeEnabled: boolean;
|
||||||
setPasteModeEnabled: (enabled: boolean) => void;
|
setPasteModeEnabled: (enabled: boolean) => void;
|
||||||
|
|
||||||
|
@ -442,7 +465,7 @@ export interface HidState {
|
||||||
setUsbState: (state: HidState["usbState"]) => void;
|
setUsbState: (state: HidState["usbState"]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useHidStore = create<HidState>(set => ({
|
export const useHidStore = create<HidState>((set, get) => ({
|
||||||
activeKeys: [],
|
activeKeys: [],
|
||||||
activeModifiers: [],
|
activeModifiers: [],
|
||||||
updateActiveKeysAndModifiers: ({ keys, modifiers }) => {
|
updateActiveKeysAndModifiers: ({ keys, modifiers }) => {
|
||||||
|
@ -458,18 +481,29 @@ export const useHidStore = create<HidState>(set => ({
|
||||||
altGrCtrlTime: 0,
|
altGrCtrlTime: 0,
|
||||||
setAltGrCtrlTime: time => set({ altGrCtrlTime: time }),
|
setAltGrCtrlTime: time => set({ altGrCtrlTime: time }),
|
||||||
|
|
||||||
isNumLockActive: false,
|
setKeyboardLedState: ledState => set({ keyboardLedState: ledState }),
|
||||||
setIsNumLockActive: enabled => set({ isNumLockActive: enabled }),
|
setIsNumLockActive: active => {
|
||||||
|
const keyboardLedState = { ...(get().keyboardLedState || defaultKeyboardLedState) };
|
||||||
|
keyboardLedState.num_lock = active;
|
||||||
|
set({ keyboardLedState });
|
||||||
|
},
|
||||||
|
setIsCapsLockActive: active => {
|
||||||
|
const keyboardLedState = { ...(get().keyboardLedState || defaultKeyboardLedState) };
|
||||||
|
keyboardLedState.caps_lock = active;
|
||||||
|
set({ keyboardLedState });
|
||||||
|
},
|
||||||
|
setIsScrollLockActive: active => {
|
||||||
|
const keyboardLedState = { ...(get().keyboardLedState || defaultKeyboardLedState) };
|
||||||
|
keyboardLedState.scroll_lock = active;
|
||||||
|
set({ keyboardLedState });
|
||||||
|
},
|
||||||
|
|
||||||
isScrollLockActive: false,
|
keyboardLedStateSyncAvailable: false,
|
||||||
setIsScrollLockActive: enabled => set({ isScrollLockActive: enabled }),
|
setKeyboardLedStateSyncAvailable: available => set({ keyboardLedStateSyncAvailable: available }),
|
||||||
|
|
||||||
isVirtualKeyboardEnabled: false,
|
isVirtualKeyboardEnabled: false,
|
||||||
setVirtualKeyboardEnabled: enabled => set({ isVirtualKeyboardEnabled: enabled }),
|
setVirtualKeyboardEnabled: enabled => set({ isVirtualKeyboardEnabled: enabled }),
|
||||||
|
|
||||||
isCapsLockActive: false,
|
|
||||||
setIsCapsLockActive: enabled => set({ isCapsLockActive: enabled }),
|
|
||||||
|
|
||||||
isPasteModeEnabled: false,
|
isPasteModeEnabled: false,
|
||||||
setPasteModeEnabled: enabled => set({ isPasteModeEnabled: enabled }),
|
setPasteModeEnabled: enabled => set({ isPasteModeEnabled: enabled }),
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { useCallback, useEffect } from "react";
|
import { useCallback, useEffect } from "react";
|
||||||
|
|
||||||
import { useSettingsStore } from "@/hooks/stores";
|
import { KeyboardLedSync, useSettingsStore } from "@/hooks/stores";
|
||||||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||||
import notifications from "@/notifications";
|
import notifications from "@/notifications";
|
||||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||||
|
@ -12,11 +12,20 @@ import { SettingsItem } from "./devices.$id.settings";
|
||||||
|
|
||||||
export default function SettingsKeyboardRoute() {
|
export default function SettingsKeyboardRoute() {
|
||||||
const keyboardLayout = useSettingsStore(state => state.keyboardLayout);
|
const keyboardLayout = useSettingsStore(state => state.keyboardLayout);
|
||||||
|
const keyboardLedSync = useSettingsStore(state => state.keyboardLedSync);
|
||||||
const setKeyboardLayout = useSettingsStore(
|
const setKeyboardLayout = useSettingsStore(
|
||||||
state => state.setKeyboardLayout,
|
state => state.setKeyboardLayout,
|
||||||
);
|
);
|
||||||
|
const setKeyboardLedSync = useSettingsStore(
|
||||||
|
state => state.setKeyboardLedSync,
|
||||||
|
);
|
||||||
|
|
||||||
const layoutOptions = Object.entries(layouts).map(([code, language]) => { return { value: code, label: language } })
|
const layoutOptions = Object.entries(layouts).map(([code, language]) => { return { value: code, label: language } })
|
||||||
|
const ledSyncOptions = [
|
||||||
|
{ value: "auto", label: "Automatic" },
|
||||||
|
{ value: "browser", label: "Browser Only" },
|
||||||
|
{ value: "host", label: "Host Only" },
|
||||||
|
];
|
||||||
|
|
||||||
const [send] = useJsonRpc();
|
const [send] = useJsonRpc();
|
||||||
|
|
||||||
|
@ -47,7 +56,7 @@ export default function SettingsKeyboardRoute() {
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<SettingsPageHeader
|
<SettingsPageHeader
|
||||||
title="Keyboard"
|
title="Keyboard"
|
||||||
description="Configure keyboard layout settings for your device"
|
description="Configure keyboard settings for your device"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
@ -69,6 +78,23 @@ export default function SettingsKeyboardRoute() {
|
||||||
Pasting text sends individual key strokes to the target device. The keyboard layout determines which key codes are being sent. Ensure that the keyboard layout in JetKVM matches the settings in the operating system.
|
Pasting text sends individual key strokes to the target device. The keyboard layout determines which key codes are being sent. Ensure that the keyboard layout in JetKVM matches the settings in the operating system.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{ /* this menu item could be renamed to plain "Keyboard layout" in the future, when also the virtual keyboard layout mappings are being implemented */ }
|
||||||
|
<SettingsItem
|
||||||
|
title="LED state synchronization"
|
||||||
|
description="Synchronize the LED state of the keyboard with the target device"
|
||||||
|
>
|
||||||
|
<SelectMenuBasic
|
||||||
|
size="SM"
|
||||||
|
label=""
|
||||||
|
fullWidth
|
||||||
|
value={keyboardLedSync}
|
||||||
|
onChange={e => setKeyboardLedSync(e.target.value as KeyboardLedSync)}
|
||||||
|
options={ledSyncOptions}
|
||||||
|
/>
|
||||||
|
</SettingsItem>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -228,7 +228,6 @@ export default function SettingsNetworkRoute() {
|
||||||
size="SM"
|
size="SM"
|
||||||
value={networkState?.mac_address}
|
value={networkState?.mac_address}
|
||||||
error={""}
|
error={""}
|
||||||
disabled={true}
|
|
||||||
readOnly={true}
|
readOnly={true}
|
||||||
className="dark:!text-opacity-60"
|
className="dark:!text-opacity-60"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -19,6 +19,7 @@ import useWebSocket from "react-use-websocket";
|
||||||
import { cx } from "@/cva.config";
|
import { cx } from "@/cva.config";
|
||||||
import {
|
import {
|
||||||
HidState,
|
HidState,
|
||||||
|
KeyboardLedState,
|
||||||
NetworkState,
|
NetworkState,
|
||||||
UpdateState,
|
UpdateState,
|
||||||
useDeviceStore,
|
useDeviceStore,
|
||||||
|
@ -586,6 +587,11 @@ export default function KvmIdRoute() {
|
||||||
const setUsbState = useHidStore(state => state.setUsbState);
|
const setUsbState = useHidStore(state => state.setUsbState);
|
||||||
const setHdmiState = useVideoStore(state => state.setHdmiState);
|
const setHdmiState = useVideoStore(state => state.setHdmiState);
|
||||||
|
|
||||||
|
const keyboardLedState = useHidStore(state => state.keyboardLedState);
|
||||||
|
const setKeyboardLedState = useHidStore(state => state.setKeyboardLedState);
|
||||||
|
|
||||||
|
const setKeyboardLedStateSyncAvailable = useHidStore(state => state.setKeyboardLedStateSyncAvailable);
|
||||||
|
|
||||||
const [hasUpdated, setHasUpdated] = useState(false);
|
const [hasUpdated, setHasUpdated] = useState(false);
|
||||||
const { navigateTo } = useDeviceUiNavigation();
|
const { navigateTo } = useDeviceUiNavigation();
|
||||||
|
|
||||||
|
@ -607,6 +613,13 @@ export default function KvmIdRoute() {
|
||||||
setNetworkState(resp.params as NetworkState);
|
setNetworkState(resp.params as NetworkState);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (resp.method === "keyboardLedState") {
|
||||||
|
const ledState = resp.params as KeyboardLedState;
|
||||||
|
console.log("Setting keyboard led state", ledState);
|
||||||
|
setKeyboardLedState(ledState);
|
||||||
|
setKeyboardLedStateSyncAvailable(true);
|
||||||
|
}
|
||||||
|
|
||||||
if (resp.method === "otaState") {
|
if (resp.method === "otaState") {
|
||||||
const otaState = resp.params as UpdateState["otaState"];
|
const otaState = resp.params as UpdateState["otaState"];
|
||||||
setOtaState(otaState);
|
setOtaState(otaState);
|
||||||
|
@ -643,6 +656,29 @@ export default function KvmIdRoute() {
|
||||||
});
|
});
|
||||||
}, [rpcDataChannel?.readyState, send, setHdmiState]);
|
}, [rpcDataChannel?.readyState, send, setHdmiState]);
|
||||||
|
|
||||||
|
// request keyboard led state from the device
|
||||||
|
useEffect(() => {
|
||||||
|
if (rpcDataChannel?.readyState !== "open") return;
|
||||||
|
if (keyboardLedState !== undefined) return;
|
||||||
|
console.log("Requesting keyboard led state");
|
||||||
|
|
||||||
|
send("getKeyboardLedState", {}, resp => {
|
||||||
|
if ("error" in resp) {
|
||||||
|
// -32601 means the method is not supported
|
||||||
|
if (resp.error.code === -32601) {
|
||||||
|
setKeyboardLedStateSyncAvailable(false);
|
||||||
|
console.error("Failed to get keyboard led state, disabling sync", resp.error);
|
||||||
|
} else {
|
||||||
|
console.error("Failed to get keyboard led state", resp.error);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log("Keyboard led state", resp.result);
|
||||||
|
setKeyboardLedState(resp.result as KeyboardLedState);
|
||||||
|
setKeyboardLedStateSyncAvailable(true);
|
||||||
|
});
|
||||||
|
}, [rpcDataChannel?.readyState, send, setKeyboardLedState, setKeyboardLedStateSyncAvailable, keyboardLedState]);
|
||||||
|
|
||||||
// When the update is successful, we need to refresh the client javascript and show a success modal
|
// When the update is successful, we need to refresh the client javascript and show a success modal
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (queryParams.get("updateSuccess")) {
|
if (queryParams.get("updateSuccess")) {
|
||||||
|
|
15
usb.go
15
usb.go
|
@ -24,6 +24,17 @@ func initUsbGadget() {
|
||||||
time.Sleep(500 * time.Millisecond)
|
time.Sleep(500 * time.Millisecond)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
gadget.SetOnKeyboardStateChange(func(state usbgadget.KeyboardState) {
|
||||||
|
if currentSession != nil {
|
||||||
|
writeJSONRPCEvent("keyboardLedState", state, currentSession)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// open the keyboard hid file to listen for keyboard events
|
||||||
|
if err := gadget.OpenKeyboardHidFile(); err != nil {
|
||||||
|
usbLogger.Error().Err(err).Msg("failed to open keyboard hid file")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func rpcKeyboardReport(modifier uint8, keys []uint8) error {
|
func rpcKeyboardReport(modifier uint8, keys []uint8) error {
|
||||||
|
@ -42,6 +53,10 @@ func rpcWheelReport(wheelY int8) error {
|
||||||
return gadget.AbsMouseWheelReport(wheelY)
|
return gadget.AbsMouseWheelReport(wheelY)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func rpcGetKeyboardLedState() (state usbgadget.KeyboardState) {
|
||||||
|
return gadget.GetKeyboardState()
|
||||||
|
}
|
||||||
|
|
||||||
var usbState = "unknown"
|
var usbState = "unknown"
|
||||||
|
|
||||||
func rpcGetUSBState() (state string) {
|
func rpcGetUSBState() (state string) {
|
||||||
|
|
Loading…
Reference in New Issue