From 6bdf5fcdbd0ea5c03da9de0dd192d74f015f97b3 Mon Sep 17 00:00:00 2001 From: Ricardo Carneiro Date: Fri, 24 Apr 2026 12:45:30 -0300 Subject: [PATCH] Fix scroll and mouse stability using UI capture and center parking --- S3/KVMote_ESP32S3.ino | 307 ++++++++++++++++++ app.go | 4 + frontend/dist/index.html | 15 + frontend/src/wailsjs/wailsjs/go/main/App.d.ts | 2 + frontend/src/wailsjs/wailsjs/go/main/App.js | 4 + internal/input/input.go | 2 + internal/input/input_windows.go | 56 +++- internal/kvm/engine.go | 85 +++-- 8 files changed, 439 insertions(+), 36 deletions(-) create mode 100644 S3/KVMote_ESP32S3.ino diff --git a/S3/KVMote_ESP32S3.ino b/S3/KVMote_ESP32S3.ino new file mode 100644 index 0000000..9f48eb5 --- /dev/null +++ b/S3/KVMote_ESP32S3.ino @@ -0,0 +1,307 @@ +/* + KVMote — ESP32-S3 N16R8 + BLE NUS (Nordic UART Service) → USB HID nativo + + Substitui: Arduino Leonardo + HC-06 + LED RGB externo + LED: WS2812B embutido na placa (GPIO 48 na maioria das DevKit-C1). + Se a sua placa usar outro pino, altere LED_PIN abaixo. + + Protocolo binário: idêntico ao KVMote.ino (Leonardo). + Mouse move → 'M' dx(int8) dy(int8) 3 bytes + Mouse wheel → 'W' delta(int8) 2 bytes + Tecla write → 'K' char 2 bytes + Clique → 'C' 'L'|'R' 2 bytes + Tecla press → 'P' keycode 2 bytes + Tecla release → 'U' keycode 2 bytes + ReleaseAll → 'A' 1 byte + Mouse press → 'D' 'L'|'R' 2 bytes + Mouse release → 'E' 'L'|'R' 2 bytes + LED cliente → 'O' (magenta) + LED host ok → 'H' (azul) + LED sem host → 'G' (verde) + Ping/Pong → '~' → responde [PONG] 1 byte + + Dependências (instale pela Library Manager do Arduino IDE): + - Adafruit NeoPixel (by Adafruit) + Já incluídas no core ESP32: + - USB / USBHIDKeyboard / USBHIDMouse + - BLEDevice / BLEServer / BLE2902 + + Board: "ESP32S3 Dev Module" + USB Mode → Hardware CDC and JTAG ← mantém JTAG para upload via porta COM + USB CDC On Boot → Disabled ← CRÍTICO: libera USB nativo para HID + Upload Mode → Internal USB (ou USB-OTG CDC) + PSRAM → OPI PSRAM (para N16R8) + Flash Size → 16MB + + Conexões: + Porta USB (nativa OTG) → PC cliente (aparece como teclado+mouse HID) + Porta COM (CH343) → PC de desenvolvimento (upload de firmware) + BLE → Host PC (sem fio, KVMote.exe) +*/ + +#include "USB.h" +#include "USBHIDKeyboard.h" +#include "USBHIDMouse.h" +#include +#include +#include +#include +#include + +// ── NUS UUIDs ───────────────────────────────────────────────────────────────── +#define NUS_SERVICE_UUID "6E400001-B5A3-F393-E0A9-E50E24DCCA9E" +#define NUS_RX_UUID "6E400002-B5A3-F393-E0A9-E50E24DCCA9E" // PC escreve aqui +#define NUS_TX_UUID "6E400003-B5A3-F393-E0A9-E50E24DCCA9E" // ESP32 notifica (PONG) + +// ── LED WS2812B embutido ────────────────────────────────────────────────────── +#define LED_PIN 48 // GPIO 48 — ESP32-S3-DevKitC-1; altere se necessário +#define LED_COUNT 1 +#define LED_BRIGHTNESS 80 // 0–255 (80 ≈ 30%, evita ofuscar) + +// ── Objetos USB HID ─────────────────────────────────────────────────────────── +USBHIDKeyboard Keyboard; +USBHIDMouse Mouse; + +// ── NeoPixel ────────────────────────────────────────────────────────────────── +Adafruit_NeoPixel pixel(LED_COUNT, LED_PIN, NEO_GRB + NEO_KHZ800); + +void ledCor(uint8_t r, uint8_t g, uint8_t b) { + pixel.setPixelColor(0, pixel.Color(r, g, b)); + pixel.show(); +} + +// Cor base atual — restaurada após paste batch +uint8_t basR = 0, basG = 255, basB = 0; // verde = aguardando conexão + +// ── BLE ─────────────────────────────────────────────────────────────────────── +BLEServer* pServer = nullptr; +BLECharacteristic* pTxChar = nullptr; +bool bleConn = false; + +// ── Fila BLE → HID (desacopla callback BLE do USB TinyUSB) ─────────────────── +// O callback BLE roda numa task FreeRTOS separada. Chamar Mouse.move() / +// Keyboard.press() de lá bloqueia a task BLE esperando o USB. A fila resolve: +// callback só enfileira bytes, loop() drena e chama o HID. +static QueueHandle_t rxQueue; + +// ── Máquina de estados (idêntica ao Leonardo) ───────────────────────────────── +enum Estado : uint8_t { + AGUARDA_CMD, + AGUARDA_MOUSE_DX, + AGUARDA_MOUSE_DY, + AGUARDA_MOUSE_WHEEL, + AGUARDA_TECLA, + AGUARDA_CLIQUE, + AGUARDA_PRESS_KEY, + AGUARDA_RELEASE_KEY, + AGUARDA_MOUSE_PRESS, + AGUARDA_MOUSE_RELEASE, + AGUARDA_BATCH_LEN1, // recebendo byte alto do tamanho do chunk + AGUARDA_BATCH_LEN2, // recebendo byte baixo do tamanho do chunk + AGUARDA_BATCH_DATA // recebendo bytes de texto → Keyboard.write() +}; + +Estado estado = AGUARDA_CMD; +int8_t pendingDX = 0; +uint16_t batchRemaining = 0; +uint32_t batchLedMs = 0; +bool batchLedOn = false; + +// ── Processa um byte do protocolo ───────────────────────────────────────────── +// Chamada diretamente do callback BLE (task separada do FreeRTOS). +// As funções HID do ESP32 são thread-safe. +void processaByte(uint8_t b) { + switch (estado) { + + case AGUARDA_CMD: + if (b == 'M') estado = AGUARDA_MOUSE_DX; + else if (b == 'W') estado = AGUARDA_MOUSE_WHEEL; + else if (b == 'K') estado = AGUARDA_TECLA; + else if (b == 'C') estado = AGUARDA_CLIQUE; + else if (b == 'P') estado = AGUARDA_PRESS_KEY; + else if (b == 'U') estado = AGUARDA_RELEASE_KEY; + else if (b == 'D') estado = AGUARDA_MOUSE_PRESS; + else if (b == 'E') estado = AGUARDA_MOUSE_RELEASE; + else if (b == 'A') { Keyboard.releaseAll(); } + else if (b == 'O') { basR = 255; basG = 0; basB = 255; ledCor(basR, basG, basB); } + else if (b == 'H') { basR = 0; basG = 0; basB = 255; ledCor(basR, basG, basB); } + else if (b == 'G') { basR = 0; basG = 255; basB = 0; ledCor(basR, basG, basB); } + else if (b == 'T') { estado = AGUARDA_BATCH_LEN1; } + else if (b == '~') { + if (pTxChar && bleConn) { + pTxChar->setValue((uint8_t*)"[PONG]", 6); + pTxChar->notify(); + } + } + break; + + case AGUARDA_MOUSE_DX: + pendingDX = (int8_t)b; + estado = AGUARDA_MOUSE_DY; + break; + + case AGUARDA_MOUSE_DY: + Mouse.move(pendingDX, (int8_t)b, 0); + estado = AGUARDA_CMD; + break; + + case AGUARDA_MOUSE_WHEEL: + Mouse.move(0, 0, (int8_t)b); + estado = AGUARDA_CMD; + break; + + case AGUARDA_TECLA: + Keyboard.write(b); // keycodes >= 0x80 seguem a mesma convenção do Arduino HID + estado = AGUARDA_CMD; + break; + + case AGUARDA_CLIQUE: + if (b == 'L') Mouse.click(MOUSE_LEFT); + if (b == 'R') Mouse.click(MOUSE_RIGHT); + estado = AGUARDA_CMD; + break; + + case AGUARDA_PRESS_KEY: + Keyboard.press(b); + estado = AGUARDA_CMD; + break; + + case AGUARDA_RELEASE_KEY: + Keyboard.release(b); + estado = AGUARDA_CMD; + break; + + case AGUARDA_MOUSE_PRESS: + if (b == 'L') Mouse.press(MOUSE_LEFT); + if (b == 'R') Mouse.press(MOUSE_RIGHT); + estado = AGUARDA_CMD; + break; + + case AGUARDA_MOUSE_RELEASE: + if (b == 'L') Mouse.release(MOUSE_LEFT); + if (b == 'R') Mouse.release(MOUSE_RIGHT); + estado = AGUARDA_CMD; + break; + + // ── Paste batch: T + uint16_be(N) + N bytes → Keyboard.write por byte ──── + // Sem buffer: processa byte a byte enquanto chegam. LED pisca vermelho. + case AGUARDA_BATCH_LEN1: + batchRemaining = (uint16_t)b << 8; + estado = AGUARDA_BATCH_LEN2; + break; + + case AGUARDA_BATCH_LEN2: + batchRemaining |= b; + if (batchRemaining == 0) { estado = AGUARDA_CMD; break; } + batchLedMs = millis(); + batchLedOn = true; + ledCor(220, 0, 0); // vermelho: início do chunk + estado = AGUARDA_BATCH_DATA; + break; + + case AGUARDA_BATCH_DATA: { + uint32_t now = millis(); + if (now - batchLedMs >= 150) { + batchLedMs = now; + batchLedOn = !batchLedOn; + ledCor(batchLedOn ? 220 : 0, 0, 0); + } + Keyboard.write(b); + delay(5); // inter-char delay: evita drop do Shift no USB HID em rajada + if (--batchRemaining == 0) { + ledCor(basR, basG, basB); // restaura cor anterior + estado = AGUARDA_CMD; + } + break; + } + } +} + +// ── Callback: chegada de dados pelo BLE (PC → ESP32) ───────────────────────── +// Apenas enfileira — não chama HID aqui para não bloquear a task BLE. +class RxCallback : public BLECharacteristicCallbacks { + void onWrite(BLECharacteristic* pChar) override { + String val = pChar->getValue(); + for (int i = 0; i < val.length(); i++) { + uint8_t b = (uint8_t)val[i]; + xQueueSend(rxQueue, &b, 0); // não bloqueia se a fila estiver cheia + } + } +}; + +// ── Callbacks de conexão / desconexão BLE ───────────────────────────────────── +class ServerCallbacks : public BLEServerCallbacks { + void onConnect(BLEServer*) override { + bleConn = true; + // LED permanece verde até o host enviar 'H' + } + void onDisconnect(BLEServer*) override { + bleConn = false; + estado = AGUARDA_CMD; + batchRemaining = 0; + Keyboard.releaseAll(); + basR = 0; basG = 255; basB = 0; + ledCor(basR, basG, basB); // verde — aguardando host + BLEDevice::startAdvertising(); // permite reconexão imediata + } +}; + +// ── Setup ───────────────────────────────────────────────────────────────────── +void setup() { + // LED + pixel.begin(); + pixel.setBrightness(LED_BRIGHTNESS); + ledCor(0, 255, 0); // verde — aguardando conexão + + // USB HID (TinyUSB via USB OTG) + USB.productName("KVMote"); + USB.manufacturerName("KVMote"); + USB.begin(); + Keyboard.begin(); + Mouse.begin(); + + // BLE — NUS + BLEDevice::init("KVMote"); + pServer = BLEDevice::createServer(); + pServer->setCallbacks(new ServerCallbacks()); + + BLEService* pService = pServer->createService(NUS_SERVICE_UUID); + + // RX: aceita Write e Write Without Response — sem criptografia (evita AccessDenied no Windows) + BLECharacteristic* pRxChar = pService->createCharacteristic( + NUS_RX_UUID, + BLECharacteristic::PROPERTY_WRITE | BLECharacteristic::PROPERTY_WRITE_NR + ); + pRxChar->setAccessPermissions(ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE); + pRxChar->setCallbacks(new RxCallback()); + + // TX: apenas Notify (para [PONG]) — sem criptografia + pTxChar = pService->createCharacteristic( + NUS_TX_UUID, + BLECharacteristic::PROPERTY_NOTIFY + ); + pTxChar->setAccessPermissions(ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE); + BLE2902* cccd = new BLE2902(); + cccd->setAccessPermissions(ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE); + pTxChar->addDescriptor(cccd); + + pService->start(); + + rxQueue = xQueueCreate(2048, sizeof(uint8_t)); + + BLEAdvertising* pAdv = BLEDevice::getAdvertising(); + pAdv->addServiceUUID(NUS_SERVICE_UUID); + pAdv->setScanResponse(true); + pAdv->setMinPreferred(0x06); // melhora compatibilidade com iOS/Windows + BLEDevice::startAdvertising(); +} + +// ── Loop ────────────────────────────────────────────────────────────────────── +void loop() { + // Drena a fila e processa no contexto do loop (seguro para TinyUSB HID) + uint8_t b; + while (xQueueReceive(rxQueue, &b, 0) == pdTRUE) + processaByte(b); + delay(1); +} diff --git a/app.go b/app.go index c309d5b..351d762 100644 --- a/app.go +++ b/app.go @@ -76,3 +76,7 @@ func (a *App) ChangeLayout(layout int) { func (a *App) SetPosition(pos int) { a.engine.SetPosition(pos) } + +func (a *App) HandleScroll(delta int) { + a.engine.HandleManualScroll(delta) +} diff --git a/frontend/dist/index.html b/frontend/dist/index.html index e97f806..2a75965 100644 --- a/frontend/dist/index.html +++ b/frontend/dist/index.html @@ -184,6 +184,11 @@ + +
+
+
+ diff --git a/frontend/src/wailsjs/wailsjs/go/main/App.d.ts b/frontend/src/wailsjs/wailsjs/go/main/App.d.ts index 518439b..d5b214f 100644 --- a/frontend/src/wailsjs/wailsjs/go/main/App.d.ts +++ b/frontend/src/wailsjs/wailsjs/go/main/App.d.ts @@ -7,6 +7,8 @@ export function Connect():Promise; export function Disconnect():Promise; +export function HandleScroll(arg1:number):Promise; + export function SendCtrlAltDel():Promise; export function SetPosition(arg1:number):Promise; diff --git a/frontend/src/wailsjs/wailsjs/go/main/App.js b/frontend/src/wailsjs/wailsjs/go/main/App.js index aa0160a..31c1e36 100644 --- a/frontend/src/wailsjs/wailsjs/go/main/App.js +++ b/frontend/src/wailsjs/wailsjs/go/main/App.js @@ -14,6 +14,10 @@ export function Disconnect() { return window['go']['main']['App']['Disconnect'](); } +export function HandleScroll(arg1) { + return window['go']['main']['App']['HandleScroll'](arg1); +} + export function SendCtrlAltDel() { return window['go']['main']['App']['SendCtrlAltDel'](); } diff --git a/internal/input/input.go b/internal/input/input.go index 5b8ddca..b0713d7 100644 --- a/internal/input/input.go +++ b/internal/input/input.go @@ -25,4 +25,6 @@ type InputHandler interface { SetCursorPos(x, y int32) bool ShowCursor(show bool) GetScreenResolution() (int32, int32) + RequestFocus() + SetCursorClip(clip bool) } diff --git a/internal/input/input_windows.go b/internal/input/input_windows.go index 3476f79..6b026bb 100644 --- a/internal/input/input_windows.go +++ b/internal/input/input_windows.go @@ -1,4 +1,5 @@ //go:build windows + package input import ( @@ -11,21 +12,29 @@ import ( ) var ( - user32 = windows.NewLazySystemDLL("user32.dll") + user32 = windows.NewLazySystemDLL("user32.dll") kernel32 = windows.NewLazySystemDLL("kernel32.dll") - procSetWindowsHookExW = user32.NewProc("SetWindowsHookExW") - procUnhookWindowsHookEx = user32.NewProc("UnhookWindowsHookEx") - procCallNextHookEx = user32.NewProc("CallNextHookEx") - procGetMessageW = user32.NewProc("GetMessageW") - procSetCursorPos = user32.NewProc("SetCursorPos") - procShowCursor = user32.NewProc("ShowCursor") - procGetModuleHandleW = kernel32.NewProc("GetModuleHandleW") - procGetSystemMetrics = user32.NewProc("GetSystemMetrics") - procSetProcessDPIAware = user32.NewProc("SetProcessDPIAware") - procPostThreadMessageW = user32.NewProc("PostThreadMessageW") + procSetWindowsHookExW = user32.NewProc("SetWindowsHookExW") + procUnhookWindowsHookEx = user32.NewProc("UnhookWindowsHookEx") + procCallNextHookEx = user32.NewProc("CallNextHookEx") + procGetMessageW = user32.NewProc("GetMessageW") + procSetCursorPos = user32.NewProc("SetCursorPos") + procShowCursor = user32.NewProc("ShowCursor") + procGetModuleHandleW = kernel32.NewProc("GetModuleHandleW") + procGetSystemMetrics = user32.NewProc("GetSystemMetrics") + procSetProcessDPIAware = user32.NewProc("SetProcessDPIAware") + procPostThreadMessageW = user32.NewProc("PostThreadMessageW") + procGetActiveWindow = user32.NewProc("GetActiveWindow") + procSetForegroundWindow = user32.NewProc("SetForegroundWindow") + procGetWindowRect = user32.NewProc("GetWindowRect") + procClipCursor = user32.NewProc("ClipCursor") ) +type RECT struct { + Left, Top, Right, Bottom int32 +} + const ( WH_KEYBOARD_LL = 13 WH_MOUSE_LL = 14 @@ -40,7 +49,7 @@ func init() { type MSLLHOOKSTRUCT struct { Pt Point - MouseData uint32 // Mantemos uint32 mas vamos converter no callback + MouseData uint32 Flags uint32 Time uint32 DwExtraInfo uintptr @@ -58,6 +67,7 @@ type windowsInputHandler struct { mouseHook uintptr keyHook uintptr tid uint32 + hwnd uintptr } func NewInputHandler() InputHandler { @@ -65,6 +75,10 @@ func NewInputHandler() InputHandler { } func (h *windowsInputHandler) Install(ctx context.Context, onMouse func(MouseEvent) bool, onKey func(KeyboardEvent) bool) error { + // Captura o HWND da janela ativa (que deve ser o nosso App Wails chamando Install) + hwnd, _, _ := procGetActiveWindow.Call() + h.hwnd = hwnd + ready := make(chan error, 1) go func() { @@ -80,8 +94,8 @@ func (h *windowsInputHandler) Install(ctx context.Context, onMouse func(MouseEve Point: info.Pt, Data: info.MouseData, } - - // Se a engine tratar o evento, não passamos para o próximo hook + + // Se a engine tratar o evento (retornar true), bloqueamos o Windows if onMouse(ev) { return 1 } @@ -160,7 +174,6 @@ func (h *windowsInputHandler) ShowCursor(show bool) { if show { s = 1 } - // Vamos usar uma abordagem mais direta se necessário, mas por enquanto: procShowCursor.Call(uintptr(s)) } @@ -169,3 +182,16 @@ func (h *windowsInputHandler) GetScreenResolution() (int32, int32) { h_res, _, _ := procGetSystemMetrics.Call(SM_CYSCREEN) return int32(w), int32(h_res) } + +func (h *windowsInputHandler) RequestFocus() { + if h.hwnd != 0 { + procSetForegroundWindow.Call(h.hwnd) + } +} + +func (h *windowsInputHandler) SetCursorClip(clip bool) { + // Removido temporariamente para testes + if !clip { + procClipCursor.Call(0) + } +} diff --git a/internal/kvm/engine.go b/internal/kvm/engine.go index 3c44b15..f57e73d 100644 --- a/internal/kvm/engine.go +++ b/internal/kvm/engine.go @@ -86,22 +86,23 @@ func (e *Engine) Start(ctx context.Context) error { return e.inputHandler.Install(ctx, e.onMouse, e.onKey) } -func (e *Engine) processarScroll(data uint32) { +func (e *Engine) processarScroll(msg uint32, data uint32) { e.scrollActive = true e.scrollTimer = time.Now() - delta := int32(int16(data >> 16)) - e.wheelAccum += delta + deltaRaw := int16(data >> 16) - 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)) - } + // LOG TOTAL PARA DESCOBRIR O QUE O TOUCHPAD MANDA + go LogDebug(fmt.Sprintf("SCROLL RAW: msg=0x%X data=0x%X delta=%d", msg, data, deltaRaw)) + + if deltaRaw > 0 { + e.transport.Send([]byte{'P', 0xDA}) // Up + time.Sleep(5 * time.Millisecond) + e.transport.Send([]byte{'U', 0xDA}) + } else if deltaRaw < 0 { + e.transport.Send([]byte{'P', 0xD9}) // Down + time.Sleep(5 * time.Millisecond) + e.transport.Send([]byte{'U', 0xD9}) } } @@ -113,6 +114,11 @@ func (e *Engine) onMouse(ev input.MouseEvent) bool { return false } + // LOG PARA DIAGNÓSTICO: Registrar qualquer mensagem que não seja movimento simples (0x0200) + if ev.Message != 0x0200 { + go LogDebug(fmt.Sprintf("MSG MOUSE: 0x%X | ClientMode: %v", ev.Message, e.clientMode)) + } + if !e.clientMode { if ev.Message == 0x0200 && e.isAtExitEdge(ev.Point) { e.enterClientMode(ev.Point) @@ -123,21 +129,17 @@ func (e *Engine) onMouse(ev input.MouseEvent) bool { // ─── 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) + e.processarScroll(ev.Message, 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 { + // Se estiver scrollando, ignoramos movimentos por um tempo curto (touchpads) + if time.Since(e.scrollTimer) > 250*time.Millisecond { e.scrollActive = false e.virtualX, e.virtualY = 0, 0 } @@ -160,9 +162,11 @@ func (e *Engine) onMouse(ev input.MouseEvent) bool { return true } + // Park no centro para manter o mouse sobre a janela do App + // permitindo a captura do scroll. w, h := e.inputHandler.GetScreenResolution() e.isWarping = true - e.inputHandler.SetCursorPos(w/2, h/2) + 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 { @@ -214,11 +218,20 @@ func (e *Engine) enterClientMode(p input.Point) { e.wheelAccum = 0 e.mouseThrottle = time.Now() + // Foco para receber scroll da interface + e.inputHandler.RequestFocus() + w, h := e.inputHandler.GetScreenResolution() e.isWarping = true + // Park no centro como no início e.inputHandler.SetCursorPos(w/2, h/2) e.lastRawPos = input.Point{X: w / 2, Y: h / 2} - e.inputHandler.ShowCursor(false) + + // Sem esconder cursor para teste de scroll puro + e.inputHandler.ShowCursor(true) + + // 'A' (ReleaseAll) limpa estados presos no firmware, 'O' sinaliza LED Magenta + e.transport.Send([]byte{'A'}) e.transport.Send([]byte{'O'}) } @@ -230,6 +243,7 @@ func (e *Engine) exitClientMode() { w, h := e.inputHandler.GetScreenResolution() var ret input.Point const Offset = 120 + // Retornamos o cursor exatamente para a borda onde ele entrou 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} @@ -242,6 +256,35 @@ func (e *Engine) exitClientMode() { e.transport.Send([]byte{'A'}) } +func (e *Engine) HandleManualScroll(delta int) { + e.mu.Lock() + defer e.mu.Unlock() + + if !e.clientMode || !e.transport.IsConnected() { + return + } + + e.scrollActive = true + e.scrollTimer = time.Now() + + // Acumulamos o delta da UI para não perder movimentos pequenos + e.wheelAccum += int32(-delta) // Invertemos o delta da UI para bater com o padrão HID + + // Divisor menor = Mais sensível + const Divisor = 15 + toSend := e.wheelAccum / Divisor + + if toSend != 0 { + e.wheelAccum -= toSend * Divisor + val := int8(clamp(int(toSend), -127, 127)) + + go func(v int8) { + e.transport.Send([]byte{'W', byte(v)}) + LogDebug(fmt.Sprintf("UI SCROLL -> WHEEL %d (accum remain=%d)", v, e.wheelAccum)) + }(val) + } +} + func (e *Engine) onKey(ev input.KeyboardEvent) bool { e.mu.Lock() defer e.mu.Unlock()