Generic Mode

September 17, 2026 ยท View on GitHub

Generic HID gamepad mode. The ESP32 presents as a standard Bluetooth LE gamepad with a fully configurable HID descriptor. Works on all major operating systems without special drivers.

Overview

This is the default mode. The library builds a HID Report Descriptor at runtime based on your BleGamepadConfiguration settings, producing a device that any OS recognizes as a standard gamepad/joystick via HID-over-GATT (HOGP).

Use Generic mode when:

  • You need maximum configurability (buttons, axes, hats, sliders, simulation controls)
  • You're targeting multiple OS platforms including Android
  • You're building a custom app that reads the device via hidraw/hidapi/evdev
  • You don't need SDL3 native recognition or Xbox compatibility

Protocol

HID Report Descriptor

The descriptor is built dynamically in BleGamepad::buildGenericDescriptor(). It uses standard USB HID usage pages:

Usage PageUsagePurpose
0x01 (Generic Desktop)0x05 (Game Pad)Top-level collection
0x01 (Generic Desktop)0x30 (X), 0x31 (Y), etc.Axes
0x01 (Generic Desktop)0x39 (Hat switch)D-pad
0x09 (Button)0x01-0x80Buttons
0x01 (Generic Desktop)0x36 (Slider)Sliders
0x01 (Generic Desktop)0xB6 (Rudder), etc.Simulation controls

Report IDs

All data is sent on a single Report ID (default: 3, configurable via setHidReportId()). The report structure is:

[Report ID] [Buttons...] [Axes...] [Hats...] [Sliders...] [Simulation...]

Configuration

All configuration is done via BleGamepadConfiguration before calling begin().

Buttons

OptionDefaultRangeDescription
setButtonCount()161-128Number of digital buttons
setWhichSpecialButtons()start/select enabled8 booleansStart, Select, Menu, Home, Back, Volume Inc, Volume Dec, Volume Mute

Buttons are reported as a bitmask. With 16 buttons, 2 bytes are used; with 128 buttons, 16 bytes.

Axes

OptionDefaultDescription
setWhichAxes(x,y,z,rx,ry,rz,s1,s2)all trueEnable/disable individual axes
setAxesMin()0x8000 (-32768)Minimum axis value
setAxesMax()0x7FFF (32767)Maximum axis value

Default range is -32768..32767 (signed). Call setAxesMin(0) for an unsigned-like 0..32767 range.

Axis mapping by OS:

Library AxisWindowsLinuxAndroid
XLeft Thumb XABS_XLeft Thumb X
YLeft Thumb YABS_YLeft Thumb Y
ZRight Thumb XABS_ZRight Thumb X
RxLeft TriggerABS_RXBRAKE
RyRight TriggerABS_RYGAS
RzRight Thumb YABS_RZRight Thumb Y

Android maps triggers differently -- see Notes below.

Hat Switches

OptionDefaultDescription
setHatSwitchCount()1Number of hat switches (0-4)

Each hat switch uses 4 bits. Values: HAT_CENTERED, HAT_UP, HAT_UP_RIGHT, HAT_RIGHT, HAT_DOWN_RIGHT, HAT_DOWN, HAT_DOWN_LEFT, HAT_LEFT, HAT_UP_LEFT.

Sliders

OptionDefaultDescription
setWhichAxes()slider1=false, slider2=falseEnable via axis flags
setAxesMin()/setAxesMax()-32768/32767Sliders share the axes range (no separate slider min/max)

Simulation Controls

OptionDefaultDescription
setWhichSimulationControls(rudder,throttle,accel,brake,steering)all falseEnable individual controls
ControlWindows UsageLinux Usage
RudderRUDDERABS_RUDDER
ThrottleTHROTTLEABS_THROTTLE
AcceleratorGASABS_GAS
BrakeBRAKEABS_BRAKE
SteeringWHEELABS_WHEEL

VID/PID

OptionDefaultDescription
setVid()0xE502USB Vendor ID
setPid()0xBBABUSB Product ID

Custom VID/PID values affect how the OS identifies the device. Some games use VID/PID matching for controller-specific features. This is only meaningful in Generic mode โ€” SInput and XInput modes use fixed VID/PIDs that must not be changed.

BLE Characteristics

OptionDefaultDescription
setControllerType()0x03 (Gamepad)HID controller type
setModelNumber()"1.0.0"Device Information model
setSoftwareRevision()"1.0.0"Device Information software
setSerialNumber()"0123456789"Device Information serial
setFirmwareRevision()"0.8.0"Device Information firmware
setHardwareRevision()"1.0.0"Device Information hardware

Report Options

OptionDefaultDescription
setAutoReport()trueSend report automatically on state change
setEnableOutputReport()falseEnable HID Output Report characteristic
setEnableFeatureReport()falseEnable HID Feature Report characteristic

Other

OptionDefaultDescription
setTXPowerLevel()9BLE transmit power (-12 to 9 dBm)

API Reference

Construction

BleGamepad bleGamepad;  // default name "ESP32 BLE Gamepad", manufacturer "Espressif", battery 100
BleGamepad bleGamepad("My Gamepad", "My Mfg", 80);

Lifecycle

MethodDescription
begin(BleGamepadConfiguration *config)Start BLE with given config (or defaults)
end()Stop BLE, release resources
isConnected()Returns true when a host is connected

Buttons

MethodDescription
press(button)Press a button (BUTTON_1 to BUTTON_128)
release(button)Release a button
isPressed(button)Check if button is currently pressed
pressStart() / releaseStart()Convenience for Start button
pressSelect() / releaseSelect()Convenience for Select button
pressHome() / releaseHome()Convenience for Home button
pressBack() / releaseBack()Convenience for Back button
pressMenu() / releaseMenu()Convenience for Menu button
pressVolumeInc() / releaseVolumeInc()Convenience for Volume Up
pressVolumeDec() / releaseVolumeDec()Convenience for Volume Down
pressVolumeMute() / releaseVolumeMute()Convenience for Mute
resetButtons()Release all buttons

Axes

MethodDescription
setX(val)Set X axis
setY(val)Set Y axis
setZ(val)Set Z axis
setRX(val)Set Rx axis
setRY(val)Set Ry axis
setRZ(val)Set Rz axis
setSlider(val)Set slider 1
setSlider1(val)Set slider 1
setSlider2(val)Set slider 2
setSliders(s1, s2)Set both sliders
setLeftThumb(x, y)Set left stick (X, Y)
setRightThumb(z, rz)Set right stick (Z, Rz)
setLeftTrigger(rx)Set left trigger
setRightTrigger(ry)Set right trigger
setTriggers(rx, ry)Set both triggers
setAxes(x,y,z,rx,ry,rz,s1,s2)Set all axes at once
setHIDAxes(x,y,z,rz,rx,ry,s1,s2)Set all axes (HID order)

Hat Switches

MethodDescription
setHat1(val)Set hat 1 (HAT_CENTERED to HAT_UP_LEFT)
setHat2(val)Set hat 2
setHat3(val)Set hat 3
setHat4(val)Set hat 4
setHats(h1,h2,h3,h4)Set all hats

Simulation Controls

MethodDescription
setRudder(val)Set rudder
setThrottle(val)Set throttle
setAccelerator(val)Set accelerator
setBrake(val)Set brake
setSteering(val)Set steering
setSimulationControls(r,t,a,b,s)Set all simulation controls

Motion

MethodDescription
setGyroscope(gX, gY, gZ)Set gyroscope (int16, -32768..32767)
setAccelerometer(aX, aY, aZ)Set accelerometer (int16, -32768..32767)
setMotionControls(gX,gY,gZ,aX,aY,aZ)Set both

Battery

MethodDescription
setBatteryLevel(level)Set battery percentage (0-100)
setBatteryPowerInformation(state)Set power state (PRESENT/NOT_PRESENT/NOT_SUPPORTED)
setDischargingState(state)Set discharging state
setChargingState(state)Set charging state
setPowerLevel(level)Set power level

Reports

MethodDescription
sendReport()Manually send current state to host

Output/Feature Reports

MethodDescription
isOutputReceived()Check if Output Report was received
getOutputBuffer()Get Output Report data
isFeatureReceived()Check if Feature Report was received
getFeatureBuffer()Get Feature Report data
setFeatureBuffer(data, len)Set Feature Report response

Pairing

MethodDescription
deleteBond()Delete current bond
deleteAllBonds()Delete all bonds
enterPairingMode()Force pairing mode
getAddress()Get BLE address
getPeerInfo()Get connected peer info

Examples

See the examples/Generic/ directory. Key examples:

  • Gamepad.ino -- Minimal button/axis example
  • IndividualAxes.ino -- Set each axis independently with sendReport()
  • TestAll.ino -- Exercise all configurable features
  • FlightControllerTest.ino -- Simulation controls (rudder, throttle, brake)
  • DrivingControllerTest.ino -- Steering, accelerator, brake
  • MotionController.ino -- Gyroscope and accelerometer
  • TestFeatureReports.ino -- Bidirectional Feature Report exchange
  • TestReceivingOutputReport.ino -- Receive host-to-device Output Reports

Features & Limitations

What works

  • Up to 128 buttons with press/release
  • 6 axes with configurable range
  • Up to 4 hat switches (D-pads)
  • 2 sliders
  • 5 simulation controls
  • Gyroscope and accelerometer
  • Battery level and power state
  • HID Output and Feature Reports
  • Force pairing / bond management
  • Configurable VID/PID and BLE characteristics
  • Works in Steam (may need manual button mapping via Steam Input)

Limitations

  • No built-in rumble support (requires custom Output Report handling)
  • No player LED or RGB LED support
  • No SDL3 native recognition (will work as generic gamepad)
  • No XInput compatibility
  • Android maps triggers to GAS/BRAKE instead of standard trigger axes
  • iOS is not supported (not an MFi device)

Linux Compatibility

How It Works on Linux

When the ESP32 pairs over BLE, BlueZ's input plugin recognizes it as a HID device and bridges it into the kernel via uhid. This creates:

  • /dev/hidraw* -- raw HID access (for hidapi/hidraw apps)
  • /dev/input/js* -- joystick device (for jstest, SDL, games)
  • /dev/input/event* -- evdev device (for evtest, evdev-aware apps)

The device appears in /proc/bus/input/devices with Icon: input-gaming.

Kernel Driver

Generic mode uses the hid-generic kernel driver, which handles any standard HID device. No custom driver is needed. The device is recognized automatically as a gamepad/joystick.

Pairing

bluetoothctl
agent NoInputNoOutput
default-agent
scan on
# Wait for "ESP32 BLE Gamepad" to appear
pair XX:XX:XX:XX:XX:XX
trust XX:XX:XX:XX:XX:XX
connect XX:XX:XX:XX:XX:XX

Permissions (udev Rule)

/dev/hidraw* nodes are root-only by default. Add a udev rule for user access:

# /etc/udev/rules.d/99-esp32-gamepad.rules
SUBSYSTEM=="hidraw", KERNELS=="0005:E502:BBAB.*", MODE="0660", GROUP="plugdev"

Replace E502:BBAB if you changed the VID/PID. Then:

sudo udevadm control --reload-rules && sudo udevadm trigger
sudo gpasswd -a "$USER" plugdev
# Log out and back in for group changes to take effect

Finding Your hidraw Node

ls /sys/bus/hid/devices/ | grep -i e502
# 0005:E502:BBAB.0003
ls -la /dev/hidraw*

Testing

# Quick sanity check -- shows buttons/axes in real time
jstest --normal /dev/input/js0

# Lower-level evdev view
evtest /dev/input/event0

# Monitor Bluetooth traffic
sudo btmon -i hci0

Battery Monitoring

Battery level appears in UPower automatically:

upower -e | grep gaming_input
upower -i /org/freedesktop/UPower/devices/gaming_input_dev_XX_XX_XX_XX_XX_XX

Python hidapi Access

import hid
for d in hid.enumerate():
    if d["product_string"] == "ESP32 BLE Gamepad":
        print(f"Path: {d['path']}")

Troubleshooting

  • Device not found by hidenumerate(): Confirm paired with bluetoothctl info, check /dev/hidraw* exists
  • PermissionError on hidraw: Use sudo or set up the udev rule above
  • Pairing hangs: Set agent NoInputNoOutput before pairing
  • GATT client can't see HID characteristics: Expected -- BlueZ claims the HID service; use hidraw/hidapi instead

See LinuxHIDTesting.md for the full testing walkthrough.

macOS Compatibility

How It Works on macOS

macOS recognizes Generic mode as a standard Bluetooth HID gamepad via HID-over-GATT (HOGP). The device appears in System Settings > Bluetooth and is accessible via Apple's GCController (GameController framework) and IOHIDManager.

Pairing

  1. Open System Settings > Bluetooth
  2. Put the ESP32 into pairing mode (call begin() in your sketch)
  3. Click "Connect" next to the device name
  4. The device appears as a gamepad in any app that supports controllers

Or via command line:

blueutil --pair XX:XX:XX:XX:XX:XX

HID API Access (hidapi)

On macOS, the hidapi library uses IOHIDManager as its backend. The device can be opened directly:

import hid
for d in hid.enumerate():
    if d["product_string"] == "ESP32 BLE Gamepad":
        dev = hid.Device(path=d["path"])
        # read/write reports...
        dev.close()

GameController Framework (Swift/Objective-C)

The device is accessible via GCController:

import GameController

NotificationCenter.default.addObserver(
    forName: .GCControllerDidConnect, object: nil, queue: nil
) { notification in
    if let controller = notification.object as? GCController {
        print("Connected: \(controller.vendorName ?? "Unknown")")
        // Map buttons, axes, etc.
    }
}

Battery Level

Battery level is not automatically surfaced to macOS system tools (unlike Linux's upower). You must read it via the Battery Service characteristic using hidapi or equivalent.

Troubleshooting

  • Device doesn't appear in Bluetooth settings: Confirm sketch is running begin(), wait for BLE scan
  • hid.enumerate() doesn't find it: Use blueutil --info XX:XX:XX:XX:XX:XX to confirm pairing
  • Controller detected but buttons/axes wrong: Check your BleGamepadConfiguration matches what the game expects
  • Multiple HID devices conflict: macOS may grab the wrong IOHIDManager device; set a unique setDeviceName()
  • Connection drops intermittently: Ensure the ESP32 has adequate power; BLE on macOS can be sensitive to signal strength

Linux Testing (Quick Reference)

For detailed Linux testing see LinuxHIDTesting.md. Key commands:

# Pair
bluetoothctl
agent NoInputNoOutput
default-agent
scan on
pair XX:XX:XX:XX:XX:XX
trust XX:XX:XX:XX:XX:XX
connect XX:XX:XX:XX:XX:XX

# Test input
jstest --normal /dev/input/js0
evtest /dev/input/event0

# Monitor
sudo btmon -i hci0

# Battery
upower -e | grep gaming_input

Android Axis Mapping

Android maps gamepad axes differently than Windows/Linux:

FunctionWindows/LinuxAndroid
Left TriggerRx axisGAS (simulation)
Right TriggerRy axisBRAKE (simulation)
Right Stick XZ axisZ axis
Right Stick YRz axisRx axis

To work around this on Android, enable the Accelerator and Brake simulation controls:

config.setWhichSimulationControls(false, false, true, true, false);

Then use setAccelerator() for right trigger and setBrake() for left trigger on Android.

For right thumbstick, use setZ() and setRX() instead of setRightThumb(), or use setRightThumbAndroid(z, rx).

References