Refactor: Rename NanoKVM to BatchuKVM and update server URL

This commit is contained in:
2025-12-09 20:35:38 +09:00
commit 8cf674c9e5
396 changed files with 54380 additions and 0 deletions

161
server/service/hid/hid.go Normal file
View File

@@ -0,0 +1,161 @@
package hid
import (
"errors"
"os"
"sync"
"time"
log "github.com/sirupsen/logrus"
)
type Hid struct {
g0 *os.File
g1 *os.File
g2 *os.File
kbMutex sync.Mutex
mouseMutex sync.Mutex
}
const (
HID0 = "/dev/hidg0"
HID1 = "/dev/hidg1"
HID2 = "/dev/hidg2"
)
var (
hid *Hid
hidOnce sync.Once
)
func GetHid() *Hid {
hidOnce.Do(func() {
hid = &Hid{}
})
return hid
}
func (h *Hid) Lock() {
h.kbMutex.Lock()
h.mouseMutex.Lock()
}
func (h *Hid) Unlock() {
h.kbMutex.Unlock()
h.mouseMutex.Unlock()
}
func (h *Hid) OpenNoLock() {
var err error
h.CloseNoLock()
h.g0, err = os.OpenFile(HID0, os.O_WRONLY, 0o666)
if err != nil {
log.Errorf("open %s failed: %s", HID0, err)
}
h.g1, err = os.OpenFile(HID1, os.O_WRONLY, 0o666)
if err != nil {
log.Errorf("open %s failed: %s", HID1, err)
}
h.g2, err = os.OpenFile(HID2, os.O_WRONLY, 0o666)
if err != nil {
log.Errorf("open %s failed: %s", HID2, err)
}
}
func (h *Hid) CloseNoLock() {
for _, file := range []*os.File{h.g0, h.g1, h.g2} {
if file != nil {
_ = file.Sync()
_ = file.Close()
}
}
}
func (h *Hid) Open() {
h.kbMutex.Lock()
defer h.kbMutex.Unlock()
h.mouseMutex.Lock()
defer h.mouseMutex.Unlock()
h.CloseNoLock()
h.OpenNoLock()
}
func (h *Hid) Close() {
h.kbMutex.Lock()
defer h.kbMutex.Unlock()
h.mouseMutex.Lock()
defer h.mouseMutex.Unlock()
h.CloseNoLock()
}
func (h *Hid) WriteHid0(data []byte) {
h.kbMutex.Lock()
_, err := h.g0.Write(data)
h.kbMutex.Unlock()
if err != nil {
if errors.Is(err, os.ErrClosed) {
log.Errorf("hid already closed, reopen it...")
h.OpenNoLock()
} else {
log.Debugf("write to %s failed: %s", HID0, err)
}
return
}
log.Debugf("write to %s: %v", HID0, data)
}
func (h *Hid) WriteHid1(data []byte) {
deadline := time.Now().Add(8 * time.Millisecond)
h.mouseMutex.Lock()
_ = h.g1.SetWriteDeadline(deadline)
_, err := h.g1.Write(data)
h.mouseMutex.Unlock()
if err != nil {
switch {
case errors.Is(err, os.ErrClosed):
log.Errorf("hid already closed, reopen it...")
h.OpenNoLock()
case errors.Is(err, os.ErrDeadlineExceeded):
log.Debugf("write to %s timeout", HID1)
default:
log.Errorf("write to %s failed: %s", HID1, err)
}
return
}
log.Debugf("write to %s: %v", HID1, data)
}
func (h *Hid) WriteHid2(data []byte) {
deadline := time.Now().Add(8 * time.Millisecond)
h.mouseMutex.Lock()
_ = h.g2.SetWriteDeadline(deadline)
_, err := h.g2.Write(data)
h.mouseMutex.Unlock()
if err != nil {
switch {
case errors.Is(err, os.ErrClosed):
log.Errorf("hid already closed, reopen it...")
h.OpenNoLock()
case errors.Is(err, os.ErrDeadlineExceeded):
log.Debugf("write to %s timeout", HID2)
default:
log.Errorf("write to %s failed: %s", HID2, err)
}
return
}
log.Debugf("write to %s: %v", HID2, data)
}

View File

@@ -0,0 +1,15 @@
package hid
func (h *Hid) Keyboard(queue <-chan []int) {
for event := range queue {
code := byte(event[0])
var modifier byte = 0x00
if code > 0 {
modifier = byte(event[1]) | byte(event[2]) | byte(event[3]) | byte(event[4])
}
data := []byte{modifier, 0x00, code, 0x00, 0x00, 0x00, 0x00, 0x00}
h.WriteHid0(data)
}
}

View File

@@ -0,0 +1,82 @@
package hid
import (
"encoding/binary"
log "github.com/sirupsen/logrus"
)
const (
MouseUp = iota
MouseDown
MouseMoveAbsolute
MouseMoveRelative
MouseScroll
)
var mouseButtonMap = map[byte]bool{
0x01: true,
0x02: true,
0x04: true,
}
func (h *Hid) Mouse(queue <-chan []int) {
for event := range queue {
switch event[0] {
case MouseDown:
h.mouseDown(event)
case MouseUp:
h.mouseUp()
case MouseMoveAbsolute:
h.mouseMoveAbsolute(event)
case MouseMoveRelative:
h.mouseMoveRelative(event)
case MouseScroll:
h.mouseScroll(event)
default:
log.Debugf("invalid mouse event: %v", event)
}
}
}
func (h *Hid) mouseDown(event []int) {
button := byte(event[1])
if _, ok := mouseButtonMap[button]; !ok {
log.Errorf("invalid mouse button: %v", event)
return
}
data := []byte{button, 0, 0, 0}
h.WriteHid1(data)
}
func (h *Hid) mouseUp() {
data := []byte{0, 0, 0, 0}
h.WriteHid1(data)
}
func (h *Hid) mouseScroll(event []int) {
direction := 0x01
if event[3] > 0 {
direction = -0x1
}
data := []byte{0, 0, 0, byte(direction)}
h.WriteHid1(data)
}
func (h *Hid) mouseMoveAbsolute(event []int) {
x := make([]byte, 2)
y := make([]byte, 2)
binary.LittleEndian.PutUint16(x, uint16(event[2]))
binary.LittleEndian.PutUint16(y, uint16(event[3]))
data := []byte{0, x[0], x[1], y[0], y[1], 0}
h.WriteHid2(data)
}
func (h *Hid) mouseMoveRelative(event []int) {
data := []byte{byte(event[1]), byte(event[2]), byte(event[3]), 0}
h.WriteHid1(data)
}

197
server/service/hid/paste.go Normal file
View File

@@ -0,0 +1,197 @@
package hid
import (
"time"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
"NanoKVM-Server/proto"
)
type Char struct {
Modifiers int
Code int
}
type PasteReq struct {
Content string `form:"content" validate:"required"`
Langue string `form:"langue"`
}
func LangueSwitch(base map[rune]Char, lang string) map[rune]Char {
// wenn kein lang angegeben → Standardmap zurück
if lang == "" {
return base
}
// immer Kopie erstellen
m := copyMap(base)
switch lang {
case "de":
// Y tauschen
m['y'] = Char{0, 29}
m['Y'] = Char{2, 29}
// Z tauschen
m['z'] = Char{0, 28}
m['Z'] = Char{2, 28}
// deutsche Sonderzeichen hinzufügen oder remappen
m['\u00E4'] = Char{0, 52} // ä
m['\u00C4'] = Char{2, 52} // Ä
m['\u00F6'] = Char{0, 51} // ö
m['\u00D6'] = Char{2, 51} // Ö
m['\u00FC'] = Char{0, 47} // ü
m['\u00DC'] = Char{2, 47} // Ü
m['\u00DF'] = Char{0, 45} // ß
//Tauschen
m['^'] = Char{0, 53} // muss doppelt sein
m['/'] = Char{2, 36} // Shift + 7
m['('] = Char{2, 37} // Shift + 8
m['&'] = Char{2, 35} // Shift + 6
m[')'] = Char{2, 38} // Shift + 9
m['`'] = Char{2, 46} // Grave Accent / Backtick
m['"'] = Char{2, 31} // Shift + 2
m['?'] = Char{2, 45} // Shift + ß
m['{'] = Char{0x40, 36} // ALt Gr + 7
m['['] = Char{0x40, 37} // ALt Gr + 8
m[']'] = Char{0x40, 38} // ALt Gr + 6
m['}'] = Char{0x40, 39} // ALt Gr + 0
m['\\'] = Char{0x40, 45} // ALt Gr + ß
m['@'] = Char{0x40, 20} // ALt Gr + q
m['+'] = Char{0, 48} // Shift + +
m['*'] = Char{2, 48} // Shift + +
m['~'] = Char{0x40, 48} // Shift + +
m['#'] = Char{0, 49} // Shift + #
m['\''] = Char{2, 49} // Shift + #
m['<'] = Char{0, 100} // Shift + <
m['>'] = Char{2, 100} // Shift + <
m['|'] = Char{0x40, 100} // ALt Gr + <
m[';'] = Char{2, 54} // Shift + ,
m[':'] = Char{2, 55} // Shift + .
m['-'] = Char{0, 56} // Shift + -
m['_'] = Char{2, 56} // Shift + -
//neu
m['\u00B4'] = Char{0, 46} // ´
m['\u00B0'] = Char{2, 53} // °
m['\u00A7'] = Char{2, 32} // §
m['\u20AC'] = Char{0x40, 8} // €
m['\u00B2'] = Char{0x40, 31} // ²
m['\u00B3'] = Char{0x40, 32} // ³
}
return m
}
func (s *Service) Paste(c *gin.Context) {
var req PasteReq
var rsp proto.Response
if err := proto.ParseFormRequest(c, &req); err != nil {
rsp.ErrRsp(c, -1, "invalid arguments")
return
}
if len(req.Content) > 1024 {
rsp.ErrRsp(c, -2, "content too long")
return
}
charMapLocal := LangueSwitch(charMap, req.Langue)
keyUp := []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
for _, char := range req.Content {
key, ok := charMapLocal[char]
if !ok {
log.Debugf("unknown key '%c' (rune: %d)", char, char)
continue
}
keyDown := []byte{byte(key.Modifiers), 0x00, byte(key.Code), 0x00, 0x00, 0x00, 0x00, 0x00}
hid.WriteHid0(keyDown)
hid.WriteHid0(keyUp)
time.Sleep(30 * time.Millisecond)
}
rsp.OkRsp(c)
log.Debugf("hid paste success, total %d characters processed", len(req.Content))
}
func copyMap(src map[rune]Char) map[rune]Char {
dst := make(map[rune]Char, len(src))
for k, v := range src {
dst[k] = v
}
return dst
}
var charMap = map[rune]Char{
// Lowercase letters
'a': {0, 4}, 'b': {0, 5}, 'c': {0, 6}, 'd': {0, 7}, 'e': {0, 8},
'f': {0, 9}, 'g': {0, 10}, 'h': {0, 11}, 'i': {0, 12}, 'j': {0, 13},
'k': {0, 14}, 'l': {0, 15}, 'm': {0, 16}, 'n': {0, 17}, 'o': {0, 18},
'p': {0, 19}, 'q': {0, 20}, 'r': {0, 21}, 's': {0, 22}, 't': {0, 23},
'u': {0, 24}, 'v': {0, 25}, 'w': {0, 26}, 'x': {0, 27}, 'y': {0, 28},
'z': {0, 29},
// Uppercase letters (Modifier 2 typically means Left Shift)
'A': {2, 4}, 'B': {2, 5}, 'C': {2, 6}, 'D': {2, 7}, 'E': {2, 8},
'F': {2, 9}, 'G': {2, 10}, 'H': {2, 11}, 'I': {2, 12}, 'J': {2, 13},
'K': {2, 14}, 'L': {2, 15}, 'M': {2, 16}, 'N': {2, 17}, 'O': {2, 18},
'P': {2, 19}, 'Q': {2, 20}, 'R': {2, 21}, 'S': {2, 22}, 'T': {2, 23},
'U': {2, 24}, 'V': {2, 25}, 'W': {2, 26}, 'X': {2, 27}, 'Y': {2, 28},
'Z': {2, 29},
// Numbers
'1': {0, 30}, '2': {0, 31}, '3': {0, 32}, '4': {0, 33}, '5': {0, 34},
'6': {0, 35}, '7': {0, 36}, '8': {0, 37}, '9': {0, 38}, '0': {0, 39},
// Shifted numbers / Symbols
'!': {2, 30}, // Shift + 1
'@': {2, 31}, // Shift + 2
'#': {2, 32}, // Shift + 3
'$': {2, 33}, // Shift + 4
'%': {2, 34}, // Shift + 5
'^': {2, 35}, // Shift + 6
'&': {2, 36}, // Shift + 7
'*': {2, 37}, // Shift + 8
'(': {2, 38}, // Shift + 9
')': {2, 39}, // Shift + 0
// Other common characters
'\n': {0, 40}, // Enter (Return)
'\t': {0, 43}, // Tab
' ': {0, 44}, // Space
'-': {0, 45}, // Hyphen / Minus
'=': {0, 46}, // Equals
'[': {0, 47}, // Left Square Bracket
']': {0, 48}, // Right Square Bracket
'\\': {0, 49}, // Backslash
';': {0, 51}, // Semicolon
'\'': {0, 52}, // Apostrophe / Single Quote
'`': {0, 53}, // Grave Accent / Backtick
',': {0, 54}, // Comma
'.': {0, 55}, // Period / Dot
'/': {0, 56}, // Slash
// Shifted symbols
'_': {2, 45}, // Underscore (Shift + Hyphen)
'+': {2, 46}, // Plus (Shift + Equals)
'{': {2, 47}, // Left Curly Brace (Shift + Left Square Bracket)
'}': {2, 48}, // Right Curly Brace (Shift + Right Square Bracket)
'|': {2, 49}, // Pipe (Shift + Backslash)
':': {2, 51}, // Colon (Shift + Semicolon)
'"': {2, 52}, // Double Quote (Shift + Apostrophe)
'~': {2, 53}, // Tilde (Shift + Grave Accent)
'<': {2, 54}, // Less Than (Shift + Comma)
'>': {2, 55}, // Greater Than (Shift + Period)
'?': {2, 56}, // Question Mark (Shift + Slash)
}

View File

@@ -0,0 +1,11 @@
package hid
type Service struct {
hid *Hid
}
func NewService() *Service {
return &Service{
hid: GetHid(),
}
}

View File

@@ -0,0 +1,191 @@
package hid
import (
"NanoKVM-Server/proto"
"errors"
"fmt"
"io"
"os"
"os/exec"
"strings"
"time"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
)
const (
ModeNormal = "normal"
ModeHidOnly = "hid-only"
ModeFlag = "/sys/kernel/config/usb_gadget/g0/bcdDevice"
ModeNormalScript = "/kvmapp/system/init.d/S03usbdev"
ModeHidOnlyScript = "/kvmapp/system/init.d/S03usbhid"
USBDevScript = "/etc/init.d/S03usbdev"
)
var modeMap = map[string]string{
"0x0510": ModeNormal,
"0x0623": ModeHidOnly,
}
func (s *Service) GetHidMode(c *gin.Context) {
var rsp proto.Response
mode, err := getHidMode()
if err != nil {
rsp.ErrRsp(c, -1, "get HID mode failed")
return
}
rsp.OkRspWithData(c, &proto.GetHidModeRsp{
Mode: mode,
})
log.Debugf("get hid mode: %s", mode)
}
func (s *Service) SetHidMode(c *gin.Context) {
var req proto.SetHidModeReq
var rsp proto.Response
if err := proto.ParseFormRequest(c, &req); err != nil {
rsp.ErrRsp(c, -1, "invalid arguments")
return
}
if req.Mode != ModeNormal && req.Mode != ModeHidOnly {
rsp.ErrRsp(c, -2, "invalid arguments")
return
}
if mode, _ := getHidMode(); req.Mode == mode {
rsp.OkRsp(c)
return
}
h := GetHid()
h.Lock()
h.CloseNoLock()
defer func() {
h.OpenNoLock()
h.Unlock()
}()
srcScript := ModeNormalScript
if req.Mode == ModeHidOnly {
srcScript = ModeHidOnlyScript
}
if err := copyModeFile(srcScript); err != nil {
rsp.ErrRsp(c, -3, "operation failed")
return
}
rsp.OkRsp(c)
log.Println("reboot system...")
time.Sleep(500 * time.Millisecond)
_ = exec.Command("reboot").Run()
}
func (s *Service) ResetHid(c *gin.Context) {
var rsp proto.Response
h := GetHid()
h.Lock()
h.CloseNoLock()
defer func() {
h.OpenNoLock()
h.Unlock()
}()
command := fmt.Sprintf("%s restart_phy", USBDevScript)
err := exec.Command("sh", "-c", command).Run()
if err != nil {
log.Errorf("failed to reset hid: %v", err)
rsp.ErrRsp(c, -1, "failed to reset hid")
return
}
rsp.OkRsp(c)
log.Debugf("reset hid success")
}
func copyModeFile(srcScript string) error {
// open the source file
srcFile, err := os.Open(srcScript)
if err != nil {
log.Errorf("failed to open %s: %s", srcScript, err)
return err
}
defer func() {
_ = srcFile.Close()
}()
srcInfo, err := srcFile.Stat()
if err != nil {
log.Errorf("failed to get %s info: %s", srcScript, err)
return err
}
// create and copy to temporary file
tmpFile, err := os.CreateTemp("/etc/init.d/", ".S03usbdev-")
if err != nil {
log.Errorf("failed to create temp %s: %s", USBDevScript, err)
return err
}
tmpPath := tmpFile.Name()
defer func() {
_ = os.Remove(tmpPath)
}()
log.Debugf("create temporary file: %s", tmpPath)
if err := tmpFile.Chmod(srcInfo.Mode()); err != nil {
_ = tmpFile.Close()
log.Errorf("failed to set %s mode: %s", tmpPath, err)
return err
}
if _, err := io.Copy(tmpFile, srcFile); err != nil {
_ = tmpFile.Close()
log.Errorf("failed to copy %s: %s", srcScript, err)
return err
}
if err := tmpFile.Sync(); err != nil {
_ = tmpFile.Close()
log.Errorf("failed to sync %s: %s", tmpPath, err)
return err
}
if err := tmpFile.Close(); err != nil {
log.Errorf("failed to close %s: %s", tmpPath, err)
return err
}
// replace the target file with the temporary file
if err := os.Rename(tmpPath, USBDevScript); err != nil {
log.Errorf("failed to rename %s: %s", tmpPath, err)
return err
}
log.Debugf("copy %s to %s successful", srcScript, USBDevScript)
return nil
}
func getHidMode() (string, error) {
data, err := os.ReadFile(ModeFlag)
if err != nil {
log.Errorf("failed to read %s: %s", ModeFlag, err)
return "", err
}
key := strings.TrimSpace(string(data))
mode, ok := modeMap[key]
if !ok {
log.Errorf("invalid mode flag: %s", key)
return "", errors.New("invalid mode flag")
}
return mode, nil
}