soon…

You’ll learn how to use a ESP32-WROOM as a Bluetooth receiver for a PS4 Controller to control any RC Car.

It’s incredibly easy.

Buy the parts needed:

  • PS4 Controller……………..………………….… Amazon / Banggood
  • ESP32 Wroom 30 pins…………….…….… Amazon/ Banggood
  • RC Car Wltoys K989……………………….… Amazon/ Banggood / Shopee
  • Brushed ESC 10A with brakes.…….… Amazon/ Banggood
  • Thin wires 22 Awg (optional) ………… Amazon / Banggood
  • JST 2 Conectors (optional).………..….… AmazonBanggood

Disclosure: These are affiliate links. I earn a little comission if you use my links to buy the parts.
Please use them to help me continue building cool projects.

3D Print the parts

If you got the same car, you can just download and print the parts to attatch it to your car.
Otherwise you’ll have to design apartto fit your car. If you do, please let me know so i can link it here.

Use the same screws on the frame to attach the prints.

How to connect the wires

Wiring Diagram Overview:​

Let’s break down the wiring based on the schematic provided.

Servo:

  • Servo SIG (Orange Wire): Pin 12
  • VCC (Red Wire): 5V
  • GND (Black Wire): GND

ESC:

  • ESC SIG (White Wire): Pin 13
  • VCC (Red Wire): 5V
  • GND (Black Wire): GND

Power:

Connect  your ESP32 to 5V and GNDto power it.

Upload the Example Code

Make sure to select the correct Board:DOIT ESP32 DEVKIT V1.
After you selected the board, you can find the Example code.

Bluepad32_ESP32 -> Controller

Upload the example code as is:

Get the Modified Arduino Code

  #include <Bluepad32.h> #include <ESP32Servo.h> const int servoPin = 12; // yellow wire const int escPin = 13; // white wire Servo servo1; Servo esc; // ================= LED VALUES ========================= bool copLightsActive, lightActive = false; // LED state bool escAlreadySet = false; bool previousButtonState = HIGH; // Previous button state const int ledBrakePin = 14; const int ledHeadlightPin = 27; // ================= PEDALS VALUES ========================= int throttleValue, brakeValue, escValue, servoValue, panValue, tiltValue, gyroValue, accelValue; int escSlowSpeed = 200; // int escSlowSpeed = 1700; int escNormalSpeed = 300; // int escNormalSpeed = 1800; int escMaxSpeed = 500; // int escMaxSpeed = 2000; // ================= NITRO VALUES ========================= int nitroAmmo = 1; int nitroDuration = 5000; // 5 seconds unsigned long nitroStartTime = 0; bool nitroActivated = false; // 1==false ; 0==true; // ================= GUN VALUES ========================= int gunAmmo = 1; int gunDuration = 3000; // 3 seconds unsigned long gunStartTime = 0; bool gunActivated = false; // ================= HORN VALUES ========================= int hornAmmo = 1; int hornDuration = 3000; // 3 seconds unsigned long hornStartTime = 0; bool hornActivated = false; // ================= BUTTON PRESSED VALUES ========================= // Define global variables float value = 100.0; // Initial value (100%) unsigned long lastSubtractTime = 0; // Time tracker for subtracting unsigned long lastAddTime = 0; // Time tracker for adding unsigned long buttonReleaseTime = 0;// Time when the button was released bool buttonHeld = false; // Whether the button is held bool buttonReleased = false; // Whether the button was released bool addingActive = false; // Array of values int values[] = {10, 20, 30, 40, 50}; int arrayLength = sizeof(values) / sizeof(values[0]); // Calculate the array length int currentIndex = 0; // Start at the first value in the array ControllerPtr myControllers[BP32_MAX_GAMEPADS]; // This callback gets called any time a new gamepad is connected. // Up to 4 gamepads can be connected at the same time. void onConnectedController(ControllerPtr ctl) { bool foundEmptySlot = false; for (int i = 0; i < BP32_MAX_GAMEPADS; i++) { if (myControllers[i] == nullptr) { Serial.printf("CALLBACK: Controller is connected, index=%d\n", i); // Additionally, you can get certain gamepad properties like: // Model, VID, PID, BTAddr, flags, etc. ControllerProperties properties = ctl->getProperties(); Serial.printf("Controller model: %s, VID=0x%04x, PID=0x%04x\n", ctl->getModelName().c_str(), properties.vendor_id, properties.product_id); myControllers[i] = ctl; foundEmptySlot = true; break; } } if (!foundEmptySlot) { Serial.println("CALLBACK: Controller connected, but could not found empty slot"); } } void onDisconnectedController(ControllerPtr ctl) { bool foundController = false; for (int i = 0; i < BP32_MAX_GAMEPADS; i++) { if (myControllers[i] == ctl) { Serial.printf("CALLBACK: Controller disconnected from index=%d\n", i); myControllers[i] = nullptr; foundController = true; break; } } if (!foundController) { Serial.println("CALLBACK: Controller disconnected, but not found in myControllers"); } } // ========= SEE CONTROLLER VALUES IN SERIAL MONITOR ========= // void dumpGamepad(ControllerPtr ctl) { Serial.printf( "idx=%d, dpad: 0x%02x, buttons: 0x%04x, axis L: %4d, %4d, axis R: %4d, %4d, brake: %4d, throttle: %4d, " "misc: 0x%02x, gyro x:%6d y:%6d z:%6d, accel x:%6d y:%6d z:%6d\n", ctl->index(), // Controller Index ctl->dpad(), // D-pad ctl->buttons(), // bitmask of pressed buttons ctl->axisX(), // (-511 - 512) left X Axis ctl->axisY(), // (-511 - 512) left Y axis ctl->axisRX(), // (-511 - 512) right X axis ctl->axisRY(), // (-511 - 512) right Y axis ctl->brake(), // (0 - 1023): brake button ctl->throttle(), // (0 - 1023): throttle (AKA gas) button ctl->miscButtons(), // bitmask of pressed "misc" buttons ctl->gyroX(), // Gyro X ctl->gyroY(), // Gyro Y ctl->gyroZ(), // Gyro Z ctl->accelX(), // Accelerometer X ctl->accelY(), // Accelerometer Y ctl->accelZ() // Accelerometer Z ); } // ========= GAME CONTROLLER ACTIONS SECTION ========= // void processGamepad(ControllerPtr ctl) { // There are different ways to query whether a button is pressed. // By query each button individually: // a(), b(), x(), y(), l1(), etc... //== PS4 X button = 0x0001 ==// if (ctl->buttons() == 0x0001) { // code for when X button is pushed checkNitroButton(); } if (ctl->buttons() != 0x0001) { // code for when X button is released } //== PS4 Square button = 0x0004 ==// if (ctl->buttons() == 0x0004) { // code for when square button is pushed checkGunButton(); } if (ctl->buttons() != 0x0004) { // code for when square button is released } //== PS4 Triangle button = 0x0008 ==// if (ctl->buttons() == 0x0008) { checkHornButton(); // code for when triangle button is pushed } if (ctl->buttons() != 0x0008) { // code for when triangle button is released } //== PS4 Circle button = 0x0002 ==// if (ctl->buttons() == 0x0002) { // code for when circle button is pushed checkHornButton(); } if (ctl->buttons() != 0x0002) { // code for when circle button is released } //== PS4 Share miscButtons = 0x02 ==// if (ctl->miscButtons() == 0x02) { // code for when Share button is pushed Serial.print("Share button is pushed "); } //== PS4 Option miscButtons = 0x04 ==// if (ctl->miscButtons() == 0x04) { // code for when Option button is pushed Serial.print("Option button is pushed "); } //== PS4 PS miscButtons = 0x01 ==// if (ctl->miscButtons() == 0x01) { // code for when PS button is pushed Serial.print("PS button is pushed "); } //== PS4 Dpad UP button = 0x01 ==// if (ctl->dpad() == 0x01) { // code for when dpad up button is pushed escValue = 2000; } if (ctl->dpad() != 0x01) { // code for when dpad up button is released escValue = 1500; } //==PS4 Dpad DOWN button = 0x02==// if (ctl->dpad() == 0x02) { // code for when dpad down button is pushed escValue = 1000; } if (ctl->dpad() != 0x02) { // code for when dpad down button is released escValue = 1500; } //== PS4 Dpad LEFT button = 0x08 ==// if (ctl->dpad() == 0x08) { // code for when dpad left button is pushed servoValue = 0; } if (ctl->dpad() != 0x08) { // code for when dpad left button is released servoValue = 90; } //== PS4 Dpad RIGHT button = 0x04 ==// if (ctl->dpad() == 0x04) { // code for when dpad right button is pushed servoValue = 180; } if (ctl->dpad() != 0x04) { // code for when dpad right button is released servoValue = 90; } //== PS4 R1 trigger button = 0x0020 ==// if (ctl->buttons() == 0x0020) { // code for when R1 button is pushed } if (ctl->buttons() != 0x0020) { // code for when R1 button is released } //== PS4 R2 trigger button = 0x0080 ==// if (ctl->throttle() >= 25) { // code for when R2 button is pushed if (nitroActivated == true){ throttleValue = map(ctl->throttle(), 0, 1023, 0, escMaxSpeed); }else{ throttleValue = map(ctl->throttle(), 0, 1023, 0, escNormalSpeed); } Serial.print("throttleValue: "); Serial.println(throttleValue); } if (ctl->throttle() < 25) { // code for when R2 button is released throttleValue = 0; } //== PS4 L1 trigger button = 0x0010 ==// if (ctl->buttons() == 0x0010) { // code for when L1 button is pushed toggleLED(); } if (ctl->buttons() != 0x0010) { // code for when L1 button is released } //== PS4 L2 trigger button = 0x0040 ==// if (ctl->brake() >= 25) { // code for when L2 button is pushed brakeValue = map(ctl->brake(), 0, 1023, 0, 500); Serial.print("brakeValue: "); Serial.println(brakeValue); } if (ctl->brake() < 25) { // code for when L2 button is released brakeValue = 0; } //== LEFT JOYSTICK - UP ==// if (ctl->axisY() <= -25) { // code for when left joystick is pushed up } //== LEFT JOYSTICK - DOWN ==// if (ctl->axisY() >= 25) { // code for when left joystick is pushed down } //== LEFT JOYSTICK - LEFT/RIGHT ==// if (ctl->axisX()) { // code for when left joystick is pushed left servoValue = map(ctl->axisX(), -512, 512, 0, 180); Serial.print("servoValue: "); Serial.println(servoValue); } //== LEFT JOYSTICK - LEFT ==// if (ctl->axisX() <= -25) { // code for when left joystick is pushed left } //== LEFT JOYSTICK - RIGHT ==// if (ctl->axisX() >= 25) { // code for when left joystick is pushed right } //== LEFT JOYSTICK - LEFT ==// if (ctl->gyroY()) { // code for when left joystick is pushed left gyroValue = map(ctl->gyroY(), -512, 512, 0, 180); Serial.print("gyroValue: "); Serial.println(gyroValue); } if (ctl->accelY()) { // code for when left joystick is pushed left accelValue = map(ctl->accelY(), -512, 512, 0, 180); Serial.print("accelValue: "); Serial.println(accelValue); } //== LEFT JOYSTICK DEADZONE ==// if (ctl->axisY() > -25 && ctl->axisY() < 25 && ctl->axisX() > -25 && ctl->axisX() < 25) { // code for when left joystick is at idle // escValue = 1500; } //== RIGHT JOYSTICK - X AXIS ==// if (ctl->axisRX()) { // code for when right joystick moves along x-axis panValue = map(ctl->axisRX(), -512, 512, 0, 180); // Serial.print("panValue: "); // Serial.println(panValue); } //== RIGHT JOYSTICK - Y AXIS ==// if (ctl->axisRY()) { // code for when right joystick moves along y-axis tiltValue = map(ctl->axisRY(), -512, 512, 0, 180); // Serial.print("tiltValue: "); // Serial.println(tiltValue); } //== RIGHT JOYSTICK DEADZONE ==// if (ctl->axisRY() > -25 && ctl->axisRY() < 25 && ctl->axisRX() > -25 && ctl->axisRX() < 25) { // code for when left joystick is at idle } // Mix both pedal values before sending to the ESC and make sure it's in range escValue = constrain((1500 + throttleValue - brakeValue), 1000, 2000); // if (escValue >= 1450 && escValue <= 1550 ){ escValue = 1500; } //make it smoother esc.writeMicroseconds(escValue); servo1.write(servoValue); // if the car is going backwards, light up the tail lights if (escValue < 1450) { digitalWrite(ledBrakePin, HIGH); }else{ digitalWrite(ledBrakePin, LOW); } dumpGamepad(ctl); } void processControllers() { for (auto myController : myControllers) { if (myController && myController->isConnected() && myController->hasData()) { if (myController->isGamepad()) { processGamepad(myController); } else { Serial.println("Unsupported controller"); } } } } // Arduino setup function. Runs in CPU 1 void setup() { Serial.begin(115200); servo1.attach(servoPin); esc.attach(escPin); pinMode(ledHeadlightPin, OUTPUT); pinMode(ledBrakePin, OUTPUT); digitalWrite(ledBrakePin, LOW); digitalWrite(ledHeadlightPin, LOW); Serial.printf("Firmware: %s\n", BP32.firmwareVersion()); const uint8_t* addr = BP32.localBdAddress(); Serial.printf("BD Addr: %2X:%2X:%2X:%2X:%2X:%2X\n", addr[0], addr[1], addr[2], addr[3], addr[4], addr[5]); // Setup the Bluepad32 callbacks BP32.setup(&onConnectedController, &onDisconnectedController); // "forgetBluetoothKeys()" should be called when the user performs // a "device factory reset", or similar. // Calling "forgetBluetoothKeys" in setup() just as an example. // Forgetting Bluetooth keys prevents "paired" gamepads to reconnect. // But it might also fix some connection / re-connection issues. BP32.forgetBluetoothKeys(); // Enables mouse / touchpad support for gamepads that support them. // When enabled, controllers like DualSense and DualShock4 generate two connected devices: // - First one: the gamepad // - Second one, which is a "virtual device", is a mouse. // By default, it is disabled. BP32.enableVirtualDevice(false); } // Arduino loop function. Runs in CPU 1. void loop() { // This call fetches all the controllers' data. // Call this function in your main loop. bool dataUpdated = BP32.update(); if (dataUpdated) processControllers(); // The main loop must have some kind of "yield to lower priority task" event. // Otherwise, the watchdog will get triggered. // If your main loop doesn't have one, just add a simple `vTaskDelay(1)`. // Detailed info here: // https://stackoverflow.com/questions/66278271/task-watchdog-got-triggered-the-tasks-did-not-reset-the-watchdog-in-time vTaskDelay(1); // delay(150); } void setupESC() { Serial.print("setupESC triggered "); // this is so this code run only once. if (escAlreadySet == false) { esc.writeMicroseconds(1500); delay(500); esc.writeMicroseconds(2000); delay(1000); esc.writeMicroseconds(1500); delay(500); esc.writeMicroseconds(1000); delay(1000); esc.writeMicroseconds(1500); delay(500); esc.writeMicroseconds(1000); delay(1000); esc.writeMicroseconds(1500); escAlreadySet = true; } }; void toggleLED() { Serial.print("toggleLED triggered "); // Read the current button state bool currentButtonState = digitalRead(ledHeadlightPin);; // Check if the button is pressed and released if (previousButtonState == HIGH && currentButtonState == LOW) { // Toggle the LED state lightActive = !lightActive; // Set the LED to the new state digitalWrite(ledHeadlightPin, lightActive ? HIGH : LOW); } // Update the previous button state for the next loop previousButtonState = currentButtonState; } void checkNitroButton() { Serial.print("checkNitroButton triggered "); if (!nitroActivated) { // Button pressed and nitro is not currently in use nitroStartTime = millis(); // Record the time when nitro was activated nitroActivated = true; nitroAmmo--; } // Check if X seconds have passed since nitro was activated if (nitroActivated && (millis() - nitroStartTime >= nitroDuration)) { nitroActivated = false; } }; void checkGunButton() { Serial.print("checkGunButton triggered "); if (!gunActivated) { // Button pressed and gun is not currently in use gunStartTime = millis(); // Record the time when gun was activated gunActivated = true; gunAmmo--; } // Check if X seconds have passed since gun was activated if (gunActivated && (millis() - gunStartTime >= gunDuration)) { gunActivated = false; } }; void checkHornButton() { Serial.print("checkHornButton triggered "); if (!hornActivated) { // Button pressed and nitro is not currently in use hornStartTime = millis(); // Record the time when nitro was activated hornActivated = true; hornAmmo--; } // Check if X seconds have passed since nitro was activated if (hornActivated && (millis() - hornStartTime >= hornDuration)) { hornActivated = false; } };

Upload the Modified Code

Make sure to select the correct Board:DOIT ESP32 DEVKIT V1.NOT ESP32 Family Device.

After you selected the board, you can find the Example code.

Bluepad32_ESP32 -> Controller

Upload the example code as is:

Scroll to Top