/* 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(); } // ── 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); }