my_robot
ROS 2 package for the Yahboom Raspbot V2 platform — differential-drive motor control and pan/tilt camera orientation.
Architecture
Both nodes share the same I²C bus. The Linux kernel serialises individual transactions, so they can run as separate processes without additional locking.
Motor controller
┌───────────────────────────────────┐
│ MotorControllerNode │
│ │
/cmd_vel ──────────>│ Twist → differential kinematics │
(geometry_msgs/Twist)│ left = linear − (angular × wb/2)│
│ right = linear + (angular × wb/2)│
│ │
/wheel_speeds ──────>│ Direct per-wheel override │
(Float32MultiArray │ [FL, FR, RL, RR] │
4 × float32) │ │
│ ▼ │
│ raspbot_v2_interface │
│ I²C bus 1, addr 0x2B │
│ ▼ │
│ /dev/i2c-1 ─────────> Motors │
│ │
/current_wheel_speeds│<─ telemetry @ 10 Hz │
(Float32MultiArray) │ [FL, FR, RL, RR] │
└───────────────────────────────────┘
Topics
| Topic | Direction | Type | Description |
|---|---|---|---|
/cmd_vel |
Subscribed | geometry_msgs/Twist |
Velocity command — linear.x (m/s) and angular.z (rad/s) |
/wheel_speeds |
Subscribed | std_msgs/Float32MultiArray |
Direct per-wheel speed override [FL, FR, RL, RR] in library units (0–255) |
/current_wheel_speeds |
Published | std_msgs/Float32MultiArray |
Current wheel speeds read from hardware, published at 10 Hz |
Parameters
| Parameter | Default | Description |
|---|---|---|
wheel_base |
0.3 |
Distance between left and right wheels in metres |
max_speed |
1.0 |
Maximum motor speed in library units |
Camera orientation controller
┌──────────────────────────────────────┐
│ CameraOrientationNode │
│ │
/joint_command ────────>│ JointState (names: pan, tilt) │
(sensor_msgs/ │ position in radians │
JointState) │ │
│ pan → servo 1 (0°–180°) │
│ tilt → servo 2 (0°–110°) │
│ │
│ ▼ │
│ raspbot_v2_interface │
│ I²C bus 1, addr 0x2B │
│ ▼ │
│ /dev/i2c-1 ──────> Pan/tilt servos │
│ │
/joint_states <────────│ current angles @ 10 Hz │
(sensor_msgs/ │ position in radians │
JointState) │ │
└──────────────────────────────────────┘
Topics
| Topic | Direction | Type | Description |
|---|---|---|---|
/joint_command |
Subscribed | sensor_msgs/JointState |
Commanded pan/tilt angles. Joint names "pan" and "tilt", positions in radians. Unknown joint names are ignored. |
/joint_states |
Published | sensor_msgs/JointState |
Current angles reflected from the last command, published at 10 Hz |
Parameters
| Parameter | Default | Description |
|---|---|---|
pan_servo_id |
1 |
Raspbot servo channel for pan |
tilt_servo_id |
2 |
Raspbot servo channel for tilt |
pan_min_deg |
0.0 |
Pan lower limit (degrees) |
pan_max_deg |
180.0 |
Pan upper limit (degrees) |
tilt_min_deg |
0.0 |
Tilt lower limit (degrees) |
tilt_max_deg |
110.0 |
Tilt upper limit (degrees) — hardware cap |
pan_center_deg |
90.0 |
Startup and shutdown park position for pan |
tilt_center_deg |
60.0 |
Startup and shutdown park position for tilt |
state_rate_hz |
10.0 |
~/joint_states publish rate |
Hardware interface
The node drives the pan and tilt servos over I²C bus 1 (device address 0x2B). The same /dev/i2c-1 device used by the motor controller is sufficient — no additional device node is required.
Setting up the robot
1. Flash Raspberry Pi OS
Use the Raspberry Pi Imager to write Raspberry Pi OS (64-bit, Lite recommended) to a microSD card.
Before writing, open the imager's Advanced options (⚙) and configure:
| Setting | Value |
|---|---|
| Hostname | raspbot-v2.local |
| SSH | Enabled |
| Username / Password | Your preferred credentials |
| Wi-Fi | Your network SSID and password (if not using Ethernet) |
Write the image, insert the card, and power on the Pi. Once it has booted and is reachable on the network (test with ping raspbot-v2.local), proceed to the next step.
2. Provision with Ansible
The ansible/ directory contains a playbook that handles the remaining setup (enabling SPI, installing Docker). See ansible/README.md for full instructions.
Building
Prerequisites
-
Docker (with BuildKit enabled)
-
For cross-compilation from an amd64 host, QEMU user-space emulation must be registered with the kernel. If you haven't done this before, run once:
docker run --rm --privileged tonistiigi/binfmt --install arm64
Build the image
The Raspberry Pi is arm64, so the image must be built for that platform. On an amd64 host use docker buildx:
docker build --platform linux/arm64 -t my_robot:latest .
--load exports the built image into the local Docker image store so it can be deployed with docker save.
The build is split into two stages:
- builder — installs the Raspbot hardware library, then compiles the ROS package with
colcon - runtime — copies only the colcon install overlay and hardware library into a clean
ros:kiltedbase; no build tools are included in the final image
Deploying
Once the image is built, pipe it directly to the target over SSH — no intermediate file or registry needed:
docker save my_robot:latest | ssh matt@raspbot-v2.local docker load
Replace matt with the username configured in ansible/inventory.ini.
Launching
The default CMD starts both nodes together via the launch file. The container needs access to the I²C bus — pass only that device rather than running privileged:
docker run --rm \
--network=host \
--device /dev/i2c-1 \
--env ROS_DOMAIN_ID=0 \
my_robot:latest
If your board exposes the controller on a different bus (check with ls /dev/i2c-* on the host), substitute the correct device node (e.g. --device /dev/i2c-0).
Overriding parameters at launch
Launch arguments can be appended after the image name:
docker run --rm \
--network=host \
--device /dev/i2c-1 \
--env ROS_DOMAIN_ID=0 \
my_robot:latest \
ros2 launch my_robot robot.launch.py \
wheel_base:=0.25 max_speed:=0.8 tilt_center_deg:=45.0
Available launch arguments:
| Argument | Default | Description |
|---|---|---|
wheel_base |
0.3 |
Distance between left and right wheels (m) |
max_speed |
1.0 |
Maximum motor speed in library units |
pan_center_deg |
90.0 |
Pan angle at startup and shutdown (degrees) |
tilt_center_deg |
60.0 |
Tilt angle at startup and shutdown (degrees) |
Sending velocity commands from the host
With the container running, publish from another terminal (requires ROS 2 on the host or a second container on the same network):
# Drive forward at 0.2 m/s
ros2 topic pub --once /cmd_vel geometry_msgs/msg/Twist \
"{linear: {x: 0.02}, angular: {z: 0.0}}"
# Turn on the spot
ros2 topic pub --once /cmd_vel geometry_msgs/msg/Twist \
"{linear: {x: 0.0}, angular: {z: 0.5}}"
# Stop
ros2 topic pub --once /cmd_vel geometry_msgs/msg/Twist \
"{linear: {x: 0.0}, angular: {z: 0.0}}"
Commanding the camera from the host
Pan to centre (90°) and tilt to 30°:
ros2 topic pub --once /joint_command sensor_msgs/msg/JointState \
"{name: ['pan', 'tilt'], position: [1.5708, 0.5236]}"
A single axis can be commanded by omitting the other joint name:
# Pan only
ros2 topic pub --once /joint_command sensor_msgs/msg/JointState \
"{name: ['pan'], position: [0.0]}"
Verifying telemetry
# Wheel speeds
ros2 topic echo /current_wheel_speeds
# Camera orientation
ros2 topic echo /joint_states
Project layout
.
├── Dockerfile # Two-stage production image
├── docker-entrypoint.sh # Sources ROS overlays before exec
├── src/
│ └── my_robot/
│ ├── package.xml # ROS package manifest
│ ├── setup.py # ament_python build definition
│ ├── launch/
│ │ └── robot.launch.py # Starts both nodes together
│ └── my_robot/
│ ├── __init__.py
│ ├── motor_controller_node.py # Differential-drive motor control
│ └── camera_orientation_node.py # Pan/tilt servo control
└── raspbot_v2_interface/ # Vendored Yahboom hardware library
└── Raspbot_Lib/
└── Raspbot_Lib.py # I²C driver (smbus, bus 1, addr 0x2B)