341 lines
8.8 KiB
Go
341 lines
8.8 KiB
Go
package kvm
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/atotto/clipboard"
|
|
"kvmote/internal/input"
|
|
"kvmote/internal/transport"
|
|
)
|
|
|
|
func LogDebug(msg string) {
|
|
f, err := os.OpenFile("kvmote_debug.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer f.Close()
|
|
timestamp := time.Now().Format("15:04:05.000")
|
|
f.WriteString(fmt.Sprintf("[%s] %s\n", timestamp, msg))
|
|
}
|
|
|
|
type ClientPos int
|
|
|
|
const (
|
|
PosNone ClientPos = iota
|
|
PosLeft
|
|
PosRight
|
|
PosAbove
|
|
PosBelow
|
|
)
|
|
|
|
type ClientLayout int
|
|
|
|
const (
|
|
LayoutUS ClientLayout = iota
|
|
LayoutAbnt2
|
|
LayoutUsIntl
|
|
)
|
|
|
|
type Engine struct {
|
|
mu sync.Mutex
|
|
transport transport.Transport
|
|
inputHandler input.InputHandler
|
|
|
|
clientMode bool
|
|
clientPos ClientPos
|
|
clientLayout ClientLayout
|
|
|
|
ctrlHeld bool
|
|
shiftHeld bool
|
|
altHeld bool
|
|
clipboardReady bool
|
|
|
|
virtualX, virtualY int32
|
|
pendingDX, pendingDY int32
|
|
lastRawPos input.Point
|
|
edgeEntry input.Point
|
|
isWarping bool
|
|
lastModeChange time.Time
|
|
|
|
scrollActive bool
|
|
scrollTimer time.Time
|
|
wheelAccum int32
|
|
|
|
mouseThrottle time.Time
|
|
}
|
|
|
|
func NewEngine(t transport.Transport, h input.InputHandler) *Engine {
|
|
return &Engine{
|
|
transport: t,
|
|
inputHandler: h,
|
|
clientPos: PosRight,
|
|
}
|
|
}
|
|
|
|
func (e *Engine) Transport() transport.Transport {
|
|
return e.transport
|
|
}
|
|
|
|
func (e *Engine) Start(ctx context.Context) error {
|
|
w, h := e.inputHandler.GetScreenResolution()
|
|
LogDebug(fmt.Sprintf("Engine Iniciada. Tela: %dx%d. Pos: %v", w, h, e.clientPos))
|
|
return e.inputHandler.Install(ctx, e.onMouse, e.onKey)
|
|
}
|
|
|
|
func (e *Engine) processarScroll(data uint32) {
|
|
e.scrollActive = true
|
|
e.scrollTimer = time.Now()
|
|
|
|
delta := int32(int16(data >> 16))
|
|
e.wheelAccum += delta
|
|
|
|
const Divisor = 40 // Bem sensível para touchpad
|
|
toSend := e.wheelAccum / Divisor
|
|
|
|
if toSend != 0 {
|
|
e.wheelAccum -= toSend * Divisor
|
|
err := e.transport.Send([]byte{'W', byte(int8(clamp(int(toSend), -127, 127)))})
|
|
if err == nil {
|
|
LogDebug(fmt.Sprintf("SCROLL: delta=%d enviado=%d", delta, toSend))
|
|
}
|
|
}
|
|
}
|
|
|
|
func (e *Engine) onMouse(ev input.MouseEvent) bool {
|
|
e.mu.Lock()
|
|
defer e.mu.Unlock()
|
|
|
|
if !e.transport.IsConnected() {
|
|
return false
|
|
}
|
|
|
|
if !e.clientMode {
|
|
if ev.Message == 0x0200 && e.isAtExitEdge(ev.Point) {
|
|
e.enterClientMode(ev.Point)
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// ─── MODO CLIENTE ATIVO ───
|
|
|
|
// Log de qualquer evento que não seja movimento simples (para descobrir o ID do touchpad)
|
|
if ev.Message != 0x0200 {
|
|
LogDebug(fmt.Sprintf("Evento Mouse: 0x%X | Data: %d", ev.Message, ev.Data))
|
|
}
|
|
|
|
switch ev.Message {
|
|
case 0x020A, 0x020E: // Roda Vertical ou Horizontal
|
|
e.processarScroll(ev.Data)
|
|
return true
|
|
|
|
case 0x0200: // Move
|
|
if e.isWarping { e.isWarping = false; return true }
|
|
|
|
if e.scrollActive {
|
|
if time.Since(e.scrollTimer) > 300*time.Millisecond {
|
|
e.scrollActive = false
|
|
e.virtualX, e.virtualY = 0, 0
|
|
}
|
|
e.lastRawPos = ev.Point
|
|
return true
|
|
}
|
|
|
|
dx, dy := ev.Point.X - e.lastRawPos.X, ev.Point.Y - e.lastRawPos.Y
|
|
e.virtualX += dx
|
|
e.virtualY += dy
|
|
e.pendingDX += dx
|
|
e.pendingDY += dy
|
|
|
|
if e.shouldReturnToHost() {
|
|
if time.Since(e.lastModeChange) > 800*time.Millisecond {
|
|
e.exitClientMode()
|
|
return true
|
|
}
|
|
e.virtualX, e.virtualY = 0, 0
|
|
return true
|
|
}
|
|
|
|
w, h := e.inputHandler.GetScreenResolution()
|
|
e.isWarping = true
|
|
e.inputHandler.SetCursorPos(w/2, h/2)
|
|
e.lastRawPos = input.Point{X: w / 2, Y: h / 2}
|
|
|
|
if time.Since(e.mouseThrottle) >= 40*time.Millisecond {
|
|
e.mouseThrottle = time.Now()
|
|
sdx, sdy := int8(clamp(int(e.pendingDX), -127, 127)), int8(clamp(int(e.pendingDY), -127, 127))
|
|
e.pendingDX, e.pendingDY = 0, 0
|
|
e.transport.SendLossy([]byte{'M', byte(sdx), byte(sdy)})
|
|
}
|
|
return true
|
|
|
|
case 0x0201: e.transport.Send([]byte{'D', 'L'}); return true
|
|
case 0x0202: e.transport.Send([]byte{'E', 'L'}); return true
|
|
case 0x0204: e.transport.Send([]byte{'D', 'R'}); return true
|
|
case 0x0205: e.transport.Send([]byte{'E', 'R'}); return true
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func (e *Engine) isAtExitEdge(p input.Point) bool {
|
|
w, h := e.inputHandler.GetScreenResolution()
|
|
const Margin = 10
|
|
switch e.clientPos {
|
|
case PosLeft: return p.X <= 0
|
|
case PosRight: return p.X >= w-Margin
|
|
case PosAbove: return p.Y <= 0
|
|
case PosBelow: return p.Y >= h-Margin
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (e *Engine) shouldReturnToHost() bool {
|
|
switch e.clientPos {
|
|
case PosLeft: return e.virtualX > 600
|
|
case PosRight: return e.virtualX < -500
|
|
case PosBelow: return e.virtualY < -150
|
|
case PosAbove: return e.virtualY > 150
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (e *Engine) enterClientMode(p input.Point) {
|
|
LogDebug(fmt.Sprintf("Entrando Modo Cliente em (%d, %d)", p.X, p.Y))
|
|
e.clientMode = true
|
|
e.edgeEntry = p
|
|
e.lastModeChange = time.Now()
|
|
e.virtualX, e.virtualY = 0, 0
|
|
e.pendingDX, e.pendingDY = 0, 0
|
|
e.wheelAccum = 0
|
|
e.mouseThrottle = time.Now()
|
|
|
|
w, h := e.inputHandler.GetScreenResolution()
|
|
e.isWarping = true
|
|
e.inputHandler.SetCursorPos(w/2, h/2)
|
|
e.lastRawPos = input.Point{X: w / 2, Y: h / 2}
|
|
e.inputHandler.ShowCursor(false)
|
|
e.transport.Send([]byte{'O'})
|
|
}
|
|
|
|
func (e *Engine) exitClientMode() {
|
|
LogDebug("Saindo Modo Cliente.")
|
|
e.clientMode = false
|
|
e.lastModeChange = time.Now()
|
|
e.inputHandler.ShowCursor(true)
|
|
w, h := e.inputHandler.GetScreenResolution()
|
|
var ret input.Point
|
|
const Offset = 120
|
|
switch e.clientPos {
|
|
case PosRight: ret = input.Point{X: w - Offset, Y: e.edgeEntry.Y}
|
|
case PosLeft: ret = input.Point{X: Offset, Y: e.edgeEntry.Y}
|
|
case PosAbove: ret = input.Point{X: e.edgeEntry.X, Y: Offset}
|
|
case PosBelow: ret = input.Point{X: e.edgeEntry.X, Y: h - Offset}
|
|
default: ret = input.Point{X: w / 2, Y: h / 2}
|
|
}
|
|
e.inputHandler.SetCursorPos(ret.X, ret.Y)
|
|
e.transport.Send([]byte{'H'})
|
|
e.transport.Send([]byte{'A'})
|
|
}
|
|
|
|
func (e *Engine) onKey(ev input.KeyboardEvent) bool {
|
|
e.mu.Lock()
|
|
defer e.mu.Unlock()
|
|
if !e.transport.IsConnected() { return false }
|
|
isDown := ev.Message == 0x0100 || ev.Message == 0x0104
|
|
switch ev.VKCode {
|
|
case 0xA2, 0xA3, 0x11: e.ctrlHeld = isDown
|
|
case 0xA0, 0xA1, 0x10: e.shiftHeld = isDown
|
|
case 0xA4, 0xA5, 0x12: e.altHeld = isDown
|
|
}
|
|
if !e.clientMode {
|
|
if isDown && ev.VKCode == 0x43 && e.ctrlHeld { e.clipboardReady = true }
|
|
return false
|
|
}
|
|
if isDown && ev.VKCode == 0x56 && e.ctrlHeld && e.clipboardReady {
|
|
e.clipboardReady = false
|
|
go e.sendClipboard()
|
|
return true
|
|
}
|
|
code, ok := vkToArduino(ev.VKCode)
|
|
if ok {
|
|
cmd := byte('U'); if isDown { cmd = 'P' }
|
|
e.transport.Send([]byte{cmd, code})
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (e *Engine) sendClipboard() {
|
|
text, _ := clipboard.ReadAll()
|
|
if text == "" { return }
|
|
if len(text) > 2000 { text = text[:2000] }
|
|
data := []byte(text)
|
|
l := len(data)
|
|
e.transport.Send(append([]byte{'T', byte(l >> 8), byte(l & 0xFF)}, data...))
|
|
}
|
|
|
|
func clamp(v, min, max int) int {
|
|
if v < min { return min }; if v > max { return max }; return v
|
|
}
|
|
|
|
func (e *Engine) SendCtrlAltDel() {
|
|
LogDebug("Enviando CTRL+ALT+DEL...")
|
|
if !e.transport.IsConnected() {
|
|
LogDebug("Erro: Transporte não conectado.")
|
|
return
|
|
}
|
|
go func() {
|
|
e.transport.Send([]byte{'P', 0x80})
|
|
time.Sleep(10 * time.Millisecond)
|
|
e.transport.Send([]byte{'P', 0x82})
|
|
time.Sleep(10 * time.Millisecond)
|
|
e.transport.Send([]byte{'P', 0xD4})
|
|
time.Sleep(100 * time.Millisecond)
|
|
e.transport.Send([]byte{'U', 0xD4})
|
|
time.Sleep(10 * time.Millisecond)
|
|
e.transport.Send([]byte{'U', 0x82})
|
|
time.Sleep(10 * time.Millisecond)
|
|
e.transport.Send([]byte{'U', 0x80})
|
|
LogDebug("Sequência CTRL+ALT+DEL enviada.")
|
|
}()
|
|
}
|
|
|
|
func (e *Engine) SetPosition(pos int) {
|
|
e.mu.Lock()
|
|
defer e.mu.Unlock()
|
|
e.clientPos = ClientPos(pos)
|
|
}
|
|
|
|
func (e *Engine) SetLayout(layout int) {
|
|
e.mu.Lock()
|
|
defer e.mu.Unlock()
|
|
e.clientLayout = ClientLayout(layout)
|
|
}
|
|
|
|
var keyMap = map[uint32]byte{
|
|
0xA0: 0x81, 0xA1: 0x85, 0xA2: 0x80, 0xA3: 0x84, 0xA4: 0x82, 0xA5: 0x86, 0x5B: 0x83, 0x5C: 0x87,
|
|
0x10: 0x81, 0x11: 0x80, 0x12: 0x82, 0x70: 0xC2, 0x71: 0xC3, 0x72: 0xC4, 0x73: 0xC5, 0x74: 0xC6,
|
|
0x75: 0xC7, 0x76: 0xC8, 0x77: 0xC9, 0x78: 0xCA, 0x79: 0xCB, 0x7A: 0xCC, 0x7B: 0xCD, 0x26: 0xDA,
|
|
0x28: 0xD9, 0x25: 0xD8, 0x27: 0xD7, 0x24: 0xD2, 0x23: 0xD5, 0x21: 0xD3, 0x22: 0xD6, 0x2D: 0xD1,
|
|
0x2E: 0xD4, 0x0D: 0xB0, 0x1B: 0xB1, 0x08: 0xB2, 0x09: 0xB3, 0x14: 0xC1, 0x2C: 0xCE, 0x91: 0xCF, 0x13: 0xD0,
|
|
}
|
|
|
|
func vkToArduino(vk uint32) (byte, bool) {
|
|
if m, ok := keyMap[vk]; ok { return m, true }
|
|
if vk >= 0x41 && vk <= 0x5A { return byte(vk + 0x20), true }
|
|
if vk >= 0x30 && vk <= 0x39 { return byte(vk), true }
|
|
if vk >= 0x60 && vk <= 0x69 { return byte('0' + vk - 0x60), true }
|
|
switch vk {
|
|
case 0x20: return ' ', true; case 0xBD: return '-', true; case 0xBB: return '=', true
|
|
case 0xDB: return '[', true; case 0xDD: return ']', true; case 0xDC: return '\\', true
|
|
case 0xBA: return ';', true; case 0xDE: return '\'', true; case 0xBC: return ',', true
|
|
case 0xBE: return '.', true; case 0xBF: return '/', true; case 0xC0: return '`', true
|
|
case 0xE2: return 0xEC, true
|
|
}
|
|
return 0, false
|
|
}
|