Simple 3D printed RC car with a brushless drone motor. Drive it from your phone over WiFi, or from a PS5 controller over Bluetooth.
Demo.mp4
firmware/controller/— phone controller (WiFi web UI, runs on XIAO ESP32-S3)firmware/controller_ps5/— PS5 controller (Bluepad32 + ESP-NOW, runs on a classic ESP32)firmware/car/— car code (same for both control modes — the car just listens for ESP-NOW packets, it doesn't care which controller sent them)cad/— fixed printed parts (3mf), print as-is
Customizable printed parts (chassis, wheels, knuckles, bearing inserts, spur gear) live on MakerWorld — pick the variant that matches your bearings and shaft size: https://makerworld.com/en/models/2819296-rock-small-brushless-rc-car
Electronics
- ReadyToSky MT2204 2300KV brushless drone motor
- EMAX Bullet 20A BLHeli-S ESC
- MG90S servo
- Car: LilyGO T-Energy S3
- Controller (phone mode): XIAO ESP32-S3
- Controller (PS5 mode): a classic ESP32 with Bluetooth Classic — e.g. DFRobot FireBeetle 2 ESP32-E, or any ESP-WROOM-32E dev board. Not an ESP32-S3 or C3 — those are BLE-only and Bluepad32 needs Bluetooth Classic.
- 2S 7.4V 1800mAh LiPo, T-plug (Reely)
- T-plug pigtail
- Wire
Pick one controller mode. You only need one transmitter ESP32, not both.
Drivetrain
- 10T pinion (store bought, fits motor shaft)
- 58T spur gear (printed)
- 1/4" (6.35 mm) square steel drive shaft
- 4x flanged ball bearings (~22 mm OD x ~7.6 mm ID x ~7 mm wide)
- 4x shaft collars for 1/4" square shaft
3D printed parts
From this repo (cad/) — print as-is:
- battery cover
- electronics cover
- steering link
From MakerWorld — pick the variant matching your hardware (bearing size, shaft size):
- chassis
- wheel (×4)
- knuckle (×2)
- bearing insert (×4)
- spur gear 58T
Hardware
- M3 heat set inserts, screws, nuts
- 4x rubber tires for the printed wheels
Car ESP32:
ESC signal -> GPIO 9
Servo signal -> GPIO 10
Servo V+ -> 5V (ESC BEC)
ESP32 5V -> 5V (ESC BEC)
GND -> GND
GPIO numbers refer to the ESP32 GPIO scheme, not silkscreen pin numbers. If you flash on a different ESP32 variant, check the board's pinout — GPIO 9/10 may map to a different physical pin.
LiPo to ESC via T-plug. Don't run USB and BEC into the ESP32 at the same time.
Controller is on USB. No wiring.
Do these in order.
1. Knuckle prep
- Press bearings into each knuckle.
- Slide the square steel shaft through and fit the square inserts.
- Secure the wheels on the shaft with the shaft collars.
2. Motor
- Drop the motor into its mount area behind the chassis.
- Screw it down with the screws that match your motor's hole pattern (varies by motor).
3. Knuckle to chassis
- Orient each knuckle so the extra hole — the one for the steering link — faces inward (toward the center of the car) and up.
- Attach with one M3 screw on top and one on the bottom. Heat-set inserts live in the top mount, not the knuckle, so the screws pass through clearance holes in the knuckle and thread into the mount above.
- Snug, not gorilla. Don't over-tighten or you'll deform the print.
4. Steering link to servo
- Bolt the steering link to the horn hole ~13 mm from the servo spline center, with the servo's wire connector facing down and the horn sitting on top of the link.
- Press the horn-link assembly onto the servo spline.
5. Steering link to knuckles
- Connect each end of the link to its knuckle with an M3 screw. This is the one joint where the heat-set insert lives in the knuckle itself — A-arm / shaft link exception to the rule above.
6. Wires
- Run servo + ESC wires through the routing channel on the chassis before screwing the cover down, otherwise they stick out and snag.
Arduino IDE, esp32 core 2.0+ from Boards Manager.
The car firmware is the same regardless of which controller you use — flash car.ino once and forget about it. Pick one controller below.
Open firmware/car/car.ino, pick your LilyGO T-Energy S3, upload (LiPo unplugged).
ESC setup is a one-time thing — done with BLHeli Suite via an Arduino Nano running the BLHeli passthrough sketch.
Board: XIAO ESP32-S3.
Libraries:
- ESP32Servo
- WebSockets (Markus Sattler)
Open firmware/controller/controller.ino, pick your board, upload.
Want a different WiFi name or password? Change them in controller.ino before flashing.
Board: a classic ESP32 (FireBeetle 2 ESP32-E or any ESP-WROOM-32E dev board).
One-time setup:
- File → Preferences → Additional Boards Manager URLs, add:
https://raw.githubusercontent.com/ricardoquesada/esp32-arduino-lib-builder/master/bluepad32_files/package_esp32_bluepad32_index.json
- Tools → Board → Boards Manager → search
bluepad32→ install esp32_bluepad32 by Ricardo Quesada. - Tools → Board → ESP32 + Bluepad32 Arduino → ESP32 Dev Module. Use the Bluepad32 group, not the regular
esp32group — that's what gives you DualSense support.
Open firmware/controller_ps5/controller_ps5.ino and upload.
- Power the phone controller.
- Phone WiFi →
ROCK_RC, password12345678. - Browser →
http://192.168.4.1 - Power the car. UI shows "Car: Connected".
- Left stick throttle, right stick steering. Show Settings for trim, max speed, ramp step.
- Power the PS5 controller ESP32.
- On the DualSense, hold the PS button + Create button until the light bar starts flashing rapidly.
- ESP32 picks it up automatically — you'll feel a connection buzz, then 1–5 short pulses indicating the current max-speed level (1 pulse = 20%, 5 pulses = 100%).
- Power the car. Drive.
Two drive modes — toggle with Circle:
- Stick mode (default): left stick = throttle, right stick X = steering
- Trigger mode: R2 = forward, L2 = reverse, right stick X = steering
Full mapping:
| Input | Action |
|---|---|
| Left stick Y | Throttle (stick mode) |
| Right stick X | Steering (both modes) |
| R2 | Forward (trigger mode) |
| L2 | Reverse (trigger mode) |
| Circle | Toggle stick / trigger drive mode |
| D-pad up | Max speed +20% (range 20–100%) |
| D-pad down | Max speed −20% |
| D-pad left | Steering trim −1° (range ±30°) |
| D-pad right | Steering trim +1° |
| Triangle | Ramp step −1 (smoother / slower acceleration) |
| Cross (X) | Ramp step +1 (sharper / faster acceleration) |
Vibration feedback:
- Connection: one buzz + 1–5 pulses (current max-speed level)
- Mode toggle (Circle): single stronger buzz
- Max-speed change (D-pad up/down): pulse pattern showing new level
- Trim / ramp change: short rumble
If pairing gets cursed (controller refuses to reconnect), set FORGET_BT_KEYS_ON_BOOT = true in controller_ps5.ino, flash, boot once, then set it back to false and flash again.
LiPo batteries are fire hazards if you mistreat them. Charge with a proper LiPo balance charger, ideally in a LiPo-safe bag. Don't puncture, don't overcharge, don't leave on charge unattended.
If forward/reverse or left/right is swapped:
- In
car.ino— swapESC_FORWARD/ESC_REVERSE, orSERVO_LEFT/SERVO_RIGHT(90 = center). - In
controller_ps5.ino— flip theINVERT_THROTTLEorINVERT_STEERINGconstants near the top.
Trim, max speed, and ramp step are adjustable live — sliders in the phone UI, D-pad / Triangle / Cross on the PS5 controller (see mapping table above).
- Wiring diagram image
- Battery voltage in UI
- Proper retention for battery / ESC / ESP32 (snap-fit or clamps instead of tape)
- Assembly photos / exploded view
- Code (
firmware/): MIT — see LICENSE - CAD (
cad/): CC BY-SA 4.0 — see cad/LICENSE
