264 lines
10 KiB
C++
264 lines
10 KiB
C++
/*
|
||
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();
|
||
}
|
||
|
||
// ── 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
|
||
};
|
||
|
||
Estado estado = AGUARDA_CMD;
|
||
int8_t pendingDX = 0;
|
||
|
||
// ── 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') { ledCor(255, 0, 255); } // magenta — mouse no cliente
|
||
else if (b == 'H') { ledCor( 0, 0, 255); } // azul — host conectado
|
||
else if (b == 'G') { ledCor( 0, 255, 0); } // verde — host desconectado
|
||
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;
|
||
}
|
||
}
|
||
|
||
// ── 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;
|
||
Keyboard.releaseAll();
|
||
ledCor(0, 255, 0); // 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(256, 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);
|
||
}
|