// Raspberry Pi 3 · Firmware Rewrite · Python

RC Car
Rewrite

Ground-up rewrite of the Freenove 4WD Smart Car firmware. Custom TCP client/server, real-time WASD control over WiFi, live camera streaming, motor and servo control — no frameworks, minimal dependencies.

// System Architecture

Laptop
client.py
──5000──▶ commands ◀──5001── camera
Raspberry Pi 3
server.py

pygame input
TCP socket
TCP_NODELAY
camera thread
motor.py
servo.py
pca9685.py (I2C)
camera.py

The Car

Freenove 4WD Smart Car chassis running a Raspberry Pi 3. The PCA9685 PWM driver sits between the Pi and all actuators, communicating over I2C and outputting independent PWM signals on 16 channels.

Keybindings

WASD drives the motors. Arrow keys pan and tilt the camera.

WMove forward
SReverse
ARotate left
DRotate right
W+DForward right curve
W+AForward left curve
S+DReverse right curve
S+AReverse left curve
Tilt camera up
Tilt camera down
Pan camera left
Pan camera right

Implementation

Server side runs on the Pi with zero pip installs. Client runs on the laptop with pygame for input.

TCP Sockets

Two parallel sockets — port 5000 for commands, port 5001 for the camera stream. Raw Python stdlib, no frameworks.

PCA9685

I2C PWM driver chip. 16 independent channels. Motors use two channels each — one forward, one backward — with 12-bit duty cycles.

pygame

Client-side input via key.get_pressed() — polls all keys simultaneously every loop iteration. No OS repeat delay.

picamera2

Captures raw RGB888 frames at 640×480. Streamed over TCP with a 4-byte size header so the client knows exactly how many bytes to read.

threading

Commands and camera streaming run in separate threads on both sides. Prevents either socket from blocking the other.

SSHFS

Pi filesystem mounted on the bare-metal workstation. Edit in Neovim locally, files live and execute on the car. No sync step.

// How It Works

Under the Hood

Each subsystem is isolated into its own module. The server dispatches commands to the right module based on the string received.

Motors motor.py

Each motor uses two PCA9685 channels — one forward, one backward. Duty cycles run 0–4095 (12-bit). Setting both channels to 4095 simultaneously triggers an active brake. Turning is skid-steer — one side drives, the other stops — the same mechanism used in tanks.

Servos servo.py

Driven at 50Hz via PCA9685. Pulse width maps to angle — 500μs to 2500μs covers 0–180°. Pan and tilt are incremental — each keypress adjusts the stored pulse by a fixed offset, so the camera holds position when the key is released.

Camera camera.py

Pi captures RGB888 frames via picamera2. Each frame is prefixed with a 4-byte big-endian size header before sending. Client reads the size first, then reads exactly that many bytes — reassembles the numpy array and blits it to the pygame surface.

Commands server.py

Commands are plain UTF-8 strings — move_forward, tilt_up, stop etc. TCP_NODELAY is set on both sockets to disable Nagle's algorithm, ensuring each 12-byte command is sent immediately without buffering.

// Reflection

What I Learned

Things that weren't obvious until I built it.