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

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();
}