From aa578de902220f1ec43c2dd18ace5d6b06904318 Mon Sep 17 00:00:00 2001 From: Andrew Jong Date: Thu, 20 Aug 2026 19:38:30 -0400 Subject: [PATCH 1/8] =?UTF-8?q?fix(tests):=20widen=20landing=20phase-timeo?= =?UTF-8?q?ut=20margin=20=E2=80=94=20evidence-based?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 2026-08-20 runs show v=0.5 landings completing at 45.1-45.2s against a 45s send_goal cap (TARGET_ALTITUDE/v + 15 + 10): a coin flip producing spurious landing timeouts on 3-robot campaigns (legacy AND stack identically) and the optitrack e2e (v=1.0, 40s cap). The constant now covers the velocity-independent overhead (touchdown detection, land-detector dwell, disarm): max(45, alt/v + 35). Co-Authored-By: Claude Fable 5 --- tests/system/test_takeoff_hover_land.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/system/test_takeoff_hover_land.py b/tests/system/test_takeoff_hover_land.py index b9c954e9a..c560d31d3 100644 --- a/tests/system/test_takeoff_hover_land.py +++ b/tests/system/test_takeoff_hover_land.py @@ -64,8 +64,15 @@ def _phase_timeout(velocity): - """Takeoff/land timeout scaled so 0.5 m/s runs don't time out spuriously.""" - return max(30.0, TARGET_ALTITUDE_M / velocity + 15.0) + """Takeoff/land timeout scaled so 0.5 m/s runs don't time out spuriously. + + The constant covers velocity-independent overhead (touchdown detection, + land-detector dwell, disarm). +15 was marginal: 2026-08-20 runs show + v=0.5 landings completing at 45.1-45.2s against a 45s send_goal cap — + a coin-flip that produced spurious landing timeouts (also observed on + 3-robot campaigns and the optitrack e2e). + """ + return max(45.0, TARGET_ALTITUDE_M / velocity + 35.0) # ── pytest hooks ─────────────────────────────────────────────────────────── From b951fd30abb191ec6a228d4357d10580bf2b204b Mon Sep 17 00:00:00 2001 From: Andrew Jong Date: Thu, 20 Aug 2026 19:53:06 -0400 Subject: [PATCH 2/8] =?UTF-8?q?M3(rfc-379):=20remove=20MACVO=20from=20trun?= =?UTF-8?q?k=20=E2=80=94=20extracted=20to=20castacks/asm=5Fmacvo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dogfood case for Docker dep tiers: macvo_ros2 (+ the 116MB MAC-VO submodule) moves to the module, and Dockerfile.robot sheds the SKIP_MACVO payload — TensorRT apt blocks, torch/torchvision/onnx/tensorrt pips, the 14 MACVO-only pips (audited: only tabulate has another consumer, supplied by the tests venv, never the image), ~220MB model weights, and the huggingface/matplotlib fix block. numpy~=1.26 pin kept (own-risk change, deferred). Behavior note: matplotlib now stays in published images (the fix block previously uninstalled it). - perception.launch.xml: launch_macvo gate removed; the module ships the canonical macvo.launch.xml (also fixes the node's hardcoded camera_info sub) - stacks/full_macvo: includes the module launch under the perception namespace (graph-identical to the committed wiring.md); modules.repos pins asm_macvo; README documents 'airstack module add asm_macvo' - topic_keepalive: macvo rows removed - compose SKIP_MACVO/SKIP_TENSORRT args dropped; docs/skills re-pointed Image-size measurement and wiring.md regeneration follow in the next commits. Co-Authored-By: Claude Fable 5 --- .agents/skills/docker-build-profiles/SKILL.md | 3 +- .devcontainer/robot/launch.json | 6 - .gitmodules | 3 - .../intermediate/docker-build-profiles.md | 7 +- docs/development/stacks.md | 2 +- docs/robot/autonomy/integration_checklist.md | 2 +- docs/robot/autonomy/system_architecture.md | 12 +- docs/robot/docker/index.md | 2 +- mkdocs.yml | 1 - robot/docker/Dockerfile.robot | 76 +----- robot/docker/docker-compose.yaml | 4 - .../src/perception/macvo_ros2/.gitignore | 8 - .../src/perception/macvo_ros2/README.md | 25 -- .../config/MACVO_fast_for_orin.yaml | 103 ------- .../macvo_ros2/config/interface_config.yaml | 15 - .../macvo_ros2/config/rviz_macvo.rviz | 204 -------------- .../macvo_ros2/launch/macvo_ros2.launch.xml | 7 - .../macvo_ros2/DispartyPublisher.py | 53 ---- .../macvo_ros2/macvo_ros2/MessageFactory.py | 196 -------------- .../macvo_ros2/macvo_ros2/__init__.py | 0 .../macvo_ros2/config/zedcam_config.yaml | 178 ------------ .../macvo_ros2/config/zedcam_macvo.yaml | 106 -------- .../perception/macvo_ros2/macvo_ros2/macvo | 1 - .../macvo_ros2/macvo_ros2/macvo_node.py | 256 ------------------ .../macvo_ros2/macvo_ros2/model/README.md | 1 - .../src/perception/macvo_ros2/package.xml | 13 - .../perception/macvo_ros2/resource/macvo_ros2 | 0 .../src/perception/macvo_ros2/setup.cfg | 4 - .../ros_ws/src/perception/macvo_ros2/setup.py | 43 --- .../launch/perception.launch.xml | 26 +- .../scripts/topic_keepalive_node.py | 9 +- stacks/full_macvo/README.md | 27 +- stacks/full_macvo/docker-compose.yaml | 22 +- stacks/full_macvo/launch/stack.launch.xml | 24 +- stacks/full_macvo/modules.repos | 13 +- 35 files changed, 81 insertions(+), 1371 deletions(-) delete mode 100644 robot/ros_ws/src/perception/macvo_ros2/.gitignore delete mode 100644 robot/ros_ws/src/perception/macvo_ros2/README.md delete mode 100644 robot/ros_ws/src/perception/macvo_ros2/config/MACVO_fast_for_orin.yaml delete mode 100644 robot/ros_ws/src/perception/macvo_ros2/config/interface_config.yaml delete mode 100644 robot/ros_ws/src/perception/macvo_ros2/config/rviz_macvo.rviz delete mode 100644 robot/ros_ws/src/perception/macvo_ros2/launch/macvo_ros2.launch.xml delete mode 100644 robot/ros_ws/src/perception/macvo_ros2/macvo_ros2/DispartyPublisher.py delete mode 100644 robot/ros_ws/src/perception/macvo_ros2/macvo_ros2/MessageFactory.py delete mode 100644 robot/ros_ws/src/perception/macvo_ros2/macvo_ros2/__init__.py delete mode 100644 robot/ros_ws/src/perception/macvo_ros2/macvo_ros2/config/zedcam_config.yaml delete mode 100644 robot/ros_ws/src/perception/macvo_ros2/macvo_ros2/config/zedcam_macvo.yaml delete mode 160000 robot/ros_ws/src/perception/macvo_ros2/macvo_ros2/macvo delete mode 100644 robot/ros_ws/src/perception/macvo_ros2/macvo_ros2/macvo_node.py delete mode 100644 robot/ros_ws/src/perception/macvo_ros2/macvo_ros2/model/README.md delete mode 100644 robot/ros_ws/src/perception/macvo_ros2/package.xml delete mode 100644 robot/ros_ws/src/perception/macvo_ros2/resource/macvo_ros2 delete mode 100644 robot/ros_ws/src/perception/macvo_ros2/setup.cfg delete mode 100644 robot/ros_ws/src/perception/macvo_ros2/setup.py diff --git a/.agents/skills/docker-build-profiles/SKILL.md b/.agents/skills/docker-build-profiles/SKILL.md index 3cff9e38c..9304be3a4 100644 --- a/.agents/skills/docker-build-profiles/SKILL.md +++ b/.agents/skills/docker-build-profiles/SKILL.md @@ -93,7 +93,8 @@ This section shows the minimal, recommended steps an agent or maintainer should ROS_DISTRO: humble PYTHON_VERSION: "3.10" REAL_ROBOT: true - SKIP_MACVO: true + # module-owned deps (e.g. MAC-VO's torch/TensorRT) are NOT build-args: + # they layer on via `airstack module lock --build` (RFC #379 S6) # for L4T builds only when necessary # network: host ``` diff --git a/.devcontainer/robot/launch.json b/.devcontainer/robot/launch.json index 01a103eee..b01ccf1c0 100644 --- a/.devcontainer/robot/launch.json +++ b/.devcontainer/robot/launch.json @@ -7,12 +7,6 @@ "request": "launch", "target": "${workspaceFolder}/robot/ros_ws/src/robot_bringup/launch/robot.launch.xml" }, - { - "name": "ROS2: Launch macvo_ros2", - "type": "ros2", - "request": "launch", - "target": "${workspaceFolder}/robot/ros_ws/src/autonomy/perception/macvo_ros2/launch/macvo_ros2.launch.xml" - }, { "name": "ROS2: Attach", "type": "ros2", diff --git a/.gitmodules b/.gitmodules index 8722432a8..1f8c8189a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,9 +4,6 @@ [submodule "robot/ros_ws/src/global/world_models/vdb_mapping_ros2"] path = robot/ros_ws/src/global/world_models/vdb_mapping_ros2 url = https://github.com/castacks/vdb_mapping_ros2.git -[submodule "robot/ros_ws/src/perception/macvo_ros2/macvo_ros2/macvo"] - path = robot/ros_ws/src/perception/macvo_ros2/macvo_ros2/macvo - url = https://github.com/MAC-VO/MAC-VO.git [submodule "robot/ros_ws/src/rviz_behavior_tree_panel/xdot_cpp"] path = common/ros_packages/gui/rviz/rviz_behavior_tree_panel/xdot_cpp url = https://github.com/castacks/xdot_cpp.git diff --git a/docs/development/intermediate/docker-build-profiles.md b/docs/development/intermediate/docker-build-profiles.md index bbd3ebff1..6fd99df5a 100644 --- a/docs/development/intermediate/docker-build-profiles.md +++ b/docs/development/intermediate/docker-build-profiles.md @@ -16,8 +16,8 @@ Key inputs: | `ROS_DISTRO` | ROS 2 distro to install (e.g. `jazzy`) | | `PYTHON_VERSION` | Must match the ROS Python path; **quote in YAML** (e.g. `"3.12"`) | | `REAL_ROBOT` | Platform-specific content toggles | -| `SKIP_MACVO` | Skip MACVO in image when set | -| `SKIP_TENSORRT` | Skip TensorRT in image when set | + +Heavy, algorithm-specific dependencies (e.g. MAC-VO's torch/TensorRT stack) are **not** build-args anymore — they live in each module's `Dockerfile.module` and are layered onto the trunk base image by `airstack module lock --build` (RFC #379 §6). ## Example profiles @@ -44,8 +44,6 @@ robot-l4t: args: BASE_IMAGE: *l4t_stack_base_image # from robot-l4t-stack-base REAL_ROBOT: true - SKIP_MACVO: true - SKIP_TENSORRT: true ROS_DISTRO: jazzy ``` @@ -61,7 +59,6 @@ robot-myboard: ROS_DISTRO: jazzy PYTHON_VERSION: "3.12" REAL_ROBOT: true - SKIP_MACVO: true # L4T only, when needed: # network: host ``` diff --git a/docs/development/stacks.md b/docs/development/stacks.md index 51bb73754..3a6ddc1c2 100644 --- a/docs/development/stacks.md +++ b/docs/development/stacks.md @@ -27,7 +27,7 @@ Anatomy is enforced by a unit test: `tests/meta/test_stack_layout_contract.py` |-------|----------| | [`full_default`](https://github.com/castacks/AirStack/tree/develop/stacks/full_default) | The current full-autonomy topology (GPU `droan_gl` planner) — baseline, graph-identical to legacy `AUTONOMY_ROLE=full`. | | [`full_droan_cpu`](https://github.com/castacks/AirStack/tree/develop/stacks/full_droan_cpu) | CPU DROAN planner + live `disparity_expansion` — absorbs `local_droan_cpu.launch.xml`. | -| [`full_macvo`](https://github.com/castacks/AirStack/tree/develop/stacks/full_macvo) | MAC-VO as the planner's disparity source — supersedes (and fixes) the broken `local_macvo_obstacle_avoidance.launch.xml` variant. | +| [`full_macvo`](https://github.com/castacks/AirStack/tree/develop/stacks/full_macvo) | MAC-VO as the planner's disparity source — supersedes (and fixes) the broken `local_macvo_obstacle_avoidance.launch.xml` variant. Requires the `asm_macvo` module (`airstack module add asm_macvo`). | ## Wrap vs. flatten — current status diff --git a/docs/robot/autonomy/integration_checklist.md b/docs/robot/autonomy/integration_checklist.md index 24c045767..c411f41e7 100644 --- a/docs/robot/autonomy/integration_checklist.md +++ b/docs/robot/autonomy/integration_checklist.md @@ -62,7 +62,7 @@ Topics follow this pattern: Examples: -- `/drone1/perception/macvo/odometry` +- `/drone1/perception/macvo/odometry` (a module-provided topic — macvo comes from the `asm_macvo` module) - `/drone1/local_planner/droan/trajectory` - `/drone1/trajectory_controller/tracking_point` diff --git a/docs/robot/autonomy/system_architecture.md b/docs/robot/autonomy/system_architecture.md index 8e199a07d..afc14e342 100644 --- a/docs/robot/autonomy/system_architecture.md +++ b/docs/robot/autonomy/system_architecture.md @@ -234,19 +234,23 @@ graph TB **Key Modules:** -- `macvo_ros2`: Visual-inertial odometry system +- `stereo_image_proc`: Stereo disparity + point-cloud estimation (trunk default) +- `macvo_ros2`: MAC-VO learned visual odometry — **provided by the + [asm_macvo](https://github.com/castacks/asm_macvo) module** + (`airstack module add asm_macvo`), not trunk. When a stack includes its + launch file under the perception namespace it publishes + `/[robot]/perception/macvo/{odometry,point_cloud,disparity}`. **Topics:** - **Published:** - `/[robot]/odometry` - Primary state estimate - - `/[robot]/perception/macvo/depth` - - `/[robot]/perception/macvo/features` + - `/[robot]/perception/stereo_image_proc/disparity` + - `/[robot]/perception/stereo_image_proc/point_cloud` - **Subscribed:** - `/[robot]/sensors/*/image` - `/[robot]/sensors/*/camera_info` - - `/[robot]/interface/mavros/imu` ### Local Layer diff --git a/docs/robot/docker/index.md b/docs/robot/docker/index.md index 1502fdeb6..c62de8be1 100644 --- a/docs/robot/docker/index.md +++ b/docs/robot/docker/index.md @@ -69,7 +69,7 @@ Runs with `airstack up --profile voxl`. Use this profile when deploying on a Mod - **Image:** `...:v_robot-voxl_` - **Base image:** `ubuntu:22.04` (no CUDA; VOXL has its own compute stack) -- **Skipped components:** OpenVDB, MACVO, TensorRT +- **Skipped components:** OpenVDB (MAC-VO and TensorRT no longer ship in any trunk robot image — they arrive via the `asm_macvo` module's `Dockerfile.module`) - **Network:** `host` (relies on the physical network for DDS discovery) - **Robot identity:** derived from the device hostname → `ROBOT_NAME_SOURCE=hostname` diff --git a/mkdocs.yml b/mkdocs.yml index 90f006059..a551521f3 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -7,7 +7,6 @@ site_url: "https://docs.theairlab.org/docs/" # Trailing slash is recommended exclude_docs: | **/ros_ws/build **/docker/Foxglove - **/macvo **/ros_ws/install **/kit-app-template/** **/isaac_sim_data/** diff --git a/robot/docker/Dockerfile.robot b/robot/docker/Dockerfile.robot index 13ab03102..661d9ee88 100644 --- a/robot/docker/Dockerfile.robot +++ b/robot/docker/Dockerfile.robot @@ -10,8 +10,6 @@ ARG BASE_IMAGE ARG REAL_ROBOT ARG UPDATE_FLAGS="-o Acquire::AllowInsecureRepositories=true -o Acquire::AllowDowngradeToInsecureRepositories=true" ARG INSTALL_FLAGS="-o APT::Get::AllowUnauthenticated=true" -ARG SKIP_MACVO=false -ARG SKIP_TENSORRT=false ARG TARGET_ARCH=x86_64 ARG PIP_VERSION=24.0 @@ -114,19 +112,6 @@ RUN apt update -y && apt install -y --no-install-recommends \ RUN /opt/ros/${ROS_DISTRO}/lib/mavros/install_geographiclib_datasets.sh -# Install TensorRT (NVIDIA/L4T images only, unless SKIP_TENSORRT=true) -# Note: TensorRT 8 packages may not be available for Ubuntu 24.04, so this is optional -RUN if echo "$BASE_IMAGE" | grep -qE "(nvidia|l4t)" && [ "${SKIP_TENSORRT}" != "true" ]; then \ - if [ ! -f /etc/apt/sources.list.d/cuda*.list ]; then \ - wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu$(lsb_release -rs | tr -d .)/x86_64/cuda-keyring_1.1-1_all.deb && \ - dpkg -i cuda-keyring_1.1-1_all.deb || true; \ - fi && \ - apt update -y && \ - apt install -y --no-install-recommends \ - libnvinfer10 libnvinfer-dev libnvinfer-plugin10 \ - python3-libnvinfer python3-libnvinfer-dev; \ - fi - # Install Python dependencies (unconditional) # Note: numpy>=1.26 required for Python 3.12 compatibility # Using --ignore-installed to avoid conflicts with system packages @@ -136,7 +121,7 @@ RUN pip3 install --break-system-packages --ignore-installed \ future \ lxml \ matplotlib==3.8.4 \ - # numpy must be <2.0 for MACVO + # kept <2.0 conservatively; audit consumers before relaxing numpy~=1.26.4 \ pkgconfig \ psutil \ @@ -150,23 +135,9 @@ RUN pip3 install --break-system-packages --ignore-installed \ six \ toml \ scipy \ - pypose \ rich \ tqdm \ - pillow \ - flow_vis \ - h5py \ - evo \ - tabulate \ - einops \ - timm==0.9.12 \ - rerun-sdk==0.22.0 \ - yacs \ - wandb \ - loguru \ - jaxtyping \ - kornia \ - typeguard==2.13.3 + pillow # Keep pytest < 8.1. ROS Jazzy launch_testing still implements # pytest_pycollect_makemodule(path=...), which pluggy rejects after pytest 8.1 @@ -174,32 +145,6 @@ RUN pip3 install --break-system-packages --ignore-installed \ RUN python3 -m pip install --no-cache-dir --break-system-packages \ "pytest>=7.4,<8.1" -# Install MACVO Python dependencies (skipped if SKIP_MACVO=true) -RUN if [ "${SKIP_MACVO}" != "true" ]; then \ - pip3 install --break-system-packages \ - torch \ - torchvision \ - onnx \ - tensorrt; \ - fi - -# Downloading model weights for MACVO (skipped if SKIP_MACVO=true) -WORKDIR /model_weights -RUN if [ "${SKIP_MACVO}" != "true" ]; then \ - wget -r "https://github.com/MAC-VO/MAC-VO/releases/download/model/MACVO_FrontendCov.pth" && \ - wget -r "https://github.com/MAC-VO/MAC-VO/releases/download/model/MACVO_posenet.pkl" && \ - pwd && ls -R && \ - mv /model_weights/github.com/MAC-VO/MAC-VO/releases/download/model/MACVO_FrontendCov.pth /model_weights/MACVO_FrontendCov.pth && \ - mv /model_weights/github.com/MAC-VO/MAC-VO/releases/download/model/MACVO_posenet.pkl /model_weights/MACVO_posenet.pkl && \ - rm -rf /model_weights/github.com; \ - fi - -# Fixes for MACVO Integration (skipped if SKIP_MACVO=true) -RUN if [ "${SKIP_MACVO}" != "true" ]; then \ - pip install --break-system-packages huggingface_hub && \ - pip uninstall --break-system-packages matplotlib -y; \ - fi - # TMux config RUN git clone --depth 1 https://github.com/tmux-plugins/tpm /root/.tmux/plugins/tpm @@ -252,8 +197,6 @@ ARG BASE_IMAGE ARG REAL_ROBOT ARG UPDATE_FLAGS="-o Acquire::AllowInsecureRepositories=true -o Acquire::AllowDowngradeToInsecureRepositories=true" ARG INSTALL_FLAGS="-o APT::Get::AllowUnauthenticated=true" -ARG SKIP_MACVO=false -ARG SKIP_TENSORRT=false ARG TARGET_ARCH=x86_64 ARG PIP_VERSION=24.0 @@ -374,20 +317,6 @@ RUN apt update && apt install -y --no-install-recommends \ libopenvdb-dev \ && rm -rf /var/lib/apt/lists/* -# Install NVIDIA runtime apt packages (no -dev counterparts; NVIDIA/L4T images only, unless SKIP_TENSORRT=true) -# Note: TensorRT 8 packages may not be available for Ubuntu 24.04, so this is optional -RUN if echo "$BASE_IMAGE" | grep -qE "(nvidia|l4t)" && [ "${SKIP_TENSORRT}" != "true" ]; then \ - if [ ! -f /etc/apt/sources.list.d/cuda*.list ]; then \ - wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu$(lsb_release -rs | tr -d .)/x86_64/cuda-keyring_1.1-1_all.deb && \ - dpkg -i cuda-keyring_1.1-1_all.deb || true; \ - fi && \ - apt update -y && \ - apt install -y \ - libnvinfer10 libnvinfer-plugin10 \ - python3-libnvinfer \ - && rm -rf /var/lib/apt/lists/*; \ - fi - # Install Foxglove Studio desktop app only for non-real-robot images RUN if [ "${REAL_ROBOT}" != "true" ] && [ "$(dpkg --print-architecture)" = "amd64" ]; then \ wget -q https://get.foxglove.dev/desktop/latest/foxglove-studio-latest-linux-amd64.deb -O /tmp/foxglove-studio.deb && \ @@ -412,7 +341,6 @@ RUN mkdir /var/run/sshd COPY --from=builder /usr/local/bin /usr/local/bin COPY --from=builder /usr/local/lib /usr/local/lib COPY --from=builder /usr/local/include /usr/local/include -COPY --from=builder /model_weights /model_weights COPY --from=builder /root/.tmux /root/.tmux # Password is airstack diff --git a/robot/docker/docker-compose.yaml b/robot/docker/docker-compose.yaml index 427bfa212..5168de0c4 100644 --- a/robot/docker/docker-compose.yaml +++ b/robot/docker/docker-compose.yaml @@ -124,8 +124,6 @@ services: args: BASE_IMAGE: ubuntu:24.04 REAL_ROBOT: true - SKIP_MACVO: true - SKIP_TENSORRT: true TARGET_ARCH: aarch64 ROS_DISTRO: jazzy tags: @@ -185,8 +183,6 @@ services: args: BASE_IMAGE: *l4t_stack_base_image REAL_ROBOT: true - SKIP_MACVO: true - SKIP_TENSORRT: true TARGET_ARCH: aarch64 ROS_DISTRO: jazzy tags: diff --git a/robot/ros_ws/src/perception/macvo_ros2/.gitignore b/robot/ros_ws/src/perception/macvo_ros2/.gitignore deleted file mode 100644 index 0cdaa587b..000000000 --- a/robot/ros_ws/src/perception/macvo_ros2/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -build/* -install/* -log/* -MACVO_ROS2/Model/* -*.pth -*.pkl -*.pyc -__pycache__/ diff --git a/robot/ros_ws/src/perception/macvo_ros2/README.md b/robot/ros_ws/src/perception/macvo_ros2/README.md deleted file mode 100644 index e9dc3df4e..000000000 --- a/robot/ros_ws/src/perception/macvo_ros2/README.md +++ /dev/null @@ -1,25 +0,0 @@ -# MAC-VO ROS2 Wrapper - -Fork of the MAC-VO ROS2 Wrapper for AirStack - -## Install and Configuration - -To install as a ROS2 node, clone this directory in your ROS2 workspace and run - -> [!NOTE] -> Please clone the repository with `--recursive` flag to clone all the submodules automatically. - -```bash -colcon build -source install/local_setup.bash -``` - -To launch the MAC-VO Node, use the following config, substitute `[PATH_TO_CONFIG]` with your own `.yaml` config path. - -``` -ros2 run MACVO_ROS2 MACVO --config [PATH_TO_CONFIG] -``` - -## Pretrained Model - -Please follow the `README.md` on [https://github.com/MAC-VO/MAC-VO](https://github.com/MAC-VO/MAC-VO) to download the pre-trained model. The default path for the pre-trained model is set to be `MACVO_ROS2/src/Module` diff --git a/robot/ros_ws/src/perception/macvo_ros2/config/MACVO_fast_for_orin.yaml b/robot/ros_ws/src/perception/macvo_ros2/config/MACVO_fast_for_orin.yaml deleted file mode 100644 index 5530d1c1a..000000000 --- a/robot/ros_ws/src/perception/macvo_ros2/config/MACVO_fast_for_orin.yaml +++ /dev/null @@ -1,103 +0,0 @@ -# -# This configuration is **not** used to report numbers in the paper. -# To reproduce result in the paper, please use Paper_Reproduce.yaml -# -# -# MAC-VO Fast Mode (Jun 2025 Update) -# Comparing to the Performant mode (MACVO_Performant), the Fast Mode uses -# mixed-precision inference of fp32, fp16 and bf16. The fast mode can run -# at 12.5fps on 480x640 images on RTX Ada 6000 GPU, almost twice the speed -# as Performant Mode. -# -# If minor degrade in accuracy (~5% increase in RTE and ROE) is acceptable, -# then you should use Fast Mode for most of the time. -# - -Common: - # Some configurations are shared across multiple modules in Odometry, so I write them here. - device: &device cuda - -Odometry: - name: MACVO-Fast-for-Orin - args: - # Device directive to the VO system - # NOTE: the system may not follow this device config strictly since some module - # e.g. those rely on PWC-Net, only support running on cuda device. - device: *device - edgewidth: 32 - num_point: 200 # Upper bound of KPs in each frame - - # Match covariance for keypoint on first observation (sub-pixel uncertainty - # caused by the quantized pixel) - match_cov_default: 0.25 - - # Profiling the system using torch, generate chrome json trace file. - profile: false - - # Mapping mode provides the dense mapping - mapping: true - - cov: - obs: - type: MatchCovariance - args: - device: *device - kernel_size: 7 - match_cov_default: 0.25 - min_depth_cov: 0.05 - min_flow_cov: 0.25 - -# keypoint and mappoint tuned for AGX Orin by Yutian Chen - keypoint: - type: CovAwareSelector_NoDepth - args: - device: *device - kernel_size: 7 - mask_width: 8 - max_match_cov: 100.0 - - mappoint: - # Mapping feature can be conveniently turn off by using - # "type: NoKeypointSelector" instead of the config below. - type: MappingPointSelector - args: - device: *device - max_depth: 5.0 - max_depth_cov: 0.003 - mask_width: 8 - - frontend: - type: CUDAGraph_FlowFormerCovFrontend - args: - device: *device - # downloaded in Dockerfile.robot - weight: /model_weights/MACVO_FrontendCov.pth - enc_dtype: fp16 - dec_dtype: bf16 - decoder_depth: 12 - enforce_positive_disparity: false - - motion: - type: StaticMotionModel - args: - - outlier: - type: CovarianceSanityFilter - args: - - postprocess: - type: MotionInterpolate - args: - - keyframe: - type: AllKeyframe - args: - - optimizer: - type: TwoFrame_PGO - args: - device: cpu - vectorize: true - parallel: true - graph_type: disp - autodiff: false diff --git a/robot/ros_ws/src/perception/macvo_ros2/config/interface_config.yaml b/robot/ros_ws/src/perception/macvo_ros2/config/interface_config.yaml deleted file mode 100644 index 9b50bb21f..000000000 --- a/robot/ros_ws/src/perception/macvo_ros2/config/interface_config.yaml +++ /dev/null @@ -1,15 +0,0 @@ -/**: - ros__parameters: - camera_name: "front_stereo" - # camera_param_server_client_topic: "sensors/camera_param_server/get_parameters" - camera_info_sub_topic: "camera_info" - coordinate_frame: "macvo_ned" - imageL_sub_topic: "left/image_rect" - imageR_sub_topic: "right/image_rect" - pose_pub_topic: "pose" - odom_pub_topic: "odometry" - point_pub_topic: "point_cloud" - disp_pub_topic: "disparity" - img_pub_topic: "image_features" - inference_dim_u: 420 - inference_dim_v: 420 diff --git a/robot/ros_ws/src/perception/macvo_ros2/config/rviz_macvo.rviz b/robot/ros_ws/src/perception/macvo_ros2/config/rviz_macvo.rviz deleted file mode 100644 index c783068f4..000000000 --- a/robot/ros_ws/src/perception/macvo_ros2/config/rviz_macvo.rviz +++ /dev/null @@ -1,204 +0,0 @@ -Panels: - - Class: rviz_common/Displays - Help Height: 78 - Name: Displays - Property Tree Widget: - Expanded: - - /Global Options1 - - /Status1 - - /PointCloud1 - Splitter Ratio: 0.5 - Tree Height: 725 - - Class: rviz_common/Selection - Name: Selection - - Class: rviz_common/Tool Properties - Expanded: - - /2D Goal Pose1 - - /Publish Point1 - Name: Tool Properties - Splitter Ratio: 0.5886790156364441 - - Class: rviz_common/Views - Expanded: - - /Current View1 - Name: Views - Splitter Ratio: 0.5 - - Class: rviz_common/Time - Experimental: false - Name: Time - SyncMode: 0 - SyncSource: "" -Visualization Manager: - Class: "" - Displays: - - Alpha: 0.5 - Cell Size: 1 - Class: rviz_default_plugins/Grid - Color: 160; 160; 164 - Enabled: true - Line Style: - Line Width: 0.029999999329447746 - Value: Lines - Name: Grid - Normal Cell Count: 0 - Offset: - X: 0 - Y: 0 - Z: 0 - Plane: XY - Plane Cell Count: 10 - Reference Frame: - Value: true - - Alpha: 1 - Axes Length: 1 - Axes Radius: 0.10000000149011612 - Class: rviz_default_plugins/Axes - Color: 255; 25; 0 - Enabled: true - Head Length: 0.30000001192092896 - Head Radius: 0.10000000149011612 - Name: Pose - Shaft Length: 1 - Shaft Radius: 0.05000000074505806 - Shape: Axes - Topic: - Depth: 5 - Durability Policy: Volatile - Filter size: 10 - History Policy: Keep Last - Reliability Policy: Reliable - Value: /robot_1/perception/visual_odometry_pose - Value: true - - Alpha: 1 - Autocompute Intensity Bounds: true - Autocompute Value Bounds: - Max Value: 4.518574237823486 - Min Value: -0.5837738513946533 - Value: true - Axis: Z - Channel Name: intensity - Class: rviz_default_plugins/PointCloud - Color: 255; 255; 255 - Color Transformer: AxisColor - Decay Time: 0 - Enabled: true - Invert Rainbow: false - Max Color: 255; 255; 255 - Max Intensity: 4096 - Min Color: 0; 0; 0 - Min Intensity: 0 - Name: PointCloud - Position Transformer: XYZ - Selectable: true - Size (Pixels): 3 - Size (m): 0.5 - Style: Points - Topic: - Depth: 5 - Durability Policy: Volatile - Filter size: 10 - History Policy: Keep Last - Reliability Policy: Reliable - Value: /robot_1/perception/visual_odometry_points - Use Fixed Frame: true - Use rainbow: true - Value: true - - Class: rviz_default_plugins/Image - Enabled: true - Max Value: 1 - Median window: 5 - Min Value: 0 - Name: Image - Normalize Range: true - Topic: - Depth: 5 - Durability Policy: Volatile - History Policy: Keep Last - Reliability Policy: Reliable - Value: /robot_1/perception/visual_odometry_img - Value: true - Enabled: true - Global Options: - Background Color: 48; 48; 48 - Fixed Frame: left_camera - Frame Rate: 30 - Name: root - Tools: - - Class: rviz_default_plugins/Interact - Hide Inactive Objects: true - - Class: rviz_default_plugins/MoveCamera - - Class: rviz_default_plugins/Select - - Class: rviz_default_plugins/FocusCamera - - Class: rviz_default_plugins/Measure - Line color: 128; 128; 0 - - Class: rviz_default_plugins/SetInitialPose - Covariance x: 0.25 - Covariance y: 0.25 - Covariance yaw: 0.06853891909122467 - Topic: - Depth: 5 - Durability Policy: Volatile - History Policy: Keep Last - Reliability Policy: Reliable - Value: /initialpose - - Class: rviz_default_plugins/SetGoal - Topic: - Depth: 5 - Durability Policy: Volatile - History Policy: Keep Last - Reliability Policy: Reliable - Value: /goal_pose - - Class: rviz_default_plugins/PublishPoint - Single click: true - Topic: - Depth: 5 - Durability Policy: Volatile - History Policy: Keep Last - Reliability Policy: Reliable - Value: /clicked_point - Transformation: - Current: - Class: rviz_default_plugins/TF - Value: true - Views: - Current: - Class: rviz_default_plugins/Orbit - Distance: 24.403091430664062 - Enable Stereo Rendering: - Stereo Eye Separation: 0.05999999865889549 - Stereo Focal Distance: 1 - Swap Stereo Eyes: false - Value: false - Focal Point: - X: 0 - Y: 0 - Z: 0 - Focal Shape Fixed Size: true - Focal Shape Size: 0.05000000074505806 - Invert Z Axis: false - Name: Current View - Near Clip Distance: 0.009999999776482582 - Pitch: 0.3003981113433838 - Target Frame: - Value: Orbit (rviz) - Yaw: 2.8553879261016846 - Saved: ~ -Window Geometry: - Displays: - collapsed: false - Height: 1016 - Hide Left Dock: false - Hide Right Dock: false - Image: - collapsed: false - QMainWindow State: 000000ff00000000fd0000000400000000000001560000035efc0200000008fb0000001200530065006c0065006300740069006f006e00000001e10000009b0000005c00fffffffb0000001e0054006f006f006c002000500072006f007000650072007400690065007302000001ed000001df00000185000000a3fb000000120056006900650077007300200054006f006f02000001df000002110000018500000122fb000000200054006f006f006c002000500072006f0070006500720074006900650073003203000002880000011d000002210000017afc0000003b0000035e000000c700fffffffa000000010100000002fb0000000a0049006d0061006700650000000000ffffffff0000000000000000fb000000100044006900730070006c0061007900730100000000000001560000015600fffffffb0000002000730065006c0065006300740069006f006e00200062007500660066006500720200000138000000aa0000023a00000294fb00000014005700690064006500530074006500720065006f02000000e6000000d2000003ee0000030bfb0000000c004b0069006e0065006300740200000186000001060000030c0000026100000001000002760000035efc0200000005fb0000000a0049006d006100670065010000003b0000035e0000002800fffffffb0000000a0049006d006100670065010000003b000003790000000000000000fb0000001e0054006f006f006c002000500072006f00700065007200740069006500730100000041000000780000000000000000fb0000000a00560069006500770073000000003b00000379000000a000fffffffb0000001200530065006c0065006300740069006f006e010000025a000000b200000000000000000000000200000490000000a9fc0100000001fb0000000a00560069006500770073030000004e00000080000002e100000197000000030000073a0000003efc0100000002fb0000000800540069006d006501000000000000073a0000025300fffffffb0000000800540069006d00650100000000000004500000000000000000000003620000035e00000004000000040000000800000008fc0000000100000002000000010000000a0054006f006f006c00730100000000ffffffff0000000000000000 - Selection: - collapsed: false - Time: - collapsed: false - Tool Properties: - collapsed: false - Views: - collapsed: false - Width: 1850 - X: 70 - Y: 27 diff --git a/robot/ros_ws/src/perception/macvo_ros2/launch/macvo_ros2.launch.xml b/robot/ros_ws/src/perception/macvo_ros2/launch/macvo_ros2.launch.xml deleted file mode 100644 index 7cdf78ca7..000000000 --- a/robot/ros_ws/src/perception/macvo_ros2/launch/macvo_ros2.launch.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/robot/ros_ws/src/perception/macvo_ros2/macvo_ros2/DispartyPublisher.py b/robot/ros_ws/src/perception/macvo_ros2/macvo_ros2/DispartyPublisher.py deleted file mode 100644 index fbe151b88..000000000 --- a/robot/ros_ws/src/perception/macvo_ros2/macvo_ros2/DispartyPublisher.py +++ /dev/null @@ -1,53 +0,0 @@ -import numpy as np -from typing import overload - -from rclpy.node import Node -from sensor_msgs.msg import Image -from builtin_interfaces.msg import Time -from typing import TYPE_CHECKING -import os, sys - -from .MessageFactory import to_image - -# Add the macvo directory to the Python path -macvo_path = os.path.abspath(os.path.join(os.path.dirname(__file__), 'macvo')) -sys.path.insert(0, macvo_path) -if TYPE_CHECKING: - # To make static type checker happy : ) - from Module import IFrontend, IMatcher, IStereoDepth - from DataLoader import StereoData -else: - from Module import IFrontend, IMatcher, IStereoDepth - from DataLoader import StereoData - - -class DisparityPublisher(IFrontend): - def __init__(self, node: Node, internal_module: IFrontend, publish_topic: str, frame_id: str): - self.ros2_node = node - self.internal_module = internal_module - self.publish_topic = publish_topic - self.publisher = node.create_publisher(Image, publish_topic, qos_profile=1) - self.frame_id = frame_id - self.curr_timestamp: Time | None = None - - @property - def provide_cov(self) -> tuple[bool, bool]: return self.internal_module.provide_cov - - def init_context(self): return None - - def estimate_pair(self, frame_t1: StereoData, frame_t2: StereoData) -> tuple[IStereoDepth.Output, IMatcher.Output]: - depth, match = self.internal_module.estimate_pair(frame_t1, frame_t2) - - if (depth.disparity is not None) and (self.curr_timestamp is not None): - disparity_msg = to_image( - depth.disparity[0].permute(1, 2, 0).cpu().numpy().astype(np.uint16), - self.frame_id, - self.curr_timestamp, - encoding="mono16" - ) - self.publisher.publish(disparity_msg) - - return depth, match - - def estimate_depth(self, frame: StereoData) -> IStereoDepth.Output: - return self.internal_module.estimate_depth(frame) diff --git a/robot/ros_ws/src/perception/macvo_ros2/macvo_ros2/MessageFactory.py b/robot/ros_ws/src/perception/macvo_ros2/macvo_ros2/MessageFactory.py deleted file mode 100644 index 97a606ba9..000000000 --- a/robot/ros_ws/src/perception/macvo_ros2/macvo_ros2/MessageFactory.py +++ /dev/null @@ -1,196 +0,0 @@ -import std_msgs.msg as std_msgs -import nav_msgs.msg as nav_msgs -import sensor_msgs.msg as sensor_msgs -import geometry_msgs.msg as geometry_msgs -from builtin_interfaces.msg import Time - -import sys -import torch -import pypose as pp -import numpy as np - -_name_to_dtypes = { - "rgb8": (np.uint8, 3), - "rgba8": (np.uint8, 4), - "rgb16": (np.uint16, 3), - "rgba16": (np.uint16, 4), - "bgr8": (np.uint8, 3), - "bgra8": (np.uint8, 4), - "bgr16": (np.uint16, 3), - "bgra16": (np.uint16, 4), - "mono8": (np.uint8, 1), - "mono16": (np.uint16, 1), - - # for bayer image (based on cv_bridge.cpp) - "bayer_rggb8": (np.uint8, 1), - "bayer_bggr8": (np.uint8, 1), - "bayer_gbrg8": (np.uint8, 1), - "bayer_grbg8": (np.uint8, 1), - "bayer_rggb16": (np.uint16, 1), - "bayer_bggr16": (np.uint16, 1), - "bayer_gbrg16": (np.uint16, 1), - "bayer_grbg16": (np.uint16, 1), - - # OpenCV CvMat types - "8UC1": (np.uint8, 1), - "8UC2": (np.uint8, 2), - "8UC3": (np.uint8, 3), - "8UC4": (np.uint8, 4), - "8SC1": (np.int8, 1), - "8SC2": (np.int8, 2), - "8SC3": (np.int8, 3), - "8SC4": (np.int8, 4), - "16UC1": (np.uint16, 1), - "16UC2": (np.uint16, 2), - "16UC3": (np.uint16, 3), - "16UC4": (np.uint16, 4), - "16SC1": (np.int16, 1), - "16SC2": (np.int16, 2), - "16SC3": (np.int16, 3), - "16SC4": (np.int16, 4), - "32SC1": (np.int32, 1), - "32SC2": (np.int32, 2), - "32SC3": (np.int32, 3), - "32SC4": (np.int32, 4), - "32FC1": (np.float32, 1), - "32FC2": (np.float32, 2), - "32FC3": (np.float32, 3), - "32FC4": (np.float32, 4), - "64FC1": (np.float64, 1), - "64FC2": (np.float64, 2), - "64FC3": (np.float64, 3), - "64FC4": (np.float64, 4) -} - - -def to_stamped_pose(pose: pp.LieTensor | torch.Tensor, frame_id: str, time: Time) -> geometry_msgs.PoseStamped: - pose_ = pose.detach().cpu() - out_msg = geometry_msgs.PoseStamped() - out_msg.header = std_msgs.Header() - out_msg.header.stamp = time - out_msg.header.frame_id = frame_id - - out_msg.pose.position.x = pose_[0].item() - out_msg.pose.position.y = pose_[1].item() - out_msg.pose.position.z = pose_[2].item() - - out_msg.pose.orientation.x = pose_[3].item() - out_msg.pose.orientation.y = pose_[4].item() - out_msg.pose.orientation.z = pose_[5].item() - out_msg.pose.orientation.w = pose_[6].item() - return out_msg - -def to_nav_msgs_odmetry(pose: pp.LieTensor | torch.Tensor, frame_id: str, time: Time) -> nav_msgs.Odometry: - pose_ = pose.detach().cpu() - out_msg = nav_msgs.Odometry() - out_msg.header = std_msgs.Header() - out_msg.header.stamp = time - out_msg.header.frame_id = frame_id - out_msg.child_frame_id = "base_link" # TODO: UNHARDCODE - - out_msg.pose.pose.position.x = pose_[0].item() - out_msg.pose.pose.position.y = pose_[1].item() - out_msg.pose.pose.position.z = pose_[2].item() - - out_msg.pose.pose.orientation.x = pose_[3].item() - out_msg.pose.pose.orientation.y = pose_[4].item() - out_msg.pose.pose.orientation.z = pose_[5].item() - out_msg.pose.pose.orientation.w = pose_[6].item() - return out_msg - -def from_image(msg: sensor_msgs.Image) -> np.ndarray: - if msg.encoding not in _name_to_dtypes: - raise KeyError(f"Unsupported image encoding {msg.encoding}") - - dtype_name, channel = _name_to_dtypes[msg.encoding] - dtype = np.dtype(dtype_name) - dtype = dtype.newbyteorder('>' if msg.is_bigendian else '<') - shape = (msg.height, msg.width, channel) - - data = np.frombuffer(msg.data, dtype=dtype).reshape(shape) - data.strides = (msg.step, dtype.itemsize * channel, dtype.itemsize) - return data - - -def to_image(arr: np.ndarray, frame_id: str, time: Time, encoding: str = "bgra8") -> sensor_msgs.Image: - if not encoding in _name_to_dtypes: - raise TypeError('Unrecognized encoding {}'.format(encoding)) - - im = sensor_msgs.Image(encoding=encoding) - - # extract width, height, and channels - dtype_class, exp_channels = _name_to_dtypes[encoding] - dtype = np.dtype(dtype_class) - if len(arr.shape) == 2: - im.height, im.width, channels = arr.shape + (1,) - elif len(arr.shape) == 3: - im.height, im.width, channels = arr.shape - else: - raise TypeError("Array must be two or three dimensional") - - # check type and channels - if exp_channels != channels: - raise TypeError("Array has {} channels, {} requires {}".format( - channels, encoding, exp_channels - )) - if dtype_class != arr.dtype.type: - raise TypeError("Array is {}, {} requires {}".format( - arr.dtype.type, encoding, dtype_class - )) - - # make the array contiguous in memory, as mostly required by the format - contig = np.ascontiguousarray(arr) - im.data = contig.tobytes() - im.step = contig.strides[0] - im.header.stamp = time - im.header.frame_id = frame_id - im.is_bigendian = ( - arr.dtype.byteorder == '>' or - arr.dtype.byteorder == '=' and sys.byteorder == 'big' - ) - - return im - - -def to_pointcloud(position: torch.Tensor, keypoints: torch.Tensor | None, colors: torch.Tensor, frame_id: str, time: Time) -> sensor_msgs.PointCloud: - """ - position should be a Nx3 pytorch Tensor (dtype=float) - keypoints should be a Nx2 pytorch Tensor (dtype=float) - """ - - out_msg = sensor_msgs.PointCloud() - position_ = position.detach().cpu().numpy() - colors_ = colors.detach().cpu().numpy() - - out_msg.header = std_msgs.Header() - out_msg.header.stamp = time - out_msg.header.frame_id = frame_id - - - out_msg.points = [ - geometry_msgs.Point32(x=float(position_[pt_idx, 0]), y=float(position_[pt_idx, 1]), z=float(position_[pt_idx, 2])) - for pt_idx in range(position.size(0)) - ] - out_msg.channels = [ - sensor_msgs.ChannelFloat32( - name="r" , values=colors_[..., 2].tolist() - ), - sensor_msgs.ChannelFloat32( - name="g" , values=colors_[..., 1].tolist() - ), - sensor_msgs.ChannelFloat32( - name="b" , values=colors_[..., 0].tolist() - ) - ] - - if keypoints is not None: - assert position.size(0) == keypoints.size(0) - keypoints_ = keypoints.detach().cpu().numpy() - out_msg.channels.append(sensor_msgs.ChannelFloat32( - name="kp_u", values=keypoints_[..., 0].tolist() - )) - out_msg.channels.append(sensor_msgs.ChannelFloat32( - name="kp_v", values=keypoints_[..., 1].tolist() - )) - - return out_msg diff --git a/robot/ros_ws/src/perception/macvo_ros2/macvo_ros2/__init__.py b/robot/ros_ws/src/perception/macvo_ros2/macvo_ros2/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/robot/ros_ws/src/perception/macvo_ros2/macvo_ros2/config/zedcam_config.yaml b/robot/ros_ws/src/perception/macvo_ros2/macvo_ros2/config/zedcam_config.yaml deleted file mode 100644 index 1a2f7fb6c..000000000 --- a/robot/ros_ws/src/perception/macvo_ros2/macvo_ros2/config/zedcam_config.yaml +++ /dev/null @@ -1,178 +0,0 @@ -# config/common_yaml -# Common parameters to Stereolabs ZED and ZED mini cameras - ---- -/**: - ros__parameters: - use_sim_time: false # Set to `true` only if there is a publisher for the simulated clock to the `/clock` topic. Normally used in simulation mode. - - simulation: - sim_enabled: false # Set to `true` to enable the simulation mode and connect to a simulation server - sim_address: "127.0.0.1" # The connection address of the simulation server. See the documentation of the supported simulation plugins for more information. - sim_port: 30000 # The connection port of the simulation server. See the documentation of the supported simulation plugins for more information. - - svo: - svo_loop: false # Enable loop mode when using an SVO as input source - svo_realtime: true # if true the SVO will be played trying to respect the original framerate eventually skipping frames, otherwise every frame will be processed respecting the `pub_frame_rate` setting - - general: - camera_timeout_sec: 5 - camera_max_reconnect: 5 - camera_flip: false - serial_number: 0 # usually overwritten by launch file - pub_resolution: CUSTOM # The resolution used for output. 'NATIVE' to use the same `general.grab_resolution` - `CUSTOM` to apply the `general.pub_downscale_factor` downscale factory to reduce bandwidth in transmission - pub_downscale_factor: 4.0 # rescale factor used to rescale image before publishing when 'pub_resolution' is 'CUSTOM' - pub_frame_rate: 3.0 # frequency of publishing of visual images and depth images - gpu_id: -1 - optional_opencv_calibration_file: "" # Optional path where the ZED SDK can find a file containing the calibration information of the camera computed by OpenCV. Read the ZED SDK documentation for more information: https://www.stereolabs.com/docs/api/structsl_1_1InitParameters.html#a9eab2753374ef3baec1d31960859ba19 - - video: - brightness: 4 # [DYNAMIC] Not available for ZED X/ZED X Mini - contrast: 4 # [DYNAMIC] Not available for ZED X/ZED X Mini - hue: 0 # [DYNAMIC] Not available for ZED X/ZED X Mini - saturation: 4 # [DYNAMIC] - sharpness: 4 # [DYNAMIC] - gamma: 8 # [DYNAMIC] - auto_exposure_gain: true # [DYNAMIC] - exposure: 80 # [DYNAMIC] - gain: 80 # [DYNAMIC] - auto_whitebalance: true # [DYNAMIC] - whitebalance_temperature: 42 # [DYNAMIC] - [28,65] works only if `auto_whitebalance` is false - - region_of_interest: - automatic_roi: false # Enable the automatic ROI generation to automatically detect part of the robot in the FoV and remove them from the processing. Note: if enabled the value of `manual_polygon` is ignored - depth_far_threshold_meters: 2.5 # Filtering how far object in the ROI should be considered, this is useful for a vehicle for instance - image_height_ratio_cutoff: 0.5 # By default consider only the lower half of the image, can be useful to filter out the sky - #manual_polygon: '[]' # A polygon defining the ROI where the ZED SDK perform the processing ignoring the rest. Coordinates must be normalized to '1.0' to be resolution independent. - #manual_polygon: "[[0.25,0.33],[0.75,0.33],[0.75,0.5],[0.5,0.75],[0.25,0.5]]" # A polygon defining the ROI where the ZED SDK perform the processing ignoring the rest. Coordinates must be normalized to '1.0' to be resolution independent. - #manual_polygon: '[[0.25,0.25],[0.75,0.25],[0.75,0.75],[0.25,0.75]]' # A polygon defining the ROI where the ZED SDK perform the processing ignoring the rest. Coordinates must be normalized to '1.0' to be resolution independent. - #manual_polygon: '[[0.5,0.25],[0.75,0.5],[0.5,0.75],[0.25,0.5]]' # A polygon defining the ROI where the ZED SDK perform the processing ignoring the rest. Coordinates must be normalized to '1.0' to be resolution independent. - apply_to_depth: true # Apply ROI to depth processing - apply_to_positional_tracking: true # Apply ROI to positional tracking processing - apply_to_object_detection: true # Apply ROI to object detection processing - apply_to_body_tracking: true # Apply ROI to body tracking processing - apply_to_spatial_mapping: true # Apply ROI to spatial mapping processing - - depth: - depth_mode: "NONE" # Matches the ZED SDK setting: 'NONE', 'PERFORMANCE', 'QUALITY', 'ULTRA', 'NEURAL', 'NEURAL_PLUS' - Note: if 'NONE' all the modules that requires depth extraction are disabled by default (Pos. Tracking, Obj. Detection, Mapping, ...) - depth_stabilization: 1 # Forces positional tracking to start if major than 0 - Range: [0,100] - openni_depth_mode: false # 'false': 32bit float [meters], 'true': 16bit unsigned int [millimeters] - point_cloud_freq: 10.0 # [DYNAMIC] - frequency of the pointcloud publishing (equal or less to `grab_frame_rate` value) - depth_confidence: 50 # [DYNAMIC] - depth_texture_conf: 100 # [DYNAMIC] - remove_saturated_areas: true # [DYNAMIC] - - pos_tracking: - pos_tracking_enabled: false # True to enable positional tracking from start - pos_tracking_mode: "GEN_2" # Matches the ZED SDK setting: 'GEN_1', 'GEN_2' - imu_fusion: false # enable/disable IMU fusion. When set to false, only the optical odometry will be used. - publish_tf: false # [usually overwritten by launch file] publish `odom -> camera_link` TF - publish_map_tf: false # [usually overwritten by launch file] publish `map -> odom` TF - map_frame: "map" - odometry_frame: "odom" - area_memory_db_path: "" - area_memory: false # Enable to detect loop closure - reset_odom_with_loop_closure: false # Re-initialize odometry to the last valid pose when loop closure happens (reset camera odometry drift) - depth_min_range: 0.0 # Set this value for removing fixed zones of the robot in the FoV of the camerafrom the visual odometry evaluation - set_as_static: false # If 'true' the camera will be static and not move in the environment - set_gravity_as_origin: false # If 'true' align the positional tracking world to imu gravity measurement. Keep the yaw from the user initial pose. - floor_alignment: false # Enable to automatically calculate camera/floor offset - initial_base_pose: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0] # Initial position of the `camera_link` frame in the map -> [X, Y, Z, R, P, Y] - path_pub_rate: 2.0 # [DYNAMIC] - Camera trajectory publishing frequency - path_max_count: -1 # use '-1' for unlimited path size - two_d_mode: false # Force navigation on a plane. If true the Z value will be fixed to 'fixed_z_value', roll and pitch to zero - fixed_z_value: 0.00 # Value to be used for Z coordinate if `two_d_mode` is true - transform_time_offset: 0.0 # The value added to the timestamp of `map->odom` and `odom->camera_link` transform being generated - - gnss_fusion: - gnss_fusion_enabled: false # fuse 'sensor_msg/NavSatFix' message information into pose data - gnss_fix_topic: "/fix" # Name of the GNSS topic of type NavSatFix to subscribe [Default: '/gps/fix'] - gnss_zero_altitude: false # Set to `true` to ignore GNSS altitude information - h_covariance_mul: 1.0 # Multiplier factor to be applied to horizontal covariance of the received fix (plane X/Y) - v_covariance_mul: 1.0 # Multiplier factor to be applied to vertical covariance of the received fix (Z axis) - publish_utm_tf: false # Publish `utm` -> `map` TF - broadcast_utm_transform_as_parent_frame: false # if 'true' publish `utm` -> `map` TF, otherwise `map` -> `utm` - enable_reinitialization: false # determines whether reinitialization should be performed between GNSS and VIO fusion when a significant disparity is detected between GNSS data and the current fusion data. It becomes particularly crucial during prolonged GNSS signal loss scenarios. - enable_rolling_calibration: true # If this parameter is set to true, the fusion algorithm will used a rough VIO / GNSS calibration at first and then refine it. This allow you to quickly get a fused position. - enable_translation_uncertainty_target: false # When this parameter is enabled (set to true), the calibration process between GNSS and VIO accounts for the uncertainty in the determined translation, thereby facilitating the calibration termination. The maximum allowable uncertainty is controlled by the 'target_translation_uncertainty' parameter. - gnss_vio_reinit_threshold: 5.0 # determines the threshold for GNSS/VIO reinitialization. If the fused position deviates beyond out of the region defined by the product of the GNSS covariance and the gnss_vio_reinit_threshold, a reinitialization will be triggered. - target_translation_uncertainty: 10e-2 # defines the target translation uncertainty at which the calibration process between GNSS and VIO concludes. By default, the threshold is set at 10 centimeters. - target_yaw_uncertainty: 1e-2 # defines the target yaw uncertainty at which the calibration process between GNSS and VIO concludes. The unit of this parameter is in radian. By default, the threshold is set at 0.1 radians. - - mapping: - mapping_enabled: false # True to enable mapping and fused point cloud pubblication - resolution: 0.05 # maps resolution in meters [min: 0.01f - max: 0.2f] - max_mapping_range: 5.0 # maximum depth range while mapping in meters (-1 for automatic calculation) [2.0, 20.0] - fused_pointcloud_freq: 1.0 # frequency of the publishing of the fused colored point cloud - clicked_point_topic: "/clicked_point" # Topic published by Rviz when a point of the cloud is clicked. Used for plane detection - pd_max_distance_threshold: 0.15 # Plane detection: controls the spread of plane by checking the position difference. - pd_normal_similarity_threshold: 15.0 # Plane detection: controls the spread of plane by checking the angle difference. - - sensors: - publish_imu_tf: false # [usually overwritten by launch file] enable/disable the IMU TF broadcasting - sensors_image_sync: false # Synchronize Sensors messages with latest published video/depth message - sensors_pub_rate: 200. # frequency of publishing of sensors data. MAX: 400. - MIN: grab rate - - object_detection: - od_enabled: false # True to enable Object Detection - model: "MULTI_CLASS_BOX_MEDIUM" # 'MULTI_CLASS_BOX_FAST', 'MULTI_CLASS_BOX_MEDIUM', 'MULTI_CLASS_BOX_ACCURATE', 'PERSON_HEAD_BOX_FAST', 'PERSON_HEAD_BOX_ACCURATE' - allow_reduced_precision_inference: true # Allow inference to run at a lower precision to improve runtime and memory usage - max_range: 20.0 # [m] Defines a upper depth range for detections - confidence_threshold: 50.0 # [DYNAMIC] - Minimum value of the detection confidence of an object [0,99] - prediction_timeout: 0.5 # During this time [sec], the object will have OK state even if it is not detected. Set this parameter to 0 to disable SDK predictions - filtering_mode: 1 # '0': NONE - '1': NMS3D - '2': NMS3D_PER_CLASS - mc_people: true # [DYNAMIC] - Enable/disable the detection of persons for 'MULTI_CLASS_X' models - mc_vehicle: true # [DYNAMIC] - Enable/disable the detection of vehicles for 'MULTI_CLASS_X' models - mc_bag: true # [DYNAMIC] - Enable/disable the detection of bags for 'MULTI_CLASS_X' models - mc_animal: true # [DYNAMIC] - Enable/disable the detection of animals for 'MULTI_CLASS_X' models - mc_electronics: true # [DYNAMIC] - Enable/disable the detection of electronic devices for 'MULTI_CLASS_X' models - mc_fruit_vegetable: true # [DYNAMIC] - Enable/disable the detection of fruits and vegetables for 'MULTI_CLASS_X' models - mc_sport: true # [DYNAMIC] - Enable/disable the detection of sport-related objects for 'MULTI_CLASS_X' models - - body_tracking: - bt_enabled: false # True to enable Body Tracking - model: "HUMAN_BODY_MEDIUM" # 'HUMAN_BODY_FAST', 'HUMAN_BODY_MEDIUM', 'HUMAN_BODY_ACCURATE' - body_format: "BODY_38" # 'BODY_18','BODY_34','BODY_38','BODY_70' - allow_reduced_precision_inference: false # Allow inference to run at a lower precision to improve runtime and memory usage - max_range: 20.0 # [m] Defines a upper depth range for detections - body_kp_selection: "FULL" # 'FULL', 'UPPER_BODY' - enable_body_fitting: false # Defines if the body fitting will be applied - enable_tracking: true # Defines if the object detection will track objects across images flow - prediction_timeout_s: 0.5 # During this time [sec], the skeleton will have OK state even if it is not detected. Set this parameter to 0 to disable SDK predictions - confidence_threshold: 50.0 # [DYNAMIC] - Minimum value of the detection confidence of skeleton key points [0,99] - minimum_keypoints_threshold: 5 # [DYNAMIC] - Minimum number of skeleton key points to be detected for a valid skeleton - - stream_server: - stream_enabled: false # enable the streaming server when the camera is open - codec: 'H264' # different encoding types for image streaming: 'H264', 'H265' - port: 30000 # Port used for streaming. Port must be an even number. Any odd number will be rejected. - bitrate: 12500 # [1000 - 60000] Streaming bitrate (in Kbits/s) used for streaming. See https://www.stereolabs.com/docs/api/structsl_1_1StreamingParameters.html#a873ba9440e3e9786eb1476a3bfa536d0 - gop_size: -1 # [max 256] The GOP size determines the maximum distance between IDR/I-frames. Very high GOP size will result in slightly more efficient compression, especially on static scenes. But latency will increase. - adaptative_bitrate: false # Bitrate will be adjusted depending the number of packet dropped during streaming. If activated, the bitrate can vary between [bitrate/4, bitrate]. - chunk_size: 16084 # [1024 - 65000] Stream buffers are divided into X number of chunks where each chunk is chunk_size bytes long. You can lower chunk_size value if network generates a lot of packet lost: this will generates more chunk for a single image, but each chunk sent will be lighter to avoid inside-chunk corruption. Increasing this value can decrease latency. - target_framerate: 0 # Framerate for the streaming output. This framerate must be below or equal to the camera framerate. Allowed framerates are 15, 30, 60 or 100 if possible. Any other values will be discarded and camera FPS will be taken. - - advanced: # WARNING: do not modify unless you are confident of what you are doing - # Reference documentation: https://man7.org/linux/man-pages/man7/sched.7.html - thread_sched_policy: "SCHED_BATCH" # 'SCHED_OTHER', 'SCHED_BATCH', 'SCHED_FIFO', 'SCHED_RR' - NOTE: 'SCHED_FIFO' and 'SCHED_RR' require 'sudo' - thread_grab_priority: 50 # ONLY with 'SCHED_FIFO' and 'SCHED_RR' - [1 (LOW) z-> 99 (HIGH)] - NOTE: 'sudo' required - thread_sensor_priority: 70 # ONLY with 'SCHED_FIFO' and 'SCHED_RR' - [1 (LOW) z-> 99 (HIGH)] - NOTE: 'sudo' required - thread_pointcloud_priority: 60 # ONLY with 'SCHED_FIFO' and 'SCHED_RR' - [1 (LOW) z-> 99 (HIGH)] - NOTE: 'sudo' required - - debug: - sdk_verbose: 1 # Set the verbose level of the ZED SDK - debug_common: false - debug_sim: false - debug_video_depth: false - debug_camera_controls: false - debug_point_cloud: false - debug_positional_tracking: false - debug_gnss: false - debug_sensors: false - debug_mapping: false - debug_terrain_mapping: false - debug_object_detection: false - debug_body_tracking: false - debug_roi: false - debug_streaming: false - debug_advanced: false diff --git a/robot/ros_ws/src/perception/macvo_ros2/macvo_ros2/config/zedcam_macvo.yaml b/robot/ros_ws/src/perception/macvo_ros2/macvo_ros2/config/zedcam_macvo.yaml deleted file mode 100644 index 76fcf6900..000000000 --- a/robot/ros_ws/src/perception/macvo_ros2/macvo_ros2/config/zedcam_macvo.yaml +++ /dev/null @@ -1,106 +0,0 @@ -Common: - # Some configurations are shared across multiple modules in Odometry, so I write them here. - device: &device cuda - max_depth: &max_depth auto - -Odometry: - name: MACVO_Jetson - args: - # Device directive to the VO system - # NOTE: the system may not follow this device config strictly since some module - # e.g. those rely on PWC-Net, only support running on cuda device. - device: *device - edgewidth: 32 - num_point: 200 # Upper bound of KPs in each frame - - # Match covariance for keypoint on first observation (sub-pixel uncertainty - # caused by the quantized pixel) - match_cov_default: 0.25 - - # Profiling the system using torch, generate chrome json trace file. - profile: false - - mapping: true - - cov: - obs: - type: MatchCovariance - args: - device: *device - kernel_size: 31 - match_cov_default: 0.25 - min_depth_cov: 0.05 - min_flow_cov: 0.25 - - keypoint: - type: CovAwareSelector - args: - device: *device - kernel_size: 7 - mask_width: 32 - max_depth: *max_depth - max_depth_cov: 250.0 - max_match_cov: 100.0 - - mappoint: - type: MappingPointSelector - args: - device: *device - max_depth: 5.0 - max_depth_cov: 0.005 - mask_width: 32 - - frontend: - type: CUDAGraph_FlowFormerCovFrontend - args: - device: *device - weight: ./model/MACVO_FrontendCov.pth - dtype: fp32 - max_flow: -1 - enforce_positive_disparity: false - - motion: - type: StaticMotionModel - args: - - outlier: - type: FilterCompose - args: - filter_args: - - type: CovarianceSanityFilter - args: - - type: SimpleDepthFilter - args: - min_depth: 0.05 - max_depth: *max_depth - - type: LikelyFrontOfCamFilter - args: - - postprocess: - type: MotionInterpolate - args: - - keyframe: - type: AllKeyframe - args: - - optimizer: - type: TwoFramePoseOptimizer - args: - device: cpu - vectorize: true - parallel: true - -Camera: - # NOTE: Since in zedcam_config.yaml we have down-sampled the image by 4, changed - # the intrinsic accordingly. - # fx: 732.527587890625 - # fy: 732.527587890625 - # cx: 982.960693359375 - # cy: 660.4515991210938 - fx: 183.1318969727 - fy: 183.1318969727 - cx: 245.7401733398 - cy: 165.1128997803 - - bl: 0.120022 diff --git a/robot/ros_ws/src/perception/macvo_ros2/macvo_ros2/macvo b/robot/ros_ws/src/perception/macvo_ros2/macvo_ros2/macvo deleted file mode 160000 index 8683b532d..000000000 --- a/robot/ros_ws/src/perception/macvo_ros2/macvo_ros2/macvo +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 8683b532d6f6e8981f6b1a646fb5f52f02c1ea61 diff --git a/robot/ros_ws/src/perception/macvo_ros2/macvo_ros2/macvo_node.py b/robot/ros_ws/src/perception/macvo_ros2/macvo_ros2/macvo_node.py deleted file mode 100644 index 637e2bb15..000000000 --- a/robot/ros_ws/src/perception/macvo_ros2/macvo_ros2/macvo_node.py +++ /dev/null @@ -1,256 +0,0 @@ -import rclpy -import torch -import pypose as pp - -from rclpy.node import Node -from sensor_msgs.msg import Image, PointCloud, CameraInfo -from geometry_msgs.msg import PoseStamped -from nav_msgs.msg import Odometry -from message_filters import ApproximateTimeSynchronizer, Subscriber -from ament_index_python.packages import get_package_share_directory -from builtin_interfaces.msg import Time - -from pathlib import Path -from typing import TYPE_CHECKING -import os, sys -import logging - -from .DispartyPublisher import DisparityPublisher -from .MessageFactory import to_stamped_pose, from_image, to_pointcloud, to_image, to_nav_msgs_odmetry -from sensor_interfaces.srv import GetCameraParams - -# Add the src directory to the Python path -src_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "macvo")) -sys.path.insert(0, src_path) -if TYPE_CHECKING: - # To make static type checker happy : ) - from Odometry.MACVO import MACVO - from DataLoader import StereoFrame, StereoData, SmartResizeFrame - from Utility.Config import load_config -else: - import DataLoader - from Odometry.MACVO import MACVO - from DataLoader import StereoFrame, StereoData, SmartResizeFrame - from Utility.Config import load_config - - -PACKAGE_NAME = "macvo_ros2" - - -class MacvoNode(Node): - def __init__(self) -> None: - super().__init__("macvo_node") - self.coord_frame = "macvo_ned" # Coordinate frame for the camera, in NED (North-East-Down) convention - self.frame_id = 0 # Frame ID - self.init_time = None # ROS2 time stamp - self.get_logger().set_level(logging.INFO) - self.get_logger().info(f"{os.getcwd()}") - self.declared_parameters = set() - - self.coord_frame = self.get_string_param("coordinate_frame") - self.frame_id = 0 # Frame ID - - # Load the Camera model ------------------------------------ - - # Declare subscriptions and publishers ---------------- - # Subscriptions - self.imageL_sub = Subscriber( - self, Image, self.get_string_param("imageL_sub_topic"), qos_profile=1 - ) - self.imageR_sub = Subscriber( - self, Image, self.get_string_param("imageR_sub_topic"), qos_profile=1 - ) - self.sync_stereo = ApproximateTimeSynchronizer( - [self.imageL_sub, self.imageR_sub], queue_size=2, slop=0.1 - ) - self.sync_stereo.registerCallback(self.receive_stereo) - - # camera info subscriber with callback - self.camera_info_sub = self.create_subscription( - CameraInfo, - "camera_info", - self.get_camera_params, - qos_profile=1, - ) - - self.camera_info = None - - # Publishers - # self.pose_send = self.create_publisher( - # PoseStamped, self.get_string_param("pose_pub_topic"), qos_profile=1 - # ) - self.odom_send = self.create_publisher( - Odometry, self.get_string_param("odom_pub_topic"), qos_profile=1 - ) - - self.map_send = self.create_publisher( - PointCloud, self.get_string_param("point_pub_topic"), qos_profile=1 - ) - # self.img_send = self.create_publisher( - # Image, self.get_string_param("img_pub_topic"), qos_profile=1 - # ) - # End - - # Load the MACVO model ------------------------------------ - macvo_config_path = self.get_string_param("macvo_config") - self.get_logger().info( - f"Loading macvo model from {macvo_config_path}, this might take a while..." - ) - cfg, _ = load_config(Path(macvo_config_path)) - - original_cwd = os.getcwd() - try: - os.chdir(get_package_share_directory(PACKAGE_NAME)) - self.get_logger().info(get_package_share_directory(PACKAGE_NAME)) - self.odometry = MACVO[StereoFrame].from_config(cfg) - self.odometry.register_on_optimize_finish(self.publish_data) - finally: - os.chdir(original_cwd) - - # Publish disparity if needed. - self.disparity_publisher = DisparityPublisher( - self, - self.odometry.Frontend, - publish_topic=self.get_string_param("disp_pub_topic"), - frame_id=self.coord_frame, - ) - self.odometry.Frontend = self.disparity_publisher - # End - - self.time, self.prev_time = None, None - self.get_logger().info("macvo initialized") - - def get_integer_param(self, parameter_name: str) -> int: - if parameter_name not in self.declared_parameters: - self.declare_parameter(parameter_name, rclpy.Parameter.Type.INTEGER) - self.declared_parameters.add(parameter_name) - return self.get_parameter(parameter_name).get_parameter_value().integer_value - - def get_string_param(self, parameter_name: str) -> str: - if parameter_name not in self.declared_parameters: - self.declare_parameter(parameter_name, rclpy.Parameter.Type.STRING) - self.declared_parameters.add(parameter_name) - return self.get_parameter(parameter_name).get_parameter_value().string_value - - def get_camera_params(self, msg): - if self.camera_info is not None: - return # Already received camera info - - self.camera_info = msg - self.get_logger().info(f"Camera info received: {self.camera_info.width}x{self.camera_info.height}") - - # calculate the baseline from the camera P matrix - self.baseline = abs(self.camera_info.p[3] / self.camera_info.p[0]) # in meters - self.get_logger().info(f"Camera baseline: {self.baseline} m") - - def publish_data(self, system: MACVO): - # Latest pose - pose = pp.SE3(system.graph.frames.data["pose"][-1]) - self.get_logger().debug(f"Publish {pose}") - time_ns = int(system.graph.frames.data["time_ns"][-1].item()) - - time = Time() - time.sec = (time_ns // 1_000_000_000) + self.init_time.sec - time.nanosec = (time_ns % 1_000_000_000) + self.init_time.nanosec - - # pose_msg = to_stamped_pose(pose, self.coord_frame, time) - odom_msg = to_nav_msgs_odmetry(pose, self.coord_frame, time) - - # Latest map - if system.mapping: - points = system.graph.get_frame2map(system.graph.frames[-2:-1]) - else: - points = system.graph.get_match2point( - system.graph.get_frame2match(system.graph.frames[-1:]) - ) - - map_pc_msg = to_pointcloud( - position=points.data["pos_Tw"], - keypoints=None, - frame_id=self.coord_frame, - colors=points.data["color"], - time=time, - ) - - # self.pose_send.publish(pose_msg) - self.odom_send.publish(odom_msg) - self.map_send.publish(map_pc_msg) - - @staticmethod - def time_to_ns(time: Time) -> int: - return int(time.sec * 1e9) + time.nanosec - - def receive_stereo(self, msg_imageL: Image, msg_imageR: Image) -> None: - if self.camera_info is None: - # throttle the log messages with rospy throttle - self.get_logger().warn("Skipped a frame since camera info is not received yet", throttle_duration_sec=5) - return - - self.get_logger().debug(f"Frame {self.frame_id}") - imageL, timestamp = from_image(msg_imageL), msg_imageL.header.stamp - imageR = from_image(msg_imageR) - if self.init_time is None: - self.init_time = timestamp - elapsed = int(self.time_to_ns(timestamp) - self.time_to_ns(self.init_time)) - self.disparity_publisher.curr_timestamp = timestamp - - # Instantiate a frame and scale to the desired height & width - stereo_frame = SmartResizeFrame( - { - "height": self.get_integer_param("inference_dim_u"), - "width": self.get_integer_param("inference_dim_v"), - "interp": "bilinear", - } - )( - StereoFrame( - idx=torch.tensor([self.frame_id], dtype=torch.long), - time_ns=[elapsed], - stereo=StereoData( - T_BS=pp.identity_SE3(1, dtype=torch.float64), - K=torch.tensor( - [ - [ - [self.camera_info.k[0], 0.0, self.camera_info.k[2]], - [0.0, self.camera_info.k[4], self.camera_info.k[5]], - [0.0, 0.0, 1.0], - ] - ], - dtype=torch.float, - ), - baseline=torch.tensor([self.baseline], dtype=torch.float), - time_ns=[elapsed], - height=imageL.shape[0], - width=imageL.shape[1], - imageL=torch.tensor(imageL)[..., :3] - .float() - .permute(2, 0, 1) - .unsqueeze(0) - / 255.0, - imageR=torch.tensor(imageR)[..., :3] - .float() - .permute(2, 0, 1) - .unsqueeze(0) - / 255.0, - ), - ) - ) - self.odometry.run(stereo_frame) - - # Pose-processing - self.frame_id += 1 - - def destroy_node(self): - self.odometry.terminate() - - -def main(): - rclpy.init() - node = MacvoNode() - rclpy.spin(node) - - node.destroy_node() - rclpy.shutdown() - - -if __name__ == "__main__": - main() diff --git a/robot/ros_ws/src/perception/macvo_ros2/macvo_ros2/model/README.md b/robot/ros_ws/src/perception/macvo_ros2/macvo_ros2/model/README.md deleted file mode 100644 index d6aaeb67b..000000000 --- a/robot/ros_ws/src/perception/macvo_ros2/macvo_ros2/model/README.md +++ /dev/null @@ -1 +0,0 @@ -Place your model here! They will be copied to the correct place automatically on `colcon build`. diff --git a/robot/ros_ws/src/perception/macvo_ros2/package.xml b/robot/ros_ws/src/perception/macvo_ros2/package.xml deleted file mode 100644 index bba7e86ed..000000000 --- a/robot/ros_ws/src/perception/macvo_ros2/package.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - macvo_ros2 - 0.0.0 - Metric-aware Covariance for Learning Based Stereo Visual Odometry - Yutian Chen - Please see license term for MACVO/MACVO repository. - - - ament_python - - \ No newline at end of file diff --git a/robot/ros_ws/src/perception/macvo_ros2/resource/macvo_ros2 b/robot/ros_ws/src/perception/macvo_ros2/resource/macvo_ros2 deleted file mode 100644 index e69de29bb..000000000 diff --git a/robot/ros_ws/src/perception/macvo_ros2/setup.cfg b/robot/ros_ws/src/perception/macvo_ros2/setup.cfg deleted file mode 100644 index 1c13f7ccf..000000000 --- a/robot/ros_ws/src/perception/macvo_ros2/setup.cfg +++ /dev/null @@ -1,4 +0,0 @@ -[develop] -script_dir=$base/lib/macvo_ros2 -[install] -install_scripts=$base/lib/macvo_ros2 diff --git a/robot/ros_ws/src/perception/macvo_ros2/setup.py b/robot/ros_ws/src/perception/macvo_ros2/setup.py deleted file mode 100644 index 7a3c6fec5..000000000 --- a/robot/ros_ws/src/perception/macvo_ros2/setup.py +++ /dev/null @@ -1,43 +0,0 @@ -import os -from setuptools import find_packages, setup - -package_name = "macvo_ros2" - - -def package_files(directory): - paths = [] - for path, directories, filenames in os.walk(directory): - for filename in filenames: - paths.append(os.path.join("..", path, filename)) - return paths - - -extra_files = package_files("macvo_ros2/macvo") + package_files("macvo_ros2/config") - -setup( - name=package_name, - version="0.0.0", - packages=find_packages(include=[package_name, f"{package_name}.*"]), - data_files=[ - ("share/ament_index/resource_index/packages", ["resource/" + package_name]), - ("share/" + package_name, ["package.xml"]), - ("share/" + package_name, ["launch/macvo_ros2.launch.xml"]), - ( - "share/" + package_name + "/config", - ["config/interface_config.yaml", "config/MACVO_fast_for_orin.yaml"], - ), - ], - package_data={package_name: extra_files}, - install_requires=["setuptools"], - zip_safe=True, - maintainer="Yutian Chen", - maintainer_email="yutianch@andrew.cmu.edu", - description="ROS2 node wrapper for the MAC-VO", - license="TODO: License declaration", - tests_require=["pytest"], - entry_points={ - "console_scripts": [ - "macvo_node = macvo_ros2.macvo_node:main", - ], - }, -) diff --git a/robot/ros_ws/src/perception/perception_bringup/launch/perception.launch.xml b/robot/ros_ws/src/perception/perception_bringup/launch/perception.launch.xml index 9fe878dd7..374206c3d 100644 --- a/robot/ros_ws/src/perception/perception_bringup/launch/perception.launch.xml +++ b/robot/ros_ws/src/perception/perception_bringup/launch/perception.launch.xml @@ -1,33 +1,13 @@ - - - - - - - - - - - - - - - + diff --git a/robot/ros_ws/src/perception/perception_bringup/scripts/topic_keepalive_node.py b/robot/ros_ws/src/perception/perception_bringup/scripts/topic_keepalive_node.py index 0b1dad273..c57028722 100755 --- a/robot/ros_ws/src/perception/perception_bringup/scripts/topic_keepalive_node.py +++ b/robot/ros_ws/src/perception/perception_bringup/scripts/topic_keepalive_node.py @@ -6,7 +6,9 @@ DDS reader does not count, so without a local subscriber the topic stops publishing and downstream GCS / Foxglove sees nothing. The list mirrors every Topic.Value entry in desktop_bringup/rviz/robot.rviz so disabling -rviz no longer drops topics off the GCS side. +rviz no longer drops topics off the GCS side — except module-owned topics +(e.g. MAC-VO's, provided by the asm_macvo module): trunk's keepalive must +not advertise topics whose publishers live outside trunk. """ import os @@ -15,7 +17,7 @@ from rclpy.node import Node from rclpy.qos import (DurabilityPolicy, HistoryPolicy, QoSProfile, ReliabilityPolicy) -from sensor_msgs.msg import CameraInfo, Image, PointCloud, PointCloud2 +from sensor_msgs.msg import CameraInfo, Image, PointCloud2 from nav_msgs.msg import Odometry, Path from visualization_msgs.msg import Marker, MarkerArray from tf2_msgs.msg import TFMessage @@ -36,16 +38,13 @@ ('sensors/front_stereo/right/image_rect', Image, False, SENSOR_QOS), ('sensors/front_stereo/left/depth_ground_truth', Image, False, SENSOR_QOS), ('sensors/front_stereo/right/depth_ground_truth', Image, False, SENSOR_QOS), - ('perception/macvo/disparity', Image, False, SENSOR_QOS), ('sensors/front_stereo/left/camera_info', CameraInfo, False, DEFAULT_QOS), ('sensors/front_stereo/right/camera_info', CameraInfo, False, DEFAULT_QOS), ('sensors/lidar/point_cloud', PointCloud2, False, SENSOR_QOS), ('perception/stereo_image_proc/point_cloud', PointCloud2, False, SENSOR_QOS), ('droan/expansion_cloud', PointCloud2, False, SENSOR_QOS), ('droan/fg_bg_cloud', PointCloud2, False, SENSOR_QOS), - ('perception/macvo/point_cloud', PointCloud, False, SENSOR_QOS), ('odometry_conversion/odometry', Odometry, False, DEFAULT_QOS), - ('macvo/odometry', Odometry, False, DEFAULT_QOS), ('global_plan', Path, False, DEFAULT_QOS), ('vdb_mapping/vdb_map_visualization', Marker, False, DEFAULT_QOS), ('droan/frustum', Marker, False, DEFAULT_QOS), diff --git a/stacks/full_macvo/README.md b/stacks/full_macvo/README.md index 44f9a4be7..63b632f10 100644 --- a/stacks/full_macvo/README.md +++ b/stacks/full_macvo/README.md @@ -4,14 +4,22 @@ Full autonomy with **MAC-VO** as the disparity source for the local planner. One of the presets absorbing the `local_*.launch.xml` variant explosion into named stacks a few include lines apart (RFC #379 §3). +**Requires: `airstack module add asm_macvo`.** MAC-VO no longer lives in +trunk — the `macvo_ros2` package, its Python/TensorRT dependencies, and the +model weights all ship in the [asm_macvo](https://github.com/castacks/asm_macvo) +module. Until the module is synced (`modules.repos` pins it), +`$(find-pkg-share macvo_ros2)` in this stack's launch file will not resolve +and bring-up fails at the macvo include. + ## What it launches Local layer flattened (E2): identical to [`full_default`](../full_default/README.md) except: -1. Perception is included with `launch_macvo:=true`, so the `macvo_ros2` node - runs and publishes `/$ROBOT_NAME/perception/macvo/disparity` (plus macvo - odometry/pose/point-cloud topics). +1. The module-provided `macvo_ros2/launch/macvo.launch.xml` is included under + the `perception` namespace, so the `macvo_ros2` node runs and publishes + `/$ROBOT_NAME/perception/macvo/{odometry,point_cloud,disparity}` (all + canonical-default args — zero remaps). 2. The `droan_gl.launch.xml` include passes `droan_gl_disparity_topic:=/$ROBOT_NAME/perception/macvo/disparity`, wiring the planner's disparity input to MAC-VO's real output topic. @@ -34,6 +42,11 @@ which was broken three ways: ## How to run ```bash +# One-time: pull the asm_macvo module and build its dependency layer +airstack module add asm_macvo +airstack module sync +airstack module lock --build + airstack up --stack full_macvo --sim isaac --robots 1 airstack ready ``` @@ -49,9 +62,11 @@ airstack ready macvo-only preset can flip that arg once downstream consumers are audited. - MAC-VO is GPU-heavy; expect reduced sim real-time factor on a shared GPU. - No committed equivalence baseline exists for this topology (the legacy - variant never worked), so the first `wiring.md` snapshot IS the baseline. -- `modules.repos` pins no external modules yet; `docker-compose.yaml` is a - stub (trunk compose profiles provide all services). + variant never worked), so the first post-module-swap `wiring.md` snapshot + IS the baseline. +- `modules.repos` pins `asm_macvo`; `docker-compose.yaml` stays an empty stub + until `airstack module lock --build` generates the per-module compose + override. ## wiring.md diff --git a/stacks/full_macvo/docker-compose.yaml b/stacks/full_macvo/docker-compose.yaml index 85c1ef99f..bd645d8b1 100644 --- a/stacks/full_macvo/docker-compose.yaml +++ b/stacks/full_macvo/docker-compose.yaml @@ -1,13 +1,13 @@ -# Per-stack image composition (RFC #379 S6) arrives with this stack's first -# module pins: the P4 machinery (tools/compose_module_layers.py) composes -# per-module dependency layers on top of the trunk base image and emits a -# compose override for `airstack up`. +# Per-stack image composition (RFC #379 S6): modules.repos pins asm_macvo, and +# the P4 machinery (tools/compose_module_layers.py) composes each module's +# dependency layer (its Dockerfile.module) on top of the trunk base image via +# `airstack module lock --build`, emitting a compose override for `airstack up`. # -# full_macvo pins no modules (see modules.repos), so there is nothing to -# compose yet -- the trunk compose profiles (root docker-compose.yaml, -# robot/docker/docker-compose.yaml) provide every service meanwhile (MAC-VO's -# networks are baked into the trunk robot image today; they migrate into a -# module layer when macvo_ros2 moves out of trunk). The empty services map -# keeps this file valid YAML for the stack-anatomy contract -# (tests/meta/test_stack_layout_contract.py). +# MAC-VO's dependencies (torch, TensorRT, model weights, ...) are NO longer +# baked into the trunk robot image — they live in asm_macvo's +# Dockerfile.module. Run `airstack module add asm_macvo && airstack module +# sync && airstack module lock --build` before bringing this stack up. The +# empty services map keeps this file valid YAML for the stack-anatomy contract +# (tests/meta/test_stack_layout_contract.py); the lock step generates the real +# override. services: {} diff --git a/stacks/full_macvo/launch/stack.launch.xml b/stacks/full_macvo/launch/stack.launch.xml index e9db7f292..c40795bec 100644 --- a/stacks/full_macvo/launch/stack.launch.xml +++ b/stacks/full_macvo/launch/stack.launch.xml @@ -3,10 +3,12 @@ Preset that absorbed (and FIXED) the deleted MAC-VO obstacle-avoidance local bringup variant (see README): full autonomy with MAC-VO as the - disparity source for the local planner. Two include-line differences from - full_default/launch/stack.launch.xml: - 1. Perception is included with launch_macvo:=true, so the macvo_ros2 node - runs and publishes /$ROBOT_NAME/perception/macvo/disparity. + disparity source for the local planner. MAC-VO arrives via the asm_macvo + MODULE (`airstack module add asm_macvo`, then `module sync` and the lock + build step; macvo_ros2 no longer lives in trunk — see the README). Two + include-line differences from full_default/launch/stack.launch.xml: + 1. The macvo_ros2 module launch is included under the perception + namespace, so it publishes /$ROBOT_NAME/perception/macvo/disparity. 2. The droan_gl include overrides its disparity arg to point at MAC-VO's real output topic instead of the canonical stereo_image_proc default. (The legacy variant file was broken: it passed arg names local.launch.xml @@ -35,10 +37,16 @@ - - - - + + + + + + + + diff --git a/stacks/full_macvo/modules.repos b/stacks/full_macvo/modules.repos index 1abe4c500..512849856 100644 --- a/stacks/full_macvo/modules.repos +++ b/stacks/full_macvo/modules.repos @@ -8,9 +8,14 @@ # AirStack tooling reads it) declaring the trunk semver range this stack was # tested against. sync warns on mismatch; it never gates. # -# This reference stack pulls no external modules yet: every package it launches -# is trunk-resident (macvo_ros2 lives in robot/ros_ws/src/perception; it is a -# prime candidate to migrate out as a module later). +# NOTE(submodules): asm_macvo's macvo_ros2/macvo_ros2/macvo is a git submodule +# of that repo (the MAC-VO network). `airstack module sync` imports with +# `vcs import --recursive`, so the submodule arrives automatically. airstack_compat: ">=0.19.0-alpha.18 <0.21.0" -repositories: {} +repositories: + asm_macvo: + type: git + # Placeholder — update when asm_macvo is pushed and tagged (TRUNK_REMOVAL.md §0). + url: git@github.com:castacks/asm_macvo.git + version: v0.1.0 x-local-modules: [] From 343fc5c4a4fb93d2215646528bffd96549a7a80e Mon Sep 17 00:00:00 2001 From: Andrew Jong Date: Thu, 20 Aug 2026 20:29:45 -0400 Subject: [PATCH 3/8] M3(rfc-379): regen wiring baselines post-MACVO removal; exclude sim render-pipeline nodes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - keepalive macvo edges (3) gone from legacy golden + full_default + full_droan_cpu — verified the diffs contained exactly those edges before re-blessing; legacy and full_default remain graph-identical (83/402) - normalize_graph now excludes Isaac SDG render-pipeline writer nodes and the PX4MultirotorGraph clock publisher: their visibility on the robot domain is timing-dependent (join the graph when the render pipeline spins up), which made goldens flaky; RFC #380 §1 later normalizes sim sensor endpoints to vehicle-manifest ids properly - all three baselines verified drift-clean on live bring-ups Co-Authored-By: Claude Fable 5 --- stacks/full_default/wiring.md | 134 +++++------------ stacks/full_droan_cpu/wiring.md | 136 +++++------------ .../wiring/full_default.isaacsim.1robot.md | 142 +++++------------- tests/wiring_snapshot.py | 16 +- 4 files changed, 121 insertions(+), 307 deletions(-) diff --git a/stacks/full_default/wiring.md b/stacks/full_default/wiring.md index ca8e779e8..ff7bf8785 100644 --- a/stacks/full_default/wiring.md +++ b/stacks/full_default/wiring.md @@ -1,10 +1,10 @@ # Wiring snapshot: full_default - **generated-by**: tests/system/test_wiring_snapshot.py -- **date**: 2026-08-20 22:14:34 +- **date**: 2026-08-21 00:23:27 - **sim**: isaacsim - **num_robots**: 1 -- **source-sha**: d02a3467b72d +- **source-sha**: 19ec8184f64d ```mermaid graph LR @@ -403,7 +403,6 @@ graph LR d169(["/robot_1/interface/torque_thrust_command (no publishers)"]) -->|"/robot_1/interface/torque_thrust_command
TorqueThrust"| n70 d170(["/robot_1/interface/velocity_command (no publishers)"]) -->|"/robot_1/interface/velocity_command
TwistStamped"| n70 d171(["/robot_1/joint_states (no publishers)"]) -->|"/robot_1/joint_states
JointState"| n75 - d172(["/robot_1/macvo/odometry (no publishers)"]) -->|"/robot_1/macvo/odometry
Odometry"| n78 n71 -->|"/robot_1/odometry_conversion/odometry
Odometry"| n2 n71 -->|"/robot_1/odometry_conversion/odometry
Odometry"| n3 n71 -->|"/robot_1/odometry_conversion/odometry
Odometry"| n69 @@ -412,55 +411,53 @@ graph LR n71 -->|"/robot_1/odometry_conversion/odometry
Odometry"| n78 n71 -->|"/robot_1/odometry_conversion/odometry
Odometry"| n79 n71 -->|"/robot_1/odometry_conversion/odometry
Odometry"| n80 - d173(["/robot_1/perception/macvo/disparity (no publishers)"]) -->|"/robot_1/perception/macvo/disparity
Image"| n78 - d174(["/robot_1/perception/macvo/point_cloud (no publishers)"]) -->|"/robot_1/perception/macvo/point_cloud
PointCloud"| n78 n72 -->|"/robot_1/perception/stereo_image_proc/disparity
DisparityImage"| n4 n72 -->|"/robot_1/perception/stereo_image_proc/disparity
DisparityImage"| n73 n73 -->|"/robot_1/perception/stereo_image_proc/point_cloud
PointCloud2"| n78 - n74 -->|"/robot_1/random_walk_node/goal_point_viz
Marker"| d175(["/robot_1/random_walk_node/goal_point_viz (no subscribers)"]) - n74 -->|"/robot_1/random_walk_node/traj_viz
Marker"| d176(["/robot_1/random_walk_node/traj_viz (no subscribers)"]) - n75 -->|"/robot_1/robot_description
String"| d177(["/robot_1/robot_description (no subscribers)"]) - d178(["/robot_1/sensors/front_stereo/left/camera_info (no publishers)"]) -->|"/robot_1/sensors/front_stereo/left/camera_info
CameraInfo"| n72 - d178 -->|"/robot_1/sensors/front_stereo/left/camera_info
CameraInfo"| n73 - d178 -->|"/robot_1/sensors/front_stereo/left/camera_info
CameraInfo"| n78 - d179(["/robot_1/sensors/front_stereo/left/depth_ground_truth (no publishers)"]) -->|"/robot_1/sensors/front_stereo/left/depth_ground_truth
Image"| n78 - d180(["/robot_1/sensors/front_stereo/left/image_rect (no publishers)"]) -->|"/robot_1/sensors/front_stereo/left/image_rect
Image"| n72 - d180 -->|"/robot_1/sensors/front_stereo/left/image_rect
Image"| n73 - d180 -->|"/robot_1/sensors/front_stereo/left/image_rect
Image"| n78 - d181(["/robot_1/sensors/front_stereo/right/camera_info (no publishers)"]) -->|"/robot_1/sensors/front_stereo/right/camera_info
CameraInfo"| n4 - d181 -->|"/robot_1/sensors/front_stereo/right/camera_info
CameraInfo"| n72 - d181 -->|"/robot_1/sensors/front_stereo/right/camera_info
CameraInfo"| n73 - d181 -->|"/robot_1/sensors/front_stereo/right/camera_info
CameraInfo"| n78 - d182(["/robot_1/sensors/front_stereo/right/depth_ground_truth (no publishers)"]) -->|"/robot_1/sensors/front_stereo/right/depth_ground_truth
Image"| n78 - d183(["/robot_1/sensors/front_stereo/right/image_rect (no publishers)"]) -->|"/robot_1/sensors/front_stereo/right/image_rect
Image"| n72 - d183 -->|"/robot_1/sensors/front_stereo/right/image_rect
Image"| n78 - d184(["/robot_1/sensors/lidar/point_cloud (no publishers)"]) -->|"/robot_1/sensors/lidar/point_cloud
PointCloud2"| n78 + n74 -->|"/robot_1/random_walk_node/goal_point_viz
Marker"| d172(["/robot_1/random_walk_node/goal_point_viz (no subscribers)"]) + n74 -->|"/robot_1/random_walk_node/traj_viz
Marker"| d173(["/robot_1/random_walk_node/traj_viz (no subscribers)"]) + n75 -->|"/robot_1/robot_description
String"| d174(["/robot_1/robot_description (no subscribers)"]) + d175(["/robot_1/sensors/front_stereo/left/camera_info (no publishers)"]) -->|"/robot_1/sensors/front_stereo/left/camera_info
CameraInfo"| n72 + d175 -->|"/robot_1/sensors/front_stereo/left/camera_info
CameraInfo"| n73 + d175 -->|"/robot_1/sensors/front_stereo/left/camera_info
CameraInfo"| n78 + d176(["/robot_1/sensors/front_stereo/left/depth_ground_truth (no publishers)"]) -->|"/robot_1/sensors/front_stereo/left/depth_ground_truth
Image"| n78 + d177(["/robot_1/sensors/front_stereo/left/image_rect (no publishers)"]) -->|"/robot_1/sensors/front_stereo/left/image_rect
Image"| n72 + d177 -->|"/robot_1/sensors/front_stereo/left/image_rect
Image"| n73 + d177 -->|"/robot_1/sensors/front_stereo/left/image_rect
Image"| n78 + d178(["/robot_1/sensors/front_stereo/right/camera_info (no publishers)"]) -->|"/robot_1/sensors/front_stereo/right/camera_info
CameraInfo"| n4 + d178 -->|"/robot_1/sensors/front_stereo/right/camera_info
CameraInfo"| n72 + d178 -->|"/robot_1/sensors/front_stereo/right/camera_info
CameraInfo"| n73 + d178 -->|"/robot_1/sensors/front_stereo/right/camera_info
CameraInfo"| n78 + d179(["/robot_1/sensors/front_stereo/right/depth_ground_truth (no publishers)"]) -->|"/robot_1/sensors/front_stereo/right/depth_ground_truth
Image"| n78 + d180(["/robot_1/sensors/front_stereo/right/image_rect (no publishers)"]) -->|"/robot_1/sensors/front_stereo/right/image_rect
Image"| n72 + d180 -->|"/robot_1/sensors/front_stereo/right/image_rect
Image"| n78 + d181(["/robot_1/sensors/lidar/point_cloud (no publishers)"]) -->|"/robot_1/sensors/lidar/point_cloud
PointCloud2"| n78 n76 -->|"/robot_1/sensors/ouster/point_cloud
PointCloud2"| n81 - d185(["/robot_1/sensors/ouster/point_cloud_raw (no publishers)"]) -->|"/robot_1/sensors/ouster/point_cloud_raw
PointCloud2"| n76 - n77 -->|"/robot_1/takeoff_landing_planner/is_airborne
Bool"| d186(["/robot_1/takeoff_landing_planner/is_airborne (no subscribers)"]) - d187(["/robot_1/takeoff_landing_planner/trajectory_completion_percentage (no publishers)"]) -->|"/robot_1/takeoff_landing_planner/trajectory_completion_percentage
Float32"| n77 - n80 -->|"/robot_1/trajectory_controller/closest_point
Odometry"| d188(["/robot_1/trajectory_controller/closest_point (no subscribers)"]) + d182(["/robot_1/sensors/ouster/point_cloud_raw (no publishers)"]) -->|"/robot_1/sensors/ouster/point_cloud_raw
PointCloud2"| n76 + n77 -->|"/robot_1/takeoff_landing_planner/is_airborne
Bool"| d183(["/robot_1/takeoff_landing_planner/is_airborne (no subscribers)"]) + d184(["/robot_1/takeoff_landing_planner/trajectory_completion_percentage (no publishers)"]) -->|"/robot_1/takeoff_landing_planner/trajectory_completion_percentage
Float32"| n77 + n80 -->|"/robot_1/trajectory_controller/closest_point
Odometry"| d185(["/robot_1/trajectory_controller/closest_point (no subscribers)"]) n80 -->|"/robot_1/trajectory_controller/look_ahead
Odometry"| n4 n80 -->|"/robot_1/trajectory_controller/projected_drone_pose
PoseStamped"| n69 - n80 -->|"/robot_1/trajectory_controller/tracking_error
Float32"| d189(["/robot_1/trajectory_controller/tracking_error (no subscribers)"]) + n80 -->|"/robot_1/trajectory_controller/tracking_error
Float32"| d186(["/robot_1/trajectory_controller/tracking_error (no subscribers)"]) n80 -->|"/robot_1/trajectory_controller/tracking_point
Odometry"| n3 n80 -->|"/robot_1/trajectory_controller/tracking_point
Odometry"| n4 n80 -->|"/robot_1/trajectory_controller/tracking_point
Odometry"| n69 n80 -->|"/robot_1/trajectory_controller/tracking_point
Odometry"| n77 - n80 -->|"/robot_1/trajectory_controller/tracking_point_velocity_magnitude
Float32"| d190(["/robot_1/trajectory_controller/tracking_point_velocity_magnitude (no subscribers)"]) - n80 -->|"/robot_1/trajectory_controller/traj_drone_point
Odometry"| d191(["/robot_1/trajectory_controller/traj_drone_point (no subscribers)"]) + n80 -->|"/robot_1/trajectory_controller/tracking_point_velocity_magnitude
Float32"| d187(["/robot_1/trajectory_controller/tracking_point_velocity_magnitude (no subscribers)"]) + n80 -->|"/robot_1/trajectory_controller/traj_drone_point
Odometry"| d188(["/robot_1/trajectory_controller/traj_drone_point (no subscribers)"]) n80 -->|"/robot_1/trajectory_controller/trajectory_completion_percentage
Float32"| n79 n80 -->|"/robot_1/trajectory_controller/trajectory_controller_debug_markers
MarkerArray"| n78 n77 -->|"/robot_1/trajectory_controller/trajectory_override
TrajectoryXYZVYaw"| n80 n79 -->|"/robot_1/trajectory_controller/trajectory_override
TrajectoryXYZVYaw"| n80 n4 -->|"/robot_1/trajectory_controller/trajectory_segment_to_add
TrajectoryXYZVYaw"| n80 - n80 -->|"/robot_1/trajectory_controller/trajectory_time
Float32"| d192(["/robot_1/trajectory_controller/trajectory_time (no subscribers)"]) + n80 -->|"/robot_1/trajectory_controller/trajectory_time
Float32"| d189(["/robot_1/trajectory_controller/trajectory_time (no subscribers)"]) n80 -->|"/robot_1/trajectory_controller/trajectory_vis
MarkerArray"| n78 - n80 -->|"/robot_1/trajectory_controller/virtual_tracking_point
Odometry"| d193(["/robot_1/trajectory_controller/virtual_tracking_point (no subscribers)"]) - n81 -->|"/robot_1/vdb_mapping/vdb_map_overwrites
UpdateGrid"| d194(["/robot_1/vdb_mapping/vdb_map_overwrites (no subscribers)"]) - n81 -->|"/robot_1/vdb_mapping/vdb_map_pointcloud
PointCloud2"| d195(["/robot_1/vdb_mapping/vdb_map_pointcloud (no subscribers)"]) - n81 -->|"/robot_1/vdb_mapping/vdb_map_sections
UpdateGrid"| d196(["/robot_1/vdb_mapping/vdb_map_sections (no subscribers)"]) - n81 -->|"/robot_1/vdb_mapping/vdb_map_updates
UpdateGrid"| d197(["/robot_1/vdb_mapping/vdb_map_updates (no subscribers)"]) + n80 -->|"/robot_1/trajectory_controller/virtual_tracking_point
Odometry"| d190(["/robot_1/trajectory_controller/virtual_tracking_point (no subscribers)"]) + n81 -->|"/robot_1/vdb_mapping/vdb_map_overwrites
UpdateGrid"| d191(["/robot_1/vdb_mapping/vdb_map_overwrites (no subscribers)"]) + n81 -->|"/robot_1/vdb_mapping/vdb_map_pointcloud
PointCloud2"| d192(["/robot_1/vdb_mapping/vdb_map_pointcloud (no subscribers)"]) + n81 -->|"/robot_1/vdb_mapping/vdb_map_sections
UpdateGrid"| d193(["/robot_1/vdb_mapping/vdb_map_sections (no subscribers)"]) + n81 -->|"/robot_1/vdb_mapping/vdb_map_updates
UpdateGrid"| d194(["/robot_1/vdb_mapping/vdb_map_updates (no subscribers)"]) n81 -->|"/robot_1/vdb_mapping/vdb_map_visualization
Marker"| n74 n81 -->|"/robot_1/vdb_mapping/vdb_map_visualization
Marker"| n78 n34 -->|"/tf
TFMessage"| n69 @@ -4289,18 +4286,6 @@ graph LR "type": "sensor_msgs/msg/JointState" }, { -"dir": "sub", -"node": "/robot_1/topic_keepalive", -"qos_profile": { -"depth": "UNKNOWN", -"durability": "VOLATILE", -"history": "UNKNOWN", -"reliability": "RELIABLE" -}, -"topic": "/robot_1/macvo/odometry", -"type": "nav_msgs/msg/Odometry" -}, -{ "dir": "pub", "node": "/robot_1/odometry_conversion/odometry_conversion", "qos_profile": { @@ -4409,30 +4394,6 @@ graph LR "type": "nav_msgs/msg/Odometry" }, { -"dir": "sub", -"node": "/robot_1/topic_keepalive", -"qos_profile": { -"depth": "UNKNOWN", -"durability": "VOLATILE", -"history": "UNKNOWN", -"reliability": "BEST_EFFORT" -}, -"topic": "/robot_1/perception/macvo/disparity", -"type": "sensor_msgs/msg/Image" -}, -{ -"dir": "sub", -"node": "/robot_1/topic_keepalive", -"qos_profile": { -"depth": "UNKNOWN", -"durability": "VOLATILE", -"history": "UNKNOWN", -"reliability": "BEST_EFFORT" -}, -"topic": "/robot_1/perception/macvo/point_cloud", -"type": "sensor_msgs/msg/PointCloud" -}, -{ "dir": "pub", "node": "/robot_1/perception/stereo_image_proc/disparity_node", "qos_profile": { @@ -7240,15 +7201,6 @@ graph LR }, "type": "sensor_msgs/msg/JointState" }, -"/robot_1/macvo/odometry": { -"qos": { -"depth": "UNKNOWN", -"durability": "VOLATILE", -"history": "UNKNOWN", -"reliability": "RELIABLE" -}, -"type": "nav_msgs/msg/Odometry" -}, "/robot_1/odometry_conversion/odometry": { "qos": { "depth": "UNKNOWN", @@ -7258,24 +7210,6 @@ graph LR }, "type": "nav_msgs/msg/Odometry" }, -"/robot_1/perception/macvo/disparity": { -"qos": { -"depth": "UNKNOWN", -"durability": "VOLATILE", -"history": "UNKNOWN", -"reliability": "BEST_EFFORT" -}, -"type": "sensor_msgs/msg/Image" -}, -"/robot_1/perception/macvo/point_cloud": { -"qos": { -"depth": "UNKNOWN", -"durability": "VOLATILE", -"history": "UNKNOWN", -"reliability": "BEST_EFFORT" -}, -"type": "sensor_msgs/msg/PointCloud" -}, "/robot_1/perception/stereo_image_proc/disparity": { "qos": { "depth": "UNKNOWN", @@ -7353,7 +7287,7 @@ graph LR "depth": "UNKNOWN", "durability": "VOLATILE", "history": "UNKNOWN", -"reliability": "RELIABLE" +"reliability": "BEST_EFFORT" }, "type": "sensor_msgs/msg/CameraInfo" }, diff --git a/stacks/full_droan_cpu/wiring.md b/stacks/full_droan_cpu/wiring.md index 92ab3380a..f10dc9655 100644 --- a/stacks/full_droan_cpu/wiring.md +++ b/stacks/full_droan_cpu/wiring.md @@ -1,10 +1,10 @@ -# Wiring snapshot: full_droan_cpu +# Wiring snapshot: full_default - **generated-by**: tests/system/test_wiring_snapshot.py -- **date**: 2026-08-20 22:16:26 +- **date**: 2026-08-21 00:24:30 - **sim**: isaacsim - **num_robots**: 1 -- **source-sha**: d02a3467b72d +- **source-sha**: 19ec8184f64d ```mermaid graph LR @@ -411,7 +411,6 @@ graph LR d169(["/robot_1/interface/torque_thrust_command (no publishers)"]) -->|"/robot_1/interface/torque_thrust_command
TorqueThrust"| n71 d170(["/robot_1/interface/velocity_command (no publishers)"]) -->|"/robot_1/interface/velocity_command
TwistStamped"| n71 d171(["/robot_1/joint_states (no publishers)"]) -->|"/robot_1/joint_states
JointState"| n76 - d172(["/robot_1/macvo/odometry (no publishers)"]) -->|"/robot_1/macvo/odometry
Odometry"| n79 n72 -->|"/robot_1/odometry_conversion/odometry
Odometry"| n2 n72 -->|"/robot_1/odometry_conversion/odometry
Odometry"| n3 n72 -->|"/robot_1/odometry_conversion/odometry
Odometry"| n70 @@ -420,56 +419,54 @@ graph LR n72 -->|"/robot_1/odometry_conversion/odometry
Odometry"| n79 n72 -->|"/robot_1/odometry_conversion/odometry
Odometry"| n80 n72 -->|"/robot_1/odometry_conversion/odometry
Odometry"| n81 - d173(["/robot_1/perception/macvo/disparity (no publishers)"]) -->|"/robot_1/perception/macvo/disparity
Image"| n79 - d174(["/robot_1/perception/macvo/point_cloud (no publishers)"]) -->|"/robot_1/perception/macvo/point_cloud
PointCloud"| n79 n73 -->|"/robot_1/perception/stereo_image_proc/disparity
DisparityImage"| n4 n73 -->|"/robot_1/perception/stereo_image_proc/disparity
DisparityImage"| n74 n74 -->|"/robot_1/perception/stereo_image_proc/point_cloud
PointCloud2"| n79 - n75 -->|"/robot_1/random_walk_node/goal_point_viz
Marker"| d175(["/robot_1/random_walk_node/goal_point_viz (no subscribers)"]) - n75 -->|"/robot_1/random_walk_node/traj_viz
Marker"| d176(["/robot_1/random_walk_node/traj_viz (no subscribers)"]) - n76 -->|"/robot_1/robot_description
String"| d177(["/robot_1/robot_description (no subscribers)"]) - d178(["/robot_1/sensors/front_stereo/left/camera_info (no publishers)"]) -->|"/robot_1/sensors/front_stereo/left/camera_info
CameraInfo"| n73 - d178 -->|"/robot_1/sensors/front_stereo/left/camera_info
CameraInfo"| n74 - d178 -->|"/robot_1/sensors/front_stereo/left/camera_info
CameraInfo"| n79 - d179(["/robot_1/sensors/front_stereo/left/depth_ground_truth (no publishers)"]) -->|"/robot_1/sensors/front_stereo/left/depth_ground_truth
Image"| n79 - d180(["/robot_1/sensors/front_stereo/left/image_rect (no publishers)"]) -->|"/robot_1/sensors/front_stereo/left/image_rect
Image"| n73 - d180 -->|"/robot_1/sensors/front_stereo/left/image_rect
Image"| n74 - d180 -->|"/robot_1/sensors/front_stereo/left/image_rect
Image"| n79 - d181(["/robot_1/sensors/front_stereo/right/camera_info (no publishers)"]) -->|"/robot_1/sensors/front_stereo/right/camera_info
CameraInfo"| n4 - d181 -->|"/robot_1/sensors/front_stereo/right/camera_info
CameraInfo"| n5 - d181 -->|"/robot_1/sensors/front_stereo/right/camera_info
CameraInfo"| n73 - d181 -->|"/robot_1/sensors/front_stereo/right/camera_info
CameraInfo"| n74 - d181 -->|"/robot_1/sensors/front_stereo/right/camera_info
CameraInfo"| n79 - d182(["/robot_1/sensors/front_stereo/right/depth_ground_truth (no publishers)"]) -->|"/robot_1/sensors/front_stereo/right/depth_ground_truth
Image"| n79 - d183(["/robot_1/sensors/front_stereo/right/image_rect (no publishers)"]) -->|"/robot_1/sensors/front_stereo/right/image_rect
Image"| n73 - d183 -->|"/robot_1/sensors/front_stereo/right/image_rect
Image"| n79 - d184(["/robot_1/sensors/lidar/point_cloud (no publishers)"]) -->|"/robot_1/sensors/lidar/point_cloud
PointCloud2"| n79 + n75 -->|"/robot_1/random_walk_node/goal_point_viz
Marker"| d172(["/robot_1/random_walk_node/goal_point_viz (no subscribers)"]) + n75 -->|"/robot_1/random_walk_node/traj_viz
Marker"| d173(["/robot_1/random_walk_node/traj_viz (no subscribers)"]) + n76 -->|"/robot_1/robot_description
String"| d174(["/robot_1/robot_description (no subscribers)"]) + d175(["/robot_1/sensors/front_stereo/left/camera_info (no publishers)"]) -->|"/robot_1/sensors/front_stereo/left/camera_info
CameraInfo"| n73 + d175 -->|"/robot_1/sensors/front_stereo/left/camera_info
CameraInfo"| n74 + d175 -->|"/robot_1/sensors/front_stereo/left/camera_info
CameraInfo"| n79 + d176(["/robot_1/sensors/front_stereo/left/depth_ground_truth (no publishers)"]) -->|"/robot_1/sensors/front_stereo/left/depth_ground_truth
Image"| n79 + d177(["/robot_1/sensors/front_stereo/left/image_rect (no publishers)"]) -->|"/robot_1/sensors/front_stereo/left/image_rect
Image"| n73 + d177 -->|"/robot_1/sensors/front_stereo/left/image_rect
Image"| n74 + d177 -->|"/robot_1/sensors/front_stereo/left/image_rect
Image"| n79 + d178(["/robot_1/sensors/front_stereo/right/camera_info (no publishers)"]) -->|"/robot_1/sensors/front_stereo/right/camera_info
CameraInfo"| n4 + d178 -->|"/robot_1/sensors/front_stereo/right/camera_info
CameraInfo"| n5 + d178 -->|"/robot_1/sensors/front_stereo/right/camera_info
CameraInfo"| n73 + d178 -->|"/robot_1/sensors/front_stereo/right/camera_info
CameraInfo"| n74 + d178 -->|"/robot_1/sensors/front_stereo/right/camera_info
CameraInfo"| n79 + d179(["/robot_1/sensors/front_stereo/right/depth_ground_truth (no publishers)"]) -->|"/robot_1/sensors/front_stereo/right/depth_ground_truth
Image"| n79 + d180(["/robot_1/sensors/front_stereo/right/image_rect (no publishers)"]) -->|"/robot_1/sensors/front_stereo/right/image_rect
Image"| n73 + d180 -->|"/robot_1/sensors/front_stereo/right/image_rect
Image"| n79 + d181(["/robot_1/sensors/lidar/point_cloud (no publishers)"]) -->|"/robot_1/sensors/lidar/point_cloud
PointCloud2"| n79 n77 -->|"/robot_1/sensors/ouster/point_cloud
PointCloud2"| n82 - d185(["/robot_1/sensors/ouster/point_cloud_raw (no publishers)"]) -->|"/robot_1/sensors/ouster/point_cloud_raw
PointCloud2"| n77 - n78 -->|"/robot_1/takeoff_landing_planner/is_airborne
Bool"| d186(["/robot_1/takeoff_landing_planner/is_airborne (no subscribers)"]) - d187(["/robot_1/takeoff_landing_planner/trajectory_completion_percentage (no publishers)"]) -->|"/robot_1/takeoff_landing_planner/trajectory_completion_percentage
Float32"| n78 - n81 -->|"/robot_1/trajectory_controller/closest_point
Odometry"| d188(["/robot_1/trajectory_controller/closest_point (no subscribers)"]) + d182(["/robot_1/sensors/ouster/point_cloud_raw (no publishers)"]) -->|"/robot_1/sensors/ouster/point_cloud_raw
PointCloud2"| n77 + n78 -->|"/robot_1/takeoff_landing_planner/is_airborne
Bool"| d183(["/robot_1/takeoff_landing_planner/is_airborne (no subscribers)"]) + d184(["/robot_1/takeoff_landing_planner/trajectory_completion_percentage (no publishers)"]) -->|"/robot_1/takeoff_landing_planner/trajectory_completion_percentage
Float32"| n78 + n81 -->|"/robot_1/trajectory_controller/closest_point
Odometry"| d185(["/robot_1/trajectory_controller/closest_point (no subscribers)"]) n81 -->|"/robot_1/trajectory_controller/look_ahead
Odometry"| n5 n81 -->|"/robot_1/trajectory_controller/projected_drone_pose
PoseStamped"| n70 - n81 -->|"/robot_1/trajectory_controller/tracking_error
Float32"| d189(["/robot_1/trajectory_controller/tracking_error (no subscribers)"]) + n81 -->|"/robot_1/trajectory_controller/tracking_error
Float32"| d186(["/robot_1/trajectory_controller/tracking_error (no subscribers)"]) n81 -->|"/robot_1/trajectory_controller/tracking_point
Odometry"| n3 n81 -->|"/robot_1/trajectory_controller/tracking_point
Odometry"| n5 n81 -->|"/robot_1/trajectory_controller/tracking_point
Odometry"| n70 n81 -->|"/robot_1/trajectory_controller/tracking_point
Odometry"| n78 - n81 -->|"/robot_1/trajectory_controller/tracking_point_velocity_magnitude
Float32"| d190(["/robot_1/trajectory_controller/tracking_point_velocity_magnitude (no subscribers)"]) - n81 -->|"/robot_1/trajectory_controller/traj_drone_point
Odometry"| d191(["/robot_1/trajectory_controller/traj_drone_point (no subscribers)"]) + n81 -->|"/robot_1/trajectory_controller/tracking_point_velocity_magnitude
Float32"| d187(["/robot_1/trajectory_controller/tracking_point_velocity_magnitude (no subscribers)"]) + n81 -->|"/robot_1/trajectory_controller/traj_drone_point
Odometry"| d188(["/robot_1/trajectory_controller/traj_drone_point (no subscribers)"]) n81 -->|"/robot_1/trajectory_controller/trajectory_completion_percentage
Float32"| n80 n81 -->|"/robot_1/trajectory_controller/trajectory_controller_debug_markers
MarkerArray"| n79 n78 -->|"/robot_1/trajectory_controller/trajectory_override
TrajectoryXYZVYaw"| n81 n80 -->|"/robot_1/trajectory_controller/trajectory_override
TrajectoryXYZVYaw"| n81 n5 -->|"/robot_1/trajectory_controller/trajectory_segment_to_add
TrajectoryXYZVYaw"| n81 - n81 -->|"/robot_1/trajectory_controller/trajectory_time
Float32"| d192(["/robot_1/trajectory_controller/trajectory_time (no subscribers)"]) + n81 -->|"/robot_1/trajectory_controller/trajectory_time
Float32"| d189(["/robot_1/trajectory_controller/trajectory_time (no subscribers)"]) n81 -->|"/robot_1/trajectory_controller/trajectory_vis
MarkerArray"| n79 - n81 -->|"/robot_1/trajectory_controller/virtual_tracking_point
Odometry"| d193(["/robot_1/trajectory_controller/virtual_tracking_point (no subscribers)"]) - n82 -->|"/robot_1/vdb_mapping/vdb_map_overwrites
UpdateGrid"| d194(["/robot_1/vdb_mapping/vdb_map_overwrites (no subscribers)"]) - n82 -->|"/robot_1/vdb_mapping/vdb_map_pointcloud
PointCloud2"| d195(["/robot_1/vdb_mapping/vdb_map_pointcloud (no subscribers)"]) - n82 -->|"/robot_1/vdb_mapping/vdb_map_sections
UpdateGrid"| d196(["/robot_1/vdb_mapping/vdb_map_sections (no subscribers)"]) - n82 -->|"/robot_1/vdb_mapping/vdb_map_updates
UpdateGrid"| d197(["/robot_1/vdb_mapping/vdb_map_updates (no subscribers)"]) + n81 -->|"/robot_1/trajectory_controller/virtual_tracking_point
Odometry"| d190(["/robot_1/trajectory_controller/virtual_tracking_point (no subscribers)"]) + n82 -->|"/robot_1/vdb_mapping/vdb_map_overwrites
UpdateGrid"| d191(["/robot_1/vdb_mapping/vdb_map_overwrites (no subscribers)"]) + n82 -->|"/robot_1/vdb_mapping/vdb_map_pointcloud
PointCloud2"| d192(["/robot_1/vdb_mapping/vdb_map_pointcloud (no subscribers)"]) + n82 -->|"/robot_1/vdb_mapping/vdb_map_sections
UpdateGrid"| d193(["/robot_1/vdb_mapping/vdb_map_sections (no subscribers)"]) + n82 -->|"/robot_1/vdb_mapping/vdb_map_updates
UpdateGrid"| d194(["/robot_1/vdb_mapping/vdb_map_updates (no subscribers)"]) n82 -->|"/robot_1/vdb_mapping/vdb_map_visualization
Marker"| n75 n82 -->|"/robot_1/vdb_mapping/vdb_map_visualization
Marker"| n79 n35 -->|"/tf
TFMessage"| n70 @@ -4454,18 +4451,6 @@ graph LR "type": "sensor_msgs/msg/JointState" }, { -"dir": "sub", -"node": "/robot_1/topic_keepalive", -"qos_profile": { -"depth": "UNKNOWN", -"durability": "VOLATILE", -"history": "UNKNOWN", -"reliability": "RELIABLE" -}, -"topic": "/robot_1/macvo/odometry", -"type": "nav_msgs/msg/Odometry" -}, -{ "dir": "pub", "node": "/robot_1/odometry_conversion/odometry_conversion", "qos_profile": { @@ -4574,30 +4559,6 @@ graph LR "type": "nav_msgs/msg/Odometry" }, { -"dir": "sub", -"node": "/robot_1/topic_keepalive", -"qos_profile": { -"depth": "UNKNOWN", -"durability": "VOLATILE", -"history": "UNKNOWN", -"reliability": "BEST_EFFORT" -}, -"topic": "/robot_1/perception/macvo/disparity", -"type": "sensor_msgs/msg/Image" -}, -{ -"dir": "sub", -"node": "/robot_1/topic_keepalive", -"qos_profile": { -"depth": "UNKNOWN", -"durability": "VOLATILE", -"history": "UNKNOWN", -"reliability": "BEST_EFFORT" -}, -"topic": "/robot_1/perception/macvo/point_cloud", -"type": "sensor_msgs/msg/PointCloud" -}, -{ "dir": "pub", "node": "/robot_1/perception/stereo_image_proc/disparity_node", "qos_profile": { @@ -7472,15 +7433,6 @@ graph LR }, "type": "sensor_msgs/msg/JointState" }, -"/robot_1/macvo/odometry": { -"qos": { -"depth": "UNKNOWN", -"durability": "VOLATILE", -"history": "UNKNOWN", -"reliability": "RELIABLE" -}, -"type": "nav_msgs/msg/Odometry" -}, "/robot_1/odometry_conversion/odometry": { "qos": { "depth": "UNKNOWN", @@ -7490,24 +7442,6 @@ graph LR }, "type": "nav_msgs/msg/Odometry" }, -"/robot_1/perception/macvo/disparity": { -"qos": { -"depth": "UNKNOWN", -"durability": "VOLATILE", -"history": "UNKNOWN", -"reliability": "BEST_EFFORT" -}, -"type": "sensor_msgs/msg/Image" -}, -"/robot_1/perception/macvo/point_cloud": { -"qos": { -"depth": "UNKNOWN", -"durability": "VOLATILE", -"history": "UNKNOWN", -"reliability": "BEST_EFFORT" -}, -"type": "sensor_msgs/msg/PointCloud" -}, "/robot_1/perception/stereo_image_proc/disparity": { "qos": { "depth": "UNKNOWN", diff --git a/tests/goldens/wiring/full_default.isaacsim.1robot.md b/tests/goldens/wiring/full_default.isaacsim.1robot.md index 98471d6d5..999ee2989 100644 --- a/tests/goldens/wiring/full_default.isaacsim.1robot.md +++ b/tests/goldens/wiring/full_default.isaacsim.1robot.md @@ -1,10 +1,10 @@ # Wiring snapshot: full_default - **generated-by**: tests/system/test_wiring_snapshot.py -- **date**: 2026-08-20 20:59:49 +- **date**: 2026-08-21 00:22:30 - **sim**: isaacsim - **num_robots**: 1 -- **source-sha**: 5c6c4a4191bd +- **source-sha**: 19ec8184f64d ```mermaid graph LR @@ -403,7 +403,6 @@ graph LR d169(["/robot_1/interface/torque_thrust_command (no publishers)"]) -->|"/robot_1/interface/torque_thrust_command
TorqueThrust"| n70 d170(["/robot_1/interface/velocity_command (no publishers)"]) -->|"/robot_1/interface/velocity_command
TwistStamped"| n70 d171(["/robot_1/joint_states (no publishers)"]) -->|"/robot_1/joint_states
JointState"| n75 - d172(["/robot_1/macvo/odometry (no publishers)"]) -->|"/robot_1/macvo/odometry
Odometry"| n78 n71 -->|"/robot_1/odometry_conversion/odometry
Odometry"| n2 n71 -->|"/robot_1/odometry_conversion/odometry
Odometry"| n3 n71 -->|"/robot_1/odometry_conversion/odometry
Odometry"| n69 @@ -412,55 +411,53 @@ graph LR n71 -->|"/robot_1/odometry_conversion/odometry
Odometry"| n78 n71 -->|"/robot_1/odometry_conversion/odometry
Odometry"| n79 n71 -->|"/robot_1/odometry_conversion/odometry
Odometry"| n80 - d173(["/robot_1/perception/macvo/disparity (no publishers)"]) -->|"/robot_1/perception/macvo/disparity
Image"| n78 - d174(["/robot_1/perception/macvo/point_cloud (no publishers)"]) -->|"/robot_1/perception/macvo/point_cloud
PointCloud"| n78 n72 -->|"/robot_1/perception/stereo_image_proc/disparity
DisparityImage"| n4 n72 -->|"/robot_1/perception/stereo_image_proc/disparity
DisparityImage"| n73 n73 -->|"/robot_1/perception/stereo_image_proc/point_cloud
PointCloud2"| n78 - n74 -->|"/robot_1/random_walk_node/goal_point_viz
Marker"| d175(["/robot_1/random_walk_node/goal_point_viz (no subscribers)"]) - n74 -->|"/robot_1/random_walk_node/traj_viz
Marker"| d176(["/robot_1/random_walk_node/traj_viz (no subscribers)"]) - n75 -->|"/robot_1/robot_description
String"| d177(["/robot_1/robot_description (no subscribers)"]) - d178(["/robot_1/sensors/front_stereo/left/camera_info (no publishers)"]) -->|"/robot_1/sensors/front_stereo/left/camera_info
CameraInfo"| n72 - d178 -->|"/robot_1/sensors/front_stereo/left/camera_info
CameraInfo"| n73 - d178 -->|"/robot_1/sensors/front_stereo/left/camera_info
CameraInfo"| n78 - d179(["/robot_1/sensors/front_stereo/left/depth_ground_truth (no publishers)"]) -->|"/robot_1/sensors/front_stereo/left/depth_ground_truth
Image"| n78 - d180(["/robot_1/sensors/front_stereo/left/image_rect (no publishers)"]) -->|"/robot_1/sensors/front_stereo/left/image_rect
Image"| n72 - d180 -->|"/robot_1/sensors/front_stereo/left/image_rect
Image"| n73 - d180 -->|"/robot_1/sensors/front_stereo/left/image_rect
Image"| n78 - d181(["/robot_1/sensors/front_stereo/right/camera_info (no publishers)"]) -->|"/robot_1/sensors/front_stereo/right/camera_info
CameraInfo"| n4 - d181 -->|"/robot_1/sensors/front_stereo/right/camera_info
CameraInfo"| n72 - d181 -->|"/robot_1/sensors/front_stereo/right/camera_info
CameraInfo"| n73 - d181 -->|"/robot_1/sensors/front_stereo/right/camera_info
CameraInfo"| n78 - d182(["/robot_1/sensors/front_stereo/right/depth_ground_truth (no publishers)"]) -->|"/robot_1/sensors/front_stereo/right/depth_ground_truth
Image"| n78 - d183(["/robot_1/sensors/front_stereo/right/image_rect (no publishers)"]) -->|"/robot_1/sensors/front_stereo/right/image_rect
Image"| n72 - d183 -->|"/robot_1/sensors/front_stereo/right/image_rect
Image"| n78 - d184(["/robot_1/sensors/lidar/point_cloud (no publishers)"]) -->|"/robot_1/sensors/lidar/point_cloud
PointCloud2"| n78 + n74 -->|"/robot_1/random_walk_node/goal_point_viz
Marker"| d172(["/robot_1/random_walk_node/goal_point_viz (no subscribers)"]) + n74 -->|"/robot_1/random_walk_node/traj_viz
Marker"| d173(["/robot_1/random_walk_node/traj_viz (no subscribers)"]) + n75 -->|"/robot_1/robot_description
String"| d174(["/robot_1/robot_description (no subscribers)"]) + d175(["/robot_1/sensors/front_stereo/left/camera_info (no publishers)"]) -->|"/robot_1/sensors/front_stereo/left/camera_info
CameraInfo"| n72 + d175 -->|"/robot_1/sensors/front_stereo/left/camera_info
CameraInfo"| n73 + d175 -->|"/robot_1/sensors/front_stereo/left/camera_info
CameraInfo"| n78 + d176(["/robot_1/sensors/front_stereo/left/depth_ground_truth (no publishers)"]) -->|"/robot_1/sensors/front_stereo/left/depth_ground_truth
Image"| n78 + d177(["/robot_1/sensors/front_stereo/left/image_rect (no publishers)"]) -->|"/robot_1/sensors/front_stereo/left/image_rect
Image"| n72 + d177 -->|"/robot_1/sensors/front_stereo/left/image_rect
Image"| n73 + d177 -->|"/robot_1/sensors/front_stereo/left/image_rect
Image"| n78 + d178(["/robot_1/sensors/front_stereo/right/camera_info (no publishers)"]) -->|"/robot_1/sensors/front_stereo/right/camera_info
CameraInfo"| n4 + d178 -->|"/robot_1/sensors/front_stereo/right/camera_info
CameraInfo"| n72 + d178 -->|"/robot_1/sensors/front_stereo/right/camera_info
CameraInfo"| n73 + d178 -->|"/robot_1/sensors/front_stereo/right/camera_info
CameraInfo"| n78 + d179(["/robot_1/sensors/front_stereo/right/depth_ground_truth (no publishers)"]) -->|"/robot_1/sensors/front_stereo/right/depth_ground_truth
Image"| n78 + d180(["/robot_1/sensors/front_stereo/right/image_rect (no publishers)"]) -->|"/robot_1/sensors/front_stereo/right/image_rect
Image"| n72 + d180 -->|"/robot_1/sensors/front_stereo/right/image_rect
Image"| n78 + d181(["/robot_1/sensors/lidar/point_cloud (no publishers)"]) -->|"/robot_1/sensors/lidar/point_cloud
PointCloud2"| n78 n76 -->|"/robot_1/sensors/ouster/point_cloud
PointCloud2"| n81 - d185(["/robot_1/sensors/ouster/point_cloud_raw (no publishers)"]) -->|"/robot_1/sensors/ouster/point_cloud_raw
PointCloud2"| n76 - n77 -->|"/robot_1/takeoff_landing_planner/is_airborne
Bool"| d186(["/robot_1/takeoff_landing_planner/is_airborne (no subscribers)"]) - d187(["/robot_1/takeoff_landing_planner/trajectory_completion_percentage (no publishers)"]) -->|"/robot_1/takeoff_landing_planner/trajectory_completion_percentage
Float32"| n77 - n80 -->|"/robot_1/trajectory_controller/closest_point
Odometry"| d188(["/robot_1/trajectory_controller/closest_point (no subscribers)"]) + d182(["/robot_1/sensors/ouster/point_cloud_raw (no publishers)"]) -->|"/robot_1/sensors/ouster/point_cloud_raw
PointCloud2"| n76 + n77 -->|"/robot_1/takeoff_landing_planner/is_airborne
Bool"| d183(["/robot_1/takeoff_landing_planner/is_airborne (no subscribers)"]) + d184(["/robot_1/takeoff_landing_planner/trajectory_completion_percentage (no publishers)"]) -->|"/robot_1/takeoff_landing_planner/trajectory_completion_percentage
Float32"| n77 + n80 -->|"/robot_1/trajectory_controller/closest_point
Odometry"| d185(["/robot_1/trajectory_controller/closest_point (no subscribers)"]) n80 -->|"/robot_1/trajectory_controller/look_ahead
Odometry"| n4 n80 -->|"/robot_1/trajectory_controller/projected_drone_pose
PoseStamped"| n69 - n80 -->|"/robot_1/trajectory_controller/tracking_error
Float32"| d189(["/robot_1/trajectory_controller/tracking_error (no subscribers)"]) + n80 -->|"/robot_1/trajectory_controller/tracking_error
Float32"| d186(["/robot_1/trajectory_controller/tracking_error (no subscribers)"]) n80 -->|"/robot_1/trajectory_controller/tracking_point
Odometry"| n3 n80 -->|"/robot_1/trajectory_controller/tracking_point
Odometry"| n4 n80 -->|"/robot_1/trajectory_controller/tracking_point
Odometry"| n69 n80 -->|"/robot_1/trajectory_controller/tracking_point
Odometry"| n77 - n80 -->|"/robot_1/trajectory_controller/tracking_point_velocity_magnitude
Float32"| d190(["/robot_1/trajectory_controller/tracking_point_velocity_magnitude (no subscribers)"]) - n80 -->|"/robot_1/trajectory_controller/traj_drone_point
Odometry"| d191(["/robot_1/trajectory_controller/traj_drone_point (no subscribers)"]) + n80 -->|"/robot_1/trajectory_controller/tracking_point_velocity_magnitude
Float32"| d187(["/robot_1/trajectory_controller/tracking_point_velocity_magnitude (no subscribers)"]) + n80 -->|"/robot_1/trajectory_controller/traj_drone_point
Odometry"| d188(["/robot_1/trajectory_controller/traj_drone_point (no subscribers)"]) n80 -->|"/robot_1/trajectory_controller/trajectory_completion_percentage
Float32"| n79 n80 -->|"/robot_1/trajectory_controller/trajectory_controller_debug_markers
MarkerArray"| n78 n77 -->|"/robot_1/trajectory_controller/trajectory_override
TrajectoryXYZVYaw"| n80 n79 -->|"/robot_1/trajectory_controller/trajectory_override
TrajectoryXYZVYaw"| n80 n4 -->|"/robot_1/trajectory_controller/trajectory_segment_to_add
TrajectoryXYZVYaw"| n80 - n80 -->|"/robot_1/trajectory_controller/trajectory_time
Float32"| d192(["/robot_1/trajectory_controller/trajectory_time (no subscribers)"]) + n80 -->|"/robot_1/trajectory_controller/trajectory_time
Float32"| d189(["/robot_1/trajectory_controller/trajectory_time (no subscribers)"]) n80 -->|"/robot_1/trajectory_controller/trajectory_vis
MarkerArray"| n78 - n80 -->|"/robot_1/trajectory_controller/virtual_tracking_point
Odometry"| d193(["/robot_1/trajectory_controller/virtual_tracking_point (no subscribers)"]) - n81 -->|"/robot_1/vdb_mapping/vdb_map_overwrites
UpdateGrid"| d194(["/robot_1/vdb_mapping/vdb_map_overwrites (no subscribers)"]) - n81 -->|"/robot_1/vdb_mapping/vdb_map_pointcloud
PointCloud2"| d195(["/robot_1/vdb_mapping/vdb_map_pointcloud (no subscribers)"]) - n81 -->|"/robot_1/vdb_mapping/vdb_map_sections
UpdateGrid"| d196(["/robot_1/vdb_mapping/vdb_map_sections (no subscribers)"]) - n81 -->|"/robot_1/vdb_mapping/vdb_map_updates
UpdateGrid"| d197(["/robot_1/vdb_mapping/vdb_map_updates (no subscribers)"]) + n80 -->|"/robot_1/trajectory_controller/virtual_tracking_point
Odometry"| d190(["/robot_1/trajectory_controller/virtual_tracking_point (no subscribers)"]) + n81 -->|"/robot_1/vdb_mapping/vdb_map_overwrites
UpdateGrid"| d191(["/robot_1/vdb_mapping/vdb_map_overwrites (no subscribers)"]) + n81 -->|"/robot_1/vdb_mapping/vdb_map_pointcloud
PointCloud2"| d192(["/robot_1/vdb_mapping/vdb_map_pointcloud (no subscribers)"]) + n81 -->|"/robot_1/vdb_mapping/vdb_map_sections
UpdateGrid"| d193(["/robot_1/vdb_mapping/vdb_map_sections (no subscribers)"]) + n81 -->|"/robot_1/vdb_mapping/vdb_map_updates
UpdateGrid"| d194(["/robot_1/vdb_mapping/vdb_map_updates (no subscribers)"]) n81 -->|"/robot_1/vdb_mapping/vdb_map_visualization
Marker"| n74 n81 -->|"/robot_1/vdb_mapping/vdb_map_visualization
Marker"| n78 n34 -->|"/tf
TFMessage"| n69 @@ -4289,18 +4286,6 @@ graph LR "type": "sensor_msgs/msg/JointState" }, { -"dir": "sub", -"node": "/robot_1/topic_keepalive", -"qos_profile": { -"depth": "UNKNOWN", -"durability": "VOLATILE", -"history": "UNKNOWN", -"reliability": "RELIABLE" -}, -"topic": "/robot_1/macvo/odometry", -"type": "nav_msgs/msg/Odometry" -}, -{ "dir": "pub", "node": "/robot_1/odometry_conversion/odometry_conversion", "qos_profile": { @@ -4409,30 +4394,6 @@ graph LR "type": "nav_msgs/msg/Odometry" }, { -"dir": "sub", -"node": "/robot_1/topic_keepalive", -"qos_profile": { -"depth": "UNKNOWN", -"durability": "VOLATILE", -"history": "UNKNOWN", -"reliability": "BEST_EFFORT" -}, -"topic": "/robot_1/perception/macvo/disparity", -"type": "sensor_msgs/msg/Image" -}, -{ -"dir": "sub", -"node": "/robot_1/topic_keepalive", -"qos_profile": { -"depth": "UNKNOWN", -"durability": "VOLATILE", -"history": "UNKNOWN", -"reliability": "BEST_EFFORT" -}, -"topic": "/robot_1/perception/macvo/point_cloud", -"type": "sensor_msgs/msg/PointCloud" -}, -{ "dir": "pub", "node": "/robot_1/perception/stereo_image_proc/disparity_node", "qos_profile": { @@ -5436,7 +5397,7 @@ graph LR "depth": "UNKNOWN", "durability": "VOLATILE", "history": "UNKNOWN", -"reliability": "BEST_EFFORT" +"reliability": "RELIABLE" }, "type": "rosgraph_msgs/msg/Clock" }, @@ -7240,15 +7201,6 @@ graph LR }, "type": "sensor_msgs/msg/JointState" }, -"/robot_1/macvo/odometry": { -"qos": { -"depth": "UNKNOWN", -"durability": "VOLATILE", -"history": "UNKNOWN", -"reliability": "RELIABLE" -}, -"type": "nav_msgs/msg/Odometry" -}, "/robot_1/odometry_conversion/odometry": { "qos": { "depth": "UNKNOWN", @@ -7258,24 +7210,6 @@ graph LR }, "type": "nav_msgs/msg/Odometry" }, -"/robot_1/perception/macvo/disparity": { -"qos": { -"depth": "UNKNOWN", -"durability": "VOLATILE", -"history": "UNKNOWN", -"reliability": "BEST_EFFORT" -}, -"type": "sensor_msgs/msg/Image" -}, -"/robot_1/perception/macvo/point_cloud": { -"qos": { -"depth": "UNKNOWN", -"durability": "VOLATILE", -"history": "UNKNOWN", -"reliability": "BEST_EFFORT" -}, -"type": "sensor_msgs/msg/PointCloud" -}, "/robot_1/perception/stereo_image_proc/disparity": { "qos": { "depth": "UNKNOWN", @@ -7335,7 +7269,7 @@ graph LR "depth": "UNKNOWN", "durability": "VOLATILE", "history": "UNKNOWN", -"reliability": "BEST_EFFORT" +"reliability": "RELIABLE" }, "type": "sensor_msgs/msg/Image" }, @@ -7344,7 +7278,7 @@ graph LR "depth": "UNKNOWN", "durability": "VOLATILE", "history": "UNKNOWN", -"reliability": "BEST_EFFORT" +"reliability": "RELIABLE" }, "type": "sensor_msgs/msg/Image" }, @@ -7362,7 +7296,7 @@ graph LR "depth": "UNKNOWN", "durability": "VOLATILE", "history": "UNKNOWN", -"reliability": "BEST_EFFORT" +"reliability": "RELIABLE" }, "type": "sensor_msgs/msg/Image" }, @@ -7371,7 +7305,7 @@ graph LR "depth": "UNKNOWN", "durability": "VOLATILE", "history": "UNKNOWN", -"reliability": "BEST_EFFORT" +"reliability": "RELIABLE" }, "type": "sensor_msgs/msg/Image" }, diff --git a/tests/wiring_snapshot.py b/tests/wiring_snapshot.py index 174918fa6..0b344c769 100644 --- a/tests/wiring_snapshot.py +++ b/tests/wiring_snapshot.py @@ -52,15 +52,27 @@ "_CREATED_BY_BARE_DDS_APP_", ) +# Substrings marking sim render-pipeline internals whose visibility on the +# robot domain is timing-dependent (they join the graph once Isaac's SDG +# pipeline spins up — sometimes before the snapshot, sometimes after). They +# are prim-path-derived names, not stack wiring; RFC #380 §1 later normalizes +# sim sensor endpoints to vehicle-manifest sensor ids properly. +_EXCLUDED_NODE_SUBSTRINGS = ( + "_Render_PostProcess_SDGPipeline", + "_PX4MultirotorGraph_", +) + _QOS_UNKNOWN = "UNKNOWN" _HISTORY_RE = re.compile(r"History\s*\(Depth\):\s*([A-Za-z_]+)(?:\s*\((\d+)\))?") def _is_excluded_node(name): - """True for launch/CLI helper nodes whose names are pid/hex-suffixed.""" + """True for launch/CLI helper and sim render-pipeline nodes.""" segment = name.rsplit("/", 1)[-1] - return segment.startswith(_EXCLUDED_NODE_PREFIXES) + if segment.startswith(_EXCLUDED_NODE_PREFIXES): + return True + return any(s in name for s in _EXCLUDED_NODE_SUBSTRINGS) def parse_node_list(text): From 115c991acfa1835a9482e9847e7ad75ec94c5b68 Mon Sep 17 00:00:00 2001 From: Andrew Jong Date: Thu, 20 Aug 2026 20:45:03 -0400 Subject: [PATCH 4/8] M3(rfc-379): bless full_macvo wiring baseline on the composed module image Captured with asm_macvo synced + module lock --build (tier-2 layer on the slim base). Strictly more truthful than the E2-era baseline: macvo_node now present with its real publishers (the old capture raced the first-ever model load and saw macvo topics only via subscribers), the anonymous perception static TF is the module's named macvo_ned_tf, and the keepalive phantom rows are gone. Verified drift-clean on an independent bring-up. Co-Authored-By: Claude Fable 5 --- stacks/full_macvo/wiring.md | 317 +++++++++++++++++++++--------------- 1 file changed, 185 insertions(+), 132 deletions(-) diff --git a/stacks/full_macvo/wiring.md b/stacks/full_macvo/wiring.md index ffcc15e8a..70089453e 100644 --- a/stacks/full_macvo/wiring.md +++ b/stacks/full_macvo/wiring.md @@ -1,10 +1,10 @@ # Wiring snapshot: full_macvo - **generated-by**: tests/system/test_wiring_snapshot.py -- **date**: 2026-08-20 22:17:32 +- **date**: 2026-08-21 00:42:32 - **sim**: isaacsim - **num_robots**: 1 -- **source-sha**: d02a3467b72d +- **source-sha**: f769eecc38b7 ```mermaid graph LR @@ -88,31 +88,32 @@ graph LR n71["/robot_1/odometry_conversion/odometry_conversion"] end subgraph g5["perception"] - n72["/robot_1/perception/static_transform_publisher"] - n73["/robot_1/perception/stereo_image_proc/disparity_node"] - n74["/robot_1/perception/stereo_pointcloud"] + n72["/robot_1/perception/macvo/macvo_node"] + n73["/robot_1/perception/macvo_ned_tf"] + n74["/robot_1/perception/stereo_image_proc/disparity_node"] + n75["/robot_1/perception/stereo_pointcloud"] end subgraph g6["robot_1"] n1["/robot_1/Container"] n5["/robot_1/gossip_node"] - n75["/robot_1/random_walk_node"] - n76["/robot_1/robot_state_publisher"] - n79["/robot_1/topic_keepalive"] - n82["/robot_1/vdb_mapping"] - n83["/robot_1/world_to_map_broadcaster"] + n76["/robot_1/random_walk_node"] + n77["/robot_1/robot_state_publisher"] + n80["/robot_1/topic_keepalive"] + n83["/robot_1/vdb_mapping"] + n84["/robot_1/world_to_map_broadcaster"] end subgraph g7["root"] n0["/action_relay_client"] end subgraph g8["sensors"] - n77["/robot_1/sensors/lidar_point_cloud_filter"] + n78["/robot_1/sensors/lidar_point_cloud_filter"] end subgraph g9["takeoff_landing_planner"] - n78["/robot_1/takeoff_landing_planner/takeoff_landing_task"] + n79["/robot_1/takeoff_landing_planner/takeoff_landing_task"] end subgraph g10["trajectory_controller"] - n80["/robot_1/trajectory_controller/fixed_trajectory_task"] - n81["/robot_1/trajectory_controller/trajectory_control_node"] + n81["/robot_1/trajectory_controller/fixed_trajectory_task"] + n82["/robot_1/trajectory_controller/trajectory_control_node"] end d0(["/clock (no publishers)"]) -->|"/clock
Clock"| n1 d0 -->|"/clock
Clock"| n2 @@ -197,12 +198,13 @@ graph LR d0 -->|"/clock
Clock"| n81 d0 -->|"/clock
Clock"| n82 d0 -->|"/clock
Clock"| n83 + d0 -->|"/clock
Clock"| n84 n34 -->|"/diagnostics
DiagnosticArray"| d1(["/diagnostics (no subscribers)"]) n36 -->|"/diagnostics
DiagnosticArray"| d1 n5 -->|"/gossip/peers
PeerProfile"| n5 n25 -->|"/move_base_simple/goal
PoseStamped"| d2(["/move_base_simple/goal (no subscribers)"]) d3(["/robot_1/behavior/drone_safety_monitor/command (no publishers)"]) -->|"/robot_1/behavior/drone_safety_monitor/command
String"| n2 - n2 -->|"/robot_1/behavior/drone_safety_monitor/state_estimate_timed_out
Bool"| n78 + n2 -->|"/robot_1/behavior/drone_safety_monitor/state_estimate_timed_out
Bool"| n79 n70 -->|"/robot_1/control/reset_integrators
Empty"| n3 n3 -->|"/robot_1/control/vx_pid_info
PIDInfo"| d4(["/robot_1/control/vx_pid_info (no subscribers)"]) n3 -->|"/robot_1/control/vy_pid_info
PIDInfo"| d5(["/robot_1/control/vy_pid_info (no subscribers)"]) @@ -214,27 +216,27 @@ graph LR n69 -->|"/robot_1/cross_track_error
PoseStamped"| d11(["/robot_1/cross_track_error (no subscribers)"]) n4 -->|"/robot_1/droan/background_expanded
Image"| d12(["/robot_1/droan/background_expanded (no subscribers)"]) d13(["/robot_1/droan/clear_map (no publishers)"]) -->|"/robot_1/droan/clear_map
Empty"| n4 - d14(["/robot_1/droan/disparity_graph (no publishers)"]) -->|"/robot_1/droan/disparity_graph
MarkerArray"| n79 - d15(["/robot_1/droan/disparity_map_debug (no publishers)"]) -->|"/robot_1/droan/disparity_map_debug
MarkerArray"| n79 - d16(["/robot_1/droan/expansion_cloud (no publishers)"]) -->|"/robot_1/droan/expansion_cloud
PointCloud2"| n79 - d17(["/robot_1/droan/expansion_poly (no publishers)"]) -->|"/robot_1/droan/expansion_poly
MarkerArray"| n79 - n4 -->|"/robot_1/droan/fg_bg_cloud
PointCloud2"| n79 + d14(["/robot_1/droan/disparity_graph (no publishers)"]) -->|"/robot_1/droan/disparity_graph
MarkerArray"| n80 + d15(["/robot_1/droan/disparity_map_debug (no publishers)"]) -->|"/robot_1/droan/disparity_map_debug
MarkerArray"| n80 + d16(["/robot_1/droan/expansion_cloud (no publishers)"]) -->|"/robot_1/droan/expansion_cloud
PointCloud2"| n80 + d17(["/robot_1/droan/expansion_poly (no publishers)"]) -->|"/robot_1/droan/expansion_poly
MarkerArray"| n80 + n4 -->|"/robot_1/droan/fg_bg_cloud
PointCloud2"| n80 n4 -->|"/robot_1/droan/foreground_expanded
Image"| d18(["/robot_1/droan/foreground_expanded (no subscribers)"]) - d19(["/robot_1/droan/frustum (no publishers)"]) -->|"/robot_1/droan/frustum
Marker"| n79 - n4 -->|"/robot_1/droan/graph_vis
MarkerArray"| n79 - n4 -->|"/robot_1/droan/local_planner_global_plan_vis
MarkerArray"| n79 + d19(["/robot_1/droan/frustum (no publishers)"]) -->|"/robot_1/droan/frustum
Marker"| n80 + n4 -->|"/robot_1/droan/graph_vis
MarkerArray"| n80 + n4 -->|"/robot_1/droan/local_planner_global_plan_vis
MarkerArray"| n80 d20(["/robot_1/droan/reset_stuck (no publishers)"]) -->|"/robot_1/droan/reset_stuck
Empty"| n4 - n4 -->|"/robot_1/droan/rewind_info
MarkerArray"| n79 + n4 -->|"/robot_1/droan/rewind_info
MarkerArray"| n80 n4 -->|"/robot_1/droan/stuck
Bool"| d21(["/robot_1/droan/stuck (no subscribers)"]) - n4 -->|"/robot_1/droan/traj_debug
MarkerArray"| n79 - d22(["/robot_1/droan/trajectory_library_vis (no publishers)"]) -->|"/robot_1/droan/trajectory_library_vis
MarkerArray"| n79 - d23(["/robot_1/droan/virtual_obstacles (no publishers)"]) -->|"/robot_1/droan/virtual_obstacles
MarkerArray"| n79 + n4 -->|"/robot_1/droan/traj_debug
MarkerArray"| n80 + d22(["/robot_1/droan/trajectory_library_vis (no publishers)"]) -->|"/robot_1/droan/trajectory_library_vis
MarkerArray"| n80 + d23(["/robot_1/droan/virtual_obstacles (no publishers)"]) -->|"/robot_1/droan/virtual_obstacles
MarkerArray"| n80 n69 -->|"/robot_1/global_plan
Path"| n4 n69 -->|"/robot_1/global_plan
Path"| n5 - n69 -->|"/robot_1/global_plan
Path"| n79 - n75 -->|"/robot_1/global_plan
Path"| n4 - n75 -->|"/robot_1/global_plan
Path"| n5 - n75 -->|"/robot_1/global_plan
Path"| n79 + n69 -->|"/robot_1/global_plan
Path"| n80 + n76 -->|"/robot_1/global_plan
Path"| n4 + n76 -->|"/robot_1/global_plan
Path"| n5 + n76 -->|"/robot_1/global_plan
Path"| n80 d24(["/robot_1/interface/attitude_thrust_command (no publishers)"]) -->|"/robot_1/interface/attitude_thrust_command
AttitudeThrust"| n70 d25(["/robot_1/interface/cmd_attitude_thrust (no publishers)"]) -->|"/robot_1/interface/cmd_attitude_thrust
AttitudeThrust"| n70 n69 -->|"/robot_1/interface/cmd_pose
PoseStamped"| n70 @@ -242,8 +244,8 @@ graph LR n3 -->|"/robot_1/interface/cmd_roll_pitch_yawrate_thrust
RollPitchYawrateThrust"| n70 d27(["/robot_1/interface/cmd_torque_thrust (no publishers)"]) -->|"/robot_1/interface/cmd_torque_thrust
TorqueThrust"| n70 n69 -->|"/robot_1/interface/cmd_velocity
TwistStamped"| n70 - n70 -->|"/robot_1/interface/has_control
Bool"| n78 - n70 -->|"/robot_1/interface/is_armed
Bool"| n78 + n70 -->|"/robot_1/interface/has_control
Bool"| n79 + n70 -->|"/robot_1/interface/is_armed
Bool"| n79 d28(["/robot_1/interface/mavros/actuator_control (no publishers)"]) -->|"/robot_1/interface/mavros/actuator_control
ActuatorControl"| n6 d29(["/robot_1/interface/mavros/adsb/send (no publishers)"]) -->|"/robot_1/interface/mavros/adsb/send
ADSBVehicle"| n7 n7 -->|"/robot_1/interface/mavros/adsb/vehicle
ADSBVehicle"| d30(["/robot_1/interface/mavros/adsb/vehicle (no subscribers)"]) @@ -263,7 +265,7 @@ graph LR n15 -->|"/robot_1/interface/mavros/esc_status/status
ESCStatus"| d44(["/robot_1/interface/mavros/esc_status/status (no subscribers)"]) n16 -->|"/robot_1/interface/mavros/esc_telemetry/telemetry
ESCTelemetry"| d45(["/robot_1/interface/mavros/esc_telemetry/telemetry (no subscribers)"]) n59 -->|"/robot_1/interface/mavros/estimator_status
EstimatorStatus"| d46(["/robot_1/interface/mavros/estimator_status (no subscribers)"]) - n59 -->|"/robot_1/interface/mavros/extended_state
ExtendedState"| n78 + n59 -->|"/robot_1/interface/mavros/extended_state
ExtendedState"| n79 d47(["/robot_1/interface/mavros/fake_gps/mocap/tf (no publishers)"]) -->|"/robot_1/interface/mavros/fake_gps/mocap/tf
TransformStamped"| n17 n19 -->|"/robot_1/interface/mavros/geofence/fences
WaypointList"| d48(["/robot_1/interface/mavros/geofence/fences (no subscribers)"]) n20 -->|"/robot_1/interface/mavros/gimbal_control/device/attitude_status
GimbalDeviceAttitudeStatus"| d49(["/robot_1/interface/mavros/gimbal_control/device/attitude_status (no subscribers)"]) @@ -404,83 +406,85 @@ graph LR d168(["/robot_1/interface/roll_pitch_yawrate_thrust_command (no publishers)"]) -->|"/robot_1/interface/roll_pitch_yawrate_thrust_command
RollPitchYawrateThrust"| n70 d169(["/robot_1/interface/torque_thrust_command (no publishers)"]) -->|"/robot_1/interface/torque_thrust_command
TorqueThrust"| n70 d170(["/robot_1/interface/velocity_command (no publishers)"]) -->|"/robot_1/interface/velocity_command
TwistStamped"| n70 - d171(["/robot_1/joint_states (no publishers)"]) -->|"/robot_1/joint_states
JointState"| n76 - d172(["/robot_1/macvo/odometry (no publishers)"]) -->|"/robot_1/macvo/odometry
Odometry"| n79 + d171(["/robot_1/joint_states (no publishers)"]) -->|"/robot_1/joint_states
JointState"| n77 n71 -->|"/robot_1/odometry_conversion/odometry
Odometry"| n2 n71 -->|"/robot_1/odometry_conversion/odometry
Odometry"| n3 n71 -->|"/robot_1/odometry_conversion/odometry
Odometry"| n69 - n71 -->|"/robot_1/odometry_conversion/odometry
Odometry"| n75 - n71 -->|"/robot_1/odometry_conversion/odometry
Odometry"| n78 + n71 -->|"/robot_1/odometry_conversion/odometry
Odometry"| n76 n71 -->|"/robot_1/odometry_conversion/odometry
Odometry"| n79 n71 -->|"/robot_1/odometry_conversion/odometry
Odometry"| n80 n71 -->|"/robot_1/odometry_conversion/odometry
Odometry"| n81 - d173(["/robot_1/perception/macvo/disparity (no publishers)"]) -->|"/robot_1/perception/macvo/disparity
DisparityImage"| n4 - d173 -->|"/robot_1/perception/macvo/disparity
DisparityImage"| n79 - d174(["/robot_1/perception/macvo/point_cloud (no publishers)"]) -->|"/robot_1/perception/macvo/point_cloud
PointCloud"| n79 - n73 -->|"/robot_1/perception/stereo_image_proc/disparity
DisparityImage"| n74 - n74 -->|"/robot_1/perception/stereo_image_proc/point_cloud
PointCloud2"| n79 - n75 -->|"/robot_1/random_walk_node/goal_point_viz
Marker"| d175(["/robot_1/random_walk_node/goal_point_viz (no subscribers)"]) - n75 -->|"/robot_1/random_walk_node/traj_viz
Marker"| d176(["/robot_1/random_walk_node/traj_viz (no subscribers)"]) - n76 -->|"/robot_1/robot_description
String"| d177(["/robot_1/robot_description (no subscribers)"]) - d178(["/robot_1/sensors/front_stereo/left/camera_info (no publishers)"]) -->|"/robot_1/sensors/front_stereo/left/camera_info
CameraInfo"| n73 - d178 -->|"/robot_1/sensors/front_stereo/left/camera_info
CameraInfo"| n74 - d178 -->|"/robot_1/sensors/front_stereo/left/camera_info
CameraInfo"| n79 - d179(["/robot_1/sensors/front_stereo/left/depth_ground_truth (no publishers)"]) -->|"/robot_1/sensors/front_stereo/left/depth_ground_truth
Image"| n79 - d180(["/robot_1/sensors/front_stereo/left/image_rect (no publishers)"]) -->|"/robot_1/sensors/front_stereo/left/image_rect
Image"| n73 - d180 -->|"/robot_1/sensors/front_stereo/left/image_rect
Image"| n74 - d180 -->|"/robot_1/sensors/front_stereo/left/image_rect
Image"| n79 - d181(["/robot_1/sensors/front_stereo/right/camera_info (no publishers)"]) -->|"/robot_1/sensors/front_stereo/right/camera_info
CameraInfo"| n4 - d181 -->|"/robot_1/sensors/front_stereo/right/camera_info
CameraInfo"| n73 - d181 -->|"/robot_1/sensors/front_stereo/right/camera_info
CameraInfo"| n74 - d181 -->|"/robot_1/sensors/front_stereo/right/camera_info
CameraInfo"| n79 - d182(["/robot_1/sensors/front_stereo/right/depth_ground_truth (no publishers)"]) -->|"/robot_1/sensors/front_stereo/right/depth_ground_truth
Image"| n79 - d183(["/robot_1/sensors/front_stereo/right/image_rect (no publishers)"]) -->|"/robot_1/sensors/front_stereo/right/image_rect
Image"| n73 - d183 -->|"/robot_1/sensors/front_stereo/right/image_rect
Image"| n79 - d184(["/robot_1/sensors/lidar/point_cloud (no publishers)"]) -->|"/robot_1/sensors/lidar/point_cloud
PointCloud2"| n79 - n77 -->|"/robot_1/sensors/ouster/point_cloud
PointCloud2"| n82 - d185(["/robot_1/sensors/ouster/point_cloud_raw (no publishers)"]) -->|"/robot_1/sensors/ouster/point_cloud_raw
PointCloud2"| n77 - n78 -->|"/robot_1/takeoff_landing_planner/is_airborne
Bool"| d186(["/robot_1/takeoff_landing_planner/is_airborne (no subscribers)"]) - d187(["/robot_1/takeoff_landing_planner/trajectory_completion_percentage (no publishers)"]) -->|"/robot_1/takeoff_landing_planner/trajectory_completion_percentage
Float32"| n78 - n81 -->|"/robot_1/trajectory_controller/closest_point
Odometry"| d188(["/robot_1/trajectory_controller/closest_point (no subscribers)"]) - n81 -->|"/robot_1/trajectory_controller/look_ahead
Odometry"| n4 - n81 -->|"/robot_1/trajectory_controller/projected_drone_pose
PoseStamped"| n69 - n81 -->|"/robot_1/trajectory_controller/tracking_error
Float32"| d189(["/robot_1/trajectory_controller/tracking_error (no subscribers)"]) - n81 -->|"/robot_1/trajectory_controller/tracking_point
Odometry"| n3 - n81 -->|"/robot_1/trajectory_controller/tracking_point
Odometry"| n4 - n81 -->|"/robot_1/trajectory_controller/tracking_point
Odometry"| n69 - n81 -->|"/robot_1/trajectory_controller/tracking_point
Odometry"| n78 - n81 -->|"/robot_1/trajectory_controller/tracking_point_velocity_magnitude
Float32"| d190(["/robot_1/trajectory_controller/tracking_point_velocity_magnitude (no subscribers)"]) - n81 -->|"/robot_1/trajectory_controller/traj_drone_point
Odometry"| d191(["/robot_1/trajectory_controller/traj_drone_point (no subscribers)"]) - n81 -->|"/robot_1/trajectory_controller/trajectory_completion_percentage
Float32"| n80 - n81 -->|"/robot_1/trajectory_controller/trajectory_controller_debug_markers
MarkerArray"| n79 - n78 -->|"/robot_1/trajectory_controller/trajectory_override
TrajectoryXYZVYaw"| n81 - n80 -->|"/robot_1/trajectory_controller/trajectory_override
TrajectoryXYZVYaw"| n81 - n4 -->|"/robot_1/trajectory_controller/trajectory_segment_to_add
TrajectoryXYZVYaw"| n81 - n81 -->|"/robot_1/trajectory_controller/trajectory_time
Float32"| d192(["/robot_1/trajectory_controller/trajectory_time (no subscribers)"]) - n81 -->|"/robot_1/trajectory_controller/trajectory_vis
MarkerArray"| n79 - n81 -->|"/robot_1/trajectory_controller/virtual_tracking_point
Odometry"| d193(["/robot_1/trajectory_controller/virtual_tracking_point (no subscribers)"]) - n82 -->|"/robot_1/vdb_mapping/vdb_map_overwrites
UpdateGrid"| d194(["/robot_1/vdb_mapping/vdb_map_overwrites (no subscribers)"]) - n82 -->|"/robot_1/vdb_mapping/vdb_map_pointcloud
PointCloud2"| d195(["/robot_1/vdb_mapping/vdb_map_pointcloud (no subscribers)"]) - n82 -->|"/robot_1/vdb_mapping/vdb_map_sections
UpdateGrid"| d196(["/robot_1/vdb_mapping/vdb_map_sections (no subscribers)"]) - n82 -->|"/robot_1/vdb_mapping/vdb_map_updates
UpdateGrid"| d197(["/robot_1/vdb_mapping/vdb_map_updates (no subscribers)"]) - n82 -->|"/robot_1/vdb_mapping/vdb_map_visualization
Marker"| n75 - n82 -->|"/robot_1/vdb_mapping/vdb_map_visualization
Marker"| n79 + n71 -->|"/robot_1/odometry_conversion/odometry
Odometry"| n82 + n72 -->|"/robot_1/perception/macvo/disparity
Image"| n4 + n72 -->|"/robot_1/perception/macvo/odometry
Odometry"| d172(["/robot_1/perception/macvo/odometry (no subscribers)"]) + n72 -->|"/robot_1/perception/macvo/point_cloud
PointCloud"| d173(["/robot_1/perception/macvo/point_cloud (no subscribers)"]) + n74 -->|"/robot_1/perception/stereo_image_proc/disparity
DisparityImage"| n75 + n75 -->|"/robot_1/perception/stereo_image_proc/point_cloud
PointCloud2"| n80 + n76 -->|"/robot_1/random_walk_node/goal_point_viz
Marker"| d174(["/robot_1/random_walk_node/goal_point_viz (no subscribers)"]) + n76 -->|"/robot_1/random_walk_node/traj_viz
Marker"| d175(["/robot_1/random_walk_node/traj_viz (no subscribers)"]) + n77 -->|"/robot_1/robot_description
String"| d176(["/robot_1/robot_description (no subscribers)"]) + d177(["/robot_1/sensors/front_stereo/left/camera_info (no publishers)"]) -->|"/robot_1/sensors/front_stereo/left/camera_info
CameraInfo"| n74 + d177 -->|"/robot_1/sensors/front_stereo/left/camera_info
CameraInfo"| n75 + d177 -->|"/robot_1/sensors/front_stereo/left/camera_info
CameraInfo"| n80 + d178(["/robot_1/sensors/front_stereo/left/depth_ground_truth (no publishers)"]) -->|"/robot_1/sensors/front_stereo/left/depth_ground_truth
Image"| n80 + d179(["/robot_1/sensors/front_stereo/left/image_rect (no publishers)"]) -->|"/robot_1/sensors/front_stereo/left/image_rect
Image"| n72 + d179 -->|"/robot_1/sensors/front_stereo/left/image_rect
Image"| n74 + d179 -->|"/robot_1/sensors/front_stereo/left/image_rect
Image"| n75 + d179 -->|"/robot_1/sensors/front_stereo/left/image_rect
Image"| n80 + d180(["/robot_1/sensors/front_stereo/right/camera_info (no publishers)"]) -->|"/robot_1/sensors/front_stereo/right/camera_info
CameraInfo"| n4 + d180 -->|"/robot_1/sensors/front_stereo/right/camera_info
CameraInfo"| n72 + d180 -->|"/robot_1/sensors/front_stereo/right/camera_info
CameraInfo"| n74 + d180 -->|"/robot_1/sensors/front_stereo/right/camera_info
CameraInfo"| n75 + d180 -->|"/robot_1/sensors/front_stereo/right/camera_info
CameraInfo"| n80 + d181(["/robot_1/sensors/front_stereo/right/depth_ground_truth (no publishers)"]) -->|"/robot_1/sensors/front_stereo/right/depth_ground_truth
Image"| n80 + d182(["/robot_1/sensors/front_stereo/right/image_rect (no publishers)"]) -->|"/robot_1/sensors/front_stereo/right/image_rect
Image"| n72 + d182 -->|"/robot_1/sensors/front_stereo/right/image_rect
Image"| n74 + d182 -->|"/robot_1/sensors/front_stereo/right/image_rect
Image"| n80 + d183(["/robot_1/sensors/lidar/point_cloud (no publishers)"]) -->|"/robot_1/sensors/lidar/point_cloud
PointCloud2"| n80 + n78 -->|"/robot_1/sensors/ouster/point_cloud
PointCloud2"| n83 + d184(["/robot_1/sensors/ouster/point_cloud_raw (no publishers)"]) -->|"/robot_1/sensors/ouster/point_cloud_raw
PointCloud2"| n78 + n79 -->|"/robot_1/takeoff_landing_planner/is_airborne
Bool"| d185(["/robot_1/takeoff_landing_planner/is_airborne (no subscribers)"]) + d186(["/robot_1/takeoff_landing_planner/trajectory_completion_percentage (no publishers)"]) -->|"/robot_1/takeoff_landing_planner/trajectory_completion_percentage
Float32"| n79 + n82 -->|"/robot_1/trajectory_controller/closest_point
Odometry"| d187(["/robot_1/trajectory_controller/closest_point (no subscribers)"]) + n82 -->|"/robot_1/trajectory_controller/look_ahead
Odometry"| n4 + n82 -->|"/robot_1/trajectory_controller/projected_drone_pose
PoseStamped"| n69 + n82 -->|"/robot_1/trajectory_controller/tracking_error
Float32"| d188(["/robot_1/trajectory_controller/tracking_error (no subscribers)"]) + n82 -->|"/robot_1/trajectory_controller/tracking_point
Odometry"| n3 + n82 -->|"/robot_1/trajectory_controller/tracking_point
Odometry"| n4 + n82 -->|"/robot_1/trajectory_controller/tracking_point
Odometry"| n69 + n82 -->|"/robot_1/trajectory_controller/tracking_point
Odometry"| n79 + n82 -->|"/robot_1/trajectory_controller/tracking_point_velocity_magnitude
Float32"| d189(["/robot_1/trajectory_controller/tracking_point_velocity_magnitude (no subscribers)"]) + n82 -->|"/robot_1/trajectory_controller/traj_drone_point
Odometry"| d190(["/robot_1/trajectory_controller/traj_drone_point (no subscribers)"]) + n82 -->|"/robot_1/trajectory_controller/trajectory_completion_percentage
Float32"| n81 + n82 -->|"/robot_1/trajectory_controller/trajectory_controller_debug_markers
MarkerArray"| n80 + n79 -->|"/robot_1/trajectory_controller/trajectory_override
TrajectoryXYZVYaw"| n82 + n81 -->|"/robot_1/trajectory_controller/trajectory_override
TrajectoryXYZVYaw"| n82 + n4 -->|"/robot_1/trajectory_controller/trajectory_segment_to_add
TrajectoryXYZVYaw"| n82 + n82 -->|"/robot_1/trajectory_controller/trajectory_time
Float32"| d191(["/robot_1/trajectory_controller/trajectory_time (no subscribers)"]) + n82 -->|"/robot_1/trajectory_controller/trajectory_vis
MarkerArray"| n80 + n82 -->|"/robot_1/trajectory_controller/virtual_tracking_point
Odometry"| d192(["/robot_1/trajectory_controller/virtual_tracking_point (no subscribers)"]) + n83 -->|"/robot_1/vdb_mapping/vdb_map_overwrites
UpdateGrid"| d193(["/robot_1/vdb_mapping/vdb_map_overwrites (no subscribers)"]) + n83 -->|"/robot_1/vdb_mapping/vdb_map_pointcloud
PointCloud2"| d194(["/robot_1/vdb_mapping/vdb_map_pointcloud (no subscribers)"]) + n83 -->|"/robot_1/vdb_mapping/vdb_map_sections
UpdateGrid"| d195(["/robot_1/vdb_mapping/vdb_map_sections (no subscribers)"]) + n83 -->|"/robot_1/vdb_mapping/vdb_map_updates
UpdateGrid"| d196(["/robot_1/vdb_mapping/vdb_map_updates (no subscribers)"]) + n83 -->|"/robot_1/vdb_mapping/vdb_map_visualization
Marker"| n76 + n83 -->|"/robot_1/vdb_mapping/vdb_map_visualization
Marker"| n80 n34 -->|"/tf
TFMessage"| n69 - n34 -->|"/tf
TFMessage"| n79 + n34 -->|"/tf
TFMessage"| n80 n71 -->|"/tf
TFMessage"| n69 - n71 -->|"/tf
TFMessage"| n79 - n76 -->|"/tf
TFMessage"| n69 - n76 -->|"/tf
TFMessage"| n79 - n81 -->|"/tf
TFMessage"| n69 - n81 -->|"/tf
TFMessage"| n79 + n71 -->|"/tf
TFMessage"| n80 + n77 -->|"/tf
TFMessage"| n69 + n77 -->|"/tf
TFMessage"| n80 + n82 -->|"/tf
TFMessage"| n69 + n82 -->|"/tf
TFMessage"| n80 n34 -->|"/tf_static
TFMessage"| n69 - n34 -->|"/tf_static
TFMessage"| n79 - n72 -->|"/tf_static
TFMessage"| n69 - n72 -->|"/tf_static
TFMessage"| n79 - n76 -->|"/tf_static
TFMessage"| n69 - n76 -->|"/tf_static
TFMessage"| n79 - n83 -->|"/tf_static
TFMessage"| n69 - n83 -->|"/tf_static
TFMessage"| n79 + n34 -->|"/tf_static
TFMessage"| n80 + n73 -->|"/tf_static
TFMessage"| n69 + n73 -->|"/tf_static
TFMessage"| n80 + n77 -->|"/tf_static
TFMessage"| n69 + n77 -->|"/tf_static
TFMessage"| n80 + n84 -->|"/tf_static
TFMessage"| n69 + n84 -->|"/tf_static
TFMessage"| n80 n34 -->|"/uas2/mavlink_sink
Mavlink"| n36 n36 -->|"/uas2/mavlink_source
Mavlink"| n34 ``` @@ -1342,7 +1346,19 @@ graph LR }, { "dir": "sub", -"node": "/robot_1/perception/static_transform_publisher", +"node": "/robot_1/perception/macvo/macvo_node", +"qos_profile": { +"depth": "UNKNOWN", +"durability": "VOLATILE", +"history": "UNKNOWN", +"reliability": "BEST_EFFORT" +}, +"topic": "/clock", +"type": "rosgraph_msgs/msg/Clock" +}, +{ +"dir": "sub", +"node": "/robot_1/perception/macvo_ned_tf", "qos_profile": { "depth": "UNKNOWN", "durability": "VOLATILE", @@ -4305,18 +4321,6 @@ graph LR "type": "sensor_msgs/msg/JointState" }, { -"dir": "sub", -"node": "/robot_1/topic_keepalive", -"qos_profile": { -"depth": "UNKNOWN", -"durability": "VOLATILE", -"history": "UNKNOWN", -"reliability": "RELIABLE" -}, -"topic": "/robot_1/macvo/odometry", -"type": "nav_msgs/msg/Odometry" -}, -{ "dir": "pub", "node": "/robot_1/odometry_conversion/odometry_conversion", "qos_profile": { @@ -4425,6 +4429,18 @@ graph LR "type": "nav_msgs/msg/Odometry" }, { +"dir": "pub", +"node": "/robot_1/perception/macvo/macvo_node", +"qos_profile": { +"depth": "UNKNOWN", +"durability": "VOLATILE", +"history": "UNKNOWN", +"reliability": "RELIABLE" +}, +"topic": "/robot_1/perception/macvo/disparity", +"type": "sensor_msgs/msg/Image" +}, +{ "dir": "sub", "node": "/robot_1/droan/disparity_expander_node", "qos_profile": { @@ -4437,25 +4453,25 @@ graph LR "type": "stereo_msgs/msg/DisparityImage" }, { -"dir": "sub", -"node": "/robot_1/topic_keepalive", +"dir": "pub", +"node": "/robot_1/perception/macvo/macvo_node", "qos_profile": { "depth": "UNKNOWN", "durability": "VOLATILE", "history": "UNKNOWN", -"reliability": "BEST_EFFORT" +"reliability": "RELIABLE" }, -"topic": "/robot_1/perception/macvo/disparity", -"type": "sensor_msgs/msg/Image" +"topic": "/robot_1/perception/macvo/odometry", +"type": "nav_msgs/msg/Odometry" }, { -"dir": "sub", -"node": "/robot_1/topic_keepalive", +"dir": "pub", +"node": "/robot_1/perception/macvo/macvo_node", "qos_profile": { "depth": "UNKNOWN", "durability": "VOLATILE", "history": "UNKNOWN", -"reliability": "BEST_EFFORT" +"reliability": "RELIABLE" }, "topic": "/robot_1/perception/macvo/point_cloud", "type": "sensor_msgs/msg/PointCloud" @@ -4594,6 +4610,18 @@ graph LR }, { "dir": "sub", +"node": "/robot_1/perception/macvo/macvo_node", +"qos_profile": { +"depth": "UNKNOWN", +"durability": "VOLATILE", +"history": "UNKNOWN", +"reliability": "RELIABLE" +}, +"topic": "/robot_1/sensors/front_stereo/left/image_rect", +"type": "sensor_msgs/msg/Image" +}, +{ +"dir": "sub", "node": "/robot_1/perception/stereo_image_proc/disparity_node", "qos_profile": { "depth": "UNKNOWN", @@ -4642,6 +4670,18 @@ graph LR }, { "dir": "sub", +"node": "/robot_1/perception/macvo/macvo_node", +"qos_profile": { +"depth": "UNKNOWN", +"durability": "VOLATILE", +"history": "UNKNOWN", +"reliability": "RELIABLE" +}, +"topic": "/robot_1/sensors/front_stereo/right/camera_info", +"type": "sensor_msgs/msg/CameraInfo" +}, +{ +"dir": "sub", "node": "/robot_1/perception/stereo_image_proc/disparity_node", "qos_profile": { "depth": "UNKNOWN", @@ -4690,6 +4730,18 @@ graph LR }, { "dir": "sub", +"node": "/robot_1/perception/macvo/macvo_node", +"qos_profile": { +"depth": "UNKNOWN", +"durability": "VOLATILE", +"history": "UNKNOWN", +"reliability": "RELIABLE" +}, +"topic": "/robot_1/sensors/front_stereo/right/image_rect", +"type": "sensor_msgs/msg/Image" +}, +{ +"dir": "sub", "node": "/robot_1/perception/stereo_image_proc/disparity_node", "qos_profile": { "depth": "UNKNOWN", @@ -5266,7 +5318,7 @@ graph LR }, { "dir": "pub", -"node": "/robot_1/perception/static_transform_publisher", +"node": "/robot_1/perception/macvo_ned_tf", "qos_profile": { "depth": "UNKNOWN", "durability": "TRANSIENT_LOCAL", @@ -5446,7 +5498,8 @@ graph LR "/robot_1/interface/odom_modifier", "/robot_1/interface/robot_interface", "/robot_1/odometry_conversion/odometry_conversion", -"/robot_1/perception/static_transform_publisher", +"/robot_1/perception/macvo/macvo_node", +"/robot_1/perception/macvo_ned_tf", "/robot_1/perception/stereo_image_proc/disparity_node", "/robot_1/perception/stereo_pointcloud", "/robot_1/random_walk_node", @@ -7269,7 +7322,7 @@ graph LR }, "type": "sensor_msgs/msg/JointState" }, -"/robot_1/macvo/odometry": { +"/robot_1/odometry_conversion/odometry": { "qos": { "depth": "UNKNOWN", "durability": "VOLATILE", @@ -7278,30 +7331,30 @@ graph LR }, "type": "nav_msgs/msg/Odometry" }, -"/robot_1/odometry_conversion/odometry": { +"/robot_1/perception/macvo/disparity": { "qos": { "depth": "UNKNOWN", "durability": "VOLATILE", "history": "UNKNOWN", "reliability": "RELIABLE" }, -"type": "nav_msgs/msg/Odometry" +"type": "['sensor_msgs/msg/Image', 'stereo_msgs/msg/DisparityImage']" }, -"/robot_1/perception/macvo/disparity": { +"/robot_1/perception/macvo/odometry": { "qos": { "depth": "UNKNOWN", "durability": "VOLATILE", "history": "UNKNOWN", -"reliability": "BEST_EFFORT" +"reliability": "RELIABLE" }, -"type": "['sensor_msgs/msg/Image', 'stereo_msgs/msg/DisparityImage']" +"type": "nav_msgs/msg/Odometry" }, "/robot_1/perception/macvo/point_cloud": { "qos": { "depth": "UNKNOWN", "durability": "VOLATILE", "history": "UNKNOWN", -"reliability": "BEST_EFFORT" +"reliability": "RELIABLE" }, "type": "sensor_msgs/msg/PointCloud" }, From 6f436d879842abb28e1ca6faa3f6af2bc87abdec Mon Sep 17 00:00:00 2001 From: Andrew Jong Date: Thu, 20 Aug 2026 21:17:58 -0400 Subject: [PATCH 5/8] P5-E3a(rfc-379): flatten perception/sensors/global/behavior into stacks; prefix generic launch args MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - canonical module launch files (prefixed described args, set_remap in namespace groups, params via yaml): stereo_image_proc, topic_keepalive, lidar_point_cloud_filter, random_walk_planner (also fixes the old orphan's name= bug), drone_safety_monitor; stack entries wire every layer flat except interface (stays wrapped BY DESIGN until the RFC #380 platform-module refactor — it is the safety boundary) and logging (already self-contained) - generic launch-config names killed (the global-collision class that bit asm_optitrack): interpolate_dds_router config_file/args → dds_router_config_file/dds_router_args, gossip publish_rate → gossip_publish_rate, domain_bridge likewise — deprecated aliases kept and execution-tested; all trunk callers updated - allowlist 16→14 (deleted two dead orphan launches); add-ros2-package template no longer teaches remap-in-module patterns - frozen legacy files untouched except graph-neutral arg-name call sites Co-Authored-By: Claude Fable 5 --- .agents/skills/add-ros2-package/SKILL.md | 45 ++++---- .../assets/package_template/README.md | 8 +- .../launch/template.launch.xml | 101 ++++++++++-------- .agents/skills/update-documentation/SKILL.md | 8 +- .../launch/gossip.launch.xml | 23 ++-- docs/robot/autonomy/coordination/index.md | 2 +- docs/robot/autonomy/dds_router.md | 13 ++- .../launch/interpolate_dds_router.launch.py | 55 ++++++++-- .../interpolate_domain_bridge.launch.py | 55 ++++++++-- .../autonomy_bringup/launch/robot.launch.xml | 16 +-- .../drone_safety_monitor/CMakeLists.txt | 3 + .../launch/drone_safety_monitor.launch.xml | 33 ++++++ .../random_walk/launch/random_walk_launch.xml | 14 --- .../launch/random_walk_planner.launch.xml | 54 ++++++++++ .../launch/disparity_pcd.launch.xml | 18 ---- .../launch/stereo_image_proc.launch.xml | 68 ++++++++++++ .../launch/topic_keepalive.launch.xml | 14 +++ .../perception/perception_bringup/package.xml | 1 + .../lidar_point_cloud_filter.launch.xml | 28 ++++- stacks/full_default/README.md | 28 ++--- stacks/full_default/launch/stack.launch.xml | 88 +++++++++++---- stacks/full_droan_cpu/README.md | 7 +- stacks/full_droan_cpu/launch/stack.launch.xml | 88 +++++++++++---- stacks/full_macvo/README.md | 15 +-- stacks/full_macvo/launch/stack.launch.xml | 87 +++++++++++---- tests/meta/launch_lint_allowlist.txt | 2 - 26 files changed, 644 insertions(+), 230 deletions(-) create mode 100644 robot/ros_ws/src/behavior/drone_safety_monitor/launch/drone_safety_monitor.launch.xml delete mode 100644 robot/ros_ws/src/global/planners/random_walk/launch/random_walk_launch.xml create mode 100644 robot/ros_ws/src/global/planners/random_walk/launch/random_walk_planner.launch.xml delete mode 100644 robot/ros_ws/src/local/world_models/disparity_expansion/launch/disparity_pcd.launch.xml create mode 100644 robot/ros_ws/src/perception/perception_bringup/launch/stereo_image_proc.launch.xml create mode 100644 robot/ros_ws/src/perception/perception_bringup/launch/topic_keepalive.launch.xml diff --git a/.agents/skills/add-ros2-package/SKILL.md b/.agents/skills/add-ros2-package/SKILL.md index 4f0db5f8b..0e344f9ab 100644 --- a/.agents/skills/add-ros2-package/SKILL.md +++ b/.agents/skills/add-ros2-package/SKILL.md @@ -354,33 +354,40 @@ Create `config/.yaml` with default parameters: ### 8. Create Launch File -Create `launch/.launch.xml` with topic remapping: +Create `launch/.launch.xml` following the canonical module +launch pattern (RFC #379 §4 — see `.agents/skills/write-launch-file`): ```xml - - - - - - - - - - - - - - + + + + + + + + + + + + + + + ``` **Key points:** - Use `allow_substs="true"` to enable environment variable substitution in config files -- Define launch arguments for all topic names (enables flexible remapping) +- Declare a prefixed launch argument (with `description=`) for every topic endpoint, defaulting to the canonical name — never generic names like `config_file` (launch configurations leak across sibling includes) +- Use `set_remap` inside the module's group; module launch files never use `remap` tags — cross-module rewiring lives in the stack entry file (`stacks//launch/`), enforced by the single-locus lint (`tests/meta/test_launch_single_locus.py`) - Use `$(var arg_name)` to reference launch arguments - Use `$(env VAR_NAME)` for environment variables in configs diff --git a/.agents/skills/add-ros2-package/assets/package_template/README.md b/.agents/skills/add-ros2-package/assets/package_template/README.md index 20310deff..f8207befe 100644 --- a/.agents/skills/add-ros2-package/assets/package_template/README.md +++ b/.agents/skills/add-ros2-package/assets/package_template/README.md @@ -184,12 +184,12 @@ ros2 launch your_package your_package.launch.xml # With custom config ros2 launch your_package your_package.launch.xml \ - config_file:=/path/to/custom/config.yaml + your_module_config:=/path/to/custom/config.yaml -# With topic remapping +# With custom topic wiring (prefixed args; in a stack these are include args) ros2 launch your_package your_package.launch.xml \ - odometry_topic:=/robot/custom_odom \ - output_topic:=/robot/custom_output + your_module_odometry_topic:=/robot/custom_odom \ + your_module_output_topic:=/robot/custom_output ``` ### Integrated in Autonomy Stack diff --git a/.agents/skills/add-ros2-package/assets/package_template/launch/template.launch.xml b/.agents/skills/add-ros2-package/assets/package_template/launch/template.launch.xml index 91f753bbf..90d918e92 100644 --- a/.agents/skills/add-ros2-package/assets/package_template/launch/template.launch.xml +++ b/.agents/skills/add-ros2-package/assets/package_template/launch/template.launch.xml @@ -1,68 +1,77 @@ + - + - - - - - - - - - + + + + + + + + + + - + - - - - - + description="Node parameter YAML (loaded with allow_substs)"/> + - - + - + - - + + + + + + + - - + - - + + - - - - - - - - - - - - - - - + + - + diff --git a/.agents/skills/update-documentation/SKILL.md b/.agents/skills/update-documentation/SKILL.md index 595d18981..999a8b3e3 100644 --- a/.agents/skills/update-documentation/SKILL.md +++ b/.agents/skills/update-documentation/SKILL.md @@ -183,12 +183,12 @@ ros2 launch your_package your_package.launch.xml # With custom config ros2 launch your_package your_package.launch.xml \ - config_file:=/path/to/custom/config.yaml + your_module_config:=/path/to/custom/config.yaml -# With topic remapping +# With custom topic wiring (prefixed args; in a stack these are include args) ros2 launch your_package your_package.launch.xml \ - odometry_topic:=/robot/custom_odom \ - output_topic:=/robot/custom_output + your_module_odometry_topic:=/robot/custom_odom \ + your_module_output_topic:=/robot/custom_output ``` ### Integrated in Autonomy Stack diff --git a/common/ros_packages/coordination/coordination_bringup/launch/gossip.launch.xml b/common/ros_packages/coordination/coordination_bringup/launch/gossip.launch.xml index c293be260..484cc7592 100644 --- a/common/ros_packages/coordination/coordination_bringup/launch/gossip.launch.xml +++ b/common/ros_packages/coordination/coordination_bringup/launch/gossip.launch.xml @@ -1,22 +1,33 @@ - - + + + - - + - + diff --git a/docs/robot/autonomy/coordination/index.md b/docs/robot/autonomy/coordination/index.md index cabff92fd..1dd909252 100644 --- a/docs/robot/autonomy/coordination/index.md +++ b/docs/robot/autonomy/coordination/index.md @@ -49,7 +49,7 @@ Key parameters: | Parameter | Default | Description | |---|---|---| | `robot_name` | `$ROBOT_NAME` | Robot identifier and topic namespace | -| `publish_rate` | `1.0` | Publish rate in Hz (wall-clock) | +| `gossip_publish_rate` | `1.0` | Publish rate in Hz (wall-clock); launch arg (`publish_rate` is a deprecated alias) | | `gossip_domain` | `99` | Shared DDS domain | ## Monitoring diff --git a/docs/robot/autonomy/dds_router.md b/docs/robot/autonomy/dds_router.md index 2e36385e5..b6b619abe 100644 --- a/docs/robot/autonomy/dds_router.md +++ b/docs/robot/autonomy/dds_router.md @@ -57,19 +57,26 @@ The launch file recognises the same `$(...)` token syntax used in ROS 2 XML laun | Token | Resolved from | Error if missing? | |---|---|---| | `$(env VAR_NAME)` | Shell environment variable | Yes — `RuntimeError` | -| `$(var VAR_NAME)` | `args` launch argument (space-separated `key:=value` pairs) | Yes — `RuntimeError` | +| `$(var VAR_NAME)` | `dds_router_args` launch argument (space-separated `key:=value` pairs) | Yes — `RuntimeError` | | `$(find-pkg-share PKG)` | `ament_index` share directory of `PKG` | Yes — `RuntimeError` | **Example** — calling the launch file from XML: ```xml - - + ``` +!!! note "Renamed launch arguments (RFC #379 §4)" + The arguments were renamed from the generic `config_file` / `args` to the + prefixed `dds_router_config_file` / `dds_router_args` — generic launch + configurations leak across sibling includes in the same launch scope. The + old names still work as **deprecated aliases** (the prefixed name wins + when both are set); update external callers when convenient. + ### 2 — Config inheritance via `extends:` A YAML config may declare a top-level `extends:` key pointing to a base config file. The base is loaded first (chains are supported), and then the extending file's keys are **deep-merged** on top: diff --git a/robot/ros_ws/src/autonomy_bringup/launch/interpolate_dds_router.launch.py b/robot/ros_ws/src/autonomy_bringup/launch/interpolate_dds_router.launch.py index 13885e1ad..e6fac5df0 100644 --- a/robot/ros_ws/src/autonomy_bringup/launch/interpolate_dds_router.launch.py +++ b/robot/ros_ws/src/autonomy_bringup/launch/interpolate_dds_router.launch.py @@ -37,19 +37,26 @@ allowlist: !reset Launch arguments: - config_file (required) + dds_router_config_file (required) Absolute path to the DDS Router YAML config file. Typically resolved with find-pkg-share in the calling launch file, e.g.: $(find-pkg-share autonomy_bringup)/config/my_router.yaml - args (optional, default: "") + dds_router_args (optional, default: "") Space-separated key:=value pairs that resolve $(var key) tokens in the config file, e.g.: "gcs_domain:=0 robot_domain:=5" + config_file / args (DEPRECATED aliases) + Pre-RFC#379 generic names for the two arguments above. Generic launch + configurations leak across sibling includes in the same launch scope + (they are not scoped to the include), so the prefixed names are + canonical. The aliases are kept so external module stacks keep working; + the prefixed argument wins when both are set. + Example (XML caller): - - + + """ @@ -199,8 +206,26 @@ def _load_and_merge_config(config_file): def launch_dds_router(context, *args, **kwargs): - config_file = LaunchConfiguration('config_file').perform(context) - args_str = LaunchConfiguration('args').perform(context) + config_file = LaunchConfiguration('dds_router_config_file').perform(context) + args_str = LaunchConfiguration('dds_router_args').perform(context) + + # DEPRECATED aliases (pre-RFC#379 generic names): used only when the + # prefixed argument is unset. + legacy_config = LaunchConfiguration('config_file').perform(context) + legacy_args = LaunchConfiguration('args').perform(context) + if not config_file and legacy_config: + print("[interpolate_dds_router] WARNING: launch argument " + "'config_file' is deprecated — use 'dds_router_config_file'") + config_file = legacy_config + if not args_str and legacy_args: + print("[interpolate_dds_router] WARNING: launch argument " + "'args' is deprecated — use 'dds_router_args'") + args_str = legacy_args + if not config_file: + raise RuntimeError( + "interpolate_dds_router: required launch argument " + "'dds_router_config_file' was not provided" + ) # Parse args string: space-separated "key:=value" pairs, e.g. "gcs_domain:=0 foo:=bar" variables = {} @@ -264,20 +289,32 @@ def replace_var(match): def generate_launch_description(): return LaunchDescription([ DeclareLaunchArgument( - 'config_file', + 'dds_router_config_file', + default_value='', description=( 'Absolute path to the DDS Router YAML config file. ' 'Supports $(find-pkg-share PKG), $(env ENV_VAR), $(var key) substitution ' - 'syntax and an "extends:" key for config inheritance.' + 'syntax and an "extends:" key for config inheritance. Required ' + '(default is empty only so the deprecated alias can fill in).' ), ), DeclareLaunchArgument( - 'args', + 'dds_router_args', default_value='', description=( 'Space-separated key:=value pairs used to resolve $(var key) ' 'substitutions in the config file, e.g. "gcs_domain:=0 foo:=bar".' ), ), + DeclareLaunchArgument( + 'config_file', + default_value='', + description='DEPRECATED alias for dds_router_config_file.', + ), + DeclareLaunchArgument( + 'args', + default_value='', + description='DEPRECATED alias for dds_router_args.', + ), OpaqueFunction(function=launch_dds_router), ]) diff --git a/robot/ros_ws/src/autonomy_bringup/launch/interpolate_domain_bridge.launch.py b/robot/ros_ws/src/autonomy_bringup/launch/interpolate_domain_bridge.launch.py index 96b644498..3deb889a7 100644 --- a/robot/ros_ws/src/autonomy_bringup/launch/interpolate_domain_bridge.launch.py +++ b/robot/ros_ws/src/autonomy_bringup/launch/interpolate_domain_bridge.launch.py @@ -41,19 +41,26 @@ topics: !reset Launch arguments: - config_file (required) + domain_bridge_config_file (required) Absolute path to the domain bridge YAML config file. Typically resolved with find-pkg-share in the calling launch file, e.g.: $(find-pkg-share autonomy_bringup)/config/my_bridge.yaml - args (optional, default: "") + domain_bridge_args (optional, default: "") Space-separated key:=value pairs that resolve $(var key) tokens in the config file, e.g.: "gcs_domain:=0 robot_domain:=5" + config_file / args (DEPRECATED aliases) + Pre-RFC#379 generic names for the two arguments above. Generic launch + configurations leak across sibling includes in the same launch scope + (they are not scoped to the include), so the prefixed names are + canonical. The aliases are kept for old callers; the prefixed argument + wins when both are set. + Example (XML caller): - - + + """ @@ -204,8 +211,26 @@ def _load_and_merge_config(config_file): def launch_domain_bridge(context, *args, **kwargs): - config_file = LaunchConfiguration('config_file').perform(context) - args_str = LaunchConfiguration('args').perform(context) + config_file = LaunchConfiguration('domain_bridge_config_file').perform(context) + args_str = LaunchConfiguration('domain_bridge_args').perform(context) + + # DEPRECATED aliases (pre-RFC#379 generic names): used only when the + # prefixed argument is unset. + legacy_config = LaunchConfiguration('config_file').perform(context) + legacy_args = LaunchConfiguration('args').perform(context) + if not config_file and legacy_config: + print("[interpolate_domain_bridge] WARNING: launch argument " + "'config_file' is deprecated — use 'domain_bridge_config_file'") + config_file = legacy_config + if not args_str and legacy_args: + print("[interpolate_domain_bridge] WARNING: launch argument " + "'args' is deprecated — use 'domain_bridge_args'") + args_str = legacy_args + if not config_file: + raise RuntimeError( + "interpolate_domain_bridge: required launch argument " + "'domain_bridge_config_file' was not provided" + ) # Parse args string: space-separated "key:=value" pairs, e.g. "gcs_domain:=0 foo:=bar" variables = {} @@ -267,20 +292,32 @@ def replace_var(match): def generate_launch_description(): return LaunchDescription([ DeclareLaunchArgument( - 'config_file', + 'domain_bridge_config_file', + default_value='', description=( 'Absolute path to the domain bridge YAML config file. ' 'Supports $(find-pkg-share PKG), $(env ENV_VAR), $(var key) substitution ' - 'syntax and an "extends:" key for config inheritance.' + 'syntax and an "extends:" key for config inheritance. Required ' + '(default is empty only so the deprecated alias can fill in).' ), ), DeclareLaunchArgument( - 'args', + 'domain_bridge_args', default_value='', description=( 'Space-separated key:=value pairs used to resolve $(var key) ' 'substitutions in the config file, e.g. "gcs_domain:=0 foo:=bar".' ), ), + DeclareLaunchArgument( + 'config_file', + default_value='', + description='DEPRECATED alias for domain_bridge_config_file.', + ), + DeclareLaunchArgument( + 'args', + default_value='', + description='DEPRECATED alias for domain_bridge_args.', + ), OpaqueFunction(function=launch_domain_bridge), ]) diff --git a/robot/ros_ws/src/autonomy_bringup/launch/robot.launch.xml b/robot/ros_ws/src/autonomy_bringup/launch/robot.launch.xml index e93244e73..77cba4f66 100644 --- a/robot/ros_ws/src/autonomy_bringup/launch/robot.launch.xml +++ b/robot/ros_ws/src/autonomy_bringup/launch/robot.launch.xml @@ -58,15 +58,15 @@ lite, desktop_split sim) - - + - - + +
@@ -77,15 +77,15 @@ lite, desktop_split sim) - - + - - + +
diff --git a/robot/ros_ws/src/behavior/drone_safety_monitor/CMakeLists.txt b/robot/ros_ws/src/behavior/drone_safety_monitor/CMakeLists.txt index d9350e958..00877de7c 100644 --- a/robot/ros_ws/src/behavior/drone_safety_monitor/CMakeLists.txt +++ b/robot/ros_ws/src/behavior/drone_safety_monitor/CMakeLists.txt @@ -29,6 +29,9 @@ ament_target_dependencies( install(TARGETS drone_safety_monitor DESTINATION lib/${PROJECT_NAME}) +install(DIRECTORY launch + DESTINATION share/${PROJECT_NAME}) + if(BUILD_TESTING) find_package(ament_lint_auto REQUIRED) set(ament_cmake_copyright_FOUND TRUE) diff --git a/robot/ros_ws/src/behavior/drone_safety_monitor/launch/drone_safety_monitor.launch.xml b/robot/ros_ws/src/behavior/drone_safety_monitor/launch/drone_safety_monitor.launch.xml new file mode 100644 index 000000000..f97055f62 --- /dev/null +++ b/robot/ros_ws/src/behavior/drone_safety_monitor/launch/drone_safety_monitor.launch.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + diff --git a/robot/ros_ws/src/global/planners/random_walk/launch/random_walk_launch.xml b/robot/ros_ws/src/global/planners/random_walk/launch/random_walk_launch.xml deleted file mode 100644 index dfe624ca9..000000000 --- a/robot/ros_ws/src/global/planners/random_walk/launch/random_walk_launch.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - diff --git a/robot/ros_ws/src/global/planners/random_walk/launch/random_walk_planner.launch.xml b/robot/ros_ws/src/global/planners/random_walk/launch/random_walk_planner.launch.xml new file mode 100644 index 000000000..fcb73f270 --- /dev/null +++ b/robot/ros_ws/src/global/planners/random_walk/launch/random_walk_planner.launch.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/robot/ros_ws/src/local/world_models/disparity_expansion/launch/disparity_pcd.launch.xml b/robot/ros_ws/src/local/world_models/disparity_expansion/launch/disparity_pcd.launch.xml deleted file mode 100644 index 52ab50616..000000000 --- a/robot/ros_ws/src/local/world_models/disparity_expansion/launch/disparity_pcd.launch.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/robot/ros_ws/src/perception/perception_bringup/launch/stereo_image_proc.launch.xml b/robot/ros_ws/src/perception/perception_bringup/launch/stereo_image_proc.launch.xml new file mode 100644 index 000000000..45a031273 --- /dev/null +++ b/robot/ros_ws/src/perception/perception_bringup/launch/stereo_image_proc.launch.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/robot/ros_ws/src/perception/perception_bringup/launch/topic_keepalive.launch.xml b/robot/ros_ws/src/perception/perception_bringup/launch/topic_keepalive.launch.xml new file mode 100644 index 000000000..d0cd89e34 --- /dev/null +++ b/robot/ros_ws/src/perception/perception_bringup/launch/topic_keepalive.launch.xml @@ -0,0 +1,14 @@ + + + + diff --git a/robot/ros_ws/src/perception/perception_bringup/package.xml b/robot/ros_ws/src/perception/perception_bringup/package.xml index 7e5799046..89b2a3a12 100644 --- a/robot/ros_ws/src/perception/perception_bringup/package.xml +++ b/robot/ros_ws/src/perception/perception_bringup/package.xml @@ -10,6 +10,7 @@ ament_cmake rclpy + stereo_image_proc sensor_msgs visualization_msgs nav_msgs diff --git a/robot/ros_ws/src/sensors/lidar_point_cloud_filter/launch/lidar_point_cloud_filter.launch.xml b/robot/ros_ws/src/sensors/lidar_point_cloud_filter/launch/lidar_point_cloud_filter.launch.xml index 9f92302e2..85c111a24 100644 --- a/robot/ros_ws/src/sensors/lidar_point_cloud_filter/launch/lidar_point_cloud_filter.launch.xml +++ b/robot/ros_ws/src/sensors/lidar_point_cloud_filter/launch/lidar_point_cloud_filter.launch.xml @@ -1,9 +1,35 @@ + + + + + - + + + + diff --git a/stacks/full_default/README.md b/stacks/full_default/README.md index 3bc0b5f09..11744ea0d 100644 --- a/stacks/full_default/README.md +++ b/stacks/full_default/README.md @@ -5,16 +5,18 @@ This is the stack most users start from and the baseline other stacks are copied ## What it launches -The entry point `launch/stack.launch.xml` composes the **Local layer as a -flat set of module-launch includes** (flattened in E2: takeoff/land task -server, fixed-trajectory task server, GPU DROAN planner, trajectory -controller, PID controller — each module launch file declares its topic -endpoints as args with canonical defaults, so bare includes mean canonical -wiring), and includes the remaining layer bringups — interface, sensors, -perception, global, behavior, logging — exactly as -`onboard_autonomy_all.launch.xml` does with default args, plus the two +The entry point `launch/stack.launch.xml` composes **every layer as a flat +set of module-launch includes** (Local flattened in E2; sensors, perception, +global, and behavior in E3): LiDAR near-range filter, stereo +disparity/point-cloud pair, topic keepalive, takeoff/land task server, +fixed-trajectory task server, GPU DROAN planner, trajectory controller, PID +controller, VDB mapping, random-walk global planner, and the drone safety +monitor — each module launch file declares its topic endpoints as args with +canonical defaults, so bare includes mean canonical wiring. Plus the two role-`full` extras (DDS-router domain bridge to the GCS, gossip coordination -layer). Flattening the remaining layers arrives in E3. +layer). Two blocks stay wrapped by design: `interface.launch.py` (the safety +boundary; flattens with RFC #380 Part 2's platform modules) and +`logging.launch.xml` (already a single self-contained module). ## Equivalence claim @@ -39,10 +41,10 @@ The shared per-robot preamble (ROBOT_NAME namespace, `use_sim_time`, ## Known limits -- Partially flattened: the Local layer is composed module-by-module in - `stack.launch.xml`; the other layers' cross-module remaps still live inside - their bringup launch files until E3. Grep those bringups (or read - `wiring.md`) for their actual topic wiring. +- The interface layer is a wrapped include (`interface.launch.py`) — its + MAVROS wiring is not visible in `stack.launch.xml`; read `wiring.md` for + the observed graph. It flattens with the platform-module refactor + (RFC #380 Part 2). - `modules.repos` pins no external modules yet; every package is trunk-resident. - `docker-compose.yaml` is a stub — per-stack image composition arrives with the first module pins; trunk compose profiles provide all services. diff --git a/stacks/full_default/launch/stack.launch.xml b/stacks/full_default/launch/stack.launch.xml index d7c442cf3..fa8fd0618 100644 --- a/stacks/full_default/launch/stack.launch.xml +++ b/stacks/full_default/launch/stack.launch.xml @@ -1,12 +1,24 @@ - + - - + - - + + + + + + + + + + + + + @@ -59,32 +88,49 @@ interface (cmd_roll_pitch_yawrate_thrust) --> - + + + + + + + + + - - + - - + + - + - + - - + - + diff --git a/stacks/full_droan_cpu/README.md b/stacks/full_droan_cpu/README.md index 3e286f6d9..c5c1290e4 100644 --- a/stacks/full_droan_cpu/README.md +++ b/stacks/full_droan_cpu/README.md @@ -33,9 +33,10 @@ airstack ready ## Known limits -- Partially flattened: the Local layer is composed module-by-module in - `stack.launch.xml`; the other layers are still wrapped bringup includes - (they flatten in E3). +- Fully flattened (E2 local, E3 the rest): every layer is composed + module-by-module in `stack.launch.xml`, except `interface.launch.py` + (wrapped by design — the safety boundary; flattens with RFC #380 Part 2) + and `logging.launch.xml` (already a single self-contained module). - CPU DROAN is the slower planner path — use `full_default` (GPU `droan_gl`) unless you need to run without the GPU planner or are debugging `disparity_expansion`. diff --git a/stacks/full_droan_cpu/launch/stack.launch.xml b/stacks/full_droan_cpu/launch/stack.launch.xml index b046fa9be..63efb3875 100644 --- a/stacks/full_droan_cpu/launch/stack.launch.xml +++ b/stacks/full_droan_cpu/launch/stack.launch.xml @@ -7,12 +7,24 @@ instead of the GPU droan_gl module. Everything else is byte-identical to full_default/launch/stack.launch.xml. - LOCAL LAYER FLATTENED (P5-E2): the Local layer is composed here as a flat - set of module-launch includes — each block below reads "this module, these - connections". Every module launch file declares its topic endpoints as args - with canonical defaults (today's graph), so a bare include means "wired - canonically"; only deviations from canonical appear as include args. The - other layers remain wrapped (their bringup includes) until E3. + ALL LAYERS FLATTENED (P5-E2 local, P5-E3 the rest): every layer is composed + here as a flat set of module-launch includes — each block below reads "this + module, these connections". Every module launch file declares its topic + endpoints as args with canonical defaults (today's graph), so a bare + include means "wired canonically"; only deviations from canonical appear as + include args. + + Two blocks stay WRAPPED BY DESIGN (not a migration leftover): + - interface.launch.py — MAVROS wiring is dense, FCU_URL is env-computed + in python, and the interface layer is the safety boundary (arming, + command authority, takeover). It stays a single include until the + platform-module refactor (RFC #380 Part 2). + - interpolate_dds_router / gossip — cross-domain bridging helpers, not + robot-graph modules; their wiring lives in DDS-router YAML configs, so + the include + config-file arg IS their single locus. + logging.launch.xml is included as-is because it already is a single + self-contained module (an env-gated bag recorder behind RECORD_BAGS / + LOG_CONFIG) — there is nothing to flatten. Launched by autonomy_bringup/launch/robot.launch.xml when AIRSTACK_STACK_DIR points at this folder (set by `airstack up` with the stack flag). The shared @@ -26,16 +38,33 @@ --> - + - - + - - + + + + + + + + + + + + + @@ -70,32 +99,49 @@ interface (cmd_roll_pitch_yawrate_thrust) --> - + + + + + + + + + - - + - - + + - + - + - - + - + diff --git a/stacks/full_macvo/README.md b/stacks/full_macvo/README.md index 63b632f10..f7e4653b6 100644 --- a/stacks/full_macvo/README.md +++ b/stacks/full_macvo/README.md @@ -53,13 +53,14 @@ airstack ready ## Known limits -- Partially flattened: the Local layer is composed module-by-module in - `stack.launch.xml`; the other layers are still wrapped bringup includes - (they flatten in E3). -- `stereo_image_proc` still runs alongside MAC-VO (perception's - `launch_stereo_image_proc` default is kept `true`): the stereo point cloud - feeds other consumers, so this stack runs both estimators. A leaner - macvo-only preset can flip that arg once downstream consumers are audited. +- Fully flattened (E2 local, E3 the rest): every layer is composed + module-by-module in `stack.launch.xml`, except `interface.launch.py` + (wrapped by design — the safety boundary; flattens with RFC #380 Part 2) + and `logging.launch.xml` (already a single self-contained module). +- `stereo_image_proc` still runs alongside MAC-VO (its module include is + kept in `stack.launch.xml`): the stereo point cloud feeds other consumers, + so this stack runs both estimators. A leaner macvo-only preset can drop + that include once downstream consumers are audited. - MAC-VO is GPU-heavy; expect reduced sim real-time factor on a shared GPU. - No committed equivalence baseline exists for this topology (the legacy variant never worked), so the first post-module-swap `wiring.md` snapshot diff --git a/stacks/full_macvo/launch/stack.launch.xml b/stacks/full_macvo/launch/stack.launch.xml index c40795bec..904c60cfe 100644 --- a/stacks/full_macvo/launch/stack.launch.xml +++ b/stacks/full_macvo/launch/stack.launch.xml @@ -15,12 +15,24 @@ never declared, and pointed at the stale /$ROBOT_NAME/macvo/disparity topic. It was deleted in P5-E2; the README records the details.) - LOCAL LAYER FLATTENED (P5-E2): the Local layer is composed here as a flat - set of module-launch includes — each block below reads "this module, these - connections". Every module launch file declares its topic endpoints as args - with canonical defaults (today's graph), so a bare include means "wired - canonically"; only deviations from canonical appear as include args. The - other layers remain wrapped (their bringup includes) until E3. + ALL LAYERS FLATTENED (P5-E2 local, P5-E3 the rest): every layer is composed + here as a flat set of module-launch includes — each block below reads "this + module, these connections". Every module launch file declares its topic + endpoints as args with canonical defaults (today's graph), so a bare + include means "wired canonically"; only deviations from canonical appear as + include args. + + Two blocks stay WRAPPED BY DESIGN (not a migration leftover): + - interface.launch.py — MAVROS wiring is dense, FCU_URL is env-computed + in python, and the interface layer is the safety boundary (arming, + command authority, takeover). It stays a single include until the + platform-module refactor (RFC #380 Part 2). + - interpolate_dds_router / gossip — cross-domain bridging helpers, not + robot-graph modules; their wiring lives in DDS-router YAML configs, so + the include + config-file arg IS their single locus. + logging.launch.xml is included as-is because it already is a single + self-contained module (an env-gated bag recorder behind RECORD_BAGS / + LOG_CONFIG) — there is nothing to flatten. Launched by autonomy_bringup/launch/robot.launch.xml when AIRSTACK_STACK_DIR points at this folder (set by `airstack up` with the stack flag). The shared @@ -29,16 +41,32 @@ --> - + - - + - - + + + + + + + + + + + + + - + + + + + + + + + - - + - - + + - + - + - - + - + diff --git a/tests/meta/launch_lint_allowlist.txt b/tests/meta/launch_lint_allowlist.txt index c66c31ffc..f1125c3aa 100644 --- a/tests/meta/launch_lint_allowlist.txt +++ b/tests/meta/launch_lint_allowlist.txt @@ -17,10 +17,8 @@ robot/ros_ws/src/global/planners/exploration/launch/robot_launch_gazebo/gz_behav robot/ros_ws/src/global/planners/exploration/launch/robot_launch_gazebo/gz_global_launch.xml robot/ros_ws/src/global/planners/exploration/launch/robot_launch_gazebo/gz_interface_launch.xml robot/ros_ws/src/global/planners/exploration/launch/robot_launch_gazebo/gz_local_launch.xml -robot/ros_ws/src/global/planners/random_walk/launch/random_walk_launch.xml robot/ros_ws/src/interface/interface_bringup/launch/interface.launch.py robot/ros_ws/src/interface/px4_interface/launch/px4_interface.launch.xml robot/ros_ws/src/interface/robot_interface/launch/odometry_conversion.xml robot/ros_ws/src/local/local_bringup/launch/local.launch.xml -robot/ros_ws/src/local/world_models/disparity_expansion/launch/disparity_pcd.launch.xml robot/ros_ws/src/perception/perception_bringup/launch/perception.launch.xml From 9cc7b3383ac544417f7d24b5939bcaf061a93a8e Mon Sep 17 00:00:00 2001 From: Andrew Jong Date: Thu, 20 Aug 2026 21:18:16 -0400 Subject: [PATCH 6/8] P5-E3b(rfc-379): split stack + bridge.yaml, doctor, interface conventions spec, stack CLI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - stacks/lite_default (onboard-lite) + stacks/lite_offload_global — the first SPLIT stack: onboard/offboard entry files + bridge.yaml explicitly listing every boundary crossing (seeded from the legacy dds_router allowlist; set_trajectory_mode deliberately NOT bridged — the legacy config bridged it, violating what is now doctor hard gate #2) - tools/gen_dds_router.py: bridge.yaml → deterministic DDS-router config; --check enforces hard gate #2 (control/trajectory topics in a bridge) - airstack doctor: compose-time checks (manifests, overlay, dep-conflict gate, stack anatomy, bridge gate) — observe-and-report except the two RFC- enumerated hard gates; --live diffs the running graph vs wiring.md + flags unblessed control-setpoint publishers; --snapshot writes hardware-observed wiring.md with unverified-in-CI provenance - airstack stack list|new|diff (diff compares generated wiring, not XML) - docs/robot/autonomy/interface_conventions.md v1.0.0: the versioned narrow- waist spec (names/types/QoS/frames/rate/placement per interchange point, verified against observed wiring; fixes stale PointStamped claims) - 45+ new unit tests; integrate-module-into-layer rewritten for stacks; create-stack skill; .agents README index rebuilt (was 8 rows, now 22) Split-stack live two-host bring-up deferred to P6 (needs hosts: placement). Co-Authored-By: Claude Fable 5 --- .agents/README.md | 69 ++- .agents/skills/create-stack/SKILL.md | 204 +++++++ .../integrate-module-into-layer/SKILL.md | 505 ++++----------- .airstack/modules/doctor.sh | 37 ++ .airstack/modules/stack.sh | 152 +++++ airstack.sh | 57 ++ docs/development/stacks.md | 120 +++- docs/robot/autonomy/integration_checklist.md | 5 + docs/robot/autonomy/interface_conventions.md | 247 ++++++++ mkdocs.yml | 1 + stacks/lite_default/README.md | 75 +++ stacks/lite_default/docker-compose.yaml | 11 + stacks/lite_default/launch/stack.launch.xml | 98 +++ stacks/lite_default/modules.repos | 15 + stacks/lite_offload_global/README.md | 78 +++ stacks/lite_offload_global/bridge.yaml | 119 ++++ .../lite_offload_global/docker-compose.yaml | 12 + .../launch/offboard.launch.xml | 32 + .../launch/onboard.launch.xml | 102 ++++ stacks/lite_offload_global/modules.repos | 15 + tests/meta/test_bridge_contract.py | 257 ++++++++ tests/meta/test_doctor_contract.py | 327 ++++++++++ tests/meta/test_stack_layout_contract.py | 21 + tools/doctor/__init__.py | 152 +++++ tools/doctor/checks.py | 573 ++++++++++++++++++ tools/gen_dds_router.py | 358 +++++++++++ tools/stack_diff.py | 123 ++++ 27 files changed, 3342 insertions(+), 423 deletions(-) create mode 100644 .agents/skills/create-stack/SKILL.md create mode 100644 .airstack/modules/doctor.sh create mode 100644 .airstack/modules/stack.sh create mode 100644 docs/robot/autonomy/interface_conventions.md create mode 100644 stacks/lite_default/README.md create mode 100644 stacks/lite_default/docker-compose.yaml create mode 100644 stacks/lite_default/launch/stack.launch.xml create mode 100644 stacks/lite_default/modules.repos create mode 100644 stacks/lite_offload_global/README.md create mode 100644 stacks/lite_offload_global/bridge.yaml create mode 100644 stacks/lite_offload_global/docker-compose.yaml create mode 100644 stacks/lite_offload_global/launch/offboard.launch.xml create mode 100644 stacks/lite_offload_global/launch/onboard.launch.xml create mode 100644 stacks/lite_offload_global/modules.repos create mode 100644 tests/meta/test_bridge_contract.py create mode 100644 tests/meta/test_doctor_contract.py create mode 100644 tools/doctor/__init__.py create mode 100644 tools/doctor/checks.py create mode 100644 tools/gen_dds_router.py create mode 100644 tools/stack_diff.py diff --git a/.agents/README.md b/.agents/README.md index 863cef1db..3cd3a8b18 100644 --- a/.agents/README.md +++ b/.agents/README.md @@ -7,43 +7,38 @@ This directory contains Agent Skills following the [Agent Skills standard](https ``` .agents/ ├── README.md # This file -└── skills/ # Agent Skills directory - ├── add-ros2-package/ - │ └── SKILL.md # Create new ROS 2 packages - ├── add-task-executor/ - │ └── SKILL.md # Implement a task executor as a ROS 2 action server - ├── integrate-module-into-layer/ - │ └── SKILL.md # Integrate modules into layer bringup - ├── write-isaac-sim-scene/ - │ └── SKILL.md # Create Isaac Sim simulation scenes - ├── debug-module/ - │ └── SKILL.md # Autonomous debugging strategies - ├── update-documentation/ - │ └── SKILL.md # Document modules and update mkdocs - ├── test-in-simulation/ - │ └── SKILL.md # End-to-end simulation testing - └── add-behavior-tree-node/ - └── SKILL.md # Create behavior tree nodes +└── skills/ # One directory per skill + └── / + ├── SKILL.md # YAML frontmatter + step-by-step instructions + └── assets/ # (optional) templates and reference files ``` -## Skills Overview - -Each skill is a directory containing a `SKILL.md` file with: -- **YAML frontmatter:** Name, description, license, metadata -- **Markdown body:** Step-by-step instructions for the workflow - -### Available Skills +## Available Skills | Skill | Purpose | |-------|---------| -| [add-ros2-package](skills/add-ros2-package) | Create a new ROS 2 package for the autonomy stack | -| [add-task-executor](skills/add-task-executor) | Implement a task executor as a ROS 2 action server | -| [integrate-module-into-layer](skills/integrate-module-into-layer) | Integrate a module into layer bringup | -| [write-isaac-sim-scene](skills/write-isaac-sim-scene) | Create custom simulation environments | -| [debug-module](skills/debug-module) | Systematically debug ROS 2 modules | -| [update-documentation](skills/update-documentation) | Document modules and update mkdocs | -| [test-in-simulation](skills/test-in-simulation) | Test modules in Isaac Sim | -| [add-behavior-tree-node](skills/add-behavior-tree-node) | Create behavior tree nodes | +| [add-behavior-tree-node](skills/add-behavior-tree-node) | Create behavior tree nodes for high-level mission logic | +| [add-ros2-package](skills/add-ros2-package) | Create a new ROS 2 package (module) from the template | +| [add-task-executor](skills/add-task-executor) | Implement a task executor as a ROS 2 action server (`tasks/*`) | +| [add-unit-tests](skills/add-unit-tests) | Add co-located Python/C++ unit tests and register them for CI | +| [attach-gossip-payload](skills/attach-gossip-payload) | Broadcast custom ROS messages to peers via PeerProfile gossip payloads | +| [bump-version-and-release](skills/bump-version-and-release) | Bump `.env` VERSION + CHANGELOG to clear the version-check gate | +| [capture-discovered-knowledge](skills/capture-discovered-knowledge) | Persist hard-won discoveries to AGENTS.md or a skill | +| [configure-multi-robot](skills/configure-multi-robot) | Multi-robot setup: replicas, ROBOT_NAME, ROS_DOMAIN_ID | +| [create-module](skills/create-module) | Author a thin module repo (module.yaml manifest, CI, test_stack) | +| [create-stack](skills/create-stack) | Create a stack folder: `airstack stack new`, wiring bootstrap, split stacks + bridge.yaml, doctor | +| [debug-module](skills/debug-module) | Systematic autonomous debugging of ROS 2 modules | +| [docker-build-profiles](skills/docker-build-profiles) | Build-time validation for Docker compose profiles and build args | +| [integrate-module-into-layer](skills/integrate-module-into-layer) | Integrate a module into a **stack** (stack entry include, canonical defaults, wiring.md regen, lint) — the layer-bringup workflow is legacy | +| [run-system-tests](skills/run-system-tests) | Run/extend the pytest system-test harness (marks, MetricsRecorder, /pytest) | +| [test-in-simulation](skills/test-in-simulation) | End-to-end module testing in Isaac Sim | +| [update-documentation](skills/update-documentation) | Document modules and update mkdocs navigation | +| [use-airstack-cli](skills/use-airstack-cli) | The `airstack` CLI and the non-interactive `docker exec` pattern | +| [use-feature-notebook](skills/use-feature-notebook) | Local notebook/ entry (design spec + results) for every feature | +| [visualize-in-foxglove](skills/visualize-in-foxglove) | Add topic visualization to Foxglove/GCS | +| [write-isaac-sim-scene](skills/write-isaac-sim-scene) | Create custom Isaac Sim scenes on pegasus_app | +| [write-launch-file](skills/write-launch-file) | Launch-file conventions: canonical-default topic args, ROBOT_NAME namespacing, single-locus rule | +| [write-mkdocs-documentation](skills/write-mkdocs-documentation) | Writing effective MkDocs documentation | ## Usage @@ -62,12 +57,20 @@ When adding new skills: 2. Add `SKILL.md` with proper YAML frontmatter 3. Follow Agent Skills format specification 4. Reference related skills using relative paths (`../other-skill/`) -5. Update this README with the new skill +5. Update this README **and** the skills table in [AGENTS.md](../AGENTS.md) + +Skills ship with the mechanism, not after it (RFC #379 §10): when a workflow +changes (e.g. layer bringups → stacks), update the affected skills in the same +PR as the machinery, or every agent session will faithfully reintroduce the +old pattern. ## References - **Main Guide:** [AGENTS.md](../AGENTS.md) - **Agent Skills Spec:** [https://agentskills.io](https://agentskills.io) - **System Architecture:** [docs/robot/autonomy/system_architecture.md](../docs/robot/autonomy/system_architecture.md) +- **Interface Conventions Spec:** [docs/robot/autonomy/interface_conventions.md](../docs/robot/autonomy/interface_conventions.md) - **Integration Checklist:** [docs/robot/autonomy/integration_checklist.md](../docs/robot/autonomy/integration_checklist.md) +- **Stacks Guide:** [docs/development/stacks.md](../docs/development/stacks.md) +- **Modules Guide:** [docs/development/modules.md](../docs/development/modules.md) - **AI Agent Guide:** [docs/development/ai_agent_guide.md](../docs/development/ai_agent_guide.md) diff --git a/.agents/skills/create-stack/SKILL.md b/.agents/skills/create-stack/SKILL.md new file mode 100644 index 000000000..8a6ab6001 --- /dev/null +++ b/.agents/skills/create-stack/SKILL.md @@ -0,0 +1,204 @@ +--- +name: create-stack +description: Create a new AirStack stack folder — copy a reference stack with `airstack stack new`, edit the entry launch file(s), bootstrap wiring.md, and validate with doctor and the unit lints. Covers stack anatomy, split stacks (multiple entry points + bridge.yaml), the control/trajectory placement hard gate, and gen_dds_router. +license: Apache-2.0 +metadata: + author: AirLab CMU + repository: AirStack +--- + +# Skill: Create a Stack + +## When to Use + +When you need a topology different from an existing stack: a different module +mix, an experiment variant, an onboard/offboard split, or an unconventional +graph (an end-to-end planner is the same case, not a special one — RFC #379 +§3). A **stack** is a self-contained folder; making one never touches +`autonomy_bringup` or any `*_bringup` package. + +## Stack anatomy (the contract) + +```text +stacks// +├── modules.repos # pinned module list (vcstool) + top-level airstack_compat +├── launch/ # entry point(s): stack.launch.xml for unsplit; +│ └── ... # one .launch.xml per host for split stacks +├── docker-compose.yaml # per-stack image composition (stub until module pins) +├── wiring.md # GENERATED from the running graph — never hand-edited +├── README.md # purpose, how to run, known limits +└── bridge.yaml # SPLIT STACKS ONLY — every crossing topic/service/action +``` + +Enforced by `tests/meta/test_stack_layout_contract.py` (unit mark): the four +files always; ≥2 entry points ⇒ `bridge.yaml` required; `wiring.md` optional +only until the first snapshot lands, and it must carry the machine-readable +trailer when present. + +## Steps — unsplit stack + +### 1. Copy a reference + +```bash +airstack stack list # see what exists +airstack stack new full_default my_experiment # refuses overwrite +``` + +Pick the closest starting point: `full_default` (everything onboard), +`lite_default` (no global/logging), `full_droan_cpu`, `full_macvo`. +`stack new` deliberately does **not** copy `wiring.md` — that file is the +*source* stack's observed graph and would lie about yours. + +### 2. Edit the entry file + +`stacks/my_experiment/launch/stack.launch.xml` is the wiring document: a flat +list of module ``s, each with a comment reading "this module, these +connections". Swap/add/remove includes; pass ONLY deviations from canonical +defaults (see [integrate-module-into-layer](../integrate-module-into-layer) +and the [Interface Conventions Spec](../../../docs/robot/autonomy/interface_conventions.md)). + +Rules the unit lint enforces (`test_launch_single_locus.py`, +`test_stack_layout_contract.py`): + +- XML only at the stack level; every declared `` has a `description=`. +- Never include `robot.launch.xml` (it's the dispatcher that includes YOU — + infinite recursion). +- Remaps are allowed *only* here (`stacks/*/launch/`). + +External modules: pin them in `modules.repos` (`airstack module add +--version ` writes the checkout-level file; a stack commits its own +pins) and update `airstack_compat`. + +### 3. Update README.md + +Purpose, what it launches, how to run, known limits. The layout contract +rejects trivial READMEs (<200 chars). + +### 4. Run it + +```bash +airstack up --stack my_experiment --sim isaac --robots 1 +airstack ready +``` + +Stack launch files are bind-mounted — edit and re-launch without rebuilding. + +### 5. Bootstrap wiring.md + +```bash +airstack test -m wiring --stack my_experiment --sim isaacsim --num-robots 1 +``` + +With no committed `wiring.md` the test PASSES and logs an INSTRUCTION: +validate `tests/results//wiring/observed_my_experiment.md`, copy it to +`stacks/my_experiment/wiring.md`, commit. From then on CI drift-checks the +running graph against it. **Needs a GPU + sim license** (the ephemeral CI +runner, or a workstation). Hardware-only stacks instead run +`airstack doctor --snapshot --stack my_experiment`, which writes `wiring.md` +with a provenance line (`observed on , , — +unverified-in-CI`). + +### 6. Validate + +```bash +airstack doctor # anatomy, module manifests, overlay, both hard gates +airstack test -m unit -v # layout contract + single-locus lint + bridge contract +airstack stack diff full_default my_experiment # see the topology delta +``` + +## Steps — split stack (RFC #380 §2) + +A split is a stack **shape**: one entry file per host role + `bridge.yaml`. +Reference: `stacks/lite_offload_global/` (onboard lite vehicle, offboard +global planning). + +### 1. Copy the reference split + +```bash +airstack stack new lite_offload_global my_split +``` + +### 2. Decide the boundary — edit bridge.yaml FIRST + +`bridge.yaml` is the authoritative, reviewable list of everything crossing +the machine boundary. Entry shape: + +```yaml +bridge: + - topic: global_plan # relative name (no /robot_1/, no $(env)) + type: nav_msgs/msg/Path + direction: offboard_to_onboard # or onboard_to_offboard + qos: reliable # or best_effort (topics only) + - service: interface/robot_command + type: airstack_msgs/srv/RobotCommand + direction: offboard_to_onboard + - action: tasks/navigate + type: task_msgs/action/NavigateTask + direction: offboard_to_onboard +``` + +**THE HARD GATE (memorize this):** `control_setpoint` and trajectory-group +names — `trajectory_override`, `trajectory_segment_to_add`, +`set_trajectory_mode`, `tracking_point`, `look_ahead`, anything under +`trajectory_controller/*`, and `interface/cmd_*` — must NEVER appear in a +`bridge.yaml`. The controller and safety executive are onboard-only: link +loss must leave the vehicle able to failsafe. **`global_plan` crosses; +trajectory commands don't.** This is one of `doctor`'s two enumerated hard +gates (RFC #379 §4 / RFC #380 §2) — `gen_dds_router.py --check` exits 1 +naming the offender; don't fight it, redesign the split (offload planning, +not control). + +### 3. Generate the router config + +```bash +python3 tools/gen_dds_router.py stacks/my_split/bridge.yaml +# validates + writes .airstack/generated/dds_router.my_split.yaml (deterministic) +python3 tools/gen_dds_router.py stacks/my_split/bridge.yaml --check # gate only +``` + +The onboard entry file loads the generated config through +`interpolate_dds_router.launch.py` (args `dds_router_config_file` / +`dds_router_args`) — update the default path if you renamed the stack +(`dds_router..yaml` matches the `stack:` key in bridge.yaml). + +### 4. Edit the per-role entry files + +- `launch/onboard.launch.xml` — everything the vehicle runs. The trajectory + controller, PID controller, and safety executive stay HERE, always. +- `launch/offboard.launch.xml` — what the ground host runs. +- A third machine = a third entry file + its bridge sections; run each half + with `airstack up --stack my_split:onboard` / `:offboard`. + +### 5. Validate + +Same as unsplit, plus: + +```bash +airstack doctor # bridge hard gate runs on every bridge.yaml +airstack doctor --live --stack my_split # running graph vs wiring.md + safety-floor scan +``` + +One `wiring.md` per stack, split or not — the snapshot run brings up every +entry point; nodes group by host, bridge edges render as boundary crossings. + +## Common Pitfalls + +- ❌ Hand-editing `wiring.md` → ✅ it is generated; regenerate via the wiring + test or `doctor --snapshot`. +- ❌ Keeping the copied stack's `wiring.md` (stale baseline) → ✅ `stack new` + already drops it; bootstrap your own. +- ❌ Two entry points, no `bridge.yaml` → ✅ layout contract fails; declare + the boundary. +- ❌ Bridging `set_trajectory_mode` "because the GCS needs it" (the legacy + allowlist did) → ✅ hard-gated; mode changes belong to onboard task servers. +- ❌ Absolute names or `$(env ...)` inside `bridge.yaml` → ✅ relative names; + the generator adds the namespace tokens. +- ❌ Branch refs in `modules.repos` → ✅ pins only (tags/SHAs). + +## References + +- [Stacks guide](../../../docs/development/stacks.md) — anatomy, split stacks, doctor, CLI +- [integrate-module-into-layer](../integrate-module-into-layer) — adding modules to a stack +- [Interface Conventions Spec](../../../docs/robot/autonomy/interface_conventions.md) +- [create-module](../create-module) — module-side scaffolding +- `stacks/lite_offload_global/bridge.yaml` — the annotated reference bridge diff --git a/.agents/skills/integrate-module-into-layer/SKILL.md b/.agents/skills/integrate-module-into-layer/SKILL.md index 46166877b..2ac6b1dca 100644 --- a/.agents/skills/integrate-module-into-layer/SKILL.md +++ b/.agents/skills/integrate-module-into-layer/SKILL.md @@ -1,450 +1,183 @@ --- name: integrate-module-into-layer -description: Integrate a ROS 2 module into the appropriate layer bringup package. Use after creating a package to add it to the autonomy stack launch flow. Covers topic remapping, namespace configuration, and bringup package integration. +description: Integrate a ROS 2 module into a stack. Use after creating a package to add it to a running topology. Covers the stack entry launch file, canonical-default topic args (usually zero include args), the single-locus wiring rule, wiring.md regeneration, and the launch lint. The old layer-bringup workflow this skill used to teach is legacy. license: Apache-2.0 metadata: author: AirLab CMU repository: AirStack --- -# Skill: Integrate Module into Layer Bringup +# Skill: Integrate a Module into a Stack ## When to Use -After creating a new ROS 2 package, integrate it into the appropriate layer's bringup package so it launches automatically with the autonomy stack. +After creating a ROS 2 package (see [add-ros2-package](../add-ros2-package)), +integrate it into a **stack** — the self-contained folder under `stacks/` +whose entry launch file is the single wiring document for a running topology +(RFC #379 §3–4). This replaces the legacy layer-bringup workflow. + +> **The layer-bringup workflow is LEGACY.** Editing +> `local_bringup/launch/*.launch.xml` (or any `*_bringup` launch file) to add +> modules is the old monolith pattern. Those files are frozen: the launch +> lint (`tests/meta/test_launch_single_locus.py`) forbids new ``s +> outside `stacks/*/launch/`, and the grandfather allowlist +> (`tests/meta/launch_lint_allowlist.txt`) only shrinks. Integrate into a +> stack instead. + +## The model (read this first) + +1. **A module's interface is its launch file.** Every topic endpoint is a + declared `` with a `description=` and a **canonical default** from + the [Interface Conventions Spec](../../../docs/robot/autonomy/interface_conventions.md) + (e.g. `odometry_topic` defaults to `odometry_conversion/odometry`, + `global_plan_topic` to `global_plan`). No ``, no hardcoded + cross-module topics inside the module. See + [write-launch-file](../write-launch-file). +2. **A stack composes modules with `` blocks.** Because defaults are + canonical, a conventional integration is a **bare include — usually zero + args**. Only deviations from canonical appear as include args, which is + what keeps the stack file skimmable. +3. **Single-locus rule:** all cross-module wiring overrides live in the + stack's entry launch file(s) — one file for an unsplit stack, one per host + role for a split stack. `grep -r global_plan stacks/my_stack/` answers + "who touches this". +4. **The observed graph is the truth.** Each stack commits a generated + `wiring.md` (snapshotted from the *running* system); changing the topology + means regenerating it, so the PR diff shows the change visually. ## Prerequisites -- Module package created and tested standalone -- Package builds successfully with `bws --packages-select ` -- Know which layer the module belongs to -- Understand required topic connections to other modules - -## Bringup Package Overview - -Each layer in the autonomy stack has a corresponding bringup package that orchestrates launching all modules in that layer with proper topic remapping: - -| Layer | Bringup Package | Location | -|-------|----------------|----------| -| Interface | interface_bringup | `robot/ros_ws/src/interface/interface_bringup` | -| Sensors | sensors_bringup | `robot/ros_ws/src/sensors/sensors_bringup` | -| Perception | perception_bringup | `robot/ros_ws/src/perception/perception_bringup` | -| Local | local_bringup | `robot/ros_ws/src/local/local_bringup` | -| Global | global_bringup | `robot/ros_ws/src/global/global_bringup` | -| Behavior | behavior_bringup | `robot/ros_ws/src/behavior/behavior_bringup` | - -The top-level orchestration is in `autonomy_bringup` which calls each layer's bringup. +- Module package builds: `docker exec airstack-robot-desktop-1 bash -c "bws --packages-select "` +- The module has a launch file with canonical-default topic args + (template: `.agents/skills/add-ros2-package/assets/package_template/launch/`) +- You know which stack to integrate into (`airstack stack list`); to make a + new one, follow [create-stack](../create-stack) ## Steps -### 1. Identify the Correct Bringup Package - -Based on your module type, identify the bringup package to modify: +### 1. Pick the stack and read its entry file ```bash -# List bringup packages -ls -la robot/ros_ws/src/*/launch/*.launch.xml -ls -la robot/ros_ws/src/*/*_bringup/launch/*.launch.xml +airstack stack list +cat stacks/my_stack/launch/stack.launch.xml # or onboard/offboard for splits ``` -For example: -- New local planner → `local_bringup` -- New global planner → `global_bringup` -- New perception module → `perception_bringup` +Every existing block reads "this module, these connections" — a comment +stating the module's inputs/outputs, then the include. -### 2. Study the Existing Launch File +### 2. Add the module include -Before adding your module, understand the current structure: +Canonical wiring — the common case — is a bare include plus an honest +comment: -```bash -# View the main launch file for your layer -cat robot/ros_ws/src/local/local_bringup/launch/local.launch.xml -``` - -**Key patterns to observe:** -- Launch arguments for topic remapping -- Namespace usage with `push-ros-namespace` -- Topic remapping to AirStack standard topics -- Parameter file loading with `allow_substs="true"` -- Environment variable usage (`$(env ROBOT_NAME)`) - -**Example from local.launch.xml:** ```xml - - - - - - - - - - - - - - - + + ``` -### 3. Add Your Module to the Launch File - -Edit the layer's main launch file to include your module: +Deviation from canonical — pass ONLY the deviating args: ```xml - - - - - - - - - - - - - - - - - + + + + ``` -**Important remapping patterns:** - -For **local planners**: -```xml - - - - - - - -``` - -For **global planners**: -```xml - - - - - - -``` - -For **controllers**: -```xml - - - - - - -``` +If you find yourself passing every arg, fix the module's defaults instead — +they should BE the canonical names. -### 4. Add Launch Arguments (if needed) +Placement rules (RFC #380 §2): the trajectory controller, PID controller, and +safety executive are **onboard-only** — in a split stack they belong in the +onboard entry file, never offboard. A module that publishes +`trajectory_override` inherits the whole safety apparatus — that's the +intended integration point for new planners/behaviors. -If your module needs configurable inputs, add launch arguments at the top of the file: +### 3. Task executors: canonical action name -```xml - - - - - - - - - - - - - - - - -``` +Action servers are exposed at `tasks/` (see +[add-task-executor](../add-task-executor)). The module's launch file should +default its action arg to `tasks/`; the stack passes nothing. -### 5. Update Bringup Package Dependencies +### 4. Split stacks only: does anything new cross the boundary? -Edit the bringup package's `package.xml` to add your module as a dependency: +If the module's topics must cross the onboard/offboard boundary, add them to +the stack's `bridge.yaml` and regenerate the router config: ```bash -# Edit the bringup package.xml -vi robot/ros_ws/src/local/local_bringup/package.xml -``` - -Add your package: -```xml - - local_bringup - - - - droan_local_planner - trajectory_controller - - - your_package_name - - - +python3 tools/gen_dds_router.py stacks/my_stack/bridge.yaml ``` -### 6. Consider Conditional Launching (Future/Optional) - -For implementing a plugin-style architecture where users can select which module to use, add conditional launching: +**Never** list `control_setpoint` or trajectory-group names +(`trajectory_override`, `trajectory_segment_to_add`, `set_trajectory_mode`, +`tracking_point`, `look_ahead`) — `airstack doctor` hard-errors +(RFC #379 §4 / #380 §2). Details: [create-stack](../create-stack). -```xml - - - - - - - - - - - - - - - - - - -``` - -This pattern allows runtime selection of implementations. - -### 7. Rebuild the Bringup Package - -After modifying the launch file: +### 5. Run and verify ```bash -# Rebuild bringup package (picks up new launch file) -docker exec airstack-robot-desktop-1 bash -c "bws --packages-select local_bringup" - -# Rebuild your module too (if needed) -docker exec airstack-robot-desktop-1 bash -c "bws --packages-select your_package_name" +airstack up --stack my_stack --sim isaac --robots 1 +airstack ready +docker exec airstack-robot-desktop-1 bash -c "ros2 node list | grep my_detector" +docker exec airstack-robot-desktop-1 bash -c "ros2 topic info /robot_1/my_detector/detections --verbose" ``` -### 8. Test Full Autonomy Launch - -Launch the complete autonomy stack to test integration: - -```bash -# Stop any running containers -airstack down - -# Launch with full autonomy -AUTOLAUNCH=true airstack up robot-desktop - -# Or launch specific components -AUTOLAUNCH=true airstack up robot-desktop isaac-sim -``` +Stack launch files are read from the bind mount — edit and re-launch, no +rebuild. (The module package itself still needs `bws` after code changes.) +Debugging: [debug-module](../debug-module). -### 9. Verify Integration +### 6. Regenerate wiring.md -Check that your module is running and connected: +The committed `wiring.md` no longer matches the graph you just changed — CI's +drift check will (correctly) fail until you regenerate: ```bash -# List all running nodes (should see your node) -docker exec airstack-robot-desktop-1 bash -c "ros2 node list | grep your_node" - -# Check your module's topics -docker exec airstack-robot-desktop-1 bash -c "ros2 node info /robot_name/namespace/your_node" - -# Verify topic connections -docker exec airstack-robot-desktop-1 bash -c "ros2 topic info /robot_name/your/output/topic" - -# Check data flow -docker exec airstack-robot-desktop-1 bash -c "ros2 topic hz /robot_name/your/output/topic" -docker exec airstack-robot-desktop-1 bash -c "ros2 topic echo /robot_name/your/output/topic --once" +airstack test -m wiring --stack my_stack --sim isaacsim --num-robots 1 +# validate tests/results//wiring/observed_my_stack.md, then copy it: +cp tests/results//wiring/observed_my_stack.md stacks/my_stack/wiring.md ``` -### 10. Test with Other Modules +On hardware-only setups use `airstack doctor --snapshot --stack my_stack` +(writes wiring.md with an `unverified-in-CI` provenance line). -Verify your module integrates correctly with the rest of the system: +### 7. Lint and doctor ```bash -# Check end-to-end data flow -# For a planner: verify it receives odometry and publishes trajectories -docker exec airstack-robot-desktop-1 bash -c "ros2 topic hz /robot_name/odometry" -docker exec airstack-robot-desktop-1 bash -c "ros2 topic hz /robot_name/trajectory_controller/trajectory_segment_to_add" - -# Visualize in RViz (if GCS is running) -# Topics should appear in RViz topic list -``` - -### 11. Update Layer Documentation - -Document the integration in the layer's overview: - -Edit `docs/robot/autonomy//index.md`: - -```markdown -## Available Modules - -### Your Module Name -Brief description of your module and its purpose. -- **Location:** `robot/ros_ws/src///your_package_name` -- **Documentation:** [Your Module README](../../../robot/ros_ws/src///your_package_name/README.md) -``` - -## Common Integration Patterns - -### Pattern 1: Parallel Processing Modules - -Multiple modules process data independently: - -```xml - - - - - - - - - - - - - - - - - -``` - -### Pattern 2: Sequential Processing Pipeline - -Modules process data in sequence: - -```xml - - - - - - - - - - - +airstack doctor # anatomy + bridge hard gate + module checks +airstack test -m unit -v # single-locus lint, layout contract, bridge contract ``` -### Pattern 3: Optional/Conditional Module +The lint fails on: `` outside `stacks/*/launch/`, stack ``s +without `description=`, stale allowlist lines. -Module only launches under certain conditions: - -```xml - +### 8. Update docs - - - - - -``` +Stack README (what changed, known limits) and the module README. For new +interchange points, propose an addition to the +[Interface Conventions Spec](../../../docs/robot/autonomy/interface_conventions.md) +(spec additions are semver-minor; renames are major + RFC — see its +deprecation policy). ## Common Pitfalls -### Topic Connection Issues -- ❌ **Hardcoded topic names in module code** - - ✅ Use generic names in code, remap in launch file -- ❌ **Wrong topic remapping direction** - - ✅ `` (from = in code, to = actual topic) -- ❌ **Missing robot namespace** - - ✅ Always include `$(env ROBOT_NAME)` in topic paths for multi-robot support - -### Launch File Issues -- ❌ **Forgetting `allow_substs="true"`** - - ✅ Required for environment variable substitution in config files -- ❌ **Not using `push-ros-namespace`** - - ✅ Properly namespace your nodes to avoid conflicts -- ❌ **Missing `output="screen"`** - - ✅ Add to see node logs in terminal/docker logs - -### Build Issues -- ❌ **Not rebuilding bringup package** - - ✅ Launch files are installed at build time, must rebuild after changes -- ❌ **Missing dependency in bringup package.xml** - - ✅ Add your package to bringup's dependencies -- ❌ **Not sourcing workspace** - - ✅ Source after every build: `sws` - -### Integration Issues -- ❌ **Module launches but doesn't receive data** - - ✅ Check topic remapping with `ros2 topic info` - - ✅ Verify publishers exist: `ros2 topic hz ` -- ❌ **Multiple nodes publishing to same topic** - - ✅ Check for topic conflicts with `ros2 topic info` - - ✅ Use namespaces to separate nodes -- ❌ **Module crashes on launch** - - ✅ Check docker logs: `docker logs airstack-robot-desktop-1` - - ✅ Verify dependencies are built - -## Debugging Integration - -If integration fails, use systematic debugging: - -```bash -# 1. Verify your node is in the node list -docker exec airstack-robot-desktop-1 bash -c "ros2 node list" - -# 2. Check node info (topics, services, actions) -docker exec airstack-robot-desktop-1 bash -c "ros2 node info /robot_name/namespace/your_node" - -# 3. Verify expected input topics exist and are publishing -docker exec airstack-robot-desktop-1 bash -c "ros2 topic hz /expected/input/topic" - -# 4. Check output topics are being published -docker exec airstack-robot-desktop-1 bash -c "ros2 topic hz /your/output/topic" - -# 5. Inspect actual topic data -docker exec airstack-robot-desktop-1 bash -c "ros2 topic echo /your/output/topic --once" - -# 6. Check for errors in logs -docker logs airstack-robot-desktop-1 2>&1 | grep -i error - -# 7. View full launch output -docker logs airstack-robot-desktop-1 -``` - -See [debug-module](../debug-module) for comprehensive debugging strategies. - -## Next Steps - -After successful integration: - -1. **Update documentation:** Follow [update-documentation](../update-documentation) -2. **Test in simulation:** Follow [test-in-simulation](../test-in-simulation) -3. **Create integration tests:** Add tests to verify module works with full stack +- ❌ Adding the module to a `*_bringup` launch file → ✅ add the include to + the stack entry file (the lint will catch you). +- ❌ `` inside the module launch file → ✅ declared topic args with + canonical defaults; overrides only at the stack level. +- ❌ Passing every topic arg in the stack include → ✅ fix the module's + defaults to the canonical names; pass only deviations. +- ❌ Forgetting to regenerate `wiring.md` → ✅ step 6; CI drift check fails + otherwise. +- ❌ Bridging trajectory/control topics in a split stack → ✅ hard-gated; + bridge `global_plan`/task goals instead. +- ❌ Hardcoded `/robot_1/...` topics → ✅ relative names resolve under the + `$(env ROBOT_NAME)` namespace pushed by the preamble. ## References -- **AirStack Launch Files:** - - Top-level: `robot/ros_ws/src/autonomy_bringup/launch/robot.launch.xml` - - Local layer: `robot/ros_ws/src/local/local_bringup/launch/local.launch.xml` - - Global layer: `robot/ros_ws/src/global/global_bringup/launch/global.launch.xml` - -- **ROS 2 Launch Documentation:** - - [ROS 2 Launch Files](https://docs.ros.org/en/jazzy/Tutorials/Intermediate/Launch/Launch-Main.html) - - [Launch XML Format](https://docs.ros.org/en/jazzy/Tutorials/Intermediate/Launch/Using-ROS2-Launch-For-Large-Projects.html) - -- **Related Skills:** - - [add-ros2-package](../add-ros2-package) - Creating the module - - [debug-module](../debug-module) - Debugging integration issues - - [update-documentation](../update-documentation) - Documenting integration +- [create-stack](../create-stack) — making/copying stacks, split stacks, bridge.yaml +- [write-launch-file](../write-launch-file) — module launch authoring rules +- [Interface Conventions Spec](../../../docs/robot/autonomy/interface_conventions.md) +- [Stacks guide](../../../docs/development/stacks.md) +- [Integration Checklist](../../../docs/robot/autonomy/integration_checklist.md) diff --git a/.airstack/modules/doctor.sh b/.airstack/modules/doctor.sh new file mode 100644 index 000000000..557c6b6d4 --- /dev/null +++ b/.airstack/modules/doctor.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash + +# doctor.sh — `airstack doctor` (RFC #379 §4, Phase P5-E3). +# +# Observe-and-report health checks. Default mode runs the compose-time battery +# (module manifests, overlay integrity, dep-conflict HARD GATE, stack anatomy, +# bridge.yaml placement HARD GATE); --live diffs the running graph against the +# selected stack's committed wiring.md; --snapshot writes the stack's +# wiring.md from a real bring-up with unverified-in-CI provenance. +# +# All logic lives in tools/doctor/ (python3 + PyYAML); this file is only the +# CLI registration. Note `airstack module doctor` remains the module-scoped +# subset — `airstack doctor` is the whole-checkout battery. + +DOCTOR_TOOL="${PROJECT_ROOT}/tools/doctor/__init__.py" + +function cmd_doctor { + if [ "${1:-}" = "help" ] || [ "${1:-}" = "-h" ] || [ "${1:-}" = "--help" ]; then + print_command_help doctor + return 0 + fi + if ! command -v python3 >/dev/null 2>&1; then + log_error "python3 is required for 'airstack doctor'." + return 1 + fi + if ! python3 -c 'import yaml' 2>/dev/null; then + log_error "PyYAML is required (pip3 install --user pyyaml)." + return 1 + fi + python3 "$DOCTOR_TOOL" --project-root "$PROJECT_ROOT" "$@" +} + +# Register commands from this module. +function register_doctor_commands { + COMMANDS["doctor"]="cmd_doctor" + COMMAND_HELP["doctor"]="Observe-and-report health checks: compose-time gates, --live wiring drift, --snapshot (RFC #379 §4)" +} diff --git a/.airstack/modules/stack.sh b/.airstack/modules/stack.sh new file mode 100644 index 000000000..d64a01444 --- /dev/null +++ b/.airstack/modules/stack.sh @@ -0,0 +1,152 @@ +#!/usr/bin/env bash + +# stack.sh — `airstack stack` command group (RFC #379 §3, Phase P5-E3). +# +# Manages AirStack *stacks*: self-contained folders under stacks/ composing +# modules into a running system (pinned modules.repos, launch/ entry points, +# docker-compose stub, generated wiring.md, README). +# +# Subcommands: list | new | diff | help +# See `airstack help stack` and docs/development/stacks.md. + +STACKS_DIR="${PROJECT_ROOT}/stacks" +STACK_DIFF_TOOL="${PROJECT_ROOT}/tools/stack_diff.py" + +function _stack_check_python { + if ! command -v python3 >/dev/null 2>&1; then + log_error "python3 is required for 'airstack stack' commands." + return 1 + fi + if ! python3 -c 'import yaml' 2>/dev/null; then + log_error "PyYAML is required (pip3 install --user pyyaml)." + return 1 + fi +} + +# ── subcommands ────────────────────────────────────────────────────────────── + +function cmd_stack_list { + _stack_check_python || return 1 + if [ ! -d "$STACKS_DIR" ]; then + log_error "No stacks/ directory at ${STACKS_DIR}." + return 1 + fi + STACKS_DIR="$STACKS_DIR" python3 - <<'PY' +import os, yaml + +stacks_dir = os.environ["STACKS_DIR"] +rows = [] +for name in sorted(os.listdir(stacks_dir)): + stack = os.path.join(stacks_dir, name) + if not os.path.isdir(stack) or name.startswith("."): + continue # .external holds fetched third-party stack repos + launch_dir = os.path.join(stack, "launch") + entries = sorted( + f[: -len(".launch.xml")] + for f in (os.listdir(launch_dir) if os.path.isdir(launch_dir) else []) + if f.endswith(".launch.xml") + ) + compat = "?" + repos = os.path.join(stack, "modules.repos") + if os.path.isfile(repos): + try: + with open(repos, encoding="utf-8") as f: + data = yaml.safe_load(f) or {} + compat = data.get("airstack_compat") or "?" + except yaml.YAMLError: + compat = "(invalid yaml)" + wiring = "yes" if os.path.isfile(os.path.join(stack, "wiring.md")) else "-" + bridge = os.path.isfile(os.path.join(stack, "bridge.yaml")) + entry_text = ",".join(entries) or "(none)" + if bridge: + entry_text += " +bridge" + rows.append((name, entry_text, wiring, compat)) + +if not rows: + print("No stacks. Copy a reference: airstack stack new full_default ") + raise SystemExit(0) + +headers = ("NAME", "ENTRY POINTS", "WIRING.MD", "AIRSTACK_COMPAT") +widths = [max(len(str(r[i])) for r in rows + [headers]) for i in range(4)] +fmt = " ".join("{:<%d}" % w for w in widths) +print(fmt.format(*headers)) +for row in rows: + print(fmt.format(*row)) +PY +} + +function cmd_stack_new { + local src="${1:-}" dest="${2:-}" + if [ -z "$src" ] || [ -z "$dest" ] || [ $# -gt 2 ]; then + log_error "Usage: airstack stack new " + log_error " e.g. airstack stack new full_default my_experiment" + return 1 + fi + local src_dir="$STACKS_DIR/$src" + local dest_dir="$STACKS_DIR/$dest" + if [ ! -d "$src_dir" ]; then + log_error "Source stack not found: ${src_dir}" + log_error "Available stacks:" + cmd_stack_list + return 1 + fi + if [ -e "$dest_dir" ]; then + log_error "Refusing to overwrite: ${dest_dir} already exists." + return 1 + fi + if ! [[ "$dest" =~ ^[a-z][a-z0-9_]*$ ]]; then + log_error "Stack name must be lowercase snake_case starting with a letter: '$dest'" + return 1 + fi + + cp -r "$src_dir" "$dest_dir" + # wiring.md is the SOURCE stack's observed graph — it would lie about the + # copy the moment an include changes. New stacks bootstrap their own. + rm -f "$dest_dir/wiring.md" + + log_info "Created ${dest_dir} from ${src}." + log_info "Next steps:" + log_info " 1. Edit ${dest_dir}/launch/*.launch.xml — swap/add module s" + log_info " 2. Update ${dest_dir}/README.md (purpose, how to run, known limits)" + log_info " 3. Run it: airstack up --stack ${dest} --sim isaac" + log_info " 4. Regenerate the wiring baseline (wiring.md was NOT copied):" + log_info " airstack test -m wiring --stack ${dest}" + log_info " then validate the observed snapshot and commit it as wiring.md." + log_info " 5. Check it: airstack doctor (and 'airstack test -m unit' for the lint)" + if [ -f "$dest_dir/bridge.yaml" ]; then + log_warn "Copied a SPLIT stack: review ${dest_dir}/bridge.yaml and regenerate" + log_warn " the router config: python3 tools/gen_dds_router.py ${dest_dir#$PROJECT_ROOT/}/bridge.yaml" + fi +} + +function cmd_stack_diff { + _stack_check_python || return 1 + if [ $# -lt 2 ]; then + log_error "Usage: airstack stack diff [--json]" + return 1 + fi + python3 "$STACK_DIFF_TOOL" --stacks-dir "$STACKS_DIR" "$@" +} + +# Dispatcher for the `stack` command group. +function cmd_stack_dispatch { + local sub="${1:-help}" + if [ $# -gt 0 ]; then shift; fi + case "$sub" in + list) cmd_stack_list "$@" ;; + new) cmd_stack_new "$@" ;; + diff) cmd_stack_diff "$@" ;; + help|-h|--help) print_command_help stack ;; + *) + log_error "Unknown stack subcommand: '$sub'" + print_command_help stack + return 1 + ;; + esac +} + +# Register commands from this module. +function register_stack_commands { + COMMANDS["stack"]="cmd_stack_dispatch" + COMMAND_HELP["stack"]="Manage stack folders: list|new|diff (RFC #379 §3; see 'airstack help stack')" +} diff --git a/airstack.sh b/airstack.sh index 084e462cf..9cc34cf7b 100755 --- a/airstack.sh +++ b/airstack.sh @@ -305,6 +305,63 @@ function print_command_help { echo "After sync, start the stack with the module mounts:" echo " airstack up -f .airstack/generated/docker-compose.modules.yaml" ;; + stack) + echo "Usage: airstack stack [options]" + echo "" + echo "Manage stack folders (RFC #379 §3): self-contained topologies under" + echo "stacks/ — pinned modules.repos, launch/ entry points (one per host" + echo "role for split stacks, plus bridge.yaml), docker-compose stub, and a" + echo "generated wiring.md. Guide: docs/development/stacks.md" + echo "" + echo "Subcommands:" + echo " list Table of stacks: name, entry points (+bridge marker for" + echo " split stacks), wiring.md present?, airstack_compat." + echo " new " + echo " Copy a reference stack to stacks/. Refuses to" + echo " overwrite; deliberately does NOT copy wiring.md (it is" + echo " the source's observed graph) — regenerate it with" + echo " 'airstack test -m wiring --stack '." + echo " diff [--json]" + echo " Compare two stacks' generated wiring.md graphs" + echo " (tools/stack_diff.py over the wiring-graph trailers):" + echo " nodes/edges/topics added or removed, QoS and type" + echo " mismatches — topology differences, not XML noise." + echo "" + echo "Run a stack: airstack up --stack [:]" + echo "Check a checkout: airstack doctor" + ;; + doctor) + echo "Usage: airstack doctor [--live | --snapshot] [--stack NAME] [--strict]" + echo "" + echo "Observe-and-report health checks (RFC #379 §4). Doctor never edits" + echo "anything and exits non-zero in default mode only on the two enumerated" + echo "hard gates." + echo "" + echo "Default (compose-time) checks, in order:" + echo " 1. module manifests valid (tools/validate_module.py)" + echo " 2. overlay integrity (module_overlay.py --check)" + echo " 3. HARD GATE #1: module dep conflicts (compose_module_layers.py" + echo " --check-conflicts — a conflict would compose a broken image)" + echo " 4. stack folder anatomy (incl. split stack => bridge.yaml required)" + echo " 5. HARD GATE #2: control-setpoint / trajectory-group names in any" + echo " bridge.yaml (gen_dds_router.py --check — command authority stays" + echo " onboard, RFC #380 §2)" + echo "" + echo "Modes:" + echo " --live Capture the RUNNING graph (docker exec per robot) and" + echo " diff it against the stack's committed wiring.md; exit 1" + echo " on drift. Also scans for publishers of control-setpoint" + echo " / trajectory-group topics outside the blessed controller" + echo " chain (reported loudly; fatal only with --strict)." + echo " --snapshot Same capture, WRITTEN to stacks//wiring.md with a" + echo " provenance line ('observed on , , —" + echo " unverified-in-CI') — the hardware bring-up path." + echo " --stack NAME Stack to diff/snapshot (default: inferred from" + echo " AIRSTACK_STACK_DIR as exported by 'airstack up ...stack')." + echo " --strict Make --live safety-floor warnings fatal." + echo "" + echo "Module-scoped checks only: airstack module doctor [--drift]" + ;; *) # For commands without specific help, just show the general help echo "Usage: airstack $command [options]" diff --git a/docs/development/stacks.md b/docs/development/stacks.md index 3a6ddc1c2..b44db4ac6 100644 --- a/docs/development/stacks.md +++ b/docs/development/stacks.md @@ -28,6 +28,8 @@ Anatomy is enforced by a unit test: `tests/meta/test_stack_layout_contract.py` | [`full_default`](https://github.com/castacks/AirStack/tree/develop/stacks/full_default) | The current full-autonomy topology (GPU `droan_gl` planner) — baseline, graph-identical to legacy `AUTONOMY_ROLE=full`. | | [`full_droan_cpu`](https://github.com/castacks/AirStack/tree/develop/stacks/full_droan_cpu) | CPU DROAN planner + live `disparity_expansion` — absorbs `local_droan_cpu.launch.xml`. | | [`full_macvo`](https://github.com/castacks/AirStack/tree/develop/stacks/full_macvo) | MAC-VO as the planner's disparity source — supersedes (and fixes) the broken `local_macvo_obstacle_avoidance.launch.xml` variant. Requires the `asm_macvo` module (`airstack module add asm_macvo`). | +| [`lite_default`](https://github.com/castacks/AirStack/tree/develop/stacks/lite_default) | Onboard-lite topology, unsplit — the `AUTONOMY_ROLE=onboard` equivalent: interface, sensors, perception, flat Local layer, behavior; **no global, no logging**. | +| [`lite_offload_global`](https://github.com/castacks/AirStack/tree/develop/stacks/lite_offload_global) | The first **split stack** (RFC #380 §2): `onboard.launch.xml` (= lite topology) + `offboard.launch.xml` (global layer only) + `bridge.yaml`. Replaces the `onboard`/`offboard` role pair. | ## Wrap vs. flatten — current status @@ -109,14 +111,123 @@ for the authoring workflow. ## Making a new stack today -1. Copy a reference stack: `cp -r stacks/full_default stacks/my_stack` - (`airstack stack new` arrives in a later phase). +1. Copy a reference stack: `airstack stack new full_default my_stack` + (refuses to overwrite; deliberately does **not** copy `wiring.md` — that + is the source stack's observed graph). 2. Edit `launch/stack.launch.xml` — swap/add/remove module ``s and their topic args. Update `README.md`. 3. Run it: `airstack up --stack my_stack --sim isaac`. 4. Snapshot the wiring: `airstack test -m wiring --stack my_stack`, validate the observed file, commit it as `stacks/my_stack/wiring.md`. -5. `airstack test -m unit` — the layout contract and the launch lint must pass. +5. `airstack doctor` and `airstack test -m unit` — the layout contract, the + bridge gate, and the launch lint must pass. + +The full workflow (including split stacks) is the +[create-stack skill](https://github.com/castacks/AirStack/blob/develop/.agents/skills/create-stack/SKILL.md). + +## Split stacks and `bridge.yaml` (RFC #380 §2) + +**A split is a stack shape, not special machinery.** A split stack folder +carries **multiple launch entry points** — one per host role — plus a +`bridge.yaml` explicitly listing every topic/service/action that crosses the +machine boundary (name, type, direction, QoS). The bridge list feeds +DDS-router config generation and *is* the split, readable in source — +explicit beats derived. The reference split is +[`lite_offload_global`](https://github.com/castacks/AirStack/tree/develop/stacks/lite_offload_global): + +```text +stacks/lite_offload_global/ +├── launch/onboard.launch.xml # the vehicle: lite topology + the DDS router +├── launch/offboard.launch.xml # the ground host: global layer only +├── bridge.yaml # THE boundary document +└── (modules.repos, docker-compose.yaml, README.md, wiring.md as usual) +``` + +- **Run each half** with the `NAME:ENTRY` form: + `airstack up --stack lite_offload_global:onboard` on the vehicle, + `... :offboard` on the ground host. Today's coarse `AUTONOMY_ROLE` + trichotomy becomes "which entry point does this host run"; a third machine + is just another entry file + bridge section. +- **Generate the router config** from the bridge list (never hand-edit the + output): + + ```bash + python3 tools/gen_dds_router.py stacks/lite_offload_global/bridge.yaml + # -> .airstack/generated/dds_router..yaml (deterministic) + ``` + +- **Anatomy rule:** a stack with two or more entry points **must** carry a + `bridge.yaml` (`tests/meta/test_stack_layout_contract.py`). +- **The placement hard gate:** `control_setpoint` and trajectory-group names + (`trajectory_override`, `trajectory_segment_to_add`, `set_trajectory_mode`, + `tracking_point`, `look_ahead` — the `trajectory_controller/*` group) must + never appear in any `bridge.yaml`. The controller and safety executive are + **onboard-only** (link loss must leave the vehicle able to failsafe): + `global_plan` crosses, trajectory commands don't. `gen_dds_router.py + --check` — run by `airstack doctor` and by + `tests/meta/test_bridge_contract.py` — exits 1 naming any violation + (RFC #379 §4 / RFC #380 §2). This is one of doctor's **two** enumerated + hard gates; growing that list takes the RFC process. +- **One `wiring.md` per stack, split or not**: the snapshot groups nodes by + host and draws bridge edges as boundary crossings. + +Canonical names/types/QoS for everything crossing (or forbidden from +crossing) are in the +[Interface Conventions Spec](../robot/autonomy/interface_conventions.md). + +## The `airstack stack` CLI + +```bash +airstack stack list # name, entry points (+bridge), wiring.md?, airstack_compat +airstack stack new # copy a reference stack (no wiring.md; refuses overwrite) +airstack stack diff # topology diff of two stacks' generated wiring.md +``` + +`stack diff` (backed by `tools/stack_diff.py`) compares the **wiring-graph +trailers** of the two stacks' `wiring.md` files via the same +`tests/wiring_snapshot.py` machinery CI uses: nodes/edges/topics added or +removed, QoS and type mismatches — topology differences, never XML +formatting noise. That, plus shared includable sub-launches, is how +copy-drift between stack folders stays bounded (RFC #379 §3). + +## `airstack doctor` + +Observe-and-report health checks (RFC #379 §4). Doctor never generates or +edits wiring; in default mode it exits non-zero only on the **two enumerated +hard gates**: + +```bash +airstack doctor # compose-time battery: + # 1. module manifests valid (report) + # 2. overlay integrity (report) + # 3. HARD GATE: module dep conflicts (RFC #379 §6) + # 4. stack anatomy incl. split=>bridge.yaml (report) + # 5. HARD GATE: control/trajectory names in a + # bridge.yaml (RFC #380 §2) + +airstack doctor --live --stack + # capture the RUNNING graph (docker exec per + # robot, same capture as the wiring test) and + # diff it against stacks//wiring.md — + # exit 1 on drift, 'graph matches wiring.md' + # when identical. Also flags publishers of + # control-setpoint / controller-output topics + # outside the blessed controller chain + # (loud WARN; fatal only with --strict). + +airstack doctor --snapshot --stack + # the identical capture, WRITTEN as the + # stack's wiring.md with a provenance line: + # 'observed on , , — + # unverified-in-CI'. The hardware path for + # stacks that cannot run in CI: an observed + # artifact still can't lie, it is just + # refreshed by hand instead of by CI. +``` + +`--stack` is inferred from `AIRSTACK_STACK_DIR` when omitted (exported by +`airstack up` with the stack flag). `airstack module doctor` remains the +module-scoped subset (manifests + overlay + `--drift`). ## AUTONOMY_ROLE deprecation @@ -130,5 +241,6 @@ an explicitly set `AUTONOMY_ROLE`: | Explicit `AUTONOMY_ROLE=full` (env / `.env` / `--env-file`) | Legacy dispatch + deprecation warning | `--stack full_default` | | `local_droan_cpu.launch.xml` variant | **Deleted in P5-E2** — the CPU-DROAN topology lives only in the stack | `--stack full_droan_cpu` | | `local_macvo_obstacle_avoidance.launch.xml` | **Deleted in P5-E2** (was broken: wrong arg names, stale topic) | `--stack full_macvo` (fixed) | -| `AUTONOMY_ROLE=onboard` / `offboard` (split) | Legacy dispatch + warning | split stacks arrive with RFC #380 §2 (`lite_offload_global`) | +| `AUTONOMY_ROLE=onboard` (lite, no split) | Legacy dispatch + warning | `--stack lite_default` | +| `AUTONOMY_ROLE=onboard` / `offboard` (split) | Legacy dispatch + warning | `--stack lite_offload_global:onboard` / `:offboard` (+ `bridge.yaml`) | | `--stack X` **and** explicit `AUTONOMY_ROLE` | Stack wins — the role is ignored by the launch dispatch; warning says so | Drop `AUTONOMY_ROLE` | diff --git a/docs/robot/autonomy/integration_checklist.md b/docs/robot/autonomy/integration_checklist.md index c411f41e7..5fd986db9 100644 --- a/docs/robot/autonomy/integration_checklist.md +++ b/docs/robot/autonomy/integration_checklist.md @@ -2,6 +2,11 @@ This document provides a comprehensive checklist and guidelines for integrating new modules into the AirStack autonomy stack. +> **Canonical names, types, QoS, and frames now live in the versioned +> [Interface Conventions Specification](interface_conventions.md)** (v1.0.0) — +> cite that spec for interchange-point contracts; this page remains the +> step-by-step integration workflow. + ## Overview When adding a new module to AirStack, proper integration ensures: diff --git a/docs/robot/autonomy/interface_conventions.md b/docs/robot/autonomy/interface_conventions.md new file mode 100644 index 000000000..f7dc4440b --- /dev/null +++ b/docs/robot/autonomy/interface_conventions.md @@ -0,0 +1,247 @@ +# Interface Conventions Specification + +**Spec version: v1.0.0** (semver — see [Versioning and deprecation](#versioning-and-deprecation)) + +This is the versioned specification of AirStack's **interchange points** — the +narrow waists where modules meet: canonical topic/service/action names, +message types, QoS profiles, TF frames and units, and rate classes +([RFC #379 §4.2](https://github.com/castacks/AirStack/discussions/379)). It +upgrades the topic tables of the +[Module Integration Checklist](integration_checklist.md) into a citable +contract; the checklist remains the step-by-step integration workflow. + +**Documentation, not enforcement.** This spec is documentation that modules +*default to* and conformance tests check — it is never input to any wiring +machinery. No schema compiles against it, no resolver reads it, and no code +is generated from it. A module's launch file exposes every topic endpoint as +a launch arg and **defaults it to the canonical name below**; in a +conventional stack, including the module therefore requires zero remaps, and +only deviations appear in stack entry files — which is what keeps them +skimmable. Enforcement is by test and by observation: the system-test suite +doubles as conformance tests, each stack's generated `wiring.md` is the +observed truth, and `airstack doctor --live` diffs reality against it. + +**Conventions in this table are verified against the observed graph** — types +and QoS below come from `stacks/full_default/wiring.md` (the committed +wiring-snapshot of the running reference stack), not from memory. Where a +column reads differently from older docs (e.g. `tracking_point`'s message +type), the observed graph wins. + +All names are relative to the robot namespace: canonical topic +`odometry_conversion/odometry` means `/{robot_name}/odometry_conversion/odometry` +at runtime (`ROBOT_NAME` namespacing is pushed by the launch preamble). + +## Reading the tables + +- **QoS** — publisher profile as observed: RELIABLE or BEST_EFFORT + reliability; durability is VOLATILE unless noted (TRANSIENT_LOCAL is called + out explicitly). QoS is named because it is a classic silent failure: a + best-effort subscriber under a reliable-only publisher (or vice versa) + receives *nothing*, with no error anywhere. +- **Rate class** — qualitative bands, not measured guarantees: + `state` (~10–100 Hz), `sensor` (~10–30 Hz), `plan` (~0.1–2 Hz), + `event` (on change / on command), `latched` (transient-local state). +- **Placement** — `onboard-only` marks interchanges that must never cross a + machine boundary ([RFC #380 §2](https://github.com/castacks/AirStack/discussions/380)): + the **controller** and the **safety executive** stay on the vehicle so link + loss leaves it able to failsafe. `doctor` **hard-errors** when + `control_setpoint` or trajectory-group names appear in any split stack's + `bridge.yaml` — one of doctor's two enumerated hard gates; everywhere else + it observes and reports. + +--- + +## 1. `sensors/*` — sensor naming convention + +Sensor topics are namespaced by sensor **id**: `sensors//`. +Sensor ids become first-class in the vehicle manifest (RFC #380 §1), where +each id pairs the real driver with its sim representation; wiring snapshots +normalize driver nodes to these ids so sim baselines diff cleanly against +hardware bring-ups. + +| Canonical name | Type | QoS | Rate class | Notes | +|---|---|---|---|---| +| `sensors/front_stereo/left/image_rect` | `sensor_msgs/msg/Image` | BEST_EFFORT | sensor | rectified; `right/` mirrors | +| `sensors/front_stereo/left/camera_info` | `sensor_msgs/msg/CameraInfo` | BEST_EFFORT | sensor | frame = the camera's optical frame | +| `sensors/ouster/point_cloud` | `sensor_msgs/msg/PointCloud2` | RELIABLE | sensor | *filtered* lidar cloud (post `lidar_point_cloud_filter`); raw is `sensors/ouster/point_cloud_raw` | +| `sensors/lidar/point_cloud` | `sensor_msgs/msg/PointCloud2` | — | sensor | generic lidar slot (sim publishes here when `ENABLE_LIDAR`) | + +Units: SI throughout (meters, seconds); image encodings per ROS convention. + +## 2. `odometry` — primary state estimate + +| Canonical name | Type | QoS | Rate class | Placement | +|---|---|---|---|---| +| `odometry_conversion/odometry` **(v1 canonical)** | `nav_msgs/msg/Odometry` | RELIABLE | state | produced onboard | + +> **v2 target:** plain `odometry` (`/{robot_name}/odometry`) is the intended +> canonical name; today every consumer (safety monitor, PID, DROAN, +> random_walk, trajectory controller, task servers) subscribes to +> `odometry_conversion/odometry`, so **v1 records reality**. Renaming is a +> spec-major change (see deprecation policy) with a coexistence window. + +Frames/units: `pose` in the `map` frame (ENU, meters); `twist` in the body +frame (`child_frame_id`); yaw right-handed about +Z. + +## 3. `global_map` — global world model + +| Canonical name | Type | QoS | Rate class | Notes | +|---|---|---|---|---| +| `vdb_mapping/vdb_map_visualization` | `visualization_msgs/msg/Marker` | RELIABLE | plan | today's *de facto* map interchange — the reference global planner consumes it | +| `vdb_mapping/vdb_map_updates` / `_sections` / `_overwrites` | `vdb_mapping_interfaces/msg/UpdateGrid` | RELIABLE | plan | remote/split map synchronization | +| `vdb_mapping/vdb_map_pointcloud` | `sensor_msgs/msg/PointCloud2` | RELIABLE | plan | point-cloud export | + +The map lives in the `map` frame. A structured (non-visualization) map +interchange is an acknowledged v2 candidate; v1 documents what the running +graph does. + +## 4. `global_plan` — global waypoint path + +| Canonical name | Type | QoS | Rate class | +|---|---|---|---| +| `global_plan` | `nav_msgs/msg/Path` | RELIABLE | plan | + +Frames: `map` (ENU, meters). Producer: the global planner (onboard in +`full_default`, offboard in `lite_offload_global`); consumers: the local +planner, gossip, keepalive. **`global_plan` is the interchange that MAY cross +a machine boundary** — it is the entire point of the global-offload split. +Contrast §5. + +## 5. `trajectory` group — the trajectory controller's contract — **onboard-only** + +All names live under the `trajectory_controller/` namespace (served by +relative name inside it). **None of these may appear in a `bridge.yaml`** — +doctor hard gate (RFC #379 §4, RFC #380 §2): `global_plan` crosses, +trajectory commands don't. + +| Canonical name | Kind | Type | QoS | Rate class | Direction | +|---|---|---|---|---|---| +| `trajectory_controller/trajectory_override` | topic | `airstack_msgs/msg/TrajectoryXYZVYaw` | RELIABLE | event | any module → controller (replaces current trajectory) | +| `trajectory_controller/trajectory_segment_to_add` | topic | `airstack_msgs/msg/TrajectoryXYZVYaw` | RELIABLE | plan | local planner → controller (appends) | +| `trajectory_controller/set_trajectory_mode` | service | `airstack_msgs/srv/TrajectoryMode` | (service) | event | task servers → controller | +| `trajectory_controller/tracking_point` | topic | `airstack_msgs/msg/Odometry` | RELIABLE | state | controller → PID/planners (note: **airstack_msgs**, not nav_msgs; older docs saying `PointStamped` are wrong) | +| `trajectory_controller/look_ahead` | topic | `airstack_msgs/msg/Odometry` | RELIABLE | state | controller → local planner | +| `trajectory_controller/trajectory_completion_percentage` | topic | `std_msgs/msg/Float32` | RELIABLE | state | controller → task servers | + +**Safety floor:** command authority flows through the trajectory controller — +a module emitting `trajectory_override` inherits arming, safety monitoring, +and takeover for free (that is the selling point). Publishing +`tracking_point`/`look_ahead` from anything but the controller is +impersonation; `doctor --live` flags it loudly. + +## 6. `control_setpoint` — controller → interface command — **onboard-only** + +| Canonical name | Type | QoS | Rate class | Placement | +|---|---|---|---|---| +| `interface/cmd_roll_pitch_yawrate_thrust` | `mav_msgs/msg/RollPitchYawrateThrust` | RELIABLE | state | **onboard-only**; blessed publisher: the PID controller | +| `interface/cmd_pose`, `interface/cmd_velocity` | `geometry_msgs/msg/PoseStamped` / `TwistStamped` | RELIABLE | state | **onboard-only**; alternate command dialects into `robot_interface` | + +`control_setpoint` is the spec name for this interchange point; the rows +above are its concrete v1 spellings. Never bridged, never remapped offboard — +the second doctor hard gate covers these alongside the trajectory group. + +## 7. `interface_status` group — vehicle state out of the interface layer + +| Canonical name | Type | QoS | Rate class | +|---|---|---|---| +| `interface/is_armed` | `std_msgs/msg/Bool` | RELIABLE | state | +| `interface/has_control` | `std_msgs/msg/Bool` | RELIABLE | state | +| `interface/mavros/state` | `mavros_msgs/msg/State` | RELIABLE, **TRANSIENT_LOCAL** | latched | +| `interface/mavros/extended_state` | `mavros_msgs/msg/ExtendedState` | RELIABLE, **TRANSIENT_LOCAL** | latched | +| `interface/mavros/global_position/global` | `sensor_msgs/msg/NavSatFix` | BEST_EFFORT | sensor | +| `interface/robot_command` | service `airstack_msgs/srv/RobotCommand` | (service) | event | + +Late-joining subscribers rely on the TRANSIENT_LOCAL rows — a VOLATILE +subscriber there works, but a VOLATILE *re-publisher* silently loses the +latch. + +## 8. `tasks/*` — task action servers + +Every task executor's action server is exposed at `tasks/` +(remapped there in launch; see the +[add-task-executor](https://github.com/castacks/AirStack/blob/develop/.agents/skills/add-task-executor/SKILL.md) +skill). All types come from `task_msgs`: + +| Canonical name | Action type | v1 server | +|---|---|---| +| `tasks/takeoff` | `task_msgs/action/TakeoffTask` | takeoff_landing_planner (onboard) | +| `tasks/land` | `task_msgs/action/LandTask` | takeoff_landing_planner (onboard) | +| `tasks/navigate` | `task_msgs/action/NavigateTask` | droan (local planner, onboard) | +| `tasks/fixed_trajectory` | `task_msgs/action/FixedTrajectoryTask` | trajectory_controller pkg (onboard) | +| `tasks/exploration` | `task_msgs/action/ExplorationTask` | random_walk (global planner) | +| `tasks/semantic_search` | `task_msgs/action/SemanticSearchTask` | (module-provided) | + +Related service: `takeoff_landing_planner/set_takeoff_landing_command` +(`airstack_msgs/srv/TakeoffLandingCommand`) — the GCS-facing takeoff/land +command. Task goals MAY cross machine boundaries (they are high-level +intents, not control): a split stack lists the crossing actions in its +`bridge.yaml`. + +## 9. `safety` — safety executive — **onboard-only** + +| Canonical name | Type | QoS | Rate class | Placement | +|---|---|---|---|---| +| `behavior/drone_safety_monitor/state_estimate_timed_out` | `std_msgs/msg/Bool` | RELIABLE | state | **onboard-only** | +| `behavior/drone_safety_monitor/command` | `std_msgs/msg/String` | RELIABLE | event | **onboard-only** | + +The safety executive (drone_safety_monitor + the interface's takeover path) +is marked **onboard-only** per RFC #380 §2: link loss must leave the robot +able to failsafe without any ground host in the loop. + +## 10. `gossip` — multi-robot coordination + +Gossip runs on its own DDS domain (default **99**) so peer discovery does not +flood per-robot domains; the dedicated gossip router bridges it (never the +robot↔GCS router — double-bridging amplifies). + +| Canonical name | Type | QoS | Rate class | Notes | +|---|---|---|---|---| +| `/gossip/peers` | `coordination_msgs/msg/PeerProfile` | BEST_EFFORT | state | global (unnamespaced), domain 99 | +| `coordination/peer_registry` | `coordination_msgs/msg/PeerProfile` | RELIABLE, **TRANSIENT_LOCAL** | latched | per-robot registry output | + +Custom payloads: see the +[attach-gossip-payload](https://github.com/castacks/AirStack/blob/develop/.agents/skills/attach-gossip-payload/SKILL.md) +skill. + +## TF frames and units + +| Frame | Parent | Convention | +|---|---|---| +| `world` | — | fixed origin; `world → map` published as a static identity by the launch preamble | +| `map` | `world` | ENU, meters; the planning/state frame (`odometry.pose`, `global_plan`, the map) | +| `base_link` (via `robot_description`) | `map` (through the state estimate) | body frame; `odometry.twist` lives here | + +**ENU vs NED is the classic silent failure** at the PX4 boundary: MAVROS +performs the NED↔ENU conversion — everything ROS-side in this spec is ENU. +Angles in radians; right-handed; yaw about +Z. + +`/tf` and `/tf_static` are wiring: the wiring snapshot deliberately keeps +them (frame plumbing drifts too). + +--- + +## Versioning and deprecation + +This spec is **public API** even though nothing compiles against it — +modules' launch-arg *defaults* and the conformance tests encode it +(RFC #379 §8). Changing a canonical name, type, QoS profile, or frame +convention requires: + +1. a **semver-major** bump of this spec, +2. a **coexistence window** (old and new names both served/accepted), +3. a short **RFC in the registry repo** (`rfcs/` — the deprecation registry; + until the registry repo exists, RFCs live as GitHub Discussions like + [#379](https://github.com/castacks/AirStack/discussions/379) / + [#380](https://github.com/castacks/AirStack/discussions/380)). + +Additions (new interchange points) are semver-minor and are discovered +through drift reports: three forks patching the same tap point = a missing +convention (RFC #379 §11). The `doctor` hard-gate list (dep conflicts; +control/trajectory names in `bridge.yaml`) grows only through the same RFC +process. + +## Change log + +| Spec | Date | Change | +|---|---|---| +| v1.0.0 | 2026-08-20 | Initial versioned spec, recorded from `full_default`'s observed wiring (P5-E3, RFC #379 §4.2). Known v2 candidates: plain `odometry` as the canonical state topic; a structured `global_map` interchange. | diff --git a/mkdocs.yml b/mkdocs.yml index a551521f3..84f3fb27c 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -151,6 +151,7 @@ nav: - Behavior: - docs/robot/autonomy/behavior/index.md - Integration Guide: docs/robot/autonomy/integration_checklist.md + - Interface Conventions Spec: docs/robot/autonomy/interface_conventions.md - Coordination: - Overview: docs/robot/autonomy/coordination/index.md - Payloads & Foxglove: docs/robot/autonomy/coordination/payloads.md diff --git a/stacks/lite_default/README.md b/stacks/lite_default/README.md new file mode 100644 index 000000000..9c33274c6 --- /dev/null +++ b/stacks/lite_default/README.md @@ -0,0 +1,75 @@ +# `lite_default` — onboard-lite reference stack + +The compute-lite topology, unsplit, as a self-contained stack folder +(RFC #379 §3). This is the stack equivalent of the legacy +`AUTONOMY_ROLE=onboard` dispatch: everything a small vehicle runs on its own +compute, with the heavy global layer left out entirely. + +## What it launches + +The single entry point `launch/stack.launch.xml` composes: + +- **Interface, Sensors, Perception, Behavior** — the layer bringups, exactly + as `onboard_autonomy_local.launch.xml` composes them (wrap form). +- **Local layer, flat** (module launch files with canonical defaults, copied + from `full_default`): takeoff/land task server, fixed-trajectory task + server, GPU DROAN planner, trajectory controller, PID controller. +- **Role-onboard extras**: the robot↔GCS DDS router (lean onboard allowlist) + and the gossip coordination layer. + +**Deliberately absent:** the global layer (vdb_mapping, random_walk) and the +logging layer — the `AUTONOMY_ROLE=onboard` subtractions. + +## When to use it + +- Compute-constrained vehicles (VOXL, Jetson lite) flying task-driven + missions (takeoff, land, fixed trajectory, direct navigate goals) with no + onboard global planning. +- As the **onboard half** reference when authoring a split stack — the + [`lite_offload_global`](../lite_offload_global/README.md) split stack's + `onboard.launch.xml` is this topology paired with an offboard global half + and an explicit `bridge.yaml`. + +## Equivalence claim + +`airstack up --stack lite_default` produces a ROS graph identical to the +legacy `AUTONOMY_ROLE=onboard` dispatch. Verify with the wiring snapshot test: + +```bash +airstack test -m wiring --stack lite_default --sim isaacsim --num-robots 1 +``` + +## How to run + +```bash +airstack up --stack lite_default --sim isaac --robots 1 +airstack ready +``` + +The shared per-robot preamble (ROBOT_NAME namespace, `use_sim_time`, +`robot_state_publisher`, world→map static TF) still runs in +`autonomy_bringup/launch/robot.launch.xml`, which dispatches to this stack +when `AIRSTACK_STACK_DIR` is set. + +## Known limits + +- **No global planner**: `global_plan` has no publisher in this stack. + DROAN's navigate task still works with direct goals; exploration-style + missions need `full_default` or the `lite_offload_global` split. +- Partially flattened: the Local layer is composed module-by-module; the + other layers' cross-module remaps still live inside their bringup launch + files (grep those bringups, or read `wiring.md` once committed, for their + actual topic wiring). +- `modules.repos` pins no external modules yet; every package is + trunk-resident. +- `docker-compose.yaml` is a stub — per-stack image composition arrives with + the first module pins; trunk compose profiles provide all services. + +## wiring.md + +Generated — the commit arrives with the first snapshot run of +`airstack test -m wiring --stack lite_default` (the harness writes +`observed_lite_default.md` under the run directory; validate it and copy it +here as `wiring.md`). Once committed, CI drift-checks the running graph +against it. Until a GPU/sim runner flies this stack, `wiring.md` is absent +and the layout contract skips it (bootstrap rule). diff --git a/stacks/lite_default/docker-compose.yaml b/stacks/lite_default/docker-compose.yaml new file mode 100644 index 000000000..95b35df09 --- /dev/null +++ b/stacks/lite_default/docker-compose.yaml @@ -0,0 +1,11 @@ +# Per-stack image composition (RFC #379 S6) arrives with this stack's first +# module pins: the P4 machinery (tools/compose_module_layers.py) composes +# per-module dependency layers on top of the trunk base image and emits a +# compose override for `airstack up`. +# +# lite_default pins no modules (see modules.repos), so there is nothing to +# compose yet -- the trunk compose profiles (root docker-compose.yaml, +# robot/docker/docker-compose.yaml) provide every service meanwhile. The empty +# services map keeps this file valid YAML for the stack-anatomy contract +# (tests/meta/test_stack_layout_contract.py). +services: {} diff --git a/stacks/lite_default/launch/stack.launch.xml b/stacks/lite_default/launch/stack.launch.xml new file mode 100644 index 000000000..18bca8f55 --- /dev/null +++ b/stacks/lite_default/launch/stack.launch.xml @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/stacks/lite_default/modules.repos b/stacks/lite_default/modules.repos new file mode 100644 index 000000000..363b9db96 --- /dev/null +++ b/stacks/lite_default/modules.repos @@ -0,0 +1,15 @@ +# modules.repos : module pins for the lite_default reference stack (RFC #379 S3). +# +# vcstool format, PINNED to tags/commits (never branches). `airstack module sync` +# reads this file into the gitignored modules/ dir; a stack with a pinned .repos +# IS a localized release set. +# +# airstack_compat is a top-level sibling of repositories: (vcstool ignores it, +# AirStack tooling reads it) declaring the trunk semver range this stack was +# tested against. sync warns on mismatch; it never gates. +# +# This reference stack pulls no external modules yet: every package it launches +# is trunk-resident (robot/ros_ws/src + common/ros_packages). +airstack_compat: ">=0.19.0-alpha.18 <0.21.0" +repositories: {} +x-local-modules: [] diff --git a/stacks/lite_offload_global/README.md b/stacks/lite_offload_global/README.md new file mode 100644 index 000000000..1dd4f27ea --- /dev/null +++ b/stacks/lite_offload_global/README.md @@ -0,0 +1,78 @@ +# `lite_offload_global` — split stack: lite vehicle, offboard global planning + +The first **split stack** (RFC #380 §2): a lite vehicle half plus an offboard +global-planning half, with an explicit [`bridge.yaml`](bridge.yaml) listing +everything that crosses the machine boundary. This is the stack-shaped +successor of the legacy `AUTONOMY_ROLE=onboard` / `offboard` pair. + +## Anatomy + +| File | Runs where | What | +|------|-----------|------| +| `launch/onboard.launch.xml` | the vehicle | [`lite_default`](../lite_default/README.md)'s topology (interface, sensors, perception, flat Local layer, behavior — no global, no logging) + the DDS router configured **from `bridge.yaml`** + gossip | +| `launch/offboard.launch.xml` | the ground host (conventionally the GCS machine, domain 0) | the global layer only: `vdb_mapping` + `random_walk` (the `global_bringup` include, as in `full_default`) | +| `bridge.yaml` | — | **THE boundary document**: every topic/service/action crossing between the halves — name, type, direction, QoS. Feeds DDS-router config generation; readable in source. | + +A split is a stack *shape*, not special machinery: same four-file anatomy, +plus one entry file per host role and the bridge list +(`tests/meta/test_stack_layout_contract.py` requires `bridge.yaml` for any +stack with two or more entry points). + +## The bridge + +`bridge.yaml` is authoritative. Generate the DDS-router config from it (the +onboard entry loads the generated file): + +```bash +python3 tools/gen_dds_router.py stacks/lite_offload_global/bridge.yaml +# writes .airstack/generated/dds_router.lite_offload_global.yaml +``` + +What crosses (seeded from the legacy `onboard_local_offboard_global` +DDS-router allowlist, then curated — the full rationale is in `bridge.yaml`'s +header comments): + +- **onboard → offboard:** filtered lidar cloud (`sensors/ouster/point_cloud`) + and odometry (`odometry_conversion/odometry`) — the global layer's inputs — + plus the operator camera streams, stereo point cloud, and GPS fix. +- **offboard → onboard:** `global_plan` (the split's whole point), the + `robot_command` / `set_takeoff_landing_command` services, and + `tasks/navigate` goals (the offboard `random_walk` is a NavigateTask client + of the onboard `droan_gl` server). `tasks/exploration` crosses the other + way (its server moves offboard with `random_walk`). + +**What must never cross (doctor hard gate — RFC #379 §4 / #380 §2):** +`control_setpoint` and the trajectory group (`trajectory_override`, +`trajectory_segment_to_add`, `set_trajectory_mode`, `tracking_point`, +`look_ahead` — the `trajectory_controller/*` group). Command authority stays +onboard so link loss leaves the vehicle able to failsafe: **`global_plan` +crosses; trajectory commands don't.** `gen_dds_router.py --check` (run inside +`airstack doctor`) exits 1 naming any violation. Note the legacy allowlist +bridged the `set_trajectory_mode` service to the GCS; this stack deliberately +does not. + +## How to run + +```bash +# vehicle (or the robot container in sim): +airstack up --stack lite_offload_global:onboard --sim isaac --robots 1 + +# ground host (offboard container / GCS machine, domain 0): +airstack up --stack lite_offload_global:offboard +``` + +Generate the router config first (`gen_dds_router.py` command above) — the +onboard entry fails fast if `.airstack/generated/dds_router.lite_offload_global.yaml` +is missing. Check the composition any time with `airstack doctor` and, against +a running system, `airstack doctor --live --stack lite_offload_global`. + +## Known limits + +- One `wiring.md` per stack, split or not: nodes grouped by host, bridge edges + drawn as boundary crossings. Not committed yet — bootstrap via the wiring + snapshot run (both entry points up) or `airstack doctor --snapshot` on a + real bring-up (committed with an `unverified-in-CI` provenance line). +- Host placement is by convention (offboard = GCS machine, domain 0) until the + fleet configuration layer (`hosts:` maps, RFC #380 §2–3) lands. +- `modules.repos` pins no external modules yet; `docker-compose.yaml` is a + stub (trunk compose profiles provide the services). diff --git a/stacks/lite_offload_global/bridge.yaml b/stacks/lite_offload_global/bridge.yaml new file mode 100644 index 000000000..77470afbd --- /dev/null +++ b/stacks/lite_offload_global/bridge.yaml @@ -0,0 +1,119 @@ +# bridge.yaml — the machine boundary of the lite_offload_global split stack +# (RFC #380 S2: a split stack carries one launch entry point per host role +# plus THIS explicit list of every topic/service/action crossing the +# boundary — the bridge list feeds DDS-router config generation and *is* the +# split, readable in source; explicit beats derived). +# +# Names are RELATIVE to the robot namespace: tools/gen_dds_router.py prefixes +# rt/$(env ROBOT_NAME)/ (topics), rq/rr/$(env ROBOT_NAME)/ (services), and +# expands actions into their five DDS sub-endpoints. Regenerate the router +# config after every edit: +# +# python3 tools/gen_dds_router.py stacks/lite_offload_global/bridge.yaml +# +# Entry fields: +# topic|service|action : exactly one — the relative name +# type : ROS interface type (pkg/msg|srv|action/Name) +# direction : onboard_to_offboard | offboard_to_onboard +# (data flow for topics; request/goal flow for +# services and actions). The DDS router itself +# bridges bidirectionally; direction documents intent +# and drives the wiring.md boundary rendering. +# qos : reliable | best_effort (topics; QoS verified +# against full_default's observed wiring.md). +# +# HARD GATE (RFC #379 S4 / RFC #380 S2): control_setpoint and trajectory-group +# names (trajectory_override, trajectory_segment_to_add, set_trajectory_mode, +# tracking_point, look_ahead — the trajectory_controller/* group) must NEVER +# appear here. Command authority stays onboard; link loss must leave the +# vehicle able to failsafe. `gen_dds_router.py --check` (run by +# `airstack doctor`) exits 1 naming any violation. +# +# Seeded from the legacy onboard_local_offboard_global dds_router.yaml +# allowlist (its extension + the onboard_all base it extends). Deliberately +# NOT carried over from that allowlist: +# - trajectory_controller/set_trajectory_mode (service) — trajectory group: +# the hard gate above. Mode changes are owned by the onboard task servers. +# - trajectory_controller/trajectory_vis — the trajectory_controller/* group +# stays off the bridge wholesale. +# - vdb_mapping/vdb_map_visualization — vdb_mapping runs OFFBOARD in this +# split; its viz is already on the ground host, nothing onboard consumes it. +# - bag_record/* — lite stacks run no logging layer. +# - behavior/global_plan_toggle (service) — no node serves it in the current +# graph (vestigial remap in global.launch.xml). +# - sensors/front_stereo/right/depth_ground_truth — sim-only ground truth. + +version: 1 +stack: lite_offload_global + +bridge: + # ── onboard → offboard: what the global layer (and the operator) needs ── + + - topic: sensors/ouster/point_cloud + type: sensor_msgs/msg/PointCloud2 + direction: onboard_to_offboard + qos: reliable # filtered lidar cloud → vdb_mapping (offboard) + + - topic: odometry_conversion/odometry + type: nav_msgs/msg/Odometry + direction: onboard_to_offboard + qos: reliable # v1 canonical odometry → random_walk + ground state + + - topic: sensors/front_stereo/left/image_rect + type: sensor_msgs/msg/Image + direction: onboard_to_offboard + qos: best_effort # operator video (legacy onboard allowlist) + + - topic: sensors/front_stereo/left/camera_info + type: sensor_msgs/msg/CameraInfo + direction: onboard_to_offboard + qos: best_effort + + - topic: sensors/front_stereo/right/image_rect + type: sensor_msgs/msg/Image + direction: onboard_to_offboard + qos: best_effort + + - topic: sensors/front_stereo/right/camera_info + type: sensor_msgs/msg/CameraInfo + direction: onboard_to_offboard + qos: best_effort + + - topic: perception/stereo_image_proc/point_cloud + type: sensor_msgs/msg/PointCloud2 + direction: onboard_to_offboard + qos: reliable # ground visualization (legacy base allowlist) + + - topic: interface/mavros/global_position/global + type: sensor_msgs/msg/NavSatFix + direction: onboard_to_offboard + qos: best_effort # ground map view / GPS-frame task goals + + # ── offboard → onboard: the split's outputs ───────────────────────────── + # global_plan crosses; trajectory commands DON'T (hard gate above). + + - topic: global_plan + type: nav_msgs/msg/Path + direction: offboard_to_onboard + qos: reliable # random_walk (offboard) → droan_gl (onboard) + + # ── services: ground/offboard requests into the vehicle ───────────────── + + - service: interface/robot_command + type: airstack_msgs/srv/RobotCommand + direction: offboard_to_onboard # arm/takeoff-authority requests + + - service: takeoff_landing_planner/set_takeoff_landing_command + type: airstack_msgs/srv/TakeoffLandingCommand + direction: offboard_to_onboard # operator takeoff/land commands + + # ── actions: task goals across the boundary ───────────────────────────── + + - action: tasks/navigate + type: task_msgs/action/NavigateTask + direction: offboard_to_onboard # offboard random_walk → onboard droan_gl + + - action: tasks/exploration + type: task_msgs/action/ExplorationTask + direction: onboard_to_offboard # server moves offboard with random_walk; + # onboard/ground clients reach it here diff --git a/stacks/lite_offload_global/docker-compose.yaml b/stacks/lite_offload_global/docker-compose.yaml new file mode 100644 index 000000000..a4a6dacd1 --- /dev/null +++ b/stacks/lite_offload_global/docker-compose.yaml @@ -0,0 +1,12 @@ +# Per-stack image composition (RFC #379 S6) arrives with this stack's first +# module pins: the P4 machinery (tools/compose_module_layers.py) composes +# per-module dependency layers on top of the trunk base image and emits a +# compose override for `airstack up`. +# +# lite_offload_global pins no modules (see modules.repos), so there is nothing +# to compose yet -- the trunk compose profiles provide every service meanwhile +# (the onboard half runs in the robot service; the offboard half maps to the +# robot-offboard service pattern until fleet generation, RFC #380 S2, lands). +# The empty services map keeps this file valid YAML for the stack-anatomy +# contract (tests/meta/test_stack_layout_contract.py). +services: {} diff --git a/stacks/lite_offload_global/launch/offboard.launch.xml b/stacks/lite_offload_global/launch/offboard.launch.xml new file mode 100644 index 000000000..b9ab5b42f --- /dev/null +++ b/stacks/lite_offload_global/launch/offboard.launch.xml @@ -0,0 +1,32 @@ + + + + + + + diff --git a/stacks/lite_offload_global/launch/onboard.launch.xml b/stacks/lite_offload_global/launch/onboard.launch.xml new file mode 100644 index 000000000..50b4f392c --- /dev/null +++ b/stacks/lite_offload_global/launch/onboard.launch.xml @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/stacks/lite_offload_global/modules.repos b/stacks/lite_offload_global/modules.repos new file mode 100644 index 000000000..edefa0d10 --- /dev/null +++ b/stacks/lite_offload_global/modules.repos @@ -0,0 +1,15 @@ +# modules.repos : module pins for the lite_offload_global split stack (RFC #379 S3). +# +# vcstool format, PINNED to tags/commits (never branches). `airstack module sync` +# reads this file into the gitignored modules/ dir; a stack with a pinned .repos +# IS a localized release set. +# +# airstack_compat is a top-level sibling of repositories: (vcstool ignores it, +# AirStack tooling reads it) declaring the trunk semver range this stack was +# tested against. sync warns on mismatch; it never gates. +# +# This reference stack pulls no external modules yet: every package it launches +# is trunk-resident (robot/ros_ws/src + common/ros_packages). +airstack_compat: ">=0.19.0-alpha.18 <0.21.0" +repositories: {} +x-local-modules: [] diff --git a/tests/meta/test_bridge_contract.py b/tests/meta/test_bridge_contract.py new file mode 100644 index 000000000..45448e150 --- /dev/null +++ b/tests/meta/test_bridge_contract.py @@ -0,0 +1,257 @@ +# Copyright (c) 2026 Carnegie Mellon University +# MIT License - see LICENSE in the repository root for full text. +"""Contract tests for split-stack bridge.yaml + tools/gen_dds_router.py +(RFC #380 §2, RFC #379 §4). + +A split stack's ``bridge.yaml`` is the explicit list of every topic/service/ +action crossing the machine boundary; ``gen_dds_router.py`` derives the +DDS-router config from it. Contracts pinned here: + +- **Schema** — the shipped ``lite_offload_global/bridge.yaml`` validates; the + usual authoring mistakes (absolute names, wrong direction/qos enums, two + kinds in one entry, kind/type mismatch) are named errors. +- **Hard gate #2** (RFC #379 §4 / #380 §2) — a bridge listing + ``trajectory_override`` (or any control-setpoint / trajectory-group / + ``trajectory_controller/*`` name) fails ``--check`` with exit 1, naming the + entry and citing the RFCs. ``global_plan`` crosses; trajectory commands + don't. +- **Determinism** — generation is a pure function of bridge.yaml: identical + inputs produce byte-identical router configs, with no timestamps and no + absolute paths. +- **Router format** — output parses as YAML and follows the legacy + ``onboard_local_offboard_global/config/dds_router.yaml`` conventions: + ``$(env ROBOT_NAME)`` interpolation, rt/ topics, rq/rr service pairs, the + five action sub-endpoints, participants on ``$(env ROS_DOMAIN_ID)`` / + ``$(var gcs_domain)``. +""" +import copy +import importlib.util +import json + +import pytest +import yaml + +from harness.discovery import repo_path + +pytestmark = pytest.mark.unit + +REPO = repo_path() +BRIDGE_PATH = REPO / "stacks" / "lite_offload_global" / "bridge.yaml" + + +def _load(path, name): + spec = importlib.util.spec_from_file_location(name, path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +@pytest.fixture(scope="module") +def gen(): + return _load(REPO / "tools" / "gen_dds_router.py", "airstack_gen_dds_router") + + +@pytest.fixture(scope="module") +def real_bridge(): + return yaml.safe_load(BRIDGE_PATH.read_text(encoding="utf-8")) + + +def _minimal_bridge(*entries): + return { + "version": 1, + "stack": "synthetic_split", + "bridge": list(entries), + } + + +GOOD_TOPIC = { + "topic": "global_plan", + "type": "nav_msgs/msg/Path", + "direction": "offboard_to_onboard", + "qos": "reliable", +} + + +# ── schema ─────────────────────────────────────────────────────────────────── + +def test_real_bridge_is_schema_valid(gen, real_bridge): + errors = gen.validate_bridge(real_bridge) + assert errors == [], ( + "lite_offload_global/bridge.yaml must validate:\n" + + json.dumps(errors, indent=2) + ) + + +def test_real_bridge_check_mode_exits_zero(gen, capsys): + assert gen.main([str(BRIDGE_PATH), "--check"]) == 0 + verdict = json.loads(capsys.readouterr().out) + assert verdict == {"valid": True, "errors": []} + + +def test_real_bridge_carries_the_split_essentials(real_bridge): + """The split's raison d'être is readable in the bridge list itself.""" + names = { + next(k for k in ("topic", "service", "action") if k in e): e[ + next(k for k in ("topic", "service", "action") if k in e)] + for e in real_bridge["bridge"] + } + topics = {e["topic"] for e in real_bridge["bridge"] if "topic" in e} + assert "global_plan" in topics, "global_plan must cross (offboard planner)" + assert "odometry_conversion/odometry" in topics + assert "sensors/ouster/point_cloud" in topics, "vdb_mapping's input" + assert names # sanity + + +@pytest.mark.parametrize("mutate, expect_fragment", [ + (lambda e: e.update(topic="/robot_1/global_plan"), "relative"), + (lambda e: e.update(direction="up"), "not one of"), + (lambda e: e.update(qos="mostly_reliable"), "not one of"), + (lambda e: e.update(type="nav_msgs/Path"), "interface type"), + (lambda e: e.update(type="nav_msgs/srv/Path"), "does not match the entry kind"), + (lambda e: e.update(service="also_a_service"), "exactly one of"), + (lambda e: e.update(extra_field=True), "unknown fields"), + (lambda e: e.pop("qos"), "required for topics"), +]) +def test_schema_rejects_common_mistakes(gen, mutate, expect_fragment): + entry = copy.deepcopy(GOOD_TOPIC) + mutate(entry) + errors = gen.validate_bridge(_minimal_bridge(entry)) + assert errors, f"expected an error after mutating to {entry}" + assert any(expect_fragment in e["message"] for e in errors), ( + f"no error mentioned {expect_fragment!r}:\n" + json.dumps(errors, indent=2) + ) + + +# ── hard gate #2 ───────────────────────────────────────────────────────────── + +@pytest.mark.parametrize("name", [ + "trajectory_controller/trajectory_override", + "trajectory_override", # bare basename still gated + "trajectory_controller/trajectory_segment_to_add", + "trajectory_controller/set_trajectory_mode", + "trajectory_controller/tracking_point", + "trajectory_controller/look_ahead", + "trajectory_controller/trajectory_vis", # the whole group stays off + "control_setpoint", + "interface/cmd_roll_pitch_yawrate_thrust", # concrete control setpoint +]) +def test_hard_gate_rejects_control_and_trajectory_names(gen, name): + entry = { + "topic": name, + "type": "airstack_msgs/msg/TrajectoryXYZVYaw", + "direction": "offboard_to_onboard", + "qos": "reliable", + } + errors = gen.validate_bridge(_minimal_bridge(entry)) + assert errors, f"{name} must be rejected by the placement hard gate" + messages = " ".join(e["message"] for e in errors) + assert name in messages, "the violation must NAME the offending entry" + assert "RFC #379" in messages and "RFC #380" in messages, ( + "the violation must cite RFC #379 §4 / RFC #380 §2" + ) + + +def test_hard_gate_gates_services_too(gen): + entry = { + "service": "trajectory_controller/set_trajectory_mode", + "type": "airstack_msgs/srv/TrajectoryMode", + "direction": "offboard_to_onboard", + } + errors = gen.validate_bridge(_minimal_bridge(entry)) + assert any("set_trajectory_mode" in e["message"] for e in errors) + + +def test_check_mode_exits_one_on_synthetic_trajectory_bridge(gen, tmp_path, capsys): + bad = _minimal_bridge(GOOD_TOPIC, { + "topic": "trajectory_controller/trajectory_override", + "type": "airstack_msgs/msg/TrajectoryXYZVYaw", + "direction": "offboard_to_onboard", + "qos": "reliable", + }) + path = tmp_path / "bridge.yaml" + path.write_text(yaml.safe_dump(bad), encoding="utf-8") + assert gen.main([str(path), "--check"]) == 1 + verdict = json.loads(capsys.readouterr().out) + assert verdict["valid"] is False + assert any("trajectory_override" in e["message"] for e in verdict["errors"]) + + +def test_generation_refuses_invalid_bridge(gen, tmp_path): + bad = _minimal_bridge({ + "topic": "trajectory_controller/trajectory_override", + "type": "airstack_msgs/msg/TrajectoryXYZVYaw", + "direction": "offboard_to_onboard", + "qos": "reliable", + }) + path = tmp_path / "bridge.yaml" + path.write_text(yaml.safe_dump(bad), encoding="utf-8") + out = tmp_path / "router.yaml" + assert gen.main([str(path), "--out", str(out)]) == 1 + assert not out.exists(), "no router config may be generated past the gate" + + +def test_allowed_names_pass_the_gate(gen): + """The neighboring interchanges the gate must NOT catch.""" + for name, type_name in [ + ("global_plan", "nav_msgs/msg/Path"), + ("odometry_conversion/odometry", "nav_msgs/msg/Odometry"), + ("takeoff_landing_planner/trajectory_completion_percentage", + "std_msgs/msg/Float32"), + ]: + entry = {"topic": name, "type": type_name, + "direction": "onboard_to_offboard", "qos": "reliable"} + assert gen.validate_bridge(_minimal_bridge(entry)) == [], name + + +# ── determinism + router format ────────────────────────────────────────────── + +def test_generation_is_deterministic(gen, tmp_path): + out_a = tmp_path / "a.yaml" + out_b = tmp_path / "b.yaml" + assert gen.main([str(BRIDGE_PATH), "--out", str(out_a)]) == 0 + assert gen.main([str(BRIDGE_PATH), "--out", str(out_b)]) == 0 + text_a = out_a.read_text(encoding="utf-8") + assert text_a == out_b.read_text(encoding="utf-8") + assert str(REPO) not in text_a, "no absolute paths in generated output" + assert str(tmp_path) not in text_a + + +def test_generated_config_matches_legacy_router_format(gen, tmp_path, real_bridge): + out = tmp_path / "router.yaml" + assert gen.main([str(BRIDGE_PATH), "--out", str(out)]) == 0 + text = out.read_text(encoding="utf-8") + data = yaml.safe_load(text) + + # participants follow the legacy interpolation conventions + assert data["participants"][0]["domain"] == "$(env ROS_DOMAIN_ID)" + assert data["participants"][1]["domain"] == "$(var gcs_domain)" + + allow = [e["name"] for e in data["allowlist"]] + # every topic entry becomes exactly one rt/ name in robot namespace + assert "rt/$(env ROBOT_NAME)/global_plan" in allow + assert "rt/$(env ROBOT_NAME)/sensors/ouster/point_cloud" in allow + # services expand to the rq/rr pair + assert "rq/$(env ROBOT_NAME)/interface/robot_commandRequest" in allow + assert "rr/$(env ROBOT_NAME)/interface/robot_commandReply" in allow + # actions expand to the five DDS sub-endpoints (8 names) + nav = [n for n in allow if "tasks/navigate/_action" in n] + assert len(nav) == 8, nav + assert "rt/$(env ROBOT_NAME)/tasks/navigate/_action/feedback" in nav + assert "rq/$(env ROBOT_NAME)/tasks/navigate/_action/send_goalRequest" in nav + + # the gate's guarantee holds in the OUTPUT too — belt and braces + forbidden = [n for n in allow if "trajectory_controller" in n + or n.rsplit("/", 1)[-1] in gen.FORBIDDEN_BASENAMES] + assert forbidden == [], forbidden + + # entry count bookkeeping: topics=1, services=2, actions=8 endpoints each + kinds = {"topic": 0, "service": 0, "action": 0} + for entry in real_bridge["bridge"]: + kinds[next(k for k in kinds if k in entry)] += 1 + expected = kinds["topic"] + 2 * kinds["service"] + 8 * kinds["action"] + assert len(allow) == expected + + +def test_default_out_path_uses_stack_name(gen): + path = gen.default_out_path(BRIDGE_PATH, REPO) + assert path == REPO / ".airstack" / "generated" / "dds_router.lite_offload_global.yaml" diff --git a/tests/meta/test_doctor_contract.py b/tests/meta/test_doctor_contract.py new file mode 100644 index 000000000..28d386043 --- /dev/null +++ b/tests/meta/test_doctor_contract.py @@ -0,0 +1,327 @@ +# Copyright (c) 2026 Carnegie Mellon University +# MIT License - see LICENSE in the repository root for full text. +"""Contract tests for `airstack doctor` (tools/doctor/ — RFC #379 §4). + +Doctor's posture is observe-and-report: it edits nothing and, in default +(compose-time) mode, exits non-zero in exactly **two enumerated places**: + +1. module dep conflicts that would compose a broken image (RFC #379 §6); +2. control-setpoint / trajectory-group names in any stack's bridge.yaml + (RFC #380 §2). + +Contracts pinned here: + +- the compose-time battery is green (exit 0) on this repository; +- each hard-gate path exits 1 against a synthetic sandbox (built in tmp_path + from tests/fixtures-style synthetic manifests — the developer's checkout is + never mutated); +- soft findings (broken stack anatomy, invalid manifests) are REPORTED but do + not gate — exit stays 0; +- the stack-layout check agrees with tests/meta/test_stack_layout_contract.py, + extended with the split-stack rule: two or more entry points require a + bridge.yaml; +- the --live safety-floor scan (unit-tested on synthetic graphs, since CI has + no running stack here) flags unblessed control-setpoint publishers and + controller impersonators, and stays quiet for the blessed chain. +""" +import importlib.util + +import pytest +import yaml + +from harness.discovery import repo_path + +pytestmark = pytest.mark.unit + +REPO = repo_path() + +ENV_TEXT = ( + 'VERSION="0.19.0"\n' + 'PROJECT_NAME="airstack"\n' + 'PROJECT_DOCKER_REGISTRY="registry.example.com/airstack"\n' + 'DOCKER_IMAGE_BUILD_MODE="dev"\n' +) + + +def _load(path, name): + spec = importlib.util.spec_from_file_location(name, path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +@pytest.fixture(scope="module") +def doctor(): + return _load(REPO / "tools" / "doctor" / "__init__.py", "airstack_doctor") + + +# ── sandbox builders (tmp_path only; the real checkout is never touched) ──── + +def make_sandbox(tmp_path): + root = tmp_path / "airstack" + root.mkdir() + (root / ".env").write_text(ENV_TEXT, encoding="utf-8") + return root + + +def add_stack(root, name, entries=("stack",), bridge=None, readme=None, + compat=">=0.19.0 <0.21.0"): + stack = root / "stacks" / name + (stack / "launch").mkdir(parents=True) + (stack / "modules.repos").write_text( + yaml.safe_dump({"airstack_compat": compat, "repositories": {}}), + encoding="utf-8", + ) + for entry in entries: + (stack / "launch" / f"{entry}.launch.xml").write_text( + "\n \n\n", + encoding="utf-8", + ) + (stack / "docker-compose.yaml").write_text("services: {}\n", encoding="utf-8") + (stack / "README.md").write_text( + readme if readme is not None else + (f"# {name}\n\nSynthetic doctor-contract stack. " + "Purpose text. " * 20), + encoding="utf-8", + ) + if bridge is not None: + (stack / "bridge.yaml").write_text(yaml.safe_dump(bridge), encoding="utf-8") + return stack + + +def add_module(root, name, pip=()): + mdir = root / "modules" / name + mdir.mkdir(parents=True) + manifest = { + "name": name, + "description": f"Synthetic doctor fixture {name}.", + "maintainer": "test@example.com", + "license": "MIT", + "type": "ros_package", + "airstack_compat": ">=0.19.0 <0.21.0", + "targets": ["robot"], + "deps": {"apt": [], "pip": list(pip)}, + "tests": {"packages": [], "marks": []}, + } + (mdir / "module.yaml").write_text(yaml.safe_dump(manifest), encoding="utf-8") + return mdir + + +GOOD_BRIDGE = { + "version": 1, + "stack": "split_ok", + "bridge": [ + {"topic": "global_plan", "type": "nav_msgs/msg/Path", + "direction": "offboard_to_onboard", "qos": "reliable"}, + ], +} + +BAD_BRIDGE = { + "version": 1, + "stack": "split_bad", + "bridge": [ + {"topic": "trajectory_controller/trajectory_override", + "type": "airstack_msgs/msg/TrajectoryXYZVYaw", + "direction": "offboard_to_onboard", "qos": "reliable"}, + ], +} + + +# ── green on the repository ────────────────────────────────────────────────── + +def test_compose_time_doctor_green_on_repo(doctor, capsys): + assert doctor.main(["--project-root", str(REPO)]) == 0, ( + "doctor must exit 0 on the repository (only the two hard gates gate):\n" + + capsys.readouterr().out + ) + + +def test_repo_hard_gates_individually_clean(doctor): + assert doctor.check_layer_conflicts(REPO).status == doctor.OK + gate = doctor.check_bridge_gates(REPO) + assert gate.status == doctor.OK, gate.messages + # the shipped split stack was actually inspected, not vacuously skipped + assert any("lite_offload_global" in m for m in gate.messages) + + +def test_repo_stack_layout_clean(doctor): + result = doctor.check_stack_layout(REPO) + assert result.status == doctor.OK, result.messages + + +# ── hard gate #2: bridge placement ─────────────────────────────────────────── + +def test_bridge_hard_gate_exits_one(doctor, tmp_path, capsys): + root = make_sandbox(tmp_path) + add_stack(root, "split_bad", entries=("onboard", "offboard"), + bridge=BAD_BRIDGE) + assert doctor.main(["--project-root", str(root)]) == 1 + out = capsys.readouterr().out + assert "trajectory_override" in out, "the gate must NAME the violation" + assert "hard-gate failure" in out + + +def test_bridge_hard_gate_check_names_rfcs(doctor, tmp_path): + root = make_sandbox(tmp_path) + add_stack(root, "split_bad", entries=("onboard", "offboard"), + bridge=BAD_BRIDGE) + result = doctor.check_bridge_gates(root) + assert result.hard and result.status == doctor.FAIL + text = " ".join(result.messages) + assert "RFC #379" in text and "RFC #380" in text + + +def test_valid_split_stack_passes_gate(doctor, tmp_path): + root = make_sandbox(tmp_path) + add_stack(root, "split_ok", entries=("onboard", "offboard"), + bridge=GOOD_BRIDGE) + assert doctor.main(["--project-root", str(root)]) == 0 + assert doctor.check_bridge_gates(root).status == doctor.OK + + +# ── hard gate #1: dep conflicts ────────────────────────────────────────────── + +def test_dep_conflict_hard_gate_exits_one(doctor, tmp_path, capsys): + root = make_sandbox(tmp_path) + add_module(root, "mod_a", pip=["numpy==1.26.0"]) + add_module(root, "mod_b", pip=["numpy==2.0.0"]) + assert doctor.main(["--project-root", str(root)]) == 1 + out = capsys.readouterr().out + assert "hard-gate failure" in out + assert "module-dep-conflicts" in out + + +def test_same_pin_is_not_a_conflict(doctor, tmp_path): + root = make_sandbox(tmp_path) + add_module(root, "mod_a", pip=["numpy==2.0.0"]) + add_module(root, "mod_b", pip=["numpy==2.0.0"]) + assert doctor.check_layer_conflicts(root).status == doctor.OK + + +# ── soft findings never gate (observe-and-report posture) ──────────────────── + +def test_split_stack_without_bridge_reports_but_does_not_gate(doctor, tmp_path): + root = make_sandbox(tmp_path) + add_stack(root, "split_missing_bridge", entries=("onboard", "offboard")) + result = doctor.check_stack_layout(root) + assert result.status == doctor.WARN + assert any("bridge.yaml" in m for m in result.messages) + # anatomy problems report; only the two enumerated gates exit non-zero + assert doctor.main(["--project-root", str(root)]) == 0 + + +def test_unsplit_stack_needs_no_bridge(doctor, tmp_path): + root = make_sandbox(tmp_path) + add_stack(root, "plain", entries=("stack",)) + assert doctor.check_stack_layout(root).status == doctor.OK + + +def test_broken_anatomy_reports_but_does_not_gate(doctor, tmp_path): + root = make_sandbox(tmp_path) + stack = add_stack(root, "ragged", readme="too short") + (stack / "modules.repos").write_text( + yaml.safe_dump({"repositories": {}}), encoding="utf-8") # no compat + result = doctor.check_stack_layout(root) + assert result.status == doctor.WARN + text = " ".join(result.messages) + assert "airstack_compat" in text and "README.md" in text + assert doctor.main(["--project-root", str(root)]) == 0 + + +def test_invalid_manifest_reports_but_does_not_gate(doctor, tmp_path): + root = make_sandbox(tmp_path) + (root / "modules" / "broken").mkdir(parents=True) + (root / "modules" / "broken" / "module.yaml").write_text( + "name: broken\n", encoding="utf-8") # missing required fields + result = doctor.check_module_manifests(root) + assert result.status == doctor.WARN + assert any("INVALID" in m for m in result.messages) + assert doctor.main(["--project-root", str(root)]) == 0 + + +# ── consistency with the stack-layout contract test ────────────────────────── + +def test_layout_check_matches_layout_contract_semantics(doctor, tmp_path): + """Same anatomy rules as tests/meta/test_stack_layout_contract.py: the + dispatcher-include ban, the trailer requirement when wiring.md exists.""" + root = make_sandbox(tmp_path) + stack = add_stack(root, "recursive") + (stack / "launch" / "stack.launch.xml").write_text( + '\n \n\n', encoding="utf-8") + (stack / "wiring.md").write_text("# hand-written, no trailer\n", + encoding="utf-8") + result = doctor.check_stack_layout(root) + assert result.status == doctor.WARN + text = " ".join(result.messages) + assert "robot.launch.xml" in text, "dispatcher include must be reported" + assert "trailer" in text, "hand-edited wiring.md must be reported" + + +# ── safety-floor scan (unit-level; --live capture needs a running stack) ───── + +def _graph(edges): + return {"version": 1, "nodes": sorted({e["node"] for e in edges}), + "topics": {}, "edges": edges} + + +def _pub(node, topic): + return {"node": node, "topic": topic, "dir": "pub", + "type": "x/msg/Y", "qos_profile": {}} + + +def test_safety_floor_quiet_for_blessed_chain(doctor): + graph = _graph([ + _pub("/robot_1/control/pid_controller", + "/robot_1/interface/cmd_roll_pitch_yawrate_thrust"), + _pub("/robot_1/interface/odom_modifier", "/robot_1/interface/cmd_pose"), + _pub("/robot_1/trajectory_controller/trajectory_control_node", + "/robot_1/trajectory_controller/tracking_point"), + _pub("/robot_1/trajectory_controller/trajectory_control_node", + "/robot_1/trajectory_controller/look_ahead"), + ]) + result = doctor.check_safety_floor(graph) + assert result.status == doctor.OK, result.messages + + +def test_safety_floor_flags_unblessed_setpoint_publisher(doctor): + graph = _graph([ + _pub("/robot_1/my_rogue_rl_policy", + "/robot_1/interface/cmd_roll_pitch_yawrate_thrust"), + ]) + result = doctor.check_safety_floor(graph) + assert result.status == doctor.WARN + assert any("my_rogue_rl_policy" in m and "UNBLESSED" in m + for m in result.messages) + + +def test_safety_floor_flags_controller_impersonator(doctor): + graph = _graph([ + _pub("/robot_1/fake_controller", "/robot_1/trajectory_controller/tracking_point"), + ]) + result = doctor.check_safety_floor(graph) + assert result.status == doctor.WARN + assert any("impersonating" in m for m in result.messages) + + +def test_safety_floor_lists_command_authority_without_flagging(doctor): + """trajectory_override publishers inherit the safety apparatus — they are + the command-authority map (informational), never violations.""" + graph = _graph([ + _pub("/robot_1/takeoff_landing_planner/takeoff_landing_task", + "/robot_1/trajectory_controller/trajectory_override"), + _pub("/robot_1/droan/planner", + "/robot_1/trajectory_controller/trajectory_segment_to_add"), + ]) + result = doctor.check_safety_floor(graph) + assert result.status == doctor.OK + assert any("command-authority map" in m for m in result.messages) + + +# ── stack inference ────────────────────────────────────────────────────────── + +def test_infer_stack_prefers_explicit_then_env(doctor, monkeypatch): + monkeypatch.setenv("AIRSTACK_STACK_DIR", "/root/AirStack/stacks/lite_default") + assert doctor.infer_stack("explicit") == "explicit" + assert doctor.infer_stack(None) == "lite_default" + monkeypatch.delenv("AIRSTACK_STACK_DIR") + assert doctor.infer_stack(None) is None diff --git a/tests/meta/test_stack_layout_contract.py b/tests/meta/test_stack_layout_contract.py index 6b54cf50c..c8f4b5755 100644 --- a/tests/meta/test_stack_layout_contract.py +++ b/tests/meta/test_stack_layout_contract.py @@ -16,6 +16,12 @@ ``wiring.md`` is NOT required yet (bootstrap: it is generated from the first validated wiring-snapshot run), but when present it must carry the machine-readable ``wiring-graph-v1`` trailer that the drift check reads. + +Split stacks (RFC #380 §2) extend the anatomy: a stack with **two or more** +launch entry points is a split stack and MUST carry a ``bridge.yaml`` +explicitly listing every topic/service/action crossing the machine boundary +(its schema and the control/trajectory placement hard gate are covered by +``tests/meta/test_bridge_contract.py``). """ import re @@ -98,6 +104,21 @@ def test_readme_non_trivial(self, stack): "what the stack is for, how to run it, and its known limits" ) + def test_split_stack_requires_bridge(self, stack): + """Two or more entry points = a split stack = an explicit bridge.yaml + (RFC #380 §2: the bridge list IS the split, readable in source).""" + entries = sorted((stack / "launch").glob("*.launch.xml")) + if len(entries) < 2: + pytest.skip(f"{stack.name}: unsplit stack " + f"({len(entries)} entry point)") + assert (stack / "bridge.yaml").is_file(), ( + f"{stack.name}: {len(entries)} launch entry points " + f"({', '.join(e.name for e in entries)}) but no bridge.yaml — a " + "split stack must declare every topic/service/action crossing " + "the machine boundary explicitly (RFC #380 §2); generate the " + "router config from it with tools/gen_dds_router.py" + ) + def test_wiring_md_trailer_when_present(self, stack): """wiring.md is optional (bootstrap) but must be machine-readable.""" path = stack / "wiring.md" diff --git a/tools/doctor/__init__.py b/tools/doctor/__init__.py new file mode 100644 index 000000000..c22244662 --- /dev/null +++ b/tools/doctor/__init__.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 +# Copyright (c) 2026 Carnegie Mellon University +# MIT License - see LICENSE in the repository root for full text. +"""``airstack doctor`` — observe-and-report health checks (RFC #379 §4). + +Default (compose-time) mode runs, in order: + +1. module manifests valid (``tools/validate_module.py``) — reports; +2. overlay integrity (``tools/module_overlay.py --check``) — reports; +3. **hard gate #1**: module dep conflicts + (``tools/compose_module_layers.py --check-conflicts``, RFC #379 §6); +4. stack folder anatomy, incl. the split-stack ⇒ ``bridge.yaml`` rule — reports; +5. **hard gate #2**: control-setpoint / trajectory-group names in any + ``bridge.yaml`` (``tools/gen_dds_router.py --check``, RFC #380 §2). + +Only the two enumerated hard gates set a non-zero exit in default mode; +everything else is reported and stepped aside from. Doctor never edits +anything (``--snapshot`` writes exactly one file — the stack's ``wiring.md`` — +because that file is *defined* as an observed artifact). + +Modes:: + + doctor # compose-time battery (above) + doctor --live [--stack NAME] # diff the RUNNING graph vs the stack's + # committed wiring.md (exit 1 on drift) + # + safety-floor publisher scan (WARN; + # --strict makes those fatal) + doctor --snapshot [--stack NAME] # same capture, WRITTEN to the stack's + # wiring.md with hardware provenance + # ('observed on , , — + # unverified-in-CI') + +``--stack`` is inferred from ``AIRSTACK_STACK_DIR`` when omitted (the env var +``airstack up ...stack `` exports). +""" +import argparse +import sys +from pathlib import Path + +try: + from .checks import ( # noqa: F401 (re-exported for tests) + OK, WARN, FAIL, + CheckResult, + capture_live_graph, + check_bridge_gates, + check_layer_conflicts, + check_module_manifests, + check_overlay, + check_safety_floor, + check_stack_layout, + infer_stack, + run_compose_time_checks, + run_live, + run_snapshot, + ) +except ImportError: # executed as a script: python3 tools/doctor/__init__.py + sys.path.insert(0, str(Path(__file__).resolve().parent)) + from checks import ( # noqa: F401 + OK, WARN, FAIL, + CheckResult, + capture_live_graph, + check_bridge_gates, + check_layer_conflicts, + check_module_manifests, + check_overlay, + check_safety_floor, + check_stack_layout, + infer_stack, + run_compose_time_checks, + run_live, + run_snapshot, + ) + +_STATUS_LABEL = {OK: "[ OK ]", WARN: "[WARN]", FAIL: "[FAIL]"} + + +def _print_results(results, log=print): + for result in results: + gate = " (hard gate)" if result.hard else "" + log(f"{_STATUS_LABEL[result.status]} {result.name}{gate}") + for message in result.messages: + for line in message.splitlines(): + log(f" {line}") + + +def run_default(root, stack=None, log=print): + """Compose-time battery; exit 1 iff a hard gate failed.""" + results = run_compose_time_checks(root, stack=stack) + _print_results(results, log=log) + gated = [r for r in results if r.gates] + warned = [r for r in results if r.status != OK and not r.gates] + if gated: + log(f"doctor: {len(gated)} hard-gate failure(s) " + f"({', '.join(r.name for r in gated)}) — see RFC #379 §4 for the " + "two enumerated gates") + return 1 + if warned: + log(f"doctor: findings reported in {len(warned)} check(s) — doctor " + "observes and steps aside (exit 0)") + else: + log("doctor: all checks clean") + return 0 + + +def main(argv=None): + parser = argparse.ArgumentParser( + prog="airstack doctor", + description="Observe-and-report health checks for an AirStack " + "checkout or a running stack (RFC #379 §4).", + ) + parser.add_argument( + "--live", action="store_true", + help="diff the running ROS graph against the stack's committed " + "wiring.md (exit 1 on drift) and scan for unblessed " + "control-setpoint publishers") + parser.add_argument( + "--snapshot", action="store_true", + help="capture the running graph and WRITE it as the stack's " + "wiring.md with an unverified-in-CI provenance line " + "(hardware bring-up path)") + parser.add_argument( + "--stack", + help="stack name under stacks/ (default: inferred from " + "AIRSTACK_STACK_DIR)") + parser.add_argument( + "--strict", action="store_true", + help="--live only: safety-floor warnings become fatal (exit 1)") + parser.add_argument( + "--project-root", + default=str(Path(__file__).resolve().parent.parent.parent), + help="AirStack checkout root (default: the repo containing this tool)") + args = parser.parse_args(argv) + + root = Path(args.project_root) + if args.live and args.snapshot: + parser.error("--live and --snapshot are exclusive modes") + try: + if args.snapshot: + return run_snapshot(root, args.stack) + if args.live: + return run_live(root, args.stack, strict=args.strict) + except FileNotFoundError as exc: # docker binary missing + print(f"[doctor] cannot capture the running graph: {exc}") + return 1 + except RuntimeError as exc: # no containers / identity resolution failed + print(f"[doctor] {exc}") + return 1 + return run_default(root, stack=args.stack) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/doctor/checks.py b/tools/doctor/checks.py new file mode 100644 index 000000000..7bccfb9a8 --- /dev/null +++ b/tools/doctor/checks.py @@ -0,0 +1,573 @@ +# Copyright (c) 2026 Carnegie Mellon University +# MIT License - see LICENSE in the repository root for full text. +"""Check implementations for ``airstack doctor`` (RFC #379 §4, RFC #380 §2). + +Doctor is **entirely observational**: it never generates, edits, or infers +anything — it observes the checkout (compose time) or the running graph +(``--live``) and reports. It hardens into a hard error (exit 1 in default +mode) in exactly **two enumerated places** and nowhere else: + +1. **Dep conflicts** that would compose a broken image + (``compose_module_layers.py --check-conflicts`` — RFC #379 §6); +2. **Safety-placement violations** — control-setpoint or trajectory-group + names in any stack's ``bridge.yaml`` + (``gen_dds_router.py --check`` — RFC #380 §2). + +Everything else reports and steps aside. Growing that list requires the +RFC #379 §8 process. + +Compose-time checks reuse the existing single-purpose tools +(``validate_module.py``, ``module_overlay.py --check``, +``compose_module_layers.py --check-conflicts``, ``gen_dds_router.py --check``) +rather than reimplementing them; ``--live`` reuses ``tests/wiring_snapshot.py`` +(the same capture/normalize/diff machinery as the CI wiring-snapshot test). +""" +import importlib.util +import json +import os +import re +import socket +import subprocess +import sys +import time +from pathlib import Path + +import yaml + +# Doctor always runs its own repo's tools, even when --project-root points at +# another checkout (a sandbox under test has no tools/ of its own). +TOOLS_DIR = Path(__file__).resolve().parent.parent +REPO_ROOT = TOOLS_DIR.parent + +# Container-side ROS environment (mirrors .airstack/modules/ready.sh). +ROS_DISTRO_SETUP = "/opt/ros/jazzy/setup.bash" +ROBOT_WS_SETUP = "/root/AirStack/robot/ros_ws/install/setup.bash" + +_TOPICS_PER_EXEC = 40 +_INFO_TIMEOUT_S = 20 + +OK, WARN, FAIL = "ok", "warn", "fail" + + +class CheckResult: + """One named check: status (ok/warn/fail), hard-gate flag, and messages.""" + + def __init__(self, name, hard=False): + self.name = name + self.hard = hard + self.status = OK + self.messages = [] + + def note(self, message): + self.messages.append(message) + + def warn(self, message): + self.messages.append(message) + if self.status == OK: + self.status = WARN + + def fail(self, message): + self.messages.append(message) + self.status = FAIL + + @property + def gates(self): + """True when this result must fail the doctor run (exit 1).""" + return self.hard and self.status == FAIL + + +def _load_tool(name): + """Import a tools/*.py module by path (mirrors the meta-test idiom).""" + spec = importlib.util.spec_from_file_location( + f"airstack_doctor_{name}", TOOLS_DIR / f"{name}.py") + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _run_tool(script, args, timeout=120): + """Run a tools/*.py script in a subprocess; return (returncode, output).""" + proc = subprocess.run( + [sys.executable, str(TOOLS_DIR / script), *args], + capture_output=True, text=True, timeout=timeout, + ) + output = (proc.stdout or "") + (proc.stderr or "") + return proc.returncode, output.strip() + + +def _stack_dirs(root, stack=None): + stacks_dir = Path(root) / "stacks" + if not stacks_dir.is_dir(): + return [] + dirs = sorted( + d for d in stacks_dir.iterdir() + if d.is_dir() and not d.name.startswith(".") + ) + if stack: + dirs = [d for d in dirs if d.name == stack] + return dirs + + +# ── compose-time checks ────────────────────────────────────────────────────── + +def check_module_manifests(root): + """Every synced / in-tree module manifest validates (observe-only).""" + result = CheckResult("module-manifests") + validator = _load_tool("validate_module") + module_dirs = [] + for base in (Path(root) / "modules", + Path(root) / "robot" / "ros_ws" / "src" / "modules"): + if not base.is_dir(): + continue + for child in sorted(base.iterdir()): + # in-tree overlay symlinks point back at modules/ checkouts, + # already covered by the first base (same rule as module doctor) + if base.name == "modules" and base.parent.name == "src" and child.is_symlink(): + continue + if child.is_dir() and (child / "module.yaml").is_file(): + module_dirs.append(child) + if not module_dirs: + result.note("no modules synced — nothing to validate") + return result + for module_dir in module_dirs: + verdict, warnings = validator.validate_module(module_dir) + rel = os.path.relpath(module_dir, root) + for warning in warnings: + result.warn(f"{rel}: {warning}") + if verdict["valid"]: + result.note(f"{rel}: manifest OK") + else: + for error in verdict["errors"]: + result.warn(f"{rel}: {error['path']}: {error['message']}") + result.warn(f"{rel}: manifest INVALID (sync is what fails on this)") + return result + + +def check_overlay(root): + """Overlay integrity: symlinks + generated compose fresh (observe-only).""" + result = CheckResult("module-overlay") + code, output = _run_tool( + "module_overlay.py", ["--check", "--project-root", str(root)]) + if code == 0: + result.note("overlay symlinks and generated compose are consistent") + else: + result.warn("overlay is broken or stale — run 'airstack module sync'") + if output: + result.warn(output) + return result + + +def check_layer_conflicts(root): + """HARD GATE #1 (RFC #379 §6): dep conflicts across module layers.""" + result = CheckResult("module-dep-conflicts", hard=True) + code, output = _run_tool( + "compose_module_layers.py", + ["--check-conflicts", "--project-root", str(root)]) + if code == 0: + result.note("no apt/pip pin conflicts across modules") + else: + result.fail("module dependency conflict — composing would build a " + "broken image (doctor hard gate #1, RFC #379 §6)") + if output: + result.fail(output) + return result + + +_COMMENT_RE = re.compile(r"", re.DOTALL) + + +def check_stack_layout(root, stack=None): + """Stack folder anatomy (RFC #379 §3, observe-only): the four committed + files, plus the split-stack rule — two or more entry points require a + bridge.yaml (RFC #380 §2). Mirrors tests/meta/test_stack_layout_contract.py.""" + result = CheckResult("stack-layout") + stack_dirs = _stack_dirs(root, stack) + if not stack_dirs: + result.warn(f"no stack folders under {Path(root) / 'stacks'}" + + (f" matching {stack!r}" if stack else "")) + return result + for stack_dir in stack_dirs: + name = stack_dir.name + repos = stack_dir / "modules.repos" + if not repos.is_file(): + result.warn(f"{name}: missing modules.repos") + else: + try: + data = yaml.safe_load(repos.read_text(encoding="utf-8")) + except yaml.YAMLError as exc: + data = None + result.warn(f"{name}: modules.repos is invalid YAML: {exc}") + if isinstance(data, dict): + compat = data.get("airstack_compat") + if not (isinstance(compat, str) and compat.strip()): + result.warn(f"{name}: modules.repos needs a top-level " + "airstack_compat semver-range string") + if "repositories" not in data: + result.warn(f"{name}: modules.repos needs a repositories: key") + + entries = sorted((stack_dir / "launch").glob("*.launch.xml")) \ + if (stack_dir / "launch").is_dir() else [] + if not entries: + result.warn(f"{name}: launch/ has no *.launch.xml entry point") + for entry in entries: + text = _COMMENT_RE.sub("", entry.read_text(encoding="utf-8", + errors="replace")) + if "robot.launch.xml" in text: + result.warn(f"{name}/launch/{entry.name}: includes the " + "dispatcher robot.launch.xml (infinite recursion)") + + compose = stack_dir / "docker-compose.yaml" + if not compose.is_file(): + result.warn(f"{name}: missing docker-compose.yaml") + else: + try: + if yaml.safe_load(compose.read_text(encoding="utf-8")) is None: + result.warn(f"{name}: docker-compose.yaml is empty") + except yaml.YAMLError as exc: + result.warn(f"{name}: docker-compose.yaml invalid YAML: {exc}") + + readme = stack_dir / "README.md" + if not readme.is_file(): + result.warn(f"{name}: missing README.md") + elif len(readme.read_text(encoding="utf-8").strip()) < 200: + result.warn(f"{name}: README.md is trivial") + + # split-stack rule: >= 2 entry points require an explicit bridge.yaml + if len(entries) >= 2 and not (stack_dir / "bridge.yaml").is_file(): + result.warn( + f"{name}: {len(entries)} launch entry points but no " + "bridge.yaml — a split stack must declare its machine " + "boundary explicitly (RFC #380 §2)") + + wiring = stack_dir / "wiring.md" + if wiring.is_file(): + ws = _wiring_snapshot() + try: + ws.extract_graph_from_md(wiring.read_text(encoding="utf-8")) + except ValueError as exc: + result.warn(f"{name}: wiring.md has no parseable trailer " + f"({exc}) — regenerate, never hand-edit") + else: + result.note(f"{name}: wiring.md not committed yet (bootstrap)") + if result.status == OK: + result.note(f"{len(stack_dirs)} stack folder(s) pass the anatomy check") + return result + + +def check_bridge_gates(root, stack=None): + """HARD GATE #2 (RFC #380 §2): bridge.yaml schema + placement gate, via + gen_dds_router --check, for every stack that carries a bridge.yaml.""" + result = CheckResult("bridge-placement-gate", hard=True) + gen = _load_tool("gen_dds_router") + checked = 0 + for stack_dir in _stack_dirs(root, stack): + bridge = stack_dir / "bridge.yaml" + if not bridge.is_file(): + continue + checked += 1 + try: + data = gen.load_bridge(bridge) + except yaml.YAMLError as exc: + result.fail(f"{stack_dir.name}/bridge.yaml: invalid YAML: {exc}") + continue + errors = gen.validate_bridge(data) + if errors: + for error in errors: + result.fail(f"{stack_dir.name}/bridge.yaml: {error['path']}: " + f"{error['message']}") + else: + result.note(f"{stack_dir.name}/bridge.yaml: valid; no " + "control-setpoint/trajectory-group names cross " + "the boundary") + if checked == 0: + result.note("no stack carries a bridge.yaml — gate vacuously satisfied") + return result + + +def run_compose_time_checks(root, stack=None): + """The default `airstack doctor` check battery, in order.""" + return [ + check_module_manifests(root), + check_overlay(root), + check_layer_conflicts(root), + check_stack_layout(root, stack=stack), + check_bridge_gates(root, stack=stack), + ] + + +# ── live capture (docker exec per robot; reuses tests/wiring_snapshot.py) ──── + +def _wiring_snapshot(): + """Import tests/wiring_snapshot.py (stdlib-only, runnable anywhere).""" + spec = importlib.util.spec_from_file_location( + "airstack_doctor_wiring_snapshot", + REPO_ROOT / "tests" / "wiring_snapshot.py") + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _docker(args, timeout=60, check=True): + proc = subprocess.run(["docker", *args], capture_output=True, text=True, + timeout=timeout) + if check and proc.returncode != 0: + raise RuntimeError( + f"docker {' '.join(args[:3])}... failed: {proc.stderr.strip()}") + return proc.stdout + + +def discover_robot_containers(): + """Running robot containers (compose replicas), sorted (as ready.sh does).""" + out = _docker(["ps", "--format", "{{.Names}}"]) + return sorted(n for n in out.splitlines() if "-robot-" in n) + + +def container_identity(container): + """(robot_name, domain_id) via the container's login-shell resolution — + the same .bashrc path `airstack status` and `airstack ready` use.""" + out = _docker([ + "exec", container, "bash", "--login", "-c", + 'printf "AIRSTACK_VARS:%s:%s\\n" "$ROBOT_NAME" "$ROS_DOMAIN_ID"', + ], timeout=30) + lines = [l for l in out.splitlines() if l.startswith("AIRSTACK_VARS:")] + if not lines: + raise RuntimeError(f"{container}: could not resolve ROBOT_NAME/ROS_DOMAIN_ID") + _, robot_name, domain = lines[-1].split(":", 2) + return robot_name, domain + + +def _ros2_exec(container, domain, command, timeout=40): + script = ( + f"source {ROS_DISTRO_SETUP} >/dev/null 2>&1\n" + f"[ -f {ROBOT_WS_SETUP} ] && source {ROBOT_WS_SETUP} >/dev/null 2>&1\n" + f"export ROS_DOMAIN_ID={domain}\n" + f"timeout {timeout - 10} {command}" + ) + return _docker(["exec", container, "bash", "-c", script], + timeout=timeout, check=False) + + +def _batched_topic_info(container, domain, topics, ws): + """`ros2 topic info --verbose` per topic, chunk-batched per docker exec — + the tests/system/test_wiring_snapshot.py idiom (backgrounded probes into + /tmp files, catted back with ===FILE=== sentinels).""" + outputs = {} + for start in range(0, len(topics), _TOPICS_PER_EXEC): + chunk = topics[start:start + _TOPICS_PER_EXEC] + temp_files = {} + probes = [] + for i, topic in enumerate(chunk, start=start): + fname = f"/tmp/doctor_wiring_{i}.out" + temp_files[topic] = fname + probes.append( + f"(ROS_DOMAIN_ID={domain} timeout {_INFO_TIMEOUT_S} " + f"ros2 topic info --verbose {topic} > {fname} 2>&1) &") + lines = [ + f"source {ROS_DISTRO_SETUP} >/dev/null 2>&1", + f"[ -f {ROBOT_WS_SETUP} ] && source {ROBOT_WS_SETUP} >/dev/null 2>&1", + ] + lines += probes + ["wait"] + for fname in temp_files.values(): + lines.append(f"echo '===FILE {fname}==='") + lines.append(f"cat {fname} 2>/dev/null || true") + out = _docker(["exec", container, "bash", "-c", "\n".join(lines)], + timeout=_INFO_TIMEOUT_S + 90, check=False) + for piece in out.split("===FILE ")[1:]: + header, _, content = piece.partition("===") + fname = header.strip() + topic = next((t for t, f in temp_files.items() if f == fname), None) + if topic is not None: + outputs[topic] = content + return outputs + + +def capture_live_graph(log=print): + """Snapshot every running robot's graph and merge into one normalized + graph — the same capture tests/system/test_wiring_snapshot.py performs, + driven by plain `docker exec` so it runs on any host with the stack up.""" + ws = _wiring_snapshot() + containers = discover_robot_containers() + if not containers: + raise RuntimeError("no running robot containers (docker ps shows no " + "'-robot-' names) — bring the stack up first") + nodes, topics, edges = set(), {}, [] + for container in containers: + robot_name, domain = container_identity(container) + node_out = _ros2_exec(container, domain, "ros2 node list 2>/dev/null") + robot_nodes = ws.parse_node_list(node_out) + topic_out = _ros2_exec(container, domain, "ros2 topic list 2>/dev/null") + robot_topics = sorted({ + line.strip() for line in topic_out.splitlines() + if line.strip().startswith("/") + }) + log(f"[doctor] {container} ({robot_name}, domain {domain}): " + f"{len(robot_nodes)} nodes, {len(robot_topics)} topics") + nodes.update(robot_nodes) + infos = _batched_topic_info(container, domain, robot_topics, ws) + for topic in robot_topics: + text = infos.get(topic) + if not text: + continue + entry, topic_edges = ws.parse_topic_info_verbose(topic, text) + topics.setdefault(topic, entry) + edges.extend(topic_edges) + graph = {"version": 1, "nodes": sorted(nodes), "topics": topics, + "edges": edges} + return ws.normalize_graph(graph) + + +# ── safety-floor visibility (RFC #379 §4: report loudly, never gate) ───────── + +# Trajectory-group command inputs: ANY module may publish these — that is the +# selling point (a trajectory_override publisher inherits the whole safety +# apparatus). They are listed as the command-authority map, never flagged. +_COMMAND_INPUT_BASENAMES = {"trajectory_override", "trajectory_segment_to_add"} +# Trajectory-group outputs: only the trajectory controller may publish these. +_CONTROLLER_OUTPUT_BASENAMES = {"tracking_point", "look_ahead"} +# Control setpoints: the interface's command inputs (today's concrete spelling +# of the conventions-spec `control_setpoint` interchange). +_SETPOINT_TOPIC_RE = re.compile(r"/interface/cmd_[A-Za-z0-9_]+$") +# Blessed publishers of control setpoints (node-name suffixes). +_BLESSED_SETPOINT_NODES = ("/control/pid_controller", "/interface/odom_modifier") + + +def check_safety_floor(graph): + """Flag publishers of control-setpoint / controller-output topics that are + not the blessed controller chain. Visibility, not enforcement: WARN only + (exit 1 only under --strict).""" + result = CheckResult("safety-floor") + authority_map = [] + for edge in graph.get("edges") or []: + if edge.get("dir") != "pub": + continue + topic = edge.get("topic") or "" + node = edge.get("node") or "" + basename = topic.rsplit("/", 1)[-1] + if basename in _COMMAND_INPUT_BASENAMES: + authority_map.append(f"{node} -> {topic}") + elif basename in _CONTROLLER_OUTPUT_BASENAMES: + if "/trajectory_controller/" not in node + "/": + result.warn( + f"UNBLESSED controller-output publisher: {node} publishes " + f"{topic} but is not the trajectory controller — " + "something is impersonating the blessed controller " + "(RFC #379 §4 safety floor)") + elif _SETPOINT_TOPIC_RE.search(topic) or basename == "control_setpoint": + if not node.endswith(_BLESSED_SETPOINT_NODES): + result.warn( + f"UNBLESSED control-setpoint publisher: {node} publishes " + f"{topic} — command authority is bypassing the blessed " + "controller chain (RFC #379 §4 safety floor)") + if authority_map: + result.note("command-authority map (trajectory-group publishers — " + "informational):") + for line in sorted(authority_map): + result.note(f" {line}") + if result.status == OK: + result.note("all control-setpoint/controller-output publishers are " + "the blessed controller chain") + return result + + +# ── live / snapshot drivers ────────────────────────────────────────────────── + +def infer_stack(stack): + """--stack wins; else infer from AIRSTACK_STACK_DIR (how `airstack up` + selects a stack).""" + if stack: + return stack + stack_dir = os.environ.get("AIRSTACK_STACK_DIR", "").rstrip("/") + if stack_dir: + return os.path.basename(stack_dir) + return None + + +def run_live(root, stack, strict=False, log=print): + """`doctor --live`: capture the running graph, diff against the stack's + committed wiring.md, and run the safety-floor scan. Returns exit code.""" + ws = _wiring_snapshot() + stack = infer_stack(stack) + if not stack: + log("[doctor] --live needs a stack: pass --stack or set " + "AIRSTACK_STACK_DIR (airstack up ...stack )") + return 1 + + graph = capture_live_graph(log=log) + + exit_code = 0 + safety = check_safety_floor(graph) + for message in safety.messages: + log(f"[doctor] {message}") + if safety.status != OK and strict: + log("[doctor] --strict: safety-floor warnings are fatal") + exit_code = 1 + + wiring_path = Path(root) / "stacks" / stack / "wiring.md" + if not wiring_path.is_file(): + log(f"[doctor] no committed wiring.md at {wiring_path} — nothing to " + "diff against. Bootstrap it from a validated run " + "(airstack test -m wiring --stack {0}) or, on hardware, " + "'airstack doctor --snapshot --stack {0}'.".format(stack)) + return 1 + expected = ws.extract_graph_from_md(wiring_path.read_text(encoding="utf-8")) + verdict = ws.diff_graphs(expected, graph) + if verdict["identical"]: + log(f"[doctor] graph matches wiring.md ({wiring_path})") + return exit_code + log(f"[doctor] DRIFT: the running graph differs from {wiring_path}:") + log(json.dumps(verdict, indent=2)) + log("[doctor] if the change is intentional, regenerate wiring.md from a " + "wiring-snapshot run (or --snapshot on hardware) and commit it.") + return 1 + + +def run_snapshot(root, stack, log=print): + """`doctor --snapshot`: the identical capture, committed as the stack's + wiring.md with hardware provenance (RFC #379 §4.4: a stack that cannot run + in CI still gets an *observed* wiring.md, refreshed by hand; the CI drift + check reports it as unverified-in-CI rather than silently passing).""" + ws = _wiring_snapshot() + stack = infer_stack(stack) + if not stack: + log("[doctor] --snapshot needs a stack: pass --stack or set " + "AIRSTACK_STACK_DIR") + return 1 + stack_dir = Path(root) / "stacks" / stack + if not stack_dir.is_dir(): + log(f"[doctor] no stack folder at {stack_dir}") + return 1 + + graph = capture_live_graph(log=log) + safety = check_safety_floor(graph) + for message in safety.messages: + log(f"[doctor] {message}") + + hostname = socket.gethostname() + date = time.strftime("%Y-%m-%d %H:%M:%S") + sha = _git_short_sha(root) + meta = { + "stack": stack, + "generated-by": "airstack doctor --snapshot", + "date": date, + "source-sha": sha, + "provenance": f"observed on {hostname}, {date}, {sha} — " + "unverified-in-CI", + } + wiring_path = stack_dir / "wiring.md" + wiring_path.write_text(ws.render_wiring_md(graph, meta), encoding="utf-8") + log(f"[doctor] wrote {wiring_path} ({len(graph['nodes'])} nodes, " + f"{len(graph['edges'])} edges) — review and commit it; the CI drift " + "check will report this stack as unverified-in-CI") + return 0 + + +def _git_short_sha(root): + try: + proc = subprocess.run(["git", "rev-parse", "--short", "HEAD"], + cwd=str(root), capture_output=True, text=True, + timeout=10) + if proc.stdout.strip(): + return proc.stdout.strip() + except (OSError, subprocess.TimeoutExpired): + pass + return "unknown" diff --git a/tools/gen_dds_router.py b/tools/gen_dds_router.py new file mode 100644 index 000000000..f4a568300 --- /dev/null +++ b/tools/gen_dds_router.py @@ -0,0 +1,358 @@ +#!/usr/bin/env python3 +# Copyright (c) 2026 Carnegie Mellon University +# MIT License - see LICENSE in the repository root for full text. +"""Generate a DDS-router config from a split stack's ``bridge.yaml`` (RFC #380 §2). + +A split stack carries one launch entry point per host role plus a ``bridge.yaml`` +explicitly listing every topic/service/action crossing the machine boundary. +That list is authoritative and human-readable; this tool derives the eProsima +DDS Router allowlist config from it — the same format as +``autonomy_bringup/onboard_local_offboard_global/config/dds_router.yaml`` and +consumed by the same ``interpolate_dds_router.launch.py`` (``$(env ROBOT_NAME)`` +/ ``$(var gcs_domain)`` tokens are resolved at launch, per-robot). + +Output is **deterministic**: a pure function of ``bridge.yaml`` (no timestamps, +no absolute paths), written to ``.airstack/generated/dds_router..yaml`` +so identical inputs regenerate byte-identical configs. + +``--check`` validates the ``bridge.yaml`` schema and enforces **doctor hard +gate #2** (RFC #379 §4 / RFC #380 §2): ``control_setpoint`` and +trajectory-group names (``trajectory_override``, ``trajectory_segment_to_add``, +``set_trajectory_mode``, ``tracking_point``, ``look_ahead`` — the +``trajectory_controller/*`` group) must never appear in a bridge list. Command +authority stays onboard; link loss must leave the vehicle able to failsafe. +Violations exit 1 naming each offending entry. + +CLI:: + + gen_dds_router.py [--out PATH] [--check] [--project-root DIR] + +Human-readable errors go to stderr; a JSON verdict +``{"valid": bool, "errors": [{"path", "message"}]}`` goes to stdout in +``--check`` mode; exit 0/1. +""" +import argparse +import json +import re +import sys +from pathlib import Path + +import yaml + +GENERATED_REL = Path(".airstack") / "generated" + +DIRECTIONS = ("onboard_to_offboard", "offboard_to_onboard") +QOS_VALUES = ("reliable", "best_effort") +ENTRY_KINDS = ("topic", "service", "action") + +_TYPE_RE = re.compile(r"^[a-z][a-z0-9_]*/(msg|srv|action)/[A-Za-z][A-Za-z0-9_]*$") +_NAME_RE = re.compile(r"^[A-Za-z0-9_][A-Za-z0-9_/]*$") + +# ── Doctor hard gate #2 (RFC #379 §4, RFC #380 §2) ────────────────────────── +# The enumerated trajectory-group names plus the control-setpoint interchange. +# Matched against the entry name's final path segment. +FORBIDDEN_BASENAMES = frozenset({ + "trajectory_override", + "trajectory_segment_to_add", + "set_trajectory_mode", + "tracking_point", + "look_ahead", + "control_setpoint", +}) +# The whole trajectory_controller/* group stays off the bridge, and so do the +# concrete control-setpoint topics (the interface's cmd_* command inputs). +FORBIDDEN_SEGMENTS = frozenset({"trajectory_controller"}) +_FORBIDDEN_BASENAME_PREFIXES = ("cmd_",) + +RFC_CITE = "RFC #379 §4 / RFC #380 §2 (doctor hard gate: command authority stays onboard)" + + +def check_hard_gate(name): + """Return an error message when *name* violates the placement gate, else None.""" + segments = [s for s in name.split("/") if s] + if not segments: + return None + basename = segments[-1] + if basename in FORBIDDEN_BASENAMES: + return ( + f"{name!r} is a control-setpoint/trajectory-group name " + f"({basename!r}) and must not cross a machine boundary — {RFC_CITE}" + ) + if basename.startswith(_FORBIDDEN_BASENAME_PREFIXES): + return ( + f"{name!r} looks like a control-setpoint command input " + f"({basename!r}) and must not cross a machine boundary — {RFC_CITE}" + ) + hit = FORBIDDEN_SEGMENTS.intersection(segments) + if hit: + return ( + f"{name!r} is under the {sorted(hit)[0]}/* group, which stays off " + f"the bridge wholesale — {RFC_CITE}" + ) + return None + + +# ── schema validation ──────────────────────────────────────────────────────── + +def validate_bridge(data): + """Validate a parsed bridge.yaml. Returns a list of {path, message} errors.""" + errors = [] + + def err(path, message): + errors.append({"path": path, "message": message}) + + if not isinstance(data, dict): + err("(root)", "bridge.yaml top level must be a mapping") + return errors + + stack = data.get("stack") + if stack is not None and (not isinstance(stack, str) or not stack.strip()): + err("stack", "when present, stack: must be a non-empty string") + + entries = data.get("bridge") + if not isinstance(entries, list): + err("bridge", "bridge: must be a list of boundary entries") + return errors + + seen = set() + for i, entry in enumerate(entries): + path = f"bridge[{i}]" + if not isinstance(entry, dict): + err(path, "entry must be a mapping") + continue + + kinds = [k for k in ENTRY_KINDS if k in entry] + if len(kinds) != 1: + err(path, f"entry must have exactly one of {'/'.join(ENTRY_KINDS)} " + f"(got {kinds or 'none'})") + continue + kind = kinds[0] + name = entry[kind] + + if not isinstance(name, str) or not name.strip(): + err(f"{path}.{kind}", "name must be a non-empty string") + continue + if name.startswith("/"): + err(f"{path}.{kind}", + f"{name!r} must be relative to the robot namespace — the " + "generator prefixes $(env ROBOT_NAME) itself") + elif "$(" in name: + err(f"{path}.{kind}", + f"{name!r} must not carry substitution tokens — interpolation " + "conventions live in the generated config, not the bridge list") + elif not _NAME_RE.match(name): + err(f"{path}.{kind}", f"{name!r} is not a valid relative ROS name") + + if (kind, name) in seen: + err(f"{path}.{kind}", f"duplicate entry for {name!r}") + seen.add((kind, name)) + + type_name = entry.get("type") + if not isinstance(type_name, str) or not _TYPE_RE.match(type_name): + err(f"{path}.type", + f"{type_name!r} is not a pkg/(msg|srv|action)/Name interface type") + else: + expected_ns = {"topic": "msg", "service": "srv", "action": "action"}[kind] + if f"/{expected_ns}/" not in type_name: + err(f"{path}.type", + f"{type_name!r} does not match the entry kind {kind!r} " + f"(expected a */{expected_ns}/* type)") + + direction = entry.get("direction") + if direction not in DIRECTIONS: + err(f"{path}.direction", + f"{direction!r} is not one of {list(DIRECTIONS)}") + + qos = entry.get("qos") + if kind == "topic": + if qos not in QOS_VALUES: + err(f"{path}.qos", + f"{qos!r} is not one of {list(QOS_VALUES)} (required for topics)") + elif qos is not None and qos not in QOS_VALUES: + err(f"{path}.qos", f"{qos!r} is not one of {list(QOS_VALUES)}") + + unknown = set(entry) - {kind, "type", "direction", "qos"} + if unknown: + err(path, f"unknown fields: {sorted(unknown)}") + + # Hard gate #2 — checked here so schema-valid-but-unsafe still fails. + if isinstance(name, str): + gate = check_hard_gate(name) + if gate: + err(f"{path}.{kind}", gate) + + return errors + + +# ── config generation ──────────────────────────────────────────────────────── + +def _allowlist_lines(kind, name): + """DDS endpoint allowlist entries for one bridge entry (relative name). + + Topic prefixes per the DDS/ROS 2 mapping documented in + onboard_all/config/dds_router.yaml: rt/ topics, rq/…Request + rr/…Reply + service pairs, and the five action sub-endpoints. + """ + ns = "$(env ROBOT_NAME)" + if kind == "topic": + return [f"rt/{ns}/{name}"] + if kind == "service": + return [f"rq/{ns}/{name}Request", f"rr/{ns}/{name}Reply"] + # action: goal/cancel/result services + feedback/status topics + base = f"{ns}/{name}/_action" + return [ + f"rq/{base}/send_goalRequest", f"rr/{base}/send_goalReply", + f"rq/{base}/cancel_goalRequest", f"rr/{base}/cancel_goalReply", + f"rq/{base}/get_resultRequest", f"rr/{base}/get_resultReply", + f"rt/{base}/feedback", f"rt/{base}/status", + ] + + +def render_router_config(data, source_rel="bridge.yaml"): + """Render the DDS-router YAML text for a validated bridge mapping. + + Deterministic: pure function of the input (entries grouped by direction in + input order; no timestamps, no absolute paths). The output format mirrors + autonomy_bringup/onboard_local_offboard_global/config/dds_router.yaml — + participants on $(env ROS_DOMAIN_ID) / $(var gcs_domain), an rt/rq/rr + allowlist with $(env ROBOT_NAME) interpolation — so the same + interpolate_dds_router.launch.py consumes it unchanged. + """ + stack = data.get("stack", "unknown") + groups = {d: [] for d in DIRECTIONS} + for entry in data.get("bridge") or []: + kind = next(k for k in ENTRY_KINDS if k in entry) + groups[entry["direction"]].append((kind, entry[kind], entry.get("qos"))) + + lines = [ + f"# GENERATED by tools/gen_dds_router.py from {source_rel} — do not edit.", + f"# Split stack: {stack} (RFC #380 S2). Edit the bridge.yaml and regenerate.", + "#", + "# The DDS router bridges allowlisted endpoints bidirectionally; the", + "# direction comments below document intent (from bridge.yaml).", + "# $(env ...) / $(var ...) tokens are resolved per-robot at launch by", + "# interpolate_dds_router.launch.py.", + "participants:", + ' - name: "onboard"', + ' kind: "local"', + " domain: $(env ROS_DOMAIN_ID)", + ' - name: "offboard"', + ' kind: "local"', + " domain: $(var gcs_domain)", + "allowlist:", + ] + headers = { + "onboard_to_offboard": " # ===== onboard --> offboard =====", + "offboard_to_onboard": " # ===== offboard --> onboard =====", + } + for direction in DIRECTIONS: + entries = groups[direction] + if not entries: + continue + lines.append(headers[direction]) + for kind, name, qos in entries: + annotation = f"{kind}: {name}" + (f" ({qos})" if qos else "") + lines.append(f" # {annotation}") + for endpoint in _allowlist_lines(kind, name): + lines.append(f' - name: "{endpoint}"') + return "\n".join(lines) + "\n" + + +# ── entry points ───────────────────────────────────────────────────────────── + +def load_bridge(path): + with open(path, encoding="utf-8") as f: + return yaml.safe_load(f) + + +def default_out_path(bridge_path, project_root, stack=None): + name = stack or _stack_name(bridge_path) + return Path(project_root) / GENERATED_REL / f"dds_router.{name}.yaml" + + +def _stack_name(bridge_path): + bridge_path = Path(bridge_path) + try: + data = load_bridge(bridge_path) + if isinstance(data, dict) and isinstance(data.get("stack"), str): + return data["stack"] + except (OSError, yaml.YAMLError): + pass + return bridge_path.resolve().parent.name + + +def main(argv=None): + parser = argparse.ArgumentParser( + description="Generate (or --check) a DDS-router config from a split " + "stack's bridge.yaml (RFC #380 §2).", + ) + parser.add_argument("bridge", help="path to the stack's bridge.yaml") + parser.add_argument( + "--out", + help="output path (default: /.airstack/generated/" + "dds_router..yaml)", + ) + parser.add_argument( + "--check", action="store_true", + help="validate schema + the control/trajectory hard gate; write nothing", + ) + parser.add_argument( + "--project-root", + default=str(Path(__file__).resolve().parent.parent), + help="AirStack checkout root (default: parent of tools/)", + ) + args = parser.parse_args(argv) + + bridge_path = Path(args.bridge) + if not bridge_path.is_file(): + print(f"error: bridge file not found: {bridge_path}", file=sys.stderr) + if args.check: + print(json.dumps({"valid": False, "errors": [ + {"path": "(file)", "message": f"not found: {bridge_path}"}]}, + indent=2)) + return 1 + + try: + data = load_bridge(bridge_path) + except yaml.YAMLError as exc: + print(f"error: invalid YAML in {bridge_path}: {exc}", file=sys.stderr) + if args.check: + print(json.dumps({"valid": False, "errors": [ + {"path": "(file)", "message": f"invalid YAML: {exc}"}]}, indent=2)) + return 1 + + errors = validate_bridge(data) + if args.check: + for error in errors: + print(f"error: {error['path']}: {error['message']}", file=sys.stderr) + if not errors: + print(f"{bridge_path}: bridge.yaml is valid and passes the " + f"placement hard gate ({RFC_CITE})", file=sys.stderr) + print(json.dumps({"valid": not errors, "errors": errors}, indent=2)) + return 0 if not errors else 1 + + if errors: + for error in errors: + print(f"error: {error['path']}: {error['message']}", file=sys.stderr) + print(f"error: refusing to generate from an invalid bridge.yaml " + f"({len(errors)} error(s) above)", file=sys.stderr) + return 1 + + project_root = Path(args.project_root) + out_path = Path(args.out) if args.out else default_out_path( + bridge_path, project_root, stack=(data or {}).get("stack")) + + try: + source_rel = bridge_path.resolve().relative_to(project_root.resolve()) + except ValueError: + source_rel = bridge_path.name # keep the output free of absolute paths + + text = render_router_config(data, source_rel=str(source_rel)) + out_path.parent.mkdir(parents=True, exist_ok=True) + out_path.write_text(text, encoding="utf-8") + print(f"wrote {out_path}", file=sys.stderr) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/stack_diff.py b/tools/stack_diff.py new file mode 100644 index 000000000..7d9c73f0d --- /dev/null +++ b/tools/stack_diff.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 +# Copyright (c) 2026 Carnegie Mellon University +# MIT License - see LICENSE in the repository root for full text. +"""Compare two stacks' observed wiring (RFC #379 §3: ``airstack stack diff``). + +Copy-drift between stack folders is the accepted trade for folder +independence; this tool bounds it by diffing the stacks' *generated* +``wiring.md`` trailers (normalized, CI-verified observed graphs), so the diff +shows topology differences — nodes/edges/topics added or removed, QoS and +type mismatches — never XML formatting noise. + +Thin wrapper over ``tests/wiring_snapshot.py``'s ``extract_graph_from_md`` + +``diff_graphs`` (the same machinery the CI drift check uses). + +CLI:: + + stack_diff.py [--stacks-dir DIR] [--json] + +Stack arguments are stack names under ``stacks/`` or paths to stack folders / +``wiring.md`` files. Exit 0 when the graphs are identical, 1 when they +differ, 2 when a wiring.md is missing or unreadable. +""" +import argparse +import json +import sys +from pathlib import Path + +_REPO = Path(__file__).resolve().parent.parent +_TESTS_DIR = _REPO / "tests" +if str(_TESTS_DIR) not in sys.path: + sys.path.insert(0, str(_TESTS_DIR)) + +import wiring_snapshot as ws # noqa: E402 (tests/wiring_snapshot.py, stdlib-only) + + +def resolve_wiring_path(spec, stacks_dir): + """Resolve a stack name / stack folder / wiring.md path to a wiring.md path.""" + path = Path(spec) + if path.is_file(): + return path + if path.is_dir(): + return path / "wiring.md" + return Path(stacks_dir) / spec / "wiring.md" + + +def load_graph(spec, stacks_dir): + path = resolve_wiring_path(spec, stacks_dir) + if not path.is_file(): + raise FileNotFoundError( + f"{spec}: no wiring.md at {path} — generate it first " + "(airstack test -m wiring --stack , or airstack doctor " + "--snapshot on a real bring-up)" + ) + return ws.extract_graph_from_md(path.read_text(encoding="utf-8")), path + + +def _print_section(title, items, limit=50): + print(f" {title}: {len(items)}") + for item in items[:limit]: + print(f" {item}") + if len(items) > limit: + print(f" ... and {len(items) - limit} more") + + +def summarize(name_a, name_b, verdict): + """Human summary of a diff_graphs verdict (a→b: 'missing' = only in a).""" + if verdict["identical"]: + print(f"{name_a} and {name_b} have identical wiring " + "(normalized observed graphs match).") + return + print(f"Wiring differs between {name_a} (A) and {name_b} (B):") + _print_section(f"nodes only in {name_a} (A)", verdict["missing_nodes"]) + _print_section(f"nodes only in {name_b} (B)", verdict["extra_nodes"]) + _print_section(f"edges only in {name_a} (A)", verdict["missing_edges"]) + _print_section(f"edges only in {name_b} (B)", verdict["extra_edges"]) + _print_section( + "QoS mismatches on shared edges", + [f'{m["edge"]}: A={m["expected"]} B={m["observed"]}' + for m in verdict["qos_mismatches"]], + ) + _print_section( + "type mismatches on shared topics", + [f'{m["topic"]}: A={m["expected"]} B={m["observed"]}' + for m in verdict["type_mismatches"]], + ) + + +def main(argv=None): + parser = argparse.ArgumentParser( + description="Diff two stacks' generated wiring.md graphs " + "(RFC #379 §3, airstack stack diff).", + ) + parser.add_argument("stack_a", help="stack name, stack folder, or wiring.md path") + parser.add_argument("stack_b", help="stack name, stack folder, or wiring.md path") + parser.add_argument( + "--stacks-dir", default=str(_REPO / "stacks"), + help="where stack names resolve (default: /stacks)", + ) + parser.add_argument( + "--json", action="store_true", + help="print the raw diff_graphs verdict JSON instead of the summary", + ) + args = parser.parse_args(argv) + + try: + graph_a, path_a = load_graph(args.stack_a, args.stacks_dir) + graph_b, path_b = load_graph(args.stack_b, args.stacks_dir) + except (FileNotFoundError, ValueError) as exc: + print(f"error: {exc}", file=sys.stderr) + return 2 + + verdict = ws.diff_graphs(graph_a, graph_b) + if args.json: + print(json.dumps(verdict, indent=2)) + else: + print(f"A: {path_a}") + print(f"B: {path_b}") + summarize(args.stack_a, args.stack_b, verdict) + return 0 if verdict["identical"] else 1 + + +if __name__ == "__main__": + sys.exit(main()) From 2a178e6812d0b8a47096a1eff112d424d86a75f0 Mon Sep 17 00:00:00 2001 From: Andrew Jong Date: Thu, 20 Aug 2026 21:24:02 -0400 Subject: [PATCH 7/8] P5-E3(rfc-379): bless lite_default wiring baseline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Onboard-lite topology observed live; verified no global-layer nodes present. All four pre-existing baselines passed drift-clean through the E3a flatten — the refactor is machine-proven graph-preserving. Co-Authored-By: Claude Fable 5 --- stacks/lite_default/wiring.md | 7325 +++++++++++++++++++++++++++++++++ 1 file changed, 7325 insertions(+) create mode 100644 stacks/lite_default/wiring.md diff --git a/stacks/lite_default/wiring.md b/stacks/lite_default/wiring.md new file mode 100644 index 000000000..0630080c4 --- /dev/null +++ b/stacks/lite_default/wiring.md @@ -0,0 +1,7325 @@ +# Wiring snapshot: lite_default + +- **generated-by**: tests/system/test_wiring_snapshot.py +- **date**: 2026-08-21 01:23:22 +- **sim**: isaacsim +- **num_robots**: 1 +- **source-sha**: badfde5f8f62 + +```mermaid +graph LR + subgraph g0["behavior"] + n1["/robot_1/behavior/drone_safety_monitor/drone_safety_monitor"] + end + subgraph g1["control"] + n2["/robot_1/control/pid_controller"] + end + subgraph g2["droan"] + n3["/robot_1/droan/disparity_expander_node"] + end + subgraph g3["interface"] + n5["/robot_1/interface/mavros/actuator_control"] + n6["/robot_1/interface/mavros/adsb"] + n7["/robot_1/interface/mavros/altitude"] + n8["/robot_1/interface/mavros/cam_imu_sync"] + n9["/robot_1/interface/mavros/camera"] + n10["/robot_1/interface/mavros/cellular_status"] + n11["/robot_1/interface/mavros/cmd"] + n12["/robot_1/interface/mavros/companion_process"] + n13["/robot_1/interface/mavros/debug_value"] + n14["/robot_1/interface/mavros/esc_status"] + n15["/robot_1/interface/mavros/esc_telemetry"] + n16["/robot_1/interface/mavros/fake_gps"] + n17["/robot_1/interface/mavros/ftp"] + n18["/robot_1/interface/mavros/geofence"] + n19["/robot_1/interface/mavros/gimbal_control"] + n20["/robot_1/interface/mavros/global_position"] + n21["/robot_1/interface/mavros/gps_input"] + n22["/robot_1/interface/mavros/gps_rtk"] + n23["/robot_1/interface/mavros/gpsstatus"] + n24["/robot_1/interface/mavros/guided_target"] + n25["/robot_1/interface/mavros/hil"] + n26["/robot_1/interface/mavros/home_position"] + n27["/robot_1/interface/mavros/imu"] + n28["/robot_1/interface/mavros/landing_target"] + n29["/robot_1/interface/mavros/local_position"] + n30["/robot_1/interface/mavros/log_transfer"] + n31["/robot_1/interface/mavros/mag_calibration"] + n32["/robot_1/interface/mavros/manual_control"] + n33["/robot_1/interface/mavros/mavros"] + n34["/robot_1/interface/mavros/mavros_node"] + n35["/robot_1/interface/mavros/mavros_router"] + n36["/robot_1/interface/mavros/mission"] + n37["/robot_1/interface/mavros/mocap"] + n38["/robot_1/interface/mavros/mount_control"] + n39["/robot_1/interface/mavros/nav_controller_output"] + n40["/robot_1/interface/mavros/obstacle"] + n41["/robot_1/interface/mavros/obstacle_distance_3d"] + n42["/robot_1/interface/mavros/odometry"] + n43["/robot_1/interface/mavros/onboard_computer"] + n44["/robot_1/interface/mavros/open_drone_id"] + n45["/robot_1/interface/mavros/optical_flow"] + n46["/robot_1/interface/mavros/param"] + n47["/robot_1/interface/mavros/play_tune"] + n48["/robot_1/interface/mavros/px4flow"] + n49["/robot_1/interface/mavros/rallypoint"] + n50["/robot_1/interface/mavros/rc"] + n51["/robot_1/interface/mavros/setpoint_accel"] + n52["/robot_1/interface/mavros/setpoint_attitude"] + n53["/robot_1/interface/mavros/setpoint_position"] + n54["/robot_1/interface/mavros/setpoint_raw"] + n55["/robot_1/interface/mavros/setpoint_trajectory"] + n56["/robot_1/interface/mavros/setpoint_velocity"] + n57["/robot_1/interface/mavros/sim_state"] + n58["/robot_1/interface/mavros/sys"] + n59["/robot_1/interface/mavros/tdr_radio"] + n60["/robot_1/interface/mavros/terrain"] + n61["/robot_1/interface/mavros/time"] + n62["/robot_1/interface/mavros/trajectory"] + n63["/robot_1/interface/mavros/tunnel"] + n64["/robot_1/interface/mavros/vfr_hud"] + n65["/robot_1/interface/mavros/vision_pose"] + n66["/robot_1/interface/mavros/vision_speed"] + n67["/robot_1/interface/mavros/wind"] + n68["/robot_1/interface/odom_modifier"] + n69["/robot_1/interface/robot_interface"] + end + subgraph g4["odometry_conversion"] + n70["/robot_1/odometry_conversion/odometry_conversion"] + end + subgraph g5["perception"] + n71["/robot_1/perception/stereo_image_proc/disparity_node"] + n72["/robot_1/perception/stereo_pointcloud"] + end + subgraph g6["robot_1"] + n4["/robot_1/gossip_node"] + n73["/robot_1/robot_state_publisher"] + n76["/robot_1/topic_keepalive"] + n79["/robot_1/world_to_map_broadcaster"] + end + subgraph g7["root"] + n0["/action_relay_client"] + end + subgraph g8["sensors"] + n74["/robot_1/sensors/lidar_point_cloud_filter"] + end + subgraph g9["takeoff_landing_planner"] + n75["/robot_1/takeoff_landing_planner/takeoff_landing_task"] + end + subgraph g10["trajectory_controller"] + n77["/robot_1/trajectory_controller/fixed_trajectory_task"] + n78["/robot_1/trajectory_controller/trajectory_control_node"] + end + d0(["/clock (no publishers)"]) -->|"/clock
Clock"| n1 + d0 -->|"/clock
Clock"| n2 + d0 -->|"/clock
Clock"| n3 + d0 -->|"/clock
Clock"| n4 + d0 -->|"/clock
Clock"| n5 + d0 -->|"/clock
Clock"| n6 + d0 -->|"/clock
Clock"| n7 + d0 -->|"/clock
Clock"| n8 + d0 -->|"/clock
Clock"| n9 + d0 -->|"/clock
Clock"| n10 + d0 -->|"/clock
Clock"| n11 + d0 -->|"/clock
Clock"| n12 + d0 -->|"/clock
Clock"| n13 + d0 -->|"/clock
Clock"| n14 + d0 -->|"/clock
Clock"| n15 + d0 -->|"/clock
Clock"| n16 + d0 -->|"/clock
Clock"| n17 + d0 -->|"/clock
Clock"| n18 + d0 -->|"/clock
Clock"| n19 + d0 -->|"/clock
Clock"| n20 + d0 -->|"/clock
Clock"| n21 + d0 -->|"/clock
Clock"| n22 + d0 -->|"/clock
Clock"| n23 + d0 -->|"/clock
Clock"| n24 + d0 -->|"/clock
Clock"| n25 + d0 -->|"/clock
Clock"| n26 + d0 -->|"/clock
Clock"| n27 + d0 -->|"/clock
Clock"| n28 + d0 -->|"/clock
Clock"| n29 + d0 -->|"/clock
Clock"| n30 + d0 -->|"/clock
Clock"| n31 + d0 -->|"/clock
Clock"| n32 + d0 -->|"/clock
Clock"| n33 + d0 -->|"/clock
Clock"| n34 + d0 -->|"/clock
Clock"| n35 + d0 -->|"/clock
Clock"| n36 + d0 -->|"/clock
Clock"| n37 + d0 -->|"/clock
Clock"| n38 + d0 -->|"/clock
Clock"| n39 + d0 -->|"/clock
Clock"| n40 + d0 -->|"/clock
Clock"| n41 + d0 -->|"/clock
Clock"| n42 + d0 -->|"/clock
Clock"| n43 + d0 -->|"/clock
Clock"| n44 + d0 -->|"/clock
Clock"| n45 + d0 -->|"/clock
Clock"| n46 + d0 -->|"/clock
Clock"| n47 + d0 -->|"/clock
Clock"| n48 + d0 -->|"/clock
Clock"| n49 + d0 -->|"/clock
Clock"| n50 + d0 -->|"/clock
Clock"| n51 + d0 -->|"/clock
Clock"| n52 + d0 -->|"/clock
Clock"| n53 + d0 -->|"/clock
Clock"| n54 + d0 -->|"/clock
Clock"| n55 + d0 -->|"/clock
Clock"| n56 + d0 -->|"/clock
Clock"| n57 + d0 -->|"/clock
Clock"| n58 + d0 -->|"/clock
Clock"| n59 + d0 -->|"/clock
Clock"| n60 + d0 -->|"/clock
Clock"| n61 + d0 -->|"/clock
Clock"| n62 + d0 -->|"/clock
Clock"| n63 + d0 -->|"/clock
Clock"| n64 + d0 -->|"/clock
Clock"| n65 + d0 -->|"/clock
Clock"| n66 + d0 -->|"/clock
Clock"| n67 + d0 -->|"/clock
Clock"| n68 + d0 -->|"/clock
Clock"| n69 + d0 -->|"/clock
Clock"| n70 + d0 -->|"/clock
Clock"| n71 + d0 -->|"/clock
Clock"| n72 + d0 -->|"/clock
Clock"| n73 + d0 -->|"/clock
Clock"| n74 + d0 -->|"/clock
Clock"| n75 + d0 -->|"/clock
Clock"| n76 + d0 -->|"/clock
Clock"| n77 + d0 -->|"/clock
Clock"| n78 + d0 -->|"/clock
Clock"| n79 + n33 -->|"/diagnostics
DiagnosticArray"| d1(["/diagnostics (no subscribers)"]) + n35 -->|"/diagnostics
DiagnosticArray"| d1 + n4 -->|"/gossip/peers
PeerProfile"| n4 + n24 -->|"/move_base_simple/goal
PoseStamped"| d2(["/move_base_simple/goal (no subscribers)"]) + d3(["/robot_1/behavior/drone_safety_monitor/command (no publishers)"]) -->|"/robot_1/behavior/drone_safety_monitor/command
String"| n1 + n1 -->|"/robot_1/behavior/drone_safety_monitor/state_estimate_timed_out
Bool"| n75 + n69 -->|"/robot_1/control/reset_integrators
Empty"| n2 + n2 -->|"/robot_1/control/vx_pid_info
PIDInfo"| d4(["/robot_1/control/vx_pid_info (no subscribers)"]) + n2 -->|"/robot_1/control/vy_pid_info
PIDInfo"| d5(["/robot_1/control/vy_pid_info (no subscribers)"]) + n2 -->|"/robot_1/control/vz_pid_info
PIDInfo"| d6(["/robot_1/control/vz_pid_info (no subscribers)"]) + n2 -->|"/robot_1/control/x_pid_info
PIDInfo"| d7(["/robot_1/control/x_pid_info (no subscribers)"]) + n2 -->|"/robot_1/control/y_pid_info
PIDInfo"| d8(["/robot_1/control/y_pid_info (no subscribers)"]) + n2 -->|"/robot_1/control/z_pid_info
PIDInfo"| d9(["/robot_1/control/z_pid_info (no subscribers)"]) + n4 -->|"/robot_1/coordination/peer_registry
PeerProfile"| d10(["/robot_1/coordination/peer_registry (no subscribers)"]) + n68 -->|"/robot_1/cross_track_error
PoseStamped"| d11(["/robot_1/cross_track_error (no subscribers)"]) + n3 -->|"/robot_1/droan/background_expanded
Image"| d12(["/robot_1/droan/background_expanded (no subscribers)"]) + d13(["/robot_1/droan/clear_map (no publishers)"]) -->|"/robot_1/droan/clear_map
Empty"| n3 + d14(["/robot_1/droan/disparity_graph (no publishers)"]) -->|"/robot_1/droan/disparity_graph
MarkerArray"| n76 + d15(["/robot_1/droan/disparity_map_debug (no publishers)"]) -->|"/robot_1/droan/disparity_map_debug
MarkerArray"| n76 + d16(["/robot_1/droan/expansion_cloud (no publishers)"]) -->|"/robot_1/droan/expansion_cloud
PointCloud2"| n76 + d17(["/robot_1/droan/expansion_poly (no publishers)"]) -->|"/robot_1/droan/expansion_poly
MarkerArray"| n76 + n3 -->|"/robot_1/droan/fg_bg_cloud
PointCloud2"| n76 + n3 -->|"/robot_1/droan/foreground_expanded
Image"| d18(["/robot_1/droan/foreground_expanded (no subscribers)"]) + d19(["/robot_1/droan/frustum (no publishers)"]) -->|"/robot_1/droan/frustum
Marker"| n76 + n3 -->|"/robot_1/droan/graph_vis
MarkerArray"| n76 + n3 -->|"/robot_1/droan/local_planner_global_plan_vis
MarkerArray"| n76 + d20(["/robot_1/droan/reset_stuck (no publishers)"]) -->|"/robot_1/droan/reset_stuck
Empty"| n3 + n3 -->|"/robot_1/droan/rewind_info
MarkerArray"| n76 + n3 -->|"/robot_1/droan/stuck
Bool"| d21(["/robot_1/droan/stuck (no subscribers)"]) + n3 -->|"/robot_1/droan/traj_debug
MarkerArray"| n76 + d22(["/robot_1/droan/trajectory_library_vis (no publishers)"]) -->|"/robot_1/droan/trajectory_library_vis
MarkerArray"| n76 + d23(["/robot_1/droan/virtual_obstacles (no publishers)"]) -->|"/robot_1/droan/virtual_obstacles
MarkerArray"| n76 + n68 -->|"/robot_1/global_plan
Path"| n3 + n68 -->|"/robot_1/global_plan
Path"| n4 + n68 -->|"/robot_1/global_plan
Path"| n76 + d24(["/robot_1/interface/attitude_thrust_command (no publishers)"]) -->|"/robot_1/interface/attitude_thrust_command
AttitudeThrust"| n69 + d25(["/robot_1/interface/cmd_attitude_thrust (no publishers)"]) -->|"/robot_1/interface/cmd_attitude_thrust
AttitudeThrust"| n69 + n68 -->|"/robot_1/interface/cmd_pose
PoseStamped"| n69 + d26(["/robot_1/interface/cmd_rate_thrust (no publishers)"]) -->|"/robot_1/interface/cmd_rate_thrust
RateThrust"| n69 + n2 -->|"/robot_1/interface/cmd_roll_pitch_yawrate_thrust
RollPitchYawrateThrust"| n69 + d27(["/robot_1/interface/cmd_torque_thrust (no publishers)"]) -->|"/robot_1/interface/cmd_torque_thrust
TorqueThrust"| n69 + n68 -->|"/robot_1/interface/cmd_velocity
TwistStamped"| n69 + n69 -->|"/robot_1/interface/has_control
Bool"| n75 + n69 -->|"/robot_1/interface/is_armed
Bool"| n75 + d28(["/robot_1/interface/mavros/actuator_control (no publishers)"]) -->|"/robot_1/interface/mavros/actuator_control
ActuatorControl"| n5 + d29(["/robot_1/interface/mavros/adsb/send (no publishers)"]) -->|"/robot_1/interface/mavros/adsb/send
ADSBVehicle"| n6 + n6 -->|"/robot_1/interface/mavros/adsb/vehicle
ADSBVehicle"| d30(["/robot_1/interface/mavros/adsb/vehicle (no subscribers)"]) + n7 -->|"/robot_1/interface/mavros/altitude
Altitude"| d31(["/robot_1/interface/mavros/altitude (no subscribers)"]) + n58 -->|"/robot_1/interface/mavros/battery
BatteryState"| d32(["/robot_1/interface/mavros/battery (no subscribers)"]) + n8 -->|"/robot_1/interface/mavros/cam_imu_sync/cam_imu_stamp
CamIMUStamp"| d33(["/robot_1/interface/mavros/cam_imu_sync/cam_imu_stamp (no subscribers)"]) + n9 -->|"/robot_1/interface/mavros/camera/image_captured
CameraImageCaptured"| d34(["/robot_1/interface/mavros/camera/image_captured (no subscribers)"]) + d35(["/robot_1/interface/mavros/cellular_status/status (no publishers)"]) -->|"/robot_1/interface/mavros/cellular_status/status
CellularStatus"| n10 + d36(["/robot_1/interface/mavros/companion_process/status (no publishers)"]) -->|"/robot_1/interface/mavros/companion_process/status
CompanionProcessStatus"| n12 + n13 -->|"/robot_1/interface/mavros/debug_value/debug
DebugValue"| d37(["/robot_1/interface/mavros/debug_value/debug (no subscribers)"]) + n13 -->|"/robot_1/interface/mavros/debug_value/debug_float_array
DebugValue"| d38(["/robot_1/interface/mavros/debug_value/debug_float_array (no subscribers)"]) + n13 -->|"/robot_1/interface/mavros/debug_value/debug_vector
DebugValue"| d39(["/robot_1/interface/mavros/debug_value/debug_vector (no subscribers)"]) + n13 -->|"/robot_1/interface/mavros/debug_value/named_value_float
DebugValue"| d40(["/robot_1/interface/mavros/debug_value/named_value_float (no subscribers)"]) + n13 -->|"/robot_1/interface/mavros/debug_value/named_value_int
DebugValue"| d41(["/robot_1/interface/mavros/debug_value/named_value_int (no subscribers)"]) + d42(["/robot_1/interface/mavros/debug_value/send (no publishers)"]) -->|"/robot_1/interface/mavros/debug_value/send
DebugValue"| n13 + n14 -->|"/robot_1/interface/mavros/esc_status/info
ESCInfo"| d43(["/robot_1/interface/mavros/esc_status/info (no subscribers)"]) + n14 -->|"/robot_1/interface/mavros/esc_status/status
ESCStatus"| d44(["/robot_1/interface/mavros/esc_status/status (no subscribers)"]) + n15 -->|"/robot_1/interface/mavros/esc_telemetry/telemetry
ESCTelemetry"| d45(["/robot_1/interface/mavros/esc_telemetry/telemetry (no subscribers)"]) + n58 -->|"/robot_1/interface/mavros/estimator_status
EstimatorStatus"| d46(["/robot_1/interface/mavros/estimator_status (no subscribers)"]) + n58 -->|"/robot_1/interface/mavros/extended_state
ExtendedState"| n75 + d47(["/robot_1/interface/mavros/fake_gps/mocap/tf (no publishers)"]) -->|"/robot_1/interface/mavros/fake_gps/mocap/tf
TransformStamped"| n16 + n18 -->|"/robot_1/interface/mavros/geofence/fences
WaypointList"| d48(["/robot_1/interface/mavros/geofence/fences (no subscribers)"]) + n19 -->|"/robot_1/interface/mavros/gimbal_control/device/attitude_status
GimbalDeviceAttitudeStatus"| d49(["/robot_1/interface/mavros/gimbal_control/device/attitude_status (no subscribers)"]) + n19 -->|"/robot_1/interface/mavros/gimbal_control/device/info
GimbalDeviceInformation"| d50(["/robot_1/interface/mavros/gimbal_control/device/info (no subscribers)"]) + d51(["/robot_1/interface/mavros/gimbal_control/device/set_attitude (no publishers)"]) -->|"/robot_1/interface/mavros/gimbal_control/device/set_attitude
GimbalDeviceSetAttitude"| n19 + n19 -->|"/robot_1/interface/mavros/gimbal_control/manager/info
GimbalManagerInformation"| d52(["/robot_1/interface/mavros/gimbal_control/manager/info (no subscribers)"]) + d53(["/robot_1/interface/mavros/gimbal_control/manager/set_attitude (no publishers)"]) -->|"/robot_1/interface/mavros/gimbal_control/manager/set_attitude
GimbalManagerSetAttitude"| n19 + d54(["/robot_1/interface/mavros/gimbal_control/manager/set_manual_control (no publishers)"]) -->|"/robot_1/interface/mavros/gimbal_control/manager/set_manual_control
GimbalManagerSetPitchyaw"| n19 + d55(["/robot_1/interface/mavros/gimbal_control/manager/set_pitchyaw (no publishers)"]) -->|"/robot_1/interface/mavros/gimbal_control/manager/set_pitchyaw
GimbalManagerSetPitchyaw"| n19 + n19 -->|"/robot_1/interface/mavros/gimbal_control/manager/status
GimbalManagerStatus"| d56(["/robot_1/interface/mavros/gimbal_control/manager/status (no subscribers)"]) + n20 -->|"/robot_1/interface/mavros/global_position/compass_hdg
Float64"| n4 + n20 -->|"/robot_1/interface/mavros/global_position/global
NavSatFix"| n53 + n20 -->|"/robot_1/interface/mavros/global_position/global
NavSatFix"| n69 + n20 -->|"/robot_1/interface/mavros/global_position/gp_lp_offset
PoseStamped"| d57(["/robot_1/interface/mavros/global_position/gp_lp_offset (no subscribers)"]) + n20 -->|"/robot_1/interface/mavros/global_position/gp_origin
GeoPointStamped"| n24 + n20 -->|"/robot_1/interface/mavros/global_position/local
Odometry"| n69 + n20 -->|"/robot_1/interface/mavros/global_position/raw/fix
NavSatFix"| n4 + n20 -->|"/robot_1/interface/mavros/global_position/raw/gps_vel
TwistStamped"| d58(["/robot_1/interface/mavros/global_position/raw/gps_vel (no subscribers)"]) + n20 -->|"/robot_1/interface/mavros/global_position/raw/satellites
UInt32"| d59(["/robot_1/interface/mavros/global_position/raw/satellites (no subscribers)"]) + n20 -->|"/robot_1/interface/mavros/global_position/rel_alt
Float64"| d60(["/robot_1/interface/mavros/global_position/rel_alt (no subscribers)"]) + d61(["/robot_1/interface/mavros/global_position/set_gp_origin (no publishers)"]) -->|"/robot_1/interface/mavros/global_position/set_gp_origin
GeoPointStamped"| n20 + d62(["/robot_1/interface/mavros/gps_input/gps_input (no publishers)"]) -->|"/robot_1/interface/mavros/gps_input/gps_input
GPSINPUT"| n21 + n22 -->|"/robot_1/interface/mavros/gps_rtk/rtk_baseline
RTKBaseline"| d63(["/robot_1/interface/mavros/gps_rtk/rtk_baseline (no subscribers)"]) + d64(["/robot_1/interface/mavros/gps_rtk/send_rtcm (no publishers)"]) -->|"/robot_1/interface/mavros/gps_rtk/send_rtcm
RTCM"| n22 + n23 -->|"/robot_1/interface/mavros/gpsstatus/gps1/raw
GPSRAW"| d65(["/robot_1/interface/mavros/gpsstatus/gps1/raw (no subscribers)"]) + n23 -->|"/robot_1/interface/mavros/gpsstatus/gps1/rtk
GPSRTK"| d66(["/robot_1/interface/mavros/gpsstatus/gps1/rtk (no subscribers)"]) + n23 -->|"/robot_1/interface/mavros/gpsstatus/gps2/raw
GPSRAW"| d67(["/robot_1/interface/mavros/gpsstatus/gps2/raw (no subscribers)"]) + n23 -->|"/robot_1/interface/mavros/gpsstatus/gps2/rtk
GPSRTK"| d68(["/robot_1/interface/mavros/gpsstatus/gps2/rtk (no subscribers)"]) + n25 -->|"/robot_1/interface/mavros/hil/actuator_controls
HilActuatorControls"| d69(["/robot_1/interface/mavros/hil/actuator_controls (no subscribers)"]) + n25 -->|"/robot_1/interface/mavros/hil/controls
HilControls"| d70(["/robot_1/interface/mavros/hil/controls (no subscribers)"]) + d71(["/robot_1/interface/mavros/hil/gps (no publishers)"]) -->|"/robot_1/interface/mavros/hil/gps
HilGPS"| n25 + d72(["/robot_1/interface/mavros/hil/imu_ned (no publishers)"]) -->|"/robot_1/interface/mavros/hil/imu_ned
HilSensor"| n25 + d73(["/robot_1/interface/mavros/hil/optical_flow (no publishers)"]) -->|"/robot_1/interface/mavros/hil/optical_flow
OpticalFlowRad"| n25 + d74(["/robot_1/interface/mavros/hil/rc_inputs (no publishers)"]) -->|"/robot_1/interface/mavros/hil/rc_inputs
RCIn"| n25 + d75(["/robot_1/interface/mavros/hil/state (no publishers)"]) -->|"/robot_1/interface/mavros/hil/state
HilStateQuaternion"| n25 + n26 -->|"/robot_1/interface/mavros/home_position/home
HomePosition"| n20 + n26 -->|"/robot_1/interface/mavros/home_position/home
HomePosition"| n69 + n69 -->|"/robot_1/interface/mavros/home_position/set
HomePosition"| n26 + n27 -->|"/robot_1/interface/mavros/imu/data
Imu"| d76(["/robot_1/interface/mavros/imu/data (no subscribers)"]) + n27 -->|"/robot_1/interface/mavros/imu/data_raw
Imu"| d77(["/robot_1/interface/mavros/imu/data_raw (no subscribers)"]) + n27 -->|"/robot_1/interface/mavros/imu/diff_pressure
FluidPressure"| d78(["/robot_1/interface/mavros/imu/diff_pressure (no subscribers)"]) + n27 -->|"/robot_1/interface/mavros/imu/mag
MagneticField"| d79(["/robot_1/interface/mavros/imu/mag (no subscribers)"]) + n27 -->|"/robot_1/interface/mavros/imu/static_pressure
FluidPressure"| d80(["/robot_1/interface/mavros/imu/static_pressure (no subscribers)"]) + n27 -->|"/robot_1/interface/mavros/imu/temperature_baro
Temperature"| d81(["/robot_1/interface/mavros/imu/temperature_baro (no subscribers)"]) + n27 -->|"/robot_1/interface/mavros/imu/temperature_imu
Temperature"| d82(["/robot_1/interface/mavros/imu/temperature_imu (no subscribers)"]) + n28 -->|"/robot_1/interface/mavros/landing_target/lt_marker
Vector3Stamped"| d83(["/robot_1/interface/mavros/landing_target/lt_marker (no subscribers)"]) + d84(["/robot_1/interface/mavros/landing_target/pose (no publishers)"]) -->|"/robot_1/interface/mavros/landing_target/pose
PoseStamped"| n28 + n28 -->|"/robot_1/interface/mavros/landing_target/pose_in
PoseStamped"| d85(["/robot_1/interface/mavros/landing_target/pose_in (no subscribers)"]) + n29 -->|"/robot_1/interface/mavros/local_position/accel
AccelWithCovarianceStamped"| d86(["/robot_1/interface/mavros/local_position/accel (no subscribers)"]) + n29 -->|"/robot_1/interface/mavros/local_position/odom
Odometry"| n70 + n29 -->|"/robot_1/interface/mavros/local_position/pose
PoseStamped"| n53 + n29 -->|"/robot_1/interface/mavros/local_position/pose_cov
PoseWithCovarianceStamped"| d87(["/robot_1/interface/mavros/local_position/pose_cov (no subscribers)"]) + n29 -->|"/robot_1/interface/mavros/local_position/velocity_body
TwistStamped"| d88(["/robot_1/interface/mavros/local_position/velocity_body (no subscribers)"]) + n29 -->|"/robot_1/interface/mavros/local_position/velocity_body_cov
TwistWithCovarianceStamped"| d89(["/robot_1/interface/mavros/local_position/velocity_body_cov (no subscribers)"]) + n29 -->|"/robot_1/interface/mavros/local_position/velocity_local
TwistStamped"| d90(["/robot_1/interface/mavros/local_position/velocity_local (no subscribers)"]) + n30 -->|"/robot_1/interface/mavros/log_transfer/raw/log_data
LogData"| d91(["/robot_1/interface/mavros/log_transfer/raw/log_data (no subscribers)"]) + n30 -->|"/robot_1/interface/mavros/log_transfer/raw/log_entry
LogEntry"| d92(["/robot_1/interface/mavros/log_transfer/raw/log_entry (no subscribers)"]) + n31 -->|"/robot_1/interface/mavros/mag_calibration/report
MagnetometerReporter"| d93(["/robot_1/interface/mavros/mag_calibration/report (no subscribers)"]) + n31 -->|"/robot_1/interface/mavros/mag_calibration/status
UInt8"| d94(["/robot_1/interface/mavros/mag_calibration/status (no subscribers)"]) + n32 -->|"/robot_1/interface/mavros/manual_control/control
ManualControl"| d95(["/robot_1/interface/mavros/manual_control/control (no subscribers)"]) + d96(["/robot_1/interface/mavros/manual_control/send (no publishers)"]) -->|"/robot_1/interface/mavros/manual_control/send
ManualControl"| n32 + n36 -->|"/robot_1/interface/mavros/mission/reached
WaypointReached"| d97(["/robot_1/interface/mavros/mission/reached (no subscribers)"]) + n36 -->|"/robot_1/interface/mavros/mission/waypoints
WaypointList"| d98(["/robot_1/interface/mavros/mission/waypoints (no subscribers)"]) + d99(["/robot_1/interface/mavros/mocap/pose (no publishers)"]) -->|"/robot_1/interface/mavros/mocap/pose
PoseStamped"| n37 + d100(["/robot_1/interface/mavros/mocap/tf (no publishers)"]) -->|"/robot_1/interface/mavros/mocap/tf
TransformStamped"| n37 + d101(["/robot_1/interface/mavros/mount_control/command (no publishers)"]) -->|"/robot_1/interface/mavros/mount_control/command
MountControl"| n38 + n38 -->|"/robot_1/interface/mavros/mount_control/orientation
Quaternion"| d102(["/robot_1/interface/mavros/mount_control/orientation (no subscribers)"]) + n38 -->|"/robot_1/interface/mavros/mount_control/status
Vector3Stamped"| d103(["/robot_1/interface/mavros/mount_control/status (no subscribers)"]) + n39 -->|"/robot_1/interface/mavros/nav_controller_output/output
NavControllerOutput"| d104(["/robot_1/interface/mavros/nav_controller_output/output (no subscribers)"]) + d105(["/robot_1/interface/mavros/obstacle/send (no publishers)"]) -->|"/robot_1/interface/mavros/obstacle/send
LaserScan"| n40 + d106(["/robot_1/interface/mavros/obstacle_distance_3d/send (no publishers)"]) -->|"/robot_1/interface/mavros/obstacle_distance_3d/send
ObstacleDistance3D"| n41 + n42 -->|"/robot_1/interface/mavros/odometry/in
Odometry"| d107(["/robot_1/interface/mavros/odometry/in (no subscribers)"]) + d108(["/robot_1/interface/mavros/odometry/out (no publishers)"]) -->|"/robot_1/interface/mavros/odometry/out
Odometry"| n42 + d109(["/robot_1/interface/mavros/onboard_computer/status (no publishers)"]) -->|"/robot_1/interface/mavros/onboard_computer/status
OnboardComputerStatus"| n43 + d110(["/robot_1/interface/mavros/open_drone_id/basic_id (no publishers)"]) -->|"/robot_1/interface/mavros/open_drone_id/basic_id
OpenDroneIDBasicID"| n44 + d111(["/robot_1/interface/mavros/open_drone_id/operator_id (no publishers)"]) -->|"/robot_1/interface/mavros/open_drone_id/operator_id
OpenDroneIDOperatorID"| n44 + d112(["/robot_1/interface/mavros/open_drone_id/self_id (no publishers)"]) -->|"/robot_1/interface/mavros/open_drone_id/self_id
OpenDroneIDSelfID"| n44 + d113(["/robot_1/interface/mavros/open_drone_id/system (no publishers)"]) -->|"/robot_1/interface/mavros/open_drone_id/system
OpenDroneIDSystem"| n44 + d114(["/robot_1/interface/mavros/open_drone_id/system_update (no publishers)"]) -->|"/robot_1/interface/mavros/open_drone_id/system_update
OpenDroneIDSystemUpdate"| n44 + n45 -->|"/robot_1/interface/mavros/optical_flow/ground_distance
Range"| d115(["/robot_1/interface/mavros/optical_flow/ground_distance (no subscribers)"]) + n45 -->|"/robot_1/interface/mavros/optical_flow/raw/optical_flow
OpticalFlow"| d116(["/robot_1/interface/mavros/optical_flow/raw/optical_flow (no subscribers)"]) + d117(["/robot_1/interface/mavros/optical_flow/raw/send (no publishers)"]) -->|"/robot_1/interface/mavros/optical_flow/raw/send
OpticalFlow"| n45 + n46 -->|"/robot_1/interface/mavros/param/event
ParamEvent"| d118(["/robot_1/interface/mavros/param/event (no subscribers)"]) + d119(["/robot_1/interface/mavros/play_tune (no publishers)"]) -->|"/robot_1/interface/mavros/play_tune
PlayTuneV2"| n47 + n48 -->|"/robot_1/interface/mavros/px4flow/ground_distance
Range"| d120(["/robot_1/interface/mavros/px4flow/ground_distance (no subscribers)"]) + n48 -->|"/robot_1/interface/mavros/px4flow/raw/optical_flow_rad
OpticalFlowRad"| d121(["/robot_1/interface/mavros/px4flow/raw/optical_flow_rad (no subscribers)"]) + d122(["/robot_1/interface/mavros/px4flow/raw/send (no publishers)"]) -->|"/robot_1/interface/mavros/px4flow/raw/send
OpticalFlowRad"| n48 + n48 -->|"/robot_1/interface/mavros/px4flow/temperature
Temperature"| d123(["/robot_1/interface/mavros/px4flow/temperature (no subscribers)"]) + n59 -->|"/robot_1/interface/mavros/radio_status
RadioStatus"| d124(["/robot_1/interface/mavros/radio_status (no subscribers)"]) + n49 -->|"/robot_1/interface/mavros/rallypoint/rallypoints
WaypointList"| d125(["/robot_1/interface/mavros/rallypoint/rallypoints (no subscribers)"]) + n50 -->|"/robot_1/interface/mavros/rc/in
RCIn"| d126(["/robot_1/interface/mavros/rc/in (no subscribers)"]) + n50 -->|"/robot_1/interface/mavros/rc/out
RCOut"| d127(["/robot_1/interface/mavros/rc/out (no subscribers)"]) + d128(["/robot_1/interface/mavros/rc/override (no publishers)"]) -->|"/robot_1/interface/mavros/rc/override
OverrideRCIn"| n50 + d129(["/robot_1/interface/mavros/setpoint_accel/accel (no publishers)"]) -->|"/robot_1/interface/mavros/setpoint_accel/accel
Vector3Stamped"| n51 + d130(["/robot_1/interface/mavros/setpoint_attitude/cmd_vel (no publishers)"]) -->|"/robot_1/interface/mavros/setpoint_attitude/cmd_vel
TwistStamped"| n52 + d131(["/robot_1/interface/mavros/setpoint_attitude/thrust (no publishers)"]) -->|"/robot_1/interface/mavros/setpoint_attitude/thrust
Thrust"| n52 + n69 -->|"/robot_1/interface/mavros/setpoint_position/global
GeoPoseStamped"| n53 + d132(["/robot_1/interface/mavros/setpoint_position/global_to_local (no publishers)"]) -->|"/robot_1/interface/mavros/setpoint_position/global_to_local
GeoPoseStamped"| n53 + n69 -->|"/robot_1/interface/mavros/setpoint_position/local
PoseStamped"| n53 + n69 -->|"/robot_1/interface/mavros/setpoint_raw/attitude
AttitudeTarget"| n54 + d133(["/robot_1/interface/mavros/setpoint_raw/global (no publishers)"]) -->|"/robot_1/interface/mavros/setpoint_raw/global
GlobalPositionTarget"| n54 + n69 -->|"/robot_1/interface/mavros/setpoint_raw/local
PositionTarget"| n54 + n54 -->|"/robot_1/interface/mavros/setpoint_raw/target_attitude
AttitudeTarget"| d134(["/robot_1/interface/mavros/setpoint_raw/target_attitude (no subscribers)"]) + n54 -->|"/robot_1/interface/mavros/setpoint_raw/target_global
GlobalPositionTarget"| d135(["/robot_1/interface/mavros/setpoint_raw/target_global (no subscribers)"]) + n54 -->|"/robot_1/interface/mavros/setpoint_raw/target_local
PositionTarget"| d136(["/robot_1/interface/mavros/setpoint_raw/target_local (no subscribers)"]) + n55 -->|"/robot_1/interface/mavros/setpoint_trajectory/desired
Path"| d137(["/robot_1/interface/mavros/setpoint_trajectory/desired (no subscribers)"]) + d138(["/robot_1/interface/mavros/setpoint_trajectory/local (no publishers)"]) -->|"/robot_1/interface/mavros/setpoint_trajectory/local
MultiDOFJointTrajectory"| n55 + d139(["/robot_1/interface/mavros/setpoint_velocity/cmd_vel (no publishers)"]) -->|"/robot_1/interface/mavros/setpoint_velocity/cmd_vel
TwistStamped"| n56 + d140(["/robot_1/interface/mavros/setpoint_velocity/cmd_vel_unstamped (no publishers)"]) -->|"/robot_1/interface/mavros/setpoint_velocity/cmd_vel_unstamped
Twist"| n56 + n57 -->|"/robot_1/interface/mavros/sim_state/acceleration
Vector3Stamped"| d141(["/robot_1/interface/mavros/sim_state/acceleration (no subscribers)"]) + n57 -->|"/robot_1/interface/mavros/sim_state/attitude
Imu"| d142(["/robot_1/interface/mavros/sim_state/attitude (no subscribers)"]) + n57 -->|"/robot_1/interface/mavros/sim_state/global_position
NavSatFix"| d143(["/robot_1/interface/mavros/sim_state/global_position (no subscribers)"]) + n57 -->|"/robot_1/interface/mavros/sim_state/velocity_body
TwistStamped"| d144(["/robot_1/interface/mavros/sim_state/velocity_body (no subscribers)"]) + n57 -->|"/robot_1/interface/mavros/sim_state/velocity_local
TwistStamped"| d145(["/robot_1/interface/mavros/sim_state/velocity_local (no subscribers)"]) + n58 -->|"/robot_1/interface/mavros/state
State"| n69 + n58 -->|"/robot_1/interface/mavros/status_event
StatusEvent"| d146(["/robot_1/interface/mavros/status_event (no subscribers)"]) + n58 -->|"/robot_1/interface/mavros/statustext/recv
StatusText"| d147(["/robot_1/interface/mavros/statustext/recv (no subscribers)"]) + d148(["/robot_1/interface/mavros/statustext/send (no publishers)"]) -->|"/robot_1/interface/mavros/statustext/send
StatusText"| n58 + n58 -->|"/robot_1/interface/mavros/sys_status
SysStatus"| d149(["/robot_1/interface/mavros/sys_status (no subscribers)"]) + n5 -->|"/robot_1/interface/mavros/target_actuator_control
ActuatorControl"| d150(["/robot_1/interface/mavros/target_actuator_control (no subscribers)"]) + n60 -->|"/robot_1/interface/mavros/terrain/report
TerrainReport"| d151(["/robot_1/interface/mavros/terrain/report (no subscribers)"]) + n61 -->|"/robot_1/interface/mavros/time_reference
TimeReference"| d152(["/robot_1/interface/mavros/time_reference (no subscribers)"]) + n61 -->|"/robot_1/interface/mavros/timesync_status
TimesyncStatus"| d153(["/robot_1/interface/mavros/timesync_status (no subscribers)"]) + n62 -->|"/robot_1/interface/mavros/trajectory/desired
Trajectory"| d154(["/robot_1/interface/mavros/trajectory/desired (no subscribers)"]) + d155(["/robot_1/interface/mavros/trajectory/generated (no publishers)"]) -->|"/robot_1/interface/mavros/trajectory/generated
Trajectory"| n62 + d156(["/robot_1/interface/mavros/trajectory/path (no publishers)"]) -->|"/robot_1/interface/mavros/trajectory/path
Path"| n62 + d157(["/robot_1/interface/mavros/tunnel/in (no publishers)"]) -->|"/robot_1/interface/mavros/tunnel/in
Tunnel"| n63 + n63 -->|"/robot_1/interface/mavros/tunnel/out
Tunnel"| d158(["/robot_1/interface/mavros/tunnel/out (no subscribers)"]) + n64 -->|"/robot_1/interface/mavros/vfr_hud
VfrHud"| d159(["/robot_1/interface/mavros/vfr_hud (no subscribers)"]) + d160(["/robot_1/interface/mavros/vision_pose/pose (no publishers)"]) -->|"/robot_1/interface/mavros/vision_pose/pose
PoseStamped"| n65 + d161(["/robot_1/interface/mavros/vision_pose/pose_cov (no publishers)"]) -->|"/robot_1/interface/mavros/vision_pose/pose_cov
PoseWithCovarianceStamped"| n65 + d162(["/robot_1/interface/mavros/vision_speed/speed_twist (no publishers)"]) -->|"/robot_1/interface/mavros/vision_speed/speed_twist
TwistStamped"| n66 + d163(["/robot_1/interface/mavros/vision_speed/speed_twist_cov (no publishers)"]) -->|"/robot_1/interface/mavros/vision_speed/speed_twist_cov
TwistWithCovarianceStamped"| n66 + d164(["/robot_1/interface/mavros/vision_speed/speed_vector (no publishers)"]) -->|"/robot_1/interface/mavros/vision_speed/speed_vector
Vector3Stamped"| n66 + n67 -->|"/robot_1/interface/mavros/wind_estimation
TwistWithCovarianceStamped"| d165(["/robot_1/interface/mavros/wind_estimation (no subscribers)"]) + d166(["/robot_1/interface/pose_command (no publishers)"]) -->|"/robot_1/interface/pose_command
PoseStamped"| n69 + d167(["/robot_1/interface/rate_thrust_command (no publishers)"]) -->|"/robot_1/interface/rate_thrust_command
RateThrust"| n69 + d168(["/robot_1/interface/roll_pitch_yawrate_thrust_command (no publishers)"]) -->|"/robot_1/interface/roll_pitch_yawrate_thrust_command
RollPitchYawrateThrust"| n69 + d169(["/robot_1/interface/torque_thrust_command (no publishers)"]) -->|"/robot_1/interface/torque_thrust_command
TorqueThrust"| n69 + d170(["/robot_1/interface/velocity_command (no publishers)"]) -->|"/robot_1/interface/velocity_command
TwistStamped"| n69 + d171(["/robot_1/joint_states (no publishers)"]) -->|"/robot_1/joint_states
JointState"| n73 + n70 -->|"/robot_1/odometry_conversion/odometry
Odometry"| n1 + n70 -->|"/robot_1/odometry_conversion/odometry
Odometry"| n2 + n70 -->|"/robot_1/odometry_conversion/odometry
Odometry"| n68 + n70 -->|"/robot_1/odometry_conversion/odometry
Odometry"| n75 + n70 -->|"/robot_1/odometry_conversion/odometry
Odometry"| n76 + n70 -->|"/robot_1/odometry_conversion/odometry
Odometry"| n77 + n70 -->|"/robot_1/odometry_conversion/odometry
Odometry"| n78 + n71 -->|"/robot_1/perception/stereo_image_proc/disparity
DisparityImage"| n3 + n71 -->|"/robot_1/perception/stereo_image_proc/disparity
DisparityImage"| n72 + n72 -->|"/robot_1/perception/stereo_image_proc/point_cloud
PointCloud2"| n76 + n73 -->|"/robot_1/robot_description
String"| d172(["/robot_1/robot_description (no subscribers)"]) + d173(["/robot_1/sensors/front_stereo/left/camera_info (no publishers)"]) -->|"/robot_1/sensors/front_stereo/left/camera_info
CameraInfo"| n71 + d173 -->|"/robot_1/sensors/front_stereo/left/camera_info
CameraInfo"| n72 + d173 -->|"/robot_1/sensors/front_stereo/left/camera_info
CameraInfo"| n76 + d174(["/robot_1/sensors/front_stereo/left/depth_ground_truth (no publishers)"]) -->|"/robot_1/sensors/front_stereo/left/depth_ground_truth
Image"| n76 + d175(["/robot_1/sensors/front_stereo/left/image_rect (no publishers)"]) -->|"/robot_1/sensors/front_stereo/left/image_rect
Image"| n71 + d175 -->|"/robot_1/sensors/front_stereo/left/image_rect
Image"| n72 + d175 -->|"/robot_1/sensors/front_stereo/left/image_rect
Image"| n76 + d176(["/robot_1/sensors/front_stereo/right/camera_info (no publishers)"]) -->|"/robot_1/sensors/front_stereo/right/camera_info
CameraInfo"| n3 + d176 -->|"/robot_1/sensors/front_stereo/right/camera_info
CameraInfo"| n71 + d176 -->|"/robot_1/sensors/front_stereo/right/camera_info
CameraInfo"| n72 + d176 -->|"/robot_1/sensors/front_stereo/right/camera_info
CameraInfo"| n76 + d177(["/robot_1/sensors/front_stereo/right/depth_ground_truth (no publishers)"]) -->|"/robot_1/sensors/front_stereo/right/depth_ground_truth
Image"| n76 + d178(["/robot_1/sensors/front_stereo/right/image_rect (no publishers)"]) -->|"/robot_1/sensors/front_stereo/right/image_rect
Image"| n71 + d178 -->|"/robot_1/sensors/front_stereo/right/image_rect
Image"| n76 + d179(["/robot_1/sensors/lidar/point_cloud (no publishers)"]) -->|"/robot_1/sensors/lidar/point_cloud
PointCloud2"| n76 + n74 -->|"/robot_1/sensors/ouster/point_cloud
PointCloud2"| d180(["/robot_1/sensors/ouster/point_cloud (no subscribers)"]) + d181(["/robot_1/sensors/ouster/point_cloud_raw (no publishers)"]) -->|"/robot_1/sensors/ouster/point_cloud_raw
PointCloud2"| n74 + n75 -->|"/robot_1/takeoff_landing_planner/is_airborne
Bool"| d182(["/robot_1/takeoff_landing_planner/is_airborne (no subscribers)"]) + d183(["/robot_1/takeoff_landing_planner/trajectory_completion_percentage (no publishers)"]) -->|"/robot_1/takeoff_landing_planner/trajectory_completion_percentage
Float32"| n75 + n78 -->|"/robot_1/trajectory_controller/closest_point
Odometry"| d184(["/robot_1/trajectory_controller/closest_point (no subscribers)"]) + n78 -->|"/robot_1/trajectory_controller/look_ahead
Odometry"| n3 + n78 -->|"/robot_1/trajectory_controller/projected_drone_pose
PoseStamped"| n68 + n78 -->|"/robot_1/trajectory_controller/tracking_error
Float32"| d185(["/robot_1/trajectory_controller/tracking_error (no subscribers)"]) + n78 -->|"/robot_1/trajectory_controller/tracking_point
Odometry"| n2 + n78 -->|"/robot_1/trajectory_controller/tracking_point
Odometry"| n3 + n78 -->|"/robot_1/trajectory_controller/tracking_point
Odometry"| n68 + n78 -->|"/robot_1/trajectory_controller/tracking_point
Odometry"| n75 + n78 -->|"/robot_1/trajectory_controller/tracking_point_velocity_magnitude
Float32"| d186(["/robot_1/trajectory_controller/tracking_point_velocity_magnitude (no subscribers)"]) + n78 -->|"/robot_1/trajectory_controller/traj_drone_point
Odometry"| d187(["/robot_1/trajectory_controller/traj_drone_point (no subscribers)"]) + n78 -->|"/robot_1/trajectory_controller/trajectory_completion_percentage
Float32"| n77 + n78 -->|"/robot_1/trajectory_controller/trajectory_controller_debug_markers
MarkerArray"| n76 + n75 -->|"/robot_1/trajectory_controller/trajectory_override
TrajectoryXYZVYaw"| n78 + n77 -->|"/robot_1/trajectory_controller/trajectory_override
TrajectoryXYZVYaw"| n78 + n3 -->|"/robot_1/trajectory_controller/trajectory_segment_to_add
TrajectoryXYZVYaw"| n78 + n78 -->|"/robot_1/trajectory_controller/trajectory_time
Float32"| d188(["/robot_1/trajectory_controller/trajectory_time (no subscribers)"]) + n78 -->|"/robot_1/trajectory_controller/trajectory_vis
MarkerArray"| n76 + n78 -->|"/robot_1/trajectory_controller/virtual_tracking_point
Odometry"| d189(["/robot_1/trajectory_controller/virtual_tracking_point (no subscribers)"]) + d190(["/robot_1/vdb_mapping/vdb_map_visualization (no publishers)"]) -->|"/robot_1/vdb_mapping/vdb_map_visualization
Marker"| n76 + n33 -->|"/tf
TFMessage"| n68 + n33 -->|"/tf
TFMessage"| n76 + n70 -->|"/tf
TFMessage"| n68 + n70 -->|"/tf
TFMessage"| n76 + n73 -->|"/tf
TFMessage"| n68 + n73 -->|"/tf
TFMessage"| n76 + n78 -->|"/tf
TFMessage"| n68 + n78 -->|"/tf
TFMessage"| n76 + n33 -->|"/tf_static
TFMessage"| n68 + n33 -->|"/tf_static
TFMessage"| n76 + n73 -->|"/tf_static
TFMessage"| n68 + n73 -->|"/tf_static
TFMessage"| n76 + n79 -->|"/tf_static
TFMessage"| n68 + n79 -->|"/tf_static
TFMessage"| n76 + n33 -->|"/uas2/mavlink_sink
Mavlink"| n35 + n35 -->|"/uas2/mavlink_source
Mavlink"| n33 +``` + + From 4044aa15ef3cb6c620efba9cfa691556af256c01 Mon Sep 17 00:00:00 2001 From: Andrew Jong Date: Sat, 22 Aug 2026 04:02:10 -0400 Subject: [PATCH 8/8] Bump version to 0.20.0-alpha.5 Co-Authored-By: Claude Fable 5 --- .env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env b/.env index 8570ffedd..45b3cac58 100644 --- a/.env +++ b/.env @@ -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.20.0-alpha.4" +VERSION="0.20.0-alpha.5" # 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.