|
| 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 [", |
| 58 | + report->reportId, report->x, report->y, report->buttons); |
| 59 | + |
| 60 | + // Display which buttons are pressed |
| 61 | + for (int i = 0; i < 8; i++) { |
| 62 | + if (report->buttons & (1 << i)) { |
| 63 | + Serial.printf("%d ", i + 1); |
| 64 | + } |
| 65 | + } |
| 66 | + Serial.println("]"); |
| 67 | + } else { |
| 68 | + // Unknown format, just display hex dump |
| 69 | + Serial.print("Raw data: "); |
| 70 | + for (size_t i = 0; i < length; i++) { |
| 71 | + Serial.printf("%02X ", pData[i]); |
| 72 | + } |
| 73 | + Serial.println(); |
| 74 | + } |
| 75 | +} |
| 76 | + |
| 77 | +// Client callbacks to handle connection events |
| 78 | +class MyClientCallback : public BLEClientCallbacks { |
| 79 | + void onConnect(BLEClient *pclient) { |
| 80 | + Serial.println("Connected to gamepad"); |
| 81 | + } |
| 82 | + |
| 83 | + void onDisconnect(BLEClient *pclient) { |
| 84 | + connected = false; |
| 85 | + Serial.println("Disconnected from gamepad"); |
| 86 | + } |
| 87 | +}; |
| 88 | + |
| 89 | +// Function to connect to the gamepad |
| 90 | +bool connectToServer() { |
| 91 | + Serial.print("Connecting to gamepad at "); |
| 92 | + Serial.println(myDevice->getAddress().toString().c_str()); |
| 93 | + |
| 94 | + pClient = BLEDevice::createClient(); |
| 95 | + Serial.println(" - Created client"); |
| 96 | + |
| 97 | + pClient->setClientCallbacks(new MyClientCallback()); |
| 98 | + |
| 99 | + // Connect to the gamepad |
| 100 | + pClient->connect(myDevice); |
| 101 | + Serial.println(" - Connected to server"); |
| 102 | + pClient->setMTU(185); // Set MTU for larger data transfers |
| 103 | + |
| 104 | + // Obtain a reference to the HID service |
| 105 | + BLERemoteService *pRemoteService = pClient->getService(hidServiceUUID); |
| 106 | + if (pRemoteService == nullptr) { |
| 107 | + Serial.println("Failed to find HID service"); |
| 108 | + pClient->disconnect(); |
| 109 | + return false; |
| 110 | + } |
| 111 | + Serial.println(" - Found HID service"); |
| 112 | + |
| 113 | + // Get all characteristics to find input reports |
| 114 | + std::map<std::string, BLERemoteCharacteristic *> *pCharMap = pRemoteService->getCharacteristics(); |
| 115 | + |
| 116 | + // Look for input report characteristics (UUID 0x2A4D) |
| 117 | + for (auto const &entry : *pCharMap) { |
| 118 | + BLERemoteCharacteristic *pChar = entry.second; |
| 119 | + |
| 120 | + if (pChar->getUUID().equals(reportCharUUID)) { |
| 121 | + // Check if this characteristic has notify property (input report) |
| 122 | + if (pChar->canNotify()) { |
| 123 | + Serial.printf(" - Found input report characteristic (handle: 0x%04X)\n", pChar->getHandle()); |
| 124 | + |
| 125 | + // Try to read Report Reference Descriptor to identify report type and ID |
| 126 | + BLERemoteDescriptor *pReportRefDesc = pChar->getDescriptor(BLEUUID((uint16_t)0x2908)); |
| 127 | + if (pReportRefDesc != nullptr) { |
| 128 | + String refValue = pReportRefDesc->readValue(); |
| 129 | + if (refValue.length() >= 2) { |
| 130 | + uint8_t reportId = refValue[0]; |
| 131 | + uint8_t reportType = refValue[1]; |
| 132 | + Serial.printf(" Report ID: %d, Type: %d (1=Input, 2=Output, 3=Feature)\n", reportId, reportType); |
| 133 | + |
| 134 | + // We want input reports (type = 1) |
| 135 | + if (reportType == 1) { |
| 136 | + pInputReportCharacteristic = pChar; |
| 137 | + } |
| 138 | + } |
| 139 | + } else { |
| 140 | + // No report reference descriptor, assume it's an input report |
| 141 | + pInputReportCharacteristic = pChar; |
| 142 | + } |
| 143 | + } |
| 144 | + } |
| 145 | + } |
| 146 | + |
| 147 | + if (pInputReportCharacteristic == nullptr) { |
| 148 | + Serial.println("Failed to find input report characteristic"); |
| 149 | + pClient->disconnect(); |
| 150 | + return false; |
| 151 | + } |
| 152 | + |
| 153 | + // Subscribe to input report notifications |
| 154 | + Serial.println(" - Subscribing to input report notifications"); |
| 155 | + pInputReportCharacteristic->registerForNotify(notifyCallback); |
| 156 | + |
| 157 | + connected = true; |
| 158 | + Serial.println("Successfully connected and subscribed to gamepad!"); |
| 159 | + return true; |
| 160 | +} |
| 161 | + |
| 162 | +// Scan callback to detect gamepad devices |
| 163 | +class MyAdvertisedDeviceCallbacks : public BLEAdvertisedDeviceCallbacks { |
| 164 | + void onResult(BLEAdvertisedDevice advertisedDevice) { |
| 165 | + Serial.print("BLE Device found: "); |
| 166 | + Serial.print(advertisedDevice.toString().c_str()); |
| 167 | + |
| 168 | + // Check if device advertises HID service |
| 169 | + if (advertisedDevice.haveServiceUUID() && advertisedDevice.isAdvertisingService(hidServiceUUID)) { |
| 170 | + Serial.print(" - HID Device!"); |
| 171 | + |
| 172 | + // Check if it's a gamepad by appearance (0x03C4 = HID Gamepad) |
| 173 | + if (advertisedDevice.haveAppearance()) { |
| 174 | + uint16_t appearance = advertisedDevice.getAppearance(); |
| 175 | + Serial.printf(" (Appearance: 0x%04X)", appearance); |
| 176 | + if (appearance == 0x03C4) { |
| 177 | + Serial.print(" - GAMEPAD!"); |
| 178 | + } |
| 179 | + } |
| 180 | + Serial.println(); |
| 181 | + |
| 182 | + // Stop scanning and connect |
| 183 | + BLEDevice::getScan()->stop(); |
| 184 | + myDevice = new BLEAdvertisedDevice(advertisedDevice); |
| 185 | + doConnect = true; |
| 186 | + doScan = true; |
| 187 | + } else { |
| 188 | + Serial.println(); |
| 189 | + } |
| 190 | + } |
| 191 | +}; |
| 192 | + |
| 193 | +void setup() { |
| 194 | + Serial.begin(115200); |
| 195 | + Serial.println("\n=== BLE HID Gamepad Client ==="); |
| 196 | + Serial.println("Scanning for BLE HID gamepads...\n"); |
| 197 | + |
| 198 | + BLEDevice::init("ESP32-Gamepad-Client"); |
| 199 | + |
| 200 | + // Create scanner and set callbacks |
| 201 | + BLEScan *pBLEScan = BLEDevice::getScan(); |
| 202 | + pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks()); |
| 203 | + pBLEScan->setInterval(1349); |
| 204 | + pBLEScan->setWindow(449); |
| 205 | + pBLEScan->setActiveScan(true); |
| 206 | + pBLEScan->start(5, false); |
| 207 | +} |
| 208 | + |
| 209 | +void loop() { |
| 210 | + // Connect to gamepad if found |
| 211 | + if (doConnect == true) { |
| 212 | + if (connectToServer()) { |
| 213 | + Serial.println("\n*** Ready to receive gamepad input ***\n"); |
| 214 | + } else { |
| 215 | + Serial.println("Failed to connect to gamepad"); |
| 216 | + } |
| 217 | + doConnect = false; |
| 218 | + } |
| 219 | + |
| 220 | + // Restart scanning if disconnected |
| 221 | + if (!connected && doScan) { |
| 222 | + Serial.println("\nScanning for gamepads..."); |
| 223 | + BLEDevice::getScan()->start(5, false); |
| 224 | + delay(1000); |
| 225 | + } |
| 226 | + |
| 227 | + delay(100); |
| 228 | +} |
0 commit comments