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) {
|
func (a *App) SetPosition(pos int) {
|
||||||
a.engine.SetPosition(pos)
|
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>
|
<span x-text="statusText"></span>
|
||||||
</div>
|
</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>
|
<script>
|
||||||
function kvmApp() {
|
function kvmApp() {
|
||||||
return {
|
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>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -7,6 +7,8 @@ export function Connect():Promise<string>;
|
|||||||
|
|
||||||
export function Disconnect():Promise<string>;
|
export function Disconnect():Promise<string>;
|
||||||
|
|
||||||
|
export function HandleScroll(arg1:number):Promise<void>;
|
||||||
|
|
||||||
export function SendCtrlAltDel():Promise<void>;
|
export function SendCtrlAltDel():Promise<void>;
|
||||||
|
|
||||||
export function SetPosition(arg1:number):Promise<void>;
|
export function SetPosition(arg1:number):Promise<void>;
|
||||||
|
|||||||
@ -14,6 +14,10 @@ export function Disconnect() {
|
|||||||
return window['go']['main']['App']['Disconnect']();
|
return window['go']['main']['App']['Disconnect']();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function HandleScroll(arg1) {
|
||||||
|
return window['go']['main']['App']['HandleScroll'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
export function SendCtrlAltDel() {
|
export function SendCtrlAltDel() {
|
||||||
return window['go']['main']['App']['SendCtrlAltDel']();
|
return window['go']['main']['App']['SendCtrlAltDel']();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,4 +25,6 @@ type InputHandler interface {
|
|||||||
SetCursorPos(x, y int32) bool
|
SetCursorPos(x, y int32) bool
|
||||||
ShowCursor(show bool)
|
ShowCursor(show bool)
|
||||||
GetScreenResolution() (int32, int32)
|
GetScreenResolution() (int32, int32)
|
||||||
|
RequestFocus()
|
||||||
|
SetCursorClip(clip bool)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
//go:build windows
|
//go:build windows
|
||||||
|
|
||||||
package input
|
package input
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -24,8 +25,16 @@ var (
|
|||||||
procGetSystemMetrics = user32.NewProc("GetSystemMetrics")
|
procGetSystemMetrics = user32.NewProc("GetSystemMetrics")
|
||||||
procSetProcessDPIAware = user32.NewProc("SetProcessDPIAware")
|
procSetProcessDPIAware = user32.NewProc("SetProcessDPIAware")
|
||||||
procPostThreadMessageW = user32.NewProc("PostThreadMessageW")
|
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 (
|
const (
|
||||||
WH_KEYBOARD_LL = 13
|
WH_KEYBOARD_LL = 13
|
||||||
WH_MOUSE_LL = 14
|
WH_MOUSE_LL = 14
|
||||||
@ -40,7 +49,7 @@ func init() {
|
|||||||
|
|
||||||
type MSLLHOOKSTRUCT struct {
|
type MSLLHOOKSTRUCT struct {
|
||||||
Pt Point
|
Pt Point
|
||||||
MouseData uint32 // Mantemos uint32 mas vamos converter no callback
|
MouseData uint32
|
||||||
Flags uint32
|
Flags uint32
|
||||||
Time uint32
|
Time uint32
|
||||||
DwExtraInfo uintptr
|
DwExtraInfo uintptr
|
||||||
@ -58,6 +67,7 @@ type windowsInputHandler struct {
|
|||||||
mouseHook uintptr
|
mouseHook uintptr
|
||||||
keyHook uintptr
|
keyHook uintptr
|
||||||
tid uint32
|
tid uint32
|
||||||
|
hwnd uintptr
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewInputHandler() InputHandler {
|
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 {
|
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)
|
ready := make(chan error, 1)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
@ -81,7 +95,7 @@ func (h *windowsInputHandler) Install(ctx context.Context, onMouse func(MouseEve
|
|||||||
Data: info.MouseData,
|
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) {
|
if onMouse(ev) {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
@ -160,7 +174,6 @@ func (h *windowsInputHandler) ShowCursor(show bool) {
|
|||||||
if show {
|
if show {
|
||||||
s = 1
|
s = 1
|
||||||
}
|
}
|
||||||
// Vamos usar uma abordagem mais direta se necessário, mas por enquanto:
|
|
||||||
procShowCursor.Call(uintptr(s))
|
procShowCursor.Call(uintptr(s))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -169,3 +182,16 @@ func (h *windowsInputHandler) GetScreenResolution() (int32, int32) {
|
|||||||
h_res, _, _ := procGetSystemMetrics.Call(SM_CYSCREEN)
|
h_res, _, _ := procGetSystemMetrics.Call(SM_CYSCREEN)
|
||||||
return int32(w), int32(h_res)
|
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)
|
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.scrollActive = true
|
||||||
e.scrollTimer = time.Now()
|
e.scrollTimer = time.Now()
|
||||||
|
|
||||||
delta := int32(int16(data >> 16))
|
deltaRaw := int16(data >> 16)
|
||||||
e.wheelAccum += delta
|
|
||||||
|
|
||||||
const Divisor = 40 // Bem sensível para touchpad
|
// LOG TOTAL PARA DESCOBRIR O QUE O TOUCHPAD MANDA
|
||||||
toSend := e.wheelAccum / Divisor
|
go LogDebug(fmt.Sprintf("SCROLL RAW: msg=0x%X data=0x%X delta=%d", msg, data, deltaRaw))
|
||||||
|
|
||||||
if toSend != 0 {
|
if deltaRaw > 0 {
|
||||||
e.wheelAccum -= toSend * Divisor
|
e.transport.Send([]byte{'P', 0xDA}) // Up
|
||||||
err := e.transport.Send([]byte{'W', byte(int8(clamp(int(toSend), -127, 127)))})
|
time.Sleep(5 * time.Millisecond)
|
||||||
if err == nil {
|
e.transport.Send([]byte{'U', 0xDA})
|
||||||
LogDebug(fmt.Sprintf("SCROLL: delta=%d enviado=%d", delta, toSend))
|
} 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
|
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 !e.clientMode {
|
||||||
if ev.Message == 0x0200 && e.isAtExitEdge(ev.Point) {
|
if ev.Message == 0x0200 && e.isAtExitEdge(ev.Point) {
|
||||||
e.enterClientMode(ev.Point)
|
e.enterClientMode(ev.Point)
|
||||||
@ -123,21 +129,17 @@ func (e *Engine) onMouse(ev input.MouseEvent) bool {
|
|||||||
|
|
||||||
// ─── MODO CLIENTE ATIVO ───
|
// ─── 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 {
|
switch ev.Message {
|
||||||
case 0x020A, 0x020E: // Roda Vertical ou Horizontal
|
case 0x020A, 0x020E: // Roda Vertical ou Horizontal
|
||||||
e.processarScroll(ev.Data)
|
e.processarScroll(ev.Message, ev.Data)
|
||||||
return true
|
return true
|
||||||
|
|
||||||
case 0x0200: // Move
|
case 0x0200: // Move
|
||||||
if e.isWarping { e.isWarping = false; return true }
|
if e.isWarping { e.isWarping = false; return true }
|
||||||
|
|
||||||
if e.scrollActive {
|
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.scrollActive = false
|
||||||
e.virtualX, e.virtualY = 0, 0
|
e.virtualX, e.virtualY = 0, 0
|
||||||
}
|
}
|
||||||
@ -160,6 +162,8 @@ func (e *Engine) onMouse(ev input.MouseEvent) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Park no centro para manter o mouse sobre a janela do App
|
||||||
|
// permitindo a captura do scroll.
|
||||||
w, h := e.inputHandler.GetScreenResolution()
|
w, h := e.inputHandler.GetScreenResolution()
|
||||||
e.isWarping = true
|
e.isWarping = true
|
||||||
e.inputHandler.SetCursorPos(w/2, h/2)
|
e.inputHandler.SetCursorPos(w/2, h/2)
|
||||||
@ -214,11 +218,20 @@ func (e *Engine) enterClientMode(p input.Point) {
|
|||||||
e.wheelAccum = 0
|
e.wheelAccum = 0
|
||||||
e.mouseThrottle = time.Now()
|
e.mouseThrottle = time.Now()
|
||||||
|
|
||||||
|
// Foco para receber scroll da interface
|
||||||
|
e.inputHandler.RequestFocus()
|
||||||
|
|
||||||
w, h := e.inputHandler.GetScreenResolution()
|
w, h := e.inputHandler.GetScreenResolution()
|
||||||
e.isWarping = true
|
e.isWarping = true
|
||||||
|
// Park no centro como no início
|
||||||
e.inputHandler.SetCursorPos(w/2, h/2)
|
e.inputHandler.SetCursorPos(w/2, h/2)
|
||||||
e.lastRawPos = input.Point{X: w / 2, Y: 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'})
|
e.transport.Send([]byte{'O'})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -230,6 +243,7 @@ func (e *Engine) exitClientMode() {
|
|||||||
w, h := e.inputHandler.GetScreenResolution()
|
w, h := e.inputHandler.GetScreenResolution()
|
||||||
var ret input.Point
|
var ret input.Point
|
||||||
const Offset = 120
|
const Offset = 120
|
||||||
|
// Retornamos o cursor exatamente para a borda onde ele entrou
|
||||||
switch e.clientPos {
|
switch e.clientPos {
|
||||||
case PosRight: ret = input.Point{X: w - Offset, Y: e.edgeEntry.Y}
|
case PosRight: ret = input.Point{X: w - Offset, Y: e.edgeEntry.Y}
|
||||||
case PosLeft: ret = input.Point{X: 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'})
|
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 {
|
func (e *Engine) onKey(ev input.KeyboardEvent) bool {
|
||||||
e.mu.Lock()
|
e.mu.Lock()
|
||||||
defer e.mu.Unlock()
|
defer e.mu.Unlock()
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user