Fix scroll and mouse stability using UI capture and center parking
This commit is contained in:
parent
552b2ca62c
commit
6bdf5fcdbd
307
S3/KVMote_ESP32S3.ino
Normal file
307
S3/KVMote_ESP32S3.ino
Normal file
@ -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 <BLEDevice.h>
|
||||
#include <BLEServer.h>
|
||||
#include <BLEUtils.h>
|
||||
#include <BLE2902.h>
|
||||
#include <Adafruit_NeoPixel.h>
|
||||
|
||||
// ── 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);
|
||||
}
|
||||
4
app.go
4
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)
|
||||
}
|
||||
|
||||
15
frontend/dist/index.html
vendored
15
frontend/dist/index.html
vendored
@ -184,6 +184,11 @@
|
||||
<span x-text="statusText"></span>
|
||||
</div>
|
||||
|
||||
<!-- Área rolável fake para forçar captura de scroll -->
|
||||
<div id="scroll-sink" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; overflow-y: scroll; z-index: -1; pointer-events: none;">
|
||||
<div style="height: 5000px; width: 1px;"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function kvmApp() {
|
||||
return {
|
||||
@ -221,6 +226,16 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Captura o scroll globalmente na janela do KVMote
|
||||
window.addEventListener('wheel', (event) => {
|
||||
// Log no console do navegador para você ver se disparar (inspecionar elemento)
|
||||
console.log("Wheel event:", event.deltaY);
|
||||
|
||||
if (window.go && window.go.main && window.go.main.App) {
|
||||
window.go.main.App.HandleScroll(Math.round(event.deltaY));
|
||||
}
|
||||
}, { passive: false });
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -7,6 +7,8 @@ export function Connect():Promise<string>;
|
||||
|
||||
export function Disconnect():Promise<string>;
|
||||
|
||||
export function HandleScroll(arg1:number):Promise<void>;
|
||||
|
||||
export function SendCtrlAltDel():Promise<void>;
|
||||
|
||||
export function SetPosition(arg1:number):Promise<void>;
|
||||
|
||||
@ -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']();
|
||||
}
|
||||
|
||||
@ -25,4 +25,6 @@ type InputHandler interface {
|
||||
SetCursorPos(x, y int32) bool
|
||||
ShowCursor(show bool)
|
||||
GetScreenResolution() (int32, int32)
|
||||
RequestFocus()
|
||||
SetCursorClip(clip bool)
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
//go:build windows
|
||||
|
||||
package input
|
||||
|
||||
import (
|
||||
@ -24,8 +25,16 @@ var (
|
||||
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() {
|
||||
@ -81,7 +95,7 @@ func (h *windowsInputHandler) Install(ctx context.Context, onMouse func(MouseEve
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
// 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 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))
|
||||
}
|
||||
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,6 +162,8 @@ 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)
|
||||
@ -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()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user