# 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](https://www.raspberrypi.com/software/) 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/](ansible/) directory contains a playbook that handles the remaining setup (enabling SPI, installing Docker). See [ansible/README.md](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: ```bash 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`: ```bash 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: 1. **builder** — installs the Raspbot hardware library, then compiles the ROS package with `colcon` 2. **runtime** — copies only the colcon install overlay and hardware library into a clean `ros:kilted` base; 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: ```bash docker save my_robot:latest | ssh matt@raspbot-v2.local docker load ``` Replace `matt` with the username configured in [ansible/inventory.ini](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: ```bash 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: ```bash 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): ```bash # 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°: ```bash 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: ```bash # Pan only ros2 topic pub --once /joint_command sensor_msgs/msg/JointState \ "{name: ['pan'], position: [0.0]}" ``` ### Verifying telemetry ```bash # 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) ```