Here’s an image that captures the heart of your project: a Raspberry Pi–powered RC car—built with the Raspberry Pi 5 and ready for a full DIY build and test drive.
Project Overview: RC Car Using Raspberry Pi 5—Full DIY Build + Test Drive
What We Know So Far
A recent YouTube video titled “RC Car Using Raspberry Pi 5 | Full DIY Build + Test Drive” showcases a functional build using an 8 GB Pi 5. It highlights improved speed and responsiveness thanks to the newer board (YouTube).
Though the video provides a dynamic demonstration, you’ll likely need additional sources or breakdowns to guide through your own build—such as wiring diagrams, code walkthroughs, and parts lists. Let’s explore what’s typically involved in such a build:
Typical Components & Setup
- Chassis & Drive System
A standard RC car chassis with steering servo and ESC (Electronic Speed Controller) forms the foundation. You’ll need to interface these with the Pi’s GPIO pins—often using a PWM-capable HAT or driver board (forums.raspberrypi.com, element14 Community). - Motor Control
L298N or PCA9685 motor driver modules are commonly used. The driver receives PWM signals from the Pi to control steering and throttle (Instructables, techwithsach.com). - Power Supply
Use a battery pack suitable for the Pi and motors. Many builders opt for separate power sources—5 V for the Pi, and 12 V or higher for motors via the ESC or driver board (plainenglish.io, Instructables). - Camera & Control Interface
A Pi Camera or USB webcam streams live video to a web interface. Control inputs can be handled via keyboard (WASD), game controllers, or custom scripts—often using frameworks like Flask or pygame (Instructables, Reddit, techwithsach.com).
Overview: Core Steps to Build
- Prepare the Chassis
Secure the motors, wheels, servo, and mount points. Ensure the ESC and steering servo are accessible for wiring. - Wire and Test Motor Drivers
Connect the steering and throttle PWM signals to the Pi via a driver board. Test with basic PWM scripts to ensure proper movement. - Set Up Power
Supply stable power to both the Pi and motors, ensuring voltage compatibility and current capacity. - Add the Camera & Stream Video
Mount the camera to the chassis and set up a streaming server or lightweight web interface to view what the car sees. - Control Logic & Code
Implement control logic—whether manual via keyboard/controller or immersive via a web dashboard. Tools like Flask or pygame are popular options (techwithsach.com, Instructables). - Test & Refine
Conduct test drives, calibrate steering and throttle response, and iterate on control code and latency.
Inspiration & Extensions
- Autonomous RC Car with Donkey Car
Platforms like Donkey Car enable adding self-driving capabilities using machine learning. These use Raspberry Pi, camera modules, throttle/steering control, and a training workflow—often using Google Colab (techwithsach.com, Emlid Community Forum). - Internet-Controlled Builds
Some creators use platforms like Surrogate.tv to host RC cars that anyone can drive remotely through the internet—great for interactive demos (Reddit).
How I Can Help You Now
Let me know if you’d like support with any of the following:
- Component selection: Ideal motor drivers, power configurations, or camera modules.
- Wiring diagrams: Visual layout of connections between Pi 5, ESC, servo, battery, and camera.
- Control code templates: Python scripts for manual keyboard control, or Flask-based streaming + control.
- Autonomous options: Guidance on integrating Donkey Car or similar ML-based control.
- Remote access setup: How to control your car over Wi-Fi or the internet.
Awesome — let’s add an AI Autopilot that does camera-based obstacle avoidance, while keeping manual override from your gamepad and web sliders. If you touch the sticks or the web UI, manual instantly takes over; if you stop touching for a moment, autopilot resumes.
Below you’ll get:
- Updated
index.html(adds Autopilot toggle + status) - Single-file
app.py(Flask + camera stream + PCA9685 + gamepad + autopilot thread) - Two autopilot modes:
- Lite (default): classical vision (edges + motion) — no extra downloads
- DNN (optional): MobileNet-SSD person/object awareness — drop in model files later
1) Install packages
sudo apt update
sudo apt install python3-flask python3-picamera2 python3-pip python3-opencv python3-pygame
pip3 install adafruit-circuitpython-pca9685
Optional (for DNN mode later — put files in
models/):
models/MobileNetSSD_deploy.caffemodelmodels/MobileNetSSD_deploy.prototxt
2) Web UI — static/index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Pi RC Car</title>
<style>
body { font-family: Arial, sans-serif; max-width: 900px; margin: 0 auto; padding: 16px; }
img { width: 100%; max-width: 860px; border: 1px solid #ddd; border-radius: 8px; }
.row { display:flex; gap:16px; flex-wrap:wrap; margin-top:12px;}
.card { flex:1; min-width:260px; border:1px solid #eee; border-radius:12px; padding:12px; box-shadow:0 2px 10px rgba(0,0,0,0.05);}
.label { font-weight:bold; }
.slider { width:100%; }
.pill { display:inline-block; padding:4px 10px; border-radius:999px; background:#efefef; margin-left:6px;}
button { padding:8px 12px; border-radius:10px; border:1px solid #ddd; cursor:pointer; }
.on { background:#d9f8df; border-color:#9ae6a3; }
.danger { background:#ffe3e3; border-color:#ffbdbd; }
</style>
</head>
<body>
<h1>Raspberry Pi 5 RC Car</h1>
<img src="{{ url_for('video_feed') }}" />
<div class="row">
<div class="card">
<div class="label">Steering</div>
<input id="steering" class="slider" type="range" min="60" max="120" value="90">
<div><span>Angle:</span> <span id="steerVal" class="pill">90</span></div>
</div>
<div class="card">
<div class="label">Throttle</div>
<input id="throttle" class="slider" type="range" min="60" max="120" value="90">
<div><span>Angle:</span> <span id="throtVal" class="pill">90</span></div>
</div>
<div class="card">
<div class="label">Actions</div>
<button onclick="stopCar()" class="danger">Stop</button>
<button id="apBtn" onclick="toggleAP()">Autopilot: OFF</button>
<div style="margin-top:8px;">
<span class="label">Mode:</span>
<select id="apMode" onchange="setMode(this.value)">
<option value="lite">Lite (Edges/Motion)</option>
<option value="dnn">DNN (MobileNet-SSD)</option>
</select>
</div>
<div style="margin-top:8px;">
<span class="label">Status:</span>
<span id="apStatus" class="pill">idle</span>
</div>
</div>
</div>
<script>
const send = (p, v) => fetch(`/${p}/${v}`);
const post = (p, body={}) => fetch(`/${p}`, {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(body)});
const s = document.getElementById('steering');
const t = document.getElementById('throttle');
const steerVal = document.getElementById('steerVal');
const throtVal = document.getElementById('throtVal');
const apBtn = document.getElementById('apBtn');
const apStatus = document.getElementById('apStatus');
const apMode = document.getElementById('apMode');
s.oninput = e => { steerVal.textContent = e.target.value; send('steering', e.target.value); };
t.oninput = e => { throtVal.textContent = e.target.value; send('throttle', e.target.value); };
function stopCar() {
send('throttle', 90); send('steering', 90);
s.value = 90; t.value = 90; steerVal.textContent='90'; throtVal.textContent='90';
}
async function toggleAP() {
const r = await post('autopilot/toggle');
const j = await r.json();
apBtn.textContent = `Autopilot: ${j.enabled ? 'ON' : 'OFF'}`;
apBtn.className = j.enabled ? 'on' : '';
}
async function setMode(m) {
const r = await post('autopilot/mode', {mode:m});
const j = await r.json();
apStatus.textContent = `mode: ${j.mode}`;
}
// Poll autopilot status every 1s
async function poll() {
try {
const r = await fetch('/autopilot/status');
const j = await r.json();
apBtn.textContent = `Autopilot: ${j.enabled ? 'ON' : 'OFF'}`;
apBtn.className = j.enabled ? 'on' : '';
apMode.value = j.mode;
apStatus.textContent = j.state;
} catch(e) {}
setTimeout(poll, 1000);
}
poll();
</script>
</body>
</html>
3) Server — app.py
from flask import Flask, render_template, Response, request, jsonify
from adafruit_servokit import ServoKit
from picamera2 import Picamera2
import cv2, time, threading, pygame, numpy as np
import os
app = Flask(__name__, static_folder="static", template_folder="static")
# === Servo / ESC ===
kit = ServoKit(channels=16)
STEERING_CENTER = 90
THROTTLE_STOP = 90
kit.servo[0].angle = STEERING_CENTER
kit.servo[1].angle = THROTTLE_STOP
# Limits
STEER_MIN, STEER_MAX = 60, 120
THROT_MIN, THROT_MAX = 60, 120
# Manual override logic
last_manual_time = time.time()
MANUAL_TIMEOUT_S = 1.0 # if no manual input for 1s, autopilot may act
def clamp(v, lo, hi): return max(lo, min(hi, v))
def set_steering(angle):
global last_manual_time
kit.servo[0].angle = clamp(int(angle), STEER_MIN, STEER_MAX)
last_manual_time = time.time()
def set_throttle(angle):
global last_manual_time
kit.servo[1].angle = clamp(int(angle), THROT_MIN, THROT_MAX)
last_manual_time = time.time()
# === Camera ===
camera = Picamera2()
camera.configure(camera.create_video_configuration(main={"size": (640, 480)}))
camera.start()
frame_lock = threading.Lock()
latest_frame = None
def cam_loop():
global latest_frame
while True:
frame = camera.capture_array()
with frame_lock:
latest_frame = frame
threading.Thread(target=cam_loop, daemon=True).start()
def gen_frames():
while True:
with frame_lock:
frame = latest_frame.copy() if latest_frame is not None else None
if frame is None:
time.sleep(0.01); continue
_, buffer = cv2.imencode('.jpg', frame)
yield (b'--frame\r\nContent-Type: image/jpeg\r\n\r\n' + buffer.tobytes() + b'\r\n')
@app.route('/')
def index(): return render_template('index.html')
@app.route('/video_feed')
def video_feed(): return Response(gen_frames(), mimetype='multipart/x-mixed-replace; boundary=frame')
@app.route('/steering/<int:angle>')
def http_steer(angle): set_steering(angle); return ("", 204)
@app.route('/throttle/<int:angle>')
def http_throt(angle): set_throttle(angle); return ("", 204)
# === Autopilot state ===
ap_enabled = False
ap_mode = "lite" # "lite" or "dnn"
ap_state = "idle"
@app.route('/autopilot/toggle', methods=['POST'])
def ap_toggle():
global ap_enabled
ap_enabled = not ap_enabled
return jsonify({"enabled": ap_enabled})
@app.route('/autopilot/mode', methods=['POST'])
def ap_set_mode():
global ap_mode
ap_mode = request.json.get("mode","lite")
return jsonify({"mode": ap_mode})
@app.route('/autopilot/status')
def ap_status():
return jsonify({"enabled": ap_enabled, "mode": ap_mode, "state": ap_state})
# === Gamepad thread ===
def gamepad_loop():
global last_manual_time
pygame.init()
pygame.joystick.init()
if pygame.joystick.get_count() == 0:
print("No gamepad found. (Autopilot and web control still work.)")
return
js = pygame.joystick.Joystick(0); js.init()
print(f"Gamepad connected: {js.get_name()}")
while True:
pygame.event.pump()
steer_axis = js.get_axis(0) # left stick X
throttle_axis = -js.get_axis(1) # left stick Y (invert)
if abs(steer_axis) > 0.05 or abs(throttle_axis) > 0.05:
# Apply manual input and refresh override timer
s_angle = STEERING_CENTER + steer_axis * 30
t_angle = THROTTLE_STOP + throttle_axis * 30
kit.servo[0].angle = clamp(int(s_angle), STEER_MIN, STEER_MAX)
kit.servo[1].angle = clamp(int(t_angle), THROT_MIN, THROT_MAX)
last_manual_time = time.time()
time.sleep(0.03)
threading.Thread(target=gamepad_loop, daemon=True).start()
# === Autopilot helpers ===
# Lite mode: edge+motion occupancy left/center/right
def lite_nav(frame):
"""
Returns (steer_delta_degrees, throttle_angle) where positive steer_delta turns right.
Simple strategy:
- Edge map + motion map to estimate obstacle density L/C/R.
- Prefer region with least density; slow if center is crowded.
"""
h, w = frame.shape[:2]
roi = frame[int(h*0.45):h, :] # use lower half
gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)
edges = cv2.Canny(gray, 60, 150)
# Simple motion (frame differencing) — keep a tiny ring buffer
if not hasattr(lite_nav, "prev"):
lite_nav.prev = gray
diff = cv2.absdiff(gray, lite_nav.prev)
lite_nav.prev = gray
_, motion = cv2.threshold(diff, 25, 255, cv2.THRESH_BINARY)
# Combine signals
occ = cv2.addWeighted(edges, 0.7, motion, 0.3, 0)
thirds = np.array_split(occ, 3, axis=1)
dens = [cv2.countNonZero(t) for t in thirds] # [L, C, R]
L, C, R = dens
# Steering: aim for least dense side
target = np.argmin(dens) # 0=L,1=C,2=R
steer_delta = {0: -20, 1: 0, 2: 20}[target]
# Throttle: slow if center crowded
crowd = C / (occ.shape[0]*occ.shape[1]/3)
if crowd > 0.12: # heuristic
throttle = THROTTLE_STOP + 5 # creep
else:
throttle = THROTTLE_STOP + 15 # cruise
return steer_delta, clamp(int(throttle), THROT_MIN, THROT_MAX)
# DNN mode: optional MobileNet-SSD person/obstacle awareness
net = None
def ensure_dnn():
global net
if net is not None: return True
proto = "models/MobileNetSSD_deploy.prototxt"
model = "models/MobileNetSSD_deploy.caffemodel"
if not (os.path.exists(proto) and os.path.exists(model)):
return False
net = cv2.dnn.readNetFromCaffe(proto, model)
return True
def dnn_nav(frame):
"""
Detect common objects; if a large box is centered/near bottom, reduce speed and steer away.
"""
h, w = frame.shape[:2]
blob = cv2.dnn.blobFromImage(cv2.resize(frame, (300, 300)), 0.007843, (300,300), 127.5)
net.setInput(blob)
dets = net.forward()
Lpen = Cpen = Rpen = 0.0
for i in range(dets.shape[2]):
conf = dets[0,0,i,2]
if conf < 0.5: continue
box = dets[0,0,i,3:7] * np.array([w,h,w,h])
x1,y1,x2,y2 = box.astype(int)
area = (x2-x1)*(y2-y1)
# weight near lower center more (potential path)
ymid = (y1+y2)/2 / h
weight = area / (w*h) * (0.5 + ymid) # bigger/closer = higher weight
xm = (x1+x2)/2
if xm < w/3: Lpen += weight
elif xm < 2*w/3: Cpen += weight
else: Rpen += weight
# steer to lowest penalty
dens = [Lpen, Cpen, Rpen]
target = int(np.argmin(dens))
steer_delta = {0:-20, 1:0, 2:20}[target]
# throttle: slower if center penalty high
throttle = THROTTLE_STOP + (8 if Cpen > 0.015 else 18)
return steer_delta, clamp(int(throttle), THROT_MIN, THROT_MAX)
# === Autopilot loop ===
def autopilot_loop():
global ap_state, last_manual_time
while True:
time.sleep(0.03)
if not ap_enabled:
ap_state = "off"
continue
with frame_lock:
frame = latest_frame.copy() if latest_frame is not None else None
if frame is None:
ap_state = "waiting_camera"; continue
# Manual override check
if time.time() - last_manual_time < MANUAL_TIMEOUT_S:
ap_state = "manual_override"
continue
try:
if ap_mode == "dnn" and ensure_dnn():
ap_state = "autopilot_dnn"
steer_delta, throttle = dnn_nav(frame)
else:
ap_state = "autopilot_lite"
steer_delta, throttle = lite_nav(frame)
# Apply command
current_steer = kit.servo[0].angle or STEERING_CENTER
new_steer = clamp(int(current_steer + steer_delta), STEER_MIN, STEER_MAX)
kit.servo[0].angle = new_steer
kit.servo[1].angle = throttle
except Exception as e:
ap_state = f"error:{type(e).__name__}"
threading.Thread(target=autopilot_loop, daemon=True).start()
if __name__ == '__main__':
# TIP: for lower latency on LAN, consider threaded server like waitress or gunicorn
app.run(host='0.0.0.0', port=5000, debug=False)
How the hybrid control works
- Manual inputs (gamepad or web sliders) update servos immediately and set a timestamp.
- Autopilot only acts if there’s been no manual input for 1 second (configurable via
MANUAL_TIMEOUT_S). - Lite mode uses edges+motion on the road area to choose Left / Center / Right with least “crowding,” and adjusts speed.
- DNN mode (optional) uses MobileNet-SSD to estimate obstacle density L/C/R (more aware of “things” like people/signs) and steers away.
Safety & tuning tips
- Start with wheels off the ground; verify directions and neutral at
90. - Tune ranges:
- Steering throw: change
±30ingamepad_loopandsteer_deltavalues. - Speed: tweak
THROTTLE_STOP + {5..20}in nav functions.
- Steering throw: change
- If your ESC requires arming (neutral 1–2s), add a short delay at boot keeping throttle at
90. - For DNN mode, copy model files into
models/and switch the UI to DNN.
Want me to bundle this into a ready-to-run zip (folders + files) or add on-screen HUD overlays (FPS, mode, detections) to the video stream?
All packed up! Grab your ready-to-run project bundle:
What you’re getting
app.py— Flask server with web UI, gamepad control, autopilot, and HUD overlay (FPS + AP state/mode).static/index.html— Web controls (sliders + Autopilot toggle + mode selector).models/— Placeholder folder for optional MobileNet-SSD files (for DNN mode).requirements.txt&README.md— Install + run instructions.
Quick start
unzip rc_car_ai_bundle.zip
cd rc_car_ai
sudo apt update
sudo apt install -y python3-flask python3-picamera2 python3-opencv python3-pygame python3-pip
pip3 install -r requirements.txt
python3 app.py
# open http://<PI_IP>:5000
Want me to add a systemd service so it auto-starts on boot, or tailor the PWM ranges for your exact ESC/servo?

