ESP32-BLE-Gamepad
September 11, 2026 · View on GitHub
Bluetooth LE Gamepad library for the ESP32. Supports three operating modes: Generic HID, SInput (SDL3 native), and XInput (Xbox emulation).
Supported Boards
This library depends only on NimBLE-Arduino and has no chip-specific code, so it should work on any Espressif MCU with a BLE radio. The following are currently built and compile-tested in CI (see main.yml):
- ESP32
- ESP32-S3
- ESP32-C3
- ESP32-C6
Other BLE-capable variants (e.g. ESP32-C2, ESP32-H2) are likely to work too, but aren't currently covered by CI. Note that plain ESP32-S2 has no Bluetooth radio at all, so it can't run this library.
Mode Comparison
| Feature | Generic | SInput | XInput |
|---|---|---|---|
| Buttons | 1-128 | Up to 25 (face, shoulders, stick clicks, triggers, paddles, capture, touchpad clicks, power, misc) | 11 (A/B/X/Y/LB/RB/LS/RS/Select/Start/Home) |
| Thumbsticks | 2 (configurable axes) | 2 (left/right) | 2 (left/right) |
| Triggers | 2 (analog) | 2 (analog) | 2 (analog) |
| D-pad | Up to 4 hat switches | 1 hat switch | 1 hat switch |
| Gyroscope/Accelerometer | Via motion API | Via SInput IMU | No |
| Touchpad | No | 1-2 touchpads | No |
| Rumble | Via Output Report | ERM simulation | Strong/weak motors + trigger vibration |
| Player LED | No | Yes (1-based index) | No |
| RGB LED | No | Yes (24-bit color) | No |
| Battery reporting | Yes (standard BLE) | Yes (SInput power state) | No |
| SDL3 native recognition | No | Yes | No |
| Windows XInput support | No | No | Yes |
| Linux compatibility | Yes | Yes (SDL3 3.4+) | Yes (xpad driver) |
| macOS compatibility | Yes (GCController) | Yes (GCController + SDL3) | Yes (GCController, native Xbox support since Big Sur) |
| Android compatibility | Yes (different mapping) | No | No |
| Configurable VID/PID | Yes | Fixed (0x2E8A/0x10C6) | Fixed (0x045E/0x02FD or 0x0B13) |
| Use case | Custom apps, any OS | SDL3 games, Steam | Xbox-compatible games |
Which mode should I use?
- Generic -- Default. Works everywhere as a standard HID gamepad. Use this if you're building a custom app or need maximum configurability.
- SInput -- Use this for SDL3 games, Steam, or any
SDL_GameController-aware app. Gets you native recognition, rumble, player LED, RGB, IMU, and touchpad without per-VID/PID driver support. - XInput -- Use this for Windows games that expect an Xbox controller. Windows users should use
GamepadMode::XInputSeriesX(PID 0x0B13, Share button) for native XInput over BLE —XInputOneS(PID 0x02FD) shows as Generic HID on Win11 22H2+ (WGI only allowlists Series X, One S is forxpad<6.5/broad compat, like Mystfit). Also works natively on macOS (Xbox controllers are supported since macOS Big Sur) and Linux (viaxpaddriver).
Note on VID/PID: SInput and XInput modes automatically set a specific USB Vendor ID and Product ID that host drivers expect. Do not override
setVid()/setPid()in these modes — the host OS identifies the device by VID/PID and loads the matching driver (SDL's SInput driver for0x2E8A:0x10C6, Xbox drivers for0x045E:*). Changing the VID/PID silently breaks driver recognition.setVid()/setPid()are only intended for Generic mode, where any VID/PID is fine.
Quick Start
Generic Mode (default)
#include <BleGamepad.h>
BleGamepad bleGamepad;
void setup() {
bleGamepad.begin(); // 16 buttons, all axes, 1 hat
}
void loop() {
if (bleGamepad.isConnected()) {
bleGamepad.press(BUTTON_1);
bleGamepad.sendReport();
delay(500);
bleGamepad.release(BUTTON_1);
bleGamepad.sendReport();
delay(500);
}
}
SInput Mode
#include <BleGamepad.h>
BleGamepad bleGamepad;
BleGamepadConfiguration config;
void setup() {
config.setGamepadMode(GamepadMode::SInput);
config.setEnableRumble(true);
bleGamepad.begin(&config);
}
void loop() {
if (bleGamepad.isConnected()) {
bleGamepad.press(BUTTON_1);
bleGamepad.sendReport();
if (bleGamepad.isRumbleReceived()) {
Serial.printf("Rumble: weak=%d strong=%d\n",
bleGamepad.getRumbleLeftAmplitude(),
bleGamepad.getRumbleRightAmplitude());
}
delay(10);
}
}
XInput Mode
#include <BleGamepad.h>
BleGamepad bleGamepad;
BleGamepadConfiguration config;
void setup() {
config.setGamepadMode(GamepadMode::XInputSeriesX);
bleGamepad.begin(&config);
}
void loop() {
if (bleGamepad.isConnected()) {
bleGamepad.press(BUTTON_1); // A
bleGamepad.sendReport();
if (bleGamepad.isXInputRumbleReceived()) {
Serial.printf("Rumble: strong=%d weak=%d\n",
bleGamepad.getXInputStrongMotor(),
bleGamepad.getXInputWeakMotor());
}
delay(10);
}
}
Installation
- Make sure you can use the ESP32 with the Arduino IDE. Instructions can be found here.
- Download the latest release of this library from the release page.
- In the Arduino IDE go to "Sketch" -> "Include Library" -> "Add .ZIP Library..." and select the file you just downloaded.
- In the Arduino IDE go to "Tools" -> "Manage Libraries..." -> Filter for "NimBLE-Arduino" by h2zero and install.
- (Optional, only for the
examples/NuS/sketches) Install "NuS-NimBLE-Serial" by afpineda from the Library Manager as well. - You can now go to "File" -> "Examples" -> "ESP32 BLE Gamepad" and select an example to get started.
PlatformIO: add h2zero/NimBLE-Arduino to your lib_deps and esp32:esp32 to your platform.
Examples
Generic Examples
| Example | Description |
|---|---|
| Gamepad | Basic button presses and axis movement |
| IndividualAxes | Set each axis independently |
| TestAll | Exercise all features |
| FlightControllerTest | Flight controller with simulation controls |
| DrivingControllerTest | Driving controller with steering/brake/accelerator |
| MotionController | Gyroscope and accelerometer |
| PotAsAxis | Map analog pot to axis |
| SpecialButtons | Start, Select, Home, etc. |
| TestFeatureReports | HID Feature Report exchange |
| TestReceivingOutputReport | Receive HID Output Reports |
| MultipleButtons | Multiple simultaneous buttons |
| MultipleButtonsAndHats | Multiple buttons and hat switches |
| CharacteristicsConfiguration | Custom BLE characteristics |
| Keypad4x4 | 4x4 keypad as buttons |
| ForcePairingMode | Force re-pairing |
| GetPeerInfo | Query connected peer |
| SetBatteryLevel | Set battery percentage |
| SetBatteryPowerState | Set battery power state |
| SingleButton | Single button debounce |
| SingleButtonDebounce | Debounced single button |
| MultipleButtonsDebounce | Debounced multiple buttons |
| Fightstick | Fightstick layout |
SInput Examples
| Example | Description |
|---|---|
| SInputRumble | Rumble/vibration reception |
| SInputPlayerLED | Player LED assignment |
| SInputRGB | RGB LED via discrete PWM pins |
| SInputRGB_NeoPixel | RGB LED via WS2812/NeoPixel strip |
| SInputIMU | Gyroscope and accelerometer |
| SInputTouchpad | Dual touchpad input |
| SInputBattery | Battery level reporting |
| SInputFullGamepad | All features combined |
XInput Examples
| Example | Description |
|---|---|
| XInputOneS | Xbox One S mode with rumble |
| XInputSeriesX | Xbox Series X mode with Share button |
| XInputAllTest | Cycles every Xbox input for 1:1 host comparison |
NuS Examples (require NuS-NimBLE-Serial)
| Example | Description |
|---|---|
| NuSSerialDiag | Connection diagnostics over BLE serial (replaces removed Diagnostics) |
| NuSGenericBridge | Drive a Generic pad from a BLE terminal (pure defaults) |
| NuSGenericAdvanced | Generic bridge + start/select specials and HID output/feature reports |
| NuSSInputBridge | Drive an SInput pad from a BLE terminal, surface player LED/rumble/RGB |
| NuSXInputBridge | Drive an Xbox pad from a BLE terminal, surface host rumble |
See examples/NuS/README.md and docs/NuSCompatibility.md.
OS Compatibility
Windows
- Generic mode: Recognized as a standard HID gamepad via
hid-generic. Works in DirectInput-compatible games and any app usinghidapi. - XInput mode: Recognized natively as an Xbox controller. Works in all XInput-compatible games (virtually every modern Windows game with controller support). Shows as "Xbox Wireless Controller" in Settings > Bluetooth & devices > Controllers.
- SInput mode: Recognized as a HID gamepad. Works in Steam via SDL3.
Linux
- Generic mode: BlueZ bridges the device into the kernel via
uhid, creating/dev/hidraw*,/dev/input/js*, and/dev/input/event*nodes. Recognized byjstest,evtest, SDL, and any game using the Linux joystick or evdev subsystems. - XInput mode: Works via the
xpadkernel driver, included in most distributions. The device appears as a standard Xbox controller. Share button requires Linux 6.5+ (Series X PID). - SInput mode: Recognized via SDL3's HIDAPI SInput driver (SDL 3.4+). Steam uses SDL3 and recognizes the device natively.
For detailed Linux testing (pairing, udev rules, hidapi, monitoring), see LinuxHIDTesting.md.
macOS
- Generic mode: Recognized as a Bluetooth HID gamepad. Works via Apple's
GCController(GameController framework) andIOHIDManager. Any game or emulator supporting GCController will detect it. - XInput mode: macOS natively supports Xbox Wireless Controllers with Bluetooth (since macOS Big Sur 11.0). The device appears as "Xbox Wireless Controller" and works via
GCController. Rumble is supported via GCController haptics. No driver needed. - SInput mode: Recognized as a Bluetooth HID gamepad. Works via
GCControllerand SDL3 (3.4+) on macOS.
| macOS Version | Xbox Controller Support |
|---|---|
| Big Sur (11.0)+ | Xbox One S via Bluetooth |
| Monterey (12.0)+ | GCController framework |
| Ventura (13.0)+ | Improved mapping |
| Sonoma (14.0)+ | Rumble via GCController haptics |
| Sequoia (15.0)+ | Wired Xbox support (USB-C) |
| Tahoe (26.0)+ | Current |
Android
- Generic mode only: Works as a HID gamepad. Triggers are mapped to GAS/BRAKE instead of standard trigger axes. Right thumbstick may use z/rx instead of z/rz. See GenericMode.md for details.
- SInput and XInput modes: Not supported on Android.
Steam (All Platforms)
Steam has built-in SDL3 support and recognizes gamepads automatically. Each mode works differently in Steam:
| Mode | Steam Recognition | What You Get |
|---|---|---|
| Generic | Detected as generic gamepad | Basic input; may need manual button mapping in Steam Input |
| SInput | Native SDL_GameController | Automatic mapping, rumble, player LED, IMU, touchpad -- no configuration needed |
| XInput | Recognized as Xbox controller | Automatic mapping on Windows/Linux; macOS via GCController |
Recommendation: Use SInput mode for Steam. It gets you native recognition with full feature support (rumble, IMU, touchpad) without per-game configuration. Steam ships with SDL3 and handles the SInput driver automatically.
If you prefer XInput mode, Steam Input maps Xbox controllers by default -- it will work, but you won't get IMU/touchpad/rumble via the SInput protocol (you'll get Xbox-style rumble instead).
Deep Dives
- Generic Mode -- Full protocol reference, HID descriptor, configuration, API
- SInput Mode -- SInput protocol, SDL3 integration, touchpad, IMU, haptics, RGB
- XInput Mode -- Xbox emulation protocol, rumble, PID differences
- GATT vs HID-over-GATT -- Architecture, how SDL/game engines reach each service
- NuS Compatibility -- Using this library alongside NuS-NimBLE-Serial for a BLE serial side channel
- Linux HID Testing -- Testing with hidraw/hidapi on Linux
- Troubleshooting Guide -- Common issues and fixes
NimBLE
Since version 3 of this library, the more efficient NimBLE library is used instead of the default BLE implementation. Please use the library manager to install it, or get it from here: https://github.com/h2zero/NimBLE-Arduino
Since version 3, this library also supports a configurable HID descriptor, which allows users to customise how the device presents itself to the OS (number of buttons, hats, axes, sliders, simulation controls etc). See the examples for guidance.
This version endeavors to be compatible with the latest released version of NimBLE-Arduino through the Arduino Library Manager.
License
Published under the MIT license. Please see license.txt.
It would be great however if any improvements are fed back into this version.
The examples/NuS/ sketches and docs/NuSCompatibility.md build on the third-party
NuS-NimBLE-Serial library by Ángel Fernández Pineda,
which is licensed separately under CC BY 4.0
(see the attribution notice in license.txt). That license covers the NuS library itself, not this library.
Troubleshooting Guide
Troubleshooting guide and suggestions can be found in TroubleshootingGuide
Testing Your Gamepad
Cross-Platform Gamepad Testers
These free tools visualize all gamepad inputs (buttons, axes, triggers, D-pad) in real time and work on Windows, macOS, and Linux:
| Tool | Platform | Source | Notes |
|---|---|---|---|
| HIDTester | Windows, macOS, Linux | Source | Lightweight, no install needed. Shows buttons, axes, D-pad, deadzone analysis, signal curves, polling rate. Built on SDL3. |
| Gamepad_Tester | Windows, macOS, Linux | Source | C++23/SDL3/ImGui. Latency measurement, polling rate analysis, rumble testing, input visualization. Pre-built binaries available. |
| gamepad-tester.net | Browser (all OS) | N/A | Browser-based, no install. Works in Chrome/Edge/Firefox. Shows all buttons/axes/triggers. |
Both HIDTester and Gamepad_Tester are built on SDL3, so they recognize SInput controllers natively (VID 0x2E8A/PID 0x10C6) and map buttons/axes correctly. The browser-based tester works with any mode but shows raw button indices instead of named buttons.
Platform-Specific Testing
- Linux: See LinuxHIDTesting.md for testing Input/Output/Feature Reports via hidraw/hidapi,
jstest, andevtest. - macOS: Gamepad appears in System Settings > Bluetooth. Use Chrome or Firefox for browser-based testing (Safari has partial Gamepad API support).
- Windows: Gamepad appears in Settings > Bluetooth & devices > Controllers. Use HIDTester or Gamepad_Tester for detailed input visualization.
Hardware-in-the-Loop Testing (HIL)
ESP32-BLE-Gamepad-HIL is a hardware-in-the-loop test rig that automates end-to-end validation of the library. An ESP32 runs test firmware; the harness drives it over USB serial and asserts the resulting BLE HID behavior on a Linux host (typically a Raspberry Pi). It tests:
- Buttons — every configured button produces one distinct evdev key event
- Axes — each axis maps to the correct ABS code with exact min/centre/max endpoints
- Hats — 8 directions + centre
- HID descriptor — golden file comparison per profile
- Device Information / PnP / Battery — GATT service validation
- Feature / Output reports — bidirectional round-trip
- Latency / throughput — ~18.6ms median button press, burst testing
The rig uses a builder/tester split (builder needs PlatformIO; tester needs only esptool + pytest) and runs in CI via GitHub Actions + Tailscale SSH. See the HIL README for setup instructions.
GATT vs HID-over-GATT
For an explanation of how this library's HID Service and NUS service differ, how SDL/game engines and the Linux input stack actually reach each one, and how to extend it with features like rumble or RGB/player LEDs, see GattVsHid
Notes
This library allows you to make the ESP32 act as a Bluetooth Gamepad and control what it does. Relies on NimBLE-Arduino
For Windows testing, use HIDTester or Gamepad_Tester — both are cross-platform and don't require DirectX.
Gamepads designed for Android use a different button mapping. This affects analog triggers, where the standard left and right trigger axes are not detected. Android calls the HID report for right trigger "GAS" and left trigger "BRAKE". Enabling the "Accelerator" and "Brake" simulation controls allows them to be used instead of right and left trigger.
Right thumbstick on Windows is usually z, rz, whereas on Android, this may be z, rx, so you may want to set them separately with setZ and setRX, instead of using setRightThumb(z, rz), or use setRightThumbAndroid(z, rx)
For the most consistent behavior across Windows, macOS, and Linux, use XInput mode (emulates an Xbox controller — universally recognized by games and OSes) or SInput mode (native SDL3 support with full button/axis/rumble/RGB mapping). Generic mode relies on each OS's HID stack to interpret the descriptor, so axis ordering, trigger behavior, and button naming can vary between platforms and drivers. See XInputMode.md and SInputMode.md for setup details.
You might also be interested in:
- ESP32-BLE-Mouse
- ESP32-BLE-Keyboard
- Composite Gamepad/Mouse/Keyboard and Xinput capable fork of this library
or the NimBLE versions at
Credits
Credits to T-vK as this library is based on his ESP32-BLE-Mouse library (https://github.com/T-vK/ESP32-BLE-Mouse) that he provided.
Credits to chegewara as the ESP32-BLE-Mouse library is based on this piece of code that he provided.
Credits to wakwak-koba for the NimBLE code that he provided.
Credits to LeeNX for the initial SInput research, pull requests, extensive help with GitHub issues, and building the ESP32-BLE-Gamepad-HIL hardware-in-the-loop test rig. Their contributions were instrumental in driving the SInput implementation forward and keeping the project moving.
Credits to Mystfit for the ESP32-BLE-CompositeHID library, which served as a reference for the XInput implementation and Xbox HID descriptors.