Raspberry Pi System Prep
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.
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.
# 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
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.
sudo raspi-config1. 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.
# 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
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
| Problem | Cause | Fix |
|---|---|---|
E: Release file is not valid yet | Pi 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 file | I2C was not enabled or the kernel module did not load | Re-run sudo raspi-config → Interface Options → I2C → Enable, then reboot. |
| Filesystem still shows 4–6 GB after reboot | The Expand Filesystem step was skipped | Re-run sudo raspi-config → Advanced Options → Expand Filesystem, then reboot again. |
DonkeyCar Installation
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.
--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.# 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
.h5 files). Without these, the TensorFlow pip installation will either fail or produce a broken build.sudo apt install -y libcap-dev libhdf5-dev libhdf5-serial-dev
[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.pip install donkeycar[pi]-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.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]
~/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.donkey createcar --path ~/mycar
Verification Test — Confirm Installation
# 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/
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.Camera Setup — Oak-D Lite
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.
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.
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.# 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.
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.
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
DepthAI version: 2.21.2.0
Available devices: [DeviceInfo(OAK-D-LITE-FF)]
Opened device: OAK-D-LITE-FF
MXID: 18443010D10BF40F00
USB speed: UsbSpeed.HIGHUsbSpeed.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 / Symptom | Cause | Fix |
|---|---|---|
RuntimeError: Failed to find device | Camera not detected on USB | Run 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 group | Verify 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 fails | Wrong depthai version installed | Run 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 repeatedly | Insufficient USB power from the Pi's built-in ports | Use a powered USB 3.0 hub between the Pi and the camera. The Oak-D Lite draws up to ~900 mA at peak load. |
Motor Controller — VESC
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.
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.
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.
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.
# 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 ...
Joystick — Logitech F710
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
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.
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.
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.
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.TensorFlow Verification
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
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.
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())
EOFmatmul 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.
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())
EOFTF version: 2.15.1
Built with CUDA: False
Physical devices: [PhysicalDevice(name='/physical_device:CPU:0', device_type='CPU')]print() statements produce.Patch VESC.py — Firmware Version Compatibility
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
# 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.
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:
# 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_fieldsThen type in the following replacement block in its place:
# 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
Why This Patch Works
| Scenario | Original Behaviour | Patched Behaviour |
|---|---|---|
VESC returns None as the version | Crashes immediately: AttributeError: 'NoneType' object has no attribute 'split' | Sets version = "3.0" as a safe default and continues normally |
| VESC firmware v5.3 | Runs correctly (5 ≥ 3, no crash) | Runs correctly (same logic, unchanged behaviour) |
| VESC firmware v2.x (legacy hardware) | Sets the legacy field format correctly | Same — still sets legacy fields correctly |
Verification Test
python3 -c "import pyvesc; import serial; print('pyvesc OK, pyserial', serial.__version__)" # Expected: pyvesc OK, pyserial 3.5
Patch tub_v2.py — Image File Naming
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
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.
name = Tub._image_file_name(self.manifest.current_index, key, ext='.png')name = Tub._image_file_name(self.manifest.current_index, key, extension='.png')Verification Test — Confirm Images Save Correctly
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.
Patch oak_d.py — Separate RGB and Depth Queues
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
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.
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_frameAfter commenting out the block above, it 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.
# 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
Verification Test
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
myconfig.py — Full Hardware Configuration
~/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.
nano ~/mycar/myconfig.pyAdd or update the following parameters. Each comment explains why the value is set the way it is.
# ── 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
manage.py for the change to take effect.Key Parameter Reference
| Parameter | Value | Why |
|---|---|---|
VESC_STEERING_SCALE | 0.5 | Reduces the maximum steering angle by half, preventing violent over-corrections that can flip the car. |
VESC_STEERING_OFFSET | 0.5 | Shifts 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.0 | The joystick "forward" axis is typically reported as a negative value. This flips it to match the car's physical forward direction. |
VESC_MAX_SPEED_PERCENT | 0.5 | Start conservative at 50%. Increase only after confirming the model handles the track reliably. |
AUTO_RECORD_ON_THROTTLE | True | Eliminates the need to manually toggle recording — data is saved automatically whenever the car is moving. |
CONTROLLER_TYPE | match your hardware | Must match your physical controller. Valid options are 'ps3', 'ps4', 'xbox', or 'F710'. |
Driving & Data Collection
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
cd ~/mycar python3 manage.py drive
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
| Factor | Best Practice | Why It Matters |
|---|---|---|
| Lap count | Collect 20–25 smooth laps minimum | More data produces better generalization. Below 15 laps, models often overfit to a single lane position and fail anywhere else. |
| Lane position | Stay consistently in the center of the lane | The model learns to replicate your exact behavior. Inconsistent positioning creates an ambiguous training signal. |
| Steering inputs | Make smooth, gradual corrections | Jerky or abrupt inputs teach the model to be jerky. It will reproduce your exact driving style on every curve. |
| Speed | Maintain a consistent, moderate pace | Highly variable speeds make throttle prediction harder. The autopilot should drive at approximately the same speed you drove during training. |
| Lighting | Collect data when lighting is stable | Moving 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
# 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
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.Training the Model
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.
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.
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.
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
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.
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.Autonomous Driving
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
cd ~/mycar python3 manage.py drive --model models/MyModel_AllData.h5
Step 2 — Engage Autopilot
| Method | Action | Result |
|---|---|---|
| Joystick | Press the Start button (or whichever button is mapped to toggle_mode) to cycle through driving modes | Cycles: Manual → Full Auto → Angle Only → Manual |
| Web UI | Open http://<hostname>.local:8887 and select a mode from the Mode dropdown | Choose Full Auto or Local Angle from the dropdown |
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.
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 Observed | Fix |
|---|---|
| Car drifts consistently to one side | Collect 5–10 additional laps with deliberate corrections toward the failing side, then retrain with all data combined. |
| Car handles straight sections but fails on corners | Drive corner-focused laps, collecting 10+ passes through each difficult turn, then retrain. |
| Car oscillates or fishtails | Collect smoother driving data. Also try reducing VESC_STEERING_SCALE in myconfig.py by 0.1 increments. |
| Car drove well yesterday but fails today | Lighting conditions likely changed. Collect fresh data in the current lighting and retrain. |
Troubleshooting & Known Issues
Startup & Installation
| Error | Fix |
|---|---|
ModuleNotFoundError: donkeycar | The 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 directory | You are not in the correct directory. Run cd ~/mycar first. |
Camera Issues
| Problem | Fix |
|---|---|
RuntimeError: Failed to find device | Run 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 interface | Insufficient USB power. Use a powered hub. Also check that no other application has already opened the camera. |
RuntimeError: depth queue not found | The oak_d.py patch has not been applied. See Section 9. |
| Camera opens but all images are the same single color | DepthAI firmware version mismatch. Reinstall: pip install depthai==2.21.2.0. |
VESC / Motor Issues
| Problem | Fix |
|---|---|
/dev/ttyACM0: No such file or directory | The VESC is not connected or not powered on. Check the USB cable and run dmesg | grep ttyACM for details. |
| Car steers but does not move | Check 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 reversed | Flip the sign of JOYSTICK_THROTTLE_DIR in myconfig.py (e.g., change -1.0 to 1.0). |
| VESC heartbeat timeout crash on startup | Set VESC_START_HEARTBEAT = True in myconfig.py. |
| VESC receives commands but the car does not move at all | Confirm that Write App Configuration was completed in VESCTool with the UART interface enabled. See the VESC Setup guide. |
Training & Data
| Problem | Fix |
|---|---|
| Only one image in the tub despite driving many laps | The 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 inference | The 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 poorly | Data 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 training | Reduce 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