UCSD Robocar
ECE / MAE 148
DonkeyCar Setup & Training Guide
UCSD Jacobs School of Engineering

DonkeyCar Framework
Setup, Training & Autonomous Driving

Complete reference for ECE/MAE 148 — covering Raspberry Pi preparation, DonkeyCar installation, Oak-D Lite camera setup, VESC motor controller integration, source code patches, data collection, model training, and autonomous deployment.

🍓Raspberry Pi 5
🤖DonkeyCar
📷Oak-D Lite
VESC
🧠TensorFlow 2.15
01

Raspberry Pi System Prep

Prepare the OS, enable hardware interfaces, and expand storage

Before installing any DonkeyCar software, the Raspberry Pi OS must be up-to-date and properly configured. This section enables the I2C interface (required for some sensors), expands the filesystem to use your full SD card, and ensures the package manager is current. Skipping these steps is the most common cause of cryptic installation failures later.

What is I2C?
I2C (Inter-Integrated Circuit) is a communication protocol that lets the Raspberry Pi talk to sensors and peripherals using just two wires. It must be explicitly enabled in the OS before any device can use it. Even if you are not using I2C sensors right now, enabling it is required for certain DonkeyCar components to initialize correctly.

Step 1 — Update the System

Fetch updated package lists from the Debian/Raspberry Pi repositories and upgrade all installed packages. The --allow-releaseinfo-change flag suppresses errors that appear when repository metadata (such as suite names) changes between point releases of Raspberry Pi OS.

Pi terminal
# Refresh package lists — safe even if Pi OS was recently updated
sudo apt-get update --allow-releaseinfo-change

# Upgrade all installed packages to the latest available versions
sudo apt-get upgrade
Use Home Wi-Fi for Upgrades
The campus Wi-Fi blocks or throttles large package downloads. Run this step from home or via a mobile hotspot. An interrupted upgrade can leave broken packages that are difficult to recover from.

Step 2 — Configure Hardware Settings via raspi-config

raspi-config is the Raspberry Pi's built-in configuration tool. It presents a text-based menu that lets you change OS-level settings without editing config files manually. Two critical settings must be changed here: I2C must be enabled so the Pi can communicate with sensors over the I2C bus, and Expand Filesystem ensures the OS uses the full capacity of your SD card rather than the smaller compressed image size it starts with.

Pi terminal
sudo raspi-config
Navigation Inside raspi-config
Use the arrow keys to move between options, Enter to select, and Tab to jump to the <Finish> button at the bottom. Perform both of these actions before exiting:

1. Interface Options → I2C → Enable
2. Advanced Options → Expand Filesystem

Select Yes when prompted to reboot after you exit.

Verification Test — Confirm I2C is Active

After rebooting, run the following commands to confirm that I2C was enabled successfully. The device file /dev/i2c-1 should exist. If any I2C devices are physically wired to the Pi, their addresses will appear in the i2cdetect output.

Pi terminal — after reboot
# Verify the I2C kernel device file exists
ls /dev/i2c-*
# Expected: /dev/i2c-1

# Install i2c-tools, then scan for connected I2C devices
sudo apt install -y i2c-tools
i2cdetect -y 1
# Each cell shows -- if no device is at that address, or a hex address if a device is found
Filesystem Expansion Confirmation
Run df -h / after rebooting. The available space on /dev/root should now match your full SD card size (e.g., ~28 GB on a 32 GB card), not the compressed 4–6 GB image size it started at.

Common Issues & Fixes

ProblemCauseFix
E: Release file is not valid yetPi system clock is wrong (NTP not yet synced)Run sudo date -s "$(curl -s --head http://google.com | grep Date | sed 's/Date: //')" to set the clock, then retry.
/dev/i2c-1: No such fileI2C was not enabled or the kernel module did not loadRe-run sudo raspi-config → Interface Options → I2C → Enable, then reboot.
Filesystem still shows 4–6 GB after rebootThe Expand Filesystem step was skippedRe-run sudo raspi-config → Advanced Options → Expand Filesystem, then reboot again.
02

DonkeyCar Installation

Create a virtual environment and install all DonkeyCar dependencies

DonkeyCar and its dependencies (TensorFlow, OpenCV, NumPy) can conflict with the system-level Python packages the Raspberry Pi OS uses internally. A virtual environment isolates DonkeyCar's libraries completely, so you can safely upgrade or reinstall them without breaking any OS tools.

1
Create the Virtual Environment
The --system-site-packages flag is important — it allows the virtual environment to inherit system-level packages like numpy that were compiled against the Pi's hardware-accelerated math libraries. Without it, pip would install a slower, generic version. The second command adds source ~/env/bin/activate to your ~/.bashrc startup file so the environment activates automatically on every new login session.
Pi terminal
# Create a virtualenv named 'env' that can see system-level packages
python3 -m venv env --system-site-packages

# Auto-activate this environment on every new terminal session
echo "source ~/env/bin/activate" >> ~/.bashrc
source ~/.bashrc
2
Install System Dependencies
libcap-dev enables POSIX capability access needed by some DonkeyCar components. libhdf5-dev and libhdf5-serial-dev provide the HDF5 file format library that TensorFlow uses to save and load trained model weights (.h5 files). Without these, the TensorFlow pip installation will either fail or produce a broken build.
Pi terminal
sudo apt install -y libcap-dev libhdf5-dev libhdf5-serial-dev
3
Install DonkeyCar (Standard)
The [pi] extras selector tells pip to also install the Raspberry Pi-specific dependencies, including TensorFlow Lite, DepthAI camera bindings, and VESC serial libraries. This is the recommended installation method for production use.
Pi terminal — virtual environment must be active (prompt shows "(env)")
pip install donkeycar[pi]
4
Alternative: Clone from GitHub (Custom Version)
Use this method only if you need a specific version or want to apply custom patches before installing. The -e flag installs in "editable" mode — any changes you make to the source files inside ~/projects/donkeycar/ take effect immediately without reinstalling. Replace 5.1.0 with the specific git tag or branch name you need.
Pi terminal — only if the standard pip install above is insufficient
mkdir ~/projects
cd ~/projects
git clone https://github.com/autorope/donkeycar
cd donkeycar
git checkout 5.1.0      # Replace with the version tag you need
pip install -e .[pi]
5
Create Your Car Directory
This command scaffolds a complete DonkeyCar project at ~/mycar. It creates the manage.py drive script, a default myconfig.py configuration file, a models/ directory for trained model weights, and a data/ directory for driving recordings (called "tubs" — each tub is one folder of images paired with steering and throttle values recorded during a manual drive session). All subsequent configuration happens inside this directory.
Pi terminal
donkey createcar --path ~/mycar

Verification Test — Confirm Installation

Pi terminal
# Verify DonkeyCar installed correctly and the car directory was created
python3 -c "import donkeycar; print('DonkeyCar version:', donkeycar.__version__)"
ls ~/mycar
# Expected output includes: manage.py  myconfig.py  models/  data/
Common Error: pip Not Found
If you get command not found: pip, the virtual environment is not active. Run source ~/env/bin/activate and verify your terminal prompt shows (env) before the hostname. Then re-run the pip command.
03

Camera Setup — Oak-D Lite

Install DepthAI drivers and configure USB permissions for the Oak-D Lite
Critical

The Oak-D Lite is an Intel Movidius MyriadX-based depth camera that also provides a high-quality RGB stream. It communicates with the Pi over USB and requires a udev rule to allow non-root access. Without this rule, every attempt to open the camera will fail with a permission error, even if the camera is physically detected by the system.

Step 1 — Install the DepthAI Python Library

This specific version (2.21.2.0) is pinned because later versions changed the pipeline API in ways that conflict with DonkeyCar's oak_d.py component. Using a different version will likely cause import errors or silent camera failures at runtime.

Pi terminal
pip install depthai==2.21.2.0

# Confirm the camera is detected at the USB level before testing the driver
lsusb
# Look for a line containing: Intel Corp. Movidius MyriadX   or the USB ID   03e7:2485

Step 2 — Create the udev Permission Rule

By default on Linux, USB devices are only accessible to the root user. A udev rule is a small configuration file that tells the OS to change the permissions on a device file as soon as it is plugged in. The rule below sets the file mode to 0666 (readable and writable by all users) for any USB device with Intel's Movidius vendor ID (03e7). This is what allows a regular Python script running as your user account to open the camera without requiring sudo.

About the tee Command Below
The command uses a "heredoc" — everything between the two EOF markers is written directly to the file. You do not need to type the file contents manually; just copy and run the entire block as one command.
Pi terminal — creates the udev rule file
# Write the udev rule file (copy and run this entire block as one command)
sudo tee /etc/udev/rules.d/80-movidius.rules > /dev/null <<'EOF'
# Intel Movidius / Luxonis OAK-D-Lite (MyriadX) — allow non-root user access
SUBSYSTEM=="usb", ATTR{idVendor}=="03e7", MODE:="0666"
EOF

# Apply the new rule immediately — no reboot required for this step
sudo udevadm control --reload-rules
sudo udevadm trigger

Step 3 — Add Your User to the Required Groups

The plugdev group grants access to removable USB devices. The dialout group grants access to serial ports, which is required for the VESC motor controller in Section 4. Both groups must be added now. Group changes require a full reboot to take effect.

Pi terminal
sudo usermod -aG plugdev $USER
sudo usermod -aG dialout $USER
sudo reboot

Step 4 — Test the Camera

This Python script opens a connection to the Oak-D Lite and prints its device information. A successful result confirms that the udev rule, group permissions, and DepthAI driver are all working correctly.

Pi terminal — run after the reboot
python3 - << 'EOF'
import depthai as dai
print("DepthAI version:", dai.__version__)

# List all cameras visible on USB (should show OAK-D-LITE)
print("Available devices:", dai.Device.getAllAvailableDevices())

# Open the camera and read its identity
with dai.Device() as device:
    print("Opened device:", device.getDeviceName())
    print("MXID:", device.getMxId())
    print("USB speed:", device.getUsbSpeed())
EOF
Expected output
DepthAI version: 2.21.2.0
Available devices: [DeviceInfo(OAK-D-LITE-FF)]
Opened device: OAK-D-LITE-FF
MXID: 18443010D10BF40F00
USB speed: UsbSpeed.HIGH
USB Speed Note
UsbSpeed.HIGH means USB 2.0 (480 Mbps), which is sufficient for the 160×120 RGB stream used in this course. If you see UsbSpeed.SUPER, the camera is running at USB 3.0 speed — either is fine. If you see UsbSpeed.LOW or UsbSpeed.FULL, try a different cable or USB port, as the camera may not perform reliably at those speeds.

Troubleshooting Camera Issues

Error / SymptomCauseFix
RuntimeError: Failed to find deviceCamera not detected on USBRun lsusb | grep 03e7. If nothing appears, try a different USB port or cable. A powered USB hub is recommended if the Pi's ports cannot supply enough current.
Permission denied opening /dev/bus/usb/...udev rule was not applied, or the user is not in the plugdev groupVerify the rule exists: cat /etc/udev/rules.d/80-movidius.rules. Then reboot and retry — group membership only takes effect after a full reboot.
Camera found by lsusb but Python failsWrong depthai version installedRun pip show depthai. The version must be exactly 2.21.2.0. Reinstall with pip install depthai==2.21.2.0.
Camera opens, then disconnects repeatedlyInsufficient USB power from the Pi's built-in portsUse a powered USB 3.0 hub between the Pi and the camera. The Oak-D Lite draws up to ~900 mA at peak load.
04

Motor Controller — VESC

Verify serial connection and install the PyVESC library

The VESC (Vedder Electronic Speed Controller) controls the car's drive motor and steering servo. It communicates with the Raspberry Pi over a USB-to-serial connection. When plugged in, the Pi creates a device file at /dev/ttyACM0. The PyVESC library handles the VESC serial protocol, translating Python steering and throttle commands into the low-level binary packets the VESC expects.

Prerequisite: VESCTool Motor & App Configuration Must Be Complete
Before the VESC will respond to any commands from the Pi, it must first be configured using the VESCTool desktop application. If you have not done this yet, complete the VESC Setup guide first, then return here.

Specifically, both of these operations must have been performed in VESCTool:

Write Motor Configuration — configures the motor type, pole count, and current limits so the VESC drives the motor correctly.

Write App Configuration — sets the communication interface to UART, which is the serial mode the Pi uses. Without this step, the VESC will completely ignore all commands sent from the Pi over USB.

Step 1 — Verify the Serial Device is Visible

With the VESC powered on and connected to the Pi via USB, a device file should appear automatically. Run the command below to confirm it exists.

Pi terminal — VESC must be powered on and connected via USB
ls /dev/ttyACM*
# Expected: /dev/ttyACM0   (may appear as ttyACM1 if the Oak-D camera is also connected)

# If nothing appears, check the kernel log for USB enumeration errors
dmesg | grep -i "usb\|tty\|acm" | tail -20

Step 2 — Install the PyVESC Library

PyVESC handles the binary serial protocol used to send commands to and read data from the VESC. It must be installed directly from GitHub because the PyPI version is outdated. The pyserial library (which handles the underlying serial port communication) is installed automatically as a dependency.

Pi terminal
pip install git+https://github.com/LiamBindle/PyVESC.git@master

# Verify both pyvesc and pyserial installed correctly
python3 -c "import pyvesc; import serial; print('pyvesc OK, pyserial', serial.__version__)"
# Expected: pyvesc OK, pyserial 3.5

Step 3 — Verify Serial Port Permissions

The dialout group (added in Section 3) must be active for your user account. Confirm both the group membership and the device file permissions.

Pi terminal
# Confirm your user account is a member of the dialout group
groups $USER | grep dialout
# Expected output contains: dialout

# Confirm the device file is owned by the dialout group
ls -la /dev/ttyACM0
# Expected: crw-rw---- 1 root dialout 166, 0 ...
VESC Firmware Version Warning
Modern VESC firmware (v5.x and later) returns a version string that the stock PyVESC library cannot handle, causing an unhandled exception on startup. Section 7 documents the required code patch to fix this. Do not attempt to drive the car until that patch has been applied.
05

Joystick — Logitech F710

Install input tools and verify joystick recognition

The Logitech F710 connects via a USB wireless dongle and appears to the OS as a standard Linux input event device. DonkeyCar reads joystick input through the Linux joystick interface at /dev/input/js0. The evtest tool lets you verify that button presses and analog stick movements are being registered before you launch manage.py.

Step 1 — Install Joystick Tools

Pi terminal
sudo apt install -y joystick evtest

Step 2 — Identify the Controller's Event Node

Running evtest prints a numbered list of all detected input devices. Select the number next to "Logitech Gamepad F710" (or your specific controller name). Once selected, move the analog sticks — you should see a continuous stream of axis-change events printed to the terminal, confirming the OS is reading the controller correctly. Press Ctrl+C to exit.

Pi terminal — wireless USB dongle must be plugged in and controller powered on
sudo evtest
# Select the event number for "Logitech Gamepad F710" from the list
# Move the analog sticks — you should see lines like:
#   Event: type 3 (EV_ABS), code 0 (ABS_X), value 14253
# Press Ctrl+C to exit when done

Step 3 — Verify the JS0 Device File Exists

DonkeyCar reads the joystick from /dev/input/js0. This Python script confirms the file is present before you run the full car stack.

Pi terminal
python3 - << 'EOF'
import os
js_exists = os.path.exists("/dev/input/js0")
print("Joystick device /dev/input/js0 present:", js_exists)
assert js_exists, "ERROR: /dev/input/js0 not found — is the dongle plugged in and the controller powered on?"
print("Joystick ready.")
EOF
# Expected:
# Joystick device /dev/input/js0 present: True
# Joystick ready.
F710 Mode Switch
The Logitech F710 has a Mode button on the front face that swaps the X/Y axis assignments between D mode (DirectInput) and X mode (XInput). For DonkeyCar with CONTROLLER_TYPE = 'F710', the LED near the Mode button must be OFF (DirectInput mode). If your steering or throttle axes feel swapped or unresponsive, press the Mode button once to toggle between modes and test again.
06

TensorFlow Verification

Confirm TensorFlow 2.15.1 is installed and functional on the Pi

TensorFlow is the machine learning engine used to train and run the DonkeyCar autopilot model. On Raspberry Pi, TensorFlow runs on the CPU only — there is no GPU acceleration. This is expected and perfectly fine for the small models DonkeyCar uses. These tests verify that the installation is complete and not corrupted.

Test 1 — Version Check

Pi terminal
python3 - << 'EOF'
import tensorflow as tf
print("TensorFlow version:", tf.__version__)
EOF
# Expected: TensorFlow version: 2.15.1

Test 2 — Matrix Multiplication

This confirms TensorFlow can actually execute tensor operations, not just import. The output should match exactly — if it does, the mathematical backend is working correctly.

Pi terminal
python3 - << 'EOF'
import tensorflow as tf
a = tf.constant([[1.0, 2.0],[3.0, 4.0]])
b = tf.constant([[5.0, 6.0],[7.0, 8.0]])
c = tf.matmul(a, b)
print("matmul result:\n", c.numpy())
EOF
Expected output
matmul result:
 [[19. 22.]
 [43. 50.]]

Test 3 — Hardware & CUDA Status

This confirms the Pi is correctly identified as a CPU-only device. Built with CUDA: False is the correct and expected result on Raspberry Pi — do not attempt to install CUDA on a Pi.

Pi terminal
python3 - << 'EOF'
import tensorflow as tf
print("TF version:", tf.__version__)
print("Built with CUDA:", tf.test.is_built_with_cuda())
print("Physical devices:", tf.config.list_physical_devices())
EOF
Expected output
TF version: 2.15.1
Built with CUDA: False
Physical devices: [PhysicalDevice(name='/physical_device:CPU:0', device_type='CPU')]
TensorFlow Import Warnings Are Normal
You will likely see several warning messages about oneDNN optimizations, AVX CPU instructions, and deprecated API usage. These are informational only and do not affect functionality. The only output that matters is what your print() statements produce.
07

Patch VESC.py — Firmware Version Compatibility

Fix a PyVESC crash caused by unrecognized VESC firmware version strings
Bug Fix

Modern VESC firmware (versions 5.x and later) returns a firmware version string during startup. The original PyVESC code tries to parse this string with int(version.split('.')[0]) to determine whether to use legacy field formats, but crashes with a TypeError if the version is None or returns an unexpected format. This crash happens before the car can move at all.

Step 1 — Locate the File

Pi terminal
# Find the exact path to VESC.py in your virtualenv
find ~/env -name "VESC.py" -path "*/pyvesc/*"
# Expected: /home/pi/env/lib/python3.11/site-packages/pyvesc/VESC/VESC.py

Step 2 — Open the File and Find the Bug

Open the file in the nano text editor. Once inside, press Ctrl+W to open the search prompt, type get_firmware_version, and press Enter. This will jump directly to the relevant section.

Pi terminal
nano ~/env/lib/python3.11/site-packages/pyvesc/VESC/VESC.py
# Once inside nano: press Ctrl+W, type "get_firmware_version", press Enter

Step 3 — Replace the Original Code Block

Find the following three-line block inside the __init__ method and delete it entirely:

ORIGINAL — find and delete these 3 lines
# check firmware version and set GetValue fields to old values if pre version 3.xx
version = self.get_firmware_version()
if int(version.split('.')[0]) < 3:
    GetValues.fields = pre_v3_33_fields

Then type in the following replacement block in its place:

PATCHED — type this in place of the deleted block
# check firmware version and set GetValue fields to old values if pre version 3.xx
version = self.get_firmware_version()
# PATCH: Safely handle None or unexpected version strings from modern VESC firmware
if not version or str(version).lower() == "none":
    version = "3.0"    # Default to v3 behaviour if the version string cannot be read
if int(str(version).split('.')[0]) < 3:
    GetValues.fields = pre_v3_33_fields
How to Save and Exit nano
When you are done editing, press Ctrl+O to write the file (nano will show the filename at the bottom — press Enter to confirm), then press Ctrl+X to exit.

Why This Patch Works

ScenarioOriginal BehaviourPatched Behaviour
VESC returns None as the versionCrashes immediately: AttributeError: 'NoneType' object has no attribute 'split'Sets version = "3.0" as a safe default and continues normally
VESC firmware v5.3Runs correctly (5 ≥ 3, no crash)Runs correctly (same logic, unchanged behaviour)
VESC firmware v2.x (legacy hardware)Sets the legacy field format correctlySame — still sets legacy fields correctly

Verification Test

Pi terminal
python3 -c "import pyvesc; import serial; print('pyvesc OK, pyserial', serial.__version__)"
# Expected: pyvesc OK, pyserial 3.5
08

Patch tub_v2.py — Image File Naming

Fix a parameter name mismatch that causes all training images to be silently overwritten
Bug Fix

The tub_v2.py file manages how training data (images paired with steering and throttle values) is saved to disk in "tub" folders. There is a critical mismatch between how the image file-naming function is called and what the function actually expects: the calling code passes ext='.png', but the function's signature defines the parameter as extension='.png'. Because Python does not match the keyword argument, the extension is silently ignored and all images are saved with no file extension at all, causing every new frame to overwrite the last one. You can drive 20 laps and end up with only a single image file on disk. There is no error message — the bug is completely silent.

Step 1 — Open the File

Pi terminal
nano ~/env/lib/python3.11/site-packages/donkeycar/parts/tub_v2.py
# Once inside nano: press Ctrl+W, type "_image_file_name", press Enter

Step 2 — Find the Incorrect Call and Fix It

Search for the line that calls _image_file_name with ext=. Change only the keyword argument name from ext= to extension=. Do not change anything else on the line.

ORIGINAL — find this line
name = Tub._image_file_name(self.manifest.current_index, key, ext='.png')
PATCHED — change only the keyword name as shown
name = Tub._image_file_name(self.manifest.current_index, key, extension='.png')
Save and Exit nano
Press Ctrl+OEnter to save, then Ctrl+X to exit.

Verification Test — Confirm Images Save Correctly

Pi terminal — run a very short test drive, then inspect the tub directory
cd ~/mycar
python3 manage.py drive    # Drive for ~5 seconds using the joystick, then press Ctrl+C

# Inspect the tub that was just created — you should see multiple numbered .png files
ls data/tub_*/images/ | head -5
# Expected: files named like:
#   0_cam-image_array_.png
#   1_cam-image_array_.png
#   2_cam-image_array_.png
# If you see only ONE file with no .png extension, the patch was not saved correctly.
09

Patch oak_d.py — Separate RGB and Depth Queues

Fix queue initialization that crashes when only one camera stream is enabled
Bug Fix

The Oak-D Lite provides two separate data streams: an RGB color image and a depth map. The default oak_d.py code always tries to open both streams simultaneously, regardless of which ones are enabled in myconfig.py. When only RGB is enabled (the typical configuration for lane following), the code still tries to open the depth queue, which was never configured in the pipeline — causing a RuntimeError on startup.

Step 1 — Open the File

Pi terminal
nano ~/env/lib/python3.11/site-packages/donkeycar/parts/oak_d.py
# Once inside nano: press Ctrl+W, type "enable_rgb or self.enable_depth", press Enter

Step 2 — Comment Out the Original Block

Find the block shown below. In nano, move your cursor to the start of each line and add a # character to comment it out. Comment out every line in this block — from the if statement down to the last assignment.

ORIGINAL block — add a # at the start of each of these lines
if self.enable_rgb or self.enable_depth:

    self.depth_queue: DataOutputQueue = self.oak_d_device.getOutputQueue(
        name="depth", maxSize=1, blocking=False
    )
    self.rgb_queue: DataOutputQueue = self.oak_d_device.getOutputQueue(
        "rgb", maxSize=1, blocking=False
    )

    depth_frame = self.get_frame(self.depth_queue)
    rgb_frame = self.get_frame(self.rgb_queue)

    self.depth_image = depth_frame
    self.color_image = rgb_frame

After commenting out the block above, it should look like this:

After commenting — the block should look like this
# if self.enable_rgb or self.enable_depth:
#
#     self.depth_queue: DataOutputQueue = self.oak_d_device.getOutputQueue(
#         name="depth", maxSize=1, blocking=False
#     )
#     self.rgb_queue: DataOutputQueue = self.oak_d_device.getOutputQueue(
#         "rgb", maxSize=1, blocking=False
#     )
#
#     depth_frame = self.get_frame(self.depth_queue)
#     rgb_frame = self.get_frame(self.rgb_queue)
#
#     self.depth_image = depth_frame
#     self.color_image = rgb_frame

Step 3 — Add the Patched Block Immediately Below

Directly below the commented-out block, add the following replacement code. This version checks each stream independently before trying to open its queue.

PATCHED block — add this immediately after the commented-out code above
# PATCH: Initialize RGB and depth queues independently based on myconfig.py settings.
# This prevents a RuntimeError when only one stream is enabled.
if self.enable_rgb:
    self.rgb_queue: DataOutputQueue = self.oak_d_device.getOutputQueue(
        "rgb", maxSize=1, blocking=False
    )
    rgb_frame = self.get_frame(self.rgb_queue)
    self.color_image = rgb_frame
else:
    self.color_image = None   # Explicitly set None to prevent AttributeError later

if self.enable_depth:
    self.depth_queue: DataOutputQueue = self.oak_d_device.getOutputQueue(
        name="depth", maxSize=1, blocking=False
    )
    depth_frame = self.get_frame(self.depth_queue)
    self.depth_image = depth_frame
else:
    self.depth_image = None   # Explicitly set None to prevent AttributeError later
Save and Exit nano
Press Ctrl+OEnter to save, then Ctrl+X to exit.

Verification Test

Pi terminal — test the patched oak_d.py in RGB-only mode
python3 - << 'EOF'
import sys
sys.path.insert(0, '/home/pi/env/lib/python3.11/site-packages')
from donkeycar.parts.oak_d import OakD

# Test RGB-only mode — this is the standard lane-following configuration
cam = OakD(enable_rgb=True, enable_depth=False)
print("OakD RGB-only init: OK")
cam.shutdown()
EOF
# Expected: OakD RGB-only init: OK
10

myconfig.py — Full Hardware Configuration

Configure all hardware parameters for the Oak-D camera, VESC, and joystick

~/mycar/myconfig.py is the single source of truth for all hardware parameters. Any setting you define here overrides the corresponding default in DonkeyCar's main config.py. The values below are tuned for the ECE/MAE 148 hardware kit: Oak-D Lite camera, VESC-based drivetrain, and Logitech F710 joystick. Read through the inline comments carefully — several values must be adjusted to match your specific setup.

Pi terminal — open myconfig.py for editing
nano ~/mycar/myconfig.py

Add or update the following parameters. Each comment explains why the value is set the way it is.

~/mycar/myconfig.py — complete hardware configuration block
# ── Image Transformations ────────────────────────────────────────────
TRANSFORMATIONS = ['RESIZE']
RESIZE_WIDTH = 160      # Small resolution keeps CPU inference fast on the Pi
RESIZE_HEIGHT = 120

# ── Drive Loop Timing ────────────────────────────────────────────────
DRIVE_LOOP_HZ = 20      # 20 Hz = one update every 50 ms; fast enough for smooth driving
MAX_LOOPS = None        # None = run until Ctrl+C; set a number to auto-stop after N loops

# ── Camera ───────────────────────────────────────────────────────────
CAMERA_TYPE = "OAKD"   # Use the Oak-D Lite via DepthAI
IMAGE_W = 160
IMAGE_H = 120
IMAGE_DEPTH = 3         # 3 = RGB color; set to 1 for grayscale
CAMERA_FRAMERATE = DRIVE_LOOP_HZ
CAMERA_VFLIP = False    # Set True if the camera image appears upside down
CAMERA_HFLIP = False    # Set True if the camera image appears mirrored left-right
CAMERA_INDEX = 0

# ── Drivetrain ───────────────────────────────────────────────────────
DRIVE_TRAIN_TYPE = "VESC"
VESC_MAX_SPEED_PERCENT = .5     # Start at 50% of max speed; increase gradually as the model improves
VESC_SERIAL_PORT = "/dev/ttyACM0"  # Change to /dev/ttyACM1 if the camera is using ACM0
VESC_HAS_SENSOR = True          # Hall effect encoder sensor is present on this drivetrain
VESC_START_HEARTBEAT = True     # Sends periodic keepalive packets to prevent VESC timeout
VESC_BAUDRATE = 115200
VESC_TIMEOUT = 0.05
VESC_STEERING_SCALE = 0.5   # Maps joystick range (-1 to 1) → VESC range (-0.5 to 0.5)
VESC_STEERING_OFFSET = 0.5  # Shifts the result so the final range is 0 to 1 (what VESC expects)

# ── Joystick ─────────────────────────────────────────────────────────
USE_JOYSTICK_AS_DEFAULT = True      # Removes the need to pass --js on every launch
JOYSTICK_MAX_THROTTLE = 0.5         # Limit top speed during data collection for safety
JOYSTICK_STEERING_SCALE = 1.0       # Set to -1.0 to reverse the steering direction
AUTO_RECORD_ON_THROTTLE = True      # Data records automatically while moving; stops when throttle is zero
CONTROLLER_TYPE = 'F710'            # ← CHANGE THIS to match your controller: ps3 | ps4 | xbox | F710
USE_NETWORKED_JS = False
NETWORK_JS_SERVER_IP = None
JOYSTICK_DEADZONE = 0.01            # Ignore stick inputs below 1% to eliminate drift at rest
JOYSTICK_THROTTLE_DIR = -1.0        # -1.0 flips the throttle axis so "up" on the stick = forward
JOYSTICK_DEVICE_FILE = "/dev/input/js0"

# ── Oak-D Specific ───────────────────────────────────────────────────
OAKD_RGB = True         # Capture RGB images for lane-following training
OAKD_DEPTH = False      # Depth stream not needed for basic lane following
OAKD_ID = None          # None = auto-detect (works when only one camera is connected)

# ── FPV Web Stream ────────────────────────────────────────────────────
USE_FPV = False         # Set True to stream the camera to http://<hostname>.local:8887
Save and Exit nano
Press Ctrl+OEnter to save, then Ctrl+X to exit. Every time you change a value in myconfig.py, you must restart manage.py for the change to take effect.

Key Parameter Reference

ParameterValueWhy
VESC_STEERING_SCALE0.5Reduces the maximum steering angle by half, preventing violent over-corrections that can flip the car.
VESC_STEERING_OFFSET0.5Shifts the -0.5 to 0.5 scaled range up to 0 to 1, which is the input range the VESC expects for steering.
JOYSTICK_THROTTLE_DIR-1.0The joystick "forward" axis is typically reported as a negative value. This flips it to match the car's physical forward direction.
VESC_MAX_SPEED_PERCENT0.5Start conservative at 50%. Increase only after confirming the model handles the track reliably.
AUTO_RECORD_ON_THROTTLETrueEliminates the need to manually toggle recording — data is saved automatically whenever the car is moving.
CONTROLLER_TYPEmatch your hardwareMust match your physical controller. Valid options are 'ps3', 'ps4', 'xbox', or 'F710'.
11

Driving & Data Collection

Manually drive the car to collect high-quality training data

The quality of your training data is the single biggest factor determining how well the autonomous model performs. Smooth, consistent driving with deliberate lap patterns gives the neural network a clear signal to learn from. Data collection takes place at the EBU2 Courtyard — the designated lab track for ECE/MAE 148 located outside the Engineering Building Unit 2 on the UCSD campus.

Step 1 — Launch the Car

Pi terminal — run from your ~/mycar directory
cd ~/mycar
python3 manage.py drive
Web Interface
After launching, open http://<your_hostname>.local:8887 in a browser on any device connected to the same network. You will see the live camera feed, the current driving mode, and real-time throttle and steering values. This is useful for diagnosing camera or joystick issues before you start collecting data.

Step 2 — Data Collection Best Practices

FactorBest PracticeWhy It Matters
Lap countCollect 20–25 smooth laps minimumMore data produces better generalization. Below 15 laps, models often overfit to a single lane position and fail anywhere else.
Lane positionStay consistently in the center of the laneThe model learns to replicate your exact behavior. Inconsistent positioning creates an ambiguous training signal.
Steering inputsMake smooth, gradual correctionsJerky or abrupt inputs teach the model to be jerky. It will reproduce your exact driving style on every curve.
SpeedMaintain a consistent, moderate paceHighly variable speeds make throttle prediction harder. The autopilot should drive at approximately the same speed you drove during training.
LightingCollect data when lighting is stableMoving shadows and changing brightness make the same track look like a different environment to the model. Train at a consistent time of day.

Step 3 — Verify Data Was Saved Correctly

Pi terminal — after driving
# List all tub directories (each represents one drive session)
ls ~/mycar/data/

# Count how many image frames are in the most recent tub
ls ~/mycar/data/tub_*/images/*.png | wc -l
# 20 laps at 20 Hz for roughly 2 minutes each ≈ 2,400 frames minimum

# Inspect the manifest to confirm steering/throttle values were recorded alongside the images
head -3 ~/mycar/data/tub_*/manifest.json
Delete Bad Laps Before Training
If you drive off the track, spin out, or make a very poor correction during a session, stop and delete that entire tub before training: rm -rf ~/mycar/data/tub_X_XX_XX. Bad training data is worse than no data — it explicitly teaches the model to repeat the same mistake.
12

Training the Model

Use TensorFlow to train a neural network on your collected driving data

DonkeyCar's train.py script takes your recorded driving data — pairs of camera images and their corresponding steering and throttle labels — and trains a small convolutional neural network to predict the correct steering and throttle values from a camera image alone. Training on the Pi itself takes approximately 20–45 minutes depending on your dataset size. If you have access to a laptop or desktop with a dedicated GPU, training there and copying the result back to the Pi is significantly faster.

Option A — Train on a Single Tub

Use this when you want to train on one specific recording session — for example, after deleting bad tubs or after collecting a focused session on a specific part of the track.

Pi terminal
cd ~/mycar
python3 train.py --tub data/tub_1_26_26 --model models/MyFirstModel.h5

Option B — Train on All Tubs (Recommended)

More data generally produces a more robust driver. Passing the entire data/ directory combines all tub sessions automatically.

Pi terminal
cd ~/mycar
python3 train.py --tub data --model models/MyModel_AllData.h5

Monitor Training Progress

During training, TensorFlow prints the loss and validation loss after each epoch. You want both numbers to decrease together over time. If the validation loss starts increasing while the training loss continues to drop, the model is overfitting — meaning it is memorizing your specific dataset rather than learning to generalize. Stop training early and collect more diverse data to address this.

Example training output — what to look for
Epoch  1/100  loss: 0.8432  val_loss: 0.7901   ← both decreasing: good
Epoch 10/100  loss: 0.2341  val_loss: 0.2189   ← converging toward the same value: good
Epoch 20/100  loss: 0.1102  val_loss: 0.1287   ← small gap between train and val: acceptable
Epoch 30/100  loss: 0.0423  val_loss: 0.2891   ← val_loss rising while train loss falls: overfitting

Verify the Model File Was Created

Pi terminal
ls -lh ~/mycar/models/
# Expected: MyModel_AllData.h5   (file size is typically 2–8 MB)

# Quick sanity check — load the model and confirm its expected input and output shapes
python3 - << 'EOF'
import tensorflow as tf
model = tf.keras.models.load_model('/home/pi/mycar/models/MyModel_AllData.h5')
print("Model loaded OK. Input shape:", model.input_shape)
print("Model loaded OK. Output shape:", model.output_shape)
EOF
# Expected: Input shape: (None, 120, 160, 3)   Output shape: (None, 2)
# The 2 outputs are steering and throttle.
Train on a Laptop for Speed
Transfer your data/ directory to a laptop with a discrete GPU, install DonkeyCar there, and run train.py with the same command. Training that takes 40 minutes on a Pi typically completes in 2–4 minutes on a modern GPU. Copy the resulting .h5 file back to ~/mycar/models/ on the Pi when done.
13

Autonomous Driving

Deploy your trained model and let the AI drive
Safety

With a trained model saved to disk, you launch manage.py with the --model flag. The model is loaded into TensorFlow on startup and runs inference at 20 Hz — producing a steering and throttle prediction for every camera frame. You always start in manual mode and engage the autopilot only when the car is in a safe position on the track.

Step 1 — Launch with the Model

Pi terminal
cd ~/mycar
python3 manage.py drive --model models/MyModel_AllData.h5

Step 2 — Engage Autopilot

MethodActionResult
JoystickPress the Start button (or whichever button is mapped to toggle_mode) to cycle through driving modesCycles: Manual → Full Auto → Angle Only → Manual
Web UIOpen http://<hostname>.local:8887 and select a mode from the Mode dropdownChoose Full Auto or Local Angle from the dropdown
Understanding the Driving Modes
Full Auto (Local Pilot) — The model controls both steering and throttle. The car drives entirely on its own.

Angle Only (Local Angle) — The model controls steering only. You control the throttle manually with the joystick. This is a useful intermediate mode: it lets you verify the model is steering correctly before trusting it with speed control as well.
Safety Warning — Always Be Ready to Override
A newly trained model will rarely drive perfectly on the first attempt. Keep your hand near the joystick at all times. Moving any analog stick immediately returns the car to manual control. Start with VESC_MAX_SPEED_PERCENT = 0.3 and only increase the speed limit after confirming the model handles all corners reliably at the slower speed.

Iterative Improvement Workflow

Problem ObservedFix
Car drifts consistently to one sideCollect 5–10 additional laps with deliberate corrections toward the failing side, then retrain with all data combined.
Car handles straight sections but fails on cornersDrive corner-focused laps, collecting 10+ passes through each difficult turn, then retrain.
Car oscillates or fishtailsCollect smoother driving data. Also try reducing VESC_STEERING_SCALE in myconfig.py by 0.1 increments.
Car drove well yesterday but fails todayLighting conditions likely changed. Collect fresh data in the current lighting and retrain.
14

Troubleshooting & Known Issues

Quick reference for the most common failure modes in the DonkeyCar stack

Startup & Installation

ErrorFix
ModuleNotFoundError: donkeycarThe virtual environment is not active. Run source ~/env/bin/activate and verify your prompt shows (env).
No module named 'depthai'DepthAI is not installed in the active environment. Run pip install depthai==2.21.2.0.
TypeError: 'NoneType' object has no attribute 'split'The VESC.py patch has not been applied. See Section 7.
manage.py: No such file or directoryYou are not in the correct directory. Run cd ~/mycar first.

Camera Issues

ProblemFix
RuntimeError: Failed to find deviceRun lsusb | grep 03e7. If nothing appears, try a different USB port or cable. Use a powered hub if the Pi cannot supply enough current.
Black or grey camera feed in the web interfaceInsufficient USB power. Use a powered hub. Also check that no other application has already opened the camera.
RuntimeError: depth queue not foundThe oak_d.py patch has not been applied. See Section 9.
Camera opens but all images are the same single colorDepthAI firmware version mismatch. Reinstall: pip install depthai==2.21.2.0.

VESC / Motor Issues

ProblemFix
/dev/ttyACM0: No such file or directoryThe VESC is not connected or not powered on. Check the USB cable and run dmesg | grep ttyACM for details.
Car steers but does not moveCheck that VESC_MAX_SPEED_PERCENT is not set to 0. Verify the VESC battery is charged and the ESC is armed.
Steering is reversed (left turns right and vice versa)Set JOYSTICK_STEERING_SCALE = -1.0 in myconfig.py.
Forward and backward are reversedFlip the sign of JOYSTICK_THROTTLE_DIR in myconfig.py (e.g., change -1.0 to 1.0).
VESC heartbeat timeout crash on startupSet VESC_START_HEARTBEAT = True in myconfig.py.
VESC receives commands but the car does not move at allConfirm that Write App Configuration was completed in VESCTool with the UART interface enabled. See the VESC Setup guide.

Training & Data

ProblemFix
Only one image in the tub despite driving many lapsThe tub_v2.py patch has not been applied. See Section 8. Delete the bad tub and recollect your data.
ValueError: Input 0 is incompatible with layer during inferenceThe image dimensions in myconfig.py do not match the dimensions used during training. Verify IMAGE_W = 160 and IMAGE_H = 120 are set correctly.
Training loss converges but the model drives poorlyData quality issue. Inspect the images in your tub directory manually. Delete any tubs with bad or inconsistent driving, then retrain.
OOM: Cannot allocate memory during trainingReduce the training batch size in train.py from its default of 128 down to 32. Alternatively, train on a laptop with more RAM.

UCSD Jacobs School of Engineering — ECE/MAE 148 Intro to Autonomous Vehicles