├── .gitattributes ├── BLE-VRBOX.ino ├── BoxBack.png ├── BoxFront.png ├── ControllerTop.png ├── ESP32Board.png ├── Gatt-VRBOX-Tasks └── Gatt-VRBOX-Tasks.ino └── Readme.md /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /BLE-VRBOX.ino: -------------------------------------------------------------------------------- 1 | //============================================================================== 2 | // Robot controlled by VR Box using BLE on an ESP32 3 | // Target: DoIt ESP32 DEVKIT V1, ESP-VROOM-32 module 4 | // Compiler: Arduino 1.8.10 5 | // SDK: ESP32 Arduino SDK by Expressif Ver 1.0.4 6 | //============================================================================== 7 | // ESP32 (DoIt ESP32 DEVKIT V1, ESP-VROOM-32 module) board 8 | // In Arduino Board Manager, Select the following; 9 | // Arduino Board: "ESP32 Dev Module" 10 | // Flash Mode: QIO 11 | // Flash Size: 4MB (32Mb) 12 | // Flash Frequency: "80MHz" 13 | // Upload Speed: 921600 14 | // Core Debug Level: None 15 | //****************************************************************************** 16 | 17 | // VRBOX is a handheld Bluetooth BLE device with a joystick and six useful 18 | // buttons. This code shows how to setup the ESP32 as a BLE client to work 19 | // with the VRBOX BLE server. Most of the code is generic to any BLE server. 20 | // The name of the BLE server to search for and the services and characteristics 21 | // are specific to the VRBOX server. 22 | // You can use this code as a primer on how to connect other BLE servers to the 23 | // ESP32 or as the basis of any ESP32 project that you want to control using the 24 | // VRBOX device. The code should work with any ESP32 module. 25 | 26 | // The VR BOX server will shut itself off if there is no activity for 5 minutes. 27 | // This will cause the ESP32 to lose the Bluetooth connection and do a reset. 28 | 29 | // I bought mine at the local 5 Below store. The box is labeled; "Spektrum VR Control 30 | // Bluetooth RemoteController". I have also seen similar looking devices on the 31 | // Internet for upwards of $20.00. "VR BOX" is the name the device uses to identify 32 | // itself to other bluetooth devices. 33 | 34 | // Each defined task requires its own dedicated RAM allocation for its stack 35 | // How much RAM to allocate is determined by how nested the task is and how 36 | // many variables are stored on the stack (local variables). The default size 37 | // defined here is for 5K * 4 bytes/32 bit word, so 20K bytes 38 | // If you are getting; 39 | // JGuru Meditation Error: Core 0 panoc'd (Unhandled debug exception) 40 | // Debug excaption reason: Stack canary watchpoint triggered (task name) 41 | // then increase the TaskStackSize by 1024 until the Stack canary errors stop. 42 | // The ESP32 VROOM module has 288K bytes of RAM. 43 | #define TaskStackSize 5120 44 | 45 | // The blue and green LEDs are used to indicate when the ESP32 is scanning for 46 | // BLE servers and when the ESP32 has connected to the BLE server. The LEDs 47 | // can be moved to any GPIO pin (except LED, that is the builtin blue LED) that 48 | // is not input only. 49 | 50 | #include "BLEDevice.h" 51 | 52 | //------ VR Box Definitions ----- 53 | enum 54 | { 55 | VB_TRIGGERS = 0, 56 | VB_JOYX, 57 | VB_JOYY, 58 | VB_BTNAB, 59 | VB_BTNCD, 60 | VB_NUMBYTES 61 | }; 62 | 63 | // ===== VR Box Button Masks ===== 64 | #define VB_LOW_TRIGGER 0x01 65 | #define VB_UPR_TRIGGER 0x02 66 | #define VB_BUTTON_A 0x10 67 | #define VB_BUTTON_B 0x20 68 | #define VB_BUTTON_C 0x01 69 | #define VB_BUTTON_D 0x02 70 | #define FRESHFLAG 0x80 71 | 72 | #define JOYTIMEOUT 30 // joystick no activity timeout in mS 73 | 74 | #define JoyStickDeadZone 0 75 | 76 | #define ServerName "VR BOX" // change this if your server uses a different name 77 | 78 | //----- ESP32 Definitions ------------------------------------------------------ 79 | //----- ESP32 GPIO Allocation ----- 80 | #define SCL 22 // GPIO 22 -> I2C SCL 81 | #define SDA 21 // GPIO 21 -> I2C SDA 82 | 83 | #define BUILTINLED 2 // GPIO 2 -> built-in blue LED, on ->high, off -> low 84 | #define GREENLED 15 // GPIO 15 -> Green LED - lit -> scanning 85 | #define BLUELED 5 // GPIO 2 -> Blue LED - lit -> connected 86 | #define REDLED 4 // GPIO 4 -> Red LED 87 | 88 | // these values are for GPIO driven LEDs. BUILTIN Blue LED is opposite. 89 | #define LEDOFF HIGH 90 | #define LEDON LOW 91 | 92 | // ===== VRBOX Modes ===== 93 | // This code assumes you are using the Mouse Mode 94 | // @ + A -> Music & Video mode 95 | // @ + B -> Horizontal Game mode 96 | // @ + C -> Vertical Game mode 97 | // @ + D -> Mouse Mode // use this mode 98 | // 4 byte notification, Trigger Sws, Joystick X, Joystick Y, 0x00 99 | 100 | // All four modes send data. However each mode uses different byte positions and 101 | // values for each of the switches. The joystick acts like a 'D' pad when not in 102 | // Mouse Mode (no analog value). 103 | 104 | typedef void (*NotifyCallback)(BLERemoteCharacteristic*, uint8_t*, size_t, bool); 105 | 106 | // this is the service UUID of the VR Control handheld mouse/joystick device (HID) 107 | static BLEUUID serviceUUID("00001812-0000-1000-8000-00805f9b34fb"); 108 | 109 | // Battery Service UUID 110 | static BLEUUID BatteryServiceUUID("0000180F-0000-1000-8000-00805f9b34fb"); 111 | 112 | // this characteristic UUID works for joystick & triggers (report) 113 | static BLEUUID ReportCharUUID("00002A4D-0000-1000-8000-00805f9b34fb"); // report 114 | 115 | 116 | static boolean doConnect = false; 117 | static boolean connected = false; 118 | static boolean doScan = false; 119 | static BLERemoteCharacteristic* pRemoteCharacteristic; 120 | static BLEAdvertisedDevice* myDevice; 121 | 122 | static BLERemoteCharacteristic* pBatRemoteCharacteristic; 123 | 124 | 125 | // pointer to a list of characteristics of the active service, 126 | // sorted by characteristic UUID 127 | std::map *pmap; 128 | std::map :: iterator itr; 129 | 130 | // pointer to a list of characteristics of the active service, 131 | // sorted by characteristic handle 132 | std::map *pmapbh; 133 | std::map :: iterator itrbh; 134 | 135 | // storage for pointers to characteristics we want to work with 136 | // to do: change to linked list ? 137 | BLERemoteCharacteristic *bleRcs[4]; 138 | 139 | // This is where we store the data from the buttons and joystick 140 | volatile byte VrBoxData[VB_NUMBYTES]; 141 | volatile bool flag = false; // indicates new data to process 142 | 143 | // joyTimer is a 30 millisecond re-triggerable timer that sets the joystick 144 | // back to center if no activity on the joystick or trigger buttons. 145 | volatile uint32_t joyTimer = millis(); 146 | 147 | // task handles 148 | TaskHandle_t HandleJS = NULL; // handle of the joystick task 149 | TaskHandle_t HandleAB = NULL; // handle of the A/B button task 150 | TaskHandle_t HandleCD = NULL; // handle of the C/D button task 151 | 152 | char bfr[80]; 153 | 154 | //****************************************************************************** 155 | // HID notification callback handler. 156 | //****************************************************************************** 157 | static void notifyCallback( 158 | BLERemoteCharacteristic* pBLERemoteCharacteristic, 159 | uint8_t* pData, 160 | size_t length, 161 | bool isNotify) 162 | { 163 | Serial.print("Notify callback for characteristic "); 164 | 165 | Serial.print(pBLERemoteCharacteristic->getUUID().toString().c_str()); 166 | Serial.print(" of data length "); 167 | Serial.println(length); 168 | Serial.print("data: "); 169 | 170 | for (int i = 0; i < length; i++) 171 | Serial.printf("%02X ", pData[i]); 172 | Serial.println(); 173 | 174 | // we are getting the two trigger buttons in the first byte, joyX & joyY in 2nd & 3rd bytes 175 | // A four byte report is the joystick/trigger buttons. 176 | // A two byte report is either the A/B buttons or the C/D buttons 177 | // Low nibble equal to 0x05 indicates A/B buttons. 178 | // A/B buttons auto-repeat if held. No other buttons do this. 179 | if (4 == length) 180 | { 181 | // copy data to VrBoxData 182 | for (int i = VB_TRIGGERS; i < VB_BTNAB; i++) 183 | VrBoxData[i] = pData[i]; 184 | 185 | // wake up the joystick/trigger buttons handler task 186 | if (HandleJS) 187 | vTaskResume(HandleJS); 188 | 189 | // restart the joystick timer 190 | joyTimer = millis() + JOYTIMEOUT; 191 | } 192 | else if (2 == length) 193 | { 194 | // show the received data 195 | if (0x05 == (pData[0] & 0x0F)) 196 | { 197 | // A/B button report, wake the A/B button handler task 198 | VrBoxData[VB_BTNAB] = pData[0] & 0xF0; 199 | if (HandleAB) 200 | vTaskResume(HandleAB); 201 | } 202 | else 203 | { 204 | // C/D button report, wake the C/D button handler task 205 | VrBoxData[VB_BTNCD] = pData[0]; 206 | if (HandleCD) 207 | vTaskResume(HandleCD); 208 | } 209 | } 210 | } // notifyCallback 211 | 212 | //****************************************************************************** 213 | // Battery notification callback handler. 214 | //****************************************************************************** 215 | static void BatteryNotifyCallback( 216 | BLERemoteCharacteristic* pBLERemoteCharacteristic, 217 | uint8_t* pData, 218 | size_t length, 219 | bool isNotify) 220 | { 221 | Serial.println("Battery Notification Callback Event"); 222 | Serial.print("Data length "); 223 | Serial.println(length); 224 | Serial.print("data: "); 225 | 226 | for (int i = 0; i < length; i++) 227 | Serial.printf("%02X ", pData[i]); 228 | Serial.println(); 229 | } // BatteryNotifyCallback 230 | 231 | //****************************************************************************** 232 | // Connection state change event callback handler. 233 | //****************************************************************************** 234 | class MyClientCallback : public BLEClientCallbacks 235 | { 236 | void onConnect(BLEClient* pclient) 237 | { 238 | Serial.println("onConnect event"); 239 | digitalWrite(GREENLED, LEDON); // indicate connected 240 | } 241 | 242 | void onDisconnect(BLEClient* pclient) 243 | { 244 | Serial.println("onDisconnect event"); 245 | connected = false; 246 | digitalWrite(GREENLED, LEDOFF); // indicate disconnected 247 | } 248 | }; 249 | 250 | //****************************************************************************** 251 | // Connect to a service, register for notifications from Report Characteristics. 252 | //****************************************************************************** 253 | bool setupCharacteristics(BLERemoteService* pRemoteService, NotifyCallback pNotifyCallback) 254 | { 255 | // get all the characteristics of the service using the handle as the key 256 | pmapbh = pRemoteService->getCharacteristicsByHandle(); 257 | 258 | // only interested in report characteristics that have the notify capability 259 | for (itrbh = pmapbh->begin(); itrbh != pmapbh->end(); itrbh++) 260 | { 261 | BLEUUID x = itrbh->second->getUUID(); 262 | Serial.print("Characteristic UUID: "); 263 | Serial.println(x.toString().c_str()); 264 | // the uuid must match the report uuid 265 | 266 | if (ReportCharUUID.equals(itrbh->second->getUUID())) 267 | { 268 | // found a report characteristic 269 | Serial.println("Found a report characteristic"); 270 | 271 | if (itrbh->second->canNotify()) 272 | { 273 | Serial.println("Can notify"); 274 | // register for notifications from this characteristic 275 | itrbh->second->registerForNotify(pNotifyCallback); 276 | 277 | sprintf(bfr, "Callback registered for: Handle: 0x%08X, %d", itrbh->first, itrbh->first); 278 | Serial.println(bfr); 279 | } 280 | else 281 | { 282 | Serial.println("No notification"); 283 | } 284 | } 285 | else 286 | { 287 | sprintf(bfr, "Found Characteristic UUID: %s\n", itrbh->second->getUUID().toString().c_str()); 288 | Serial.println(bfr); 289 | } 290 | } // for 291 | } // setupCharacteristics 292 | 293 | //****************************************************************************** 294 | // Validate the server has the correct name and services we are looking for. 295 | // The server must have the HID service, the Battery Service is optional. 296 | //****************************************************************************** 297 | bool connectToServer() 298 | { 299 | Serial.print("Forming a connection to "); 300 | Serial.println(myDevice->getAddress().toString().c_str()); 301 | 302 | BLEClient* pClient = BLEDevice::createClient(); 303 | Serial.println(" - Created client"); 304 | 305 | pClient->setClientCallbacks(new MyClientCallback()); 306 | 307 | // Connect to the remote BLE Server. 308 | pClient->connect(myDevice); // if you pass BLEAdvertisedDevice instead of address, it will be recognized type of peer device address (public or private) 309 | Serial.println(" - Connected to server"); 310 | 311 | // BLE servers may offer several services, each with unique characteristics 312 | // we can identify the type of service by using the service UUID 313 | 314 | // Obtain a reference to the service we are after in the remote BLE server. 315 | // this will return a pointer to the remote service if it has a matching service UUID 316 | BLERemoteService* pRemoteService = pClient->getService(serviceUUID); 317 | if (pRemoteService == nullptr) 318 | { 319 | Serial.print("Failed to find HID service UUID: "); 320 | Serial.println(serviceUUID.toString().c_str()); 321 | pClient->disconnect(); 322 | return false; 323 | } 324 | 325 | Serial.println(" - Found HID service"); 326 | setupCharacteristics(pRemoteService, notifyCallback); 327 | pRemoteService = pClient->getService(BatteryServiceUUID); 328 | if (pRemoteService == nullptr) 329 | { 330 | Serial.print("Failed to find battery service UUID: "); 331 | Serial.println(serviceUUID.toString().c_str()); 332 | } 333 | else 334 | { 335 | Serial.println(" - Found battery service"); 336 | setupCharacteristics(pRemoteService, BatteryNotifyCallback); 337 | } 338 | 339 | connected = true; 340 | } // connectToServer 341 | 342 | //****************************************************************************** 343 | // Scan for BLE servers and find the first one that advertises the service we are looking for. 344 | //****************************************************************************** 345 | class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks 346 | { 347 | // Called for each advertising BLE server. 348 | void onResult(BLEAdvertisedDevice advertisedDevice) 349 | { 350 | Serial.print("BLE Advertised Device found: "); 351 | Serial.println(advertisedDevice.toString().c_str()); 352 | 353 | // we have found a server, see if it has the name we are looking for 354 | if (advertisedDevice.haveName()) 355 | { 356 | if (0 == strcmp(ServerName, advertisedDevice.getName().c_str())) 357 | { 358 | Serial.println("Found VRBOX Server"); 359 | 360 | // we found a server with the correct name, see if it has the service we are 361 | // interested in (HID) 362 | 363 | if (advertisedDevice.haveServiceUUID() && advertisedDevice.isAdvertisingService(serviceUUID)) 364 | { 365 | Serial.println("Server has HID service"); 366 | 367 | BLEDevice::getScan()->stop(); 368 | digitalWrite(BLUELED, LEDOFF); // indicate not scanning 369 | 370 | myDevice = new BLEAdvertisedDevice(advertisedDevice); 371 | doConnect = true; 372 | doScan = true; 373 | } // Found our server 374 | else 375 | { 376 | Serial.println("Server does not have an HID service, not our server"); 377 | } 378 | } 379 | } 380 | else 381 | { 382 | Serial.println("Server name does not match, not our server"); 383 | } 384 | } // onResult 385 | }; // MyAdvertisedDeviceCallbacks 386 | 387 | 388 | // All of these tasks are designed to run forever. The tasks are resumed when 389 | // a notification message is received with new data. 390 | //****************************************************************************** 391 | // Joystick handler Task. 392 | // Moving the joystick off center causes this task to be resumed about every 393 | // 15ms. Press or release of either trigger button will also resume this task. 394 | // If this task does not complete in less than 15mS you will lose joystick 395 | // movement data !!! 396 | // Holding the lower button will prevent the server from detecting that the 397 | // upper button has been pressed. 398 | // Holding the upper trigger and pressing the lower trigger results in the server 399 | // sending a notification that the lower trigger is pressed (the upper trigger 400 | // will be zero!). Releasing the lower trigger will cause the server to send a 401 | // notification that the upper trigger is pressed and the lower trigger is 402 | // released. 403 | //****************************************************************************** 404 | void taskJoyStick(void *parameter) 405 | { 406 | int8_t x; 407 | int8_t y; 408 | uint8_t triggers; 409 | 410 | //===== if the task requires any one time initialization, put it here ===== 411 | 412 | // forever loop 413 | while(true) 414 | { 415 | // give up the CPU, wait for new data 416 | vTaskSuspend(NULL); 417 | 418 | // we just woke up, new data is available, convert joystick data to 419 | // signed 8 bit integers 420 | x = (int8_t)VrBoxData[VB_JOYX]; 421 | y = (int8_t)VrBoxData[VB_JOYY]; 422 | triggers = VrBoxData[VB_TRIGGERS]; 423 | 424 | Serial.printf("Joystick X: %d, Y: %d Triggers: %02X\n", x, y, triggers); 425 | 426 | if (y < -JoyStickDeadZone) 427 | { 428 | // move forward 429 | Serial.println("Forward"); 430 | 431 | //===== add your code here ===== 432 | 433 | } 434 | else if (y > JoyStickDeadZone) 435 | { 436 | // move backward 437 | Serial.println("Backward"); 438 | 439 | //===== add your code here ===== 440 | 441 | } 442 | 443 | if (x > JoyStickDeadZone) 444 | { 445 | // turn right 446 | Serial.println("Turn Right"); 447 | 448 | //===== add your code here ===== 449 | 450 | } 451 | else if (x < -JoyStickDeadZone) 452 | { 453 | // turn left 454 | Serial.println("Turn Left"); 455 | 456 | //===== add your code here ===== 457 | 458 | } 459 | 460 | if (triggers & VB_LOW_TRIGGER) 461 | { 462 | // the lower trigger button is pressed 463 | Serial.println("Low Trigger Pressed"); 464 | 465 | //===== add your code here ===== 466 | } 467 | 468 | if (triggers & VB_UPR_TRIGGER) 469 | { 470 | // the upper trigger button is pressed 471 | Serial.println("Upper Trigger Pressed"); 472 | 473 | //===== add your code here ===== 474 | 475 | } 476 | } // for 477 | } // taskJoyStick 478 | 479 | //****************************************************************************** 480 | // A & B Buttons handler Task. 481 | // Holding the A or B button down will cause this task to be invoked about every 482 | // 15ms. If this task does not complete within 15mS you will lose button events. 483 | // The AB buttons work similar to the trigger buttons in that the A button will 484 | // prevent the B button from being detected and will override the B button when 485 | // pressed while the B button is held down. 486 | //****************************************************************************** 487 | void taskButtonAB(void *parameter) 488 | { 489 | uint8_t buttons; 490 | 491 | //===== if the task requires any one time initialization, put it here ===== 492 | 493 | while(true) 494 | { 495 | // give up the CPU, wait for new data 496 | vTaskSuspend(NULL); 497 | 498 | // we just woke up, new data is available 499 | buttons = VrBoxData[VB_BTNAB]; 500 | Serial.printf("A/B Buttons: %02X\n", buttons); 501 | 502 | if (buttons & VB_BUTTON_A) 503 | { 504 | // button A pressed or is being held down 505 | Serial.println("Button A"); 506 | 507 | //===== add your code here ===== 508 | 509 | } 510 | 511 | if (buttons & VB_BUTTON_B) 512 | { 513 | // button B pressed or is being held down 514 | Serial.println("Button B"); 515 | 516 | //===== add your code here ===== 517 | 518 | } 519 | } // for 520 | } // taskButtonAB 521 | 522 | //****************************************************************************** 523 | // C & D Buttons handler Task. 524 | // Press or release of either the C or D button will resume this task. Holding 525 | // one button down blocks the Server from detecting the other button being 526 | // pressed. 527 | //****************************************************************************** 528 | void taskButtonCD(void *parameter) 529 | { 530 | uint8_t buttons; 531 | 532 | //===== if the task requires any one time initialization, put it here ===== 533 | 534 | while(true) 535 | { 536 | // give up the CPU 537 | vTaskSuspend(NULL); 538 | 539 | // we just woke up, new data is available 540 | buttons = VrBoxData[VB_BTNCD]; 541 | Serial.printf("C/D Buttons: %02X\n", buttons); 542 | 543 | if (buttons & VB_BUTTON_C) 544 | { 545 | // button C pressed 546 | Serial.println("Button C"); 547 | 548 | //===== add your code here ===== 549 | 550 | } 551 | 552 | if (buttons & VB_BUTTON_D) 553 | { 554 | // button D pressed 555 | Serial.println("Button D"); 556 | 557 | //===== add your code here ===== 558 | 559 | } 560 | } // for 561 | } // taskButtonCD 562 | 563 | //****************************************************************************** 564 | //****************************************************************************** 565 | void setup() 566 | { 567 | BaseType_t xReturned; 568 | 569 | Serial.begin(115200); 570 | 571 | pinMode(GREENLED, OUTPUT); 572 | digitalWrite(GREENLED, LEDOFF); 573 | pinMode(BLUELED, OUTPUT); 574 | digitalWrite(BLUELED, LEDOFF); 575 | pinMode(REDLED, OUTPUT); 576 | digitalWrite(REDLED, LEDOFF); 577 | 578 | // create tasks to handle the joystick and buttons 579 | xReturned = xTaskCreate(taskJoyStick, // task to handle activity on the joystick. 580 | "Joystick", // String with name of task. 581 | TaskStackSize, // Stack size in 32 bit words. 582 | NULL, // Parameter passed as input of the task 583 | 1, // Priority of the task. 584 | &HandleJS); // Task handle. 585 | if (pdPASS == xReturned) 586 | { 587 | Serial.println("Joystick Task Created"); 588 | } 589 | 590 | xReturned = xTaskCreate(taskButtonAB, // task to handle activity on the A & B buttons. 591 | "ButtonsAB", // String with name of task. 592 | TaskStackSize, // Stack size in 32 bit words. 593 | NULL, // Parameter passed as input of the task 594 | 1, // Priority of the task. 595 | &HandleAB); // Task handle. 596 | if (pdPASS == xReturned) 597 | { 598 | Serial.println("AB Button Task Created"); 599 | 600 | } 601 | 602 | xReturned = xTaskCreate(taskButtonCD, // task to handle activity on the C & D buttons. 603 | "ButtonsCD", // String with name of task. 604 | TaskStackSize, // Stack size in 32 bit words. 605 | NULL, // Parameter passed as input of the task 606 | 1, // Priority of the task. 607 | &HandleCD); // Task handle. 608 | if (pdPASS == xReturned) 609 | { 610 | Serial.println("CD Button Task Created"); 611 | 612 | } 613 | Serial.println("Starting ESP32 BLE Client..."); 614 | BLEDevice::init(""); 615 | 616 | // Retrieve a GATT Scanner and set the callback we want to use to be informed 617 | // when we have detected a new device. Specify that we want active scanning 618 | // and start the scan to run for 5 seconds. 619 | digitalWrite(BLUELED, LEDON); // indicate scanning 620 | BLEScan* pBLEScan = BLEDevice::getScan(); 621 | pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks()); 622 | pBLEScan->setInterval(1349); 623 | pBLEScan->setWindow(449); 624 | pBLEScan->setActiveScan(true); 625 | pBLEScan->start(5, false); // scan for 5 seconds 626 | 627 | if (!connected) 628 | { 629 | doScan = true; 630 | Serial.println("Offline, start a scan"); 631 | } 632 | } // End of setup. 633 | 634 | //****************************************************************************** 635 | // This is the Arduino main loop function. 636 | //****************************************************************************** 637 | void loop() 638 | { 639 | // If the flag "doConnect" is true then we have scanned for and found the desired 640 | // BLE Server with which we wish to connect. Now we connect to it. Once we are 641 | // connected we set the connected flag to be true. 642 | if (doConnect == true) 643 | { 644 | if (connectToServer()) 645 | { 646 | Serial.println("We are now connected to the BLE Server."); 647 | } 648 | else 649 | { 650 | Serial.println("We have failed to connect to the server; there is nothin more we will do."); 651 | } 652 | doConnect = false; 653 | } 654 | 655 | if (connected) 656 | { 657 | // joystick no activity detector 658 | if (joyTimer && (joyTimer < millis())) 659 | { 660 | // Serial.println("Timeout"); 661 | // no joystick notification for 30mS, center the joystik 662 | VrBoxData[VB_JOYX] = VrBoxData[VB_JOYY] = 0; 663 | 664 | // wake up the joystick task 665 | vTaskResume(HandleJS); 666 | 667 | joyTimer = 0; 668 | } 669 | } 670 | else if (doScan) 671 | { 672 | Serial.println("Start scanning after disconnect"); 673 | // this is just example to start scan after disconnect, most likely there is 674 | // a better way to do it in Arduino 675 | digitalWrite(BLUELED, LEDON); // indicate scanning 676 | digitalWrite(GREENLED, LEDOFF); // indicate not connected 677 | BLEDevice::getScan()->start(0); 678 | } 679 | 680 | delay(10); // Delay a second between loops. 681 | } // End of loop 682 | -------------------------------------------------------------------------------- /BoxBack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BigJBehr/ESP32-Bluetooth-BLE-Remote-Control/acb5979f3b07eb3acb3a305cb3e56b4b405f0be0/BoxBack.png -------------------------------------------------------------------------------- /BoxFront.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BigJBehr/ESP32-Bluetooth-BLE-Remote-Control/acb5979f3b07eb3acb3a305cb3e56b4b405f0be0/BoxFront.png -------------------------------------------------------------------------------- /ControllerTop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BigJBehr/ESP32-Bluetooth-BLE-Remote-Control/acb5979f3b07eb3acb3a305cb3e56b4b405f0be0/ControllerTop.png -------------------------------------------------------------------------------- /ESP32Board.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BigJBehr/ESP32-Bluetooth-BLE-Remote-Control/acb5979f3b07eb3acb3a305cb3e56b4b405f0be0/ESP32Board.png -------------------------------------------------------------------------------- /Gatt-VRBOX-Tasks/Gatt-VRBOX-Tasks.ino: -------------------------------------------------------------------------------- 1 | //***************************************************************************** 2 | // File Name: Gatt-VRBOX-Tasks.ino 3 | // Author: James R. Behrens 4 | // Project: ESP32 Bluetooth BLE client for VRBOX BLE server 5 | // Compiler: Arduino IDE Ver 1.8.5 with ESP32 plugin 6 | // Target: ESP32 on DoIt Devkit Ver 1 7 | //***************************************************************************** 8 | // REVISION HISTORY: 9 | // 10 | // Date Who Remark: 11 | // ======== === =============================================================== 12 | // 03-15-18 JRB Ported from Gatt-Client. Modified to work with VRBOX BLE server 13 | // 04-18-18 JRB Modified to use FreeRTOS tasks for the notification events. 14 | //***************************************************************************** 15 | // This project is a port of the "how-to-use-arduino-esp32-ble-as-gatt-client" 16 | // demo downloaded from; 17 | // http://www.iotsharing.com/2017/07/how-to-use-arduino-esp32-ble-as-gatt-client.html 18 | // The demo has been extensively modified to work with the VRBOX server device. 19 | //***************************************************************************** 20 | 21 | // VRBOX is a handheld Bluetooth BLE device with a joystick and six useful 22 | // buttons. This code shows how to setup the ESP32 as a BLE client to work 23 | // with the VRBOX BLE server. Most of the code is generic to any BLE server. 24 | // The name of the BLE server to search for and the services and characteristics 25 | // are specific to the VRBOX server. 26 | // You can use this code as a primer on how to connect other BLE servers to the 27 | // ESP32 or as the basis of any ESP32 project that you want to control using the 28 | // VRBOX device. The code should work with any ESP32 module. 29 | 30 | // The VR BOX server will shut itself off if there is no activity for 5 minutes. 31 | // This will cause the ESP32 to lose the Bluetooth connection and do a reset. 32 | 33 | // Bluetooth files are here: C:\Users\BigJ\Documents\Arduino\hardware\espressif\esp32\tools\sdk\include\bluedroid 34 | // The user name (BigJ) will be different on your computer 35 | 36 | #pragma GCC diagnostic push 37 | #pragma GCC diagnostic warning "-fpermissive" 38 | #include 39 | #include 40 | #include 41 | #include 42 | #include "controller.h" 43 | #include "bt.h" 44 | #include "bt_trace.h" 45 | #include "bt_types.h" 46 | #include "btm_api.h" 47 | #include "bta_api.h" 48 | #include "bta_gatt_api.h" 49 | #include "esp_gap_ble_api.h" 50 | #include "esp_gattc_api.h" 51 | #include "esp_gatt_defs.h" 52 | #include "esp_bt_main.h" 53 | 54 | #pragma GCC diagnostic pop 55 | 56 | // uncomment to see all of the Bluetooth debug messages 57 | //#define VERBOSE 58 | 59 | // Each defined task requires its own dedicated RAM allocation for its stack 60 | // How much RAM to allocate is determined by how nested the task is and how 61 | // many variables are stored on the stack (local variables). The default size 62 | // defined here is for 5K * 4 bytes/32 bit word, so 20K bytes 63 | // If you are getting; 64 | // JGuru Meditation Error: Core 0 panoc'd (Unhandled debug exception) 65 | // Debug excaption reason: Stack canary watchpoint triggered (task name) 66 | // then increase the TaskStackSize by 1024 until the Stack canary errors stop. 67 | // The ESP32 VROOM module has 288K bytes of RAM. 68 | #define TaskStackSize 5120 69 | 70 | // The blue and green LEDs are used to indicate when the ESP32 is scanning for 71 | // BLE servers and when the ESP32 has connected to the VRBOX server. The LEDs 72 | // can be moved to any GPIO pin (except LED, that is the builtin blue LED) that 73 | // is not input only. 74 | 75 | // ===== Allocate GPIO of the ESP32 ===== 76 | #define LED 2 // built-in blue LED, high -> on, low -> off 77 | #define BLUELED 15 // lit -> connected 78 | #define GREENLED 5 // lit -> scanning 79 | #define REDLED 4 80 | 81 | #define SCL 22 // I2C SCL 82 | #define SDA 21 // I2C SDA 83 | 84 | // these values are for a common anode tri-color LED. if you use a common 85 | // cathode then swap the on/off definitions 86 | #define LEDON LOW 87 | #define LEDOFF HIGH 88 | 89 | // Services Available on VR Box 90 | #define GATT_UUID_GENERIC_ACCESS 0x1800 // device name, appearance, peripheral preferred connection parameters 91 | #define GATT_UUID_GENERIC_ATTRIBUTE 0x1801 // generic attribute 92 | 93 | // These are defined in esp_gatt_defs.h 94 | // ESP_GATT_UUID_DEVICE_INFO_SVC 0x180A // PNP ID (Vid & Pid) 95 | // ESP_GATT_UUID_BATTERY_SERVICE_SVC 0x180F // Battery Service 96 | // ESP_GATT_UUID_HID_SVC 0x1812 // HID Info, report map, reports, HID control point, protocol mode 97 | 98 | // forward declaration of callback functions 99 | static void esp_gap_cb(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param); 100 | static void esp_gattc_cb(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param); 101 | static void gattc_hid_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param); 102 | static void gattc_battery_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param); 103 | 104 | // name of BLE server that you want to connect to 105 | static const char device_name[] = "VR BOX"; 106 | static bool connect = false; 107 | 108 | // BLE scan params 109 | static esp_ble_scan_params_t ble_scan_params = { 110 | .scan_type = BLE_SCAN_TYPE_ACTIVE, 111 | .own_addr_type = BLE_ADDR_TYPE_PUBLIC, 112 | .scan_filter_policy = BLE_SCAN_FILTER_ALLOW_ALL, 113 | .scan_interval = 0x50, 114 | .scan_window = 0x30 115 | }; 116 | 117 | // HID service has ten characteristics 118 | #define CHAR_NUM 10 119 | 120 | // a GATT profile is a GATT service. we need an instance of this structure 121 | // for each service we want to use. 122 | // this structure holds the information for a GATT profile 123 | typedef struct gattc_profile_inst 124 | { 125 | esp_gattc_cb_t gattc_cb; // callback handler for this profile 126 | uint16_t gattc_if; // client interface number 127 | uint16_t app_id; // profile ID 128 | uint16_t conn_id; // connection ID 129 | uint16_t start_handle; // service starting handle 130 | uint16_t end_handle; // service ending handle 131 | uint16_t char_handle[CHAR_NUM]; // array of active characteristic handles for this service 132 | esp_bd_addr_t remote_bda; // MAC address 133 | } gattc_profile_t; 134 | 135 | 136 | // these values will be used to index the array of service profiles 137 | enum 138 | { 139 | HID_PROFILE = 0, 140 | BATTERY_PROFILE, 141 | NUMPROFILES 142 | }; 143 | 144 | // this is an array of gattc profiles. each entry is the profile for a service we want to use 145 | static gattc_profile_t profiles[NUMPROFILES] = { 146 | {.gattc_cb = gattc_hid_event_handler, // pointer to the GATTC event handler for the service 147 | .gattc_if = ESP_GATT_IF_NONE}, // gsttc_if is unknown at this point 148 | {.gattc_cb = gattc_battery_event_handler, // pointer to the GATTC event handler for the service 149 | .gattc_if = ESP_GATT_IF_NONE}, // gsttc_if is unknown at this point 150 | }; 151 | 152 | 153 | enum 154 | { 155 | VB_TRIGGERS = 0, 156 | VB_JOYX, 157 | VB_JOYY, 158 | VB_BTNAB, 159 | VB_BTNCD, 160 | VB_NUMBYTES 161 | }; 162 | 163 | // ===== VR Box Buttom Masks ===== 164 | #define VB_LOW_TRIGGER 0x01 165 | #define VB_UPR_TRIGGER 0x02 166 | #define VB_BUTTON_A 0x10 167 | #define VB_BUTTON_B 0x20 168 | #define VB_BUTTON_C 0x01 169 | #define VB_BUTTON_D 0x02 170 | #define FRESHFLAG 0x80 171 | 172 | #define JOYTIMEOUT 30 // joystick no activity timeout in mS 173 | 174 | #define JoyStickDeadZone 0 175 | 176 | // This is where we store the data from the buttons and joystick 177 | volatile byte VrBoxData[VB_NUMBYTES]; 178 | volatile bool flag = false; // indicates new data to process 179 | bool scanning = false; 180 | 181 | byte lastTrig = 0; // trigger buttons last value 182 | byte lastAB = 0; // A/B buttons last value 183 | byte lastCD = 0; // C/D buttons last value 184 | byte lastX = 0; // joystick's last values 185 | byte lastY = 0; 186 | 187 | // joyTimer is a 30 millisecond re-triggerable timer that sets the joystick 188 | // back to center if no activity on the joystick or trigger buttons. 189 | volatile uint32_t joyTimer = millis; 190 | 191 | // task handles 192 | TaskHandle_t HandleJS = NULL; // handle of the joystick task 193 | TaskHandle_t HandleAB = NULL; // handle of the joystick task 194 | TaskHandle_t HandleCD = NULL; // handle of the joystick task 195 | 196 | //****************************************************************************** 197 | // This is the GATT Client callback handler for the HID service 198 | //****************************************************************************** 199 | static void gattc_hid_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) 200 | { 201 | #ifdef VERBOSE 202 | Serial.println("\nHID Profile event"); 203 | #endif 204 | gattc_profile_event_handler(event, gattc_if, param, HID_PROFILE); 205 | } // gattc_hid_event_handler 206 | 207 | //****************************************************************************** 208 | // This is the GATT Client callback handler for the Battery service 209 | //****************************************************************************** 210 | static void gattc_battery_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) 211 | { 212 | #ifdef VERBOSE 213 | Serial.println("\nBattery Profile event"); 214 | #endif 215 | gattc_profile_event_handler(event, gattc_if, param, BATTERY_PROFILE); 216 | } // gattc_battery_event_handler 217 | 218 | //****************************************************************************** 219 | // This callback will be invoked when GATT BLE events come. 220 | // Refer GATT Client callback function events here: 221 | // https://github.com/espressif/esp-idf/blob/master/components/bt/bluedroid/api/include/esp_gattc_api.h 222 | //****************************************************************************** 223 | static void gattc_profile_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param, int idx) 224 | { 225 | uint16_t conn_id = 0; 226 | esp_ble_gattc_cb_param_t *p_data = (esp_ble_gattc_cb_param_t *)param; 227 | 228 | #ifdef VERBOSE 229 | Serial.printf("Entry Profile index: %d\n", idx); 230 | #endif 231 | 232 | switch (event) 233 | { 234 | case ESP_GATTC_REG_EVT: 235 | { 236 | #ifdef VERBOSE 237 | Serial.println("ESP_GATTC_REG_EVT"); 238 | #endif 239 | break; 240 | } 241 | 242 | // this event occurs when the connection is set up 243 | case ESP_GATTC_OPEN_EVT: 244 | { 245 | #ifdef VERBOSE 246 | Serial.println("ESP_GATTC_OPEN_EVT"); 247 | #endif 248 | 249 | conn_id = p_data->open.conn_id; 250 | 251 | // copy MAC address to both service profiles 252 | memcpy(profiles[HID_PROFILE].remote_bda, p_data->open.remote_bda, sizeof(esp_bd_addr_t)); 253 | memcpy(profiles[BATTERY_PROFILE].remote_bda, p_data->open.remote_bda, sizeof(esp_bd_addr_t)); 254 | 255 | // initialte service search 256 | #ifdef VERBOSE 257 | Serial.printf("Service index: %d, gattc_if: %d, conn_id: %d\n",idx, gattc_if, conn_id); 258 | // Serial.printf("Battery service gattc_if: %d, conn_id: %d\n", profiles[BATTERY_PROFILE].gattc_if, conn_id); 259 | #endif 260 | 261 | if (profiles[HID_PROFILE].gattc_if == gattc_if) 262 | { 263 | // last parameter NULL -> find all services, UUID of a service -> find only that service 264 | esp_ble_gattc_search_service(profiles[HID_PROFILE].gattc_if, conn_id, NULL); 265 | } 266 | else if (profiles[BATTERY_PROFILE].gattc_if == gattc_if) 267 | { 268 | esp_ble_gattc_search_service(profiles[BATTERY_PROFILE].gattc_if, conn_id, NULL); 269 | } 270 | 271 | profiles[HID_PROFILE].conn_id = conn_id; 272 | profiles[BATTERY_PROFILE].conn_id = conn_id; 273 | break; 274 | } 275 | 276 | // this event occurs when GATT service discovery result happens 277 | // this event occurs once for each service discovered (five times for VRBOX) 278 | case ESP_GATTC_SEARCH_RES_EVT: 279 | { 280 | #ifdef VERBOSE 281 | Serial.println("ESP_GATTC_SEARCH_RES_EVT"); 282 | #endif 283 | 284 | esp_gatt_srvc_id_t *srvc_id = (esp_gatt_srvc_id_t *)&p_data->search_res.srvc_id; 285 | conn_id = p_data->search_res.conn_id; 286 | 287 | // see if the remote device has the services we are interested in 288 | if (srvc_id->id.uuid.len == ESP_UUID_LEN_16) 289 | { 290 | switch (srvc_id->id.uuid.uuid.uuid16) 291 | { 292 | case ESP_GATT_UUID_HID_SVC: 293 | #ifdef VERBOSE 294 | Serial.println("HID service found"); 295 | #endif 296 | 297 | // save the start and end handles for the service's characteristics 298 | profiles[HID_PROFILE].start_handle = p_data->search_res.start_handle; 299 | profiles[HID_PROFILE].end_handle = p_data->search_res.end_handle; 300 | 301 | #ifdef VERBOSE 302 | Serial.printf("HID handles: %d - %d\n", p_data->search_res.start_handle, p_data->search_res.end_handle); 303 | #endif 304 | break; 305 | 306 | case ESP_GATT_UUID_BATTERY_SERVICE_SVC: 307 | #ifdef VERBOSE 308 | Serial.println("Battery service found"); 309 | #endif 310 | 311 | // save the start and end handles for the service's characteristics 312 | profiles[BATTERY_PROFILE].start_handle = p_data->search_res.start_handle; 313 | profiles[BATTERY_PROFILE].end_handle = p_data->search_res.end_handle; 314 | 315 | #ifdef VERBOSE 316 | Serial.printf("Battery handles: %d - %d\n", p_data->search_res.start_handle, p_data->search_res.end_handle); 317 | #endif 318 | break; 319 | 320 | default: 321 | #ifdef VERBOSE 322 | Serial.printf("Service UUID: %X found\n", srvc_id->id.uuid.uuid.uuid16); 323 | #endif 324 | break; 325 | } // switch 326 | } 327 | break; 328 | } 329 | 330 | // this event occurs when GATT service discovery is completed 331 | case ESP_GATTC_SEARCH_CMPL_EVT: 332 | { 333 | #ifdef VERBOSE 334 | Serial.println("ESP_GATTC_SEARCH_CMPL_EVT"); 335 | Serial.printf("Profile index: %d\n", idx); 336 | #endif 337 | 338 | // store connection id for later usage 339 | conn_id = p_data->search_cmpl.conn_id; 340 | 341 | esp_gattc_char_elem_t *char_elements; 342 | uint16_t char_offset = 0; 343 | uint16_t count = 0; 344 | gattc_profile_t *pProfile; 345 | 346 | // we need to do this for each service we are using 347 | // for (int service = 0; service < NUMPROFILES; service++) 348 | int service = idx; 349 | { 350 | pProfile = &profiles[service]; 351 | #ifdef VERBOSE 352 | Serial.printf("Characteristics for Service: %d, gattc_if: %d\n", service, pProfile->gattc_if); 353 | #endif 354 | 355 | // get characteristics of service. It just gets characteristic from local cache, won't get from remote devices 356 | esp_gatt_status_t status = esp_ble_gattc_get_attr_count(pProfile->gattc_if, 357 | pProfile->conn_id, 358 | ESP_GATT_DB_CHARACTERISTIC, 359 | pProfile->start_handle, 360 | pProfile->end_handle, 361 | ESP_GATT_INVALID_HANDLE, 362 | &count); 363 | if (count > 0) 364 | { 365 | char_elements = (esp_gattc_char_elem_t *)malloc(sizeof(esp_gattc_char_elem_t) * count); 366 | status = esp_ble_gattc_get_all_char(pProfile->gattc_if, 367 | pProfile->conn_id, 368 | pProfile->start_handle, 369 | pProfile->end_handle, 370 | char_elements, &count, char_offset); 371 | // show the characteristc info 372 | for (int i = 0; i < count; i++) 373 | { 374 | #ifdef VERBOSE 375 | Serial.printf("%d) char uuid = %x, char_handle = %d\n", i, char_elements[i].uuid.uuid.uuid16, char_elements[i].char_handle); 376 | #endif 377 | pProfile->char_handle[i] = char_elements[i].char_handle; 378 | 379 | // We use BLE notifications to monitor the buttons and joystick of the server. 380 | // VR Box sends a notification to a registered handle when a change occurs. 381 | // Here we register the characteristics we want to receive notifications for. 382 | // if notify bit set then register to receive notification from GATT server 383 | 384 | // if the characteristic is a report and can send notifications then register it for 385 | // notifications 386 | if (((ESP_GATT_UUID_BATTERY_LEVEL == char_elements[i].uuid.uuid.uuid16) || 387 | (ESP_GATT_UUID_HID_REPORT == char_elements[i].uuid.uuid.uuid16)) && 388 | (char_elements[i].properties & ESP_GATT_CHAR_PROP_BIT_NOTIFY)) 389 | { 390 | #ifdef VERBOSE 391 | Serial.println("Register for notification"); 392 | #endif 393 | 394 | // if it set then we enable notify, the event ESP_GATTC_REG_FOR_NOTIFY_EVT will be raised when finishing 395 | esp_ble_gattc_register_for_notify (gattc_if, pProfile->remote_bda, pProfile->char_handle[i]); 396 | } 397 | } // for each characteristic 398 | 399 | free(char_elements); 400 | } 401 | else 402 | { 403 | #ifdef VERBOSE 404 | Serial.println("No characteristics found"); 405 | #endif 406 | } 407 | } // for each service 408 | 409 | if (idx == 0) 410 | { 411 | #ifdef VERBOSE 412 | Serial.printf("GATT Opening Battery service: %d\n", profiles[BATTERY_PROFILE].gattc_if); 413 | #endif 414 | 415 | esp_ble_gattc_open(profiles[BATTERY_PROFILE].gattc_if, profiles[BATTERY_PROFILE].remote_bda, true); 416 | } 417 | 418 | break; 419 | } 420 | 421 | // this event occurs when the physical connection is disconnected 422 | case ESP_GATTC_DISCONNECT_EVT: 423 | { 424 | #ifdef VERBOSE 425 | Serial.println("ESP_GATTC_DISCONNECT_EVT"); 426 | #endif 427 | 428 | // brute force method, but works and is fairly quick !!! 429 | ESP.restart(); 430 | break; 431 | } 432 | 433 | // this event occurs when register for notification of a characteristic completes 434 | case ESP_GATTC_REG_FOR_NOTIFY_EVT: 435 | { 436 | #ifdef VERBOSE 437 | Serial.println("ESP_GATTC_REG_FOR_NOTIFY_EVT"); 438 | #endif 439 | break; 440 | } 441 | 442 | // this event occurs when the server sends a notification for a registered service/characteristic 443 | case ESP_GATTC_NOTIFY_EVT: 444 | { 445 | #ifdef VERBOSE 446 | Serial.println("ESP_GATTC_NOTIFY_EVT"); 447 | // idx == HID_PROFILE -> HID service notification 448 | // idx == BATTERY_PROFILE -> Battery service notification 449 | Serial.printf("Profile index: %d\n", idx); 450 | #endif 451 | 452 | if (HID_PROFILE == idx) 453 | { 454 | // we are getting the two trigger buttons in the first byte, joyX & joyY in 2nd & 3rd bytes 455 | // A four byte report is the joystick/trigger buttons. 456 | // A two byte report is either the A/B buttons or the C/D buttons 457 | // Low nibble equal to 0x05 indicates A/B buttons. 458 | // A/B buttons auto-repeat if held. No other buttons do this. 459 | if (4 == p_data->notify.value_len) 460 | { 461 | // show the received data 462 | // for (int i = 0; i < p_data->notify.value_len; i++) 463 | // Serial.printf("%02X ", p_data->notify.value[i]); 464 | // Serial.println(); 465 | 466 | // copy data to VrBoxData 467 | for (int i = VB_TRIGGERS; i < VB_BTNAB; i++) 468 | VrBoxData[i] = p_data->notify.value[i]; 469 | 470 | // wake up the joystick/trigger buttons handler task 471 | if (HandleJS) 472 | vTaskResume(HandleJS); 473 | 474 | // restart the joystick timer 475 | joyTimer = millis() + JOYTIMEOUT; 476 | } 477 | else if (2 == p_data->notify.value_len) 478 | { 479 | // show the received data 480 | // Serial.printf("data: %02X\n", p_data->notify.value[0]); 481 | if (0x05 == (p_data->notify.value[0] & 0x0F)) 482 | { 483 | // A/B button report, wake the A/B button handler task 484 | VrBoxData[VB_BTNAB] = p_data->notify.value[0] & 0xF0; 485 | if (HandleAB) 486 | vTaskResume(HandleAB); 487 | } 488 | else 489 | { 490 | // C/D button report, wake the C/D button handler task 491 | VrBoxData[VB_BTNCD] = p_data->notify.value[0]; 492 | if (HandleCD) 493 | vTaskResume(HandleCD); 494 | } 495 | } 496 | } 497 | else if (BATTERY_PROFILE == idx) 498 | { 499 | Serial.println("Battery Data"); 500 | for (int i = 0; i < p_data->notify.value_len; i++) 501 | Serial.printf("%02X ", p_data->notify.value[i]); 502 | Serial.println(); 503 | } 504 | break; 505 | } 506 | 507 | default: 508 | #ifdef VERBOSE 509 | Serial.printf("Unhandled GATT Profile Event: %X\n", event); 510 | #endif 511 | break; 512 | } // switch event 513 | } // gattc_profile_event_handler 514 | 515 | //****************************************************************************** 516 | // This callback will be invoked when GAP advertising events come. GAP is used 517 | // to scan for available servers. 518 | // Refer GAP BLE callback event type here: 519 | // https://github.com/espressif/esp-idf/blob/master/components/bt/bluedroid/api/include/esp_gap_ble_api.h 520 | //****************************************************************************** 521 | static void esp_gap_cb(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) 522 | { 523 | uint8_t *adv_name = NULL; 524 | uint8_t adv_name_len = 0; 525 | switch (event) 526 | { 527 | case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: 528 | { 529 | #ifdef VERBOSE 530 | Serial.println("ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT"); 531 | #endif 532 | 533 | StartScan(); 534 | break; 535 | } 536 | 537 | case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT: 538 | { 539 | #ifdef VERBOSE 540 | Serial.println("ESP_GAP_BLE_SCAN_START_COMPLETE_EVT"); 541 | #endif 542 | 543 | //scan start complete event to indicate scan start successfully or failed 544 | if (param->scan_start_cmpl.status != ESP_BT_STATUS_SUCCESS) 545 | { 546 | Serial.printf("\nScan start failed"); 547 | } 548 | break; 549 | } 550 | 551 | // processing scan result 552 | case ESP_GAP_BLE_SCAN_RESULT_EVT: 553 | { 554 | #ifdef VERBOSE 555 | Serial.println("ESP_GAP_BLE_SCAN_RESULT_EVT"); 556 | #endif 557 | 558 | esp_ble_gap_cb_param_t *scan_result = (esp_ble_gap_cb_param_t *)param; 559 | switch (scan_result->scan_rst.search_evt) 560 | { 561 | case ESP_GAP_SEARCH_INQ_RES_EVT: 562 | #ifdef VERBOSE 563 | Serial.println("ESP_GAP_SEARCH_INQ_RES_EVT"); 564 | #endif 565 | 566 | adv_name = esp_ble_resolve_adv_data(scan_result->scan_rst.ble_adv, ESP_BLE_AD_TYPE_NAME_CMPL, &adv_name_len); 567 | if (adv_name != NULL) 568 | { 569 | if (strlen(device_name) == adv_name_len && strncmp((char *)adv_name, device_name, adv_name_len) == 0) 570 | { 571 | // if connection is established then stop scanning 572 | if (connect == false) 573 | { 574 | connect = true; 575 | digitalWrite(BLUELED, LEDON); 576 | 577 | Serial.printf("Connected to the remote device %s\n", device_name); 578 | esp_ble_gap_stop_scanning(); 579 | #ifdef VERBOSE 580 | Serial.printf("GAP Opening HID service: %d\n", profiles[HID_PROFILE].gattc_if); 581 | #endif 582 | esp_ble_gattc_open(profiles[HID_PROFILE].gattc_if, scan_result->scan_rst.bda, true); 583 | } 584 | } 585 | } 586 | break; 587 | case ESP_GAP_SEARCH_INQ_CMPL_EVT: 588 | #ifdef VERBOSE 589 | Serial.println("ESP_GAP_SEARCH_INQ_CMPL_EVT"); 590 | #endif 591 | scanning = false; 592 | digitalWrite(GREENLED, LEDOFF); 593 | break; 594 | 595 | default: 596 | break; 597 | } 598 | break; 599 | } 600 | 601 | case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT: 602 | { 603 | #ifdef VERBOSE 604 | Serial.println("ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT"); 605 | #endif 606 | if (param->scan_stop_cmpl.status != ESP_BT_STATUS_SUCCESS) 607 | { 608 | Serial.println("Scan stop failed"); 609 | } 610 | else 611 | { 612 | #ifdef VERBOSE 613 | Serial.println("Stop scan successfully"); 614 | #endif 615 | scanning = false; 616 | digitalWrite(GREENLED, LEDOFF); 617 | } 618 | break; 619 | } 620 | 621 | case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT: 622 | { 623 | #ifdef VERBOSE 624 | Serial.printf("\nESP_GAP_BLE_ADV_STOP_COMPLETE_EVT\n"); 625 | #endif 626 | if (param->adv_stop_cmpl.status != ESP_BT_STATUS_SUCCESS){ 627 | Serial.printf("\nAdv stop failed\n"); 628 | } 629 | else 630 | { 631 | #ifdef VERBOSE 632 | Serial.printf("\nStop adv successfully"); 633 | #endif 634 | } 635 | break; 636 | } 637 | 638 | default: 639 | #ifdef VERBOSE 640 | Serial.printf("Unhandled GAP event: %X\n", event); 641 | #endif 642 | break; 643 | } // switch event 644 | } // esp_gap_cb 645 | 646 | //****************************************************************************** 647 | // This is the GATT event callback handler function. Handles GATTC events common 648 | // to all services. Uses gattc_if parameter to decide which GATTC service 649 | // handler to invoke. 650 | //****************************************************************************** 651 | // modify this to trap ESP_GATTC_NOTIFY_EVT. then select handler based on gattc_if. 652 | 653 | static void esp_gattc_cb(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) 654 | { 655 | // If event is register event, store the gattc_if for each profile 656 | if (event == ESP_GATTC_REG_EVT) 657 | { 658 | // aram->reg.app_id as the service profile index is only valid for this event !! 659 | #ifdef VERBOSE 660 | Serial.printf("ESP_GATTC_REG_EVT trap for: %d, gattc_if: %d\n", param->reg.app_id, gattc_if); 661 | #endif 662 | 663 | if (param->reg.status == ESP_GATT_OK) 664 | { 665 | // save gsttc_if value for the service profile selected by param->reg.app_id 666 | profiles[param->reg.app_id].gattc_if = gattc_if; 667 | } 668 | else 669 | { 670 | Serial.printf("Reg app failed, profile: %04x, status %d\n", param->reg.app_id, param->reg.status); 671 | return; 672 | } 673 | } 674 | 675 | // invoke the event handler of each registered service profile 676 | for (int idx = 0; idx < NUMPROFILES; idx++) 677 | { 678 | if (gattc_if == ESP_GATT_IF_NONE || gattc_if == profiles[idx].gattc_if) 679 | { 680 | if (profiles[idx].gattc_cb) 681 | { 682 | profiles[idx].gattc_cb(event, profiles[idx].gattc_if, param); 683 | } 684 | } 685 | } // for 686 | } // esp_gattc_cb 687 | 688 | //****************************************************************************** 689 | // This registers the functions we want to use to handler GAP and GATTC 690 | // callbacks. We only need to do this once for all profiles. 691 | //****************************************************************************** 692 | void ble_client_appRegister(void) 693 | { 694 | esp_err_t status; 695 | 696 | #ifdef VERBOSE 697 | Serial.printf("\nregister callback"); 698 | #endif 699 | 700 | // register the scan callback function to the gap module 701 | if ((status = esp_ble_gap_register_callback(esp_gap_cb)) != ESP_OK) 702 | { 703 | Serial.printf("\ngap register error, error code = %x\n", status); 704 | return; 705 | } 706 | 707 | // register the callback function to the gattc module 708 | if ((status = esp_ble_gattc_register_callback(esp_gattc_cb)) != ESP_OK) 709 | { 710 | Serial.printf("gattc register error, error code = %x\n", status); 711 | return; 712 | } 713 | 714 | esp_ble_gattc_app_register(HID_PROFILE); 715 | esp_ble_gattc_app_register(BATTERY_PROFILE); 716 | } // ble_client_appRegister 717 | 718 | //****************************************************************************** 719 | //****************************************************************************** 720 | void gattc_client_test(void) 721 | { 722 | esp_bluedroid_init(); 723 | esp_bluedroid_enable(); 724 | ble_client_appRegister(); 725 | } // gattc_client_test 726 | 727 | //****************************************************************************** 728 | //****************************************************************************** 729 | void StartScan(void) 730 | { 731 | if (!scanning) 732 | { 733 | #ifdef VERBOSE 734 | Serial.println("Scanning Started"); 735 | #endif 736 | 737 | scanning = true; 738 | digitalWrite(GREENLED, LEDON); 739 | // the unit of the duration is a second 740 | uint32_t duration = 30; 741 | esp_ble_gap_start_scanning(duration); 742 | } 743 | } // StartScan 744 | 745 | // All of these tasks are designed to run forever. The tasks are resumed when 746 | // a notification message is received with new data. 747 | //****************************************************************************** 748 | // Joystick handler Task. 749 | // Moving the joystick off center causes this task to be resumed about every 750 | // 15ms. Press or release of either trigger button will also resume this task. 751 | // If this task does not complete in less than 15mS you will lose joystick 752 | // movement data !!! 753 | // Holding the lower button will prevent the server from detecting that the 754 | // upper button has been pressed. 755 | // Holding the upper trigger and pressing the lower trigger results in the server 756 | // sending a notification that the lower trigger is pressed (the upper trigger 757 | // will be zero!). Releasing the lower trigger will cause the server to send a 758 | // notification that the upper trigger is pressed and the lower trigger is 759 | // released. 760 | //****************************************************************************** 761 | void taskJoyStick(void *parameter) 762 | { 763 | int8_t x; 764 | int8_t y; 765 | uint8_t triggers; 766 | 767 | //===== if the task requires any one time initialization, put it here ===== 768 | 769 | // forever loop 770 | while(true) 771 | { 772 | // give up the CPU, wait for new data 773 | vTaskSuspend(NULL); 774 | 775 | // we just woke up, new data is available, convert joystick data to 776 | // signed 8 bit integers 777 | x = (int8_t)VrBoxData[VB_JOYX]; 778 | y = (int8_t)VrBoxData[VB_JOYY]; 779 | triggers = VrBoxData[VB_TRIGGERS]; 780 | 781 | Serial.printf("Joystick X: %d, Y: %d Triggers: %02X\n", x, y, triggers); 782 | 783 | if (y < -JoyStickDeadZone) 784 | { 785 | // move forward 786 | Serial.println("Forward"); 787 | 788 | //===== add your code here ===== 789 | 790 | } 791 | else if (y > JoyStickDeadZone) 792 | { 793 | // move backward 794 | Serial.println("Backward"); 795 | 796 | //===== add your code here ===== 797 | 798 | } 799 | 800 | if (x > JoyStickDeadZone) 801 | { 802 | // turn right 803 | Serial.println("Turn Right"); 804 | 805 | //===== add your code here ===== 806 | 807 | } 808 | else if (x < -JoyStickDeadZone) 809 | { 810 | // turn left 811 | Serial.println("Turn Left"); 812 | 813 | //===== add your code here ===== 814 | 815 | } 816 | 817 | if (triggers & VB_LOW_TRIGGER) 818 | { 819 | // the lower trigger button is pressed 820 | Serial.println("Low Trigger Pressed"); 821 | 822 | //===== add your code here ===== 823 | } 824 | 825 | if (triggers & VB_UPR_TRIGGER) 826 | { 827 | // the upper trigger button is pressed 828 | Serial.println("Upper Trigger Pressed"); 829 | 830 | //===== add your code here ===== 831 | 832 | } 833 | } // for 834 | } // taskJoyStick 835 | 836 | //****************************************************************************** 837 | // A & B Buttons handler Task. 838 | // Holding the A or B button down will cause this task to be invoked about every 839 | // 15ms. If this task does not complete within 15mS you will lose button events. 840 | // The AB buttons work similar to the trigger buttons in that the A button will 841 | // prevent the B button from being detected and will override the B button when 842 | // pressed while the B button is held down. 843 | //****************************************************************************** 844 | void taskButtonAB(void *parameter) 845 | { 846 | uint8_t buttons; 847 | 848 | //===== if the task requires any one time initialization, put it here ===== 849 | 850 | while(true) 851 | { 852 | // give up the CPU, wait for new data 853 | vTaskSuspend(NULL); 854 | 855 | // we just woke up, new data is available 856 | buttons = VrBoxData[VB_BTNAB]; 857 | Serial.printf("A/B Buttons: %02X\n", buttons); 858 | 859 | if (buttons & VB_BUTTON_A) 860 | { 861 | // button A pressed or is being held down 862 | Serial.println("Button A"); 863 | 864 | //===== add your code here ===== 865 | 866 | } 867 | 868 | if (buttons & VB_BUTTON_B) 869 | { 870 | // button B pressed or is being held down 871 | Serial.println("Button B"); 872 | 873 | //===== add your code here ===== 874 | 875 | } 876 | } // for 877 | } // taskButtonAB 878 | 879 | //****************************************************************************** 880 | // C & D Buttons handler Task. 881 | // Press or release of either the C or D button will resume this task. Holding 882 | // one button down blocks the Server from detecting the other button being 883 | // pressed. 884 | //****************************************************************************** 885 | void taskButtonCD(void *parameter) 886 | { 887 | uint8_t buttons; 888 | 889 | //===== if the task requires any one time initialization, put it here ===== 890 | 891 | while(true) 892 | { 893 | // give up the CPU 894 | vTaskSuspend(NULL); 895 | 896 | // we just woke up, new data is available 897 | buttons = VrBoxData[VB_BTNCD]; 898 | Serial.printf("C/D Buttons: %02X\n", buttons); 899 | 900 | if (buttons & VB_BUTTON_C) 901 | { 902 | // button C pressed 903 | Serial.println("Button C"); 904 | 905 | //===== add your code here ===== 906 | 907 | } 908 | 909 | if (buttons & VB_BUTTON_D) 910 | { 911 | // button D pressed 912 | Serial.println("Button D"); 913 | 914 | //===== add your code here ===== 915 | 916 | } 917 | } // for 918 | } // taskButtonCD 919 | 920 | //****************************************************************************** 921 | //****************************************************************************** 922 | void setup() 923 | { 924 | BaseType_t xReturned; 925 | 926 | Serial.begin(115200); 927 | 928 | pinMode(LED, OUTPUT); 929 | digitalWrite(LED, LOW); 930 | pinMode(BLUELED, OUTPUT); // lit -> connected 931 | digitalWrite(BLUELED, LEDOFF); 932 | pinMode(REDLED, OUTPUT); 933 | digitalWrite(REDLED, LEDOFF); 934 | pinMode(GREENLED, OUTPUT); // lit -> scanning 935 | digitalWrite(GREENLED, LEDOFF); 936 | 937 | xReturned = xTaskCreate(taskJoyStick, // task to handle activity on the joystick. 938 | "Joystick", // String with name of task. 939 | TaskStackSize, // Stack size in 32 bit words. 940 | NULL, // Parameter passed as input of the task 941 | 1, // Priority of the task. 942 | &HandleJS); // Task handle. 943 | if (pdPASS == xReturned) 944 | { 945 | Serial.println("Joystick Task Created"); 946 | } 947 | 948 | xReturned = xTaskCreate(taskButtonAB, // task to handle activity on the A & B buttons. 949 | "ButtonsAB", // String with name of task. 950 | TaskStackSize, // Stack size in 32 bit words. 951 | NULL, // Parameter passed as input of the task 952 | 1, // Priority of the task. 953 | &HandleAB); // Task handle. 954 | if (pdPASS == xReturned) 955 | { 956 | Serial.println("AB Button Task Created"); 957 | 958 | } 959 | 960 | xReturned = xTaskCreate(taskButtonCD, // task to handle activity on the C & D buttons. 961 | "ButtonsCD", // String with name of task. 962 | TaskStackSize, // Stack size in 32 bit words. 963 | NULL, // Parameter passed as input of the task 964 | 1, // Priority of the task. 965 | &HandleCD); // Task handle. 966 | if (pdPASS == xReturned) 967 | { 968 | Serial.println("CD Button Task Created"); 969 | 970 | } 971 | 972 | btStart(); 973 | gattc_client_test(); 974 | } // setup 975 | 976 | //****************************************************************************** 977 | // connect flag is not reset when onnection is lost !!!! 978 | //****************************************************************************** 979 | void loop() 980 | { 981 | if (connect) 982 | { 983 | // joystick no activity detector 984 | if (joyTimer && (joyTimer < millis())) 985 | { 986 | // Serial.println("Timeout"); 987 | // no joystick notification for 30mS, center the joystik 988 | VrBoxData[VB_JOYX] = VrBoxData[VB_JOYY] = 0; 989 | 990 | // wake up the joystick task 991 | vTaskResume(HandleJS); 992 | 993 | joyTimer = 0; 994 | } 995 | // delay(10); 996 | } 997 | else if (!scanning) 998 | { 999 | digitalWrite(BLUELED, LEDOFF); 1000 | delay(5000); 1001 | StartScan(); 1002 | } 1003 | } // loop 1004 | 1005 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # ESP32 Bluetooth BLE Framework 2 | This project is a framework for an ESP32 to use an inexpensive hand-held Bluetooth BLE joystick. The joystick is readily available on the Internet for less than $20.00 or from your local fiveBELoW store for $5.00. The code provided was written for the Arduino IDE Ver 1.8.5 with the ESP32 addon. 3 | 4 | # Instalation and Setup of the Arduino IDE 5 | There are numerous web sites with detailed instructions for installing the Arduino IDE and the ESP32 addon. I am not going to attempt to repeat that information here. Google it and follow the directions. 6 | 7 | # Instalation of ESP32 BLE Library by Neil Kolban 8 | if you get; undefined reference to `BLERemoteService::getCharacteristicsByHandle() on compilation. then Use this link to get the library; https://github.com/nkolban/ESP32_BLE_Arduino 9 | Download as a zip file and then use the Arduino Libray Manager to install the zip library. 10 | 11 | # About the Framework 12 | This has been completely rewritten and is now based on Neil Kolban's BLE example code for a BLE Client. The original code is included in the Arduino ESP32 addon. I have modified the example code to work with the VRBOX device. The joystick and all six buttons are working. Ignore the GATT-VRBOX-Tasks folder and all the files in it. They no longer work due to changes made by Expressif in the ESP32 SDK. Use the BLE-VRBOX.ino file. This file will compile and run without problems using Arduino 1.8.12 or later. You must also use the ESP32 addon provided by Expressif or the ESP32 BLE Library by Neil Kolban. 13 | 14 | I am calling this a framework because it provides all of the ESP32 code to do the Bluetooth communications with the joystick device. You will have to fill in what to do with the data received from the joystick device. 15 | 16 | # About the Joystick 17 | The joystick that is used is retailed as: VR Control, Bluetooth Remote Controller made by Spektrum. 18 | 19 | ![Retail Box](BoxFront.png) 20 | ![Retail Box Back](BoxBack.png) 21 | ![The Controller](ControllerTop.png.jpg) 22 | ![ESP32 DevKit](ESP32Board.png) 23 | 24 | This is not a BLE tutorial. I will not be explaining how the code works in detail. I will use BLE terminology to describe some of the features of the joystick. The part of the code that you need to modify for your project will be explained in detail to help you to modify it. The scope is limited to keep this document as short as possible. 25 | 26 | ## Joystick BLE Description 27 | The device has a joystick, two trigger buttons and six other buttons on the handle, four of which you can use. The joystick is a BLE Server with five BLE Services. The Framework is only interested in the BLE HID Service, which has ten BLE Characteristics. Some of the ten Characteristics are duplicates. The Framework is only interested in the BLE Report Characteristics that have Read & Notify capabilities. The Characteristics are thus limited to three Reports that have their Notifications enabled. The Notify capability means that when the Server detects a change in the joystick position of one of the buttons is pressed then a Notification message is sent with information about the change. 28 | 29 | The Framework handles all of the dirty work of the BLE interface. The ESP32's Bluetooth radio is configured as a BLE Client (GATT Client). It then scans for a BLE Server named "VR BOX". Once found, it will connect to the Server, validate the HID Service is available and setup the appropriate Characteristics to use Notifications. 30 | 31 | When the user moves the joystick or presses a button, the server sends a notification message to the client. The data field of the notification has the joystick value or the value of the button that was pressed. The framework parses the data from the notification meaasge stores it in memory and wakes up a FreeRTOS task to handle the data. When the data hasndler finishes consuming the data it goes back to sleep waiting for the next notification message. Three tasks are provided. One each for the joystick and trigger buttons, AB buttons and the CD buttons. 32 | 33 | # Tasks 34 | Each of the three tasks parses the data from the notification message and executes a sub-section of the code for a specific event. For example, the joystick task determines which direction the X & Y axes have moved in and runs the code that you provide to handle movement along the axes. In each task there is a comment; "//===== add your code here =====" to indicate where to add your code. There is also a coment and Serial.println() indication the event your handler should handle. 35 | 36 | ## Indicators 37 | The Framework drives two LEDs. The GREENLED pin is used to light up a green LED to indicate when the Bluetooth is scanning for a Server. The BLUELED pin is used to light up a blue LED when the connection to the Server is established. These pins are determined by #include statements. You can change the pins by changing the pin numbers. 38 | 39 | The Framework will scan for the Server for thirty seconds. If the "VR BOX" Server is not found then the green LED turns off. After a five second delay, the green LED lights up and another scan is tried. This goes on until the "VR BOX" Server is found and a connection is made. 40 | 41 | Once connected to the Server, the blue LED is lit up. If the connection is lost the Framework goes back to scanning for the Server. The ESP32 is reset to start the Bluetooth stack over. Currently the ESP32 has no method to re-run a scan after a connection has been made. 42 | 43 | # VR BOX Operation 44 | ## Joystick 45 | If the joystick is left in the center position, no joystick notifications are sent. Once the joystick is moved off center, a notification message with joystick data and trigger button data is sent about every 15mS. When the joystick is moved back to center, a notification that it has moved to center is not sent. In other words, it tells you the joystick has moved off center, but not that it has moved to center. The end result is that you receive messages indicating the joystick is moving toward center, but not that it has reached center. Very annoying. The two trigger buttons are included with the joystick data. Pressing one of the trigger buttons after returning the joystick to center will update the joystick position to zero. The Framework has a timeout timer built-in to it that automatically simulates a joystick notification message a short time after all joystick/trigger button notification messages stop arriving. The timer sets the joystick to zero. The joystick has a range of about +/- 25 on each axis. 46 | 47 | ## Trigger Buttons 48 | The trigger buttons will send a notification message once when pressed and again when released. The pressed notification message will indicate the button that was pressed. The release notification message indicates that both buttons are released. 49 | 50 | Holding the lower trigger button will prevent the server from detecting that the upper trigger button has been pressed. Holding the upper trigger button and pressing the lower trigger button results in the server sending a notification message that the lower trigger button is pressed (the upper trigger button will be zero!). Releasing the lower trigger button will cause the server to send a notification that the upper trigger button is pressed and the lower trigger is released. 51 | 52 | In other words, the lower trigger button is dominant over the upper trigger button and will override it when both are pressed. You have to determine how to handle the case of both buttons being pressed. 53 | 54 | ## A/B Buttons 55 | The A and B buttons act like the joystick and continuously send notification messages when pressed and held down. The messages stop when the button is released. 56 | 57 | The A and B buttons work similar to the Trigger buttons in that the A button dominates the B button just like the lower trigger button dominates the upper trigger button. 58 | 59 | ## C/D Buttons 60 | The C and D buttons send a notification message once when pressed and again when released. If held down, no further messages are sent until they are released. 61 | 62 | Holding down either one of the C or D buttons will prevent the server from detecting activity on the other button. 63 | 64 | ## General 65 | All of the buttons are bit mapped into one byte of the associated notification message. 66 | 67 | The operation of the buttons is in my opion a little wonky. The Framework provides for places to put your code to act on when a button is pressed. If you also need to detect button releases, that is left for you to figure out how to do. 68 | 69 | # Conclusion 70 | It is entirely up to you to determine what you want each button to do and what moving the joystick should do. How you handle the differences in the trigger, A & B and the C & D buttons is up to you. 71 | 72 | Look in the code for the; taskJoyStick(), taskButtonAB(), taskButtonCD() functions and add your code after the "//===== add your code here =====" comment. 73 | 74 | You will need upto four functions to handle the joystick (forward, backward, right & left) and up to six functions to handle the various buttons. Implement them all or just what you need. The choice is yours. 75 | 76 | You can find the Instructable here; https://www.instructables.com/id/ESP32-Bluetooth-BLE-Remote-Control/ 77 | 78 | Enjoy 79 | --------------------------------------------------------------------------------