josefkyrian

Untitled

Jan 15th, 2026
34
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
C++ 37.18 KB | None | 0 0
  1. // WiFi + Async WebSocket logger for ESP32-S3
  2. #include <WiFi.h>
  3. #include <WiFiUdp.h>
  4. #include <ArduinoOTA.h>
  5. #include <ArduinoJson.h>
  6. #include <AsyncTCP.h>
  7. #include <ESPAsyncWebServer.h>
  8. #include <esp_wifi.h>
  9. #include "USB.h"
  10. #include "USBHIDKeyboard.h"
  11. #include "USBHID.h"
  12. #include "class/hid/hid.h"
  13. #include <time.h>
  14. #include <sys/time.h>
  15. #include <stdlib.h>
  16. #include <deque>
  17.  
  18. // WiFi settings
  19. const char *WIFI_SSID = "eBayAir";
  20. const char *WIFI_PASS = "XXX";
  21.  
  22. // NTP settings
  23. const char *NTP_SERVER = "pool.ntp.org";
  24. const long GMT_OFFSET_SEC = 0;
  25. const int DAYLIGHT_OFFSET_SEC = 0;
  26.  
  27. // logging settings
  28. const bool LOGGING_ENABLED = false;
  29. const IPAddress LOG_HOST(192, 168, 2, 225);
  30. const uint16_t LOG_PORT = 4567;
  31. const uint16_t LOCAL_UDP_PORT = 4568;
  32. WiFiUDP udp;
  33.  
  34. void logMessage(const String &message) {
  35.     if (!LOGGING_ENABLED) return;
  36.     String payload = "[" + currentTimestamp() + "] " + message;
  37.     String payloadNl = payload + "\n";
  38.     Serial.println(payload);
  39.     if (WiFi.status() == WL_CONNECTED) {
  40.         udp.beginPacket(LOG_HOST, LOG_PORT);
  41.         udp.write(reinterpret_cast<const uint8_t *>(payloadNl.c_str()), payloadNl.length());
  42.         udp.endPacket();
  43.     }
  44. }
  45.  
  46. // Mouse button masks
  47. #define MOUSE_LEFT      0x01
  48. #define MOUSE_RIGHT     0x02
  49. #define MOUSE_MIDDLE    0x04
  50. #define MOUSE_BACKWARD  0x08
  51. #define MOUSE_FORWARD   0x10
  52. #define MOUSE_ALL       0x1F
  53.  
  54. // Reliable HID Mouse implementation with retry mechanism
  55. class ReliableHIDMouse: public USBHIDDevice {
  56. private:
  57.     USBHID hid;
  58.     uint8_t _buttons;
  59.    
  60.     // Pending report that failed to send
  61.     bool _hasPending;
  62.     hid_mouse_report_t _pendingReport;
  63.     unsigned long _lastRetryTime;
  64.     static const unsigned long RETRY_INTERVAL_US = 1000; // 1ms between retries
  65.    
  66.     // Rate limiting
  67.     unsigned long _lastSendTime;
  68.     unsigned long _minReportIntervalUs; // Minimum interval between reports
  69.    
  70.     bool sendReport(const hid_mouse_report_t &report) {
  71.         // Check if enough time has passed since last send attempt
  72.         unsigned long now = micros();
  73.         if (_lastSendTime > 0) {
  74.             unsigned long elapsed = now - _lastSendTime;
  75.             if (elapsed < _minReportIntervalUs) {
  76.                 // Too soon to send, always throttle regardless of success/failure
  77.                 return false;
  78.             }
  79.         }
  80.        
  81.         // Update timestamp regardless of success to enforce rate limit
  82.         _lastSendTime = now;
  83.  
  84.         bool success = hid.SendReport(HID_REPORT_ID_MOUSE, &report, sizeof(report));
  85.        
  86.         return success;
  87.     }
  88.    
  89. public:
  90.     ReliableHIDMouse(): hid(), _buttons(0), _hasPending(false), _lastRetryTime(0), _lastSendTime(0), _minReportIntervalUs(1000000/120/* max 120x/s */) {
  91.         static bool initialized = false;
  92.         if(!initialized){
  93.             initialized = true;
  94.             hid.addDevice(this, _onGetDescriptor(nullptr));
  95.         }
  96.     }
  97.    
  98.     // Set minimum interval between reports in microseconds
  99.     void setMinReportInterval(unsigned long intervalUs) {
  100.         _minReportIntervalUs = intervalUs;
  101.     }
  102.    
  103.     // Get current minimum interval setting
  104.     unsigned long getMinReportInterval() const {
  105.         return _minReportIntervalUs;
  106.     }
  107.    
  108.     uint16_t _onGetDescriptor(uint8_t* dst) {
  109.         static const uint8_t report_descriptor[] = {
  110.             TUD_HID_REPORT_DESC_MOUSE(HID_REPORT_ID(HID_REPORT_ID_MOUSE))
  111.         };
  112.         if (dst != nullptr) {
  113.             memcpy(dst, report_descriptor, sizeof(report_descriptor));
  114.         }
  115.         return sizeof(report_descriptor);
  116.     }
  117.    
  118.     void begin() {
  119.         hid.begin();
  120.     }
  121.    
  122.     void end() {
  123.     }
  124.    
  125.     // Try to send pending report if any
  126.     // Returns true if no pending report or if it was successfully sent
  127.     bool retryPending() {
  128.         if (!_hasPending) {
  129.             return true;
  130.         }
  131.        
  132.         // Throttle retry attempts
  133.         unsigned long now = micros();
  134.         if (now - _lastRetryTime < RETRY_INTERVAL_US) {
  135.             return false;
  136.         }
  137.         _lastRetryTime = now;
  138.        
  139.         if (sendReport(_pendingReport)) {
  140.             _hasPending = false;
  141.             return true;
  142.         }
  143.        
  144.         return false;
  145.     }
  146.    
  147.     bool hasPending() const {
  148.         return _hasPending;
  149.     }
  150.    
  151.     bool move(int8_t x, int8_t y, int8_t wheel = 0, int8_t pan = 0) {
  152.         // First retry any pending report
  153.         if (!retryPending()) {
  154.             return false; // Still has pending, can't send new report
  155.         }
  156.        
  157.         hid_mouse_report_t report = {
  158.             .buttons = _buttons,
  159.             .x       = x,
  160.             .y       = y,
  161.             .wheel   = wheel,
  162.             .pan     = pan
  163.         };
  164.        
  165.         if (sendReport(report)) {
  166.             return true;
  167.         }
  168.        
  169.         // Failed to send - store as pending ONLY if we don't already have one
  170.         if (!_hasPending) {
  171.             _pendingReport = report;
  172.             _hasPending = true;
  173.             _lastRetryTime = micros();
  174.         }
  175.         return false;
  176.     }
  177.    
  178.     bool click(uint8_t b = MOUSE_LEFT) {
  179.         uint8_t oldButtons = _buttons;
  180.         _buttons = b;
  181.         bool success1 = move(0, 0);
  182.         _buttons = 0;
  183.         bool success2 = move(0, 0);
  184.         if (!success1 || !success2) {
  185.             _buttons = oldButtons; // Restore on failure
  186.             return false;
  187.         }
  188.         return true;
  189.     }
  190.    
  191.     bool press(uint8_t b = MOUSE_LEFT) {
  192.         if ((_buttons & b) == b) {
  193.             return true; // Already pressed
  194.         }
  195.         _buttons |= b;
  196.         return move(0, 0);
  197.     }
  198.    
  199.     bool release(uint8_t b = MOUSE_LEFT) {
  200.         if ((_buttons & b) == 0) {
  201.             return true; // Already released
  202.         }
  203.         _buttons &= ~b;
  204.         return move(0, 0);
  205.     }
  206.    
  207.     bool isPressed(uint8_t b = MOUSE_LEFT) const {
  208.         return (_buttons & b) != 0;
  209.     }
  210. };
  211.  
  212. // Web settings
  213. AsyncWebServer server(80);
  214. AsyncWebSocket wsKeyboard("/keyboard");
  215. AsyncWebSocket wsMouse("/mouse");
  216. AsyncWebSocket wsBench("/bench");
  217.  
  218. unsigned long lastReportTime = 0;
  219. const unsigned long REPORT_INTERVAL_MS = 1000;
  220. size_t benchCount = 0;
  221. size_t benchBytes = 0;
  222. USBHIDKeyboard hidKeyboard;
  223. ReliableHIDMouse hidMouse;
  224.  
  225. // Mouse action queue
  226. enum MouseActionType {
  227.     MOUSE_ACTION_MOVE,
  228.     MOUSE_ACTION_CLICK,
  229.     MOUSE_ACTION_PRESS,
  230.     MOUSE_ACTION_RELEASE
  231. };
  232.  
  233. struct MouseAction {
  234.     MouseActionType type;
  235.     int dx;
  236.     int dy;
  237.     int wheel;
  238.     int pan;
  239.     uint8_t button;
  240. };
  241.  
  242. struct MouseController {
  243.     std::deque<MouseAction> queue;
  244.    
  245.     // Statistics
  246.     size_t totalActions = 0;
  247.     size_t totalEnqueued = 0;
  248.     size_t totalProcessed = 0;
  249.    
  250.     void move(bool immediate, int dx, int dy, int wheel = 0, int pan = 0) {
  251.         totalActions++;
  252.  
  253.         if (immediate) {
  254.             // Immediate mode: try to send, if it fails, enqueue it
  255.             if (!hidMouse.move(dx, dy, wheel, pan)) {
  256.                 enqueueMove(dx, dy, wheel, pan);
  257.             }
  258.         } else {
  259.             enqueueMove(dx, dy, wheel, pan);
  260.         }
  261.     }
  262.    
  263.     void click(bool immediate, uint8_t button) {
  264.         totalActions++;
  265.  
  266.         if (immediate) {
  267.             if (!hidMouse.click(button)) {
  268.                 enqueueAction(MOUSE_ACTION_CLICK, button);
  269.             }
  270.         } else {
  271.             enqueueAction(MOUSE_ACTION_CLICK, button);
  272.         }
  273.     }
  274.    
  275.     void press(bool immediate, uint8_t button) {
  276.         totalActions++;
  277.        
  278.         if (immediate) {
  279.             if (!hidMouse.press(button)) {
  280.                 enqueueAction(MOUSE_ACTION_PRESS, button);
  281.             }
  282.         } else {
  283.             enqueueAction(MOUSE_ACTION_PRESS, button);
  284.         }
  285.     }
  286.    
  287.     void release(bool immediate, uint8_t button) {
  288.         totalActions++;
  289.        
  290.         if (immediate) {
  291.             if (!hidMouse.release(button)) {
  292.                 enqueueAction(MOUSE_ACTION_RELEASE, button);
  293.             }
  294.         } else {
  295.             enqueueAction(MOUSE_ACTION_RELEASE, button);
  296.         }
  297.     }
  298.    
  299.     void processQueue() {
  300.         // First, try to retry any pending report in the HID layer
  301.         if (!hidMouse.retryPending()) {
  302.             return; // Still has pending report, can't process queue yet
  303.         }
  304.        
  305.         // Now process queue items
  306.         while (!queue.empty()) {
  307.             MouseAction &action = queue.front();
  308.            
  309.             switch (action.type) {
  310.                 case MOUSE_ACTION_MOVE:
  311.                     hidMouse.move(action.dx, action.dy, action.wheel, action.pan);
  312.                     break;
  313.                 case MOUSE_ACTION_CLICK:
  314.                     hidMouse.click(action.button);
  315.                     break;
  316.                 case MOUSE_ACTION_PRESS:
  317.                     hidMouse.press(action.button);
  318.                     break;
  319.                 case MOUSE_ACTION_RELEASE:
  320.                     hidMouse.release(action.button);
  321.                     break;
  322.             }
  323.            
  324.             // Always remove from queue, rely on pending mechanism in hidMouse
  325.             queue.pop_front();
  326.             totalProcessed++;
  327.         }
  328.     }
  329.    
  330.     String getStats() {
  331.         String stats = "Mouse stats:";
  332.         stats += "\n total actions=" + String(totalActions);
  333.         stats += "\n QUEUE:";
  334.         stats += "\n  total=" + String(totalEnqueued) + " (processed=" + String(totalProcessed) + ", queue=" + String(queue.size()) + ")";
  335.         stats += "\n  pending=" + String(hidMouse.hasPending() ? "yes" : "no");
  336.         return stats;
  337.     }
  338.    
  339. private:
  340.     void enqueueMove(int dx, int dy, int wheel, int pan) {
  341.         // Check if last action in queue is also a move - if yes, merge them
  342.         if (!queue.empty() && queue.back().type == MOUSE_ACTION_MOVE) {
  343.             // Merge with last move action
  344.             queue.back().dx += dx;
  345.             queue.back().dy += dy;
  346.             queue.back().wheel += wheel;
  347.             queue.back().pan += pan;
  348.             return;
  349.         }
  350.        
  351.         // Add new move action
  352.         enqueueAction(MOUSE_ACTION_MOVE, 0, dx, dy, wheel, pan);
  353.     }
  354.    
  355.     void enqueueAction(MouseActionType type, uint8_t button, int dx = 0, int dy = 0, int wheel = 0, int pan = 0) {
  356.         MouseAction action;
  357.         action.type = type;
  358.         action.button = button;
  359.         action.dx = dx;
  360.         action.dy = dy;
  361.         action.wheel = wheel;
  362.         action.pan = pan;
  363.        
  364.         queue.push_back(action);
  365.         totalEnqueued++;
  366.     }
  367. };
  368.  
  369. MouseController mouse;
  370.  
  371. struct IntToHid {
  372.     int code;
  373.     uint8_t hid;
  374. };
  375.  
  376. bool parseIntField(JsonVariantConst v, int &out) {
  377.     if (v.isNull()) return false;
  378.     if (v.is<long>()) {
  379.         out = v.as<long>();
  380.         return true;
  381.     }
  382.     if (v.is<int>()) {
  383.         out = v.as<int>();
  384.         return true;
  385.     }
  386.     if (v.is<const char *>()) {
  387.         const char *s = v.as<const char *>();
  388.         if (!s) return false;
  389.         char *endp = nullptr;
  390.         long val = strtol(s, &endp, 0);
  391.         if (endp && *endp == '\0') {
  392.             out = static_cast<int>(val);
  393.             return true;
  394.         }
  395.     }
  396.     return false;
  397. }
  398.  
  399. uint8_t mapBrowserCode(const String &codeStr) {
  400.     if (codeStr.length() >= 3 && codeStr.startsWith("Key")) {
  401.         char c = codeStr.charAt(3);
  402.         if (c >= 'A' && c <= 'Z') return HID_KEY_A + (c - 'A');
  403.     }
  404.     if (codeStr.length() >= 5 && codeStr.startsWith("Digit")) {
  405.         char d = codeStr.charAt(5);
  406.         if (d == '0') return HID_KEY_0;
  407.         if (d >= '1' && d <= '9') return HID_KEY_1 + (d - '1');
  408.     }
  409.     if (codeStr.startsWith("Numpad")) {
  410.         String tail = codeStr.substring(6);
  411.         if (tail == "Enter") return HID_KEY_KEYPAD_ENTER;
  412.         if (tail == "Add") return HID_KEY_KEYPAD_ADD;
  413.         if (tail == "Subtract") return HID_KEY_KEYPAD_SUBTRACT;
  414.         if (tail == "Multiply") return HID_KEY_KEYPAD_MULTIPLY;
  415.         if (tail == "Divide") return HID_KEY_KEYPAD_DIVIDE;
  416.         if (tail == "Decimal") return HID_KEY_KEYPAD_DECIMAL;
  417.         if (tail.length() == 1 && tail[0] >= '0' && tail[0] <= '9') {
  418.             return HID_KEY_KEYPAD_0 + (tail[0] - '0');
  419.         }
  420.     }
  421.     if (codeStr.startsWith("F") && codeStr.length() <= 3) {
  422.         int n = codeStr.substring(1).toInt();
  423.         if (n >= 1 && n <= 24) return HID_KEY_F1 + (n - 1);
  424.     }
  425.  
  426.     if (codeStr == "Space") return HID_KEY_SPACE;
  427.     if (codeStr == "Enter") return HID_KEY_ENTER;
  428.     if (codeStr == "Escape") return HID_KEY_ESCAPE;
  429.     if (codeStr == "Backspace") return HID_KEY_BACKSPACE;
  430.     if (codeStr == "Tab") return HID_KEY_TAB;
  431.     if (codeStr == "Minus") return HID_KEY_MINUS;
  432.     if (codeStr == "Equal") return HID_KEY_EQUAL;
  433.     if (codeStr == "BracketLeft") return HID_KEY_BRACKET_LEFT;
  434.     if (codeStr == "BracketRight") return HID_KEY_BRACKET_RIGHT;
  435.     if (codeStr == "Backslash") return HID_KEY_BACKSLASH;
  436.     if (codeStr == "Semicolon") return HID_KEY_SEMICOLON;
  437.     if (codeStr == "Quote") return HID_KEY_APOSTROPHE;
  438.     if (codeStr == "Backquote") return HID_KEY_GRAVE;
  439.     if (codeStr == "Comma") return HID_KEY_COMMA;
  440.     if (codeStr == "Period") return HID_KEY_PERIOD;
  441.     if (codeStr == "Slash") return HID_KEY_SLASH;
  442.     if (codeStr == "CapsLock") return HID_KEY_CAPS_LOCK;
  443.     if (codeStr == "ShiftLeft") return HID_KEY_SHIFT_LEFT;
  444.     if (codeStr == "ShiftRight") return HID_KEY_SHIFT_RIGHT;
  445.     if (codeStr == "ControlLeft") return HID_KEY_CONTROL_LEFT;
  446.     if (codeStr == "ControlRight") return HID_KEY_CONTROL_RIGHT;
  447.     if (codeStr == "AltLeft") return HID_KEY_ALT_LEFT;
  448.     if (codeStr == "AltRight") return HID_KEY_ALT_RIGHT;
  449.     if (codeStr == "ArrowRight") return HID_KEY_ARROW_RIGHT;
  450.     if (codeStr == "ArrowLeft") return HID_KEY_ARROW_LEFT;
  451.     if (codeStr == "ArrowDown") return HID_KEY_ARROW_DOWN;
  452.     if (codeStr == "ArrowUp") return HID_KEY_ARROW_UP;
  453.     if (codeStr == "Insert") return HID_KEY_INSERT;
  454.     if (codeStr == "Home") return HID_KEY_HOME;
  455.     if (codeStr == "PageUp") return HID_KEY_PAGE_UP;
  456.     if (codeStr == "Delete") return HID_KEY_DELETE;
  457.     if (codeStr == "End") return HID_KEY_END;
  458.     if (codeStr == "PageDown") return HID_KEY_PAGE_DOWN;
  459.     if (codeStr == "PrintScreen") return HID_KEY_PRINT_SCREEN;
  460.     if (codeStr == "ScrollLock") return HID_KEY_SCROLL_LOCK;
  461.     if (codeStr == "Pause") return HID_KEY_PAUSE;
  462.     if (codeStr == "NumLock") return HID_KEY_NUM_LOCK;
  463.     if (codeStr == "MetaLeft" || codeStr == "OSLeft") return HID_KEY_GUI_LEFT;
  464.     if (codeStr == "MetaRight" || codeStr == "OSRight") return HID_KEY_GUI_RIGHT;
  465.     return 0;
  466. }
  467.  
  468. String normalizeKeyFlat(String s) {
  469.     s.trim();
  470.     s.toUpperCase();
  471.     s.replace("-", "");
  472.     s.replace("_", "");
  473.     s.replace(" ", "");
  474.     return s;
  475. }
  476.  
  477. uint8_t evdevToHid(int evdevCode) {
  478.     static const IntToHid map[] = {
  479.         {1, HID_KEY_ESCAPE},
  480.         {2, HID_KEY_1}, {3, HID_KEY_2}, {4, HID_KEY_3}, {5, HID_KEY_4}, {6, HID_KEY_5},
  481.         {7, HID_KEY_6}, {8, HID_KEY_7}, {9, HID_KEY_8}, {10, HID_KEY_9}, {11, HID_KEY_0},
  482.         {12, HID_KEY_MINUS}, {13, HID_KEY_EQUAL}, {14, HID_KEY_BACKSPACE}, {15, HID_KEY_TAB},
  483.         {16, HID_KEY_Q}, {17, HID_KEY_W}, {18, HID_KEY_E}, {19, HID_KEY_R}, {20, HID_KEY_T},
  484.         {21, HID_KEY_Y}, {22, HID_KEY_U}, {23, HID_KEY_I}, {24, HID_KEY_O}, {25, HID_KEY_P},
  485.         {26, HID_KEY_BRACKET_LEFT}, {27, HID_KEY_BRACKET_RIGHT}, {28, HID_KEY_ENTER},
  486.         {30, HID_KEY_A}, {31, HID_KEY_S}, {32, HID_KEY_D}, {33, HID_KEY_F}, {34, HID_KEY_G},
  487.         {35, HID_KEY_H}, {36, HID_KEY_J}, {37, HID_KEY_K}, {38, HID_KEY_L},
  488.         {39, HID_KEY_SEMICOLON}, {40, HID_KEY_APOSTROPHE}, {41, HID_KEY_GRAVE},
  489.         {42, HID_KEY_SHIFT_LEFT}, {43, HID_KEY_BACKSLASH},
  490.         {44, HID_KEY_Z}, {45, HID_KEY_X}, {46, HID_KEY_C}, {47, HID_KEY_V}, {48, HID_KEY_B},
  491.         {49, HID_KEY_N}, {50, HID_KEY_M},
  492.         {51, HID_KEY_COMMA}, {52, HID_KEY_PERIOD}, {53, HID_KEY_SLASH},
  493.         {54, HID_KEY_SHIFT_RIGHT}, {55, HID_KEY_KEYPAD_MULTIPLY}, {56, HID_KEY_ALT_LEFT}, {57, HID_KEY_SPACE},
  494.         {58, HID_KEY_CAPS_LOCK},
  495.         {59, HID_KEY_F1}, {60, HID_KEY_F2}, {61, HID_KEY_F3}, {62, HID_KEY_F4}, {63, HID_KEY_F5},
  496.         {64, HID_KEY_F6}, {65, HID_KEY_F7}, {66, HID_KEY_F8}, {67, HID_KEY_F9}, {68, HID_KEY_F10},
  497.         {69, HID_KEY_NUM_LOCK}, {70, HID_KEY_SCROLL_LOCK},
  498.         {71, HID_KEY_KEYPAD_7}, {72, HID_KEY_KEYPAD_8}, {73, HID_KEY_KEYPAD_9}, {74, HID_KEY_KEYPAD_SUBTRACT},
  499.         {75, HID_KEY_KEYPAD_4}, {76, HID_KEY_KEYPAD_5}, {77, HID_KEY_KEYPAD_6}, {78, HID_KEY_KEYPAD_ADD},
  500.         {79, HID_KEY_KEYPAD_1}, {80, HID_KEY_KEYPAD_2}, {81, HID_KEY_KEYPAD_3}, {82, HID_KEY_KEYPAD_0},
  501.         {83, HID_KEY_KEYPAD_DECIMAL},
  502.         {87, HID_KEY_F11}, {88, HID_KEY_F12},
  503.         {96, HID_KEY_KEYPAD_ENTER}, {97, HID_KEY_CONTROL_RIGHT}, {98, HID_KEY_KEYPAD_DIVIDE},
  504.         {99, HID_KEY_PRINT_SCREEN}, {100, HID_KEY_ALT_RIGHT},
  505.         {102, HID_KEY_HOME}, {103, HID_KEY_ARROW_UP}, {104, HID_KEY_PAGE_UP},
  506.         {105, HID_KEY_ARROW_LEFT}, {106, HID_KEY_ARROW_RIGHT},
  507.         {107, HID_KEY_END}, {108, HID_KEY_ARROW_DOWN}, {109, HID_KEY_PAGE_DOWN},
  508.         {110, HID_KEY_INSERT}, {111, HID_KEY_DELETE},
  509.         {117, HID_KEY_KEYPAD_EQUAL}, {119, HID_KEY_PAUSE},
  510.         {121, HID_KEY_KEYPAD_COMMA}, {125, HID_KEY_GUI_LEFT}, {126, HID_KEY_GUI_RIGHT},
  511.         {139, HID_KEY_APPLICATION}, {86, HID_KEY_EUROPE_1}
  512.     };
  513.     for (const auto &entry : map) {
  514.         if (entry.code == evdevCode) return entry.hid;
  515.     }
  516.     return 0;
  517. }
  518.  
  519. uint8_t systemToHid(int systemCode) {
  520.     if (systemCode >= 65 && systemCode <= 90) {
  521.         return HID_KEY_A + (systemCode - 65);
  522.     }
  523.     if (systemCode >= 48 && systemCode <= 57) { // digits 0-9
  524.         if (systemCode == 48) return HID_KEY_0;
  525.         return HID_KEY_1 + (systemCode - 49);
  526.     }
  527.     if (systemCode >= 96 && systemCode <= 105) { // numpad 0-9
  528.         return HID_KEY_KEYPAD_0 + (systemCode - 96);
  529.     }
  530.     if (systemCode >= 112 && systemCode <= 135) { // F1-F24
  531.         return HID_KEY_F1 + (systemCode - 112);
  532.     }
  533.  
  534.     static const IntToHid map[] = {
  535.         {8, HID_KEY_BACKSPACE}, {9, HID_KEY_TAB}, {13, HID_KEY_ENTER},
  536.         {16, HID_KEY_SHIFT_LEFT}, {17, HID_KEY_CONTROL_LEFT}, {18, HID_KEY_ALT_LEFT},
  537.         {19, HID_KEY_PAUSE}, {20, HID_KEY_CAPS_LOCK}, {27, HID_KEY_ESCAPE}, {32, HID_KEY_SPACE},
  538.         {33, HID_KEY_PAGE_UP}, {34, HID_KEY_PAGE_DOWN}, {35, HID_KEY_END}, {36, HID_KEY_HOME},
  539.         {37, HID_KEY_ARROW_LEFT}, {38, HID_KEY_ARROW_UP}, {39, HID_KEY_ARROW_RIGHT}, {40, HID_KEY_ARROW_DOWN},
  540.         {44, HID_KEY_PRINT_SCREEN}, {45, HID_KEY_INSERT}, {46, HID_KEY_DELETE},
  541.         {91, HID_KEY_GUI_LEFT}, {92, HID_KEY_GUI_RIGHT}, {93, HID_KEY_APPLICATION},
  542.         {106, HID_KEY_KEYPAD_MULTIPLY}, {107, HID_KEY_KEYPAD_ADD},
  543.         {109, HID_KEY_KEYPAD_SUBTRACT}, {110, HID_KEY_KEYPAD_DECIMAL}, {111, HID_KEY_KEYPAD_DIVIDE},
  544.         {144, HID_KEY_NUM_LOCK}, {145, HID_KEY_SCROLL_LOCK},
  545.         {186, HID_KEY_SEMICOLON}, {187, HID_KEY_EQUAL}, {188, HID_KEY_COMMA},
  546.         {189, HID_KEY_MINUS}, {190, HID_KEY_PERIOD}, {191, HID_KEY_SLASH},
  547.         {192, HID_KEY_GRAVE}, {219, HID_KEY_BRACKET_LEFT}, {220, HID_KEY_BACKSLASH},
  548.         {221, HID_KEY_BRACKET_RIGHT}, {222, HID_KEY_APOSTROPHE},
  549.         {173, HID_KEY_MINUS} // some browsers report 173 for minus
  550.     };
  551.     for (const auto &entry : map) {
  552.         if (entry.code == systemCode) return entry.hid;
  553.     }
  554.     return 0;
  555. }
  556.  
  557. uint8_t nameToHid(const String &name) {
  558.     if (name.length() == 0) return 0;
  559.  
  560.     uint8_t byBrowserCode = mapBrowserCode(name);
  561.     if (byBrowserCode) return byBrowserCode;
  562.  
  563.     String upper = name;
  564.     upper.trim();
  565.     if (upper.startsWith("KEY_")) return nameToHid(upper.substring(4));
  566.     if (upper.startsWith("HID_KEY_")) return nameToHid(upper.substring(8));
  567.  
  568.     upper.replace("-", "_");
  569.     upper.replace(" ", "_");
  570.     upper.toUpperCase();
  571.  
  572.     String flat = normalizeKeyFlat(upper);
  573.  
  574.     if (upper.length() == 1) {
  575.         char c = upper.charAt(0);
  576.         if (c >= 'A' && c <= 'Z') return HID_KEY_A + (c - 'A');
  577.         if (c >= '0' && c <= '9') return (c == '0') ? HID_KEY_0 : HID_KEY_1 + (c - '1');
  578.     }
  579.  
  580.     if (upper.startsWith("F")) {
  581.         int n = upper.substring(1).toInt();
  582.         if (n >= 1 && n <= 24) return HID_KEY_F1 + (n - 1);
  583.     }
  584.  
  585.     if (upper.startsWith("NUMPAD")) {
  586.         String tail = upper.substring(6);
  587.         if (tail == "ENTER") return HID_KEY_KEYPAD_ENTER;
  588.         if (tail == "ADD" || tail == "PLUS") return HID_KEY_KEYPAD_ADD;
  589.         if (tail == "SUBTRACT" || tail == "MINUS") return HID_KEY_KEYPAD_SUBTRACT;
  590.         if (tail == "MULTIPLY" || tail == "STAR") return HID_KEY_KEYPAD_MULTIPLY;
  591.         if (tail == "DIVIDE" || tail == "SLASH") return HID_KEY_KEYPAD_DIVIDE;
  592.         if (tail == "DECIMAL" || tail == "DOT") return HID_KEY_KEYPAD_DECIMAL;
  593.         if (tail.length() == 1 && tail[0] >= '0' && tail[0] <= '9') {
  594.             return HID_KEY_KEYPAD_0 + (tail[0] - '0');
  595.         }
  596.     }
  597.  
  598.     if (flat == "ESC" || flat == "ESCAPE") return HID_KEY_ESCAPE;
  599.     if (flat == "BACKSPACE" || flat == "BS") return HID_KEY_BACKSPACE;
  600.     if (flat == "TAB") return HID_KEY_TAB;
  601.     if (flat == "ENTER" || flat == "RETURN") return HID_KEY_ENTER;
  602.     if (flat == "SPACE" || flat == "SPACEBAR") return HID_KEY_SPACE;
  603.     if (flat == "CAPSLOCK") return HID_KEY_CAPS_LOCK;
  604.     if (flat == "PRINTSCREEN" || flat == "PRNTSCRN" || flat == "SYSRQ") return HID_KEY_PRINT_SCREEN;
  605.     if (flat == "SCROLLLOCK") return HID_KEY_SCROLL_LOCK;
  606.     if (flat == "PAUSE" || flat == "BREAK") return HID_KEY_PAUSE;
  607.  
  608.     if (flat == "LEFTSHIFT" || flat == "SHIFTLEFT" || flat == "LSHIFT") return HID_KEY_SHIFT_LEFT;
  609.     if (flat == "RIGHTSHIFT" || flat == "SHIFTRIGHT" || flat == "RSHIFT") return HID_KEY_SHIFT_RIGHT;
  610.     if (flat == "LEFTCTRL" || flat == "CTRLLEFT" || flat == "LCTRL" || flat == "CONTROLLEFT") return HID_KEY_CONTROL_LEFT;
  611.     if (flat == "RIGHTCTRL" || flat == "CTRLRIGHT" || flat == "RCTRL" || flat == "CONTROLRIGHT") return HID_KEY_CONTROL_RIGHT;
  612.     if (flat == "LEFTALT" || flat == "ALTLEFT" || flat == "LALT") return HID_KEY_ALT_LEFT;
  613.     if (flat == "RIGHTALT" || flat == "ALTRIGHT" || flat == "RALT" || flat == "ALTGR") return HID_KEY_ALT_RIGHT;
  614.     if (flat == "LEFTMETA" || flat == "METALEFT" || flat == "LWIN" || flat == "LGUI" || flat == "CMDLEFT") return HID_KEY_GUI_LEFT;
  615.     if (flat == "RIGHTMETA" || flat == "METARIGHT" || flat == "RWIN" || flat == "RGUI" || flat == "CMDRIGHT") return HID_KEY_GUI_RIGHT;
  616.  
  617.     if (flat == "UP" || flat == "ARROWUP" || flat == "UPARROW") return HID_KEY_ARROW_UP;
  618.     if (flat == "DOWN" || flat == "ARROWDOWN" || flat == "DOWNARROW") return HID_KEY_ARROW_DOWN;
  619.     if (flat == "LEFT" || flat == "ARROWLEFT" || flat == "LEFTARROW") return HID_KEY_ARROW_LEFT;
  620.     if (flat == "RIGHT" || flat == "ARROWRIGHT" || flat == "RIGHTARROW") return HID_KEY_ARROW_RIGHT;
  621.  
  622.     if (flat == "DELETE" || flat == "DEL") return HID_KEY_DELETE;
  623.     if (flat == "HOME") return HID_KEY_HOME;
  624.     if (flat == "END") return HID_KEY_END;
  625.     if (flat == "PAGEUP") return HID_KEY_PAGE_UP;
  626.     if (flat == "PAGEDOWN") return HID_KEY_PAGE_DOWN;
  627.     if (flat == "INSERT" || flat == "INS") return HID_KEY_INSERT;
  628.     if (flat == "NUMLOCK") return HID_KEY_NUM_LOCK;
  629.  
  630.     if (flat == "MINUS" || flat == "HYPHEN") return HID_KEY_MINUS;
  631.     if (flat == "EQUAL" || flat == "EQUALS") return HID_KEY_EQUAL;
  632.     if (flat == "BRACKETLEFT" || flat == "LBRACKET" || flat == "LEFTBRACKET") return HID_KEY_BRACKET_LEFT;
  633.     if (flat == "BRACKETRIGHT" || flat == "RBRACKET" || flat == "RIGHTBRACKET") return HID_KEY_BRACKET_RIGHT;
  634.     if (flat == "BACKSLASH") return HID_KEY_BACKSLASH;
  635.     if (flat == "SEMICOLON") return HID_KEY_SEMICOLON;
  636.     if (flat == "APOSTROPHE" || flat == "QUOTE") return HID_KEY_APOSTROPHE;
  637.     if (flat == "GRAVE" || flat == "BACKQUOTE" || flat == "BACKTICK") return HID_KEY_GRAVE;
  638.     if (flat == "COMMA") return HID_KEY_COMMA;
  639.     if (flat == "PERIOD" || flat == "DOT") return HID_KEY_PERIOD;
  640.     if (flat == "SLASH" || flat == "FORWARDSLASH") return HID_KEY_SLASH;
  641.  
  642.     if (flat == "APPLICATION" || flat == "MENU") return HID_KEY_APPLICATION;
  643.     if (flat == "POWER") return HID_KEY_POWER;
  644.  
  645.     static const IntToHid evdevNameMap[] = {
  646.         {1, HID_KEY_ESCAPE}, {14, HID_KEY_BACKSPACE}, {15, HID_KEY_TAB}, {28, HID_KEY_ENTER}, {57, HID_KEY_SPACE},
  647.         {12, HID_KEY_MINUS}, {13, HID_KEY_EQUAL}, {26, HID_KEY_BRACKET_LEFT}, {27, HID_KEY_BRACKET_RIGHT}, {43, HID_KEY_BACKSLASH},
  648.         {39, HID_KEY_SEMICOLON}, {40, HID_KEY_APOSTROPHE}, {41, HID_KEY_GRAVE}, {51, HID_KEY_COMMA}, {52, HID_KEY_PERIOD}, {53, HID_KEY_SLASH},
  649.         {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},
  650.         {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},
  651.         {99, HID_KEY_PRINT_SCREEN}, {70, HID_KEY_SCROLL_LOCK}, {119, HID_KEY_PAUSE},
  652.         {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},
  653.         {106, HID_KEY_ARROW_RIGHT}, {105, HID_KEY_ARROW_LEFT}, {108, HID_KEY_ARROW_DOWN}, {103, HID_KEY_ARROW_UP},
  654.         {69, HID_KEY_NUM_LOCK}
  655.     };
  656.  
  657.     if (upper.startsWith("KEY_")) upper = upper.substring(4);
  658.     if (upper.startsWith("EVDEV_")) upper = upper.substring(6);
  659.     String upperFlat = normalizeKeyFlat(upper);
  660.     for (const auto &entry : evdevNameMap) {
  661.         String evName;
  662.         switch (entry.code) {
  663.         case 1: evName = "ESC"; break;
  664.         case 14: evName = "BACKSPACE"; break;
  665.         case 15: evName = "TAB"; break;
  666.         case 28: evName = "ENTER"; break;
  667.         case 57: evName = "SPACE"; break;
  668.         case 12: evName = "MINUS"; break;
  669.         case 13: evName = "EQUAL"; break;
  670.         case 26: evName = "LEFTBRACE"; break;
  671.         case 27: evName = "RIGHTBRACE"; break;
  672.         case 43: evName = "BACKSLASH"; break;
  673.         case 39: evName = "SEMICOLON"; break;
  674.         case 40: evName = "APOSTROPHE"; break;
  675.         case 41: evName = "GRAVE"; break;
  676.         case 51: evName = "COMMA"; break;
  677.         case 52: evName = "DOT"; break;
  678.         case 53: evName = "SLASH"; break;
  679.         case 58: evName = "CAPSLOCK"; break;
  680.         case 59: evName = "F1"; break;
  681.         case 60: evName = "F2"; break;
  682.         case 61: evName = "F3"; break;
  683.         case 62: evName = "F4"; break;
  684.         case 63: evName = "F5"; break;
  685.         case 64: evName = "F6"; break;
  686.         case 65: evName = "F7"; break;
  687.         case 66: evName = "F8"; break;
  688.         case 67: evName = "F9"; break;
  689.         case 68: evName = "F10"; break;
  690.         case 87: evName = "F11"; break;
  691.         case 88: evName = "F12"; break;
  692.         case 99: evName = "SYSRQ"; break;
  693.         case 70: evName = "SCROLLLOCK"; break;
  694.         case 119: evName = "PAUSE"; break;
  695.         case 110: evName = "INSERT"; break;
  696.         case 102: evName = "HOME"; break;
  697.         case 104: evName = "PAGEUP"; break;
  698.         case 111: evName = "DELETE"; break;
  699.         case 107: evName = "END"; break;
  700.         case 109: evName = "PAGEDOWN"; break;
  701.         case 106: evName = "RIGHT"; break;
  702.         case 105: evName = "LEFT"; break;
  703.         case 108: evName = "DOWN"; break;
  704.         case 103: evName = "UP"; break;
  705.         case 69: evName = "NUMLOCK"; break;
  706.         default: break;
  707.         }
  708.         if (evName.length() > 0 && upperFlat == normalizeKeyFlat(evName)) {
  709.             return entry.hid;
  710.         }
  711.     }
  712.  
  713.     // As a last resort, check evdev numeric names like KEY_30 etc.
  714.     if (upper.startsWith("KEY")) {
  715.         int n = upper.substring(3).toInt();
  716.         if (n > 0) {
  717.             uint8_t evMap = evdevToHid(n);
  718.             if (evMap) return evMap;
  719.         }
  720.     }
  721.  
  722.     return 0;
  723. }
  724.  
  725. uint8_t resolveHidFromPayload(JsonVariantConst node, String *reason = nullptr) {
  726.     int raw = 0;
  727.     if (parseIntField(node["hid_code"], raw) && raw >= 0 && raw <= 0xFF) {
  728.         if (reason) *reason = "hid_code";
  729.         return static_cast<uint8_t>(raw);
  730.     }
  731.  
  732.     if (parseIntField(node["evdev_code"], raw)) {
  733.         uint8_t mapped = evdevToHid(raw);
  734.         if (mapped) {
  735.             if (reason) *reason = "evdev_code=" + String(raw);
  736.             return mapped;
  737.         }
  738.     }
  739.  
  740.     if (parseIntField(node["system_keycode"], raw)) {
  741.         uint8_t mapped = systemToHid(raw);
  742.         if (mapped) {
  743.             if (reason) *reason = "system_keycode=" + String(raw);
  744.             return mapped;
  745.         }
  746.     }
  747.  
  748.     String name = node["key_name"] | "";
  749.     if (name.length() > 0) {
  750.         uint8_t mapped = nameToHid(name);
  751.         if (mapped) {
  752.             if (reason) *reason = "key_name=" + name;
  753.             return mapped;
  754.         }
  755.     }
  756.  
  757.     return 0;
  758. }
  759.  
  760. String currentTimestamp() {
  761.     struct timeval tv;
  762.     gettimeofday(&tv, nullptr);
  763.  
  764.     time_t now = tv.tv_sec;
  765.     struct tm timeinfo;
  766.     localtime_r(&now, &timeinfo);
  767.  
  768.     char buffer[40];
  769.     snprintf(buffer, sizeof(buffer),
  770.                      "%04d-%02d-%02d %02d:%02d:%02d.%03ld",
  771.                      timeinfo.tm_year + 1900,
  772.                      timeinfo.tm_mon + 1,
  773.                      timeinfo.tm_mday,
  774.                      timeinfo.tm_hour,
  775.                      timeinfo.tm_min,
  776.                      timeinfo.tm_sec,
  777.                      tv.tv_usec / 1000);
  778.     return String(buffer);
  779. }
  780.  
  781. void connectWiFi() {
  782.     WiFi.mode(WIFI_STA);
  783.     WiFi.setSleep(false);
  784.     esp_wifi_set_ps(WIFI_PS_NONE);
  785.     WiFi.begin(WIFI_SSID, WIFI_PASS);
  786.     logMessage("Connecting to WiFi...");
  787.  
  788.     unsigned long start = millis();
  789.     while (WiFi.status() != WL_CONNECTED) {
  790.         delay(250);
  791.         if (millis() - start > 20000) {
  792.             logMessage("WiFi connection timeout, retrying...");
  793.             start = millis();
  794.             WiFi.disconnect();
  795.             WiFi.begin(WIFI_SSID, WIFI_PASS);
  796.         }
  797.     }
  798.  
  799.     logMessage("Connected to WiFi, IP: " + WiFi.localIP().toString());
  800. }
  801.  
  802. void syncTime() {
  803.     configTime(GMT_OFFSET_SEC, DAYLIGHT_OFFSET_SEC, NTP_SERVER);
  804.     logMessage("Waiting for NTP sync...");
  805.  
  806.     struct tm timeinfo;
  807.     for (int i = 0; i < 20; i++) {
  808.         if (getLocalTime(&timeinfo)) {
  809.             logMessage("Time synced: " + currentTimestamp());
  810.             return;
  811.         }
  812.         delay(500);
  813.     }
  814.     logMessage("Failed to sync time via NTP");
  815. }
  816.  
  817. String wsClientLabel(AsyncWebSocketClient *client) {
  818.     if (!client) return "unknown";
  819.     return "#" + String(client->id()) + " " + client->remoteIP().toString();
  820. }
  821.  
  822. String wsChannel(AsyncWebSocket *server) {
  823.     if (server == &wsKeyboard) return "keyboard";
  824.     if (server == &wsMouse) return "mouse";
  825.     if (server == &wsBench) return "bench";
  826.     return server ? server->url() : "unknown";
  827. }
  828.  
  829. void handleMouseJson(const JsonDocument &doc, bool logFlag, JsonObject responseData = JsonObject()) {
  830.     const char *action = doc["action"] | "";
  831.     if (!action || strlen(action) == 0) {
  832.         if (logFlag) logMessage("Mouse JSON missing action");
  833.         return;
  834.     }
  835.  
  836.     // Check if immediate execution is requested (default is false = queued)
  837.     bool immediate = false;
  838.     if (!doc["immediate"].isNull()) {
  839.         immediate = doc["immediate"].as<bool>();
  840.     }
  841.  
  842.     auto buttonMask = [](const String &btn) -> uint8_t {
  843.         String b = btn; b.toLowerCase();
  844.         if (b == "left") return MOUSE_LEFT;
  845.         if (b == "right") return MOUSE_RIGHT;
  846.         if (b == "middle" || b == "wheel") return MOUSE_MIDDLE;
  847.         if (b == "back" || b == "backward") return MOUSE_BACKWARD;
  848.         if (b == "forward") return MOUSE_FORWARD;
  849.         return MOUSE_LEFT;
  850.     };
  851.  
  852.     if (strcmp(action, "move") == 0) {
  853.         int dx = doc["dx"] | 0;
  854.         int dy = doc["dy"] | 0;
  855.         int wheel = doc["wheel"] | 0;
  856.         int pan = doc["pan"] | 0;
  857.         mouse.move(immediate, dx, dy, wheel, pan);
  858.         if (logFlag) {
  859.             String mode = immediate ? "immediate" : "queued";
  860.             logMessage("Mouse move (" + mode + ") dx=" + String(dx) + " dy=" + String(dy) + " wheel=" + String(wheel) + " pan=" + String(pan));
  861.         }
  862.         return;
  863.     }
  864.  
  865.     if (strcmp(action, "click") == 0) {
  866.         String btn = doc["button"] | "left";
  867.         uint8_t mask = buttonMask(btn);
  868.         mouse.click(immediate, mask);
  869.         if (logFlag) {
  870.             String mode = immediate ? "immediate" : "queued";
  871.             logMessage("Mouse click (" + mode + ") button=" + btn);
  872.         }
  873.         return;
  874.     }
  875.  
  876.     if (strcmp(action, "button") == 0) {
  877.         String btn = doc["button"] | "left";
  878.         String state = doc["state"] | "down";
  879.         state.toLowerCase();
  880.         uint8_t mask = buttonMask(btn);
  881.         if (state == "down" || state == "press" || state == "mousedown") {
  882.             mouse.press(immediate, mask);
  883.             if (logFlag) {
  884.                 String mode = immediate ? "immediate" : "queued";
  885.                 logMessage("Mouse button down (" + mode + "): " + btn);
  886.             }
  887.         } else if (state == "up" || state == "release" || state == "mouseup") {
  888.             mouse.release(immediate, mask);
  889.             if (logFlag) {
  890.                 String mode = immediate ? "immediate" : "queued";
  891.                 logMessage("Mouse button up (" + mode + "): " + btn);
  892.             }
  893.         } else {
  894.             if (logFlag) logMessage("Mouse button unknown state: " + state);
  895.         }
  896.         return;
  897.     }
  898.  
  899.     if (strcmp(action, "set_rate_limit") == 0) {
  900.         int intervalUs = doc["interval_us"] | -1;
  901.         if (intervalUs >= 0) {
  902.             hidMouse.setMinReportInterval(intervalUs);
  903.             if (logFlag) logMessage("Mouse rate limit set to " + String(intervalUs) + " us");
  904.         }
  905.         return;
  906.     }
  907.  
  908.     if (strcmp(action, "get_rate_limit") == 0) {
  909.         unsigned long interval = hidMouse.getMinReportInterval();
  910.         if (logFlag) logMessage("Mouse rate limit: " + String(interval) + " us");
  911.         return;
  912.     }
  913.  
  914.     if (logFlag) logMessage("Mouse JSON unknown action: " + String(action));
  915. }
  916.  
  917. void handleKeyboardJson(const JsonDocument &doc, bool logFlag, JsonObject responseData = JsonObject()) {
  918.     const char *action = doc["action"] | "";
  919.     if (!action || strlen(action) == 0) {
  920.         if (logFlag) logMessage("Keyboard JSON missing action");
  921.         return;
  922.     }
  923.  
  924.     if (strcmp(action, "key_event") == 0) {
  925.         String state = doc["state"] | "down";
  926.         String why;
  927.         uint8_t code = resolveHidFromPayload(doc, &why);
  928.         if (code == 0) {
  929.             if (logFlag) logMessage("Keyboard key_event missing/unknown code");
  930.             return;
  931.         }
  932.         state.toLowerCase();
  933.         if (state == "down") {
  934.             hidKeyboard.pressRaw(code);
  935.             if (logFlag) {
  936.                 String info = "Keyboard key down: " + String(code);
  937.                 if (why.length() > 0) info += " (" + why + ")";
  938.                 logMessage(info);
  939.             }
  940.         } else if (state == "up") {
  941.             hidKeyboard.releaseRaw(code);
  942.             if (logFlag) {
  943.                 String info = "Keyboard key up: " + String(code);
  944.                 if (why.length() > 0) info += " (" + why + ")";
  945.                 logMessage(info);
  946.             }
  947.         } else {
  948.             if (logFlag) logMessage("Keyboard key_event ignored state: " + state);
  949.         }
  950.         return;
  951.     }
  952.  
  953.     if (strcmp(action, "release_all") == 0) {
  954.         hidKeyboard.releaseAll();
  955.         if (logFlag) logMessage("Keyboard release all");
  956.         return;
  957.     }
  958.  
  959.     if (logFlag) logMessage("Keyboard JSON unknown action: " + String(action));
  960. }
  961.  
  962. void handleWsEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len) {
  963.     String channel = wsChannel(server);
  964.     switch (type) {
  965.     case WS_EVT_CONNECT:
  966.         logMessage("WS connect [" + channel + "] " + wsClientLabel(client));
  967.         break;
  968.     case WS_EVT_DISCONNECT:
  969.         logMessage("WS disconnect [" + channel + "] " + wsClientLabel(client));
  970.         break;
  971.     case WS_EVT_DATA: {
  972.         AwsFrameInfo *info = static_cast<AwsFrameInfo *>(arg);
  973.         if (info->final && info->index == 0 && info->len == len) {
  974.             String msg;
  975.             msg.reserve(len);
  976.             for (size_t i = 0; i < len; i++) {
  977.                 msg += static_cast<char>(data[i]);
  978.             }
  979.  
  980.             if (channel == "mouse" || channel == "keyboard") {
  981.                 StaticJsonDocument<512> doc;
  982.                 DeserializationError err = deserializeJson(doc, msg);
  983.                 bool logFlag = true;
  984.                 if (!doc["log"].isNull()) {
  985.                     logFlag = doc["log"].as<bool>();
  986.                 }
  987.                 bool responseFlag = false;
  988.                 if (!doc["response"].isNull()) {
  989.                     responseFlag = doc["response"].as<bool>();
  990.                 }
  991.                
  992.                 if (err) {
  993.                     if (logFlag) logMessage("WS " + channel + " JSON parse error: " + String(err.c_str()));
  994.                 } else {
  995.                     unsigned long startMicros = micros();
  996.                    
  997.                     if (logFlag) {
  998.                         logMessage("WS text [" + channel + "] from " + wsClientLabel(client) + ": " + msg);
  999.                     }
  1000.                    
  1001.                     // Prepare response object if needed
  1002.                     JsonObject responseData;
  1003.                     StaticJsonDocument<768> responseDoc;
  1004.                     if (responseFlag) {
  1005.                         responseDoc["request"] = doc;
  1006.                         responseData = responseDoc.createNestedObject("response");
  1007.                         responseData["duration"] = 0; // will be updated later
  1008.                     }
  1009.                    
  1010.                     if (channel == "mouse") {
  1011.                         handleMouseJson(doc, logFlag, responseData);
  1012.                     } else {
  1013.                         handleKeyboardJson(doc, logFlag, responseData);
  1014.                     }
  1015.                    
  1016.                     // Send response if requested
  1017.                     if (responseFlag) {
  1018.                         unsigned long durationMicros = micros() - startMicros;
  1019.                         responseData["duration"] = durationMicros;
  1020.                        
  1021.                         String responseJson;
  1022.                         serializeJson(responseDoc, responseJson);
  1023.                         client->text(responseJson);
  1024.                         if (logFlag) logMessage("Sent response: " + responseJson);
  1025.                     }
  1026.                 }
  1027.             } else {
  1028.                 logMessage("WS text [" + channel + "] from " + wsClientLabel(client) + ": " + msg);
  1029.             }
  1030.         } else {
  1031.             logMessage("WS fragmented/binary data [" + channel + "] from " + wsClientLabel(client));
  1032.         }
  1033.         break;
  1034.     }
  1035.     case WS_EVT_ERROR:
  1036.         logMessage("WS error [" + channel + "] " + wsClientLabel(client));
  1037.         break;
  1038.     case WS_EVT_PONG:
  1039.         break;
  1040.     default:
  1041.         break;
  1042.     }
  1043. }
  1044.  
  1045. void setupWeb() {
  1046.     DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", "*");
  1047.     DefaultHeaders::Instance().addHeader("Access-Control-Allow-Headers", "*");
  1048.     DefaultHeaders::Instance().addHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
  1049.  
  1050.     server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
  1051.         request->send(200, "text/plain",
  1052.                                     "ESP32-S3 Async WebSocket running.\n"
  1053.                                     "WS endpoints: ws://<device-ip>/keyboard , /mouse , /bench");
  1054.     });
  1055.  
  1056.     auto handleOptions = [](AsyncWebServerRequest *request) {
  1057.         request->send(204);
  1058.     };
  1059.     server.on("/", HTTP_OPTIONS, handleOptions);
  1060.     server.on("/keyboard", HTTP_OPTIONS, handleOptions);
  1061.     server.on("/mouse", HTTP_OPTIONS, handleOptions);
  1062.     server.on("/bench", HTTP_OPTIONS, handleOptions);
  1063.  
  1064.     server.onNotFound([](AsyncWebServerRequest *request) {
  1065.         if (request->method() == HTTP_OPTIONS) {
  1066.             request->send(204);
  1067.         } else {
  1068.             request->send(404, "text/plain", "Not found");
  1069.         }
  1070.     });
  1071.  
  1072.     wsKeyboard.onEvent(handleWsEvent);
  1073.     wsMouse.onEvent(handleWsEvent);
  1074.     wsBench.onEvent([](AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len) {
  1075.         (void)server;
  1076.         (void)client;
  1077.         if (type == WS_EVT_DATA) {
  1078.             AwsFrameInfo *info = static_cast<AwsFrameInfo *>(arg);
  1079.             benchBytes += len;
  1080.             if (info->final) {
  1081.                 benchCount++;
  1082.             }
  1083.         }
  1084.     });
  1085.     server.addHandler(&wsKeyboard);
  1086.     server.addHandler(&wsMouse);
  1087.     server.addHandler(&wsBench);
  1088.  
  1089.     server.begin();
  1090.     logMessage("HTTP+WebSocket server started on port 80");
  1091. }
  1092.  
  1093. void setupOTA() {
  1094.     ArduinoOTA
  1095.             .onStart([]() { logMessage("OTA start"); })
  1096.             .onEnd([]() { logMessage("OTA end"); })
  1097.             .onProgress([](unsigned int progress, unsigned int total) {
  1098.                 char buf[48];
  1099.                 snprintf(buf, sizeof(buf), "OTA progress %u%%", (progress * 100) / total);
  1100.                 logMessage(String(buf));
  1101.             })
  1102.             .onError([](ota_error_t error) {
  1103.                 String msg = "OTA error " + String(error);
  1104.                 if (error == OTA_AUTH_ERROR) msg += " (auth)";
  1105.                 else if (error == OTA_BEGIN_ERROR) msg += " (begin)";
  1106.                 else if (error == OTA_CONNECT_ERROR) msg += " (connect)";
  1107.                 else if (error == OTA_RECEIVE_ERROR) msg += " (receive)";
  1108.                 else if (error == OTA_END_ERROR) msg += " (end)";
  1109.                 logMessage(msg);
  1110.             });
  1111.     ArduinoOTA.begin();
  1112.     logMessage("OTA ready");
  1113. }
  1114.  
  1115. void setup() {
  1116.     Serial.begin(115200);
  1117.     delay(200);
  1118.  
  1119.     logMessage("Booting...");
  1120.  
  1121.     USB.begin();
  1122.     hidKeyboard.begin();
  1123.     hidMouse.begin();
  1124.     logMessage("USB HID ready");
  1125.  
  1126.     connectWiFi();
  1127.  
  1128.     udp.begin(LOCAL_UDP_PORT);
  1129.     logMessage("UDP logger ready, sending to " + LOG_HOST.toString() + ":" + String(LOG_PORT));
  1130.  
  1131.     syncTime();
  1132.     setupOTA();
  1133.     setupWeb();
  1134. }
  1135.  
  1136. void loop() {
  1137.     ArduinoOTA.handle();
  1138.    
  1139.     // Process mouse action queue
  1140.     mouse.processQueue();
  1141.  
  1142.     unsigned long now = millis();
  1143.     if (now - lastReportTime >= REPORT_INTERVAL_MS) {
  1144.         lastReportTime = now;
  1145.  
  1146.         logMessage("...");
  1147.         logMessage(mouse.getStats());
  1148.  
  1149.         if (0) {
  1150.             String uptimeMsg = "uptime_ms=" + String(now);
  1151.             wsKeyboard.textAll(uptimeMsg);
  1152.             wsMouse.textAll(uptimeMsg);
  1153.             logMessage("Sent uptime to WS clients: " + uptimeMsg);
  1154.         }
  1155.  
  1156.         if (0) {
  1157.             String benchMsg = "bench: msg/s=" + String(benchCount) + ", byte/s=" + String(benchBytes);
  1158.             wsBench.textAll(benchMsg);
  1159.             logMessage("Bench report: " + benchMsg);
  1160.             benchCount = 0;
  1161.             benchBytes = 0;
  1162.         }
  1163.     }
  1164. }
  1165.  
Advertisement
Add Comment
Please, Sign In to add comment