Files
ros-raspbot-v2/README.md
T

429 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# raspbot_v2
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 (0255) |
| `/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.
---
### Ultrasonic range sensor
```
┌──────────────────────────────────────┐
│ UltrasonicNode │
│ │
│ Sensor off when no subscribers │
│ Sensor on when subscribers > 0 │
│ 1 s warm-up after power-on │
│ │
│ ▼ │
│ raspbot_v2_interface │
│ I²C bus 1, addr 0x2B │
│ ▼ │
│ /dev/i2c-1 ──────> HC-SR04 sensor │
│ │
/ultrasonic/range <────│ Range @ configurable rate │
(sensor_msgs/Range) │ radiation_type = ULTRASOUND │
│ range in metres (REP-117) │
└──────────────────────────────────────┘
```
#### Topics
| Topic | Direction | Type | Description |
|---|---|---|---|
| `/ultrasonic/range` | Published | `sensor_msgs/Range` | Distance in metres. `+inf` when beyond max range, `-inf` when closer than min range (REP-117). Only published while subscribers are connected. |
#### Parameters
| Parameter | Default | Description |
|---|---|---|
| `publish_rate_hz` | `10.0` | Sensor poll and publish rate |
| `frame_id` | `'ultrasonic'` | `header.frame_id` on published messages |
| `min_range_m` | `0.02` | Minimum valid range in metres |
| `max_range_m` | `4.0` | Maximum valid range in metres |
| `field_of_view` | `0.2618` | Sensor cone width in radians (~15°) |
| `warmup_s` | `1.0` | Seconds to wait after powering the sensor on before publishing |
#### Verifying range readings
```bash
ros2 topic echo /ultrasonic/range
```
The sensor will activate automatically when this command runs and deactivate when it is stopped.
---
### RPLIDAR A1
Runs in a separate container built from [lidar/Dockerfile](lidar/Dockerfile).
```
┌──────────────────────────────────────┐
│ sllidar_ros2 (rplidar_node) │
│ │
│ serial 115200 baud │
│ angle_compensate = true │
│ scan_mode = Sensitivity │
│ ▼ │
│ /dev/ttyUSB0 ──────> RPLIDAR A1 │
│ │
/scan <───────│ LaserScan @ ~10 Hz │
(sensor_msgs/ │ 360° scan, range 0.1512 m │
LaserScan) │ │
└──────────────────────────────────────┘
```
#### Topics
| Topic | Direction | Type | Description |
|---|---|---|---|
| `/scan` | Published | `sensor_msgs/LaserScan` | 360° laser scan in the `laser` frame |
#### Configuration
The LIDAR container is configured via environment variables in `.env` or `docker-compose.yml`. See the [Launching](#launching) section for details.
#### Verifying LIDAR data
```bash
ros2 topic echo /scan
```
---
## 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 with Docker Compose (recommended)
Both images are defined in `docker-compose.yml`. Build them together:
```bash
docker compose build
```
Or build a single service:
```bash
docker compose build robot
docker compose build lidar
```
The builds are split into two stages each:
1. **builder** — compiles the ROS package(s) with `colcon`; the lidar builder also clones `sllidar_ros2` from GitHub
2. **runtime** — copies only the install overlay into a clean `ros:kilted-ros-core` base; no build tools in the final image
### Build images individually
```bash
# Robot controller
docker build --platform linux/arm64 -f robot/Dockerfile -t raspbot_v2:latest .
# LIDAR
docker build --platform linux/arm64 -f lidar/Dockerfile -t raspbot_v2_lidar:latest .
```
---
## Deploying
Pipe both images directly to the target over SSH — no intermediate file or registry needed:
```bash
docker save raspbot_v2:latest raspbot_v2_lidar:latest \
| ssh matt@raspbot-v2.local docker load
```
Then copy the compose file to the target:
```bash
scp docker-compose.yml matt@raspbot-v2.local:~/
```
Replace `matt` with the username configured in [ansible/inventory.ini](ansible/inventory.ini).
---
## Launching
### Start everything with Docker Compose (recommended)
```bash
docker compose up
```
This starts both the robot controller and LIDAR containers. Logs from both are interleaved in the terminal, each line prefixed with the service name. To run in the background:
```bash
docker compose up -d
docker compose logs -f # follow logs
docker compose down # stop and remove containers
```
### Environment variables
Create a `.env` file in the same directory as `docker-compose.yml` to override defaults:
```bash
ROS_DOMAIN_ID=0
LIDAR_PORT=/dev/ttyUSB0
LIDAR_FRAME_ID=laser
```
| Variable | Default | Description |
|---|---|---|
| `ROS_DOMAIN_ID` | `0` | ROS 2 domain — must match on all nodes |
| `LIDAR_PORT` | `/dev/ttyUSB0` | Host device node for the RPLIDAR |
| `LIDAR_FRAME_ID` | `laser` | `frame_id` in published `LaserScan` messages |
### Run containers individually
```bash
# Robot controller
docker run --rm \
--network=host \
--device /dev/i2c-1 \
--env ROS_DOMAIN_ID=0 \
raspbot_v2:latest
# LIDAR
docker run --rm \
--network=host \
--device /dev/ttyUSB0 \
--env ROS_DOMAIN_ID=0 \
raspbot_v2_lidar:latest
```
### Overriding robot launch parameters
Launch arguments can be appended after the image name:
```bash
docker run --rm \
--network=host \
--device /dev/i2c-1 \
--env ROS_DOMAIN_ID=0 \
raspbot_v2:latest \
ros2 launch raspbot_v2 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) |
| `ultrasonic_rate_hz` | `10.0` | Ultrasonic sensor publish rate (Hz) |
### LIDAR device permissions
The RPLIDAR connects as a USB serial device. If the user running Docker is not in the `dialout` group, add them and log back in:
```bash
sudo usermod -aG dialout $USER
```
### 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
```
.
├── docker-compose.yml # Launches robot and lidar containers together
├── docker-entrypoint.sh # Sources ROS overlays before exec (shared by both images)
├── robot/
│ ├── Dockerfile # Robot controller image (two-stage)
│ ├── src/
│ │ └── raspbot_v2/
│ │ ├── package.xml # ROS package manifest
│ │ ├── setup.py # ament_python build definition
│ │ ├── launch/
│ │ │ └── robot.launch.py # Starts all robot nodes together
│ │ └── raspbot_v2/
│ │ ├── __init__.py
│ │ ├── motor_controller_node.py # Differential-drive motor control
│ │ ├── camera_orientation_node.py # Pan/tilt servo control
│ │ └── ultrasonic_node.py # HC-SR04 range sensor
│ └── raspbot_v2_interface/ # Vendored Yahboom hardware library
│ └── Raspbot_Lib/
│ └── Raspbot_Lib.py # I²C driver (smbus, bus 1, addr 0x2B)
└── lidar/
└── Dockerfile # RPLIDAR A1 image (two-stage, clones sllidar_ros2)
```