Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 31 additions & 5 deletions .agents/skills/configure-multi-robot/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ docker-compose.yaml (ROBOT_NAME_SOURCE=container_name | hostname,
robot/docker/.bashrc (runs on container shell start)
├─ ROBOT_NAME already set in env? → KEEP IT, skip resolution entirely
│ (guard: `if [ -z "${ROBOT_NAME:-}" ]`; lets an override/compose pin the name)
├─ ROBOT_NAME_SOURCE=container_name → resolve `hostname` back to docker container name
│ (e.g. `airstack-robot-desktop-1`)
Expand All @@ -67,7 +70,7 @@ The default mapping rule in [`robot/docker/robot_name_map/default_robot_name_map
robot: 'robot_{1}'
domain_id: '{1}'
- pattern: '.*' # catch-all
robot: 'unknown-robot'
robot: 'unknown_robot' # must be a valid ROS token (no hyphen) or launch fails
domain_id: '0'
```

Expand Down Expand Up @@ -97,7 +100,18 @@ docker exec airstack-robot-desktop-1 bash -c 'echo $ROBOT_NAME $ROS_DOMAIN_ID'
# robot_1 1
```

If you need a non-default name (custom hostname scheme on a physical robot, or you want `drone_alpha` instead of `robot_1`), write a new mapping YAML in `robot/docker/robot_name_map/` and point `ROBOT_NAME_MAP_CONFIG_FILE` at it. Do **not** hardcode `ROBOT_NAME=...` in compose unless you know what you are doing — it bypasses the resolver and you lose `ROS_DOMAIN_ID` co-assignment.
If you need a non-default name (custom hostname scheme on a physical robot, or you want `drone_alpha` instead of `robot_1`), you have two options:

1. **Write a mapping YAML** in `robot/docker/robot_name_map/` and point `ROBOT_NAME_MAP_CONFIG_FILE` at it. Preferred when the name should be derived from the machine (hostname/container) — keeps the resolver in charge of `ROS_DOMAIN_ID` co-assignment.
2. **Pin `ROBOT_NAME` directly** in a per-deployment override env file. `.bashrc` honors a pre-set `ROBOT_NAME` (guard: `if [ -z "${ROBOT_NAME:-}" ]`) and skips the map lookup. This is the clean shortcut for a **single real robot** whose hostname doesn't match `robot-<n>` (see [Real robots and the `unknown_robot` fallback](#real-robots-and-the-unknown_robot-fallback) below):

```bash
# overrides/<deployment>.env — single robot, named directly
ROBOT_NAME=robot_1
ROS_DOMAIN_ID=1 # set alongside — pinning ROBOT_NAME skips the map's domain co-assignment
```

**Only pin `ROBOT_NAME` in an *override env file*, never on the shared `robot-desktop`/`robot-l4t` *service* in compose.** The service is reused for every replica; a hardcoded `ROBOT_NAME` there collapses all robots onto one name/domain and silently breaks multi-robot. And when you pin it, set `ROS_DOMAIN_ID` too — the resolver is what normally co-assigns the domain, and skipping it leaves the domain at whatever the environment defaults to.

For a one-off override (e.g. ad hoc debugging):

Expand Down Expand Up @@ -286,7 +300,7 @@ Without `allow_substs="true"`, the substitution string is loaded literally and t
If two robots share a domain, every topic collides — both `/robot_1/odometry` publishers will be visible to both subscribers, and DDS will sometimes deliver crossed data. The default `robot_name_map` derives the domain from the robot index, so this only happens if you:

- Hardcode `ROS_DOMAIN_ID` in compose to the same value for two replicas
- Use a hostname that doesn't match any rule and falls through to the catch-all (both robots get `unknown-robot`, domain `0`)
- Use a hostname that doesn't match any rule and falls through to the catch-all (both robots get `unknown_robot`, domain `0`)

Always verify after starting:

Expand Down Expand Up @@ -329,9 +343,21 @@ This is a common foot-gun:

Either keep the remap relative (`to="odometry"`) so it joins the namespace, or write the full path explicitly (`to="/$(env ROBOT_NAME)/odometry"`).

### 9. Hostname doesn't match any rule on real robots
### 9. Real robots and the `unknown_robot` fallback

On VOXL/Jetson the service uses `ROBOT_NAME_SOURCE=hostname`, so the **OS hostname** is what gets mapped — not a compose replica index. The stock `default_robot_name_map.yaml` only matches `robot-<n>`, so a device named `airlab-jetson-42` falls through to the catch-all and comes up as **`ROBOT_NAME=unknown_robot`, domain `0`** (with a map that has *no* catch-all, the resolver instead exits non-zero and `ROBOT_NAME` is left unset — same confusing "empty namespace" symptom). This is the usual "why is my real robot `unknown_robot`?" report.

On VOXL/Jetson with `ROBOT_NAME_SOURCE=hostname`, the device hostname must match a rule in the mapping YAML. If `hostname` returns `airlab-jetson-42` and your config only matches `robot-N`, the resolver exits non-zero and `ROBOT_NAME` is unset — the autonomy stack will then launch with empty namespaces and break in confusing ways. Either rename the device or extend the mapping config.
Pick whichever fix matches your topology (see [Configuring a Single Robot](#configuring-a-single-robot)):

- **One robot, quickest:** pin `ROBOT_NAME=robot_1` + `ROS_DOMAIN_ID=1` in the deployment's override env file. The `.bashrc` guard honors it and skips the lookup — no hostname change, no map file.
- **One robot, machine-derived:** rename the device hostname to `robot-1` so the default map resolves it automatically.
- **A fleet:** name each machine `robot-<n>` (default map handles it) **or** ship a mapping YAML that matches your hostnames and point `ROBOT_NAME_MAP_CONFIG_FILE` at it. Do **not** pin a single `ROBOT_NAME` on the shared service — every robot would collide on it.

Verify on the device:

```bash
docker exec <container> bash -c 'echo "$(hostname) -> ROBOT_NAME=$ROBOT_NAME ROS_DOMAIN_ID=$ROS_DOMAIN_ID"'
```

## Pre-Merge Checklist

Expand Down
2 changes: 2 additions & 0 deletions .agents/skills/docker-build-profiles/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ Troubleshooting notes
- YAML quirk: unquoted `3.10` may be parsed as float `3.1` — this changes path strings and breaks imports (e.g., `python3.1` instead of `python3.10`).
- Jetson/L4T builds may require `network: host` during the build to avoid kernel iptables/raw table missing-module errors.
- Jetson **`robot-l4t`** builds from **`robot-l4t-stack-base`** (`robot/docker/Dockerfile.l4t-stack-base`), not raw dustynv, so **`Dockerfile.robot` stays Ubuntu-shaped.** `airstack image-build --profile l4t robot-l4t` triggers **`robot-l4t-stack-base`** first (`airstack.sh`); bare `compose build robot-l4t` can still parallelize badly, so list stack-base explicitly if not using AirStack CLI.
- **dustynv `/ros_entrypoint.sh` shadows the apt Jazzy runtime (mavros symbol-lookup crash).** The dustynv base sources a prebuilt *source* ROS at `$ROS_ROOT/install` from PID 1, prepending its older libs (e.g. `fastcdr` 2.2.5) ahead of the apt Jazzy (2.2.7) that `Dockerfile.robot` layers on top — apt-built nodes like mavros then die with symbol-lookup errors under tmux autolaunch. `Dockerfile.l4t-stack-base` neutralizes it by overwriting `/ros_entrypoint.sh` with a `exec "$@"` passthrough; shells get ROS from `/opt/ros/jazzy/setup.bash` via `.bashrc`. If a Jetson node suddenly can't resolve symbols after a base-image bump, check whether the entrypoint passthrough is still in place.
- **ZED SDK version is pinned across `zed/Dockerfile.zed-l4t`** — the `ZED_SDK_URL` (e.g. `.../zedsdk/5.2/...`) and the ROS dep args (`ZED_MSGS_VERSION`, `POINTCLOUD_TRANSPORT*_VERSION`, `BACKWARD_ROS_VERSION`) must move together; a mismatched `zed_msgs` vs SDK breaks the driver build. Bumping the SDK is camera-firmware-coupled, so confirm the target camera runs that SDK line before merging.

Examples of agent prompts
- "Check `robot/docker/docker-compose.yaml` for `PYTHON_VERSION` entries and quote any unquoted numeric values; open a PR with the fixes and include a test log from a builder-stage build."
Expand Down
2 changes: 1 addition & 1 deletion .env
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ PROJECT_NAME="airstack"
# If you've run ./airstack.sh setup, then this will auto-generate from the git commit hash every time a change is made
# to a Dockerfile or docker-compose.yaml file. Otherwise this can also be set explicitly to make a release version.
# auto-generated from git commit hash
VERSION="0.19.0-alpha.6"
VERSION="0.19.0-alpha.7"
# Choose "dev" or "prebuilt". "dev" is for mounted code that must be built live. "prebuilt" is for built ros_ws baked into the image
DOCKER_IMAGE_BUILD_MODE="dev"
# Where to push and pull images from. Can replace with your docker hub username if using docker hub.
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Battery and telemetry display in GCS RQT control panel (voltage and percentage per robot when MAVROS battery topic is bridged)
- `TARGET_ARCH` build arg (default `x86_64`) in `Dockerfile.robot` to arch-parametrize `LD_LIBRARY_PATH`; `docker-compose.yaml` passes `TARGET_ARCH: aarch64` to the `voxl` and `l4t` real-robot image builds
- `ros-${ROS_DISTRO}-mavros-extras` in the robot image (provides the vision_pose plugin used for external-pose deployments)
- `overrides/l4t-px4-realrobot.env` — site-agnostic deployment override for a single real PX4 robot on a Jetson (aarch64/l4t)

### Changed

- `robot-l4t` compose service knobs are now env-overridable (`AUTONOMY_ROLE`, `FCU_URL`, and the rosbag path via `BAG_STORAGE_PATH`); `FCU_URL` unquoted so the literal serial path reaches MAVROS
- `zed-l4t` image: ZED SDK 4.2 → 5.2 with the coupled ROS deps (`zed_msgs` 5.2.1, `point_cloud_transport(_plugins)` 4.x, add `backward_ros`)

### Fixed

- Robot name resolution now honors a pre-set `ROBOT_NAME` (e.g. injected via docker compose) instead of always overriding it from the container/hostname mapping (`robot/docker/.bashrc`)
- Robot name-map catch-all fallback now maps to `unknown_robot` (valid ROS namespace token) instead of `unknown-robot` (`default_robot_name_map.yaml`)
- l4t robot image: replace dustynv's `/ros_entrypoint.sh` with a passthrough so its prebuilt source-ROS libs (older `fastcdr`) no longer shadow the apt Jazzy runtime and crash apt-built nodes like MAVROS

## [1.0.0] - 2024-12-19

Expand Down
2 changes: 1 addition & 1 deletion docs/robot/docker/robot_identity.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ mappings:
domain_id: '{1}'

- pattern: '.*'
robot: 'unknown-robot'
robot: 'unknown_robot' # must be a valid ROS token (no hyphen) or launch fails
domain_id: '0'
```

Expand Down
34 changes: 34 additions & 0 deletions overrides/l4t-px4-realrobot.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Real-robot deployment on an NVIDIA Jetson (aarch64 / l4t) with a PX4 flight
# controller (e.g. Cube Orange) over serial.

# Build (first time / after image changes):
# airstack image-build --profile l4t robot-l4t
# Run:
# airstack up --env-file overrides/l4t-px4-realrobot.env robot-l4t

# Only bring up the Jetson stack (robot-l4t + zed-l4t).
COMPOSE_PROFILES="l4t"

# Launch the autonomy stack automatically on container start.
AUTOLAUNCH="true"
NUM_ROBOTS="1"

# --- Robot identity -----------------------------------------------------------
# Shortcut to name only a single agent.
ROBOT_NAME="robot_1"
ROS_DOMAIN_ID="1"

# Launches entire robot autonomy stack
AUTONOMY_ROLE="full"

# --- Flight controller (MAVROS) ----------------------------------------------
# Default is the Jetson UART (ttyTHS4).
FCU_URL="/dev/ttyTHS4:115200"

# --- Robot description (PX4 iris w/ sensors; override for your airframe) ------
URDF_FILE="robot_descriptions/iris/urdf/iris_with_sensors.pegasus.robot.urdf"

# --- Flight-data recording ----------------------------------------------------
# Where rosbags land on the host (bind-mounted to /bags in-container).
BAG_STORAGE_PATH="/media/airlab/Storage/airstack_collection"
RECORD_BAGS="false"
4 changes: 4 additions & 0 deletions robot/docker/Dockerfile.l4t-stack-base
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,7 @@ ENV PYTHON_EXECUTABLE=/usr/bin/python3

# Prefer system/cuda tooling over any removed venv prefix (CUDA symlink is usually /usr/local/cuda).
ENV PATH="/usr/local/cuda/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"

# Neutralize dustynv's /ros_entrypoint.sh: it prepends a prebuilt source-ROS lib dir
# that shadows the apt Jazzy (older fastcdr) and crashes mavros. See docker-build-profiles skill.
RUN printf '#!/bin/bash\nexec "$@"\n' > /ros_entrypoint.sh && chmod +x /ros_entrypoint.sh
6 changes: 3 additions & 3 deletions robot/docker/docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -209,15 +209,15 @@ services:
- ROBOT_NAME_SOURCE=hostname
- AUTOLAUNCH=${AUTOLAUNCH:-true}
- LAUNCH_PACKAGE=autonomy_bringup
- AUTONOMY_ROLE=full # l4t profile: Jetson runs everything onboard
- AUTONOMY_ROLE=${AUTONOMY_ROLE:-full}
- LAUNCH_NATNET=${LAUNCH_NATNET:-false}
# mavros mavlink settings
- FCU_URL="/dev/ttyTHS4:115200"
- FCU_URL=${FCU_URL:-/dev/ttyTHS4:115200}
- TGT_SYSTEM=1
# assumes network isolation via a physical router, so uses network_mode=host
network_mode: host
volumes:
- /media/airlab/Storage/airstack_collection:/bags:rw
- ${BAG_STORAGE_PATH:-/media/airlab/Storage/airstack_collection}:/bags:rw

# -----------------------
# Jetson in lite mode: only local/perception/interface modules run onboard.
Expand Down
10 changes: 6 additions & 4 deletions robot/docker/zed/Dockerfile.zed-l4t
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ ARG ROS2_DIST=jazzy
ENV DEBIAN_FRONTEND noninteractive

# ZED SDK link
ENV ZED_SDK_URL="https://download.stereolabs.com/zedsdk/4.2/l4t$L4T_MAJOR.$L4T_MINOR/jetsons"
ENV ZED_SDK_URL="https://download.stereolabs.com/zedsdk/5.2/l4t$L4T_MAJOR.$L4T_MINOR/jetsons"

RUN mkdir -p /tmp && chmod 1777 /tmp

Expand Down Expand Up @@ -53,12 +53,13 @@ ARG XACRO_VERSION=2.0.8
ARG DIAGNOSTICS_VERSION=4.0.0
ARG AMENT_LINT_VERSION=0.12.11
ARG ROBOT_LOCALIZATION_VERSION=3.5.3
ARG ZED_MSGS_VERSION=4.2.2
ARG ZED_MSGS_VERSION=5.2.1
ARG NMEA_MSGS_VERSION=2.0.0
ARG ANGLES_VERSION=1.15.0
ARG GEOGRAPHIC_INFO_VERSION=1.0.6
ARG POINTCLOUD_TRANSPORT_VERSION=1.0.18
ARG POINTCLOUD_TRANSPORT_PLUGINS_VERSION=1.0.11
ARG POINTCLOUD_TRANSPORT_VERSION=4.0.9
ARG POINTCLOUD_TRANSPORT_PLUGINS_VERSION=4.0.4
ARG BACKWARD_ROS_VERSION=1.0.8

RUN wget https://github.com/ros/xacro/archive/refs/tags/${XACRO_VERSION}.tar.gz -O - | tar -xvz && mv xacro-${XACRO_VERSION} xacro && \
wget https://github.com/ros/diagnostics/archive/refs/tags/${DIAGNOSTICS_VERSION}.tar.gz -O - | tar -xvz && mv diagnostics-${DIAGNOSTICS_VERSION} diagnostics && \
Expand All @@ -69,6 +70,7 @@ RUN wget https://github.com/ros/xacro/archive/refs/tags/${XACRO_VERSION}.tar.gz
wget https://github.com/ros/angles/archive/refs/tags/${ANGLES_VERSION}.tar.gz -O - | tar -xvz && mv angles-${ANGLES_VERSION} angles && \
wget https://github.com/ros-perception/point_cloud_transport/archive/refs/tags/${POINTCLOUD_TRANSPORT_VERSION}.tar.gz -O - | tar -xvz && mv point_cloud_transport-${POINTCLOUD_TRANSPORT_VERSION} point_cloud_transport && \
wget https://github.com/ros-perception/point_cloud_transport_plugins/archive/refs/tags/${POINTCLOUD_TRANSPORT_PLUGINS_VERSION}.tar.gz -O - | tar -xvz && mv point_cloud_transport_plugins-${POINTCLOUD_TRANSPORT_PLUGINS_VERSION} point_cloud_transport_plugins && \
wget https://github.com/pal-robotics/backward_ros/archive/refs/tags/${BACKWARD_ROS_VERSION}.tar.gz -O - | tar -xvz && mv backward_ros-${BACKWARD_ROS_VERSION} backward_ros && \
wget https://github.com/ros-geographic-info/geographic_info/archive/refs/tags/${GEOGRAPHIC_INFO_VERSION}.tar.gz -O - | tar -xvz && mv geographic_info-${GEOGRAPHIC_INFO_VERSION} geographic-info && \
cp -r geographic-info/geographic_msgs/ . && \
rm -rf geographic-info
Expand Down
Loading