BS ECE · Carnegie Mellon · May 2028

zhaojinchu@cmu.edu github.com/zhaojinchu linkedin.com/in/zhaojinc


AN-004 · Embedded & Robotics · Finished
Started Aug 2026

TeleKart: the Pi owns safety; the Mac only asks


1.0Abstract

An RC car driven from a laptop over Wi-Fi with a live camera feed. Every safety decision lives on the Pi that owns the motors, and all of v1 was built and tested against a simulator before the car was allowed to move.

At a glance

Parameter Value
Status Finished
When and where Aug 2026, personal project
Built with Raspberry Pi with an L298N motor driver via pigpio, an HS-311 steering servo and picamera2; Python; a pygame client on a Mac
Code github.com/zhaojinchu/TeleKart
Links Driving intent as JSON over UDP, telemetry back the same way, MJPEG video over HTTP
Control loop 100 Hz on the Pi, with a watchdog, slew limiting and an arm gate
Simulator Shares the vehicle model with the real Pi, so v1 was verified before the car could move

Scope, deliberately small

v1 does one thing: drive the car from a Mac with WASD while watching the Pi’s camera. The wheel encoders are wired and ignored: no odometry, no closed loop. A steering wheel as the input device is the goal for v2 and explicitly not part of this version.

Who computes what

MAC (client)
┌──────────────────────────────────┐
│ main.py · pygame window          │
│  video pane  ◀── MJPEG /stream   │ HTTP :8090
│  WASD keys   ──▶ cmd JSON        │ UDP  :8091
│  debug panel ◀── tlm JSON        │ UDP
│  logs/session-*.jsonl            │
└────────────────┬─────────────────┘
                 │  identical protocol
      ┌──────────┴──────────┐
      ▼                     ▼
PI (vehicle)            SIM (on the Mac)
┌────────────────────┐  ┌────────────────────┐
│ video.py picamera2 │  │ sim/fake_pi.py     │
│ main.py  control   │  │ same protocol,     │
│  @100 Hz: watchdog │  │ same               │
│  · slew · arm gate │  │ common/vehicle.py  │
│ car.py   pigpio →  │  └────────────────────┘
│  L298N · HS-311    │
└────────────────────┘

The Mac sends raw intent; the Pi computes all vehicle behavior. Ramping, deadband, duty mapping, direction guards and limits live in one place: the side that owns the hardware. The Mac never computes a PWM value.

Two things follow from that. A buggy or crashed client can’t bypass the safety logic, because the safety logic isn’t on the client. And v2 can swap key-booleans for wheel-axis floats without the Pi changing at all.

Nothing responds to throttle until you arm it (Enter). A page load or a reconnect should never be able to make the car lurch.

The simulator

sim/fake_pi.py runs on the Mac and speaks the identical protocol with a synthetic camera. It doesn’t approximate the Pi: it imports the same common/vehicle.py, so the failsafes tested against it are the same code that runs on the car. That is what made it possible to build and verify all of v1 while the real Pi was off-limits, running other software.

Debugging by design

Both ends write logs/session-<ts>.jsonl: one JSON object per line, every message, both directions, tagged tx or rx. Two independent captures of the same conversation is what turns “it worked on the Mac but the car didn’t move” into a five-second diagnosis:

# did the slew limiter engage?
jq 'select(.dir=="rx") | .throttle_out' logs/session-*.jsonl
# when did the link drop?
jq 'select(.state=="FAILSAFE")' logs/session-*.jsonl

The live debug panel shows state, round-trip time, throttle_cmd → throttle_out, servo microseconds, duty percentage, link rates and drop counts. If throttle looks stuck, comparing throttle_cmd with throttle_out tells you immediately whether it’s the slew limiter or something real.

What’s next

v2: a steering wheel as the input device. The Pi does not change for it, since all it ever receives is intent.

← All projects