321 lines
13 KiB
Markdown
321 lines
13 KiB
Markdown
# 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 (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.
|
||
|
||
---
|
||
|
||
### 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.
|
||
|
||
---
|
||
|
||
## 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 raspbot_v2: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 raspbot_v2: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 \
|
||
raspbot_v2: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 \
|
||
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) |
|
||
|
||
### 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/
|
||
│ └── raspbot_v2/
|
||
│ ├── package.xml # ROS package manifest
|
||
│ ├── setup.py # ament_python build definition
|
||
│ ├── launch/
|
||
│ │ └── robot.launch.py # Starts both 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)
|
||
```
|