Add lider ROS node and move robot control into its own directory

This commit is contained in:
2026-04-21 11:39:01 +00:00
parent 1d49e45240
commit f5ea157e21
28 changed files with 221 additions and 31 deletions
+137 -29
View File
@@ -149,6 +149,44 @@ The sensor will activate automatically when this command runs and deactivate whe
---
### 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
@@ -183,29 +221,51 @@ The [ansible/](ansible/) directory contains a playbook that handles the remainin
docker run --rm --privileged tonistiigi/binfmt --install arm64
```
### Build the image
### Build with Docker Compose (recommended)
The Raspberry Pi is `arm64`, so the image must be built for that platform. On an amd64 host use `docker buildx`:
Both images are defined in `docker-compose.yml`. Build them together:
```bash
docker build --platform linux/arm64 -t raspbot_v2:latest .
docker compose build
```
`--load` exports the built image into the local Docker image store so it can be deployed with `docker save`.
Or build a single service:
The build is split into two stages:
```bash
docker compose build robot
docker compose build lidar
```
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
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
Once the image is built, pipe it directly to the target over SSH — no intermediate file or registry needed:
Pipe both images 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
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).
@@ -214,19 +274,55 @@ Replace `matt` with the username configured in [ansible/inventory.ini](ansible/i
## 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:
### 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
```
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
### Overriding robot launch parameters
Launch arguments can be appended after the image name:
@@ -250,6 +346,14 @@ Available launch arguments:
| `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):
@@ -301,20 +405,24 @@ ros2 topic echo /joint_states
```
.
├── 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)
├── 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)
```
+42
View File
@@ -0,0 +1,42 @@
# Both containers share the host network so ROS 2 DDS discovery works without
# any extra multicast configuration. Each container is given access only to
# the specific device it needs rather than running in privileged mode.
services:
robot:
build:
context: .
dockerfile: robot/Dockerfile
platforms:
- linux/arm64
image: raspbot_v2:latest
network_mode: host
devices:
- /dev/i2c-1:/dev/i2c-1
environment:
- ROS_DOMAIN_ID=${ROS_DOMAIN_ID:-0}
restart: unless-stopped
lidar:
build:
context: .
dockerfile: lidar/Dockerfile
platforms:
- linux/arm64
image: raspbot_v2_lidar:latest
network_mode: host
devices:
- ${LIDAR_PORT:-/dev/ttyUSB0}:${LIDAR_PORT:-/dev/ttyUSB0}
environment:
- ROS_DOMAIN_ID=${ROS_DOMAIN_ID:-0}
command:
- ros2
- launch
- sllidar_ros2
- sllidar_a1_launch.py
- serial_port:=${LIDAR_PORT:-/dev/ttyUSB0}
- frame_id:=${LIDAR_FRAME_ID:-laser}
- serial_baudrate:=115200
- angle_compensate:=true
restart: unless-stopped
+40
View File
@@ -0,0 +1,40 @@
# syntax=docker/dockerfile:1
# ── Stage 1: build ────────────────────────────────────────────────────────────
FROM ros:kilted AS builder
SHELL ["/bin/bash", "-c"]
RUN apt-get update && apt-get install -y --no-install-recommends \
python3-colcon-common-extensions \
git \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /ws
RUN git clone --depth 1 https://github.com/slamtec/sllidar_ros2.git src/sllidar_ros2
RUN source /opt/ros/${ROS_DISTRO}/setup.bash && \
colcon build --packages-select sllidar_ros2
# ── Stage 2: runtime ──────────────────────────────────────────────────────────
FROM ros:kilted-ros-core
RUN apt-get update && apt-get install -y --no-install-recommends \
ros-${ROS_DISTRO}-rclcpp \
ros-${ROS_DISTRO}-sensor-msgs \
ros-${ROS_DISTRO}-std-srvs \
&& rm -rf /var/lib/apt/lists/*
COPY --from=builder /ws/install /ws/install
# Source both ROS base and the workspace overlay on every shell/exec
RUN echo "source /opt/ros/${ROS_DISTRO}/setup.bash" >> /etc/bash.bashrc && \
echo "source /ws/install/setup.bash" >> /etc/bash.bashrc
# Reuse the same entrypoint as the robot container (sources overlays, starts daemon)
COPY docker-entrypoint.sh /docker-entrypoint.sh
RUN chmod +x /docker-entrypoint.sh
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["ros2", "launch", "sllidar_ros2", "sllidar_a1_launch.py"]
+2 -2
View File
@@ -12,7 +12,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
WORKDIR /ws
# Copy the ROS package into the standard colcon src/ layout and build it
COPY src/raspbot_v2/ src/raspbot_v2/
COPY robot/src/raspbot_v2/ src/raspbot_v2/
RUN source /opt/ros/${ROS_DISTRO}/setup.bash && \
colcon build --packages-select raspbot_v2
@@ -30,7 +30,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
&& rm -rf /var/lib/apt/lists/*
# Install the Raspbot hardware library directly into site-packages
COPY raspbot_v2_interface/ /usr/local/lib/python3.12/dist-packages/raspbot_v2_interface/
COPY robot/raspbot_v2_interface/ /usr/local/lib/python3.12/dist-packages/raspbot_v2_interface/
# Bring across the built ROS overlay
COPY --from=builder /ws/install /ws/install