diff --git a/.vscode/settings.json b/.vscode/settings.json
index c578b33..fba2659 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,5 +1,9 @@
{
"python.analysis.extraPaths": [
"/opt/ros/kilted/lib/python3.12/site-packages"
+ ],
+ "ROS2.distro": "kilted",
+ "python.autoComplete.extraPaths": [
+ "/opt/ros/kilted/lib/python3.12/site-packages"
]
}
diff --git a/CMakeLists.txt b/CMakeLists.txt
new file mode 100644
index 0000000..6da01b4
--- /dev/null
+++ b/CMakeLists.txt
@@ -0,0 +1,9 @@
+cmake_minimum_required(VERSION 3.8)
+project(my_robot)
+
+find_package(ament_cmake REQUIRED)
+find_package(ament_cmake_python REQUIRED)
+
+ament_python_install_package(${PROJECT_NAME})
+
+ament_package()
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..51cbf14
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,48 @@
+# syntax=docker/dockerfile:1
+
+# ── Stage 1: build ────────────────────────────────────────────────────────────
+FROM ros:kilted AS builder
+
+RUN apt-get update && apt-get install -y --no-install-recommends \
+ python3-colcon-common-extensions \
+ python3-pip \
+ ros-${ROS_DISTRO}-ament-cmake-python \
+ && rm -rf /var/lib/apt/lists/*
+
+WORKDIR /ws
+
+# Install the vendored Raspbot hardware library
+COPY raspbot_v2_interface/ raspbot_v2_interface/
+# PEP 668: newer Debian/Ubuntu marks the system Python as externally managed and blocks pip
+# by default. --break-system-packages overrides this; safe here as the container is isolated.
+RUN pip3 install --no-cache-dir --break-system-packages ./raspbot_v2_interface
+
+# Copy the ROS package into the standard colcon src/ layout and build it
+COPY package.xml setup.py setup.cfg CMakeLists.txt src/my_robot/
+COPY my_robot/ src/my_robot/my_robot/
+
+RUN . /opt/ros/${ROS_DISTRO}/setup.sh && \
+ colcon build --packages-select my_robot
+
+# ── Stage 2: runtime ──────────────────────────────────────────────────────────
+FROM ros:kilted-ros-core
+
+RUN apt-get update && apt-get install -y --no-install-recommends \
+ ros-${ROS_DISTRO}-rclpy \
+ ros-${ROS_DISTRO}-geometry-msgs \
+ ros-${ROS_DISTRO}-std-msgs \
+ && rm -rf /var/lib/apt/lists/*
+
+# Bring across the installed Raspbot library and the built ROS overlay
+COPY --from=builder /usr/local/lib /usr/local/lib
+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
+
+COPY docker-entrypoint.sh /docker-entrypoint.sh
+RUN chmod +x /docker-entrypoint.sh
+
+ENTRYPOINT ["/docker-entrypoint.sh"]
+CMD ["ros2", "run", "my_robot", "motor_controller"]
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..4de0221
--- /dev/null
+++ b/README.md
@@ -0,0 +1,179 @@
+# my_robot — Motor Controller Node
+
+ROS 2 package for differential-drive motor control on the Yahboom Raspbot V2 platform.
+
+---
+
+## Architecture
+
+```
+ ┌──────────────────────────────────┐
+ │ 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 |
+
+### Hardware interface
+
+The node drives the Yahboom Raspbot V2 motor controller over **I²C bus 1** (device address `0x2B`) using the bundled `raspbot_v2_interface` library. The only host device required is `/dev/i2c-1`.
+
+---
+
+## 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.
+
+---
+
+## 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).
+
+---
+
+## 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
+
+---
+
+## Launching
+
+The container needs access to the I²C bus that the motor controller is wired to. Pass only that device rather than running the container in privileged mode:
+
+```bash
+docker run --rm \
+ --device /dev/i2c-1 \
+ my_robot:latest
+```
+
+If your board exposes the motor 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
+
+ROS 2 parameters can be passed through `--ros-args`:
+
+```bash
+docker run --rm \
+ --device /dev/i2c-1 \
+ my_robot:latest \
+ ros2 run my_robot motor_controller \
+ --ros-args -p wheel_base:=0.25 -p max_speed:=0.8
+```
+
+### Sending velocity commands from the host
+
+With the container running, publish a `cmd_vel` message from another terminal (requires ROS 2 installed 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.2}, 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}}"
+```
+
+### Verifying telemetry
+
+```bash
+ros2 topic echo /current_wheel_speeds
+```
+
+---
+
+## Project layout
+
+```
+.
+├── Dockerfile # Two-stage production image
+├── docker-entrypoint.sh # Sources ROS overlays before exec
+├── package.xml # ROS package manifest
+├── setup.py # ament_python build definition
+├── my_robot/
+│ ├── __init__.py
+│ └── motor_controller_node.py
+└── raspbot_v2_interface/ # Vendored Yahboom hardware library
+ └── Raspbot_Lib/
+ └── Raspbot_Lib.py # I²C driver (smbus, bus 1, addr 0x2B)
+```
diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh
new file mode 100644
index 0000000..e357a52
--- /dev/null
+++ b/docker-entrypoint.sh
@@ -0,0 +1,7 @@
+#!/bin/bash
+set -e
+
+source /opt/ros/${ROS_DISTRO}/setup.bash
+source /ws/install/setup.bash
+
+exec "$@"
diff --git a/package.xml b/package.xml
index 96e4b03..9ab40c3 100644
--- a/package.xml
+++ b/package.xml
@@ -10,7 +10,8 @@
geometry_msgs
std_msgs
- ament_python
+ ament_cmake
+ ament_cmake_python
ament_copyright
ament_flake8
ament_pep257
diff --git a/setup.cfg b/setup.cfg
index e69de29..284e957 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -0,0 +1,13 @@
+[metadata]
+name = my_robot
+version = 0.0.1
+
+[options]
+packages = find:
+install_requires =
+ setuptools
+
+[develop]
+script_dir=$base/lib/my_robot
+[install]
+install_scripts=$base/lib/my_robot