Page 1 of 1

Esp32 as BT Hid Gamepad

Posted: Sun Mar 23, 2025 6:16 pm
by qheinal
i have tried for a couple of days to get esp32 BT Hid to work but it seems im missing something but i cant quite figure out what. It connects and shows up as a game controller in device manager however pressing buttons / simulating a button press does nothing. Windows doesnt even register it or hid input report could somehow be rejected or ignored. From verbose debug level you can see esp32 sending the report but not sure whats happening after.

if someone can provide a basic BT hid gamepad example or help fix this for Arduino IDE . Id appreciate it. I also know esp idf with vscode has bt hid examples but its only for keyboard, mouse and remote and but still dont know what im missing. Plus im more experienced using arduino ide and wanna learn how to setup bt without relying on a premade solution like the Ble Gamepad library which can break changes at anytime.

the code to reproduce the same issue.

Code: Select all

#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEServer.h>
#include <BLE2902.h>
#include "BLEHIDDevice.h"
#include "HIDTypes.h"

BLEHIDDevice* hid;
BLECharacteristic* inputReport;
BLEServer* server;
BLEAdvertising* advertising;


#define CHARACTERISTIC_UUID_MODEL_NUMBER        "2A24"      // Characteristic - Model Number String - 0x2A24
#define CHARACTERISTIC_UUID_SOFTWARE_REVISION   "2A28"      // Characteristic - Software Revision String - 0x2A28
#define CHARACTERISTIC_UUID_SERIAL_NUMBER       "2A25"      // Characteristic - Serial Number String - 0x2A25
#define CHARACTERISTIC_UUID_FIRMWARE_REVISION   "2A26"      // Characteristic - Firmware Revision String - 0x2A26
#define CHARACTERISTIC_UUID_HARDWARE_REVISION   "2A27"      // Characteristic - Hardware Revision String - 0x2A27


uint8_t reportData[3] = { 0, 0, 0};

static uint8_t reportMapSimple[] = {
  // Usage Page (Generic Desktop Controls)
  0x05, 0x01,  
  // Usage (Gamepad)
  0x09, 0x05,  
  // Collection (Application)
  0xA1, 0x01,  
  0x85, 0x01,     // Report ID 1
  // 16 Buttons (2 bytes)
  0x19, 0x01,     // Usage Minimum (Button 1) 
  0x29, 0x10,     // Usage Maximum (Button 16) 
  0x15, 0x00,     // Logical Minimum (0)
  0x25, 0x01,     // Logical Maximum (1)
  0x75, 0x01,     // Report Size (1 bit per button)
  0x95, 0x10,     // Report Count (16 buttons)
  0x81, 0x02,     // Input (Data, Variable, Absolute) 
  0xC0,
};  

bool deviceConnected = false;
// Handles Windows BLE HID Disconnection
class MyServerCallbacks : public BLEServerCallbacks {
    void onConnect(BLEServer* pServer) {
        deviceConnected = true;
        Serial.println(" Connected!");
    }

    void onDisconnect(BLEServer* pServer) {
        deviceConnected = false;
        Serial.println("Disconnected! Restarting Advertising...");
        advertising->start();  // Restart BLE Advertising
    }
};



void taskServer(void*) {

    vTaskDelay(1000 / portTICK_PERIOD_MS);  // Prevent race conditions
    BLEDevice::init("X1 Controller");
    server = BLEDevice::createServer();
    server->setCallbacks(new MyServerCallbacks());

    hid = new BLEHIDDevice(server); 
    inputReport = hid->inputReport(1); 
 


  BLECharacteristic* pCharacteristic_Software_Revision = hid->deviceInfo()->createCharacteristic(
        CHARACTERISTIC_UUID_SOFTWARE_REVISION,
        BLECharacteristic::PROPERTY_READ
      );

  pCharacteristic_Software_Revision->setValue("1.0b");

  BLECharacteristic* pCharacteristic_Serial_Number =  hid->deviceInfo()->createCharacteristic(
        CHARACTERISTIC_UUID_SERIAL_NUMBER,
        BLECharacteristic::PROPERTY_READ
      );
  pCharacteristic_Serial_Number->setValue("1234567");

  BLECharacteristic* pCharacteristic_Firmware_Revision =  hid->deviceInfo()->createCharacteristic(
        CHARACTERISTIC_UUID_FIRMWARE_REVISION,
        BLECharacteristic::PROPERTY_READ
      );
  pCharacteristic_Firmware_Revision->setValue("1.0");

  BLECharacteristic* pCharacteristic_Hardware_Revision =  hid->deviceInfo()->createCharacteristic(
        CHARACTERISTIC_UUID_HARDWARE_REVISION,
        BLECharacteristic::PROPERTY_READ
      );
  pCharacteristic_Hardware_Revision->setValue("1.0");
  

 
    //inputReport->addDescriptor(xBLE2902);  // Client Configuration Descriptor
    inputReport->setNotifyProperty(true);
    inputReport->setReadProperty(true);
    inputReport->setWriteProperty(true);

    hid->manufacturer()->setValue("Test");
    hid->pnp(0x04, 0xe502, 0xa111, 0x0110);  // Vendor ID, Product ID
    hid->hidInfo(0x00, 0x01);  // HID version
    //int mode = 1;
   // hid->protocolMode()->setValue(mode);

    hid->reportMap((uint8_t*)reportMapSimple, sizeof(reportMapSimple)); 
    hid->hidService()->start();
    hid->deviceInfo()->start();
    hid->startServices();
    hid->setBatteryLevel(100);  // Fixes Windows HID Driver Issue (Driver Error)
   

      // Initialize the security
    BLESecurity* pSecurity = new BLESecurity();
    pSecurity->setAuthenticationMode(ESP_LE_AUTH_BOND);  // Set the bonding mode (pairing required)
    pSecurity->setCapability(ESP_IO_CAP_NONE);  // No input/output capabilities (use Just Works)
   
    advertising = server->getAdvertising();
    advertising->setAppearance(HID_GAMEPAD);
    advertising->addServiceUUID(hid->deviceInfo()->getUUID());
    advertising->addServiceUUID(hid->hidService()->getUUID());
    advertising->start();

    Serial.println(" Gamepad Ready. Scan for Controller'");


    while (true) {
       vTaskDelay(10);
    }
}

//  Sends Gamepad Data
void sendGamepadReport() {
     if (deviceConnected && inputReport) {     
        reportData[0] = 0x1; //Input report
        inputReport->setValue(reportData, sizeof(reportData));
        inputReport->notify(true);

      /*uint8_t *cccd = inputReport->getDescriptorByUUID(BLEUUID((uint16_t)0x2902))->getValue();
        Serial.print("CCCD Value: "); 
        Serial.println(*cccd, HEX);*/
    } 
}

void setup() {
  // put your setup code here, to run once:
Serial.begin(115200);
Serial.setDebugOutput(true);
xTaskCreatePinnedToCore(taskServer, "BT Server", 65536, NULL, tskIDLE_PRIORITY +1, NULL,1);
}

void loop() {
      if (deviceConnected) {
        // Simulate button press and joystick movement
        reportData[1] ^= 1;    // Toggle button press
        reportData[2] =  1;   // Toggle button press     
        sendGamepadReport();   // Send update to connected device  
        vTaskDelay(10);
    } 
    else
    {
      vTaskDelay(200);
    } 
}

Re: Esp32 as BT Hid Gamepad

Posted: Mon Mar 24, 2025 5:01 am
by chegewara
I have not worked with ble hid for few years, so i am a bit rusted (sorry), but i spent couple hours to see why it is not working.
To be honest i still dont know whats wrong with this code, but after few changes i can see my android device finally can receive something.
Its not much, but i think its something you can start with.

Code: Select all

#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEServer.h>
#include <BLE2902.h>
#include "BLEHIDDevice.h"
#include "HIDTypes.h"
#include "Arduino.h"

BLEHIDDevice *hid;
BLECharacteristic *inputReport;
BLEServer *server;
BLEAdvertising *advertising;

#define CHARACTERISTIC_UUID_MODEL_NUMBER "2A24"			 // Characteristic - Model Number String - 0x2A24
#define CHARACTERISTIC_UUID_SOFTWARE_REVISION "2A28" // Characteristic - Software Revision String - 0x2A28
#define CHARACTERISTIC_UUID_SERIAL_NUMBER "2A25"		 // Characteristic - Serial Number String - 0x2A25
#define CHARACTERISTIC_UUID_FIRMWARE_REVISION "2A26" // Characteristic - Firmware Revision String - 0x2A26
#define CHARACTERISTIC_UUID_HARDWARE_REVISION "2A27" // Characteristic - Hardware Revision String - 0x2A27

uint8_t reportData[3] = {0, 0, 0};

static uint8_t reportMapSimple[] = {
		// Usage Page (Generic Desktop Controls)
		0x05,
		0x01,
		// Usage (Gamepad)
		0x09,
		0x05,
		// Collection (Application)
		0xA1,
		0x01,
		0x85,
		0x01, // Report ID 1
		// 16 Buttons (2 bytes)
		0x19,
		0x01, // Usage Minimum (Button 1)
		0x29,
		0x10, // Usage Maximum (Button 16)
		0x15,
		0x00, // Logical Minimum (0)
		0x25,
		0x01, // Logical Maximum (1)
		0x75,
		0x01, // Report Size (1 bit per button)
		0x95,
		0x10, // Report Count (16 buttons)
		0x81,
		0x02, // Input (Data, Variable, Absolute)
		0xC0
};

bool deviceConnected = false;
// Handles Windows BLE HID Disconnection
class MyServerCallbacks : public BLEServerCallbacks
{
	void onConnect(BLEServer *pServer)
	{
		deviceConnected = true;
		Serial.println(" Connected!");
	}

	void onDisconnect(BLEServer *pServer)
	{
		deviceConnected = false;
		Serial.println("Disconnected! Restarting Advertising...");
		advertising->start(); // Restart BLE Advertising
	}
};

// [991994.534494] input: X1 Controller as /devices/virtual/misc/uhid/0005:02E5:11A1.0006/input/input25
// [991994.535347] hid-generic 0005:02E5:11A1.0006: input,hidraw2: BLUETOOTH HID v10.01 Gamepad [X1 Controller] on 20:0b:74:27:25:d1
// [992173.417069] Bluetooth: hci0: Bad flag given (0x1) vs supported (0x0)
// [992179.371961] input: X1 Controller as /devices/virtual/misc/uhid/0005:02E5:11A1.0007/input/input26
// [992179.372036] hid-generic 0005:02E5:11A1.0007: input,hidraw2: BLUETOOTH HID v10.01 Gamepad [X1 Controller] on 20:0b:74:27:25:d1

void taskServer(void *)
{

	vTaskDelay(1000 / portTICK_PERIOD_MS); // Prevent race conditions
	BLEDevice::init("X1 Controller");
	server = BLEDevice::createServer();
	server->setCallbacks(new MyServerCallbacks());

	hid = new BLEHIDDevice(server);
	inputReport = hid->inputReport(1);

	BLECharacteristic *pCharacteristic_Software_Revision = hid->deviceInfo()->createCharacteristic(
			CHARACTERISTIC_UUID_SOFTWARE_REVISION,
			BLECharacteristic::PROPERTY_READ);

	pCharacteristic_Software_Revision->setValue("1.0b");

	BLECharacteristic *pCharacteristic_Serial_Number = hid->deviceInfo()->createCharacteristic(
			CHARACTERISTIC_UUID_SERIAL_NUMBER,
			BLECharacteristic::PROPERTY_READ);
	pCharacteristic_Serial_Number->setValue("1234567");

	BLECharacteristic *pCharacteristic_Firmware_Revision = hid->deviceInfo()->createCharacteristic(
			CHARACTERISTIC_UUID_FIRMWARE_REVISION,
			BLECharacteristic::PROPERTY_READ);
	pCharacteristic_Firmware_Revision->setValue("1.0");

	BLECharacteristic *pCharacteristic_Hardware_Revision = hid->deviceInfo()->createCharacteristic(
			CHARACTERISTIC_UUID_HARDWARE_REVISION,
			BLECharacteristic::PROPERTY_READ);
	pCharacteristic_Hardware_Revision->setValue("1.0");

	// inputReport->addDescriptor(xBLE2902);  // Client Configuration Descriptor
	inputReport->setNotifyProperty(true);
	inputReport->setReadProperty(true);
	inputReport->setWriteProperty(true);

	hid->manufacturer()->setValue("Test");
	hid->pnp(0x02, 0x02e5, 0xa111, 0x0110); // Vendor ID, Product ID
	hid->hidInfo(0x00, 0x01);								// HID version
	// int mode = 1;
	// hid->protocolMode()->setValue(mode);

	hid->reportMap((uint8_t *)reportMapSimple, sizeof(reportMapSimple));
	hid->hidService()->start();
	hid->deviceInfo()->start();
	hid->startServices();
	hid->setBatteryLevel(100); // Fixes Windows HID Driver Issue (Driver Error)

	// Initialize the security
	BLESecurity *pSecurity = new BLESecurity();
	pSecurity->setAuthenticationMode(ESP_LE_AUTH_BOND); // Set the bonding mode (pairing required)
	pSecurity->setCapability(ESP_IO_CAP_NONE);					// No input/output capabilities (use Just Works)

	advertising = server->getAdvertising();
	advertising->setAppearance(HID_GAMEPAD);
	advertising->addServiceUUID(hid->deviceInfo()->getUUID());
	advertising->addServiceUUID(hid->hidService()->getUUID());
	advertising->start();

	Serial.println(" Gamepad Ready. Scan for Controller'");

	while (true)
	{
		vTaskDelay(10);
	}
}

//  Sends Gamepad Data
void sendGamepadReport()
{
	if (deviceConnected && inputReport)
	{
		reportData[0] = 0x1; // Input report
		inputReport->setValue(reportData, sizeof(reportData));
		inputReport->notify();
		printf("test\n");
		for (size_t i = 0; i < 3; i++)
		{
			Serial.println(reportData[i]);
		}

		/*uint8_t *cccd = inputReport->getDescriptorByUUID(BLEUUID((uint16_t)0x2902))->getValue();
			Serial.print("CCCD Value: ");
			Serial.println(*cccd, HEX);*/
	}
}

void setup()
{
	// put your setup code here, to run once:
	Serial.begin(115200);
	Serial.setDebugOutput(true);
	xTaskCreatePinnedToCore(taskServer, "BT Server", 8000, NULL, tskIDLE_PRIORITY + 1, NULL, 1);
}

void loop()
{
	if (deviceConnected)
	{
		// Simulate button press and joystick movement
		reportData[1] ^= 1;	 // Toggle button press
		reportData[2] = 1;	 // Toggle button press
		sendGamepadReport(); // Send update to connected device
		vTaskDelay(1000);
	}
	else
	{
		vTaskDelay(200);
	}
}

Re: Esp32 as BT Hid Gamepad

Posted: Mon Mar 24, 2025 4:43 pm
by qheinal
i kind solved the issue by removing 1 byte from reportData and setting debug to none. It seems that when i was using verbose debug in arduino ide it causes windows to ignore hid reports because of some timing issue? idk and so while i was seeing it send 16 bytes in verbose mode i thought the reportData needed to be 16 bytes (report id + 15 bytes) even though the reportmap says its 15 bytes. but i also tried with 15 bytes while in verbose debugging mode and windows didnt receive anything. So its a combination of debug modes and wrong size hid report data.


But now i have two issues one is that Rx Axis of Gamepad seems to not smoothly go from between -1 to 1 or (-32768 to 32767).The controller im making has 6 axises, X,Y,Z, Rx,Ry,Rz with Z and Rz being Left and Right Triggers (analog trigger). so 5 of the 6 axises work as expected but only Rx behaves this way. Its as if windows expects one of the triggers to be a button or only supports 5 axises.
Looking at a 3rd party controller their hid report map only has 5 axises with the other trigger being button.

need to know if its a problem with my report map or a windows issue. ( The issue is not the data transfer or corruption. It is only present when using more than 5 axises)

static uint8_t reportMap[] = {
// Usage Page (Generic Desktop Controls)
0x05, 0x01,
// Usage (Gamepad)
0x09, 0x04, //0x05 Gamepad. 0x04 Joystick
// Collection (Application)
0xA1, 0x01,
0x85, 0x01, // Report ID 1

// Left Stick (X, Y , Z)
0xA1, 0x00, // Collection (Physical)
0x09, 0x30, // Usage (X)
0x09, 0x31, // Usage (Y)
0x09, 0x32, // Usage (Z)
0x15, 0x00, // Logical Minimum (0)
0x26, 0xFF, 0x0F,// Logical Maximum (4095) (0x0FFF = 4095)
0x75, 0x10, // Report Size (16 bits)
0x95, 0x03, // Report Count 3 axes)
0x81, 0x02, // Input (Data, Variable, Absolute)
0xC0, // End Collection

// Right Stick (Rx, Ry , Rz)
0xA1, 0x00, // Collection (Physical)
0x09, 0x33, // Usage (Rx)
0x09, 0x34, // Usage (Ry)
0x09, 0x35, // Usage (Rz)
0x15, 0x00, // Logical Minimum (0)
0x26, 0xFF, 0x0F,// Logical Maximum (4095) (0x0FFF = 4095)
0x75, 0x10, // Report Size (16 bits)
0x95, 0x03, // Report Count (3 axes)
0x81, 0x02, // Input (Data, Variable, Absolute)
0xC0, // End Collection

/// 16 Buttons (2 bytes)
0x19, 0x01, // Usage Minimum (Button 1) - 1
0x29, 0x10, // Usage Maximum (Button 16) - 10
0x15, 0x00, // Logical Minimum (0)
0x25, 0x01, // Logical Maximum (1)
0x75, 0x01, // Report Size (1 bit per button)
0x95, 0x10, // Report Count (16 buttons)
0x81, 0x02, // Input (Data, Variable, Absolute)

// D-pad (1 byte)
0x09, 0x39, // Usage (Hat switch)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x07, // Logical Maximum (7)
0x35, 0x00, // Physical Minimum (0)
0x46, 0x07, 0x00, // Physical Maximum (7)
0x75, 0x04, // Report Size (4 bits)
0x95, 0x01, // Report Count (1 hat switch)
0x81, 0x42, // Input (Data, Variable, Absolute, Null State)
0x75, 0x04, // Report Size (4 bits) (Padding)
0x95, 0x01, // Report Count (1)
0x81, 0x03, // Input (Constant, Variable, Absolute)
// End Collection
0xC0,

};


The other issue i have is that when you first pair controller is works and gets input reports but if you disconnect esp32 from power or something happens to cause a disconnect on esp32 , when it reconnects it no longer receives the input reports until you remove it from windows and pair it again with windows

Also i found this tool useful in seeing if the hid reports was being sent and what the hid report contain https://nondebug.github.io/webhid-explorer/