diff --git a/README.md b/README.md index 91d95a0..f6d4189 100644 --- a/README.md +++ b/README.md @@ -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.15–12 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) ``` diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..0094da5 --- /dev/null +++ b/docker-compose.yml @@ -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 diff --git a/lidar/Dockerfile b/lidar/Dockerfile new file mode 100644 index 0000000..cb1ad42 --- /dev/null +++ b/lidar/Dockerfile @@ -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"] diff --git a/Dockerfile b/robot/Dockerfile similarity index 92% rename from Dockerfile rename to robot/Dockerfile index 369712c..48efa03 100644 --- a/Dockerfile +++ b/robot/Dockerfile @@ -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 diff --git a/raspbot_v2_interface/README.md b/robot/raspbot_v2_interface/README.md similarity index 100% rename from raspbot_v2_interface/README.md rename to robot/raspbot_v2_interface/README.md diff --git a/raspbot_v2_interface/Raspbot_Lib/.ipynb_checkpoints/Raspbot_Lib-checkpoint.py b/robot/raspbot_v2_interface/Raspbot_Lib/.ipynb_checkpoints/Raspbot_Lib-checkpoint.py similarity index 100% rename from raspbot_v2_interface/Raspbot_Lib/.ipynb_checkpoints/Raspbot_Lib-checkpoint.py rename to robot/raspbot_v2_interface/Raspbot_Lib/.ipynb_checkpoints/Raspbot_Lib-checkpoint.py diff --git a/raspbot_v2_interface/Raspbot_Lib/Raspbot_Lib.py b/robot/raspbot_v2_interface/Raspbot_Lib/Raspbot_Lib.py similarity index 100% rename from raspbot_v2_interface/Raspbot_Lib/Raspbot_Lib.py rename to robot/raspbot_v2_interface/Raspbot_Lib/Raspbot_Lib.py diff --git a/raspbot_v2_interface/Raspbot_Lib/__init__.py b/robot/raspbot_v2_interface/Raspbot_Lib/__init__.py similarity index 100% rename from raspbot_v2_interface/Raspbot_Lib/__init__.py rename to robot/raspbot_v2_interface/Raspbot_Lib/__init__.py diff --git a/raspbot_v2_interface/Raspbot_Lib/__pycache__/Raspblock.cpython-311.pyc b/robot/raspbot_v2_interface/Raspbot_Lib/__pycache__/Raspblock.cpython-311.pyc similarity index 100% rename from raspbot_v2_interface/Raspbot_Lib/__pycache__/Raspblock.cpython-311.pyc rename to robot/raspbot_v2_interface/Raspbot_Lib/__pycache__/Raspblock.cpython-311.pyc diff --git a/raspbot_v2_interface/Raspbot_Lib/__pycache__/Raspblock_Lib.cpython-311.pyc b/robot/raspbot_v2_interface/Raspbot_Lib/__pycache__/Raspblock_Lib.cpython-311.pyc similarity index 100% rename from raspbot_v2_interface/Raspbot_Lib/__pycache__/Raspblock_Lib.cpython-311.pyc rename to robot/raspbot_v2_interface/Raspbot_Lib/__pycache__/Raspblock_Lib.cpython-311.pyc diff --git a/raspbot_v2_interface/Raspbot_Lib/__pycache__/Raspbot_Lib.cpython-311.pyc b/robot/raspbot_v2_interface/Raspbot_Lib/__pycache__/Raspbot_Lib.cpython-311.pyc similarity index 100% rename from raspbot_v2_interface/Raspbot_Lib/__pycache__/Raspbot_Lib.cpython-311.pyc rename to robot/raspbot_v2_interface/Raspbot_Lib/__pycache__/Raspbot_Lib.cpython-311.pyc diff --git a/raspbot_v2_interface/Raspbot_Lib/__pycache__/__init__.cpython-311.pyc b/robot/raspbot_v2_interface/Raspbot_Lib/__pycache__/__init__.cpython-311.pyc similarity index 100% rename from raspbot_v2_interface/Raspbot_Lib/__pycache__/__init__.cpython-311.pyc rename to robot/raspbot_v2_interface/Raspbot_Lib/__pycache__/__init__.cpython-311.pyc diff --git a/raspbot_v2_interface/__init__.py b/robot/raspbot_v2_interface/__init__.py similarity index 100% rename from raspbot_v2_interface/__init__.py rename to robot/raspbot_v2_interface/__init__.py diff --git a/raspbot_v2_interface/build/lib/Raspbot_Lib/Raspbot_Lib.py b/robot/raspbot_v2_interface/build/lib/Raspbot_Lib/Raspbot_Lib.py similarity index 100% rename from raspbot_v2_interface/build/lib/Raspbot_Lib/Raspbot_Lib.py rename to robot/raspbot_v2_interface/build/lib/Raspbot_Lib/Raspbot_Lib.py diff --git a/raspbot_v2_interface/build/lib/Raspbot_Lib/__init__.py b/robot/raspbot_v2_interface/build/lib/Raspbot_Lib/__init__.py similarity index 100% rename from raspbot_v2_interface/build/lib/Raspbot_Lib/__init__.py rename to robot/raspbot_v2_interface/build/lib/Raspbot_Lib/__init__.py diff --git a/raspbot_v2_interface/dist/Raspblock_Lib-0.0.2-py3.11.egg b/robot/raspbot_v2_interface/dist/Raspblock_Lib-0.0.2-py3.11.egg similarity index 100% rename from raspbot_v2_interface/dist/Raspblock_Lib-0.0.2-py3.11.egg rename to robot/raspbot_v2_interface/dist/Raspblock_Lib-0.0.2-py3.11.egg diff --git a/raspbot_v2_interface/dist/Raspbot_Lib-0.0.2-py3.11.egg b/robot/raspbot_v2_interface/dist/Raspbot_Lib-0.0.2-py3.11.egg similarity index 100% rename from raspbot_v2_interface/dist/Raspbot_Lib-0.0.2-py3.11.egg rename to robot/raspbot_v2_interface/dist/Raspbot_Lib-0.0.2-py3.11.egg diff --git a/raspbot_v2_interface/motor_controller.py b/robot/raspbot_v2_interface/motor_controller.py similarity index 100% rename from raspbot_v2_interface/motor_controller.py rename to robot/raspbot_v2_interface/motor_controller.py diff --git a/raspbot_v2_interface/setup.py b/robot/raspbot_v2_interface/setup.py similarity index 100% rename from raspbot_v2_interface/setup.py rename to robot/raspbot_v2_interface/setup.py diff --git a/src/raspbot_v2/launch/robot.launch.py b/robot/src/raspbot_v2/launch/robot.launch.py similarity index 100% rename from src/raspbot_v2/launch/robot.launch.py rename to robot/src/raspbot_v2/launch/robot.launch.py diff --git a/src/raspbot_v2/package.xml b/robot/src/raspbot_v2/package.xml similarity index 100% rename from src/raspbot_v2/package.xml rename to robot/src/raspbot_v2/package.xml diff --git a/src/raspbot_v2/raspbot_v2/__init__.py b/robot/src/raspbot_v2/raspbot_v2/__init__.py similarity index 100% rename from src/raspbot_v2/raspbot_v2/__init__.py rename to robot/src/raspbot_v2/raspbot_v2/__init__.py diff --git a/src/raspbot_v2/raspbot_v2/camera_orientation_node.py b/robot/src/raspbot_v2/raspbot_v2/camera_orientation_node.py similarity index 100% rename from src/raspbot_v2/raspbot_v2/camera_orientation_node.py rename to robot/src/raspbot_v2/raspbot_v2/camera_orientation_node.py diff --git a/src/raspbot_v2/raspbot_v2/motor_controller_node.py b/robot/src/raspbot_v2/raspbot_v2/motor_controller_node.py similarity index 100% rename from src/raspbot_v2/raspbot_v2/motor_controller_node.py rename to robot/src/raspbot_v2/raspbot_v2/motor_controller_node.py diff --git a/src/raspbot_v2/raspbot_v2/ultrasonic_node.py b/robot/src/raspbot_v2/raspbot_v2/ultrasonic_node.py similarity index 100% rename from src/raspbot_v2/raspbot_v2/ultrasonic_node.py rename to robot/src/raspbot_v2/raspbot_v2/ultrasonic_node.py diff --git a/src/raspbot_v2/resource/raspbot_v2 b/robot/src/raspbot_v2/resource/raspbot_v2 similarity index 100% rename from src/raspbot_v2/resource/raspbot_v2 rename to robot/src/raspbot_v2/resource/raspbot_v2 diff --git a/src/raspbot_v2/setup.cfg b/robot/src/raspbot_v2/setup.cfg similarity index 100% rename from src/raspbot_v2/setup.cfg rename to robot/src/raspbot_v2/setup.cfg diff --git a/src/raspbot_v2/setup.py b/robot/src/raspbot_v2/setup.py similarity index 100% rename from src/raspbot_v2/setup.py rename to robot/src/raspbot_v2/setup.py