build-guides
Guide 2: Raspberry Pi Setup
Full software environment — Python venv, libraries, camera deep test, GPS service, project structure
Build Guide 2: Raspberry Pi Setup
Type: Build Guide
Now that every component checks out on the bench, we'll set up the Pi as a proper development environment. By the end of this guide you'll have a Pi that's ready to control hardware, capture images, read GPS, and talk to your web app.
What You Need
- Raspberry Pi 4 with Raspberry Pi OS already installed (from Guide 1)
- WiFi connection
- SSH access to the Pi
Step 1: System Updates and Essentials
SSH into the Pi and get everything current:
sudo apt update && sudo apt upgrade -y
Install the system packages we'll need throughout the build:
sudo apt install -y \
python3-pip python3-venv \
i2c-tools \
git \
libcamera-apps \
gpsd gpsd-clients
Step 2: Python Environment
Create a dedicated virtual environment for the rover software. This keeps our dependencies isolated from the system Python:
mkdir -p ~/rover
cd ~/rover
python3 -m venv venv
source venv/bin/activate
Install the libraries we'll use:
pip install \
adafruit-circuitpython-pca9685 \
adafruit-circuitpython-ads1x15 \
adafruit-circuitpython-motor \
picamera2 \
pynmea2 \
pyserial \
requests
| Library | Purpose |
|---|---|
adafruit-circuitpython-pca9685 | PWM driver control (steering & throttle) |
adafruit-circuitpython-ads1x15 | ADC for battery voltage monitoring |
adafruit-circuitpython-motor | Servo abstraction layer |
picamera2 | Camera capture (Pi Camera Module 3) |
pynmea2 | Parse GPS NMEA sentences |
pyserial | Serial port communication with GPS |
requests | HTTP calls to the Convex backend |
Add the venv activation to your .bashrc so it's always ready:
echo 'source ~/rover/venv/bin/activate' >> ~/.bashrc
Step 3: Enable Hardware Interfaces
Make sure everything is turned on:
sudo raspi-config
Enable all of these:
- Interface Options → I2C → Yes
- Interface Options → Serial Port → Login shell: No, Hardware: Yes
- Interface Options → Camera → Yes (if listed separately)
Reboot:
sudo reboot
Step 4: Camera Deep Test
Now let's go beyond the basic test from Guide 1. We want to make sure the camera is dialed in for the kind of images we'll actually capture — top-down shots of white cloth:
# ~/rover/test_camera.py
from picamera2 import Picamera2
import time
cam = Picamera2()
# Configure for still capture — high res
config = cam.create_still_configuration(
main={"size": (4608, 2592)}, # Full resolution
)
cam.configure(config)
cam.start()
# Let auto-exposure and auto-focus settle
time.sleep(2)
# Capture
cam.capture_file("/home/pi/test_full_res.jpg")
print("Captured test_full_res.jpg")
cam.stop()
python test_camera.py
Transfer the image to your computer and inspect it:
# From your computer:
scp pi@tickslayer.local:~/test_full_res.jpg .
What to look for:
- Is the image sharp? Auto-focus should handle this, but check.
- What's the field of view? Lay a piece of white cloth (or paper) at the distance you expect the camera to be mounted and capture again. Can you see the full width of the cloth?
- How's the exposure on white fabric? It shouldn't be blown out.
Step 5: GPS Service Setup
The gpsd daemon handles GPS communication so our Python code doesn't need to manage the serial port directly:
sudo nano /etc/default/gpsd
Set these values:
DEVICES="/dev/serial0"
GPSD_OPTIONS="-n"
START_DAEMON="true"
Restart the service:
sudo systemctl enable gpsd
sudo systemctl restart gpsd
Test it:
cgps -s
You should see a live display of GPS data. If you're indoors, the fix fields will be empty — that's expected. The important thing is that cgps connects and shows the interface.
Now test from Python:
# ~/rover/test_gps.py
import serial
import pynmea2
ser = serial.Serial('/dev/serial0', baudrate=9600, timeout=1)
while True:
line = ser.readline().decode('ascii', errors='replace').strip()
if line.startswith('$GPGGA') or line.startswith('$GPRMC'):
try:
msg = pynmea2.parse(line)
print(f"Lat: {msg.latitude}, Lon: {msg.longitude}, Fix: {msg.gps_qual}")
except pynmea2.ParseError:
pass
python test_gps.py
Pass: Latitude and longitude printing (take the Pi outside for a real fix).
Step 6: I2C Verification
Confirm both I2C devices are still talking:
i2cdetect -y 1
Expected output shows both 0x40 (PCA9685) and 0x48 (ADS1115).
Quick Python test for the ADC with voltage divider:
# ~/rover/test_adc.py
import board
import busio
import adafruit_ads1x15.ads1115 as ADS
from adafruit_ads1x15.analog_in import AnalogIn
i2c = busio.I2C(board.SCL, board.SDA)
ads = ADS.ADS1115(i2c)
chan = AnalogIn(ads, ADS.P0)
# Multiply by your divider ratio to get actual battery voltage
# For a typical 0-25V divider: ratio ≈ 5
DIVIDER_RATIO = 5.0
raw_v = chan.voltage
battery_v = raw_v * DIVIDER_RATIO
print(f"ADC reading: {raw_v:.3f}V")
print(f"Estimated battery: {battery_v:.1f}V")
Step 7: PWM Servo Test
Now we'll make the PCA9685 actually move something. If you have a spare servo (not the TRX-4's steering servo — we'll connect that later), plug it into channel 0:
# ~/rover/test_pwm.py
from adafruit_pca9685 import PCA9685
from adafruit_motor import servo
import board
import busio
import time
i2c = busio.I2C(board.SCL, board.SDA)
pca = PCA9685(i2c)
pca.frequency = 50 # Standard servo frequency
# Create a servo on channel 0
test_servo = servo.Servo(pca.channels[0])
# Sweep
print("Center...")
test_servo.angle = 90
time.sleep(1)
print("Left...")
test_servo.angle = 45
time.sleep(1)
print("Right...")
test_servo.angle = 135
time.sleep(1)
print("Center...")
test_servo.angle = 90
time.sleep(1)
pca.deinit()
print("Done.")
If you don't have a spare servo: Skip this — we'll test with the actual TRX-4 servo in Guide 4. The important thing is that I2C detection passed.
Note on PCA9685 power: The servo power rail (V+) on the PCA9685 is separate from VCC. For bench testing a single small servo, you can jumper V+ to a 5V source. For the actual TRX-4 servos, we'll use the vehicle's BEC (Battery Eliminator Circuit) — covered in Guide 4.
Step 8: Project Structure
Set up the directory structure for the rover code:
mkdir -p ~/rover/{config,logs,captures,routes}
| Directory | Purpose |
|---|---|
~/rover/config/ | Calibration values, servo limits, settings |
~/rover/logs/ | GPS logs, mission logs, error logs |
~/rover/captures/ | Captured images (before upload) |
~/rover/routes/ | Saved GPS routes for replay |
Create a config file for values we'll calibrate later:
# ~/rover/config/rover_config.py
# Steering servo
STEERING_CHANNEL = 0
STEERING_CENTER = 90 # Will calibrate in Guide 5
STEERING_LEFT_MAX = 50 # Will calibrate in Guide 5
STEERING_RIGHT_MAX = 130 # Will calibrate in Guide 5
# Throttle (ESC)
THROTTLE_CHANNEL = 1
THROTTLE_NEUTRAL = 90 # Will calibrate in Guide 5
THROTTLE_FORWARD_MIN = 95 # Will calibrate in Guide 5
THROTTLE_FORWARD_MAX = 110 # Keep low - we're dragging cloth, not racing
THROTTLE_REVERSE = 70 # Will calibrate in Guide 5
# Camera
CAPTURE_RESOLUTION = (4608, 2592)
CAPTURE_INTERVAL_METERS = 15 # Per entomological standard
# GPS
GPS_SERIAL_PORT = "/dev/serial0"
GPS_BAUD_RATE = 9600
# Battery
BATTERY_DIVIDER_RATIO = 5.0
BATTERY_LOW_THRESHOLD = 6.8 # Volts - triggers return-to-home
BATTERY_CRITICAL = 6.4 # Volts - immediate stop
# Network
CONVEX_UPLOAD_URL = "" # Set after Convex integration
Checklist
- System updated, all packages installed
- Python venv created with all libraries
- Camera captures full-resolution images
- GPS reads via
cgpsand Python script - Both I2C devices detected (
0x40,0x48) - ADC reads voltage through divider
- PWM board tested (servo sweep or just I2C detection)
- Project directories and config file created
The Pi is ready. Next up: Guide 3: Vehicle Prep & Mounting — time to get your hands on the TRX-4.
