Skip to content

Commit 1a25d60

Browse files
l4t deployment fixes: make the Jetson profile build + boot on real hardware (#371)
* feat(l4t): make robot-l4t deployment knobs overridable + document name resolution Parametrize the robot-l4t compose service so a single service covers real deployments without editing compose: - AUTONOMY_ROLE and FCU_URL are now ${VAR:-default} overridable (and FCU_URL is unquoted so the literal serial path reaches mavros). - Rosbag output path is BAG_STORAGE_PATH-overridable. Update the configure-multi-robot skill to reflect the honor-pre-set-ROBOT_NAME guard (#370): document pinning ROBOT_NAME in an override for a single real robot, the never-on-the-shared-service caveat, and the unknown_robot fallback fixes by topology. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(l4t): add site-agnostic l4t-px4-realrobot override template Deployment override for a single real PX4 robot on a Jetson (aarch64/l4t). Surfaces the common knobs at the top with sensible defaults: ROBOT_NAME pinned directly (single-robot shortcut honored by .bashrc), FCU_URL, AUTONOMY_ROLE, BAG_STORAGE_PATH, and RECORD_BAGS. Mocap-agnostic — NatNet/external-vision settings are added by a separate optitrack override. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(l4t): entrypoint passthrough + ZED SDK 5.2; document build gotchas Two real-hardware build fixes for the Jetson profile: - Dockerfile.l4t-stack-base: overwrite dustynv's /ros_entrypoint.sh with an `exec "$@"` passthrough. Its prebuilt source-ROS libs (fastcdr 2.2.5) were shadowing the apt Jazzy (2.2.7) that Dockerfile.robot layers on, crashing apt-built nodes like mavros with symbol-lookup errors under tmux autolaunch. - zed/Dockerfile.zed-l4t: bump ZED SDK 4.2 -> 5.2 and move the coupled ROS deps together (zed_msgs 5.2.1, point_cloud_transport(_plugins) 4.x, add backward_ros). Document both gotchas in the docker-build-profiles skill, and correct the stale unknown-robot -> unknown_robot in the robot_identity reference doc. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore: bump version to 0.19.0-alpha.7 Version-increment gate: bump above develop's 0.19.0-alpha.6 and record the l4t deployment changes in the changelog. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent c476db3 commit 1a25d60

9 files changed

Lines changed: 89 additions & 14 deletions

File tree

.agents/skills/configure-multi-robot/SKILL.md

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ docker-compose.yaml (ROBOT_NAME_SOURCE=container_name | hostname,
4242
4343
4444
robot/docker/.bashrc (runs on container shell start)
45+
46+
├─ ROBOT_NAME already set in env? → KEEP IT, skip resolution entirely
47+
│ (guard: `if [ -z "${ROBOT_NAME:-}" ]`; lets an override/compose pin the name)
4548
4649
├─ ROBOT_NAME_SOURCE=container_name → resolve `hostname` back to docker container name
4750
│ (e.g. `airstack-robot-desktop-1`)
@@ -67,7 +70,7 @@ The default mapping rule in [`robot/docker/robot_name_map/default_robot_name_map
6770
robot: 'robot_{1}'
6871
domain_id: '{1}'
6972
- pattern: '.*' # catch-all
70-
robot: 'unknown-robot'
73+
robot: 'unknown_robot' # must be a valid ROS token (no hyphen) or launch fails
7174
domain_id: '0'
7275
```
7376
@@ -97,7 +100,18 @@ docker exec airstack-robot-desktop-1 bash -c 'echo $ROBOT_NAME $ROS_DOMAIN_ID'
97100
# robot_1 1
98101
```
99102

100-
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.
103+
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:
104+
105+
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.
106+
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):
107+
108+
```bash
109+
# overrides/<deployment>.env — single robot, named directly
110+
ROBOT_NAME=robot_1
111+
ROS_DOMAIN_ID=1 # set alongside — pinning ROBOT_NAME skips the map's domain co-assignment
112+
```
113+
114+
**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.
101115

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

@@ -286,7 +300,7 @@ Without `allow_substs="true"`, the substitution string is loaded literally and t
286300
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:
287301

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

291305
Always verify after starting:
292306

@@ -329,9 +343,21 @@ This is a common foot-gun:
329343

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

332-
### 9. Hostname doesn't match any rule on real robots
346+
### 9. Real robots and the `unknown_robot` fallback
347+
348+
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.
333349

334-
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.
350+
Pick whichever fix matches your topology (see [Configuring a Single Robot](#configuring-a-single-robot)):
351+
352+
- **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.
353+
- **One robot, machine-derived:** rename the device hostname to `robot-1` so the default map resolves it automatically.
354+
- **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.
355+
356+
Verify on the device:
357+
358+
```bash
359+
docker exec <container> bash -c 'echo "$(hostname) -> ROBOT_NAME=$ROBOT_NAME ROS_DOMAIN_ID=$ROS_DOMAIN_ID"'
360+
```
335361

336362
## Pre-Merge Checklist
337363

.agents/skills/docker-build-profiles/SKILL.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ Troubleshooting notes
4949
- 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`).
5050
- Jetson/L4T builds may require `network: host` during the build to avoid kernel iptables/raw table missing-module errors.
5151
- 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.
52+
- **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.
53+
- **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.
5254

5355
Examples of agent prompts
5456
- "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."

.env

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ PROJECT_NAME="airstack"
1212
# If you've run ./airstack.sh setup, then this will auto-generate from the git commit hash every time a change is made
1313
# to a Dockerfile or docker-compose.yaml file. Otherwise this can also be set explicitly to make a release version.
1414
# auto-generated from git commit hash
15-
VERSION="0.19.0-alpha.6"
15+
VERSION="0.19.0-alpha.7"
1616
# Choose "dev" or "prebuilt". "dev" is for mounted code that must be built live. "prebuilt" is for built ros_ws baked into the image
1717
DOCKER_IMAGE_BUILD_MODE="dev"
1818
# Where to push and pull images from. Can replace with your docker hub username if using docker hub.

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212
- Battery and telemetry display in GCS RQT control panel (voltage and percentage per robot when MAVROS battery topic is bridged)
1313
- `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
1414
- `ros-${ROS_DISTRO}-mavros-extras` in the robot image (provides the vision_pose plugin used for external-pose deployments)
15+
- `overrides/l4t-px4-realrobot.env` — site-agnostic deployment override for a single real PX4 robot on a Jetson (aarch64/l4t)
16+
17+
### Changed
18+
19+
- `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
20+
- `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`)
1521

1622
### Fixed
1723

1824
- 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`)
1925
- Robot name-map catch-all fallback now maps to `unknown_robot` (valid ROS namespace token) instead of `unknown-robot` (`default_robot_name_map.yaml`)
26+
- 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
2027

2128
## [1.0.0] - 2024-12-19
2229

docs/robot/docker/robot_identity.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ mappings:
5151
domain_id: '{1}'
5252

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

overrides/l4t-px4-realrobot.env

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Real-robot deployment on an NVIDIA Jetson (aarch64 / l4t) with a PX4 flight
2+
# controller (e.g. Cube Orange) over serial.
3+
4+
# Build (first time / after image changes):
5+
# airstack image-build --profile l4t robot-l4t
6+
# Run:
7+
# airstack up --env-file overrides/l4t-px4-realrobot.env robot-l4t
8+
9+
# Only bring up the Jetson stack (robot-l4t + zed-l4t).
10+
COMPOSE_PROFILES="l4t"
11+
12+
# Launch the autonomy stack automatically on container start.
13+
AUTOLAUNCH="true"
14+
NUM_ROBOTS="1"
15+
16+
# --- Robot identity -----------------------------------------------------------
17+
# Shortcut to name only a single agent.
18+
ROBOT_NAME="robot_1"
19+
ROS_DOMAIN_ID="1"
20+
21+
# Launches entire robot autonomy stack
22+
AUTONOMY_ROLE="full"
23+
24+
# --- Flight controller (MAVROS) ----------------------------------------------
25+
# Default is the Jetson UART (ttyTHS4).
26+
FCU_URL="/dev/ttyTHS4:115200"
27+
28+
# --- Robot description (PX4 iris w/ sensors; override for your airframe) ------
29+
URDF_FILE="robot_descriptions/iris/urdf/iris_with_sensors.pegasus.robot.urdf"
30+
31+
# --- Flight-data recording ----------------------------------------------------
32+
# Where rosbags land on the host (bind-mounted to /bags in-container).
33+
BAG_STORAGE_PATH="/media/airlab/Storage/airstack_collection"
34+
RECORD_BAGS="false"

robot/docker/Dockerfile.l4t-stack-base

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,7 @@ ENV PYTHON_EXECUTABLE=/usr/bin/python3
4848

4949
# Prefer system/cuda tooling over any removed venv prefix (CUDA symlink is usually /usr/local/cuda).
5050
ENV PATH="/usr/local/cuda/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
51+
52+
# Neutralize dustynv's /ros_entrypoint.sh: it prepends a prebuilt source-ROS lib dir
53+
# that shadows the apt Jazzy (older fastcdr) and crashes mavros. See docker-build-profiles skill.
54+
RUN printf '#!/bin/bash\nexec "$@"\n' > /ros_entrypoint.sh && chmod +x /ros_entrypoint.sh

robot/docker/docker-compose.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -209,15 +209,15 @@ services:
209209
- ROBOT_NAME_SOURCE=hostname
210210
- AUTOLAUNCH=${AUTOLAUNCH:-true}
211211
- LAUNCH_PACKAGE=autonomy_bringup
212-
- AUTONOMY_ROLE=full # l4t profile: Jetson runs everything onboard
212+
- AUTONOMY_ROLE=${AUTONOMY_ROLE:-full}
213213
- LAUNCH_NATNET=${LAUNCH_NATNET:-false}
214214
# mavros mavlink settings
215-
- FCU_URL="/dev/ttyTHS4:115200"
215+
- FCU_URL=${FCU_URL:-/dev/ttyTHS4:115200}
216216
- TGT_SYSTEM=1
217217
# assumes network isolation via a physical router, so uses network_mode=host
218218
network_mode: host
219219
volumes:
220-
- /media/airlab/Storage/airstack_collection:/bags:rw
220+
- ${BAG_STORAGE_PATH:-/media/airlab/Storage/airstack_collection}:/bags:rw
221221

222222
# -----------------------
223223
# Jetson in lite mode: only local/perception/interface modules run onboard.

robot/docker/zed/Dockerfile.zed-l4t

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ ARG ROS2_DIST=jazzy
1515
ENV DEBIAN_FRONTEND noninteractive
1616

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

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

@@ -53,12 +53,13 @@ ARG XACRO_VERSION=2.0.8
5353
ARG DIAGNOSTICS_VERSION=4.0.0
5454
ARG AMENT_LINT_VERSION=0.12.11
5555
ARG ROBOT_LOCALIZATION_VERSION=3.5.3
56-
ARG ZED_MSGS_VERSION=4.2.2
56+
ARG ZED_MSGS_VERSION=5.2.1
5757
ARG NMEA_MSGS_VERSION=2.0.0
5858
ARG ANGLES_VERSION=1.15.0
5959
ARG GEOGRAPHIC_INFO_VERSION=1.0.6
60-
ARG POINTCLOUD_TRANSPORT_VERSION=1.0.18
61-
ARG POINTCLOUD_TRANSPORT_PLUGINS_VERSION=1.0.11
60+
ARG POINTCLOUD_TRANSPORT_VERSION=4.0.9
61+
ARG POINTCLOUD_TRANSPORT_PLUGINS_VERSION=4.0.4
62+
ARG BACKWARD_ROS_VERSION=1.0.8
6263

6364
RUN wget https://github.com/ros/xacro/archive/refs/tags/${XACRO_VERSION}.tar.gz -O - | tar -xvz && mv xacro-${XACRO_VERSION} xacro && \
6465
wget https://github.com/ros/diagnostics/archive/refs/tags/${DIAGNOSTICS_VERSION}.tar.gz -O - | tar -xvz && mv diagnostics-${DIAGNOSTICS_VERSION} diagnostics && \
@@ -69,6 +70,7 @@ RUN wget https://github.com/ros/xacro/archive/refs/tags/${XACRO_VERSION}.tar.gz
6970
wget https://github.com/ros/angles/archive/refs/tags/${ANGLES_VERSION}.tar.gz -O - | tar -xvz && mv angles-${ANGLES_VERSION} angles && \
7071
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 && \
7172
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 && \
73+
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 && \
7274
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 && \
7375
cp -r geographic-info/geographic_msgs/ . && \
7476
rm -rf geographic-info

0 commit comments

Comments
 (0)