Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- // WiFi + Async WebSocket logger for ESP32-S3
- #include <WiFi.h>
- #include <WiFiUdp.h>
- #include <ArduinoOTA.h>
- #include <ArduinoJson.h>
- #include <AsyncTCP.h>
- #include <ESPAsyncWebServer.h>
- #include <esp_wifi.h>
- #include "USB.h"
- #include "USBHIDKeyboard.h"
- #include "USBHID.h"
- #include "class/hid/hid.h"
- #include <time.h>
- #include <sys/time.h>
- #include <stdlib.h>
- #include <deque>
- // WiFi settings
- const char *WIFI_SSID = "eBayAir";
- const char *WIFI_PASS = "XXX";
- // NTP settings
- const char *NTP_SERVER = "pool.ntp.org";
- const long GMT_OFFSET_SEC = 0;
- const int DAYLIGHT_OFFSET_SEC = 0;
- // logging settings
- const bool LOGGING_ENABLED = false;
- const IPAddress LOG_HOST(192, 168, 2, 225);
- const uint16_t LOG_PORT = 4567;
- const uint16_t LOCAL_UDP_PORT = 4568;
- WiFiUDP udp;
- void logMessage(const String &message) {
- if (!LOGGING_ENABLED) return;
- String payload = "[" + currentTimestamp() + "] " + message;
- String payloadNl = payload + "\n";
- Serial.println(payload);
- if (WiFi.status() == WL_CONNECTED) {
- udp.beginPacket(LOG_HOST, LOG_PORT);
- udp.write(reinterpret_cast<const uint8_t *>(payloadNl.c_str()), payloadNl.length());
- udp.endPacket();
- }
- }
- // Mouse button masks
- #define MOUSE_LEFT 0x01
- #define MOUSE_RIGHT 0x02
- #define MOUSE_MIDDLE 0x04
- #define MOUSE_BACKWARD 0x08
- #define MOUSE_FORWARD 0x10
- #define MOUSE_ALL 0x1F
- // Reliable HID Mouse implementation with retry mechanism
- class ReliableHIDMouse: public USBHIDDevice {
- private:
- USBHID hid;
- uint8_t _buttons;
- // Pending report that failed to send
- bool _hasPending;
- hid_mouse_report_t _pendingReport;
- unsigned long _lastRetryTime;
- static const unsigned long RETRY_INTERVAL_US = 1000; // 1ms between retries
- // Rate limiting
- unsigned long _lastSendTime;
- unsigned long _minReportIntervalUs; // Minimum interval between reports
- bool sendReport(const hid_mouse_report_t &report) {
- // Check if enough time has passed since last send attempt
- unsigned long now = micros();
- if (_lastSendTime > 0) {
- unsigned long elapsed = now - _lastSendTime;
- if (elapsed < _minReportIntervalUs) {
- // Too soon to send, always throttle regardless of success/failure
- return false;
- }
- }
- // Update timestamp regardless of success to enforce rate limit
- _lastSendTime = now;
- bool success = hid.SendReport(HID_REPORT_ID_MOUSE, &report, sizeof(report));
- return success;
- }
- public:
- ReliableHIDMouse(): hid(), _buttons(0), _hasPending(false), _lastRetryTime(0), _lastSendTime(0), _minReportIntervalUs(1000000/120/* max 120x/s */) {
- static bool initialized = false;
- if(!initialized){
- initialized = true;
- hid.addDevice(this, _onGetDescriptor(nullptr));
- }
- }
- // Set minimum interval between reports in microseconds
- void setMinReportInterval(unsigned long intervalUs) {
- _minReportIntervalUs = intervalUs;
- }
- // Get current minimum interval setting
- unsigned long getMinReportInterval() const {
- return _minReportIntervalUs;
- }
- uint16_t _onGetDescriptor(uint8_t* dst) {
- static const uint8_t report_descriptor[] = {
- TUD_HID_REPORT_DESC_MOUSE(HID_REPORT_ID(HID_REPORT_ID_MOUSE))
- };
- if (dst != nullptr) {
- memcpy(dst, report_descriptor, sizeof(report_descriptor));
- }
- return sizeof(report_descriptor);
- }
- void begin() {
- hid.begin();
- }
- void end() {
- }
- // Try to send pending report if any
- // Returns true if no pending report or if it was successfully sent
- bool retryPending() {
- if (!_hasPending) {
- return true;
- }
- // Throttle retry attempts
- unsigned long now = micros();
- if (now - _lastRetryTime < RETRY_INTERVAL_US) {
- return false;
- }
- _lastRetryTime = now;
- if (sendReport(_pendingReport)) {
- _hasPending = false;
- return true;
- }
- return false;
- }
- bool hasPending() const {
- return _hasPending;
- }
- bool move(int8_t x, int8_t y, int8_t wheel = 0, int8_t pan = 0) {
- // First retry any pending report
- if (!retryPending()) {
- return false; // Still has pending, can't send new report
- }
- hid_mouse_report_t report = {
- .buttons = _buttons,
- .x = x,
- .y = y,
- .wheel = wheel,
- .pan = pan
- };
- if (sendReport(report)) {
- return true;
- }
- // Failed to send - store as pending ONLY if we don't already have one
- if (!_hasPending) {
- _pendingReport = report;
- _hasPending = true;
- _lastRetryTime = micros();
- }
- return false;
- }
- bool click(uint8_t b = MOUSE_LEFT) {
- uint8_t oldButtons = _buttons;
- _buttons = b;
- bool success1 = move(0, 0);
- _buttons = 0;
- bool success2 = move(0, 0);
- if (!success1 || !success2) {
- _buttons = oldButtons; // Restore on failure
- return false;
- }
- return true;
- }
- bool press(uint8_t b = MOUSE_LEFT) {
- if ((_buttons & b) == b) {
- return true; // Already pressed
- }
- _buttons |= b;
- return move(0, 0);
- }
- bool release(uint8_t b = MOUSE_LEFT) {
- if ((_buttons & b) == 0) {
- return true; // Already released
- }
- _buttons &= ~b;
- return move(0, 0);
- }
- bool isPressed(uint8_t b = MOUSE_LEFT) const {
- return (_buttons & b) != 0;
- }
- };
- // Web settings
- AsyncWebServer server(80);
- AsyncWebSocket wsKeyboard("/keyboard");
- AsyncWebSocket wsMouse("/mouse");
- AsyncWebSocket wsBench("/bench");
- unsigned long lastReportTime = 0;
- const unsigned long REPORT_INTERVAL_MS = 1000;
- size_t benchCount = 0;
- size_t benchBytes = 0;
- USBHIDKeyboard hidKeyboard;
- ReliableHIDMouse hidMouse;
- // Mouse action queue
- enum MouseActionType {
- MOUSE_ACTION_MOVE,
- MOUSE_ACTION_CLICK,
- MOUSE_ACTION_PRESS,
- MOUSE_ACTION_RELEASE
- };
- struct MouseAction {
- MouseActionType type;
- int dx;
- int dy;
- int wheel;
- int pan;
- uint8_t button;
- };
- struct MouseController {
- std::deque<MouseAction> queue;
- // Statistics
- size_t totalActions = 0;
- size_t totalEnqueued = 0;
- size_t totalProcessed = 0;
- void move(bool immediate, int dx, int dy, int wheel = 0, int pan = 0) {
- totalActions++;
- if (immediate) {
- // Immediate mode: try to send, if it fails, enqueue it
- if (!hidMouse.move(dx, dy, wheel, pan)) {
- enqueueMove(dx, dy, wheel, pan);
- }
- } else {
- enqueueMove(dx, dy, wheel, pan);
- }
- }
- void click(bool immediate, uint8_t button) {
- totalActions++;
- if (immediate) {
- if (!hidMouse.click(button)) {
- enqueueAction(MOUSE_ACTION_CLICK, button);
- }
- } else {
- enqueueAction(MOUSE_ACTION_CLICK, button);
- }
- }
- void press(bool immediate, uint8_t button) {
- totalActions++;
- if (immediate) {
- if (!hidMouse.press(button)) {
- enqueueAction(MOUSE_ACTION_PRESS, button);
- }
- } else {
- enqueueAction(MOUSE_ACTION_PRESS, button);
- }
- }
- void release(bool immediate, uint8_t button) {
- totalActions++;
- if (immediate) {
- if (!hidMouse.release(button)) {
- enqueueAction(MOUSE_ACTION_RELEASE, button);
- }
- } else {
- enqueueAction(MOUSE_ACTION_RELEASE, button);
- }
- }
- void processQueue() {
- // First, try to retry any pending report in the HID layer
- if (!hidMouse.retryPending()) {
- return; // Still has pending report, can't process queue yet
- }
- // Now process queue items
- while (!queue.empty()) {
- MouseAction &action = queue.front();
- switch (action.type) {
- case MOUSE_ACTION_MOVE:
- hidMouse.move(action.dx, action.dy, action.wheel, action.pan);
- break;
- case MOUSE_ACTION_CLICK:
- hidMouse.click(action.button);
- break;
- case MOUSE_ACTION_PRESS:
- hidMouse.press(action.button);
- break;
- case MOUSE_ACTION_RELEASE:
- hidMouse.release(action.button);
- break;
- }
- // Always remove from queue, rely on pending mechanism in hidMouse
- queue.pop_front();
- totalProcessed++;
- }
- }
- String getStats() {
- String stats = "Mouse stats:";
- stats += "\n total actions=" + String(totalActions);
- stats += "\n QUEUE:";
- stats += "\n total=" + String(totalEnqueued) + " (processed=" + String(totalProcessed) + ", queue=" + String(queue.size()) + ")";
- stats += "\n pending=" + String(hidMouse.hasPending() ? "yes" : "no");
- return stats;
- }
- private:
- void enqueueMove(int dx, int dy, int wheel, int pan) {
- // Check if last action in queue is also a move - if yes, merge them
- if (!queue.empty() && queue.back().type == MOUSE_ACTION_MOVE) {
- // Merge with last move action
- queue.back().dx += dx;
- queue.back().dy += dy;
- queue.back().wheel += wheel;
- queue.back().pan += pan;
- return;
- }
- // Add new move action
- enqueueAction(MOUSE_ACTION_MOVE, 0, dx, dy, wheel, pan);
- }
- void enqueueAction(MouseActionType type, uint8_t button, int dx = 0, int dy = 0, int wheel = 0, int pan = 0) {
- MouseAction action;
- action.type = type;
- action.button = button;
- action.dx = dx;
- action.dy = dy;
- action.wheel = wheel;
- action.pan = pan;
- queue.push_back(action);
- totalEnqueued++;
- }
- };
- MouseController mouse;
- struct IntToHid {
- int code;
- uint8_t hid;
- };
- bool parseIntField(JsonVariantConst v, int &out) {
- if (v.isNull()) return false;
- if (v.is<long>()) {
- out = v.as<long>();
- return true;
- }
- if (v.is<int>()) {
- out = v.as<int>();
- return true;
- }
- if (v.is<const char *>()) {
- const char *s = v.as<const char *>();
- if (!s) return false;
- char *endp = nullptr;
- long val = strtol(s, &endp, 0);
- if (endp && *endp == '\0') {
- out = static_cast<int>(val);
- return true;
- }
- }
- return false;
- }
- uint8_t mapBrowserCode(const String &codeStr) {
- if (codeStr.length() >= 3 && codeStr.startsWith("Key")) {
- char c = codeStr.charAt(3);
- if (c >= 'A' && c <= 'Z') return HID_KEY_A + (c - 'A');
- }
- if (codeStr.length() >= 5 && codeStr.startsWith("Digit")) {
- char d = codeStr.charAt(5);
- if (d == '0') return HID_KEY_0;
- if (d >= '1' && d <= '9') return HID_KEY_1 + (d - '1');
- }
- if (codeStr.startsWith("Numpad")) {
- String tail = codeStr.substring(6);
- if (tail == "Enter") return HID_KEY_KEYPAD_ENTER;
- if (tail == "Add") return HID_KEY_KEYPAD_ADD;
- if (tail == "Subtract") return HID_KEY_KEYPAD_SUBTRACT;
- if (tail == "Multiply") return HID_KEY_KEYPAD_MULTIPLY;
- if (tail == "Divide") return HID_KEY_KEYPAD_DIVIDE;
- if (tail == "Decimal") return HID_KEY_KEYPAD_DECIMAL;
- if (tail.length() == 1 && tail[0] >= '0' && tail[0] <= '9') {
- return HID_KEY_KEYPAD_0 + (tail[0] - '0');
- }
- }
- if (codeStr.startsWith("F") && codeStr.length() <= 3) {
- int n = codeStr.substring(1).toInt();
- if (n >= 1 && n <= 24) return HID_KEY_F1 + (n - 1);
- }
- if (codeStr == "Space") return HID_KEY_SPACE;
- if (codeStr == "Enter") return HID_KEY_ENTER;
- if (codeStr == "Escape") return HID_KEY_ESCAPE;
- if (codeStr == "Backspace") return HID_KEY_BACKSPACE;
- if (codeStr == "Tab") return HID_KEY_TAB;
- if (codeStr == "Minus") return HID_KEY_MINUS;
- if (codeStr == "Equal") return HID_KEY_EQUAL;
- if (codeStr == "BracketLeft") return HID_KEY_BRACKET_LEFT;
- if (codeStr == "BracketRight") return HID_KEY_BRACKET_RIGHT;
- if (codeStr == "Backslash") return HID_KEY_BACKSLASH;
- if (codeStr == "Semicolon") return HID_KEY_SEMICOLON;
- if (codeStr == "Quote") return HID_KEY_APOSTROPHE;
- if (codeStr == "Backquote") return HID_KEY_GRAVE;
- if (codeStr == "Comma") return HID_KEY_COMMA;
- if (codeStr == "Period") return HID_KEY_PERIOD;
- if (codeStr == "Slash") return HID_KEY_SLASH;
- if (codeStr == "CapsLock") return HID_KEY_CAPS_LOCK;
- if (codeStr == "ShiftLeft") return HID_KEY_SHIFT_LEFT;
- if (codeStr == "ShiftRight") return HID_KEY_SHIFT_RIGHT;
- if (codeStr == "ControlLeft") return HID_KEY_CONTROL_LEFT;
- if (codeStr == "ControlRight") return HID_KEY_CONTROL_RIGHT;
- if (codeStr == "AltLeft") return HID_KEY_ALT_LEFT;
- if (codeStr == "AltRight") return HID_KEY_ALT_RIGHT;
- if (codeStr == "ArrowRight") return HID_KEY_ARROW_RIGHT;
- if (codeStr == "ArrowLeft") return HID_KEY_ARROW_LEFT;
- if (codeStr == "ArrowDown") return HID_KEY_ARROW_DOWN;
- if (codeStr == "ArrowUp") return HID_KEY_ARROW_UP;
- if (codeStr == "Insert") return HID_KEY_INSERT;
- if (codeStr == "Home") return HID_KEY_HOME;
- if (codeStr == "PageUp") return HID_KEY_PAGE_UP;
- if (codeStr == "Delete") return HID_KEY_DELETE;
- if (codeStr == "End") return HID_KEY_END;
- if (codeStr == "PageDown") return HID_KEY_PAGE_DOWN;
- if (codeStr == "PrintScreen") return HID_KEY_PRINT_SCREEN;
- if (codeStr == "ScrollLock") return HID_KEY_SCROLL_LOCK;
- if (codeStr == "Pause") return HID_KEY_PAUSE;
- if (codeStr == "NumLock") return HID_KEY_NUM_LOCK;
- if (codeStr == "MetaLeft" || codeStr == "OSLeft") return HID_KEY_GUI_LEFT;
- if (codeStr == "MetaRight" || codeStr == "OSRight") return HID_KEY_GUI_RIGHT;
- return 0;
- }
- String normalizeKeyFlat(String s) {
- s.trim();
- s.toUpperCase();
- s.replace("-", "");
- s.replace("_", "");
- s.replace(" ", "");
- return s;
- }
- uint8_t evdevToHid(int evdevCode) {
- static const IntToHid map[] = {
- {1, HID_KEY_ESCAPE},
- {2, HID_KEY_1}, {3, HID_KEY_2}, {4, HID_KEY_3}, {5, HID_KEY_4}, {6, HID_KEY_5},
- {7, HID_KEY_6}, {8, HID_KEY_7}, {9, HID_KEY_8}, {10, HID_KEY_9}, {11, HID_KEY_0},
- {12, HID_KEY_MINUS}, {13, HID_KEY_EQUAL}, {14, HID_KEY_BACKSPACE}, {15, HID_KEY_TAB},
- {16, HID_KEY_Q}, {17, HID_KEY_W}, {18, HID_KEY_E}, {19, HID_KEY_R}, {20, HID_KEY_T},
- {21, HID_KEY_Y}, {22, HID_KEY_U}, {23, HID_KEY_I}, {24, HID_KEY_O}, {25, HID_KEY_P},
- {26, HID_KEY_BRACKET_LEFT}, {27, HID_KEY_BRACKET_RIGHT}, {28, HID_KEY_ENTER},
- {30, HID_KEY_A}, {31, HID_KEY_S}, {32, HID_KEY_D}, {33, HID_KEY_F}, {34, HID_KEY_G},
- {35, HID_KEY_H}, {36, HID_KEY_J}, {37, HID_KEY_K}, {38, HID_KEY_L},
- {39, HID_KEY_SEMICOLON}, {40, HID_KEY_APOSTROPHE}, {41, HID_KEY_GRAVE},
- {42, HID_KEY_SHIFT_LEFT}, {43, HID_KEY_BACKSLASH},
- {44, HID_KEY_Z}, {45, HID_KEY_X}, {46, HID_KEY_C}, {47, HID_KEY_V}, {48, HID_KEY_B},
- {49, HID_KEY_N}, {50, HID_KEY_M},
- {51, HID_KEY_COMMA}, {52, HID_KEY_PERIOD}, {53, HID_KEY_SLASH},
- {54, HID_KEY_SHIFT_RIGHT}, {55, HID_KEY_KEYPAD_MULTIPLY}, {56, HID_KEY_ALT_LEFT}, {57, HID_KEY_SPACE},
- {58, HID_KEY_CAPS_LOCK},
- {59, HID_KEY_F1}, {60, HID_KEY_F2}, {61, HID_KEY_F3}, {62, HID_KEY_F4}, {63, HID_KEY_F5},
- {64, HID_KEY_F6}, {65, HID_KEY_F7}, {66, HID_KEY_F8}, {67, HID_KEY_F9}, {68, HID_KEY_F10},
- {69, HID_KEY_NUM_LOCK}, {70, HID_KEY_SCROLL_LOCK},
- {71, HID_KEY_KEYPAD_7}, {72, HID_KEY_KEYPAD_8}, {73, HID_KEY_KEYPAD_9}, {74, HID_KEY_KEYPAD_SUBTRACT},
- {75, HID_KEY_KEYPAD_4}, {76, HID_KEY_KEYPAD_5}, {77, HID_KEY_KEYPAD_6}, {78, HID_KEY_KEYPAD_ADD},
- {79, HID_KEY_KEYPAD_1}, {80, HID_KEY_KEYPAD_2}, {81, HID_KEY_KEYPAD_3}, {82, HID_KEY_KEYPAD_0},
- {83, HID_KEY_KEYPAD_DECIMAL},
- {87, HID_KEY_F11}, {88, HID_KEY_F12},
- {96, HID_KEY_KEYPAD_ENTER}, {97, HID_KEY_CONTROL_RIGHT}, {98, HID_KEY_KEYPAD_DIVIDE},
- {99, HID_KEY_PRINT_SCREEN}, {100, HID_KEY_ALT_RIGHT},
- {102, HID_KEY_HOME}, {103, HID_KEY_ARROW_UP}, {104, HID_KEY_PAGE_UP},
- {105, HID_KEY_ARROW_LEFT}, {106, HID_KEY_ARROW_RIGHT},
- {107, HID_KEY_END}, {108, HID_KEY_ARROW_DOWN}, {109, HID_KEY_PAGE_DOWN},
- {110, HID_KEY_INSERT}, {111, HID_KEY_DELETE},
- {117, HID_KEY_KEYPAD_EQUAL}, {119, HID_KEY_PAUSE},
- {121, HID_KEY_KEYPAD_COMMA}, {125, HID_KEY_GUI_LEFT}, {126, HID_KEY_GUI_RIGHT},
- {139, HID_KEY_APPLICATION}, {86, HID_KEY_EUROPE_1}
- };
- for (const auto &entry : map) {
- if (entry.code == evdevCode) return entry.hid;
- }
- return 0;
- }
- uint8_t systemToHid(int systemCode) {
- if (systemCode >= 65 && systemCode <= 90) {
- return HID_KEY_A + (systemCode - 65);
- }
- if (systemCode >= 48 && systemCode <= 57) { // digits 0-9
- if (systemCode == 48) return HID_KEY_0;
- return HID_KEY_1 + (systemCode - 49);
- }
- if (systemCode >= 96 && systemCode <= 105) { // numpad 0-9
- return HID_KEY_KEYPAD_0 + (systemCode - 96);
- }
- if (systemCode >= 112 && systemCode <= 135) { // F1-F24
- return HID_KEY_F1 + (systemCode - 112);
- }
- static const IntToHid map[] = {
- {8, HID_KEY_BACKSPACE}, {9, HID_KEY_TAB}, {13, HID_KEY_ENTER},
- {16, HID_KEY_SHIFT_LEFT}, {17, HID_KEY_CONTROL_LEFT}, {18, HID_KEY_ALT_LEFT},
- {19, HID_KEY_PAUSE}, {20, HID_KEY_CAPS_LOCK}, {27, HID_KEY_ESCAPE}, {32, HID_KEY_SPACE},
- {33, HID_KEY_PAGE_UP}, {34, HID_KEY_PAGE_DOWN}, {35, HID_KEY_END}, {36, HID_KEY_HOME},
- {37, HID_KEY_ARROW_LEFT}, {38, HID_KEY_ARROW_UP}, {39, HID_KEY_ARROW_RIGHT}, {40, HID_KEY_ARROW_DOWN},
- {44, HID_KEY_PRINT_SCREEN}, {45, HID_KEY_INSERT}, {46, HID_KEY_DELETE},
- {91, HID_KEY_GUI_LEFT}, {92, HID_KEY_GUI_RIGHT}, {93, HID_KEY_APPLICATION},
- {106, HID_KEY_KEYPAD_MULTIPLY}, {107, HID_KEY_KEYPAD_ADD},
- {109, HID_KEY_KEYPAD_SUBTRACT}, {110, HID_KEY_KEYPAD_DECIMAL}, {111, HID_KEY_KEYPAD_DIVIDE},
- {144, HID_KEY_NUM_LOCK}, {145, HID_KEY_SCROLL_LOCK},
- {186, HID_KEY_SEMICOLON}, {187, HID_KEY_EQUAL}, {188, HID_KEY_COMMA},
- {189, HID_KEY_MINUS}, {190, HID_KEY_PERIOD}, {191, HID_KEY_SLASH},
- {192, HID_KEY_GRAVE}, {219, HID_KEY_BRACKET_LEFT}, {220, HID_KEY_BACKSLASH},
- {221, HID_KEY_BRACKET_RIGHT}, {222, HID_KEY_APOSTROPHE},
- {173, HID_KEY_MINUS} // some browsers report 173 for minus
- };
- for (const auto &entry : map) {
- if (entry.code == systemCode) return entry.hid;
- }
- return 0;
- }
- uint8_t nameToHid(const String &name) {
- if (name.length() == 0) return 0;
- uint8_t byBrowserCode = mapBrowserCode(name);
- if (byBrowserCode) return byBrowserCode;
- String upper = name;
- upper.trim();
- if (upper.startsWith("KEY_")) return nameToHid(upper.substring(4));
- if (upper.startsWith("HID_KEY_")) return nameToHid(upper.substring(8));
- upper.replace("-", "_");
- upper.replace(" ", "_");
- upper.toUpperCase();
- String flat = normalizeKeyFlat(upper);
- if (upper.length() == 1) {
- char c = upper.charAt(0);
- if (c >= 'A' && c <= 'Z') return HID_KEY_A + (c - 'A');
- if (c >= '0' && c <= '9') return (c == '0') ? HID_KEY_0 : HID_KEY_1 + (c - '1');
- }
- if (upper.startsWith("F")) {
- int n = upper.substring(1).toInt();
- if (n >= 1 && n <= 24) return HID_KEY_F1 + (n - 1);
- }
- if (upper.startsWith("NUMPAD")) {
- String tail = upper.substring(6);
- if (tail == "ENTER") return HID_KEY_KEYPAD_ENTER;
- if (tail == "ADD" || tail == "PLUS") return HID_KEY_KEYPAD_ADD;
- if (tail == "SUBTRACT" || tail == "MINUS") return HID_KEY_KEYPAD_SUBTRACT;
- if (tail == "MULTIPLY" || tail == "STAR") return HID_KEY_KEYPAD_MULTIPLY;
- if (tail == "DIVIDE" || tail == "SLASH") return HID_KEY_KEYPAD_DIVIDE;
- if (tail == "DECIMAL" || tail == "DOT") return HID_KEY_KEYPAD_DECIMAL;
- if (tail.length() == 1 && tail[0] >= '0' && tail[0] <= '9') {
- return HID_KEY_KEYPAD_0 + (tail[0] - '0');
- }
- }
- if (flat == "ESC" || flat == "ESCAPE") return HID_KEY_ESCAPE;
- if (flat == "BACKSPACE" || flat == "BS") return HID_KEY_BACKSPACE;
- if (flat == "TAB") return HID_KEY_TAB;
- if (flat == "ENTER" || flat == "RETURN") return HID_KEY_ENTER;
- if (flat == "SPACE" || flat == "SPACEBAR") return HID_KEY_SPACE;
- if (flat == "CAPSLOCK") return HID_KEY_CAPS_LOCK;
- if (flat == "PRINTSCREEN" || flat == "PRNTSCRN" || flat == "SYSRQ") return HID_KEY_PRINT_SCREEN;
- if (flat == "SCROLLLOCK") return HID_KEY_SCROLL_LOCK;
- if (flat == "PAUSE" || flat == "BREAK") return HID_KEY_PAUSE;
- if (flat == "LEFTSHIFT" || flat == "SHIFTLEFT" || flat == "LSHIFT") return HID_KEY_SHIFT_LEFT;
- if (flat == "RIGHTSHIFT" || flat == "SHIFTRIGHT" || flat == "RSHIFT") return HID_KEY_SHIFT_RIGHT;
- if (flat == "LEFTCTRL" || flat == "CTRLLEFT" || flat == "LCTRL" || flat == "CONTROLLEFT") return HID_KEY_CONTROL_LEFT;
- if (flat == "RIGHTCTRL" || flat == "CTRLRIGHT" || flat == "RCTRL" || flat == "CONTROLRIGHT") return HID_KEY_CONTROL_RIGHT;
- if (flat == "LEFTALT" || flat == "ALTLEFT" || flat == "LALT") return HID_KEY_ALT_LEFT;
- if (flat == "RIGHTALT" || flat == "ALTRIGHT" || flat == "RALT" || flat == "ALTGR") return HID_KEY_ALT_RIGHT;
- if (flat == "LEFTMETA" || flat == "METALEFT" || flat == "LWIN" || flat == "LGUI" || flat == "CMDLEFT") return HID_KEY_GUI_LEFT;
- if (flat == "RIGHTMETA" || flat == "METARIGHT" || flat == "RWIN" || flat == "RGUI" || flat == "CMDRIGHT") return HID_KEY_GUI_RIGHT;
- if (flat == "UP" || flat == "ARROWUP" || flat == "UPARROW") return HID_KEY_ARROW_UP;
- if (flat == "DOWN" || flat == "ARROWDOWN" || flat == "DOWNARROW") return HID_KEY_ARROW_DOWN;
- if (flat == "LEFT" || flat == "ARROWLEFT" || flat == "LEFTARROW") return HID_KEY_ARROW_LEFT;
- if (flat == "RIGHT" || flat == "ARROWRIGHT" || flat == "RIGHTARROW") return HID_KEY_ARROW_RIGHT;
- if (flat == "DELETE" || flat == "DEL") return HID_KEY_DELETE;
- if (flat == "HOME") return HID_KEY_HOME;
- if (flat == "END") return HID_KEY_END;
- if (flat == "PAGEUP") return HID_KEY_PAGE_UP;
- if (flat == "PAGEDOWN") return HID_KEY_PAGE_DOWN;
- if (flat == "INSERT" || flat == "INS") return HID_KEY_INSERT;
- if (flat == "NUMLOCK") return HID_KEY_NUM_LOCK;
- if (flat == "MINUS" || flat == "HYPHEN") return HID_KEY_MINUS;
- if (flat == "EQUAL" || flat == "EQUALS") return HID_KEY_EQUAL;
- if (flat == "BRACKETLEFT" || flat == "LBRACKET" || flat == "LEFTBRACKET") return HID_KEY_BRACKET_LEFT;
- if (flat == "BRACKETRIGHT" || flat == "RBRACKET" || flat == "RIGHTBRACKET") return HID_KEY_BRACKET_RIGHT;
- if (flat == "BACKSLASH") return HID_KEY_BACKSLASH;
- if (flat == "SEMICOLON") return HID_KEY_SEMICOLON;
- if (flat == "APOSTROPHE" || flat == "QUOTE") return HID_KEY_APOSTROPHE;
- if (flat == "GRAVE" || flat == "BACKQUOTE" || flat == "BACKTICK") return HID_KEY_GRAVE;
- if (flat == "COMMA") return HID_KEY_COMMA;
- if (flat == "PERIOD" || flat == "DOT") return HID_KEY_PERIOD;
- if (flat == "SLASH" || flat == "FORWARDSLASH") return HID_KEY_SLASH;
- if (flat == "APPLICATION" || flat == "MENU") return HID_KEY_APPLICATION;
- if (flat == "POWER") return HID_KEY_POWER;
- static const IntToHid evdevNameMap[] = {
- {1, HID_KEY_ESCAPE}, {14, HID_KEY_BACKSPACE}, {15, HID_KEY_TAB}, {28, HID_KEY_ENTER}, {57, HID_KEY_SPACE},
- {12, HID_KEY_MINUS}, {13, HID_KEY_EQUAL}, {26, HID_KEY_BRACKET_LEFT}, {27, HID_KEY_BRACKET_RIGHT}, {43, HID_KEY_BACKSLASH},
- {39, HID_KEY_SEMICOLON}, {40, HID_KEY_APOSTROPHE}, {41, HID_KEY_GRAVE}, {51, HID_KEY_COMMA}, {52, HID_KEY_PERIOD}, {53, HID_KEY_SLASH},
- {58, HID_KEY_CAPS_LOCK}, {59, HID_KEY_F1}, {60, HID_KEY_F2}, {61, HID_KEY_F3}, {62, HID_KEY_F4}, {63, HID_KEY_F5},
- {64, HID_KEY_F6}, {65, HID_KEY_F7}, {66, HID_KEY_F8}, {67, HID_KEY_F9}, {68, HID_KEY_F10}, {87, HID_KEY_F11}, {88, HID_KEY_F12},
- {99, HID_KEY_PRINT_SCREEN}, {70, HID_KEY_SCROLL_LOCK}, {119, HID_KEY_PAUSE},
- {110, HID_KEY_INSERT}, {102, HID_KEY_HOME}, {104, HID_KEY_PAGE_UP}, {111, HID_KEY_DELETE}, {107, HID_KEY_END}, {109, HID_KEY_PAGE_DOWN},
- {106, HID_KEY_ARROW_RIGHT}, {105, HID_KEY_ARROW_LEFT}, {108, HID_KEY_ARROW_DOWN}, {103, HID_KEY_ARROW_UP},
- {69, HID_KEY_NUM_LOCK}
- };
- if (upper.startsWith("KEY_")) upper = upper.substring(4);
- if (upper.startsWith("EVDEV_")) upper = upper.substring(6);
- String upperFlat = normalizeKeyFlat(upper);
- for (const auto &entry : evdevNameMap) {
- String evName;
- switch (entry.code) {
- case 1: evName = "ESC"; break;
- case 14: evName = "BACKSPACE"; break;
- case 15: evName = "TAB"; break;
- case 28: evName = "ENTER"; break;
- case 57: evName = "SPACE"; break;
- case 12: evName = "MINUS"; break;
- case 13: evName = "EQUAL"; break;
- case 26: evName = "LEFTBRACE"; break;
- case 27: evName = "RIGHTBRACE"; break;
- case 43: evName = "BACKSLASH"; break;
- case 39: evName = "SEMICOLON"; break;
- case 40: evName = "APOSTROPHE"; break;
- case 41: evName = "GRAVE"; break;
- case 51: evName = "COMMA"; break;
- case 52: evName = "DOT"; break;
- case 53: evName = "SLASH"; break;
- case 58: evName = "CAPSLOCK"; break;
- case 59: evName = "F1"; break;
- case 60: evName = "F2"; break;
- case 61: evName = "F3"; break;
- case 62: evName = "F4"; break;
- case 63: evName = "F5"; break;
- case 64: evName = "F6"; break;
- case 65: evName = "F7"; break;
- case 66: evName = "F8"; break;
- case 67: evName = "F9"; break;
- case 68: evName = "F10"; break;
- case 87: evName = "F11"; break;
- case 88: evName = "F12"; break;
- case 99: evName = "SYSRQ"; break;
- case 70: evName = "SCROLLLOCK"; break;
- case 119: evName = "PAUSE"; break;
- case 110: evName = "INSERT"; break;
- case 102: evName = "HOME"; break;
- case 104: evName = "PAGEUP"; break;
- case 111: evName = "DELETE"; break;
- case 107: evName = "END"; break;
- case 109: evName = "PAGEDOWN"; break;
- case 106: evName = "RIGHT"; break;
- case 105: evName = "LEFT"; break;
- case 108: evName = "DOWN"; break;
- case 103: evName = "UP"; break;
- case 69: evName = "NUMLOCK"; break;
- default: break;
- }
- if (evName.length() > 0 && upperFlat == normalizeKeyFlat(evName)) {
- return entry.hid;
- }
- }
- // As a last resort, check evdev numeric names like KEY_30 etc.
- if (upper.startsWith("KEY")) {
- int n = upper.substring(3).toInt();
- if (n > 0) {
- uint8_t evMap = evdevToHid(n);
- if (evMap) return evMap;
- }
- }
- return 0;
- }
- uint8_t resolveHidFromPayload(JsonVariantConst node, String *reason = nullptr) {
- int raw = 0;
- if (parseIntField(node["hid_code"], raw) && raw >= 0 && raw <= 0xFF) {
- if (reason) *reason = "hid_code";
- return static_cast<uint8_t>(raw);
- }
- if (parseIntField(node["evdev_code"], raw)) {
- uint8_t mapped = evdevToHid(raw);
- if (mapped) {
- if (reason) *reason = "evdev_code=" + String(raw);
- return mapped;
- }
- }
- if (parseIntField(node["system_keycode"], raw)) {
- uint8_t mapped = systemToHid(raw);
- if (mapped) {
- if (reason) *reason = "system_keycode=" + String(raw);
- return mapped;
- }
- }
- String name = node["key_name"] | "";
- if (name.length() > 0) {
- uint8_t mapped = nameToHid(name);
- if (mapped) {
- if (reason) *reason = "key_name=" + name;
- return mapped;
- }
- }
- return 0;
- }
- String currentTimestamp() {
- struct timeval tv;
- gettimeofday(&tv, nullptr);
- time_t now = tv.tv_sec;
- struct tm timeinfo;
- localtime_r(&now, &timeinfo);
- char buffer[40];
- snprintf(buffer, sizeof(buffer),
- "%04d-%02d-%02d %02d:%02d:%02d.%03ld",
- timeinfo.tm_year + 1900,
- timeinfo.tm_mon + 1,
- timeinfo.tm_mday,
- timeinfo.tm_hour,
- timeinfo.tm_min,
- timeinfo.tm_sec,
- tv.tv_usec / 1000);
- return String(buffer);
- }
- void connectWiFi() {
- WiFi.mode(WIFI_STA);
- WiFi.setSleep(false);
- esp_wifi_set_ps(WIFI_PS_NONE);
- WiFi.begin(WIFI_SSID, WIFI_PASS);
- logMessage("Connecting to WiFi...");
- unsigned long start = millis();
- while (WiFi.status() != WL_CONNECTED) {
- delay(250);
- if (millis() - start > 20000) {
- logMessage("WiFi connection timeout, retrying...");
- start = millis();
- WiFi.disconnect();
- WiFi.begin(WIFI_SSID, WIFI_PASS);
- }
- }
- logMessage("Connected to WiFi, IP: " + WiFi.localIP().toString());
- }
- void syncTime() {
- configTime(GMT_OFFSET_SEC, DAYLIGHT_OFFSET_SEC, NTP_SERVER);
- logMessage("Waiting for NTP sync...");
- struct tm timeinfo;
- for (int i = 0; i < 20; i++) {
- if (getLocalTime(&timeinfo)) {
- logMessage("Time synced: " + currentTimestamp());
- return;
- }
- delay(500);
- }
- logMessage("Failed to sync time via NTP");
- }
- String wsClientLabel(AsyncWebSocketClient *client) {
- if (!client) return "unknown";
- return "#" + String(client->id()) + " " + client->remoteIP().toString();
- }
- String wsChannel(AsyncWebSocket *server) {
- if (server == &wsKeyboard) return "keyboard";
- if (server == &wsMouse) return "mouse";
- if (server == &wsBench) return "bench";
- return server ? server->url() : "unknown";
- }
- void handleMouseJson(const JsonDocument &doc, bool logFlag, JsonObject responseData = JsonObject()) {
- const char *action = doc["action"] | "";
- if (!action || strlen(action) == 0) {
- if (logFlag) logMessage("Mouse JSON missing action");
- return;
- }
- // Check if immediate execution is requested (default is false = queued)
- bool immediate = false;
- if (!doc["immediate"].isNull()) {
- immediate = doc["immediate"].as<bool>();
- }
- auto buttonMask = [](const String &btn) -> uint8_t {
- String b = btn; b.toLowerCase();
- if (b == "left") return MOUSE_LEFT;
- if (b == "right") return MOUSE_RIGHT;
- if (b == "middle" || b == "wheel") return MOUSE_MIDDLE;
- if (b == "back" || b == "backward") return MOUSE_BACKWARD;
- if (b == "forward") return MOUSE_FORWARD;
- return MOUSE_LEFT;
- };
- if (strcmp(action, "move") == 0) {
- int dx = doc["dx"] | 0;
- int dy = doc["dy"] | 0;
- int wheel = doc["wheel"] | 0;
- int pan = doc["pan"] | 0;
- mouse.move(immediate, dx, dy, wheel, pan);
- if (logFlag) {
- String mode = immediate ? "immediate" : "queued";
- logMessage("Mouse move (" + mode + ") dx=" + String(dx) + " dy=" + String(dy) + " wheel=" + String(wheel) + " pan=" + String(pan));
- }
- return;
- }
- if (strcmp(action, "click") == 0) {
- String btn = doc["button"] | "left";
- uint8_t mask = buttonMask(btn);
- mouse.click(immediate, mask);
- if (logFlag) {
- String mode = immediate ? "immediate" : "queued";
- logMessage("Mouse click (" + mode + ") button=" + btn);
- }
- return;
- }
- if (strcmp(action, "button") == 0) {
- String btn = doc["button"] | "left";
- String state = doc["state"] | "down";
- state.toLowerCase();
- uint8_t mask = buttonMask(btn);
- if (state == "down" || state == "press" || state == "mousedown") {
- mouse.press(immediate, mask);
- if (logFlag) {
- String mode = immediate ? "immediate" : "queued";
- logMessage("Mouse button down (" + mode + "): " + btn);
- }
- } else if (state == "up" || state == "release" || state == "mouseup") {
- mouse.release(immediate, mask);
- if (logFlag) {
- String mode = immediate ? "immediate" : "queued";
- logMessage("Mouse button up (" + mode + "): " + btn);
- }
- } else {
- if (logFlag) logMessage("Mouse button unknown state: " + state);
- }
- return;
- }
- if (strcmp(action, "set_rate_limit") == 0) {
- int intervalUs = doc["interval_us"] | -1;
- if (intervalUs >= 0) {
- hidMouse.setMinReportInterval(intervalUs);
- if (logFlag) logMessage("Mouse rate limit set to " + String(intervalUs) + " us");
- }
- return;
- }
- if (strcmp(action, "get_rate_limit") == 0) {
- unsigned long interval = hidMouse.getMinReportInterval();
- if (logFlag) logMessage("Mouse rate limit: " + String(interval) + " us");
- return;
- }
- if (logFlag) logMessage("Mouse JSON unknown action: " + String(action));
- }
- void handleKeyboardJson(const JsonDocument &doc, bool logFlag, JsonObject responseData = JsonObject()) {
- const char *action = doc["action"] | "";
- if (!action || strlen(action) == 0) {
- if (logFlag) logMessage("Keyboard JSON missing action");
- return;
- }
- if (strcmp(action, "key_event") == 0) {
- String state = doc["state"] | "down";
- String why;
- uint8_t code = resolveHidFromPayload(doc, &why);
- if (code == 0) {
- if (logFlag) logMessage("Keyboard key_event missing/unknown code");
- return;
- }
- state.toLowerCase();
- if (state == "down") {
- hidKeyboard.pressRaw(code);
- if (logFlag) {
- String info = "Keyboard key down: " + String(code);
- if (why.length() > 0) info += " (" + why + ")";
- logMessage(info);
- }
- } else if (state == "up") {
- hidKeyboard.releaseRaw(code);
- if (logFlag) {
- String info = "Keyboard key up: " + String(code);
- if (why.length() > 0) info += " (" + why + ")";
- logMessage(info);
- }
- } else {
- if (logFlag) logMessage("Keyboard key_event ignored state: " + state);
- }
- return;
- }
- if (strcmp(action, "release_all") == 0) {
- hidKeyboard.releaseAll();
- if (logFlag) logMessage("Keyboard release all");
- return;
- }
- if (logFlag) logMessage("Keyboard JSON unknown action: " + String(action));
- }
- void handleWsEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len) {
- String channel = wsChannel(server);
- switch (type) {
- case WS_EVT_CONNECT:
- logMessage("WS connect [" + channel + "] " + wsClientLabel(client));
- break;
- case WS_EVT_DISCONNECT:
- logMessage("WS disconnect [" + channel + "] " + wsClientLabel(client));
- break;
- case WS_EVT_DATA: {
- AwsFrameInfo *info = static_cast<AwsFrameInfo *>(arg);
- if (info->final && info->index == 0 && info->len == len) {
- String msg;
- msg.reserve(len);
- for (size_t i = 0; i < len; i++) {
- msg += static_cast<char>(data[i]);
- }
- if (channel == "mouse" || channel == "keyboard") {
- StaticJsonDocument<512> doc;
- DeserializationError err = deserializeJson(doc, msg);
- bool logFlag = true;
- if (!doc["log"].isNull()) {
- logFlag = doc["log"].as<bool>();
- }
- bool responseFlag = false;
- if (!doc["response"].isNull()) {
- responseFlag = doc["response"].as<bool>();
- }
- if (err) {
- if (logFlag) logMessage("WS " + channel + " JSON parse error: " + String(err.c_str()));
- } else {
- unsigned long startMicros = micros();
- if (logFlag) {
- logMessage("WS text [" + channel + "] from " + wsClientLabel(client) + ": " + msg);
- }
- // Prepare response object if needed
- JsonObject responseData;
- StaticJsonDocument<768> responseDoc;
- if (responseFlag) {
- responseDoc["request"] = doc;
- responseData = responseDoc.createNestedObject("response");
- responseData["duration"] = 0; // will be updated later
- }
- if (channel == "mouse") {
- handleMouseJson(doc, logFlag, responseData);
- } else {
- handleKeyboardJson(doc, logFlag, responseData);
- }
- // Send response if requested
- if (responseFlag) {
- unsigned long durationMicros = micros() - startMicros;
- responseData["duration"] = durationMicros;
- String responseJson;
- serializeJson(responseDoc, responseJson);
- client->text(responseJson);
- if (logFlag) logMessage("Sent response: " + responseJson);
- }
- }
- } else {
- logMessage("WS text [" + channel + "] from " + wsClientLabel(client) + ": " + msg);
- }
- } else {
- logMessage("WS fragmented/binary data [" + channel + "] from " + wsClientLabel(client));
- }
- break;
- }
- case WS_EVT_ERROR:
- logMessage("WS error [" + channel + "] " + wsClientLabel(client));
- break;
- case WS_EVT_PONG:
- break;
- default:
- break;
- }
- }
- void setupWeb() {
- DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", "*");
- DefaultHeaders::Instance().addHeader("Access-Control-Allow-Headers", "*");
- DefaultHeaders::Instance().addHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
- server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
- request->send(200, "text/plain",
- "ESP32-S3 Async WebSocket running.\n"
- "WS endpoints: ws://<device-ip>/keyboard , /mouse , /bench");
- });
- auto handleOptions = [](AsyncWebServerRequest *request) {
- request->send(204);
- };
- server.on("/", HTTP_OPTIONS, handleOptions);
- server.on("/keyboard", HTTP_OPTIONS, handleOptions);
- server.on("/mouse", HTTP_OPTIONS, handleOptions);
- server.on("/bench", HTTP_OPTIONS, handleOptions);
- server.onNotFound([](AsyncWebServerRequest *request) {
- if (request->method() == HTTP_OPTIONS) {
- request->send(204);
- } else {
- request->send(404, "text/plain", "Not found");
- }
- });
- wsKeyboard.onEvent(handleWsEvent);
- wsMouse.onEvent(handleWsEvent);
- wsBench.onEvent([](AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len) {
- (void)server;
- (void)client;
- if (type == WS_EVT_DATA) {
- AwsFrameInfo *info = static_cast<AwsFrameInfo *>(arg);
- benchBytes += len;
- if (info->final) {
- benchCount++;
- }
- }
- });
- server.addHandler(&wsKeyboard);
- server.addHandler(&wsMouse);
- server.addHandler(&wsBench);
- server.begin();
- logMessage("HTTP+WebSocket server started on port 80");
- }
- void setupOTA() {
- ArduinoOTA
- .onStart([]() { logMessage("OTA start"); })
- .onEnd([]() { logMessage("OTA end"); })
- .onProgress([](unsigned int progress, unsigned int total) {
- char buf[48];
- snprintf(buf, sizeof(buf), "OTA progress %u%%", (progress * 100) / total);
- logMessage(String(buf));
- })
- .onError([](ota_error_t error) {
- String msg = "OTA error " + String(error);
- if (error == OTA_AUTH_ERROR) msg += " (auth)";
- else if (error == OTA_BEGIN_ERROR) msg += " (begin)";
- else if (error == OTA_CONNECT_ERROR) msg += " (connect)";
- else if (error == OTA_RECEIVE_ERROR) msg += " (receive)";
- else if (error == OTA_END_ERROR) msg += " (end)";
- logMessage(msg);
- });
- ArduinoOTA.begin();
- logMessage("OTA ready");
- }
- void setup() {
- Serial.begin(115200);
- delay(200);
- logMessage("Booting...");
- USB.begin();
- hidKeyboard.begin();
- hidMouse.begin();
- logMessage("USB HID ready");
- connectWiFi();
- udp.begin(LOCAL_UDP_PORT);
- logMessage("UDP logger ready, sending to " + LOG_HOST.toString() + ":" + String(LOG_PORT));
- syncTime();
- setupOTA();
- setupWeb();
- }
- void loop() {
- ArduinoOTA.handle();
- // Process mouse action queue
- mouse.processQueue();
- unsigned long now = millis();
- if (now - lastReportTime >= REPORT_INTERVAL_MS) {
- lastReportTime = now;
- logMessage("...");
- logMessage(mouse.getStats());
- if (0) {
- String uptimeMsg = "uptime_ms=" + String(now);
- wsKeyboard.textAll(uptimeMsg);
- wsMouse.textAll(uptimeMsg);
- logMessage("Sent uptime to WS clients: " + uptimeMsg);
- }
- if (0) {
- String benchMsg = "bench: msg/s=" + String(benchCount) + ", byte/s=" + String(benchBytes);
- wsBench.textAll(benchMsg);
- logMessage("Bench report: " + benchMsg);
- benchCount = 0;
- benchBytes = 0;
- }
- }
- }
Advertisement
Add Comment
Please, Sign In to add comment