📡 ESP32 HID Keyboard Project

ESP32 MacroPad & HID Keyboard

Turn an ESP32 into a fully functional macro keyboard using USB HID or Bluetooth LE — complete wiring diagram, Arduino code, and setup guide included.

ESP32 / S2 / S3 BLE HID USB HID Arduino IDE 6-Button MacroPad
1

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.

📶
BLE HID
Bluetooth Low Energy Keyboard

The ESP32 broadcasts as a Bluetooth keyboard. Your computer pairs with it wirelessly — exactly like any commercial wireless keyboard.

✓ Works on original ESP32 ✓ Wireless — no cable ✓ Easy library setup ⚠ Requires BT pairing
🔌
USB HID
Wired USB 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.

✓ Plug and play ✓ Instant recognition ✓ No external library ✗ ESP32-S2 / S3 only

How the Computer Sees the ESP32

HID Communication Flow Diagram
ESP32 Running HID Firmware BLE-Keyboard / TinyUSB USB Cable HID packets Bluetooth LE Computer Receives HID events USB HID / BT HID driver OS Keyboard Events / Keystrokes Application Receives typed text Browser, Terminal, etc.
2

Hardware Requirements

Supported Boards

BoardBLE HIDUSB HIDBest For
ESP32 (original)✓ Yes✗ NoWireless BLE macropad
ESP32-S2✗ No Bluetooth✓ YesWired USB macropad
ESP32-S3✓ Yes✓ YesBest all-rounder — supports both
ESP32-C3✓ Yes⚠ LimitedBLE compact builds

Parts List

ComponentQtyNotes
ESP32-S3 (or ESP32)1DevKit breakout board recommended
Tactile push buttons6Momentary, 4-pin or 2-pin type
Status LED (optional)1Most DevKit boards have one built in on GPIO 2
Breadboard1Half-size or full-size
Jumper wires (M-M)~15Male-to-male
USB cable1USB-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.

3

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.

ESP32 MacroPad — Full Button Wiring Diagram
ESP32 DevKit Board ESP32 SoC USB GND GPIO12 GPIO14 GPIO27 GPIO26 GND GPIO25 GPIO33 GPIO2 (LED) GND BUS BTN 1 BTN 2 BTN 3 BTN 4 Auto-Login Copy (Ctrl+C) Paste (Ctrl+V) Save (Ctrl+S) GND BUS BTN 5 BTN 6 Custom Snippet Lock (Win+L) To Computer (USB) LEGEND GPIO signal wire GND wire

Button-to-Pin Reference

ButtonGPIO PinWire AWire BMacro Action
BTN 1GPIO 12Pin → GPIO 12Pin → GNDAuto-Login: username → Tab → password → Enter
BTN 2GPIO 14Pin → GPIO 14Pin → GNDCopy — Ctrl + C
BTN 3GPIO 27Pin → GPIO 27Pin → GNDPaste — Ctrl + V
BTN 4GPIO 26Pin → GPIO 26Pin → GNDSave — Ctrl + S
BTN 5GPIO 25Pin → GPIO 25Pin → GNDCustom Text Snippet
BTN 6GPIO 33Pin → GPIO 33Pin → GNDLock Screen — Win + L
4

Arduino IDE Setup

Step 1 — Install the ESP32 Board Package (required for both methods)

  1. Open Arduino IDE and go to File → Preferences
  2. In the "Additional boards manager URLs" field, paste: https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
  3. Go to Tools → Board → Boards Manager
  4. Search for esp32 and install "esp32 by Espressif Systems" version 2.x or higher

Step 2A — BLE HID (Original ESP32 / S3 / C3)

  1. Go to Sketch → Include Library → Manage Libraries
  2. Search for ESP32-BLE-Keyboard and install the library by T-vK
  3. Select your board: Tools → Board → ESP32 Arduino → ESP32 Dev Module
  4. Select the port: Tools → Port → COM___ (your board)
  5. Paste the BLE code and click Upload
  6. Open your computer's Bluetooth settings — pair with "MacroPad ESP32"
  7. LED slow blinks while waiting to pair — LED blinks 3 times fast when connected

Step 2B — USB HID (ESP32-S2 / S3 Only)

  1. Select board: Tools → Board → ESP32 Arduino → ESP32-S3 Dev Module (or S2)
  2. Set USB mode: Tools → USB Mode → USB-OTG (TinyUSB)
  3. Enable CDC: Tools → USB CDC On Boot → Enabled
  4. Plug the USB cable into the board's native USB port (NOT the UART/programming port)
  5. Paste the USB HID code and click Upload
  6. After upload, wait 3 seconds for the OS to enumerate the device as a keyboard
  7. 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.

5

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.

C++ / Arduino ble_macropad.ino
// ================================================
// 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);
}
6

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.

C++ / Arduino usb_macropad.ino
// ================================================
// 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);
}
7

Keyboard Commands Reference

All methods and key constants below work identically in both the BLE and USB HID versions.

Keyboard Methods

Special Key Constants

Ready-to-Use Shortcut Examples

💡

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 PatternMeaning
Slow blink (500ms on / 500ms off)BLE version — not yet paired / waiting for Bluetooth connection
3 fast blinks on power-onDevice connected and fully ready to receive button presses
1 short blinkButton was pressed — macro triggered and keystroke sent successfully
8

Troubleshooting

ProblemLikely CauseFix
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