Autonomous driving stack for our RC crawler — a scaled-down testbed for the Driverless subteam's perception → planning → control pipeline, built by trainees as this season's onboarding challenge.
The goal: make the crawler drive itself around a mini track lined with 3D-printed cones, with no human input. Same architecture, same discipline as the full-size car, just smaller and faster to iterate on.
Tracked on the Driverless Challenge GitHub Project board — issues and PRs in this repo are linked there for cross-team visibility.
To save you from needing to install all dependencies yourself and in order to keep the development environment the same for everyone, you'll use a Docker container during your work on the project.
The dev environment (ROS2 Humble, rqt, turtlesim, Foxglove, uv) ships as a Docker container, so nobody has to install ROS on their own machine. The compose config is a base file plus one display override for your OS.
Requirements: Docker Engine (Linux) or Docker Desktop with WSL2 integration (Windows).
1. Pick your override by creating a gitignored .env file next to docker-compose.yml:
| Your setup | .env contents |
|---|---|
| Linux, X11 | COMPOSE_FILE=docker-compose.yml:compose.x11.yml |
| Linux, Wayland | COMPOSE_FILE=docker-compose.yml:compose.wayland.yml |
| Windows (WSL2 + WSLg) | COMPOSE_FILE=docker-compose.yml:compose.wsl.yml |
2. Allow the container to use your display (X11 only, once per login):
xhost +SI:localuser:root3. Start and enter the container:
docker compose up -d --build
docker compose exec mini-pampas bashWindows notes:
- Run everything from inside the WSL2 distro, with the repo cloned in the WSL filesystem (
~/...), not under/mnt/c(slow, and file watching breaks). - There's no host networking on Windows, so
foxglove_bridgeis exposed on port 8765. Openws://localhost:8765in Foxglove. Quick GUI check: inside the container, runros2 run turtlesim turtlesim_node. If the window appears, your display setup works.
Troubleshooting:
cannot open display/ Qt can't load the xcb plugin: X11 users likely skipped thexhoststep.- No window on Wayland: try the X11 override instead, which works through XWayland.
- Compose fails on
/dev/dri: you're using a Linux override on a machine without that device (e.g. WSL). Use the WSL override.
- Drivetrain: 4 wheels, front and back pairs originally each powered by an RC540 brushed motor with angular steering servos (servo rotates an arm connected to a bar that pushes/pulls the wheel pair sideways — bellcrank-style linkage). Only the front pair is used for steering and traction in this project; the rear pair is passive. Standard Ackermann-style kinematics apply.
- Steering servo: Absima S90MH — 9kg/25T JR metal gear servo. Standard 3-pin JR connector (signal / +V / GND — one cavity in the 4-slot housing is unused by design). Operating voltage 4.8–6.6V — needs its own regulated supply, not the raw battery. Positional servo, ~1000–2000µs PWM range centered at ~1500µs.
- Drive motor: RC540 brushed motor on the front pair. Confirm whether an ESC is already present between battery/Pi and motor before assuming direct PWM control — a bare 540 only has 2 power leads.
- Perception sensor: RPLIDAR A1M8 — 360° 2D line-scan LiDAR, 0.15–12m range, up to 8000 samples/sec, USB/UART. Driver: rplidar_ros (
ros2branch). - Compute: Raspberry Pi 4 Model B.
- Battery: Zippy Compact 3000mAh 4S1P 20C LiPo (~14.8V nominal, ~16.8V full charge).
- Mounting platform: flat deck on top of the chassis for sensor + compute + battery.
Power: the 4S pack directly suits only the drive motor via ESC. Both the Pi and the servo need their own regulated 5V/5–6V supply (UBEC/BEC) — never wire either to the raw battery.
Mounting: the LiDAR, Raspberry Pi, and battery must be securely fixed to the platform (standoffs/screws, straps, printed mounts, zip-tie anchors) — not just resting on it. Vibration and sharp turns will shift anything not properly mounted. Plan and test this early.
The track is marked with 3D-printed miniature cones, scaled to the crawler. Printing needs to happen early and in parallel with software work — perception tuning (size, shape, LiDAR reflectivity) depends on having real cones to test against, not mockups.
ROS2 nodes communicating over topics, visualized live via Foxglove Studio (foxglove_bridge) — the same pattern the full-size car's stack uses.
LiDAR (/scan) → Perception (/perception/cones) → Planning (/planning/target_point) → Control (/cmd/drive) → Motor + Servo
↑
Sensors (/sensors/wheel_speed)
.
├── LICENSE
├── pyproject.toml
├── README.md
└── src
├── mini_pampas_msgs # custom message definitions (ament_cmake)
│ ├── CMakeLists.txt
│ ├── package.xml
│ └── msg
│ ├── Cone.msg
│ ├── ConeArray.msg
│ ├── DriveCommand.msg
│ └── WheelSpeed.msg
└── mini_pampas
├── bringup # launch files, starts all nodes together
├── control # steering + throttle
├── __init__.py
├── perception # LiDAR → cone clusters
├── planning # cone clusters → target point
└── sensors # hall effect wheel speed, hardware interfacing
Messages live in their own ament_cmake package (required by ROS2 for interface generation), separate from the Python packages. Any package using them needs <depend>mini_pampas_msgs</depend> in its package.xml:
from mini_pampas_msgs.msg import Cone, ConeArray, DriveCommand, WheelSpeed| Topic | Type | Published by | Consumed by |
|---|---|---|---|
/scan |
sensor_msgs/LaserScan |
rplidar_ros driver |
perception |
/perception/cones |
cone positions (custom msg or geometry_msgs/PoseArray) |
perception |
planning |
/planning/target_point |
geometry_msgs/PointStamped |
planning |
control |
/cmd/drive |
steering angle + throttle (custom msg or ackermann_msgs/AckermannDrive) |
control |
motor/servo hardware interface |
/sensors/wheel_speed |
std_msgs/Float32 or custom (one per driven wheel) |
sensors |
control (optional closed-loop), telemetry |
LiDAR scan → cone cluster positions. A spatial problem only: filter scan range/FOV, cluster points (distance/angle-gap thresholding is sufficient), compute centroid + distance per cluster. Frame-to-frame smoothing is explicitly out of scope here — that's planning's job.
Verify early that the LiDAR's scan plane height actually intersects the cones given mounting height and cone size.
Refs: RPLIDAR A1M8 datasheet · rplidar_ros · sensor_msgs/LaserScan
Cone positions → a single target point. The temporal half of the filtering split: smooth detections frame-to-frame (moving average/exponential smoothing), find the midpoint between left/right cone pairs, handle missing-cone edge cases (hold last valid point briefly).
Refs: geometry_msgs/PointStamped · midpoint/pure-pursuit path following
Target point → steering + throttle. Front-only steering/traction means standard Ackermann/bicycle-model kinematics. Constant throttle is a fine baseline; closed-loop speed via hall sensors is a stretch goal.
Refs: Absima S90MH spec · RPi.GPIO/pigpio for PWM output
Power, wiring, and mounting. Confirm motor/servo/ESC wiring, spec a BEC/UBEC for Pi + servo, get the LiDAR enumerating on the Pi, securely mount LiDAR/Pi/battery, wire hall effect sensors (one per driven wheel) with interrupt-based speed estimation into /sensors/wheel_speed. Document the wiring — this is the reference for next season.
Refs: Raspberry Pi 4 Datasheet · UBEC/BEC basics
- ROS2 (
rclpy) for all nodes and topic interfaces. - uv for Python dependency management,
colcon buildfor the ROS2 build. - Foxglove Studio for live visualization of point clouds, cone detections, target point, and drive commands during development and demos.
Attention! When running Foxglove Studio inside the container, add the --no-sandbox flag, like so:
foxglove-studio --no-sandboxEach module is evaluated separately:
- Perception: clustering accuracy/robustness (false positives/negatives), performance on track edge cases
- Planning: path validity on curves, behavior with missing/noisy cone data
- Control: smoothness, respecting steering/speed limits, response to path changes
- Sensors/Integration: wiring safety/robustness, power stability under load, documentation quality
- Working code in your module, merged via the team's standard git/PR workflow.
- A presentation using the team's template: what you built, key decisions/trade-offs, challenges, what you'd improve.
- A recorded video presenting the project, archived for the team and next season's cohort.
- If the car completes a successful run: a live demo to the rest of the team.