The sequence below is the one that actually worked on a Pi 5 + iRobot Create 2 + Intel RealSense D455 with a Mac M2 Air on the other end. It runs Gates G0–G4 end-to-end. Each gate has a stricter procedure in gates.md; this page is the path of least resistance.
For deeper context: master-plan.md (full design), architecture.md (topic/frame contract), hardware-bringup.md (wiring + Pi OS), troubleshooting.md (what to check when something is off), runbook.md (mapping + nav + RMF startup).
- Pi 5 (4 GB or 8 GB), microSD ≥ 32 GB, active cooler (passive throttles).
- iRobot Create 2 + Mini-DIN 7 → USB-serial cable.
- Intel RealSense D455 + a short USB3 cable.
- USB-C PD power bank (≥ 27 W) for the Pi — never power the Pi from the Create 2 Vpwr serial pins (200 mA fuse will brown out the Pi).
- A workstation (M2 Air, Linux laptop, whatever) for RViz + dev.
Flash Ubuntu Server 24.04 LTS (arm64) via Raspberry Pi Imager, set hostname quickbot, enable SSH, set Wi-Fi. First boot:
sudo apt update && sudo apt full-upgrade -y
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER && newgrp docker
sudo apt install -y docker-compose-plugin git
sudo usermod -aG dialout $USER # so /dev/ttyUSB0 is reachable without rootLog out and back in for the group changes to take effect.
git clone git@github.com:TafyLabs/quickbot.git ~/quickbot
cd ~/quickbot
docker build -f docker/kilted-base.Dockerfile -t quickbot:kilted-base .
docker compose build robotIf you've already built quickbot:kilted-base on another arm64 machine, you can docker save -o kilted-base.tar quickbot:kilted-base, scp it to the Pi, and docker load -i kilted-base.tar to skip the slow ROS-deb step.
Plug it in:
Pi 5 USB-A 3.0 ── short USB3 cable ── D455 USB-C
Pi 5 USB-A 2.0 ── Create 2 USB-serial ── Create 2 Mini-DIN
USB-C PD bank ────────────────── Pi 5 USB-C power
Then:
./tools/g2_devices.shYou want /dev/ttyUSB0 visible (some USB-serial adapters land on /dev/ttyACM0 — if so, edit compose.yaml and the serial_port:= argument below) and the D455 listed in lsusb.
This is the lowest-level check. No ROS, just pyserial over the OI. Clear a 1 m × 1 m floor area; finger on the Create 2 power button.
docker compose run --rm robot bash -lc \
'cd /ws/src/create2_driver && python3 -m create2_driver.smoke --port /dev/ttyUSB0 --duration 1.0 --speed 100'The robot should crawl forward ~10 cm in 1 second and stop. The cd is required because the workspace hasn't been colcon build'd yet, so create2_driver is only on PYTHONPATH from its source directory.
docker compose run --rm robot bash -lc \
'source /opt/ros/$ROS_DISTRO/setup.bash && cd /ws && \
colcon build --symlink-install && source install/setup.bash && \
ros2 launch quickbot_bringup robot.launch.py serial_port:=/dev/ttyUSB0'--symlink-install means edits to ws/src/create2_driver/create2_driver/*.py take effect without rebuilding. Only changes to setup.py, setup.cfg, or package.xml need another colcon build.
You should see, among other lines:
[create2_driver_node]: create2_driver up on /dev/ttyUSB0 @ 115200, wheel_separation=0.235 m, poll_hz=20.0
Leave that terminal running. It's the driver.
ROS is only inside the container. The Pi host shell doesn't have ros2. Open another SSH session to the Pi and start a second container shell — host networking means it discovers the running driver via DDS:
docker compose -f ~/quickbot/compose.yaml run --rm robot bash -lc \
'source /opt/ros/$ROS_DISTRO/setup.bash && source /ws/install/setup.bash && bash'Inside:
ros2 topic list # /cmd_vel /odom /tf /tf_static /battery_state /diagnostics
ros2 topic hz /odom # ~20 Hz
ros2 topic echo --once /battery_state
ros2 node info /create2_driverIn the same second-shell:
ros2 run teleop_twist_keyboard teleop_twist_keyboard --ros-args -r cmd_vel:=/cmd_velPass criteria for G4:
- Robot drives via keyboard.
- Robot stops within ~0.5 s when you release the key (the
/cmd_veltimeout). /odomand/battery_statekeep publishing throughout.- No bump/cliff false-positives on flat ground.
Capture a phone video + a ros2 bag record of /cmd_vel /odom /tf /battery_state for the validation/gates.md row.
These bit during first bring-up and the fixes are in main now. Listed so you know what to look for if you fork.
[ERROR] [launch]: ... Unable to parse the value of parameter robot_description as yaml.
Kilted's launch system YAML-parses every parameter value. The xacro-generated robot_description is XML, so it needs an explicit wrap:
from launch_ros.parameter_descriptions import ParameterValue
parameters=[{
"robot_description": ParameterValue(
Command(["xacro ", LaunchConfiguration("urdf")]),
value_type=str,
),
}],See ws/src/quickbot_description/launch/description.launch.py.
package 'create2_driver' found at '/ws/install/create2_driver', but libexec
directory '/ws/install/create2_driver/lib/create2_driver' does not exist
ament_python packages need a setup.cfg next to setup.py that points script_dir and install_scripts at lib/<package>/. Without it, setuptools puts the console_scripts in bin/, which ros2 run doesn't search:
[develop]
script_dir=$base/lib/<package_name>
[install]
install_scripts=$base/lib/<package_name>All three ament_python packages in this repo ship a setup.cfg.
You're on the Pi host shell. ROS is only installed inside the container — see step 6.
- G5 — verify the TF tree (
ros2 run tf2_tools view_frames). - G6 — bring up the D455 +
depthimage_to_laserscan; confirm stable/scan. - G7 —
slam_toolbox+ teleop the room; save a map. - G8 —
nav2_bringup+ 5 RViz goals + cancel + lifecycle restart.
Procedures: gates.md. Operator commands: runbook.md.