RC cybertruck

An arduino RC cybertruck controller by a PS4 controller
Jul 13, 2025
embedded image

I recently built an RC car that's controlled entirely by a PlayStation 4 controller. Here's a quick overview of the project.

The Setup

The build centers around three main components:

  • ESP32 microcontroller - Handles Bluetooth connectivity and controls everything
  • PS4 DualShock controller - Wireless control with analog sticks and buttons
  • L298N motor driver - Powers four DC motors for movement

The ESP32 is perfect for this project because it has built-in Bluetooth and plenty of processing power. Using the Bluepad32 library, connecting a PS4 controller is surprisingly straightforward - it handles all the complex Bluetooth pairing automatically.

Source code:

#include <Bluepad32.h>
#include <FastLED.h>

ControllerPtr myControllers[BP32_MAX_GAMEPADS];

int motor1Pin1 = 26;
int motor1Pin2 = 25;
int enable1Pin = 13;

int motor2Pin1 = 33;
int motor2Pin2 = 32;
int enable2Pin = 27;

#define HEADLIGHT_PIN     19
#define TAILLIGHT_PIN     21
#define NUM_LEDS          9
#define BRIGHTNESS        255
#define LED_TYPE          WS2812
#define COLOR_ORDER       GRB

CRGB headlights[NUM_LEDS];
CRGB taillights[NUM_LEDS];

#define WHITE CRGB(255, 255, 255)
#define WARM_WHITE CRGB(255, 247, 230)
#define BRIGHT_WHITE CRGB(255, 255, 255)
#define AMBER CRGB(255, 191, 0)
#define RED CRGB(255, 0, 0)
#define DIM_RED CRGB(80, 0, 0)
#define BLACK CRGB(0, 0, 0)

#define BTN_R1 0x0020
#define BTN_L1 0x0010
#define BTN_TRIANGLE 0x0008
#define BTN_X 0x0001
#define BTN_A 0x0080
#define BTN_B 0x0004
#define BTN_Y 0x0002

const int DEADZONE = 30;

unsigned long previousMillis = 0;
unsigned long blinkInterval = 500;
bool blinkState = false;
unsigned long animationTimer = 0;
unsigned long fadeTimer = 0;
unsigned long lightShowTimer = 0;
int lightShowMode = 0;

bool headlightOn = false;
bool taillightOn = false;
bool leftSignalOn = false;
bool rightSignalOn = false;
bool brakeOn = false;
bool reversing = false;
bool hazardOn = false;
bool lightShowActive = false;

uint16_t previousButtons = 0;

bool headlightStartupComplete = true;
unsigned long headlightStartupTimer = 0;

void onConnectedController(ControllerPtr ctl) {
    bool foundEmptySlot = false;
    for (int i = 0; i < BP32_MAX_GAMEPADS; i++) {
        if (myControllers[i] == nullptr) {
            Serial.printf("Controller connected, index=%d\n", i);
            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("Controller connected, but no empty slot found");
    }
}

void onDisconnectedController(ControllerPtr ctl) {
    for (int i = 0; i < BP32_MAX_GAMEPADS; i++) {
        if (myControllers[i] == ctl) {
            Serial.printf("Controller disconnected from index=%d\n", i);
            myControllers[i] = nullptr;
            stopMotors();
            break;
        }
    }
}

void setup() {
    Serial.begin(115200);
    
    Serial.printf("Firmware: %s\n", BP32.firmwareVersion());
    BP32.setup(&onConnectedController, &onDisconnectedController);
    BP32.forgetBluetoothKeys();
    BP32.enableVirtualDevice(false);
    
    pinMode(motor1Pin1, OUTPUT);
    pinMode(motor1Pin2, OUTPUT);
    pinMode(enable1Pin, OUTPUT);
    pinMode(motor2Pin1, OUTPUT);
    pinMode(motor2Pin2, OUTPUT);
    pinMode(enable2Pin, OUTPUT);
    
    stopMotors();
    
    FastLED.addLeds<LED_TYPE, HEADLIGHT_PIN, COLOR_ORDER>(headlights, NUM_LEDS).setCorrection(TypicalLEDStrip);
    FastLED.addLeds<LED_TYPE, TAILLIGHT_PIN, COLOR_ORDER>(taillights, NUM_LEDS).setCorrection(TypicalLEDStrip);
    FastLED.setBrightness(BRIGHTNESS);
    
    clearAllLEDs();
    
    Serial.println("Cybertruck RC Complete System Ready!");
    Serial.println("Motor Controls:");
    Serial.println("- Left stick Y-axis: Forward/Backward");
    Serial.println("- Right stick X-axis: Left/Right steering");
    Serial.println("- A button: Emergency brake");
    Serial.println("Lighting Controls:");
    Serial.println("- Triangle: Toggle Headlights/Taillights");
    Serial.println("- X: Brake Lights (while held)");
    Serial.println("- L1: Left Turn Signal");
    Serial.println("- R1: Right Turn Signal");
    Serial.println("- B: Hazard Lights");
    Serial.println("- Y (when stopped): Tesla Light Show!");
}

void loop() {
    bool dataUpdated = BP32.update();
    if (dataUpdated) {
        processControllers();
    }
    
    unsigned long currentMillis = millis();
    if (currentMillis - previousMillis >= blinkInterval) {
        previousMillis = currentMillis;
        blinkState = !blinkState;
    }
    
    if (lightShowActive) {
        updateLightShow();
    } else {
        updateHeadlightPattern();
        updateTaillightPattern();
    }
    
    FastLED.show();
    
    delay(10);
}

void stopMotors() {
    digitalWrite(motor1Pin1, LOW);
    digitalWrite(motor1Pin2, LOW);
    digitalWrite(motor2Pin1, LOW);
    digitalWrite(motor2Pin2, LOW);
    analogWrite(enable1Pin, 0);
    analogWrite(enable2Pin, 0);
}

void emergencyBrake() {
    brakeOn = true;
    updateTaillightPattern();
    FastLED.show();
    
    digitalWrite(motor1Pin1, HIGH);
    digitalWrite(motor1Pin2, HIGH);
    digitalWrite(motor2Pin1, HIGH);
    digitalWrite(motor2Pin2, HIGH);
    analogWrite(enable1Pin, 255);
    analogWrite(enable2Pin, 255);
    delay(100);
    stopMotors();
    
    brakeOn = false;
}

void controlMotors(int throttle, int steering) {
    reversing = (throttle > DEADZONE);
    
    int baseSpeed = map(abs(throttle), 0, 508, 0, 255);
    baseSpeed = constrain(baseSpeed, 0, 255);
    
    int leftSpeed = baseSpeed;
    int rightSpeed = baseSpeed;
    
    if (steering < -DEADZONE) {
        leftSpeed = 0;
    } else if (steering > DEADZONE) {
        rightSpeed = 0;
    }
    
    if (throttle > DEADZONE) {
        digitalWrite(motor2Pin1, LOW);
        digitalWrite(motor2Pin2, HIGH);
        analogWrite(enable2Pin, leftSpeed);
        
        digitalWrite(motor1Pin1, LOW);
        digitalWrite(motor1Pin2, HIGH);
        analogWrite(enable1Pin, rightSpeed);
        
    } else if (throttle < -DEADZONE) {
        digitalWrite(motor2Pin1, HIGH);
        digitalWrite(motor2Pin2, LOW);
        analogWrite(enable2Pin, leftSpeed);
        
        digitalWrite(motor1Pin1, HIGH);
        digitalWrite(motor1Pin2, LOW);
        analogWrite(enable1Pin, rightSpeed);
        
    } else {
        stopMotors();
    }
}

void processGamepad(ControllerPtr ctl) {
    uint16_t currentButtons = ctl->buttons();
    
    uint16_t buttonPressed = currentButtons & ~previousButtons;
    
    int leftY = ctl->axisY();
    int rightX = ctl->axisRX();
    
    if (abs(leftY) < DEADZONE) leftY = 0;
    if (abs(rightX) < DEADZONE) rightX = 0;
    
    bool isStationary = (abs(leftY) < DEADZONE && abs(rightX) < DEADZONE);
    
    if (buttonPressed & BTN_TRIANGLE) {
        headlightOn = !headlightOn;
        taillightOn = headlightOn;
        
        if (headlightOn) {
            headlightStartupComplete = false;
            headlightStartupTimer = millis();
            Serial.println("Headlights/Taillights ON");
        } else {
            Serial.println("Headlights/Taillights OFF");
        }
    }
    
    if (buttonPressed & BTN_B) {
        hazardOn = !hazardOn;
        if (hazardOn) {
            leftSignalOn = false;
            rightSignalOn = false;
            Serial.println("Hazard Lights ON");
        } else {
            Serial.println("Hazard Lights OFF");
        }
    }
    
    if ((buttonPressed & BTN_L1) && !hazardOn) {
        leftSignalOn = !leftSignalOn;
        rightSignalOn = false;
        Serial.println(leftSignalOn ? "Left Signal ON" : "Left Signal OFF");
    }
    
    if ((buttonPressed & BTN_R1) && !hazardOn) {
        rightSignalOn = !rightSignalOn;
        leftSignalOn = false;
        Serial.println(rightSignalOn ? "Right Signal ON" : "Right Signal OFF");
    }
    
    if ((buttonPressed & BTN_Y) && isStationary) {
        lightShowActive = !lightShowActive;
        if (lightShowActive) {
            lightShowTimer = millis();
            lightShowMode = 0;
            Serial.println("Tesla Light Show ACTIVATED!");
        } else {
            Serial.println("Light Show ended");
        }
    }
    
    if (!isStationary) {
        brakeOn = (currentButtons & BTN_X) != 0;
    }
    
    if (currentButtons & BTN_A) {
        emergencyBrake();
        Serial.println("EMERGENCY BRAKE!");
        previousButtons = currentButtons;
        return;
    }
    
    controlMotors(leftY, rightX);
    
    previousButtons = currentButtons;
    
    if (currentButtons != 0 || leftY != 0 || rightX != 0) {
        if (leftY != 0 || rightX != 0) {
            Serial.printf("Throttle: %d, Steering: %d | ", leftY, rightX);
        }
        if (headlightOn || brakeOn || leftSignalOn || rightSignalOn || hazardOn) {
            Serial.printf("Lights: H:%s B:%s L:%s R:%s Hz:%s Rev:%s\n",
                         headlightOn ? "ON" : "OFF",
                         brakeOn ? "ON" : "OFF",
                         leftSignalOn ? "ON" : "OFF",
                         rightSignalOn ? "ON" : "OFF",
                         hazardOn ? "ON" : "OFF",
                         reversing ? "ON" : "OFF");
        }
    }
}

void processControllers() {
    for (auto myController : myControllers) {
        if (myController && myController->isConnected() && myController->hasData()) {
            if (myController->isGamepad()) {
                processGamepad(myController);
            }
        }
    }
}

void updateHeadlightPattern() {
    for (int i = 0; i < NUM_LEDS; i++) {
        headlights[i] = BLACK;
    }
    
    if (headlightOn) {
        if (!headlightStartupComplete) {
            unsigned long elapsed = millis() - headlightStartupTimer;
            if (elapsed < 100) {
                for (int i = 3; i <= 5; i++) {
                    headlights[i] = BRIGHT_WHITE;
                }
            } else if (elapsed < 200) {
                for (int i = 2; i <= 6; i++) {
                    headlights[i] = BRIGHT_WHITE;
                }
            } else if (elapsed < 300) {
                for (int i = 0; i < NUM_LEDS; i++) {
                    headlights[i] = BRIGHT_WHITE;
                }
            } else {
                headlightStartupComplete = true;
            }
        } else {
            for (int i = 0; i < NUM_LEDS; i++) {
                headlights[i] = BRIGHT_WHITE;
            }
        }
    }
    
    if (hazardOn && blinkState) {
        for (int i = 0; i < 2; i++) {
            headlights[i] = AMBER;
        }
        for (int i = 7; i < 9; i++) {
            headlights[i] = AMBER;
        }
    } else {
        if (leftSignalOn && blinkState) {
            for (int i = 7; i < 9; i++) {
                headlights[i] = AMBER;
            }
        }
        
        if (rightSignalOn && blinkState) {
            for (int i = 0; i < 2; i++) {
                headlights[i] = AMBER;
            }
        }
    }
}

void updateTaillightPattern() {
    for (int i = 0; i < NUM_LEDS; i++) {
        taillights[i] = BLACK;
    }
    
    if (taillightOn && !brakeOn && !reversing) {
        for (int i = 0; i < NUM_LEDS; i++) {
            taillights[i] = DIM_RED;
        }
    }
    
    if (brakeOn) {
        for (int i = 0; i < NUM_LEDS; i++) {
            taillights[i] = RED;
        }
    }
    
    if (reversing && !brakeOn) {
        for (int i = 3; i <= 5; i++) {
            taillights[i] = WHITE;
        }
        if (taillightOn) {
            for (int i = 0; i < 3; i++) {
                taillights[i] = DIM_RED;
            }
            for (int i = 6; i < NUM_LEDS; i++) {
                taillights[i] = DIM_RED;
            }
        }
    }
    
    if (hazardOn && blinkState) {
        for (int i = 0; i < 2; i++) {
            taillights[i] = AMBER;
        }
        for (int i = 7; i < 9; i++) {
            taillights[i] = AMBER;
        }
    } else {
        if (leftSignalOn && blinkState) {
            for (int i = 0; i < 2; i++) {
                taillights[i] = AMBER;
            }
        }
        
        if (rightSignalOn && blinkState) {
            for (int i = 7; i < 9; i++) {
                taillights[i] = AMBER;
            }
        }
    }
}

void updateLightShow() {
    unsigned long elapsed = millis() - lightShowTimer;
    
    if (elapsed > 500) {
        lightShowTimer = millis();
        lightShowMode = (lightShowMode + 1) % 8;
    }
    
    for (int i = 0; i < NUM_LEDS; i++) {
        headlights[i] = BLACK;
        taillights[i] = BLACK;
    }
    
    switch (lightShowMode) {
        case 0:
            {
                int pos = (millis() / 100) % NUM_LEDS;
                headlights[pos] = WHITE;
                taillights[NUM_LEDS - 1 - pos] = RED;
            }
            break;
            
        case 1:
            for (int i = 0; i < NUM_LEDS; i++) {
                uint8_t brightness = sin8((millis() / 10 + i * 30) % 255);
                headlights[i] = CRGB(brightness, brightness, brightness);
                taillights[i] = CRGB(brightness, 0, 0);
            }
            break;
            
        case 2:
            if ((millis() / 50) % 2) {
                for (int i = 0; i < NUM_LEDS; i++) {
                    headlights[i] = WHITE;
                    taillights[i] = RED;
                }
            }
            break;
            
        case 3:
            {
                int phase = (millis() / 200) % 5;
                for (int i = 0; i <= phase && i < 5; i++) {
                    headlights[4 + i] = WHITE;
                    headlights[4 - i] = WHITE;
                    taillights[4 + i] = RED;
                    taillights[4 - i] = RED;
                }
            }
            break;
            
        case 4:
            if ((millis() / 300) % 2) {
                for (int i = 0; i < NUM_LEDS / 2; i++) {
                    headlights[i] = WHITE;
                    taillights[NUM_LEDS - 1 - i] = RED;
                }
            } else {
                for (int i = NUM_LEDS / 2; i < NUM_LEDS; i++) {
                    headlights[i] = WHITE;
                    taillights[NUM_LEDS - 1 - i] = RED;
                }
            }
            break;
            
        case 5:
            {
                int pos = (millis() / 100) % NUM_LEDS;
                for (int i = 0; i < NUM_LEDS; i++) {
                    if (i <= pos) {
                        headlights[i] = WHITE;
                        taillights[i] = RED;
                    }
                }
            }
            break;
            
        case 6:
            {
                uint8_t brightness = (sin8(millis() / 10) / 2) + 128;
                for (int i = 0; i < NUM_LEDS; i++) {
                    headlights[i] = CRGB(brightness, brightness, brightness);
                    taillights[i] = CRGB(brightness, 0, 0);
                }
            }
            break;
            
        case 7:
            for (int i = 0; i < NUM_LEDS; i++) {
                if (random(10) < 3) {
                    uint8_t brightness = random(128, 255);
                    headlights[i] = CRGB(brightness, brightness, brightness);
                    taillights[i] = CRGB(brightness, 0, 0);
                }
            }
            break;
    }
}

void clearAllLEDs() {
    for (int i = 0; i < NUM_LEDS; i++) {
        headlights[i] = BLACK;
        taillights[i] = BLACK;
    }
    FastLED.show();
}