What is HID?
HID stands for Human Interface Device — the USB and Bluetooth standard protocol used by keyboards, mice, and gamepads to communicate with computers. When a keyboard is plugged in or paired, the OS recognises it as a HID device and starts receiving keypress events from it.
An ESP32 microcontroller can be programmed to impersonate a HID keyboard — either via a wired USB cable or wirelessly over Bluetooth LE. The computer cannot distinguish it from a real keyboard.
The ESP32 broadcasts as a Bluetooth keyboard. Your computer pairs with it wirelessly — exactly like any commercial wireless keyboard.
The ESP32-S2 or S3 plugs directly into your computer via USB and is instantly recognised as a wired keyboard — no pairing, no drivers needed.
How the Computer Sees the ESP32
Hardware Requirements
Supported Boards
| Board | BLE HID | USB HID | Best For |
|---|---|---|---|
| ESP32 (original) | ✓ Yes | ✗ No | Wireless BLE macropad |
| ESP32-S2 | ✗ No Bluetooth | ✓ Yes | Wired USB macropad |
| ESP32-S3 | ✓ Yes | ✓ Yes | Best all-rounder — supports both |
| ESP32-C3 | ✓ Yes | ⚠ Limited | BLE compact builds |
Parts List
| Component | Qty | Notes |
|---|---|---|
| ESP32-S3 (or ESP32) | 1 | DevKit breakout board recommended |
| Tactile push buttons | 6 | Momentary, 4-pin or 2-pin type |
| Status LED (optional) | 1 | Most DevKit boards have one built in on GPIO 2 |
| Breadboard | 1 | Half-size or full-size |
| Jumper wires (M-M) | ~15 | Male-to-male |
| USB cable | 1 | USB-C or Micro-USB matching your board |
No pull-down resistors needed. The code enables the ESP32's internal pull-up resistors (INPUT_PULLUP), so each button only needs two wires — one to its GPIO pin and one to GND. The pin reads LOW when pressed and HIGH when released.
Wiring Diagram
Connect each button between its assigned GPIO pin and GND. Blue dashed lines are GPIO signal wires. Red lines route to a shared GND bus rail on each side, which connects back to the board's GND pin.
Button-to-Pin Reference
| Button | GPIO Pin | Wire A | Wire B | Macro Action |
|---|---|---|---|---|
| BTN 1 | GPIO 12 | Pin → GPIO 12 | Pin → GND | Auto-Login: username → Tab → password → Enter |
| BTN 2 | GPIO 14 | Pin → GPIO 14 | Pin → GND | Copy — Ctrl + C |
| BTN 3 | GPIO 27 | Pin → GPIO 27 | Pin → GND | Paste — Ctrl + V |
| BTN 4 | GPIO 26 | Pin → GPIO 26 | Pin → GND | Save — Ctrl + S |
| BTN 5 | GPIO 25 | Pin → GPIO 25 | Pin → GND | Custom Text Snippet |
| BTN 6 | GPIO 33 | Pin → GPIO 33 | Pin → GND | Lock Screen — Win + L |
Arduino IDE Setup
Step 1 — Install the ESP32 Board Package (required for both methods)
- Open Arduino IDE and go to File → Preferences
- In the "Additional boards manager URLs" field, paste:
https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json - Go to Tools → Board → Boards Manager
- Search for esp32 and install "esp32 by Espressif Systems" version 2.x or higher
Step 2A — BLE HID (Original ESP32 / S3 / C3)
- Go to Sketch → Include Library → Manage Libraries
- Search for ESP32-BLE-Keyboard and install the library by T-vK
- Select your board: Tools → Board → ESP32 Arduino → ESP32 Dev Module
- Select the port: Tools → Port → COM___ (your board)
- Paste the BLE code and click Upload
- Open your computer's Bluetooth settings — pair with "MacroPad ESP32"
- LED slow blinks while waiting to pair — LED blinks 3 times fast when connected
Step 2B — USB HID (ESP32-S2 / S3 Only)
- Select board: Tools → Board → ESP32 Arduino → ESP32-S3 Dev Module (or S2)
- Set USB mode: Tools → USB Mode → USB-OTG (TinyUSB)
- Enable CDC: Tools → USB CDC On Boot → Enabled
- Plug the USB cable into the board's native USB port (NOT the UART/programming port)
- Paste the USB HID code and click Upload
- After upload, wait 3 seconds for the OS to enumerate the device as a keyboard
- LED blinks 3 times fast when ready — the board now appears as a keyboard in Device Manager
ESP32-S3 dual-port warning: Most S3 DevKit boards have TWO USB ports — UART/COM (for uploading code) and USB (native OTG). You must use the native USB port for HID to work. Use the UART port only when uploading new sketches.
BLE HID Code
Works on ESP32, ESP32-S3, and ESP32-C3. Install the ESP32-BLE-Keyboard library by T-vK via the Library Manager before uploading.
// ================================================ // ESP32 BLE MacroPad — HID Keyboard // Board : ESP32 / ESP32-S3 / ESP32-C3 // Library: ESP32-BLE-Keyboard by T-vK // ================================================ #include <BleKeyboard.h> // Name visible in Bluetooth settings, manufacturer, battery level BleKeyboard bleKeyboard("MacroPad ESP32", "DIY", 100); // ================================================ // CUSTOMIZE — Edit your credentials and snippet // ================================================ const char* LOGIN_USERNAME = "your_username"; const char* LOGIN_PASSWORD = "your_password"; const char* CUSTOM_SNIPPET = "Hello! This is my custom snippet."; // ---- Button GPIO pins ---- const int BTN_LOGIN = 12; const int BTN_COPY = 14; const int BTN_PASTE = 27; const int BTN_SAVE = 26; const int BTN_CUSTOM = 25; const int BTN_LOCK = 33; const int LED_PIN = 2; const int NUM_BUTTONS = 6; const int buttonPins[NUM_BUTTONS] = { 12, 14, 27, 26, 25, 33 }; unsigned long lastPressTime[NUM_BUTTONS] = { 0 }; const unsigned long DEBOUNCE_MS = 250; // ================================================ // MACROS — Each function = one button action // ================================================ void macroAutoLogin() { bleKeyboard.print(LOGIN_USERNAME); // Type username delay(150); bleKeyboard.write(KEY_TAB); // Jump to password field delay(150); bleKeyboard.print(LOGIN_PASSWORD); // Type password delay(150); bleKeyboard.write(KEY_RETURN); // Submit / press Enter } void macroCopy() { bleKeyboard.press(KEY_LEFT_CTRL); bleKeyboard.press('c'); delay(50); bleKeyboard.releaseAll(); } void macroPaste() { bleKeyboard.press(KEY_LEFT_CTRL); bleKeyboard.press('v'); delay(50); bleKeyboard.releaseAll(); } void macroSave() { bleKeyboard.press(KEY_LEFT_CTRL); bleKeyboard.press('s'); delay(50); bleKeyboard.releaseAll(); } void macroCustom() { bleKeyboard.print(CUSTOM_SNIPPET); } void macroLock() { bleKeyboard.press(KEY_LEFT_GUI); // Windows key bleKeyboard.press('l'); delay(50); bleKeyboard.releaseAll(); } // ================================================ // DEBOUNCE — Prevents multiple triggers per press // ================================================ bool debounced(int index) { unsigned long now = millis(); if (now - lastPressTime[index] > DEBOUNCE_MS) { lastPressTime[index] = now; return true; } return false; } void ledBlink(int times) { for (int i = 0; i < times; i++) { digitalWrite(LED_PIN, HIGH); delay(80); digitalWrite(LED_PIN, LOW); delay(80); } } void setup() { Serial.begin(115200); pinMode(LED_PIN, OUTPUT); for (int i = 0; i < NUM_BUTTONS; i++) pinMode(buttonPins[i], INPUT_PULLUP); bleKeyboard.begin(); Serial.println("BLE MacroPad started — waiting for BT connection..."); } void loop() { // Slow blink while not connected via Bluetooth if (!bleKeyboard.isConnected()) { digitalWrite(LED_PIN, HIGH); delay(500); digitalWrite(LED_PIN, LOW); delay(500); return; } // Check each button — trigger macro if pressed if (digitalRead(BTN_LOGIN) == LOW && debounced(0)) { ledBlink(1); macroAutoLogin(); } if (digitalRead(BTN_COPY) == LOW && debounced(1)) { ledBlink(1); macroCopy(); } if (digitalRead(BTN_PASTE) == LOW && debounced(2)) { ledBlink(1); macroPaste(); } if (digitalRead(BTN_SAVE) == LOW && debounced(3)) { ledBlink(1); macroSave(); } if (digitalRead(BTN_CUSTOM) == LOW && debounced(4)) { ledBlink(1); macroCustom(); } if (digitalRead(BTN_LOCK) == LOW && debounced(5)) { ledBlink(1); macroLock(); } delay(10); }
USB HID Code
For ESP32-S2 or ESP32-S3 only. No external library required — uses the TinyUSB stack built into the ESP32 Arduino core. The logic is identical to the BLE version.
Before uploading: set Tools → USB Mode → USB-OTG (TinyUSB) and plug into the native USB port on the board.
// ================================================ // ESP32-S2 / S3 USB HID MacroPad // Board : ESP32-S2 or ESP32-S3 DevKit // Prereq : Tools → USB Mode → USB-OTG (TinyUSB) // Library: None — uses built-in TinyUSB // ================================================ #include "USB.h" #include "USBHIDKeyboard.h" USBHIDKeyboard Keyboard; // ================================================ // CUSTOMIZE — Edit your credentials and snippet // ================================================ const char* LOGIN_USERNAME = "your_username"; const char* LOGIN_PASSWORD = "your_password"; const char* CUSTOM_SNIPPET = "Hello! This is my custom snippet."; // ---- Button GPIO pins ---- const int BTN_LOGIN = 12; const int BTN_COPY = 14; const int BTN_PASTE = 27; const int BTN_SAVE = 26; const int BTN_CUSTOM = 25; const int BTN_LOCK = 33; const int LED_PIN = 2; const int NUM_BUTTONS = 6; const int buttonPins[NUM_BUTTONS] = { 12, 14, 27, 26, 25, 33 }; unsigned long lastPressTime[NUM_BUTTONS] = { 0 }; const unsigned long DEBOUNCE_MS = 250; // ================================================ // MACROS // ================================================ void macroAutoLogin() { Keyboard.print(LOGIN_USERNAME); // Type username delay(150); Keyboard.write(KEY_TAB); // Jump to password field delay(150); Keyboard.print(LOGIN_PASSWORD); // Type password delay(150); Keyboard.write(KEY_RETURN); // Submit / press Enter } void macroCopy() { Keyboard.press(KEY_LEFT_CTRL); Keyboard.press('c'); delay(50); Keyboard.releaseAll(); } void macroPaste() { Keyboard.press(KEY_LEFT_CTRL); Keyboard.press('v'); delay(50); Keyboard.releaseAll(); } void macroSave() { Keyboard.press(KEY_LEFT_CTRL); Keyboard.press('s'); delay(50); Keyboard.releaseAll(); } void macroCustom() { Keyboard.print(CUSTOM_SNIPPET); } void macroLock() { Keyboard.press(KEY_LEFT_GUI); // Windows key Keyboard.press('l'); delay(50); Keyboard.releaseAll(); } // ---- Debounce ---- bool debounced(int index) { unsigned long now = millis(); if (now - lastPressTime[index] > DEBOUNCE_MS) { lastPressTime[index] = now; return true; } return false; } void ledBlink(int times) { for (int i = 0; i < times; i++) { digitalWrite(LED_PIN, HIGH); delay(80); digitalWrite(LED_PIN, LOW); delay(80); } } void setup() { Serial.begin(115200); pinMode(LED_PIN, OUTPUT); for (int i = 0; i < NUM_BUTTONS; i++) pinMode(buttonPins[i], INPUT_PULLUP); Keyboard.begin(); USB.begin(); delay(3000); // Give OS time to enumerate the USB HID device ledBlink(3); // 3 blinks = device ready Serial.println("USB HID MacroPad ready!"); } void loop() { if (digitalRead(BTN_LOGIN) == LOW && debounced(0)) { ledBlink(1); macroAutoLogin(); } if (digitalRead(BTN_COPY) == LOW && debounced(1)) { ledBlink(1); macroCopy(); } if (digitalRead(BTN_PASTE) == LOW && debounced(2)) { ledBlink(1); macroPaste(); } if (digitalRead(BTN_SAVE) == LOW && debounced(3)) { ledBlink(1); macroSave(); } if (digitalRead(BTN_CUSTOM) == LOW && debounced(4)) { ledBlink(1); macroCustom(); } if (digitalRead(BTN_LOCK) == LOW && debounced(5)) { ledBlink(1); macroLock(); } delay(10); }
Keyboard Commands Reference
All methods and key constants below work identically in both the BLE and USB HID versions.
Keyboard Methods
- Keyboard.print()Types a full string of characters — e.g.
Keyboard.print("hello world"); - Keyboard.println()Types a string and automatically presses Enter at the end
- Keyboard.write()Sends one complete key press and release — e.g.
Keyboard.write(KEY_RETURN); - Keyboard.press()Holds a key down — used for modifier keys like Ctrl, Shift, Alt, GUI
- Keyboard.release()Releases one specific held key — e.g.
Keyboard.release(KEY_LEFT_CTRL); - Keyboard.releaseAll()Releases ALL currently held keys — always call this after using
press()
Special Key Constants
- KEY_RETURNEnter / Return key
- KEY_TABTab key — jump between form fields
- KEY_BACKSPACEDelete one character to the left
- KEY_DELETEForward delete key
- KEY_ESCEscape key
- KEY_LEFT_CTRLLeft Control modifier key — hold with
press() - KEY_LEFT_SHIFTLeft Shift modifier key
- KEY_LEFT_ALTLeft Alt modifier key
- KEY_LEFT_GUIWindows key (Win) / macOS Command key (⌘)
- KEY_UP_ARROWUp arrow key
- KEY_DOWN_ARROWDown arrow key
- KEY_LEFT_ARROWLeft arrow key
- KEY_RIGHT_ARROWRight arrow key
- KEY_F1 – KEY_F12Function keys F1 through F12
- KEY_HOMEJump to beginning of line
- KEY_ENDJump to end of line
- KEY_PAGE_UPScroll up one page
- KEY_PAGE_DOWNScroll down one page
- KEY_CAPS_LOCKToggle Caps Lock on/off
Ready-to-Use Shortcut Examples
- Ctrl + Z (Undo)
Keyboard.press(KEY_LEFT_CTRL); Keyboard.press('z'); delay(50); Keyboard.releaseAll(); - Ctrl + A (Select All)
Keyboard.press(KEY_LEFT_CTRL); Keyboard.press('a'); delay(50); Keyboard.releaseAll(); - Ctrl + X (Cut)
Keyboard.press(KEY_LEFT_CTRL); Keyboard.press('x'); delay(50); Keyboard.releaseAll(); - Alt + Tab
Keyboard.press(KEY_LEFT_ALT); Keyboard.press(KEY_TAB); delay(50); Keyboard.releaseAll(); - Ctrl+Shift+T
Keyboard.press(KEY_LEFT_CTRL); Keyboard.press(KEY_LEFT_SHIFT); Keyboard.press('t'); delay(50); Keyboard.releaseAll(); - Win + D (Desktop)
Keyboard.press(KEY_LEFT_GUI); Keyboard.press('d'); delay(50); Keyboard.releaseAll(); - Type any text
Keyboard.print("Any text you want typed automatically here"); - Press Enter
Keyboard.write(KEY_RETURN);
Critical rule: Always call Keyboard.releaseAll(); after any use of Keyboard.press() with modifier keys (Ctrl, Shift, Alt, GUI / Win). Failing to do so will leave that key held down indefinitely until the board resets.
LED Status Indicator
| LED Pattern | Meaning |
|---|---|
| Slow blink (500ms on / 500ms off) | BLE version — not yet paired / waiting for Bluetooth connection |
| 3 fast blinks on power-on | Device connected and fully ready to receive button presses |
| 1 short blink | Button was pressed — macro triggered and keystroke sent successfully |
Troubleshooting
| Problem | Likely Cause | Fix |
|---|---|---|
| BLE device not visible in Bluetooth settings | BLE not started or library missing | Open Serial Monitor — confirm bleKeyboard.begin() runs without error; ensure T-vK library is installed |
| USB device not recognised as a keyboard | Wrong USB port or wrong USB Mode setting | Plug into the native USB port (not UART); set Tools → USB Mode → USB-OTG (TinyUSB) |
| Button triggers several times on one press | Mechanical contact bounce | Increase DEBOUNCE_MS from 250 to 400–600 in the code |
| Ctrl, Shift, or Win key stays stuck after macro | Missing releaseAll() call |
Add Keyboard.releaseAll(); at the end of every combo macro function |
| Wrong characters typed on screen | OS keyboard layout mismatch | Ensure the computer's active input language matches the characters sent by the code |
| LED never blinks | Built-in LED on a different GPIO pin | Check your board's schematic and update const int LED_PIN = 2; to the correct pin |
| Button press does nothing at all | Wiring issue or wrong GPIO number | Test with Serial.println(digitalRead(BTN_LOGIN)); — it should print 0 when pressed; verify wire connections |