Skip to content

Commit 549ef5d

Browse files
fix(ble): Fix HID BLE permissions, add examples and fix characteristic handle (#12260)
* fix(ble): Fix HID BLE permissions, add examples and fix char handle * ci(pre-commit): Apply automatic fixes --------- Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
1 parent 15ef0aa commit 549ef5d

File tree

10 files changed

+532
-28
lines changed

10 files changed

+532
-28
lines changed
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
/*
2+
* BLE HID Gamepad Client Example
3+
*
4+
* This example demonstrates how to connect to a BLE HID Gamepad and read its input.
5+
* The ESP32 acts as a BLE Central (client) that connects to a BLE gamepad peripheral.
6+
*
7+
* Features:
8+
* - Scans for BLE HID gamepad devices
9+
* - Connects to the first gamepad found
10+
* - Subscribes to input report notifications
11+
* - Parses and displays gamepad input (buttons and axes)
12+
* - Automatic reconnection on disconnect
13+
*
14+
* Usage:
15+
* 1. Upload this sketch to your ESP32
16+
* 2. Turn on your BLE gamepad (or run the Server_Gamepad example on another ESP32)
17+
* 3. The ESP32 will scan, connect, and display gamepad input in the serial monitor
18+
*
19+
* Compatible with gamepads using the standard HID Report Descriptor format
20+
*
21+
* Created by lucasssvaz
22+
*/
23+
24+
#include <BLEDevice.h>
25+
26+
// HID Service UUID (standard UUID for HID over GATT)
27+
static BLEUUID hidServiceUUID((uint16_t)0x1812);
28+
// HID Report characteristic UUID (used for input/output reports)
29+
static BLEUUID reportCharUUID((uint16_t)0x2A4D);
30+
// HID Report Map characteristic UUID
31+
static BLEUUID reportMapUUID((uint16_t)0x2A4B);
32+
33+
static boolean doConnect = false;
34+
static boolean connected = false;
35+
static boolean doScan = false;
36+
static BLERemoteCharacteristic *pInputReportCharacteristic = nullptr;
37+
static BLEAdvertisedDevice *myDevice = nullptr;
38+
static BLEClient *pClient = nullptr;
39+
40+
// Gamepad report structure (adjust based on your gamepad's report descriptor)
41+
// This matches the Server_Gamepad example format
42+
struct GamepadReport {
43+
uint8_t reportId; // Report ID
44+
int8_t x; // X axis (-127 to 127)
45+
int8_t y; // Y axis (-127 to 127)
46+
uint8_t buttons; // 8 buttons (bit 0-7)
47+
} __attribute__((packed));
48+
49+
// Callback function to handle gamepad input notifications
50+
static void notifyCallback(BLERemoteCharacteristic *pBLERemoteCharacteristic, uint8_t *pData, size_t length, bool isNotify) {
51+
Serial.printf("Received %d bytes: ", length);
52+
53+
// Check if data length matches our expected gamepad report
54+
if (length == sizeof(GamepadReport)) {
55+
GamepadReport *report = (GamepadReport *)pData;
56+
57+
Serial.printf("ID=%d, X=%4d, Y=%4d, Buttons=0x%02X [", report->reportId, report->x, report->y, report->buttons);
58+
59+
// Display which buttons are pressed
60+
for (int i = 0; i < 8; i++) {
61+
if (report->buttons & (1 << i)) {
62+
Serial.printf("%d ", i + 1);
63+
}
64+
}
65+
Serial.println("]");
66+
} else {
67+
// Unknown format, just display hex dump
68+
Serial.print("Raw data: ");
69+
for (size_t i = 0; i < length; i++) {
70+
Serial.printf("%02X ", pData[i]);
71+
}
72+
Serial.println();
73+
}
74+
}
75+
76+
// Client callbacks to handle connection events
77+
class MyClientCallback : public BLEClientCallbacks {
78+
void onConnect(BLEClient *pclient) {
79+
Serial.println("Connected to gamepad");
80+
}
81+
82+
void onDisconnect(BLEClient *pclient) {
83+
connected = false;
84+
Serial.println("Disconnected from gamepad");
85+
}
86+
};
87+
88+
// Function to connect to the gamepad
89+
bool connectToServer() {
90+
Serial.print("Connecting to gamepad at ");
91+
Serial.println(myDevice->getAddress().toString().c_str());
92+
93+
pClient = BLEDevice::createClient();
94+
Serial.println(" - Created client");
95+
96+
pClient->setClientCallbacks(new MyClientCallback());
97+
98+
// Connect to the gamepad
99+
pClient->connect(myDevice);
100+
Serial.println(" - Connected to server");
101+
pClient->setMTU(185); // Set MTU for larger data transfers
102+
103+
// Obtain a reference to the HID service
104+
BLERemoteService *pRemoteService = pClient->getService(hidServiceUUID);
105+
if (pRemoteService == nullptr) {
106+
Serial.println("Failed to find HID service");
107+
pClient->disconnect();
108+
return false;
109+
}
110+
Serial.println(" - Found HID service");
111+
112+
// Get all characteristics to find input reports
113+
std::map<std::string, BLERemoteCharacteristic *> *pCharMap = pRemoteService->getCharacteristics();
114+
115+
// Look for input report characteristics (UUID 0x2A4D)
116+
for (auto const &entry : *pCharMap) {
117+
BLERemoteCharacteristic *pChar = entry.second;
118+
119+
if (pChar->getUUID().equals(reportCharUUID)) {
120+
// Check if this characteristic has notify property (input report)
121+
if (pChar->canNotify()) {
122+
Serial.printf(" - Found input report characteristic (handle: 0x%04X)\n", pChar->getHandle());
123+
124+
// Try to read Report Reference Descriptor to identify report type and ID
125+
BLERemoteDescriptor *pReportRefDesc = pChar->getDescriptor(BLEUUID((uint16_t)0x2908));
126+
if (pReportRefDesc != nullptr) {
127+
String refValue = pReportRefDesc->readValue();
128+
if (refValue.length() >= 2) {
129+
uint8_t reportId = refValue[0];
130+
uint8_t reportType = refValue[1];
131+
Serial.printf(" Report ID: %d, Type: %d (1=Input, 2=Output, 3=Feature)\n", reportId, reportType);
132+
133+
// We want input reports (type = 1)
134+
if (reportType == 1) {
135+
pInputReportCharacteristic = pChar;
136+
}
137+
}
138+
} else {
139+
// No report reference descriptor, assume it's an input report
140+
pInputReportCharacteristic = pChar;
141+
}
142+
}
143+
}
144+
}
145+
146+
if (pInputReportCharacteristic == nullptr) {
147+
Serial.println("Failed to find input report characteristic");
148+
pClient->disconnect();
149+
return false;
150+
}
151+
152+
// Subscribe to input report notifications
153+
Serial.println(" - Subscribing to input report notifications");
154+
pInputReportCharacteristic->registerForNotify(notifyCallback);
155+
156+
connected = true;
157+
Serial.println("Successfully connected and subscribed to gamepad!");
158+
return true;
159+
}
160+
161+
// Scan callback to detect gamepad devices
162+
class MyAdvertisedDeviceCallbacks : public BLEAdvertisedDeviceCallbacks {
163+
void onResult(BLEAdvertisedDevice advertisedDevice) {
164+
Serial.print("BLE Device found: ");
165+
Serial.print(advertisedDevice.toString().c_str());
166+
167+
// Check if device advertises HID service
168+
if (advertisedDevice.haveServiceUUID() && advertisedDevice.isAdvertisingService(hidServiceUUID)) {
169+
Serial.print(" - HID Device!");
170+
171+
// Check if it's a gamepad by appearance (0x03C4 = HID Gamepad)
172+
if (advertisedDevice.haveAppearance()) {
173+
uint16_t appearance = advertisedDevice.getAppearance();
174+
Serial.printf(" (Appearance: 0x%04X)", appearance);
175+
if (appearance == 0x03C4) {
176+
Serial.print(" - GAMEPAD!");
177+
}
178+
}
179+
Serial.println();
180+
181+
// Stop scanning and connect
182+
BLEDevice::getScan()->stop();
183+
myDevice = new BLEAdvertisedDevice(advertisedDevice);
184+
doConnect = true;
185+
doScan = true;
186+
} else {
187+
Serial.println();
188+
}
189+
}
190+
};
191+
192+
void setup() {
193+
Serial.begin(115200);
194+
Serial.println("\n=== BLE HID Gamepad Client ===");
195+
Serial.println("Scanning for BLE HID gamepads...\n");
196+
197+
BLEDevice::init("ESP32-Gamepad-Client");
198+
199+
// Create scanner and set callbacks
200+
BLEScan *pBLEScan = BLEDevice::getScan();
201+
pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
202+
pBLEScan->setInterval(1349);
203+
pBLEScan->setWindow(449);
204+
pBLEScan->setActiveScan(true);
205+
pBLEScan->start(5, false);
206+
}
207+
208+
void loop() {
209+
// Connect to gamepad if found
210+
if (doConnect == true) {
211+
if (connectToServer()) {
212+
Serial.println("\n*** Ready to receive gamepad input ***\n");
213+
} else {
214+
Serial.println("Failed to connect to gamepad");
215+
}
216+
doConnect = false;
217+
}
218+
219+
// Restart scanning if disconnected
220+
if (!connected && doScan) {
221+
Serial.println("\nScanning for gamepads...");
222+
BLEDevice::getScan()->start(5, false);
223+
delay(1000);
224+
}
225+
226+
delay(100);
227+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
fqbn_append: PartitionScheme=huge_app
2+
3+
requires_any:
4+
- CONFIG_SOC_BLE_SUPPORTED=y
5+
- CONFIG_ESP_HOSTED_ENABLE_BT_NIMBLE=y

0 commit comments

Comments
 (0)