Toby Chui 3 өдөр өмнө
parent
commit
dde4a9904a

BIN
KVMPCB/v2/Gerber_RemdesKVM_RemdesKVM_v2_main_2025-03-30.zip


BIN
KVMPCB/v2/Gerber_RemdesKVM_RemdesKVM_v2_usbext_2025-03-30.zip


BIN
KVMPCB/v2/Schematic_RemdesKVM_2025-03-30.pdf


+ 18 - 0
usbkvm/remdeskd_cursor_only/.vscode/c_cpp_properties.json

@@ -0,0 +1,18 @@
+{
+  "configurations": [
+    {
+      "name": "windows-gcc-x64",
+      "includePath": [
+        "${workspaceFolder}/**"
+      ],
+      "compilerPath": "C:/TDM-GCC-64/bin/gcc.exe",
+      "cStandard": "${default}",
+      "cppStandard": "${default}",
+      "intelliSenseMode": "windows-gcc-x64",
+      "compilerArgs": [
+        ""
+      ]
+    }
+  ],
+  "version": 4
+}

+ 24 - 0
usbkvm/remdeskd_cursor_only/.vscode/launch.json

@@ -0,0 +1,24 @@
+{
+  "version": "0.2.0",
+  "configurations": [
+    {
+      "name": "C/C++ Runner: Debug Session",
+      "type": "cppdbg",
+      "request": "launch",
+      "args": [],
+      "stopAtEntry": false,
+      "externalConsole": true,
+      "cwd": "d:/Invention/RedesKVM/RemdesKVM/usbkvm/usbkvm_fw/src/remdesHid",
+      "program": "d:/Invention/RedesKVM/RemdesKVM/usbkvm/usbkvm_fw/src/remdesHid/build/Debug/outDebug",
+      "MIMode": "gdb",
+      "miDebuggerPath": "gdb",
+      "setupCommands": [
+        {
+          "description": "Enable pretty-printing for gdb",
+          "text": "-enable-pretty-printing",
+          "ignoreFailures": true
+        }
+      ]
+    }
+  ]
+}

+ 59 - 0
usbkvm/remdeskd_cursor_only/.vscode/settings.json

@@ -0,0 +1,59 @@
+{
+  "C_Cpp_Runner.cCompilerPath": "gcc",
+  "C_Cpp_Runner.cppCompilerPath": "g++",
+  "C_Cpp_Runner.debuggerPath": "gdb",
+  "C_Cpp_Runner.cStandard": "",
+  "C_Cpp_Runner.cppStandard": "",
+  "C_Cpp_Runner.msvcBatchPath": "C:/Program Files/Microsoft Visual Studio/2022/Community/VC/Auxiliary/Build/vcvarsall.bat",
+  "C_Cpp_Runner.useMsvc": false,
+  "C_Cpp_Runner.warnings": [
+    "-Wall",
+    "-Wextra",
+    "-Wpedantic",
+    "-Wshadow",
+    "-Wformat=2",
+    "-Wcast-align",
+    "-Wconversion",
+    "-Wsign-conversion",
+    "-Wnull-dereference"
+  ],
+  "C_Cpp_Runner.msvcWarnings": [
+    "/W4",
+    "/permissive-",
+    "/w14242",
+    "/w14287",
+    "/w14296",
+    "/w14311",
+    "/w14826",
+    "/w44062",
+    "/w44242",
+    "/w14905",
+    "/w14906",
+    "/w14263",
+    "/w44265",
+    "/w14928"
+  ],
+  "C_Cpp_Runner.enableWarnings": true,
+  "C_Cpp_Runner.warningsAsError": false,
+  "C_Cpp_Runner.compilerArgs": [],
+  "C_Cpp_Runner.linkerArgs": [],
+  "C_Cpp_Runner.includePaths": [],
+  "C_Cpp_Runner.includeSearch": [
+    "*",
+    "**/*"
+  ],
+  "C_Cpp_Runner.excludeSearch": [
+    "**/build",
+    "**/build/**",
+    "**/.*",
+    "**/.*/**",
+    "**/.vscode",
+    "**/.vscode/**"
+  ],
+  "C_Cpp_Runner.useAddressSanitizer": false,
+  "C_Cpp_Runner.useUndefinedSanitizer": false,
+  "C_Cpp_Runner.useLeakSanitizer": false,
+  "C_Cpp_Runner.showCompilationTime": false,
+  "C_Cpp_Runner.useLinkTimeOptimization": false,
+  "C_Cpp_Runner.msvcSecureNoWarnings": false
+}

+ 11 - 0
usbkvm/remdeskd_cursor_only/go.mod

@@ -0,0 +1,11 @@
+module imuslab.com/remdeskvm/remdeskd
+
+go 1.23.4
+
+require (
+	github.com/gorilla/websocket v1.5.3
+	github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07
+	github.com/vladimirvivien/go4vl v0.0.5
+)
+
+require golang.org/x/sys v0.31.0 // indirect

+ 9 - 0
usbkvm/remdeskd_cursor_only/go.sum

@@ -0,0 +1,9 @@
+github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
+github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07 h1:UyzmZLoiDWMRywV4DUYb9Fbt8uiOSooupjTq10vpvnU=
+github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
+github.com/vladimirvivien/go4vl v0.0.5 h1:jHuo/CZOAzYGzrSMOc7anOMNDr03uWH5c1B5kQ+Chnc=
+github.com/vladimirvivien/go4vl v0.0.5/go.mod h1:FP+/fG/X1DUdbZl9uN+l33vId1QneVn+W80JMc17OL8=
+golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
+golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=

+ 75 - 0
usbkvm/remdeskd_cursor_only/main.go

@@ -0,0 +1,75 @@
+package main
+
+import (
+	"embed"
+	"flag"
+	"io/fs"
+	"log"
+	"net/http"
+	"os"
+	"os/signal"
+	"syscall"
+
+	"imuslab.com/remdeskvm/remdeskd/mod/remdeshid"
+)
+
+const development = true
+
+var (
+	usbKVMDeviceName  = flag.String("usbkvm", "COM4", "USB KVM device file path")
+	usbKVMBaudRate    = flag.Int("baudrate", 115200, "USB KVM baud rate")
+	captureDeviceName = flag.String("capture", "/dev/video0", "Video capture device file path")
+	usbKVM            *remdeshid.Controller
+)
+
+/* Web Server Static Files */
+//go:embed www
+var embeddedFiles embed.FS
+var webfs http.FileSystem
+
+func init() {
+	// Initiate the web server static files
+	if development {
+		webfs = http.Dir("./www")
+	} else {
+		// Embed the ./www folder and trim the prefix
+		subFS, err := fs.Sub(embeddedFiles, "www")
+		if err != nil {
+			log.Fatal(err)
+		}
+		webfs = http.FS(subFS)
+	}
+
+	// Initiate the HID controller
+	usbKVM = remdeshid.NewHIDController(&remdeshid.Config{
+		PortName: *usbKVMDeviceName,
+		BaudRate: *usbKVMBaudRate,
+	})
+
+}
+
+func main() {
+	//Start the HID controller
+	err := usbKVM.Connect()
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	// Handle program exit to close the HID controller
+	c := make(chan os.Signal, 1)
+	signal.Notify(c, os.Interrupt, syscall.SIGTERM)
+	go func() {
+		<-c
+		log.Println("Shutting down...")
+		usbKVM.Close()
+		os.Exit(0)
+	}()
+
+	// Start the web server
+	http.HandleFunc("/hid", usbKVM.HIDWebSocketHandler)
+	http.Handle("/", http.FileServer(webfs))
+	addr := ":9000"
+	log.Printf("Serving on http://localhost%s\n", addr)
+	log.Fatal(http.ListenAndServe(addr, nil))
+
+}

+ 89 - 0
usbkvm/remdeskd_cursor_only/mod/remdeshid/handler.go

@@ -0,0 +1,89 @@
+package remdeshid
+
+import (
+	"encoding/json"
+	"fmt"
+	"log"
+	"net/http"
+	"time"
+
+	"github.com/gorilla/websocket"
+)
+
+type HIDCommand struct {
+	EventType    string `json:"t"`
+	EventSubType string `json:"s"`
+	Data         string `json:"d"`
+	PosX         int    `json:"x"` //Only used for mouse events
+	PosY         int    `json:"y"` //Only used for mouse events
+}
+
+// HIDWebSocketHandler is a handler for the HID WebSocket connection
+var upgrader = websocket.Upgrader{
+	ReadBufferSize:  1024,
+	WriteBufferSize: 1024,
+	CheckOrigin: func(r *http.Request) bool {
+		return true
+	},
+}
+
+func (c *Controller) HIDWebSocketHandler(w http.ResponseWriter, r *http.Request) {
+	conn, err := upgrader.Upgrade(w, r, nil)
+	if err != nil {
+		log.Println("Failed to upgrade to websocket:", err)
+		return
+	}
+	defer conn.Close()
+
+	for {
+		_, message, err := conn.ReadMessage()
+		if err != nil {
+			log.Println("Error reading message:", err)
+			break
+		}
+		log.Printf("Received: %s", message)
+
+		//Try parsing the message as a HIDCommand
+		var hidCmd HIDCommand
+		if err := json.Unmarshal(message, &hidCmd); err != nil {
+			log.Println("Error parsing message:", err)
+			continue
+		}
+
+		//Send the command to the HID controller
+		bytes, err := ConvHIDCommandToBytes(hidCmd)
+		if err != nil {
+			errmsg := map[string]string{"error": err.Error()}
+			if err := conn.WriteJSON(errmsg); err != nil {
+				log.Println("Error writing message:", err)
+			}
+			continue
+		}
+
+		if bytes[0] == OPR_TYPE_MOUSE_SCROLL {
+			currentTime := time.Now().UnixMilli()
+			//Sending scroll too fast will cause the HID controller to glitch
+			if currentTime-c.lastScrollTime < 20 {
+				log.Println("Ignoring scroll event due to rate limiting")
+				continue
+			}
+			c.lastScrollTime = currentTime
+		}
+
+		fmt.Println("Sending bytes:", bytes)
+
+		//Write the bytes to the serial port
+		if err := c.Send(bytes); err != nil {
+			errmsg := map[string]string{"error": err.Error()}
+			if err := conn.WriteJSON(errmsg); err != nil {
+				log.Println("Error writing message:", err)
+			}
+			continue
+		}
+
+		if err := conn.WriteMessage(websocket.TextMessage, []byte("ok")); err != nil {
+			log.Println("Error writing message:", err)
+			continue
+		}
+	}
+}

+ 225 - 0
usbkvm/remdeskd_cursor_only/mod/remdeshid/hidcomm.go

@@ -0,0 +1,225 @@
+package remdeshid
+
+/*
+	hidcomm.go
+
+	This file contains functions to convert HID commands to bytes
+	that can be sent over the USBKVM device
+
+*/
+import (
+	"fmt"
+)
+
+// Append the keyboard event subtypes to the data
+func appendKeyboardEventSubtypes(data []byte, cmd HIDCommand) ([]byte, error) {
+	/* Keyboard Subtypes */
+	if len(cmd.Data) == 1 && cmd.Data[0] >= 32 && cmd.Data[0] <= 127 {
+		//Valid ASCII character
+		if cmd.EventSubType == FRONTEND_SUBTYPE_KEYBOARD_KEY_DOWN {
+			data = append(data, SUBTYPE_KEYBOARD_ASCII_PRESS)
+		} else if cmd.EventSubType == FRONTEND_SUBTYPE_KEYBOARD_KEY_UP {
+			data = append(data, SUBTYPE_KEYBOARD_ASCII_RELEASE)
+		} else {
+			//Key Click
+			data = append(data, SUBTYPE_KEYBOARD_ASCII_WRITE)
+		}
+		data = append(data, cmd.Data[0])
+		return data, nil
+	} else if isFuncKey(cmd.Data) {
+		//Function Key
+		if cmd.EventSubType == FRONTEND_SUBTYPE_KEYBOARD_KEY_DOWN {
+			data = append(data, SUBTYPE_KEYBOARD_FUNCTKEY_PRESS)
+		} else {
+			data = append(data, SUBTYPE_KEYBOARD_FUNCTKEY_RELEASE)
+		}
+		data = append(data, funcKeyToByte(cmd.Data))
+		if data[0] == 0xFF {
+			return nil, fmt.Errorf("invalid function key: %v", cmd.Data)
+		}
+		return data, nil
+	} else if isModifierKey(cmd.Data) {
+		//Modifier Key
+		if cmd.EventSubType == FRONTEND_SUBTYPE_KEYBOARD_KEY_DOWN {
+			data = append(data, SUBTYPE_KEYBOARD_MODIFIER_PRESS)
+		} else {
+			data = append(data, SUBTYPE_KEYBOARD_MODIFIER_RELEASE)
+		}
+		data = append(data, modifierKeyToByte(cmd.Data))
+		if data[0] == 0xFF {
+			return nil, fmt.Errorf("invalid modifier key: %v", cmd.Data)
+		}
+		return data, nil
+	} else if isOtherKeys(cmd.Data) {
+		//Other Keys
+		if cmd.EventSubType == FRONTEND_SUBTYPE_KEYBOARD_KEY_DOWN {
+			data = append(data, SUBTYPE_KEYBOARD_OTHERKEY_PRESS)
+		} else if cmd.EventSubType == FRONTEND_SUBTYPE_KEYBOARD_KEY_UP {
+			data = append(data, SUBTYPE_KEYBOARD_OTHERKEY_RELEASE)
+		} else {
+			return nil, fmt.Errorf("invalid HID command subtype: %v", cmd.Data)
+		}
+		data = append(data, nonAsciiKeysToBytes(cmd.Data)...)
+		return data, nil
+	} else if isNumpadKey(cmd.Data) {
+		//Numpad Keys
+		if cmd.EventSubType == FRONTEND_SUBTYPE_KEYBOARD_KEY_DOWN {
+			data = append(data, SUBTYPE_KEYBOARD_NUMPAD_PRESS)
+		} else if cmd.EventSubType == FRONTEND_SUBTYPE_KEYBOARD_KEY_UP {
+			data = append(data, SUBTYPE_KEYBOARD_NUMPAD_RELEASE)
+		} else {
+			return nil, fmt.Errorf("invalid HID command subtype: %v", cmd.Data)
+		}
+		data = append(data, numpadKeyToByte(string(cmd.Data)))
+		return data, nil
+	} else if cmd.Data == "NumLock" {
+		if cmd.EventSubType == FRONTEND_SUBTYPE_KEYBOARD_KEY_DOWN {
+			data = append(data, SUBTYPE_KEYBOARD_SPECIAL_NUMLOCK)
+			data = append(data, 0x00)
+			return data, nil
+		}
+		return nil, fmt.Errorf("numLock do not support key up")
+	} else if cmd.Data == "Pause" {
+		if cmd.EventSubType == FRONTEND_SUBTYPE_KEYBOARD_KEY_DOWN {
+			data = append(data, SUBTYPE_KEYBOARD_SPECIAL_PAUSE)
+			data = append(data, 0x00)
+			return data, nil
+		}
+
+		return nil, fmt.Errorf("pause do not support key up")
+	} else if cmd.Data == "PrintScreen" {
+		if cmd.EventSubType == FRONTEND_SUBTYPE_KEYBOARD_KEY_UP {
+			data = append(data, SUBTYPE_KEYBOARD_SPECIAL_PRINT_SCREEN)
+			data = append(data, 0x00)
+			return data, nil
+		}
+
+		return nil, fmt.Errorf("printScreen do not support key down")
+	} else if cmd.Data == "ScrollLock" {
+		if cmd.EventSubType == FRONTEND_SUBTYPE_KEYBOARD_KEY_DOWN {
+			data = append(data, SUBTYPE_KEYBOARD_SPECIAL_SCROLL_LOCK)
+			data = append(data, 0x00)
+			return data, nil
+		}
+
+		return nil, fmt.Errorf("scrollLock do not support key up")
+	} else if cmd.Data == "ContextMenu" {
+		//Special Key: ContextMenu
+		//TODO: Implement ContextMenu
+		return nil, fmt.Errorf("ContextMenu not implemented")
+	} else if cmd.Data == "Ctrl+Alt+Del" {
+		//Special Key: Ctrl+Alt+Del
+		data = append(data, SUBTYPE_KEYBOARD_SPECIAL_CTRLALTDEL)
+		data = append(data, 0x00)
+		return data, nil
+	} else {
+		return nil, fmt.Errorf("invalid HID command subtype: %v", cmd.Data)
+	}
+}
+
+// Append the mouse click event subtypes to the data
+func appendMouseClickEventSubtypes(data []byte, cmd HIDCommand) ([]byte, error) {
+	/* Mouse Click Subtypes */
+	isPress := cmd.EventSubType == FRONTEND_MOUSE_PRESS
+	if isPress {
+		//Mouse Button Press
+		data = append(data, 0x02)
+	} else {
+		//Mouse Button Release
+		data = append(data, 0x03)
+	}
+
+	if cmd.Data == FRONTEND_MOUSE_BTN_LEFT {
+		data = append(data, PAYLOAD_MOUSE_BTN_LEFT)
+	} else if cmd.Data == FRONTEND_MOUSE_BTN_MIDDLE {
+		data = append(data, PAYLOAD_MOUSE_BTN_MID)
+	} else if cmd.Data == FRONTEND_MOUSE_BTN_RIGHT {
+		data = append(data, PAYLOAD_MOUSE_BTN_RIGHT)
+	} else {
+		return nil, fmt.Errorf("invalid HID command subtype: %v", cmd.Data)
+	}
+	return data, nil
+}
+
+// Append the mouse move event subtypes to the data
+func appendMouseMoveEventSubtypes(data []byte, cmd HIDCommand) ([]byte, error) {
+	//The mouse move command requires x_pos, y_pos, x_sign, y_sign
+	x_pos := cmd.PosX
+	y_pos := cmd.PosY
+	xIsNegative := x_pos < 0
+	yIsNegative := y_pos < 0
+	x_sign := 0
+	y_sign := 0
+	if xIsNegative {
+		x_pos = -x_pos
+		x_sign = 1
+	}
+
+	if yIsNegative {
+		y_pos = -y_pos
+		y_sign = 1
+	}
+
+	//The max value for x_pos and y_pos are 0xFE, make sure they are within the range
+	if x_pos > 0xFE {
+		x_pos = 0xFE
+	}
+
+	if y_pos > 0xFE {
+		y_pos = 0xFE
+	}
+
+	data = append(data, byte(x_pos), byte(y_pos), byte(x_sign), byte(y_sign))
+	return data, nil
+
+}
+
+// Append the mouse scroll event subtypes to the data
+func appendMouseScrollEventSubtypes(data []byte, cmd HIDCommand) ([]byte, error) {
+	//The mouse scroll command PosY contains the scroll value
+	//The scroll command require a direction byte and a scroll value byte
+	scrollValue := cmd.PosY
+	var sensitivity byte = 0x02
+	if scrollValue < 0 {
+		//Scroll up
+		data = append(data, 0x00)        //Up
+		data = append(data, sensitivity) //Sensitive
+	} else {
+		//Scroll down
+		data = append(data, 0x01)        //Down
+		data = append(data, sensitivity) //Sensitive
+	}
+
+	return data, nil
+}
+
+// Entry function for converting a HIDCommand to bytes that can be sent over the USBKVM device
+func ConvHIDCommandToBytes(cmd HIDCommand) ([]byte, error) {
+	// Convert the HID command to bytes
+	var data []byte
+	if cmd.EventType == FRONTEND_OPR_TYPE_KEYBOARD_WRITE {
+		/* Keyboard Write Event */
+		data = []byte{OPR_TYPE_KEYBOARD_WRITE}
+		return appendKeyboardEventSubtypes(data, cmd)
+	} else if cmd.EventType == FRONTEND_OPR_TYPE_MOUSE_WRITE {
+		/* Mouse Write Event */
+		data = []byte{OPR_TYPE_MOUSE_WRITE}
+		return appendMouseClickEventSubtypes(data, cmd)
+	} else if cmd.EventType == FRONTEND_OPR_TYPE_MOUSE_MOVE {
+		/* Mouse Move Event */
+		data = []byte{OPR_TYPE_MOUSE_MOVE}
+		return appendMouseMoveEventSubtypes(data, cmd)
+	} else if cmd.EventType == FRONTEND_OPR_TYPE_MOUSE_SCROLL {
+		/* Mouse Scroll Event */
+		data = []byte{OPR_TYPE_MOUSE_SCROLL}
+		return appendMouseScrollEventSubtypes(data, cmd)
+	} else if cmd.EventType == FRONT_END_OPR_RESET {
+		/* Reset Event */
+		data = []byte{OPR_TYPE_DATA_RESET, //Reset the data queue
+			OPR_TYPE_KEYBOARD_WRITE, SUBTYPE_KEYBOARD_SPECIAL_RESET, 0x00, //Reset the keyboard press state
+			OPR_TYPE_MOUSE_WRITE, SUBTYPE_MOUSE_RESET, 0x00} //Reset the mouse press state
+		return data, nil
+	}
+
+	return nil, fmt.Errorf("invalid HID command type: %s", cmd.EventType)
+}

+ 311 - 0
usbkvm/remdeskd_cursor_only/mod/remdeshid/hidconv.go

@@ -0,0 +1,311 @@
+package remdeshid
+
+/*
+	hidconv.go
+
+	This file contains functions to convert HID commands to bytes
+	that can be sent over the USBKVM device
+*/
+
+// Operation Types
+const (
+	// Frontend Opr Types
+	FRONTEND_OPR_TYPE_KEYBOARD_WRITE = "kw"
+	FRONTEND_OPR_TYPE_MOUSE_WRITE    = "mw"
+	FRONTEND_OPR_TYPE_MOUSE_MOVE     = "mm"
+	FRONTEND_OPR_TYPE_MOUSE_SCROLL   = "ms"
+
+	// USBKVM Operation Types
+	OPR_TYPE_RESERVED       = 0x00
+	OPR_TYPE_KEYBOARD_WRITE = 0x01
+	OPR_TYPE_MOUSE_WRITE    = 0x02
+	OPR_TYPE_MOUSE_MOVE     = 0x03
+	OPR_TYPE_MOUSE_SCROLL   = 0x04
+	OPR_TYPE_DATA_RESET     = 0xFF
+)
+
+// Operation Sub-types
+const (
+	SUBTYPE_RESERVED = 0x00
+)
+
+// Keyboard Subtypes
+const (
+	// Frontend Keyboard Opr Types
+	FRONTEND_SUBTYPE_KEYBOARD_KEY_DOWN  = "kd"
+	FRONTEND_SUBTYPE_KEYBOARD_KEY_UP    = "ku"
+	FRONTEND_SUBTYPE_KEYBOARD_KEY_CLICK = "kc"
+
+	// USBKVM Keyboard Subtypes
+	SUBTYPE_KEYBOARD_ASCII_WRITE          = 0x01
+	SUBTYPE_KEYBOARD_ASCII_PRESS          = 0x02
+	SUBTYPE_KEYBOARD_ASCII_RELEASE        = 0x03
+	SUBTYPE_KEYBOARD_MODIFIER_PRESS       = 0x04
+	SUBTYPE_KEYBOARD_MODIFIER_RELEASE     = 0x05
+	SUBTYPE_KEYBOARD_FUNCTKEY_PRESS       = 0x06
+	SUBTYPE_KEYBOARD_FUNCTKEY_RELEASE     = 0x07
+	SUBTYPE_KEYBOARD_OTHERKEY_PRESS       = 0x08
+	SUBTYPE_KEYBOARD_OTHERKEY_RELEASE     = 0x09
+	SUBTYPE_KEYBOARD_NUMPAD_PRESS         = 0x0A
+	SUBTYPE_KEYBOARD_NUMPAD_RELEASE       = 0x0B
+	SUBTYPE_KEYBOARD_SPECIAL_PAUSE        = 0xF9
+	SUBTYPE_KEYBOARD_SPECIAL_PRINT_SCREEN = 0xFA
+	SUBTYPE_KEYBOARD_SPECIAL_SCROLL_LOCK  = 0xFB
+	SUBTYPE_KEYBOARD_SPECIAL_NUMLOCK      = 0xFC
+	SUBTYPE_KEYBOARD_SPECIAL_CTRLALTDEL   = 0xFD
+	SUBTYPE_KEYBOARD_SPECIAL_RESET        = 0xFE
+	SUBTYPE_KEYBOARD_SPECIAL_RESERVED     = 0xFF
+
+	// Numpad Buttons IDs
+	PAYLOAD_KEYBOARD_NUMPAD_0       = 0x00
+	PAYLOAD_KEYBOARD_NUMPAD_1       = 0x01
+	PAYLOAD_KEYBOARD_NUMPAD_2       = 0x02
+	PAYLOAD_KEYBOARD_NUMPAD_3       = 0x03
+	PAYLOAD_KEYBOARD_NUMPAD_4       = 0x04
+	PAYLOAD_KEYBOARD_NUMPAD_5       = 0x05
+	PAYLOAD_KEYBOARD_NUMPAD_6       = 0x06
+	PAYLOAD_KEYBOARD_NUMPAD_7       = 0x07
+	PAYLOAD_KEYBOARD_NUMPAD_8       = 0x08
+	PAYLOAD_KEYBOARD_NUMPAD_9       = 0x09
+	PAYLOAD_KEYBOARD_NUMPAD_DOT     = 0x0A
+	PAYLOAD_KEYBOARD_NUMPAD_TIMES   = 0x0B
+	PAYLOAD_KEYBOARD_NUMPAD_DIV     = 0x0C
+	PAYLOAD_KEYBOARD_NUMPAD_PLUS    = 0x0D
+	PAYLOAD_KEYBOARD_NUMPAD_MINUS   = 0x0E
+	PAYLOAD_KEYBOARD_NUMPAD_ENTER   = 0x0F
+	PAYLOAD_KEYBOARD_NUMPAD_NUMLOCK = 0x10
+
+	// Modifier Keys IDs
+	PAYLOAD_KEY_LEFT_CTRL   = 0x00
+	PAYLOAD_KEY_LEFT_SHIFT  = 0x01
+	PAYLOAD_KEY_LEFT_ALT    = 0x02
+	PAYLOAD_KEY_LEFT_GUI    = 0x03
+	PAYLOAD_KEY_RIGHT_CTRL  = 0x04
+	PAYLOAD_KEY_RIGHT_SHIFT = 0x05
+	PAYLOAD_KEY_RIGHT_ALT   = 0x06
+	PAYLOAD_KEY_RIGHT_GUI   = 0x07
+)
+
+const (
+	//Frontend Mouse Opr Types
+	FRONTEND_MOUSE_CLICK   = "mc"
+	FRONTEND_MOUSE_PRESS   = "md"
+	FRONTEND_MOUSE_RELEASE = "mu"
+
+	FRONTEND_MOUSE_BTN_LEFT   = "0"
+	FRONTEND_MOUSE_BTN_MIDDLE = "1"
+	FRONTEND_MOUSE_BTN_RIGHT  = "2"
+
+	// Mouse Subtypes
+	SUBTYPE_MOUSE_CLICK   = 0x01 // Mouse button click
+	SUBTYPE_MOUSE_PRESS   = 0x02 // Mouse button press
+	SUBTYPE_MOUSE_RELEASE = 0x03 // Mouse button release
+	SUBTYPE_MOUSE_SETPOS  = 0x04 // Mouse presets position
+	SUBTYPE_MOUSE_RESET   = 0x05 // Reset all mouse button states
+
+	// Mouse Buttons IDs
+	PAYLOAD_MOUSE_BTN_LEFT  = 0x01
+	PAYLOAD_MOUSE_BTN_RIGHT = 0x02
+	PAYLOAD_MOUSE_BTN_MID   = 0x03
+)
+
+// Control Code
+const (
+	FRONT_END_OPR_RESET = "reset" // Reset all the mouse and keyboard states
+)
+
+// Response Codes
+const (
+	RESP_OK                = 0x00
+	RESP_UNKNOWN_OPR       = 0x01
+	RESP_INVALID_OPR_TYPE  = 0x02
+	RESP_INVALID_KEY_VALUE = 0x03
+	RESP_NOT_IMPLEMENTED   = 0x04
+)
+
+//isModifierKey checks if a key is a modifier key
+func isModifierKey(key string) bool {
+	switch key {
+	case "LEFT_Shift", "RIGHT_Shift", "LEFT_Control", "RIGHT_Control", "LEFT_Alt", "RIGHT_Alt", "Meta", "ContextMenu":
+		return true
+	default:
+		return false
+	}
+}
+
+//Convert modifier key string to byte
+func modifierKeyToByte(key string) byte {
+	switch key {
+	case "LEFT_Shift":
+		return PAYLOAD_KEY_LEFT_SHIFT
+	case "RIGHT_Shift":
+		return PAYLOAD_KEY_RIGHT_SHIFT
+	case "LEFT_Control":
+		return PAYLOAD_KEY_LEFT_CTRL
+	case "RIGHT_Control":
+		return PAYLOAD_KEY_RIGHT_CTRL
+	case "LEFT_Alt":
+		return PAYLOAD_KEY_LEFT_ALT
+	case "RIGHT_Alt":
+		return PAYLOAD_KEY_RIGHT_ALT
+	case "Meta":
+		return PAYLOAD_KEY_LEFT_GUI
+	case "ContextMenu":
+		return PAYLOAD_KEY_RIGHT_GUI
+	default:
+		return 0xFF
+	}
+}
+
+//Is a key a function key
+func isFuncKey(key string) bool {
+	switch key {
+	case "F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12",
+		"F13", "F14", "F15", "F16", "F17", "F18", "F19", "F20", "F21", "F22", "F23", "F24":
+		return true
+	default:
+		return false
+	}
+}
+
+//Convert function key string to byte
+func funcKeyToByte(key string) byte {
+	switch key {
+	case "F1":
+		return 0xC2
+	case "F2":
+		return 0xC3
+	case "F3":
+		return 0xC4
+	case "F4":
+		return 0xC5
+	case "F5":
+		return 0xC6
+	case "F6":
+		return 0xC7
+	case "F7":
+		return 0xC8
+	case "F8":
+		return 0xC9
+	case "F9":
+		return 0xCA
+	case "F10":
+		return 0xCB
+	case "F11":
+		return 0xCC
+	case "F12":
+		return 0xCD
+	case "F13":
+		return 0xF0
+	case "F14":
+		return 0xF1
+	case "F15":
+		return 0xF2
+	case "F16":
+		return 0xF3
+	case "F17":
+		return 0xF4
+	case "F18":
+		return 0xF5
+	case "F19":
+		return 0xF6
+	case "F20":
+		return 0xF7
+	case "F21":
+		return 0xF8
+	case "F22":
+		return 0xF9
+	case "F23":
+		return 0xFA
+	case "F24":
+		return 0xFB
+	default:
+		return 0xFF
+	}
+}
+
+/* Check for other keys */
+func isOtherKeys(key string) bool {
+	return nonAsciiKeysToBytes(key)[0] != 0xFF
+}
+
+func nonAsciiKeysToBytes(key string) []byte {
+	switch key {
+	case "ArrowUp":
+		return []byte{0xDA}
+	case "ArrowDown":
+		return []byte{0xD9}
+	case "ArrowLeft":
+		return []byte{0xD8}
+	case "ArrowRight":
+		return []byte{0xD7}
+	case "Backspace":
+		return []byte{0xB2}
+	case "Tab":
+		return []byte{0xB3}
+	case "Enter":
+		return []byte{0xB0}
+	case "Escape":
+		return []byte{0xB1}
+	case "Insert":
+		return []byte{0xD1}
+	case "Delete":
+		return []byte{0xD4}
+	case "PageUp":
+		return []byte{0xD3}
+	case "PageDown":
+		return []byte{0xD6}
+	case "Home":
+		return []byte{0xD2}
+	case "End":
+		return []byte{0xD5}
+	case "CapsLock":
+		return []byte{0xC1}
+	default:
+		return []byte{0xFF}
+	}
+}
+
+/* Numpad keys */
+func isNumpadKey(key string) bool {
+	return len(key) > 7 && key[:7] == "NUMPAD_"
+}
+
+func numpadKeyToByte(key string) byte {
+	switch key {
+	case "NUMPAD_0":
+		return PAYLOAD_KEYBOARD_NUMPAD_0
+	case "NUMPAD_1":
+		return PAYLOAD_KEYBOARD_NUMPAD_1
+	case "NUMPAD_2":
+		return PAYLOAD_KEYBOARD_NUMPAD_2
+	case "NUMPAD_3":
+		return PAYLOAD_KEYBOARD_NUMPAD_3
+	case "NUMPAD_4":
+		return PAYLOAD_KEYBOARD_NUMPAD_4
+	case "NUMPAD_5":
+		return PAYLOAD_KEYBOARD_NUMPAD_5
+	case "NUMPAD_6":
+		return PAYLOAD_KEYBOARD_NUMPAD_6
+	case "NUMPAD_7":
+		return PAYLOAD_KEYBOARD_NUMPAD_7
+	case "NUMPAD_8":
+		return PAYLOAD_KEYBOARD_NUMPAD_8
+	case "NUMPAD_9":
+		return PAYLOAD_KEYBOARD_NUMPAD_9
+	case "NUMPAD_.":
+		return PAYLOAD_KEYBOARD_NUMPAD_DOT
+	case "NUMPAD_*":
+		return PAYLOAD_KEYBOARD_NUMPAD_TIMES
+	case "NUMPAD_/":
+		return PAYLOAD_KEYBOARD_NUMPAD_DIV
+	case "NUMPAD_+":
+		return PAYLOAD_KEYBOARD_NUMPAD_PLUS
+	case "NUMPAD_-":
+		return PAYLOAD_KEYBOARD_NUMPAD_MINUS
+	case "NUMPAD_Enter":
+		return PAYLOAD_KEYBOARD_NUMPAD_ENTER
+	default:
+		return 0xFF
+	}
+}

+ 150 - 0
usbkvm/remdeskd_cursor_only/mod/remdeshid/remdeshid.go

@@ -0,0 +1,150 @@
+package remdeshid
+
+import (
+	"fmt"
+	"log"
+
+	"github.com/tarm/serial"
+)
+
+type Config struct {
+	/* Bindings and callback */
+	OnWriteError  func(error)         // Callback for when an error occurs while writing to USBKVM device
+	OnReadError   func(error)         // Callback for when an error occurs while reading from USBKVM device
+	OnDataHandler func([]byte, error) // Callback for when data is received from USBKVM device
+
+	/* Serial port configs */
+	PortName string
+	BaudRate int
+}
+
+// Controller is a struct that represents a HID controller
+type Controller struct {
+	Config         *Config
+	serialPort     *serial.Port
+	serialRunning  bool
+	readStopChan   chan bool
+	writeStopChan  chan bool
+	writeQueue     chan []byte
+	lastScrollTime int64
+}
+
+func NewHIDController(config *Config) *Controller {
+	return &Controller{
+		Config:        config,
+		serialRunning: false,
+	}
+}
+
+// Connect opens the serial port and starts reading from it
+func (c *Controller) Connect() error {
+	// Open the serial port
+	config := &serial.Config{
+		Name:   c.Config.PortName,
+		Baud:   c.Config.BaudRate,
+		Size:   8,
+		Parity: serial.ParityNone,
+	}
+
+	port, err := serial.OpenPort(config)
+	if err != nil {
+		return err
+	}
+
+	c.serialPort = port
+	c.readStopChan = make(chan bool)
+	//Start reading from the serial port
+	go func() {
+		buf := make([]byte, 128)
+		for {
+			select {
+			case <-c.readStopChan:
+				return
+			default:
+				n, err := port.Read(buf)
+				if err != nil {
+					if c.Config.OnReadError != nil {
+						c.Config.OnReadError(err)
+					} else {
+						log.Println(err.Error())
+					}
+					c.readStopChan = nil
+					return
+				}
+				if n > 0 {
+					if c.Config.OnDataHandler != nil {
+						c.Config.OnDataHandler(buf[:n], nil)
+					} else {
+						fmt.Print("Received bytes: ")
+						for i := 0; i < n; i++ {
+							fmt.Printf("0x%02X ", buf[i])
+						}
+						fmt.Println()
+					}
+				}
+			}
+		}
+	}()
+
+	//Create a loop to write to the serial port
+	c.writeStopChan = make(chan bool)
+	c.writeQueue = make(chan []byte, 1)
+	c.serialRunning = true
+	go func() {
+		for {
+			select {
+			case data := <-c.writeQueue:
+				_, err := port.Write(data)
+				if err != nil {
+					if c.Config.OnWriteError != nil {
+						c.Config.OnWriteError(err)
+					} else {
+						log.Println(err.Error())
+					}
+				}
+			case <-c.writeStopChan:
+				c.serialRunning = false
+				return
+			}
+		}
+	}()
+
+	//Send over an opr queue reset signal
+	err = c.Send([]byte{OPR_TYPE_DATA_RESET})
+	if err != nil {
+		return err
+	}
+
+	//Reset keyboard press state
+	err = c.Send([]byte{OPR_TYPE_KEYBOARD_WRITE, SUBTYPE_KEYBOARD_SPECIAL_RESET, 0x00})
+	if err != nil {
+		return err
+	}
+
+	//Reset mouse press state
+	err = c.Send([]byte{OPR_TYPE_MOUSE_WRITE, SUBTYPE_MOUSE_RESET, 0x00})
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+func (c *Controller) Send(data []byte) error {
+	if !c.serialRunning {
+		return fmt.Errorf("serial port is not running")
+	}
+	c.writeQueue <- data
+	return nil
+}
+
+func (c *Controller) Close() {
+	if c.readStopChan != nil {
+		c.readStopChan <- true
+	}
+
+	if c.writeStopChan != nil {
+		c.writeStopChan <- true
+	}
+
+	c.serialPort.Close()
+}

+ 251 - 0
usbkvm/remdeskd_cursor_only/mod/usbcapture/usbcapture.go

@@ -0,0 +1,251 @@
+package usbcapture
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"log"
+	"mime/multipart"
+	"net/http"
+	"net/textproto"
+	"os"
+	"strings"
+	"syscall"
+
+	"github.com/vladimirvivien/go4vl/device"
+	"github.com/vladimirvivien/go4vl/v4l2"
+)
+
+type Config struct {
+	DeviceName string
+	Resolution *SizeInfo //The prefered resolution to start the video stream
+}
+
+type Instance struct {
+	Config               *Config
+	SupportedResolutions []FormatInfo //The supported resolutions of the video device
+	Capturing            bool
+	camera               *device.Device
+	frames_buff          <-chan []byte
+	pixfmt               v4l2.FourCCType
+	width                int
+	height               int
+	streamInfo           string
+}
+
+// NewInstance creates a new video capture instance
+func NewInstance(config *Config) (*Instance, error) {
+	if config == nil {
+		return nil, fmt.Errorf("config cannot be nil")
+	}
+
+	//Check if the video device exists
+	if _, err := os.Stat(config.DeviceName); os.IsNotExist(err) {
+		return nil, fmt.Errorf("video device %s does not exist", config.DeviceName)
+	} else if err != nil {
+		return nil, fmt.Errorf("failed to check video device: %w", err)
+	}
+
+	//Check if the device file actualy points to a video device
+	isValidDevice, err := checkVideoCaptureDevice(config.DeviceName)
+	if err != nil {
+		return nil, fmt.Errorf("failed to check video device: %w", err)
+	}
+
+	if !isValidDevice {
+		return nil, fmt.Errorf("device %s is not a video capture device", config.DeviceName)
+	}
+
+	//Get the supported resolutions of the video device
+	formatInfo, err := GetV4L2FormatInfo(config.DeviceName)
+	if err != nil {
+		return nil, fmt.Errorf("failed to get video device format info: %w", err)
+	}
+
+	if len(formatInfo) == 0 {
+		return nil, fmt.Errorf("no supported formats found for device %s", config.DeviceName)
+	}
+
+	return &Instance{
+		Config:               config,
+		Capturing:            false,
+		SupportedResolutions: formatInfo,
+	}, nil
+}
+
+// start http service
+func (i *Instance) ServeVideoStream(w http.ResponseWriter, req *http.Request) {
+	mimeWriter := multipart.NewWriter(w)
+	w.Header().Set("Content-Type", fmt.Sprintf("multipart/x-mixed-replace; boundary=%s", mimeWriter.Boundary()))
+	partHeader := make(textproto.MIMEHeader)
+	partHeader.Add("Content-Type", "image/jpeg")
+
+	var frame []byte
+	for frame = range i.frames_buff {
+		if len(frame) == 0 {
+			log.Print("skipping empty frame")
+			continue
+		}
+
+		partWriter, err := mimeWriter.CreatePart(partHeader)
+		if err != nil {
+			log.Printf("failed to create multi-part writer: %s", err)
+			return
+		}
+
+		if _, err := partWriter.Write(frame); err != nil {
+			if errors.Is(err, syscall.EPIPE) {
+				//broken pipe, the client browser has exited
+				return
+			}
+			log.Printf("failed to write image: %s", err)
+		}
+
+	}
+}
+
+// start video capture
+func (i *Instance) StartVideoCapture(selectedFPS int) error {
+	if i.Capturing {
+		return fmt.Errorf("video capture already started")
+	}
+
+	devName := i.Config.DeviceName
+	frameRate := 25
+	buffSize := 8
+	format := "mjpeg"
+
+	if i.Config.Resolution == nil {
+		return fmt.Errorf("resolution not provided")
+	}
+
+	//Check if the selected FPS is valid in the provided Resolutions
+	if i.Config.Resolution != nil {
+		for _, size := range i.Config.Resolution.FPS {
+			if size == selectedFPS {
+				frameRate = size
+				break
+			}
+		}
+
+		if frameRate != selectedFPS {
+			log.Printf("selected FPS %d is not supported, using default %d", selectedFPS, frameRate)
+		}
+	} else {
+		log.Printf("no resolution provided, using default %d", frameRate)
+	}
+
+	//Check if the video device is a capture device
+	isCaptureDev, err := checkVideoCaptureDevice(devName)
+	if err != nil {
+		return fmt.Errorf("failed to check video device: %w", err)
+	}
+	if !isCaptureDev {
+		return fmt.Errorf("device %s is not a video capture device", devName)
+	}
+
+	//Open the video device
+	camera, err := device.Open(devName,
+		device.WithIOType(v4l2.IOTypeMMAP),
+		device.WithPixFormat(v4l2.PixFormat{
+			PixelFormat: getFormatType(format),
+			Width:       uint32(i.Config.Resolution.Width),
+			Height:      uint32(i.Config.Resolution.Height),
+			Field:       v4l2.FieldAny,
+		}),
+		device.WithFPS(uint32(frameRate)),
+		device.WithBufferSize(uint32(buffSize)),
+	)
+
+	if err != nil {
+		return fmt.Errorf("failed to open video device: %w", err)
+	}
+
+	i.camera = camera
+
+	caps := camera.Capability()
+	log.Printf("device [%s] opened\n", devName)
+	log.Printf("device info: %s", caps.String())
+	//2025/03/16 15:45:25 device info: driver: uvcvideo; card: USB Video: USB Video; bus info: usb-0000:00:14.0-2
+
+	// set device format
+	currFmt, err := camera.GetPixFormat()
+	if err != nil {
+		log.Fatalf("unable to get format: %s", err)
+	}
+	log.Printf("Current format: %s", currFmt)
+	//2025/03/16 15:45:25 Current format: Motion-JPEG [1920x1080]; field=any; bytes per line=0; size image=0; colorspace=Default; YCbCr=Default; Quant=Default; XferFunc=Default
+	i.pixfmt = currFmt.PixelFormat
+	i.width = int(currFmt.Width)
+	i.height = int(currFmt.Height)
+
+	i.streamInfo = fmt.Sprintf("%s - %s [%dx%d] %d fps",
+		caps.Card,
+		v4l2.PixelFormats[currFmt.PixelFormat],
+		currFmt.Width, currFmt.Height, frameRate,
+	)
+
+	// start capture
+	ctx, cancel := context.WithCancel(context.TODO())
+	if err := camera.Start(ctx); err != nil {
+		log.Fatalf("stream capture: %s", err)
+	}
+	defer func() {
+		cancel()
+		camera.Close()
+	}()
+
+	// video stream
+	i.frames_buff = camera.GetOutput()
+
+	log.Printf("device capture started (buffer size set %d)", camera.BufferCount())
+	i.Capturing = true
+	return nil
+}
+
+// GetStreamInfo returns the stream information string
+func (i *Instance) GetStreamInfo() string {
+	return i.streamInfo
+}
+
+// IsCapturing checks if the camera is currently capturing video
+func (i *Instance) IsCapturing() bool {
+	return i.Capturing
+}
+
+// StopCapture stops the video capture and closes the camera device
+func (i *Instance) StopCapture() error {
+	if i.camera != nil {
+		i.camera.Stop()
+		i.camera.Close()
+		i.camera = nil
+	}
+	i.Capturing = false
+	return nil
+}
+
+// Close closes the camera device and releases resources
+func (i *Instance) Close() error {
+	if i.camera != nil {
+		i.StopCapture()
+	}
+	return nil
+}
+
+func getFormatType(fmtStr string) v4l2.FourCCType {
+	switch strings.ToLower(fmtStr) {
+	case "jpeg":
+		return v4l2.PixelFmtJPEG
+	case "mpeg":
+		return v4l2.PixelFmtMPEG
+	case "mjpeg":
+		return v4l2.PixelFmtMJPEG
+	case "h264", "h.264":
+		return v4l2.PixelFmtH264
+	case "yuyv":
+		return v4l2.PixelFmtYUYV
+	case "rgb":
+		return v4l2.PixelFmtRGB24
+	}
+	return v4l2.PixelFmtMPEG
+}

+ 156 - 0
usbkvm/remdeskd_cursor_only/mod/usbcapture/video_device.go

@@ -0,0 +1,156 @@
+package usbcapture
+
+import (
+	"bufio"
+	"bytes"
+	"fmt"
+	"os/exec"
+	"regexp"
+	"strconv"
+	"strings"
+)
+
+/*
+	1920 x 1080 60fps = 55Mbps //Edge not support
+	1920 x 1080 30fps = 50Mbps
+	1920 x 1080 25fps = 40Mbps
+	1920 x 1080 20fps = 30Mbps
+	1920 x 1080 10fps = 15Mbps
+
+	1360 x 768 60fps = 28Mbps
+	1360 x 768 30fps = 25Mbps
+	1360 x 768 25fps = 20Mbps
+	1360 x 768 20fps = 18Mbps
+	1360 x 768 10fps = 10Mbps
+*/
+
+// Struct to store the size and fps info
+type FormatInfo struct {
+	Format string
+	Sizes  []SizeInfo
+}
+
+type SizeInfo struct {
+	Width  int
+	Height int
+	FPS    []int
+}
+
+// CheckVideoCaptureDevice checks if the given video device is a video capture device
+func checkVideoCaptureDevice(device string) (bool, error) {
+	// Run v4l2-ctl to get device capabilities
+	cmd := exec.Command("v4l2-ctl", "--device", device, "--all")
+	output, err := cmd.CombinedOutput()
+	if err != nil {
+		return false, fmt.Errorf("failed to execute v4l2-ctl: %w", err)
+	}
+
+	// Convert output to string and check for the "Video Capture" capability
+	outputStr := string(output)
+	if strings.Contains(outputStr, "Video Capture") {
+		return true, nil
+	}
+	return false, nil
+}
+
+func PrintV4L2FormatInfo(devicePath string) {
+	// Check if the device is a video capture device
+	isCapture, err := checkVideoCaptureDevice(devicePath)
+	if err != nil {
+		fmt.Printf("Error checking device: %v\n", err)
+		return
+	}
+	if !isCapture {
+		fmt.Printf("Device %s is not a video capture device\n", devicePath)
+		return
+	}
+
+	// Get format info
+	formats, err := GetV4L2FormatInfo(devicePath)
+	if err != nil {
+		fmt.Printf("Error getting format info: %v\n", err)
+		return
+	}
+
+	// Print format info
+	for _, format := range formats {
+		fmt.Printf("Format: %s\n", format.Format)
+		for _, size := range format.Sizes {
+			fmt.Printf("  Size: %dx%d\n", size.Width, size.Height)
+			fmt.Printf("    FPS: %v\n", size.FPS)
+		}
+	}
+}
+
+// Function to run the v4l2-ctl command and parse the output
+func GetV4L2FormatInfo(devicePath string) ([]FormatInfo, error) {
+	// Run the v4l2-ctl command to list formats
+	cmd := exec.Command("v4l2-ctl", "--list-formats-ext", "-d", devicePath)
+	var out bytes.Buffer
+	cmd.Stdout = &out
+	err := cmd.Run()
+	if err != nil {
+		return nil, err
+	}
+
+	// Parse the output
+	var formats []FormatInfo
+	var currentFormat *FormatInfo
+	scanner := bufio.NewScanner(&out)
+
+	formatRegex := regexp.MustCompile(`\[(\d+)\]: '(\S+)'`)
+	sizeRegex := regexp.MustCompile(`Size: Discrete (\d+)x(\d+)`)
+	intervalRegex := regexp.MustCompile(`Interval: Discrete (\d+\.\d+)s \((\d+\.\d+) fps\)`)
+
+	for scanner.Scan() {
+		line := scanner.Text()
+
+		// Match format line
+		if matches := formatRegex.FindStringSubmatch(line); matches != nil {
+			if currentFormat != nil {
+				formats = append(formats, *currentFormat)
+			}
+			// Start a new format entry
+			currentFormat = &FormatInfo{
+				Format: matches[2],
+			}
+		}
+
+		// Match size line
+		if matches := sizeRegex.FindStringSubmatch(line); matches != nil {
+			width, _ := strconv.Atoi(matches[1])
+			height, _ := strconv.Atoi(matches[2])
+
+			// Initialize the size entry
+			sizeInfo := SizeInfo{
+				Width:  width,
+				Height: height,
+			}
+
+			// Match FPS intervals for the current size
+			for scanner.Scan() {
+				line = scanner.Text()
+				if fpsMatches := intervalRegex.FindStringSubmatch(line); fpsMatches != nil {
+					fps, _ := strconv.ParseInt(fpsMatches[2], 10, 0)
+					sizeInfo.FPS = append(sizeInfo.FPS, int(fps))
+				} else {
+					// Stop parsing FPS intervals when no more matches are found
+					break
+				}
+			}
+			// Add the size information to the current format
+			currentFormat.Sizes = append(currentFormat.Sizes, sizeInfo)
+		}
+	}
+
+	// Append the last format if present
+	if currentFormat != nil {
+		formats = append(formats, *currentFormat)
+	}
+
+	if err := scanner.Err(); err != nil {
+		return nil, err
+	}
+
+	return formats, nil
+}

+ 187 - 0
usbkvm/remdeskd_cursor_only/www/index.html

@@ -0,0 +1,187 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta name="description" content="A basic Hello World HTML page with metadata and OpenGraph headers">
+    <meta name="author" content="Your Name">
+    <title>Hello World</title>
+    
+    <!-- OpenGraph Metadata -->
+    <meta property="og:title" content="Hello World">
+    <meta property="og:description" content="A basic Hello World HTML page with metadata and OpenGraph headers">
+    <meta property="og:type" content="website">
+    <meta property="og:url" content="http://example.com">
+    <meta property="og:image" content="http://example.com/image.jpg">
+</head>
+<body>
+    <h1>Hello World</h1>
+    <p>Click start to start connection to backend and start Capture to start KVM-ing</p>
+    <button id="startButton">Start</button>
+    <button id="stopButton">Stop</button>
+    <button id="capture">Capture</button>
+    <button id="screenshot">Win + Shift + S</button>
+    <script>
+        let socket;
+        let protocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
+        let port = window.location.port ? window.location.port : (protocol === 'wss' ? 443 : 80);
+        let socketURL = `${protocol}://${window.location.hostname}:${port}/hid`;
+        
+        /* Mouse */
+        document.getElementById('capture').addEventListener('click', function(event) {
+            event.preventDefault();
+            event.stopPropagation();
+
+            // Remove old listeners if they exist
+            document.removeEventListener('mousemove', handleMouseMove);
+            document.removeEventListener('mousedown', handleMouseDown);
+            document.removeEventListener('mouseup', handleMouseUp);
+            document.removeEventListener('wheel', handleScroll);
+
+            // Add new listeners
+            document.body.requestPointerLock();
+            document.addEventListener('mousemove', handleMouseMove);
+            document.addEventListener('mousedown', handleMouseDown);
+            document.addEventListener('mouseup', handleMouseUp);
+            document.addEventListener('wheel', handleScroll);
+
+            if (socket) {
+                // Reset USBKVM state to avoid stuck keys
+                socket.send(JSON.stringify({ t: 'reset'}));
+            }
+        });
+
+        function handleMouseDown(event) {
+            console.log(`Mouse button pressed: Button=${event.button}`);
+            if (socket) {
+                socket.send(JSON.stringify({ t: 'mw', s: 'md', d: event.button+"" }));
+            }
+        }
+
+        function handleMouseUp(event) {
+            console.log(`Mouse button released: Button=${event.button}`);
+            if (socket) {
+                socket.send(JSON.stringify({ t: 'mw', s: 'mu', d: event.button+"" }));
+            }
+        }
+
+        function handleScroll(event) {
+            console.log(`Mouse scrolled: DeltaX=${event.deltaX}, DeltaY=${event.deltaY}`);
+            if (socket) {
+                socket.send(JSON.stringify({ t: 'ms', y: event.deltaY }));
+            }
+        }
+
+        document.addEventListener('pointerlockchange', function() {
+            if (document.pointerLockElement === document.body) {
+                console.log('Pointer locked');
+            } else {
+                console.log('Pointer unlocked');
+                document.removeEventListener('mousemove', handleMouseMove);
+            }
+        });
+
+        function handleMouseMove(event) {
+            console.log(`Mouse moved: X=${event.movementX}, Y=${event.movementY}`);
+            if (socket) {
+                socket.send(JSON.stringify({ t: 'mm', x: event.movementX, y: event.movementY }));
+            }
+        }
+        
+        /* Keyboard */
+        document.getElementById('startButton').addEventListener('click', function() {
+            const socketUrl = socketURL;
+            socket = new WebSocket(socketUrl);
+
+            socket.addEventListener('open', function(event) {
+                console.log('WebSocket is connected.');
+            });
+
+            socket.addEventListener('message', function(event) {
+                console.log('Message from server ', event.data);
+            });
+
+            document.addEventListener('keydown', handleKeyDown);
+            document.addEventListener('keyup', handleKeyUp);
+        });
+
+        document.getElementById('stopButton').addEventListener('click', function() {
+            if (socket) {
+                socket.close();
+                console.log('WebSocket is disconnected.');
+            }
+
+            document.removeEventListener('keydown', handleKeyDown);
+            document.removeEventListener('keyup', handleKeyUp);
+        });
+
+        document.getElementById('screenshot').addEventListener('click', function() {
+            if (socket) {
+                // Send keydown for Shift
+                socket.send(JSON.stringify({ t: 'kw', s: 'kd', d: 'LEFT_Shift' }));
+                // Send keydown for Windows key
+                socket.send(JSON.stringify({ t: 'kw', s: 'kd', d: 'Meta' }));
+                // Send keydown for S
+                socket.send(JSON.stringify({ t: 'kw', s: 'kd', d: 's' }));
+                
+                setTimeout(function() {
+                    // Send keyup for S
+                    socket.send(JSON.stringify({ t: 'kw', s: 'ku', d: 's' }));
+                    // Send keyup for Windows key
+                    socket.send(JSON.stringify({ t: 'kw', s: 'ku', d: 'Meta' }));
+                    // Send keyup for Shift
+                    socket.send(JSON.stringify({ t: 'kw', s: 'ku', d: 'LEFT_Shift' }));
+                }, 1000);
+            }
+        });
+
+        function isNumpadEvent(event) {
+            return event.location === 3;
+        }
+
+        function handleKeyDown(event) {
+            event.preventDefault();
+            event.stopImmediatePropagation();
+            const key = event.key;
+            if (socket){
+                if (key == "Shift" || key == "Control" || key == "Alt"){
+                    if (event.location == 1){
+                        socket.send(JSON.stringify({ t: 'kw', s: 'kd', d: "LEFT_" + key }));
+                    } else {
+                        socket.send(JSON.stringify({ t: 'kw', s: 'kd', d:  "RIGHT_" + key }));
+                    }
+                }else if (isNumpadEvent(event)){
+                    socket.send(JSON.stringify({ t: 'kw', s: 'kd', d: "NUMPAD_" + key }));
+                }else if (key == "PrintScreen"){
+                    //Do nothing, press is hardware offloaded
+                }else{
+                    socket.send(JSON.stringify({ t: 'kw', s: 'kd', d: key }));
+                }
+            }
+        }
+
+        function handleKeyUp(event) {
+            event.preventDefault();
+            event.stopImmediatePropagation();
+            const key = event.key;
+            if (socket) {
+                if (key == "Shift" || key == "Control" || key == "Alt") {
+                    if (event.location == 1) {
+                        socket.send(JSON.stringify({ t: 'kw', s: 'ku', d: "LEFT_" + key }));
+                    } else {
+                        socket.send(JSON.stringify({ t: 'kw', s: 'ku', d: "RIGHT_" + key }));
+                    }
+                }else if (isNumpadEvent(event)){
+                    socket.send(JSON.stringify({ t: 'kw', s: 'ku', d: "NUMPAD_" + key }));
+                }else if (key == "NumLock" || key == "Pause"){
+                   //Do nothing, release is hardware offloaded
+                } else {
+                    socket.send(JSON.stringify({ t: 'kw', s: 'ku', d: key }));
+                }
+            }
+        }
+
+        
+    </script>
+</body>
+</html>

+ 12 - 13
usbkvm/usbkvm_fw/src/remdesHid/USBconstant.c

@@ -165,24 +165,23 @@ __code uint16_t SerialDescriptor[] = {
   'd',
   'e',
   's',
-  'K',
-  'V',
-  'M',
-  ' ',
+  '_',
   'U',
   'S',
   'B',
+  'K',
+  'V',
+  'M',
 };
 __code uint16_t ProductDescriptor[] = {
   // Produce String Descriptor
-  (((10 + 1) * 2) | (DTYPE_String << 8)),
+  (((9 + 1) * 2) | (DTYPE_String << 8)),
   'R',
   'E',
   'M',
   'D',
   'E',
   'S',
-  ' ',
   'K',
   'V',
   'M',
@@ -190,11 +189,11 @@ __code uint16_t ProductDescriptor[] = {
 __code uint16_t ManufacturerDescriptor[] = {
   // SDCC is little endian
   (((7 + 1) * 2) | (DTYPE_String << 8)),
-  'i',
-  'm',
-  'u',
-  's',
-  'l',
-  'a',
-  'b',
+  'I',
+  'M',
+  'U',
+  'S',
+  'L',
+  'A',
+  'B',
 };