ESP32-BLE-Gamepad

September 11, 2026 · View on GitHub

Build PlatformIO

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

FeatureGenericSInputXInput
Buttons1-128Up 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)
Thumbsticks2 (configurable axes)2 (left/right)2 (left/right)
Triggers2 (analog)2 (analog)2 (analog)
D-padUp to 4 hat switches1 hat switch1 hat switch
Gyroscope/AccelerometerVia motion APIVia SInput IMUNo
TouchpadNo1-2 touchpadsNo
RumbleVia Output ReportERM simulationStrong/weak motors + trigger vibration
Player LEDNoYes (1-based index)No
RGB LEDNoYes (24-bit color)No
Battery reportingYes (standard BLE)Yes (SInput power state)No
SDL3 native recognitionNoYesNo
Windows XInput supportNoNoYes
Linux compatibilityYesYes (SDL3 3.4+)Yes (xpad driver)
macOS compatibilityYes (GCController)Yes (GCController + SDL3)Yes (GCController, native Xbox support since Big Sur)
Android compatibilityYes (different mapping)NoNo
Configurable VID/PIDYesFixed (0x2E8A/0x10C6)Fixed (0x045E/0x02FD or 0x0B13)
Use caseCustom apps, any OSSDL3 games, SteamXbox-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 BLEXInputOneS (PID 0x02FD) shows as Generic HID on Win11 22H2+ (WGI only allowlists Series X, One S is for xpad<6.5/broad compat, like Mystfit). Also works natively on macOS (Xbox controllers are supported since macOS Big Sur) and Linux (via xpad driver).

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 for 0x2E8A:0x10C6, Xbox drivers for 0x045E:*). 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

  1. Make sure you can use the ESP32 with the Arduino IDE. Instructions can be found here.
  2. Download the latest release of this library from the release page.
  3. In the Arduino IDE go to "Sketch" -> "Include Library" -> "Add .ZIP Library..." and select the file you just downloaded.
  4. In the Arduino IDE go to "Tools" -> "Manage Libraries..." -> Filter for "NimBLE-Arduino" by h2zero and install.
  5. (Optional, only for the examples/NuS/ sketches) Install "NuS-NimBLE-Serial" by afpineda from the Library Manager as well.
  6. 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

ExampleDescription
GamepadBasic button presses and axis movement
IndividualAxesSet each axis independently
TestAllExercise all features
FlightControllerTestFlight controller with simulation controls
DrivingControllerTestDriving controller with steering/brake/accelerator
MotionControllerGyroscope and accelerometer
PotAsAxisMap analog pot to axis
SpecialButtonsStart, Select, Home, etc.
TestFeatureReportsHID Feature Report exchange
TestReceivingOutputReportReceive HID Output Reports
MultipleButtonsMultiple simultaneous buttons
MultipleButtonsAndHatsMultiple buttons and hat switches
CharacteristicsConfigurationCustom BLE characteristics
Keypad4x44x4 keypad as buttons
ForcePairingModeForce re-pairing
GetPeerInfoQuery connected peer
SetBatteryLevelSet battery percentage
SetBatteryPowerStateSet battery power state
SingleButtonSingle button debounce
SingleButtonDebounceDebounced single button
MultipleButtonsDebounceDebounced multiple buttons
FightstickFightstick layout

SInput Examples

ExampleDescription
SInputRumbleRumble/vibration reception
SInputPlayerLEDPlayer LED assignment
SInputRGBRGB LED via discrete PWM pins
SInputRGB_NeoPixelRGB LED via WS2812/NeoPixel strip
SInputIMUGyroscope and accelerometer
SInputTouchpadDual touchpad input
SInputBatteryBattery level reporting
SInputFullGamepadAll features combined

XInput Examples

ExampleDescription
XInputOneSXbox One S mode with rumble
XInputSeriesXXbox Series X mode with Share button
XInputAllTestCycles every Xbox input for 1:1 host comparison

NuS Examples (require NuS-NimBLE-Serial)

ExampleDescription
NuSSerialDiagConnection diagnostics over BLE serial (replaces removed Diagnostics)
NuSGenericBridgeDrive a Generic pad from a BLE terminal (pure defaults)
NuSGenericAdvancedGeneric bridge + start/select specials and HID output/feature reports
NuSSInputBridgeDrive an SInput pad from a BLE terminal, surface player LED/rumble/RGB
NuSXInputBridgeDrive 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 using hidapi.
  • 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 by jstest, evtest, SDL, and any game using the Linux joystick or evdev subsystems.
  • XInput mode: Works via the xpad kernel 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) and IOHIDManager. 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 GCController and SDL3 (3.4+) on macOS.
macOS VersionXbox 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:

ModeSteam RecognitionWhat You Get
GenericDetected as generic gamepadBasic input; may need manual button mapping in Steam Input
SInputNative SDL_GameControllerAutomatic mapping, rumble, player LED, IMU, touchpad -- no configuration needed
XInputRecognized as Xbox controllerAutomatic 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

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:

ToolPlatformSourceNotes
HIDTesterWindows, macOS, LinuxSourceLightweight, no install needed. Shows buttons, axes, D-pad, deadzone analysis, signal curves, polling rate. Built on SDL3.
Gamepad_TesterWindows, macOS, LinuxSourceC++23/SDL3/ImGui. Latency measurement, polling rate analysis, rumble testing, input visualization. Pre-built binaries available.
gamepad-tester.netBrowser (all OS)N/ABrowser-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, and evtest.
  • 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:

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.